@tillstack/auth-node 0.3.1 → 0.5.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 CHANGED
@@ -17,8 +17,9 @@ npm install @tillstack/auth-node
17
17
  import { createVerifier } from '@tillstack/auth-node'
18
18
 
19
19
  const verifier = createVerifier({
20
- appId: 'tau_your_app_id', // TillAuth public app id
20
+ appId: 'tau_your_app_id', // the app's public id (its publishable key or app id works too)
21
21
  // apiBase: 'https://auth.tilldev.dev', // default
22
+ // issuer: 'https://auth.tilldev.dev/realms/<app id>', // refuse the shared key once the app has its own
22
23
  // requireMfa: true,
23
24
  })
24
25
 
@@ -48,8 +49,75 @@ const verifier = createVerifier({ appId: 'tau_your_app_id' })
48
49
  fastify.addHook('preHandler', fastifyHook(verifier)) // sets req.user
49
50
  ```
50
51
 
52
+ ### Webhooks
53
+
54
+ ```ts
55
+ import express from 'express'
56
+ import { verifyWebhook, TillAuthWebhookError } from '@tillstack/auth-node'
57
+
58
+ const app = express()
59
+
60
+ // Express: mount with express.raw({ type: 'application/json' }) so req.body is the raw bytes.
61
+ app.post('/webhooks/tillauth', express.raw({ type: 'application/json' }), async (req, res) => {
62
+ try {
63
+ const event = await verifyWebhook(req.body, req.get('x-tillauth-signature'), process.env.TILLAUTH_WEBHOOK_SECRET!)
64
+ // event.type, event.user_id, event.delivery_id
65
+ res.sendStatus(204)
66
+ } catch (e) {
67
+ if (e instanceof TillAuthWebhookError) return res.status(400).send(e.reason)
68
+ throw e
69
+ }
70
+ })
71
+ ```
72
+
73
+ Rejects a bad signature, a timestamp older than five minutes, and a body that was re-serialized.
74
+
75
+ ### Signing keys
76
+
77
+ The verifier picks the key set from the token's issuer: tokens signed with the
78
+ app's own key are checked against
79
+ `https://auth.tilldev.dev/realms/<appId>/.well-known/jwks.json`, which holds only
80
+ that app's keys; tokens from an app still on the shared key are checked against
81
+ `https://auth.tilldev.dev/.well-known/jwks.json`. Another app's tokens are
82
+ refused either way. Pin `issuer` to the app's issuer to refuse the shared key
83
+ entirely.
84
+
51
85
  For fully air-gapped verification, pass inline public keys via `publicJwks`
52
- instead of letting the SDK fetch the JWKS.
86
+ instead of letting the SDK fetch them. A rotation publishes the new key 15
87
+ minutes before it signs and the previous key verifies for 24 hours after, so
88
+ update the inline keys when you rotate.
89
+
90
+ ### Internal apps
91
+
92
+ When a TillAuth internal app sits in front of your tool, the proxy forwards the
93
+ person's identity as `X-TillDev-Email`, `X-TillDev-Role` and friends, plus
94
+ `X-TillDev-Assertion`, a token signed for that internal app and that host only.
95
+ Verify the assertion instead of trusting the plain headers, so a request that
96
+ skipped the proxy can't claim to be someone:
97
+
98
+ ```ts
99
+ import { createAccessVerifier, AccessAssertionError } from '@tillstack/auth-node'
100
+
101
+ const access = createAccessVerifier({
102
+ issuer: process.env.TILLDEV_ACCESS_ISSUER!, // shown on the internal app's page
103
+ audience: process.env.TILLDEV_ACCESS_AUDIENCE!, // its taac_… id
104
+ })
105
+
106
+ app.use(async (req, res, next) => {
107
+ try {
108
+ req.user = await access.verifyRequest(req.headers) // email, role, groups, mfa, auth_time, sub
109
+ next()
110
+ } catch (e) {
111
+ if (e instanceof AccessAssertionError) return res.status(401).send(e.code)
112
+ next(e)
113
+ }
114
+ })
115
+ ```
116
+
117
+ `verifyRequest` takes Node, Express or Fastify headers, or a fetch `Headers`, and
118
+ refuses an assertion issued for another host. It lasts 5 minutes and is reused
119
+ across that person's requests for up to 4 of them, so it always arrives with a
120
+ minute or more left; it is not a one-time token.
53
121
 
54
122
  ## Docs
55
123
 
@@ -57,4 +125,4 @@ Full documentation: https://tilldev.dev/docs
57
125
 
58
126
  ## License
59
127
 
60
- MIT
128
+ Apache-2.0
package/dist/index.cjs CHANGED
@@ -124,10 +124,10 @@ function expressCookieProxy(opts) {
124
124
  res.json(JSON.parse(out.body));
125
125
  };
126
126
  }
127
- function parseCookieHeader(header) {
127
+ function parseCookieHeader(header2) {
128
128
  const out = {};
129
- if (!header) return out;
130
- for (const part of header.split(";")) {
129
+ if (!header2) return out;
130
+ for (const part of header2.split(";")) {
131
131
  const [k, ...rest] = part.trim().split("=");
132
132
  if (!k) continue;
133
133
  out[k] = decodeURIComponent(rest.join("="));
@@ -135,17 +135,129 @@ function parseCookieHeader(header) {
135
135
  return out;
136
136
  }
137
137
 
138
+ // src/webhooks.ts
139
+ var TillAuthWebhookError = class extends Error {
140
+ constructor(reason) {
141
+ super(`TillAuth webhook rejected: ${reason}`);
142
+ this.reason = reason;
143
+ this.name = "TillAuthWebhookError";
144
+ }
145
+ };
146
+ var enc = new TextEncoder();
147
+ function hexBytes(hex) {
148
+ if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null;
149
+ const out = new Uint8Array(hex.length / 2);
150
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
151
+ return out;
152
+ }
153
+ async function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
154
+ if (!signatureHeader) throw new TillAuthWebhookError("missing_signature");
155
+ let t = null;
156
+ const sigs = [];
157
+ for (const part of signatureHeader.split(",")) {
158
+ const [k, v] = part.trim().split("=", 2);
159
+ if (k === "t" && v && /^\d+$/.test(v)) t = Number(v);
160
+ else if (k === "v1" && v) {
161
+ const b = hexBytes(v);
162
+ if (b) sigs.push(b);
163
+ }
164
+ }
165
+ if (t === null || sigs.length === 0) throw new TillAuthWebhookError("malformed_signature");
166
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
167
+ if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError("stale");
168
+ const body = typeof rawBody === "string" ? enc.encode(rawBody) : rawBody;
169
+ const prefix = enc.encode(`${t}.`);
170
+ const signed = new Uint8Array(prefix.length + body.length);
171
+ signed.set(prefix);
172
+ signed.set(body, prefix.length);
173
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
174
+ let ok = false;
175
+ for (const s of sigs) if (await crypto.subtle.verify("HMAC", key, s, signed)) ok = true;
176
+ if (!ok) throw new TillAuthWebhookError("bad_signature");
177
+ try {
178
+ return JSON.parse(new TextDecoder().decode(body));
179
+ } catch {
180
+ throw new TillAuthWebhookError("bad_payload");
181
+ }
182
+ }
183
+ var ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
184
+ var ASSERTION_TYP = "tilldev-access+jwt";
185
+ var AccessAssertionError = class extends Error {
186
+ constructor(message, code) {
187
+ super(message);
188
+ this.name = "AccessAssertionError";
189
+ this.code = code;
190
+ }
191
+ };
192
+ function header(headers, name) {
193
+ if (typeof headers.get === "function") return headers.get(name) ?? void 0;
194
+ const v = headers[name];
195
+ return Array.isArray(v) ? v[0] : v;
196
+ }
197
+ var AccessVerifier = class {
198
+ constructor(opts) {
199
+ if (!opts.issuer || !opts.audience) throw new Error("AccessVerifier: issuer and audience are required");
200
+ this.issuer = opts.issuer.replace(/\/+$/, "");
201
+ this.audience = opts.audience;
202
+ this.tolerance = opts.clockToleranceSec ?? 30;
203
+ this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => jose.importJWK(k, "EdDSA"))) : jose.createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`));
204
+ }
205
+ /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
206
+ async verify(token, expect = {}) {
207
+ if (!token) throw new AccessAssertionError("No identity assertion; is the request coming through the proxy?", "no_assertion");
208
+ const options = { algorithms: ["EdDSA"], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ["sub", "exp", "iat"] };
209
+ let payload;
210
+ try {
211
+ if (this.keys instanceof Promise) {
212
+ let last;
213
+ payload = {};
214
+ let ok = false;
215
+ for (const key of await this.keys) {
216
+ try {
217
+ payload = (await jose.jwtVerify(token, key, options)).payload;
218
+ ok = true;
219
+ break;
220
+ } catch (e) {
221
+ last = e;
222
+ }
223
+ }
224
+ if (!ok) throw last ?? new Error("No key matched");
225
+ } else {
226
+ payload = (await jose.jwtVerify(token, this.keys, options)).payload;
227
+ }
228
+ } catch (e) {
229
+ throw new AccessAssertionError(e instanceof Error ? e.message : "Invalid assertion", "invalid_assertion");
230
+ }
231
+ if (typeof payload["email"] !== "string" || typeof payload["host"] !== "string" || payload["access_app"] !== this.audience) {
232
+ throw new AccessAssertionError("Assertion is missing its identity", "invalid_assertion");
233
+ }
234
+ if (expect.host !== void 0 && payload["host"] !== expect.host.toLowerCase().replace(/:\d+$/, "")) {
235
+ throw new AccessAssertionError("Assertion was issued for another host", "wrong_host");
236
+ }
237
+ const groups = Array.isArray(payload["groups"]) ? payload["groups"].filter((g) => typeof g === "string") : [];
238
+ return { ...payload, groups };
239
+ }
240
+ /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
241
+ verifyRequest(headers) {
242
+ const host = header(headers, "host");
243
+ return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {});
244
+ }
245
+ };
246
+ function createAccessVerifier(opts) {
247
+ return new AccessVerifier(opts);
248
+ }
249
+
138
250
  // src/index.ts
139
251
  var DEFAULT_API2 = "https://auth.tilldev.dev";
140
252
  var REALM_SEGMENT = "/realms/";
141
253
  function trimSlash(s) {
142
254
  return s.replace(/\/+$/, "");
143
255
  }
144
- function issuerAccepted(iss, canonical, appId, pinnedIssuer) {
256
+ function issuerAccepted(iss, canonical, realm, pinnedIssuer) {
145
257
  if (typeof iss !== "string" || iss.length === 0) return false;
146
258
  if (pinnedIssuer) return iss === trimSlash(pinnedIssuer);
147
259
  const canon = trimSlash(canonical);
148
- return iss === canon || iss === `${canon}${REALM_SEGMENT}${appId}`;
260
+ return iss === canon || typeof realm === "string" && iss === `${canon}${REALM_SEGMENT}${realm}`;
149
261
  }
150
262
  var TillAuthVerifyError = class extends Error {
151
263
  constructor(message, code) {
@@ -160,7 +272,7 @@ var TillAuthVerifier = class {
160
272
  this.remoteRealm = null;
161
273
  if (!opts.appId) throw new Error("TillAuthVerifier: appId is required");
162
274
  this.opts = {
163
- appId: opts.appId,
275
+ appId: opts.appId.startsWith("tau_pub_") ? "tau_" + opts.appId.slice("tau_pub_".length) : opts.appId,
164
276
  apiBase: (opts.apiBase ?? DEFAULT_API2).replace(/\/$/, ""),
165
277
  requireVerifiedEmail: opts.requireVerifiedEmail ?? false,
166
278
  requireMfa: opts.requireMfa ?? false
@@ -186,12 +298,17 @@ var TillAuthVerifier = class {
186
298
  * its issuer to reach a different key set must still verify against that set — and
187
299
  * that set contains only keys entitled to the issuer it named.
188
300
  */
189
- keySetFor(iss) {
190
- if (typeof iss !== "string") return null;
191
- if (iss === this.opts.apiBase) return this.remote;
192
- if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}`) return this.remoteRealm;
301
+ keySetFor(payload) {
302
+ if (typeof payload.iss !== "string") return null;
303
+ if (payload.iss === this.opts.apiBase) return this.remote;
304
+ if (this.matchesApp(payload) && typeof payload.app === "string" && payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`) {
305
+ return this.remoteRealm;
306
+ }
193
307
  return null;
194
308
  }
309
+ matchesApp(claims) {
310
+ return claims.app === this.opts.appId || claims.pid === this.opts.appId;
311
+ }
195
312
  async verify(token) {
196
313
  if (!token) throw new TillAuthVerifyError("No token", "no_token");
197
314
  let claims;
@@ -210,14 +327,14 @@ var TillAuthVerifier = class {
210
327
  }
211
328
  throw lastErr ?? new Error("No key matched");
212
329
  }
213
- const unverifiedIss = jose.decodeJwt(token).iss;
214
- const keySet = this.keySetFor(unverifiedIss);
330
+ const unverified = jose.decodeJwt(token);
331
+ const keySet = this.keySetFor(unverified);
215
332
  if (!keySet) {
216
333
  throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
217
334
  }
218
335
  const { payload } = await jose.jwtVerify(token, keySet, {
219
336
  algorithms: ["EdDSA"],
220
- issuer: unverifiedIss
337
+ issuer: unverified.iss
221
338
  });
222
339
  claims = payload;
223
340
  } catch (e) {
@@ -230,12 +347,12 @@ var TillAuthVerifier = class {
230
347
  return this.applyClaimsChecks(claims);
231
348
  }
232
349
  applyClaimsChecks(claims) {
233
- if (!issuerAccepted(claims.iss, this.opts.apiBase, this.opts.appId, this.pinnedIssuer)) {
234
- throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
235
- }
236
- if (claims.app !== this.opts.appId) {
350
+ if (!this.matchesApp(claims)) {
237
351
  throw new TillAuthVerifyError(`Token issued for different app`, "wrong_app");
238
352
  }
353
+ if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {
354
+ throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
355
+ }
239
356
  if (this.opts.requireMfa && !claims.mfa) {
240
357
  throw new TillAuthVerifyError("MFA required", "mfa_required");
241
358
  }
@@ -292,12 +409,18 @@ function createVerifier(opts) {
292
409
  return new TillAuthVerifier(opts);
293
410
  }
294
411
 
412
+ exports.ACCESS_ASSERTION_HEADER = ACCESS_ASSERTION_HEADER;
413
+ exports.AccessAssertionError = AccessAssertionError;
414
+ exports.AccessVerifier = AccessVerifier;
295
415
  exports.TillAuthVerifier = TillAuthVerifier;
296
416
  exports.TillAuthVerifyError = TillAuthVerifyError;
417
+ exports.TillAuthWebhookError = TillAuthWebhookError;
418
+ exports.createAccessVerifier = createAccessVerifier;
297
419
  exports.createFetchHandler = createFetchHandler;
298
420
  exports.createVerifier = createVerifier;
299
421
  exports.expressCookieProxy = expressCookieProxy;
300
422
  exports.expressMiddleware = expressMiddleware;
301
423
  exports.fastifyHook = fastifyHook;
424
+ exports.verifyWebhook = verifyWebhook;
302
425
  //# sourceMappingURL=index.cjs.map
303
426
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cookie-proxy.ts","../src/index.ts"],"names":["DEFAULT_API","importJWK","createRemoteJWKSet","payload","jwtVerify","decodeJwt"],"mappings":"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","file":"index.cjs","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n appId: string,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || iss === `${canon}${REALM_SEGMENT}${appId}`\n}\n\nexport interface VerifyOptions {\n /** TillAuth public app id (`tau_…`). Used to enforce app scoping. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer (e.g. a dedicated realm's\n * `https://auth.tilldev.dev/realms/<app>`).\n *\n * Rarely needed now: when omitted, the verifier accepts the canonical HTTPS issuer\n * (derived from `apiBase`) and THIS app's own realm issuer — never another realm's.\n * It previously accepted any realm under `apiBase`, which is what made this option\n * look load-bearing; it was not, because the key still came from a merged JWKS\n * (`atrium` `00` D13). The retired legacy `\"tillauth\"` string is never accepted.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(iss: unknown): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof iss !== 'string') return null\n if (iss === this.opts.apiBase) return this.remote\n if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}`) return this.remoteRealm\n return null\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverifiedIss = decodeJwt(token).iss\n const keySet = this.keySetFor(unverifiedIss)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverifiedIss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!issuerAccepted(claims.iss, this.opts.apiBase, this.opts.appId, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (claims.app !== this.opts.appId) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\n"]}
1
+ {"version":3,"sources":["../src/cookie-proxy.ts","../src/webhooks.ts","../src/access.ts","../src/index.ts"],"names":["header","importJWK","createRemoteJWKSet","jwtVerify","DEFAULT_API","payload","decodeJwt"],"mappings":"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","file":"index.cjs","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","// Verifies TillAuth webhook deliveries (header `X-TillAuth-Signature: t=<unix>,v1=<hex>`).\n\nexport interface TillAuthWebhookEvent {\n type: string\n result: string\n ts: string\n app_id: string\n user_id: string | null\n ip: string | null\n user_agent: string | null\n metadata: Record<string, unknown>\n delivery_id: string\n attempt: number\n}\n\nexport type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload'\n\nexport class TillAuthWebhookError extends Error {\n constructor(readonly reason: WebhookErrorReason) {\n super(`TillAuth webhook rejected: ${reason}`)\n this.name = 'TillAuthWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Max age of the signed timestamp, in seconds. Default 300. */\n toleranceSeconds?: number\n /** Unix seconds; for tests. */\n now?: number\n}\n\nconst enc = new TextEncoder()\n\nfunction hexBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n return out\n}\n\n/**\n * Checks the signature over the exact bytes received, then parses them.\n * Pass the raw body, never a re-serialized object.\n */\nexport async function verifyWebhook(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): Promise<TillAuthWebhookEvent> {\n if (!signatureHeader) throw new TillAuthWebhookError('missing_signature')\n\n let t: number | null = null\n const sigs: Uint8Array[] = []\n for (const part of signatureHeader.split(',')) {\n const [k, v] = part.trim().split('=', 2)\n if (k === 't' && v && /^\\d+$/.test(v)) t = Number(v)\n else if (k === 'v1' && v) {\n const b = hexBytes(v)\n if (b) sigs.push(b)\n }\n }\n if (t === null || sigs.length === 0) throw new TillAuthWebhookError('malformed_signature')\n\n const now = opts.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError('stale')\n\n const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody\n const prefix = enc.encode(`${t}.`)\n const signed = new Uint8Array(prefix.length + body.length)\n signed.set(prefix)\n signed.set(body, prefix.length)\n\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])\n let ok = false\n for (const s of sigs) if (await crypto.subtle.verify('HMAC', key, s, signed)) ok = true\n if (!ok) throw new TillAuthWebhookError('bad_signature')\n\n try {\n return JSON.parse(new TextDecoder().decode(body)) as TillAuthWebhookEvent\n } catch {\n throw new TillAuthWebhookError('bad_payload')\n }\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, type JWK, type JWTVerifyGetKey } from 'jose'\n\nexport const ACCESS_ASSERTION_HEADER = 'x-tilldev-assertion'\nconst ASSERTION_TYP = 'tilldev-access+jwt'\n\nexport interface AccessVerifyOptions {\n /** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */\n issuer: string\n /** The internal app's audience (its `taac_…` client id). */\n audience: string\n /** Defaults to `{issuer}/.well-known/jwks.json`. */\n jwksUri?: string\n /** Inline public keys instead of fetching the key set. */\n publicJwks?: JWK[]\n /** Seconds of clock skew to allow. Defaults to 30. */\n clockToleranceSec?: number\n}\n\nexport interface AccessIdentity {\n sub: string\n email: string\n email_verified: boolean\n role: string\n /** Group names, sorted; at most 100. Empty when the person is in none. */\n groups: string[]\n mfa: boolean\n /** When the person signed in, in seconds. */\n auth_time: number\n sid: string\n /** The host the proxy checked the request for. */\n host: string\n access_app: string\n iss: string\n iat: number\n exp: number\n jti: string\n}\n\nexport class AccessAssertionError extends Error {\n code: 'no_assertion' | 'invalid_assertion' | 'wrong_host'\n constructor(message: string, code: AccessAssertionError['code']) {\n super(message)\n this.name = 'AccessAssertionError'\n this.code = code\n }\n}\n\ntype HeaderBag = Record<string, string | string[] | undefined> | { get(name: string): string | null }\n\nfunction header(headers: HeaderBag, name: string): string | undefined {\n if (typeof (headers as { get?: unknown }).get === 'function') return (headers as { get(n: string): string | null }).get(name) ?? undefined\n const v = (headers as Record<string, string | string[] | undefined>)[name]\n return Array.isArray(v) ? v[0] : v\n}\n\nexport class AccessVerifier {\n private readonly issuer: string\n private readonly audience: string\n private readonly tolerance: number\n private readonly keys: JWTVerifyGetKey | Promise<Array<Awaited<ReturnType<typeof importJWK>>>>\n\n constructor(opts: AccessVerifyOptions) {\n if (!opts.issuer || !opts.audience) throw new Error('AccessVerifier: issuer and audience are required')\n this.issuer = opts.issuer.replace(/\\/+$/, '')\n this.audience = opts.audience\n this.tolerance = opts.clockToleranceSec ?? 30\n this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => importJWK(k, 'EdDSA'))) : createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`))\n }\n\n /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */\n async verify(token: string | undefined | null, expect: { host?: string } = {}): Promise<AccessIdentity> {\n if (!token) throw new AccessAssertionError('No identity assertion; is the request coming through the proxy?', 'no_assertion')\n const options = { algorithms: ['EdDSA'], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ['sub', 'exp', 'iat'] }\n let payload: Record<string, unknown>\n try {\n if (this.keys instanceof Promise) {\n let last: unknown\n payload = {}\n let ok = false\n for (const key of await this.keys) {\n try {\n payload = (await jwtVerify(token, key, options)).payload as Record<string, unknown>\n ok = true\n break\n } catch (e) {\n last = e\n }\n }\n if (!ok) throw last ?? new Error('No key matched')\n } else {\n payload = (await jwtVerify(token, this.keys, options)).payload as Record<string, unknown>\n }\n } catch (e) {\n throw new AccessAssertionError(e instanceof Error ? e.message : 'Invalid assertion', 'invalid_assertion')\n }\n if (typeof payload['email'] !== 'string' || typeof payload['host'] !== 'string' || payload['access_app'] !== this.audience) {\n throw new AccessAssertionError('Assertion is missing its identity', 'invalid_assertion')\n }\n if (expect.host !== undefined && payload['host'] !== expect.host.toLowerCase().replace(/:\\d+$/, '')) {\n throw new AccessAssertionError('Assertion was issued for another host', 'wrong_host')\n }\n const groups = Array.isArray(payload['groups']) ? payload['groups'].filter((g): g is string => typeof g === 'string') : []\n return { ...payload, groups } as unknown as AccessIdentity\n }\n\n /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */\n verifyRequest(headers: HeaderBag): Promise<AccessIdentity> {\n const host = header(headers, 'host')\n return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {})\n }\n}\n\nexport function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier {\n return new AccessVerifier(opts)\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n realm: unknown,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || (typeof realm === 'string' && iss === `${canon}${REALM_SEGMENT}${realm}`)\n}\n\nexport interface VerifyOptions {\n /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer. Once your app signs with its own key,\n * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything\n * signed with the shared key.\n *\n * When omitted, the verifier accepts the canonical HTTPS issuer (derived from\n * `apiBase`) and THIS app's own issuer — never another app's.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** The app's public id. Absent on tokens minted before it existed. */\n pid?: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId.startsWith('tau_pub_') ? 'tau_' + opts.appId.slice('tau_pub_'.length) : opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(payload: { iss?: unknown; app?: unknown; pid?: unknown }): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof payload.iss !== 'string') return null\n if (payload.iss === this.opts.apiBase) return this.remote\n // The app's own key set is fetched by the configured app, so it holds only this app's keys.\n if (\n this.matchesApp(payload) &&\n typeof payload.app === 'string' &&\n payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`\n ) {\n return this.remoteRealm\n }\n return null\n }\n\n private matchesApp(claims: { app?: unknown; pid?: unknown }): boolean {\n return claims.app === this.opts.appId || claims.pid === this.opts.appId\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverified = decodeJwt(token)\n const keySet = this.keySetFor(unverified)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverified.iss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!this.matchesApp(claims)) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\nexport { verifyWebhook, TillAuthWebhookError } from './webhooks.js'\nexport { AccessVerifier, AccessAssertionError, createAccessVerifier, ACCESS_ASSERTION_HEADER } from './access.js'\nexport type { AccessIdentity, AccessVerifyOptions } from './access.js'\nexport type { TillAuthWebhookEvent, VerifyWebhookOptions, WebhookErrorReason } from './webhooks.js'\n"]}
package/dist/index.d.cts CHANGED
@@ -36,20 +36,101 @@ interface ExpRes {
36
36
  }
37
37
  declare function expressCookieProxy(opts: CookieProxyOptions): (req: ExpReq, res: ExpRes) => Promise<void>;
38
38
 
39
+ interface TillAuthWebhookEvent {
40
+ type: string;
41
+ result: string;
42
+ ts: string;
43
+ app_id: string;
44
+ user_id: string | null;
45
+ ip: string | null;
46
+ user_agent: string | null;
47
+ metadata: Record<string, unknown>;
48
+ delivery_id: string;
49
+ attempt: number;
50
+ }
51
+ type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload';
52
+ declare class TillAuthWebhookError extends Error {
53
+ readonly reason: WebhookErrorReason;
54
+ constructor(reason: WebhookErrorReason);
55
+ }
56
+ interface VerifyWebhookOptions {
57
+ /** Max age of the signed timestamp, in seconds. Default 300. */
58
+ toleranceSeconds?: number;
59
+ /** Unix seconds; for tests. */
60
+ now?: number;
61
+ }
62
+ /**
63
+ * Checks the signature over the exact bytes received, then parses them.
64
+ * Pass the raw body, never a re-serialized object.
65
+ */
66
+ declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): Promise<TillAuthWebhookEvent>;
67
+
68
+ declare const ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
69
+ interface AccessVerifyOptions {
70
+ /** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */
71
+ issuer: string;
72
+ /** The internal app's audience (its `taac_…` client id). */
73
+ audience: string;
74
+ /** Defaults to `{issuer}/.well-known/jwks.json`. */
75
+ jwksUri?: string;
76
+ /** Inline public keys instead of fetching the key set. */
77
+ publicJwks?: JWK[];
78
+ /** Seconds of clock skew to allow. Defaults to 30. */
79
+ clockToleranceSec?: number;
80
+ }
81
+ interface AccessIdentity {
82
+ sub: string;
83
+ email: string;
84
+ email_verified: boolean;
85
+ role: string;
86
+ /** Group names, sorted; at most 100. Empty when the person is in none. */
87
+ groups: string[];
88
+ mfa: boolean;
89
+ /** When the person signed in, in seconds. */
90
+ auth_time: number;
91
+ sid: string;
92
+ /** The host the proxy checked the request for. */
93
+ host: string;
94
+ access_app: string;
95
+ iss: string;
96
+ iat: number;
97
+ exp: number;
98
+ jti: string;
99
+ }
100
+ declare class AccessAssertionError extends Error {
101
+ code: 'no_assertion' | 'invalid_assertion' | 'wrong_host';
102
+ constructor(message: string, code: AccessAssertionError['code']);
103
+ }
104
+ type HeaderBag = Record<string, string | string[] | undefined> | {
105
+ get(name: string): string | null;
106
+ };
107
+ declare class AccessVerifier {
108
+ private readonly issuer;
109
+ private readonly audience;
110
+ private readonly tolerance;
111
+ private readonly keys;
112
+ constructor(opts: AccessVerifyOptions);
113
+ /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
114
+ verify(token: string | undefined | null, expect?: {
115
+ host?: string;
116
+ }): Promise<AccessIdentity>;
117
+ /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
118
+ verifyRequest(headers: HeaderBag): Promise<AccessIdentity>;
119
+ }
120
+ declare function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier;
121
+
39
122
  interface VerifyOptions {
40
- /** TillAuth public app id (`tau_…`). Used to enforce app scoping. */
123
+ /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */
41
124
  appId: string;
42
125
  /** TillAuth API base. Defaults to https://auth.tilldev.dev */
43
126
  apiBase?: string;
44
127
  /**
45
- * Pin verification to exactly one issuer (e.g. a dedicated realm's
46
- * `https://auth.tilldev.dev/realms/<app>`).
128
+ * Pin verification to exactly one issuer. Once your app signs with its own key,
129
+ * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything
130
+ * signed with the shared key.
47
131
  *
48
- * Rarely needed now: when omitted, the verifier accepts the canonical HTTPS issuer
49
- * (derived from `apiBase`) and THIS app's own realm issuer — never another realm's.
50
- * It previously accepted any realm under `apiBase`, which is what made this option
51
- * look load-bearing; it was not, because the key still came from a merged JWKS
52
- * (`atrium` `00` D13). The retired legacy `"tillauth"` string is never accepted.
132
+ * When omitted, the verifier accepts the canonical HTTPS issuer (derived from
133
+ * `apiBase`) and THIS app's own issuer — never another app's.
53
134
  */
54
135
  issuer?: string;
55
136
  /**
@@ -68,6 +149,8 @@ interface AuthClaims {
68
149
  sub: string;
69
150
  /** App id the token was issued for */
70
151
  app: string;
152
+ /** The app's public id. Absent on tokens minted before it existed. */
153
+ pid?: string;
71
154
  /** Lowercased email */
72
155
  email: string;
73
156
  /** Whether MFA was satisfied this session */
@@ -105,6 +188,7 @@ declare class TillAuthVerifier {
105
188
  * that set contains only keys entitled to the issuer it named.
106
189
  */
107
190
  private keySetFor;
191
+ private matchesApp;
108
192
  verify(token: string): Promise<AuthClaims>;
109
193
  private applyClaimsChecks;
110
194
  /**
@@ -136,4 +220,4 @@ declare function fastifyHook(verifier: TillAuthVerifier): (req: {
136
220
  }) => Promise<void>;
137
221
  declare function createVerifier(opts: VerifyOptions): TillAuthVerifier;
138
222
 
139
- export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, type VerifyOptions, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook };
223
+ export { ACCESS_ASSERTION_HEADER, AccessAssertionError, type AccessIdentity, AccessVerifier, type AccessVerifyOptions, type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createAccessVerifier, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
package/dist/index.d.ts CHANGED
@@ -36,20 +36,101 @@ interface ExpRes {
36
36
  }
37
37
  declare function expressCookieProxy(opts: CookieProxyOptions): (req: ExpReq, res: ExpRes) => Promise<void>;
38
38
 
39
+ interface TillAuthWebhookEvent {
40
+ type: string;
41
+ result: string;
42
+ ts: string;
43
+ app_id: string;
44
+ user_id: string | null;
45
+ ip: string | null;
46
+ user_agent: string | null;
47
+ metadata: Record<string, unknown>;
48
+ delivery_id: string;
49
+ attempt: number;
50
+ }
51
+ type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload';
52
+ declare class TillAuthWebhookError extends Error {
53
+ readonly reason: WebhookErrorReason;
54
+ constructor(reason: WebhookErrorReason);
55
+ }
56
+ interface VerifyWebhookOptions {
57
+ /** Max age of the signed timestamp, in seconds. Default 300. */
58
+ toleranceSeconds?: number;
59
+ /** Unix seconds; for tests. */
60
+ now?: number;
61
+ }
62
+ /**
63
+ * Checks the signature over the exact bytes received, then parses them.
64
+ * Pass the raw body, never a re-serialized object.
65
+ */
66
+ declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): Promise<TillAuthWebhookEvent>;
67
+
68
+ declare const ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
69
+ interface AccessVerifyOptions {
70
+ /** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */
71
+ issuer: string;
72
+ /** The internal app's audience (its `taac_…` client id). */
73
+ audience: string;
74
+ /** Defaults to `{issuer}/.well-known/jwks.json`. */
75
+ jwksUri?: string;
76
+ /** Inline public keys instead of fetching the key set. */
77
+ publicJwks?: JWK[];
78
+ /** Seconds of clock skew to allow. Defaults to 30. */
79
+ clockToleranceSec?: number;
80
+ }
81
+ interface AccessIdentity {
82
+ sub: string;
83
+ email: string;
84
+ email_verified: boolean;
85
+ role: string;
86
+ /** Group names, sorted; at most 100. Empty when the person is in none. */
87
+ groups: string[];
88
+ mfa: boolean;
89
+ /** When the person signed in, in seconds. */
90
+ auth_time: number;
91
+ sid: string;
92
+ /** The host the proxy checked the request for. */
93
+ host: string;
94
+ access_app: string;
95
+ iss: string;
96
+ iat: number;
97
+ exp: number;
98
+ jti: string;
99
+ }
100
+ declare class AccessAssertionError extends Error {
101
+ code: 'no_assertion' | 'invalid_assertion' | 'wrong_host';
102
+ constructor(message: string, code: AccessAssertionError['code']);
103
+ }
104
+ type HeaderBag = Record<string, string | string[] | undefined> | {
105
+ get(name: string): string | null;
106
+ };
107
+ declare class AccessVerifier {
108
+ private readonly issuer;
109
+ private readonly audience;
110
+ private readonly tolerance;
111
+ private readonly keys;
112
+ constructor(opts: AccessVerifyOptions);
113
+ /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
114
+ verify(token: string | undefined | null, expect?: {
115
+ host?: string;
116
+ }): Promise<AccessIdentity>;
117
+ /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
118
+ verifyRequest(headers: HeaderBag): Promise<AccessIdentity>;
119
+ }
120
+ declare function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier;
121
+
39
122
  interface VerifyOptions {
40
- /** TillAuth public app id (`tau_…`). Used to enforce app scoping. */
123
+ /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */
41
124
  appId: string;
42
125
  /** TillAuth API base. Defaults to https://auth.tilldev.dev */
43
126
  apiBase?: string;
44
127
  /**
45
- * Pin verification to exactly one issuer (e.g. a dedicated realm's
46
- * `https://auth.tilldev.dev/realms/<app>`).
128
+ * Pin verification to exactly one issuer. Once your app signs with its own key,
129
+ * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything
130
+ * signed with the shared key.
47
131
  *
48
- * Rarely needed now: when omitted, the verifier accepts the canonical HTTPS issuer
49
- * (derived from `apiBase`) and THIS app's own realm issuer — never another realm's.
50
- * It previously accepted any realm under `apiBase`, which is what made this option
51
- * look load-bearing; it was not, because the key still came from a merged JWKS
52
- * (`atrium` `00` D13). The retired legacy `"tillauth"` string is never accepted.
132
+ * When omitted, the verifier accepts the canonical HTTPS issuer (derived from
133
+ * `apiBase`) and THIS app's own issuer — never another app's.
53
134
  */
54
135
  issuer?: string;
55
136
  /**
@@ -68,6 +149,8 @@ interface AuthClaims {
68
149
  sub: string;
69
150
  /** App id the token was issued for */
70
151
  app: string;
152
+ /** The app's public id. Absent on tokens minted before it existed. */
153
+ pid?: string;
71
154
  /** Lowercased email */
72
155
  email: string;
73
156
  /** Whether MFA was satisfied this session */
@@ -105,6 +188,7 @@ declare class TillAuthVerifier {
105
188
  * that set contains only keys entitled to the issuer it named.
106
189
  */
107
190
  private keySetFor;
191
+ private matchesApp;
108
192
  verify(token: string): Promise<AuthClaims>;
109
193
  private applyClaimsChecks;
110
194
  /**
@@ -136,4 +220,4 @@ declare function fastifyHook(verifier: TillAuthVerifier): (req: {
136
220
  }) => Promise<void>;
137
221
  declare function createVerifier(opts: VerifyOptions): TillAuthVerifier;
138
222
 
139
- export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, type VerifyOptions, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook };
223
+ export { ACCESS_ASSERTION_HEADER, AccessAssertionError, type AccessIdentity, AccessVerifier, type AccessVerifyOptions, type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createAccessVerifier, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
package/dist/index.js CHANGED
@@ -122,10 +122,10 @@ function expressCookieProxy(opts) {
122
122
  res.json(JSON.parse(out.body));
123
123
  };
124
124
  }
125
- function parseCookieHeader(header) {
125
+ function parseCookieHeader(header2) {
126
126
  const out = {};
127
- if (!header) return out;
128
- for (const part of header.split(";")) {
127
+ if (!header2) return out;
128
+ for (const part of header2.split(";")) {
129
129
  const [k, ...rest] = part.trim().split("=");
130
130
  if (!k) continue;
131
131
  out[k] = decodeURIComponent(rest.join("="));
@@ -133,17 +133,129 @@ function parseCookieHeader(header) {
133
133
  return out;
134
134
  }
135
135
 
136
+ // src/webhooks.ts
137
+ var TillAuthWebhookError = class extends Error {
138
+ constructor(reason) {
139
+ super(`TillAuth webhook rejected: ${reason}`);
140
+ this.reason = reason;
141
+ this.name = "TillAuthWebhookError";
142
+ }
143
+ };
144
+ var enc = new TextEncoder();
145
+ function hexBytes(hex) {
146
+ if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null;
147
+ const out = new Uint8Array(hex.length / 2);
148
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
149
+ return out;
150
+ }
151
+ async function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
152
+ if (!signatureHeader) throw new TillAuthWebhookError("missing_signature");
153
+ let t = null;
154
+ const sigs = [];
155
+ for (const part of signatureHeader.split(",")) {
156
+ const [k, v] = part.trim().split("=", 2);
157
+ if (k === "t" && v && /^\d+$/.test(v)) t = Number(v);
158
+ else if (k === "v1" && v) {
159
+ const b = hexBytes(v);
160
+ if (b) sigs.push(b);
161
+ }
162
+ }
163
+ if (t === null || sigs.length === 0) throw new TillAuthWebhookError("malformed_signature");
164
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
165
+ if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError("stale");
166
+ const body = typeof rawBody === "string" ? enc.encode(rawBody) : rawBody;
167
+ const prefix = enc.encode(`${t}.`);
168
+ const signed = new Uint8Array(prefix.length + body.length);
169
+ signed.set(prefix);
170
+ signed.set(body, prefix.length);
171
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
172
+ let ok = false;
173
+ for (const s of sigs) if (await crypto.subtle.verify("HMAC", key, s, signed)) ok = true;
174
+ if (!ok) throw new TillAuthWebhookError("bad_signature");
175
+ try {
176
+ return JSON.parse(new TextDecoder().decode(body));
177
+ } catch {
178
+ throw new TillAuthWebhookError("bad_payload");
179
+ }
180
+ }
181
+ var ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
182
+ var ASSERTION_TYP = "tilldev-access+jwt";
183
+ var AccessAssertionError = class extends Error {
184
+ constructor(message, code) {
185
+ super(message);
186
+ this.name = "AccessAssertionError";
187
+ this.code = code;
188
+ }
189
+ };
190
+ function header(headers, name) {
191
+ if (typeof headers.get === "function") return headers.get(name) ?? void 0;
192
+ const v = headers[name];
193
+ return Array.isArray(v) ? v[0] : v;
194
+ }
195
+ var AccessVerifier = class {
196
+ constructor(opts) {
197
+ if (!opts.issuer || !opts.audience) throw new Error("AccessVerifier: issuer and audience are required");
198
+ this.issuer = opts.issuer.replace(/\/+$/, "");
199
+ this.audience = opts.audience;
200
+ this.tolerance = opts.clockToleranceSec ?? 30;
201
+ this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => importJWK(k, "EdDSA"))) : createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`));
202
+ }
203
+ /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
204
+ async verify(token, expect = {}) {
205
+ if (!token) throw new AccessAssertionError("No identity assertion; is the request coming through the proxy?", "no_assertion");
206
+ const options = { algorithms: ["EdDSA"], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ["sub", "exp", "iat"] };
207
+ let payload;
208
+ try {
209
+ if (this.keys instanceof Promise) {
210
+ let last;
211
+ payload = {};
212
+ let ok = false;
213
+ for (const key of await this.keys) {
214
+ try {
215
+ payload = (await jwtVerify(token, key, options)).payload;
216
+ ok = true;
217
+ break;
218
+ } catch (e) {
219
+ last = e;
220
+ }
221
+ }
222
+ if (!ok) throw last ?? new Error("No key matched");
223
+ } else {
224
+ payload = (await jwtVerify(token, this.keys, options)).payload;
225
+ }
226
+ } catch (e) {
227
+ throw new AccessAssertionError(e instanceof Error ? e.message : "Invalid assertion", "invalid_assertion");
228
+ }
229
+ if (typeof payload["email"] !== "string" || typeof payload["host"] !== "string" || payload["access_app"] !== this.audience) {
230
+ throw new AccessAssertionError("Assertion is missing its identity", "invalid_assertion");
231
+ }
232
+ if (expect.host !== void 0 && payload["host"] !== expect.host.toLowerCase().replace(/:\d+$/, "")) {
233
+ throw new AccessAssertionError("Assertion was issued for another host", "wrong_host");
234
+ }
235
+ const groups = Array.isArray(payload["groups"]) ? payload["groups"].filter((g) => typeof g === "string") : [];
236
+ return { ...payload, groups };
237
+ }
238
+ /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
239
+ verifyRequest(headers) {
240
+ const host = header(headers, "host");
241
+ return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {});
242
+ }
243
+ };
244
+ function createAccessVerifier(opts) {
245
+ return new AccessVerifier(opts);
246
+ }
247
+
136
248
  // src/index.ts
137
249
  var DEFAULT_API2 = "https://auth.tilldev.dev";
138
250
  var REALM_SEGMENT = "/realms/";
139
251
  function trimSlash(s) {
140
252
  return s.replace(/\/+$/, "");
141
253
  }
142
- function issuerAccepted(iss, canonical, appId, pinnedIssuer) {
254
+ function issuerAccepted(iss, canonical, realm, pinnedIssuer) {
143
255
  if (typeof iss !== "string" || iss.length === 0) return false;
144
256
  if (pinnedIssuer) return iss === trimSlash(pinnedIssuer);
145
257
  const canon = trimSlash(canonical);
146
- return iss === canon || iss === `${canon}${REALM_SEGMENT}${appId}`;
258
+ return iss === canon || typeof realm === "string" && iss === `${canon}${REALM_SEGMENT}${realm}`;
147
259
  }
148
260
  var TillAuthVerifyError = class extends Error {
149
261
  constructor(message, code) {
@@ -158,7 +270,7 @@ var TillAuthVerifier = class {
158
270
  this.remoteRealm = null;
159
271
  if (!opts.appId) throw new Error("TillAuthVerifier: appId is required");
160
272
  this.opts = {
161
- appId: opts.appId,
273
+ appId: opts.appId.startsWith("tau_pub_") ? "tau_" + opts.appId.slice("tau_pub_".length) : opts.appId,
162
274
  apiBase: (opts.apiBase ?? DEFAULT_API2).replace(/\/$/, ""),
163
275
  requireVerifiedEmail: opts.requireVerifiedEmail ?? false,
164
276
  requireMfa: opts.requireMfa ?? false
@@ -184,12 +296,17 @@ var TillAuthVerifier = class {
184
296
  * its issuer to reach a different key set must still verify against that set — and
185
297
  * that set contains only keys entitled to the issuer it named.
186
298
  */
187
- keySetFor(iss) {
188
- if (typeof iss !== "string") return null;
189
- if (iss === this.opts.apiBase) return this.remote;
190
- if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}`) return this.remoteRealm;
299
+ keySetFor(payload) {
300
+ if (typeof payload.iss !== "string") return null;
301
+ if (payload.iss === this.opts.apiBase) return this.remote;
302
+ if (this.matchesApp(payload) && typeof payload.app === "string" && payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`) {
303
+ return this.remoteRealm;
304
+ }
191
305
  return null;
192
306
  }
307
+ matchesApp(claims) {
308
+ return claims.app === this.opts.appId || claims.pid === this.opts.appId;
309
+ }
193
310
  async verify(token) {
194
311
  if (!token) throw new TillAuthVerifyError("No token", "no_token");
195
312
  let claims;
@@ -208,14 +325,14 @@ var TillAuthVerifier = class {
208
325
  }
209
326
  throw lastErr ?? new Error("No key matched");
210
327
  }
211
- const unverifiedIss = decodeJwt(token).iss;
212
- const keySet = this.keySetFor(unverifiedIss);
328
+ const unverified = decodeJwt(token);
329
+ const keySet = this.keySetFor(unverified);
213
330
  if (!keySet) {
214
331
  throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
215
332
  }
216
333
  const { payload } = await jwtVerify(token, keySet, {
217
334
  algorithms: ["EdDSA"],
218
- issuer: unverifiedIss
335
+ issuer: unverified.iss
219
336
  });
220
337
  claims = payload;
221
338
  } catch (e) {
@@ -228,12 +345,12 @@ var TillAuthVerifier = class {
228
345
  return this.applyClaimsChecks(claims);
229
346
  }
230
347
  applyClaimsChecks(claims) {
231
- if (!issuerAccepted(claims.iss, this.opts.apiBase, this.opts.appId, this.pinnedIssuer)) {
232
- throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
233
- }
234
- if (claims.app !== this.opts.appId) {
348
+ if (!this.matchesApp(claims)) {
235
349
  throw new TillAuthVerifyError(`Token issued for different app`, "wrong_app");
236
350
  }
351
+ if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {
352
+ throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
353
+ }
237
354
  if (this.opts.requireMfa && !claims.mfa) {
238
355
  throw new TillAuthVerifyError("MFA required", "mfa_required");
239
356
  }
@@ -290,6 +407,6 @@ function createVerifier(opts) {
290
407
  return new TillAuthVerifier(opts);
291
408
  }
292
409
 
293
- export { TillAuthVerifier, TillAuthVerifyError, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook };
410
+ export { ACCESS_ASSERTION_HEADER, AccessAssertionError, AccessVerifier, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, createAccessVerifier, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
294
411
  //# sourceMappingURL=index.js.map
295
412
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cookie-proxy.ts","../src/index.ts"],"names":["DEFAULT_API","payload"],"mappings":"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","file":"index.js","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n appId: string,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || iss === `${canon}${REALM_SEGMENT}${appId}`\n}\n\nexport interface VerifyOptions {\n /** TillAuth public app id (`tau_…`). Used to enforce app scoping. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer (e.g. a dedicated realm's\n * `https://auth.tilldev.dev/realms/<app>`).\n *\n * Rarely needed now: when omitted, the verifier accepts the canonical HTTPS issuer\n * (derived from `apiBase`) and THIS app's own realm issuer — never another realm's.\n * It previously accepted any realm under `apiBase`, which is what made this option\n * look load-bearing; it was not, because the key still came from a merged JWKS\n * (`atrium` `00` D13). The retired legacy `\"tillauth\"` string is never accepted.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(iss: unknown): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof iss !== 'string') return null\n if (iss === this.opts.apiBase) return this.remote\n if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}`) return this.remoteRealm\n return null\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverifiedIss = decodeJwt(token).iss\n const keySet = this.keySetFor(unverifiedIss)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverifiedIss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!issuerAccepted(claims.iss, this.opts.apiBase, this.opts.appId, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (claims.app !== this.opts.appId) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\n"]}
1
+ {"version":3,"sources":["../src/cookie-proxy.ts","../src/webhooks.ts","../src/access.ts","../src/index.ts"],"names":["header","DEFAULT_API","importJWK","createRemoteJWKSet","payload","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","// Verifies TillAuth webhook deliveries (header `X-TillAuth-Signature: t=<unix>,v1=<hex>`).\n\nexport interface TillAuthWebhookEvent {\n type: string\n result: string\n ts: string\n app_id: string\n user_id: string | null\n ip: string | null\n user_agent: string | null\n metadata: Record<string, unknown>\n delivery_id: string\n attempt: number\n}\n\nexport type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload'\n\nexport class TillAuthWebhookError extends Error {\n constructor(readonly reason: WebhookErrorReason) {\n super(`TillAuth webhook rejected: ${reason}`)\n this.name = 'TillAuthWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Max age of the signed timestamp, in seconds. Default 300. */\n toleranceSeconds?: number\n /** Unix seconds; for tests. */\n now?: number\n}\n\nconst enc = new TextEncoder()\n\nfunction hexBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n return out\n}\n\n/**\n * Checks the signature over the exact bytes received, then parses them.\n * Pass the raw body, never a re-serialized object.\n */\nexport async function verifyWebhook(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): Promise<TillAuthWebhookEvent> {\n if (!signatureHeader) throw new TillAuthWebhookError('missing_signature')\n\n let t: number | null = null\n const sigs: Uint8Array[] = []\n for (const part of signatureHeader.split(',')) {\n const [k, v] = part.trim().split('=', 2)\n if (k === 't' && v && /^\\d+$/.test(v)) t = Number(v)\n else if (k === 'v1' && v) {\n const b = hexBytes(v)\n if (b) sigs.push(b)\n }\n }\n if (t === null || sigs.length === 0) throw new TillAuthWebhookError('malformed_signature')\n\n const now = opts.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError('stale')\n\n const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody\n const prefix = enc.encode(`${t}.`)\n const signed = new Uint8Array(prefix.length + body.length)\n signed.set(prefix)\n signed.set(body, prefix.length)\n\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])\n let ok = false\n for (const s of sigs) if (await crypto.subtle.verify('HMAC', key, s, signed)) ok = true\n if (!ok) throw new TillAuthWebhookError('bad_signature')\n\n try {\n return JSON.parse(new TextDecoder().decode(body)) as TillAuthWebhookEvent\n } catch {\n throw new TillAuthWebhookError('bad_payload')\n }\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, type JWK, type JWTVerifyGetKey } from 'jose'\n\nexport const ACCESS_ASSERTION_HEADER = 'x-tilldev-assertion'\nconst ASSERTION_TYP = 'tilldev-access+jwt'\n\nexport interface AccessVerifyOptions {\n /** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */\n issuer: string\n /** The internal app's audience (its `taac_…` client id). */\n audience: string\n /** Defaults to `{issuer}/.well-known/jwks.json`. */\n jwksUri?: string\n /** Inline public keys instead of fetching the key set. */\n publicJwks?: JWK[]\n /** Seconds of clock skew to allow. Defaults to 30. */\n clockToleranceSec?: number\n}\n\nexport interface AccessIdentity {\n sub: string\n email: string\n email_verified: boolean\n role: string\n /** Group names, sorted; at most 100. Empty when the person is in none. */\n groups: string[]\n mfa: boolean\n /** When the person signed in, in seconds. */\n auth_time: number\n sid: string\n /** The host the proxy checked the request for. */\n host: string\n access_app: string\n iss: string\n iat: number\n exp: number\n jti: string\n}\n\nexport class AccessAssertionError extends Error {\n code: 'no_assertion' | 'invalid_assertion' | 'wrong_host'\n constructor(message: string, code: AccessAssertionError['code']) {\n super(message)\n this.name = 'AccessAssertionError'\n this.code = code\n }\n}\n\ntype HeaderBag = Record<string, string | string[] | undefined> | { get(name: string): string | null }\n\nfunction header(headers: HeaderBag, name: string): string | undefined {\n if (typeof (headers as { get?: unknown }).get === 'function') return (headers as { get(n: string): string | null }).get(name) ?? undefined\n const v = (headers as Record<string, string | string[] | undefined>)[name]\n return Array.isArray(v) ? v[0] : v\n}\n\nexport class AccessVerifier {\n private readonly issuer: string\n private readonly audience: string\n private readonly tolerance: number\n private readonly keys: JWTVerifyGetKey | Promise<Array<Awaited<ReturnType<typeof importJWK>>>>\n\n constructor(opts: AccessVerifyOptions) {\n if (!opts.issuer || !opts.audience) throw new Error('AccessVerifier: issuer and audience are required')\n this.issuer = opts.issuer.replace(/\\/+$/, '')\n this.audience = opts.audience\n this.tolerance = opts.clockToleranceSec ?? 30\n this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => importJWK(k, 'EdDSA'))) : createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`))\n }\n\n /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */\n async verify(token: string | undefined | null, expect: { host?: string } = {}): Promise<AccessIdentity> {\n if (!token) throw new AccessAssertionError('No identity assertion; is the request coming through the proxy?', 'no_assertion')\n const options = { algorithms: ['EdDSA'], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ['sub', 'exp', 'iat'] }\n let payload: Record<string, unknown>\n try {\n if (this.keys instanceof Promise) {\n let last: unknown\n payload = {}\n let ok = false\n for (const key of await this.keys) {\n try {\n payload = (await jwtVerify(token, key, options)).payload as Record<string, unknown>\n ok = true\n break\n } catch (e) {\n last = e\n }\n }\n if (!ok) throw last ?? new Error('No key matched')\n } else {\n payload = (await jwtVerify(token, this.keys, options)).payload as Record<string, unknown>\n }\n } catch (e) {\n throw new AccessAssertionError(e instanceof Error ? e.message : 'Invalid assertion', 'invalid_assertion')\n }\n if (typeof payload['email'] !== 'string' || typeof payload['host'] !== 'string' || payload['access_app'] !== this.audience) {\n throw new AccessAssertionError('Assertion is missing its identity', 'invalid_assertion')\n }\n if (expect.host !== undefined && payload['host'] !== expect.host.toLowerCase().replace(/:\\d+$/, '')) {\n throw new AccessAssertionError('Assertion was issued for another host', 'wrong_host')\n }\n const groups = Array.isArray(payload['groups']) ? payload['groups'].filter((g): g is string => typeof g === 'string') : []\n return { ...payload, groups } as unknown as AccessIdentity\n }\n\n /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */\n verifyRequest(headers: HeaderBag): Promise<AccessIdentity> {\n const host = header(headers, 'host')\n return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {})\n }\n}\n\nexport function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier {\n return new AccessVerifier(opts)\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n realm: unknown,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || (typeof realm === 'string' && iss === `${canon}${REALM_SEGMENT}${realm}`)\n}\n\nexport interface VerifyOptions {\n /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer. Once your app signs with its own key,\n * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything\n * signed with the shared key.\n *\n * When omitted, the verifier accepts the canonical HTTPS issuer (derived from\n * `apiBase`) and THIS app's own issuer — never another app's.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** The app's public id. Absent on tokens minted before it existed. */\n pid?: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId.startsWith('tau_pub_') ? 'tau_' + opts.appId.slice('tau_pub_'.length) : opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(payload: { iss?: unknown; app?: unknown; pid?: unknown }): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof payload.iss !== 'string') return null\n if (payload.iss === this.opts.apiBase) return this.remote\n // The app's own key set is fetched by the configured app, so it holds only this app's keys.\n if (\n this.matchesApp(payload) &&\n typeof payload.app === 'string' &&\n payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`\n ) {\n return this.remoteRealm\n }\n return null\n }\n\n private matchesApp(claims: { app?: unknown; pid?: unknown }): boolean {\n return claims.app === this.opts.appId || claims.pid === this.opts.appId\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverified = decodeJwt(token)\n const keySet = this.keySetFor(unverified)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverified.iss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!this.matchesApp(claims)) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\nexport { verifyWebhook, TillAuthWebhookError } from './webhooks.js'\nexport { AccessVerifier, AccessAssertionError, createAccessVerifier, ACCESS_ASSERTION_HEADER } from './access.js'\nexport type { AccessIdentity, AccessVerifyOptions } from './access.js'\nexport type { TillAuthWebhookEvent, VerifyWebhookOptions, WebhookErrorReason } from './webhooks.js'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tillstack/auth-node",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Node SDK for TillAuth — JWT verification + middleware",
6
6
  "license": "Apache-2.0",
@@ -25,14 +25,6 @@
25
25
  "files": [
26
26
  "dist"
27
27
  ],
28
- "scripts": {
29
- "build": "tsup",
30
- "dev": "tsup --watch",
31
- "typecheck": "tsc --noEmit",
32
- "prepack": "tsup",
33
- "clean": "rm -rf dist",
34
- "test": "node --import tsx --test test/*.test.ts"
35
- },
36
28
  "dependencies": {
37
29
  "jose": "^5.9.6"
38
30
  },
@@ -52,5 +44,12 @@
52
44
  },
53
45
  "publishConfig": {
54
46
  "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "rm -rf dist",
53
+ "test": "node --import tsx --test test/*.test.ts"
55
54
  }
56
- }
55
+ }