carry the invite's device name in the link so the phone can prefill it

This commit is contained in:
2026-08-06 13:24:45 +00:00
parent 2873948f98
commit 33ecfa989d
2 changed files with 23 additions and 4 deletions
+20 -1
View File
@@ -95,6 +95,25 @@ async function relay(res: Response | string, wrap: (body: Record<string, unknown
return Response.json(wrap(body));
}
/**
* Carry the admin's device name in the link's fragment, as `n=<percent-encoded>`.
*
* The companion already knows the name — it stores the note and hands it back as `suggestedHostname` on
* claim — but a claim only happens when the person taps Join, which is one step AFTER the screen that asks
* them to name the device. So the name has to arrive with the link if the field is to be prefilled, and the
* link is the last thing that passes through here.
*
* Safe at every hop: the fragment is never sent to a server, the companion's /join page copies it verbatim
* into the `officer-offscale://` deep link, and a build of the app that predates this ignores an unknown
* parameter and still gets the name from `suggestedHostname` at claim time. Percent-encoded rather than
* base64url (which `s` uses) because the app's fragment parser already decodeURIComponent()s every value,
* and because base64url of a non-ASCII name would decode to mojibake on Hermes.
*/
function withNameHint(url: unknown, name: string | undefined): unknown {
if (typeof url !== 'string' || !name || !url.includes('#')) return url;
return `${url}&n=${encodeURIComponent(name)}`;
}
/**
* `POST /_officer/enroll/invites` — mint an invite. The response carries the link, and only this once.
*
@@ -112,7 +131,7 @@ async function create(creds: HeadscaleServerCredentials, ctx: OfficerContext): P
const raw = body.invite ?? body;
const invite = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
const { deepLink: _deepLink, ...rest } = invite;
return { available: true, invite: rest };
return { available: true, invite: { ...rest, url: withNameHint(rest.url, input.note) } };
});
}