@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 +15 -0
- package/README.md +117 -0
- package/assets/gateway/worker.js +1 -1
- package/assets/lambda/index.mjs +32 -32
- package/lib/common.sh +16 -1
- package/lib/deploy-gateway.sh +54 -4
- package/package.json +1 -1
- package/wrangler/wrangler.toml +5 -0
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
|
package/assets/gateway/worker.js
CHANGED
|
@@ -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};
|