@vielzeug/archive 26.10.9 → 26.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,14 @@ sessions. `GET /api/v1/auth/health` exposes the typed, read-only
62
62
  `archiveAuthHealthSchema` readiness response. Issuer admission is durable D1
63
63
  state: 20 requests per minute, plus three sends per email per hour; no issuer
64
64
  rate-limiter binding is required. Magic-link GETs show a confirmation page; only
65
- a deliberate guarded POST redeems the token.
65
+ a deliberate guarded POST redeems the token. Confirmation uses escaped `APP_NAME`
66
+ branding, nonce-protected inline styles/script and no external assets. Recovery
67
+ links to the fixed `PUBLIC_ORIGIN/account` sign-in entry; successful sign-in
68
+ navigates to `PUBLIC_ORIGIN/signed-in`. Your application must serve both pages.
69
+ The initial “Back to app” exit links to `PUBLIC_ORIGIN/`. The issuer
70
+ renders only the confirmation endpoint. Expired links require a new link, while
71
+ temporary failures offer manual retry. With valid issuer configuration, malformed
72
+ GET links return HTML 400 recovery; POST errors retain their JSON contract.
66
73
 
67
74
  Full account deletion revokes old issuer/subject sessions; profile-only deletion
68
75
  does not. Re-enrollment uses a fresh issuer subject. Both Worker factories expose
@@ -0,0 +1,180 @@
1
+ const e=require("../contract.cjs"),t=require("../worker/http.cjs"),n=require("./store.cjs");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`)}`);function confirmationPage({appName:r,nonce:i,publicOrigin:a,token:o}){let s=o!==null,c=s?`Confirm sign-in`:`This sign-in link is not valid`,l=s?`
2
+ const button = document.getElementById('confirm');
3
+ const status = document.getElementById('status');
4
+ const recovery = document.getElementById('recovery');
5
+ let pending = false;
6
+ let terminal = false;
7
+ let disposed = false;
8
+ let cooldown;
9
+ let timeout;
10
+ let controller;
11
+ let retryAt = 0;
12
+ const announce = (message) => { status.textContent = message; };
13
+ const ready = () => {
14
+ button.disabled = false;
15
+ button.textContent = 'Try again';
16
+ };
17
+ const stop = () => {
18
+ clearTimeout(cooldown);
19
+ clearTimeout(timeout);
20
+ controller?.abort();
21
+ controller = undefined;
22
+ };
23
+ const tick = () => {
24
+ if (disposed) return;
25
+ const seconds = Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));
26
+ if (seconds === 0) {
27
+ ready();
28
+ announce('You can try signing in again now.');
29
+ return;
30
+ }
31
+ button.textContent = 'Try again in ' + seconds + 's';
32
+ cooldown = setTimeout(tick, 1000);
33
+ };
34
+ const recover = (message) => {
35
+ announce(message);
36
+ recovery.hidden = false;
37
+ document.getElementById('exit').hidden = true;
38
+ };
39
+ button.hidden = false;
40
+ button.focus();
41
+ addEventListener('pagehide', () => {
42
+ disposed = true;
43
+ stop();
44
+ });
45
+ addEventListener('pageshow', (event) => {
46
+ if (!event.persisted || !disposed) return;
47
+ disposed = false;
48
+ pending = false;
49
+ document.getElementById('actions').removeAttribute('aria-busy');
50
+ if (terminal) return;
51
+ if (retryAt > Date.now()) tick();
52
+ else {
53
+ ready();
54
+ announce('Confirm sign-in when you are ready.');
55
+ }
56
+ });
57
+ button.addEventListener('click', async () => {
58
+ if (disposed || pending || terminal || retryAt > Date.now()) return;
59
+ pending = true;
60
+ button.disabled = true;
61
+ button.textContent = 'Signing you in';
62
+ document.getElementById('actions').setAttribute('aria-busy', 'true');
63
+ announce('Signing you in. Please wait.');
64
+ const active = new AbortController();
65
+ controller = active;
66
+ let timedOut = false;
67
+ const requestTimeout = setTimeout(() => {
68
+ timedOut = true;
69
+ active.abort();
70
+ }, 15000);
71
+ timeout = requestTimeout;
72
+ try {
73
+ const response = await fetch('/api/v1/auth/verify', {
74
+ method: 'POST',
75
+ credentials: 'same-origin',
76
+ headers: { 'content-type': 'application/json', '${e.REQUEST_MARKER_HEADER}': '1' },
77
+ body: JSON.stringify({ token: ${scriptValue(o??``)} }),
78
+ signal: active.signal,
79
+ redirect: 'error',
80
+ referrerPolicy: 'no-referrer'
81
+ });
82
+ void response.body?.cancel().catch(() => {});
83
+ if (disposed || controller !== active) return;
84
+ if (response.status === 204) {
85
+ terminal = true;
86
+ button.textContent = 'Signed in';
87
+ announce('You are signed in. Continuing to the app.');
88
+ location.replace(${scriptValue(`${a}/signed-in`)});
89
+ } else if (response.status === 410 || response.status === 400) {
90
+ terminal = true;
91
+ button.hidden = true;
92
+ recovery.setAttribute('class', 'primary');
93
+ recover(response.status === 410
94
+ ? 'This link has expired or was already used. Request a new sign-in link.'
95
+ : 'This sign-in link is not valid. Request a new sign-in link.');
96
+ recovery.focus();
97
+ } else if (response.status === 429) {
98
+ const value = response.headers.get('retry-after');
99
+ const httpDate = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?:0[1-9]|[12]\\d|3[01]) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} (?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:0[1-9]|[12]\\d|3[01])-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{2} (?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: [1-9]|[12]\\d|3[01]) (?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d \\d{4})$/;
100
+ const seconds = value && /^\\d+$/.test(value)
101
+ ? Number(value) : (value && httpDate.test(value) ? (Date.parse(value) - Date.now()) / 1000 : NaN);
102
+ const wait = Number.isFinite(seconds) ? Math.min(300, Math.max(1, Math.ceil(seconds))) : 30;
103
+ retryAt = Date.now() + wait * 1000;
104
+ recover('Too many attempts. Wait ' + wait + ' seconds before trying again.');
105
+ tick();
106
+ } else if (response.status === 503) {
107
+ recover('Sign-in is temporarily unavailable. Try again shortly, or request a new sign-in link.');
108
+ ready();
109
+ } else {
110
+ recover('Sign-in could not be completed. Try again, or request a new sign-in link.');
111
+ ready();
112
+ }
113
+ } catch {
114
+ if (!disposed && controller === active) {
115
+ recover(timedOut
116
+ ? 'Sign-in took too long. Try again, or request a new sign-in link.'
117
+ : 'Unable to connect. Check your connection and try again, or request a new sign-in link.');
118
+ ready();
119
+ }
120
+ } finally {
121
+ active.abort();
122
+ clearTimeout(requestTimeout);
123
+ if (!disposed && controller === active) {
124
+ controller = undefined;
125
+ pending = false;
126
+ document.getElementById('actions').removeAttribute('aria-busy');
127
+ }
128
+ }
129
+ });`:``,u=`<!doctype html>
130
+ <html lang="en">
131
+ <head>
132
+ <meta charset="utf-8">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1">
134
+ <meta name="color-scheme" content="light dark">
135
+ <meta name="referrer" content="no-referrer">
136
+ <title>${escapeHtml(c)} | ${escapeHtml(r)}</title>
137
+ <script nonce="${i}">history.replaceState(null, '', location.pathname);<\/script>
138
+ <style nonce="${i}">
139
+ :root { color-scheme: light dark; --paper: #fcfbf9; --ink: #302d2b; --muted: #68615c; --accent: #934b2b; --hover: #783a20; --rule: #d6d0ca; --focus: #934b2b; }
140
+ * { box-sizing: border-box; }
141
+ body { margin: 0; background: var(--paper); color: var(--ink); font: 1rem/1.6 'Avenir Next', 'Segoe UI', sans-serif; letter-spacing: 0; }
142
+ main { width: min(100%, 38rem); margin: 0 auto; padding: 5rem 1.5rem 3rem; }
143
+ h1 { margin: 0 0 1.5rem; font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Palatino, serif; font-size: 2.25rem; line-height: 1.2; font-weight: 500; overflow-wrap: anywhere; text-wrap: balance; }
144
+ p { margin: 0 0 1rem; }
145
+ .detail { color: var(--muted); }
146
+ .actions { margin-top: 2rem; display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
147
+ button, .primary { min-height: 44px; max-width: 100%; padding: .75rem 1.25rem; border: 1px solid var(--accent); border-radius: 4px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; text-align: center; text-decoration: none; overflow-wrap: anywhere; cursor: pointer; transition: background-color 120ms ease-out; }
148
+ button { min-width: 12rem; }
149
+ button:hover:not(:disabled), .primary:hover { background: var(--hover); }
150
+ button:disabled { cursor: wait; opacity: .75; }
151
+ a { color: var(--accent); text-underline-offset: .2em; overflow-wrap: anywhere; }
152
+ a:not(.primary) { display: inline-flex; align-items: center; min-height: 44px; }
153
+ :focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; }
154
+ ::selection { background: var(--accent); color: #fff; }
155
+ #status { min-height: 3.2em; margin-top: 1.5rem; }
156
+ footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--rule); }
157
+ [hidden] { display: none !important; }
158
+ @media (prefers-color-scheme: dark) { :root { --paper: #242220; --ink: #f2efeb; --muted: #bfb7b0; --accent: #e0a07b; --hover: #efb998; --rule: #504942; --focus: #e0a07b; } button, .primary { color: #242220; } ::selection { color: #242220; } }
159
+ @media (max-width: 480px) { main { padding: 3rem 1.25rem 2rem; } h1 { font-size: 2rem; } .actions { align-items: stretch; flex-direction: column; } }
160
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; } }
161
+ </style>
162
+ </head>
163
+ <body>
164
+ <main id="main" aria-labelledby="title">
165
+ <h1 id="title" tabindex="-1">${s?`Sign in to ${escapeHtml(r)}`:escapeHtml(c)}</h1>
166
+ <p>${s?`Continue only if you requested this sign-in link.`:`Request a new sign-in link for ${escapeHtml(r)}.`}</p>
167
+ ${s?`<p class="detail">This link works once and expires ${n.LINK_TTL_MS/6e4} minutes after it was requested.</p>`:``}
168
+ <div class="actions" id="actions">
169
+ ${s?`<button type="button" id="confirm" hidden>Confirm sign-in</button>`:``}
170
+ <a${s?``:` class="primary"`} id="recovery" href="${escapeHtml(`${a}/account`)}"${s?` hidden`:``}>Request a new sign-in link</a>
171
+ </div>
172
+ <p role="status" aria-live="polite" aria-atomic="true" id="status"></p>
173
+ ${s?`<noscript><p>JavaScript is required to confirm this sign-in. No sign-in has been completed. <a href="${escapeHtml(`${a}/account`)}">Request a new sign-in link</a>.</p></noscript>`:``}
174
+ ${s?`<footer id="exit"><a href="${escapeHtml(`${a}/`)}">Back to app</a></footer>`:``}
175
+ </main>
176
+ ${s?`<script nonce="${i}">${l}
177
+ <\/script>`:``}
178
+ </body>
179
+ </html>`;return new Response(u,{headers:{...t.noStore,"content-security-policy":`default-src 'none'; script-src 'nonce-${i}'; style-src 'nonce-${i}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,"content-type":`text/html; charset=utf-8`,"referrer-policy":`no-referrer`,"x-content-type-options":`nosniff`},status:s?200:400})}exports.confirmationPage=confirmationPage,exports.escapeHtml=escapeHtml;
180
+ //# sourceMappingURL=_confirmation.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_confirmation.cjs","names":[],"sources":["../../src/auth/_confirmation.ts"],"sourcesContent":["import { REQUEST_MARKER_HEADER } from '../contract';\nimport { noStore } from '../worker/http';\nimport { LINK_TTL_MS } from './store';\n\nexport const escapeHtml = (value: string): string => value.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\n\nconst scriptValue = (value: string): string =>\n JSON.stringify(value).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`,\n );\n\nexport function confirmationPage({\n appName,\n nonce,\n publicOrigin,\n token,\n}: {\n appName: string;\n nonce: string;\n publicOrigin: string;\n token: string | null;\n}): Response {\n const valid = token !== null;\n const title = valid ? 'Confirm sign-in' : 'This sign-in link is not valid';\n const script = valid\n ? `\nconst button = document.getElementById('confirm');\nconst status = document.getElementById('status');\nconst recovery = document.getElementById('recovery');\nlet pending = false;\nlet terminal = false;\nlet disposed = false;\nlet cooldown;\nlet timeout;\nlet controller;\nlet retryAt = 0;\nconst announce = (message) => { status.textContent = message; };\nconst ready = () => {\n button.disabled = false;\n button.textContent = 'Try again';\n};\nconst stop = () => {\n clearTimeout(cooldown);\n clearTimeout(timeout);\n controller?.abort();\n controller = undefined;\n};\nconst tick = () => {\n if (disposed) return;\n const seconds = Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n if (seconds === 0) {\n ready();\n announce('You can try signing in again now.');\n return;\n }\n button.textContent = 'Try again in ' + seconds + 's';\n cooldown = setTimeout(tick, 1000);\n};\nconst recover = (message) => {\n announce(message);\n recovery.hidden = false;\n document.getElementById('exit').hidden = true;\n};\nbutton.hidden = false;\nbutton.focus();\naddEventListener('pagehide', () => {\n disposed = true;\n stop();\n});\naddEventListener('pageshow', (event) => {\n if (!event.persisted || !disposed) return;\n disposed = false;\n pending = false;\n document.getElementById('actions').removeAttribute('aria-busy');\n if (terminal) return;\n if (retryAt > Date.now()) tick();\n else {\n ready();\n announce('Confirm sign-in when you are ready.');\n }\n});\nbutton.addEventListener('click', async () => {\n if (disposed || pending || terminal || retryAt > Date.now()) return;\n pending = true;\n button.disabled = true;\n button.textContent = 'Signing you in';\n document.getElementById('actions').setAttribute('aria-busy', 'true');\n announce('Signing you in. Please wait.');\n const active = new AbortController();\n controller = active;\n let timedOut = false;\n const requestTimeout = setTimeout(() => {\n timedOut = true;\n active.abort();\n }, 15000);\n timeout = requestTimeout;\n try {\n const response = await fetch('/api/v1/auth/verify', {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': 'application/json', '${REQUEST_MARKER_HEADER}': '1' },\n body: JSON.stringify({ token: ${scriptValue(token ?? '')} }),\n signal: active.signal,\n redirect: 'error',\n referrerPolicy: 'no-referrer'\n });\n void response.body?.cancel().catch(() => {});\n if (disposed || controller !== active) return;\n if (response.status === 204) {\n terminal = true;\n button.textContent = 'Signed in';\n announce('You are signed in. Continuing to the app.');\n location.replace(${scriptValue(`${publicOrigin}/signed-in`)});\n } else if (response.status === 410 || response.status === 400) {\n terminal = true;\n button.hidden = true;\n recovery.setAttribute('class', 'primary');\n recover(response.status === 410\n ? 'This link has expired or was already used. Request a new sign-in link.'\n : 'This sign-in link is not valid. Request a new sign-in link.');\n recovery.focus();\n } else if (response.status === 429) {\n const value = response.headers.get('retry-after');\n const httpDate = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?:0[1-9]|[12]\\\\d|3[01]) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\\\d{4} (?:[01]\\\\d|2[0-3]):[0-5]\\\\d:[0-5]\\\\d GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:0[1-9]|[12]\\\\d|3[01])-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\\\d{2} (?:[01]\\\\d|2[0-3]):[0-5]\\\\d:[0-5]\\\\d GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: [1-9]|[12]\\\\d|3[01]) (?:[01]\\\\d|2[0-3]):[0-5]\\\\d:[0-5]\\\\d \\\\d{4})$/;\n const seconds = value && /^\\\\d+$/.test(value)\n ? Number(value) : (value && httpDate.test(value) ? (Date.parse(value) - Date.now()) / 1000 : NaN);\n const wait = Number.isFinite(seconds) ? Math.min(300, Math.max(1, Math.ceil(seconds))) : 30;\n retryAt = Date.now() + wait * 1000;\n recover('Too many attempts. Wait ' + wait + ' seconds before trying again.');\n tick();\n } else if (response.status === 503) {\n recover('Sign-in is temporarily unavailable. Try again shortly, or request a new sign-in link.');\n ready();\n } else {\n recover('Sign-in could not be completed. Try again, or request a new sign-in link.');\n ready();\n }\n } catch {\n if (!disposed && controller === active) {\n recover(timedOut\n ? 'Sign-in took too long. Try again, or request a new sign-in link.'\n : 'Unable to connect. Check your connection and try again, or request a new sign-in link.');\n ready();\n }\n } finally {\n active.abort();\n clearTimeout(requestTimeout);\n if (!disposed && controller === active) {\n controller = undefined;\n pending = false;\n document.getElementById('actions').removeAttribute('aria-busy');\n }\n }\n});`\n : '';\n const html = `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"color-scheme\" content=\"light dark\">\n<meta name=\"referrer\" content=\"no-referrer\">\n<title>${escapeHtml(title)} | ${escapeHtml(appName)}</title>\n<script nonce=\"${nonce}\">history.replaceState(null, '', location.pathname);</script>\n<style nonce=\"${nonce}\">\n:root { color-scheme: light dark; --paper: #fcfbf9; --ink: #302d2b; --muted: #68615c; --accent: #934b2b; --hover: #783a20; --rule: #d6d0ca; --focus: #934b2b; }\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--paper); color: var(--ink); font: 1rem/1.6 'Avenir Next', 'Segoe UI', sans-serif; letter-spacing: 0; }\nmain { width: min(100%, 38rem); margin: 0 auto; padding: 5rem 1.5rem 3rem; }\nh1 { margin: 0 0 1.5rem; font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Palatino, serif; font-size: 2.25rem; line-height: 1.2; font-weight: 500; overflow-wrap: anywhere; text-wrap: balance; }\np { margin: 0 0 1rem; }\n.detail { color: var(--muted); }\n.actions { margin-top: 2rem; display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }\nbutton, .primary { min-height: 44px; max-width: 100%; padding: .75rem 1.25rem; border: 1px solid var(--accent); border-radius: 4px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; text-align: center; text-decoration: none; overflow-wrap: anywhere; cursor: pointer; transition: background-color 120ms ease-out; }\nbutton { min-width: 12rem; }\nbutton:hover:not(:disabled), .primary:hover { background: var(--hover); }\nbutton:disabled { cursor: wait; opacity: .75; }\na { color: var(--accent); text-underline-offset: .2em; overflow-wrap: anywhere; }\na:not(.primary) { display: inline-flex; align-items: center; min-height: 44px; }\n:focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; }\n::selection { background: var(--accent); color: #fff; }\n#status { min-height: 3.2em; margin-top: 1.5rem; }\nfooter { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--rule); }\n[hidden] { display: none !important; }\n@media (prefers-color-scheme: dark) { :root { --paper: #242220; --ink: #f2efeb; --muted: #bfb7b0; --accent: #e0a07b; --hover: #efb998; --rule: #504942; --focus: #e0a07b; } button, .primary { color: #242220; } ::selection { color: #242220; } }\n@media (max-width: 480px) { main { padding: 3rem 1.25rem 2rem; } h1 { font-size: 2rem; } .actions { align-items: stretch; flex-direction: column; } }\n@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; } }\n</style>\n</head>\n<body>\n<main id=\"main\" aria-labelledby=\"title\">\n<h1 id=\"title\" tabindex=\"-1\">${valid ? `Sign in to ${escapeHtml(appName)}` : escapeHtml(title)}</h1>\n<p>${valid ? 'Continue only if you requested this sign-in link.' : `Request a new sign-in link for ${escapeHtml(appName)}.`}</p>\n${valid ? `<p class=\"detail\">This link works once and expires ${LINK_TTL_MS / 60_000} minutes after it was requested.</p>` : ''}\n<div class=\"actions\" id=\"actions\">\n${valid ? '<button type=\"button\" id=\"confirm\" hidden>Confirm sign-in</button>' : ''}\n<a${valid ? '' : ' class=\"primary\"'} id=\"recovery\" href=\"${escapeHtml(`${publicOrigin}/account`)}\"${valid ? ' hidden' : ''}>Request a new sign-in link</a>\n</div>\n<p role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" id=\"status\"></p>\n${valid ? `<noscript><p>JavaScript is required to confirm this sign-in. No sign-in has been completed. <a href=\"${escapeHtml(`${publicOrigin}/account`)}\">Request a new sign-in link</a>.</p></noscript>` : ''}\n${valid ? `<footer id=\"exit\"><a href=\"${escapeHtml(`${publicOrigin}/`)}\">Back to app</a></footer>` : ''}\n</main>\n${\n valid\n ? `<script nonce=\"${nonce}\">${script}\n</script>`\n : ''\n}\n</body>\n</html>`;\n return new Response(html, {\n headers: {\n ...noStore,\n 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n 'content-type': 'text/html; charset=utf-8',\n 'referrer-policy': 'no-referrer',\n 'x-content-type-options': 'nosniff',\n },\n status: valid ? 200 : 400,\n });\n}\n"],"mappings":"4FAIA,IAAa,WAAc,GAA0B,EAAM,QAAQ,WAAa,GAAS,KAAK,EAAK,WAAW,CAAC,EAAE,EAAE,EAE7G,YAAe,GACnB,KAAK,UAAU,CAAK,CAAC,CAAC,QACpB,qBACC,GAAS,MAAM,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,GACjE,EAEF,SAAgB,iBAAiB,CAC/B,UACA,QACA,eACA,SAMW,CACX,IAAM,EAAQ,IAAU,KAClB,EAAQ,EAAQ,kBAAoB,iCACpC,EAAS,EACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wDA2EkD,EAAA,sBAAsB;sCACxC,YAAY,GAAS,EAAE,EAAE;;;;;;;;;;;yBAWtC,YAAY,GAAG,EAAa,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0C5D,GACE,EAAO;;;;;;;SAON,WAAW,CAAK,EAAE,KAAK,WAAW,CAAO,EAAE;iBACnC,EAAM;gBACP,EAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA2BS,EAAQ,cAAc,WAAW,CAAO,IAAM,WAAW,CAAK,EAAE;KAC1F,EAAQ,oDAAsD,kCAAkC,WAAW,CAAO,EAAE,GAAG;EAC1H,EAAQ,sDAAsD,EAAA,YAAc,IAAO,sCAAwC,GAAG;;EAE9H,EAAQ,qEAAuE,GAAG;IAChF,EAAQ,GAAK,mBAAmB,uBAAuB,WAAW,GAAG,EAAa,SAAS,EAAE,GAAG,EAAQ,UAAY,GAAG;;;EAGzH,EAAQ,wGAAwG,WAAW,GAAG,EAAa,SAAS,EAAE,kDAAoD,GAAG;EAC7M,EAAQ,8BAA8B,WAAW,GAAG,EAAa,EAAE,EAAE,4BAA8B,GAAG;;EAGtG,EACI,kBAAkB,EAAM,IAAI,EAAO;YAEnC,GACL;;SAGC,OAAO,IAAI,SAAS,EAAM,CACxB,QAAS,CACP,GAAG,EAAA,QACH,0BAA2B,yCAAyC,EAAM,sBAAsB,EAAM,oFACtG,eAAgB,2BAChB,kBAAmB,cACnB,yBAA0B,SAC5B,EACA,OAAQ,EAAQ,IAAM,GACxB,CAAC,CACH"}
@@ -0,0 +1,8 @@
1
+ export declare const escapeHtml: (value: string) => string;
2
+ export declare function confirmationPage({ appName, nonce, publicOrigin, token, }: {
3
+ appName: string;
4
+ nonce: string;
5
+ publicOrigin: string;
6
+ token: string | null;
7
+ }): Response;
8
+ //# sourceMappingURL=_confirmation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_confirmation.d.ts","sourceRoot":"","sources":["../../src/auth/_confirmation.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAAyE,CAAC;AAQrH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,YAAY,EACZ,KAAK,GACN,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,GAAG,QAAQ,CAuMX"}
@@ -0,0 +1,180 @@
1
+ import{REQUEST_MARKER_HEADER as e}from"../contract.js";import{noStore as t}from"../worker/http.js";import{LINK_TTL_MS as n}from"./store.js";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`)}`);function confirmationPage({appName:r,nonce:i,publicOrigin:a,token:o}){let s=o!==null,c=s?`Confirm sign-in`:`This sign-in link is not valid`,l=s?`
2
+ const button = document.getElementById('confirm');
3
+ const status = document.getElementById('status');
4
+ const recovery = document.getElementById('recovery');
5
+ let pending = false;
6
+ let terminal = false;
7
+ let disposed = false;
8
+ let cooldown;
9
+ let timeout;
10
+ let controller;
11
+ let retryAt = 0;
12
+ const announce = (message) => { status.textContent = message; };
13
+ const ready = () => {
14
+ button.disabled = false;
15
+ button.textContent = 'Try again';
16
+ };
17
+ const stop = () => {
18
+ clearTimeout(cooldown);
19
+ clearTimeout(timeout);
20
+ controller?.abort();
21
+ controller = undefined;
22
+ };
23
+ const tick = () => {
24
+ if (disposed) return;
25
+ const seconds = Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));
26
+ if (seconds === 0) {
27
+ ready();
28
+ announce('You can try signing in again now.');
29
+ return;
30
+ }
31
+ button.textContent = 'Try again in ' + seconds + 's';
32
+ cooldown = setTimeout(tick, 1000);
33
+ };
34
+ const recover = (message) => {
35
+ announce(message);
36
+ recovery.hidden = false;
37
+ document.getElementById('exit').hidden = true;
38
+ };
39
+ button.hidden = false;
40
+ button.focus();
41
+ addEventListener('pagehide', () => {
42
+ disposed = true;
43
+ stop();
44
+ });
45
+ addEventListener('pageshow', (event) => {
46
+ if (!event.persisted || !disposed) return;
47
+ disposed = false;
48
+ pending = false;
49
+ document.getElementById('actions').removeAttribute('aria-busy');
50
+ if (terminal) return;
51
+ if (retryAt > Date.now()) tick();
52
+ else {
53
+ ready();
54
+ announce('Confirm sign-in when you are ready.');
55
+ }
56
+ });
57
+ button.addEventListener('click', async () => {
58
+ if (disposed || pending || terminal || retryAt > Date.now()) return;
59
+ pending = true;
60
+ button.disabled = true;
61
+ button.textContent = 'Signing you in';
62
+ document.getElementById('actions').setAttribute('aria-busy', 'true');
63
+ announce('Signing you in. Please wait.');
64
+ const active = new AbortController();
65
+ controller = active;
66
+ let timedOut = false;
67
+ const requestTimeout = setTimeout(() => {
68
+ timedOut = true;
69
+ active.abort();
70
+ }, 15000);
71
+ timeout = requestTimeout;
72
+ try {
73
+ const response = await fetch('/api/v1/auth/verify', {
74
+ method: 'POST',
75
+ credentials: 'same-origin',
76
+ headers: { 'content-type': 'application/json', '${e}': '1' },
77
+ body: JSON.stringify({ token: ${scriptValue(o??``)} }),
78
+ signal: active.signal,
79
+ redirect: 'error',
80
+ referrerPolicy: 'no-referrer'
81
+ });
82
+ void response.body?.cancel().catch(() => {});
83
+ if (disposed || controller !== active) return;
84
+ if (response.status === 204) {
85
+ terminal = true;
86
+ button.textContent = 'Signed in';
87
+ announce('You are signed in. Continuing to the app.');
88
+ location.replace(${scriptValue(`${a}/signed-in`)});
89
+ } else if (response.status === 410 || response.status === 400) {
90
+ terminal = true;
91
+ button.hidden = true;
92
+ recovery.setAttribute('class', 'primary');
93
+ recover(response.status === 410
94
+ ? 'This link has expired or was already used. Request a new sign-in link.'
95
+ : 'This sign-in link is not valid. Request a new sign-in link.');
96
+ recovery.focus();
97
+ } else if (response.status === 429) {
98
+ const value = response.headers.get('retry-after');
99
+ const httpDate = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?:0[1-9]|[12]\\d|3[01]) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} (?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:0[1-9]|[12]\\d|3[01])-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{2} (?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: [1-9]|[12]\\d|3[01]) (?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d \\d{4})$/;
100
+ const seconds = value && /^\\d+$/.test(value)
101
+ ? Number(value) : (value && httpDate.test(value) ? (Date.parse(value) - Date.now()) / 1000 : NaN);
102
+ const wait = Number.isFinite(seconds) ? Math.min(300, Math.max(1, Math.ceil(seconds))) : 30;
103
+ retryAt = Date.now() + wait * 1000;
104
+ recover('Too many attempts. Wait ' + wait + ' seconds before trying again.');
105
+ tick();
106
+ } else if (response.status === 503) {
107
+ recover('Sign-in is temporarily unavailable. Try again shortly, or request a new sign-in link.');
108
+ ready();
109
+ } else {
110
+ recover('Sign-in could not be completed. Try again, or request a new sign-in link.');
111
+ ready();
112
+ }
113
+ } catch {
114
+ if (!disposed && controller === active) {
115
+ recover(timedOut
116
+ ? 'Sign-in took too long. Try again, or request a new sign-in link.'
117
+ : 'Unable to connect. Check your connection and try again, or request a new sign-in link.');
118
+ ready();
119
+ }
120
+ } finally {
121
+ active.abort();
122
+ clearTimeout(requestTimeout);
123
+ if (!disposed && controller === active) {
124
+ controller = undefined;
125
+ pending = false;
126
+ document.getElementById('actions').removeAttribute('aria-busy');
127
+ }
128
+ }
129
+ });`:``,u=`<!doctype html>
130
+ <html lang="en">
131
+ <head>
132
+ <meta charset="utf-8">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1">
134
+ <meta name="color-scheme" content="light dark">
135
+ <meta name="referrer" content="no-referrer">
136
+ <title>${escapeHtml(c)} | ${escapeHtml(r)}</title>
137
+ <script nonce="${i}">history.replaceState(null, '', location.pathname);<\/script>
138
+ <style nonce="${i}">
139
+ :root { color-scheme: light dark; --paper: #fcfbf9; --ink: #302d2b; --muted: #68615c; --accent: #934b2b; --hover: #783a20; --rule: #d6d0ca; --focus: #934b2b; }
140
+ * { box-sizing: border-box; }
141
+ body { margin: 0; background: var(--paper); color: var(--ink); font: 1rem/1.6 'Avenir Next', 'Segoe UI', sans-serif; letter-spacing: 0; }
142
+ main { width: min(100%, 38rem); margin: 0 auto; padding: 5rem 1.5rem 3rem; }
143
+ h1 { margin: 0 0 1.5rem; font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Palatino, serif; font-size: 2.25rem; line-height: 1.2; font-weight: 500; overflow-wrap: anywhere; text-wrap: balance; }
144
+ p { margin: 0 0 1rem; }
145
+ .detail { color: var(--muted); }
146
+ .actions { margin-top: 2rem; display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
147
+ button, .primary { min-height: 44px; max-width: 100%; padding: .75rem 1.25rem; border: 1px solid var(--accent); border-radius: 4px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; text-align: center; text-decoration: none; overflow-wrap: anywhere; cursor: pointer; transition: background-color 120ms ease-out; }
148
+ button { min-width: 12rem; }
149
+ button:hover:not(:disabled), .primary:hover { background: var(--hover); }
150
+ button:disabled { cursor: wait; opacity: .75; }
151
+ a { color: var(--accent); text-underline-offset: .2em; overflow-wrap: anywhere; }
152
+ a:not(.primary) { display: inline-flex; align-items: center; min-height: 44px; }
153
+ :focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; }
154
+ ::selection { background: var(--accent); color: #fff; }
155
+ #status { min-height: 3.2em; margin-top: 1.5rem; }
156
+ footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--rule); }
157
+ [hidden] { display: none !important; }
158
+ @media (prefers-color-scheme: dark) { :root { --paper: #242220; --ink: #f2efeb; --muted: #bfb7b0; --accent: #e0a07b; --hover: #efb998; --rule: #504942; --focus: #e0a07b; } button, .primary { color: #242220; } ::selection { color: #242220; } }
159
+ @media (max-width: 480px) { main { padding: 3rem 1.25rem 2rem; } h1 { font-size: 2rem; } .actions { align-items: stretch; flex-direction: column; } }
160
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; } }
161
+ </style>
162
+ </head>
163
+ <body>
164
+ <main id="main" aria-labelledby="title">
165
+ <h1 id="title" tabindex="-1">${s?`Sign in to ${escapeHtml(r)}`:escapeHtml(c)}</h1>
166
+ <p>${s?`Continue only if you requested this sign-in link.`:`Request a new sign-in link for ${escapeHtml(r)}.`}</p>
167
+ ${s?`<p class="detail">This link works once and expires ${n/6e4} minutes after it was requested.</p>`:``}
168
+ <div class="actions" id="actions">
169
+ ${s?`<button type="button" id="confirm" hidden>Confirm sign-in</button>`:``}
170
+ <a${s?``:` class="primary"`} id="recovery" href="${escapeHtml(`${a}/account`)}"${s?` hidden`:``}>Request a new sign-in link</a>
171
+ </div>
172
+ <p role="status" aria-live="polite" aria-atomic="true" id="status"></p>
173
+ ${s?`<noscript><p>JavaScript is required to confirm this sign-in. No sign-in has been completed. <a href="${escapeHtml(`${a}/account`)}">Request a new sign-in link</a>.</p></noscript>`:``}
174
+ ${s?`<footer id="exit"><a href="${escapeHtml(`${a}/`)}">Back to app</a></footer>`:``}
175
+ </main>
176
+ ${s?`<script nonce="${i}">${l}
177
+ <\/script>`:``}
178
+ </body>
179
+ </html>`;return new Response(u,{headers:{...t,"content-security-policy":`default-src 'none'; script-src 'nonce-${i}'; style-src 'nonce-${i}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,"content-type":`text/html; charset=utf-8`,"referrer-policy":`no-referrer`,"x-content-type-options":`nosniff`},status:s?200:400})}export{confirmationPage,escapeHtml};
180
+ //# sourceMappingURL=_confirmation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_confirmation.js","names":[],"sources":["../../src/auth/_confirmation.ts"],"sourcesContent":["import { REQUEST_MARKER_HEADER } from '../contract';\nimport { noStore } from '../worker/http';\nimport { LINK_TTL_MS } from './store';\n\nexport const escapeHtml = (value: string): string => value.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\n\nconst scriptValue = (value: string): string =>\n JSON.stringify(value).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`,\n );\n\nexport function confirmationPage({\n appName,\n nonce,\n publicOrigin,\n token,\n}: {\n appName: string;\n nonce: string;\n publicOrigin: string;\n token: string | null;\n}): Response {\n const valid = token !== null;\n const title = valid ? 'Confirm sign-in' : 'This sign-in link is not valid';\n const script = valid\n ? `\nconst button = document.getElementById('confirm');\nconst status = document.getElementById('status');\nconst recovery = document.getElementById('recovery');\nlet pending = false;\nlet terminal = false;\nlet disposed = false;\nlet cooldown;\nlet timeout;\nlet controller;\nlet retryAt = 0;\nconst announce = (message) => { status.textContent = message; };\nconst ready = () => {\n button.disabled = false;\n button.textContent = 'Try again';\n};\nconst stop = () => {\n clearTimeout(cooldown);\n clearTimeout(timeout);\n controller?.abort();\n controller = undefined;\n};\nconst tick = () => {\n if (disposed) return;\n const seconds = Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n if (seconds === 0) {\n ready();\n announce('You can try signing in again now.');\n return;\n }\n button.textContent = 'Try again in ' + seconds + 's';\n cooldown = setTimeout(tick, 1000);\n};\nconst recover = (message) => {\n announce(message);\n recovery.hidden = false;\n document.getElementById('exit').hidden = true;\n};\nbutton.hidden = false;\nbutton.focus();\naddEventListener('pagehide', () => {\n disposed = true;\n stop();\n});\naddEventListener('pageshow', (event) => {\n if (!event.persisted || !disposed) return;\n disposed = false;\n pending = false;\n document.getElementById('actions').removeAttribute('aria-busy');\n if (terminal) return;\n if (retryAt > Date.now()) tick();\n else {\n ready();\n announce('Confirm sign-in when you are ready.');\n }\n});\nbutton.addEventListener('click', async () => {\n if (disposed || pending || terminal || retryAt > Date.now()) return;\n pending = true;\n button.disabled = true;\n button.textContent = 'Signing you in';\n document.getElementById('actions').setAttribute('aria-busy', 'true');\n announce('Signing you in. Please wait.');\n const active = new AbortController();\n controller = active;\n let timedOut = false;\n const requestTimeout = setTimeout(() => {\n timedOut = true;\n active.abort();\n }, 15000);\n timeout = requestTimeout;\n try {\n const response = await fetch('/api/v1/auth/verify', {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': 'application/json', '${REQUEST_MARKER_HEADER}': '1' },\n body: JSON.stringify({ token: ${scriptValue(token ?? '')} }),\n signal: active.signal,\n redirect: 'error',\n referrerPolicy: 'no-referrer'\n });\n void response.body?.cancel().catch(() => {});\n if (disposed || controller !== active) return;\n if (response.status === 204) {\n terminal = true;\n button.textContent = 'Signed in';\n announce('You are signed in. Continuing to the app.');\n location.replace(${scriptValue(`${publicOrigin}/signed-in`)});\n } else if (response.status === 410 || response.status === 400) {\n terminal = true;\n button.hidden = true;\n recovery.setAttribute('class', 'primary');\n recover(response.status === 410\n ? 'This link has expired or was already used. Request a new sign-in link.'\n : 'This sign-in link is not valid. Request a new sign-in link.');\n recovery.focus();\n } else if (response.status === 429) {\n const value = response.headers.get('retry-after');\n const httpDate = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?:0[1-9]|[12]\\\\d|3[01]) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\\\d{4} (?:[01]\\\\d|2[0-3]):[0-5]\\\\d:[0-5]\\\\d GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:0[1-9]|[12]\\\\d|3[01])-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\\\d{2} (?:[01]\\\\d|2[0-3]):[0-5]\\\\d:[0-5]\\\\d GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: [1-9]|[12]\\\\d|3[01]) (?:[01]\\\\d|2[0-3]):[0-5]\\\\d:[0-5]\\\\d \\\\d{4})$/;\n const seconds = value && /^\\\\d+$/.test(value)\n ? Number(value) : (value && httpDate.test(value) ? (Date.parse(value) - Date.now()) / 1000 : NaN);\n const wait = Number.isFinite(seconds) ? Math.min(300, Math.max(1, Math.ceil(seconds))) : 30;\n retryAt = Date.now() + wait * 1000;\n recover('Too many attempts. Wait ' + wait + ' seconds before trying again.');\n tick();\n } else if (response.status === 503) {\n recover('Sign-in is temporarily unavailable. Try again shortly, or request a new sign-in link.');\n ready();\n } else {\n recover('Sign-in could not be completed. Try again, or request a new sign-in link.');\n ready();\n }\n } catch {\n if (!disposed && controller === active) {\n recover(timedOut\n ? 'Sign-in took too long. Try again, or request a new sign-in link.'\n : 'Unable to connect. Check your connection and try again, or request a new sign-in link.');\n ready();\n }\n } finally {\n active.abort();\n clearTimeout(requestTimeout);\n if (!disposed && controller === active) {\n controller = undefined;\n pending = false;\n document.getElementById('actions').removeAttribute('aria-busy');\n }\n }\n});`\n : '';\n const html = `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"color-scheme\" content=\"light dark\">\n<meta name=\"referrer\" content=\"no-referrer\">\n<title>${escapeHtml(title)} | ${escapeHtml(appName)}</title>\n<script nonce=\"${nonce}\">history.replaceState(null, '', location.pathname);</script>\n<style nonce=\"${nonce}\">\n:root { color-scheme: light dark; --paper: #fcfbf9; --ink: #302d2b; --muted: #68615c; --accent: #934b2b; --hover: #783a20; --rule: #d6d0ca; --focus: #934b2b; }\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--paper); color: var(--ink); font: 1rem/1.6 'Avenir Next', 'Segoe UI', sans-serif; letter-spacing: 0; }\nmain { width: min(100%, 38rem); margin: 0 auto; padding: 5rem 1.5rem 3rem; }\nh1 { margin: 0 0 1.5rem; font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Palatino, serif; font-size: 2.25rem; line-height: 1.2; font-weight: 500; overflow-wrap: anywhere; text-wrap: balance; }\np { margin: 0 0 1rem; }\n.detail { color: var(--muted); }\n.actions { margin-top: 2rem; display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }\nbutton, .primary { min-height: 44px; max-width: 100%; padding: .75rem 1.25rem; border: 1px solid var(--accent); border-radius: 4px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; text-align: center; text-decoration: none; overflow-wrap: anywhere; cursor: pointer; transition: background-color 120ms ease-out; }\nbutton { min-width: 12rem; }\nbutton:hover:not(:disabled), .primary:hover { background: var(--hover); }\nbutton:disabled { cursor: wait; opacity: .75; }\na { color: var(--accent); text-underline-offset: .2em; overflow-wrap: anywhere; }\na:not(.primary) { display: inline-flex; align-items: center; min-height: 44px; }\n:focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; }\n::selection { background: var(--accent); color: #fff; }\n#status { min-height: 3.2em; margin-top: 1.5rem; }\nfooter { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--rule); }\n[hidden] { display: none !important; }\n@media (prefers-color-scheme: dark) { :root { --paper: #242220; --ink: #f2efeb; --muted: #bfb7b0; --accent: #e0a07b; --hover: #efb998; --rule: #504942; --focus: #e0a07b; } button, .primary { color: #242220; } ::selection { color: #242220; } }\n@media (max-width: 480px) { main { padding: 3rem 1.25rem 2rem; } h1 { font-size: 2rem; } .actions { align-items: stretch; flex-direction: column; } }\n@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; } }\n</style>\n</head>\n<body>\n<main id=\"main\" aria-labelledby=\"title\">\n<h1 id=\"title\" tabindex=\"-1\">${valid ? `Sign in to ${escapeHtml(appName)}` : escapeHtml(title)}</h1>\n<p>${valid ? 'Continue only if you requested this sign-in link.' : `Request a new sign-in link for ${escapeHtml(appName)}.`}</p>\n${valid ? `<p class=\"detail\">This link works once and expires ${LINK_TTL_MS / 60_000} minutes after it was requested.</p>` : ''}\n<div class=\"actions\" id=\"actions\">\n${valid ? '<button type=\"button\" id=\"confirm\" hidden>Confirm sign-in</button>' : ''}\n<a${valid ? '' : ' class=\"primary\"'} id=\"recovery\" href=\"${escapeHtml(`${publicOrigin}/account`)}\"${valid ? ' hidden' : ''}>Request a new sign-in link</a>\n</div>\n<p role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" id=\"status\"></p>\n${valid ? `<noscript><p>JavaScript is required to confirm this sign-in. No sign-in has been completed. <a href=\"${escapeHtml(`${publicOrigin}/account`)}\">Request a new sign-in link</a>.</p></noscript>` : ''}\n${valid ? `<footer id=\"exit\"><a href=\"${escapeHtml(`${publicOrigin}/`)}\">Back to app</a></footer>` : ''}\n</main>\n${\n valid\n ? `<script nonce=\"${nonce}\">${script}\n</script>`\n : ''\n}\n</body>\n</html>`;\n return new Response(html, {\n headers: {\n ...noStore,\n 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n 'content-type': 'text/html; charset=utf-8',\n 'referrer-policy': 'no-referrer',\n 'x-content-type-options': 'nosniff',\n },\n status: valid ? 200 : 400,\n });\n}\n"],"mappings":"4IAIA,IAAa,WAAc,GAA0B,EAAM,QAAQ,WAAa,GAAS,KAAK,EAAK,WAAW,CAAC,EAAE,EAAE,EAE7G,YAAe,GACnB,KAAK,UAAU,CAAK,CAAC,CAAC,QACpB,qBACC,GAAS,MAAM,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,GACjE,EAEF,SAAgB,iBAAiB,CAC/B,UACA,QACA,eACA,SAMW,CACX,IAAM,EAAQ,IAAU,KAClB,EAAQ,EAAQ,kBAAoB,iCACpC,EAAS,EACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wDA2EkD,EAAsB;sCACxC,YAAY,GAAS,EAAE,EAAE;;;;;;;;;;;yBAWtC,YAAY,GAAG,EAAa,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0C5D,GACE,EAAO;;;;;;;SAON,WAAW,CAAK,EAAE,KAAK,WAAW,CAAO,EAAE;iBACnC,EAAM;gBACP,EAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA2BS,EAAQ,cAAc,WAAW,CAAO,IAAM,WAAW,CAAK,EAAE;KAC1F,EAAQ,oDAAsD,kCAAkC,WAAW,CAAO,EAAE,GAAG;EAC1H,EAAQ,sDAAsD,EAAc,IAAO,sCAAwC,GAAG;;EAE9H,EAAQ,qEAAuE,GAAG;IAChF,EAAQ,GAAK,mBAAmB,uBAAuB,WAAW,GAAG,EAAa,SAAS,EAAE,GAAG,EAAQ,UAAY,GAAG;;;EAGzH,EAAQ,wGAAwG,WAAW,GAAG,EAAa,SAAS,EAAE,kDAAoD,GAAG;EAC7M,EAAQ,8BAA8B,WAAW,GAAG,EAAa,EAAE,EAAE,4BAA8B,GAAG;;EAGtG,EACI,kBAAkB,EAAM,IAAI,EAAO;YAEnC,GACL;;SAGC,OAAO,IAAI,SAAS,EAAM,CACxB,QAAS,CACP,GAAG,EACH,0BAA2B,yCAAyC,EAAM,sBAAsB,EAAM,oFACtG,eAAgB,2BAChB,kBAAmB,cACnB,yBAA0B,SAC5B,EACA,OAAQ,EAAQ,IAAM,GACxB,CAAC,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs","names":[],"sources":["../../src/auth/config.ts"],"sourcesContent":["import {\n type CryptoKey,\n calculateJwkThumbprint,\n createLocalJWKSet,\n importJWK,\n type JWK_RSA_Private,\n type JWK_RSA_Public,\n jwtVerify,\n SignJWT,\n} from 'jose';\nimport type { D1Like } from '../worker/env';\nimport type { ApiError } from '../worker/http';\nimport { AuthOperationError } from './errors';\n\n/** The send-email binding subset the issuer calls; the runtime's SendEmail satisfies it. */\nexport interface SendEmail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise<unknown>;\n}\n\n/**\n * The catalog Worker as the issuer reaches it: a Cloudflare service binding (or any\n * `fetch`-shaped stub). Account erasure calls the catalog's `DELETE /api/v1/me/account`\n * through it before removing the issuer's own row.\n */\nexport interface AuthArchiveBinding {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The bindings the auth issuer reads. Declared structurally, like the catalog\n * Worker's Env: a deployment's generated `ExportedHandler<Env>` accepts the factory's\n * result as-is, with no `@cloudflare/workers-types` dependency. Every value is\n * validated on first use per bindings object; a missing or malformed one answers\n * 503 `unavailable` on every route, never a permissive fallback.\n */\nexport interface AuthWorkerEnv {\n /** Comma-separated exact origins allowed to call unsafe routes and read CORS responses. */\n ALLOWED_ORIGINS: string;\n /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */\n APP_ENV: string;\n /** The app's display name: the magic-link email's subject and copy name it. */\n APP_NAME: string;\n AUDIENCE: string;\n /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */\n AUTH_ARCHIVE: AuthArchiveBinding;\n /** The issuer's own D1: accounts, links, per-account send and singleton admission budgets. */\n AUTH_DB: D1Like;\n AUTH_EMAIL: SendEmail;\n /** The `iss` claim every token carries, exactly as the catalog Worker's `JWT_ISSUER` knows it. */\n AUTH_ISSUER: string;\n /**\n * Optional JWKS JSON (`{\"keys\":[...]}`) of retired PUBLIC RS256 keys, still served\n * from `/jwks.json` and accepted by the issuer so sessions minted before a rotation\n * keep verifying until they expire. Private members are rejected.\n */\n AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;\n /** The active RS256 private key as a JWK JSON string (a wrangler secret): signs every new session. */\n AUTH_JWT_PRIVATE_JWK: string;\n /** Comma-separated emails whose sessions carry the operator role claim. */\n OPERATOR_EMAILS: string;\n /** The app's public origin: magic links, the signed-in redirect and the catalog erasure URL point here. */\n PUBLIC_ORIGIN: string;\n /** The address emails come from: a domain onboarded to Cloudflare Email Service. */\n SENDER_ADDRESS: string;\n}\n\nexport type AuthAppEnv = 'local' | 'production';\n\nexport interface PublicSigningKey extends JWK_RSA_Public {\n alg: 'RS256';\n kid: string;\n kty: 'RSA';\n use: 'sig';\n}\n\nexport interface AuthConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AuthAppEnv;\n appName: string;\n audience: string;\n issuer: string;\n /** Every key a session may be verified with: the active key first, then retired ones. */\n jwks: readonly PublicSigningKey[];\n operatorEmails: ReadonlySet<string>;\n publicOrigin: string;\n senderAddress: string;\n signing: { key: CryptoKey; kid: string };\n verifier: ReturnType<typeof createLocalJWKSet>;\n}\n\n// Configuration faults never name the offending field: the response must not leak\n// Env names, only a generic temporary unavailability.\nconst configError = (): ApiError => new AuthOperationError(503, 'configuration');\n\nconst EMAIL_MAX = 320;\nconst APP_NAME_MAX = 64;\n// 2048-bit RSA modulus: 256 bytes, 342 base64url characters unpadded.\nconst MIN_MODULUS_CHARS = 342;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\nconst PRIVATE_RSA_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'] as const;\n\nexport const isWellFormedEmail = (value: string): boolean =>\n value.length <= EMAIL_MAX &&\n value === value.toLowerCase() &&\n /^[^\\s@<>\"',;]+@[^\\s@<>\"',;]+\\.[^\\s@<>\"',;]+$/.test(value);\n\nconst requireString = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0) throw configError();\n return value;\n};\n\nconst requireBinding = (value: unknown, method: string): void => {\n if (typeof value !== 'object' || value === null) throw configError();\n if (typeof Reflect.get(value, method) !== 'function') throw configError();\n};\n\n/** Transport rules: local is loopback HTTP only, production is non-loopback HTTPS; no credentials or fragments. */\nconst validateUrl = (raw: string, appEnv: AuthAppEnv): URL => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw configError();\n }\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError();\n const loopback = LOOPBACK_HOSTS.has(url.hostname);\n if (appEnv === 'local' ? url.protocol !== 'http:' || !loopback : url.protocol !== 'https:' || loopback) {\n throw configError();\n }\n return url;\n};\n\n/** An exact canonical origin: scheme + host (+port), no path, query, wildcard or `null`. */\nconst validateOrigin = (raw: string, appEnv: AuthAppEnv): string => {\n const url = validateUrl(raw, appEnv);\n if (url.pathname !== '/' || url.search !== '' || url.origin !== raw) throw configError();\n return url.origin;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n throw configError();\n }\n};\n\nconst b64urlMember = (jwk: Record<string, unknown>, name: string): string => {\n const value = jwk[name];\n if (typeof value !== 'string' || !BASE64URL.test(value)) throw configError();\n return value;\n};\n\n/** RS256 signing-key metadata, when present, must agree with how the issuer uses the key. */\nconst checkRsaMetadata = (jwk: Record<string, unknown>): void => {\n if (jwk.kty !== 'RSA') throw configError();\n if (jwk.alg !== undefined && jwk.alg !== 'RS256') throw configError();\n if (jwk.use !== undefined && jwk.use !== 'sig') throw configError();\n};\n\nconst publicMembers = (jwk: Record<string, unknown>): JWK_RSA_Public => {\n const n = b64urlMember(jwk, 'n');\n if (n.length < MIN_MODULUS_CHARS) throw configError();\n return { e: b64urlMember(jwk, 'e'), kty: 'RSA', n };\n};\n\nconst toSigningKey = async (pub: JWK_RSA_Public): Promise<PublicSigningKey> => ({\n alg: 'RS256',\n e: pub.e,\n kid: await calculateJwkThumbprint({ e: pub.e, kty: 'RSA', n: pub.n }),\n kty: 'RSA',\n n: pub.n,\n use: 'sig',\n});\n\nasync function activeKey(raw: string): Promise<{ key: CryptoKey; public: PublicSigningKey }> {\n const parsed = parseJson(raw);\n if (!isRecord(parsed)) throw configError();\n checkRsaMetadata(parsed);\n const pub = publicMembers(parsed);\n const jwk: JWK_RSA_Private = {\n ...pub,\n d: b64urlMember(parsed, 'd'),\n dp: b64urlMember(parsed, 'dp'),\n dq: b64urlMember(parsed, 'dq'),\n p: b64urlMember(parsed, 'p'),\n q: b64urlMember(parsed, 'q'),\n qi: b64urlMember(parsed, 'qi'),\n };\n let key: CryptoKey;\n try {\n const imported = await importJWK(jwk, 'RS256', { extractable: false });\n if (imported instanceof Uint8Array) throw configError();\n key = imported;\n } catch {\n throw configError();\n }\n return { key, public: await toSigningKey(pub) };\n}\n\n/** Retired verification keys: public RSA members only; any private member is a misconfiguration. */\nasync function previousKeys(raw: string | undefined): Promise<PublicSigningKey[]> {\n if (raw === undefined || raw.trim().length === 0) return [];\n const parsed = parseJson(raw);\n if (!isRecord(parsed) || !Array.isArray(parsed.keys)) throw configError();\n const keys: PublicSigningKey[] = [];\n for (const entry of parsed.keys) {\n if (!isRecord(entry)) throw configError();\n checkRsaMetadata(entry);\n if (PRIVATE_RSA_MEMBERS.some((member) => member in entry)) throw configError();\n keys.push(await toSigningKey(publicMembers(entry)));\n }\n return keys;\n}\n\nconst configs = new WeakMap<AuthWorkerEnv, Promise<AuthConfig>>();\n\nasync function buildConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const appEnvRaw = requireString(env.APP_ENV);\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError();\n const appEnv: AuthAppEnv = appEnvRaw;\n\n const allowedOrigins = new Set(\n requireString(env.ALLOWED_ORIGINS)\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n .map((origin) => validateOrigin(origin, appEnv)),\n );\n if (allowedOrigins.size === 0) throw configError();\n\n const publicOrigin = validateOrigin(requireString(env.PUBLIC_ORIGIN), appEnv);\n // The issuer is matched as the exact configured string (a trailing slash is meaningful).\n const issuer = requireString(env.AUTH_ISSUER);\n validateUrl(issuer, appEnv);\n const audience = requireString(env.AUDIENCE);\n const appName = requireString(env.APP_NAME);\n if (appName.length > APP_NAME_MAX || /[\\p{Cc}\\p{Cs}]/u.test(appName)) throw configError();\n const senderAddress = requireString(env.SENDER_ADDRESS);\n if (!isWellFormedEmail(senderAddress)) throw configError();\n if (typeof env.OPERATOR_EMAILS !== 'string') throw configError();\n const operatorEmails = new Set(\n env.OPERATOR_EMAILS.split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n );\n for (const email of operatorEmails) if (!isWellFormedEmail(email)) throw configError();\n\n requireBinding(env.AUTH_DB, 'prepare');\n requireBinding(env.AUTH_DB, 'batch');\n requireBinding(env.AUTH_EMAIL, 'send');\n requireBinding(env.AUTH_ARCHIVE, 'fetch');\n\n const active = await activeKey(requireString(env.AUTH_JWT_PRIVATE_JWK));\n try {\n const proof = await new SignJWT({}).setProtectedHeader({ alg: 'RS256', kid: active.public.kid }).sign(active.key);\n await jwtVerify(proof, createLocalJWKSet({ keys: [active.public] }), { algorithms: ['RS256'] });\n } catch {\n throw configError();\n }\n const retired = (await previousKeys(env.AUTH_JWT_PREVIOUS_PUBLIC_JWKS)).filter(\n (key) => key.kid !== active.public.kid,\n );\n const jwks = [\n active.public,\n ...retired.filter((key, i) => retired.findIndex((other) => other.kid === key.kid) === i),\n ];\n\n return {\n allowedOrigins,\n appEnv,\n appName,\n audience,\n issuer,\n jwks,\n operatorEmails,\n publicOrigin,\n senderAddress,\n signing: { key: active.key, kid: active.public.kid },\n verifier: createLocalJWKSet({ keys: [...jwks] }),\n };\n}\n\n/**\n * Validates the env and imports its keys once per bindings object. A failed\n * validation is not cached, so a corrected binding set recovers without a restart.\n */\nexport function authConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const cached = configs.get(env);\n if (cached !== undefined) return cached;\n const pending = buildConfig(env);\n configs.set(env, pending);\n pending.catch(() => configs.delete(env));\n return pending;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.cjs","names":[],"sources":["../../src/auth/config.ts"],"sourcesContent":["import {\n type CryptoKey,\n calculateJwkThumbprint,\n createLocalJWKSet,\n importJWK,\n type JWK_RSA_Private,\n type JWK_RSA_Public,\n jwtVerify,\n SignJWT,\n} from 'jose';\nimport type { D1Like } from '../worker/env';\nimport type { ApiError } from '../worker/http';\nimport { AuthOperationError } from './errors';\n\n/** The send-email binding subset the issuer calls; the runtime's SendEmail satisfies it. */\nexport interface SendEmail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise<unknown>;\n}\n\n/**\n * The catalog Worker as the issuer reaches it: a Cloudflare service binding (or any\n * `fetch`-shaped stub). Account erasure calls the catalog's `DELETE /api/v1/me/account`\n * through it before removing the issuer's own row.\n */\nexport interface AuthArchiveBinding {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The bindings the auth issuer reads. Declared structurally, like the catalog\n * Worker's Env: a deployment's generated `ExportedHandler<Env>` accepts the factory's\n * result as-is, with no `@cloudflare/workers-types` dependency. Every value is\n * validated on first use per bindings object; a missing or malformed one answers\n * 503 `unavailable` on every route, never a permissive fallback.\n */\nexport interface AuthWorkerEnv {\n /** Comma-separated exact origins allowed to call unsafe routes and read CORS responses. */\n ALLOWED_ORIGINS: string;\n /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */\n APP_ENV: string;\n /** The app's display name in magic-link email and escaped confirmation-page branding. */\n APP_NAME: string;\n AUDIENCE: string;\n /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */\n AUTH_ARCHIVE: AuthArchiveBinding;\n /** The issuer's own D1: accounts, links, per-account send and singleton admission budgets. */\n AUTH_DB: D1Like;\n AUTH_EMAIL: SendEmail;\n /** The `iss` claim every token carries, exactly as the catalog Worker's `JWT_ISSUER` knows it. */\n AUTH_ISSUER: string;\n /**\n * Optional JWKS JSON (`{\"keys\":[...]}`) of retired PUBLIC RS256 keys, still served\n * from `/jwks.json` and accepted by the issuer so sessions minted before a rotation\n * keep verifying until they expire. Private members are rejected.\n */\n AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;\n /** The active RS256 private key as a JWK JSON string (a wrangler secret): signs every new session. */\n AUTH_JWT_PRIVATE_JWK: string;\n /** Comma-separated emails whose sessions carry the operator role claim. */\n OPERATOR_EMAILS: string;\n /** The app's public origin: magic links, app recovery, signed-in navigation and catalog erasure point here. */\n PUBLIC_ORIGIN: string;\n /** The address emails come from: a domain onboarded to Cloudflare Email Service. */\n SENDER_ADDRESS: string;\n}\n\nexport type AuthAppEnv = 'local' | 'production';\n\nexport interface PublicSigningKey extends JWK_RSA_Public {\n alg: 'RS256';\n kid: string;\n kty: 'RSA';\n use: 'sig';\n}\n\nexport interface AuthConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AuthAppEnv;\n appName: string;\n audience: string;\n issuer: string;\n /** Every key a session may be verified with: the active key first, then retired ones. */\n jwks: readonly PublicSigningKey[];\n operatorEmails: ReadonlySet<string>;\n publicOrigin: string;\n senderAddress: string;\n signing: { key: CryptoKey; kid: string };\n verifier: ReturnType<typeof createLocalJWKSet>;\n}\n\n// Configuration faults never name the offending field: the response must not leak\n// Env names, only a generic temporary unavailability.\nconst configError = (): ApiError => new AuthOperationError(503, 'configuration');\n\nconst EMAIL_MAX = 320;\nconst APP_NAME_MAX = 64;\n// 2048-bit RSA modulus: 256 bytes, 342 base64url characters unpadded.\nconst MIN_MODULUS_CHARS = 342;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\nconst PRIVATE_RSA_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'] as const;\n\nexport const isWellFormedEmail = (value: string): boolean =>\n value.length <= EMAIL_MAX &&\n value === value.toLowerCase() &&\n /^[^\\s@<>\"',;]+@[^\\s@<>\"',;]+\\.[^\\s@<>\"',;]+$/.test(value);\n\nconst requireString = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0) throw configError();\n return value;\n};\n\nconst requireBinding = (value: unknown, method: string): void => {\n if (typeof value !== 'object' || value === null) throw configError();\n if (typeof Reflect.get(value, method) !== 'function') throw configError();\n};\n\n/** Transport rules: local is loopback HTTP only, production is non-loopback HTTPS; no credentials or fragments. */\nconst validateUrl = (raw: string, appEnv: AuthAppEnv): URL => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw configError();\n }\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError();\n const loopback = LOOPBACK_HOSTS.has(url.hostname);\n if (appEnv === 'local' ? url.protocol !== 'http:' || !loopback : url.protocol !== 'https:' || loopback) {\n throw configError();\n }\n return url;\n};\n\n/** An exact canonical origin: scheme + host (+port), no path, query, wildcard or `null`. */\nconst validateOrigin = (raw: string, appEnv: AuthAppEnv): string => {\n const url = validateUrl(raw, appEnv);\n if (url.pathname !== '/' || url.search !== '' || url.origin !== raw) throw configError();\n return url.origin;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n throw configError();\n }\n};\n\nconst b64urlMember = (jwk: Record<string, unknown>, name: string): string => {\n const value = jwk[name];\n if (typeof value !== 'string' || !BASE64URL.test(value)) throw configError();\n return value;\n};\n\n/** RS256 signing-key metadata, when present, must agree with how the issuer uses the key. */\nconst checkRsaMetadata = (jwk: Record<string, unknown>): void => {\n if (jwk.kty !== 'RSA') throw configError();\n if (jwk.alg !== undefined && jwk.alg !== 'RS256') throw configError();\n if (jwk.use !== undefined && jwk.use !== 'sig') throw configError();\n};\n\nconst publicMembers = (jwk: Record<string, unknown>): JWK_RSA_Public => {\n const n = b64urlMember(jwk, 'n');\n if (n.length < MIN_MODULUS_CHARS) throw configError();\n return { e: b64urlMember(jwk, 'e'), kty: 'RSA', n };\n};\n\nconst toSigningKey = async (pub: JWK_RSA_Public): Promise<PublicSigningKey> => ({\n alg: 'RS256',\n e: pub.e,\n kid: await calculateJwkThumbprint({ e: pub.e, kty: 'RSA', n: pub.n }),\n kty: 'RSA',\n n: pub.n,\n use: 'sig',\n});\n\nasync function activeKey(raw: string): Promise<{ key: CryptoKey; public: PublicSigningKey }> {\n const parsed = parseJson(raw);\n if (!isRecord(parsed)) throw configError();\n checkRsaMetadata(parsed);\n const pub = publicMembers(parsed);\n const jwk: JWK_RSA_Private = {\n ...pub,\n d: b64urlMember(parsed, 'd'),\n dp: b64urlMember(parsed, 'dp'),\n dq: b64urlMember(parsed, 'dq'),\n p: b64urlMember(parsed, 'p'),\n q: b64urlMember(parsed, 'q'),\n qi: b64urlMember(parsed, 'qi'),\n };\n let key: CryptoKey;\n try {\n const imported = await importJWK(jwk, 'RS256', { extractable: false });\n if (imported instanceof Uint8Array) throw configError();\n key = imported;\n } catch {\n throw configError();\n }\n return { key, public: await toSigningKey(pub) };\n}\n\n/** Retired verification keys: public RSA members only; any private member is a misconfiguration. */\nasync function previousKeys(raw: string | undefined): Promise<PublicSigningKey[]> {\n if (raw === undefined || raw.trim().length === 0) return [];\n const parsed = parseJson(raw);\n if (!isRecord(parsed) || !Array.isArray(parsed.keys)) throw configError();\n const keys: PublicSigningKey[] = [];\n for (const entry of parsed.keys) {\n if (!isRecord(entry)) throw configError();\n checkRsaMetadata(entry);\n if (PRIVATE_RSA_MEMBERS.some((member) => member in entry)) throw configError();\n keys.push(await toSigningKey(publicMembers(entry)));\n }\n return keys;\n}\n\nconst configs = new WeakMap<AuthWorkerEnv, Promise<AuthConfig>>();\n\nasync function buildConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const appEnvRaw = requireString(env.APP_ENV);\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError();\n const appEnv: AuthAppEnv = appEnvRaw;\n\n const allowedOrigins = new Set(\n requireString(env.ALLOWED_ORIGINS)\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n .map((origin) => validateOrigin(origin, appEnv)),\n );\n if (allowedOrigins.size === 0) throw configError();\n\n const publicOrigin = validateOrigin(requireString(env.PUBLIC_ORIGIN), appEnv);\n // The issuer is matched as the exact configured string (a trailing slash is meaningful).\n const issuer = requireString(env.AUTH_ISSUER);\n validateUrl(issuer, appEnv);\n const audience = requireString(env.AUDIENCE);\n const appName = requireString(env.APP_NAME);\n if (appName.length > APP_NAME_MAX || /[\\p{Cc}\\p{Cs}]/u.test(appName)) throw configError();\n const senderAddress = requireString(env.SENDER_ADDRESS);\n if (!isWellFormedEmail(senderAddress)) throw configError();\n if (typeof env.OPERATOR_EMAILS !== 'string') throw configError();\n const operatorEmails = new Set(\n env.OPERATOR_EMAILS.split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n );\n for (const email of operatorEmails) if (!isWellFormedEmail(email)) throw configError();\n\n requireBinding(env.AUTH_DB, 'prepare');\n requireBinding(env.AUTH_DB, 'batch');\n requireBinding(env.AUTH_EMAIL, 'send');\n requireBinding(env.AUTH_ARCHIVE, 'fetch');\n\n const active = await activeKey(requireString(env.AUTH_JWT_PRIVATE_JWK));\n try {\n const proof = await new SignJWT({}).setProtectedHeader({ alg: 'RS256', kid: active.public.kid }).sign(active.key);\n await jwtVerify(proof, createLocalJWKSet({ keys: [active.public] }), { algorithms: ['RS256'] });\n } catch {\n throw configError();\n }\n const retired = (await previousKeys(env.AUTH_JWT_PREVIOUS_PUBLIC_JWKS)).filter(\n (key) => key.kid !== active.public.kid,\n );\n const jwks = [\n active.public,\n ...retired.filter((key, i) => retired.findIndex((other) => other.kid === key.kid) === i),\n ];\n\n return {\n allowedOrigins,\n appEnv,\n appName,\n audience,\n issuer,\n jwks,\n operatorEmails,\n publicOrigin,\n senderAddress,\n signing: { key: active.key, kid: active.public.kid },\n verifier: createLocalJWKSet({ keys: [...jwks] }),\n };\n}\n\n/**\n * Validates the env and imports its keys once per bindings object. A failed\n * validation is not cached, so a corrected binding set recovers without a restart.\n */\nexport function authConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const cached = configs.get(env);\n if (cached !== undefined) return cached;\n const pending = buildConfig(env);\n configs.set(env, pending);\n pending.catch(() => configs.delete(env));\n return pending;\n}\n"],"mappings":"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"}
@@ -30,7 +30,7 @@ export interface AuthWorkerEnv {
30
30
  ALLOWED_ORIGINS: string;
31
31
  /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */
32
32
  APP_ENV: string;
33
- /** The app's display name: the magic-link email's subject and copy name it. */
33
+ /** The app's display name in magic-link email and escaped confirmation-page branding. */
34
34
  APP_NAME: string;
35
35
  AUDIENCE: string;
36
36
  /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */
@@ -50,7 +50,7 @@ export interface AuthWorkerEnv {
50
50
  AUTH_JWT_PRIVATE_JWK: string;
51
51
  /** Comma-separated emails whose sessions carry the operator role claim. */
52
52
  OPERATOR_EMAILS: string;
53
- /** The app's public origin: magic links, the signed-in redirect and the catalog erasure URL point here. */
53
+ /** The app's public origin: magic links, app recovery, signed-in navigation and catalog erasure point here. */
54
54
  PUBLIC_ORIGIN: string;
55
55
  /** The address emails come from: a domain onboarded to Cloudflare Email Service. */
56
56
  SENDER_ADDRESS: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EAEd,iBAAiB,EAGjB,KAAK,cAAc,EAGpB,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,4FAA4F;AAC5F,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7G;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,2FAA2F;IAC3F,eAAe,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,6GAA6G;IAC7G,YAAY,EAAE,kBAAkB,CAAC;IACjC,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;IACtB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,sGAAsG;IACtG,oBAAoB,EAAE,MAAM,CAAC;IAC7B,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,2GAA2G;IAC3G,aAAa,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAC;AAEhD,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,KAAK,CAAC;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,IAAI,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE;QAAE,GAAG,EAAE,SAAS,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,QAAQ,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;CAChD;AAcD,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,OAGU,CAAC;AAqL7D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAOlE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EAEd,iBAAiB,EAGjB,KAAK,cAAc,EAGpB,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,4FAA4F;AAC5F,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7G;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,2FAA2F;IAC3F,eAAe,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,6GAA6G;IAC7G,YAAY,EAAE,kBAAkB,CAAC;IACjC,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;IACtB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,sGAAsG;IACtG,oBAAoB,EAAE,MAAM,CAAC;IAC7B,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,aAAa,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAC;AAEhD,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,KAAK,CAAC;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,IAAI,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE;QAAE,GAAG,EAAE,SAAS,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,QAAQ,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;CAChD;AAcD,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,OAGU,CAAC;AAqL7D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAOlE"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":[],"sources":["../../src/auth/config.ts"],"sourcesContent":["import {\n type CryptoKey,\n calculateJwkThumbprint,\n createLocalJWKSet,\n importJWK,\n type JWK_RSA_Private,\n type JWK_RSA_Public,\n jwtVerify,\n SignJWT,\n} from 'jose';\nimport type { D1Like } from '../worker/env';\nimport type { ApiError } from '../worker/http';\nimport { AuthOperationError } from './errors';\n\n/** The send-email binding subset the issuer calls; the runtime's SendEmail satisfies it. */\nexport interface SendEmail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise<unknown>;\n}\n\n/**\n * The catalog Worker as the issuer reaches it: a Cloudflare service binding (or any\n * `fetch`-shaped stub). Account erasure calls the catalog's `DELETE /api/v1/me/account`\n * through it before removing the issuer's own row.\n */\nexport interface AuthArchiveBinding {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The bindings the auth issuer reads. Declared structurally, like the catalog\n * Worker's Env: a deployment's generated `ExportedHandler<Env>` accepts the factory's\n * result as-is, with no `@cloudflare/workers-types` dependency. Every value is\n * validated on first use per bindings object; a missing or malformed one answers\n * 503 `unavailable` on every route, never a permissive fallback.\n */\nexport interface AuthWorkerEnv {\n /** Comma-separated exact origins allowed to call unsafe routes and read CORS responses. */\n ALLOWED_ORIGINS: string;\n /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */\n APP_ENV: string;\n /** The app's display name: the magic-link email's subject and copy name it. */\n APP_NAME: string;\n AUDIENCE: string;\n /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */\n AUTH_ARCHIVE: AuthArchiveBinding;\n /** The issuer's own D1: accounts, links, per-account send and singleton admission budgets. */\n AUTH_DB: D1Like;\n AUTH_EMAIL: SendEmail;\n /** The `iss` claim every token carries, exactly as the catalog Worker's `JWT_ISSUER` knows it. */\n AUTH_ISSUER: string;\n /**\n * Optional JWKS JSON (`{\"keys\":[...]}`) of retired PUBLIC RS256 keys, still served\n * from `/jwks.json` and accepted by the issuer so sessions minted before a rotation\n * keep verifying until they expire. Private members are rejected.\n */\n AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;\n /** The active RS256 private key as a JWK JSON string (a wrangler secret): signs every new session. */\n AUTH_JWT_PRIVATE_JWK: string;\n /** Comma-separated emails whose sessions carry the operator role claim. */\n OPERATOR_EMAILS: string;\n /** The app's public origin: magic links, the signed-in redirect and the catalog erasure URL point here. */\n PUBLIC_ORIGIN: string;\n /** The address emails come from: a domain onboarded to Cloudflare Email Service. */\n SENDER_ADDRESS: string;\n}\n\nexport type AuthAppEnv = 'local' | 'production';\n\nexport interface PublicSigningKey extends JWK_RSA_Public {\n alg: 'RS256';\n kid: string;\n kty: 'RSA';\n use: 'sig';\n}\n\nexport interface AuthConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AuthAppEnv;\n appName: string;\n audience: string;\n issuer: string;\n /** Every key a session may be verified with: the active key first, then retired ones. */\n jwks: readonly PublicSigningKey[];\n operatorEmails: ReadonlySet<string>;\n publicOrigin: string;\n senderAddress: string;\n signing: { key: CryptoKey; kid: string };\n verifier: ReturnType<typeof createLocalJWKSet>;\n}\n\n// Configuration faults never name the offending field: the response must not leak\n// Env names, only a generic temporary unavailability.\nconst configError = (): ApiError => new AuthOperationError(503, 'configuration');\n\nconst EMAIL_MAX = 320;\nconst APP_NAME_MAX = 64;\n// 2048-bit RSA modulus: 256 bytes, 342 base64url characters unpadded.\nconst MIN_MODULUS_CHARS = 342;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\nconst PRIVATE_RSA_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'] as const;\n\nexport const isWellFormedEmail = (value: string): boolean =>\n value.length <= EMAIL_MAX &&\n value === value.toLowerCase() &&\n /^[^\\s@<>\"',;]+@[^\\s@<>\"',;]+\\.[^\\s@<>\"',;]+$/.test(value);\n\nconst requireString = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0) throw configError();\n return value;\n};\n\nconst requireBinding = (value: unknown, method: string): void => {\n if (typeof value !== 'object' || value === null) throw configError();\n if (typeof Reflect.get(value, method) !== 'function') throw configError();\n};\n\n/** Transport rules: local is loopback HTTP only, production is non-loopback HTTPS; no credentials or fragments. */\nconst validateUrl = (raw: string, appEnv: AuthAppEnv): URL => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw configError();\n }\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError();\n const loopback = LOOPBACK_HOSTS.has(url.hostname);\n if (appEnv === 'local' ? url.protocol !== 'http:' || !loopback : url.protocol !== 'https:' || loopback) {\n throw configError();\n }\n return url;\n};\n\n/** An exact canonical origin: scheme + host (+port), no path, query, wildcard or `null`. */\nconst validateOrigin = (raw: string, appEnv: AuthAppEnv): string => {\n const url = validateUrl(raw, appEnv);\n if (url.pathname !== '/' || url.search !== '' || url.origin !== raw) throw configError();\n return url.origin;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n throw configError();\n }\n};\n\nconst b64urlMember = (jwk: Record<string, unknown>, name: string): string => {\n const value = jwk[name];\n if (typeof value !== 'string' || !BASE64URL.test(value)) throw configError();\n return value;\n};\n\n/** RS256 signing-key metadata, when present, must agree with how the issuer uses the key. */\nconst checkRsaMetadata = (jwk: Record<string, unknown>): void => {\n if (jwk.kty !== 'RSA') throw configError();\n if (jwk.alg !== undefined && jwk.alg !== 'RS256') throw configError();\n if (jwk.use !== undefined && jwk.use !== 'sig') throw configError();\n};\n\nconst publicMembers = (jwk: Record<string, unknown>): JWK_RSA_Public => {\n const n = b64urlMember(jwk, 'n');\n if (n.length < MIN_MODULUS_CHARS) throw configError();\n return { e: b64urlMember(jwk, 'e'), kty: 'RSA', n };\n};\n\nconst toSigningKey = async (pub: JWK_RSA_Public): Promise<PublicSigningKey> => ({\n alg: 'RS256',\n e: pub.e,\n kid: await calculateJwkThumbprint({ e: pub.e, kty: 'RSA', n: pub.n }),\n kty: 'RSA',\n n: pub.n,\n use: 'sig',\n});\n\nasync function activeKey(raw: string): Promise<{ key: CryptoKey; public: PublicSigningKey }> {\n const parsed = parseJson(raw);\n if (!isRecord(parsed)) throw configError();\n checkRsaMetadata(parsed);\n const pub = publicMembers(parsed);\n const jwk: JWK_RSA_Private = {\n ...pub,\n d: b64urlMember(parsed, 'd'),\n dp: b64urlMember(parsed, 'dp'),\n dq: b64urlMember(parsed, 'dq'),\n p: b64urlMember(parsed, 'p'),\n q: b64urlMember(parsed, 'q'),\n qi: b64urlMember(parsed, 'qi'),\n };\n let key: CryptoKey;\n try {\n const imported = await importJWK(jwk, 'RS256', { extractable: false });\n if (imported instanceof Uint8Array) throw configError();\n key = imported;\n } catch {\n throw configError();\n }\n return { key, public: await toSigningKey(pub) };\n}\n\n/** Retired verification keys: public RSA members only; any private member is a misconfiguration. */\nasync function previousKeys(raw: string | undefined): Promise<PublicSigningKey[]> {\n if (raw === undefined || raw.trim().length === 0) return [];\n const parsed = parseJson(raw);\n if (!isRecord(parsed) || !Array.isArray(parsed.keys)) throw configError();\n const keys: PublicSigningKey[] = [];\n for (const entry of parsed.keys) {\n if (!isRecord(entry)) throw configError();\n checkRsaMetadata(entry);\n if (PRIVATE_RSA_MEMBERS.some((member) => member in entry)) throw configError();\n keys.push(await toSigningKey(publicMembers(entry)));\n }\n return keys;\n}\n\nconst configs = new WeakMap<AuthWorkerEnv, Promise<AuthConfig>>();\n\nasync function buildConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const appEnvRaw = requireString(env.APP_ENV);\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError();\n const appEnv: AuthAppEnv = appEnvRaw;\n\n const allowedOrigins = new Set(\n requireString(env.ALLOWED_ORIGINS)\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n .map((origin) => validateOrigin(origin, appEnv)),\n );\n if (allowedOrigins.size === 0) throw configError();\n\n const publicOrigin = validateOrigin(requireString(env.PUBLIC_ORIGIN), appEnv);\n // The issuer is matched as the exact configured string (a trailing slash is meaningful).\n const issuer = requireString(env.AUTH_ISSUER);\n validateUrl(issuer, appEnv);\n const audience = requireString(env.AUDIENCE);\n const appName = requireString(env.APP_NAME);\n if (appName.length > APP_NAME_MAX || /[\\p{Cc}\\p{Cs}]/u.test(appName)) throw configError();\n const senderAddress = requireString(env.SENDER_ADDRESS);\n if (!isWellFormedEmail(senderAddress)) throw configError();\n if (typeof env.OPERATOR_EMAILS !== 'string') throw configError();\n const operatorEmails = new Set(\n env.OPERATOR_EMAILS.split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n );\n for (const email of operatorEmails) if (!isWellFormedEmail(email)) throw configError();\n\n requireBinding(env.AUTH_DB, 'prepare');\n requireBinding(env.AUTH_DB, 'batch');\n requireBinding(env.AUTH_EMAIL, 'send');\n requireBinding(env.AUTH_ARCHIVE, 'fetch');\n\n const active = await activeKey(requireString(env.AUTH_JWT_PRIVATE_JWK));\n try {\n const proof = await new SignJWT({}).setProtectedHeader({ alg: 'RS256', kid: active.public.kid }).sign(active.key);\n await jwtVerify(proof, createLocalJWKSet({ keys: [active.public] }), { algorithms: ['RS256'] });\n } catch {\n throw configError();\n }\n const retired = (await previousKeys(env.AUTH_JWT_PREVIOUS_PUBLIC_JWKS)).filter(\n (key) => key.kid !== active.public.kid,\n );\n const jwks = [\n active.public,\n ...retired.filter((key, i) => retired.findIndex((other) => other.kid === key.kid) === i),\n ];\n\n return {\n allowedOrigins,\n appEnv,\n appName,\n audience,\n issuer,\n jwks,\n operatorEmails,\n publicOrigin,\n senderAddress,\n signing: { key: active.key, kid: active.public.kid },\n verifier: createLocalJWKSet({ keys: [...jwks] }),\n };\n}\n\n/**\n * Validates the env and imports its keys once per bindings object. A failed\n * validation is not cached, so a corrected binding set recovers without a restart.\n */\nexport function authConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const cached = configs.get(env);\n if (cached !== undefined) return cached;\n const pending = buildConfig(env);\n configs.set(env, pending);\n pending.catch(() => configs.delete(env));\n return pending;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../src/auth/config.ts"],"sourcesContent":["import {\n type CryptoKey,\n calculateJwkThumbprint,\n createLocalJWKSet,\n importJWK,\n type JWK_RSA_Private,\n type JWK_RSA_Public,\n jwtVerify,\n SignJWT,\n} from 'jose';\nimport type { D1Like } from '../worker/env';\nimport type { ApiError } from '../worker/http';\nimport { AuthOperationError } from './errors';\n\n/** The send-email binding subset the issuer calls; the runtime's SendEmail satisfies it. */\nexport interface SendEmail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise<unknown>;\n}\n\n/**\n * The catalog Worker as the issuer reaches it: a Cloudflare service binding (or any\n * `fetch`-shaped stub). Account erasure calls the catalog's `DELETE /api/v1/me/account`\n * through it before removing the issuer's own row.\n */\nexport interface AuthArchiveBinding {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The bindings the auth issuer reads. Declared structurally, like the catalog\n * Worker's Env: a deployment's generated `ExportedHandler<Env>` accepts the factory's\n * result as-is, with no `@cloudflare/workers-types` dependency. Every value is\n * validated on first use per bindings object; a missing or malformed one answers\n * 503 `unavailable` on every route, never a permissive fallback.\n */\nexport interface AuthWorkerEnv {\n /** Comma-separated exact origins allowed to call unsafe routes and read CORS responses. */\n ALLOWED_ORIGINS: string;\n /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */\n APP_ENV: string;\n /** The app's display name in magic-link email and escaped confirmation-page branding. */\n APP_NAME: string;\n AUDIENCE: string;\n /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */\n AUTH_ARCHIVE: AuthArchiveBinding;\n /** The issuer's own D1: accounts, links, per-account send and singleton admission budgets. */\n AUTH_DB: D1Like;\n AUTH_EMAIL: SendEmail;\n /** The `iss` claim every token carries, exactly as the catalog Worker's `JWT_ISSUER` knows it. */\n AUTH_ISSUER: string;\n /**\n * Optional JWKS JSON (`{\"keys\":[...]}`) of retired PUBLIC RS256 keys, still served\n * from `/jwks.json` and accepted by the issuer so sessions minted before a rotation\n * keep verifying until they expire. Private members are rejected.\n */\n AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;\n /** The active RS256 private key as a JWK JSON string (a wrangler secret): signs every new session. */\n AUTH_JWT_PRIVATE_JWK: string;\n /** Comma-separated emails whose sessions carry the operator role claim. */\n OPERATOR_EMAILS: string;\n /** The app's public origin: magic links, app recovery, signed-in navigation and catalog erasure point here. */\n PUBLIC_ORIGIN: string;\n /** The address emails come from: a domain onboarded to Cloudflare Email Service. */\n SENDER_ADDRESS: string;\n}\n\nexport type AuthAppEnv = 'local' | 'production';\n\nexport interface PublicSigningKey extends JWK_RSA_Public {\n alg: 'RS256';\n kid: string;\n kty: 'RSA';\n use: 'sig';\n}\n\nexport interface AuthConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AuthAppEnv;\n appName: string;\n audience: string;\n issuer: string;\n /** Every key a session may be verified with: the active key first, then retired ones. */\n jwks: readonly PublicSigningKey[];\n operatorEmails: ReadonlySet<string>;\n publicOrigin: string;\n senderAddress: string;\n signing: { key: CryptoKey; kid: string };\n verifier: ReturnType<typeof createLocalJWKSet>;\n}\n\n// Configuration faults never name the offending field: the response must not leak\n// Env names, only a generic temporary unavailability.\nconst configError = (): ApiError => new AuthOperationError(503, 'configuration');\n\nconst EMAIL_MAX = 320;\nconst APP_NAME_MAX = 64;\n// 2048-bit RSA modulus: 256 bytes, 342 base64url characters unpadded.\nconst MIN_MODULUS_CHARS = 342;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\nconst PRIVATE_RSA_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'] as const;\n\nexport const isWellFormedEmail = (value: string): boolean =>\n value.length <= EMAIL_MAX &&\n value === value.toLowerCase() &&\n /^[^\\s@<>\"',;]+@[^\\s@<>\"',;]+\\.[^\\s@<>\"',;]+$/.test(value);\n\nconst requireString = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0) throw configError();\n return value;\n};\n\nconst requireBinding = (value: unknown, method: string): void => {\n if (typeof value !== 'object' || value === null) throw configError();\n if (typeof Reflect.get(value, method) !== 'function') throw configError();\n};\n\n/** Transport rules: local is loopback HTTP only, production is non-loopback HTTPS; no credentials or fragments. */\nconst validateUrl = (raw: string, appEnv: AuthAppEnv): URL => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw configError();\n }\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError();\n const loopback = LOOPBACK_HOSTS.has(url.hostname);\n if (appEnv === 'local' ? url.protocol !== 'http:' || !loopback : url.protocol !== 'https:' || loopback) {\n throw configError();\n }\n return url;\n};\n\n/** An exact canonical origin: scheme + host (+port), no path, query, wildcard or `null`. */\nconst validateOrigin = (raw: string, appEnv: AuthAppEnv): string => {\n const url = validateUrl(raw, appEnv);\n if (url.pathname !== '/' || url.search !== '' || url.origin !== raw) throw configError();\n return url.origin;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n throw configError();\n }\n};\n\nconst b64urlMember = (jwk: Record<string, unknown>, name: string): string => {\n const value = jwk[name];\n if (typeof value !== 'string' || !BASE64URL.test(value)) throw configError();\n return value;\n};\n\n/** RS256 signing-key metadata, when present, must agree with how the issuer uses the key. */\nconst checkRsaMetadata = (jwk: Record<string, unknown>): void => {\n if (jwk.kty !== 'RSA') throw configError();\n if (jwk.alg !== undefined && jwk.alg !== 'RS256') throw configError();\n if (jwk.use !== undefined && jwk.use !== 'sig') throw configError();\n};\n\nconst publicMembers = (jwk: Record<string, unknown>): JWK_RSA_Public => {\n const n = b64urlMember(jwk, 'n');\n if (n.length < MIN_MODULUS_CHARS) throw configError();\n return { e: b64urlMember(jwk, 'e'), kty: 'RSA', n };\n};\n\nconst toSigningKey = async (pub: JWK_RSA_Public): Promise<PublicSigningKey> => ({\n alg: 'RS256',\n e: pub.e,\n kid: await calculateJwkThumbprint({ e: pub.e, kty: 'RSA', n: pub.n }),\n kty: 'RSA',\n n: pub.n,\n use: 'sig',\n});\n\nasync function activeKey(raw: string): Promise<{ key: CryptoKey; public: PublicSigningKey }> {\n const parsed = parseJson(raw);\n if (!isRecord(parsed)) throw configError();\n checkRsaMetadata(parsed);\n const pub = publicMembers(parsed);\n const jwk: JWK_RSA_Private = {\n ...pub,\n d: b64urlMember(parsed, 'd'),\n dp: b64urlMember(parsed, 'dp'),\n dq: b64urlMember(parsed, 'dq'),\n p: b64urlMember(parsed, 'p'),\n q: b64urlMember(parsed, 'q'),\n qi: b64urlMember(parsed, 'qi'),\n };\n let key: CryptoKey;\n try {\n const imported = await importJWK(jwk, 'RS256', { extractable: false });\n if (imported instanceof Uint8Array) throw configError();\n key = imported;\n } catch {\n throw configError();\n }\n return { key, public: await toSigningKey(pub) };\n}\n\n/** Retired verification keys: public RSA members only; any private member is a misconfiguration. */\nasync function previousKeys(raw: string | undefined): Promise<PublicSigningKey[]> {\n if (raw === undefined || raw.trim().length === 0) return [];\n const parsed = parseJson(raw);\n if (!isRecord(parsed) || !Array.isArray(parsed.keys)) throw configError();\n const keys: PublicSigningKey[] = [];\n for (const entry of parsed.keys) {\n if (!isRecord(entry)) throw configError();\n checkRsaMetadata(entry);\n if (PRIVATE_RSA_MEMBERS.some((member) => member in entry)) throw configError();\n keys.push(await toSigningKey(publicMembers(entry)));\n }\n return keys;\n}\n\nconst configs = new WeakMap<AuthWorkerEnv, Promise<AuthConfig>>();\n\nasync function buildConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const appEnvRaw = requireString(env.APP_ENV);\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError();\n const appEnv: AuthAppEnv = appEnvRaw;\n\n const allowedOrigins = new Set(\n requireString(env.ALLOWED_ORIGINS)\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n .map((origin) => validateOrigin(origin, appEnv)),\n );\n if (allowedOrigins.size === 0) throw configError();\n\n const publicOrigin = validateOrigin(requireString(env.PUBLIC_ORIGIN), appEnv);\n // The issuer is matched as the exact configured string (a trailing slash is meaningful).\n const issuer = requireString(env.AUTH_ISSUER);\n validateUrl(issuer, appEnv);\n const audience = requireString(env.AUDIENCE);\n const appName = requireString(env.APP_NAME);\n if (appName.length > APP_NAME_MAX || /[\\p{Cc}\\p{Cs}]/u.test(appName)) throw configError();\n const senderAddress = requireString(env.SENDER_ADDRESS);\n if (!isWellFormedEmail(senderAddress)) throw configError();\n if (typeof env.OPERATOR_EMAILS !== 'string') throw configError();\n const operatorEmails = new Set(\n env.OPERATOR_EMAILS.split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n );\n for (const email of operatorEmails) if (!isWellFormedEmail(email)) throw configError();\n\n requireBinding(env.AUTH_DB, 'prepare');\n requireBinding(env.AUTH_DB, 'batch');\n requireBinding(env.AUTH_EMAIL, 'send');\n requireBinding(env.AUTH_ARCHIVE, 'fetch');\n\n const active = await activeKey(requireString(env.AUTH_JWT_PRIVATE_JWK));\n try {\n const proof = await new SignJWT({}).setProtectedHeader({ alg: 'RS256', kid: active.public.kid }).sign(active.key);\n await jwtVerify(proof, createLocalJWKSet({ keys: [active.public] }), { algorithms: ['RS256'] });\n } catch {\n throw configError();\n }\n const retired = (await previousKeys(env.AUTH_JWT_PREVIOUS_PUBLIC_JWKS)).filter(\n (key) => key.kid !== active.public.kid,\n );\n const jwks = [\n active.public,\n ...retired.filter((key, i) => retired.findIndex((other) => other.kid === key.kid) === i),\n ];\n\n return {\n allowedOrigins,\n appEnv,\n appName,\n audience,\n issuer,\n jwks,\n operatorEmails,\n publicOrigin,\n senderAddress,\n signing: { key: active.key, kid: active.public.kid },\n verifier: createLocalJWKSet({ keys: [...jwks] }),\n };\n}\n\n/**\n * Validates the env and imports its keys once per bindings object. A failed\n * validation is not cached, so a corrected binding set recovers without a restart.\n */\nexport function authConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const cached = configs.get(env);\n if (cached !== undefined) return cached;\n const pending = buildConfig(env);\n configs.set(env, pending);\n pending.catch(() => configs.delete(env));\n return pending;\n}\n"],"mappings":"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"}
@@ -1,16 +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("./config.cjs"),o=require("./store.cjs");let s=require("jose");var c={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$/},l={account:[`GET`,`DELETE`],health:[`GET`],jwks:[`GET`],session:[`GET`],signIn:[`POST`],signOutAll:[`POST`],verify:[`GET`,`POST`]},u={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(c).find(t=>c[t].test(new URL(e.url).pathname)),d=32,f=/^[A-Za-z0-9_-]{43}$/,p=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,m=256,h=5,randomToken=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(d)))).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``),escapeHtml=e=>e.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`),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?900:t.SESSION_MAX_AGE_SECONDS;return{maxAge:a,token:await new s.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,s.jwtVerify)(a,n.verifier,{algorithms:[`RS256`],audience:n.audience,clockTolerance:h,issuer:n.issuer,maxTokenAge:t.SESSION_MAX_AGE_SECONDS,requiredClaims:[`sub`,`iss`,`aud`,`iat`,`exp`,`sessionVersion`]}),c=e.sub??``;if(c.length===0||c.length>m||!t.isWellFormedText(c))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+h||Array.isArray(e.roles)&&e.roles.includes(`archive-operator`)&&(typeof e.exp!=`number`||e.exp-e.iat>900||Date.now()/1e3-e.iat>900+h))throw r.unauthenticated();let u=await o.readAccount(i.AUTH_DB,c);if(u===null||u.session_version!==l)throw r.unauthenticated();return{sessionVersion:l,subject:c,token:a}}catch(e){throw e instanceof s.errors.JOSEError?r.unauthenticated():e}}async function sendLink(e,t,n,r){let i=t.appName,a=`The link works once and expires in ${o.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 ${escapeHtml(i)}:</p><p><a href="${escapeHtml(r)}">${escapeHtml(r)}</a></p><p>${a}</p>`,subject:`Your ${i} sign-in link`,text:`Open this link to sign in to ${i}:\n\n${r}\n\n${a}`,to:n})}async function handleSignIn(t,n,s){requireMutationGuard(t,s);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 o.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 o.ensureUser(n.AUTH_DB,d),h=await o.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 o.insertToken(n.AUTH_DB,_,m.id,f);try{await sendLink(n,s,m.email,`${s.publicOrigin}/api/v1/auth/verify?token=${g}`)}catch(e){throw await o.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,a){if(n.method===`GET`){let e=r.queryParams(new URL(n.url),[`token`]).get(`token`)??``;if(!f.test(e))throw r.badRequest(`The link is not valid.`);let i=randomToken(),o=`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Confirm sign-in</title><h1>Sign in to ${escapeHtml(a.appName)}</h1><p>Continue only if you requested this sign-in link.</p><button type="button" id="confirm">Confirm sign-in</button><p role="status" id="status"></p><script nonce="${i}">
2
- history.replaceState(null, '', location.pathname);
3
- document.getElementById('confirm').addEventListener('click', async () => {
4
- const button = document.getElementById('confirm');
5
- button.disabled = true;
6
- try {
7
- const response = await fetch(location.pathname, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', '${t.REQUEST_MARKER_HEADER}': '1' }, body: JSON.stringify({ token: '${e}' }) });
8
- if (!response.ok) throw new Error();
9
- location.replace(${JSON.stringify(`${a.publicOrigin}/signed-in`)});
10
- } catch {
11
- document.getElementById('status').textContent = 'Sign-in failed. Request a new link or try again.';
12
- button.disabled = false;
13
- }
14
- });
15
- <\/script></html>`;return new Response(o,{headers:{...r.noStore,"content-security-policy":`default-src 'none'; script-src 'nonce-${i}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,"content-type":`text/html; charset=utf-8`,"referrer-policy":`no-referrer`,"x-content-type-options":`nosniff`}})}requireMutationGuard(n,a,!0),r.queryParams(new URL(n.url),[]);let s=await r.readJsonBody(n);if(typeof s!=`object`||!s||Array.isArray(s)||Object.keys(s).length!==1)throw r.badRequest(`The link is not valid.`);let c=Reflect.get(s,`token`);if(typeof c!=`string`||!f.test(c))throw r.badRequest(`The link is not valid.`);let l=await o.consumeToken(i.AUTH_DB,await e.sha256Hex(c),Date.now());if(l===null)throw new r.ApiError(410,`gone`,`That link has expired or was already used.`);let u=await mintSession(a,l.id,a.operatorEmails.has(l.email),l.sessionVersion);return new Response(null,{headers:{...r.noStore,"set-cookie":t.sessionCookieValue(u.token,{maxAgeSeconds:u.maxAge,secure:secure(a)})},status:204})}async function handleExport(e,t,n){let{subject:i}=await requireSession(e,n,t),a=await o.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,a,s){let c=requireMutationGuard(e,s),{subject:l,token:u}=await requireSession(e,s,a),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`}),m=e.headers.get(t.PUBLISHER_HEADER);if(m!==null){if(!p.test(m))throw r.badRequest();f.set(t.PUBLISHER_HEADER,m)}let h;try{h=await n.serviceRequest(a.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 o.deleteAccount(a.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:a,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!==a)throw new r.ApiError(409,`account-changed`,`The signed-in account changed since this request was prepared.`);if(await o.revokeSessions(n.AUTH_DB,a,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 a.authConfig(e)}catch{n=`configuration-unavailable`}if(n===null)try{i=await o.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,l[i]);if(!l[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 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 o=crypto.randomUUID(),s=Date.now(),c,l;try{c=await route(n,i)}catch(e){l=e,c=r.toErrorResponse(e,o);let t=await a.authConfig(i).catch(()=>null);t!==null&&(c=withCors(c,corsHeaders(n,t)))}if(matchRoute(n)===`verify`&&(c.headers.set(`cache-control`,`no-store`),c.headers.set(`referrer-policy`,`no-referrer`),c.headers.set(`x-content-type-options`,`nosniff`),c.headers.has(`content-security-policy`)||c.headers.set(`content-security-policy`,`default-src 'none'; base-uri 'none'; frame-ancestors 'none'`)),e.size>0){let e=matchRoute(n),i=c.status<400?`ok`:e===`health`?`unavailable`:l instanceof r.ApiError?l.code:`internal`,a=l===void 0&&c.status>=400?t.archiveErrorResponseSchema.safeParse(await c.clone().json().catch(()=>null)):null,d=e===`health`&&c.status===503?t.archiveAuthHealthSchema.safeParse(await c.clone().json().catch(()=>null)):null;emit({code:a?.success?a.data.error.code:i,ms:Math.max(0,Date.now()-s),requestId:o,route:e===void 0?`unmatched`:u[e],status:c.status,type:`request`,...c.status>=500?{diagnostic:d?.success?{kind:d.data.reason===`configuration-unavailable`?`configuration`:d.data.reason===`schema-mismatch`?`schema`:`database`}:l===void 0?{kind:`catalog`}:diagnostic(l)}:{}})}return c},async scheduled(t,n){try{await a.authConfig(n);let t=await o.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("./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?900: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>900||Date.now()/1e3-e.iat>900+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;
16
2
  //# sourceMappingURL=worker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.cjs","names":[],"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 { 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 are short-lived so a revoked\n * operator loses authority within minutes. 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 escapeHtml = (value: string): string => value.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\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 const nonce = randomToken();\n // No automatic redemption, external assets or referrers. Only a deliberate\n // button click makes the same-origin marked POST; scanners can safely GET.\n const html = `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Confirm sign-in</title><h1>Sign in to ${escapeHtml(config.appName)}</h1><p>Continue only if you requested this sign-in link.</p><button type=\"button\" id=\"confirm\">Confirm sign-in</button><p role=\"status\" id=\"status\"></p><script nonce=\"${nonce}\">\nhistory.replaceState(null, '', location.pathname);\ndocument.getElementById('confirm').addEventListener('click', async () => {\n const button = document.getElementById('confirm');\n button.disabled = true;\n try {\n const response = await fetch(location.pathname, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', '${REQUEST_MARKER_HEADER}': '1' }, body: JSON.stringify({ token: '${token}' }) });\n if (!response.ok) throw new Error();\n location.replace(${JSON.stringify(`${config.publicOrigin}/signed-in`)});\n } catch {\n document.getElementById('status').textContent = 'Sign-in failed. Request a new link or try again.';\n button.disabled = false;\n }\n});\n</script></html>`;\n return new Response(html, {\n headers: {\n ...noStore,\n 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n 'content-type': 'text/html; charset=utf-8',\n 'referrer-policy': 'no-referrer',\n 'x-content-type-options': 'nosniff',\n },\n });\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) response = withCors(response, corsHeaders(request, config));\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":[],"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 are short-lived so a revoked\n * operator loses authority within minutes. 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 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/auth/worker.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAYtB,MAAM,aAAa,CAAC;AAerB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAmB,KAAK,aAAa,EAAiC,MAAM,UAAU,CAAC;AAC9F,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,UAAU,CAAC;AAgBnE;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa;IACxE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClF,sFAAsF;IACtF,SAAS,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,MAAM,IAAI,CAAC;CACnG;AAED,MAAM,MAAM,SAAS,GACjB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EACV,eAAe,GACf,iBAAiB,GACjB,eAAe,GACf,cAAc,GACd,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,WAAW,CAAC;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC;CACtC,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;CAAE,CAAC;AAmanG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,aAAa,KAAK,iBAAiB,CAAC,CAAC,CAAC,CAoHhF"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/auth/worker.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAYtB,MAAM,aAAa,CAAC;AAerB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,EAAmB,KAAK,aAAa,EAAiC,MAAM,UAAU,CAAC;AAC9F,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,UAAU,CAAC;AAgBnE;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa;IACxE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClF,sFAAsF;IACtF,SAAS,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,MAAM,IAAI,CAAC;CACnG;AAED,MAAM,MAAM,SAAS,GACjB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EACV,eAAe,GACf,iBAAiB,GACjB,eAAe,GACf,cAAc,GACd,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,WAAW,CAAC;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC;CACtC,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;CAAE,CAAC;AAuYnG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,aAAa,KAAK,iBAAiB,CAAC,CAAC,CAAC,CA6HhF"}
@@ -1,16 +1,2 @@
1
- import{sha256Hex as e}from"../_crypto.js";import{ACCOUNT_HEADER as t,ARCHIVE_OPERATOR_ROLE as n,PUBLISHER_HEADER as r,REQUEST_MARKER_HEADER as i,SESSION_COOKIE_NAME as a,SESSION_MAX_AGE_SECONDS as o,archiveAuthHealthSchema as s,archiveErrorResponseSchema as c,isWellFormedText as l,sessionCookieValue as u,uuidSchema as d}from"../contract.js";import{serviceRequest as f}from"../worker/_service.js";import{ApiError as p,badRequest as m,expiredSessionCookie as h,forbidden as g,json as _,noStore as v,queryParams as y,readJsonBody as b,sessionToken as x,toErrorResponse as S,unauthenticated as C}from"../worker/http.js";import{AuthOperationError as w}from"./errors.js";import{authConfig as T,isWellFormedEmail as E}from"./config.js";import{LINK_TTL_MS as D,authReadiness as O,consumeToken as k,deleteAccount as A,deleteToken as j,ensureUser as M,insertToken as N,readAccount as P,revokeSessions as F,spendLinkBudget as I,spendSignInBudget as L,sweep as R}from"./store.js";import{SignJWT as z,errors as B,jwtVerify as V}from"jose";var H={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`]},W={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(H).find(t=>H[t].test(new URL(e.url).pathname)),G=32,K=/^[A-Za-z0-9_-]{43}$/,q=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,J=256,Y=5,randomToken=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(G)))).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``),escapeHtml=e=>e.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`),unavailable=()=>new w(503,`catalog`);function requireMutationGuard(e,t,n=!1){let r=e.headers.get(`origin`);if(r===null||!t.allowedOrigins.has(r)&&!(n&&r===t.publicOrigin)||e.headers.get(`x-archive-request`)!==`1`)throw g();return r}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,a){let o=corsHeaders(e,n);if(Object.keys(o).length===0)throw g();return new Response(null,{headers:{...o,"access-control-allow-headers":`Content-Type, ${t}, ${r}, ${i}`,"access-control-allow-methods":[...a,`OPTIONS`].join(`, `),"access-control-max-age":`600`},status:204})}var secure=e=>e.appEnv===`production`;async function mintSession(e,t,r,i){let a=r?900:o;return{maxAge:a,token:await new z({sessionVersion:i,...r?{roles:[n]}:{}}).setProtectedHeader({alg:`RS256`,kid:e.signing.kid,typ:`JWT`}).setIssuedAt().setJti(crypto.randomUUID()).setIssuer(e.issuer).setAudience(e.audience).setSubject(t).setExpirationTime(`${a}s`).sign(e.signing.key)}}async function requireSession(e,t,n){let r=x(e);if(r===null)throw C();try{let{payload:e}=await V(r,t.verifier,{algorithms:[`RS256`],audience:t.audience,clockTolerance:Y,issuer:t.issuer,maxTokenAge:o,requiredClaims:[`sub`,`iss`,`aud`,`iat`,`exp`,`sessionVersion`]}),i=e.sub??``;if(i.length===0||i.length>J||!l(i))throw C();let a=e.sessionVersion;if(typeof a!=`number`||!Number.isSafeInteger(a)||a<0||typeof e.iat!=`number`||e.iat>Date.now()/1e3+Y||Array.isArray(e.roles)&&e.roles.includes(`archive-operator`)&&(typeof e.exp!=`number`||e.exp-e.iat>900||Date.now()/1e3-e.iat>900+Y))throw C();let s=await P(n.AUTH_DB,i);if(s===null||s.session_version!==a)throw C();return{sessionVersion:a,subject:i,token:r}}catch(e){throw e instanceof B.JOSEError?C():e}}async function sendLink(e,t,n,r){let i=t.appName,a=`The link works once and expires in ${D/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 ${escapeHtml(i)}:</p><p><a href="${escapeHtml(r)}">${escapeHtml(r)}</a></p><p>${a}</p>`,subject:`Your ${i} sign-in link`,text:`Open this link to sign in to ${i}:\n\n${r}\n\n${a}`,to:n})}async function handleSignIn(t,n,r){requireMutationGuard(t,r);let i=await b(t);if(typeof i!=`object`||!i||Array.isArray(i))throw m();let a=Object.keys(i),o=Reflect.get(i,`email`);if(a.length!==1||a[0]!==`email`||typeof o!=`string`)throw m();let s=o.trim().toLowerCase();if(!E(s))throw m();let c=Date.now(),l=await L(n.AUTH_DB,c);if(l!==null)throw new p(429,`rate-limited`,`Too many sign-in requests. Try again later.`,{retryAfterSeconds:l});let u=await M(n.AUTH_DB,s),d=await I(n.AUTH_DB,u.id,c);if(d!==null)throw new p(429,`rate-limited`,`Too many sign-in emails. Try again later.`,{retryAfterSeconds:d});let f=randomToken(),h=await e(f);await N(n.AUTH_DB,h,u.id,c);try{await sendLink(n,r,u.email,`${r.publicOrigin}/api/v1/auth/verify?token=${f}`)}catch(e){throw await j(n.AUTH_DB,h).catch(()=>void 0),new w(503,`email`,{cause:e})}return _(202,{ok:!0},v)}async function handleVerify(t,n,r){if(t.method===`GET`){let e=y(new URL(t.url),[`token`]).get(`token`)??``;if(!K.test(e))throw m(`The link is not valid.`);let n=randomToken(),a=`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Confirm sign-in</title><h1>Sign in to ${escapeHtml(r.appName)}</h1><p>Continue only if you requested this sign-in link.</p><button type="button" id="confirm">Confirm sign-in</button><p role="status" id="status"></p><script nonce="${n}">
2
- history.replaceState(null, '', location.pathname);
3
- document.getElementById('confirm').addEventListener('click', async () => {
4
- const button = document.getElementById('confirm');
5
- button.disabled = true;
6
- try {
7
- const response = await fetch(location.pathname, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', '${i}': '1' }, body: JSON.stringify({ token: '${e}' }) });
8
- if (!response.ok) throw new Error();
9
- location.replace(${JSON.stringify(`${r.publicOrigin}/signed-in`)});
10
- } catch {
11
- document.getElementById('status').textContent = 'Sign-in failed. Request a new link or try again.';
12
- button.disabled = false;
13
- }
14
- });
15
- <\/script></html>`;return new Response(a,{headers:{...v,"content-security-policy":`default-src 'none'; script-src 'nonce-${n}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,"content-type":`text/html; charset=utf-8`,"referrer-policy":`no-referrer`,"x-content-type-options":`nosniff`}})}requireMutationGuard(t,r,!0),y(new URL(t.url),[]);let a=await b(t);if(typeof a!=`object`||!a||Array.isArray(a)||Object.keys(a).length!==1)throw m(`The link is not valid.`);let o=Reflect.get(a,`token`);if(typeof o!=`string`||!K.test(o))throw m(`The link is not valid.`);let s=await k(n.AUTH_DB,await e(o),Date.now());if(s===null)throw new p(410,`gone`,`That link has expired or was already used.`);let c=await mintSession(r,s.id,r.operatorEmails.has(s.email),s.sessionVersion);return new Response(null,{headers:{...v,"set-cookie":u(c.token,{maxAgeSeconds:c.maxAge,secure:secure(r)})},status:204})}async function handleExport(e,t,n){let{subject:r}=await requireSession(e,n,t),i=await P(t.AUTH_DB,r);if(i===null)throw C();return _(200,{createdAt:i.created_at,email:i.email,id:i.id,lastSignInAt:i.last_sign_in_at},v)}async function archiveFailure(e){let t=await e.text().catch(()=>``),n=null;try{n=JSON.parse(t)}catch{n=null}let r=c.safeParse(n);if(!r.success||e.status<400||e.status>599)throw unavailable();let{code:i,message:a,requestId:o}=r.data.error,s=e.headers.get(`retry-after`);return _(e.status,{error:{code:i,message:a,requestId:o}},s!==null&&/^\d{1,6}$/.test(s)?{...v,"retry-after":s}:v)}async function handleErase(e,n,o){let s=requireMutationGuard(e,o),{subject:c,token:l}=await requireSession(e,o,n),u=e.headers.get(t);if(u===null||!d.safeParse(u).success)throw m();if(u!==c)throw new p(409,`account-changed`,`The signed-in account changed since this request was prepared.`);let g=new Headers({cookie:`${a}=${l}`,origin:s,[i]:`1`}),_=e.headers.get(r);if(_!==null){if(!q.test(_))throw m();g.set(r,_)}let y;try{y=await f(n.AUTH_ARCHIVE,new Request(`${o.publicOrigin}/api/v1/me/account`,{headers:g,method:`DELETE`,redirect:`manual`}),async e=>e.status===204?(await e.body?.cancel(),e):archiveFailure(e))}catch(e){throw new w(503,`catalog`,{cause:e})}return y.status===204?(await A(n.AUTH_DB,c),new Response(null,{headers:{...v,"set-cookie":h(secure(o))},status:204})):y}async function handleSignOutAll(e,n,r){requireMutationGuard(e,r);let{subject:i,sessionVersion:a}=await requireSession(e,r,n),o=e.headers.get(t);if(o===null||!d.safeParse(o).success)throw m();if(o!==i)throw new p(409,`account-changed`,`The signed-in account changed since this request was prepared.`);if(await F(n.AUTH_DB,i,a)===null)throw C();return _(200,{ok:!0},{...v,"set-cookie":h(secure(r))})}async function handleHealth(e){let t=null,n=null;try{await T(e)}catch{t=`configuration-unavailable`}if(t===null)try{n=await O(e.AUTH_DB),n!==1&&(t=`schema-mismatch`)}catch{t=`database-unavailable`}return _(t===null?200:503,s.parse({dbSchemaVersion:n,reason:t,schemaVersion:1,status:t===null?`ok`:`degraded`}),v)}async function route(e,t){let n=e.method.toUpperCase(),r=matchRoute(e);if(r===void 0)throw new p(404,`not-found`,`No such route.`);if(r===`health`&&n===`GET`)return handleHealth(t);let i=await T(t),a=corsHeaders(e,i);if(n===`OPTIONS`)return preflight(e,i,U[r]);if(!U[r].includes(n))throw new p(405,`method-not-allowed`,`That method is not allowed here.`);let o;switch(r){case`health`:o=await handleHealth(t);break;case`session`:await requireSession(e,i,t),o=_(200,{ok:!0},v);break;case`signOutAll`:o=await handleSignOutAll(e,t,i);break;case`jwks`:o=_(200,{keys:i.jwks},v);break;case`signIn`:o=await handleSignIn(e,t,i);break;case`verify`:o=await handleVerify(e,t,i);break;case`account`:o=n===`GET`?await handleExport(e,t,i):await handleErase(e,t,i)}return withCors(o,a)}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 w?e.authDiagnostic:{kind:`internal`};function createAuthWorker(){let e=new Set,emit=t=>{for(let n of e)try{n(t)}catch{}};return{async fetch(t,n){let r=crypto.randomUUID(),i=Date.now(),a,o;try{a=await route(t,n)}catch(e){o=e,a=S(e,r);let i=await T(n).catch(()=>null);i!==null&&(a=withCors(a,corsHeaders(t,i)))}if(matchRoute(t)===`verify`&&(a.headers.set(`cache-control`,`no-store`),a.headers.set(`referrer-policy`,`no-referrer`),a.headers.set(`x-content-type-options`,`nosniff`),a.headers.has(`content-security-policy`)||a.headers.set(`content-security-policy`,`default-src 'none'; base-uri 'none'; frame-ancestors 'none'`)),e.size>0){let e=matchRoute(t),n=a.status<400?`ok`:e===`health`?`unavailable`:o instanceof p?o.code:`internal`,l=o===void 0&&a.status>=400?c.safeParse(await a.clone().json().catch(()=>null)):null,u=e===`health`&&a.status===503?s.safeParse(await a.clone().json().catch(()=>null)):null;emit({code:l?.success?l.data.error.code:n,ms:Math.max(0,Date.now()-i),requestId:r,route:e===void 0?`unmatched`:W[e],status:a.status,type:`request`,...a.status>=500?{diagnostic:u?.success?{kind:u.data.reason===`configuration-unavailable`?`configuration`:u.data.reason===`schema-mismatch`?`schema`:`database`}:o===void 0?{kind:`catalog`}:diagnostic(o)}:{}})}return a},async scheduled(t,n){try{await T(n);let t=await R(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}}}export{createAuthWorker};
1
+ import{sha256Hex as e}from"../_crypto.js";import{ACCOUNT_HEADER as t,ARCHIVE_OPERATOR_ROLE as n,PUBLISHER_HEADER as r,REQUEST_MARKER_HEADER as i,SESSION_COOKIE_NAME as a,SESSION_MAX_AGE_SECONDS as o,archiveAuthHealthSchema as s,archiveErrorResponseSchema as c,isWellFormedText as l,sessionCookieValue as u,uuidSchema as d}from"../contract.js";import{serviceRequest as f}from"../worker/_service.js";import{ApiError as p,badRequest as m,expiredSessionCookie as h,forbidden as g,json as _,noStore as v,queryParams as y,readJsonBody as b,sessionToken as x,toErrorResponse as S,unauthenticated as C}from"../worker/http.js";import{AuthOperationError as w}from"./errors.js";import{LINK_TTL_MS as T,authReadiness as E,consumeToken as D,deleteAccount as O,deleteToken as k,ensureUser as A,insertToken as j,readAccount as M,revokeSessions as N,spendLinkBudget as P,spendSignInBudget as F,sweep as I}from"./store.js";import{confirmationPage as L,escapeHtml as R}from"./_confirmation.js";import{authConfig as z,isWellFormedEmail as B}from"./config.js";import{SignJWT as V,errors as H,jwtVerify as U}from"jose";var W={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$/},G={account:[`GET`,`DELETE`],health:[`GET`],jwks:[`GET`],session:[`GET`],signIn:[`POST`],signOutAll:[`POST`],verify:[`GET`,`POST`]},K={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(W).find(t=>W[t].test(new URL(e.url).pathname)),q=32,J=/^[A-Za-z0-9_-]{43}$/,Y=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,X=256,Z=5,randomToken=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(q)))).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``),unavailable=()=>new w(503,`catalog`);function requireMutationGuard(e,t,n=!1){let r=e.headers.get(`origin`);if(r===null||!t.allowedOrigins.has(r)&&!(n&&r===t.publicOrigin)||e.headers.get(`x-archive-request`)!==`1`)throw g();return r}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,a){let o=corsHeaders(e,n);if(Object.keys(o).length===0)throw g();return new Response(null,{headers:{...o,"access-control-allow-headers":`Content-Type, ${t}, ${r}, ${i}`,"access-control-allow-methods":[...a,`OPTIONS`].join(`, `),"access-control-max-age":`600`},status:204})}var secure=e=>e.appEnv===`production`;async function mintSession(e,t,r,i){let a=r?900:o;return{maxAge:a,token:await new V({sessionVersion:i,...r?{roles:[n]}:{}}).setProtectedHeader({alg:`RS256`,kid:e.signing.kid,typ:`JWT`}).setIssuedAt().setJti(crypto.randomUUID()).setIssuer(e.issuer).setAudience(e.audience).setSubject(t).setExpirationTime(`${a}s`).sign(e.signing.key)}}async function requireSession(e,t,n){let r=x(e);if(r===null)throw C();try{let{payload:e}=await U(r,t.verifier,{algorithms:[`RS256`],audience:t.audience,clockTolerance:Z,issuer:t.issuer,maxTokenAge:o,requiredClaims:[`sub`,`iss`,`aud`,`iat`,`exp`,`sessionVersion`]}),i=e.sub??``;if(i.length===0||i.length>X||!l(i))throw C();let a=e.sessionVersion;if(typeof a!=`number`||!Number.isSafeInteger(a)||a<0||typeof e.iat!=`number`||e.iat>Date.now()/1e3+Z||Array.isArray(e.roles)&&e.roles.includes(`archive-operator`)&&(typeof e.exp!=`number`||e.exp-e.iat>900||Date.now()/1e3-e.iat>900+Z))throw C();let s=await M(n.AUTH_DB,i);if(s===null||s.session_version!==a)throw C();return{sessionVersion:a,subject:i,token:r}}catch(e){throw e instanceof H.JOSEError?C():e}}async function sendLink(e,t,n,r){let i=t.appName,a=`The link works once and expires in ${T/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 ${R(i)}:</p><p><a href="${R(r)}">${R(r)}</a></p><p>${a}</p>`,subject:`Your ${i} sign-in link`,text:`Open this link to sign in to ${i}:\n\n${r}\n\n${a}`,to:n})}async function handleSignIn(t,n,r){requireMutationGuard(t,r);let i=await b(t);if(typeof i!=`object`||!i||Array.isArray(i))throw m();let a=Object.keys(i),o=Reflect.get(i,`email`);if(a.length!==1||a[0]!==`email`||typeof o!=`string`)throw m();let s=o.trim().toLowerCase();if(!B(s))throw m();let c=Date.now(),l=await F(n.AUTH_DB,c);if(l!==null)throw new p(429,`rate-limited`,`Too many sign-in requests. Try again later.`,{retryAfterSeconds:l});let u=await A(n.AUTH_DB,s),d=await P(n.AUTH_DB,u.id,c);if(d!==null)throw new p(429,`rate-limited`,`Too many sign-in emails. Try again later.`,{retryAfterSeconds:d});let f=randomToken(),h=await e(f);await j(n.AUTH_DB,h,u.id,c);try{await sendLink(n,r,u.email,`${r.publicOrigin}/api/v1/auth/verify?token=${f}`)}catch(e){throw await k(n.AUTH_DB,h).catch(()=>void 0),new w(503,`email`,{cause:e})}return _(202,{ok:!0},v)}async function handleVerify(t,n,r){if(t.method===`GET`){let e=y(new URL(t.url),[`token`]).get(`token`)??``;if(!J.test(e))throw m(`The link is not valid.`);return L({...r,nonce:randomToken(),token:e})}requireMutationGuard(t,r,!0),y(new URL(t.url),[]);let i=await b(t);if(typeof i!=`object`||!i||Array.isArray(i)||Object.keys(i).length!==1)throw m(`The link is not valid.`);let a=Reflect.get(i,`token`);if(typeof a!=`string`||!J.test(a))throw m(`The link is not valid.`);let o=await D(n.AUTH_DB,await e(a),Date.now());if(o===null)throw new p(410,`gone`,`That link has expired or was already used.`);let s=await mintSession(r,o.id,r.operatorEmails.has(o.email),o.sessionVersion);return new Response(null,{headers:{...v,"set-cookie":u(s.token,{maxAgeSeconds:s.maxAge,secure:secure(r)})},status:204})}async function handleExport(e,t,n){let{subject:r}=await requireSession(e,n,t),i=await M(t.AUTH_DB,r);if(i===null)throw C();return _(200,{createdAt:i.created_at,email:i.email,id:i.id,lastSignInAt:i.last_sign_in_at},v)}async function archiveFailure(e){let t=await e.text().catch(()=>``),n=null;try{n=JSON.parse(t)}catch{n=null}let r=c.safeParse(n);if(!r.success||e.status<400||e.status>599)throw unavailable();let{code:i,message:a,requestId:o}=r.data.error,s=e.headers.get(`retry-after`);return _(e.status,{error:{code:i,message:a,requestId:o}},s!==null&&/^\d{1,6}$/.test(s)?{...v,"retry-after":s}:v)}async function handleErase(e,n,o){let s=requireMutationGuard(e,o),{subject:c,token:l}=await requireSession(e,o,n),u=e.headers.get(t);if(u===null||!d.safeParse(u).success)throw m();if(u!==c)throw new p(409,`account-changed`,`The signed-in account changed since this request was prepared.`);let g=new Headers({cookie:`${a}=${l}`,origin:s,[i]:`1`}),_=e.headers.get(r);if(_!==null){if(!Y.test(_))throw m();g.set(r,_)}let y;try{y=await f(n.AUTH_ARCHIVE,new Request(`${o.publicOrigin}/api/v1/me/account`,{headers:g,method:`DELETE`,redirect:`manual`}),async e=>e.status===204?(await e.body?.cancel(),e):archiveFailure(e))}catch(e){throw new w(503,`catalog`,{cause:e})}return y.status===204?(await O(n.AUTH_DB,c),new Response(null,{headers:{...v,"set-cookie":h(secure(o))},status:204})):y}async function handleSignOutAll(e,n,r){requireMutationGuard(e,r);let{subject:i,sessionVersion:a}=await requireSession(e,r,n),o=e.headers.get(t);if(o===null||!d.safeParse(o).success)throw m();if(o!==i)throw new p(409,`account-changed`,`The signed-in account changed since this request was prepared.`);if(await N(n.AUTH_DB,i,a)===null)throw C();return _(200,{ok:!0},{...v,"set-cookie":h(secure(r))})}async function handleHealth(e){let t=null,n=null;try{await z(e)}catch{t=`configuration-unavailable`}if(t===null)try{n=await E(e.AUTH_DB),n!==1&&(t=`schema-mismatch`)}catch{t=`database-unavailable`}return _(t===null?200:503,s.parse({dbSchemaVersion:n,reason:t,schemaVersion:1,status:t===null?`ok`:`degraded`}),v)}async function route(e,t){let n=e.method.toUpperCase(),r=matchRoute(e);if(r===void 0)throw new p(404,`not-found`,`No such route.`);if(r===`health`&&n===`GET`)return handleHealth(t);let i=await z(t),a=corsHeaders(e,i);if(n===`OPTIONS`)return preflight(e,i,G[r]);if(!G[r].includes(n))throw new p(405,`method-not-allowed`,`That method is not allowed here.`);let o;switch(r){case`health`:o=await handleHealth(t);break;case`session`:await requireSession(e,i,t),o=_(200,{ok:!0},v);break;case`signOutAll`:o=await handleSignOutAll(e,t,i);break;case`jwks`:o=_(200,{keys:i.jwks},v);break;case`signIn`:o=await handleSignIn(e,t,i);break;case`verify`:o=await handleVerify(e,t,i);break;case`account`:o=n===`GET`?await handleExport(e,t,i):await handleErase(e,t,i)}return withCors(o,a)}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 w?e.authDiagnostic:{kind:`internal`};function createAuthWorker(){let e=new Set,emit=t=>{for(let n of e)try{n(t)}catch{}};return{async fetch(t,n){let r=crypto.randomUUID(),i=Date.now(),a,o;try{a=await route(t,n)}catch(e){o=e,a=S(e,r);let i=await z(n).catch(()=>null);i!==null&&(matchRoute(t)===`verify`&&t.method===`GET`&&e instanceof p&&e.status===400&&(a=L({...i,nonce:randomToken(),token:null})),a=withCors(a,corsHeaders(t,i)))}if(matchRoute(t)===`verify`&&(a.headers.set(`cache-control`,`no-store`),a.headers.set(`referrer-policy`,`no-referrer`),a.headers.set(`x-content-type-options`,`nosniff`),a.headers.has(`content-security-policy`)||a.headers.set(`content-security-policy`,`default-src 'none'; base-uri 'none'; frame-ancestors 'none'`)),e.size>0){let e=matchRoute(t),n=a.status<400?`ok`:e===`health`?`unavailable`:o instanceof p?o.code:`internal`,l=o===void 0&&a.status>=400?c.safeParse(await a.clone().json().catch(()=>null)):null,u=e===`health`&&a.status===503?s.safeParse(await a.clone().json().catch(()=>null)):null;emit({code:l?.success?l.data.error.code:n,ms:Math.max(0,Date.now()-i),requestId:r,route:e===void 0?`unmatched`:K[e],status:a.status,type:`request`,...a.status>=500?{diagnostic:u?.success?{kind:u.data.reason===`configuration-unavailable`?`configuration`:u.data.reason===`schema-mismatch`?`schema`:`database`}:o===void 0?{kind:`catalog`}:diagnostic(o)}:{}})}return a},async scheduled(t,n){try{await z(n);let t=await I(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}}}export{createAuthWorker};
16
2
  //# sourceMappingURL=worker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","names":[],"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 { 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 are short-lived so a revoked\n * operator loses authority within minutes. 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 escapeHtml = (value: string): string => value.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\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 const nonce = randomToken();\n // No automatic redemption, external assets or referrers. Only a deliberate\n // button click makes the same-origin marked POST; scanners can safely GET.\n const html = `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Confirm sign-in</title><h1>Sign in to ${escapeHtml(config.appName)}</h1><p>Continue only if you requested this sign-in link.</p><button type=\"button\" id=\"confirm\">Confirm sign-in</button><p role=\"status\" id=\"status\"></p><script nonce=\"${nonce}\">\nhistory.replaceState(null, '', location.pathname);\ndocument.getElementById('confirm').addEventListener('click', async () => {\n const button = document.getElementById('confirm');\n button.disabled = true;\n try {\n const response = await fetch(location.pathname, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', '${REQUEST_MARKER_HEADER}': '1' }, body: JSON.stringify({ token: '${token}' }) });\n if (!response.ok) throw new Error();\n location.replace(${JSON.stringify(`${config.publicOrigin}/signed-in`)});\n } catch {\n document.getElementById('status').textContent = 'Sign-in failed. Request a new link or try again.';\n button.disabled = false;\n }\n});\n</script></html>`;\n return new Response(html, {\n headers: {\n ...noStore,\n 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n 'content-type': 'text/html; charset=utf-8',\n 'referrer-policy': 'no-referrer',\n 'x-content-type-options': 'nosniff',\n },\n });\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) response = withCors(response, corsHeaders(request, config));\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.js","names":[],"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 are short-lived so a revoked\n * operator loses authority within minutes. 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/archive",
3
- "version": "26.10.9",
3
+ "version": "26.10.11",
4
4
  "description": "Community content publishing for Cloudflare Workers + D1: a hardened Worker core over per-collection payload schemas",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,7 +61,7 @@
61
61
  "test:types": "tsc -p tsconfig.json --noEmit"
62
62
  },
63
63
  "dependencies": {
64
- "@vielzeug/spell": "26.10.9",
64
+ "@vielzeug/spell": "26.10.11",
65
65
  "jose": "6.2.12"
66
66
  },
67
67
  "devDependencies": {