@@ -181,6 +181,40 @@ describe("deviceHandlers", () => {
181181 ) ;
182182 } ) ;
183183
184+ it ( "rejects removing mixed-role devices without admin scope" , async ( ) => {
185+ getPairedDeviceMock . mockResolvedValue ( {
186+ deviceId : "device-1" ,
187+ role : "operator" ,
188+ roles : [ "operator" , "node" ] ,
189+ tokens : {
190+ operator : {
191+ token : "operator-token" ,
192+ role : "operator" ,
193+ scopes : [ "operator.pairing" ] ,
194+ createdAtMs : 100 ,
195+ } ,
196+ node : {
197+ token : "node-token" ,
198+ role : "node" ,
199+ scopes : [ ] ,
200+ createdAtMs : 100 ,
201+ revokedAtMs : 200 ,
202+ } ,
203+ } ,
204+ } ) ;
205+ const opts = createOptions (
206+ "device.pair.remove" ,
207+ { deviceId : "device-1" } ,
208+ { client : createClient ( [ "operator.pairing" ] , "device-1" , { isDeviceTokenAuth : true } ) } ,
209+ ) ;
210+
211+ await deviceHandlers [ "device.pair.remove" ] ( opts ) ;
212+
213+ expect ( removePairedDeviceMock ) . not . toHaveBeenCalled ( ) ;
214+ expect ( opts . context . disconnectClientsForDevice ) . not . toHaveBeenCalled ( ) ;
215+ expectRespondedErrorMessage ( opts , "device pairing removal denied" ) ;
216+ } ) ;
217+
184218 it ( "disconnects active clients after revoking a device token" , async ( ) => {
185219 revokeDeviceTokenMock . mockResolvedValue ( {
186220 ok : true ,
@@ -234,6 +268,20 @@ describe("deviceHandlers", () => {
234268 ) ;
235269 } ) ;
236270
271+ it ( "rejects revoking node tokens without admin scope" , async ( ) => {
272+ const opts = createOptions (
273+ "device.token.revoke" ,
274+ { deviceId : "device-1" , role : "node" } ,
275+ { client : createClient ( [ "operator.pairing" ] , "device-1" , { isDeviceTokenAuth : true } ) } ,
276+ ) ;
277+
278+ await deviceHandlers [ "device.token.revoke" ] ( opts ) ;
279+
280+ expect ( revokeDeviceTokenMock ) . not . toHaveBeenCalled ( ) ;
281+ expect ( opts . context . disconnectClientsForDevice ) . not . toHaveBeenCalled ( ) ;
282+ expectRespondedErrorMessage ( opts , "device token revocation denied" ) ;
283+ } ) ;
284+
237285 it ( "treats normalized device ids as self-owned for token revocation" , async ( ) => {
238286 revokeDeviceTokenMock . mockResolvedValue ( {
239287 ok : true ,
@@ -336,6 +384,65 @@ describe("deviceHandlers", () => {
336384 ) ;
337385 } ) ;
338386
387+ it ( "allows pairing-scoped device sessions to manage their own operator token" , async ( ) => {
388+ rotateDeviceTokenMock . mockResolvedValue ( {
389+ ok : true ,
390+ entry : {
391+ token : "rotated-token" ,
392+ role : "operator" ,
393+ scopes : [ "operator.pairing" ] ,
394+ createdAtMs : 456 ,
395+ rotatedAtMs : 789 ,
396+ } ,
397+ } ) ;
398+ revokeDeviceTokenMock . mockResolvedValue ( {
399+ ok : true ,
400+ entry : { role : "operator" , revokedAtMs : 987 } ,
401+ } ) ;
402+
403+ const rotateOpts = createOptions (
404+ "device.token.rotate" ,
405+ { deviceId : "device-1" , role : "operator" , scopes : [ "operator.pairing" ] } ,
406+ { client : createClient ( [ "operator.pairing" ] , "device-1" , { isDeviceTokenAuth : true } ) } ,
407+ ) ;
408+ const revokeOpts = createOptions (
409+ "device.token.revoke" ,
410+ { deviceId : "device-1" , role : "operator" } ,
411+ { client : createClient ( [ "operator.pairing" ] , "device-1" , { isDeviceTokenAuth : true } ) } ,
412+ ) ;
413+
414+ await deviceHandlers [ "device.token.rotate" ] ( rotateOpts ) ;
415+ await deviceHandlers [ "device.token.revoke" ] ( revokeOpts ) ;
416+
417+ expect ( rotateDeviceTokenMock ) . toHaveBeenCalledWith ( {
418+ deviceId : "device-1" ,
419+ role : "operator" ,
420+ scopes : [ "operator.pairing" ] ,
421+ callerScopes : [ "operator.pairing" ] ,
422+ } ) ;
423+ expect ( revokeDeviceTokenMock ) . toHaveBeenCalledWith ( {
424+ deviceId : "device-1" ,
425+ role : "operator" ,
426+ callerScopes : [ "operator.pairing" ] ,
427+ } ) ;
428+ expect ( rotateOpts . respond ) . toHaveBeenCalledWith (
429+ true ,
430+ {
431+ deviceId : "device-1" ,
432+ role : "operator" ,
433+ token : "rotated-token" ,
434+ scopes : [ "operator.pairing" ] ,
435+ rotatedAtMs : 789 ,
436+ } ,
437+ undefined ,
438+ ) ;
439+ expect ( revokeOpts . respond ) . toHaveBeenCalledWith (
440+ true ,
441+ { deviceId : "device-1" , role : "operator" , revokedAtMs : 987 } ,
442+ undefined ,
443+ ) ;
444+ } ) ;
445+
339446 it ( "omits rotated tokens when an admin rotates another device token" , async ( ) => {
340447 mockPairedOperatorDevice ( ) ;
341448 mockRotateOperatorTokenSuccess ( ) ;
@@ -368,8 +475,27 @@ describe("deviceHandlers", () => {
368475 } ) ;
369476
370477 it ( "rejects rotating a token for a role that was never approved" , async ( ) => {
371- mockPairedOperatorDevice ( ) ;
372478 rotateDeviceTokenMock . mockResolvedValue ( { ok : false , reason : "unknown-device-or-role" } ) ;
479+ const opts = createOptions (
480+ "device.token.rotate" ,
481+ { deviceId : "device-1" , role : "node" } ,
482+ { client : createClient ( [ "operator.admin" ] , "admin-device" , { isDeviceTokenAuth : true } ) } ,
483+ ) ;
484+
485+ await deviceHandlers [ "device.token.rotate" ] ( opts ) ;
486+
487+ expect ( rotateDeviceTokenMock ) . toHaveBeenCalledWith ( {
488+ deviceId : "device-1" ,
489+ role : "node" ,
490+ scopes : undefined ,
491+ callerScopes : [ "operator.admin" ] ,
492+ } ) ;
493+ expect ( opts . context . disconnectClientsForDevice ) . not . toHaveBeenCalled ( ) ;
494+ expectRespondedErrorMessage ( opts , "device token rotation denied" ) ;
495+ } ) ;
496+
497+ it ( "rejects rotating node tokens without admin scope" , async ( ) => {
498+ mockPairedOperatorDevice ( ) ;
373499 const opts = createOptions (
374500 "device.token.rotate" ,
375501 {
@@ -387,12 +513,7 @@ describe("deviceHandlers", () => {
387513
388514 await deviceHandlers [ "device.token.rotate" ] ( opts ) ;
389515
390- expect ( rotateDeviceTokenMock ) . toHaveBeenCalledWith ( {
391- deviceId : "device-1" ,
392- role : "node" ,
393- scopes : undefined ,
394- callerScopes : [ "operator.pairing" ] ,
395- } ) ;
516+ expect ( rotateDeviceTokenMock ) . not . toHaveBeenCalled ( ) ;
396517 expect ( opts . context . disconnectClientsForDevice ) . not . toHaveBeenCalled ( ) ;
397518 expectRespondedErrorMessage ( opts , "device token rotation denied" ) ;
398519 } ) ;
@@ -688,6 +809,27 @@ describe("deviceHandlers", () => {
688809 ) ;
689810 } ) ;
690811
812+ it ( "rejects approving node roles for the caller device without admin scope" , async ( ) => {
813+ getPendingDevicePairingMock . mockResolvedValue ( {
814+ requestId : "req-1" ,
815+ deviceId : " device-1 " ,
816+ publicKey : "pk-1" ,
817+ role : "node" ,
818+ roles : [ "node" ] ,
819+ ts : 100 ,
820+ } ) ;
821+ const opts = createOptions (
822+ "device.pair.approve" ,
823+ { requestId : "req-1" } ,
824+ { client : createClient ( [ "operator.pairing" ] , "device-1" , { isDeviceTokenAuth : true } ) } ,
825+ ) ;
826+
827+ await deviceHandlers [ "device.pair.approve" ] ( opts ) ;
828+
829+ expect ( approveDevicePairingMock ) . not . toHaveBeenCalled ( ) ;
830+ expectRespondedErrorMessage ( opts , "device pairing approval denied" ) ;
831+ } ) ;
832+
691833 it ( "rejects rejecting another device from a non-admin device session" , async ( ) => {
692834 getPendingDevicePairingMock . mockResolvedValue ( {
693835 requestId : "req-2" ,
0 commit comments