@vielzeug/archive 26.10.14 → 26.10.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_crypto.cjs.map +1 -1
- package/dist/_crypto.js +5 -1
- package/dist/_crypto.js.map +1 -1
- package/dist/_json.cjs.map +1 -1
- package/dist/_json.js +51 -1
- package/dist/_json.js.map +1 -1
- package/dist/auth/_confirmation.cjs +5 -1
- package/dist/auth/_confirmation.cjs.map +1 -1
- package/dist/auth/_confirmation.d.ts.map +1 -1
- package/dist/auth/_confirmation.js +38 -14
- package/dist/auth/_confirmation.js.map +1 -1
- package/dist/auth/config.cjs.map +1 -1
- package/dist/auth/config.js +156 -1
- package/dist/auth/config.js.map +1 -1
- package/dist/auth/errors.cjs.map +1 -1
- package/dist/auth/errors.js +18 -1
- package/dist/auth/errors.js.map +1 -1
- package/dist/auth/store.cjs.map +1 -1
- package/dist/auth/store.js +102 -18
- package/dist/auth/store.js.map +1 -1
- package/dist/auth/worker.cjs.map +1 -1
- package/dist/auth/worker.js +360 -1
- package/dist/auth/worker.js.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/collection.cjs.map +1 -1
- package/dist/collection.js +108 -1
- package/dist/collection.js.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.js +144 -1
- package/dist/contract.js.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +14 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/worker/_pagination.cjs.map +1 -1
- package/dist/worker/_pagination.js +55 -1
- package/dist/worker/_pagination.js.map +1 -1
- package/dist/worker/_service.cjs.map +1 -1
- package/dist/worker/_service.js +16 -1
- package/dist/worker/_service.js.map +1 -1
- package/dist/worker/auth.cjs.map +1 -1
- package/dist/worker/auth.js +68 -1
- package/dist/worker/auth.js.map +1 -1
- package/dist/worker/env.cjs.map +1 -1
- package/dist/worker/env.js +76 -1
- package/dist/worker/env.js.map +1 -1
- package/dist/worker/http.cjs +1 -1
- package/dist/worker/http.cjs.map +1 -1
- package/dist/worker/http.d.ts +3 -1
- package/dist/worker/http.d.ts.map +1 -1
- package/dist/worker/http.js +118 -1
- package/dist/worker/http.js.map +1 -1
- package/dist/worker/publications.cjs +1 -1
- package/dist/worker/publications.cjs.map +1 -1
- package/dist/worker/publications.d.ts.map +1 -1
- package/dist/worker/publications.js +470 -1
- package/dist/worker/publications.js.map +1 -1
- package/dist/worker/sql.cjs +3 -3
- package/dist/worker/sql.cjs.map +1 -1
- package/dist/worker/sql.d.ts +4 -2
- package/dist/worker/sql.d.ts.map +1 -1
- package/dist/worker/sql.js +100 -56
- package/dist/worker/sql.js.map +1 -1
- package/dist/worker/worker.cjs.map +1 -1
- package/dist/worker/worker.js +352 -1
- package/dist/worker/worker.js.map +1 -1
- package/dist/worker.js +1 -1
- package/package.json +6 -6
package/dist/_crypto.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_crypto.cjs","names":[],"sources":["../src/_crypto.ts"],"sourcesContent":["/**\n * The one SHA-256→hex helper shared by both server surfaces: the collection content\n * hash (browser-safe root) and the issuer's sign-in-link token hash. Web Crypto\n * only; no D1, JWKS or binding machinery.\n */\nexport const sha256Hex = async (text: string): Promise<string> =>\n [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n"],"mappings":"AAKA,IAAa,UAAY,KAAO,IAC9B,CAAC,GAAG,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,UAAW,IAAI,YAAY,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"_crypto.cjs","names":["text","byte"],"sources":["../src/_crypto.ts"],"sourcesContent":["/**\n * The one SHA-256→hex helper shared by both server surfaces: the collection content\n * hash (browser-safe root) and the issuer's sign-in-link token hash. Web Crypto\n * only; no D1, JWKS or binding machinery.\n */\nexport const sha256Hex = async (text: string): Promise<string> =>\n [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n"],"mappings":"AAKA,IAAa,UAAY,KAAO,IAC9B,CAAC,GAAG,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,UAAW,IAAI,YAAY,CAAC,CAAC,OAAOA,CAAI,CAAC,CAAC,CAAC,CAAC,CACvF,IAAK,GAASC,EAAK,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE,E"}
|
package/dist/_crypto.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/_crypto.ts
|
|
2
|
+
var sha256Hex = async (e) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(e)))].map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
3
|
+
//#endregion
|
|
4
|
+
export { sha256Hex };
|
|
5
|
+
|
|
2
6
|
//# sourceMappingURL=_crypto.js.map
|
package/dist/_crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_crypto.js","names":[],"sources":["../src/_crypto.ts"],"sourcesContent":["/**\n * The one SHA-256→hex helper shared by both server surfaces: the collection content\n * hash (browser-safe root) and the issuer's sign-in-link token hash. Web Crypto\n * only; no D1, JWKS or binding machinery.\n */\nexport const sha256Hex = async (text: string): Promise<string> =>\n [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n"],"mappings":"AAKA,IAAa,
|
|
1
|
+
{"version":3,"file":"_crypto.js","names":["text","byte"],"sources":["../src/_crypto.ts"],"sourcesContent":["/**\n * The one SHA-256→hex helper shared by both server surfaces: the collection content\n * hash (browser-safe root) and the issuer's sign-in-link token hash. Web Crypto\n * only; no D1, JWKS or binding machinery.\n */\nexport const sha256Hex = async (text: string): Promise<string> =>\n [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n"],"mappings":";AAKA,IAAa,YAAY,OAAO,MAC9B,CAAC,GAAG,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,CAAC,CAAC,OAAOA,CAAI,CAAC,CAAC,CAAC,CAAC,CACvF,KAAK,MAASC,EAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE"}
|
package/dist/_json.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_json.cjs","names":[],"sources":["../src/_json.ts"],"sourcesContent":["/** Canonical JSON rejects values JSON.stringify would silently discard or coerce. */\nexport function canonicalJson(value: unknown, omitUndefinedFields = false, omissionSource?: unknown): string | null {\n const active = new Set<object>();\n const text = (value: string): string | null => {\n if (/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/u.test(value)) return null;\n return JSON.stringify(value);\n };\n const serialize = (value: unknown, source?: unknown): string | null => {\n if (value === null) return 'null';\n if (typeof value === 'string') return text(value);\n if (typeof value === 'boolean') return String(value);\n if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : null;\n if (typeof value !== 'object' || active.has(value)) return null;\n const array = Array.isArray(value);\n const prototype = Object.getPrototypeOf(value);\n if (!array && prototype !== Object.prototype && prototype !== null) return null;\n if (array && prototype !== Array.prototype) return null;\n active.add(value);\n try {\n const fields: string[] = [];\n const keys = Reflect.ownKeys(value);\n if (array) {\n if (keys.length !== value.length + 1) return null;\n for (let i = 0; i < value.length; i++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) return null;\n const item = serialize(descriptor.value, Array.isArray(source) ? source[i] : undefined);\n if (item === null) return null;\n fields.push(item);\n }\n return `[${fields.join(',')}]`;\n }\n if (keys.some((key) => typeof key !== 'string')) return null;\n for (const key of Object.keys(value).sort()) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (!descriptor || !('value' in descriptor)) return null;\n const sourceObject = source !== null && typeof source === 'object' ? source : null;\n // Spell materializes absent optional object fields as undefined.\n if (\n descriptor.value === undefined &&\n omitUndefinedFields &&\n (sourceObject === null || !Object.hasOwn(sourceObject, key))\n )\n continue;\n const name = text(key);\n const item = serialize(descriptor.value, sourceObject === null ? undefined : Reflect.get(sourceObject, key));\n if (name === null || item === null) return null;\n fields.push(`${name}:${item}`);\n }\n if (Object.keys(value).length !== keys.length) return null;\n return `{${fields.join(',')}}`;\n } finally {\n active.delete(value);\n }\n };\n try {\n return serialize(value, omissionSource);\n } catch {\n return null;\n }\n}\n\nexport function canonicalObjectJson(\n value: unknown,\n omitUndefinedFields = false,\n omissionSource?: unknown,\n): string | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;\n return canonicalJson(value, omitUndefinedFields, omissionSource);\n}\n"],"mappings":"AACA,SAAgB,cAAc,EAAgB,EAAsB,GAAO,EAAyC,CAClH,
|
|
1
|
+
{"version":3,"file":"_json.cjs","names":["active","value","array","prototype","fields","keys","i","descriptor","item","source","key","sourceObject","omitUndefinedFields","name","omissionSource"],"sources":["../src/_json.ts"],"sourcesContent":["/** Canonical JSON rejects values JSON.stringify would silently discard or coerce. */\nexport function canonicalJson(value: unknown, omitUndefinedFields = false, omissionSource?: unknown): string | null {\n const active = new Set<object>();\n const text = (value: string): string | null => {\n if (/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/u.test(value)) return null;\n return JSON.stringify(value);\n };\n const serialize = (value: unknown, source?: unknown): string | null => {\n if (value === null) return 'null';\n if (typeof value === 'string') return text(value);\n if (typeof value === 'boolean') return String(value);\n if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : null;\n if (typeof value !== 'object' || active.has(value)) return null;\n const array = Array.isArray(value);\n const prototype = Object.getPrototypeOf(value);\n if (!array && prototype !== Object.prototype && prototype !== null) return null;\n if (array && prototype !== Array.prototype) return null;\n active.add(value);\n try {\n const fields: string[] = [];\n const keys = Reflect.ownKeys(value);\n if (array) {\n if (keys.length !== value.length + 1) return null;\n for (let i = 0; i < value.length; i++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) return null;\n const item = serialize(descriptor.value, Array.isArray(source) ? source[i] : undefined);\n if (item === null) return null;\n fields.push(item);\n }\n return `[${fields.join(',')}]`;\n }\n if (keys.some((key) => typeof key !== 'string')) return null;\n for (const key of Object.keys(value).sort()) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (!descriptor || !('value' in descriptor)) return null;\n const sourceObject = source !== null && typeof source === 'object' ? source : null;\n // Spell materializes absent optional object fields as undefined.\n if (\n descriptor.value === undefined &&\n omitUndefinedFields &&\n (sourceObject === null || !Object.hasOwn(sourceObject, key))\n )\n continue;\n const name = text(key);\n const item = serialize(descriptor.value, sourceObject === null ? undefined : Reflect.get(sourceObject, key));\n if (name === null || item === null) return null;\n fields.push(`${name}:${item}`);\n }\n if (Object.keys(value).length !== keys.length) return null;\n return `{${fields.join(',')}}`;\n } finally {\n active.delete(value);\n }\n };\n try {\n return serialize(value, omissionSource);\n } catch {\n return null;\n }\n}\n\nexport function canonicalObjectJson(\n value: unknown,\n omitUndefinedFields = false,\n omissionSource?: unknown,\n): string | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;\n return canonicalJson(value, omitUndefinedFields, omissionSource);\n}\n"],"mappings":"AACA,SAAgB,cAAc,EAAgB,EAAsB,GAAO,EAAyC,CAClH,IAAMA,EAAS,IAAI,IACb,KAAQ,GACR,0EAA0E,KAAKC,CAAK,EAAU,KAC3F,KAAK,UAAUA,CAAK,EAEvB,WAAa,EAAgB,IAAoC,CACrE,GAAIA,IAAU,KAAM,MAAO,OAC3B,GAAI,OAAOA,GAAU,SAAU,OAAO,KAAKA,CAAK,EAChD,GAAI,OAAOA,GAAU,UAAW,OAAO,OAAOA,CAAK,EACnD,GAAI,OAAOA,GAAU,SAAU,OAAO,OAAO,SAASA,CAAK,EAAI,KAAK,UAAUA,CAAK,EAAI,KACvF,GAAI,OAAOA,GAAU,UAAYD,EAAO,IAAIC,CAAK,EAAG,OAAO,KAC3D,IAAMC,EAAQ,MAAM,QAAQD,CAAK,EAC3BE,EAAY,OAAO,eAAeF,CAAK,EAE7C,GADI,CAACC,GAASC,IAAc,OAAO,WAAaA,IAAc,MAC1DD,GAASC,IAAc,MAAM,UAAW,OAAO,KACnDH,EAAO,IAAIC,CAAK,EAChB,GAAI,CACF,IAAMG,EAAmB,CAAC,EACpBC,EAAO,QAAQ,QAAQJ,CAAK,EAClC,GAAIC,EAAO,CACT,GAAIG,EAAK,SAAWJ,EAAM,OAAS,EAAG,OAAO,KAC7C,IAAK,IAAIK,EAAI,EAAGA,EAAIL,EAAM,OAAQ,IAAK,CACrC,IAAMM,EAAa,OAAO,yBAAyBN,EAAO,OAAOK,CAAC,CAAC,EACnE,GAAI,CAACC,GAAc,CAACA,EAAW,YAAc,EAAE,UAAWA,GAAa,OAAO,KAC9E,IAAMC,EAAO,UAAUD,EAAW,MAAO,MAAM,QAAQE,CAAM,EAAIA,EAAOH,GAAK,IAAA,EAAS,EACtF,GAAIE,IAAS,KAAM,OAAO,KAC1BJ,EAAO,KAAKI,CAAI,CAClB,CACA,MAAO,IAAIJ,EAAO,KAAK,GAAG,EAAE,EAC9B,CACA,GAAIC,EAAK,KAAM,GAAQ,OAAOK,GAAQ,QAAQ,EAAG,OAAO,KACxD,IAAK,IAAMA,KAAO,OAAO,KAAKT,CAAK,CAAC,CAAC,KAAK,EAAG,CAC3C,IAAMM,EAAa,OAAO,yBAAyBN,EAAOS,CAAG,EAC7D,GAAI,CAACH,GAAc,EAAE,UAAWA,GAAa,OAAO,KACpD,IAAMI,EAAkC,OAAOF,GAAW,UAArCA,EAAgDA,EAAS,KAE9E,GACEF,EAAW,QAAU,IAAA,IACrBK,IACCD,IAAiB,MAAQ,CAAC,OAAO,OAAOA,EAAcD,CAAG,GAE1D,SACF,IAAMG,EAAO,KAAKH,CAAG,EACfF,EAAO,UAAUD,EAAW,MAAOI,IAAiB,KAAO,IAAA,GAAY,QAAQ,IAAIA,EAAcD,CAAG,CAAC,EAC3G,GAAIG,IAAS,MAAQL,IAAS,KAAM,OAAO,KAC3CJ,EAAO,KAAK,GAAGS,EAAK,GAAGL,GAAM,CAC/B,CAEA,OADI,OAAO,KAAKP,CAAK,CAAC,CAAC,SAAWI,EAAK,OAChC,IAAID,EAAO,KAAK,GAAG,EAAE,GAD0B,IAExD,QAAU,CACRJ,EAAO,OAAOC,CAAK,CACrB,CACF,EACA,GAAI,CACF,OAAO,UAAUA,EAAOa,CAAc,CACxC,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAAgB,oBACd,EACA,EAAsB,GACtB,EACe,CAEf,OADsB,OAAOb,GAAU,WAAnCA,GAA+C,MAAM,QAAQA,CAAK,EAAU,KACzE,cAAcA,EAAOW,EAAqBE,CAAc,CACjE,C"}
|
package/dist/_json.js
CHANGED
|
@@ -1,2 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/_json.ts
|
|
2
|
+
function canonicalJson(e, t = !1, n) {
|
|
3
|
+
let r = /* @__PURE__ */ new Set(), text = (e) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(e) ? null : JSON.stringify(e), serialize = (e, n) => {
|
|
4
|
+
if (e === null) return "null";
|
|
5
|
+
if (typeof e == "string") return text(e);
|
|
6
|
+
if (typeof e == "boolean") return String(e);
|
|
7
|
+
if (typeof e == "number") return Number.isFinite(e) ? JSON.stringify(e) : null;
|
|
8
|
+
if (typeof e != "object" || r.has(e)) return null;
|
|
9
|
+
let i = Array.isArray(e), a = Object.getPrototypeOf(e);
|
|
10
|
+
if (!i && a !== Object.prototype && a !== null || i && a !== Array.prototype) return null;
|
|
11
|
+
r.add(e);
|
|
12
|
+
try {
|
|
13
|
+
let r = [], a = Reflect.ownKeys(e);
|
|
14
|
+
if (i) {
|
|
15
|
+
if (a.length !== e.length + 1) return null;
|
|
16
|
+
for (let t = 0; t < e.length; t++) {
|
|
17
|
+
let i = Object.getOwnPropertyDescriptor(e, String(t));
|
|
18
|
+
if (!i || !i.enumerable || !("value" in i)) return null;
|
|
19
|
+
let a = serialize(i.value, Array.isArray(n) ? n[t] : void 0);
|
|
20
|
+
if (a === null) return null;
|
|
21
|
+
r.push(a);
|
|
22
|
+
}
|
|
23
|
+
return `[${r.join(",")}]`;
|
|
24
|
+
}
|
|
25
|
+
if (a.some((e) => typeof e != "string")) return null;
|
|
26
|
+
for (let i of Object.keys(e).sort()) {
|
|
27
|
+
let a = Object.getOwnPropertyDescriptor(e, i);
|
|
28
|
+
if (!a || !("value" in a)) return null;
|
|
29
|
+
let o = typeof n == "object" && n ? n : null;
|
|
30
|
+
if (a.value === void 0 && t && (o === null || !Object.hasOwn(o, i))) continue;
|
|
31
|
+
let s = text(i), c = serialize(a.value, o === null ? void 0 : Reflect.get(o, i));
|
|
32
|
+
if (s === null || c === null) return null;
|
|
33
|
+
r.push(`${s}:${c}`);
|
|
34
|
+
}
|
|
35
|
+
return Object.keys(e).length === a.length ? `{${r.join(",")}}` : null;
|
|
36
|
+
} finally {
|
|
37
|
+
r.delete(e);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
return serialize(e, n);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function canonicalObjectJson(e, t = !1, n) {
|
|
47
|
+
return typeof e != "object" || !e || Array.isArray(e) ? null : canonicalJson(e, t, n);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { canonicalJson, canonicalObjectJson };
|
|
51
|
+
|
|
2
52
|
//# sourceMappingURL=_json.js.map
|
package/dist/_json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_json.js","names":[],"sources":["../src/_json.ts"],"sourcesContent":["/** Canonical JSON rejects values JSON.stringify would silently discard or coerce. */\nexport function canonicalJson(value: unknown, omitUndefinedFields = false, omissionSource?: unknown): string | null {\n const active = new Set<object>();\n const text = (value: string): string | null => {\n if (/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/u.test(value)) return null;\n return JSON.stringify(value);\n };\n const serialize = (value: unknown, source?: unknown): string | null => {\n if (value === null) return 'null';\n if (typeof value === 'string') return text(value);\n if (typeof value === 'boolean') return String(value);\n if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : null;\n if (typeof value !== 'object' || active.has(value)) return null;\n const array = Array.isArray(value);\n const prototype = Object.getPrototypeOf(value);\n if (!array && prototype !== Object.prototype && prototype !== null) return null;\n if (array && prototype !== Array.prototype) return null;\n active.add(value);\n try {\n const fields: string[] = [];\n const keys = Reflect.ownKeys(value);\n if (array) {\n if (keys.length !== value.length + 1) return null;\n for (let i = 0; i < value.length; i++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) return null;\n const item = serialize(descriptor.value, Array.isArray(source) ? source[i] : undefined);\n if (item === null) return null;\n fields.push(item);\n }\n return `[${fields.join(',')}]`;\n }\n if (keys.some((key) => typeof key !== 'string')) return null;\n for (const key of Object.keys(value).sort()) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (!descriptor || !('value' in descriptor)) return null;\n const sourceObject = source !== null && typeof source === 'object' ? source : null;\n // Spell materializes absent optional object fields as undefined.\n if (\n descriptor.value === undefined &&\n omitUndefinedFields &&\n (sourceObject === null || !Object.hasOwn(sourceObject, key))\n )\n continue;\n const name = text(key);\n const item = serialize(descriptor.value, sourceObject === null ? undefined : Reflect.get(sourceObject, key));\n if (name === null || item === null) return null;\n fields.push(`${name}:${item}`);\n }\n if (Object.keys(value).length !== keys.length) return null;\n return `{${fields.join(',')}}`;\n } finally {\n active.delete(value);\n }\n };\n try {\n return serialize(value, omissionSource);\n } catch {\n return null;\n }\n}\n\nexport function canonicalObjectJson(\n value: unknown,\n omitUndefinedFields = false,\n omissionSource?: unknown,\n): string | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;\n return canonicalJson(value, omitUndefinedFields, omissionSource);\n}\n"],"mappings":"AACA,SAAgB,cAAc,
|
|
1
|
+
{"version":3,"file":"_json.js","names":["active","value","array","prototype","fields","keys","i","descriptor","item","source","key","sourceObject","omitUndefinedFields","name","omissionSource"],"sources":["../src/_json.ts"],"sourcesContent":["/** Canonical JSON rejects values JSON.stringify would silently discard or coerce. */\nexport function canonicalJson(value: unknown, omitUndefinedFields = false, omissionSource?: unknown): string | null {\n const active = new Set<object>();\n const text = (value: string): string | null => {\n if (/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/u.test(value)) return null;\n return JSON.stringify(value);\n };\n const serialize = (value: unknown, source?: unknown): string | null => {\n if (value === null) return 'null';\n if (typeof value === 'string') return text(value);\n if (typeof value === 'boolean') return String(value);\n if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : null;\n if (typeof value !== 'object' || active.has(value)) return null;\n const array = Array.isArray(value);\n const prototype = Object.getPrototypeOf(value);\n if (!array && prototype !== Object.prototype && prototype !== null) return null;\n if (array && prototype !== Array.prototype) return null;\n active.add(value);\n try {\n const fields: string[] = [];\n const keys = Reflect.ownKeys(value);\n if (array) {\n if (keys.length !== value.length + 1) return null;\n for (let i = 0; i < value.length; i++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) return null;\n const item = serialize(descriptor.value, Array.isArray(source) ? source[i] : undefined);\n if (item === null) return null;\n fields.push(item);\n }\n return `[${fields.join(',')}]`;\n }\n if (keys.some((key) => typeof key !== 'string')) return null;\n for (const key of Object.keys(value).sort()) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (!descriptor || !('value' in descriptor)) return null;\n const sourceObject = source !== null && typeof source === 'object' ? source : null;\n // Spell materializes absent optional object fields as undefined.\n if (\n descriptor.value === undefined &&\n omitUndefinedFields &&\n (sourceObject === null || !Object.hasOwn(sourceObject, key))\n )\n continue;\n const name = text(key);\n const item = serialize(descriptor.value, sourceObject === null ? undefined : Reflect.get(sourceObject, key));\n if (name === null || item === null) return null;\n fields.push(`${name}:${item}`);\n }\n if (Object.keys(value).length !== keys.length) return null;\n return `{${fields.join(',')}}`;\n } finally {\n active.delete(value);\n }\n };\n try {\n return serialize(value, omissionSource);\n } catch {\n return null;\n }\n}\n\nexport function canonicalObjectJson(\n value: unknown,\n omitUndefinedFields = false,\n omissionSource?: unknown,\n): string | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;\n return canonicalJson(value, omitUndefinedFields, omissionSource);\n}\n"],"mappings":";AACA,SAAgB,cAAc,GAAgB,IAAsB,IAAO,GAAyC;CAClH,IAAMA,oBAAS,IAAI,IAAY,GACzB,QAAQ,MACR,0EAA0E,KAAKC,CAAK,IAAU,OAC3F,KAAK,UAAUA,CAAK,GAEvB,aAAa,GAAgB,MAAoC;EACrE,IAAIA,MAAU,MAAM,OAAO;EAC3B,IAAI,OAAOA,KAAU,UAAU,OAAO,KAAKA,CAAK;EAChD,IAAI,OAAOA,KAAU,WAAW,OAAO,OAAOA,CAAK;EACnD,IAAI,OAAOA,KAAU,UAAU,OAAO,OAAO,SAASA,CAAK,IAAI,KAAK,UAAUA,CAAK,IAAI;EACvF,IAAI,OAAOA,KAAU,YAAYD,EAAO,IAAIC,CAAK,GAAG,OAAO;EAC3D,IAAMC,IAAQ,MAAM,QAAQD,CAAK,GAC3BE,IAAY,OAAO,eAAeF,CAAK;EAE7C,IADI,CAACC,KAASC,MAAc,OAAO,aAAaA,MAAc,QAC1DD,KAASC,MAAc,MAAM,WAAW,OAAO;EACnD,EAAO,IAAIF,CAAK;EAChB,IAAI;GACF,IAAMG,IAAmB,CAAC,GACpBC,IAAO,QAAQ,QAAQJ,CAAK;GAClC,IAAIC,GAAO;IACT,IAAIG,EAAK,WAAWJ,EAAM,SAAS,GAAG,OAAO;IAC7C,KAAK,IAAIK,IAAI,GAAGA,IAAIL,EAAM,QAAQ,KAAK;KACrC,IAAMM,IAAa,OAAO,yBAAyBN,GAAO,OAAOK,CAAC,CAAC;KACnE,IAAI,CAACC,KAAc,CAACA,EAAW,cAAc,EAAE,WAAWA,IAAa,OAAO;KAC9E,IAAMC,IAAO,UAAUD,EAAW,OAAO,MAAM,QAAQE,CAAM,IAAIA,EAAOH,KAAK,KAAA,CAAS;KACtF,IAAIE,MAAS,MAAM,OAAO;KAC1B,EAAO,KAAKA,CAAI;IAClB;IACA,OAAO,IAAIJ,EAAO,KAAK,GAAG,EAAE;GAC9B;GACA,IAAIC,EAAK,MAAM,MAAQ,OAAOK,KAAQ,QAAQ,GAAG,OAAO;GACxD,KAAK,IAAMA,KAAO,OAAO,KAAKT,CAAK,CAAC,CAAC,KAAK,GAAG;IAC3C,IAAMM,IAAa,OAAO,yBAAyBN,GAAOS,CAAG;IAC7D,IAAI,CAACH,KAAc,EAAE,WAAWA,IAAa,OAAO;IACpD,IAAMI,IAAkC,OAAOF,KAAW,YAArCA,IAAgDA,IAAS;IAE9E,IACEF,EAAW,UAAU,KAAA,KACrBK,MACCD,MAAiB,QAAQ,CAAC,OAAO,OAAOA,GAAcD,CAAG,IAE1D;IACF,IAAMG,IAAO,KAAKH,CAAG,GACfF,IAAO,UAAUD,EAAW,OAAOI,MAAiB,OAAO,KAAA,IAAY,QAAQ,IAAIA,GAAcD,CAAG,CAAC;IAC3G,IAAIG,MAAS,QAAQL,MAAS,MAAM,OAAO;IAC3C,EAAO,KAAK,GAAGK,EAAK,GAAGL,GAAM;GAC/B;GAEA,OADI,OAAO,KAAKP,CAAK,CAAC,CAAC,WAAWI,EAAK,SAChC,IAAID,EAAO,KAAK,GAAG,EAAE,KAD0B;EAExD,UAAU;GACR,EAAO,OAAOH,CAAK;EACrB;CACF;CACA,IAAI;EACF,OAAO,UAAUA,GAAOa,CAAc;CACxC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,oBACd,GACA,IAAsB,IACtB,GACe;CAEf,OADsB,OAAOb,KAAU,aAAnCA,KAA+C,MAAM,QAAQA,CAAK,IAAU,OACzE,cAAcA,GAAOW,GAAqBE,CAAc;AACjE"}
|
|
@@ -97,8 +97,12 @@ button.addEventListener('click', async () => {
|
|
|
97
97
|
} else if (response.status === 429) {
|
|
98
98
|
const value = response.headers.get('retry-after');
|
|
99
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
|
+
// HTTP dates are always GMT; bare asctime forms need the suffix or Date.parse reads them as local time.
|
|
100
101
|
const seconds = value && /^\\d+$/.test(value)
|
|
101
|
-
? Number(value)
|
|
102
|
+
? Number(value)
|
|
103
|
+
: (value && httpDate.test(value)
|
|
104
|
+
? (Date.parse(value.endsWith('GMT') ? value : value + ' GMT') - Date.now()) / 1000
|
|
105
|
+
: NaN);
|
|
102
106
|
const wait = Number.isFinite(seconds) ? Math.min(300, Math.max(1, Math.ceil(seconds))) : 30;
|
|
103
107
|
retryAt = Date.now() + wait * 1000;
|
|
104
108
|
recover('Too many attempts. Wait ' + wait + ' seconds before trying again.');
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"_confirmation.cjs","names":["REQUEST_MARKER_HEADER","LINK_TTL_MS","noStore","require_contract","require_http","require_store","value","char","valid","token","title","script","publicOrigin","html","appName","nonce"],"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 // HTTP dates are always GMT; bare asctime forms need the suffix or Date.parse reads them as local time.\n const seconds = value && /^\\\\d+$/.test(value)\n ? Number(value)\n : (value && httpDate.test(value)\n ? (Date.parse(value.endsWith('GMT') ? value : value + ' GMT') - Date.now()) / 1000\n : 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":"A,4FAIA,IAAa,WAAc,GAA0BM,EAAM,QAAQ,WAAa,GAAS,KAAKC,EAAK,WAAW,CAAC,EAAE,EAAE,EAE7G,YAAe,GACnB,KAAK,UAAUD,CAAK,CAAC,CAAC,QACpB,qBACC,GAAS,MAAMC,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,GACjE,EAEF,SAAgB,iBAAiB,CAC/B,UACA,QACA,eACA,SAMW,CACX,IAAMC,EAAQC,IAAU,KAClBC,EAAQF,EAAQ,kBAAoB,iCACpCG,EAASH,EACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wDA2EkDR,EAAAA,sBAAsB;sCACxC,YAAYS,GAAS,EAAE,EAAE;;;;;;;;;;;yBAWtC,YAAY,GAAGG,EAAa,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA8C5D,GACEC,EAAO;;;;;;;SAON,WAAWH,CAAK,EAAE,KAAK,WAAWI,CAAO,EAAE;iBACnCC,EAAM;gBACPA,EAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA2BSP,EAAQ,cAAc,WAAWM,CAAO,IAAM,WAAWJ,CAAK,EAAE;KAC1FF,EAAQ,oDAAsD,kCAAkC,WAAWM,CAAO,EAAE,GAAG;EAC1HN,EAAQ,sDAAsDP,EAAAA,YAAc,IAAO,sCAAwC,GAAG;;EAE9HO,EAAQ,qEAAuE,GAAG;IAChFA,EAAQ,GAAK,mBAAmB,uBAAuB,WAAW,GAAGI,EAAa,SAAS,EAAE,GAAGJ,EAAQ,UAAY,GAAG;;;EAGzHA,EAAQ,wGAAwG,WAAW,GAAGI,EAAa,SAAS,EAAE,kDAAoD,GAAG;EAC7MJ,EAAQ,8BAA8B,WAAW,GAAGI,EAAa,EAAE,EAAE,4BAA8B,GAAG;;EAGtGJ,EACI,kBAAkBO,EAAM,IAAIJ,EAAO;YAEnC,GACL;;SAGC,OAAO,IAAI,SAASE,EAAM,CACxB,QAAS,CACP,GAAGX,EAAAA,QACH,0BAA2B,yCAAyCa,EAAM,sBAAsBA,EAAM,oFACtG,eAAgB,2BAChB,kBAAmB,cACnB,yBAA0B,SAC5B,EACA,OAAQP,EAAQ,IAAM,GACxB,CAAC,CACH,C"}
|
|
@@ -1 +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,
|
|
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,CA2MX"}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import{REQUEST_MARKER_HEADER as e}from"../contract.js";
|
|
1
|
+
import { REQUEST_MARKER_HEADER as e } from "../contract.js";
|
|
2
|
+
import { noStore as t } from "../worker/http.js";
|
|
3
|
+
import { LINK_TTL_MS as n } from "./store.js";
|
|
4
|
+
//#region src/auth/_confirmation.ts
|
|
5
|
+
var escapeHtml = (e) => e.replace(/[&<>"']/g, (e) => `&#${e.charCodeAt(0)};`), scriptValue = (e) => JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
6
|
+
function confirmationPage({ appName: r, nonce: i, publicOrigin: a, token: o }) {
|
|
7
|
+
let s = o !== null, c = s ? "Confirm sign-in" : "This sign-in link is not valid", l = s ? `
|
|
2
8
|
const button = document.getElementById('confirm');
|
|
3
9
|
const status = document.getElementById('status');
|
|
4
10
|
const recovery = document.getElementById('recovery');
|
|
@@ -74,7 +80,7 @@ button.addEventListener('click', async () => {
|
|
|
74
80
|
method: 'POST',
|
|
75
81
|
credentials: 'same-origin',
|
|
76
82
|
headers: { 'content-type': 'application/json', '${e}': '1' },
|
|
77
|
-
body: JSON.stringify({ token: ${scriptValue(o
|
|
83
|
+
body: JSON.stringify({ token: ${scriptValue(o ?? "")} }),
|
|
78
84
|
signal: active.signal,
|
|
79
85
|
redirect: 'error',
|
|
80
86
|
referrerPolicy: 'no-referrer'
|
|
@@ -97,8 +103,12 @@ button.addEventListener('click', async () => {
|
|
|
97
103
|
} else if (response.status === 429) {
|
|
98
104
|
const value = response.headers.get('retry-after');
|
|
99
105
|
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})$/;
|
|
106
|
+
// HTTP dates are always GMT; bare asctime forms need the suffix or Date.parse reads them as local time.
|
|
100
107
|
const seconds = value && /^\\d+$/.test(value)
|
|
101
|
-
? Number(value)
|
|
108
|
+
? Number(value)
|
|
109
|
+
: (value && httpDate.test(value)
|
|
110
|
+
? (Date.parse(value.endsWith('GMT') ? value : value + ' GMT') - Date.now()) / 1000
|
|
111
|
+
: NaN);
|
|
102
112
|
const wait = Number.isFinite(seconds) ? Math.min(300, Math.max(1, Math.ceil(seconds))) : 30;
|
|
103
113
|
retryAt = Date.now() + wait * 1000;
|
|
104
114
|
recover('Too many attempts. Wait ' + wait + ' seconds before trying again.');
|
|
@@ -126,7 +136,7 @@ button.addEventListener('click', async () => {
|
|
|
126
136
|
document.getElementById('actions').removeAttribute('aria-busy');
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
|
-
})
|
|
139
|
+
});` : "", u = `<!doctype html>
|
|
130
140
|
<html lang="en">
|
|
131
141
|
<head>
|
|
132
142
|
<meta charset="utf-8">
|
|
@@ -162,19 +172,33 @@ footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--rule);
|
|
|
162
172
|
</head>
|
|
163
173
|
<body>
|
|
164
174
|
<main id="main" aria-labelledby="title">
|
|
165
|
-
<h1 id="title" tabindex="-1">${s
|
|
166
|
-
<p>${s
|
|
167
|
-
${s
|
|
175
|
+
<h1 id="title" tabindex="-1">${s ? `Sign in to ${escapeHtml(r)}` : escapeHtml(c)}</h1>
|
|
176
|
+
<p>${s ? "Continue only if you requested this sign-in link." : `Request a new sign-in link for ${escapeHtml(r)}.`}</p>
|
|
177
|
+
${s ? `<p class="detail">This link works once and expires ${n / 6e4} minutes after it was requested.</p>` : ""}
|
|
168
178
|
<div class="actions" id="actions">
|
|
169
|
-
${s
|
|
170
|
-
<a${s
|
|
179
|
+
${s ? "<button type=\"button\" id=\"confirm\" hidden>Confirm sign-in</button>" : ""}
|
|
180
|
+
<a${s ? "" : " class=\"primary\""} id="recovery" href="${escapeHtml(`${a}/account`)}"${s ? " hidden" : ""}>Request a new sign-in link</a>
|
|
171
181
|
</div>
|
|
172
182
|
<p role="status" aria-live="polite" aria-atomic="true" id="status"></p>
|
|
173
|
-
${s
|
|
174
|
-
${s
|
|
183
|
+
${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>` : ""}
|
|
184
|
+
${s ? `<footer id="exit"><a href="${escapeHtml(`${a}/`)}">Back to app</a></footer>` : ""}
|
|
175
185
|
</main>
|
|
176
|
-
${s
|
|
177
|
-
<\/script
|
|
186
|
+
${s ? `<script nonce="${i}">${l}
|
|
187
|
+
<\/script>` : ""}
|
|
178
188
|
</body>
|
|
179
|
-
</html>`;
|
|
189
|
+
</html>`;
|
|
190
|
+
return new Response(u, {
|
|
191
|
+
headers: {
|
|
192
|
+
...t,
|
|
193
|
+
"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'`,
|
|
194
|
+
"content-type": "text/html; charset=utf-8",
|
|
195
|
+
"referrer-policy": "no-referrer",
|
|
196
|
+
"x-content-type-options": "nosniff"
|
|
197
|
+
},
|
|
198
|
+
status: s ? 200 : 400
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { confirmationPage, escapeHtml };
|
|
203
|
+
|
|
180
204
|
//# sourceMappingURL=_confirmation.js.map
|
|
@@ -1 +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
|
+
{"version":3,"file":"_confirmation.js","names":["value","char","valid","token","title","script","REQUEST_MARKER_HEADER","publicOrigin","html","appName","nonce","LINK_TTL_MS","noStore"],"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 // HTTP dates are always GMT; bare asctime forms need the suffix or Date.parse reads them as local time.\n const seconds = value && /^\\\\d+$/.test(value)\n ? Number(value)\n : (value && httpDate.test(value)\n ? (Date.parse(value.endsWith('GMT') ? value : value + ' GMT') - Date.now()) / 1000\n : 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":";;;;AAIA,IAAa,cAAc,MAA0BA,EAAM,QAAQ,aAAa,MAAS,KAAKC,EAAK,WAAW,CAAC,EAAE,EAAE,GAE7G,eAAe,MACnB,KAAK,UAAUD,CAAK,CAAC,CAAC,QACpB,uBACC,MAAS,MAAMC,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,GACjE;AAEF,SAAgB,iBAAiB,EAC/B,YACA,UACA,iBACA,YAMW;CACX,IAAMC,IAAQC,MAAU,MAClBC,IAAQF,IAAQ,oBAAoB,kCACpCG,IAASH,IACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wDA2EkDI,EAAsB;sCACxC,YAAYH,KAAS,EAAE,EAAE;;;;;;;;;;;yBAWtC,YAAY,GAAGI,EAAa,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8C5D,IACEC,IAAO;;;;;;;SAON,WAAWJ,CAAK,EAAE,KAAK,WAAWK,CAAO,EAAE;iBACnCC,EAAM;gBACPA,EAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA2BSR,IAAQ,cAAc,WAAWO,CAAO,MAAM,WAAWL,CAAK,EAAE;KAC1FF,IAAQ,sDAAsD,kCAAkC,WAAWO,CAAO,EAAE,GAAG;EAC1HP,IAAQ,sDAAsDS,IAAc,IAAO,wCAAwC,GAAG;;EAE9HT,IAAQ,2EAAuE,GAAG;IAChFA,IAAQ,KAAK,qBAAmB,uBAAuB,WAAW,GAAGK,EAAa,SAAS,EAAE,GAAGL,IAAQ,YAAY,GAAG;;;EAGzHA,IAAQ,wGAAwG,WAAW,GAAGK,EAAa,SAAS,EAAE,oDAAoD,GAAG;EAC7ML,IAAQ,8BAA8B,WAAW,GAAGK,EAAa,EAAE,EAAE,8BAA8B,GAAG;;EAGtGL,IACI,kBAAkBQ,EAAM,IAAIL,EAAO;cAEnC,GACL;;;CAGC,OAAO,IAAI,SAASG,GAAM;EACxB,SAAS;GACP,GAAGI;GACH,2BAA2B,yCAAyCF,EAAM,sBAAsBA,EAAM;GACtG,gBAAgB;GAChB,mBAAmB;GACnB,0BAA0B;EAC5B;EACA,QAAQR,IAAQ,MAAM;CACxB,CAAC;AACH"}
|
package/dist/auth/config.cjs.map
CHANGED
|
@@ -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 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
|
+
{"version":3,"file":"config.cjs","names":["AuthOperationError","calculateJwkThumbprint","importJWK","SignJWT","jwtVerify","createLocalJWKSet","require_errors","jose","EMAIL_MAX","APP_NAME_MAX","MIN_MODULUS_CHARS","BASE64URL","LOOPBACK_HOSTS","PRIVATE_RSA_MEMBERS","value","method","url","raw","loopback","appEnv","jwk","name","n","pub","parsed","key","imported","keys","entry","member","configs","appEnvRaw","env","allowedOrigins","origin","publicOrigin","issuer","audience","appName","senderAddress","operatorEmails","email","active","proof","retired","jwks","other","i","cached","pending"],"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":"A,sDA4FA,IAAM,gBAA8B,IAAIA,EAAAA,mBAAmB,IAAK,eAAe,EAEzEQ,EAAY,IACZC,EAAe,GAEfC,EAAoB,IACpBC,EAAY,mBACZC,EAAiB,IAAI,IAAI,CAAC,YAAa,YAAa,OAAO,CAAC,EAC5DC,EAAsB,CAAC,IAAK,IAAK,IAAK,KAAM,KAAM,KAAM,MAAO,GAAG,EAE3D,kBAAqB,GAChCC,EAAM,QAAUN,GAChBM,IAAUA,EAAM,YAAY,GAC5B,+CAA+C,KAAKA,CAAK,EAErD,cAAiB,GAA2B,CAChD,GAAI,OAAOA,GAAU,UAAYA,EAAM,SAAW,EAAG,MAAM,YAAY,EACvE,OAAOA,CACT,EAEM,gBAAkB,EAAgB,IAAyB,CAE/D,GADI,OAAOA,GAAU,WAAYA,GAC7B,OAAO,QAAQ,IAAIA,EAAOC,CAAM,GAAM,WAAY,MAAM,YAAY,CAC1E,EAGM,aAAe,EAAa,IAA4B,CAC5D,IAAIC,EACJ,GAAI,CACF,EAAM,IAAI,IAAIC,CAAG,CACnB,MAAQ,CACN,MAAM,YAAY,CACpB,CACA,GAAID,EAAI,WAAa,IAAMA,EAAI,WAAa,IAAMA,EAAI,OAAS,GAAI,MAAM,YAAY,EACrF,IAAME,EAAWN,EAAe,IAAII,EAAI,QAAQ,EAChD,GAAIG,IAAW,QAAUH,EAAI,WAAa,SAAW,CAACE,EAAWF,EAAI,WAAa,UAAYE,EAC5F,MAAM,YAAY,EAEpB,OAAOF,CACT,EAGM,gBAAkB,EAAa,IAA+B,CAClE,IAAMA,EAAM,YAAYC,EAAKE,CAAM,EACnC,GAAIH,EAAI,WAAa,KAAOA,EAAI,SAAW,IAAMA,EAAI,SAAWC,EAAK,MAAM,YAAY,EACvF,OAAOD,EAAI,MACb,EAEM,SAAY,GAAqD,OAAOF,GAAU,YAAYA,EAE9F,UAAa,GAAyB,CAC1C,GAAI,CACF,OAAO,KAAK,MAAMG,CAAG,CACvB,MAAQ,CACN,MAAM,YAAY,CACpB,CACF,EAEM,cAAgB,EAA8B,IAAyB,CAC3E,IAAMH,EAAQM,EAAIC,GAClB,GAAI,OAAOP,GAAU,UAAY,CAACH,EAAU,KAAKG,CAAK,EAAG,MAAM,YAAY,EAC3E,OAAOA,CACT,EAGM,iBAAoB,GAAuC,CAG/D,GAFIM,EAAI,MAAQ,OACZA,EAAI,MAAQ,IAAA,IAAaA,EAAI,MAAQ,SACrCA,EAAI,MAAQ,IAAA,IAAaA,EAAI,MAAQ,MAAO,MAAM,YAAY,CACpE,EAEM,cAAiB,GAAiD,CACtE,IAAME,EAAI,aAAaF,EAAK,GAAG,EAC/B,GAAIE,EAAE,OAASZ,EAAmB,MAAM,YAAY,EACpD,MAAO,CAAE,EAAG,aAAaU,EAAK,GAAG,EAAG,IAAK,MAAO,GAAE,CACpD,EAEM,aAAe,KAAO,KAAoD,CAC9E,IAAK,QACL,EAAGG,EAAI,EACP,IAAK,MAAA,EAAMtB,EAAAA,uBAAAA,CAAuB,CAAE,EAAGsB,EAAI,EAAG,IAAK,MAAO,EAAGA,EAAI,CAAE,CAAC,EACpE,IAAK,MACL,EAAGA,EAAI,EACP,IAAK,KACP,GAEA,eAAe,UAAU,EAAoE,CAC3F,IAAMC,EAAS,UAAUP,CAAG,EAC5B,GAAI,CAAC,SAASO,CAAM,EAAG,MAAM,YAAY,EACzC,iBAAiBA,CAAM,EACvB,IAAMD,EAAM,cAAcC,CAAM,EAC1BJ,EAAuB,CAC3B,GAAGG,EACH,EAAG,aAAaC,EAAQ,GAAG,EAC3B,GAAI,aAAaA,EAAQ,IAAI,EAC7B,GAAI,aAAaA,EAAQ,IAAI,EAC7B,EAAG,aAAaA,EAAQ,GAAG,EAC3B,EAAG,aAAaA,EAAQ,GAAG,EAC3B,GAAI,aAAaA,EAAQ,IAAI,CAC/B,EACIC,EACJ,GAAI,CACF,IAAMC,EAAW,MAAA,EAAMxB,EAAAA,UAAAA,CAAUkB,EAAK,QAAS,CAAE,YAAa,EAAM,CAAC,EACrE,GAAIM,aAAoB,WAAY,MAAM,YAAY,EACtD,EAAMA,CACR,MAAQ,CACN,MAAM,YAAY,CACpB,CACA,MAAO,CAAE,MAAK,OAAQ,MAAM,aAAaH,CAAG,CAAE,CAChD,CAGA,eAAe,aAAa,EAAsD,CAChF,GAAIN,IAAQ,IAAA,IAAaA,EAAI,KAAK,CAAC,CAAC,SAAW,EAAG,MAAO,CAAC,EAC1D,IAAMO,EAAS,UAAUP,CAAG,EAC5B,GAAI,CAAC,SAASO,CAAM,GAAK,CAAC,MAAM,QAAQA,EAAO,IAAI,EAAG,MAAM,YAAY,EACxE,IAAMG,EAA2B,CAAC,EAClC,IAAK,IAAMC,KAASJ,EAAO,KAAM,CAG/B,GAFI,CAAC,SAASI,CAAK,IACnB,iBAAiBA,CAAK,EAClBf,EAAoB,KAAM,GAAWgB,KAAUD,CAAK,GAAG,MAAM,YAAY,EAC7ED,EAAK,KAAK,MAAM,aAAa,cAAcC,CAAK,CAAC,CAAC,CACpD,CACA,OAAOD,CACT,CAEA,IAAMG,EAAU,IAAI,QAEpB,eAAe,YAAY,EAAyC,CAClE,IAAMC,EAAY,cAAcC,EAAI,OAAO,EAC3C,GAAID,IAAc,SAAWA,IAAc,aAAc,MAAM,YAAY,EAC3E,IAAMZ,EAAqBY,EAErBE,EAAiB,IAAI,IACzB,cAAcD,EAAI,eAAe,CAAC,CAC/B,MAAM,GAAG,CAAC,CACV,IAAK,GAAWE,EAAO,KAAK,CAAC,CAAC,CAC9B,OAAQ,GAAWA,EAAO,OAAS,CAAC,CAAC,CACrC,IAAK,GAAW,eAAeA,EAAQf,CAAM,CAAC,CACnD,EACA,GAAIc,EAAe,OAAS,EAAG,MAAM,YAAY,EAEjD,IAAME,EAAe,eAAe,cAAcH,EAAI,aAAa,EAAGb,CAAM,EAEtEiB,EAAS,cAAcJ,EAAI,WAAW,EAC5C,YAAYI,EAAQjB,CAAM,EAC1B,IAAMkB,EAAW,cAAcL,EAAI,QAAQ,EACrCM,EAAU,cAAcN,EAAI,QAAQ,EAC1C,GAAIM,EAAQ,OAAS7B,GAAgB,kBAAkB,KAAK6B,CAAO,EAAG,MAAM,YAAY,EACxF,IAAMC,EAAgB,cAAcP,EAAI,cAAc,EAEtD,GADI,CAAC,kBAAkBO,CAAa,GAChC,OAAOP,EAAI,iBAAoB,SAAU,MAAM,YAAY,EAC/D,IAAMQ,EAAiB,IAAI,IACzBR,EAAI,gBAAgB,MAAM,GAAG,CAAC,CAC3B,IAAK,GAAUJ,EAAM,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAC1C,OAAQ,GAAUA,EAAM,OAAS,CAAC,CACvC,EACA,IAAK,IAAMa,KAASD,EAAgB,GAAI,CAAC,kBAAkBC,CAAK,EAAG,MAAM,YAAY,EAErF,eAAeT,EAAI,QAAS,SAAS,EACrC,eAAeA,EAAI,QAAS,OAAO,EACnC,eAAeA,EAAI,WAAY,MAAM,EACrC,eAAeA,EAAI,aAAc,OAAO,EAExC,IAAMU,EAAS,MAAM,UAAU,cAAcV,EAAI,oBAAoB,CAAC,EACtE,GAAI,CACF,IAAMW,EAAQ,MAAM,IAAIxC,EAAAA,QAAQ,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAE,IAAK,QAAS,IAAKuC,EAAO,OAAO,GAAI,CAAC,CAAC,CAAC,KAAKA,EAAO,GAAG,EAChH,MAAA,EAAMtC,EAAAA,UAAAA,CAAUuC,GAAA,EAAOtC,EAAAA,kBAAAA,CAAkB,CAAE,KAAM,CAACqC,EAAO,MAAM,CAAE,CAAC,EAAG,CAAE,WAAY,CAAC,OAAO,CAAE,CAAC,CAChG,MAAQ,CACN,MAAM,YAAY,CACpB,CACA,IAAME,GAAW,MAAM,aAAaZ,EAAI,6BAA6B,EAAA,CAAG,OACrE,GAAQP,EAAI,MAAQiB,EAAO,OAAO,GACrC,EACMG,EAAO,CACXH,EAAO,OACP,GAAGE,EAAQ,QAAQ,EAAK,IAAMA,EAAQ,UAAW,GAAUE,EAAM,MAAQrB,EAAI,GAAG,IAAMsB,CAAC,CACzF,EAEA,MAAO,CACL,iBACA,SACA,UACA,WACA,SACA,OACA,iBACA,eACA,gBACA,QAAS,CAAE,IAAKL,EAAO,IAAK,IAAKA,EAAO,OAAO,GAAI,EACnD,UAAA,EAAUrC,EAAAA,kBAAAA,CAAkB,CAAE,KAAM,CAAC,GAAGwC,CAAI,CAAE,CAAC,CACjD,CACF,CAMA,SAAgB,WAAW,EAAyC,CAClE,IAAMG,EAASlB,EAAQ,IAAIE,CAAG,EAC9B,GAAIgB,IAAW,IAAA,GAAW,OAAOA,EACjC,IAAMC,EAAU,YAAYjB,CAAG,EAG/B,OAFAF,EAAQ,IAAIE,EAAKiB,CAAO,EACxBA,EAAQ,UAAYnB,EAAQ,OAAOE,CAAG,CAAC,EAChCiB,CACT,C"}
|