@vielzeug/archive 26.11.5 → 26.11.6

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/dist/_crypto.js CHANGED
@@ -1,6 +1,5 @@
1
- //#region src/_crypto.ts
1
+ // src/_crypto.ts
2
2
  var sha256Hex = async (e) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(e)))].map((e) => e.toString(16).padStart(2, "0")).join("");
3
- //#endregion
4
3
  export { sha256Hex };
5
4
 
6
5
  //# sourceMappingURL=_crypto.js.map
package/dist/_json.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/_json.ts
1
+ // src/_json.ts
2
2
  function canonicalJson(e, t = !1, n) {
3
3
  let r = /* @__PURE__ */ new Set(), text = (e) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(e) ? null : JSON.stringify(e), serialize = (e, n) => {
4
4
  if (e === null) return "null";
@@ -46,7 +46,6 @@ function canonicalJson(e, t = !1, n) {
46
46
  function canonicalObjectJson(e, t = !1, n) {
47
47
  return typeof e != "object" || !e || Array.isArray(e) ? null : canonicalJson(e, t, n);
48
48
  }
49
- //#endregion
50
49
  export { canonicalJson, canonicalObjectJson };
51
50
 
52
51
  //# sourceMappingURL=_json.js.map
@@ -1,7 +1,7 @@
1
1
  import { REQUEST_MARKER_HEADER as e } from "../contract.js";
2
2
  import { noStore as t } from "../worker/http.js";
3
3
  import { LINK_TTL_MS as n } from "./store.js";
4
- //#region src/auth/_confirmation.ts
4
+ // src/auth/_confirmation.ts
5
5
  var escapeHtml = (e) => e.replace(/[&<>"']/g, (e) => `&#${e.charCodeAt(0)};`), scriptValue = (e) => JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
6
6
  function confirmationPage({ appName: r, nonce: i, publicOrigin: a, token: o }) {
7
7
  let s = o !== null, c = s ? "Confirm sign-in" : "This sign-in link is not valid", l = s ? `
@@ -198,7 +198,6 @@ ${s ? `<script nonce="${i}">${l}
198
198
  status: s ? 200 : 400
199
199
  });
200
200
  }
201
- //#endregion
202
201
  export { confirmationPage, escapeHtml };
203
202
 
204
203
  //# sourceMappingURL=_confirmation.js.map
@@ -1,6 +1,6 @@
1
1
  import { AuthOperationError as e } from "./errors.js";
2
2
  import { SignJWT as t, calculateJwkThumbprint as n, createLocalJWKSet as r, importJWK as i, jwtVerify as a } from "jose";
3
- //#region src/auth/config.ts
3
+ // src/auth/config.ts
4
4
  var configError = () => new e(503, "configuration"), o = 320, s = 64, c = 342, l = /^[A-Za-z0-9_-]+$/, u = /* @__PURE__ */ new Set([
5
5
  "127.0.0.1",
6
6
  "localhost",
@@ -151,7 +151,6 @@ function authConfig(e) {
151
151
  let n = buildConfig(e);
152
152
  return f.set(e, n), n.catch(() => f.delete(e)), n;
153
153
  }
154
- //#endregion
155
154
  export { authConfig, isWellFormedEmail };
156
155
 
157
156
  //# sourceMappingURL=config.js.map
@@ -1,5 +1,5 @@
1
1
  import { ApiError as e } from "../worker/http.js";
2
- //#region src/auth/errors.ts
2
+ // src/auth/errors.ts
3
3
  var AuthOperationError = class extends e {
4
4
  authDiagnostic;
5
5
  constructor(e, t, n) {
@@ -13,7 +13,6 @@ async function databaseOperation(e) {
13
13
  throw new AuthOperationError(500, "database", { cause: e });
14
14
  }
15
15
  }
16
- //#endregion
17
16
  export { AuthOperationError, databaseOperation };
18
17
 
19
18
  //# sourceMappingURL=errors.js.map
@@ -1,5 +1,5 @@
1
1
  import { databaseOperation as e } from "./errors.js";
2
- //#region src/auth/store.ts
2
+ // src/auth/store.ts
3
3
  var t = 9e5, n = 36e5, r = 6e4, i = {
4
4
  account: "SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?",
5
5
  budgetWindow: "SELECT window_start FROM auth_link_budget WHERE user_id = ?",
@@ -97,7 +97,6 @@ async function sweep(t, r) {
97
97
  t.prepare(i.sweepUnverified)
98
98
  ]))).reduce((e, t) => e + (t.meta?.changes ?? 0), 0);
99
99
  }
100
- //#endregion
101
100
  export { t as LINK_TTL_MS, n as LINK_WINDOW_MS, r as SIGN_IN_WINDOW_MS, authReadiness, consumeToken, deleteAccount, deleteToken, ensureUser, insertToken, readAccount, revokeSessions, spendLinkBudget, spendSignInBudget, sweep };
102
101
 
103
102
  //# sourceMappingURL=store.js.map
@@ -1,2 +1,2 @@
1
- const e=require("../_crypto.cjs"),t=require("../contract.cjs"),n=require("../worker/_service.cjs"),r=require("../worker/http.cjs"),i=require("./errors.cjs"),a=require("./store.cjs"),o=require("./_confirmation.cjs"),s=require("./config.cjs");let c=require("jose");var l={account:/^\/api\/v1\/auth\/account$/,health:/^\/api\/v1\/auth\/health$/,jwks:/^\/api\/v1\/auth\/jwks\.json$/,session:/^\/api\/v1\/auth\/session$/,signIn:/^\/api\/v1\/auth\/sign-in$/,signOutAll:/^\/api\/v1\/auth\/sign-out-all$/,verify:/^\/api\/v1\/auth\/verify$/},u={account:[`GET`,`DELETE`],health:[`GET`],jwks:[`GET`],session:[`GET`],signIn:[`POST`],signOutAll:[`POST`],verify:[`GET`,`POST`]},d={account:`/auth/account`,health:`/auth/health`,jwks:`/auth/jwks.json`,session:`/auth/session`,signIn:`/auth/sign-in`,signOutAll:`/auth/sign-out-all`,verify:`/auth/verify`},matchRoute=e=>Object.keys(l).find(t=>l[t].test(new URL(e.url).pathname)),f=32,p=/^[A-Za-z0-9_-]{43}$/,m=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,h=256,g=5,randomToken=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(f)))).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``),unavailable=()=>new i.AuthOperationError(503,`catalog`);function requireMutationGuard(e,t,n=!1){let i=e.headers.get(`origin`);if(i===null||!t.allowedOrigins.has(i)&&!(n&&i===t.publicOrigin)||e.headers.get(`x-archive-request`)!==`1`)throw r.forbidden();return i}function corsHeaders(e,t){let n=e.headers.get(`origin`);return n===null||!t.allowedOrigins.has(n)&&(matchRoute(e)!==`verify`||n!==t.publicOrigin)?{}:{"access-control-allow-credentials":`true`,"access-control-allow-origin":n,vary:`Origin`}}function preflight(e,n,i){let a=corsHeaders(e,n);if(Object.keys(a).length===0)throw r.forbidden();return new Response(null,{headers:{...a,"access-control-allow-headers":`Content-Type, ${t.ACCOUNT_HEADER}, ${t.PUBLISHER_HEADER}, ${t.REQUEST_MARKER_HEADER}`,"access-control-allow-methods":[...i,`OPTIONS`].join(`, `),"access-control-max-age":`600`},status:204})}var secure=e=>e.appEnv===`production`;async function mintSession(e,n,r,i){let a=r?t.OPERATOR_SESSION_MAX_AGE_SECONDS:t.SESSION_MAX_AGE_SECONDS;return{maxAge:a,token:await new c.SignJWT({sessionVersion:i,...r?{roles:[t.ARCHIVE_OPERATOR_ROLE]}:{}}).setProtectedHeader({alg:`RS256`,kid:e.signing.kid,typ:`JWT`}).setIssuedAt().setJti(crypto.randomUUID()).setIssuer(e.issuer).setAudience(e.audience).setSubject(n).setExpirationTime(`${a}s`).sign(e.signing.key)}}async function requireSession(e,n,i){let o=r.sessionToken(e);if(o===null)throw r.unauthenticated();try{let{payload:e}=await(0,c.jwtVerify)(o,n.verifier,{algorithms:[`RS256`],audience:n.audience,clockTolerance:g,issuer:n.issuer,maxTokenAge:t.SESSION_MAX_AGE_SECONDS,requiredClaims:[`sub`,`iss`,`aud`,`iat`,`exp`,`sessionVersion`]}),s=e.sub??``;if(s.length===0||s.length>h||!t.isWellFormedText(s))throw r.unauthenticated();let l=e.sessionVersion;if(typeof l!=`number`||!Number.isSafeInteger(l)||l<0||typeof e.iat!=`number`||e.iat>Date.now()/1e3+g||Array.isArray(e.roles)&&e.roles.includes(`archive-operator`)&&(typeof e.exp!=`number`||e.exp-e.iat>3600||Date.now()/1e3-e.iat>3600+g))throw r.unauthenticated();let u=await a.readAccount(i.AUTH_DB,s);if(u===null||u.session_version!==l)throw r.unauthenticated();return{sessionVersion:l,subject:s,token:o}}catch(e){throw e instanceof c.errors.JOSEError?r.unauthenticated():e}}async function sendLink(e,t,n,r){let i=t.appName,s=`The link works once and expires in ${a.LINK_TTL_MS/6e4} minutes. If you did not request it, ignore this email.`;await e.AUTH_EMAIL.send({from:t.senderAddress,html:`<p>Open this link to sign in to ${o.escapeHtml(i)}:</p><p><a href="${o.escapeHtml(r)}">${o.escapeHtml(r)}</a></p><p>${s}</p>`,subject:`Your ${i} sign-in link`,text:`Open this link to sign in to ${i}:\n\n${r}\n\n${s}`,to:n})}async function handleSignIn(t,n,o){requireMutationGuard(t,o);let c=await r.readJsonBody(t);if(typeof c!=`object`||!c||Array.isArray(c))throw r.badRequest();let l=Object.keys(c),u=Reflect.get(c,`email`);if(l.length!==1||l[0]!==`email`||typeof u!=`string`)throw r.badRequest();let d=u.trim().toLowerCase();if(!s.isWellFormedEmail(d))throw r.badRequest();let f=Date.now(),p=await a.spendSignInBudget(n.AUTH_DB,f);if(p!==null)throw new r.ApiError(429,`rate-limited`,`Too many sign-in requests. Try again later.`,{retryAfterSeconds:p});let m=await a.ensureUser(n.AUTH_DB,d),h=await a.spendLinkBudget(n.AUTH_DB,m.id,f);if(h!==null)throw new r.ApiError(429,`rate-limited`,`Too many sign-in emails. Try again later.`,{retryAfterSeconds:h});let g=randomToken(),_=await e.sha256Hex(g);await a.insertToken(n.AUTH_DB,_,m.id,f);try{await sendLink(n,o,m.email,`${o.publicOrigin}/api/v1/auth/verify?token=${g}`)}catch(e){throw await a.deleteToken(n.AUTH_DB,_).catch(()=>void 0),new i.AuthOperationError(503,`email`,{cause:e})}return r.json(202,{ok:!0},r.noStore)}async function handleVerify(n,i,s){if(n.method===`GET`){let e=r.queryParams(new URL(n.url),[`token`]).get(`token`)??``;if(!p.test(e))throw r.badRequest(`The link is not valid.`);return o.confirmationPage({...s,nonce:randomToken(),token:e})}requireMutationGuard(n,s,!0),r.queryParams(new URL(n.url),[]);let c=await r.readJsonBody(n);if(typeof c!=`object`||!c||Array.isArray(c)||Object.keys(c).length!==1)throw r.badRequest(`The link is not valid.`);let l=Reflect.get(c,`token`);if(typeof l!=`string`||!p.test(l))throw r.badRequest(`The link is not valid.`);let u=await a.consumeToken(i.AUTH_DB,await e.sha256Hex(l),Date.now());if(u===null)throw new r.ApiError(410,`gone`,`That link has expired or was already used.`);let d=await mintSession(s,u.id,s.operatorEmails.has(u.email),u.sessionVersion);return new Response(null,{headers:{...r.noStore,"set-cookie":t.sessionCookieValue(d.token,{maxAgeSeconds:d.maxAge,secure:secure(s)})},status:204})}async function handleExport(e,t,n){let{subject:i}=await requireSession(e,n,t),o=await a.readAccount(t.AUTH_DB,i);if(o===null)throw r.unauthenticated();return r.json(200,{createdAt:o.created_at,email:o.email,id:o.id,lastSignInAt:o.last_sign_in_at},r.noStore)}async function archiveFailure(e){let n=await e.text().catch(()=>``),i=null;try{i=JSON.parse(n)}catch{i=null}let a=t.archiveErrorResponseSchema.safeParse(i);if(!a.success||e.status<400||e.status>599)throw unavailable();let{code:o,message:s,requestId:c}=a.data.error,l=e.headers.get(`retry-after`);return r.json(e.status,{error:{code:o,message:s,requestId:c}},l!==null&&/^\d{1,6}$/.test(l)?{...r.noStore,"retry-after":l}:r.noStore)}async function handleErase(e,o,s){let c=requireMutationGuard(e,s),{subject:l,token:u}=await requireSession(e,s,o),d=e.headers.get(t.ACCOUNT_HEADER);if(d===null||!t.uuidSchema.safeParse(d).success)throw r.badRequest();if(d!==l)throw new r.ApiError(409,`account-changed`,`The signed-in account changed since this request was prepared.`);let f=new Headers({cookie:`${t.SESSION_COOKIE_NAME}=${u}`,origin:c,[t.REQUEST_MARKER_HEADER]:`1`}),p=e.headers.get(t.PUBLISHER_HEADER);if(p!==null){if(!m.test(p))throw r.badRequest();f.set(t.PUBLISHER_HEADER,p)}let h;try{h=await n.serviceRequest(o.AUTH_ARCHIVE,new Request(`${s.publicOrigin}/api/v1/me/account`,{headers:f,method:`DELETE`,redirect:`manual`}),async e=>e.status===204?(await e.body?.cancel(),e):archiveFailure(e))}catch(e){throw new i.AuthOperationError(503,`catalog`,{cause:e})}return h.status===204?(await a.deleteAccount(o.AUTH_DB,l),new Response(null,{headers:{...r.noStore,"set-cookie":r.expiredSessionCookie(secure(s))},status:204})):h}async function handleSignOutAll(e,n,i){requireMutationGuard(e,i);let{subject:o,sessionVersion:s}=await requireSession(e,i,n),c=e.headers.get(t.ACCOUNT_HEADER);if(c===null||!t.uuidSchema.safeParse(c).success)throw r.badRequest();if(c!==o)throw new r.ApiError(409,`account-changed`,`The signed-in account changed since this request was prepared.`);if(await a.revokeSessions(n.AUTH_DB,o,s)===null)throw r.unauthenticated();return r.json(200,{ok:!0},{...r.noStore,"set-cookie":r.expiredSessionCookie(secure(i))})}async function handleHealth(e){let n=null,i=null;try{await s.authConfig(e)}catch{n=`configuration-unavailable`}if(n===null)try{i=await a.authReadiness(e.AUTH_DB),i!==1&&(n=`schema-mismatch`)}catch{n=`database-unavailable`}return r.json(n===null?200:503,t.archiveAuthHealthSchema.parse({dbSchemaVersion:i,reason:n,schemaVersion:1,status:n===null?`ok`:`degraded`}),r.noStore)}async function route(e,t){let n=e.method.toUpperCase(),i=matchRoute(e);if(i===void 0)throw new r.ApiError(404,`not-found`,`No such route.`);if(i===`health`&&n===`GET`)return handleHealth(t);let a=await s.authConfig(t),o=corsHeaders(e,a);if(n===`OPTIONS`)return preflight(e,a,u[i]);if(!u[i].includes(n))throw new r.ApiError(405,`method-not-allowed`,`That method is not allowed here.`);let c;switch(i){case`health`:c=await handleHealth(t);break;case`session`:await requireSession(e,a,t),c=r.json(200,{ok:!0},r.noStore);break;case`signOutAll`:c=await handleSignOutAll(e,t,a);break;case`jwks`:c=r.json(200,{keys:a.jwks},r.noStore);break;case`signIn`:c=await handleSignIn(e,t,a);break;case`verify`:c=await handleVerify(e,t,a);break;case`account`:c=n===`GET`?await handleExport(e,t,a):await handleErase(e,t,a)}return withCors(c,o)}function withCors(e,t){if(Object.keys(t).length===0)return e;let n=new Response(e.body,e);for(let[e,r]of Object.entries(t))n.headers.set(e,r);return n}var diagnostic=e=>e instanceof i.AuthOperationError?e.authDiagnostic:{kind:`internal`};function createAuthWorker(){let e=new Set,emit=t=>{for(let n of e)try{n(t)}catch{}};return{async fetch(n,i){let a=crypto.randomUUID(),c=Date.now(),l,u;try{l=await route(n,i)}catch(e){u=e,l=r.toErrorResponse(e,a);let t=await s.authConfig(i).catch(()=>null);t!==null&&(matchRoute(n)===`verify`&&n.method===`GET`&&e instanceof r.ApiError&&e.status===400&&(l=o.confirmationPage({...t,nonce:randomToken(),token:null})),l=withCors(l,corsHeaders(n,t)))}if(matchRoute(n)===`verify`&&(l.headers.set(`cache-control`,`no-store`),l.headers.set(`referrer-policy`,`no-referrer`),l.headers.set(`x-content-type-options`,`nosniff`),l.headers.has(`content-security-policy`)||l.headers.set(`content-security-policy`,`default-src 'none'; base-uri 'none'; frame-ancestors 'none'`)),e.size>0){let e=matchRoute(n),i=l.status<400?`ok`:e===`health`?`unavailable`:u instanceof r.ApiError?u.code:`internal`,o=u===void 0&&l.status>=400?t.archiveErrorResponseSchema.safeParse(await l.clone().json().catch(()=>null)):null,s=e===`health`&&l.status===503?t.archiveAuthHealthSchema.safeParse(await l.clone().json().catch(()=>null)):null;emit({code:o?.success?o.data.error.code:i,ms:Math.max(0,Date.now()-c),requestId:a,route:e===void 0?`unmatched`:d[e],status:l.status,type:`request`,...l.status>=500?{diagnostic:s?.success?{kind:s.data.reason===`configuration-unavailable`?`configuration`:s.data.reason===`schema-mismatch`?`schema`:`database`}:u===void 0?{kind:`catalog`}:diagnostic(u)}:{}})}return l},async scheduled(t,n){try{await s.authConfig(n);let t=await a.sweep(n.AUTH_DB,Date.now());e.size>0&&emit({removed:t,status:`ok`,type:`scheduled`})}catch(t){throw e.size>0&&emit({diagnostic:diagnostic(t),status:`failed`,type:`scheduled`}),t}},tap(t,n){if(n?.signal?.aborted)return()=>void 0;let observer=e=>t(e),unsubscribe=()=>{e.delete(observer),n?.signal?.removeEventListener(`abort`,unsubscribe)};return e.add(observer),n?.signal?.addEventListener(`abort`,unsubscribe,{once:!0}),unsubscribe}}}exports.createAuthWorker=createAuthWorker;
1
+ const e=require("../_crypto.cjs"),t=require("../contract.cjs"),n=require("../worker/_service.cjs"),r=require("../worker/http.cjs"),i=require("./_confirmation.cjs"),a=require("./config.cjs"),o=require("./errors.cjs"),s=require("./store.cjs");let c=require("jose");var l={account:/^\/api\/v1\/auth\/account$/,health:/^\/api\/v1\/auth\/health$/,jwks:/^\/api\/v1\/auth\/jwks\.json$/,session:/^\/api\/v1\/auth\/session$/,signIn:/^\/api\/v1\/auth\/sign-in$/,signOutAll:/^\/api\/v1\/auth\/sign-out-all$/,verify:/^\/api\/v1\/auth\/verify$/},u={account:[`GET`,`DELETE`],health:[`GET`],jwks:[`GET`],session:[`GET`],signIn:[`POST`],signOutAll:[`POST`],verify:[`GET`,`POST`]},d={account:`/auth/account`,health:`/auth/health`,jwks:`/auth/jwks.json`,session:`/auth/session`,signIn:`/auth/sign-in`,signOutAll:`/auth/sign-out-all`,verify:`/auth/verify`},matchRoute=e=>Object.keys(l).find(t=>l[t].test(new URL(e.url).pathname)),f=32,p=/^[A-Za-z0-9_-]{43}$/,m=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,h=256,g=5,randomToken=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(f)))).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``),unavailable=()=>new o.AuthOperationError(503,`catalog`);function requireMutationGuard(e,t,n=!1){let i=e.headers.get(`origin`);if(i===null||!t.allowedOrigins.has(i)&&!(n&&i===t.publicOrigin)||e.headers.get(`x-archive-request`)!==`1`)throw r.forbidden();return i}function corsHeaders(e,t){let n=e.headers.get(`origin`);return n===null||!t.allowedOrigins.has(n)&&(matchRoute(e)!==`verify`||n!==t.publicOrigin)?{}:{"access-control-allow-credentials":`true`,"access-control-allow-origin":n,vary:`Origin`}}function preflight(e,n,i){let a=corsHeaders(e,n);if(Object.keys(a).length===0)throw r.forbidden();return new Response(null,{headers:{...a,"access-control-allow-headers":`Content-Type, ${t.ACCOUNT_HEADER}, ${t.PUBLISHER_HEADER}, ${t.REQUEST_MARKER_HEADER}`,"access-control-allow-methods":[...i,`OPTIONS`].join(`, `),"access-control-max-age":`600`},status:204})}var secure=e=>e.appEnv===`production`;async function mintSession(e,n,r,i){let a=r?t.OPERATOR_SESSION_MAX_AGE_SECONDS:t.SESSION_MAX_AGE_SECONDS;return{maxAge:a,token:await new c.SignJWT({sessionVersion:i,...r?{roles:[t.ARCHIVE_OPERATOR_ROLE]}:{}}).setProtectedHeader({alg:`RS256`,kid:e.signing.kid,typ:`JWT`}).setIssuedAt().setJti(crypto.randomUUID()).setIssuer(e.issuer).setAudience(e.audience).setSubject(n).setExpirationTime(`${a}s`).sign(e.signing.key)}}async function requireSession(e,n,i){let a=r.sessionToken(e);if(a===null)throw r.unauthenticated();try{let{payload:e}=await(0,c.jwtVerify)(a,n.verifier,{algorithms:[`RS256`],audience:n.audience,clockTolerance:g,issuer:n.issuer,maxTokenAge:t.SESSION_MAX_AGE_SECONDS,requiredClaims:[`sub`,`iss`,`aud`,`iat`,`exp`,`sessionVersion`]}),o=e.sub??``;if(o.length===0||o.length>h||!t.isWellFormedText(o))throw r.unauthenticated();let l=e.sessionVersion;if(typeof l!=`number`||!Number.isSafeInteger(l)||l<0||typeof e.iat!=`number`||e.iat>Date.now()/1e3+g||Array.isArray(e.roles)&&e.roles.includes(`archive-operator`)&&(typeof e.exp!=`number`||e.exp-e.iat>3600||Date.now()/1e3-e.iat>3600+g))throw r.unauthenticated();let u=await s.readAccount(i.AUTH_DB,o);if(u===null||u.session_version!==l)throw r.unauthenticated();return{sessionVersion:l,subject:o,token:a}}catch(e){throw e instanceof c.errors.JOSEError?r.unauthenticated():e}}async function sendLink(e,t,n,r){let a=t.appName,o=`The link works once and expires in ${s.LINK_TTL_MS/6e4} minutes. If you did not request it, ignore this email.`;await e.AUTH_EMAIL.send({from:t.senderAddress,html:`<p>Open this link to sign in to ${i.escapeHtml(a)}:</p><p><a href="${i.escapeHtml(r)}">${i.escapeHtml(r)}</a></p><p>${o}</p>`,subject:`Your ${a} sign-in link`,text:`Open this link to sign in to ${a}:\n\n${r}\n\n${o}`,to:n})}async function handleSignIn(t,n,i){requireMutationGuard(t,i);let c=await r.readJsonBody(t);if(typeof c!=`object`||!c||Array.isArray(c))throw r.badRequest();let l=Object.keys(c),u=Reflect.get(c,`email`);if(l.length!==1||l[0]!==`email`||typeof u!=`string`)throw r.badRequest();let d=u.trim().toLowerCase();if(!a.isWellFormedEmail(d))throw r.badRequest();let f=Date.now(),p=await s.spendSignInBudget(n.AUTH_DB,f);if(p!==null)throw new r.ApiError(429,`rate-limited`,`Too many sign-in requests. Try again later.`,{retryAfterSeconds:p});let m=await s.ensureUser(n.AUTH_DB,d),h=await s.spendLinkBudget(n.AUTH_DB,m.id,f);if(h!==null)throw new r.ApiError(429,`rate-limited`,`Too many sign-in emails. Try again later.`,{retryAfterSeconds:h});let g=randomToken(),_=await e.sha256Hex(g);await s.insertToken(n.AUTH_DB,_,m.id,f);try{await sendLink(n,i,m.email,`${i.publicOrigin}/api/v1/auth/verify?token=${g}`)}catch(e){throw await s.deleteToken(n.AUTH_DB,_).catch(()=>void 0),new o.AuthOperationError(503,`email`,{cause:e})}return r.json(202,{ok:!0},r.noStore)}async function handleVerify(n,a,o){if(n.method===`GET`){let e=r.queryParams(new URL(n.url),[`token`]).get(`token`)??``;if(!p.test(e))throw r.badRequest(`The link is not valid.`);return i.confirmationPage({...o,nonce:randomToken(),token:e})}requireMutationGuard(n,o,!0),r.queryParams(new URL(n.url),[]);let c=await r.readJsonBody(n);if(typeof c!=`object`||!c||Array.isArray(c)||Object.keys(c).length!==1)throw r.badRequest(`The link is not valid.`);let l=Reflect.get(c,`token`);if(typeof l!=`string`||!p.test(l))throw r.badRequest(`The link is not valid.`);let u=await s.consumeToken(a.AUTH_DB,await e.sha256Hex(l),Date.now());if(u===null)throw new r.ApiError(410,`gone`,`That link has expired or was already used.`);let d=await mintSession(o,u.id,o.operatorEmails.has(u.email),u.sessionVersion);return new Response(null,{headers:{...r.noStore,"set-cookie":t.sessionCookieValue(d.token,{maxAgeSeconds:d.maxAge,secure:secure(o)})},status:204})}async function handleExport(e,t,n){let{subject:i}=await requireSession(e,n,t),a=await s.readAccount(t.AUTH_DB,i);if(a===null)throw r.unauthenticated();return r.json(200,{createdAt:a.created_at,email:a.email,id:a.id,lastSignInAt:a.last_sign_in_at},r.noStore)}async function archiveFailure(e){let n=await e.text().catch(()=>``),i=null;try{i=JSON.parse(n)}catch{i=null}let a=t.archiveErrorResponseSchema.safeParse(i);if(!a.success||e.status<400||e.status>599)throw unavailable();let{code:o,message:s,requestId:c}=a.data.error,l=e.headers.get(`retry-after`);return r.json(e.status,{error:{code:o,message:s,requestId:c}},l!==null&&/^\d{1,6}$/.test(l)?{...r.noStore,"retry-after":l}:r.noStore)}async function handleErase(e,i,a){let c=requireMutationGuard(e,a),{subject:l,token:u}=await requireSession(e,a,i),d=e.headers.get(t.ACCOUNT_HEADER);if(d===null||!t.uuidSchema.safeParse(d).success)throw r.badRequest();if(d!==l)throw new r.ApiError(409,`account-changed`,`The signed-in account changed since this request was prepared.`);let f=new Headers({cookie:`${t.SESSION_COOKIE_NAME}=${u}`,origin:c,[t.REQUEST_MARKER_HEADER]:`1`}),p=e.headers.get(t.PUBLISHER_HEADER);if(p!==null){if(!m.test(p))throw r.badRequest();f.set(t.PUBLISHER_HEADER,p)}let h;try{h=await n.serviceRequest(i.AUTH_ARCHIVE,new Request(`${a.publicOrigin}/api/v1/me/account`,{headers:f,method:`DELETE`,redirect:`manual`}),async e=>e.status===204?(await e.body?.cancel(),e):archiveFailure(e))}catch(e){throw new o.AuthOperationError(503,`catalog`,{cause:e})}return h.status===204?(await s.deleteAccount(i.AUTH_DB,l),new Response(null,{headers:{...r.noStore,"set-cookie":r.expiredSessionCookie(secure(a))},status:204})):h}async function handleSignOutAll(e,n,i){requireMutationGuard(e,i);let{subject:a,sessionVersion:o}=await requireSession(e,i,n),c=e.headers.get(t.ACCOUNT_HEADER);if(c===null||!t.uuidSchema.safeParse(c).success)throw r.badRequest();if(c!==a)throw new r.ApiError(409,`account-changed`,`The signed-in account changed since this request was prepared.`);if(await s.revokeSessions(n.AUTH_DB,a,o)===null)throw r.unauthenticated();return r.json(200,{ok:!0},{...r.noStore,"set-cookie":r.expiredSessionCookie(secure(i))})}async function handleHealth(e){let n=null,i=null;try{await a.authConfig(e)}catch{n=`configuration-unavailable`}if(n===null)try{i=await s.authReadiness(e.AUTH_DB),i!==1&&(n=`schema-mismatch`)}catch{n=`database-unavailable`}return r.json(n===null?200:503,t.archiveAuthHealthSchema.parse({dbSchemaVersion:i,reason:n,schemaVersion:1,status:n===null?`ok`:`degraded`}),r.noStore)}async function route(e,t){let n=e.method.toUpperCase(),i=matchRoute(e);if(i===void 0)throw new r.ApiError(404,`not-found`,`No such route.`);if(i===`health`&&n===`GET`)return handleHealth(t);let o=await a.authConfig(t),s=corsHeaders(e,o);if(n===`OPTIONS`)return preflight(e,o,u[i]);if(!u[i].includes(n))throw new r.ApiError(405,`method-not-allowed`,`That method is not allowed here.`);let c;switch(i){case`health`:c=await handleHealth(t);break;case`session`:await requireSession(e,o,t),c=r.json(200,{ok:!0},r.noStore);break;case`signOutAll`:c=await handleSignOutAll(e,t,o);break;case`jwks`:c=r.json(200,{keys:o.jwks},r.noStore);break;case`signIn`:c=await handleSignIn(e,t,o);break;case`verify`:c=await handleVerify(e,t,o);break;case`account`:c=n===`GET`?await handleExport(e,t,o):await handleErase(e,t,o)}return withCors(c,s)}function withCors(e,t){if(Object.keys(t).length===0)return e;let n=new Response(e.body,e);for(let[e,r]of Object.entries(t))n.headers.set(e,r);return n}var diagnostic=e=>e instanceof o.AuthOperationError?e.authDiagnostic:{kind:`internal`};function createAuthWorker(){let e=new Set,emit=t=>{for(let n of e)try{n(t)}catch{}};return{async fetch(n,o){let s=crypto.randomUUID(),c=Date.now(),l,u;try{l=await route(n,o)}catch(e){u=e,l=r.toErrorResponse(e,s);let t=await a.authConfig(o).catch(()=>null);t!==null&&(matchRoute(n)===`verify`&&n.method===`GET`&&e instanceof r.ApiError&&e.status===400&&(l=i.confirmationPage({...t,nonce:randomToken(),token:null})),l=withCors(l,corsHeaders(n,t)))}if(matchRoute(n)===`verify`&&(l.headers.set(`cache-control`,`no-store`),l.headers.set(`referrer-policy`,`no-referrer`),l.headers.set(`x-content-type-options`,`nosniff`),l.headers.has(`content-security-policy`)||l.headers.set(`content-security-policy`,`default-src 'none'; base-uri 'none'; frame-ancestors 'none'`)),e.size>0){let e=matchRoute(n),i=l.status<400?`ok`:e===`health`?`unavailable`:u instanceof r.ApiError?u.code:`internal`,a=u===void 0&&l.status>=400?t.archiveErrorResponseSchema.safeParse(await l.clone().json().catch(()=>null)):null,o=e===`health`&&l.status===503?t.archiveAuthHealthSchema.safeParse(await l.clone().json().catch(()=>null)):null;emit({code:a?.success?a.data.error.code:i,ms:Math.max(0,Date.now()-c),requestId:s,route:e===void 0?`unmatched`:d[e],status:l.status,type:`request`,...l.status>=500?{diagnostic:o?.success?{kind:o.data.reason===`configuration-unavailable`?`configuration`:o.data.reason===`schema-mismatch`?`schema`:`database`}:u===void 0?{kind:`catalog`}:diagnostic(u)}:{}})}return l},async scheduled(t,n){try{await a.authConfig(n);let t=await s.sweep(n.AUTH_DB,Date.now());e.size>0&&emit({removed:t,status:`ok`,type:`scheduled`})}catch(t){throw e.size>0&&emit({diagnostic:diagnostic(t),status:`failed`,type:`scheduled`}),t}},tap(t,n){if(n?.signal?.aborted)return()=>void 0;let observer=e=>t(e),unsubscribe=()=>{e.delete(observer),n?.signal?.removeEventListener(`abort`,unsubscribe)};return e.add(observer),n?.signal?.addEventListener(`abort`,unsubscribe,{once:!0}),unsubscribe}}}exports.createAuthWorker=createAuthWorker;
2
2
  //# sourceMappingURL=worker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.cjs","names":["AuthOperationError","forbidden","ACCOUNT_HEADER","PUBLISHER_HEADER","REQUEST_MARKER_HEADER","OPERATOR_SESSION_MAX_AGE_SECONDS","SESSION_MAX_AGE_SECONDS","SignJWT","ARCHIVE_OPERATOR_ROLE","sessionToken","unauthenticated","jwtVerify","isWellFormedText","readAccount","errors","LINK_TTL_MS","escapeHtml","readJsonBody","badRequest","isWellFormedEmail","spendSignInBudget","ApiError","ensureUser","spendLinkBudget","sha256Hex","insertToken","deleteToken","json","noStore","queryParams","confirmationPage","consumeToken","sessionCookieValue","archiveErrorResponseSchema","uuidSchema","SESSION_COOKIE_NAME","serviceRequest","deleteAccount","expiredSessionCookie","revokeSessions","authConfig","authReadiness","archiveAuthHealthSchema","toErrorResponse","sweep","require__crypto","require_contract","require__service","require_http","require_errors","require_store","require__confirmation","require_config","jose","ROUTES","ROUTE_METHODS","ROUTE_LABELS","id","request","TOKEN_BYTES","LINK_TOKEN","PUBLISHER_ID","SUBJECT_MAX","CLOCK_TOLERANCE_SECONDS","origin","config","allowPublicOrigin","cors","methods","maxAge","operator","subject","token","payload","version","account","env","error","name","notice","link","body","keys","requestedEmail","email","nowMs","admissionRetry","user","retryAfterSeconds","tokenHash","session","row","text","response","parsed","envelope","retryAfter","headers","publisher","archived","sessionVersion","reason","dbSchemaVersion","method","routeId","next","key","value","tappers","handler","event","requestId","started","failure","code","forwardedCode","health","removed","options"],"sources":["../../src/auth/worker.ts"],"sourcesContent":["import { errors, jwtVerify, SignJWT } from 'jose';\nimport { sha256Hex } from '../_crypto';\nimport {\n ACCOUNT_HEADER,\n ARCHIVE_OPERATOR_ROLE,\n type ArchiveErrorCode,\n AUTH_SCHEMA_VERSION,\n archiveAuthHealthSchema,\n archiveErrorResponseSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n PUBLISHER_HEADER,\n REQUEST_MARKER_HEADER,\n SESSION_COOKIE_NAME,\n SESSION_MAX_AGE_SECONDS,\n sessionCookieValue,\n uuidSchema,\n} from '../contract';\nimport { serviceRequest } from '../worker/_service';\nimport {\n ApiError,\n badRequest,\n expiredSessionCookie,\n forbidden,\n json,\n noStore,\n queryParams,\n readJsonBody,\n sessionToken,\n toErrorResponse,\n unauthenticated,\n} from '../worker/http';\nimport type { ArchiveExecutionContext } from '../worker/worker';\nimport { confirmationPage, escapeHtml } from './_confirmation';\nimport { type AuthConfig, type AuthWorkerEnv, authConfig, isWellFormedEmail } from './config';\nimport { type AuthDiagnostic, AuthOperationError } from './errors';\nimport {\n authReadiness,\n consumeToken,\n deleteAccount,\n deleteToken,\n ensureUser,\n insertToken,\n LINK_TTL_MS,\n readAccount,\n revokeSessions,\n spendLinkBudget,\n spendSignInBudget,\n sweep,\n} from './store';\n\n/**\n * The reference identity issuer: passwordless email accounts. A POST with an email\n * address ensures the account row and sends a single-use magic link; consuming it\n * signs the browser in by setting the `archive_session` cookie a catalog Worker\n * verifies — an RS256 JWT minted by the active private key, verifiable through\n * `/jwks.json` alongside any configured retired public keys. `OPERATOR_EMAILS` is\n * the one operator-membership truth; operator sessions expire after one hour,\n * bounding authority from previously minted role claims. The account routes export the issuer's\n * own record and erase it together with the catalog profile.\n */\nexport interface AuthWorkerHandler<E extends AuthWorkerEnv = AuthWorkerEnv> {\n fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise<Response>;\n /** The cron sweep: expired links, closed send windows and never-verified accounts. */\n scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise<void>;\n tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\nexport type AuthEvent =\n | {\n readonly type: 'request';\n readonly requestId: string;\n readonly route:\n | '/auth/account'\n | '/auth/jwks.json'\n | '/auth/sign-in'\n | '/auth/verify'\n | '/auth/session'\n | '/auth/sign-out-all'\n | '/auth/health'\n | 'unmatched';\n readonly status: number;\n readonly code: ArchiveErrorCode | 'ok';\n readonly ms: number;\n readonly diagnostic?: AuthDiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };\n\nconst ROUTES = {\n account: /^\\/api\\/v1\\/auth\\/account$/,\n health: /^\\/api\\/v1\\/auth\\/health$/,\n jwks: /^\\/api\\/v1\\/auth\\/jwks\\.json$/,\n session: /^\\/api\\/v1\\/auth\\/session$/,\n signIn: /^\\/api\\/v1\\/auth\\/sign-in$/,\n signOutAll: /^\\/api\\/v1\\/auth\\/sign-out-all$/,\n verify: /^\\/api\\/v1\\/auth\\/verify$/,\n} as const;\n\nconst ROUTE_METHODS: Record<keyof typeof ROUTES, readonly string[]> = {\n account: ['GET', 'DELETE'],\n health: ['GET'],\n jwks: ['GET'],\n session: ['GET'],\n signIn: ['POST'],\n signOutAll: ['POST'],\n verify: ['GET', 'POST'],\n};\n\nconst ROUTE_LABELS = {\n account: '/auth/account',\n health: '/auth/health',\n jwks: '/auth/jwks.json',\n session: '/auth/session',\n signIn: '/auth/sign-in',\n signOutAll: '/auth/sign-out-all',\n verify: '/auth/verify',\n} as const;\n\nconst matchRoute = (request: Request): keyof typeof ROUTES | undefined =>\n (Object.keys(ROUTES) as (keyof typeof ROUTES)[]).find((id) => ROUTES[id].test(new URL(request.url).pathname));\n\nconst TOKEN_BYTES = 32;\n// Base64url of TOKEN_BYTES random bytes, unpadded.\nconst LINK_TOKEN = /^[A-Za-z0-9_-]{43}$/;\nconst PUBLISHER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\nconst SUBJECT_MAX = 256;\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\nconst randomToken = (): string =>\n btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(TOKEN_BYTES))))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n\nconst unavailable = (): ApiError => new AuthOperationError(503, 'catalog');\n\n/** Exact allowed Origin plus the request marker, like the catalog's mutation guard. */\nfunction requireMutationGuard(request: Request, config: AuthConfig, allowPublicOrigin = false): string {\n const origin = request.headers.get('origin');\n if (origin === null || (!config.allowedOrigins.has(origin) && !(allowPublicOrigin && origin === config.publicOrigin)))\n throw forbidden();\n if (request.headers.get(REQUEST_MARKER_HEADER) !== '1') throw forbidden();\n return origin;\n}\n\nfunction corsHeaders(request: Request, config: AuthConfig): Record<string, string> {\n const origin = request.headers.get('origin');\n if (\n origin === null ||\n (!config.allowedOrigins.has(origin) && !(matchRoute(request) === 'verify' && origin === config.publicOrigin))\n )\n return {};\n return { 'access-control-allow-credentials': 'true', 'access-control-allow-origin': origin, vary: 'Origin' };\n}\n\nfunction preflight(request: Request, config: AuthConfig, methods: readonly string[]): Response {\n const cors = corsHeaders(request, config);\n if (Object.keys(cors).length === 0) throw forbidden();\n return new Response(null, {\n headers: {\n ...cors,\n 'access-control-allow-headers': `Content-Type, ${ACCOUNT_HEADER}, ${PUBLISHER_HEADER}, ${REQUEST_MARKER_HEADER}`,\n 'access-control-allow-methods': [...methods, 'OPTIONS'].join(', '),\n 'access-control-max-age': '600',\n },\n status: 204,\n });\n}\n\nconst secure = (config: AuthConfig): boolean => config.appEnv === 'production';\n\nasync function mintSession(\n config: AuthConfig,\n subject: string,\n operator: boolean,\n sessionVersion: number,\n): Promise<{ maxAge: number; token: string }> {\n const maxAge = operator ? OPERATOR_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;\n const token = await new SignJWT({ sessionVersion, ...(operator ? { roles: [ARCHIVE_OPERATOR_ROLE] } : {}) })\n .setProtectedHeader({ alg: 'RS256', kid: config.signing.kid, typ: 'JWT' })\n .setIssuedAt()\n .setJti(crypto.randomUUID())\n .setIssuer(config.issuer)\n .setAudience(config.audience)\n .setSubject(subject)\n .setExpirationTime(`${maxAge}s`)\n .sign(config.signing.key);\n return { maxAge, token };\n}\n\n/** Verifies the request's own session cookie against every published key; returns the token and subject. */\nasync function requireSession(\n request: Request,\n config: AuthConfig,\n env: AuthWorkerEnv,\n): Promise<{ subject: string; token: string; sessionVersion: number }> {\n const token = sessionToken(request);\n if (token === null) throw unauthenticated();\n try {\n const { payload } = await jwtVerify(token, config.verifier, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: SESSION_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n const subject = payload.sub ?? '';\n if (subject.length === 0 || subject.length > SUBJECT_MAX || !isWellFormedText(subject)) throw unauthenticated();\n const version = payload.sessionVersion;\n if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) throw unauthenticated();\n if (typeof payload.iat !== 'number' || payload.iat > Date.now() / 1000 + CLOCK_TOLERANCE_SECONDS)\n throw unauthenticated();\n if (\n Array.isArray(payload.roles) &&\n payload.roles.includes(ARCHIVE_OPERATOR_ROLE) &&\n (typeof payload.exp !== 'number' ||\n payload.exp - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS + CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n const account = await readAccount(env.AUTH_DB, subject);\n if (account === null || account.session_version !== version) throw unauthenticated();\n return { sessionVersion: version, subject, token };\n } catch (error) {\n if (error instanceof errors.JOSEError) throw unauthenticated();\n throw error;\n }\n}\n\nasync function sendLink(env: AuthWorkerEnv, config: AuthConfig, to: string, link: string): Promise<void> {\n const name = config.appName;\n const minutes = LINK_TTL_MS / 60_000;\n const notice = `The link works once and expires in ${minutes} minutes. If you did not request it, ignore this email.`;\n await env.AUTH_EMAIL.send({\n from: config.senderAddress,\n html: `<p>Open this link to sign in to ${escapeHtml(name)}:</p><p><a href=\"${escapeHtml(link)}\">${escapeHtml(link)}</a></p><p>${notice}</p>`,\n subject: `Your ${name} sign-in link`,\n text: `Open this link to sign in to ${name}:\\n\\n${link}\\n\\n${notice}`,\n to,\n });\n}\n\nasync function handleSignIn(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body)) throw badRequest();\n const keys = Object.keys(body);\n const requestedEmail: unknown = Reflect.get(body, 'email');\n if (keys.length !== 1 || keys[0] !== 'email' || typeof requestedEmail !== 'string') {\n throw badRequest();\n }\n const email = requestedEmail.trim().toLowerCase();\n if (!isWellFormedEmail(email)) {\n throw badRequest();\n }\n const nowMs = Date.now();\n const admissionRetry = await spendSignInBudget(env.AUTH_DB, nowMs);\n if (admissionRetry !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in requests. Try again later.', {\n retryAfterSeconds: admissionRetry,\n });\n }\n const user = await ensureUser(env.AUTH_DB, email);\n const retryAfterSeconds = await spendLinkBudget(env.AUTH_DB, user.id, nowMs);\n if (retryAfterSeconds !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in emails. Try again later.', { retryAfterSeconds });\n }\n const token = randomToken();\n const tokenHash = await sha256Hex(token);\n await insertToken(env.AUTH_DB, tokenHash, user.id, nowMs);\n try {\n await sendLink(env, config, user.email, `${config.publicOrigin}/api/v1/auth/verify?token=${token}`);\n } catch (error) {\n // An unsent link is useless; the spent allowance is deliberately not refunded.\n await deleteToken(env.AUTH_DB, tokenHash).catch(() => undefined);\n throw new AuthOperationError(503, 'email', { cause: error });\n }\n return json(202, { ok: true }, noStore);\n}\n\nasync function handleVerify(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n if (request.method === 'GET') {\n const token = queryParams(new URL(request.url), ['token']).get('token') ?? '';\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n return confirmationPage({ ...config, nonce: randomToken(), token });\n }\n requireMutationGuard(request, config, true);\n queryParams(new URL(request.url), []);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body) || Object.keys(body).length !== 1) {\n throw badRequest('The link is not valid.');\n }\n const token: unknown = Reflect.get(body, 'token');\n if (typeof token !== 'string') throw badRequest('The link is not valid.');\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const user = await consumeToken(env.AUTH_DB, await sha256Hex(token), Date.now());\n if (user === null) throw new ApiError(410, 'gone', 'That link has expired or was already used.');\n const session = await mintSession(config, user.id, config.operatorEmails.has(user.email), user.sessionVersion);\n return new Response(null, {\n headers: {\n ...noStore,\n 'set-cookie': sessionCookieValue(session.token, { maxAgeSeconds: session.maxAge, secure: secure(config) }),\n },\n status: 204,\n });\n}\n\n/** The issuer's half of a complete export: the account record only, never tokens or budget state. */\nasync function handleExport(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const { subject } = await requireSession(request, config, env);\n const row = await readAccount(env.AUTH_DB, subject);\n if (row === null) throw unauthenticated();\n return json(\n 200,\n { createdAt: row.created_at, email: row.email, id: row.id, lastSignInAt: row.last_sign_in_at },\n noStore,\n );\n}\n\n/** Re-serializes a failed catalog response into the public envelope; anything unrecognizable is 503. */\nasync function archiveFailure(response: Response): Promise<Response> {\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n const envelope = archiveErrorResponseSchema.safeParse(parsed);\n if (!envelope.success || response.status < 400 || response.status > 599) throw unavailable();\n const { code, message, requestId } = envelope.data.error;\n const retryAfter = response.headers.get('retry-after');\n return json(\n response.status,\n { error: { code, message, requestId } },\n retryAfter !== null && /^\\d{1,6}$/.test(retryAfter) ? { ...noStore, 'retry-after': retryAfter } : noStore,\n );\n}\n\n/**\n * Full erasure: catalog session revocation and profile erasure first (through the AUTH_ARCHIVE service binding,\n * presenting the caller's own session), then the issuer's account row, and only then\n * the cookie is expired. Any failure keeps the browser session so the same request\n * can be retried; both halves are idempotent, so a retry after a partial success\n * re-runs the catalog no-op and finishes the issuer side. The account precondition\n * pins every attempt to the confirmed issuer subject, even without a publisher.\n */\nasync function handleErase(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const origin = requireMutationGuard(request, config);\n const { subject, token } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject) {\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n }\n const headers = new Headers({\n cookie: `${SESSION_COOKIE_NAME}=${token}`,\n origin,\n [REQUEST_MARKER_HEADER]: '1',\n });\n const publisher = request.headers.get(PUBLISHER_HEADER);\n if (publisher !== null) {\n if (!PUBLISHER_ID.test(publisher)) throw badRequest();\n headers.set(PUBLISHER_HEADER, publisher);\n }\n let archived: Response;\n try {\n archived = await serviceRequest(\n env.AUTH_ARCHIVE,\n new Request(`${config.publicOrigin}/api/v1/me/account`, { headers, method: 'DELETE', redirect: 'manual' }),\n async (response) => {\n if (response.status === 204) {\n await response.body?.cancel();\n return response;\n }\n return archiveFailure(response);\n },\n );\n } catch (error) {\n throw new AuthOperationError(503, 'catalog', { cause: error });\n }\n if (archived.status !== 204) return archived;\n await deleteAccount(env.AUTH_DB, subject);\n return new Response(null, {\n headers: { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) },\n status: 204,\n });\n}\n\nasync function handleSignOutAll(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const { subject, sessionVersion } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject)\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n if ((await revokeSessions(env.AUTH_DB, subject, sessionVersion)) === null) throw unauthenticated();\n return json(200, { ok: true }, { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) });\n}\n\nasync function handleHealth(env: AuthWorkerEnv): Promise<Response> {\n let reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null = null;\n let dbSchemaVersion: number | null = null;\n try {\n await authConfig(env);\n } catch {\n reason = 'configuration-unavailable';\n }\n if (reason === null) {\n try {\n dbSchemaVersion = await authReadiness(env.AUTH_DB);\n if (dbSchemaVersion !== AUTH_SCHEMA_VERSION) reason = 'schema-mismatch';\n } catch {\n reason = 'database-unavailable';\n }\n }\n return json(\n reason === null ? 200 : 503,\n archiveAuthHealthSchema.parse({\n dbSchemaVersion,\n reason,\n schemaVersion: AUTH_SCHEMA_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n }),\n noStore,\n );\n}\nasync function route(request: Request, env: AuthWorkerEnv): Promise<Response> {\n const method = request.method.toUpperCase();\n const routeId = matchRoute(request);\n if (routeId === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n if (routeId === 'health' && method === 'GET') return handleHealth(env);\n const config = await authConfig(env);\n const cors = corsHeaders(request, config);\n if (method === 'OPTIONS') return preflight(request, config, ROUTE_METHODS[routeId]);\n if (!ROUTE_METHODS[routeId].includes(method)) {\n throw new ApiError(405, 'method-not-allowed', 'That method is not allowed here.');\n }\n let response: Response;\n switch (routeId) {\n case 'health':\n response = await handleHealth(env);\n break;\n case 'session':\n await requireSession(request, config, env);\n response = json(200, { ok: true }, noStore);\n break;\n case 'signOutAll':\n response = await handleSignOutAll(request, env, config);\n break;\n case 'jwks':\n response = json(200, { keys: config.jwks }, noStore);\n break;\n case 'signIn':\n response = await handleSignIn(request, env, config);\n break;\n case 'verify':\n response = await handleVerify(request, env, config);\n break;\n case 'account':\n response = method === 'GET' ? await handleExport(request, env, config) : await handleErase(request, env, config);\n break;\n }\n return withCors(response, cors);\n}\n\nfunction withCors(response: Response, cors: Record<string, string>): Response {\n if (Object.keys(cors).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(cors)) next.headers.set(key, value);\n return next;\n}\n\nconst diagnostic = (error: unknown): AuthDiagnostic =>\n error instanceof AuthOperationError ? error.authDiagnostic : { kind: 'internal' };\n\n/**\n * Builds the reference identity issuer. Route behavior is fixed; every deployment\n * difference is validated configuration — the app name, trusted origins, audience and\n * issuer claims, signing keys, operator roster, the catalog service binding, and\n * where links and redirects land.\n */\nexport function createAuthWorker<E extends AuthWorkerEnv>(): AuthWorkerHandler<E> {\n const tappers = new Set<(event: AuthEvent) => void>();\n const emit = (event: AuthEvent): void => {\n for (const handler of tappers) {\n try {\n handler(event);\n } catch {\n // Observers never change request or scheduled outcomes.\n }\n }\n };\n return {\n async fetch(request: Request, env: E): Promise<Response> {\n const requestId = crypto.randomUUID();\n const started = Date.now();\n let response: Response;\n let failure: unknown;\n try {\n response = await route(request, env);\n } catch (error) {\n failure = error;\n response = toErrorResponse(error, requestId);\n // Failures stay readable by allowed origins; config faults have no trusted origin set.\n const config = await authConfig(env).catch(() => null);\n if (config !== null) {\n if (\n matchRoute(request) === 'verify' &&\n request.method === 'GET' &&\n error instanceof ApiError &&\n error.status === 400\n )\n response = confirmationPage({ ...config, nonce: randomToken(), token: null });\n response = withCors(response, corsHeaders(request, config));\n }\n }\n if (matchRoute(request) === 'verify') {\n response.headers.set('cache-control', 'no-store');\n response.headers.set('referrer-policy', 'no-referrer');\n response.headers.set('x-content-type-options', 'nosniff');\n if (!response.headers.has('content-security-policy')) {\n response.headers.set(\n 'content-security-policy',\n \"default-src 'none'; base-uri 'none'; frame-ancestors 'none'\",\n );\n }\n }\n if (tappers.size > 0) {\n const routeId = matchRoute(request);\n const code =\n response.status < 400\n ? 'ok'\n : routeId === 'health'\n ? 'unavailable'\n : failure instanceof ApiError\n ? failure.code\n : 'internal';\n // Account erasure may forward a catalog error response rather than throw.\n const forwardedCode =\n failure === undefined && response.status >= 400\n ? archiveErrorResponseSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n const health =\n routeId === 'health' && response.status === 503\n ? archiveAuthHealthSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n emit({\n code: forwardedCode?.success ? forwardedCode.data.error.code : code,\n ms: Math.max(0, Date.now() - started),\n requestId,\n route: routeId === undefined ? 'unmatched' : ROUTE_LABELS[routeId],\n status: response.status,\n type: 'request',\n ...(response.status >= 500\n ? {\n diagnostic: health?.success\n ? {\n kind:\n health.data.reason === 'configuration-unavailable'\n ? 'configuration'\n : health.data.reason === 'schema-mismatch'\n ? 'schema'\n : 'database',\n }\n : failure === undefined\n ? { kind: 'catalog' as const }\n : diagnostic(failure),\n }\n : {}),\n });\n }\n return response;\n },\n\n async scheduled(_controller: unknown, env: E): Promise<void> {\n try {\n await authConfig(env);\n const removed = await sweep(env.AUTH_DB, Date.now());\n if (tappers.size > 0) emit({ removed, status: 'ok', type: 'scheduled' });\n } catch (error) {\n if (tappers.size > 0) emit({ diagnostic: diagnostic(error), status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n tap(handler, options) {\n if (options?.signal?.aborted) return () => undefined;\n const observer = (event: AuthEvent): void => handler(event);\n const unsubscribe = (): void => {\n tappers.delete(observer);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n tappers.add(observer);\n options?.signal?.addEventListener('abort', unsubscribe, { once: true });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"worker.cjs","names":["AuthOperationError","forbidden","ACCOUNT_HEADER","PUBLISHER_HEADER","REQUEST_MARKER_HEADER","OPERATOR_SESSION_MAX_AGE_SECONDS","SESSION_MAX_AGE_SECONDS","SignJWT","ARCHIVE_OPERATOR_ROLE","sessionToken","unauthenticated","jwtVerify","isWellFormedText","readAccount","errors","LINK_TTL_MS","escapeHtml","readJsonBody","badRequest","isWellFormedEmail","spendSignInBudget","ApiError","ensureUser","spendLinkBudget","sha256Hex","insertToken","deleteToken","json","noStore","queryParams","confirmationPage","consumeToken","sessionCookieValue","archiveErrorResponseSchema","uuidSchema","SESSION_COOKIE_NAME","serviceRequest","deleteAccount","expiredSessionCookie","revokeSessions","authConfig","authReadiness","archiveAuthHealthSchema","toErrorResponse","sweep","require__crypto","require_contract","require__service","require_http","require__confirmation","require_config","require_errors","require_store","jose","ROUTES","ROUTE_METHODS","ROUTE_LABELS","id","request","TOKEN_BYTES","LINK_TOKEN","PUBLISHER_ID","SUBJECT_MAX","CLOCK_TOLERANCE_SECONDS","origin","config","allowPublicOrigin","cors","methods","maxAge","operator","subject","token","payload","version","account","env","error","name","notice","link","body","keys","requestedEmail","email","nowMs","admissionRetry","user","retryAfterSeconds","tokenHash","session","row","text","response","parsed","envelope","retryAfter","headers","publisher","archived","sessionVersion","reason","dbSchemaVersion","method","routeId","next","key","value","tappers","handler","event","requestId","started","failure","code","forwardedCode","health","removed","options"],"sources":["../../src/auth/worker.ts"],"sourcesContent":["import { errors, jwtVerify, SignJWT } from 'jose';\nimport { sha256Hex } from '../_crypto';\nimport {\n ACCOUNT_HEADER,\n ARCHIVE_OPERATOR_ROLE,\n type ArchiveErrorCode,\n AUTH_SCHEMA_VERSION,\n archiveAuthHealthSchema,\n archiveErrorResponseSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n PUBLISHER_HEADER,\n REQUEST_MARKER_HEADER,\n SESSION_COOKIE_NAME,\n SESSION_MAX_AGE_SECONDS,\n sessionCookieValue,\n uuidSchema,\n} from '../contract';\nimport { serviceRequest } from '../worker/_service';\nimport {\n ApiError,\n badRequest,\n expiredSessionCookie,\n forbidden,\n json,\n noStore,\n queryParams,\n readJsonBody,\n sessionToken,\n toErrorResponse,\n unauthenticated,\n} from '../worker/http';\nimport type { ArchiveExecutionContext } from '../worker/worker';\nimport { confirmationPage, escapeHtml } from './_confirmation';\nimport { type AuthConfig, type AuthWorkerEnv, authConfig, isWellFormedEmail } from './config';\nimport { type AuthDiagnostic, AuthOperationError } from './errors';\nimport {\n authReadiness,\n consumeToken,\n deleteAccount,\n deleteToken,\n ensureUser,\n insertToken,\n LINK_TTL_MS,\n readAccount,\n revokeSessions,\n spendLinkBudget,\n spendSignInBudget,\n sweep,\n} from './store';\n\n/**\n * The reference identity issuer: passwordless email accounts. A POST with an email\n * address ensures the account row and sends a single-use magic link; consuming it\n * signs the browser in by setting the `archive_session` cookie a catalog Worker\n * verifies — an RS256 JWT minted by the active private key, verifiable through\n * `/jwks.json` alongside any configured retired public keys. `OPERATOR_EMAILS` is\n * the one operator-membership truth; operator sessions expire after one hour,\n * bounding authority from previously minted role claims. The account routes export the issuer's\n * own record and erase it together with the catalog profile.\n */\nexport interface AuthWorkerHandler<E extends AuthWorkerEnv = AuthWorkerEnv> {\n fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise<Response>;\n /** The cron sweep: expired links, closed send windows and never-verified accounts. */\n scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise<void>;\n tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\nexport type AuthEvent =\n | {\n readonly type: 'request';\n readonly requestId: string;\n readonly route:\n | '/auth/account'\n | '/auth/jwks.json'\n | '/auth/sign-in'\n | '/auth/verify'\n | '/auth/session'\n | '/auth/sign-out-all'\n | '/auth/health'\n | 'unmatched';\n readonly status: number;\n readonly code: ArchiveErrorCode | 'ok';\n readonly ms: number;\n readonly diagnostic?: AuthDiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };\n\nconst ROUTES = {\n account: /^\\/api\\/v1\\/auth\\/account$/,\n health: /^\\/api\\/v1\\/auth\\/health$/,\n jwks: /^\\/api\\/v1\\/auth\\/jwks\\.json$/,\n session: /^\\/api\\/v1\\/auth\\/session$/,\n signIn: /^\\/api\\/v1\\/auth\\/sign-in$/,\n signOutAll: /^\\/api\\/v1\\/auth\\/sign-out-all$/,\n verify: /^\\/api\\/v1\\/auth\\/verify$/,\n} as const;\n\nconst ROUTE_METHODS: Record<keyof typeof ROUTES, readonly string[]> = {\n account: ['GET', 'DELETE'],\n health: ['GET'],\n jwks: ['GET'],\n session: ['GET'],\n signIn: ['POST'],\n signOutAll: ['POST'],\n verify: ['GET', 'POST'],\n};\n\nconst ROUTE_LABELS = {\n account: '/auth/account',\n health: '/auth/health',\n jwks: '/auth/jwks.json',\n session: '/auth/session',\n signIn: '/auth/sign-in',\n signOutAll: '/auth/sign-out-all',\n verify: '/auth/verify',\n} as const;\n\nconst matchRoute = (request: Request): keyof typeof ROUTES | undefined =>\n (Object.keys(ROUTES) as (keyof typeof ROUTES)[]).find((id) => ROUTES[id].test(new URL(request.url).pathname));\n\nconst TOKEN_BYTES = 32;\n// Base64url of TOKEN_BYTES random bytes, unpadded.\nconst LINK_TOKEN = /^[A-Za-z0-9_-]{43}$/;\nconst PUBLISHER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\nconst SUBJECT_MAX = 256;\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\nconst randomToken = (): string =>\n btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(TOKEN_BYTES))))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n\nconst unavailable = (): ApiError => new AuthOperationError(503, 'catalog');\n\n/** Exact allowed Origin plus the request marker, like the catalog's mutation guard. */\nfunction requireMutationGuard(request: Request, config: AuthConfig, allowPublicOrigin = false): string {\n const origin = request.headers.get('origin');\n if (origin === null || (!config.allowedOrigins.has(origin) && !(allowPublicOrigin && origin === config.publicOrigin)))\n throw forbidden();\n if (request.headers.get(REQUEST_MARKER_HEADER) !== '1') throw forbidden();\n return origin;\n}\n\nfunction corsHeaders(request: Request, config: AuthConfig): Record<string, string> {\n const origin = request.headers.get('origin');\n if (\n origin === null ||\n (!config.allowedOrigins.has(origin) && !(matchRoute(request) === 'verify' && origin === config.publicOrigin))\n )\n return {};\n return { 'access-control-allow-credentials': 'true', 'access-control-allow-origin': origin, vary: 'Origin' };\n}\n\nfunction preflight(request: Request, config: AuthConfig, methods: readonly string[]): Response {\n const cors = corsHeaders(request, config);\n if (Object.keys(cors).length === 0) throw forbidden();\n return new Response(null, {\n headers: {\n ...cors,\n 'access-control-allow-headers': `Content-Type, ${ACCOUNT_HEADER}, ${PUBLISHER_HEADER}, ${REQUEST_MARKER_HEADER}`,\n 'access-control-allow-methods': [...methods, 'OPTIONS'].join(', '),\n 'access-control-max-age': '600',\n },\n status: 204,\n });\n}\n\nconst secure = (config: AuthConfig): boolean => config.appEnv === 'production';\n\nasync function mintSession(\n config: AuthConfig,\n subject: string,\n operator: boolean,\n sessionVersion: number,\n): Promise<{ maxAge: number; token: string }> {\n const maxAge = operator ? OPERATOR_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;\n const token = await new SignJWT({ sessionVersion, ...(operator ? { roles: [ARCHIVE_OPERATOR_ROLE] } : {}) })\n .setProtectedHeader({ alg: 'RS256', kid: config.signing.kid, typ: 'JWT' })\n .setIssuedAt()\n .setJti(crypto.randomUUID())\n .setIssuer(config.issuer)\n .setAudience(config.audience)\n .setSubject(subject)\n .setExpirationTime(`${maxAge}s`)\n .sign(config.signing.key);\n return { maxAge, token };\n}\n\n/** Verifies the request's own session cookie against every published key; returns the token and subject. */\nasync function requireSession(\n request: Request,\n config: AuthConfig,\n env: AuthWorkerEnv,\n): Promise<{ subject: string; token: string; sessionVersion: number }> {\n const token = sessionToken(request);\n if (token === null) throw unauthenticated();\n try {\n const { payload } = await jwtVerify(token, config.verifier, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: SESSION_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n const subject = payload.sub ?? '';\n if (subject.length === 0 || subject.length > SUBJECT_MAX || !isWellFormedText(subject)) throw unauthenticated();\n const version = payload.sessionVersion;\n if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) throw unauthenticated();\n if (typeof payload.iat !== 'number' || payload.iat > Date.now() / 1000 + CLOCK_TOLERANCE_SECONDS)\n throw unauthenticated();\n if (\n Array.isArray(payload.roles) &&\n payload.roles.includes(ARCHIVE_OPERATOR_ROLE) &&\n (typeof payload.exp !== 'number' ||\n payload.exp - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS + CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n const account = await readAccount(env.AUTH_DB, subject);\n if (account === null || account.session_version !== version) throw unauthenticated();\n return { sessionVersion: version, subject, token };\n } catch (error) {\n if (error instanceof errors.JOSEError) throw unauthenticated();\n throw error;\n }\n}\n\nasync function sendLink(env: AuthWorkerEnv, config: AuthConfig, to: string, link: string): Promise<void> {\n const name = config.appName;\n const minutes = LINK_TTL_MS / 60_000;\n const notice = `The link works once and expires in ${minutes} minutes. If you did not request it, ignore this email.`;\n await env.AUTH_EMAIL.send({\n from: config.senderAddress,\n html: `<p>Open this link to sign in to ${escapeHtml(name)}:</p><p><a href=\"${escapeHtml(link)}\">${escapeHtml(link)}</a></p><p>${notice}</p>`,\n subject: `Your ${name} sign-in link`,\n text: `Open this link to sign in to ${name}:\\n\\n${link}\\n\\n${notice}`,\n to,\n });\n}\n\nasync function handleSignIn(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body)) throw badRequest();\n const keys = Object.keys(body);\n const requestedEmail: unknown = Reflect.get(body, 'email');\n if (keys.length !== 1 || keys[0] !== 'email' || typeof requestedEmail !== 'string') {\n throw badRequest();\n }\n const email = requestedEmail.trim().toLowerCase();\n if (!isWellFormedEmail(email)) {\n throw badRequest();\n }\n const nowMs = Date.now();\n const admissionRetry = await spendSignInBudget(env.AUTH_DB, nowMs);\n if (admissionRetry !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in requests. Try again later.', {\n retryAfterSeconds: admissionRetry,\n });\n }\n const user = await ensureUser(env.AUTH_DB, email);\n const retryAfterSeconds = await spendLinkBudget(env.AUTH_DB, user.id, nowMs);\n if (retryAfterSeconds !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in emails. Try again later.', { retryAfterSeconds });\n }\n const token = randomToken();\n const tokenHash = await sha256Hex(token);\n await insertToken(env.AUTH_DB, tokenHash, user.id, nowMs);\n try {\n await sendLink(env, config, user.email, `${config.publicOrigin}/api/v1/auth/verify?token=${token}`);\n } catch (error) {\n // An unsent link is useless; the spent allowance is deliberately not refunded.\n await deleteToken(env.AUTH_DB, tokenHash).catch(() => undefined);\n throw new AuthOperationError(503, 'email', { cause: error });\n }\n return json(202, { ok: true }, noStore);\n}\n\nasync function handleVerify(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n if (request.method === 'GET') {\n const token = queryParams(new URL(request.url), ['token']).get('token') ?? '';\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n return confirmationPage({ ...config, nonce: randomToken(), token });\n }\n requireMutationGuard(request, config, true);\n queryParams(new URL(request.url), []);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body) || Object.keys(body).length !== 1) {\n throw badRequest('The link is not valid.');\n }\n const token: unknown = Reflect.get(body, 'token');\n if (typeof token !== 'string') throw badRequest('The link is not valid.');\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const user = await consumeToken(env.AUTH_DB, await sha256Hex(token), Date.now());\n if (user === null) throw new ApiError(410, 'gone', 'That link has expired or was already used.');\n const session = await mintSession(config, user.id, config.operatorEmails.has(user.email), user.sessionVersion);\n return new Response(null, {\n headers: {\n ...noStore,\n 'set-cookie': sessionCookieValue(session.token, { maxAgeSeconds: session.maxAge, secure: secure(config) }),\n },\n status: 204,\n });\n}\n\n/** The issuer's half of a complete export: the account record only, never tokens or budget state. */\nasync function handleExport(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const { subject } = await requireSession(request, config, env);\n const row = await readAccount(env.AUTH_DB, subject);\n if (row === null) throw unauthenticated();\n return json(\n 200,\n { createdAt: row.created_at, email: row.email, id: row.id, lastSignInAt: row.last_sign_in_at },\n noStore,\n );\n}\n\n/** Re-serializes a failed catalog response into the public envelope; anything unrecognizable is 503. */\nasync function archiveFailure(response: Response): Promise<Response> {\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n const envelope = archiveErrorResponseSchema.safeParse(parsed);\n if (!envelope.success || response.status < 400 || response.status > 599) throw unavailable();\n const { code, message, requestId } = envelope.data.error;\n const retryAfter = response.headers.get('retry-after');\n return json(\n response.status,\n { error: { code, message, requestId } },\n retryAfter !== null && /^\\d{1,6}$/.test(retryAfter) ? { ...noStore, 'retry-after': retryAfter } : noStore,\n );\n}\n\n/**\n * Full erasure: catalog session revocation and profile erasure first (through the AUTH_ARCHIVE service binding,\n * presenting the caller's own session), then the issuer's account row, and only then\n * the cookie is expired. Any failure keeps the browser session so the same request\n * can be retried; both halves are idempotent, so a retry after a partial success\n * re-runs the catalog no-op and finishes the issuer side. The account precondition\n * pins every attempt to the confirmed issuer subject, even without a publisher.\n */\nasync function handleErase(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const origin = requireMutationGuard(request, config);\n const { subject, token } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject) {\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n }\n const headers = new Headers({\n cookie: `${SESSION_COOKIE_NAME}=${token}`,\n origin,\n [REQUEST_MARKER_HEADER]: '1',\n });\n const publisher = request.headers.get(PUBLISHER_HEADER);\n if (publisher !== null) {\n if (!PUBLISHER_ID.test(publisher)) throw badRequest();\n headers.set(PUBLISHER_HEADER, publisher);\n }\n let archived: Response;\n try {\n archived = await serviceRequest(\n env.AUTH_ARCHIVE,\n new Request(`${config.publicOrigin}/api/v1/me/account`, { headers, method: 'DELETE', redirect: 'manual' }),\n async (response) => {\n if (response.status === 204) {\n await response.body?.cancel();\n return response;\n }\n return archiveFailure(response);\n },\n );\n } catch (error) {\n throw new AuthOperationError(503, 'catalog', { cause: error });\n }\n if (archived.status !== 204) return archived;\n await deleteAccount(env.AUTH_DB, subject);\n return new Response(null, {\n headers: { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) },\n status: 204,\n });\n}\n\nasync function handleSignOutAll(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const { subject, sessionVersion } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject)\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n if ((await revokeSessions(env.AUTH_DB, subject, sessionVersion)) === null) throw unauthenticated();\n return json(200, { ok: true }, { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) });\n}\n\nasync function handleHealth(env: AuthWorkerEnv): Promise<Response> {\n let reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null = null;\n let dbSchemaVersion: number | null = null;\n try {\n await authConfig(env);\n } catch {\n reason = 'configuration-unavailable';\n }\n if (reason === null) {\n try {\n dbSchemaVersion = await authReadiness(env.AUTH_DB);\n if (dbSchemaVersion !== AUTH_SCHEMA_VERSION) reason = 'schema-mismatch';\n } catch {\n reason = 'database-unavailable';\n }\n }\n return json(\n reason === null ? 200 : 503,\n archiveAuthHealthSchema.parse({\n dbSchemaVersion,\n reason,\n schemaVersion: AUTH_SCHEMA_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n }),\n noStore,\n );\n}\nasync function route(request: Request, env: AuthWorkerEnv): Promise<Response> {\n const method = request.method.toUpperCase();\n const routeId = matchRoute(request);\n if (routeId === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n if (routeId === 'health' && method === 'GET') return handleHealth(env);\n const config = await authConfig(env);\n const cors = corsHeaders(request, config);\n if (method === 'OPTIONS') return preflight(request, config, ROUTE_METHODS[routeId]);\n if (!ROUTE_METHODS[routeId].includes(method)) {\n throw new ApiError(405, 'method-not-allowed', 'That method is not allowed here.');\n }\n let response: Response;\n switch (routeId) {\n case 'health':\n response = await handleHealth(env);\n break;\n case 'session':\n await requireSession(request, config, env);\n response = json(200, { ok: true }, noStore);\n break;\n case 'signOutAll':\n response = await handleSignOutAll(request, env, config);\n break;\n case 'jwks':\n response = json(200, { keys: config.jwks }, noStore);\n break;\n case 'signIn':\n response = await handleSignIn(request, env, config);\n break;\n case 'verify':\n response = await handleVerify(request, env, config);\n break;\n case 'account':\n response = method === 'GET' ? await handleExport(request, env, config) : await handleErase(request, env, config);\n break;\n }\n return withCors(response, cors);\n}\n\nfunction withCors(response: Response, cors: Record<string, string>): Response {\n if (Object.keys(cors).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(cors)) next.headers.set(key, value);\n return next;\n}\n\nconst diagnostic = (error: unknown): AuthDiagnostic =>\n error instanceof AuthOperationError ? error.authDiagnostic : { kind: 'internal' };\n\n/**\n * Builds the reference identity issuer. Route behavior is fixed; every deployment\n * difference is validated configuration — the app name, trusted origins, audience and\n * issuer claims, signing keys, operator roster, the catalog service binding, and\n * where links and redirects land.\n */\nexport function createAuthWorker<E extends AuthWorkerEnv>(): AuthWorkerHandler<E> {\n const tappers = new Set<(event: AuthEvent) => void>();\n const emit = (event: AuthEvent): void => {\n for (const handler of tappers) {\n try {\n handler(event);\n } catch {\n // Observers never change request or scheduled outcomes.\n }\n }\n };\n return {\n async fetch(request: Request, env: E): Promise<Response> {\n const requestId = crypto.randomUUID();\n const started = Date.now();\n let response: Response;\n let failure: unknown;\n try {\n response = await route(request, env);\n } catch (error) {\n failure = error;\n response = toErrorResponse(error, requestId);\n // Failures stay readable by allowed origins; config faults have no trusted origin set.\n const config = await authConfig(env).catch(() => null);\n if (config !== null) {\n if (\n matchRoute(request) === 'verify' &&\n request.method === 'GET' &&\n error instanceof ApiError &&\n error.status === 400\n )\n response = confirmationPage({ ...config, nonce: randomToken(), token: null });\n response = withCors(response, corsHeaders(request, config));\n }\n }\n if (matchRoute(request) === 'verify') {\n response.headers.set('cache-control', 'no-store');\n response.headers.set('referrer-policy', 'no-referrer');\n response.headers.set('x-content-type-options', 'nosniff');\n if (!response.headers.has('content-security-policy')) {\n response.headers.set(\n 'content-security-policy',\n \"default-src 'none'; base-uri 'none'; frame-ancestors 'none'\",\n );\n }\n }\n if (tappers.size > 0) {\n const routeId = matchRoute(request);\n const code =\n response.status < 400\n ? 'ok'\n : routeId === 'health'\n ? 'unavailable'\n : failure instanceof ApiError\n ? failure.code\n : 'internal';\n // Account erasure may forward a catalog error response rather than throw.\n const forwardedCode =\n failure === undefined && response.status >= 400\n ? archiveErrorResponseSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n const health =\n routeId === 'health' && response.status === 503\n ? archiveAuthHealthSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n emit({\n code: forwardedCode?.success ? forwardedCode.data.error.code : code,\n ms: Math.max(0, Date.now() - started),\n requestId,\n route: routeId === undefined ? 'unmatched' : ROUTE_LABELS[routeId],\n status: response.status,\n type: 'request',\n ...(response.status >= 500\n ? {\n diagnostic: health?.success\n ? {\n kind:\n health.data.reason === 'configuration-unavailable'\n ? 'configuration'\n : health.data.reason === 'schema-mismatch'\n ? 'schema'\n : 'database',\n }\n : failure === undefined\n ? { kind: 'catalog' as const }\n : diagnostic(failure),\n }\n : {}),\n });\n }\n return response;\n },\n\n async scheduled(_controller: unknown, env: E): Promise<void> {\n try {\n await authConfig(env);\n const removed = await sweep(env.AUTH_DB, Date.now());\n if (tappers.size > 0) emit({ removed, status: 'ok', type: 'scheduled' });\n } catch (error) {\n if (tappers.size > 0) emit({ diagnostic: diagnostic(error), status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n tap(handler, options) {\n if (options?.signal?.aborted) return () => undefined;\n const observer = (event: AuthEvent): void => handler(event);\n const unsubscribe = (): void => {\n tappers.delete(observer);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n tappers.add(observer);\n options?.signal?.addEventListener('abort', unsubscribe, { once: true });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
@@ -2,12 +2,12 @@ import { sha256Hex as e } from "../_crypto.js";
2
2
  import { ACCOUNT_HEADER as t, ARCHIVE_OPERATOR_ROLE as n, OPERATOR_SESSION_MAX_AGE_SECONDS as r, PUBLISHER_HEADER as i, REQUEST_MARKER_HEADER as a, SESSION_COOKIE_NAME as o, SESSION_MAX_AGE_SECONDS as s, archiveAuthHealthSchema as c, archiveErrorResponseSchema as l, isWellFormedText as u, sessionCookieValue as d, uuidSchema as f } from "../contract.js";
3
3
  import { serviceRequest as p } from "../worker/_service.js";
4
4
  import { ApiError as m, badRequest as h, expiredSessionCookie as g, forbidden as _, json as v, noStore as y, queryParams as b, readJsonBody as x, sessionToken as S, toErrorResponse as C, unauthenticated as w } from "../worker/http.js";
5
- import { AuthOperationError as T } from "./errors.js";
6
- import { LINK_TTL_MS as E, authReadiness as D, consumeToken as O, deleteAccount as k, deleteToken as A, ensureUser as j, insertToken as M, readAccount as N, revokeSessions as P, spendLinkBudget as F, spendSignInBudget as I, sweep as L } from "./store.js";
7
- import { confirmationPage as R, escapeHtml as z } from "./_confirmation.js";
8
- import { authConfig as B, isWellFormedEmail as V } from "./config.js";
5
+ import { confirmationPage as T, escapeHtml as E } from "./_confirmation.js";
6
+ import { authConfig as D, isWellFormedEmail as O } from "./config.js";
7
+ import { AuthOperationError as k } from "./errors.js";
8
+ import { LINK_TTL_MS as A, authReadiness as j, consumeToken as M, deleteAccount as N, deleteToken as P, ensureUser as F, insertToken as I, readAccount as L, revokeSessions as R, spendLinkBudget as z, spendSignInBudget as B, sweep as V } from "./store.js";
9
9
  import { SignJWT as H, errors as U, jwtVerify as W } from "jose";
10
- //#region src/auth/worker.ts
10
+ // src/auth/worker.ts
11
11
  var G = {
12
12
  account: /^\/api\/v1\/auth\/account$/,
13
13
  health: /^\/api\/v1\/auth\/health$/,
@@ -32,7 +32,7 @@ var G = {
32
32
  signIn: "/auth/sign-in",
33
33
  signOutAll: "/auth/sign-out-all",
34
34
  verify: "/auth/verify"
35
- }, matchRoute = (e) => Object.keys(G).find((t) => G[t].test(new URL(e.url).pathname)), J = 32, Y = /^[A-Za-z0-9_-]{43}$/, X = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, Z = 256, Q = 5, randomToken = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(J)))).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""), unavailable = () => new T(503, "catalog");
35
+ }, matchRoute = (e) => Object.keys(G).find((t) => G[t].test(new URL(e.url).pathname)), J = 32, Y = /^[A-Za-z0-9_-]{43}$/, X = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, Z = 256, Q = 5, randomToken = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(J)))).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""), unavailable = () => new k(503, "catalog");
36
36
  function requireMutationGuard(e, t, n = !1) {
37
37
  let r = e.headers.get("origin");
38
38
  if (r === null || !t.allowedOrigins.has(r) && !(n && r === t.publicOrigin) || e.headers.get("x-archive-request") !== "1") throw _();
@@ -96,7 +96,7 @@ async function requireSession(e, t, n) {
96
96
  if (i.length === 0 || i.length > Z || !u(i)) throw w();
97
97
  let a = e.sessionVersion;
98
98
  if (typeof a != "number" || !Number.isSafeInteger(a) || a < 0 || typeof e.iat != "number" || e.iat > Date.now() / 1e3 + Q || Array.isArray(e.roles) && e.roles.includes("archive-operator") && (typeof e.exp != "number" || e.exp - e.iat > 3600 || Date.now() / 1e3 - e.iat > 3600 + Q)) throw w();
99
- let o = await N(n.AUTH_DB, i);
99
+ let o = await L(n.AUTH_DB, i);
100
100
  if (o === null || o.session_version !== a) throw w();
101
101
  return {
102
102
  sessionVersion: a,
@@ -108,10 +108,10 @@ async function requireSession(e, t, n) {
108
108
  }
109
109
  }
110
110
  async function sendLink(e, t, n, r) {
111
- let i = t.appName, a = `The link works once and expires in ${E / 6e4} minutes. If you did not request it, ignore this email.`;
111
+ let i = t.appName, a = `The link works once and expires in ${A / 6e4} minutes. If you did not request it, ignore this email.`;
112
112
  await e.AUTH_EMAIL.send({
113
113
  from: t.senderAddress,
114
- html: `<p>Open this link to sign in to ${z(i)}:</p><p><a href="${z(r)}">${z(r)}</a></p><p>${a}</p>`,
114
+ html: `<p>Open this link to sign in to ${E(i)}:</p><p><a href="${E(r)}">${E(r)}</a></p><p>${a}</p>`,
115
115
  subject: `Your ${i} sign-in link`,
116
116
  text: `Open this link to sign in to ${i}:\n\n${r}\n\n${a}`,
117
117
  to: n
@@ -124,17 +124,17 @@ async function handleSignIn(t, n, r) {
124
124
  let a = Object.keys(i), o = Reflect.get(i, "email");
125
125
  if (a.length !== 1 || a[0] !== "email" || typeof o != "string") throw h();
126
126
  let s = o.trim().toLowerCase();
127
- if (!V(s)) throw h();
128
- let c = Date.now(), l = await I(n.AUTH_DB, c);
127
+ if (!O(s)) throw h();
128
+ let c = Date.now(), l = await B(n.AUTH_DB, c);
129
129
  if (l !== null) throw new m(429, "rate-limited", "Too many sign-in requests. Try again later.", { retryAfterSeconds: l });
130
- let u = await j(n.AUTH_DB, s), d = await F(n.AUTH_DB, u.id, c);
130
+ let u = await F(n.AUTH_DB, s), d = await z(n.AUTH_DB, u.id, c);
131
131
  if (d !== null) throw new m(429, "rate-limited", "Too many sign-in emails. Try again later.", { retryAfterSeconds: d });
132
132
  let f = randomToken(), p = await e(f);
133
- await M(n.AUTH_DB, p, u.id, c);
133
+ await I(n.AUTH_DB, p, u.id, c);
134
134
  try {
135
135
  await sendLink(n, r, u.email, `${r.publicOrigin}/api/v1/auth/verify?token=${f}`);
136
136
  } catch (e) {
137
- throw await A(n.AUTH_DB, p).catch(() => void 0), new T(503, "email", { cause: e });
137
+ throw await P(n.AUTH_DB, p).catch(() => void 0), new k(503, "email", { cause: e });
138
138
  }
139
139
  return v(202, { ok: !0 }, y);
140
140
  }
@@ -142,7 +142,7 @@ async function handleVerify(t, n, r) {
142
142
  if (t.method === "GET") {
143
143
  let e = b(new URL(t.url), ["token"]).get("token") ?? "";
144
144
  if (!Y.test(e)) throw h("The link is not valid.");
145
- return R({
145
+ return T({
146
146
  ...r,
147
147
  nonce: randomToken(),
148
148
  token: e
@@ -153,7 +153,7 @@ async function handleVerify(t, n, r) {
153
153
  if (typeof i != "object" || !i || Array.isArray(i) || Object.keys(i).length !== 1) throw h("The link is not valid.");
154
154
  let a = Reflect.get(i, "token");
155
155
  if (typeof a != "string" || !Y.test(a)) throw h("The link is not valid.");
156
- let o = await O(n.AUTH_DB, await e(a), Date.now());
156
+ let o = await M(n.AUTH_DB, await e(a), Date.now());
157
157
  if (o === null) throw new m(410, "gone", "That link has expired or was already used.");
158
158
  let s = await mintSession(r, o.id, r.operatorEmails.has(o.email), o.sessionVersion);
159
159
  return new Response(null, {
@@ -168,7 +168,7 @@ async function handleVerify(t, n, r) {
168
168
  });
169
169
  }
170
170
  async function handleExport(e, t, n) {
171
- let { subject: r } = await requireSession(e, n, t), i = await N(t.AUTH_DB, r);
171
+ let { subject: r } = await requireSession(e, n, t), i = await L(t.AUTH_DB, r);
172
172
  if (i === null) throw w();
173
173
  return v(200, {
174
174
  createdAt: i.created_at,
@@ -217,9 +217,9 @@ async function handleErase(e, n, r) {
217
217
  redirect: "manual"
218
218
  }), async (e) => e.status === 204 ? (await e.body?.cancel(), e) : archiveFailure(e));
219
219
  } catch (e) {
220
- throw new T(503, "catalog", { cause: e });
220
+ throw new k(503, "catalog", { cause: e });
221
221
  }
222
- return v.status === 204 ? (await k(n.AUTH_DB, c), new Response(null, {
222
+ return v.status === 204 ? (await N(n.AUTH_DB, c), new Response(null, {
223
223
  headers: {
224
224
  ...y,
225
225
  "set-cookie": g(secure(r))
@@ -232,7 +232,7 @@ async function handleSignOutAll(e, n, r) {
232
232
  let { subject: i, sessionVersion: a } = await requireSession(e, r, n), o = e.headers.get(t);
233
233
  if (o === null || !f.safeParse(o).success) throw h();
234
234
  if (o !== i) throw new m(409, "account-changed", "The signed-in account changed since this request was prepared.");
235
- if (await P(n.AUTH_DB, i, a) === null) throw w();
235
+ if (await R(n.AUTH_DB, i, a) === null) throw w();
236
236
  return v(200, { ok: !0 }, {
237
237
  ...y,
238
238
  "set-cookie": g(secure(r))
@@ -241,12 +241,12 @@ async function handleSignOutAll(e, n, r) {
241
241
  async function handleHealth(e) {
242
242
  let t = null, n = null;
243
243
  try {
244
- await B(e);
244
+ await D(e);
245
245
  } catch {
246
246
  t = "configuration-unavailable";
247
247
  }
248
248
  if (t === null) try {
249
- n = await D(e.AUTH_DB), n !== 1 && (t = "schema-mismatch");
249
+ n = await j(e.AUTH_DB), n !== 1 && (t = "schema-mismatch");
250
250
  } catch {
251
251
  t = "database-unavailable";
252
252
  }
@@ -261,7 +261,7 @@ async function route(e, t) {
261
261
  let n = e.method.toUpperCase(), r = matchRoute(e);
262
262
  if (r === void 0) throw new m(404, "not-found", "No such route.");
263
263
  if (r === "health" && n === "GET") return handleHealth(t);
264
- let i = await B(t), a = corsHeaders(e, i);
264
+ let i = await D(t), a = corsHeaders(e, i);
265
265
  if (n === "OPTIONS") return preflight(e, i, K[r]);
266
266
  if (!K[r].includes(n)) throw new m(405, "method-not-allowed", "That method is not allowed here.");
267
267
  let o;
@@ -294,7 +294,7 @@ function withCors(e, t) {
294
294
  for (let [e, r] of Object.entries(t)) n.headers.set(e, r);
295
295
  return n;
296
296
  }
297
- var diagnostic = (e) => e instanceof T ? e.authDiagnostic : { kind: "internal" };
297
+ var diagnostic = (e) => e instanceof k ? e.authDiagnostic : { kind: "internal" };
298
298
  function createAuthWorker() {
299
299
  let e = /* @__PURE__ */ new Set(), emit = (t) => {
300
300
  for (let n of e) try {
@@ -308,8 +308,8 @@ function createAuthWorker() {
308
308
  a = await route(t, n);
309
309
  } catch (e) {
310
310
  o = e, a = C(e, r);
311
- let i = await B(n).catch(() => null);
312
- i !== null && (matchRoute(t) === "verify" && t.method === "GET" && e instanceof m && e.status === 400 && (a = R({
311
+ let i = await D(n).catch(() => null);
312
+ i !== null && (matchRoute(t) === "verify" && t.method === "GET" && e instanceof m && e.status === 400 && (a = T({
313
313
  ...i,
314
314
  nonce: randomToken(),
315
315
  token: null
@@ -331,8 +331,8 @@ function createAuthWorker() {
331
331
  },
332
332
  async scheduled(t, n) {
333
333
  try {
334
- await B(n);
335
- let t = await L(n.AUTH_DB, Date.now());
334
+ await D(n);
335
+ let t = await V(n.AUTH_DB, Date.now());
336
336
  e.size > 0 && emit({
337
337
  removed: t,
338
338
  status: "ok",
@@ -355,7 +355,6 @@ function createAuthWorker() {
355
355
  }
356
356
  };
357
357
  }
358
- //#endregion
359
358
  export { createAuthWorker };
360
359
 
361
360
  //# sourceMappingURL=worker.js.map
@@ -1,2 +1,2 @@
1
- const e=require("./_crypto.cjs"),t=require("./contract.cjs"),n=require("./_json.cjs"),r=require("./errors.cjs");let i=require("@vielzeug/spell");var envelopeShape=(e,n)=>({author:t.archiveAuthorSchema,contentHash:t.hex64Schema,contentVersion:i.s.literal(n),id:t.uuidSchema,likeCount:i.s.number().int().min(0).max(2**53-1),payload:e,publishedAt:t.isoTimestampSchema,revision:i.s.number().int().min(1).max(2**53-1),schemaVersion:i.s.literal(1),updatedAt:t.isoTimestampSchema});function defineArchiveCollection(a){let{canonicalize:o,contentVersion:s,index:c,name:l,payloadSchema:u}=a;if(typeof c!=`function`||!u||typeof u.safeParse!=`function`||o!==void 0&&typeof o!=`function`)throw new r.ArchiveDefinitionError(`Collection requires a payload schema and synchronous indexing/canonicalization functions.`);if(a.filters!==void 0&&!Array.isArray(a.filters)||a.tags!==void 0&&!Array.isArray(a.tags))throw new r.ArchiveDefinitionError(`Collection vocabularies must be arrays.`);let d=Object.freeze([...a.filters??[]]),f=Object.freeze([...a.tags??[]]);if(typeof l!=`string`||!t.ARCHIVE_COLLECTION_NAME_PATTERN.test(l))throw new r.ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(l)}`);if(!Number.isSafeInteger(s)||s<1)throw new r.ArchiveDefinitionError(`Archive collection ${l}: contentVersion must be a safe integer >= 1.`);let validateVocabulary=(e,n)=>{for(let i of n)if(typeof i!=`string`||i.length===0||i.length>t.ARCHIVE_LIMITS.vocabChars||!t.isWellFormedText(i))throw new r.ArchiveDefinitionError(`Archive collection ${l}: every ${e} value must be well-formed text of 1..${t.ARCHIVE_LIMITS.vocabChars} chars.`)};validateVocabulary(`filters`,d),validateVocabulary(`tags`,f);let p=new Set(d),m=new Set(f),derive=async i=>{let a=`schema`;try{if(n.canonicalObjectJson(i)===null)return null;let l=u.safeParse(i);if(!l.success)return null;let d=n.canonicalObjectJson(l.data,!0,i);if(d===null)throw new r.ArchiveDerivationError(a,`Payload schema produced invalid or lossy JSON.`);a=`canonicalize`;let f=o===void 0?l.data:o(l.data),h=n.canonicalObjectJson(f,!0,JSON.parse(d));if(h===null)throw new r.ArchiveDerivationError(a,`Canonicalization produced invalid JSON.`);a=`schema`;let g=u.safeParse(JSON.parse(h));if(!g.success)throw new r.ArchiveDerivationError(o===void 0?`schema`:`canonicalize`,`Canonical payload is outside the payload schema.`,{cause:g.error});let _=n.canonicalObjectJson(g.data,!0,JSON.parse(h));if(_===null||_!==h)throw new r.ArchiveDerivationError(a,`Payload schema is not stable on canonical JSON.`);if(o!==void 0){let e=u.parse(JSON.parse(_));a=`canonicalize`;let t=o(e);if(n.canonicalObjectJson(t,!0)!==_)throw new r.ArchiveDerivationError(a,`Canonicalization is not idempotent.`)}a=`index`;let{filter:v,tags:y,title:b}=c(g.data);if(n.canonicalObjectJson(g.data,!0)!==_)throw new r.ArchiveDerivationError(a,`Indexing mutated the canonical payload.`);if(typeof b!=`string`||!Array.isArray(y))throw new r.ArchiveDerivationError(a,`Indexing must return a string title and a tags array.`);let x=b.trim();if(x.length===0||x.length>t.ARCHIVE_LIMITS.titleChars||!t.isWellFormedText(x))return null;if(v!==null&&(typeof v!=`string`||!t.isWellFormedText(v)))throw new r.ArchiveDerivationError(a,`Indexing returned an invalid filter.`);let S=[];for(let e of y){if(typeof e!=`string`||!t.isWellFormedText(e))throw new r.ArchiveDerivationError(a,`Indexing returned an invalid tag.`);S.includes(e)||S.push(e)}if(v!==null&&!p.has(v)||S.some(e=>!m.has(e)))throw new r.ArchiveDerivationError(a,`Indexing returned a value outside the declared vocabularies.`);return S.length>t.ARCHIVE_LIMITS.tagsPerEntry||new TextEncoder().encode(_).byteLength>t.ARCHIVE_LIMITS.payloadBytes?null:(a=`hash`,{canonicalJson:_,contentHash:await e.sha256Hex(`{"contentVersion":${s},"payload":${_}}`),contentVersion:s,filter:v,tags:S,title:x})}catch(e){throw e instanceof r.ArchiveDerivationError?e:new r.ArchiveDerivationError(a,`Collection content derivation failed.`,{cause:e})}},h=i.s.object(envelopeShape(u,s)),g=i.s.object({...envelopeShape(u,s),clientKey:t.uuidSchema,hidden:i.s.boolean()}),_={entry:h,ownedEntries:i.s.object({entries:i.s.array(g)}),ownedEntry:g,page:i.s.object({cursor:i.s.string().nullable(),entries:i.s.array(h)}),publishInput:i.s.object({clientKey:t.uuidSchema,payload:u}),updateInput:i.s.object({expectedRevision:i.s.number().int().min(1).max(2**53-1),payload:u})};return Object.freeze({contentVersion:s,contract:Object.freeze(_),derive,filters:d,name:l,tags:f})}exports.defineArchiveCollection=defineArchiveCollection;
1
+ const e=require("./_crypto.cjs"),t=require("./_json.cjs"),n=require("./contract.cjs"),r=require("./errors.cjs");let i=require("@vielzeug/spell");var envelopeShape=(e,t)=>({author:n.archiveAuthorSchema,contentHash:n.hex64Schema,contentVersion:i.s.literal(t),id:n.uuidSchema,likeCount:i.s.number().int().min(0).max(2**53-1),payload:e,publishedAt:n.isoTimestampSchema,revision:i.s.number().int().min(1).max(2**53-1),schemaVersion:i.s.literal(1),updatedAt:n.isoTimestampSchema});function defineArchiveCollection(a){let{canonicalize:o,contentVersion:s,index:c,name:l,payloadSchema:u}=a;if(typeof c!=`function`||!u||typeof u.safeParse!=`function`||o!==void 0&&typeof o!=`function`)throw new r.ArchiveDefinitionError(`Collection requires a payload schema and synchronous indexing/canonicalization functions.`);if(a.filters!==void 0&&!Array.isArray(a.filters)||a.tags!==void 0&&!Array.isArray(a.tags))throw new r.ArchiveDefinitionError(`Collection vocabularies must be arrays.`);let d=Object.freeze([...a.filters??[]]),f=Object.freeze([...a.tags??[]]);if(typeof l!=`string`||!n.ARCHIVE_COLLECTION_NAME_PATTERN.test(l))throw new r.ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(l)}`);if(!Number.isSafeInteger(s)||s<1)throw new r.ArchiveDefinitionError(`Archive collection ${l}: contentVersion must be a safe integer >= 1.`);let validateVocabulary=(e,t)=>{for(let i of t)if(typeof i!=`string`||i.length===0||i.length>n.ARCHIVE_LIMITS.vocabChars||!n.isWellFormedText(i))throw new r.ArchiveDefinitionError(`Archive collection ${l}: every ${e} value must be well-formed text of 1..${n.ARCHIVE_LIMITS.vocabChars} chars.`)};validateVocabulary(`filters`,d),validateVocabulary(`tags`,f);let p=new Set(d),m=new Set(f),derive=async i=>{let a=`schema`;try{if(t.canonicalObjectJson(i)===null)return null;let l=u.safeParse(i);if(!l.success)return null;let d=t.canonicalObjectJson(l.data,!0,i);if(d===null)throw new r.ArchiveDerivationError(a,`Payload schema produced invalid or lossy JSON.`);a=`canonicalize`;let f=o===void 0?l.data:o(l.data),h=t.canonicalObjectJson(f,!0,JSON.parse(d));if(h===null)throw new r.ArchiveDerivationError(a,`Canonicalization produced invalid JSON.`);a=`schema`;let g=u.safeParse(JSON.parse(h));if(!g.success)throw new r.ArchiveDerivationError(o===void 0?`schema`:`canonicalize`,`Canonical payload is outside the payload schema.`,{cause:g.error});let _=t.canonicalObjectJson(g.data,!0,JSON.parse(h));if(_===null||_!==h)throw new r.ArchiveDerivationError(a,`Payload schema is not stable on canonical JSON.`);if(o!==void 0){let e=u.parse(JSON.parse(_));a=`canonicalize`;let n=o(e);if(t.canonicalObjectJson(n,!0)!==_)throw new r.ArchiveDerivationError(a,`Canonicalization is not idempotent.`)}a=`index`;let{filter:v,tags:y,title:b}=c(g.data);if(t.canonicalObjectJson(g.data,!0)!==_)throw new r.ArchiveDerivationError(a,`Indexing mutated the canonical payload.`);if(typeof b!=`string`||!Array.isArray(y))throw new r.ArchiveDerivationError(a,`Indexing must return a string title and a tags array.`);let x=b.trim();if(x.length===0||x.length>n.ARCHIVE_LIMITS.titleChars||!n.isWellFormedText(x))return null;if(v!==null&&(typeof v!=`string`||!n.isWellFormedText(v)))throw new r.ArchiveDerivationError(a,`Indexing returned an invalid filter.`);let S=[];for(let e of y){if(typeof e!=`string`||!n.isWellFormedText(e))throw new r.ArchiveDerivationError(a,`Indexing returned an invalid tag.`);S.includes(e)||S.push(e)}if(v!==null&&!p.has(v)||S.some(e=>!m.has(e)))throw new r.ArchiveDerivationError(a,`Indexing returned a value outside the declared vocabularies.`);return S.length>n.ARCHIVE_LIMITS.tagsPerEntry||new TextEncoder().encode(_).byteLength>n.ARCHIVE_LIMITS.payloadBytes?null:(a=`hash`,{canonicalJson:_,contentHash:await e.sha256Hex(`{"contentVersion":${s},"payload":${_}}`),contentVersion:s,filter:v,tags:S,title:x})}catch(e){throw e instanceof r.ArchiveDerivationError?e:new r.ArchiveDerivationError(a,`Collection content derivation failed.`,{cause:e})}},h=i.s.object(envelopeShape(u,s)),g=i.s.object({...envelopeShape(u,s),clientKey:n.uuidSchema,hidden:i.s.boolean()}),_={entry:h,ownedEntries:i.s.object({entries:i.s.array(g)}),ownedEntry:g,page:i.s.object({cursor:i.s.string().nullable(),entries:i.s.array(h)}),publishInput:i.s.object({clientKey:n.uuidSchema,payload:u}),updateInput:i.s.object({expectedRevision:i.s.number().int().min(1).max(2**53-1),payload:u})};return Object.freeze({contentVersion:s,contract:Object.freeze(_),derive,filters:d,name:l,tags:f})}exports.defineArchiveCollection=defineArchiveCollection;
2
2
  //# sourceMappingURL=collection.cjs.map