From 9dd3f76e56477cabbe206465f729dc4e6f01e453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 5 Aug 2026 19:02:20 +0000 Subject: [PATCH] headscale: read the invite list out of whatever envelope the companion sends --- src/servers/sidecar/headscale/invites.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/servers/sidecar/headscale/invites.ts b/src/servers/sidecar/headscale/invites.ts index 34e44acf..4719974d 100644 --- a/src/servers/sidecar/headscale/invites.ts +++ b/src/servers/sidecar/headscale/invites.ts @@ -104,10 +104,27 @@ async function create(creds: HeadscaleServerCredentials, ctx: OfficerContext): P return relay(res, (body) => ({ available: true, invite: body.invite ?? body })); } +/** + * Pull the invite array out of whatever envelope the companion used. + * + * §4.3 specifies the fields but not the wrapper, and the create response came back flat (no `invite` key), + * so the list may equally be a bare array or sit under `invites`/`items`/`data`. Taking the first + * array-valued property is shape-agnostic without being credulous: the body has exactly one array in it. + */ +function pickInvites(body: Record): unknown[] { + if (Array.isArray(body)) return body; + for (const key of ['invites', 'items', 'data', 'results']) { + const value = body[key]; + if (Array.isArray(value)) return value; + } + const found = Object.values(body).find(Array.isArray); + return Array.isArray(found) ? found : []; +} + /** `GET /_officer/enroll/invites` — the admin's audit list. Never carries a token or a key. */ async function list(creds: HeadscaleServerCredentials): Promise { const res = await callCompanion(creds, { path: INVITES_PATH }); - return relay(res, (body) => ({ available: true, invites: Array.isArray(body.invites) ? body.invites : [] })); + return relay(res, (body) => ({ available: true, invites: pickInvites(body) })); } /** `DELETE /_officer/enroll/invites/:id` — revoke an unclaimed invite. A no-op on a claimed one. */