
























@@ -23,6 +23,7 @@ export type DeviceBootstrapTokenRecord = {
2323publicKey?: string;
2424profile?: DeviceBootstrapProfile;
2525redeemedProfile?: DeviceBootstrapProfile;
26+pendingProfile?: DeviceBootstrapProfile;
2627roles?: string[];
2728scopes?: string[];
2829issuedAtMs: number;
@@ -67,6 +68,35 @@ function resolvePersistedRedeemedProfile(
6768return normalizeDeviceBootstrapProfile(record.redeemedProfile);
6869}
697071+function resolvePersistedPendingProfile(
72+record: Partial<DeviceBootstrapTokenRecord>,
73+): DeviceBootstrapProfile | null {
74+return record.pendingProfile ? normalizeDeviceBootstrapProfile(record.pendingProfile) : null;
75+}
76+77+function resolveRequestedBootstrapProfile(params: {
78+role: string;
79+scopes: readonly string[];
80+}): DeviceBootstrapProfile {
81+return normalizeDeviceBootstrapProfile({
82+roles: [params.role],
83+scopes: resolveBootstrapProfileScopesForRole(params.role, params.scopes),
84+});
85+}
86+87+function sameBootstrapProfile(
88+left: DeviceBootstrapProfile,
89+right: DeviceBootstrapProfile,
90+): boolean {
91+if (left.roles.length !== right.roles.length || left.scopes.length !== right.scopes.length) {
92+return false;
93+}
94+return (
95+left.roles.every((role, index) => role === right.roles[index]) &&
96+left.scopes.every((scope, index) => scope === right.scopes[index])
97+);
98+}
99+70100function resolveIssuedBootstrapProfile(params: {
71101profile?: DeviceBootstrapProfileInput;
72102roles?: readonly string[];
@@ -173,10 +203,12 @@ async function loadState(baseDir?: string): Promise<DeviceBootstrapStateFile> {
173203typeof record.token === "string" && record.token.trim().length > 0 ? record.token : tokenKey;
174204const issuedAtMs = typeof record.issuedAtMs === "number" ? record.issuedAtMs : 0;
175205const profile = resolvePersistedBootstrapProfile(record);
206+const pendingProfile = resolvePersistedPendingProfile(record);
176207state[tokenKey] = {
177208 token,
178209 profile,
179210redeemedProfile: resolvePersistedRedeemedProfile(record),
211+ ...(pendingProfile ? { pendingProfile } : {}),
180212deviceId: typeof record.deviceId === "string" ? record.deviceId : undefined,
181213publicKey: typeof record.publicKey === "string" ? record.publicKey : undefined,
182214 issuedAtMs,
@@ -304,18 +336,33 @@ export async function redeemDeviceBootstrapTokenProfile(params: {
304336}
305337const [tokenKey, record] = found;
306338const issuedProfile = resolvePersistedBootstrapProfile(record);
339+const pendingProfile = resolvePersistedPendingProfile(record);
307340const redeemedProfile = normalizeDeviceBootstrapProfile({
308341roles: [...resolvePersistedRedeemedProfile(record).roles, params.role],
309342scopes: [
310343 ...resolvePersistedRedeemedProfile(record).scopes,
311344 ...resolveBootstrapProfileScopesForRole(params.role, params.scopes),
312345],
313346});
314-state[tokenKey] = {
347+const nextPendingProfile =
348+pendingProfile &&
349+!bootstrapProfileSatisfiesProfile({
350+actualProfile: redeemedProfile,
351+requiredProfile: pendingProfile,
352+})
353+ ? pendingProfile
354+ : undefined;
355+const nextRecord: DeviceBootstrapTokenRecord = {
315356 ...record,
316357profile: issuedProfile,
317358 redeemedProfile,
318359};
360+if (nextPendingProfile) {
361+nextRecord.pendingProfile = nextPendingProfile;
362+} else {
363+delete nextRecord.pendingProfile;
364+}
365+state[tokenKey] = nextRecord;
319366await persistState(state, params.baseDir);
320367return {
321368recorded: true,
@@ -368,6 +415,10 @@ export async function verifyDeviceBootstrapToken(params: {
368415) {
369416return { ok: false, reason: "bootstrap_token_invalid" };
370417}
418+const requestedProfile = resolveRequestedBootstrapProfile({
419+ role,
420+scopes: params.scopes,
421+});
371422372423const boundDeviceId = record.deviceId?.trim();
373424const boundPublicKey =
@@ -378,9 +429,14 @@ export async function verifyDeviceBootstrapToken(params: {
378429if (boundDeviceId !== deviceId || boundPublicKey !== publicKey) {
379430return { ok: false, reason: "bootstrap_token_invalid" };
380431}
432+const pendingProfile = resolvePersistedPendingProfile(record);
433+if (pendingProfile && !sameBootstrapProfile(pendingProfile, requestedProfile)) {
434+return { ok: false, reason: "bootstrap_token_invalid" };
435+}
381436state[tokenKey] = {
382437 ...record,
383438profile: allowedProfile,
439+pendingProfile: pendingProfile ?? requestedProfile,
384440 deviceId,
385441 publicKey,
386442lastUsedAtMs: Date.now(),
@@ -392,6 +448,7 @@ export async function verifyDeviceBootstrapToken(params: {
392448state[tokenKey] = {
393449 ...record,
394450profile: allowedProfile,
451+pendingProfile: requestedProfile,
395452 deviceId,
396453 publicKey,
397454lastUsedAtMs: Date.now(),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。