@vielzeug/archive 26.11.12 → 26.11.14
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/_server/d1.d.ts +23 -0
- package/dist/_server/d1.d.ts.map +1 -0
- package/dist/_server/execution.d.ts +5 -0
- package/dist/_server/execution.d.ts.map +1 -0
- package/dist/_server/http.cjs +2 -0
- package/dist/_server/http.cjs.map +1 -0
- package/dist/{worker → _server}/http.d.ts +6 -11
- package/dist/_server/http.d.ts.map +1 -0
- package/dist/{worker → _server}/http.js +18 -44
- package/dist/_server/http.js.map +1 -0
- package/dist/{worker/_service.cjs → _server/service.cjs} +1 -1
- package/dist/{worker/_service.js.map → _server/service.cjs.map} +1 -1
- package/dist/{worker/_service.d.ts → _server/service.d.ts} +1 -1
- package/dist/_server/service.d.ts.map +1 -0
- package/dist/{worker/_service.js → _server/service.js} +2 -2
- package/dist/_server/service.js.map +1 -0
- package/dist/_server/uuid.cjs +2 -0
- package/dist/_server/uuid.cjs.map +1 -0
- package/dist/_server/uuid.d.ts +3 -0
- package/dist/_server/uuid.d.ts.map +1 -0
- package/dist/_server/uuid.js +5 -0
- package/dist/_server/uuid.js.map +1 -0
- package/dist/auth/_confirmation.cjs +3 -3
- package/dist/auth/_confirmation.cjs.map +1 -1
- package/dist/auth/_confirmation.js +4 -4
- package/dist/auth/_confirmation.js.map +1 -1
- package/dist/auth/config.cjs.map +1 -1
- package/dist/auth/config.d.ts +1 -1
- package/dist/auth/config.js.map +1 -1
- package/dist/auth/errors.cjs +1 -1
- package/dist/auth/errors.cjs.map +1 -1
- package/dist/auth/errors.d.ts +1 -1
- package/dist/auth/errors.d.ts.map +1 -1
- package/dist/auth/errors.js +1 -1
- package/dist/auth/errors.js.map +1 -1
- package/dist/auth/store.cjs.map +1 -1
- package/dist/auth/store.d.ts +1 -1
- package/dist/auth/store.js.map +1 -1
- package/dist/auth/worker.cjs +1 -1
- package/dist/auth/worker.cjs.map +1 -1
- package/dist/auth/worker.d.ts +1 -1
- package/dist/auth/worker.d.ts.map +1 -1
- package/dist/auth/worker.js +153 -152
- package/dist/auth/worker.js.map +1 -1
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/worker/_cors.cjs +2 -0
- package/dist/worker/_cors.cjs.map +1 -0
- package/dist/worker/_cors.d.ts +13 -0
- package/dist/worker/_cors.d.ts.map +1 -0
- package/dist/worker/_cors.js +32 -0
- package/dist/worker/_cors.js.map +1 -0
- package/dist/worker/_pagination.cjs +1 -1
- package/dist/worker/_pagination.cjs.map +1 -1
- package/dist/worker/_pagination.js +18 -18
- package/dist/worker/_pagination.js.map +1 -1
- package/dist/worker/auth.cjs +1 -1
- package/dist/worker/auth.cjs.map +1 -1
- package/dist/worker/auth.d.ts.map +1 -1
- package/dist/worker/auth.js +18 -18
- package/dist/worker/auth.js.map +1 -1
- package/dist/worker/env.cjs +1 -1
- package/dist/worker/env.cjs.map +1 -1
- package/dist/worker/env.d.ts +1 -22
- package/dist/worker/env.d.ts.map +1 -1
- package/dist/worker/env.js +1 -1
- package/dist/worker/env.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 +263 -262
- package/dist/worker/publications.js.map +1 -1
- package/dist/worker/worker.cjs +1 -1
- package/dist/worker/worker.cjs.map +1 -1
- package/dist/worker/worker.d.ts +2 -4
- package/dist/worker/worker.d.ts.map +1 -1
- package/dist/worker/worker.js +124 -122
- package/dist/worker/worker.js.map +1 -1
- package/dist/worker.d.ts +5 -3
- package/dist/worker.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/worker/_service.cjs.map +0 -1
- package/dist/worker/_service.d.ts.map +0 -1
- package/dist/worker/http.cjs +0 -2
- package/dist/worker/http.cjs.map +0 -1
- package/dist/worker/http.d.ts.map +0 -1
- package/dist/worker/http.js.map +0 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The D1 statement subset the core binds; null params model nullable columns. */
|
|
2
|
+
export interface D1Statement {
|
|
3
|
+
all<T>(): Promise<{
|
|
4
|
+
results?: T[];
|
|
5
|
+
}>;
|
|
6
|
+
bind(...params: (string | number | null)[]): D1Statement;
|
|
7
|
+
first<T>(): Promise<T | null>;
|
|
8
|
+
run(): Promise<{
|
|
9
|
+
meta?: {
|
|
10
|
+
changes?: number;
|
|
11
|
+
};
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */
|
|
15
|
+
export interface D1Like {
|
|
16
|
+
batch(statements: D1Statement[]): Promise<{
|
|
17
|
+
meta?: {
|
|
18
|
+
changes?: number;
|
|
19
|
+
};
|
|
20
|
+
}[]>;
|
|
21
|
+
prepare(sql: string): D1Statement;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=d1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"d1.d.ts","sourceRoot":"","sources":["../../src/_server/d1.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,WAAW,CAAC;IACzD,KAAK,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CACjD;AAED,8FAA8F;AAC9F,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,EAAE,CAAC,CAAC;IAC7E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../../src/_server/execution.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5C"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../contract.cjs");var ApiError=class extends Error{code;retryAfterSeconds;status;diagnostic;constructor(e,t,n,r){super(n,r),this.name=`ApiError`,this.code=t,this.retryAfterSeconds=r?.retryAfterSeconds,this.status=e,this.diagnostic=r?.diagnostic}},badRequest=(e=`The request is not valid.`)=>new ApiError(400,`bad-request`,e),unauthenticated=()=>new ApiError(401,`unauthenticated`,`Sign-in is required.`),forbidden=(e=`That action is not allowed.`)=>new ApiError(403,`forbidden`,e),notFound=()=>new ApiError(404,`not-found`,`No such entry.`),conflict=(e=`The entry changed since you saw it.`)=>new ApiError(409,`conflict`,e),invalid=(e=`The payload is not valid.`)=>new ApiError(422,`invalid-payload`,e),json=(e,t,n={})=>new Response(JSON.stringify(t),{headers:{"content-type":`application/json; charset=utf-8`,...n},status:e}),t={"cache-control":`no-store`},errorBody=(e,n,r,i)=>json(e,{error:{code:n,message:r,requestId:i}},t);function toErrorResponse(e,n){if(e instanceof ApiError){let r=e.retryAfterSeconds===void 0?t:{...t,"retry-after":String(e.retryAfterSeconds)};return json(e.status,{error:{code:e.code,message:e.message,requestId:n}},r)}return errorBody(500,`internal`,`Something went wrong.`,n)}function queryParams(e,t,n=[]){let r=e.searchParams,i=new Set;for(let e of r.keys()){if(!t.includes(e)||i.has(e)&&!n.includes(e))throw badRequest();i.add(e)}return r}var n=1e4,bodyTimeout=()=>new ApiError(408,`timeout`,`The request body took too long.`);async function readJsonBody(t){if((t.headers.get(`content-type`)??``).split(`;`,1)[0].trim().toLowerCase()!==`application/json`)throw new ApiError(415,`unsupported-media`,`JSON bodies only.`);let r=t.body?.getReader();if(!r)throw badRequest();let i=Date.now(),a,o=[],s=0;try{for(;;){let{done:t,value:c}=await Promise.race([r.read(),new Promise((e,t)=>{a=setTimeout(()=>t(bodyTimeout()),Math.max(0,n-(Date.now()-i)))})]);if(clearTimeout(a),a=void 0,t)break;if(s+=c.byteLength,s>e.ARCHIVE_LIMITS.requestBytes)throw new ApiError(413,`too-large`,`The request is too large.`);o.push(c)}}catch(e){throw r.cancel().catch(()=>void 0),e}finally{a!==void 0&&clearTimeout(a),r.releaseLock()}let c=new Uint8Array(s),l=0;for(let e of o)c.set(e,l),l+=e.byteLength;if(s===0)throw badRequest();try{return JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(c))}catch{throw badRequest()}}var r=4096;function sessionToken(e){let t=e.headers.get(`cookie`);if(t===null)return null;if(t.length>r)throw badRequest();let n=[];for(let e of t.split(`;`)){let t=e.indexOf(`=`);t!==-1&&e.slice(0,t).trim()===`archive_session`&&n.push(e.slice(t+1).trim())}if(n.length>1)throw badRequest();return n.length===1&&n[0].length>0?n[0]:null}var expiredSessionCookie=t=>e.sessionCookieValue(``,{maxAgeSeconds:0,secure:t});exports.ApiError=ApiError,exports.HTTP_BODY_TIMEOUT_MS=n,exports.badRequest=badRequest,exports.conflict=conflict,exports.errorBody=errorBody,exports.expiredSessionCookie=expiredSessionCookie,exports.forbidden=forbidden,exports.invalid=invalid,exports.json=json,exports.noStore=t,exports.notFound=notFound,exports.queryParams=queryParams,exports.readJsonBody=readJsonBody,exports.sessionToken=sessionToken,exports.toErrorResponse=toErrorResponse,exports.unauthenticated=unauthenticated;
|
|
2
|
+
//# sourceMappingURL=http.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.cjs","names":["ARCHIVE_LIMITS","sessionCookieValue","require_contract","message","opts","code","status","body","headers","noStore","error","requestId","params","url","seen","key","allowed","repeatable","HTTP_BODY_TIMEOUT_MS","request","reader","startedAt","timeoutId","chunks","total","reject","done","value","merged","offset","chunk","SESSION_COOKIE_MAX","header","matches","part","eq"],"sources":["../../src/_server/http.ts"],"sourcesContent":["import { ARCHIVE_LIMITS, type ArchiveErrorCode, SESSION_COOKIE_NAME, sessionCookieValue } from '../contract';\n\n/**\n * HTTP plumbing shared by the catalog Worker and the identity issuer: the one error\n * shape, bounded body reads, and strict query/header/cookie guards. Handlers throw\n * ApiError; the entry point sanitizes — no stack, SQL, body, or PII ever reaches a\n * response. Internal to the package: clients parse the wire envelope\n * (`archiveErrorResponseSchema`), never this class. Origin/CORS posture lives in\n * `worker/_cors.ts` and `auth/worker.ts` because each surface's policy differs.\n */\nexport class ApiError extends Error {\n readonly code: ArchiveErrorCode;\n readonly retryAfterSeconds?: number;\n readonly status: number;\n readonly diagnostic?: ArchiveDiagnostic;\n constructor(\n status: number,\n code: ArchiveErrorCode,\n message: string,\n opts?: ErrorOptions & { retryAfterSeconds?: number; diagnostic?: ArchiveDiagnostic },\n ) {\n super(message, opts);\n this.name = 'ApiError';\n this.code = code;\n this.retryAfterSeconds = opts?.retryAfterSeconds;\n this.status = status;\n this.diagnostic = opts?.diagnostic;\n }\n}\n\nexport type ArchiveDiagnostic =\n | { readonly kind: 'configuration'; readonly field: string }\n | { readonly kind: 'database' | 'record' | 'schema' | 'internal' | 'cache' | 'collection' };\n\nexport const badRequest = (message = 'The request is not valid.'): ApiError =>\n new ApiError(400, 'bad-request', message);\nexport const unauthenticated = (): ApiError => new ApiError(401, 'unauthenticated', 'Sign-in is required.');\nexport const forbidden = (message = 'That action is not allowed.'): ApiError => new ApiError(403, 'forbidden', message);\nexport const notFound = (): ApiError => new ApiError(404, 'not-found', 'No such entry.');\nexport const conflict = (message = 'The entry changed since you saw it.'): ApiError =>\n new ApiError(409, 'conflict', message);\nexport const invalid = (message = 'The payload is not valid.'): ApiError =>\n new ApiError(422, 'invalid-payload', message);\n\nexport const json = (status: number, body: unknown, headers: HeadersInit = {}): Response =>\n new Response(JSON.stringify(body), {\n headers: { 'content-type': 'application/json; charset=utf-8', ...headers },\n status,\n });\n\nexport const noStore = { 'cache-control': 'no-store' } as const;\n\nexport const errorBody = (status: number, code: string, message: string, requestId: string): Response =>\n json(status, { error: { code, message, requestId } }, noStore);\n\n/** Sanitizes any thrown value into the public error shape; unknowns become 500. */\nexport function toErrorResponse(error: unknown, requestId: string): Response {\n if (error instanceof ApiError) {\n const headers: HeadersInit =\n error.retryAfterSeconds === undefined ? noStore : { ...noStore, 'retry-after': String(error.retryAfterSeconds) };\n return json(error.status, { error: { code: error.code, message: error.message, requestId } }, headers);\n }\n return errorBody(500, 'internal', 'Something went wrong.', requestId);\n}\n\n/**\n * Rejects query strings with unknown or repeated parameters. Every accepted route\n * names its full allow-list; anything else is a client bug, not something to ignore.\n * Keys listed in `repeatable` may carry several values (read back with `getAll`);\n * every other key stays single-valued.\n */\nexport function queryParams(url: URL, allowed: readonly string[], repeatable: readonly string[] = []): URLSearchParams {\n const params = url.searchParams;\n const seen = new Set<string>();\n for (const key of params.keys()) {\n if (!allowed.includes(key) || (seen.has(key) && !repeatable.includes(key))) throw badRequest();\n seen.add(key);\n }\n return params;\n}\n\n/** Hard deadline for reading a request body; slow streams fail as 408. */\nexport const HTTP_BODY_TIMEOUT_MS = 10000;\n\nconst bodyTimeout = (): ApiError => new ApiError(408, 'timeout', 'The request body took too long.');\n\n/**\n * Reads a JSON body with a hard byte ceiling and a wall-clock deadline enforced while\n * streaming — a lying or absent Content-Length and a stalled sender are both cut off.\n * Wrong media types are 415, oversized 413, timed out 408, malformed JSON 400; schema\n * validation happens afterwards at the route boundary.\n */\nexport async function readJsonBody(request: Request): Promise<unknown> {\n // Exactly the application/json media type; parameters (charset etc.) may follow.\n const mediaType = (request.headers.get('content-type') ?? '').split(';', 1)[0].trim().toLowerCase();\n if (mediaType !== 'application/json') {\n throw new ApiError(415, 'unsupported-media', 'JSON bodies only.');\n }\n const reader = request.body?.getReader();\n if (!reader) throw badRequest();\n const startedAt = Date.now();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n for (;;) {\n const { done, value } = await Promise.race([\n reader.read(),\n new Promise<never>((_r, reject) => {\n timeoutId = setTimeout(\n () => reject(bodyTimeout()),\n Math.max(0, HTTP_BODY_TIMEOUT_MS - (Date.now() - startedAt)),\n );\n }),\n ]);\n clearTimeout(timeoutId);\n timeoutId = undefined;\n if (done) break;\n total += value.byteLength;\n if (total > ARCHIVE_LIMITS.requestBytes) throw new ApiError(413, 'too-large', 'The request is too large.');\n chunks.push(value);\n }\n } catch (error) {\n // Release the remainder so the failure isn't blocked on a stalled sender.\n reader.cancel().catch(() => undefined);\n throw error;\n } finally {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n reader.releaseLock();\n }\n const merged = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n if (total === 0) throw badRequest();\n try {\n return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(merged));\n } catch {\n throw badRequest();\n }\n}\n\nconst SESSION_COOKIE_MAX = 4096;\n\n/**\n * Reads the session cookie. A duplicate cookie name or an oversized header is a\n * malformed request, not a missing session: callers distinguish null (anonymous)\n * from a token to verify.\n */\nexport function sessionToken(request: Request): string | null {\n const header = request.headers.get('cookie');\n if (header === null) return null;\n if (header.length > SESSION_COOKIE_MAX) throw badRequest();\n const matches: string[] = [];\n for (const part of header.split(';')) {\n const eq = part.indexOf('=');\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === SESSION_COOKIE_NAME) matches.push(part.slice(eq + 1).trim());\n }\n if (matches.length > 1) throw badRequest();\n return matches.length === 1 && matches[0].length > 0 ? matches[0] : null;\n}\n\n/** The expired-session Set-Cookie value the sign-out route and account erasure send. */\nexport const expiredSessionCookie = (secure: boolean): string => sessionCookieValue('', { maxAgeSeconds: 0, secure });\n"],"mappings":"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"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type ArchiveErrorCode } from '../contract';
|
|
2
|
-
import type { ApiConfig } from './env';
|
|
3
2
|
/**
|
|
4
|
-
* HTTP plumbing shared by
|
|
5
|
-
*
|
|
6
|
-
* sanitizes — no stack, SQL, body, or PII ever reaches a
|
|
7
|
-
*
|
|
3
|
+
* HTTP plumbing shared by the catalog Worker and the identity issuer: the one error
|
|
4
|
+
* shape, bounded body reads, and strict query/header/cookie guards. Handlers throw
|
|
5
|
+
* ApiError; the entry point sanitizes — no stack, SQL, body, or PII ever reaches a
|
|
6
|
+
* response. Internal to the package: clients parse the wire envelope
|
|
7
|
+
* (`archiveErrorResponseSchema`), never this class. Origin/CORS posture lives in
|
|
8
|
+
* `worker/_cors.ts` and `auth/worker.ts` because each surface's policy differs.
|
|
8
9
|
*/
|
|
9
10
|
export declare class ApiError extends Error {
|
|
10
11
|
readonly code: ArchiveErrorCode;
|
|
@@ -42,12 +43,6 @@ export declare function toErrorResponse(error: unknown, requestId: string): Resp
|
|
|
42
43
|
* every other key stays single-valued.
|
|
43
44
|
*/
|
|
44
45
|
export declare function queryParams(url: URL, allowed: readonly string[], repeatable?: readonly string[]): URLSearchParams;
|
|
45
|
-
/** Exact-origin plus request-marker guard for every mutation: no wildcards, no nulls. */
|
|
46
|
-
export declare function requireMutationGuard(request: Request, config: ApiConfig): void;
|
|
47
|
-
/** CORS response headers for one already-allowed origin; absent for foreign origins. */
|
|
48
|
-
export declare function corsHeaders(request: Request, config: ApiConfig): Record<string, string>;
|
|
49
|
-
/** Answers a preflight for an allowed origin; foreign or missing origins get 403. */
|
|
50
|
-
export declare function preflight(request: Request, config: ApiConfig): Response;
|
|
51
46
|
/** Hard deadline for reading a request body; slow streams fail as 408. */
|
|
52
47
|
export declare const HTTP_BODY_TIMEOUT_MS = 10000;
|
|
53
48
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/_server/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,gBAAgB,EAA2C,MAAM,aAAa,CAAC;AAE7G;;;;;;;GAOG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC;gBAEtC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,YAAY,GAAG;QAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;CASvF;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,CAAA;CAAE,CAAC;AAE9F,eAAO,MAAM,UAAU,GAAI,gBAAqC,KAAG,QACxB,CAAC;AAC5C,eAAO,MAAM,eAAe,QAAO,QAAwE,CAAC;AAC5G,eAAO,MAAM,SAAS,GAAI,gBAAuC,KAAG,QAAmD,CAAC;AACxH,eAAO,MAAM,QAAQ,QAAO,QAA4D,CAAC;AACzF,eAAO,MAAM,QAAQ,GAAI,gBAA+C,KAAG,QACnC,CAAC;AACzC,eAAO,MAAM,OAAO,GAAI,gBAAqC,KAAG,QACjB,CAAC;AAEhD,eAAO,MAAM,IAAI,GAAI,QAAQ,MAAM,EAAE,MAAM,OAAO,EAAE,UAAS,WAAgB,KAAG,QAI5E,CAAC;AAEL,eAAO,MAAM,OAAO;;CAA2C,CAAC;AAEhE,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,SAAS,MAAM,EAAE,WAAW,MAAM,KAAG,QAC7B,CAAC;AAEjE,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,CAO3E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,GAAE,SAAS,MAAM,EAAO,GAAG,eAAe,CAQrH;AAED,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,QAAQ,CAAC;AAI1C;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAkDrE;AAID;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAY5D;AAED,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,OAAO,KAAG,MAA8D,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ARCHIVE_LIMITS as e,
|
|
2
|
-
// src/
|
|
1
|
+
import { ARCHIVE_LIMITS as e, sessionCookieValue as t } from "../contract.js";
|
|
2
|
+
// src/_server/http.ts
|
|
3
3
|
var ApiError = class extends Error {
|
|
4
4
|
code;
|
|
5
5
|
retryAfterSeconds;
|
|
@@ -14,22 +14,22 @@ var ApiError = class extends Error {
|
|
|
14
14
|
...n
|
|
15
15
|
},
|
|
16
16
|
status: e
|
|
17
|
-
}),
|
|
17
|
+
}), n = { "cache-control": "no-store" }, errorBody = (e, t, r, i) => json(e, { error: {
|
|
18
18
|
code: t,
|
|
19
|
-
message:
|
|
20
|
-
requestId:
|
|
21
|
-
} },
|
|
19
|
+
message: r,
|
|
20
|
+
requestId: i
|
|
21
|
+
} }, n);
|
|
22
22
|
function toErrorResponse(e, t) {
|
|
23
23
|
if (e instanceof ApiError) {
|
|
24
|
-
let
|
|
25
|
-
...
|
|
24
|
+
let r = e.retryAfterSeconds === void 0 ? n : {
|
|
25
|
+
...n,
|
|
26
26
|
"retry-after": String(e.retryAfterSeconds)
|
|
27
27
|
};
|
|
28
28
|
return json(e.status, { error: {
|
|
29
29
|
code: e.code,
|
|
30
30
|
message: e.message,
|
|
31
31
|
requestId: t
|
|
32
|
-
} },
|
|
32
|
+
} }, r);
|
|
33
33
|
}
|
|
34
34
|
return errorBody(500, "internal", "Something went wrong.", t);
|
|
35
35
|
}
|
|
@@ -41,51 +41,25 @@ function queryParams(e, t, n = []) {
|
|
|
41
41
|
}
|
|
42
42
|
return r;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
let n = e.headers.get("origin");
|
|
46
|
-
if (n === null || !t.allowedOrigins.has(n) || e.headers.get("x-archive-request") !== "1") throw forbidden();
|
|
47
|
-
}
|
|
48
|
-
function corsHeaders(e, t) {
|
|
49
|
-
let n = e.headers.get("origin");
|
|
50
|
-
return n === null || !t.allowedOrigins.has(n) ? {} : {
|
|
51
|
-
"access-control-allow-credentials": "true",
|
|
52
|
-
"access-control-allow-origin": n,
|
|
53
|
-
vary: "Origin"
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function preflight(e, r) {
|
|
57
|
-
let i = e.headers.get("origin");
|
|
58
|
-
return i === null || !r.allowedOrigins.has(i) ? errorBody(403, "forbidden", "That action is not allowed.", crypto.randomUUID()) : new Response(null, {
|
|
59
|
-
headers: {
|
|
60
|
-
"access-control-allow-credentials": "true",
|
|
61
|
-
"access-control-allow-headers": `Content-Type, ${t}, ${n}`,
|
|
62
|
-
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
63
|
-
"access-control-allow-origin": i,
|
|
64
|
-
"access-control-max-age": "600",
|
|
65
|
-
vary: "Origin"
|
|
66
|
-
},
|
|
67
|
-
status: 204
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
var a = 1e4, bodyTimeout = () => new ApiError(408, "timeout", "The request body took too long.");
|
|
44
|
+
var r = 1e4, bodyTimeout = () => new ApiError(408, "timeout", "The request body took too long.");
|
|
71
45
|
async function readJsonBody(t) {
|
|
72
46
|
if ((t.headers.get("content-type") ?? "").split(";", 1)[0].trim().toLowerCase() !== "application/json") throw new ApiError(415, "unsupported-media", "JSON bodies only.");
|
|
73
47
|
let n = t.body?.getReader();
|
|
74
48
|
if (!n) throw badRequest();
|
|
75
|
-
let
|
|
49
|
+
let i = Date.now(), a, o = [], s = 0;
|
|
76
50
|
try {
|
|
77
51
|
for (;;) {
|
|
78
52
|
let { done: t, value: c } = await Promise.race([n.read(), new Promise((e, t) => {
|
|
79
|
-
|
|
53
|
+
a = setTimeout(() => t(bodyTimeout()), Math.max(0, r - (Date.now() - i)));
|
|
80
54
|
})]);
|
|
81
|
-
if (clearTimeout(
|
|
55
|
+
if (clearTimeout(a), a = void 0, t) break;
|
|
82
56
|
if (s += c.byteLength, s > e.requestBytes) throw new ApiError(413, "too-large", "The request is too large.");
|
|
83
57
|
o.push(c);
|
|
84
58
|
}
|
|
85
59
|
} catch (e) {
|
|
86
60
|
throw n.cancel().catch(() => void 0), e;
|
|
87
61
|
} finally {
|
|
88
|
-
|
|
62
|
+
a !== void 0 && clearTimeout(a), n.releaseLock();
|
|
89
63
|
}
|
|
90
64
|
let c = new Uint8Array(s), l = 0;
|
|
91
65
|
for (let e of o) c.set(e, l), l += e.byteLength;
|
|
@@ -96,11 +70,11 @@ async function readJsonBody(t) {
|
|
|
96
70
|
throw badRequest();
|
|
97
71
|
}
|
|
98
72
|
}
|
|
99
|
-
var
|
|
73
|
+
var i = 4096;
|
|
100
74
|
function sessionToken(e) {
|
|
101
75
|
let t = e.headers.get("cookie");
|
|
102
76
|
if (t === null) return null;
|
|
103
|
-
if (t.length >
|
|
77
|
+
if (t.length > i) throw badRequest();
|
|
104
78
|
let n = [];
|
|
105
79
|
for (let e of t.split(";")) {
|
|
106
80
|
let t = e.indexOf("=");
|
|
@@ -109,10 +83,10 @@ function sessionToken(e) {
|
|
|
109
83
|
if (n.length > 1) throw badRequest();
|
|
110
84
|
return n.length === 1 && n[0].length > 0 ? n[0] : null;
|
|
111
85
|
}
|
|
112
|
-
var expiredSessionCookie = (e) =>
|
|
86
|
+
var expiredSessionCookie = (e) => t("", {
|
|
113
87
|
maxAgeSeconds: 0,
|
|
114
88
|
secure: e
|
|
115
89
|
});
|
|
116
|
-
export { ApiError,
|
|
90
|
+
export { ApiError, r as HTTP_BODY_TIMEOUT_MS, badRequest, conflict, errorBody, expiredSessionCookie, forbidden, invalid, json, n as noStore, notFound, queryParams, readJsonBody, sessionToken, toErrorResponse, unauthenticated };
|
|
117
91
|
|
|
118
92
|
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","names":["message","opts","code","status","body","headers","noStore","error","requestId","params","url","seen","key","allowed","repeatable","HTTP_BODY_TIMEOUT_MS","request","reader","startedAt","timeoutId","chunks","total","reject","done","value","ARCHIVE_LIMITS","merged","offset","chunk","SESSION_COOKIE_MAX","header","matches","part","eq","sessionCookieValue"],"sources":["../../src/_server/http.ts"],"sourcesContent":["import { ARCHIVE_LIMITS, type ArchiveErrorCode, SESSION_COOKIE_NAME, sessionCookieValue } from '../contract';\n\n/**\n * HTTP plumbing shared by the catalog Worker and the identity issuer: the one error\n * shape, bounded body reads, and strict query/header/cookie guards. Handlers throw\n * ApiError; the entry point sanitizes — no stack, SQL, body, or PII ever reaches a\n * response. Internal to the package: clients parse the wire envelope\n * (`archiveErrorResponseSchema`), never this class. Origin/CORS posture lives in\n * `worker/_cors.ts` and `auth/worker.ts` because each surface's policy differs.\n */\nexport class ApiError extends Error {\n readonly code: ArchiveErrorCode;\n readonly retryAfterSeconds?: number;\n readonly status: number;\n readonly diagnostic?: ArchiveDiagnostic;\n constructor(\n status: number,\n code: ArchiveErrorCode,\n message: string,\n opts?: ErrorOptions & { retryAfterSeconds?: number; diagnostic?: ArchiveDiagnostic },\n ) {\n super(message, opts);\n this.name = 'ApiError';\n this.code = code;\n this.retryAfterSeconds = opts?.retryAfterSeconds;\n this.status = status;\n this.diagnostic = opts?.diagnostic;\n }\n}\n\nexport type ArchiveDiagnostic =\n | { readonly kind: 'configuration'; readonly field: string }\n | { readonly kind: 'database' | 'record' | 'schema' | 'internal' | 'cache' | 'collection' };\n\nexport const badRequest = (message = 'The request is not valid.'): ApiError =>\n new ApiError(400, 'bad-request', message);\nexport const unauthenticated = (): ApiError => new ApiError(401, 'unauthenticated', 'Sign-in is required.');\nexport const forbidden = (message = 'That action is not allowed.'): ApiError => new ApiError(403, 'forbidden', message);\nexport const notFound = (): ApiError => new ApiError(404, 'not-found', 'No such entry.');\nexport const conflict = (message = 'The entry changed since you saw it.'): ApiError =>\n new ApiError(409, 'conflict', message);\nexport const invalid = (message = 'The payload is not valid.'): ApiError =>\n new ApiError(422, 'invalid-payload', message);\n\nexport const json = (status: number, body: unknown, headers: HeadersInit = {}): Response =>\n new Response(JSON.stringify(body), {\n headers: { 'content-type': 'application/json; charset=utf-8', ...headers },\n status,\n });\n\nexport const noStore = { 'cache-control': 'no-store' } as const;\n\nexport const errorBody = (status: number, code: string, message: string, requestId: string): Response =>\n json(status, { error: { code, message, requestId } }, noStore);\n\n/** Sanitizes any thrown value into the public error shape; unknowns become 500. */\nexport function toErrorResponse(error: unknown, requestId: string): Response {\n if (error instanceof ApiError) {\n const headers: HeadersInit =\n error.retryAfterSeconds === undefined ? noStore : { ...noStore, 'retry-after': String(error.retryAfterSeconds) };\n return json(error.status, { error: { code: error.code, message: error.message, requestId } }, headers);\n }\n return errorBody(500, 'internal', 'Something went wrong.', requestId);\n}\n\n/**\n * Rejects query strings with unknown or repeated parameters. Every accepted route\n * names its full allow-list; anything else is a client bug, not something to ignore.\n * Keys listed in `repeatable` may carry several values (read back with `getAll`);\n * every other key stays single-valued.\n */\nexport function queryParams(url: URL, allowed: readonly string[], repeatable: readonly string[] = []): URLSearchParams {\n const params = url.searchParams;\n const seen = new Set<string>();\n for (const key of params.keys()) {\n if (!allowed.includes(key) || (seen.has(key) && !repeatable.includes(key))) throw badRequest();\n seen.add(key);\n }\n return params;\n}\n\n/** Hard deadline for reading a request body; slow streams fail as 408. */\nexport const HTTP_BODY_TIMEOUT_MS = 10000;\n\nconst bodyTimeout = (): ApiError => new ApiError(408, 'timeout', 'The request body took too long.');\n\n/**\n * Reads a JSON body with a hard byte ceiling and a wall-clock deadline enforced while\n * streaming — a lying or absent Content-Length and a stalled sender are both cut off.\n * Wrong media types are 415, oversized 413, timed out 408, malformed JSON 400; schema\n * validation happens afterwards at the route boundary.\n */\nexport async function readJsonBody(request: Request): Promise<unknown> {\n // Exactly the application/json media type; parameters (charset etc.) may follow.\n const mediaType = (request.headers.get('content-type') ?? '').split(';', 1)[0].trim().toLowerCase();\n if (mediaType !== 'application/json') {\n throw new ApiError(415, 'unsupported-media', 'JSON bodies only.');\n }\n const reader = request.body?.getReader();\n if (!reader) throw badRequest();\n const startedAt = Date.now();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n for (;;) {\n const { done, value } = await Promise.race([\n reader.read(),\n new Promise<never>((_r, reject) => {\n timeoutId = setTimeout(\n () => reject(bodyTimeout()),\n Math.max(0, HTTP_BODY_TIMEOUT_MS - (Date.now() - startedAt)),\n );\n }),\n ]);\n clearTimeout(timeoutId);\n timeoutId = undefined;\n if (done) break;\n total += value.byteLength;\n if (total > ARCHIVE_LIMITS.requestBytes) throw new ApiError(413, 'too-large', 'The request is too large.');\n chunks.push(value);\n }\n } catch (error) {\n // Release the remainder so the failure isn't blocked on a stalled sender.\n reader.cancel().catch(() => undefined);\n throw error;\n } finally {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n reader.releaseLock();\n }\n const merged = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n if (total === 0) throw badRequest();\n try {\n return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(merged));\n } catch {\n throw badRequest();\n }\n}\n\nconst SESSION_COOKIE_MAX = 4096;\n\n/**\n * Reads the session cookie. A duplicate cookie name or an oversized header is a\n * malformed request, not a missing session: callers distinguish null (anonymous)\n * from a token to verify.\n */\nexport function sessionToken(request: Request): string | null {\n const header = request.headers.get('cookie');\n if (header === null) return null;\n if (header.length > SESSION_COOKIE_MAX) throw badRequest();\n const matches: string[] = [];\n for (const part of header.split(';')) {\n const eq = part.indexOf('=');\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === SESSION_COOKIE_NAME) matches.push(part.slice(eq + 1).trim());\n }\n if (matches.length > 1) throw badRequest();\n return matches.length === 1 && matches[0].length > 0 ? matches[0] : null;\n}\n\n/** The expired-session Set-Cookie value the sign-out route and account erasure send. */\nexport const expiredSessionCookie = (secure: boolean): string => sessionCookieValue('', { maxAgeSeconds: 0, secure });\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
async function serviceRequest(e,t,n){let r=new AbortController,i;try{return await Promise.race([e.fetch(new Request(t,{signal:r.signal})).then(n),new Promise((e,t)=>{i=setTimeout(()=>{r.abort(),t(Error(`Service request timed out.`))},5e3)})])}finally{i!==void 0&&clearTimeout(i)}}exports.serviceRequest=serviceRequest;
|
|
2
|
-
//# sourceMappingURL=
|
|
2
|
+
//# sourceMappingURL=service.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"
|
|
1
|
+
{"version":3,"file":"service.cjs","names":["controller","timer","binding","request","consume","reject"],"sources":["../../src/_server/service.ts"],"sourcesContent":["/** Bound the complete service exchange, including response-body decoding. */\nexport async function serviceRequest<T>(\n binding: { fetch(request: Request): Promise<Response> },\n request: Request,\n consume: (response: Response) => Promise<T>,\n): Promise<T> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n binding.fetch(new Request(request, { signal: controller.signal })).then(consume),\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(new Error('Service request timed out.'));\n }, 5000);\n }),\n ]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n}\n"],"mappings":"AACA,eAAsB,eACpB,EACA,EACA,EACY,CACZ,IAAMA,EAAa,IAAI,gBACnBC,EACJ,GAAI,CACF,OAAO,MAAM,QAAQ,KAAK,CACxBC,EAAQ,MAAM,IAAI,QAAQC,EAAS,CAAE,OAAQH,EAAW,MAAO,CAAC,CAAC,CAAC,CAAC,KAAKI,CAAO,EAC/E,IAAI,SAAgB,EAAU,IAAW,CACvC,EAAQ,eAAiB,CACvBJ,EAAW,MAAM,EACjBK,EAAW,MAAM,4BAA4B,CAAC,CAChD,EAAG,GAAI,CACT,CAAC,CACH,CAAC,CACH,QAAU,CACJJ,IAAU,IAAA,IAAW,aAAaA,CAAK,CAC7C,CACF,C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/_server/service.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,wBAAsB,cAAc,CAAC,CAAC,EACpC,OAAO,EAAE;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,EACvD,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAC1C,OAAO,CAAC,CAAC,CAAC,CAgBZ"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/_server/service.ts
|
|
2
2
|
async function serviceRequest(e, t, n) {
|
|
3
3
|
let r = new AbortController(), i;
|
|
4
4
|
try {
|
|
@@ -13,4 +13,4 @@ async function serviceRequest(e, t, n) {
|
|
|
13
13
|
}
|
|
14
14
|
export { serviceRequest };
|
|
15
15
|
|
|
16
|
-
//# sourceMappingURL=
|
|
16
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","names":["controller","timer","binding","request","consume","reject"],"sources":["../../src/_server/service.ts"],"sourcesContent":["/** Bound the complete service exchange, including response-body decoding. */\nexport async function serviceRequest<T>(\n binding: { fetch(request: Request): Promise<Response> },\n request: Request,\n consume: (response: Response) => Promise<T>,\n): Promise<T> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n binding.fetch(new Request(request, { signal: controller.signal })).then(consume),\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(new Error('Service request timed out.'));\n }, 5000);\n }),\n ]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n}\n"],"mappings":";AACA,eAAsB,eACpB,GACA,GACA,GACY;CACZ,IAAMA,IAAa,IAAI,gBAAgB,GACnCC;CACJ,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CACxBC,EAAQ,MAAM,IAAI,QAAQC,GAAS,EAAE,QAAQH,EAAW,OAAO,CAAC,CAAC,CAAC,CAAC,KAAKI,CAAO,GAC/E,IAAI,SAAgB,GAAU,MAAW;GACvC,IAAQ,iBAAiB;IAEvB,AADAJ,EAAW,MAAM,GACjBK,EAAO,gBAAI,MAAM,4BAA4B,CAAC;GAChD,GAAG,GAAI;EACT,CAAC,CACH,CAAC;CACH,UAAU;EACR,AAAIJ,MAAU,KAAA,KAAW,aAAaA,CAAK;CAC7C;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.cjs","names":["UUID_RE"],"sources":["../../src/_server/uuid.ts"],"sourcesContent":["/** The one canonical UUID shape the server layers match; the root layer validates via `uuidSchema`. */\nexport const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n"],"mappings":"AACA,IAAaA,EAAU,kE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../../src/_server/uuid.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,eAAO,MAAM,OAAO,QAAoE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.js","names":["UUID_RE"],"sources":["../../src/_server/uuid.ts"],"sourcesContent":["/** The one canonical UUID shape the server layers match; the root layer validates via `uuidSchema`. */\nexport const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n"],"mappings":";AACA,IAAaA,IAAU"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const e=require("../
|
|
1
|
+
const e=require("../_server/http.cjs"),t=require("../contract.cjs"),n=require("./store.cjs");var escapeHtml=e=>e.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`),scriptValue=e=>JSON.stringify(e).replace(/[<>&\u2028\u2029]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,`0`)}`);function confirmationPage({appName:r,nonce:i,publicOrigin:a,token:o}){let s=o!==null,c=s?`Confirm sign-in`:`This sign-in link is not valid`,l=s?`
|
|
2
2
|
const button = document.getElementById('confirm');
|
|
3
3
|
const status = document.getElementById('status');
|
|
4
4
|
const recovery = document.getElementById('recovery');
|
|
@@ -73,7 +73,7 @@ button.addEventListener('click', async () => {
|
|
|
73
73
|
const response = await fetch('/api/v1/auth/verify', {
|
|
74
74
|
method: 'POST',
|
|
75
75
|
credentials: 'same-origin',
|
|
76
|
-
headers: { 'content-type': 'application/json', '${
|
|
76
|
+
headers: { 'content-type': 'application/json', '${t.REQUEST_MARKER_HEADER}': '1' },
|
|
77
77
|
body: JSON.stringify({ token: ${scriptValue(o??``)} }),
|
|
78
78
|
signal: active.signal,
|
|
79
79
|
redirect: 'error',
|
|
@@ -180,5 +180,5 @@ ${s?`<footer id="exit"><a href="${escapeHtml(`${a}/`)}">Back to app</a></footer>
|
|
|
180
180
|
${s?`<script nonce="${i}">${l}
|
|
181
181
|
<\/script>`:``}
|
|
182
182
|
</body>
|
|
183
|
-
</html>`;return new Response(u,{headers:{...
|
|
183
|
+
</html>`;return new Response(u,{headers:{...e.noStore,"content-security-policy":`default-src 'none'; script-src 'nonce-${i}'; style-src 'nonce-${i}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,"content-type":`text/html; charset=utf-8`,"referrer-policy":`no-referrer`,"x-content-type-options":`nosniff`},status:s?200:400})}exports.confirmationPage=confirmationPage,exports.escapeHtml=escapeHtml;
|
|
184
184
|
//# sourceMappingURL=_confirmation.cjs.map
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"version":3,"file":"_confirmation.cjs","names":["REQUEST_MARKER_HEADER","LINK_TTL_MS","noStore","require_http","require_contract","require_store","value","char","valid","token","title","script","publicOrigin","html","appName","nonce"],"sources":["../../src/auth/_confirmation.ts"],"sourcesContent":["import { noStore } from '../_server/http';\nimport { REQUEST_MARKER_HEADER } from '../contract';\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,6FAIA,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,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { noStore as e } from "../_server/http.js";
|
|
2
|
+
import { REQUEST_MARKER_HEADER as t } from "../contract.js";
|
|
3
3
|
import { LINK_TTL_MS as n } from "./store.js";
|
|
4
4
|
// src/auth/_confirmation.ts
|
|
5
5
|
var escapeHtml = (e) => e.replace(/[&<>"']/g, (e) => `&#${e.charCodeAt(0)};`), scriptValue = (e) => JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
@@ -79,7 +79,7 @@ button.addEventListener('click', async () => {
|
|
|
79
79
|
const response = await fetch('/api/v1/auth/verify', {
|
|
80
80
|
method: 'POST',
|
|
81
81
|
credentials: 'same-origin',
|
|
82
|
-
headers: { 'content-type': 'application/json', '${
|
|
82
|
+
headers: { 'content-type': 'application/json', '${t}': '1' },
|
|
83
83
|
body: JSON.stringify({ token: ${scriptValue(o ?? "")} }),
|
|
84
84
|
signal: active.signal,
|
|
85
85
|
redirect: 'error',
|
|
@@ -189,7 +189,7 @@ ${s ? `<script nonce="${i}">${l}
|
|
|
189
189
|
</html>`;
|
|
190
190
|
return new Response(u, {
|
|
191
191
|
headers: {
|
|
192
|
-
...
|
|
192
|
+
...e,
|
|
193
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
194
|
"content-type": "text/html; charset=utf-8",
|
|
195
195
|
"referrer-policy": "no-referrer",
|
|
@@ -1 +1 @@
|
|
|
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"}
|
|
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 { noStore } from '../_server/http';\nimport { REQUEST_MARKER_HEADER } from '../contract';\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"}
|