@auth-craft/aws-cf-stack 1.7.0 → 1.8.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 +12 -4
- package/README.md +24 -11
- package/assets/gateway/worker.js +1 -1
- package/bin/deploy.sh +5 -1
- package/cdk/bin/app.ts +27 -20
- package/cdk/cdk.out/asset.530567cf1e6fd5236cd0e483ba02427949a4a68b619db930921f8d3ba9f03689/index.mjs +135 -0
- package/cdk/cdk.out/cdk.out +1 -0
- package/cdk/cdk.out/manifest.json +565 -0
- package/cdk/cdk.out/snapshot-commerce-prod-auth-api.assets.json +36 -0
- package/cdk/cdk.out/snapshot-commerce-prod-auth-api.metadata.json +277 -0
- package/cdk/cdk.out/snapshot-commerce-prod-auth-api.template.json +412 -0
- package/cdk/cdk.out/snapshot-commerce-prod-auth-data.assets.json +21 -0
- package/cdk/cdk.out/snapshot-commerce-prod-auth-data.metadata.json +151 -0
- package/cdk/cdk.out/snapshot-commerce-prod-auth-data.template.json +371 -0
- package/cdk/cdk.out/tree.json +1 -0
- package/cdk/lib/dynamodb-stack.ts +4 -6
- package/cdk/lib/lambda-stack.ts +9 -8
- package/lib/common.sh +24 -14
- package/lib/deploy-gateway.sh +2 -2
- package/lib/deploy-lambda.sh +2 -2
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
# Required vs optional is marked per line.
|
|
12
12
|
|
|
13
13
|
# ─── Stage / project / naming ────────────────────────────────────────────────
|
|
14
|
+
# Resources are named {app}-{env}-{layer}-{component}, e.g. snapshot-commerce-prod-auth-data.
|
|
15
|
+
# app = AUTH_CRAFT_APP_NAME, else auth-craft[-<project>]
|
|
16
|
+
# env = STAGE
|
|
17
|
+
# layer = LAYER (default "auth")
|
|
18
|
+
# component = per-resource: data (table), api (Lambda), system/tenant/customer (workers)
|
|
19
|
+
# The {app}-{env}-{layer} base (no component) is also AUTH_SYSTEM_NAME / the JWT issuer.
|
|
14
20
|
STAGE=dev # optional (flag --stage), default dev | staging | prod
|
|
15
21
|
# also accepts production -> prod, development -> dev
|
|
16
|
-
PROJECT=default # optional (flag --project), default "default"
|
|
17
|
-
AUTH_CRAFT_APP_NAME= # optional (flag --app-name) —
|
|
18
|
-
# (e.g. snapshot-
|
|
19
|
-
#
|
|
22
|
+
PROJECT=default # optional (flag --project), default "default". app = auth-craft-<project>
|
|
23
|
+
AUTH_CRAFT_APP_NAME= # optional (flag --app-name) — full rebrand of the app name
|
|
24
|
+
# (e.g. snapshot-commerce, drops the auth-craft prefix).
|
|
25
|
+
# Unset = auth-craft[-<project>].
|
|
26
|
+
LAYER=auth # optional (flag --layer), default "auth" — layer namespace within
|
|
27
|
+
# the app; use a larger app's other layers (e.g. billing) per deploy.
|
|
20
28
|
|
|
21
29
|
# ─── Tool command overrides (optional — pin your own binaries) ───────────────
|
|
22
30
|
AUTH_CRAFT_WRANGLER_CMD= # optional — e.g. "pnpm wrangler"; default "npx --yes wrangler"
|
package/README.md
CHANGED
|
@@ -28,17 +28,30 @@ process env.
|
|
|
28
28
|
|
|
29
29
|
### Naming convention
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
Pattern: **`{app}-{env}-{layer}-{component}`**.
|
|
32
|
+
|
|
33
|
+
- **`app`** — `auth-craft` by default; `auth-craft-<project>` with `--project`; or a full
|
|
34
|
+
rebrand with `--app-name` / `AUTH_CRAFT_APP_NAME` (e.g. `snapshot-commerce`).
|
|
35
|
+
- **`env`** — `--stage` (`dev|staging|prod`).
|
|
36
|
+
- **`layer`** — `--layer` / `LAYER`, default `auth`. Namespaces this deploy within a larger
|
|
37
|
+
app that has other layers (e.g. `--layer billing`), so the CLI is reused per layer.
|
|
38
|
+
- **`component`** — per-resource: `data` (table), `api` (Lambda), `system`/`tenant`/`customer`
|
|
39
|
+
(gateway workers).
|
|
33
40
|
|
|
34
41
|
| Resource | Name |
|
|
35
42
|
|----------|------|
|
|
36
|
-
| CDK stacks | `<app>-<
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
| Gateway
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
| CDK stacks | `<app>-<env>-<layer>-data`, `<app>-<env>-<layer>-api` |
|
|
44
|
+
| DynamoDB table | `<app>-<env>-<layer>-data` |
|
|
45
|
+
| Lambda fn (+ log group) | `<app>-<env>-<layer>-api` |
|
|
46
|
+
| Gateway workers (`multi`) | `<app>-<env>-<layer>-system` / `-tenant` / `-customer` |
|
|
47
|
+
| Gateway worker (`shared`) | `<app>-<env>-<layer>` |
|
|
48
|
+
| `AUTH_SYSTEM_NAME` / JWT issuer | `<app>-<env>-<layer>` (no component — keeps the `iss` claim stable) |
|
|
49
|
+
|
|
50
|
+
Example (`--app-name snapshot-commerce --stage prod`, default layer):
|
|
51
|
+
`snapshot-commerce-prod-auth-data` (table), `snapshot-commerce-prod-auth-api` (Lambda),
|
|
52
|
+
`snapshot-commerce-prod-auth-system` (worker), issuer `snapshot-commerce-prod-auth`.
|
|
53
|
+
|
|
54
|
+
So `outputs`/`gateway` look up the api stack `<app>-<env>-<layer>-api`. The CDK output keys are
|
|
42
55
|
`LambdaFunctionUrl`, `ApiBasePath`, `AuthSystemName`, `DynamoDBTableName`.
|
|
43
56
|
|
|
44
57
|
### Deploy model (multi vs shared)
|
|
@@ -214,8 +227,8 @@ worker names + a ready-to-paste snippet are in `auth-craft-stack outputs`:
|
|
|
214
227
|
```jsonc
|
|
215
228
|
// auth-craft-stack outputs →
|
|
216
229
|
{
|
|
217
|
-
"workerNames": { "system": "auth-craft-prod-system", "tenant": "...", "customer": "auth-craft-prod-customer" },
|
|
218
|
-
"pagesProxy": { "serviceBindingExample": "[[services]]\nbinding = \"AUTH_GATEWAY\"\nservice = \"auth-craft-prod-customer\"\n" }
|
|
230
|
+
"workerNames": { "system": "auth-craft-prod-auth-system", "tenant": "...", "customer": "auth-craft-prod-auth-customer" },
|
|
231
|
+
"pagesProxy": { "serviceBindingExample": "[[services]]\nbinding = \"AUTH_GATEWAY\"\nservice = \"auth-craft-prod-auth-customer\"\n" }
|
|
219
232
|
}
|
|
220
233
|
```
|
|
221
234
|
|
|
@@ -225,7 +238,7 @@ In the Pages app:
|
|
|
225
238
|
# wrangler.toml (your Pages project) — bind the gateway worker for the scope this app serves
|
|
226
239
|
[[services]]
|
|
227
240
|
binding = "AUTH_GATEWAY"
|
|
228
|
-
service = "auth-craft-prod-customer" # from outputs.workerNames.<scope>
|
|
241
|
+
service = "auth-craft-prod-auth-customer" # from outputs.workerNames.<scope>
|
|
229
242
|
```
|
|
230
243
|
|
|
231
244
|
```ts
|
package/assets/gateway/worker.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var pt=typeof process<"u"&&!1;function y(t){return pt?Object.freeze(t):t}function m(t,e){if(arguments.length===0)return y({ok:!0,data:void 0});let n={ok:!0,data:t};return e!==void 0&&(n.meta=e),y(n)}function a(t,e){let r={ok:!1,errors:y(Array.isArray(t)?[...t]:[t])};return e!==void 0&&(r.meta=e),y(r)}function E(t,e){return n=>{let r={code:t,message:n?.message??e};return n?.field&&(r.field=n.field),r}}function U(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 F(t){let e=["BACKEND_URL","BACKEND_GATEWAY_SECRET","JWT_PUBLIC_KEY","SERVICE_JWT_PUBLIC_KEY"];J(t)||e.push("EDGE_GATE_SECRET");let n=e.filter(r=>!t[r]);if(n.length>0)throw new Error(`Missing required environment variables: ${n.join(", ")}`)}function J(t){return t.SKIP_EDGE_GATE==="true"}function gt(t){return t.ENVIRONMENT||"development"}function S(t){return gt(t)==="development"}function k(t){return t.ALLOWED_ORIGINS?t.ALLOWED_ORIGINS.split(",").map(e=>e.trim()).filter(Boolean):["*"]}function At(t){return t.ALLOWED_ORIGIN_SUFFIXES?t.ALLOWED_ORIGIN_SUFFIXES.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):[]}function D(t){return`origin:${t}`}async function $(t,e){if(!t)return!1;if(k(e).includes(t))return!0;let r=At(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(D(t))!==null)}function O(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=O(e);return n?t===n?"/":t.startsWith(`${n}/`)?t.slice(n.length):t:t}var ht=new Set(["system","tenant","customer"]);function W(t){return t&&ht.has(t)?t:void 0}function x(t,e){let n=W(t.AUTH_SCOPE);if(n)return{scope:n,forwardPath:e};let r=W(O(t).replace(/^\//,""));if(r)return{scope:r,forwardPath:e};let o=e.split("/").filter(Boolean),i=W(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 X(t,e){let n=O(e);return n?t===n||t.startsWith(`${n}/`):!0}function Y(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 j(t){return Y(t.JWT_ALLOWED_ALGORITHMS)}function z(t){return Y(t.SERVICE_JWT_ALLOWED_ALGORITHMS)}function mt(t){let e=t.SERVICE_ROUTE_PREFIX||"/internal",n=e.startsWith("/")?e:`/${e}`;return n.endsWith("/")?n.slice(0,-1):n}function Q(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 tt(t,e){let n=t.split("?")[0].replace(/\/$/,"")||"/",r=mt(e);return n===r||n.startsWith(`${r}/`)}var St=new Set(["authenticate","login","register","refresh","logout","complete-challenge","complete-mfa-challenge","issue-mfa-challenge","forgot-password","reset-password","verify-email","resend-verification"]),v=new Set(["/verification/resend","/verification/verify","/password/forgot","/password/reset"]);function et(t){let e=t.split("?")[0].replace(/\/$/,"")||"/";if(e==="/"||e.startsWith("/oauth/")||e==="/oauth"||v.has(e))return!0;let n=e.match(/^\/[^/]+(\/(?:verification|password)\/[^/]+)$/);if(n?.[1]&&v.has(n[1]))return!0;let r=e.match(/^(?:\/[^/]+)?\/auth\/([^/]+)/);if(r){let o=r[1];if(o&&St.has(o)||o?.startsWith("oauth"))return!0}return!1}function rt(t){return t.GATEWAY_JWT_PRIVATE_KEY||void 0}function nt(t){return t.TID_VALUE||void 0}function ot(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 T={BAD_REQUEST:E("BAD_REQUEST","Bad request"),UNAUTHORIZED:E("UNAUTHORIZED","Authentication required"),FORBIDDEN:E("FORBIDDEN","Access denied"),NOT_FOUND:E("NOT_FOUND","Resource not found"),RATE_LIMIT_EXCEEDED:E("RATE_LIMIT_EXCEEDED","Too many requests"),INTERNAL_ERROR:E("INTERNAL_ERROR","Internal server error"),BAD_GATEWAY:E("BAD_GATEWAY","Gateway error"),SERVICE_UNAVAILABLE:E("SERVICE_UNAVAILABLE","Service temporarily unavailable"),GATEWAY_TIMEOUT:E("GATEWAY_TIMEOUT","Gateway timeout")},l={INVALID_FORMAT:E("JWT_INVALID_FORMAT","Invalid JWT format"),UNSUPPORTED_ALGORITHM:E("JWT_UNSUPPORTED_ALGORITHM","Unsupported JWT algorithm"),INVALID_TOKEN_TYPE:E("JWT_INVALID_TOKEN_TYPE","Invalid token type: expected access token"),TOKEN_EXPIRED:E("JWT_TOKEN_EXPIRED","JWT token expired"),SIGNATURE_INVALID:E("JWT_SIGNATURE_INVALID","JWT signature verification failed"),MISSING_TOKEN:E("JWT_MISSING_TOKEN","Missing access token"),INVALID_ISSUER:E("JWT_INVALID_ISSUER","Invalid JWT issuer"),SCOPE_MISMATCH:E("JWT_SCOPE_MISMATCH","Token not valid for this scope")},it={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,JWT_SCOPE_MISMATCH:403};function Tt(t){if(t.ok)return t.data===void 0?204:200;let e=t.errors?.[0];return e?.code&&e.code in it?it[e.code]:500}function f(t,e){let n=new Date().toISOString(),r=e||Rt(),o={...t.meta,timestamp:n,traceId:r},i=t.ok?m(t.data,o):a(t.errors,o),s=Tt(i),c=U(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 It(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 Ot(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 N(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 st(t){return atob(t)}var Pt=/^[0-9a-fA-F]{64}$/;async function Dt(t,e){if(Pt.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(st(t))}catch{throw new Error(`Invalid JWK base64 for algorithm ${e}`)}return await crypto.subtle.importKey("jwk",n,It(e),!1,["verify"])}function at(t){let e=t.split(".");if(e.length!==3)return null;let n=e[1];if(!n)return null;try{let r=N(n);return JSON.parse(new TextDecoder().decode(r))}catch{return null}}function Ct(t){let n=t.split(".")[0];if(!n)return a(l.INVALID_FORMAT());try{let r=N(n);return m(JSON.parse(new TextDecoder().decode(r)))}catch{return a(l.INVALID_FORMAT())}}function xt(t){let n=t.split(".")[1];if(!n)return a(l.INVALID_FORMAT());try{let r=N(n);return m(JSON.parse(new TextDecoder().decode(r)))}catch{return a(l.INVALID_FORMAT())}}async function Nt(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=N(s);try{return await crypto.subtle.verify(Ot(n),e,u,c)}catch{return!1}}async function K(t,e,n=1,r=new Set(["EdDSA"]),o){let i=Ct(t);if(!i.ok)return a(i.errors,i.meta);let s=i.data.alg;if(!r.has(s))return a(l.UNSUPPORTED_ALGORITHM());if(!yt(s))return a(l.UNSUPPORTED_ALGORITHM());let c=xt(t);if(!c.ok)return a(c.errors,c.meta);let u=c.data;if(u.typ!==n)return a(l.INVALID_TOKEN_TYPE());if(o!==void 0&&u.iss!==o)return a(l.INVALID_ISSUER());if(u.exp&&Math.floor(Date.now()/1e3)>=u.exp)return a(l.TOKEN_EXPIRED());let d;try{d=await Dt(e,s)}catch{return a(l.INVALID_FORMAT())}return await Nt(t,d,s)?m(u):a(l.SIGNATURE_INVALID())}var b=null;async function Lt(t){let e;try{e=JSON.parse(st(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 G(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 ct(t,e,n,r,o=30){b||(b=Lt(t));let i=await b,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()},d=G(new TextEncoder().encode(JSON.stringify(c))),A=G(new TextEncoder().encode(JSON.stringify(u))),R=new TextEncoder().encode(`${d}.${A}`),h=await crypto.subtle.sign("Ed25519",i,R),_=G(new Uint8Array(h));return`${d}.${A}.${_}`}var Ut=["content-type","accept","accept-encoding","accept-language","user-agent","cookie"];function Wt(t){return t.getSetCookie?.()??[]}async function Jt(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 kt(t,e,n,r,o,i){let s=new Headers;for(let R of Ut){let h=t.headers.get(R);h&&s.set(R,h)}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 d=new URL(t.url);s.set("X-Forwarded-Host",d.hostname);let A=t.headers.get("cf-ipcountry");return A&&s.set("X-Cloudflare-Country",A),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 Gt(t,e,n){let r=new Headers(t.headers);r.set("X-Request-Id",e);let o=Wt(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 V(t,e){let n=crypto.randomUUID();try{let r=new URL(t.url),o=C(r.pathname,e),{scope:i,forwardPath:s}=x(e,o),c=bt(t,e,s),u,d,A=nt(e);if(A&&ot(s)&&t.method==="POST"){let I=await Jt(t,A);I&&(u=I.body,d=I.contentType,S(e)&&console.log("[Gateway] Injected tid:",A,"into",s))}let R,h=rt(e);h&&(R=await ct(h,"bff-worker","auth-craft:backend","cloudflare-auth-gateway"));let _=kt(t,e.BACKEND_GATEWAY_SECRET,n,i,d,R),B=u!==void 0?u:t.body?await t.clone().arrayBuffer():void 0,w=await fetch(c,{method:t.method,headers:_,body:B});return Gt(w,n,e)}catch(r){console.error("[Gateway] Proxy error:",r);let o=r instanceof Error?r.message:"Unknown error";return f(a(E("BAD_GATEWAY",`Failed to reach backend: ${o}`)()))}}var P=new Map,ut=0,Kt=[{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 Vt(t){let e=Date.now();if(e-ut<3e4)return;ut=e;let n=e-t;for(let[r,o]of P){let i=o.hits.filter(s=>s>n);i.length===0?P.delete(r):o.hits=i}}function ft(t,e,n){if(n!=="POST")return{allowed:!0,retryAfterSec:0};let r=Kt.find(d=>d.match(e));if(!r)return{allowed:!0,retryAfterSec:0};Vt(3e5);let o=`${r.prefix}:${t}`,i=Date.now(),s=i-r.windowMs,c=P.get(o)??{hits:[]},u=c.hits.filter(d=>d>s);if(u.length>=r.limit){let d=u[0]??i;return c.hits=u,P.set(o,c),{allowed:!1,retryAfterSec:Math.max(1,Math.ceil((d+r.windowMs-i)/1e3))}}return u.push(i),c.hits=u,P.set(o,c),{allowed:!0,retryAfterSec:0}}function lt(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 dt(t,e){return t&&await $(t,e)?{"Access-Control-Allow-Origin":t,"Access-Control-Allow-Credentials":"true",Vary:"Origin"}:k(e).includes("*")?{"Access-Control-Allow-Origin":"*"}:{Vary:"Origin"}}async function Mt(t,e){let n=t.headers.get("Origin")??"*",r=t.headers.get("Access-Control-Request-Headers")??"";return new Response(null,{status:204,headers:{...await dt(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 dt(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 Ht(t){return t.split("?")[0].replace(/\/$/,"")===Bt}async function vt(t,e){if(!e.CORS_KV)return M();if(t.method==="GET"){let r=(await e.CORS_KV.list({prefix:"origin:"})).keys.map(o=>o.name.slice(7));return f(m({origins:r}))}if(t.method==="PUT"||t.method==="DELETE"){let n;try{n=(await t.json()).origin}catch{return f(a(T.BAD_REQUEST()))}if(typeof n!="string"||!n)return f(a(T.BAD_REQUEST()));let r;try{if(r=new URL(n).origin,r==="null"||r!==n.replace(/\/$/,""))return f(a(T.BAD_REQUEST()))}catch{return f(a(T.BAD_REQUEST()))}return t.method==="PUT"?await e.CORS_KV.put(D(r),"1"):await e.CORS_KV.delete(D(r)),f(m({origin:r}))}return f(a(T.BAD_REQUEST()))}function M(){return new Response(null,{status:404})}var Ce={async fetch(t,e,n){try{F(e);let r=new URL(t.url);if(!X(r.pathname,e))return M();if(t.method==="OPTIONS")return await Mt(t,e);if(!J(e)){let g=t.headers.get("X-Edge-Gate");if(!g||!e.EDGE_GATE_SECRET||!lt(g,e.EDGE_GATE_SECRET))return S(e)&&console.warn("[Gateway] X-Edge-Gate missing or invalid"),M()}let o=O(e),i=o?`${o}/health`:"/health";if(r.pathname===i)return await p(f(m({status:"healthy",timestamp:new Date().toISOString()})),t,e);let s=C(r.pathname,e);S(e)&&console.log("[Gateway] Request:",t.method,r.pathname,"\u2192",s);let c=t.headers.get("cf-connecting-ip")??"unknown",u=ft(c,s,t.method);if(!u.allowed){let g=f(a(T.RATE_LIMIT_EXCEEDED()));return g.headers.set("Retry-After",String(u.retryAfterSec)),await p(g,t,e)}let d=tt(s,e);if(!d&&et(s))return await p(await V(t,e),t,e);let A=t.headers.get("Authorization");if(!A)return await p(f(a(l.MISSING_TOKEN())),t,e);let R=A.match(/^Bearer\s+(.+)$/i);if(!R)return await p(f(a(l.INVALID_FORMAT())),t,e);let h=R[1];if(h.length>4096)return await p(f(a(l.INVALID_FORMAT())),t,e);let _=h.split(".");if(_.length!==3)return await p(f(a(l.INVALID_FORMAT())),t,e);if(_[1].length>2048)return await p(f(a(l.INVALID_FORMAT())),t,e);let w=at(h);if(!w)return await p(f(a(l.INVALID_FORMAT())),t,e);let I=w.typ,Et=d?3:1;if(I!==Et)return await p(f(a(l.INVALID_TOKEN_TYPE())),t,e);if(d&&w.aud!==Q(e))return await p(f(a(l.INVALID_TOKEN_TYPE())),t,e);let H=w.exp;if(typeof H=="number"&&Math.floor(Date.now()/1e3)>=H)return await p(f(a(l.TOKEN_EXPIRED())),t,e);if(d){if(!e.SERVICE_JWT_PUBLIC_KEY)return await p(f(a(T.INTERNAL_ERROR())),t,e);let g=await K(h,e.SERVICE_JWT_PUBLIC_KEY,3,z(e),q(e));if(!g.ok)return S(e)&&console.error("[Gateway] Service JWT failed:",g.errors),await p(f(a(g.errors,g.meta)),t,e)}else{let g=await K(h,e.JWT_PUBLIC_KEY,1,j(e),Z(e));if(!g.ok)return S(e)&&console.error("[Gateway] User JWT failed:",g.errors),await p(f(a(g.errors,g.meta)),t,e);let{scope:L}=x(e,s);if(L&&g.data.aud!==L)return S(e)&&console.error(`[Gateway] Scope mismatch: token aud="${g.data.aud}" scope="${L}"`),await p(f(a(l.SCOPE_MISMATCH())),t,e)}return d&&Ht(s)?await p(await vt(t,e),t,e):await p(await V(t,e),t,e)}catch(r){return S(e)&&console.error("[Gateway] Unhandled error:",r),f(a(T.INTERNAL_ERROR()))}}};export{Ce as default};
|
|
1
|
+
var gt=typeof process<"u"&&!1;function y(t){return gt?Object.freeze(t):t}function m(t,e){if(arguments.length===0)return y({ok:!0,data:void 0});let r={ok:!0,data:t};return e!==void 0&&(r.meta=e),y(r)}function a(t,e){let n={ok:!1,errors:y(Array.isArray(t)?[...t]:[t])};return e!==void 0&&(n.meta=e),y(n)}function E(t,e){return r=>{let n={code:t,message:r?.message??e};return r?.field&&(n.field=r.field),n}}function U(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 F(t){let e=["BACKEND_URL","BACKEND_GATEWAY_SECRET","JWT_PUBLIC_KEY","SERVICE_JWT_PUBLIC_KEY"];J(t)||e.push("EDGE_GATE_SECRET");let r=e.filter(n=>!t[n]);if(r.length>0)throw new Error(`Missing required environment variables: ${r.join(", ")}`)}function J(t){return t.SKIP_EDGE_GATE==="true"}function At(t){return t.ENVIRONMENT||"development"}function S(t){return At(t)==="development"}function k(t){return t.ALLOWED_ORIGINS?t.ALLOWED_ORIGINS.split(",").map(e=>e.trim()).filter(Boolean):["*"]}function ht(t){return t.ALLOWED_ORIGIN_SUFFIXES?t.ALLOWED_ORIGIN_SUFFIXES.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):[]}function C(t){return`origin:${t}`}async function $(t,e){if(!t)return!1;if(k(e).includes(t))return!0;let n=ht(e);if(n.length>0){let o;try{o=new URL(t).host.toLowerCase()}catch{return!1}if(n.some(i=>o===i.replace(/^\./,"")||o.endsWith(i)))return!0}return!!(e.CORS_KV&&await e.CORS_KV.get(C(t))!==null)}function O(t){let e=t.ROUTE_PREFIX||"";if(!e)return"";let r=e.startsWith("/")?e:`/${e}`;return r.endsWith("/")?r.slice(0,-1):r}function D(t,e){let r=O(e);return r?t===r?"/":t.startsWith(`${r}/`)?t.slice(r.length):t:t}var mt=new Set(["system","tenant","customer"]);function W(t){return t&&mt.has(t)?t:void 0}function x(t,e){let r=W(t.AUTH_SCOPE);if(r)return{scope:r,forwardPath:e};let n=W(O(t).replace(/^\//,""));if(n)return{scope:n,forwardPath:e};let o=e.split("/").filter(Boolean),i=W(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 X(t,e){let r=O(e);return r?t===r||t.startsWith(`${r}/`):!0}function Y(t){if(!t)return new Set(["EdDSA"]);let e=t.split(",").map(r=>r.trim()).filter(Boolean);return e.length>0?new Set(e):new Set(["EdDSA"])}function j(t){return Y(t.JWT_ALLOWED_ALGORITHMS)}function z(t){return Y(t.SERVICE_JWT_ALLOWED_ALGORITHMS)}function St(t){let e=t.SERVICE_ROUTE_PREFIX||"/internal",r=e.startsWith("/")?e:`/${e}`;return r.endsWith("/")?r.slice(0,-1):r}function Q(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 tt(t,e){let r=t.split("?")[0].replace(/\/$/,"")||"/",n=St(e);return r===n||r.startsWith(`${n}/`)}var Rt=new Set(["authenticate","login","register","refresh","logout","complete-challenge","complete-mfa-challenge","issue-mfa-challenge","forgot-password","reset-password","verify-email","resend-verification"]),v=new Set(["/verification/resend","/verification/verify","/password/forgot","/password/reset"]),Tt=new Set(["select-tenant"]);function et(t){let r=(t.split("?")[0].replace(/\/$/,"")||"/").match(/^(?:\/[^/]+)?\/auth\/([^/]+)/);return!!r?.[1]&&Tt.has(r[1])}function rt(t){let e=t.split("?")[0].replace(/\/$/,"")||"/";if(e==="/"||e.startsWith("/oauth/")||e==="/oauth"||v.has(e))return!0;let r=e.match(/^\/[^/]+(\/(?:verification|password)\/[^/]+)$/);if(r?.[1]&&v.has(r[1]))return!0;let n=e.match(/^(?:\/[^/]+)?\/auth\/([^/]+)/);if(n){let o=n[1];if(o&&Rt.has(o)||o?.startsWith("oauth"))return!0}return!1}function nt(t){return t.GATEWAY_JWT_PRIVATE_KEY||void 0}function ot(t){return t.TID_VALUE||void 0}function it(t){let e=t.split("?")[0].replace(/\/$/,"");return e.endsWith("/auth/login")||e.endsWith("/auth/authenticate")}function _t(){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 r=new Uint8Array(10);crypto.getRandomValues(r),e.set(r,6),e[6]=e[6]&15|112,e[8]=e[8]&63|128;let n="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+=n[s>>>i-5&31],i-=5;return i>0&&(o+=n[s<<5-i&31]),o}var T={BAD_REQUEST:E("BAD_REQUEST","Bad request"),UNAUTHORIZED:E("UNAUTHORIZED","Authentication required"),FORBIDDEN:E("FORBIDDEN","Access denied"),NOT_FOUND:E("NOT_FOUND","Resource not found"),RATE_LIMIT_EXCEEDED:E("RATE_LIMIT_EXCEEDED","Too many requests"),INTERNAL_ERROR:E("INTERNAL_ERROR","Internal server error"),BAD_GATEWAY:E("BAD_GATEWAY","Gateway error"),SERVICE_UNAVAILABLE:E("SERVICE_UNAVAILABLE","Service temporarily unavailable"),GATEWAY_TIMEOUT:E("GATEWAY_TIMEOUT","Gateway timeout")},f={INVALID_FORMAT:E("JWT_INVALID_FORMAT","Invalid JWT format"),UNSUPPORTED_ALGORITHM:E("JWT_UNSUPPORTED_ALGORITHM","Unsupported JWT algorithm"),INVALID_TOKEN_TYPE:E("JWT_INVALID_TOKEN_TYPE","Invalid token type: expected access token"),TOKEN_EXPIRED:E("JWT_TOKEN_EXPIRED","JWT token expired"),SIGNATURE_INVALID:E("JWT_SIGNATURE_INVALID","JWT signature verification failed"),MISSING_TOKEN:E("JWT_MISSING_TOKEN","Missing access token"),INVALID_ISSUER:E("JWT_INVALID_ISSUER","Invalid JWT issuer"),SCOPE_MISMATCH:E("JWT_SCOPE_MISMATCH","Token not valid for this scope")},st={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,JWT_SCOPE_MISMATCH:403};function wt(t){if(t.ok)return t.data===void 0?204:200;let e=t.errors?.[0];return e?.code&&e.code in st?st[e.code]:500}function l(t,e){let r=new Date().toISOString(),n=e||_t(),o={...t.meta,timestamp:r,traceId:n},i=t.ok?m(t.data,o):a(t.errors,o),s=wt(i),c=U(i);return new Response(JSON.stringify(c),{status:s,headers:{"Content-Type":"application/json"}})}var It=new Set(["EdDSA","ES256","RS256","PS256"]);function Ot(t){return It.has(t)}function Pt(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 Ct(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 N(t){let e=t.replace(/-/g,"+").replace(/_/g,"/"),r=e+"==".slice(0,(4-e.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 at(t){return atob(t)}var Dt=/^[0-9a-fA-F]{64}$/;async function xt(t,e){if(Dt.test(t)){if(e!=="EdDSA")throw new Error(`Hex key format is only supported for EdDSA, got ${e}`);let n=new Uint8Array(32);for(let o=0;o<32;o++)n[o]=parseInt(t.slice(o*2,o*2+2),16);return crypto.subtle.importKey("raw",n,{name:"Ed25519"},!1,["verify"])}let r;try{r=JSON.parse(at(t))}catch{throw new Error(`Invalid JWK base64 for algorithm ${e}`)}return await crypto.subtle.importKey("jwk",r,Pt(e),!1,["verify"])}function ct(t){let e=t.split(".");if(e.length!==3)return null;let r=e[1];if(!r)return null;try{let n=N(r);return JSON.parse(new TextDecoder().decode(n))}catch{return null}}function Nt(t){let r=t.split(".")[0];if(!r)return a(f.INVALID_FORMAT());try{let n=N(r);return m(JSON.parse(new TextDecoder().decode(n)))}catch{return a(f.INVALID_FORMAT())}}function Lt(t){let r=t.split(".")[1];if(!r)return a(f.INVALID_FORMAT());try{let n=N(r);return m(JSON.parse(new TextDecoder().decode(n)))}catch{return a(f.INVALID_FORMAT())}}async function Ut(t,e,r){let n=t.split("."),o=n[0],i=n[1],s=n[2];if(!o||!i||!s)return!1;let c=new TextEncoder().encode(`${o}.${i}`),u=N(s);try{return await crypto.subtle.verify(Ct(r),e,u,c)}catch{return!1}}async function K(t,e,r=1,n=new Set(["EdDSA"]),o){let i=Nt(t);if(!i.ok)return a(i.errors,i.meta);let s=i.data.alg;if(!n.has(s))return a(f.UNSUPPORTED_ALGORITHM());if(!Ot(s))return a(f.UNSUPPORTED_ALGORITHM());let c=Lt(t);if(!c.ok)return a(c.errors,c.meta);let u=c.data;if(u.typ!==r)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 d;try{d=await xt(e,s)}catch{return a(f.INVALID_FORMAT())}return await Ut(t,d,s)?m(u):a(f.SIGNATURE_INVALID())}var b=null;async function Wt(t){let e;try{e=JSON.parse(at(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 G(t){let e="";for(let r=0;r<t.length;r++)e+=String.fromCharCode(t[r]);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function ut(t,e,r,n,o=30){b||(b=Wt(t));let i=await b,s=Math.floor(Date.now()/1e3),c={alg:"EdDSA",typ:"JWT"},u={iss:e,aud:r,sub:n,iat:s,exp:s+o,jti:crypto.randomUUID()},d=G(new TextEncoder().encode(JSON.stringify(c))),A=G(new TextEncoder().encode(JSON.stringify(u))),R=new TextEncoder().encode(`${d}.${A}`),h=await crypto.subtle.sign("Ed25519",i,R),_=G(new Uint8Array(h));return`${d}.${A}.${_}`}var Jt=["content-type","accept","accept-encoding","accept-language","user-agent","cookie"];function kt(t){return t.getSetCookie?.()??[]}async function bt(t,e){if(!(t.headers.get("content-type")||"").includes("application/json"))return null;let n;try{n=await t.clone().json()}catch{return null}return delete n.tid,n.tid=e,{body:JSON.stringify(n),contentType:"application/json"}}function Gt(t,e,r,n,o,i){let s=new Headers;for(let R of Jt){let h=t.headers.get(R);h&&s.set(R,h)}o&&s.set("content-type",o);let c=t.headers.get("Authorization");c&&s.set("Authorization",c),n&&s.set("X-Auth-Context",n),s.set("X-Gateway-Secret",e),i&&s.set("x-gateway-auth",`Bearer ${i}`),s.set("X-Request-Id",r);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 d=new URL(t.url);s.set("X-Forwarded-Host",d.hostname);let A=t.headers.get("cf-ipcountry");return A&&s.set("X-Cloudflare-Country",A),s}function Kt(t,e,r){let n=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:n.pathname,forwarded:r}));let i=new URL(`${o}${r}`);return i.search=n.search,i.toString()}function Vt(t,e,r){let n=new Headers(t.headers);n.set("X-Request-Id",e);let o=kt(t.headers);if(o.length>0){n.delete("Set-Cookie");for(let i of o)n.append("Set-Cookie",i);S(r)&&console.log("[Gateway] Forwarding Set-Cookie:",o.length,"cookie(s)")}return new Response(t.body,{status:t.status,statusText:t.statusText,headers:n})}async function V(t,e){let r=crypto.randomUUID();try{let n=new URL(t.url),o=D(n.pathname,e),{scope:i,forwardPath:s}=x(e,o),c=Kt(t,e,s),u,d,A=ot(e);if(A&&it(s)&&t.method==="POST"){let I=await bt(t,A);I&&(u=I.body,d=I.contentType,S(e)&&console.log("[Gateway] Injected tid:",A,"into",s))}let R,h=nt(e);h&&(R=await ut(h,"bff-worker","auth-craft:backend","cloudflare-auth-gateway"));let _=Gt(t,e.BACKEND_GATEWAY_SECRET,r,i,d,R),B=u!==void 0?u:t.body?await t.clone().arrayBuffer():void 0,w=await fetch(c,{method:t.method,headers:_,body:B});return Vt(w,r,e)}catch(n){console.error("[Gateway] Proxy error:",n);let o=n instanceof Error?n.message:"Unknown error";return l(a(E("BAD_GATEWAY",`Failed to reach backend: ${o}`)()))}}var P=new Map,lt=0,Mt=[{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 Bt(t){let e=Date.now();if(e-lt<3e4)return;lt=e;let r=e-t;for(let[n,o]of P){let i=o.hits.filter(s=>s>r);i.length===0?P.delete(n):o.hits=i}}function ft(t,e,r){if(r!=="POST")return{allowed:!0,retryAfterSec:0};let n=Mt.find(d=>d.match(e));if(!n)return{allowed:!0,retryAfterSec:0};Bt(3e5);let o=`${n.prefix}:${t}`,i=Date.now(),s=i-n.windowMs,c=P.get(o)??{hits:[]},u=c.hits.filter(d=>d>s);if(u.length>=n.limit){let d=u[0]??i;return c.hits=u,P.set(o,c),{allowed:!1,retryAfterSec:Math.max(1,Math.ceil((d+n.windowMs-i)/1e3))}}return u.push(i),c.hits=u,P.set(o,c),{allowed:!0,retryAfterSec:0}}function dt(t,e){if(t.length!==e.length)return!1;let r=0;for(let n=0;n<t.length;n++)r|=t.charCodeAt(n)^e.charCodeAt(n);return r===0}async function Et(t,e){return t&&await $(t,e)?{"Access-Control-Allow-Origin":t,"Access-Control-Allow-Credentials":"true",Vary:"Origin"}:k(e).includes("*")?{"Access-Control-Allow-Origin":"*"}:{Vary:"Origin"}}async function Ht(t,e){let r=t.headers.get("Origin")??"*",n=t.headers.get("Access-Control-Request-Headers")??"";return new Response(null,{status:204,headers:{...await Et(r,e),"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"}})}async function p(t,e,r){let n=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 Et(n,r);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 vt="/internal/cors-origins";function Ft(t){return t.split("?")[0].replace(/\/$/,"")===vt}async function $t(t,e){if(!e.CORS_KV)return M();if(t.method==="GET"){let n=(await e.CORS_KV.list({prefix:"origin:"})).keys.map(o=>o.name.slice(7));return l(m({origins:n}))}if(t.method==="PUT"||t.method==="DELETE"){let r;try{r=(await t.json()).origin}catch{return l(a(T.BAD_REQUEST()))}if(typeof r!="string"||!r)return l(a(T.BAD_REQUEST()));let n;try{if(n=new URL(r).origin,n==="null"||n!==r.replace(/\/$/,""))return l(a(T.BAD_REQUEST()))}catch{return l(a(T.BAD_REQUEST()))}return t.method==="PUT"?await e.CORS_KV.put(C(n),"1"):await e.CORS_KV.delete(C(n)),l(m({origin:n}))}return l(a(T.BAD_REQUEST()))}function M(){return new Response(null,{status:404})}var Ne={async fetch(t,e,r){try{F(e);let n=new URL(t.url);if(!X(n.pathname,e))return M();if(t.method==="OPTIONS")return await Ht(t,e);if(!J(e)){let g=t.headers.get("X-Edge-Gate");if(!g||!e.EDGE_GATE_SECRET||!dt(g,e.EDGE_GATE_SECRET))return S(e)&&console.warn("[Gateway] X-Edge-Gate missing or invalid"),M()}let o=O(e),i=o?`${o}/health`:"/health";if(n.pathname===i)return await p(l(m({status:"healthy",timestamp:new Date().toISOString()})),t,e);let s=D(n.pathname,e);S(e)&&console.log("[Gateway] Request:",t.method,n.pathname,"\u2192",s);let c=t.headers.get("cf-connecting-ip")??"unknown",u=ft(c,s,t.method);if(!u.allowed){let g=l(a(T.RATE_LIMIT_EXCEEDED()));return g.headers.set("Retry-After",String(u.retryAfterSec)),await p(g,t,e)}let d=tt(s,e);if(!d&&rt(s))return await p(await V(t,e),t,e);let A=t.headers.get("Authorization");if(!A)return await p(l(a(f.MISSING_TOKEN())),t,e);let R=A.match(/^Bearer\s+(.+)$/i);if(!R)return await p(l(a(f.INVALID_FORMAT())),t,e);let h=R[1];if(h.length>4096)return await p(l(a(f.INVALID_FORMAT())),t,e);let _=h.split(".");if(_.length!==3)return await p(l(a(f.INVALID_FORMAT())),t,e);if(_[1].length>2048)return await p(l(a(f.INVALID_FORMAT())),t,e);let w=ct(h);if(!w)return await p(l(a(f.INVALID_FORMAT())),t,e);let I=w.typ,pt=d?3:1;if(I!==pt)return await p(l(a(f.INVALID_TOKEN_TYPE())),t,e);if(d&&w.aud!==Q(e))return await p(l(a(f.INVALID_TOKEN_TYPE())),t,e);let H=w.exp;if(typeof H=="number"&&Math.floor(Date.now()/1e3)>=H)return await p(l(a(f.TOKEN_EXPIRED())),t,e);if(d){if(!e.SERVICE_JWT_PUBLIC_KEY)return await p(l(a(T.INTERNAL_ERROR())),t,e);let g=await K(h,e.SERVICE_JWT_PUBLIC_KEY,3,z(e),q(e));if(!g.ok)return S(e)&&console.error("[Gateway] Service JWT failed:",g.errors),await p(l(a(g.errors,g.meta)),t,e)}else{let g=await K(h,e.JWT_PUBLIC_KEY,1,j(e),Z(e));if(!g.ok)return S(e)&&console.error("[Gateway] User JWT failed:",g.errors),await p(l(a(g.errors,g.meta)),t,e);let{scope:L}=x(e,s);if(L&&!et(s)&&g.data.aud!==L)return S(e)&&console.error(`[Gateway] Scope mismatch: token aud="${g.data.aud}" scope="${L}"`),await p(l(a(f.SCOPE_MISMATCH())),t,e)}return d&&Ft(s)?await p(await $t(t,e),t,e):await p(await V(t,e),t,e)}catch(n){return S(e)&&console.error("[Gateway] Unhandled error:",n),l(a(T.INTERNAL_ERROR()))}}};export{Ne as default};
|
package/bin/deploy.sh
CHANGED
|
@@ -44,6 +44,9 @@ Options (each also reads an env var; the flag wins):
|
|
|
44
44
|
--stage <dev|staging|prod> Stage (env STAGE, default dev; accepts production/development)
|
|
45
45
|
--project <name> CDK project (env PROJECT, default "default")
|
|
46
46
|
--app-name <name> Override app name (env AUTH_CRAFT_APP_NAME; default auth-craft[-project])
|
|
47
|
+
--layer <name> Layer namespace within the app (env LAYER, default "auth").
|
|
48
|
+
Resource names are {app}-{env}-{layer}-{component}, so a larger
|
|
49
|
+
app with other layers (e.g. billing) reuses this CLI per layer.
|
|
47
50
|
--region <region> AWS region (env LAMBDA_AWS_REGION, default us-east-1)
|
|
48
51
|
--gateway-secret <v> env LAMBDA_GATEWAY_SECRET
|
|
49
52
|
--jwt-public-key <v> env LAMBDA_JWT_PUBLIC_KEY
|
|
@@ -78,6 +81,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
78
81
|
--stage) FLAG[STAGE]="${2:?}"; shift 2 ;;
|
|
79
82
|
--project) FLAG[PROJECT]="${2:?}"; shift 2 ;;
|
|
80
83
|
--app-name) FLAG[AUTH_CRAFT_APP_NAME]="${2:?}"; shift 2 ;;
|
|
84
|
+
--layer) FLAG[LAYER]="${2:?}"; shift 2 ;;
|
|
81
85
|
--region) FLAG[LAMBDA_AWS_REGION]="${2:?}"; shift 2 ;;
|
|
82
86
|
--gateway-secret) FLAG[LAMBDA_GATEWAY_SECRET]="${2:?}"; shift 2 ;;
|
|
83
87
|
--jwt-public-key) FLAG[LAMBDA_JWT_PUBLIC_KEY]="${2:?}"; shift 2 ;;
|
|
@@ -142,7 +146,7 @@ case "$COMMAND" in
|
|
|
142
146
|
# if not already exported in this process (e.g. snapshot drove `lambda` separately).
|
|
143
147
|
if [[ -z "${LAMBDA_FUNCTION_URL:-}" || -z "${API_BASE_PATH:-}" ]]; then
|
|
144
148
|
require_aws_identity
|
|
145
|
-
api_stack="${
|
|
149
|
+
api_stack="${RESOURCE_NAME}-api"
|
|
146
150
|
LAMBDA_FUNCTION_URL="$(cfn_output "$api_stack" LambdaFunctionUrl)"
|
|
147
151
|
API_BASE_PATH="$(cfn_output "$api_stack" ApiBasePath)"
|
|
148
152
|
AUTH_SYSTEM_NAME="$(cfn_output "$api_stack" AuthSystemName)"
|
package/cdk/bin/app.ts
CHANGED
|
@@ -10,6 +10,7 @@ const app = new cdk.App();
|
|
|
10
10
|
const rawStage = process.env.STAGE || 'dev';
|
|
11
11
|
const stage = rawStage === 'production' ? 'prod' : rawStage === 'development' ? 'dev' : rawStage;
|
|
12
12
|
const project = process.env.PROJECT || 'default'; // Client project name
|
|
13
|
+
const layer = process.env.LAYER || 'auth'; // Layer namespace within the app (default "auth")
|
|
13
14
|
|
|
14
15
|
const env = {
|
|
15
16
|
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
@@ -17,18 +18,25 @@ const env = {
|
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
// ==================== Naming Convention ====================
|
|
20
|
-
//
|
|
21
|
-
// - app:
|
|
22
|
-
// - env:
|
|
23
|
-
// - layer: "
|
|
24
|
-
//
|
|
21
|
+
// Pattern: {app}-{env}-{layer}-{component}
|
|
22
|
+
// - app: AUTH_CRAFT_APP_NAME if set, else "auth-craft" (base) / "auth-craft-{project}"
|
|
23
|
+
// - env: "dev", "staging", "prod"
|
|
24
|
+
// - layer: LAYER (default "auth") — namespaces this deploy within a larger app
|
|
25
|
+
// - component: per-resource suffix ("data" for the table, "api" for the Lambda)
|
|
26
|
+
// The {app}-{env}-{layer} base is the resourceName (AUTH_SYSTEM_NAME / JWT issuer);
|
|
27
|
+
// it carries no component so the iss claim stays stable across resources.
|
|
28
|
+
// Examples: snapshot-commerce-prod-auth-data, snapshot-commerce-prod-auth-api
|
|
25
29
|
|
|
26
30
|
const appName =
|
|
27
31
|
process.env.AUTH_CRAFT_APP_NAME || (project === 'default' ? 'auth-craft' : `auth-craft-${project}`);
|
|
28
32
|
|
|
33
|
+
// Base name shared by every resource (also AUTH_SYSTEM_NAME / JWT issuer).
|
|
34
|
+
const resourceBase = `${appName}-${stage}-${layer}`;
|
|
35
|
+
|
|
29
36
|
console.log(`🚀 Deploying Auth Craft to AWS`);
|
|
30
37
|
console.log(` App: ${appName}`);
|
|
31
38
|
console.log(` Stage: ${stage}`);
|
|
39
|
+
console.log(` Layer: ${layer}`);
|
|
32
40
|
console.log(` Region: ${env.region}`);
|
|
33
41
|
|
|
34
42
|
// Common tags for cost allocation
|
|
@@ -39,30 +47,29 @@ const costTags = {
|
|
|
39
47
|
ManagedBy: 'CDK',
|
|
40
48
|
};
|
|
41
49
|
|
|
42
|
-
// Stack naming helper: {app}-{env}-{layer}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
// Resource naming helper: {app}-{env}
|
|
46
|
-
const getResourceName = () => `${appName}-${stage}`;
|
|
50
|
+
// Stack / resource naming helper: {app}-{env}-{layer}-{component}
|
|
51
|
+
const nameFor = (component: string) => `${resourceBase}-${component}`;
|
|
47
52
|
|
|
48
|
-
// DynamoDB Stack (data
|
|
49
|
-
const dynamodbStack = new DynamoDBStack(app,
|
|
53
|
+
// DynamoDB Stack (data component)
|
|
54
|
+
const dynamodbStack = new DynamoDBStack(app, nameFor('data'), {
|
|
50
55
|
env,
|
|
51
|
-
stackName:
|
|
52
|
-
description: `Auth Craft DynamoDB (${
|
|
56
|
+
stackName: nameFor('data'),
|
|
57
|
+
description: `Auth Craft DynamoDB (${resourceBase})`,
|
|
53
58
|
tags: costTags,
|
|
54
|
-
|
|
59
|
+
tableName: nameFor('data'),
|
|
55
60
|
stage,
|
|
56
61
|
});
|
|
57
62
|
|
|
58
|
-
// Lambda Stack (api
|
|
59
|
-
const lambdaStack = new LambdaStack(app,
|
|
63
|
+
// Lambda Stack (api component)
|
|
64
|
+
const lambdaStack = new LambdaStack(app, nameFor('api'), {
|
|
60
65
|
env,
|
|
61
|
-
stackName:
|
|
62
|
-
description: `Auth Craft Lambda API (${
|
|
66
|
+
stackName: nameFor('api'),
|
|
67
|
+
description: `Auth Craft Lambda API (${resourceBase})`,
|
|
63
68
|
table: dynamodbStack.table,
|
|
64
69
|
tags: costTags,
|
|
65
|
-
|
|
70
|
+
functionName: nameFor('api'),
|
|
71
|
+
// Base {app}-{env}-{layer} — AUTH_SYSTEM_NAME / JWT issuer (no component).
|
|
72
|
+
resourceName: resourceBase,
|
|
66
73
|
stage,
|
|
67
74
|
});
|
|
68
75
|
|