Skip to content

Commit 4047612

Browse files
committed
fix(api): inline ephemeral-jwt signer — provisioner source not in api image
1 parent c67b474 commit 4047612

1 file changed

Lines changed: 44 additions & 6 deletions

File tree

packages/api/src/lib/ephemeral-jwt.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
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-
1513
const TTL_SECONDS = 300;
1614

1715
export 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+
2866
export 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

Comments
 (0)