headscale: invites live under the companion's /api/v1 mount

This commit is contained in:
2026-08-05 18:49:41 +00:00
parent 4cee0335c2
commit 3ca5968605
3 changed files with 27 additions and 11 deletions
+10 -3
View File
@@ -21,6 +21,13 @@ import { activeCreds, callCompanion, readBody, unavailable } from './companion';
// companion route: most registered servers have no companion at all, and that is a state to render rather
// than a request that failed.
/**
* Where the invite API sits on the companion, under its own `/officer-api` mount — so the full URL is
* `${server.url}/officer-api/api/v1/enroll/invites`. Versioned separately from the companion's container
* routes (`/health`, `/logs`, `/restart`), which are unversioned; one constant so the two cannot drift.
*/
const INVITES_PATH = '/api/v1/enroll/invites';
/** Spec §4.1: default 900, max 86400. The floor is ours — a sub-minute invite cannot be sent to anyone. */
const DEFAULT_TTL_SECONDS = 900;
const MIN_TTL_SECONDS = 60;
@@ -93,19 +100,19 @@ async function create(creds: HeadscaleServerCredentials, ctx: OfficerContext): P
const input = parseCreate(await readJson(ctx.req));
if (input instanceof Response) return input;
const res = await callCompanion(creds, { path: '/enroll/invites', method: 'POST', body: input });
const res = await callCompanion(creds, { path: INVITES_PATH, method: 'POST', body: input });
return relay(res, (body) => ({ available: true, invite: body.invite ?? body }));
}
/** `GET /_officer/enroll/invites` — the admin's audit list. Never carries a token or a key. */
async function list(creds: HeadscaleServerCredentials): Promise<Response> {
const res = await callCompanion(creds, { path: '/enroll/invites' });
const res = await callCompanion(creds, { path: INVITES_PATH });
return relay(res, (body) => ({ available: true, invites: Array.isArray(body.invites) ? body.invites : [] }));
}
/** `DELETE /_officer/enroll/invites/:id` — revoke an unclaimed invite. A no-op on a claimed one. */
async function revoke(creds: HeadscaleServerCredentials, id: string): Promise<Response> {
const res = await callCompanion(creds, { path: `/enroll/invites/${encodeURIComponent(id)}`, method: 'DELETE' });
const res = await callCompanion(creds, { path: `${INVITES_PATH}/${encodeURIComponent(id)}`, method: 'DELETE' });
return relay(res, (body) => ({ available: true, ...body }));
}