@auth-craft/aws-cf-stack 1.0.0 → 1.0.1

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 CHANGED
@@ -10,9 +10,17 @@
10
10
  #
11
11
  # Required vs optional is marked per line.
12
12
 
13
- # ─── Stage / project ─────────────────────────────────────────────────────────
13
+ # ─── Stage / project / naming ────────────────────────────────────────────────
14
14
  STAGE=dev # optional (flag --stage), default dev | staging | prod
15
+ # also accepts production -> prod, development -> dev
15
16
  PROJECT=default # optional (flag --project), default "default" (multi-tenant naming)
17
+ AUTH_CRAFT_APP_NAME= # optional (flag --app-name) — override the app/resource name
18
+ # (e.g. snapshot-auth). Unset = auth-craft[-<project>]. Drives
19
+ # stack names, table, Lambda fn, worker names.
20
+
21
+ # ─── Tool command overrides (optional — pin your own binaries) ───────────────
22
+ AUTH_CRAFT_WRANGLER_CMD= # optional — e.g. "pnpm wrangler"; default "npx --yes wrangler"
23
+ AUTH_CRAFT_CDK_CMD= # optional — e.g. "pnpm cdk"; default "npx cdk"
16
24
 
17
25
  # ─── AWS ─────────────────────────────────────────────────────────────────────
18
26
  LAMBDA_AWS_REGION=us-east-1 # optional (flag --region), default us-east-1
@@ -33,7 +41,9 @@ LAMBDA_JWT_REFRESH_TOKEN_AUDIENCE=refresh # optional, default refresh
33
41
  LAMBDA_GATEWAY_SECRET= # required for staging/prod (flag --gateway-secret)
34
42
 
35
43
  # ─── API base path (stable obfuscated path; keep it fixed across deploys) ─────
36
- LAMBDA_APP_BASE_PATH= # optionalif unset, a random path is generated each deploy
44
+ LAMBDA_APP_BASE_PATH= # recommendedset a stable, non-guessable value (e.g. /api-7f3a).
45
+ # Unset = fixed default '/api' (NOT random) + a warning. Never
46
+ # changes between deploys, so clients/workers don't break.
37
47
 
38
48
  # ─── Cloudflare gateway workers ──────────────────────────────────────────────
39
49
  CLOUDFLARE_API_TOKEN= # required for gateway (flag --cf-api-token)
@@ -59,7 +69,11 @@ LAMBDA_INTERNAL_JWT_ISSUER=auth-craft # optional
59
69
  LAMBDA_SERVICE_ROUTE_PATH=/internal # optional, default /internal
60
70
 
61
71
  # ─── Gateway JWT verification on the Lambda (optional) ────────────────────────
62
- LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX= # optional 64 hex chars; Lambda verifies worker JWT
72
+ # One key pair: the worker signs with CF_GATEWAY_JWT_PRIVATE_KEY (JWK base64, above),
73
+ # the Lambda verifies with the matching public key as hex below. If you set ONLY the
74
+ # private JWK, the package DERIVES this hex from it automatically (single source, no drift) —
75
+ # so leave this blank unless you intentionally want a different verify key.
76
+ LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX= # optional — 64 hex chars; auto-derived from CF_GATEWAY_JWT_PRIVATE_KEY if unset
63
77
  LAMBDA_GATEWAY_JWT_ISSUER=bff-worker # optional
64
78
  LAMBDA_GATEWAY_JWT_AUDIENCE=auth-craft:backend # optional
65
79
  LAMBDA_GATEWAY_JWT_HEADER=x-gateway-auth # optional
package/README.md CHANGED
@@ -147,6 +147,19 @@ time). Bump the version to deploy newer auth-craft.
147
147
  `gateway` from the **same** value.
148
148
  - **`gateway` can't read Lambda outputs** — deploy the `lambda` stage first (it reads the live
149
149
  `<app>-<stage>-api` stack).
150
+ - **Queue email silently not sent** — with `LAMBDA_EMAIL_MODE=queue`, the SQS queue must exist
151
+ **before** the `lambda` stage: pass `LAMBDA_EMAIL_QUEUE_ARN`/`LAMBDA_EMAIL_QUEUE_URL` of an
152
+ already-deployed queue. The stack grants the Lambda `sqs:SendMessage` on that ARN, but it
153
+ can't create a cross-stack queue — deploy your email/queue stack first, then this one.
154
+
155
+ ## Email delivery modes
156
+
157
+ `LAMBDA_EMAIL_MODE` = `console` (default) | `smtp` | `queue`.
158
+ - **queue** (e.g. an existing SQS-backed email service): set `LAMBDA_EMAIL_QUEUE_ARN` +
159
+ `LAMBDA_EMAIL_QUEUE_URL` (+ region/envelope, see `.env.example`). **Ordering matters** — the
160
+ queue is a cross-stack resource this package does not create; deploy it first. The `lambda`
161
+ stage then grants the auth Lambda `sqs:SendMessage` on that ARN automatically.
162
+ - **smtp**: set `LAMBDA_SMTP_*`. **console**: logs only (dev).
150
163
 
151
164
  ## How it's built (maintainers)
152
165
 
package/bin/deploy.sh CHANGED
@@ -41,8 +41,9 @@ Commands:
41
41
  derived backendUrl (FunctionUrl+ApiBasePath) — for self-driving callers.
42
42
 
43
43
  Options (each also reads an env var; the flag wins):
44
- --stage <dev|staging|prod> Stage (env STAGE, default dev)
44
+ --stage <dev|staging|prod> Stage (env STAGE, default dev; accepts production/development)
45
45
  --project <name> CDK project (env PROJECT, default "default")
46
+ --app-name <name> Override app name (env AUTH_CRAFT_APP_NAME; default auth-craft[-project])
46
47
  --region <region> AWS region (env LAMBDA_AWS_REGION, default us-east-1)
47
48
  --gateway-secret <v> env LAMBDA_GATEWAY_SECRET
48
49
  --jwt-public-key <v> env LAMBDA_JWT_PUBLIC_KEY
@@ -76,6 +77,7 @@ while [[ $# -gt 0 ]]; do
76
77
  lambda|gateway|admin|all|outputs) COMMAND="$1"; shift ;;
77
78
  --stage) FLAG[STAGE]="${2:?}"; shift 2 ;;
78
79
  --project) FLAG[PROJECT]="${2:?}"; shift 2 ;;
80
+ --app-name) FLAG[AUTH_CRAFT_APP_NAME]="${2:?}"; shift 2 ;;
79
81
  --region) FLAG[LAMBDA_AWS_REGION]="${2:?}"; shift 2 ;;
80
82
  --gateway-secret) FLAG[LAMBDA_GATEWAY_SECRET]="${2:?}"; shift 2 ;;
81
83
  --jwt-public-key) FLAG[LAMBDA_JWT_PUBLIC_KEY]="${2:?}"; shift 2 ;;
@@ -103,6 +105,9 @@ export AWS_REGION="${LAMBDA_AWS_REGION:-${AWS_REGION:-us-east-1}}"
103
105
  export LAMBDA_AWS_REGION="$AWS_REGION"
104
106
 
105
107
  derive_naming
108
+ # #5 — derive the Lambda's gateway-JWT public hex from the worker's private JWK when
109
+ # only the private key is provided, so the verify/sign pair can never drift.
110
+ derive_gateway_jwt_public_hex
106
111
 
107
112
  # ── Dispatch ──────────────────────────────────────────────────────────────────
108
113
  print_summary() {
package/cdk/bin/app.ts CHANGED
@@ -5,7 +5,10 @@ import { LambdaStack } from '../lib/lambda-stack';
5
5
 
6
6
  const app = new cdk.App();
7
7
 
8
- const stage = process.env.STAGE || 'dev';
8
+ // Stage normalize the common aliases so the stack accepts either form (the shell
9
+ // normalizes too; this keeps the CDK app correct if invoked directly).
10
+ const rawStage = process.env.STAGE || 'dev';
11
+ const stage = rawStage === 'production' ? 'prod' : rawStage === 'development' ? 'dev' : rawStage;
9
12
  const project = process.env.PROJECT || 'default'; // Client project name
10
13
 
11
14
  const env = {
@@ -15,13 +18,13 @@ const env = {
15
18
 
16
19
  // ==================== Naming Convention ====================
17
20
  // Stack naming: {app}-{env}-{layer}
18
- // - app: "auth-craft" (base) or "auth-craft-{project}" (multi-project)
21
+ // - app: AUTH_CRAFT_APP_NAME if set, else "auth-craft" (base) / "auth-craft-{project}"
19
22
  // - env: "dev", "staging", "prod"
20
23
  // - layer: "data", "api"
21
- // Examples: auth-craft-dev-data, auth-craft-staging-api, auth-craft-prod-data
22
- // Multi-project: auth-craft-ecommerce-dev-data, auth-craft-saas-prod-api
24
+ // Examples: auth-craft-dev-data, auth-craft-staging-api, snapshot-auth-prod-api
23
25
 
24
- const appName = project === 'default' ? 'auth-craft' : `auth-craft-${project}`;
26
+ const appName =
27
+ process.env.AUTH_CRAFT_APP_NAME || (project === 'default' ? 'auth-craft' : `auth-craft-${project}`);
25
28
 
26
29
  console.log(`🚀 Deploying Auth Craft to AWS`);
27
30
  console.log(` App: ${appName}`);
@@ -49,6 +52,7 @@ const dynamodbStack = new DynamoDBStack(app, `${appName}-${stage}-data`, {
49
52
  description: `Auth Craft DynamoDB (${appName}-${stage})`,
50
53
  tags: costTags,
51
54
  resourceName: getResourceName(),
55
+ stage,
52
56
  });
53
57
 
54
58
  // Lambda Stack (api layer)
@@ -59,6 +63,7 @@ const lambdaStack = new LambdaStack(app, `${appName}-${stage}-api`, {
59
63
  table: dynamodbStack.table,
60
64
  tags: costTags,
61
65
  resourceName: getResourceName(),
66
+ stage,
62
67
  });
63
68
 
64
69
  // Lambda stack depends on DynamoDB stack
@@ -4,6 +4,7 @@ import type { Construct } from 'constructs';
4
4
 
5
5
  interface DynamoDBStackProps extends cdk.StackProps {
6
6
  resourceName: string; // e.g., "auth-craft-dev", "auth-craft-prod"
7
+ stage: string; // "dev" | "staging" | "prod" — passed from the app, single source of truth
7
8
  }
8
9
 
9
10
  export class DynamoDBStack extends cdk.Stack {
@@ -12,8 +13,7 @@ export class DynamoDBStack extends cdk.Stack {
12
13
  constructor(scope: Construct, id: string, props: DynamoDBStackProps) {
13
14
  super(scope, id, props);
14
15
 
15
- const stage = process.env.STAGE || 'dev';
16
- const { resourceName } = props;
16
+ const { resourceName, stage } = props;
17
17
 
18
18
  // Resource naming: {app}-{env}
19
19
  // Examples: auth-craft-dev, auth-craft-staging, auth-craft-prod
@@ -1,4 +1,3 @@
1
- import * as crypto from 'node:crypto';
2
1
  import * as path from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
3
  import * as cdk from 'aws-cdk-lib';
@@ -20,6 +19,7 @@ const LAMBDA_ASSET_DIR = path.resolve(__dirname, '../../assets/lambda');
20
19
  interface LambdaStackProps extends cdk.StackProps {
21
20
  table: dynamodb.ITable;
22
21
  resourceName: string; // e.g., "auth-craft-dev", "auth-craft-prod"
22
+ stage: string; // "dev" | "staging" | "prod" — passed from the app, single source of truth
23
23
  }
24
24
 
25
25
  export class LambdaStack extends cdk.Stack {
@@ -30,23 +30,25 @@ export class LambdaStack extends cdk.Stack {
30
30
  constructor(scope: Construct, id: string, props: LambdaStackProps) {
31
31
  super(scope, id, props);
32
32
 
33
- const stage = process.env.STAGE || 'dev';
34
- const { resourceName } = props;
33
+ const { resourceName, stage } = props;
35
34
 
36
- // API base path — obscures the Lambda route surface.
37
- // Prefer an explicit, stable value from env (CI/CD: LAMBDA_APP_BASE_PATH) so
38
- // it does NOT change between deploys (a random one breaks every client that
39
- // has the path configured). Falls back to a one-off random path only when
40
- // unset. Normalized to a single leading '/', no trailing '/'.
35
+ // API base path — obscures the Lambda route surface and is forwarded onto by
36
+ // the gateway worker (BACKEND_URL = FunctionUrl + this path). It MUST be stable
37
+ // across deploys a value that changes breaks every client/worker that has the
38
+ // path configured. So this package NEVER randomizes it: take the explicit env
39
+ // value, else a fixed default. Normalized to a single leading '/', no trailing '/'.
40
+ const DEFAULT_BASE_PATH = '/api';
41
41
  const configuredBasePath = process.env.LAMBDA_APP_BASE_PATH?.trim();
42
- if (configuredBasePath) {
43
- const withLeading = configuredBasePath.startsWith('/') ? configuredBasePath : `/${configuredBasePath}`;
44
- this.apiBasePath = withLeading.endsWith('/') ? withLeading.slice(0, -1) : withLeading;
45
- } else {
46
- // Format: /{16-char-random-hex} e.g., /a1b2c3d4e5f6g7h8
47
- const randomPath = crypto.randomBytes(8).toString('hex');
48
- this.apiBasePath = `/${randomPath}`;
42
+ const rawBasePath = configuredBasePath || DEFAULT_BASE_PATH;
43
+ if (!configuredBasePath) {
44
+ console.warn(
45
+ `⚠️ LAMBDA_APP_BASE_PATH not set — using the fixed default '${DEFAULT_BASE_PATH}'. ` +
46
+ 'Set a stable, non-guessable value (e.g. /api-7f3a) to obscure the route surface; ' +
47
+ 'it must stay the same across deploys.',
48
+ );
49
49
  }
50
+ const withLeading = rawBasePath.startsWith('/') ? rawBasePath : `/${rawBasePath}`;
51
+ this.apiBasePath = withLeading.endsWith('/') ? withLeading.slice(0, -1) : withLeading;
50
52
 
51
53
  // Get environment variables
52
54
  // CI/CD passes secrets with LAMBDA_ prefix — strip when injecting into Lambda runtime
package/lib/common.sh CHANGED
@@ -76,24 +76,59 @@ load_env_file() {
76
76
  }
77
77
 
78
78
  # ── Stage / naming derivation (mirrors the CI workflow + CDK app.ts) ──────────
79
- # Inputs: STAGE, PROJECT. Exports APP_NAME, RESOURCE_NAME, RUNTIME_NODE_ENV.
79
+ # Inputs: STAGE, PROJECT, optional AUTH_CRAFT_APP_NAME.
80
+ # Exports STAGE (normalized), PROJECT, APP_NAME, RESOURCE_NAME, RUNTIME_NODE_ENV.
80
81
  derive_naming() {
81
82
  STAGE="${STAGE:-dev}"
82
83
  PROJECT="${PROJECT:-default}"
84
+
85
+ # #2 Normalize common stage aliases so a consumer can pass either form.
86
+ # production -> prod, development -> dev, staging stays staging.
87
+ case "$STAGE" in
88
+ production) STAGE="prod" ;;
89
+ development) STAGE="dev" ;;
90
+ esac
83
91
  case "$STAGE" in
84
92
  dev|staging|prod) ;;
85
- *) die "Invalid --stage: $STAGE (expected dev|staging|prod)" ;;
93
+ *) die "Invalid --stage: $STAGE (expected dev|staging|prod, or production/development)" ;;
86
94
  esac
87
- if [[ "$PROJECT" == "default" ]]; then
95
+
96
+ # #1 App name: explicit override wins, else derive from PROJECT (default auth-craft).
97
+ # Lets a consumer brand it (e.g. snapshot-auth) without changing the default.
98
+ if [[ -n "${AUTH_CRAFT_APP_NAME:-}" ]]; then
99
+ APP_NAME="$AUTH_CRAFT_APP_NAME"
100
+ elif [[ "$PROJECT" == "default" ]]; then
88
101
  APP_NAME="auth-craft"
89
102
  else
90
103
  APP_NAME="auth-craft-${PROJECT}"
91
104
  fi
105
+
92
106
  RESOURCE_NAME="${APP_NAME}-${STAGE}"
93
107
  RUNTIME_NODE_ENV="$([[ "$STAGE" == "prod" ]] && echo production || echo development)"
94
108
  export STAGE PROJECT APP_NAME RESOURCE_NAME RUNTIME_NODE_ENV
95
109
  }
96
110
 
111
+ # ── Gateway JWT key pair (#5: one source, derive the rest) ────────────────────
112
+ # The worker signs a Gateway JWT with an Ed25519 private key (JWK base64,
113
+ # CF_GATEWAY_JWT_PRIVATE_KEY); the Lambda verifies it with the matching public key
114
+ # as raw hex (LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX). These are ONE key pair passed as two
115
+ # vars in two formats — easy to drift. If the private JWK is set but the public hex is
116
+ # not, derive the hex from the JWK so both always come from a single source.
117
+ derive_gateway_jwt_public_hex() {
118
+ [[ -z "${CF_GATEWAY_JWT_PRIVATE_KEY:-}" ]] && return 0
119
+ [[ -n "${LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX:-}" ]] && return 0
120
+ require_cmd node
121
+ local hex
122
+ hex="$(CF_GATEWAY_JWT_PRIVATE_KEY="$CF_GATEWAY_JWT_PRIVATE_KEY" node -e '
123
+ const raw = process.env.CF_GATEWAY_JWT_PRIVATE_KEY;
124
+ const jwk = JSON.parse(Buffer.from(raw, "base64").toString());
125
+ if (!jwk.x) throw new Error("CF_GATEWAY_JWT_PRIVATE_KEY JWK has no public component (x)");
126
+ process.stdout.write(Buffer.from(jwk.x, "base64url").toString("hex"));
127
+ ')" || die "Failed to derive gateway JWT public hex from CF_GATEWAY_JWT_PRIVATE_KEY"
128
+ export LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX="$hex"
129
+ info "Derived LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX from CF_GATEWAY_JWT_PRIVATE_KEY (single source)."
130
+ }
131
+
97
132
  # ── AWS ───────────────────────────────────────────────────────────────────────
98
133
  require_aws_identity() {
99
134
  require_cmd aws
@@ -107,13 +142,12 @@ require_aws_identity() {
107
142
  }
108
143
 
109
144
  ensure_bootstrap() {
110
- require_cmd npx
111
145
  local region="${1:-$AWS_REGION}"
112
146
  if aws ssm get-parameter --name /cdk-bootstrap/hnb659fds/version --region "$region" >/dev/null 2>&1; then
113
147
  info "CDK already bootstrapped in $region"
114
148
  else
115
149
  info "Bootstrapping CDK in $region…"
116
- (cd "$CDK_DIR" && npx cdk bootstrap "aws://${AWS_ACCOUNT_ID}/${region}")
150
+ (cd "$CDK_DIR" && cdk bootstrap "aws://${AWS_ACCOUNT_ID}/${region}")
117
151
  fi
118
152
  }
119
153
 
@@ -132,15 +166,33 @@ cfn_output() {
132
166
  --output text --region "$AWS_REGION" 2>/dev/null || true
133
167
  }
134
168
 
169
+ # Derive a gateway worker URL for a scope (custom domain > workers.dev > empty).
170
+ gateway_url_for() {
171
+ local scope="$1" upper dom_var custom
172
+ upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
173
+ dom_var="CF_WORKER_CUSTOM_DOMAIN_${upper}"
174
+ custom="${!dom_var:-}"
175
+ if [[ -n "$custom" ]]; then
176
+ echo "https://$custom"
177
+ elif [[ -n "${CF_WORKERS_SUBDOMAIN:-}" ]]; then
178
+ echo "https://${APP_NAME}-${STAGE}-${scope}.${CF_WORKERS_SUBDOMAIN}.workers.dev"
179
+ else
180
+ echo ""
181
+ fi
182
+ }
183
+
135
184
  # Print the live deploy outputs of the existing api stack as JSON, for an
136
- # orchestrator that drives its own steps between stages. Keys mirror what the
137
- # stages produce: lambdaFunctionUrl, apiBasePath, authSystemName, dynamodbTable,
138
- # and the derived backendUrl (= functionUrl + basePath, the anti-drift value).
185
+ # orchestrator that drives its own steps between stages and wants to AUTO-FILL its
186
+ # own *_AUTH_* vars without copying anything by hand. Includes:
187
+ # - lambdaFunctionUrl, apiBasePath, authSystemName, dynamodbTable
188
+ # - backendUrl (= functionUrl + basePath, the anti-drift value)
189
+ # - jwtPublicKey + jwtIssuer (what verifiers need; issuer = LAMBDA_JWT_ISSUER || authSystemName)
190
+ # - gatewayUrls{system,tenant,customer} + routePrefixes{...}
139
191
  print_outputs_json() {
140
192
  require_cmd aws; require_cmd jq
141
193
  require_aws_identity
142
194
  local api_stack="${APP_NAME}-${STAGE}-api"
143
- local url base name table backend
195
+ local url base name table backend issuer
144
196
  url="$(cfn_output "$api_stack" LambdaFunctionUrl)"
145
197
  base="$(cfn_output "$api_stack" ApiBasePath)"
146
198
  name="$(cfn_output "$api_stack" AuthSystemName)"
@@ -148,17 +200,47 @@ print_outputs_json() {
148
200
  [[ -n "$url" && "$url" != "None" ]] \
149
201
  || die "No outputs for stack $api_stack in $AWS_REGION — deploy the lambda stage first."
150
202
  backend="${url%/}${base}"
203
+ issuer="${LAMBDA_JWT_ISSUER:-$name}"
151
204
  jq -n \
152
205
  --arg url "$url" --arg base "$base" --arg name "$name" \
153
206
  --arg table "$table" --arg backend "$backend" \
154
- --arg stage "$STAGE" --arg project "$PROJECT" --arg region "$AWS_REGION" \
207
+ --arg stage "$STAGE" --arg project "${PROJECT:-default}" --arg region "$AWS_REGION" \
208
+ --arg jwtPub "${LAMBDA_JWT_PUBLIC_KEY:-}" --arg jwtIssuer "$issuer" \
209
+ --arg jwtAlg "${LAMBDA_JWT_ALGORITHM:-EdDSA}" \
210
+ --arg svcPub "${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}" \
211
+ --arg gwSystem "$(gateway_url_for system)" \
212
+ --arg gwTenant "$(gateway_url_for tenant)" \
213
+ --arg gwCustomer "$(gateway_url_for customer)" \
214
+ --arg rpSystem "${CF_WORKER_ROUTE_PREFIX_SYSTEM:-/system}" \
215
+ --arg rpTenant "${CF_WORKER_ROUTE_PREFIX_TENANT:-/tenant}" \
216
+ --arg rpCustomer "${CF_WORKER_ROUTE_PREFIX_CUSTOMER:-/customer}" \
155
217
  '{stage:$stage, project:$project, region:$region,
156
218
  lambdaFunctionUrl:$url, apiBasePath:$base, authSystemName:$name,
157
- dynamodbTable:$table, backendUrl:$backend}'
219
+ dynamodbTable:$table, backendUrl:$backend,
220
+ jwtPublicKey:$jwtPub, jwtIssuer:$jwtIssuer, jwtAlgorithm:$jwtAlg,
221
+ serviceJwtPublicKey:$svcPub,
222
+ gatewayUrls:{system:$gwSystem, tenant:$gwTenant, customer:$gwCustomer},
223
+ routePrefixes:{system:$rpSystem, tenant:$rpTenant, customer:$rpCustomer}}'
158
224
  }
159
225
 
160
- # ── wrangler wrapper (quiet, non-interactive) ─────────────────────────────────
226
+ # ── Tool command resolution (#7: let a consumer pin its own binaries) ─────────
227
+ # AUTH_CRAFT_WRANGLER_CMD / AUTH_CRAFT_CDK_CMD override how we invoke the tools.
228
+ # Default to `npx --yes <tool>`. A consumer with a pinned binary (e.g. `pnpm wrangler`
229
+ # or an absolute path) sets these to avoid version drift vs `npx --yes` pulling latest.
161
230
  wrangler() {
162
- require_cmd npx
163
- CI=true WRANGLER_SEND_METRICS=false npx --yes wrangler "$@"
231
+ if [[ -n "${AUTH_CRAFT_WRANGLER_CMD:-}" ]]; then
232
+ CI=true WRANGLER_SEND_METRICS=false ${AUTH_CRAFT_WRANGLER_CMD} "$@"
233
+ else
234
+ require_cmd npx
235
+ CI=true WRANGLER_SEND_METRICS=false npx --yes wrangler "$@"
236
+ fi
237
+ }
238
+
239
+ cdk() {
240
+ if [[ -n "${AUTH_CRAFT_CDK_CMD:-}" ]]; then
241
+ ${AUTH_CRAFT_CDK_CMD} "$@"
242
+ else
243
+ require_cmd npx
244
+ npx cdk "$@"
245
+ fi
164
246
  }
@@ -8,6 +8,12 @@
8
8
  deploy_lambda() {
9
9
  header "Deploy Lambda (stage=$STAGE project=$PROJECT region=$AWS_REGION)"
10
10
 
11
+ # #5 — ensure the gateway-JWT public hex is derived BEFORE cdk reads process.env
12
+ # (the bundled lambda-stack reads LAMBDA_GATEWAY_JWT_PUBLIC_KEY_HEX). Idempotent:
13
+ # no-op if already set or no private JWK given. Belt-and-suspenders so the pair is
14
+ # wired even if deploy_lambda is invoked directly (not via the bin entrypoint).
15
+ derive_gateway_jwt_public_hex
16
+
11
17
  require_aws_identity
12
18
  ensure_bootstrap "$AWS_REGION"
13
19
 
@@ -24,7 +30,7 @@ deploy_lambda() {
24
30
  cd "$CDK_DIR"
25
31
  CDK_DEFAULT_ACCOUNT="$AWS_ACCOUNT_ID" \
26
32
  CDK_DEFAULT_REGION="$AWS_REGION" \
27
- npx cdk deploy --all --require-approval never --outputs-file "$OUTPUTS_FILE"
33
+ cdk deploy --all --require-approval never --outputs-file "$OUTPUTS_FILE"
28
34
  )
29
35
 
30
36
  [[ -f "$OUTPUTS_FILE" ]] || die "CDK outputs file not written: $OUTPUTS_FILE"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-craft/aws-cf-stack",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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": {