@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.
- package/.env.example +20 -2
- package/README.md +37 -5
- package/assets/admin/cli.mjs +16 -16
- package/assets/gateway/worker.js +1 -1
- package/lib/common.sh +36 -11
- package/lib/create-admin.sh +11 -7
- package/lib/deploy-gateway.sh +218 -92
- package/package.json +1 -1
package/assets/gateway/worker.js
CHANGED
|
@@ -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://${
|
|
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 (
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
local wnTenant
|
|
237
|
-
|
|
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' "$
|
|
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" \
|
package/lib/create-admin.sh
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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="$(
|
|
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
|
|
package/lib/deploy-gateway.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
"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": {
|