@auth-craft/aws-cf-stack 1.1.1 → 1.2.0

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/.env.example CHANGED
@@ -52,6 +52,21 @@ CF_EDGE_GATE_SECRET= # required for gateway (flag --edge-gate-secret)
52
52
  CF_ALLOWED_ORIGINS=* # optional, default * (CORS; use explicit origins with cookies)
53
53
  CF_WORKERS_SUBDOMAIN= # optional — your *.<subdomain>.workers.dev (to print URLs)
54
54
  CF_GATEWAY_JWT_PRIVATE_KEY= # optional — Ed25519 JWK base64; worker signs x-gateway-auth
55
+ # ── CORS (per scope) ─────────────────────────────────────────────────────────
56
+ # CORS is resolved PER SCOPE. CF_ALLOWED_ORIGINS above is the shared fallback;
57
+ # override a single scope with CF_ALLOWED_ORIGINS_<SCOPE>. Use explicit origins
58
+ # (not '*') wherever cookies/credentials are used — '*' disables credentials.
59
+ CF_ALLOWED_ORIGINS_SYSTEM= # optional, e.g. https://admin.example.com
60
+ CF_ALLOWED_ORIGINS_TENANT= # optional, e.g. https://app.example.com
61
+ CF_ALLOWED_ORIGINS_CUSTOMER= # optional, static customer origins (small/stable set)
62
+ # Dynamic CORS (customer: many stores, no redeploy). Two independent mechanisms:
63
+ # 1) Suffix match (static, deploy-time) — allow any subdomain of a domain you own.
64
+ CF_ALLOWED_ORIGIN_SUFFIXES= # shared fallback, e.g. .myplatform.com
65
+ CF_ALLOWED_ORIGIN_SUFFIXES_CUSTOMER= # e.g. .snapshot.app — '*.snapshot.app' allowed
66
+ # 2) KV allowlist (runtime, per-store custom domains) — set the KV namespace id to
67
+ # enable; the worker owns CRUD via PUT/DELETE/GET /internal/cors-origins (Service
68
+ # JWT). Leave empty to disable. Typically only the customer scope sets this.
69
+ CF_CORS_KV_ID_CUSTOMER= # Cloudflare KV namespace id (create once: wrangler kv namespace create CORS_KV)
55
70
  # Per-scope route prefixes (default /system, /tenant, /customer):
56
71
  CF_WORKER_ROUTE_PREFIX_SYSTEM=/system
57
72
  CF_WORKER_ROUTE_PREFIX_TENANT=/tenant
package/README.md CHANGED
@@ -128,6 +128,116 @@ CF_EDGE_GATE_SECRET="$SNAP_EDGE" CLOUDFLARE_ACCOUNT_ID="$SNAP_CF_ACCT" \
128
128
  npx auth-craft-stack all --stage "$STAGE"
129
129
  ```
130
130
 
131
+ ## CORS (per scope + dynamic allowlist)
132
+
133
+ CORS is resolved **per scope** (system / tenant / customer) — the 3 workers no longer
134
+ share one origin list. `CF_ALLOWED_ORIGINS` is the shared fallback; override a single
135
+ scope with `CF_ALLOWED_ORIGINS_<SCOPE>` (e.g. `CF_ALLOWED_ORIGINS_SYSTEM=https://admin.example.com`).
136
+
137
+ For **customer** (snapshot-commerce style — stores added continuously, static lists
138
+ aren't viable), two dynamic mechanisms can be enabled independently. Neither requires a
139
+ redeploy to add a store:
140
+
141
+ 1. **Suffix match** (static, deploy-time) — allow any subdomain of a domain you own.
142
+ Set `CF_ALLOWED_ORIGIN_SUFFIXES_CUSTOMER=.snapshot.app` → every `*.snapshot.app`
143
+ origin is allowed. Zero lookup, zero latency. Best when stores are subdomains.
144
+ 2. **KV allowlist** (runtime, per-store **custom domains**) — set the KV namespace id
145
+ with `CF_CORS_KV_ID_CUSTOMER`. The worker then **owns CRUD** on the allowlist via a
146
+ service route (Service JWT, typ=3) — your backend calls it when a store is created /
147
+ removed, so no Cloudflare credentials ever leave the worker:
148
+
149
+ ```
150
+ PUT /internal/cors-origins { "origin": "https://shop.brand.com" } # allow
151
+ DELETE /internal/cors-origins { "origin": "https://shop.brand.com" } # revoke
152
+ GET /internal/cors-origins # list
153
+ ```
154
+
155
+ Create the namespace once: `wrangler kv namespace create CORS_KV` → put its id in
156
+ `CF_CORS_KV_ID_CUSTOMER`. A KV binding can't be set via `--var`, so the deploy
157
+ appends a `[[kv_namespaces]]` block to a temp wrangler config for that scope only.
158
+
159
+ If neither is set, the customer worker falls back to its static `ALLOWED_ORIGINS`
160
+ (dynamic CORS off). System/tenant normally use only static origins.
161
+
162
+ > **Cookies:** an allowed origin (static, suffix, **or KV**) is reflected with
163
+ > `Access-Control-Allow-Credentials: true`, so credentialed requests work. `*` never is
164
+ > (browsers forbid credentials with `*`) — use explicit origins / suffix / KV when you
165
+ > need cookies. Cross-**site** cookies (store on a different registrable domain than the
166
+ > gateway) additionally need the backend to set `SameSite=None; Secure` on the cookie.
167
+
168
+ ## Access models & token transport
169
+
170
+ There are two ways a frontend reaches auth, and they can be **mixed per scope** against
171
+ the same gateway + backend:
172
+
173
+ ### Model 1 — Direct to gateway (cross-origin) + token in body
174
+ The app calls the gateway worker on a different origin. Refresh token is returned **in
175
+ the response body** (client stores it; `@auth-craft/client` `createBodyProvider`). No
176
+ auth cookie → **no cross-site cookie problem, immune to Safari ITP**, and the customer
177
+ worker can run with `ALLOWED_ORIGINS=*` (Bearer needs no credentials → no dynamic CORS,
178
+ no KV). Trade-off: the refresh token lives in client storage (XSS-reachable) — mitigated
179
+ by rotation + short TTL. Good default for **customer** (many stores, many domains).
180
+
181
+ ### Model 2 — Cloudflare Pages reverse-proxy (same-origin) + cookie
182
+ If the app runs on Cloudflare Pages, mount `@auth-craft/pages-auth-proxy` so `/{scope}/auth/*`
183
+ is served on the app's **own origin** and forwarded to the gateway. The refresh cookie is
184
+ then **first-party** (`HttpOnly; Secure; SameSite=Strict`) — XSS can't read it, there's no
185
+ cross-site CORS, and it's immune to ITP. Best security; requires the app to be on Pages.
186
+ Good for **system/tenant** (your own admin apps).
187
+
188
+ **Wiring (in the consumer's Pages project — not this stack).** The Service Binding lives in
189
+ your Pages app's `wrangler.toml` and points at the gateway worker this stack deployed. The
190
+ worker names + a ready-to-paste snippet are in `auth-craft-stack outputs`:
191
+
192
+ ```jsonc
193
+ // auth-craft-stack outputs →
194
+ {
195
+ "workerNames": { "system": "auth-craft-prod-system", "tenant": "...", "customer": "auth-craft-prod-customer" },
196
+ "pagesProxy": { "serviceBindingExample": "[[services]]\nbinding = \"AUTH_GATEWAY\"\nservice = \"auth-craft-prod-customer\"\n" }
197
+ }
198
+ ```
199
+
200
+ In the Pages app:
201
+
202
+ ```toml
203
+ # wrangler.toml (your Pages project) — bind the gateway worker for the scope this app serves
204
+ [[services]]
205
+ binding = "AUTH_GATEWAY"
206
+ service = "auth-craft-prod-customer" # from outputs.workerNames.<scope>
207
+ ```
208
+
209
+ ```ts
210
+ // functions/customer/[[path]].ts
211
+ import { createPagesAuthProxy } from '@auth-craft/pages-auth-proxy/pages';
212
+ export const onRequest = createPagesAuthProxy({ prefix: '/customer' });
213
+ ```
214
+
215
+ Set `EDGE_GATE_SECRET` as a Pages secret (same value as the stack's `CF_EDGE_GATE_SECRET`).
216
+ With a Service Binding the worker→worker hop stays in-network (lowest latency; typically not
217
+ billed as a second request). If you can't use a binding, set `AUTH_GATEWAY_URL` to the
218
+ gateway URL from `outputs.gatewayUrls.<scope>` instead — that path is a normal billable
219
+ request. See [@auth-craft/pages-auth-proxy](../pages-auth-proxy/README.md).
220
+
221
+ ### Per-scope token transport
222
+
223
+ `REFRESH_TOKEN_STRATEGY` sets the transport for every scope. To mix, use the per-scope
224
+ vars (any one of them switches to per-scope mode; unset scopes fall back to the global
225
+ value, else `body`):
226
+
227
+ ```bash
228
+ REFRESH_TOKEN_STRATEGY_CUSTOMER=body # storefront, many domains → body
229
+ REFRESH_TOKEN_STRATEGY_TENANT=cookie # your tenant admin app → cookie
230
+ REFRESH_TOKEN_STRATEGY_SYSTEM=cookie # your system admin app → cookie
231
+ ```
232
+
233
+ For any scope using **cookie**, set `trustedOrigins` (the app's CORS origins) so the
234
+ backend's CSRF Origin check is active — `SameSite` is the primary defence, this is
235
+ defense-in-depth. See [CORS](#cors-per-scope--dynamic-allowlist) for the origin config.
236
+
237
+ > Cookie isolation does **not** come from CORS. It comes from first-party `SameSite` +
238
+ > `HttpOnly` + the backend CSRF Origin check. Removing CORS (same-origin via Pages proxy,
239
+ > or Bearer in body) does not weaken access control.
240
+
131
241
  ## Versioning
132
242
 
133
243
  Pin `@auth-craft/aws-cf-stack` in your `package.json` like any dependency. The package version
@@ -147,6 +257,13 @@ time). Bump the version to deploy newer auth-craft.
147
257
  `gateway` from the **same** value.
148
258
  - **`gateway` can't read Lambda outputs** — deploy the `lambda` stage first (it reads the live
149
259
  `<app>-<stage>-api` stack).
260
+ - **Customer CORS rejects a store origin (no `Access-Control-Allow-Origin`)** — the origin isn't
261
+ on any allowlist. Either it doesn't match `CF_ALLOWED_ORIGIN_SUFFIXES_CUSTOMER`, or it wasn't
262
+ `PUT` into KV (`/internal/cors-origins`), or `CF_CORS_KV_ID_CUSTOMER` is unset so KV lookup is
263
+ off. See [CORS](#cors-per-scope--dynamic-allowlist).
264
+ - **Cookies not sent from a store on another domain** — CORS being correct isn't enough for
265
+ cross-**site** cookies; the backend must set `SameSite=None; Secure`. Same-site (store is a
266
+ subdomain via suffix match) works with `SameSite=Lax`.
150
267
  - **Queue email silently not sent** — with `LAMBDA_EMAIL_MODE=queue`, the SQS queue must exist
151
268
  **before** the `lambda` stage: pass `LAMBDA_EMAIL_QUEUE_ARN`/`LAMBDA_EMAIL_QUEUE_URL` of an
152
269
  already-deployed queue. The stack grants the Lambda `sqs:SendMessage` on that ARN, but it
@@ -1 +1 @@
1
- var le=typeof process<"u"&&!1;function _(e){return le?Object.freeze(e):e}function S(e,t){if(arguments.length===0)return _({ok:!0,data:void 0});let r={ok:!0,data:e};return t!==void 0&&(r.meta=t),_(r)}function c(e,t){let n={ok:!1,errors:_(Array.isArray(e)?[...e]:[e])};return t!==void 0&&(n.meta=t),_(n)}function l(e,t){return r=>{let n={code:e,message:r?.message??t};return r?.field&&(n.field=r.field),n}}function C(e){return e.ok?e.data===void 0?e.meta?{ok:!0,meta:e.meta}:{ok:!0}:e.meta?{ok:!0,data:e.data,meta:e.meta}:{ok:!0,data:e.data}:e.meta?{ok:!1,errors:e.errors,meta:e.meta}:{ok:!1,errors:e.errors}}function V(e){let r=["BACKEND_URL","BACKEND_GATEWAY_SECRET","EDGE_GATE_SECRET","JWT_PUBLIC_KEY","SERVICE_JWT_PUBLIC_KEY"].filter(n=>!e[n]);if(r.length>0)throw new Error(`Missing required environment variables: ${r.join(", ")}`)}function de(e){return e.ENVIRONMENT||"development"}function R(e){return de(e)==="development"}function K(e){return e.ALLOWED_ORIGINS?e.ALLOWED_ORIGINS.split(",").map(t=>t.trim()).filter(Boolean):["*"]}function w(e){let t=e.ROUTE_PREFIX||"";if(!t)return"";let r=t.startsWith("/")?t:`/${t}`;return r.endsWith("/")?r.slice(0,-1):r}function O(e,t){let r=w(t);return r?e===r?"/":e.startsWith(`${r}/`)?e.slice(r.length):e:e}var pe=new Set(["system","tenant","customer"]);function N(e){return e&&pe.has(e)?e:void 0}function M(e,t){let r=N(e.AUTH_SCOPE);if(r)return{scope:r,forwardPath:t};let n=N(w(e).replace(/^\//,""));if(n)return{scope:n,forwardPath:t};let o=t.split("/").filter(Boolean),i=N(o[0]);if(i){let s=t.slice(t.indexOf(o[0])+o[0].length);return{scope:i,forwardPath:s||"/"}}return{scope:void 0,forwardPath:t}}function v(e,t){let r=w(t);return r?e===r||e.startsWith(`${r}/`):!0}function B(e){if(!e)return new Set(["EdDSA"]);let t=e.split(",").map(r=>r.trim()).filter(Boolean);return t.length>0?new Set(t):new Set(["EdDSA"])}function H(e){return B(e.JWT_ALLOWED_ALGORITHMS)}function F(e){return B(e.SERVICE_JWT_ALLOWED_ALGORITHMS)}function Ee(e){let t=e.SERVICE_ROUTE_PREFIX||"/internal",r=t.startsWith("/")?t:`/${t}`;return r.endsWith("/")?r.slice(0,-1):r}function Y(e){return e.SERVICE_JWT_AUDIENCE||"auth-craft:internal"}function X(e){return e.JWT_ISSUER||void 0}function $(e){return e.SERVICE_JWT_ISSUER||void 0}function j(e,t){let r=e.split("?")[0].replace(/\/$/,"")||"/",n=Ee(t);return r===n||r.startsWith(`${n}/`)}var ge=new Set(["authenticate","login","register","refresh","logout","complete-challenge","complete-mfa-challenge","issue-mfa-challenge","forgot-password","reset-password","verify-email","resend-verification"]),G=new Set(["/verification/resend","/verification/verify","/password/forgot","/password/reset"]);function z(e){let t=e.split("?")[0].replace(/\/$/,"")||"/";if(t==="/"||t.startsWith("/oauth/")||t==="/oauth"||G.has(t))return!0;let r=t.match(/^\/[^/]+(\/(?:verification|password)\/[^/]+)$/);if(r?.[1]&&G.has(r[1]))return!0;let n=t.match(/^(?:\/[^/]+)?\/auth\/([^/]+)/);if(n){let o=n[1];if(o&&ge.has(o)||o?.startsWith("oauth"))return!0}return!1}function Q(e){return e.GATEWAY_JWT_PRIVATE_KEY||void 0}function Z(e){return e.TID_VALUE||void 0}function q(e){let t=e.split("?")[0].replace(/\/$/,"");return t.endsWith("/auth/login")||t.endsWith("/auth/authenticate")}function Ae(){let e=Date.now(),t=new Uint8Array(16);t[0]=e/1099511627776&255,t[1]=e/4294967296&255,t[2]=e/16777216&255,t[3]=e/65536&255,t[4]=e/256&255,t[5]=e&255;let r=new Uint8Array(10);crypto.getRandomValues(r),t.set(r,6),t[6]=t[6]&15|112,t[8]=t[8]&63|128;let n="0123456789ABCDEFGHJKMNPQRSTVWXYZ",o="",i=0,s=0;for(let a=0;a<t.length;a++)for(s=s<<8|t[a],i+=8;i>=5;)o+=n[s>>>i-5&31],i-=5;return i>0&&(o+=n[s<<5-i&31]),o}var P={BAD_REQUEST:l("BAD_REQUEST","Bad request"),UNAUTHORIZED:l("UNAUTHORIZED","Authentication required"),FORBIDDEN:l("FORBIDDEN","Access denied"),NOT_FOUND:l("NOT_FOUND","Resource not found"),RATE_LIMIT_EXCEEDED:l("RATE_LIMIT_EXCEEDED","Too many requests"),INTERNAL_ERROR:l("INTERNAL_ERROR","Internal server error"),BAD_GATEWAY:l("BAD_GATEWAY","Gateway error"),SERVICE_UNAVAILABLE:l("SERVICE_UNAVAILABLE","Service temporarily unavailable"),GATEWAY_TIMEOUT:l("GATEWAY_TIMEOUT","Gateway timeout")},f={INVALID_FORMAT:l("JWT_INVALID_FORMAT","Invalid JWT format"),UNSUPPORTED_ALGORITHM:l("JWT_UNSUPPORTED_ALGORITHM","Unsupported JWT algorithm"),INVALID_TOKEN_TYPE:l("JWT_INVALID_TOKEN_TYPE","Invalid token type: expected access token"),TOKEN_EXPIRED:l("JWT_TOKEN_EXPIRED","JWT token expired"),SIGNATURE_INVALID:l("JWT_SIGNATURE_INVALID","JWT signature verification failed"),MISSING_TOKEN:l("JWT_MISSING_TOKEN","Missing access token"),INVALID_ISSUER:l("JWT_INVALID_ISSUER","Invalid JWT issuer")},ee={BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,RATE_LIMIT_EXCEEDED:429,INTERNAL_ERROR:500,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504,JWT_INVALID_FORMAT:401,JWT_UNSUPPORTED_ALGORITHM:401,JWT_INVALID_TOKEN_TYPE:401,JWT_TOKEN_EXPIRED:401,JWT_SIGNATURE_INVALID:401,JWT_MISSING_TOKEN:401,JWT_INVALID_ISSUER:401};function he(e){if(e.ok)return e.data===void 0?204:200;let t=e.errors?.[0];return t?.code&&t.code in ee?ee[t.code]:500}function p(e,t){let r=new Date().toISOString(),n=t||Ae(),o={...e.meta,timestamp:r,traceId:n},i=e.ok?S(e.data,o):c(e.errors,o),s=he(i),a=C(i);return new Response(JSON.stringify(a),{status:s,headers:{"Content-Type":"application/json"}})}var Re=new Set(["EdDSA","ES256","RS256","PS256"]);function Te(e){return Re.has(e)}function Se(e){switch(e){case"EdDSA":return{name:"Ed25519"};case"ES256":return{name:"ECDSA",namedCurve:"P-256"};case"RS256":return{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"};case"PS256":return{name:"RSA-PSS",hash:"SHA-256"}}}function ye(e){switch(e){case"EdDSA":return"Ed25519";case"ES256":return{name:"ECDSA",hash:"SHA-256"};case"RS256":return{name:"RSASSA-PKCS1-v1_5"};case"PS256":return{name:"RSA-PSS",saltLength:32}}}function x(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=t+"==".slice(0,(4-t.length%4)%4),n=atob(r),o=new Uint8Array(n.length);for(let i=0;i<n.length;i++)o[i]=n.charCodeAt(i);return o}function te(e){return atob(e)}var _e=/^[0-9a-fA-F]{64}$/;async function we(e,t){if(_e.test(e)){if(t!=="EdDSA")throw new Error(`Hex key format is only supported for EdDSA, got ${t}`);let n=new Uint8Array(32);for(let o=0;o<32;o++)n[o]=parseInt(e.slice(o*2,o*2+2),16);return crypto.subtle.importKey("raw",n,{name:"Ed25519"},!1,["verify"])}let r;try{r=JSON.parse(te(e))}catch{throw new Error(`Invalid JWK base64 for algorithm ${t}`)}return await crypto.subtle.importKey("jwk",r,Se(t),!1,["verify"])}function re(e){let t=e.split(".");if(t.length!==3)return null;let r=t[1];if(!r)return null;try{let n=x(r);return JSON.parse(new TextDecoder().decode(n))}catch{return null}}function Ie(e){let r=e.split(".")[0];if(!r)return c(f.INVALID_FORMAT());try{let n=x(r);return S(JSON.parse(new TextDecoder().decode(n)))}catch{return c(f.INVALID_FORMAT())}}function De(e){let r=e.split(".")[1];if(!r)return c(f.INVALID_FORMAT());try{let n=x(r);return S(JSON.parse(new TextDecoder().decode(n)))}catch{return c(f.INVALID_FORMAT())}}async function Oe(e,t,r){let n=e.split("."),o=n[0],i=n[1],s=n[2];if(!o||!i||!s)return!1;let a=new TextEncoder().encode(`${o}.${i}`),u=x(s);try{return await crypto.subtle.verify(ye(r),t,u,a)}catch{return!1}}async function W(e,t,r=1,n=new Set(["EdDSA"]),o){let i=Ie(e);if(!i.ok)return c(i.errors,i.meta);let s=i.data.alg;if(!n.has(s))return c(f.UNSUPPORTED_ALGORITHM());if(!Te(s))return c(f.UNSUPPORTED_ALGORITHM());let a=De(e);if(!a.ok)return c(a.errors,a.meta);let u=a.data;if(u.typ!==r)return c(f.INVALID_TOKEN_TYPE());if(o!==void 0&&u.iss!==o)return c(f.INVALID_ISSUER());if(u.exp&&Math.floor(Date.now()/1e3)>=u.exp)return c(f.TOKEN_EXPIRED());let d;try{d=await we(t,s)}catch{return c(f.INVALID_FORMAT())}return await Oe(e,d,s)?S(u):c(f.SIGNATURE_INVALID())}var L=null;async function Pe(e){let t;try{t=JSON.parse(te(e))}catch{throw new Error("Invalid Gateway JWT private key: expected base64-encoded JWK")}return await crypto.subtle.importKey("jwk",t,{name:"Ed25519"},!1,["sign"])}function U(e){let t="";for(let r=0;r<e.length;r++)t+=String.fromCharCode(e[r]);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function ne(e,t,r,n,o=30){L||(L=Pe(e));let i=await L,s=Math.floor(Date.now()/1e3),a={alg:"EdDSA",typ:"JWT"},u={iss:t,aud:r,sub:n,iat:s,exp:s+o,jti:crypto.randomUUID()},d=U(new TextEncoder().encode(JSON.stringify(a))),g=U(new TextEncoder().encode(JSON.stringify(u))),h=new TextEncoder().encode(`${d}.${g}`),m=await crypto.subtle.sign("Ed25519",i,h),T=U(new Uint8Array(m));return`${d}.${g}.${T}`}var xe=["content-type","accept","accept-encoding","accept-language","user-agent","cookie"];function Ce(e){return e.getSetCookie?.()??[]}async function Ne(e,t){if(!(e.headers.get("content-type")||"").includes("application/json"))return null;let n;try{n=await e.clone().json()}catch{return null}return delete n.tid,n.tid=t,{body:JSON.stringify(n),contentType:"application/json"}}function Le(e,t,r,n,o,i){let s=new Headers;for(let h of xe){let m=e.headers.get(h);m&&s.set(h,m)}o&&s.set("content-type",o);let a=e.headers.get("Authorization");a&&s.set("Authorization",a),n&&s.set("X-Auth-Context",n),s.set("X-Gateway-Secret",t),i&&s.set("x-gateway-auth",`Bearer ${i}`),s.set("X-Request-Id",r);let u=e.headers.get("cf-connecting-ip");u&&(s.set("X-Forwarded-For",u),s.set("X-Real-IP",u)),s.set("X-Forwarded-Proto","https");let d=new URL(e.url);s.set("X-Forwarded-Host",d.hostname);let g=e.headers.get("cf-ipcountry");return g&&s.set("X-Cloudflare-Country",g),s}function Ue(e,t,r){let n=new URL(e.url),o=t.BACKEND_URL.endsWith("/")?t.BACKEND_URL.slice(0,-1):t.BACKEND_URL;R(t)&&console.log("[Gateway] Path transform:",JSON.stringify({original:n.pathname,forwarded:r}));let i=new URL(`${o}${r}`);return i.search=n.search,i.toString()}function We(e,t,r){let n=new Headers(e.headers);n.set("X-Request-Id",t);let o=Ce(e.headers);if(o.length>0){n.delete("Set-Cookie");for(let i of o)n.append("Set-Cookie",i);R(r)&&console.log("[Gateway] Forwarding Set-Cookie:",o.length,"cookie(s)")}return new Response(e.body,{status:e.status,statusText:e.statusText,headers:n})}async function J(e,t){let r=crypto.randomUUID();try{let n=new URL(e.url),o=O(n.pathname,t),{scope:i,forwardPath:s}=M(t,o),a=Ue(e,t,s),u,d,g=Z(t);if(g&&q(s)&&e.method==="POST"){let y=await Ne(e,g);y&&(u=y.body,d=y.contentType,R(t)&&console.log("[Gateway] Injected tid:",g,"into",s))}let h,m=Q(t);m&&(h=await ne(m,"bff-worker","auth-craft:backend","cloudflare-auth-gateway"));let T=Le(e,t.BACKEND_GATEWAY_SECRET,r,i,d,h),D=u!==void 0?u:e.body?await e.clone().arrayBuffer():void 0,b=await fetch(a,{method:e.method,headers:T,body:D});return We(b,r,t)}catch(n){console.error("[Gateway] Proxy error:",n);let o=n instanceof Error?n.message:"Unknown error";return p(c(l("BAD_GATEWAY",`Failed to reach backend: ${o}`)()))}}var I=new Map,oe=0,Je=[{match:e=>e==="/auth/authenticate",limit:15,windowMs:6e4,prefix:"login"},{match:e=>e==="/auth/register",limit:8,windowMs:6e4,prefix:"register"},{match:e=>e==="/password/forgot",limit:8,windowMs:3e5,prefix:"forgot"},{match:e=>e.startsWith("/oauth/"),limit:30,windowMs:6e4,prefix:"oauth"},{match:e=>e.startsWith("/auth/complete-"),limit:30,windowMs:6e4,prefix:"complete"},{match:e=>e==="/auth/issue-mfa-challenge",limit:15,windowMs:6e4,prefix:"issue-mfa"}];function be(e){let t=Date.now();if(t-oe<3e4)return;oe=t;let r=t-e;for(let[n,o]of I){let i=o.hits.filter(s=>s>r);i.length===0?I.delete(n):o.hits=i}}function ie(e,t,r){if(r!=="POST")return{allowed:!0,retryAfterSec:0};let n=Je.find(d=>d.match(t));if(!n)return{allowed:!0,retryAfterSec:0};be(3e5);let o=`${n.prefix}:${e}`,i=Date.now(),s=i-n.windowMs,a=I.get(o)??{hits:[]},u=a.hits.filter(d=>d>s);if(u.length>=n.limit){let d=u[0]??i;return a.hits=u,I.set(o,a),{allowed:!1,retryAfterSec:Math.max(1,Math.ceil((d+n.windowMs-i)/1e3))}}return u.push(i),a.hits=u,I.set(o,a),{allowed:!0,retryAfterSec:0}}function se(e,t){if(e.length!==t.length)return!1;let r=0;for(let n=0;n<e.length;n++)r|=e.charCodeAt(n)^t.charCodeAt(n);return r===0}function ce(e,t){let r=K(t);return e&&r.includes(e)?{"Access-Control-Allow-Origin":e,"Access-Control-Allow-Credentials":"true",Vary:"Origin"}:r.includes("*")?{"Access-Control-Allow-Origin":"*"}:{Vary:"Origin"}}function ke(e,t){let r=e.headers.get("Origin")??"*",n=e.headers.get("Access-Control-Request-Headers")??"";return new Response(null,{status:204,headers:{...ce(r,t),"Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS, PATCH","Access-Control-Allow-Headers":n||"Content-Type, Authorization, X-Requested-With, X-Edge-Gate","Access-Control-Max-Age":"86400"}})}function E(e,t,r){let n=t.headers.get("Origin")??"*",o=new Response(e.body,e),i=e.headers.getSetCookie?.()??[];if(i.length>0){o.headers.delete("Set-Cookie");for(let a of i)o.headers.append("Set-Cookie",a)}let s=ce(n,r);for(let[a,u]of Object.entries(s))o.headers.set(a,u);return o.headers.set("Access-Control-Expose-Headers","Content-Length, X-Request-Id, Set-Cookie"),o}function ae(){return new Response(null,{status:404})}var yt={async fetch(e,t,r){try{V(t);let n=new URL(e.url);if(!v(n.pathname,t))return ae();if(e.method==="OPTIONS")return ke(e,t);let o=e.headers.get("X-Edge-Gate");if(!o||!t.EDGE_GATE_SECRET||!se(o,t.EDGE_GATE_SECRET))return R(t)&&console.warn("[Gateway] X-Edge-Gate missing or invalid"),ae();let i=w(t),s=i?`${i}/health`:"/health";if(n.pathname===s)return E(p(S({status:"healthy",timestamp:new Date().toISOString()})),e,t);let a=O(n.pathname,t);R(t)&&console.log("[Gateway] Request:",e.method,n.pathname,"\u2192",a);let u=e.headers.get("cf-connecting-ip")??"unknown",d=ie(u,a,e.method);if(!d.allowed){let A=p(c(P.RATE_LIMIT_EXCEEDED()));return A.headers.set("Retry-After",String(d.retryAfterSec)),E(A,e,t)}let g=j(a,t);if(!g&&z(a))return E(await J(e,t),e,t);let h=e.headers.get("Authorization");if(!h)return E(p(c(f.MISSING_TOKEN())),e,t);let m=h.match(/^Bearer\s+(.+)$/i);if(!m)return E(p(c(f.INVALID_FORMAT())),e,t);let T=m[1];if(T.length>4096)return E(p(c(f.INVALID_FORMAT())),e,t);let D=T.split(".");if(D.length!==3)return E(p(c(f.INVALID_FORMAT())),e,t);if(D[1].length>2048)return E(p(c(f.INVALID_FORMAT())),e,t);let y=re(T);if(!y)return E(p(c(f.INVALID_FORMAT())),e,t);let ue=y.typ,fe=g?3:1;if(ue!==fe)return E(p(c(f.INVALID_TOKEN_TYPE())),e,t);if(g&&y.aud!==Y(t))return E(p(c(f.INVALID_TOKEN_TYPE())),e,t);let k=y.exp;if(typeof k=="number"&&Math.floor(Date.now()/1e3)>=k)return E(p(c(f.TOKEN_EXPIRED())),e,t);if(g){if(!t.SERVICE_JWT_PUBLIC_KEY)return E(p(c(P.INTERNAL_ERROR())),e,t);let A=await W(T,t.SERVICE_JWT_PUBLIC_KEY,3,F(t),$(t));if(!A.ok)return R(t)&&console.error("[Gateway] Service JWT failed:",A.errors),E(p(c(A.errors,A.meta)),e,t)}else{let A=await W(T,t.JWT_PUBLIC_KEY,1,H(t),X(t));if(!A.ok)return R(t)&&console.error("[Gateway] User JWT failed:",A.errors),E(p(c(A.errors,A.meta)),e,t)}return E(await J(e,t),e,t)}catch(n){return R(t)&&console.error("[Gateway] Unhandled error:",n),p(c(P.INTERNAL_ERROR()))}}};export{yt as default};
1
+ var Et=typeof process<"u"&&!1;function _(t){return Et?Object.freeze(t):t}function h(t,e){if(arguments.length===0)return _({ok:!0,data:void 0});let n={ok:!0,data:t};return e!==void 0&&(n.meta=e),_(n)}function a(t,e){let r={ok:!1,errors:_(Array.isArray(t)?[...t]:[t])};return e!==void 0&&(r.meta=e),_(r)}function d(t,e){return n=>{let r={code:t,message:n?.message??e};return n?.field&&(r.field=n.field),r}}function N(t){return t.ok?t.data===void 0?t.meta?{ok:!0,meta:t.meta}:{ok:!0}:t.meta?{ok:!0,data:t.data,meta:t.meta}:{ok:!0,data:t.data}:t.meta?{ok:!1,errors:t.errors,meta:t.meta}:{ok:!1,errors:t.errors}}function M(t){let n=["BACKEND_URL","BACKEND_GATEWAY_SECRET","EDGE_GATE_SECRET","JWT_PUBLIC_KEY","SERVICE_JWT_PUBLIC_KEY"].filter(r=>!t[r]);if(n.length>0)throw new Error(`Missing required environment variables: ${n.join(", ")}`)}function pt(t){return t.ENVIRONMENT||"development"}function S(t){return pt(t)==="development"}function U(t){return t.ALLOWED_ORIGINS?t.ALLOWED_ORIGINS.split(",").map(e=>e.trim()).filter(Boolean):["*"]}function gt(t){return t.ALLOWED_ORIGIN_SUFFIXES?t.ALLOWED_ORIGIN_SUFFIXES.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):[]}function P(t){return`origin:${t}`}async function v(t,e){if(!t)return!1;if(U(e).includes(t))return!0;let r=gt(e);if(r.length>0){let o;try{o=new URL(t).host.toLowerCase()}catch{return!1}if(r.some(i=>o===i.replace(/^\./,"")||o.endsWith(i)))return!0}return!!(e.CORS_KV&&await e.CORS_KV.get(P(t))!==null)}function I(t){let e=t.ROUTE_PREFIX||"";if(!e)return"";let n=e.startsWith("/")?e:`/${e}`;return n.endsWith("/")?n.slice(0,-1):n}function C(t,e){let n=I(e);return n?t===n?"/":t.startsWith(`${n}/`)?t.slice(n.length):t:t}var At=new Set(["system","tenant","customer"]);function L(t){return t&&At.has(t)?t:void 0}function H(t,e){let n=L(t.AUTH_SCOPE);if(n)return{scope:n,forwardPath:e};let r=L(I(t).replace(/^\//,""));if(r)return{scope:r,forwardPath:e};let o=e.split("/").filter(Boolean),i=L(o[0]);if(i){let s=e.slice(e.indexOf(o[0])+o[0].length);return{scope:i,forwardPath:s||"/"}}return{scope:void 0,forwardPath:e}}function F(t,e){let n=I(e);return n?t===n||t.startsWith(`${n}/`):!0}function X(t){if(!t)return new Set(["EdDSA"]);let e=t.split(",").map(n=>n.trim()).filter(Boolean);return e.length>0?new Set(e):new Set(["EdDSA"])}function $(t){return X(t.JWT_ALLOWED_ALGORITHMS)}function Y(t){return X(t.SERVICE_JWT_ALLOWED_ALGORITHMS)}function ht(t){let e=t.SERVICE_ROUTE_PREFIX||"/internal",n=e.startsWith("/")?e:`/${e}`;return n.endsWith("/")?n.slice(0,-1):n}function j(t){return t.SERVICE_JWT_AUDIENCE||"auth-craft:internal"}function z(t){return t.JWT_ISSUER||void 0}function Q(t){return t.SERVICE_JWT_ISSUER||void 0}function Z(t,e){let n=t.split("?")[0].replace(/\/$/,"")||"/",r=ht(e);return n===r||n.startsWith(`${r}/`)}var mt=new Set(["authenticate","login","register","refresh","logout","complete-challenge","complete-mfa-challenge","issue-mfa-challenge","forgot-password","reset-password","verify-email","resend-verification"]),B=new Set(["/verification/resend","/verification/verify","/password/forgot","/password/reset"]);function q(t){let e=t.split("?")[0].replace(/\/$/,"")||"/";if(e==="/"||e.startsWith("/oauth/")||e==="/oauth"||B.has(e))return!0;let n=e.match(/^\/[^/]+(\/(?:verification|password)\/[^/]+)$/);if(n?.[1]&&B.has(n[1]))return!0;let r=e.match(/^(?:\/[^/]+)?\/auth\/([^/]+)/);if(r){let o=r[1];if(o&&mt.has(o)||o?.startsWith("oauth"))return!0}return!1}function tt(t){return t.GATEWAY_JWT_PRIVATE_KEY||void 0}function et(t){return t.TID_VALUE||void 0}function rt(t){let e=t.split("?")[0].replace(/\/$/,"");return e.endsWith("/auth/login")||e.endsWith("/auth/authenticate")}function Rt(){let t=Date.now(),e=new Uint8Array(16);e[0]=t/1099511627776&255,e[1]=t/4294967296&255,e[2]=t/16777216&255,e[3]=t/65536&255,e[4]=t/256&255,e[5]=t&255;let n=new Uint8Array(10);crypto.getRandomValues(n),e.set(n,6),e[6]=e[6]&15|112,e[8]=e[8]&63|128;let r="0123456789ABCDEFGHJKMNPQRSTVWXYZ",o="",i=0,s=0;for(let c=0;c<e.length;c++)for(s=s<<8|e[c],i+=8;i>=5;)o+=r[s>>>i-5&31],i-=5;return i>0&&(o+=r[s<<5-i&31]),o}var w={BAD_REQUEST:d("BAD_REQUEST","Bad request"),UNAUTHORIZED:d("UNAUTHORIZED","Authentication required"),FORBIDDEN:d("FORBIDDEN","Access denied"),NOT_FOUND:d("NOT_FOUND","Resource not found"),RATE_LIMIT_EXCEEDED:d("RATE_LIMIT_EXCEEDED","Too many requests"),INTERNAL_ERROR:d("INTERNAL_ERROR","Internal server error"),BAD_GATEWAY:d("BAD_GATEWAY","Gateway error"),SERVICE_UNAVAILABLE:d("SERVICE_UNAVAILABLE","Service temporarily unavailable"),GATEWAY_TIMEOUT:d("GATEWAY_TIMEOUT","Gateway timeout")},f={INVALID_FORMAT:d("JWT_INVALID_FORMAT","Invalid JWT format"),UNSUPPORTED_ALGORITHM:d("JWT_UNSUPPORTED_ALGORITHM","Unsupported JWT algorithm"),INVALID_TOKEN_TYPE:d("JWT_INVALID_TOKEN_TYPE","Invalid token type: expected access token"),TOKEN_EXPIRED:d("JWT_TOKEN_EXPIRED","JWT token expired"),SIGNATURE_INVALID:d("JWT_SIGNATURE_INVALID","JWT signature verification failed"),MISSING_TOKEN:d("JWT_MISSING_TOKEN","Missing access token"),INVALID_ISSUER:d("JWT_INVALID_ISSUER","Invalid JWT issuer")},nt={BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,RATE_LIMIT_EXCEEDED:429,INTERNAL_ERROR:500,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504,JWT_INVALID_FORMAT:401,JWT_UNSUPPORTED_ALGORITHM:401,JWT_INVALID_TOKEN_TYPE:401,JWT_TOKEN_EXPIRED:401,JWT_SIGNATURE_INVALID:401,JWT_MISSING_TOKEN:401,JWT_INVALID_ISSUER:401};function St(t){if(t.ok)return t.data===void 0?204:200;let e=t.errors?.[0];return e?.code&&e.code in nt?nt[e.code]:500}function l(t,e){let n=new Date().toISOString(),r=e||Rt(),o={...t.meta,timestamp:n,traceId:r},i=t.ok?h(t.data,o):a(t.errors,o),s=St(i),c=N(i);return new Response(JSON.stringify(c),{status:s,headers:{"Content-Type":"application/json"}})}var wt=new Set(["EdDSA","ES256","RS256","PS256"]);function yt(t){return wt.has(t)}function _t(t){switch(t){case"EdDSA":return{name:"Ed25519"};case"ES256":return{name:"ECDSA",namedCurve:"P-256"};case"RS256":return{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"};case"PS256":return{name:"RSA-PSS",hash:"SHA-256"}}}function It(t){switch(t){case"EdDSA":return"Ed25519";case"ES256":return{name:"ECDSA",hash:"SHA-256"};case"RS256":return{name:"RSASSA-PKCS1-v1_5"};case"PS256":return{name:"RSA-PSS",saltLength:32}}}function x(t){let e=t.replace(/-/g,"+").replace(/_/g,"/"),n=e+"==".slice(0,(4-e.length%4)%4),r=atob(n),o=new Uint8Array(r.length);for(let i=0;i<r.length;i++)o[i]=r.charCodeAt(i);return o}function ot(t){return atob(t)}var Ot=/^[0-9a-fA-F]{64}$/;async function Dt(t,e){if(Ot.test(t)){if(e!=="EdDSA")throw new Error(`Hex key format is only supported for EdDSA, got ${e}`);let r=new Uint8Array(32);for(let o=0;o<32;o++)r[o]=parseInt(t.slice(o*2,o*2+2),16);return crypto.subtle.importKey("raw",r,{name:"Ed25519"},!1,["verify"])}let n;try{n=JSON.parse(ot(t))}catch{throw new Error(`Invalid JWK base64 for algorithm ${e}`)}return await crypto.subtle.importKey("jwk",n,_t(e),!1,["verify"])}function it(t){let e=t.split(".");if(e.length!==3)return null;let n=e[1];if(!n)return null;try{let r=x(n);return JSON.parse(new TextDecoder().decode(r))}catch{return null}}function Pt(t){let n=t.split(".")[0];if(!n)return a(f.INVALID_FORMAT());try{let r=x(n);return h(JSON.parse(new TextDecoder().decode(r)))}catch{return a(f.INVALID_FORMAT())}}function Ct(t){let n=t.split(".")[1];if(!n)return a(f.INVALID_FORMAT());try{let r=x(n);return h(JSON.parse(new TextDecoder().decode(r)))}catch{return a(f.INVALID_FORMAT())}}async function xt(t,e,n){let r=t.split("."),o=r[0],i=r[1],s=r[2];if(!o||!i||!s)return!1;let c=new TextEncoder().encode(`${o}.${i}`),u=x(s);try{return await crypto.subtle.verify(It(n),e,u,c)}catch{return!1}}async function b(t,e,n=1,r=new Set(["EdDSA"]),o){let i=Pt(t);if(!i.ok)return a(i.errors,i.meta);let s=i.data.alg;if(!r.has(s))return a(f.UNSUPPORTED_ALGORITHM());if(!yt(s))return a(f.UNSUPPORTED_ALGORITHM());let c=Ct(t);if(!c.ok)return a(c.errors,c.meta);let u=c.data;if(u.typ!==n)return a(f.INVALID_TOKEN_TYPE());if(o!==void 0&&u.iss!==o)return a(f.INVALID_ISSUER());if(u.exp&&Math.floor(Date.now()/1e3)>=u.exp)return a(f.TOKEN_EXPIRED());let E;try{E=await Dt(e,s)}catch{return a(f.INVALID_FORMAT())}return await xt(t,E,s)?h(u):a(f.SIGNATURE_INVALID())}var W=null;async function Nt(t){let e;try{e=JSON.parse(ot(t))}catch{throw new Error("Invalid Gateway JWT private key: expected base64-encoded JWK")}return await crypto.subtle.importKey("jwk",e,{name:"Ed25519"},!1,["sign"])}function J(t){let e="";for(let n=0;n<t.length;n++)e+=String.fromCharCode(t[n]);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function st(t,e,n,r,o=30){W||(W=Nt(t));let i=await W,s=Math.floor(Date.now()/1e3),c={alg:"EdDSA",typ:"JWT"},u={iss:e,aud:n,sub:r,iat:s,exp:s+o,jti:crypto.randomUUID()},E=J(new TextEncoder().encode(JSON.stringify(c))),g=J(new TextEncoder().encode(JSON.stringify(u))),m=new TextEncoder().encode(`${E}.${g}`),R=await crypto.subtle.sign("Ed25519",i,m),T=J(new Uint8Array(R));return`${E}.${g}.${T}`}var Lt=["content-type","accept","accept-encoding","accept-language","user-agent","cookie"];function Ut(t){return t.getSetCookie?.()??[]}async function Wt(t,e){if(!(t.headers.get("content-type")||"").includes("application/json"))return null;let r;try{r=await t.clone().json()}catch{return null}return delete r.tid,r.tid=e,{body:JSON.stringify(r),contentType:"application/json"}}function Jt(t,e,n,r,o,i){let s=new Headers;for(let m of Lt){let R=t.headers.get(m);R&&s.set(m,R)}o&&s.set("content-type",o);let c=t.headers.get("Authorization");c&&s.set("Authorization",c),r&&s.set("X-Auth-Context",r),s.set("X-Gateway-Secret",e),i&&s.set("x-gateway-auth",`Bearer ${i}`),s.set("X-Request-Id",n);let u=t.headers.get("cf-connecting-ip");u&&(s.set("X-Forwarded-For",u),s.set("X-Real-IP",u)),s.set("X-Forwarded-Proto","https");let E=new URL(t.url);s.set("X-Forwarded-Host",E.hostname);let g=t.headers.get("cf-ipcountry");return g&&s.set("X-Cloudflare-Country",g),s}function bt(t,e,n){let r=new URL(t.url),o=e.BACKEND_URL.endsWith("/")?e.BACKEND_URL.slice(0,-1):e.BACKEND_URL;S(e)&&console.log("[Gateway] Path transform:",JSON.stringify({original:r.pathname,forwarded:n}));let i=new URL(`${o}${n}`);return i.search=r.search,i.toString()}function kt(t,e,n){let r=new Headers(t.headers);r.set("X-Request-Id",e);let o=Ut(t.headers);if(o.length>0){r.delete("Set-Cookie");for(let i of o)r.append("Set-Cookie",i);S(n)&&console.log("[Gateway] Forwarding Set-Cookie:",o.length,"cookie(s)")}return new Response(t.body,{status:t.status,statusText:t.statusText,headers:r})}async function k(t,e){let n=crypto.randomUUID();try{let r=new URL(t.url),o=C(r.pathname,e),{scope:i,forwardPath:s}=H(e,o),c=bt(t,e,s),u,E,g=et(e);if(g&&rt(s)&&t.method==="POST"){let y=await Wt(t,g);y&&(u=y.body,E=y.contentType,S(e)&&console.log("[Gateway] Injected tid:",g,"into",s))}let m,R=tt(e);R&&(m=await st(R,"bff-worker","auth-craft:backend","cloudflare-auth-gateway"));let T=Jt(t,e.BACKEND_GATEWAY_SECRET,n,i,E,m),D=u!==void 0?u:t.body?await t.clone().arrayBuffer():void 0,K=await fetch(c,{method:t.method,headers:T,body:D});return kt(K,n,e)}catch(r){console.error("[Gateway] Proxy error:",r);let o=r instanceof Error?r.message:"Unknown error";return l(a(d("BAD_GATEWAY",`Failed to reach backend: ${o}`)()))}}var O=new Map,at=0,Gt=[{match:t=>t==="/auth/authenticate",limit:15,windowMs:6e4,prefix:"login"},{match:t=>t==="/auth/register",limit:8,windowMs:6e4,prefix:"register"},{match:t=>t==="/password/forgot",limit:8,windowMs:3e5,prefix:"forgot"},{match:t=>t.startsWith("/oauth/"),limit:30,windowMs:6e4,prefix:"oauth"},{match:t=>t.startsWith("/auth/complete-"),limit:30,windowMs:6e4,prefix:"complete"},{match:t=>t==="/auth/issue-mfa-challenge",limit:15,windowMs:6e4,prefix:"issue-mfa"}];function Kt(t){let e=Date.now();if(e-at<3e4)return;at=e;let n=e-t;for(let[r,o]of O){let i=o.hits.filter(s=>s>n);i.length===0?O.delete(r):o.hits=i}}function ct(t,e,n){if(n!=="POST")return{allowed:!0,retryAfterSec:0};let r=Gt.find(E=>E.match(e));if(!r)return{allowed:!0,retryAfterSec:0};Kt(3e5);let o=`${r.prefix}:${t}`,i=Date.now(),s=i-r.windowMs,c=O.get(o)??{hits:[]},u=c.hits.filter(E=>E>s);if(u.length>=r.limit){let E=u[0]??i;return c.hits=u,O.set(o,c),{allowed:!1,retryAfterSec:Math.max(1,Math.ceil((E+r.windowMs-i)/1e3))}}return u.push(i),c.hits=u,O.set(o,c),{allowed:!0,retryAfterSec:0}}function ut(t,e){if(t.length!==e.length)return!1;let n=0;for(let r=0;r<t.length;r++)n|=t.charCodeAt(r)^e.charCodeAt(r);return n===0}async function lt(t,e){return t&&await v(t,e)?{"Access-Control-Allow-Origin":t,"Access-Control-Allow-Credentials":"true",Vary:"Origin"}:U(e).includes("*")?{"Access-Control-Allow-Origin":"*"}:{Vary:"Origin"}}async function Vt(t,e){let n=t.headers.get("Origin")??"*",r=t.headers.get("Access-Control-Request-Headers")??"";return new Response(null,{status:204,headers:{...await lt(n,e),"Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS, PATCH","Access-Control-Allow-Headers":r||"Content-Type, Authorization, X-Requested-With, X-Edge-Gate","Access-Control-Max-Age":"86400"}})}async function p(t,e,n){let r=e.headers.get("Origin")??"*",o=new Response(t.body,t),i=t.headers.getSetCookie?.()??[];if(i.length>0){o.headers.delete("Set-Cookie");for(let c of i)o.headers.append("Set-Cookie",c)}let s=await lt(r,n);for(let[c,u]of Object.entries(s))o.headers.set(c,u);return o.headers.set("Access-Control-Expose-Headers","Content-Length, X-Request-Id, Set-Cookie"),o}var Bt="/internal/cors-origins";function Mt(t){return t.split("?")[0].replace(/\/$/,"")===Bt}async function vt(t,e){if(!e.CORS_KV)return G();if(t.method==="GET"){let r=(await e.CORS_KV.list({prefix:"origin:"})).keys.map(o=>o.name.slice(7));return l(h({origins:r}))}if(t.method==="PUT"||t.method==="DELETE"){let n;try{n=(await t.json()).origin}catch{return l(a(w.BAD_REQUEST()))}if(typeof n!="string"||!n)return l(a(w.BAD_REQUEST()));let r;try{if(r=new URL(n).origin,r==="null"||r!==n.replace(/\/$/,""))return l(a(w.BAD_REQUEST()))}catch{return l(a(w.BAD_REQUEST()))}return t.method==="PUT"?await e.CORS_KV.put(P(r),"1"):await e.CORS_KV.delete(P(r)),l(h({origin:r}))}return l(a(w.BAD_REQUEST()))}function G(){return new Response(null,{status:404})}var Pe={async fetch(t,e,n){try{M(e);let r=new URL(t.url);if(!F(r.pathname,e))return G();if(t.method==="OPTIONS")return await Vt(t,e);let o=t.headers.get("X-Edge-Gate");if(!o||!e.EDGE_GATE_SECRET||!ut(o,e.EDGE_GATE_SECRET))return S(e)&&console.warn("[Gateway] X-Edge-Gate missing or invalid"),G();let i=I(e),s=i?`${i}/health`:"/health";if(r.pathname===s)return await p(l(h({status:"healthy",timestamp:new Date().toISOString()})),t,e);let c=C(r.pathname,e);S(e)&&console.log("[Gateway] Request:",t.method,r.pathname,"\u2192",c);let u=t.headers.get("cf-connecting-ip")??"unknown",E=ct(u,c,t.method);if(!E.allowed){let A=l(a(w.RATE_LIMIT_EXCEEDED()));return A.headers.set("Retry-After",String(E.retryAfterSec)),await p(A,t,e)}let g=Z(c,e);if(!g&&q(c))return await p(await k(t,e),t,e);let m=t.headers.get("Authorization");if(!m)return await p(l(a(f.MISSING_TOKEN())),t,e);let R=m.match(/^Bearer\s+(.+)$/i);if(!R)return await p(l(a(f.INVALID_FORMAT())),t,e);let T=R[1];if(T.length>4096)return await p(l(a(f.INVALID_FORMAT())),t,e);let D=T.split(".");if(D.length!==3)return await p(l(a(f.INVALID_FORMAT())),t,e);if(D[1].length>2048)return await p(l(a(f.INVALID_FORMAT())),t,e);let y=it(T);if(!y)return await p(l(a(f.INVALID_FORMAT())),t,e);let ft=y.typ,dt=g?3:1;if(ft!==dt)return await p(l(a(f.INVALID_TOKEN_TYPE())),t,e);if(g&&y.aud!==j(e))return await p(l(a(f.INVALID_TOKEN_TYPE())),t,e);let V=y.exp;if(typeof V=="number"&&Math.floor(Date.now()/1e3)>=V)return await p(l(a(f.TOKEN_EXPIRED())),t,e);if(g){if(!e.SERVICE_JWT_PUBLIC_KEY)return await p(l(a(w.INTERNAL_ERROR())),t,e);let A=await b(T,e.SERVICE_JWT_PUBLIC_KEY,3,Y(e),Q(e));if(!A.ok)return S(e)&&console.error("[Gateway] Service JWT failed:",A.errors),await p(l(a(A.errors,A.meta)),t,e)}else{let A=await b(T,e.JWT_PUBLIC_KEY,1,$(e),z(e));if(!A.ok)return S(e)&&console.error("[Gateway] User JWT failed:",A.errors),await p(l(a(A.errors,A.meta)),t,e)}return g&&Mt(c)?await p(await vt(t,e),t,e):await p(await k(t,e),t,e)}catch(r){return S(e)&&console.error("[Gateway] Unhandled error:",r),l(a(w.INTERNAL_ERROR()))}}};export{Pe as default};