55 * caller gets back a 5-minute token the CLI can use to hit its tenant directly,
66 * so no long-lived service keys ever touch disk.
77 *
8- * Reuses the same HS256 signer as the provision-time keys so tenant API
9- * validation (which checks signature + `gen` claim against its env vars)
10- * doesn't need a separate code path .
8+ * The signer is inlined (duplicated from provisioner/src/jwt.ts) rather than
9+ * imported. The provisioner is a separate Docker image not bundled with the
10+ * platform API image — a direct import breaks at runtime .
1111 */
1212
13- import { signHs256Jwt } from "@secondlayer/provisioner/src/jwt" ;
14-
1513const TTL_SECONDS = 300 ;
1614
1715export interface MintEphemeralInput {
@@ -25,12 +23,52 @@ export interface MintEphemeralOutput {
2523 expiresAt : string ;
2624}
2725
26+ interface EphemeralJwtPayload {
27+ role : "service" ;
28+ sub : string ;
29+ gen : number ;
30+ iat : number ;
31+ exp : number ;
32+ }
33+
34+ function base64UrlEncode ( input : string | Uint8Array ) : string {
35+ const b64 =
36+ typeof input === "string"
37+ ? Buffer . from ( input ) . toString ( "base64" )
38+ : Buffer . from ( input ) . toString ( "base64" ) ;
39+ return b64 . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / , "" ) ;
40+ }
41+
42+ async function signHs256 (
43+ payload : EphemeralJwtPayload ,
44+ secret : string ,
45+ ) : Promise < string > {
46+ const header = { alg : "HS256" , typ : "JWT" } ;
47+ const encodedHeader = base64UrlEncode ( JSON . stringify ( header ) ) ;
48+ const encodedPayload = base64UrlEncode ( JSON . stringify ( payload ) ) ;
49+ const data = `${ encodedHeader } .${ encodedPayload } ` ;
50+
51+ const enc = new TextEncoder ( ) ;
52+ const key = await crypto . subtle . importKey (
53+ "raw" ,
54+ enc . encode ( secret ) ,
55+ { name : "HMAC" , hash : "SHA-256" } ,
56+ false ,
57+ [ "sign" ] ,
58+ ) ;
59+ const sigBytes = new Uint8Array (
60+ await crypto . subtle . sign ( "HMAC" , key , enc . encode ( data ) ) ,
61+ ) ;
62+ const sig = base64UrlEncode ( sigBytes ) ;
63+ return `${ data } .${ sig } ` ;
64+ }
65+
2866export async function mintEphemeralServiceJwt (
2967 input : MintEphemeralInput ,
3068) : Promise < MintEphemeralOutput > {
3169 const now = Math . floor ( Date . now ( ) / 1000 ) ;
3270 const exp = now + TTL_SECONDS ;
33- const serviceKey = await signHs256Jwt (
71+ const serviceKey = await signHs256 (
3472 {
3573 role : "service" ,
3674 sub : input . slug ,
0 commit comments