@auth-craft/aws-cf-stack 1.0.0 → 1.1.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 +17 -3
- package/README.md +14 -1
- package/bin/deploy.sh +6 -1
- package/cdk/bin/app.ts +10 -5
- package/cdk/lib/dynamodb-stack.ts +2 -2
- package/cdk/lib/lambda-stack.ts +17 -15
- package/lib/common.sh +122 -14
- package/lib/deploy-lambda.sh +7 -1
- package/package.json +1 -1
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= #
|
|
44
|
+
LAMBDA_APP_BASE_PATH= # recommended — set 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
|
-
|
|
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
|
@@ -46,7 +46,7 @@ So `outputs`/`gateway` look up the api stack `<app>-<stage>-api`. The CDK output
|
|
|
46
46
|
| `admin` | Create the super-admin (idempotent; **skips** if `LAMBDA_SUPER_ADMIN_*` / `LAMBDA_AUTH_SETUP_TOKEN` unset; "already exists" = success). |
|
|
47
47
|
| `gateway` | Deploy the 3 gateway workers. Re-reads the Lambda outputs from CloudFormation if run as a separate invocation. |
|
|
48
48
|
| `all` | `lambda` → `admin` → `gateway`, one run. |
|
|
49
|
-
| `outputs` | Print the live api-stack outputs as JSON (no deploy)
|
|
49
|
+
| `outputs` | Print the live api-stack outputs as JSON (no deploy): `backendUrl`, `apiBasePath`, `dynamodbTable`, and the auth keys read **from the live Lambda config** (`jwtPublicKey`, `jwtIssuer`, `jwtAlgorithm`, `serviceJwtPublicKey`) + `gatewayUrls`/`routePrefixes`. Self-contained — works standalone (no env), so an orchestrator can auto-fill its `*_AUTH_*` vars. |
|
|
50
50
|
|
|
51
51
|
## Requirements (on the deploying machine)
|
|
52
52
|
|
|
@@ -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
|
-
|
|
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)
|
|
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-
|
|
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 =
|
|
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 =
|
|
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
|
package/cdk/lib/lambda-stack.ts
CHANGED
|
@@ -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 =
|
|
34
|
-
const { resourceName } = props;
|
|
33
|
+
const { resourceName, stage } = props;
|
|
35
34
|
|
|
36
|
-
// API base path — obscures the Lambda route surface
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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" &&
|
|
150
|
+
(cd "$CDK_DIR" && cdk bootstrap "aws://${AWS_ACCOUNT_ID}/${region}")
|
|
117
151
|
fi
|
|
118
152
|
}
|
|
119
153
|
|
|
@@ -132,33 +166,107 @@ cfn_output() {
|
|
|
132
166
|
--output text --region "$AWS_REGION" 2>/dev/null || true
|
|
133
167
|
}
|
|
134
168
|
|
|
169
|
+
# Read one environment variable from a deployed Lambda's live config. Lets `outputs`
|
|
170
|
+
# be self-contained (read what the Lambda actually runs with) instead of depending on
|
|
171
|
+
# the caller's process env. Note the runtime var names differ from the LAMBDA_* inputs
|
|
172
|
+
# (e.g. JWT_PUBLIC_KEY, not LAMBDA_JWT_PUBLIC_KEY).
|
|
173
|
+
lambda_env() {
|
|
174
|
+
local fn="$1" key="$2"
|
|
175
|
+
require_cmd aws
|
|
176
|
+
aws lambda get-function-configuration --function-name "$fn" --region "$AWS_REGION" \
|
|
177
|
+
--query "Environment.Variables.${key}" --output text 2>/dev/null | sed 's/^None$//' || true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Derive a gateway worker URL for a scope (custom domain > workers.dev > empty).
|
|
181
|
+
gateway_url_for() {
|
|
182
|
+
local scope="$1" upper dom_var custom
|
|
183
|
+
upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
|
|
184
|
+
dom_var="CF_WORKER_CUSTOM_DOMAIN_${upper}"
|
|
185
|
+
custom="${!dom_var:-}"
|
|
186
|
+
if [[ -n "$custom" ]]; then
|
|
187
|
+
echo "https://$custom"
|
|
188
|
+
elif [[ -n "${CF_WORKERS_SUBDOMAIN:-}" ]]; then
|
|
189
|
+
echo "https://${APP_NAME}-${STAGE}-${scope}.${CF_WORKERS_SUBDOMAIN}.workers.dev"
|
|
190
|
+
else
|
|
191
|
+
echo ""
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
135
195
|
# Print the live deploy outputs of the existing api stack as JSON, for an
|
|
136
|
-
# orchestrator that drives its own steps between stages
|
|
137
|
-
#
|
|
138
|
-
#
|
|
196
|
+
# orchestrator that drives its own steps between stages and wants to AUTO-FILL its
|
|
197
|
+
# own *_AUTH_* vars without copying anything by hand. SELF-CONTAINED: the JWT keys /
|
|
198
|
+
# issuer / alg are read from the LIVE Lambda config (get-function-configuration), so it
|
|
199
|
+
# works when run standalone (no env), falling back to process env only if the read is
|
|
200
|
+
# empty. Includes:
|
|
201
|
+
# - lambdaFunctionUrl, apiBasePath, authSystemName, dynamodbTable
|
|
202
|
+
# - backendUrl (= functionUrl + basePath, the anti-drift value)
|
|
203
|
+
# - jwtPublicKey + jwtIssuer + jwtAlgorithm + serviceJwtPublicKey (from the Lambda)
|
|
204
|
+
# - gatewayUrls{system,tenant,customer} + routePrefixes{...}
|
|
139
205
|
print_outputs_json() {
|
|
140
206
|
require_cmd aws; require_cmd jq
|
|
141
207
|
require_aws_identity
|
|
142
208
|
local api_stack="${APP_NAME}-${STAGE}-api"
|
|
143
|
-
local url base name table backend
|
|
209
|
+
local url base name table fn backend issuer
|
|
144
210
|
url="$(cfn_output "$api_stack" LambdaFunctionUrl)"
|
|
145
211
|
base="$(cfn_output "$api_stack" ApiBasePath)"
|
|
146
212
|
name="$(cfn_output "$api_stack" AuthSystemName)"
|
|
147
213
|
table="$(cfn_output "$api_stack" DynamoDBTableName)"
|
|
214
|
+
fn="$(cfn_output "$api_stack" LambdaFunctionName)"
|
|
148
215
|
[[ -n "$url" && "$url" != "None" ]] \
|
|
149
216
|
|| die "No outputs for stack $api_stack in $AWS_REGION — deploy the lambda stage first."
|
|
150
217
|
backend="${url%/}${base}"
|
|
218
|
+
|
|
219
|
+
# Read the auth-relevant values from the LIVE Lambda config so `outputs` is
|
|
220
|
+
# self-contained (works when run standalone, e.g. deploying the BFF later in a
|
|
221
|
+
# separate process). Fall back to the caller's process env if the Lambda read is
|
|
222
|
+
# empty. Runtime var names differ from the LAMBDA_* inputs.
|
|
223
|
+
local jwtPub jwtIssuer jwtAlg svcPub
|
|
224
|
+
[[ -n "$fn" && "$fn" != "None" ]] || fn="$name"
|
|
225
|
+
jwtPub="$(lambda_env "$fn" JWT_PUBLIC_KEY)"; jwtPub="${jwtPub:-${LAMBDA_JWT_PUBLIC_KEY:-}}"
|
|
226
|
+
jwtIssuer="$(lambda_env "$fn" JWT_ISSUER)"; jwtIssuer="${jwtIssuer:-${LAMBDA_JWT_ISSUER:-$name}}"
|
|
227
|
+
jwtAlg="$(lambda_env "$fn" JWT_ALGORITHM)"; jwtAlg="${jwtAlg:-${LAMBDA_JWT_ALGORITHM:-EdDSA}}"
|
|
228
|
+
svcPub="$(lambda_env "$fn" INTERNAL_JWT_PUBLIC_KEY)"; svcPub="${svcPub:-${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}}"
|
|
229
|
+
|
|
151
230
|
jq -n \
|
|
152
231
|
--arg url "$url" --arg base "$base" --arg name "$name" \
|
|
153
232
|
--arg table "$table" --arg backend "$backend" \
|
|
154
|
-
--arg stage "$STAGE" --arg project "$PROJECT" --arg region "$AWS_REGION" \
|
|
233
|
+
--arg stage "$STAGE" --arg project "${PROJECT:-default}" --arg region "$AWS_REGION" \
|
|
234
|
+
--arg jwtPub "$jwtPub" --arg jwtIssuer "$jwtIssuer" \
|
|
235
|
+
--arg jwtAlg "$jwtAlg" \
|
|
236
|
+
--arg svcPub "$svcPub" \
|
|
237
|
+
--arg gwSystem "$(gateway_url_for system)" \
|
|
238
|
+
--arg gwTenant "$(gateway_url_for tenant)" \
|
|
239
|
+
--arg gwCustomer "$(gateway_url_for customer)" \
|
|
240
|
+
--arg rpSystem "${CF_WORKER_ROUTE_PREFIX_SYSTEM:-/system}" \
|
|
241
|
+
--arg rpTenant "${CF_WORKER_ROUTE_PREFIX_TENANT:-/tenant}" \
|
|
242
|
+
--arg rpCustomer "${CF_WORKER_ROUTE_PREFIX_CUSTOMER:-/customer}" \
|
|
155
243
|
'{stage:$stage, project:$project, region:$region,
|
|
156
244
|
lambdaFunctionUrl:$url, apiBasePath:$base, authSystemName:$name,
|
|
157
|
-
dynamodbTable:$table, backendUrl:$backend
|
|
245
|
+
dynamodbTable:$table, backendUrl:$backend,
|
|
246
|
+
jwtPublicKey:$jwtPub, jwtIssuer:$jwtIssuer, jwtAlgorithm:$jwtAlg,
|
|
247
|
+
serviceJwtPublicKey:$svcPub,
|
|
248
|
+
gatewayUrls:{system:$gwSystem, tenant:$gwTenant, customer:$gwCustomer},
|
|
249
|
+
routePrefixes:{system:$rpSystem, tenant:$rpTenant, customer:$rpCustomer}}'
|
|
158
250
|
}
|
|
159
251
|
|
|
160
|
-
# ──
|
|
252
|
+
# ── Tool command resolution (#7: let a consumer pin its own binaries) ─────────
|
|
253
|
+
# AUTH_CRAFT_WRANGLER_CMD / AUTH_CRAFT_CDK_CMD override how we invoke the tools.
|
|
254
|
+
# Default to `npx --yes <tool>`. A consumer with a pinned binary (e.g. `pnpm wrangler`
|
|
255
|
+
# or an absolute path) sets these to avoid version drift vs `npx --yes` pulling latest.
|
|
161
256
|
wrangler() {
|
|
162
|
-
|
|
163
|
-
|
|
257
|
+
if [[ -n "${AUTH_CRAFT_WRANGLER_CMD:-}" ]]; then
|
|
258
|
+
CI=true WRANGLER_SEND_METRICS=false ${AUTH_CRAFT_WRANGLER_CMD} "$@"
|
|
259
|
+
else
|
|
260
|
+
require_cmd npx
|
|
261
|
+
CI=true WRANGLER_SEND_METRICS=false npx --yes wrangler "$@"
|
|
262
|
+
fi
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
cdk() {
|
|
266
|
+
if [[ -n "${AUTH_CRAFT_CDK_CMD:-}" ]]; then
|
|
267
|
+
${AUTH_CRAFT_CDK_CMD} "$@"
|
|
268
|
+
else
|
|
269
|
+
require_cmd npx
|
|
270
|
+
npx cdk "$@"
|
|
271
|
+
fi
|
|
164
272
|
}
|
package/lib/deploy-lambda.sh
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "1.1.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": {
|