@adrianhall/cloudflare-toolkit 0.0.1

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/THIRD-PARTY-NOTICES.md +75 -0
  4. package/dist/cli/generate-wrangler-types/index.d.ts +1 -0
  5. package/dist/cli/generate-wrangler-types/index.js +329 -0
  6. package/dist/cli/generate-wrangler-types/index.js.map +1 -0
  7. package/dist/error-CLYcAvBM.d.ts +28 -0
  8. package/dist/errors/index.d.ts +2 -0
  9. package/dist/errors/index.js +3 -0
  10. package/dist/errors-Ciipq_zr.js +58 -0
  11. package/dist/errors-Ciipq_zr.js.map +1 -0
  12. package/dist/factory-BI5gVL_P.js +220 -0
  13. package/dist/factory-BI5gVL_P.js.map +1 -0
  14. package/dist/generators-D8WWEHa1.js +165 -0
  15. package/dist/generators-D8WWEHa1.js.map +1 -0
  16. package/dist/guards/index.d.ts +2 -0
  17. package/dist/guards/index.js +2 -0
  18. package/dist/guards-6K1CVAr5.js +61 -0
  19. package/dist/guards-6K1CVAr5.js.map +1 -0
  20. package/dist/hono/index.d.ts +347 -0
  21. package/dist/hono/index.js +307 -0
  22. package/dist/hono/index.js.map +1 -0
  23. package/dist/index-434HN8jN.d.ts +43 -0
  24. package/dist/index-Byl-ZrCy.d.ts +138 -0
  25. package/dist/index-CUICemFw.d.ts +129 -0
  26. package/dist/index-DRIhR-Xn.d.ts +81 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +8 -0
  29. package/dist/jwt-BvuKtvby.js +328 -0
  30. package/dist/jwt-BvuKtvby.js.map +1 -0
  31. package/dist/logging/index.d.ts +3 -0
  32. package/dist/logging/index.js +3 -0
  33. package/dist/logging-CGHjOVLM.js +24 -0
  34. package/dist/logging-CGHjOVLM.js.map +1 -0
  35. package/dist/policy-CvS6AvvD.js +20 -0
  36. package/dist/policy-CvS6AvvD.js.map +1 -0
  37. package/dist/problem-details/index.d.ts +4 -0
  38. package/dist/problem-details/index.js +3 -0
  39. package/dist/problem-details-CuRsLy3Q.js +37 -0
  40. package/dist/problem-details-CuRsLy3Q.js.map +1 -0
  41. package/dist/silent-CWpHE65X.js +652 -0
  42. package/dist/silent-CWpHE65X.js.map +1 -0
  43. package/dist/testing/index.d.ts +44 -0
  44. package/dist/testing/index.js +2 -0
  45. package/dist/types-Cx6NNILW.d.ts +44 -0
  46. package/dist/types-DCSMb1cp.d.ts +195 -0
  47. package/dist/types-DXboaWOT.d.ts +29 -0
  48. package/dist/vite/index.d.ts +79 -0
  49. package/dist/vite/index.js +417 -0
  50. package/dist/vite/index.js.map +1 -0
  51. package/package.json +137 -0
@@ -0,0 +1,328 @@
1
+ import { SignJWT, createRemoteJWKSet, errors, jwtVerify } from "jose";
2
+ //#region src/lib/auth-internal/jwks.ts
3
+ /**
4
+ * @file Remote JWKS management for Cloudflare Access JWT verification.
5
+ *
6
+ * Only Web-standard APIs (`jose`, `URL`, `Map`) are used, so this module is both Worker-safe
7
+ * (for `hono/`) and Node-safe (for `vite/`). Kept in its own module so tests can mock
8
+ * `getRemoteJwks` and supply a local key set instead of hitting the real Cloudflare Access certs
9
+ * endpoint.
10
+ */
11
+ /** Remote JWKS cache keyed by team-domain URL. */
12
+ const jwksCache = /* @__PURE__ */ new Map();
13
+ /**
14
+ * Pattern a Cloudflare Access team-domain hostname must match: a single DNS label followed by
15
+ * the literal `cloudflareaccess.com` suffix (e.g. `"my-team.cloudflareaccess.com"`). Matched
16
+ * against `URL.hostname`, which the WHATWG URL parser always normalizes to ASCII-lowercase and
17
+ * which already resolves away userinfo (`user@host`) and path-based host-spoofing tricks, so
18
+ * this single check is sufficient to reject a `teamDomain` that does not genuinely point at
19
+ * Cloudflare Access's own certs host.
20
+ */
21
+ const TEAM_DOMAIN_HOST_PATTERN = /^[a-z0-9-]+\.cloudflareaccess\.com$/;
22
+ /** Matches a leading URI scheme (e.g. `"http://"`, `"ftp://"`), case-insensitively. */
23
+ const SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
24
+ /**
25
+ * Normalize and validate a Cloudflare Access team domain to its canonical `https://` origin.
26
+ *
27
+ * Used both to build the JWKS certs URL in {@link getRemoteJwks} and to compute the expected
28
+ * `iss` (Issuer) claim value for `jwtVerify` in `verifyAccessJwt` (SEC-003) — a single shared
29
+ * implementation keeps the two in permanent agreement instead of risking two independent
30
+ * normalizations drifting apart.
31
+ *
32
+ * @param teamDomain - The Cloudflare Access team domain (e.g. `"my-team.cloudflareaccess.com"`).
33
+ * A missing `https://` prefix is added automatically, and a trailing slash is stripped, so
34
+ * `"my-team.cloudflareaccess.com"`, `"my-team.cloudflareaccess.com/"`, and
35
+ * `"https://my-team.cloudflareaccess.com"` all normalize to the same origin.
36
+ * @returns The canonical origin, e.g. `"https://my-team.cloudflareaccess.com"` — no trailing
37
+ * slash, no path.
38
+ * @throws {Error} If `teamDomain` has an explicit non-`https://` scheme (see {@link ensureHttps}),
39
+ * or if the resulting hostname is not a `*.cloudflareaccess.com` team domain — closing off both
40
+ * the malformed-URL footgun and the SSRF/JWKS-poisoning surface of a dynamically-sourced
41
+ * `teamDomain` pointing somewhere unexpected.
42
+ */
43
+ function normalizeTeamDomain(teamDomain) {
44
+ const url = ensureHttps(teamDomain.replace(/\/+$/, ""));
45
+ const parsed = new URL(url);
46
+ if (!TEAM_DOMAIN_HOST_PATTERN.test(parsed.hostname)) throw new Error(`Invalid Cloudflare Access team domain: "${teamDomain}"`);
47
+ return parsed.origin;
48
+ }
49
+ /**
50
+ * Return (or create-and-cache) a remote JWKS function for the given Cloudflare Access team
51
+ * domain.
52
+ *
53
+ * @param teamDomain - The Cloudflare Access team domain. See {@link normalizeTeamDomain} for the
54
+ * accepted input forms and normalization rules.
55
+ * @returns The `jose` remote JWK set function for `${teamDomain}/cdn-cgi/access/certs`, cached
56
+ * across calls for the same normalized domain.
57
+ * @throws {Error} See {@link normalizeTeamDomain}.
58
+ */
59
+ function getRemoteJwks(teamDomain) {
60
+ const origin = normalizeTeamDomain(teamDomain);
61
+ const certsUrl = new URL(`${origin}/cdn-cgi/access/certs`);
62
+ let jwks = jwksCache.get(certsUrl.href);
63
+ if (!jwks) {
64
+ if (jwksCache.size >= 20) for (const oldestKey of jwksCache.keys()) {
65
+ jwksCache.delete(oldestKey);
66
+ break;
67
+ }
68
+ jwks = createRemoteJWKSet(certsUrl);
69
+ jwksCache.set(certsUrl.href, jwks);
70
+ }
71
+ return jwks;
72
+ }
73
+ /**
74
+ * Ensures the base URL for the JWKS endpoint is HTTPS.
75
+ *
76
+ * @param url - The base URL to check.
77
+ * @returns The URL unchanged if it already starts with `https://`; otherwise `https://` is
78
+ * prepended, but only when `url` has no scheme of its own.
79
+ * @throws {Error} If `url` already has an explicit scheme other than `https://` (e.g.
80
+ * `"http://..."`, `"ftp://..."`) — such input previously became a malformed URL
81
+ * (`"https://http://..."`) rather than being upgraded or rejected. Erroring here surfaces the
82
+ * misconfiguration instead of silently constructing a broken (and, for a userinfo-style value,
83
+ * potentially misleading) JWKS URL.
84
+ */
85
+ function ensureHttps(url) {
86
+ if (url.startsWith("https://")) return url;
87
+ if (SCHEME_PATTERN.test(url)) throw new Error(`Expected an https:// URL, got: "${url}"`);
88
+ return "https://" + url;
89
+ }
90
+ //#endregion
91
+ //#region src/lib/auth-internal/jwt.ts
92
+ /**
93
+ * @file JWT helpers for the Cloudflare Access authentication internals: signing and verifying
94
+ * developer tokens, verifying real Cloudflare Access tokens, and reading/writing the
95
+ * authorization cookie.
96
+ *
97
+ * Uses `jose` v6 for all cryptographic operations and only Web-standard APIs
98
+ * (`crypto.randomUUID`, `TextEncoder`) otherwise, so this module is both Worker-safe (for
99
+ * `hono/cloudflare-access.ts`) and Node-safe (for `vite/plugin.ts`).
100
+ */
101
+ /**
102
+ * Well-known HMAC secret used by default for signing and verifying developer-generated JWTs.
103
+ *
104
+ * **This is NOT a real secret.** It only protects the local-dev login flow running on
105
+ * `localhost` and must never be relied on for production security.
106
+ */
107
+ const DEFAULT_DEV_SECRET = "cloudflare-access-dev-secret-do-not-use-in-production";
108
+ /** Algorithm used for developer-signed JWTs. */
109
+ const DEV_ALG = "HS256";
110
+ /**
111
+ * Algorithm Cloudflare Access uses to sign real application tokens (confirmed by Cloudflare's
112
+ * own application-token documentation: the JWT header is always `{"alg":"RS256",...}`, signed
113
+ * with an RSA private key). Pinned explicitly on the JWKS `jwtVerify` call in
114
+ * {@link verifyAccessJwt} (SEC-004/CODE-003) rather than left unconstrained — `jose` already
115
+ * refuses to HMAC-verify against an asymmetric JWK, so this is defense-in-depth, not a fix for a
116
+ * currently-exploitable gap, and it mirrors {@link DEV_ALG} being pinned on the dev-token path.
117
+ * Not exposed as a public override: unlike `audience` (a per-deployment Access Application
118
+ * value), the signing algorithm is a Cloudflare Access platform constant, and widening it would
119
+ * reopen the exact `alg`-confusion class this pin closes.
120
+ */
121
+ const ACCESS_ALG = "RS256";
122
+ /** Name of the cookie that stores the JWT. */
123
+ const COOKIE_NAME = "CF_Authorization";
124
+ /** Header containing the JWT (set by Cloudflare Access). */
125
+ const JWT_HEADER = "cf-access-jwt-assertion";
126
+ /** Header containing the authenticated user's email. */
127
+ const EMAIL_HEADER = "cf-access-authenticated-user-email";
128
+ /** Encode a string secret into a `CryptoKey`-compatible `Uint8Array`. */
129
+ function secretToBytes(secret) {
130
+ return new TextEncoder().encode(secret);
131
+ }
132
+ /**
133
+ * Generate a subject identifier for a developer-signed JWT.
134
+ *
135
+ * Returns a random UUID so that the `sub` claim matches the shape of a real Cloudflare Access
136
+ * subject (a UUID) and satisfies strict downstream validators (e.g. `[A-Za-z0-9-]`). Real
137
+ * Access subjects are stable per-user; dev subjects are stable for the life of an issued token
138
+ * (and can be pinned via the `sub` option).
139
+ */
140
+ function generateDevSub() {
141
+ return crypto.randomUUID();
142
+ }
143
+ /**
144
+ * Create a signed JWT that mimics a Cloudflare Access token.
145
+ *
146
+ * The `type` claim is set to `"dev"` so that the verification layer can distinguish
147
+ * locally-issued tokens from real Access tokens.
148
+ *
149
+ * @param email - The user's email address (becomes the `email` claim).
150
+ * @param options - Optional overrides.
151
+ * @param options.secret - HMAC signing secret (default {@link DEFAULT_DEV_SECRET}).
152
+ * @param options.lifetime - Token lifetime in seconds (default `86400` / 24 h).
153
+ * @param options.sub - Subject claim. When provided it is used **verbatim**; when omitted a
154
+ * random UUID is generated (matching the shape of a real Cloudflare Access `sub`) instead of
155
+ * an email-derived value.
156
+ */
157
+ async function signDevJwt(email, options = {}) {
158
+ const secret = options.secret ?? "cloudflare-access-dev-secret-do-not-use-in-production";
159
+ const lifetime = options.lifetime ?? 86400;
160
+ const now = Math.floor(Date.now() / 1e3);
161
+ return new SignJWT({
162
+ email,
163
+ sub: options.sub ?? generateDevSub(),
164
+ type: "dev",
165
+ iss: "dev-authentication"
166
+ }).setProtectedHeader({ alg: DEV_ALG }).setIssuedAt(now).setExpirationTime(now + lifetime).sign(secretToBytes(secret));
167
+ }
168
+ /**
169
+ * Attempt to verify a JWT as a developer-signed token.
170
+ *
171
+ * Returns `null` if verification fails (wrong key, expired, missing claims, etc.) — the caller
172
+ * can then fall back to Cloudflare JWKS verification.
173
+ */
174
+ async function verifyDevJwt(token, secret = DEFAULT_DEV_SECRET) {
175
+ try {
176
+ const { payload } = await jwtVerify(token, secretToBytes(secret), { algorithms: [DEV_ALG] });
177
+ return extractClaims(payload);
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+ /**
183
+ * `jose` error classes that only occur once a JWKS was successfully fetched and a real
184
+ * cryptographic/claims verification attempt against the token was made — i.e. the token itself
185
+ * is what failed, not the JWKS transport.
186
+ *
187
+ * `JWKSNoMatchingKey` is included because `createRemoteJWKSet` already retries a JWKS reload
188
+ * once (subject to a cooldown) before finally throwing it — a final throw means the token's
189
+ * `kid` genuinely doesn't match even after a fresh key fetch, not merely a stale local cache.
190
+ */
191
+ const TOKEN_VALIDATION_ERRORS = [
192
+ errors.JWSSignatureVerificationFailed,
193
+ errors.JWTExpired,
194
+ errors.JWTClaimValidationFailed,
195
+ errors.JOSEAlgNotAllowed,
196
+ errors.JWTInvalid,
197
+ errors.JWSInvalid,
198
+ errors.JWKSNoMatchingKey
199
+ ];
200
+ /**
201
+ * Classify a caught `verifyAccessJwt` failure as `"invalid"` (the token itself failed
202
+ * cryptographic or claims validation) or `"network"` (a JWKS transport/config/infra problem),
203
+ * for the `cause` field attached to the diagnostic log record.
204
+ *
205
+ * Conservatively biased toward `"network"`: only the specific {@link TOKEN_VALIDATION_ERRORS}
206
+ * classes are treated as `"invalid"`. Everything else — plain fetch/DNS errors (not a
207
+ * `JOSEError` at all, since `getRemoteJwks`'s underlying fetch failures propagate unwrapped),
208
+ * `JWKSTimeout`, and JWKS-structure errors such as `JWKSInvalid`/`JWKSMultipleMatchingKeys` — is
209
+ * classified as `"network"`, because misclassifying a real outage as "just an invalid token" is
210
+ * the exact debuggability gap this diagnostic exists to close.
211
+ *
212
+ * @param err - The value caught from the `jwtVerify`/`getRemoteJwks` call.
213
+ * @returns `"invalid"` when `err` is one of {@link TOKEN_VALIDATION_ERRORS}; `"network"`
214
+ * otherwise.
215
+ */
216
+ function classifyVerificationFailure(err) {
217
+ return TOKEN_VALIDATION_ERRORS.some((ErrorClass) => err instanceof ErrorClass) ? "invalid" : "network";
218
+ }
219
+ /**
220
+ * Verify a JWT against Cloudflare Access's remote JWKS endpoint.
221
+ *
222
+ * Only {@link ACCESS_ALG} (`"RS256"`) is accepted (SEC-004/CODE-003) — this matches what
223
+ * Cloudflare Access actually issues and closes off `alg`-confusion attacks (e.g. a crafted
224
+ * `alg: "HS256"` header) as a matter of explicit policy rather than relying solely on `jose`'s
225
+ * own asymmetric-JWK/HMAC mismatch behavior.
226
+ *
227
+ * The `iss` (Issuer) claim is always verified against the normalized `teamDomain` (e.g.
228
+ * `"https://my-team.cloudflareaccess.com"`, via {@link normalizeTeamDomain}) — SEC-003. This is
229
+ * defense-in-depth: the JWKS itself is already team-scoped (fetched from
230
+ * `teamDomain/cdn-cgi/access/certs`), so a token signed by a different team's key would already
231
+ * fail signature verification, but binding `iss` explicitly matches Cloudflare's own published
232
+ * Worker+Access reference implementation and requires the presence of the `iss` claim, so a
233
+ * token without one is rejected too.
234
+ *
235
+ * When `audience` is provided the `aud` claim is also verified. **When `audience` is omitted,
236
+ * `aud` is not checked at all** — because every Cloudflare Access application in an account
237
+ * shares the same team JWKS, this means a token minted for *any other Access application in the
238
+ * same team* is accepted here too (cross-application token replay). Callers that expose this
239
+ * through a public option (e.g. `hono/cloudflare-access.ts`'s `cloudflareAccess`) should warn
240
+ * loudly when a caller omits `audience` outside of a clearly-local-development configuration.
241
+ *
242
+ * A transient JWKS network/infra failure (bad team domain, DNS blip, certs endpoint down) is
243
+ * otherwise indistinguishable from a genuinely invalid token — both fall through to the same
244
+ * `catch` and the same `null` return, and without a `logger` nothing is recorded. When `logger`
245
+ * is provided, the caught error is recorded at `warn` (matching the generic
246
+ * `"JWT verification failed"` warning callers such as `cloudflareAccess` already emit) together
247
+ * with the raw `err` and a best-effort `cause: "network" | "invalid"` classification (see
248
+ * {@link classifyVerificationFailure}), so operators can distinguish an outage from a rejected
249
+ * token without changing the fail-closed `null` return contract.
250
+ *
251
+ * @param token - The compact JWS to verify.
252
+ * @param teamDomain - The Cloudflare Access team domain used to fetch the public JWKS and to
253
+ * compute the expected `iss` claim value (see {@link normalizeTeamDomain}).
254
+ * @param audience - Application Audience Tag to verify the `aud` claim against. When omitted,
255
+ * `aud` is not checked at all — see the security remarks above.
256
+ * @param logger - Optional structured logger. When omitted, verification failures are still
257
+ * returned as `null` but nothing is logged (unchanged prior behavior).
258
+ */
259
+ async function verifyAccessJwt(token, teamDomain, audience, logger) {
260
+ try {
261
+ const { payload } = await jwtVerify(token, getRemoteJwks(teamDomain), {
262
+ algorithms: [ACCESS_ALG],
263
+ audience: audience ?? void 0,
264
+ issuer: normalizeTeamDomain(teamDomain)
265
+ });
266
+ return extractClaims(payload);
267
+ } catch (err) {
268
+ logger?.warn("Cloudflare Access JWT verification failed", {
269
+ err,
270
+ teamDomain,
271
+ cause: classifyVerificationFailure(err)
272
+ });
273
+ return null;
274
+ }
275
+ }
276
+ /** Pull the required claims out of a verified payload. */
277
+ function extractClaims(payload) {
278
+ const email = payload.email;
279
+ const sub = payload.sub;
280
+ if (typeof email !== "string" || !email) return null;
281
+ if (typeof sub !== "string" || !sub) return null;
282
+ return {
283
+ email,
284
+ sub
285
+ };
286
+ }
287
+ /**
288
+ * Build a `Set-Cookie` header value for the authorisation cookie.
289
+ *
290
+ * Mirrors the attributes used by Cloudflare Access: `HttpOnly; Secure; SameSite=Lax; Path=/`
291
+ *
292
+ * For local dev over plain HTTP the `Secure` flag is omitted when the request was made to
293
+ * `localhost` or `127.0.0.1`.
294
+ */
295
+ function buildCookieHeader(token, isSecure) {
296
+ const parts = [
297
+ `${COOKIE_NAME}=${token}`,
298
+ "HttpOnly",
299
+ "SameSite=Lax",
300
+ "Path=/"
301
+ ];
302
+ if (isSecure) parts.push("Secure");
303
+ return parts.join("; ");
304
+ }
305
+ /**
306
+ * Build a `Set-Cookie` header that clears the `CF_Authorization` cookie by setting it to an
307
+ * empty value with `Max-Age=0`.
308
+ *
309
+ * Use this when a stale or invalid cookie needs to be removed so the user can re-authenticate.
310
+ */
311
+ function clearCookieHeader() {
312
+ return `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;
313
+ }
314
+ /**
315
+ * Parse the value of the `CF_Authorization` cookie from a `Cookie` header string. Returns
316
+ * `undefined` when the cookie is absent.
317
+ */
318
+ function parseCookie(cookieHeader) {
319
+ if (!cookieHeader) return void 0;
320
+ for (const pair of cookieHeader.split(";")) {
321
+ const [name, ...rest] = pair.split("=");
322
+ if (name.trim() === "CF_Authorization") return rest.join("=").trim();
323
+ }
324
+ }
325
+ //#endregion
326
+ export { buildCookieHeader as a, signDevJwt as c, JWT_HEADER as i, verifyAccessJwt as l, DEFAULT_DEV_SECRET as n, clearCookieHeader as o, EMAIL_HEADER as r, parseCookie as s, COOKIE_NAME as t, verifyDevJwt as u };
327
+
328
+ //# sourceMappingURL=jwt-BvuKtvby.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-BvuKtvby.js","names":["joseErrors"],"sources":["../src/lib/auth-internal/jwks.ts","../src/lib/auth-internal/jwt.ts"],"sourcesContent":["/**\n * @file Remote JWKS management for Cloudflare Access JWT verification.\n *\n * Only Web-standard APIs (`jose`, `URL`, `Map`) are used, so this module is both Worker-safe\n * (for `hono/`) and Node-safe (for `vite/`). Kept in its own module so tests can mock\n * `getRemoteJwks` and supply a local key set instead of hitting the real Cloudflare Access certs\n * endpoint.\n */\nimport { createRemoteJWKSet } from \"jose\";\n\n/** Remote JWKS cache keyed by team-domain URL. */\nconst jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\n/**\n * Upper bound on {@link jwksCache}'s size. `teamDomain` is normally static per-deployment\n * config (one, or a handful, of distinct values for the lifetime of a Worker), but nothing\n * prevents a caller from passing a dynamically-sourced value; capping the cache avoids unbounded\n * memory growth in that case. Eviction is FIFO (oldest-inserted entry first, via `Map`'s\n * insertion-order iteration) rather than true LRU — proportionate to how rarely this cache is\n * expected to near its limit at all.\n */\nexport const MAX_JWKS_CACHE_ENTRIES = 20;\n\n/**\n * Pattern a Cloudflare Access team-domain hostname must match: a single DNS label followed by\n * the literal `cloudflareaccess.com` suffix (e.g. `\"my-team.cloudflareaccess.com\"`). Matched\n * against `URL.hostname`, which the WHATWG URL parser always normalizes to ASCII-lowercase and\n * which already resolves away userinfo (`user@host`) and path-based host-spoofing tricks, so\n * this single check is sufficient to reject a `teamDomain` that does not genuinely point at\n * Cloudflare Access's own certs host.\n */\nconst TEAM_DOMAIN_HOST_PATTERN = /^[a-z0-9-]+\\.cloudflareaccess\\.com$/;\n\n/** Matches a leading URI scheme (e.g. `\"http://\"`, `\"ftp://\"`), case-insensitively. */\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n\n/**\n * Normalize and validate a Cloudflare Access team domain to its canonical `https://` origin.\n *\n * Used both to build the JWKS certs URL in {@link getRemoteJwks} and to compute the expected\n * `iss` (Issuer) claim value for `jwtVerify` in `verifyAccessJwt` (SEC-003) — a single shared\n * implementation keeps the two in permanent agreement instead of risking two independent\n * normalizations drifting apart.\n *\n * @param teamDomain - The Cloudflare Access team domain (e.g. `\"my-team.cloudflareaccess.com\"`).\n * A missing `https://` prefix is added automatically, and a trailing slash is stripped, so\n * `\"my-team.cloudflareaccess.com\"`, `\"my-team.cloudflareaccess.com/\"`, and\n * `\"https://my-team.cloudflareaccess.com\"` all normalize to the same origin.\n * @returns The canonical origin, e.g. `\"https://my-team.cloudflareaccess.com\"` — no trailing\n * slash, no path.\n * @throws {Error} If `teamDomain` has an explicit non-`https://` scheme (see {@link ensureHttps}),\n * or if the resulting hostname is not a `*.cloudflareaccess.com` team domain — closing off both\n * the malformed-URL footgun and the SSRF/JWKS-poisoning surface of a dynamically-sourced\n * `teamDomain` pointing somewhere unexpected.\n */\nexport function normalizeTeamDomain(teamDomain: string): string {\n // Normalise: strip trailing slash, ensure https prefix.\n const base = teamDomain.replace(/\\/+$/, \"\");\n const url = ensureHttps(base);\n const parsed = new URL(url);\n\n if (!TEAM_DOMAIN_HOST_PATTERN.test(parsed.hostname)) {\n throw new Error(`Invalid Cloudflare Access team domain: \"${teamDomain}\"`);\n }\n\n return parsed.origin;\n}\n\n/**\n * Return (or create-and-cache) a remote JWKS function for the given Cloudflare Access team\n * domain.\n *\n * @param teamDomain - The Cloudflare Access team domain. See {@link normalizeTeamDomain} for the\n * accepted input forms and normalization rules.\n * @returns The `jose` remote JWK set function for `${teamDomain}/cdn-cgi/access/certs`, cached\n * across calls for the same normalized domain.\n * @throws {Error} See {@link normalizeTeamDomain}.\n */\nexport function getRemoteJwks(teamDomain: string): ReturnType<typeof createRemoteJWKSet> {\n const origin = normalizeTeamDomain(teamDomain);\n const certsUrl = new URL(`${origin}/cdn-cgi/access/certs`);\n\n let jwks = jwksCache.get(certsUrl.href);\n if (!jwks) {\n if (jwksCache.size >= MAX_JWKS_CACHE_ENTRIES) {\n // `Map` iterates keys in insertion order, so the first key yielded here is the oldest\n // entry. `size >= MAX_JWKS_CACHE_ENTRIES` (checked above) with `MAX_JWKS_CACHE_ENTRIES`\n // fixed at a positive constant guarantees the cache is non-empty, so this loop always runs\n // at least once — no \"keys() was empty\" branch to guard against.\n for (const oldestKey of jwksCache.keys()) {\n jwksCache.delete(oldestKey);\n break;\n }\n }\n jwks = createRemoteJWKSet(certsUrl);\n jwksCache.set(certsUrl.href, jwks);\n }\n return jwks;\n}\n\n/**\n * Ensures the base URL for the JWKS endpoint is HTTPS.\n *\n * @param url - The base URL to check.\n * @returns The URL unchanged if it already starts with `https://`; otherwise `https://` is\n * prepended, but only when `url` has no scheme of its own.\n * @throws {Error} If `url` already has an explicit scheme other than `https://` (e.g.\n * `\"http://...\"`, `\"ftp://...\"`) — such input previously became a malformed URL\n * (`\"https://http://...\"`) rather than being upgraded or rejected. Erroring here surfaces the\n * misconfiguration instead of silently constructing a broken (and, for a userinfo-style value,\n * potentially misleading) JWKS URL.\n */\nexport function ensureHttps(url: string): string {\n if (url.startsWith(\"https://\")) {\n return url;\n }\n if (SCHEME_PATTERN.test(url)) {\n throw new Error(`Expected an https:// URL, got: \"${url}\"`);\n }\n return \"https://\" + url;\n}\n","/**\n * @file JWT helpers for the Cloudflare Access authentication internals: signing and verifying\n * developer tokens, verifying real Cloudflare Access tokens, and reading/writing the\n * authorization cookie.\n *\n * Uses `jose` v6 for all cryptographic operations and only Web-standard APIs\n * (`crypto.randomUUID`, `TextEncoder`) otherwise, so this module is both Worker-safe (for\n * `hono/cloudflare-access.ts`) and Node-safe (for `vite/plugin.ts`).\n */\nimport { SignJWT, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { AccessJwtPayload } from \"./types.js\";\nimport { getRemoteJwks, normalizeTeamDomain } from \"./jwks.js\";\nimport type { Logger } from \"../logging/types.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known HMAC secret used by default for signing and verifying developer-generated JWTs.\n *\n * **This is NOT a real secret.** It only protects the local-dev login flow running on\n * `localhost` and must never be relied on for production security.\n */\nexport const DEFAULT_DEV_SECRET = \"cloudflare-access-dev-secret-do-not-use-in-production\";\n\n/** Algorithm used for developer-signed JWTs. */\nconst DEV_ALG = \"HS256\";\n\n/**\n * Algorithm Cloudflare Access uses to sign real application tokens (confirmed by Cloudflare's\n * own application-token documentation: the JWT header is always `{\"alg\":\"RS256\",...}`, signed\n * with an RSA private key). Pinned explicitly on the JWKS `jwtVerify` call in\n * {@link verifyAccessJwt} (SEC-004/CODE-003) rather than left unconstrained — `jose` already\n * refuses to HMAC-verify against an asymmetric JWK, so this is defense-in-depth, not a fix for a\n * currently-exploitable gap, and it mirrors {@link DEV_ALG} being pinned on the dev-token path.\n * Not exposed as a public override: unlike `audience` (a per-deployment Access Application\n * value), the signing algorithm is a Cloudflare Access platform constant, and widening it would\n * reopen the exact `alg`-confusion class this pin closes.\n */\nconst ACCESS_ALG = \"RS256\";\n\n/** Name of the cookie that stores the JWT. */\nexport const COOKIE_NAME = \"CF_Authorization\";\n\n/** Header containing the JWT (set by Cloudflare Access). */\nexport const JWT_HEADER = \"cf-access-jwt-assertion\";\n\n/** Header containing the authenticated user's email. */\nexport const EMAIL_HEADER = \"cf-access-authenticated-user-email\";\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\n/** Encode a string secret into a `CryptoKey`-compatible `Uint8Array`. */\nfunction secretToBytes(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\n// ---------------------------------------------------------------------------\n// Sign (developer mode only)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a subject identifier for a developer-signed JWT.\n *\n * Returns a random UUID so that the `sub` claim matches the shape of a real Cloudflare Access\n * subject (a UUID) and satisfies strict downstream validators (e.g. `[A-Za-z0-9-]`). Real\n * Access subjects are stable per-user; dev subjects are stable for the life of an issued token\n * (and can be pinned via the `sub` option).\n */\nfunction generateDevSub(): string {\n return crypto.randomUUID();\n}\n\n/**\n * Create a signed JWT that mimics a Cloudflare Access token.\n *\n * The `type` claim is set to `\"dev\"` so that the verification layer can distinguish\n * locally-issued tokens from real Access tokens.\n *\n * @param email - The user's email address (becomes the `email` claim).\n * @param options - Optional overrides.\n * @param options.secret - HMAC signing secret (default {@link DEFAULT_DEV_SECRET}).\n * @param options.lifetime - Token lifetime in seconds (default `86400` / 24 h).\n * @param options.sub - Subject claim. When provided it is used **verbatim**; when omitted a\n * random UUID is generated (matching the shape of a real Cloudflare Access `sub`) instead of\n * an email-derived value.\n */\nexport async function signDevJwt(\n email: string,\n options: { secret?: string; lifetime?: number; sub?: string } = {}\n): Promise<string> {\n const secret = options.secret ?? DEFAULT_DEV_SECRET;\n const lifetime = options.lifetime ?? 86_400; // 24 h\n\n const now = Math.floor(Date.now() / 1000);\n const sub = options.sub ?? generateDevSub();\n\n return new SignJWT({\n email,\n sub,\n type: \"dev\",\n iss: \"dev-authentication\"\n } satisfies Omit<AccessJwtPayload, \"iat\" | \"exp\">)\n .setProtectedHeader({ alg: DEV_ALG })\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .sign(secretToBytes(secret));\n}\n\n// ---------------------------------------------------------------------------\n// Verify\n// ---------------------------------------------------------------------------\n\n/** Result of a successful JWT verification. */\nexport interface VerifiedToken {\n /** Authenticated user's email address (from the JWT `email` claim). */\n email: string;\n /** Authenticated user's unique identifier (from the JWT `sub` claim). */\n sub: string;\n}\n\n/**\n * Attempt to verify a JWT as a developer-signed token.\n *\n * Returns `null` if verification fails (wrong key, expired, missing claims, etc.) — the caller\n * can then fall back to Cloudflare JWKS verification.\n */\nexport async function verifyDevJwt(\n token: string,\n secret: string = DEFAULT_DEV_SECRET\n): Promise<VerifiedToken | null> {\n try {\n const { payload } = await jwtVerify(token, secretToBytes(secret), {\n algorithms: [DEV_ALG]\n });\n return extractClaims(payload);\n } catch {\n return null;\n }\n}\n\n/**\n * `jose` error classes that only occur once a JWKS was successfully fetched and a real\n * cryptographic/claims verification attempt against the token was made — i.e. the token itself\n * is what failed, not the JWKS transport.\n *\n * `JWKSNoMatchingKey` is included because `createRemoteJWKSet` already retries a JWKS reload\n * once (subject to a cooldown) before finally throwing it — a final throw means the token's\n * `kid` genuinely doesn't match even after a fresh key fetch, not merely a stale local cache.\n */\nconst TOKEN_VALIDATION_ERRORS = [\n joseErrors.JWSSignatureVerificationFailed,\n joseErrors.JWTExpired,\n joseErrors.JWTClaimValidationFailed,\n joseErrors.JOSEAlgNotAllowed,\n joseErrors.JWTInvalid,\n joseErrors.JWSInvalid,\n joseErrors.JWKSNoMatchingKey\n] as const;\n\n/**\n * Classify a caught `verifyAccessJwt` failure as `\"invalid\"` (the token itself failed\n * cryptographic or claims validation) or `\"network\"` (a JWKS transport/config/infra problem),\n * for the `cause` field attached to the diagnostic log record.\n *\n * Conservatively biased toward `\"network\"`: only the specific {@link TOKEN_VALIDATION_ERRORS}\n * classes are treated as `\"invalid\"`. Everything else — plain fetch/DNS errors (not a\n * `JOSEError` at all, since `getRemoteJwks`'s underlying fetch failures propagate unwrapped),\n * `JWKSTimeout`, and JWKS-structure errors such as `JWKSInvalid`/`JWKSMultipleMatchingKeys` — is\n * classified as `\"network\"`, because misclassifying a real outage as \"just an invalid token\" is\n * the exact debuggability gap this diagnostic exists to close.\n *\n * @param err - The value caught from the `jwtVerify`/`getRemoteJwks` call.\n * @returns `\"invalid\"` when `err` is one of {@link TOKEN_VALIDATION_ERRORS}; `\"network\"`\n * otherwise.\n */\nfunction classifyVerificationFailure(err: unknown): \"network\" | \"invalid\" {\n const isTokenValidationError = TOKEN_VALIDATION_ERRORS.some(\n (ErrorClass) => err instanceof ErrorClass\n );\n return isTokenValidationError ? \"invalid\" : \"network\";\n}\n\n/**\n * Verify a JWT against Cloudflare Access's remote JWKS endpoint.\n *\n * Only {@link ACCESS_ALG} (`\"RS256\"`) is accepted (SEC-004/CODE-003) — this matches what\n * Cloudflare Access actually issues and closes off `alg`-confusion attacks (e.g. a crafted\n * `alg: \"HS256\"` header) as a matter of explicit policy rather than relying solely on `jose`'s\n * own asymmetric-JWK/HMAC mismatch behavior.\n *\n * The `iss` (Issuer) claim is always verified against the normalized `teamDomain` (e.g.\n * `\"https://my-team.cloudflareaccess.com\"`, via {@link normalizeTeamDomain}) — SEC-003. This is\n * defense-in-depth: the JWKS itself is already team-scoped (fetched from\n * `teamDomain/cdn-cgi/access/certs`), so a token signed by a different team's key would already\n * fail signature verification, but binding `iss` explicitly matches Cloudflare's own published\n * Worker+Access reference implementation and requires the presence of the `iss` claim, so a\n * token without one is rejected too.\n *\n * When `audience` is provided the `aud` claim is also verified. **When `audience` is omitted,\n * `aud` is not checked at all** — because every Cloudflare Access application in an account\n * shares the same team JWKS, this means a token minted for *any other Access application in the\n * same team* is accepted here too (cross-application token replay). Callers that expose this\n * through a public option (e.g. `hono/cloudflare-access.ts`'s `cloudflareAccess`) should warn\n * loudly when a caller omits `audience` outside of a clearly-local-development configuration.\n *\n * A transient JWKS network/infra failure (bad team domain, DNS blip, certs endpoint down) is\n * otherwise indistinguishable from a genuinely invalid token — both fall through to the same\n * `catch` and the same `null` return, and without a `logger` nothing is recorded. When `logger`\n * is provided, the caught error is recorded at `warn` (matching the generic\n * `\"JWT verification failed\"` warning callers such as `cloudflareAccess` already emit) together\n * with the raw `err` and a best-effort `cause: \"network\" | \"invalid\"` classification (see\n * {@link classifyVerificationFailure}), so operators can distinguish an outage from a rejected\n * token without changing the fail-closed `null` return contract.\n *\n * @param token - The compact JWS to verify.\n * @param teamDomain - The Cloudflare Access team domain used to fetch the public JWKS and to\n * compute the expected `iss` claim value (see {@link normalizeTeamDomain}).\n * @param audience - Application Audience Tag to verify the `aud` claim against. When omitted,\n * `aud` is not checked at all — see the security remarks above.\n * @param logger - Optional structured logger. When omitted, verification failures are still\n * returned as `null` but nothing is logged (unchanged prior behavior).\n */\nexport async function verifyAccessJwt(\n token: string,\n teamDomain: string,\n audience?: string,\n logger?: Logger\n): Promise<VerifiedToken | null> {\n try {\n const jwks = getRemoteJwks(teamDomain);\n const { payload } = await jwtVerify(token, jwks, {\n algorithms: [ACCESS_ALG],\n audience: audience ?? undefined,\n issuer: normalizeTeamDomain(teamDomain)\n });\n return extractClaims(payload);\n } catch (err) {\n logger?.warn(\"Cloudflare Access JWT verification failed\", {\n err,\n teamDomain,\n cause: classifyVerificationFailure(err)\n });\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Pull the required claims out of a verified payload. */\nexport function extractClaims(payload: JWTPayload): VerifiedToken | null {\n const email = payload.email;\n const sub = payload.sub;\n\n if (typeof email !== \"string\" || !email) {\n return null;\n }\n if (typeof sub !== \"string\" || !sub) {\n return null;\n }\n\n return { email, sub };\n}\n\n// ---------------------------------------------------------------------------\n// Cookie helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `Set-Cookie` header value for the authorisation cookie.\n *\n * Mirrors the attributes used by Cloudflare Access: `HttpOnly; Secure; SameSite=Lax; Path=/`\n *\n * For local dev over plain HTTP the `Secure` flag is omitted when the request was made to\n * `localhost` or `127.0.0.1`.\n */\nexport function buildCookieHeader(token: string, isSecure: boolean): string {\n const parts = [`${COOKIE_NAME}=${token}`, \"HttpOnly\", \"SameSite=Lax\", \"Path=/\"];\n if (isSecure) {\n parts.push(\"Secure\");\n }\n return parts.join(\"; \");\n}\n\n/**\n * Build a `Set-Cookie` header that clears the `CF_Authorization` cookie by setting it to an\n * empty value with `Max-Age=0`.\n *\n * Use this when a stale or invalid cookie needs to be removed so the user can re-authenticate.\n */\nexport function clearCookieHeader(): string {\n return `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;\n}\n\n/**\n * Parse the value of the `CF_Authorization` cookie from a `Cookie` header string. Returns\n * `undefined` when the cookie is absent.\n */\nexport function parseCookie(cookieHeader: string | null | undefined): string | undefined {\n if (!cookieHeader) return undefined;\n\n for (const pair of cookieHeader.split(\";\")) {\n const [name, ...rest] = pair.split(\"=\");\n if (name.trim() === COOKIE_NAME) {\n return rest.join(\"=\").trim();\n }\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { a as Environment, c as LogRecord, d as Runtime, f as StructuredTransportOptions, i as CreateLoggerOptions, l as Logger, m as TransportErrorHandler, n as CaptureTransport, o as LogContext, p as Transport, r as ConsoleTransportOptions, s as LogLevel, t as BrowserTransportOptions, u as ResolvedLoggerConfig } from "../types-DCSMb1cp.js";
2
+ import { a as createCaptureTransport, c as resolveLoggerConfig, i as combineTransports, l as createLogger, n as createSilentTransport, o as createBrowserTransport, r as createConsoleTransport, s as serializeError, t as createStructuredTransport } from "../index-Byl-ZrCy.js";
3
+ export { type BrowserTransportOptions, type CaptureTransport, type ConsoleTransportOptions, type CreateLoggerOptions, type Environment, type LogContext, type LogLevel, type LogRecord, type Logger, type ResolvedLoggerConfig, type Runtime, type StructuredTransportOptions, type Transport, type TransportErrorHandler, combineTransports, createBrowserTransport, createCaptureTransport, createConsoleTransport, createLogger, createSilentTransport, createStructuredTransport, resolveLoggerConfig, serializeError };
@@ -0,0 +1,3 @@
1
+ import { a as createCaptureTransport, c as serializeError, i as createConsoleTransport, n as resolveLoggerConfig, o as createBrowserTransport, r as createStructuredTransport, s as createLogger, t as createSilentTransport } from "../silent-CWpHE65X.js";
2
+ import { t as combineTransports } from "../logging-CGHjOVLM.js";
3
+ export { combineTransports, createBrowserTransport, createCaptureTransport, createConsoleTransport, createLogger, createSilentTransport, createStructuredTransport, resolveLoggerConfig, serializeError };
@@ -0,0 +1,24 @@
1
+ import "./silent-CWpHE65X.js";
2
+ //#region src/lib/logging/transports/combine.ts
3
+ /**
4
+ * Create a transport that forwards records to each of the supplied transports.
5
+ *
6
+ * @param transports - One or more transports to receive every record.
7
+ * @returns A combined `Transport`.
8
+ */
9
+ function combineTransports(...transports) {
10
+ return { log(record) {
11
+ const errors = [];
12
+ for (const transport of transports) try {
13
+ transport.log(record);
14
+ } catch (error) {
15
+ errors.push(error);
16
+ }
17
+ if (errors.length === 1) throw errors[0];
18
+ if (errors.length > 1) throw new AggregateError(errors, "Multiple transports failed");
19
+ } };
20
+ }
21
+ //#endregion
22
+ export { combineTransports as t };
23
+
24
+ //# sourceMappingURL=logging-CGHjOVLM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logging-CGHjOVLM.js","names":[],"sources":["../src/lib/logging/transports/combine.ts"],"sourcesContent":["/**\n * @file A transport that fans out each record to multiple child transports in declaration order.\n *\n * `combineTransports()` attempts every child even when an earlier one throws, then re-throws the\n * collected failures so the logger-level try/catch can route them through `onTransportError`.\n *\n * Failure semantics:\n * - One child throws → that error is re-thrown after all children run.\n * - Multiple children throw → an `AggregateError` is thrown after all children run.\n * - No child throws → returns normally.\n */\nimport type { LogRecord, Transport } from \"../types.js\";\n\n/**\n * Create a transport that forwards records to each of the supplied transports.\n *\n * @param transports - One or more transports to receive every record.\n * @returns A combined `Transport`.\n */\nexport function combineTransports(...transports: readonly Transport[]): Transport {\n return {\n log(record: LogRecord): void {\n const errors: unknown[] = [];\n\n for (const transport of transports) {\n try {\n transport.log(record);\n } catch (error) {\n errors.push(error);\n }\n }\n\n if (errors.length === 1) {\n throw errors[0];\n }\n if (errors.length > 1) {\n throw new AggregateError(errors, \"Multiple transports failed\");\n }\n }\n };\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,kBAAkB,GAAG,YAA6C;CAChF,OAAO,EACL,IAAI,QAAyB;EAC3B,MAAM,SAAoB,CAAC;EAE3B,KAAK,MAAM,aAAa,YACtB,IAAI;GACF,UAAU,IAAI,MAAM;EACtB,SAAS,OAAO;GACd,OAAO,KAAK,KAAK;EACnB;EAGF,IAAI,OAAO,WAAW,GACpB,MAAM,OAAO;EAEf,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,eAAe,QAAQ,4BAA4B;CAEjE,EACF;AACF"}
@@ -0,0 +1,20 @@
1
+ //#region src/lib/auth-internal/policy.ts
2
+ /**
3
+ * Evaluate a request pathname against an ordered list of policies.
4
+ *
5
+ * Returns a {@link PolicyMatch} for the **first matching** policy, or `undefined` when no
6
+ * policy matches (the caller decides what to do in that case).
7
+ *
8
+ * The `redirect` field defaults to `true` when the matching {@link PathPolicy} does not specify
9
+ * one.
10
+ */
11
+ function matchPolicy(pathname, policies) {
12
+ for (const { pattern, authenticate, redirect } of policies) if (pattern.test(pathname)) return {
13
+ authenticate,
14
+ redirect: redirect ?? true
15
+ };
16
+ }
17
+ //#endregion
18
+ export { matchPolicy as t };
19
+
20
+ //# sourceMappingURL=policy-CvS6AvvD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-CvS6AvvD.js","names":[],"sources":["../src/lib/auth-internal/policy.ts"],"sourcesContent":["/**\n * @file Shared path-policy evaluation for the auth internals. Pure logic with no runtime\n * dependencies, so this module is both Worker-safe (for `hono/`) and Node-safe (for `vite/`).\n */\nimport type { PathPolicy, PolicyMatch } from \"./types.js\";\n\n/**\n * Evaluate a request pathname against an ordered list of policies.\n *\n * Returns a {@link PolicyMatch} for the **first matching** policy, or `undefined` when no\n * policy matches (the caller decides what to do in that case).\n *\n * The `redirect` field defaults to `true` when the matching {@link PathPolicy} does not specify\n * one.\n */\nexport function matchPolicy(pathname: string, policies: PathPolicy[]): PolicyMatch | undefined {\n for (const { pattern, authenticate, redirect } of policies) {\n if (pattern.test(pathname)) {\n return { authenticate, redirect: redirect ?? true };\n }\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;AAeA,SAAgB,YAAY,UAAkB,UAAiD;CAC7F,KAAK,MAAM,EAAE,SAAS,cAAc,cAAc,UAChD,IAAI,QAAQ,KAAK,QAAQ,GACvB,OAAO;EAAE;EAAc,UAAU,YAAY;CAAK;AAIxD"}
@@ -0,0 +1,4 @@
1
+ import { n as ProblemDetailsInput, t as ProblemDetails } from "../types-Cx6NNILW.js";
2
+ import { t as ProblemDetailsError } from "../error-CLYcAvBM.js";
3
+ import { i as problemDetails, n as statusToSlug, r as createProblemTypeRegistry, t as statusToPhrase } from "../index-DRIhR-Xn.js";
4
+ export { type ProblemDetails, ProblemDetailsError, type ProblemDetailsInput, createProblemTypeRegistry, problemDetails, statusToPhrase, statusToSlug };
@@ -0,0 +1,3 @@
1
+ import { a as statusToPhrase, n as ProblemDetailsError, o as statusToSlug, t as problemDetails } from "../factory-BI5gVL_P.js";
2
+ import { t as createProblemTypeRegistry } from "../problem-details-CuRsLy3Q.js";
3
+ export { ProblemDetailsError, createProblemTypeRegistry, problemDetails, statusToPhrase, statusToSlug };
@@ -0,0 +1,37 @@
1
+ import { n as ProblemDetailsError } from "./factory-BI5gVL_P.js";
2
+ //#region src/lib/problem-details/registry.ts
3
+ /**
4
+ * @file A registry for pre-defined, type-safe RFC 9457 problem types.
5
+ */
6
+ /**
7
+ * Create a registry of pre-defined problem types.
8
+ * Provides type-safe error creation from registered definitions.
9
+ *
10
+ * @param definitions - A map of problem type keys to their base definition (type, status, title).
11
+ * @returns A {@link ProblemTypeRegistry} for the given definitions.
12
+ * @example
13
+ * ```ts
14
+ * const problems = createProblemTypeRegistry({
15
+ * ORDER_CONFLICT: {
16
+ * type: "https://api.example.com/problems/order-conflict",
17
+ * status: 409,
18
+ * title: "Order Conflict",
19
+ * },
20
+ * });
21
+ * throw problems.create("ORDER_CONFLICT", { detail: "Already exists" });
22
+ * ```
23
+ */
24
+ function createProblemTypeRegistry(definitions) {
25
+ return {
26
+ create: (key, options) => new ProblemDetailsError({
27
+ ...definitions[key],
28
+ ...options
29
+ }),
30
+ get: (key) => ({ ...definitions[key] }),
31
+ types: () => Object.keys(definitions)
32
+ };
33
+ }
34
+ //#endregion
35
+ export { createProblemTypeRegistry as t };
36
+
37
+ //# sourceMappingURL=problem-details-CuRsLy3Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"problem-details-CuRsLy3Q.js","names":[],"sources":["../src/lib/problem-details/registry.ts"],"sourcesContent":["/**\n * @file A registry for pre-defined, type-safe RFC 9457 problem types.\n */\nimport { ProblemDetailsError } from \"./error.js\";\n\ninterface ProblemTypeDefinition {\n readonly type: string;\n readonly status: number;\n readonly title: string;\n}\n\ninterface CreateOptions<T extends Record<string, unknown> = Record<string, unknown>> {\n detail?: string;\n instance?: string;\n extensions?: T;\n}\n\ninterface ProblemTypeRegistry<K extends string> {\n /** Create a {@link ProblemDetailsError} from a registered problem type key. */\n create: <T extends Record<string, unknown>>(\n key: K,\n options?: CreateOptions<T>\n ) => ProblemDetailsError;\n /**\n * Get the base definition (type, status, title) for a registered key.\n * Returns a defensive shallow copy on every call, so mutating the result never affects the\n * registry's internal definitions.\n */\n get: (key: K) => Readonly<ProblemTypeDefinition>;\n /** List all registered problem type keys. */\n types: () => K[];\n}\n\n/**\n * Create a registry of pre-defined problem types.\n * Provides type-safe error creation from registered definitions.\n *\n * @param definitions - A map of problem type keys to their base definition (type, status, title).\n * @returns A {@link ProblemTypeRegistry} for the given definitions.\n * @example\n * ```ts\n * const problems = createProblemTypeRegistry({\n * ORDER_CONFLICT: {\n * type: \"https://api.example.com/problems/order-conflict\",\n * status: 409,\n * title: \"Order Conflict\",\n * },\n * });\n * throw problems.create(\"ORDER_CONFLICT\", { detail: \"Already exists\" });\n * ```\n */\nexport function createProblemTypeRegistry<K extends string>(\n definitions: Record<K, ProblemTypeDefinition>\n): ProblemTypeRegistry<K> {\n return {\n create: (key, options) => new ProblemDetailsError({ ...definitions[key], ...options }),\n get: (key) => ({ ...definitions[key] }),\n types: () => Object.keys(definitions) as K[]\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,0BACd,aACwB;CACxB,OAAO;EACL,SAAS,KAAK,YAAY,IAAI,oBAAoB;GAAE,GAAG,YAAY;GAAM,GAAG;EAAQ,CAAC;EACrF,MAAM,SAAS,EAAE,GAAG,YAAY,KAAK;EACrC,aAAa,OAAO,KAAK,WAAW;CACtC;AACF"}