@auth-craft/aws-cf-stack 1.1.1 → 1.3.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/lib/common.sh CHANGED
@@ -227,6 +227,17 @@ print_outputs_json() {
227
227
  jwtAlg="$(lambda_env "$fn" JWT_ALGORITHM)"; jwtAlg="${jwtAlg:-${LAMBDA_JWT_ALGORITHM:-EdDSA}}"
228
228
  svcPub="$(lambda_env "$fn" INTERNAL_JWT_PUBLIC_KEY)"; svcPub="${svcPub:-${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}}"
229
229
 
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"
238
+ local bindingSnippet
239
+ bindingSnippet=$(printf '[[services]]\nbinding = "AUTH_GATEWAY"\nservice = "%s"\n' "$wnCustomer")
240
+
230
241
  jq -n \
231
242
  --arg url "$url" --arg base "$base" --arg name "$name" \
232
243
  --arg table "$table" --arg backend "$backend" \
@@ -240,13 +251,17 @@ print_outputs_json() {
240
251
  --arg rpSystem "${CF_WORKER_ROUTE_PREFIX_SYSTEM:-/system}" \
241
252
  --arg rpTenant "${CF_WORKER_ROUTE_PREFIX_TENANT:-/tenant}" \
242
253
  --arg rpCustomer "${CF_WORKER_ROUTE_PREFIX_CUSTOMER:-/customer}" \
254
+ --arg wnSystem "$wnSystem" --arg wnTenant "$wnTenant" --arg wnCustomer "$wnCustomer" \
255
+ --arg bindingSnippet "$bindingSnippet" \
243
256
  '{stage:$stage, project:$project, region:$region,
244
257
  lambdaFunctionUrl:$url, apiBasePath:$base, authSystemName:$name,
245
258
  dynamodbTable:$table, backendUrl:$backend,
246
259
  jwtPublicKey:$jwtPub, jwtIssuer:$jwtIssuer, jwtAlgorithm:$jwtAlg,
247
260
  serviceJwtPublicKey:$svcPub,
248
261
  gatewayUrls:{system:$gwSystem, tenant:$gwTenant, customer:$gwCustomer},
249
- routePrefixes:{system:$rpSystem, tenant:$rpTenant, customer:$rpCustomer}}'
262
+ workerNames:{system:$wnSystem, tenant:$wnTenant, customer:$wnCustomer},
263
+ routePrefixes:{system:$rpSystem, tenant:$rpTenant, customer:$rpCustomer},
264
+ pagesProxy:{serviceBindingExample:$bindingSnippet}}'
250
265
  }
251
266
 
252
267
  # ── Tool command resolution (#7: let a consumer pin its own binaries) ─────────
@@ -30,6 +30,48 @@ custom_domain_for() {
30
30
  echo "${!var:-}"
31
31
  }
32
32
 
33
+ # Resolve whether to disable the public workers.dev URL for a scope (Pages-proxy /
34
+ # Service-Binding deploys reach the worker in-network and don't need a public URL).
35
+ # Precedence: CF_WORKER_DISABLE_PUBLIC_URL_<SCOPE> > CF_WORKER_DISABLE_PUBLIC_URL > '' (off).
36
+ # Truthy values: 1 / true / yes (case-insensitive). Ignored when a custom domain is set.
37
+ disable_public_url_for() {
38
+ local scope="$1" upper var val
39
+ upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
40
+ var="CF_WORKER_DISABLE_PUBLIC_URL_${upper}"
41
+ val="${!var:-${CF_WORKER_DISABLE_PUBLIC_URL:-}}"
42
+ case "$(echo "$val" | tr '[:upper:]' '[:lower:]')" in
43
+ 1 | true | yes) echo 1 ;;
44
+ *) echo '' ;;
45
+ esac
46
+ }
47
+
48
+ # Resolve per-scope static CORS origins. Precedence:
49
+ # CF_ALLOWED_ORIGINS_<SCOPE> > CF_ALLOWED_ORIGINS > LAMBDA_CORS_ORIGINS > '*'.
50
+ allowed_origins_for() {
51
+ local scope="$1" upper var
52
+ upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
53
+ var="CF_ALLOWED_ORIGINS_${upper}"
54
+ echo "${!var:-${CF_ALLOWED_ORIGINS:-${LAMBDA_CORS_ORIGINS:-*}}}"
55
+ }
56
+
57
+ # Resolve per-scope dynamic origin SUFFIXES (e.g. '.myplatform.com'). Precedence:
58
+ # CF_ALLOWED_ORIGIN_SUFFIXES_<SCOPE> > CF_ALLOWED_ORIGIN_SUFFIXES > '' (disabled).
59
+ origin_suffixes_for() {
60
+ local scope="$1" upper var
61
+ upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
62
+ var="CF_ALLOWED_ORIGIN_SUFFIXES_${upper}"
63
+ echo "${!var:-${CF_ALLOWED_ORIGIN_SUFFIXES:-}}"
64
+ }
65
+
66
+ # Resolve a per-scope CORS KV namespace id (CF_CORS_KV_ID_CUSTOMER, …) — empty = no
67
+ # dynamic KV allowlist for that scope. Typically only the customer scope sets this.
68
+ cors_kv_id_for() {
69
+ local scope="$1" upper var
70
+ upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
71
+ var="CF_CORS_KV_ID_${upper}"
72
+ echo "${!var:-}"
73
+ }
74
+
33
75
  # Enable the workers.dev subdomain for a worker with no custom domain (so it's reachable).
34
76
  enable_workers_dev() {
35
77
  local worker_name="$1"
@@ -60,11 +102,10 @@ deploy_gateway() {
60
102
  # Anti-drift: BACKEND_URL the worker forwards onto = Function URL + API base path.
61
103
  local backend_url="${LAMBDA_FUNCTION_URL%/}${API_BASE_PATH}"
62
104
 
63
- # Shared config across the 3 workers.
64
- local environment jwt_issuer allowed_origins
105
+ # Shared config across the 3 workers (CORS is now resolved PER SCOPE below).
106
+ local environment jwt_issuer
65
107
  environment="$RUNTIME_NODE_ENV"
66
108
  jwt_issuer="${LAMBDA_JWT_ISSUER:-${AUTH_SYSTEM_NAME:-$RESOURCE_NAME}}"
67
- allowed_origins="${CF_ALLOWED_ORIGINS:-${LAMBDA_CORS_ORIGINS:-*}}"
68
109
 
69
110
  local jwt_alg="${LAMBDA_JWT_ALGORITHM:-EdDSA}"
70
111
  local svc_route="${LAMBDA_SERVICE_ROUTE_PATH:-/internal}"
@@ -74,22 +115,66 @@ deploy_gateway() {
74
115
 
75
116
  GATEWAY_URLS=()
76
117
  local scope worker_name route_prefix custom_domain url
118
+ local allowed_origins origin_suffixes cors_kv_id config_file disable_public_url
77
119
  for scope in system tenant customer; do
78
120
  worker_name="${APP_NAME}-${STAGE}-${scope}"
79
121
  route_prefix="$(route_prefix_for "$scope")"
80
122
  custom_domain="$(custom_domain_for "$scope")"
123
+ allowed_origins="$(allowed_origins_for "$scope")"
124
+ origin_suffixes="$(origin_suffixes_for "$scope")"
125
+ 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")"
81
129
 
82
130
  info "Deploying $worker_name (scope=$scope route_prefix=$route_prefix)…"
83
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
167
+
84
168
  # Deploy the prebuilt bundle with per-scope vars (flags override the toml).
85
169
  wrangler deploy \
86
170
  --name "$worker_name" \
87
- --config "$WRANGLER_DIR/wrangler.toml" \
171
+ --config "$config_file" \
88
172
  --no-bundle \
89
173
  --var "ENVIRONMENT:$environment" \
90
174
  --var "AUTH_SCOPE:$scope" \
91
175
  --var "ROUTE_PREFIX:$route_prefix" \
92
176
  --var "ALLOWED_ORIGINS:$allowed_origins" \
177
+ --var "ALLOWED_ORIGIN_SUFFIXES:$origin_suffixes" \
93
178
  --var "JWT_ISSUER:$jwt_issuer" \
94
179
  --var "JWT_ALLOWED_ALGORITHMS:$jwt_alg" \
95
180
  --var "SERVICE_ROUTE_PREFIX:$svc_route" \
@@ -97,6 +182,9 @@ deploy_gateway() {
97
182
  --var "SERVICE_JWT_ISSUER:$svc_iss" \
98
183
  --var "SERVICE_JWT_ALLOWED_ALGORITHMS:$svc_alg" >/dev/null
99
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
+
100
188
  # Secrets (piped via stdin).
101
189
  put_secret "$worker_name" BACKEND_URL "$backend_url"
102
190
  put_secret "$worker_name" BACKEND_GATEWAY_SECRET "${LAMBDA_GATEWAY_SECRET:-}"
@@ -108,6 +196,10 @@ deploy_gateway() {
108
196
  if [[ -n "$custom_domain" ]]; then
109
197
  url="https://$custom_domain"
110
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)"
111
203
  else
112
204
  enable_workers_dev "$worker_name"
113
205
  if [[ -n "${CF_WORKERS_SUBDOMAIN:-}" ]]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-craft/aws-cf-stack",
3
- "version": "1.1.1",
3
+ "version": "1.3.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": {
@@ -4,6 +4,11 @@
4
4
  # (assets/gateway/worker.js). deploy-gateway.sh overrides `name` and injects vars
5
5
  # per scope via `wrangler deploy --name <name> --var K:V ...`, and pushes secrets via
6
6
  # `wrangler secret put`. No source compile happens at deploy time.
7
+ #
8
+ # CORS_KV binding (dynamic CORS allowlist) is NOT declared here: a KV binding can't be
9
+ # set via --var, so when CF_CORS_KV_ID_<SCOPE> is set deploy-gateway.sh deploys that
10
+ # scope with a temp copy of this file that appends a [[kv_namespaces]] block. Leaving
11
+ # it out here keeps scopes without a KV id (system/tenant) binding-free.
7
12
  name = "auth-craft-gateway"
8
13
  main = "../assets/gateway/worker.js"
9
14
  compatibility_date = "2024-11-27"