@auth-craft/aws-cf-stack 1.7.1 → 1.9.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 +24 -4
- package/README.md +61 -11
- package/assets/admin/cli.mjs +82 -82
- package/assets/gateway/worker.js +1 -1
- package/assets/lambda/index.mjs +42 -37
- package/bin/deploy.sh +13 -1
- package/cdk/bin/app.ts +28 -22
- package/cdk/lib/dynamodb-stack.ts +4 -6
- package/cdk/lib/lambda-stack.ts +109 -11
- package/lib/common.sh +34 -14
- package/lib/deploy-gateway.sh +2 -2
- package/lib/deploy-lambda.sh +2 -2
- package/package.json +10 -6
package/assets/gateway/worker.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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};
|
|
1
|
+
var ht=typeof process<"u"&&!1;function y(t){return ht?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 d(t,e){return r=>{let n={code:t,message:r?.message??e};return r?.field&&(n.field=r.field),n}}function b(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 $(t){let e=["BACKEND_URL","BACKEND_GATEWAY_SECRET","JWT_PUBLIC_KEY","SERVICE_JWT_PUBLIC_KEY"];G(t)||e.push("EDGE_GATE_SECRET");let r=e.filter(o=>!t[o]);if(r.length>0)throw new Error(`Missing required environment variables: ${r.join(", ")}`);let n=k(t).replace(/^\//,"").split("/")[0];if(I(n))throw new Error(`SERVICE_ROUTE_PREFIX must not begin with a scope segment (${n}); scope segments are stripped during path canonicalization, which would downgrade every service route to an ordinary Access-token route.`)}function G(t){return t.SKIP_EDGE_GATE==="true"}function mt(t){return t.ENVIRONMENT||"development"}function S(t){return mt(t)==="development"}function K(t){return t.ALLOWED_ORIGINS?t.ALLOWED_ORIGINS.split(",").map(e=>e.trim()).filter(Boolean):["*"]}function St(t){return t.ALLOWED_ORIGIN_SUFFIXES?t.ALLOWED_ORIGIN_SUFFIXES.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):[]}function U(t){return`origin:${t}`}async function X(t,e){if(!t)return!1;if(K(e).includes(t))return!0;let n=St(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(U(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 L(t,e){let r=O(e);return r?t===r?"/":t.startsWith(`${r}/`)?t.slice(r.length):t:t}var Rt=new Set(["system","tenant","customer"]);function I(t){if(!t)return;let e=t.toLowerCase();return Rt.has(e)?e:void 0}function W(t,e){let r=I(t.AUTH_SCOPE);if(r)return{scope:r,forwardPath:N(e)};let n=I(O(t).replace(/^\//,""));if(n)return{scope:n,forwardPath:N(e)};let o=e.split("/").filter(Boolean),i=I(o[0]);return i?{scope:i,forwardPath:N(e)}:{scope:void 0,forwardPath:N(e)}}function N(t){let e=t.replace(/\/{2,}/g,"/");for(;;){let r=e.split("/").filter(Boolean),n=r[0];if(!n||!I(n))return e;e=`/${r.slice(1).join("/")}`}}function Y(t,e){let r=O(e);return r?t===r||t.startsWith(`${r}/`):!0}function j(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 z(t){return j(t.JWT_ALLOWED_ALGORITHMS)}function Q(t){return j(t.SERVICE_JWT_ALLOWED_ALGORITHMS)}function k(t){let e=t.SERVICE_ROUTE_PREFIX||"/internal",r=e.startsWith("/")?e:`/${e}`;return r.endsWith("/")?r.slice(0,-1):r}function Z(t){return t.SERVICE_JWT_AUDIENCE||"auth-craft:internal"}function q(t){return t.JWT_ISSUER||void 0}function tt(t){return t.SERVICE_JWT_ISSUER||void 0}function et(t,e){let r=t.split("?")[0].replace(/\/$/,"")||"/",n=k(e);return r===n||r.startsWith(`${n}/`)}var Tt=new Set(["authenticate","login","register","refresh","logout","complete-challenge","complete-mfa-challenge","issue-mfa-challenge","forgot-password","reset-password","verify-email","resend-verification"]),wt=new Set(["callback","state"]),_t=new Set(["/verification/resend","/verification/verify","/password/forgot","/password/reset"]),yt=new Set(["select-tenant"]);function rt(t){let r=(t.split("?")[0].replace(/\/$/,"")||"/").match(/^\/auth\/([^/]+)/);return!!r?.[1]&&yt.has(r[1])}function nt(t){let e=t.split("?")[0].replace(/\/$/,"")||"/",r=e.match(/^\/oauth\/([^/]+)/);if(r?.[1]&&wt.has(r[1])||_t.has(e))return!0;let n=e.match(/^\/auth\/([^/]+)/);if(n){let o=n[1];if(o&&Tt.has(o))return!0}return!1}function ot(t){return t.GATEWAY_JWT_PRIVATE_KEY||void 0}function it(t){return t.TID_VALUE||void 0}function st(t){let e=t.split("?")[0].replace(/\/$/,"");return e.endsWith("/auth/login")||e.endsWith("/auth/authenticate")}function It(){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: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"),SCOPE_MISMATCH:d("JWT_SCOPE_MISMATCH","Token not valid for this scope")},at={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 Ot(t){if(t.ok)return t.data===void 0?204:200;let e=t.errors?.[0];return e?.code&&e.code in at?at[e.code]:500}function l(t,e){let r=new Date().toISOString(),n=e||It(),o={...t.meta,timestamp:r,traceId:n},i=t.ok?m(t.data,o):a(t.errors,o),s=Ot(i),c=b(i);return new Response(JSON.stringify(c),{status:s,headers:{"Content-Type":"application/json"}})}var Ct=new Set(["EdDSA","ES256","RS256","PS256"]);function Dt(t){return Ct.has(t)}function xt(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 Nt(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 J(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 ct(t){return atob(t)}var Ut=/^[0-9a-fA-F]{64}$/;async function Lt(t,e){if(Ut.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(ct(t))}catch{throw new Error(`Invalid JWK base64 for algorithm ${e}`)}return await crypto.subtle.importKey("jwk",r,xt(e),!1,["verify"])}function ut(t){let e=t.split(".");if(e.length!==3)return null;let r=e[1];if(!r)return null;try{let n=J(r);return JSON.parse(new TextDecoder().decode(n))}catch{return null}}function Wt(t){let r=t.split(".")[0];if(!r)return a(f.INVALID_FORMAT());try{let n=J(r);return m(JSON.parse(new TextDecoder().decode(n)))}catch{return a(f.INVALID_FORMAT())}}function kt(t){let r=t.split(".")[1];if(!r)return a(f.INVALID_FORMAT());try{let n=J(r);return m(JSON.parse(new TextDecoder().decode(n)))}catch{return a(f.INVALID_FORMAT())}}async function Jt(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=J(s);try{return await crypto.subtle.verify(Nt(r),e,u,c)}catch{return!1}}async function B(t,e,r=1,n=new Set(["EdDSA"]),o){let i=Wt(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(!Dt(s))return a(f.UNSUPPORTED_ALGORITHM());let c=kt(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 E;try{E=await Lt(e,s)}catch{return a(f.INVALID_FORMAT())}return await Jt(t,E,s)?m(u):a(f.SIGNATURE_INVALID())}var V=null;async function bt(t){let e;try{e=JSON.parse(ct(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 M(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 lt(t,e,r,n,o=30){V||(V=bt(t));let i=await V,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()},E=M(new TextEncoder().encode(JSON.stringify(c))),A=M(new TextEncoder().encode(JSON.stringify(u))),h=new TextEncoder().encode(`${E}.${A}`),R=await crypto.subtle.sign("Ed25519",i,h),_=M(new Uint8Array(R));return`${E}.${A}.${_}`}var Gt=["content-type","accept","accept-encoding","accept-language","user-agent","cookie"];function Kt(t){return t.getSetCookie?.()??[]}async function Vt(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 Mt(t,e,r,n,o,i){let s=new Headers;for(let h of Gt){let R=t.headers.get(h);R&&s.set(h,R)}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 E=new URL(t.url);s.set("X-Forwarded-Host",E.hostname);let A=t.headers.get("cf-ipcountry");return A&&s.set("X-Cloudflare-Country",A),s}function Bt(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 Ht(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 H(t,e){let r=crypto.randomUUID();try{let n=new URL(t.url),o=L(n.pathname,e),{scope:i,forwardPath:s}=W(e,o),c=Bt(t,e,s),u,E,A=it(e);if(A&&st(s)&&t.method==="POST"){let D=await Vt(t,A);D&&(u=D.body,E=D.contentType,S(e)&&console.log("[Gateway] Injected tid:",A,"into",s))}let h,R=ot(e);R&&(h=await lt(R,"bff-worker","auth-craft:backend","cloudflare-auth-gateway"));let _=Mt(t,e.BACKEND_GATEWAY_SECRET,r,i,E,h),w=u!==void 0?u:t.body?await t.clone().arrayBuffer():void 0,C=await fetch(c,{method:t.method,headers:_,body:w});return Ht(C,r,e)}catch(n){console.error("[Gateway] Proxy error:",n);let o=n instanceof Error?n.message:"Unknown error";return l(a(d("BAD_GATEWAY",`Failed to reach backend: ${o}`)()))}}var P=new Map,ft=0,vt=[{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 Ft(t){let e=Date.now();if(e-ft<3e4)return;ft=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 dt(t,e,r){if(r!=="POST")return{allowed:!0,retryAfterSec:0};let n=vt.find(E=>E.match(e));if(!n)return{allowed:!0,retryAfterSec:0};Ft(3e5);let o=`${n.prefix}:${t}`,i=Date.now(),s=i-n.windowMs,c=P.get(o)??{hits:[]},u=c.hits.filter(E=>E>s);if(u.length>=n.limit){let E=u[0]??i;return c.hits=u,P.set(o,c),{allowed:!1,retryAfterSec:Math.max(1,Math.ceil((E+n.windowMs-i)/1e3))}}return u.push(i),c.hits=u,P.set(o,c),{allowed:!0,retryAfterSec:0}}function Et(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 pt(t,e){return t&&await X(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 $t(t,e){let r=t.headers.get("Origin")??"*",n=t.headers.get("Access-Control-Request-Headers")??"";return new Response(null,{status:204,headers:{...await pt(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 pt(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 Xt="cors-origins";function Yt(t,e){return t.split("?")[0].replace(/\/$/,"")===`${k(e)}/${Xt}`}async function jt(t,e){if(!e.CORS_KV)return v();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(U(n),"1"):await e.CORS_KV.delete(U(n)),l(m({origin:n}))}return l(a(T.BAD_REQUEST()))}function v(){return new Response(null,{status:404})}var We={async fetch(t,e,r){try{$(e);let n=new URL(t.url);if(!Y(n.pathname,e))return v();if(t.method==="OPTIONS")return await $t(t,e);if(!G(e)){let g=t.headers.get("X-Edge-Gate");if(!g||!e.EDGE_GATE_SECRET||!Et(g,e.EDGE_GATE_SECRET))return S(e)&&console.warn("[Gateway] X-Edge-Gate missing or invalid"),v()}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=L(n.pathname,e),{scope:c,forwardPath:u}=W(e,s);S(e)&&console.log("[Gateway] Request:",t.method,n.pathname,"\u2192",u);let E=t.headers.get("cf-connecting-ip")??"unknown",A=dt(E,u,t.method);if(!A.allowed){let g=l(a(T.RATE_LIMIT_EXCEEDED()));return g.headers.set("Retry-After",String(A.retryAfterSec)),await p(g,t,e)}let h=et(u,e);if(!h&&nt(u))return await p(await H(t,e),t,e);let R=t.headers.get("Authorization");if(!R)return await p(l(a(f.MISSING_TOKEN())),t,e);let _=R.match(/^Bearer\s+(.+)$/i);if(!_)return await p(l(a(f.INVALID_FORMAT())),t,e);let w=_[1];if(w.length>4096)return await p(l(a(f.INVALID_FORMAT())),t,e);let C=w.split(".");if(C.length!==3)return await p(l(a(f.INVALID_FORMAT())),t,e);if(C[1].length>2048)return await p(l(a(f.INVALID_FORMAT())),t,e);let x=ut(w);if(!x)return await p(l(a(f.INVALID_FORMAT())),t,e);let gt=x.typ,At=h?3:1;if(gt!==At)return await p(l(a(f.INVALID_TOKEN_TYPE())),t,e);if(h&&x.aud!==Z(e))return await p(l(a(f.INVALID_TOKEN_TYPE())),t,e);let F=x.exp;if(typeof F=="number"&&Math.floor(Date.now()/1e3)>=F)return await p(l(a(f.TOKEN_EXPIRED())),t,e);if(h){if(!e.SERVICE_JWT_PUBLIC_KEY)return await p(l(a(T.INTERNAL_ERROR())),t,e);let g=await B(w,e.SERVICE_JWT_PUBLIC_KEY,3,Q(e),tt(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 B(w,e.JWT_PUBLIC_KEY,1,z(e),q(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);if(c&&!rt(u)&&g.data.aud!==c)return S(e)&&console.error(`[Gateway] Scope mismatch: token aud="${g.data.aud}" scope="${c}"`),await p(l(a(f.SCOPE_MISMATCH())),t,e)}return h&&Yt(u,e)?await p(await jt(t,e),t,e):await p(await H(t,e),t,e)}catch(n){return S(e)&&console.error("[Gateway] Unhandled error:",n),l(a(T.INTERNAL_ERROR()))}}};export{We as default};
|