@talismn/gandalf 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @talismn/gandalf
2
+
3
+ SDK for verifying [Gandalf](../README.md) auth tokens. Fully platform-agnostic — works with Cloudflare Workers, Node.js, Deno, Bun, Express, Hono, and any other JavaScript runtime.
4
+
5
+ - Offline JWT verification via JWKS
6
+ - In-memory JWKS cache (5-min TTL per isolate)
7
+ - ESM + CJS
8
+ - No dependency on the Fetch API `Request` type
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pnpm add @talismn/gandalf
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Verify an access token
19
+
20
+ ```ts
21
+ import { verifyAccessToken, extractBearerToken } from "@talismn/gandalf";
22
+
23
+ export default {
24
+ async fetch(request: Request, env: Env) {
25
+ try {
26
+ const token = extractBearerToken(request.headers.get("Authorization"));
27
+ const auth = await verifyAccessToken(token, {
28
+ // Optional overrides:
29
+ // jwksUrl: env.GANDALF_JWKS_URL,
30
+ // issuer: env.GANDALF_ISSUER,
31
+ });
32
+
33
+ // auth.installId — the verified installation ID
34
+ // auth.claims — full JWT payload
35
+
36
+ return new Response(JSON.stringify({ data: "..." }));
37
+ } catch (err) {
38
+ if (err instanceof GandalfAuthError) {
39
+ return Response.json(
40
+ { error: "unauthorized", message: err.message },
41
+ { status: err.status }
42
+ );
43
+ }
44
+ throw err;
45
+ }
46
+ },
47
+ };
48
+ ```
49
+
50
+ ### Express / Node.js example
51
+
52
+ ```ts
53
+ import { verifyAccessToken, extractBearerToken, GandalfAuthError } from "@talismn/gandalf";
54
+
55
+ app.use("/v1/*", async (req, res, next) => {
56
+ try {
57
+ const token = extractBearerToken(req.headers.authorization);
58
+ const auth = await verifyAccessToken(token);
59
+ req.auth = auth;
60
+ next();
61
+ } catch (err) {
62
+ if (err instanceof GandalfAuthError) {
63
+ return res.status(err.status).json({ error: "unauthorized", message: err.message });
64
+ }
65
+ next(err);
66
+ }
67
+ });
68
+ ```
69
+
70
+ ### Hono middleware example
71
+
72
+ ```ts
73
+ import { Hono } from "hono";
74
+ import {
75
+ verifyAccessToken,
76
+ extractBearerToken,
77
+ GandalfAuthError,
78
+ type AuthContext,
79
+ } from "@talismn/gandalf";
80
+
81
+ type Env = { Bindings: { GANDALF_JWKS_URL: string } };
82
+
83
+ const app = new Hono<Env>();
84
+
85
+ // Auth middleware
86
+ app.use("/v1/*", async (c, next) => {
87
+ try {
88
+ const token = extractBearerToken(c.req.header("Authorization"));
89
+ const auth = await verifyAccessToken(token, {
90
+ jwksUrl: c.env.GANDALF_JWKS_URL,
91
+ });
92
+ c.set("auth", auth);
93
+ await next();
94
+ } catch (err) {
95
+ if (err instanceof GandalfAuthError) {
96
+ return c.json({ error: "unauthorized", message: err.message }, err.status);
97
+ }
98
+ throw err;
99
+ }
100
+ });
101
+
102
+ app.get("/v1/prices", (c) => {
103
+ const auth = c.get("auth") as AuthContext;
104
+ // auth.installId is available for logging, rate limiting, etc.
105
+ return c.json({ prices: [] });
106
+ });
107
+ ```
108
+
109
+ ## Configuration
110
+
111
+ | Variable | Default | Description |
112
+ |----------|---------|-------------|
113
+ | `jwksUrl` | `https://gandalf.talisman.xyz/.well-known/jwks.json` | JWKS endpoint URL |
114
+ | `issuer` | `gandalf.talisman.xyz` | Expected JWT issuer claim |
115
+ | `clockTolerance` | `30` | Seconds of clock skew tolerance for exp/nbf checks |
116
+
117
+ ## JWKS Caching
118
+
119
+ The SDK caches JWKS in-memory per Worker isolate with a 5-minute TTL. When an unknown `kid` is encountered, `jose`'s `createRemoteJWKSet` automatically refetches.
120
+
121
+ ## API Reference
122
+
123
+ ### `verifyAccessToken(token, opts): Promise<AuthContext>`
124
+
125
+ Verifies the JWT signature and claims, returns an `AuthContext`.
126
+
127
+ | Parameter | Type | Description |
128
+ |-----------|------|-------------|
129
+ | `token` | `string` | The raw JWT string |
130
+ | `opts` | `VerifyOpts` | Optional verification overrides |
131
+
132
+ ### `extractBearerToken(authHeader): string`
133
+
134
+ Extracts the raw JWT from an `Authorization` header value (e.g. `"Bearer eyJ..."`). Throws `GandalfAuthError` if the header is missing or malformed.
135
+
136
+ ### `AuthContext`
137
+
138
+ ```ts
139
+ interface AuthContext {
140
+ installId: string; // from sub claim "install:<id>"
141
+ claims: JWTPayload; // full JWT payload
142
+ }
143
+ ```
144
+
145
+ ### Error Classes
146
+
147
+ | Class | Thrown by | Properties |
148
+ |-------|----------|------------|
149
+ | `GandalfAuthError` | `verifyAccessToken` | `.status: number` |
package/dist/index.cjs ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ GandalfAuthError: () => GandalfAuthError,
24
+ extractBearerToken: () => extractBearerToken,
25
+ getJWKS: () => getJWKS,
26
+ verifyAccessToken: () => verifyAccessToken
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/jwks.ts
31
+ var import_jose = require("jose");
32
+ var DEFAULT_JWKS_URL = "https://gandalf.talisman.xyz/.well-known/jwks.json";
33
+ var jwksCache = /* @__PURE__ */ new Map();
34
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
35
+ function getJWKS(jwksUrl = DEFAULT_JWKS_URL) {
36
+ const now = Date.now();
37
+ const cached = jwksCache.get(jwksUrl);
38
+ if (cached && now - cached.createdAt < CACHE_TTL_MS) {
39
+ return cached.jwks;
40
+ }
41
+ const jwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUrl));
42
+ jwksCache.set(jwksUrl, { jwks, createdAt: now });
43
+ return jwks;
44
+ }
45
+
46
+ // src/verify.ts
47
+ var import_jose2 = require("jose");
48
+ var DEFAULT_ISSUER = "gandalf.talisman.xyz";
49
+ function extractBearerToken(authHeader) {
50
+ if (!authHeader) {
51
+ throw new GandalfAuthError("Missing Authorization header", 401);
52
+ }
53
+ const parts = authHeader.split(" ");
54
+ if (parts.length !== 2 || parts[0] !== "Bearer" || !parts[1]) {
55
+ throw new GandalfAuthError(
56
+ "Invalid Authorization header format. Expected: Bearer <token>",
57
+ 401
58
+ );
59
+ }
60
+ return parts[1];
61
+ }
62
+ async function verifyAccessToken(token, opts = {}) {
63
+ const jwks = getJWKS(opts.jwksUrl);
64
+ const issuer = opts.issuer ?? DEFAULT_ISSUER;
65
+ try {
66
+ const { payload } = await (0, import_jose2.jwtVerify)(token, jwks, {
67
+ issuer,
68
+ clockTolerance: opts.clockTolerance ?? 30
69
+ });
70
+ const sub = payload.sub;
71
+ if (!sub?.startsWith("install:")) {
72
+ throw new GandalfAuthError("Invalid sub claim format", 401);
73
+ }
74
+ const installId = sub.slice("install:".length);
75
+ return { installId, claims: payload };
76
+ } catch (err) {
77
+ if (err instanceof GandalfAuthError) throw err;
78
+ const message = err instanceof Error ? err.message : "Token verification failed";
79
+ throw new GandalfAuthError(message, 401);
80
+ }
81
+ }
82
+ var GandalfAuthError = class extends Error {
83
+ status;
84
+ constructor(message, status) {
85
+ super(message);
86
+ this.name = "GandalfAuthError";
87
+ this.status = status;
88
+ }
89
+ };
90
+ // Annotate the CommonJS export names for ESM import in node:
91
+ 0 && (module.exports = {
92
+ GandalfAuthError,
93
+ extractBearerToken,
94
+ getJWKS,
95
+ verifyAccessToken
96
+ });
97
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/jwks.ts","../src/verify.ts"],"sourcesContent":["// Core\n\n// JWKS\nexport { getJWKS } from \"./jwks\";\n// Types\nexport type { AuthContext, VerifyOpts } from \"./types\";\nexport {\n\textractBearerToken,\n\tGandalfAuthError,\n\tverifyAccessToken,\n} from \"./verify\";\n","import { createRemoteJWKSet } from \"jose\";\n\nconst DEFAULT_JWKS_URL = \"https://gandalf.talisman.xyz/.well-known/jwks.json\";\n\n/** In-memory JWKS cache keyed by URL */\nconst jwksCache = new Map<\n\tstring,\n\t{ jwks: ReturnType<typeof createRemoteJWKSet>; createdAt: number }\n>();\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Get a cached JWKS function for the given URL.\n *\n * The returned function is compatible with `jose.jwtVerify()`.\n * On unknown `kid`, jose's `createRemoteJWKSet` automatically refetches.\n */\nexport function getJWKS(\n\tjwksUrl: string = DEFAULT_JWKS_URL,\n): ReturnType<typeof createRemoteJWKSet> {\n\tconst now = Date.now();\n\tconst cached = jwksCache.get(jwksUrl);\n\n\tif (cached && now - cached.createdAt < CACHE_TTL_MS) {\n\t\treturn cached.jwks;\n\t}\n\n\tconst jwks = createRemoteJWKSet(new URL(jwksUrl));\n\tjwksCache.set(jwksUrl, { jwks, createdAt: now });\n\n\treturn jwks;\n}\n","import { jwtVerify } from \"jose\";\nimport { getJWKS } from \"./jwks\";\nimport type { AuthContext, VerifyOpts } from \"./types\";\n\nconst DEFAULT_ISSUER = \"gandalf.talisman.xyz\";\n\n/**\n * Extract a Bearer token from an `Authorization` header value.\n *\n * @param authHeader - The full `Authorization` header string (e.g. `\"Bearer eyJ...\"`).\n * @returns The raw JWT string.\n * @throws GandalfAuthError if the header is missing or malformed.\n */\nexport function extractBearerToken(\n\tauthHeader: string | null | undefined,\n): string {\n\tif (!authHeader) {\n\t\tthrow new GandalfAuthError(\"Missing Authorization header\", 401);\n\t}\n\n\tconst parts = authHeader.split(\" \");\n\tif (parts.length !== 2 || parts[0] !== \"Bearer\" || !parts[1]) {\n\t\tthrow new GandalfAuthError(\n\t\t\t\"Invalid Authorization header format. Expected: Bearer <token>\",\n\t\t\t401,\n\t\t);\n\t}\n\n\treturn parts[1];\n}\n\n/**\n * Verify a Gandalf access token.\n *\n * Verifies the JWT signature and claims, and returns an `AuthContext` on success.\n *\n * @param token - The raw JWT string (not the full Authorization header).\n * @param opts - Optional verification overrides.\n * @throws GandalfAuthError on any verification failure.\n */\nexport async function verifyAccessToken(\n\ttoken: string,\n\topts: VerifyOpts = {},\n): Promise<AuthContext> {\n\t// Get JWKS\n\tconst jwks = getJWKS(opts.jwksUrl);\n\tconst issuer = opts.issuer ?? DEFAULT_ISSUER;\n\n\t// Verify JWT\n\ttry {\n\t\tconst { payload } = await jwtVerify(token, jwks, {\n\t\t\tissuer,\n\t\t\tclockTolerance: opts.clockTolerance ?? 30,\n\t\t});\n\n\t\t// Extract installId from `sub` claim (format: \"install:<id>\")\n\t\tconst sub = payload.sub;\n\t\tif (!sub?.startsWith(\"install:\")) {\n\t\t\tthrow new GandalfAuthError(\"Invalid sub claim format\", 401);\n\t\t}\n\t\tconst installId = sub.slice(\"install:\".length);\n\n\t\treturn { installId, claims: payload };\n\t} catch (err) {\n\t\tif (err instanceof GandalfAuthError) throw err;\n\n\t\tconst message =\n\t\t\terr instanceof Error ? err.message : \"Token verification failed\";\n\t\tthrow new GandalfAuthError(message, 401);\n\t}\n}\n\n/** Error thrown during Gandalf auth verification */\nexport class GandalfAuthError extends Error {\n\tpublic readonly status: number;\n\n\tconstructor(message: string, status: number) {\n\t\tsuper(message);\n\t\tthis.name = \"GandalfAuthError\";\n\t\tthis.status = status;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAmC;AAEnC,IAAM,mBAAmB;AAGzB,IAAM,YAAY,oBAAI,IAGpB;AAEF,IAAM,eAAe,IAAI,KAAK;AAQvB,SAAS,QACf,UAAkB,kBACsB;AACxC,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,SAAS,UAAU,IAAI,OAAO;AAEpC,MAAI,UAAU,MAAM,OAAO,YAAY,cAAc;AACpD,WAAO,OAAO;AAAA,EACf;AAEA,QAAM,WAAO,gCAAmB,IAAI,IAAI,OAAO,CAAC;AAChD,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,IAAI,CAAC;AAE/C,SAAO;AACR;;;AChCA,IAAAA,eAA0B;AAI1B,IAAM,iBAAiB;AAShB,SAAS,mBACf,YACS;AACT,MAAI,CAAC,YAAY;AAChB,UAAM,IAAI,iBAAiB,gCAAgC,GAAG;AAAA,EAC/D;AAEA,QAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,MAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,GAAG;AAC7D,UAAM,IAAI;AAAA,MACT;AAAA,MACA;AAAA,IACD;AAAA,EACD;AAEA,SAAO,MAAM,CAAC;AACf;AAWA,eAAsB,kBACrB,OACA,OAAmB,CAAC,GACG;AAEvB,QAAM,OAAO,QAAQ,KAAK,OAAO;AACjC,QAAM,SAAS,KAAK,UAAU;AAG9B,MAAI;AACH,UAAM,EAAE,QAAQ,IAAI,UAAM,wBAAU,OAAO,MAAM;AAAA,MAChD;AAAA,MACA,gBAAgB,KAAK,kBAAkB;AAAA,IACxC,CAAC;AAGD,UAAM,MAAM,QAAQ;AACpB,QAAI,CAAC,KAAK,WAAW,UAAU,GAAG;AACjC,YAAM,IAAI,iBAAiB,4BAA4B,GAAG;AAAA,IAC3D;AACA,UAAM,YAAY,IAAI,MAAM,WAAW,MAAM;AAE7C,WAAO,EAAE,WAAW,QAAQ,QAAQ;AAAA,EACrC,SAAS,KAAK;AACb,QAAI,eAAe,iBAAkB,OAAM;AAE3C,UAAM,UACL,eAAe,QAAQ,IAAI,UAAU;AACtC,UAAM,IAAI,iBAAiB,SAAS,GAAG;AAAA,EACxC;AACD;AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC3B;AAAA,EAEhB,YAAY,SAAiB,QAAgB;AAC5C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EACf;AACD;","names":["import_jose"]}
@@ -0,0 +1,52 @@
1
+ import { createRemoteJWKSet, JWTPayload } from 'jose';
2
+
3
+ /**
4
+ * Get a cached JWKS function for the given URL.
5
+ *
6
+ * The returned function is compatible with `jose.jwtVerify()`.
7
+ * On unknown `kid`, jose's `createRemoteJWKSet` automatically refetches.
8
+ */
9
+ declare function getJWKS(jwksUrl?: string): ReturnType<typeof createRemoteJWKSet>;
10
+
11
+ /** Result of a successful token verification */
12
+ interface AuthContext {
13
+ /** The installation ID extracted from the `sub` claim */
14
+ installId: string;
15
+ /** The full decoded JWT payload */
16
+ claims: JWTPayload;
17
+ }
18
+ /** Options for verifying an access token */
19
+ interface VerifyOpts {
20
+ /** URL to fetch the JWKS from (default: https://gandalf.talisman.xyz/.well-known/jwks.json) */
21
+ jwksUrl?: string;
22
+ /** Expected `iss` claim (default: gandalf.talisman.xyz) */
23
+ issuer?: string;
24
+ /** Clock tolerance in seconds for `exp`/`nbf` checks (default: 30) */
25
+ clockTolerance?: number;
26
+ }
27
+
28
+ /**
29
+ * Extract a Bearer token from an `Authorization` header value.
30
+ *
31
+ * @param authHeader - The full `Authorization` header string (e.g. `"Bearer eyJ..."`).
32
+ * @returns The raw JWT string.
33
+ * @throws GandalfAuthError if the header is missing or malformed.
34
+ */
35
+ declare function extractBearerToken(authHeader: string | null | undefined): string;
36
+ /**
37
+ * Verify a Gandalf access token.
38
+ *
39
+ * Verifies the JWT signature and claims, and returns an `AuthContext` on success.
40
+ *
41
+ * @param token - The raw JWT string (not the full Authorization header).
42
+ * @param opts - Optional verification overrides.
43
+ * @throws GandalfAuthError on any verification failure.
44
+ */
45
+ declare function verifyAccessToken(token: string, opts?: VerifyOpts): Promise<AuthContext>;
46
+ /** Error thrown during Gandalf auth verification */
47
+ declare class GandalfAuthError extends Error {
48
+ readonly status: number;
49
+ constructor(message: string, status: number);
50
+ }
51
+
52
+ export { type AuthContext, GandalfAuthError, type VerifyOpts, extractBearerToken, getJWKS, verifyAccessToken };
@@ -0,0 +1,52 @@
1
+ import { createRemoteJWKSet, JWTPayload } from 'jose';
2
+
3
+ /**
4
+ * Get a cached JWKS function for the given URL.
5
+ *
6
+ * The returned function is compatible with `jose.jwtVerify()`.
7
+ * On unknown `kid`, jose's `createRemoteJWKSet` automatically refetches.
8
+ */
9
+ declare function getJWKS(jwksUrl?: string): ReturnType<typeof createRemoteJWKSet>;
10
+
11
+ /** Result of a successful token verification */
12
+ interface AuthContext {
13
+ /** The installation ID extracted from the `sub` claim */
14
+ installId: string;
15
+ /** The full decoded JWT payload */
16
+ claims: JWTPayload;
17
+ }
18
+ /** Options for verifying an access token */
19
+ interface VerifyOpts {
20
+ /** URL to fetch the JWKS from (default: https://gandalf.talisman.xyz/.well-known/jwks.json) */
21
+ jwksUrl?: string;
22
+ /** Expected `iss` claim (default: gandalf.talisman.xyz) */
23
+ issuer?: string;
24
+ /** Clock tolerance in seconds for `exp`/`nbf` checks (default: 30) */
25
+ clockTolerance?: number;
26
+ }
27
+
28
+ /**
29
+ * Extract a Bearer token from an `Authorization` header value.
30
+ *
31
+ * @param authHeader - The full `Authorization` header string (e.g. `"Bearer eyJ..."`).
32
+ * @returns The raw JWT string.
33
+ * @throws GandalfAuthError if the header is missing or malformed.
34
+ */
35
+ declare function extractBearerToken(authHeader: string | null | undefined): string;
36
+ /**
37
+ * Verify a Gandalf access token.
38
+ *
39
+ * Verifies the JWT signature and claims, and returns an `AuthContext` on success.
40
+ *
41
+ * @param token - The raw JWT string (not the full Authorization header).
42
+ * @param opts - Optional verification overrides.
43
+ * @throws GandalfAuthError on any verification failure.
44
+ */
45
+ declare function verifyAccessToken(token: string, opts?: VerifyOpts): Promise<AuthContext>;
46
+ /** Error thrown during Gandalf auth verification */
47
+ declare class GandalfAuthError extends Error {
48
+ readonly status: number;
49
+ constructor(message: string, status: number);
50
+ }
51
+
52
+ export { type AuthContext, GandalfAuthError, type VerifyOpts, extractBearerToken, getJWKS, verifyAccessToken };
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ // src/jwks.ts
2
+ import { createRemoteJWKSet } from "jose";
3
+ var DEFAULT_JWKS_URL = "https://gandalf.talisman.xyz/.well-known/jwks.json";
4
+ var jwksCache = /* @__PURE__ */ new Map();
5
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
6
+ function getJWKS(jwksUrl = DEFAULT_JWKS_URL) {
7
+ const now = Date.now();
8
+ const cached = jwksCache.get(jwksUrl);
9
+ if (cached && now - cached.createdAt < CACHE_TTL_MS) {
10
+ return cached.jwks;
11
+ }
12
+ const jwks = createRemoteJWKSet(new URL(jwksUrl));
13
+ jwksCache.set(jwksUrl, { jwks, createdAt: now });
14
+ return jwks;
15
+ }
16
+
17
+ // src/verify.ts
18
+ import { jwtVerify } from "jose";
19
+ var DEFAULT_ISSUER = "gandalf.talisman.xyz";
20
+ function extractBearerToken(authHeader) {
21
+ if (!authHeader) {
22
+ throw new GandalfAuthError("Missing Authorization header", 401);
23
+ }
24
+ const parts = authHeader.split(" ");
25
+ if (parts.length !== 2 || parts[0] !== "Bearer" || !parts[1]) {
26
+ throw new GandalfAuthError(
27
+ "Invalid Authorization header format. Expected: Bearer <token>",
28
+ 401
29
+ );
30
+ }
31
+ return parts[1];
32
+ }
33
+ async function verifyAccessToken(token, opts = {}) {
34
+ const jwks = getJWKS(opts.jwksUrl);
35
+ const issuer = opts.issuer ?? DEFAULT_ISSUER;
36
+ try {
37
+ const { payload } = await jwtVerify(token, jwks, {
38
+ issuer,
39
+ clockTolerance: opts.clockTolerance ?? 30
40
+ });
41
+ const sub = payload.sub;
42
+ if (!sub?.startsWith("install:")) {
43
+ throw new GandalfAuthError("Invalid sub claim format", 401);
44
+ }
45
+ const installId = sub.slice("install:".length);
46
+ return { installId, claims: payload };
47
+ } catch (err) {
48
+ if (err instanceof GandalfAuthError) throw err;
49
+ const message = err instanceof Error ? err.message : "Token verification failed";
50
+ throw new GandalfAuthError(message, 401);
51
+ }
52
+ }
53
+ var GandalfAuthError = class extends Error {
54
+ status;
55
+ constructor(message, status) {
56
+ super(message);
57
+ this.name = "GandalfAuthError";
58
+ this.status = status;
59
+ }
60
+ };
61
+ export {
62
+ GandalfAuthError,
63
+ extractBearerToken,
64
+ getJWKS,
65
+ verifyAccessToken
66
+ };
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/jwks.ts","../src/verify.ts"],"sourcesContent":["import { createRemoteJWKSet } from \"jose\";\n\nconst DEFAULT_JWKS_URL = \"https://gandalf.talisman.xyz/.well-known/jwks.json\";\n\n/** In-memory JWKS cache keyed by URL */\nconst jwksCache = new Map<\n\tstring,\n\t{ jwks: ReturnType<typeof createRemoteJWKSet>; createdAt: number }\n>();\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Get a cached JWKS function for the given URL.\n *\n * The returned function is compatible with `jose.jwtVerify()`.\n * On unknown `kid`, jose's `createRemoteJWKSet` automatically refetches.\n */\nexport function getJWKS(\n\tjwksUrl: string = DEFAULT_JWKS_URL,\n): ReturnType<typeof createRemoteJWKSet> {\n\tconst now = Date.now();\n\tconst cached = jwksCache.get(jwksUrl);\n\n\tif (cached && now - cached.createdAt < CACHE_TTL_MS) {\n\t\treturn cached.jwks;\n\t}\n\n\tconst jwks = createRemoteJWKSet(new URL(jwksUrl));\n\tjwksCache.set(jwksUrl, { jwks, createdAt: now });\n\n\treturn jwks;\n}\n","import { jwtVerify } from \"jose\";\nimport { getJWKS } from \"./jwks\";\nimport type { AuthContext, VerifyOpts } from \"./types\";\n\nconst DEFAULT_ISSUER = \"gandalf.talisman.xyz\";\n\n/**\n * Extract a Bearer token from an `Authorization` header value.\n *\n * @param authHeader - The full `Authorization` header string (e.g. `\"Bearer eyJ...\"`).\n * @returns The raw JWT string.\n * @throws GandalfAuthError if the header is missing or malformed.\n */\nexport function extractBearerToken(\n\tauthHeader: string | null | undefined,\n): string {\n\tif (!authHeader) {\n\t\tthrow new GandalfAuthError(\"Missing Authorization header\", 401);\n\t}\n\n\tconst parts = authHeader.split(\" \");\n\tif (parts.length !== 2 || parts[0] !== \"Bearer\" || !parts[1]) {\n\t\tthrow new GandalfAuthError(\n\t\t\t\"Invalid Authorization header format. Expected: Bearer <token>\",\n\t\t\t401,\n\t\t);\n\t}\n\n\treturn parts[1];\n}\n\n/**\n * Verify a Gandalf access token.\n *\n * Verifies the JWT signature and claims, and returns an `AuthContext` on success.\n *\n * @param token - The raw JWT string (not the full Authorization header).\n * @param opts - Optional verification overrides.\n * @throws GandalfAuthError on any verification failure.\n */\nexport async function verifyAccessToken(\n\ttoken: string,\n\topts: VerifyOpts = {},\n): Promise<AuthContext> {\n\t// Get JWKS\n\tconst jwks = getJWKS(opts.jwksUrl);\n\tconst issuer = opts.issuer ?? DEFAULT_ISSUER;\n\n\t// Verify JWT\n\ttry {\n\t\tconst { payload } = await jwtVerify(token, jwks, {\n\t\t\tissuer,\n\t\t\tclockTolerance: opts.clockTolerance ?? 30,\n\t\t});\n\n\t\t// Extract installId from `sub` claim (format: \"install:<id>\")\n\t\tconst sub = payload.sub;\n\t\tif (!sub?.startsWith(\"install:\")) {\n\t\t\tthrow new GandalfAuthError(\"Invalid sub claim format\", 401);\n\t\t}\n\t\tconst installId = sub.slice(\"install:\".length);\n\n\t\treturn { installId, claims: payload };\n\t} catch (err) {\n\t\tif (err instanceof GandalfAuthError) throw err;\n\n\t\tconst message =\n\t\t\terr instanceof Error ? err.message : \"Token verification failed\";\n\t\tthrow new GandalfAuthError(message, 401);\n\t}\n}\n\n/** Error thrown during Gandalf auth verification */\nexport class GandalfAuthError extends Error {\n\tpublic readonly status: number;\n\n\tconstructor(message: string, status: number) {\n\t\tsuper(message);\n\t\tthis.name = \"GandalfAuthError\";\n\t\tthis.status = status;\n\t}\n}\n"],"mappings":";AAAA,SAAS,0BAA0B;AAEnC,IAAM,mBAAmB;AAGzB,IAAM,YAAY,oBAAI,IAGpB;AAEF,IAAM,eAAe,IAAI,KAAK;AAQvB,SAAS,QACf,UAAkB,kBACsB;AACxC,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,SAAS,UAAU,IAAI,OAAO;AAEpC,MAAI,UAAU,MAAM,OAAO,YAAY,cAAc;AACpD,WAAO,OAAO;AAAA,EACf;AAEA,QAAM,OAAO,mBAAmB,IAAI,IAAI,OAAO,CAAC;AAChD,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,IAAI,CAAC;AAE/C,SAAO;AACR;;;AChCA,SAAS,iBAAiB;AAI1B,IAAM,iBAAiB;AAShB,SAAS,mBACf,YACS;AACT,MAAI,CAAC,YAAY;AAChB,UAAM,IAAI,iBAAiB,gCAAgC,GAAG;AAAA,EAC/D;AAEA,QAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,MAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,GAAG;AAC7D,UAAM,IAAI;AAAA,MACT;AAAA,MACA;AAAA,IACD;AAAA,EACD;AAEA,SAAO,MAAM,CAAC;AACf;AAWA,eAAsB,kBACrB,OACA,OAAmB,CAAC,GACG;AAEvB,QAAM,OAAO,QAAQ,KAAK,OAAO;AACjC,QAAM,SAAS,KAAK,UAAU;AAG9B,MAAI;AACH,UAAM,EAAE,QAAQ,IAAI,MAAM,UAAU,OAAO,MAAM;AAAA,MAChD;AAAA,MACA,gBAAgB,KAAK,kBAAkB;AAAA,IACxC,CAAC;AAGD,UAAM,MAAM,QAAQ;AACpB,QAAI,CAAC,KAAK,WAAW,UAAU,GAAG;AACjC,YAAM,IAAI,iBAAiB,4BAA4B,GAAG;AAAA,IAC3D;AACA,UAAM,YAAY,IAAI,MAAM,WAAW,MAAM;AAE7C,WAAO,EAAE,WAAW,QAAQ,QAAQ;AAAA,EACrC,SAAS,KAAK;AACb,QAAI,eAAe,iBAAkB,OAAM;AAE3C,UAAM,UACL,eAAe,QAAQ,IAAI,UAAU;AACtC,UAAM,IAAI,iBAAiB,SAAS,GAAG;AAAA,EACxC;AACD;AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC3B;AAAA,EAEhB,YAAY,SAAiB,QAAgB;AAC5C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EACf;AACD;","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@talismn/gandalf",
3
+ "version": "0.0.0",
4
+ "description": "SDK for verifying Gandalf auth tokens in Talisman APIs",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/TalismanSociety/gandalf.git",
8
+ "directory": "sdk"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "type": "module",
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "keywords": [
36
+ "gandalf",
37
+ "talisman",
38
+ "auth",
39
+ "jwt"
40
+ ],
41
+ "license": "UNLICENSED",
42
+ "dependencies": {
43
+ "jose": "^6.1.3"
44
+ },
45
+ "devDependencies": {
46
+ "tsup": "^8.5.1",
47
+ "typescript": "^5.9.3",
48
+ "vitest": "^4.0.18"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "typecheck": "tsc --noEmit"
55
+ }
56
+ }