diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..d50b773 --- /dev/null +++ b/bun.lock @@ -0,0 +1,74 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "continuum-common", + }, + "packages/ts-core": { + "name": "@continuum/ts-core", + "version": "0.1.0", + "dependencies": { + "jose": "^5.9.6", + }, + "devDependencies": { + "bun-types": "^1.1.14", + "elysia": "^1.1.26", + "typescript": "^5.6.3", + }, + "peerDependencies": { + "elysia": ">=1.1.0", + }, + "optionalPeers": [ + "elysia", + ], + }, + }, + "packages": { + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + + "@continuum/ts-core": ["@continuum/ts-core@workspace:packages/ts-core"], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "elysia": ["elysia@1.4.30", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-S2qqV0CbM4faB1hQQ5IYoeYnAUinjHlzRduxaBc4OoNqh0GjOc8k6tt3hv5ozWcG6Svr6hugw6JL4zNke4jwfA=="], + + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "file-type": ["file-type@22.0.2", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/packages/ts-core/package.json b/packages/ts-core/package.json index 4032643..fa68f37 100644 --- a/packages/ts-core/package.json +++ b/packages/ts-core/package.json @@ -24,7 +24,7 @@ } }, "devDependencies": { - "@types/bun": "^1.1.14", + "bun-types": "^1.1.14", "typescript": "^5.6.3", "elysia": "^1.1.26" } diff --git a/packages/ts-core/src/auth.ts b/packages/ts-core/src/auth.ts index 0ed0434..7ededcd 100644 --- a/packages/ts-core/src/auth.ts +++ b/packages/ts-core/src/auth.ts @@ -4,9 +4,6 @@ export interface SupabaseClaims extends JWTPayload { sub: string; email?: string; role?: string; - aud: string | string[]; - app_metadata?: Record; - user_metadata?: Record; } export class JwtVerificationError extends Error { @@ -16,63 +13,28 @@ export class JwtVerificationError extends Error { } } -export interface VerifierOptions { - /** Supabase project URL, e.g. https://xyzcompany.supabase.co */ - supabaseUrl: string; - /** - * Legacy HS256 project JWT secret. When provided, verification uses this - * shared secret instead of fetching the project's JWKS. Prefer leaving - * this unset for projects on Supabase's newer asymmetric (ES256/RS256) - * signing keys. - */ - jwtSecret?: string; - audience?: string; -} - -export interface SupabaseJwtVerifier { - verify(token: string): Promise; -} - -export function createSupabaseJwtVerifier(options: VerifierOptions): SupabaseJwtVerifier { - const audience = options.audience ?? "authenticated"; - - if (options.jwtSecret) { - const key = new TextEncoder().encode(options.jwtSecret); - return { - async verify(token: string): Promise { - try { - const { payload } = await jwtVerify(token, key, { - algorithms: ["HS256"], - audience, - }); - return payload as SupabaseClaims; - } catch (err) { - throw new JwtVerificationError("failed to verify Supabase JWT (HS256)", err); - } - }, - }; - } - - const jwksUrl = new URL("/auth/v1/.well-known/jwks.json", options.supabaseUrl); - const jwks = createRemoteJWKSet(jwksUrl); +/** + * Verifies a Supabase-issued access token against the project's public + * JWKS (fetched once and cached). `supabaseUrl` is your project URL, e.g. + * `https://xyzcompany.supabase.co`. + */ +export function createSupabaseJwtVerifier(supabaseUrl: string) { + const jwks = createRemoteJWKSet(new URL("/auth/v1/.well-known/jwks.json", supabaseUrl)); return { async verify(token: string): Promise { try { - const { payload } = await jwtVerify(token, jwks, { audience }); + const { payload } = await jwtVerify(token, jwks, { audience: "authenticated" }); return payload as SupabaseClaims; } catch (err) { - throw new JwtVerificationError("failed to verify Supabase JWT (JWKS)", err); + throw new JwtVerificationError("failed to verify Supabase JWT", err); } }, }; } export function extractBearerToken(authorizationHeader: string | null | undefined): string { - if (!authorizationHeader) { - throw new JwtVerificationError("missing Authorization header"); - } - const [scheme, token] = authorizationHeader.split(" "); + const [scheme, token] = (authorizationHeader ?? "").split(" "); if (scheme !== "Bearer" || !token) { throw new JwtVerificationError("Authorization header is not a Bearer token"); } diff --git a/packages/ts-core/tsconfig.json b/packages/ts-core/tsconfig.json index 7c00750..55eb000 100644 --- a/packages/ts-core/tsconfig.json +++ b/packages/ts-core/tsconfig.json @@ -4,6 +4,7 @@ "module": "ESNext", "moduleResolution": "Bundler", "lib": ["ES2022"], + "types": ["bun-types"], "strict": true, "declaration": true, "declarationMap": true,