11import path from "node:path" ;
2+ import { createSubsystemLogger } from "../logging/subsystem.js" ;
23import {
4+ normalizeDeviceBootstrapHandoffProfile ,
35 normalizeDeviceBootstrapProfile ,
46 PAIRING_SETUP_BOOTSTRAP_PROFILE ,
7+ resolveBootstrapProfileScopesForRole ,
58 type DeviceBootstrapProfile ,
69 type DeviceBootstrapProfileInput ,
710} from "../shared/device-bootstrap-profile.js" ;
@@ -34,11 +37,29 @@ export type DeviceBootstrapTokenRecord = {
3437type DeviceBootstrapStateFile = Record < string , DeviceBootstrapTokenRecord > ;
3538
3639const withLock = createAsyncLock ( ) ;
40+ const log = createSubsystemLogger ( "device-bootstrap" ) ;
3741
3842function resolveBootstrapPath ( baseDir ?: string ) : string {
3943 return path . join ( resolvePairingPaths ( baseDir , "devices" ) . dir , "bootstrap.json" ) ;
4044}
4145
46+ function resolveIssuedBootstrapProfileInput ( params : {
47+ profile ?: DeviceBootstrapProfileInput ;
48+ roles ?: readonly string [ ] ;
49+ scopes ?: readonly string [ ] ;
50+ } ) : DeviceBootstrapProfileInput | undefined {
51+ if ( params . profile ) {
52+ return params . profile ;
53+ }
54+ if ( params . roles || params . scopes ) {
55+ return {
56+ roles : params . roles ,
57+ scopes : params . scopes ,
58+ } ;
59+ }
60+ return undefined ;
61+ }
62+
4263function resolvePersistedBootstrapProfile (
4364 record : Partial < DeviceBootstrapTokenRecord > ,
4465) : DeviceBootstrapProfile {
@@ -56,18 +77,39 @@ function resolveIssuedBootstrapProfile(params: {
5677 roles ?: readonly string [ ] ;
5778 scopes ?: readonly string [ ] ;
5879} ) : DeviceBootstrapProfile {
59- if ( params . profile ) {
60- return normalizeDeviceBootstrapProfile ( params . profile ) ;
61- }
62- if ( params . roles || params . scopes ) {
63- return normalizeDeviceBootstrapProfile ( {
64- roles : params . roles ,
65- scopes : params . scopes ,
66- } ) ;
80+ const input = resolveIssuedBootstrapProfileInput ( params ) ;
81+ if ( input ) {
82+ return normalizeDeviceBootstrapHandoffProfile ( input ) ;
6783 }
6884 return PAIRING_SETUP_BOOTSTRAP_PROFILE ;
6985}
7086
87+ function warnIfIssuedBootstrapScopesWereStripped ( params : {
88+ input : DeviceBootstrapProfileInput | undefined ;
89+ profile : DeviceBootstrapProfile ;
90+ } ) : void {
91+ if ( ! params . input ) {
92+ return ;
93+ }
94+ const requestedProfile = normalizeDeviceBootstrapProfile ( params . input ) ;
95+ const requestedScopes = requestedProfile . scopes ;
96+ if ( requestedScopes . length === 0 ) {
97+ return ;
98+ }
99+ const retainedScopeSet = new Set ( params . profile . scopes ) ;
100+ const strippedScopes = requestedScopes . filter ( ( scope ) => ! retainedScopeSet . has ( scope ) ) ;
101+ if ( strippedScopes . length === 0 ) {
102+ return ;
103+ }
104+ log . warn ( "bootstrap_token_scopes_stripped" , {
105+ roles : requestedProfile . roles ,
106+ requestedScopes,
107+ retainedScopes : params . profile . scopes ,
108+ strippedScopes,
109+ consoleMessage : "bootstrap token scopes stripped to bootstrap handoff allowlist" ,
110+ } ) ;
111+ }
112+
71113function bootstrapProfileAllowsRequest ( params : {
72114 allowedProfile : DeviceBootstrapProfile ;
73115 requestedRole : string ;
@@ -83,13 +125,6 @@ function bootstrapProfileAllowsRequest(params: {
83125 ) ;
84126}
85127
86- function resolveBootstrapProfileScopes ( role : string , scopes : readonly string [ ] ) : string [ ] {
87- if ( role === "operator" ) {
88- return scopes . filter ( ( scope ) => scope . startsWith ( "operator." ) ) ;
89- }
90- return scopes . filter ( ( scope ) => ! scope . startsWith ( "operator." ) ) ;
91- }
92-
93128function bootstrapProfileSatisfiesProfile ( params : {
94129 actualProfile : DeviceBootstrapProfile ;
95130 requiredProfile : DeviceBootstrapProfile ;
@@ -98,7 +133,7 @@ function bootstrapProfileSatisfiesProfile(params: {
98133 if ( ! params . actualProfile . roles . includes ( requiredRole ) ) {
99134 return false ;
100135 }
101- const requiredScopes = resolveBootstrapProfileScopes (
136+ const requiredScopes = resolveBootstrapProfileScopesForRole (
102137 requiredRole ,
103138 params . requiredProfile . scopes ,
104139 ) ;
@@ -175,7 +210,9 @@ export async function issueDeviceBootstrapToken(
175210 const state = await loadState ( params . baseDir ) ;
176211 const token = generatePairingToken ( ) ;
177212 const issuedAtMs = Date . now ( ) ;
213+ const profileInput = resolveIssuedBootstrapProfileInput ( params ) ;
178214 const profile = resolveIssuedBootstrapProfile ( params ) ;
215+ warnIfIssuedBootstrapScopesWereStripped ( { input : profileInput , profile } ) ;
179216 state [ token ] = {
180217 token,
181218 ts : issuedAtMs ,
@@ -276,7 +313,7 @@ export async function redeemDeviceBootstrapTokenProfile(params: {
276313 roles : [ ...resolvePersistedRedeemedProfile ( record ) . roles , params . role ] ,
277314 scopes : [
278315 ...resolvePersistedRedeemedProfile ( record ) . scopes ,
279- ...resolveBootstrapProfileScopes ( params . role , params . scopes ) ,
316+ ...resolveBootstrapProfileScopesForRole ( params . role , params . scopes ) ,
280317 ] ,
281318 } ) ;
282319 state [ tokenKey ] = {
0 commit comments