@vielzeug/archive 26.10.7 → 26.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/worker/env.cjs +1 -1
- package/dist/worker/env.cjs.map +1 -1
- 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/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,8 +50,10 @@ are the same schemas the Worker uses. Payloads are stable JSON objects with
|
|
|
50
50
|
sorted-key serialization; vocabularies are optional readonly arrays. The root
|
|
51
51
|
entry is browser-safe. Deploy the sibling `@vielzeug/archive/auth` issuer with its
|
|
52
52
|
required `AUTH_ARCHIVE` binding for coordinated account deletion, and bind the
|
|
53
|
-
catalog's required `AUTH_ISSUER_SERVICE` to that issuer for
|
|
54
|
-
session-version check after local JWT verification.
|
|
53
|
+
catalog's required `AUTH_ISSUER_SERVICE` to that issuer for signing-key retrieval
|
|
54
|
+
and an uncached session-version check after local JWT verification.
|
|
55
|
+
`JWT_JWKS_URL` selects the issuer's JWKS route; the catalog fetches it through
|
|
56
|
+
the service binding, not global `fetch`, including same-zone deployments.
|
|
55
57
|
|
|
56
58
|
Issuer sessions carry the required `sessionVersion` claim backed by
|
|
57
59
|
`auth_users.session_version`. `POST /api/v1/sign-out` only clears the current
|
|
@@ -75,7 +77,7 @@ not update existing rows.
|
|
|
75
77
|
- [Usage Guide](https://vielzeug.dev/archive/usage)
|
|
76
78
|
- [API Reference](https://vielzeug.dev/archive/api)
|
|
77
79
|
- [Examples](https://vielzeug.dev/archive/examples)
|
|
78
|
-
- [
|
|
80
|
+
- [Migration](https://vielzeug.dev/archive/migration)
|
|
79
81
|
|
|
80
82
|
## License
|
|
81
83
|
|
package/dist/worker/env.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./http.cjs");let t=require("jose");var n=new Set([`127.0.0.1`,`localhost`,`[::1]`]),isLoopback=e=>n.has(e.hostname),configError=t=>new e.ApiError(503,`unavailable`,`The service is temporarily unavailable.`,{diagnostic:{field:t,kind:`configuration`}}),requireVar=(e,t)=>{let n=e[t];if(typeof n!=`string`||n.length===0)throw configError(t);return n},requireUrl=(e,t)=>{try{return new URL(e)}catch{throw configError(t)}},validateUrl=(e,t,n)=>{if(e.username!==``||e.password!==``||e.hash!==``)throw configError(n);if(t===`local`){if(e.protocol!==`http:`||!isLoopback(e))throw configError(n);return e}if(e.protocol!==`https:`||isLoopback(e))throw configError(n);return e},validateOrigin=(e,t,n)=>{if(e===`null`||e===`*`)throw configError(n);let r=validateUrl(requireUrl(e,n),t,n);if(r.pathname!==`/`||r.search!==``)throw configError(n);let i=r.origin;if(i!==e)throw configError(n);return i},flag=(e,t)=>{if(e===`true`)return!0;if(e===`false`)return!1;throw configError(t)},r=`dev-only-insecure-pepper`,rateLimitPepperFor=(e,t)=>{let n=e.RATE_LIMIT_KEY_SECRET;if(typeof n!=`string`||n.length===0){if(t===`local`)return r;throw configError(`RATE_LIMIT_KEY_SECRET`)}if(t===`production`&&n.length<32)throw configError(`RATE_LIMIT_KEY_SECRET`);return n},i=new WeakMap;function apiConfig(e){let n=i.get(e);if(n)return n;let r=requireVar(e,`APP_ENV`);if(r!==`local`&&r!==`production`)throw configError(`APP_ENV`);let a=r;validateUrl(requireUrl(requireVar(e,`JWT_ISSUER`),`JWT_ISSUER`),a,`JWT_ISSUER`);let o=validateUrl(requireUrl(requireVar(e,`JWT_JWKS_URL`),`JWT_JWKS_URL`),a,`JWT_JWKS_URL`),s=requireVar(e,`JWT_AUDIENCE`),c=new Set(requireVar(e,`ALLOWED_ORIGINS`).split(`,`).map(e=>e.trim()).filter(e=>e.length>0).map(e=>validateOrigin(e,a,`ALLOWED_ORIGINS`)));if(c.size===0)throw configError(`ALLOWED_ORIGINS`);if(typeof e.DB?.prepare!=`function`||typeof e.DB?.batch!=`function`)throw configError(`DB`);if(typeof e.READ_LIMIT?.limit!=`function`)throw configError(`READ_LIMIT`);if(typeof e.WRITE_LIMIT?.limit!=`function`)throw configError(`WRITE_LIMIT`);if(typeof e.AUTH_ISSUER_SERVICE?.fetch!=`function`)throw configError(`AUTH_ISSUER_SERVICE`);let l={allowedOrigins:c,appEnv:a,audience:s,issuer:requireVar(e,`JWT_ISSUER`),jwks:(0,t.createRemoteJWKSet)(o),likesEnabled:flag(requireVar(e,`LIKES_ENABLED`),`LIKES_ENABLED`),publishingEnabled:flag(requireVar(e,`PUBLISHING_ENABLED`),`PUBLISHING_ENABLED`),rateLimitPepper:rateLimitPepperFor(e,a)};return i.set(e,l),l}exports.apiConfig=apiConfig;
|
|
1
|
+
const e=require("./http.cjs");let t=require("jose");var n=new Set([`127.0.0.1`,`localhost`,`[::1]`]),isLoopback=e=>n.has(e.hostname),configError=t=>new e.ApiError(503,`unavailable`,`The service is temporarily unavailable.`,{diagnostic:{field:t,kind:`configuration`}}),requireVar=(e,t)=>{let n=e[t];if(typeof n!=`string`||n.length===0)throw configError(t);return n},requireUrl=(e,t)=>{try{return new URL(e)}catch{throw configError(t)}},validateUrl=(e,t,n)=>{if(e.username!==``||e.password!==``||e.hash!==``)throw configError(n);if(t===`local`){if(e.protocol!==`http:`||!isLoopback(e))throw configError(n);return e}if(e.protocol!==`https:`||isLoopback(e))throw configError(n);return e},validateOrigin=(e,t,n)=>{if(e===`null`||e===`*`)throw configError(n);let r=validateUrl(requireUrl(e,n),t,n);if(r.pathname!==`/`||r.search!==``)throw configError(n);let i=r.origin;if(i!==e)throw configError(n);return i},flag=(e,t)=>{if(e===`true`)return!0;if(e===`false`)return!1;throw configError(t)},r=`dev-only-insecure-pepper`,rateLimitPepperFor=(e,t)=>{let n=e.RATE_LIMIT_KEY_SECRET;if(typeof n!=`string`||n.length===0){if(t===`local`)return r;throw configError(`RATE_LIMIT_KEY_SECRET`)}if(t===`production`&&n.length<32)throw configError(`RATE_LIMIT_KEY_SECRET`);return n},i=new WeakMap;function apiConfig(e){let n=i.get(e);if(n)return n;let r=requireVar(e,`APP_ENV`);if(r!==`local`&&r!==`production`)throw configError(`APP_ENV`);let a=r;validateUrl(requireUrl(requireVar(e,`JWT_ISSUER`),`JWT_ISSUER`),a,`JWT_ISSUER`);let o=validateUrl(requireUrl(requireVar(e,`JWT_JWKS_URL`),`JWT_JWKS_URL`),a,`JWT_JWKS_URL`),s=requireVar(e,`JWT_AUDIENCE`),c=new Set(requireVar(e,`ALLOWED_ORIGINS`).split(`,`).map(e=>e.trim()).filter(e=>e.length>0).map(e=>validateOrigin(e,a,`ALLOWED_ORIGINS`)));if(c.size===0)throw configError(`ALLOWED_ORIGINS`);if(typeof e.DB?.prepare!=`function`||typeof e.DB?.batch!=`function`)throw configError(`DB`);if(typeof e.READ_LIMIT?.limit!=`function`)throw configError(`READ_LIMIT`);if(typeof e.WRITE_LIMIT?.limit!=`function`)throw configError(`WRITE_LIMIT`);if(typeof e.AUTH_ISSUER_SERVICE?.fetch!=`function`)throw configError(`AUTH_ISSUER_SERVICE`);let l={allowedOrigins:c,appEnv:a,audience:s,issuer:requireVar(e,`JWT_ISSUER`),jwks:(0,t.createRemoteJWKSet)(o,{[t.customFetch]:(t,n)=>e.AUTH_ISSUER_SERVICE.fetch(new Request(t,n))}),likesEnabled:flag(requireVar(e,`LIKES_ENABLED`),`LIKES_ENABLED`),publishingEnabled:flag(requireVar(e,`PUBLISHING_ENABLED`),`PUBLISHING_ENABLED`),rateLimitPepper:rateLimitPepperFor(e,a)};return i.set(e,l),l}exports.apiConfig=apiConfig;
|
|
2
2
|
//# sourceMappingURL=env.cjs.map
|
package/dist/worker/env.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.cjs","names":[],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet } from 'jose';\nimport { ApiError } from './http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/** The D1 statement subset the core binds; null params model nullable columns. */\nexport interface D1Statement {\n all<T>(): Promise<{ results?: T[] }>;\n bind(...params: (string | number | null)[]): D1Statement;\n first<T>(): Promise<T | null>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\n/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */\nexport interface D1Like {\n batch(statements: D1Statement[]): Promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): D1Statement;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"env.cjs","names":[],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet, customFetch } from 'jose';\nimport { ApiError } from './http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/** The D1 statement subset the core binds; null params model nullable columns. */\nexport interface D1Statement {\n all<T>(): Promise<{ results?: T[] }>;\n bind(...params: (string | number | null)[]): D1Statement;\n first<T>(): Promise<T | null>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\n/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */\nexport interface D1Like {\n batch(statements: D1Statement[]): Promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): D1Statement;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl, {\n [customFetch]: (input, init) => env.AUTH_ISSUER_SERVICE.fetch(new Request(input, init)),\n }),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
|
package/dist/worker/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/worker/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/worker/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAe,MAAM,MAAM,CAAC;AAGvD;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IAC5C,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,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;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,mBAAmB,EAAE,wBAAwB,CAAC;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAiFD,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,SAAS,CA4C1D"}
|
package/dist/worker/env.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ApiError as e}from"./http.js";import{createRemoteJWKSet as t}from"jose";var
|
|
1
|
+
import{ApiError as e}from"./http.js";import{createRemoteJWKSet as t,customFetch as n}from"jose";var r=new Set([`127.0.0.1`,`localhost`,`[::1]`]),isLoopback=e=>r.has(e.hostname),configError=t=>new e(503,`unavailable`,`The service is temporarily unavailable.`,{diagnostic:{field:t,kind:`configuration`}}),requireVar=(e,t)=>{let n=e[t];if(typeof n!=`string`||n.length===0)throw configError(t);return n},requireUrl=(e,t)=>{try{return new URL(e)}catch{throw configError(t)}},validateUrl=(e,t,n)=>{if(e.username!==``||e.password!==``||e.hash!==``)throw configError(n);if(t===`local`){if(e.protocol!==`http:`||!isLoopback(e))throw configError(n);return e}if(e.protocol!==`https:`||isLoopback(e))throw configError(n);return e},validateOrigin=(e,t,n)=>{if(e===`null`||e===`*`)throw configError(n);let r=validateUrl(requireUrl(e,n),t,n);if(r.pathname!==`/`||r.search!==``)throw configError(n);let i=r.origin;if(i!==e)throw configError(n);return i},flag=(e,t)=>{if(e===`true`)return!0;if(e===`false`)return!1;throw configError(t)},i=`dev-only-insecure-pepper`,rateLimitPepperFor=(e,t)=>{let n=e.RATE_LIMIT_KEY_SECRET;if(typeof n!=`string`||n.length===0){if(t===`local`)return i;throw configError(`RATE_LIMIT_KEY_SECRET`)}if(t===`production`&&n.length<32)throw configError(`RATE_LIMIT_KEY_SECRET`);return n},a=new WeakMap;function apiConfig(e){let r=a.get(e);if(r)return r;let i=requireVar(e,`APP_ENV`);if(i!==`local`&&i!==`production`)throw configError(`APP_ENV`);let o=i;validateUrl(requireUrl(requireVar(e,`JWT_ISSUER`),`JWT_ISSUER`),o,`JWT_ISSUER`);let s=validateUrl(requireUrl(requireVar(e,`JWT_JWKS_URL`),`JWT_JWKS_URL`),o,`JWT_JWKS_URL`),c=requireVar(e,`JWT_AUDIENCE`),l=new Set(requireVar(e,`ALLOWED_ORIGINS`).split(`,`).map(e=>e.trim()).filter(e=>e.length>0).map(e=>validateOrigin(e,o,`ALLOWED_ORIGINS`)));if(l.size===0)throw configError(`ALLOWED_ORIGINS`);if(typeof e.DB?.prepare!=`function`||typeof e.DB?.batch!=`function`)throw configError(`DB`);if(typeof e.READ_LIMIT?.limit!=`function`)throw configError(`READ_LIMIT`);if(typeof e.WRITE_LIMIT?.limit!=`function`)throw configError(`WRITE_LIMIT`);if(typeof e.AUTH_ISSUER_SERVICE?.fetch!=`function`)throw configError(`AUTH_ISSUER_SERVICE`);let u={allowedOrigins:l,appEnv:o,audience:c,issuer:requireVar(e,`JWT_ISSUER`),jwks:t(s,{[n]:(t,n)=>e.AUTH_ISSUER_SERVICE.fetch(new Request(t,n))}),likesEnabled:flag(requireVar(e,`LIKES_ENABLED`),`LIKES_ENABLED`),publishingEnabled:flag(requireVar(e,`PUBLISHING_ENABLED`),`PUBLISHING_ENABLED`),rateLimitPepper:rateLimitPepperFor(e,o)};return a.set(e,u),u}export{apiConfig};
|
|
2
2
|
//# sourceMappingURL=env.js.map
|
package/dist/worker/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","names":[],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet } from 'jose';\nimport { ApiError } from './http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/** The D1 statement subset the core binds; null params model nullable columns. */\nexport interface D1Statement {\n all<T>(): Promise<{ results?: T[] }>;\n bind(...params: (string | number | null)[]): D1Statement;\n first<T>(): Promise<T | null>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\n/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */\nexport interface D1Like {\n batch(statements: D1Statement[]): Promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): D1Statement;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"env.js","names":[],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet, customFetch } from 'jose';\nimport { ApiError } from './http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/** The D1 statement subset the core binds; null params model nullable columns. */\nexport interface D1Statement {\n all<T>(): Promise<{ results?: T[] }>;\n bind(...params: (string | number | null)[]): D1Statement;\n first<T>(): Promise<T | null>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\n/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */\nexport interface D1Like {\n batch(statements: D1Statement[]): Promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): D1Statement;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl, {\n [customFetch]: (input, init) => env.AUTH_ISSUER_SERVICE.fetch(new Request(input, init)),\n }),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/archive",
|
|
3
|
-
"version": "26.10.
|
|
3
|
+
"version": "26.10.9",
|
|
4
4
|
"description": "Community content publishing for Cloudflare Workers + D1: a hardened Worker core over per-collection payload schemas",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"test:types": "tsc -p tsconfig.json --noEmit"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@vielzeug/spell": "26.10.
|
|
64
|
+
"@vielzeug/spell": "26.10.9",
|
|
65
65
|
"jose": "6.2.12"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|