Simplify ts-core auth.ts; fix bun-types resolution in monorepo workspace
auth.ts: dropped the legacy HS256-shared-secret verification path, keeping only the JWKS path Supabase recommends now — one code path instead of two. Also fixes a real bug: @types/bun's ambient types didn't resolve inside a bun workspace (its internal 'bun-types' reference can't hoist into a nested package's node_modules the same way it does in a flat install). Depending on bun-types directly instead of @types/bun fixes it — verified with a clean 'bun run typecheck'.
This commit is contained in:
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"bun-types": "^1.1.14",
|
||||
"typescript": "^5.6.3",
|
||||
"elysia": "^1.1.26"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ export interface SupabaseClaims extends JWTPayload {
|
||||
sub: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
aud: string | string[];
|
||||
app_metadata?: Record<string, unknown>;
|
||||
user_metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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.
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
jwtSecret?: string;
|
||||
audience?: string;
|
||||
}
|
||||
|
||||
export interface SupabaseJwtVerifier {
|
||||
verify(token: string): Promise<SupabaseClaims>;
|
||||
}
|
||||
|
||||
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<SupabaseClaims> {
|
||||
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);
|
||||
export function createSupabaseJwtVerifier(supabaseUrl: string) {
|
||||
const jwks = createRemoteJWKSet(new URL("/auth/v1/.well-known/jwks.json", supabaseUrl));
|
||||
|
||||
return {
|
||||
async verify(token: string): Promise<SupabaseClaims> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["bun-types"],
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
Reference in New Issue
Block a user