@auth-craft/aws-cf-stack 1.3.0 → 1.6.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.
@@ -1 +1 @@
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};
1
+ var Et=typeof process<"u"&&!1;function y(t){return Et?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 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 v(t){let e=["BACKEND_URL","BACKEND_GATEWAY_SECRET","JWT_PUBLIC_KEY","SERVICE_JWT_PUBLIC_KEY"];U(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 U(t){return t.SKIP_EDGE_GATE==="true"}function pt(t){return t.ENVIRONMENT||"development"}function S(t){return pt(t)==="development"}function W(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 H(t,e){if(!t)return!1;if(W(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 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 At=new Set(["system","tenant","customer"]);function L(t){return t&&At.has(t)?t:void 0}function F(t,e){let n=L(t.AUTH_SCOPE);if(n)return{scope:n,forwardPath:e};let r=L(O(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 X(t,e){let n=O(e);return n?t===n||t.startsWith(`${n}/`):!0}function $(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 Y(t){return $(t.JWT_ALLOWED_ALGORITHMS)}function j(t){return $(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 z(t){return t.SERVICE_JWT_AUDIENCE||"auth-craft:internal"}function Q(t){return t.JWT_ISSUER||void 0}function Z(t){return t.SERVICE_JWT_ISSUER||void 0}function q(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"]),M=new Set(["/verification/resend","/verification/verify","/password/forgot","/password/reset"]);function tt(t){let e=t.split("?")[0].replace(/\/$/,"")||"/";if(e==="/"||e.startsWith("/oauth/")||e==="/oauth"||M.has(e))return!0;let n=e.match(/^\/[^/]+(\/(?:verification|password)\/[^/]+)$/);if(n?.[1]&&M.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 et(t){return t.GATEWAY_JWT_PRIVATE_KEY||void 0}function rt(t){return t.TID_VALUE||void 0}function nt(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")},d={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")},ot={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 ot?ot[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=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 _t(t){return wt.has(t)}function yt(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 it(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(it(t))}catch{throw new Error(`Invalid JWK base64 for algorithm ${e}`)}return await crypto.subtle.importKey("jwk",n,yt(e),!1,["verify"])}function st(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(d.INVALID_FORMAT());try{let r=x(n);return m(JSON.parse(new TextDecoder().decode(r)))}catch{return a(d.INVALID_FORMAT())}}function Ct(t){let n=t.split(".")[1];if(!n)return a(d.INVALID_FORMAT());try{let r=x(n);return m(JSON.parse(new TextDecoder().decode(r)))}catch{return a(d.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(d.UNSUPPORTED_ALGORITHM());if(!_t(s))return a(d.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(d.INVALID_TOKEN_TYPE());if(o!==void 0&&u.iss!==o)return a(d.INVALID_ISSUER());if(u.exp&&Math.floor(Date.now()/1e3)>=u.exp)return a(d.TOKEN_EXPIRED());let l;try{l=await Dt(e,s)}catch{return a(d.INVALID_FORMAT())}return await xt(t,l,s)?m(u):a(d.SIGNATURE_INVALID())}var J=null;async function Nt(t){let e;try{e=JSON.parse(it(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 k(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 at(t,e,n,r,o=30){J||(J=Nt(t));let i=await J,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()},l=k(new TextEncoder().encode(JSON.stringify(c))),A=k(new TextEncoder().encode(JSON.stringify(u))),R=new TextEncoder().encode(`${l}.${A}`),h=await crypto.subtle.sign("Ed25519",i,R),w=k(new Uint8Array(h));return`${l}.${A}.${w}`}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 R of Lt){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 l=new URL(t.url);s.set("X-Forwarded-Host",l.hostname);let A=t.headers.get("cf-ipcountry");return A&&s.set("X-Cloudflare-Country",A),s}function kt(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 bt(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 G(t,e){let n=crypto.randomUUID();try{let r=new URL(t.url),o=C(r.pathname,e),{scope:i,forwardPath:s}=F(e,o),c=kt(t,e,s),u,l,A=rt(e);if(A&&nt(s)&&t.method==="POST"){let I=await Wt(t,A);I&&(u=I.body,l=I.contentType,S(e)&&console.log("[Gateway] Injected tid:",A,"into",s))}let R,h=et(e);h&&(R=await at(h,"bff-worker","auth-craft:backend","cloudflare-auth-gateway"));let w=Jt(t,e.BACKEND_GATEWAY_SECRET,n,i,l,R),V=u!==void 0?u:t.body?await t.clone().arrayBuffer():void 0,_=await fetch(c,{method:t.method,headers:w,body:V});return bt(_,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 D=new Map,ct=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-ct<3e4)return;ct=e;let n=e-t;for(let[r,o]of D){let i=o.hits.filter(s=>s>n);i.length===0?D.delete(r):o.hits=i}}function ut(t,e,n){if(n!=="POST")return{allowed:!0,retryAfterSec:0};let r=Gt.find(l=>l.match(e));if(!r)return{allowed:!0,retryAfterSec:0};Kt(3e5);let o=`${r.prefix}:${t}`,i=Date.now(),s=i-r.windowMs,c=D.get(o)??{hits:[]},u=c.hits.filter(l=>l>s);if(u.length>=r.limit){let l=u[0]??i;return c.hits=u,D.set(o,c),{allowed:!1,retryAfterSec:Math.max(1,Math.ceil((l+r.windowMs-i)/1e3))}}return u.push(i),c.hits=u,D.set(o,c),{allowed:!0,retryAfterSec:0}}function ft(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 H(t,e)?{"Access-Control-Allow-Origin":t,"Access-Control-Allow-Credentials":"true",Vary:"Origin"}:W(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 K();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(P(r),"1"):await e.CORS_KV.delete(P(r)),f(m({origin:r}))}return f(a(T.BAD_REQUEST()))}function K(){return new Response(null,{status:404})}var Pe={async fetch(t,e,n){try{v(e);let r=new URL(t.url);if(!X(r.pathname,e))return K();if(t.method==="OPTIONS")return await Vt(t,e);if(!U(e)){let g=t.headers.get("X-Edge-Gate");if(!g||!e.EDGE_GATE_SECRET||!ft(g,e.EDGE_GATE_SECRET))return S(e)&&console.warn("[Gateway] X-Edge-Gate missing or invalid"),K()}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=ut(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 l=q(s,e);if(!l&&tt(s))return await p(await G(t,e),t,e);let A=t.headers.get("Authorization");if(!A)return await p(f(a(d.MISSING_TOKEN())),t,e);let R=A.match(/^Bearer\s+(.+)$/i);if(!R)return await p(f(a(d.INVALID_FORMAT())),t,e);let h=R[1];if(h.length>4096)return await p(f(a(d.INVALID_FORMAT())),t,e);let w=h.split(".");if(w.length!==3)return await p(f(a(d.INVALID_FORMAT())),t,e);if(w[1].length>2048)return await p(f(a(d.INVALID_FORMAT())),t,e);let _=st(h);if(!_)return await p(f(a(d.INVALID_FORMAT())),t,e);let I=_.typ,dt=l?3:1;if(I!==dt)return await p(f(a(d.INVALID_TOKEN_TYPE())),t,e);if(l&&_.aud!==z(e))return await p(f(a(d.INVALID_TOKEN_TYPE())),t,e);let B=_.exp;if(typeof B=="number"&&Math.floor(Date.now()/1e3)>=B)return await p(f(a(d.TOKEN_EXPIRED())),t,e);if(l){if(!e.SERVICE_JWT_PUBLIC_KEY)return await p(f(a(T.INTERNAL_ERROR())),t,e);let g=await b(h,e.SERVICE_JWT_PUBLIC_KEY,3,j(e),Z(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 b(h,e.JWT_PUBLIC_KEY,1,Y(e),Q(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)}return l&&Mt(s)?await p(await vt(t,e),t,e):await p(await G(t,e),t,e)}catch(r){return S(e)&&console.error("[Gateway] Unhandled error:",r),f(a(T.INTERNAL_ERROR()))}}};export{Pe as default};
package/lib/common.sh CHANGED
@@ -177,16 +177,32 @@ lambda_env() {
177
177
  --query "Environment.Variables.${key}" --output text 2>/dev/null | sed 's/^None$//' || true
178
178
  }
179
179
 
180
+ # Resolve the gateway deploy mode: 'multi' (3 workers, one scope each — default) or
181
+ # 'shared' (1 worker that derives scope from the path's first segment). Accepts
182
+ # CF_GATEWAY_MODE = multi | shared (case-insensitive); anything else → multi.
183
+ gateway_mode() {
184
+ case "$(echo "${CF_GATEWAY_MODE:-multi}" | tr '[:upper:]' '[:lower:]')" in
185
+ shared | single | one) echo shared ;;
186
+ *) echo multi ;;
187
+ esac
188
+ }
189
+
180
190
  # Derive a gateway worker URL for a scope (custom domain > workers.dev > empty).
191
+ # In shared mode all scopes resolve to the single worker ${APP_NAME}-${STAGE}.
181
192
  gateway_url_for() {
182
- local scope="$1" upper dom_var custom
193
+ local scope="$1" upper dom_var custom worker_name
183
194
  upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
184
195
  dom_var="CF_WORKER_CUSTOM_DOMAIN_${upper}"
185
196
  custom="${!dom_var:-}"
197
+ if [[ "$(gateway_mode)" == "shared" ]]; then
198
+ worker_name="${APP_NAME}-${STAGE}"
199
+ else
200
+ worker_name="${APP_NAME}-${STAGE}-${scope}"
201
+ fi
186
202
  if [[ -n "$custom" ]]; then
187
203
  echo "https://$custom"
188
204
  elif [[ -n "${CF_WORKERS_SUBDOMAIN:-}" ]]; then
189
- echo "https://${APP_NAME}-${STAGE}-${scope}.${CF_WORKERS_SUBDOMAIN}.workers.dev"
205
+ echo "https://${worker_name}.${CF_WORKERS_SUBDOMAIN}.workers.dev"
190
206
  else
191
207
  echo ""
192
208
  fi
@@ -227,16 +243,25 @@ print_outputs_json() {
227
243
  jwtAlg="$(lambda_env "$fn" JWT_ALGORITHM)"; jwtAlg="${jwtAlg:-${LAMBDA_JWT_ALGORITHM:-EdDSA}}"
228
244
  svcPub="$(lambda_env "$fn" INTERNAL_JWT_PUBLIC_KEY)"; svcPub="${svcPub:-${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}}"
229
245
 
230
- # Worker names per scope (mirror deploy-gateway.sh: ${APP_NAME}-${STAGE}-${scope}).
231
- # These are what a consumer's Cloudflare Pages project references in a Service Binding
232
- # (`[[services]] service = "<worker name>"`) to front /{scope}/auth/* same-origin via
233
- # @auth-craft/pages-auth-proxy. The binding lives in the consumer's Pages project, not
234
- # here — `serviceBindings` is a ready-to-paste snippet they drop into their wrangler.toml.
235
- local wnSystem="${APP_NAME}-${STAGE}-system"
236
- local wnTenant="${APP_NAME}-${STAGE}-tenant"
237
- local wnCustomer="${APP_NAME}-${STAGE}-customer"
246
+ # Worker names per scope. In multi mode (default) each scope is its own worker
247
+ # (${APP_NAME}-${STAGE}-${scope}); in shared mode all scopes are the single worker
248
+ # ${APP_NAME}-${STAGE}. These are what a consumer's Cloudflare Pages project references
249
+ # in a Service Binding (`[[services]] service = "<worker name>"`) to front auth
250
+ # same-origin via @auth-craft/pages-auth-proxy. The binding lives in the consumer's
251
+ # Pages project, not here — `serviceBindings` is a ready-to-paste snippet.
252
+ local mode wnSystem wnTenant wnCustomer bindingService
253
+ mode="$(gateway_mode)"
254
+ if [[ "$mode" == "shared" ]]; then
255
+ wnSystem="${APP_NAME}-${STAGE}"; wnTenant="$wnSystem"; wnCustomer="$wnSystem"
256
+ bindingService="$wnSystem"
257
+ else
258
+ wnSystem="${APP_NAME}-${STAGE}-system"
259
+ wnTenant="${APP_NAME}-${STAGE}-tenant"
260
+ wnCustomer="${APP_NAME}-${STAGE}-customer"
261
+ bindingService="$wnCustomer"
262
+ fi
238
263
  local bindingSnippet
239
- bindingSnippet=$(printf '[[services]]\nbinding = "AUTH_GATEWAY"\nservice = "%s"\n' "$wnCustomer")
264
+ bindingSnippet=$(printf '[[services]]\nbinding = "AUTH_GATEWAY"\nservice = "%s"\n' "$bindingService")
240
265
 
241
266
  jq -n \
242
267
  --arg url "$url" --arg base "$base" --arg name "$name" \
@@ -10,11 +10,16 @@ create_admin() {
10
10
  local email="${LAMBDA_SUPER_ADMIN_EMAIL:-}"
11
11
  local password="${LAMBDA_SUPER_ADMIN_PASSWORD:-}"
12
12
  local name="${LAMBDA_SUPER_ADMIN_NAME:-Super Admin}"
13
- local setup_token="${LAMBDA_AUTH_SETUP_TOKEN:-${AUTH_SETUP_TOKEN:-}}"
14
13
 
15
- if [[ -z "$email" || -z "$password" || -z "$setup_token" ]]; then
16
- warn "Super-admin creds not configured — skipping."
17
- info "Set LAMBDA_SUPER_ADMIN_EMAIL, LAMBDA_SUPER_ADMIN_PASSWORD, LAMBDA_AUTH_SETUP_TOKEN to enable."
14
+ # Email + password are required to bootstrap the super-admin. Report exactly which are
15
+ # missing so a partial config isn't confusing. (The CLI is idempotent: if an active
16
+ # owner already exists in the system tenant it's a no-op, so re-runs are safe.)
17
+ local -a missing=()
18
+ [[ -z "$email" ]] && missing+=("LAMBDA_SUPER_ADMIN_EMAIL")
19
+ [[ -z "$password" ]] && missing+=("LAMBDA_SUPER_ADMIN_PASSWORD")
20
+ if [[ ${#missing[@]} -gt 0 ]]; then
21
+ warn "Super-admin skipped — missing: ${missing[*]}"
22
+ info "Set LAMBDA_SUPER_ADMIN_EMAIL and LAMBDA_SUPER_ADMIN_PASSWORD to enable."
18
23
  SUPER_ADMIN_STATUS="skipped"
19
24
  export SUPER_ADMIN_STATUS
20
25
  return 0
@@ -30,14 +35,13 @@ create_admin() {
30
35
 
31
36
  local output status
32
37
  set +e
33
- output="$(AUTH_SETUP_TOKEN="$setup_token" node "$entry" create-super-admin \
38
+ output="$(node "$entry" create-super-admin \
34
39
  --database-type dynamodb \
35
40
  --dynamodb-table "$DYNAMODB_TABLE" \
36
41
  --dynamodb-region "$AWS_REGION" \
37
42
  --email "$email" \
38
43
  --password "$password" \
39
- --name "$name" \
40
- --setup-token "$setup_token" 2>&1)"
44
+ --name "$name" 2>&1)"
41
45
  status=$?
42
46
  set -e
43
47
 
@@ -45,6 +45,22 @@ disable_public_url_for() {
45
45
  esac
46
46
  }
47
47
 
48
+ # Resolve whether to skip the X-Edge-Gate (Client → Worker) check for a scope. Safe ONLY
49
+ # when the worker has no public URL and is reached solely via a Service Binding — so this
50
+ # defaults to following disable_public_url and the caller additionally guards on it.
51
+ # Precedence: CF_WORKER_SKIP_EDGE_GATE_<SCOPE> > CF_WORKER_SKIP_EDGE_GATE > '' (off).
52
+ # Truthy: 1 / true / yes. Does NOT affect the Worker → Backend secret (X-Gateway-Secret).
53
+ skip_edge_gate_for() {
54
+ local scope="$1" upper var val
55
+ upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
56
+ var="CF_WORKER_SKIP_EDGE_GATE_${upper}"
57
+ val="${!var:-${CF_WORKER_SKIP_EDGE_GATE:-}}"
58
+ case "$(echo "$val" | tr '[:upper:]' '[:lower:]')" in
59
+ 1 | true | yes) echo 1 ;;
60
+ *) echo '' ;;
61
+ esac
62
+ }
63
+
48
64
  # Resolve per-scope static CORS origins. Precedence:
49
65
  # CF_ALLOWED_ORIGINS_<SCOPE> > CF_ALLOWED_ORIGINS > LAMBDA_CORS_ORIGINS > '*'.
50
66
  allowed_origins_for() {
@@ -90,8 +106,170 @@ enable_workers_dev() {
90
106
  fi
91
107
  }
92
108
 
109
+ # Merge per-scope CORS values for the shared worker (one worker serves all scopes).
110
+ # Unions the comma lists across system/tenant/customer, de-duped, '*' wins (collapses
111
+ # to '*' since it already allows everything).
112
+ merge_cors_for_shared() {
113
+ local resolver="$1" scope merged piece
114
+ merged=""
115
+ for scope in system tenant customer; do
116
+ piece="$("$resolver" "$scope")"
117
+ [[ -z "$piece" ]] && continue
118
+ merged="${merged:+$merged,}$piece"
119
+ done
120
+ # Normalize: split on comma, trim, drop blanks/dups; if any '*' present → just '*'.
121
+ echo "$merged" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | awk '
122
+ /^\*$/ { star = 1 }
123
+ NF && !seen[$0]++ { order[++n] = $0 }
124
+ END {
125
+ if (star) { print "*"; exit }
126
+ for (i = 1; i <= n; i++) printf "%s%s", (i > 1 ? "," : ""), order[i]
127
+ print ""
128
+ }'
129
+ }
130
+
131
+ # Deploy a single gateway worker. All inputs are passed explicitly so the same code path
132
+ # serves both the multi-worker loop and the shared-worker mode. The shared config
133
+ # (backend_url, environment, jwt/svc values) is read from deploy_gateway's locals via the
134
+ # caller's scope — so this stays a nested helper invoked only from deploy_gateway.
135
+ #
136
+ # Args: 1=worker_name 2=label 3=auth_scope('' = path-derived) 4=route_prefix
137
+ # 5=custom_domain 6=allowed_origins 7=origin_suffixes 8=cors_kv_id
138
+ # 9=disable_public_url 10=skip_edge_gate
139
+ deploy_one_worker() {
140
+ local worker_name="$1" label="$2" auth_scope="$3" route_prefix="$4" custom_domain="$5"
141
+ local allowed_origins="$6" origin_suffixes="$7" cors_kv_id="$8"
142
+ local disable_public_url="$9" skip_edge_gate="${10}"
143
+ local config_file url
144
+
145
+ info "Deploying $worker_name (${label}, route_prefix='${route_prefix}')…"
146
+
147
+ # A temp config is needed when either (a) a CORS KV id is set (a binding can't be
148
+ # set via --var) or (b) the public URL must be disabled (workers_dev/preview_urls are
149
+ # top-level config keys, not vars, and CF won't disable preview URLs just because
150
+ # workers.dev is off — both must be set explicitly). Otherwise use the shipped config.
151
+ config_file="$WRANGLER_DIR/wrangler.toml"
152
+ if [[ -n "$cors_kv_id" || -n "$disable_public_url" ]]; then
153
+ config_file="$(mktemp "${TMPDIR:-/tmp}/wrangler-${label}.XXXXXX.toml")"
154
+ # `main` in the shipped toml is RELATIVE to the toml's own dir (../assets/...). A temp
155
+ # config lives elsewhere (e.g. /tmp), so that relative path would resolve wrong and
156
+ # wrangler can't find the worker bundle. Rewrite it to the absolute asset path. This
157
+ # runs for BOTH branches below (disable-public-url and plain copy).
158
+ local abs_main="$GATEWAY_ASSET_DIR/worker.js"
159
+ if [[ -n "$disable_public_url" ]]; then
160
+ # Turn off the workers.dev URL *and* the per-version preview URLs in a single
161
+ # deploy (no REST API round-trip; CF won't drop preview URLs just for workers_dev
162
+ # being off, so both are set explicitly). These are ROOT keys: they must precede
163
+ # any [table] header or TOML scopes them under it (e.g. [build]). We drop the
164
+ # shipped `workers_dev = true` (TOML forbids duplicate keys) and inject both flags
165
+ # immediately after the first root key (`name = …`), before [build].
166
+ awk -v abs_main="$abs_main" '
167
+ /^[[:space:]]*workers_dev[[:space:]]*=/ { next } # drop original (anywhere)
168
+ /^[[:space:]]*main[[:space:]]*=/ { print "main = \"" abs_main "\""; next } # absolutize
169
+ { print }
170
+ !injected && /^[[:space:]]*name[[:space:]]*=/ {
171
+ print "workers_dev = false"; print "preview_urls = false"; injected = 1
172
+ }
173
+ ' "$WRANGLER_DIR/wrangler.toml" >"$config_file"
174
+ info " public URL disabled: workers.dev + preview URLs off (Pages-proxy / Service Binding only)"
175
+ else
176
+ awk -v abs_main="$abs_main" '
177
+ /^[[:space:]]*main[[:space:]]*=/ { print "main = \"" abs_main "\""; next } # absolutize
178
+ { print }
179
+ ' "$WRANGLER_DIR/wrangler.toml" >"$config_file"
180
+ fi
181
+ if [[ -n "$cors_kv_id" ]]; then
182
+ {
183
+ echo ''
184
+ echo '[[kv_namespaces]]'
185
+ echo 'binding = "CORS_KV"'
186
+ echo "id = \"$cors_kv_id\""
187
+ } >>"$config_file"
188
+ info " dynamic CORS: KV binding CORS_KV → $cors_kv_id"
189
+ fi
190
+ fi
191
+
192
+ # Per-worker vars (flags override the toml). AUTH_SCOPE is set only in multi mode; in
193
+ # shared mode it's omitted so the worker derives scope from the path's first segment.
194
+ local -a deploy_vars=(
195
+ --var "ENVIRONMENT:$environment"
196
+ --var "ROUTE_PREFIX:$route_prefix"
197
+ --var "ALLOWED_ORIGINS:$allowed_origins"
198
+ --var "ALLOWED_ORIGIN_SUFFIXES:$origin_suffixes"
199
+ --var "JWT_ISSUER:$jwt_issuer"
200
+ --var "JWT_ALLOWED_ALGORITHMS:$jwt_alg"
201
+ --var "SERVICE_ROUTE_PREFIX:$svc_route"
202
+ --var "SERVICE_JWT_AUDIENCE:$svc_aud"
203
+ --var "SERVICE_JWT_ISSUER:$svc_iss"
204
+ --var "SERVICE_JWT_ALLOWED_ALGORITHMS:$svc_alg"
205
+ )
206
+ [[ -n "$auth_scope" ]] && deploy_vars+=(--var "AUTH_SCOPE:$auth_scope")
207
+ if [[ -n "$skip_edge_gate" ]]; then
208
+ deploy_vars+=(--var "SKIP_EDGE_GATE:true")
209
+ info " edge gate SKIPPED (binding-only worker; X-Edge-Gate not required)"
210
+ fi
211
+
212
+ # Deploy the prebuilt bundle.
213
+ wrangler deploy \
214
+ --name "$worker_name" \
215
+ --config "$config_file" \
216
+ --no-bundle \
217
+ "${deploy_vars[@]}" >/dev/null
218
+
219
+ # Clean up the temp config (only created when KV was bound or public URL disabled).
220
+ [[ "$config_file" != "$WRANGLER_DIR/wrangler.toml" ]] && rm -f "$config_file"
221
+
222
+ # Secrets (piped via stdin).
223
+ put_secret "$worker_name" BACKEND_URL "$backend_url"
224
+ put_secret "$worker_name" BACKEND_GATEWAY_SECRET "${LAMBDA_GATEWAY_SECRET:-}"
225
+ # Edge-gate secret only when the gate is enforced (skipped workers don't need it).
226
+ [[ -z "$skip_edge_gate" ]] && put_secret "$worker_name" EDGE_GATE_SECRET "${CF_EDGE_GATE_SECRET:-}"
227
+ put_secret "$worker_name" JWT_PUBLIC_KEY "${LAMBDA_JWT_PUBLIC_KEY:-}"
228
+ put_secret "$worker_name" SERVICE_JWT_PUBLIC_KEY "${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}"
229
+ put_secret "$worker_name" GATEWAY_JWT_PRIVATE_KEY "${CF_GATEWAY_JWT_PRIVATE_KEY:-}"
230
+
231
+ if [[ -n "$custom_domain" ]]; then
232
+ url="https://$custom_domain"
233
+ info " custom domain: $custom_domain (configure DNS/route separately)"
234
+ elif [[ -n "$disable_public_url" ]]; then
235
+ # workers.dev + preview URLs were turned off in the deploy config above; the worker
236
+ # is reachable only via its Service Binding (Pages proxy) — no public URL to print.
237
+ url="(no public URL — reached via Pages proxy / Service Binding)"
238
+ else
239
+ enable_workers_dev "$worker_name"
240
+ if [[ -n "${CF_WORKERS_SUBDOMAIN:-}" ]]; then
241
+ url="https://${worker_name}.${CF_WORKERS_SUBDOMAIN}.workers.dev"
242
+ else
243
+ url="(workers.dev — set CF_WORKERS_SUBDOMAIN to print the URL)"
244
+ fi
245
+ fi
246
+ success " $label gateway: $url"
247
+ DEPLOYED_WORKER_URL="$url"
248
+ }
249
+
250
+ # Resolve the disable-public-url / skip-edge-gate flags for a worker, applying the safety
251
+ # rule that skip is only honored when the public URL is disabled. Sets the globals
252
+ # RESOLVED_DISABLE_PUBLIC_URL and RESOLVED_SKIP_EDGE_GATE. `key` is the resolver scope key
253
+ # ('system'|'tenant'|'customer', or 'shared' for the merged worker); `has_custom_domain`
254
+ # is non-empty when a custom domain forces the worker to stay reachable.
255
+ resolve_url_flags() {
256
+ local key="$1" has_custom_domain="$2"
257
+ RESOLVED_DISABLE_PUBLIC_URL=""
258
+ [[ -z "$has_custom_domain" ]] && RESOLVED_DISABLE_PUBLIC_URL="$(disable_public_url_for "$key")"
259
+ RESOLVED_SKIP_EDGE_GATE=""
260
+ if [[ -n "$(skip_edge_gate_for "$key")" ]]; then
261
+ if [[ -n "$RESOLVED_DISABLE_PUBLIC_URL" ]]; then
262
+ RESOLVED_SKIP_EDGE_GATE=1
263
+ else
264
+ warn " $key: SKIP_EDGE_GATE ignored — only allowed when the public URL is disabled"
265
+ fi
266
+ fi
267
+ }
268
+
93
269
  deploy_gateway() {
94
- header "Deploy Gateway Workers (stage=$STAGE)"
270
+ local mode
271
+ mode="$(gateway_mode)"
272
+ header "Deploy Gateway Worker(s) (stage=$STAGE, mode=$mode)"
95
273
 
96
274
  require_cmd npx
97
275
  require_env CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID
@@ -100,13 +278,11 @@ deploy_gateway() {
100
278
  || die "Prebuilt gateway bundle missing: $GATEWAY_ASSET_DIR/worker.js (was the package built?)"
101
279
 
102
280
  # Anti-drift: BACKEND_URL the worker forwards onto = Function URL + API base path.
281
+ # These shared values are read by deploy_one_worker (nested helper).
103
282
  local backend_url="${LAMBDA_FUNCTION_URL%/}${API_BASE_PATH}"
104
-
105
- # Shared config across the 3 workers (CORS is now resolved PER SCOPE below).
106
283
  local environment jwt_issuer
107
284
  environment="$RUNTIME_NODE_ENV"
108
285
  jwt_issuer="${LAMBDA_JWT_ISSUER:-${AUTH_SYSTEM_NAME:-$RESOURCE_NAME}}"
109
-
110
286
  local jwt_alg="${LAMBDA_JWT_ALGORITHM:-EdDSA}"
111
287
  local svc_route="${LAMBDA_SERVICE_ROUTE_PATH:-/internal}"
112
288
  local svc_aud="${LAMBDA_INTERNAL_JWT_AUDIENCE:-auth-craft:internal}"
@@ -114,8 +290,39 @@ deploy_gateway() {
114
290
  local svc_alg="${LAMBDA_INTERNAL_JWT_ALG:-EdDSA}"
115
291
 
116
292
  GATEWAY_URLS=()
117
- local scope worker_name route_prefix custom_domain url
118
- local allowed_origins origin_suffixes cors_kv_id config_file disable_public_url
293
+
294
+ if [[ "$mode" == "shared" ]]; then
295
+ # ── Shared worker: one worker serves all scopes; scope comes from the path's first
296
+ # segment (worker AUTH_SCOPE unset). CORS unions all scopes; KV uses the customer id.
297
+ local worker_name custom_domain allowed_origins origin_suffixes cors_kv_id
298
+ worker_name="${APP_NAME}-${STAGE}"
299
+ custom_domain="$(custom_domain_for shared)"
300
+ allowed_origins="$(merge_cors_for_shared allowed_origins_for)"
301
+ origin_suffixes="$(merge_cors_for_shared origin_suffixes_for)"
302
+ cors_kv_id="$(cors_kv_id_for shared)"
303
+ [[ -z "$cors_kv_id" ]] && cors_kv_id="$(cors_kv_id_for customer)"
304
+
305
+ resolve_url_flags shared "$custom_domain"
306
+
307
+ # ROUTE_PREFIX='' and AUTH_SCOPE='' → worker derives scope from /{scope}/... segment.
308
+ # NOTE: a shared worker does NOT enforce scope by itself (any client may send
309
+ # /system/...). Pair it with a scope-locked Pages proxy (@auth-craft/pages-auth-proxy
310
+ # `scope` option) if customer apps must never reach system/tenant context.
311
+ deploy_one_worker "$worker_name" "shared" "" "" "$custom_domain" \
312
+ "$allowed_origins" "$origin_suffixes" "$cors_kv_id" \
313
+ "$RESOLVED_DISABLE_PUBLIC_URL" "$RESOLVED_SKIP_EDGE_GATE"
314
+ GATEWAY_URLS+=("shared=$DEPLOYED_WORKER_URL")
315
+
316
+ export GATEWAY_URLS
317
+ success "Shared gateway worker deployed"
318
+ info " BACKEND_URL wired to: $backend_url"
319
+ warn " Shared mode does NOT enforce scope — use a scope-locked Pages proxy if needed."
320
+ return 0
321
+ fi
322
+
323
+ # ── Multi-worker: 3 workers, one scope each (AUTH_SCOPE fixed per worker = enforced).
324
+ local scope worker_name route_prefix custom_domain
325
+ local allowed_origins origin_suffixes cors_kv_id
119
326
  for scope in system tenant customer; do
120
327
  worker_name="${APP_NAME}-${STAGE}-${scope}"
121
328
  route_prefix="$(route_prefix_for "$scope")"
@@ -123,93 +330,12 @@ deploy_gateway() {
123
330
  allowed_origins="$(allowed_origins_for "$scope")"
124
331
  origin_suffixes="$(origin_suffixes_for "$scope")"
125
332
  cors_kv_id="$(cors_kv_id_for "$scope")"
126
- # A custom domain always needs the worker reachable, so the disable flag is ignored there.
127
- disable_public_url=""
128
- [[ -z "$custom_domain" ]] && disable_public_url="$(disable_public_url_for "$scope")"
129
-
130
- info "Deploying $worker_name (scope=$scope route_prefix=$route_prefix)…"
131
-
132
- # A temp config is needed when either (a) a CORS KV id is set (a binding can't be
133
- # set via --var) or (b) the public URL must be disabled (workers_dev/preview_urls are
134
- # top-level config keys, not vars, and CF won't disable preview URLs just because
135
- # workers.dev is off — both must be set explicitly). Otherwise use the shipped config.
136
- config_file="$WRANGLER_DIR/wrangler.toml"
137
- if [[ -n "$cors_kv_id" || -n "$disable_public_url" ]]; then
138
- config_file="$(mktemp "${TMPDIR:-/tmp}/wrangler-${scope}.XXXXXX.toml")"
139
- if [[ -n "$disable_public_url" ]]; then
140
- # Turn off the workers.dev URL *and* the per-version preview URLs in a single
141
- # deploy (no REST API round-trip; CF won't drop preview URLs just for workers_dev
142
- # being off, so both are set explicitly). These are ROOT keys: they must precede
143
- # any [table] header or TOML scopes them under it (e.g. [build]). We drop the
144
- # shipped `workers_dev = true` (TOML forbids duplicate keys) and inject both flags
145
- # immediately after the first root key (`name = …`), before [build].
146
- awk '
147
- /^[[:space:]]*workers_dev[[:space:]]*=/ { next } # drop original (anywhere)
148
- { print }
149
- !injected && /^[[:space:]]*name[[:space:]]*=/ {
150
- print "workers_dev = false"; print "preview_urls = false"; injected = 1
151
- }
152
- ' "$WRANGLER_DIR/wrangler.toml" >"$config_file"
153
- info " public URL disabled: workers.dev + preview URLs off (Pages-proxy / Service Binding only)"
154
- else
155
- cat "$WRANGLER_DIR/wrangler.toml" >"$config_file"
156
- fi
157
- if [[ -n "$cors_kv_id" ]]; then
158
- {
159
- echo ''
160
- echo '[[kv_namespaces]]'
161
- echo 'binding = "CORS_KV"'
162
- echo "id = \"$cors_kv_id\""
163
- } >>"$config_file"
164
- info " dynamic CORS: KV binding CORS_KV → $cors_kv_id"
165
- fi
166
- fi
333
+ resolve_url_flags "$scope" "$custom_domain"
167
334
 
168
- # Deploy the prebuilt bundle with per-scope vars (flags override the toml).
169
- wrangler deploy \
170
- --name "$worker_name" \
171
- --config "$config_file" \
172
- --no-bundle \
173
- --var "ENVIRONMENT:$environment" \
174
- --var "AUTH_SCOPE:$scope" \
175
- --var "ROUTE_PREFIX:$route_prefix" \
176
- --var "ALLOWED_ORIGINS:$allowed_origins" \
177
- --var "ALLOWED_ORIGIN_SUFFIXES:$origin_suffixes" \
178
- --var "JWT_ISSUER:$jwt_issuer" \
179
- --var "JWT_ALLOWED_ALGORITHMS:$jwt_alg" \
180
- --var "SERVICE_ROUTE_PREFIX:$svc_route" \
181
- --var "SERVICE_JWT_AUDIENCE:$svc_aud" \
182
- --var "SERVICE_JWT_ISSUER:$svc_iss" \
183
- --var "SERVICE_JWT_ALLOWED_ALGORITHMS:$svc_alg" >/dev/null
184
-
185
- # Clean up the temp config (only created when KV was bound or public URL disabled).
186
- [[ "$config_file" != "$WRANGLER_DIR/wrangler.toml" ]] && rm -f "$config_file"
187
-
188
- # Secrets (piped via stdin).
189
- put_secret "$worker_name" BACKEND_URL "$backend_url"
190
- put_secret "$worker_name" BACKEND_GATEWAY_SECRET "${LAMBDA_GATEWAY_SECRET:-}"
191
- put_secret "$worker_name" EDGE_GATE_SECRET "${CF_EDGE_GATE_SECRET:-}"
192
- put_secret "$worker_name" JWT_PUBLIC_KEY "${LAMBDA_JWT_PUBLIC_KEY:-}"
193
- put_secret "$worker_name" SERVICE_JWT_PUBLIC_KEY "${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}"
194
- put_secret "$worker_name" GATEWAY_JWT_PRIVATE_KEY "${CF_GATEWAY_JWT_PRIVATE_KEY:-}"
195
-
196
- if [[ -n "$custom_domain" ]]; then
197
- url="https://$custom_domain"
198
- info " custom domain: $custom_domain (configure DNS/route separately)"
199
- elif [[ -n "$disable_public_url" ]]; then
200
- # workers.dev + preview URLs were turned off in the deploy config above; the worker
201
- # is reachable only via its Service Binding (Pages proxy) — no public URL to print.
202
- url="(no public URL — reached via Pages proxy / Service Binding)"
203
- else
204
- enable_workers_dev "$worker_name"
205
- if [[ -n "${CF_WORKERS_SUBDOMAIN:-}" ]]; then
206
- url="https://${worker_name}.${CF_WORKERS_SUBDOMAIN}.workers.dev"
207
- else
208
- url="(workers.dev — set CF_WORKERS_SUBDOMAIN to print the URL)"
209
- fi
210
- fi
211
- success " $scope gateway: $url"
212
- GATEWAY_URLS+=("$scope=$url")
335
+ deploy_one_worker "$worker_name" "$scope" "$scope" "$route_prefix" "$custom_domain" \
336
+ "$allowed_origins" "$origin_suffixes" "$cors_kv_id" \
337
+ "$RESOLVED_DISABLE_PUBLIC_URL" "$RESOLVED_SKIP_EDGE_GATE"
338
+ GATEWAY_URLS+=("$scope=$DEPLOYED_WORKER_URL")
213
339
  done
214
340
  export GATEWAY_URLS
215
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-craft/aws-cf-stack",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
4
4
  "description": "Self-contained, versioned distribution of the Auth Craft AWS (DynamoDB + Lambda) + Cloudflare gateway stack. Bundles prebuilt Lambda/worker artifacts + CDK app so consumers deploy without cloning auth-craft.",
5
5
  "type": "module",
6
6
  "bin": {