@auth-craft/aws-cf-stack 1.7.1 → 1.9.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 +24 -4
- package/README.md +61 -11
- package/assets/admin/cli.mjs +82 -82
- package/assets/gateway/worker.js +1 -1
- package/assets/lambda/index.mjs +42 -37
- package/bin/deploy.sh +13 -1
- package/cdk/bin/app.ts +28 -22
- package/cdk/lib/dynamodb-stack.ts +4 -6
- package/cdk/lib/lambda-stack.ts +109 -11
- package/lib/common.sh +34 -14
- package/lib/deploy-gateway.sh +2 -2
- package/lib/deploy-lambda.sh +2 -2
- package/package.json +10 -6
package/bin/deploy.sh
CHANGED
|
@@ -44,6 +44,9 @@ Options (each also reads an env var; the flag wins):
|
|
|
44
44
|
--stage <dev|staging|prod> Stage (env STAGE, default dev; accepts production/development)
|
|
45
45
|
--project <name> CDK project (env PROJECT, default "default")
|
|
46
46
|
--app-name <name> Override app name (env AUTH_CRAFT_APP_NAME; default auth-craft[-project])
|
|
47
|
+
--layer <name> Layer namespace within the app (env LAYER, default "auth").
|
|
48
|
+
Resource names are {app}-{env}-{layer}-{component}, so a larger
|
|
49
|
+
app with other layers (e.g. billing) reuses this CLI per layer.
|
|
47
50
|
--region <region> AWS region (env LAMBDA_AWS_REGION, default us-east-1)
|
|
48
51
|
--gateway-secret <v> env LAMBDA_GATEWAY_SECRET
|
|
49
52
|
--jwt-public-key <v> env LAMBDA_JWT_PUBLIC_KEY
|
|
@@ -51,6 +54,12 @@ Options (each also reads an env var; the flag wins):
|
|
|
51
54
|
--edge-gate-secret <v> env CF_EDGE_GATE_SECRET
|
|
52
55
|
--cf-account-id <v> env CLOUDFLARE_ACCOUNT_ID
|
|
53
56
|
--cf-api-token <v> env CLOUDFLARE_API_TOKEN
|
|
57
|
+
--log-retention-days <n> CloudWatch log retention (env LAMBDA_LOG_RETENTION_DAYS).
|
|
58
|
+
Default: 3 in dev, 30 otherwise. Use 'infinite' to never
|
|
59
|
+
expire. Only CloudWatch's fixed set is valid
|
|
60
|
+
(1,3,5,7,14,30,60,90,120,150,180,365,400,545,731,…).
|
|
61
|
+
--tracing <mode> X-Ray tracing: active|disabled|pass_through
|
|
62
|
+
(env LAMBDA_TRACING; default disabled in dev, active otherwise)
|
|
54
63
|
--outputs-file <path> (lambda) write CDK outputs JSON here for your own steps
|
|
55
64
|
--env-file <path> Optional: source a file first (flags/env still override)
|
|
56
65
|
--skip-build No-op (artifacts are prebuilt in the package)
|
|
@@ -78,6 +87,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
78
87
|
--stage) FLAG[STAGE]="${2:?}"; shift 2 ;;
|
|
79
88
|
--project) FLAG[PROJECT]="${2:?}"; shift 2 ;;
|
|
80
89
|
--app-name) FLAG[AUTH_CRAFT_APP_NAME]="${2:?}"; shift 2 ;;
|
|
90
|
+
--layer) FLAG[LAYER]="${2:?}"; shift 2 ;;
|
|
81
91
|
--region) FLAG[LAMBDA_AWS_REGION]="${2:?}"; shift 2 ;;
|
|
82
92
|
--gateway-secret) FLAG[LAMBDA_GATEWAY_SECRET]="${2:?}"; shift 2 ;;
|
|
83
93
|
--jwt-public-key) FLAG[LAMBDA_JWT_PUBLIC_KEY]="${2:?}"; shift 2 ;;
|
|
@@ -85,6 +95,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
85
95
|
--edge-gate-secret) FLAG[CF_EDGE_GATE_SECRET]="${2:?}"; shift 2 ;;
|
|
86
96
|
--cf-account-id) FLAG[CLOUDFLARE_ACCOUNT_ID]="${2:?}"; shift 2 ;;
|
|
87
97
|
--cf-api-token) FLAG[CLOUDFLARE_API_TOKEN]="${2:?}"; shift 2 ;;
|
|
98
|
+
--log-retention-days) FLAG[LAMBDA_LOG_RETENTION_DAYS]="${2:?}"; shift 2 ;;
|
|
99
|
+
--tracing) FLAG[LAMBDA_TRACING]="${2:?}"; shift 2 ;;
|
|
88
100
|
--outputs-file) OUTPUTS_FILE="${2:?}"; export OUTPUTS_FILE; shift 2 ;;
|
|
89
101
|
--env-file) ENV_FILE="${2:?}"; shift 2 ;;
|
|
90
102
|
--skip-build) shift ;;
|
|
@@ -142,7 +154,7 @@ case "$COMMAND" in
|
|
|
142
154
|
# if not already exported in this process (e.g. snapshot drove `lambda` separately).
|
|
143
155
|
if [[ -z "${LAMBDA_FUNCTION_URL:-}" || -z "${API_BASE_PATH:-}" ]]; then
|
|
144
156
|
require_aws_identity
|
|
145
|
-
api_stack="${
|
|
157
|
+
api_stack="${RESOURCE_NAME}-api"
|
|
146
158
|
LAMBDA_FUNCTION_URL="$(cfn_output "$api_stack" LambdaFunctionUrl)"
|
|
147
159
|
API_BASE_PATH="$(cfn_output "$api_stack" ApiBasePath)"
|
|
148
160
|
AUTH_SYSTEM_NAME="$(cfn_output "$api_stack" AuthSystemName)"
|
package/cdk/bin/app.ts
CHANGED
|
@@ -10,6 +10,7 @@ const app = new cdk.App();
|
|
|
10
10
|
const rawStage = process.env.STAGE || 'dev';
|
|
11
11
|
const stage = rawStage === 'production' ? 'prod' : rawStage === 'development' ? 'dev' : rawStage;
|
|
12
12
|
const project = process.env.PROJECT || 'default'; // Client project name
|
|
13
|
+
const layer = process.env.LAYER || 'auth'; // Layer namespace within the app (default "auth")
|
|
13
14
|
|
|
14
15
|
const env = {
|
|
15
16
|
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
@@ -17,18 +18,24 @@ const env = {
|
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
// ==================== Naming Convention ====================
|
|
20
|
-
//
|
|
21
|
-
// - app:
|
|
22
|
-
// - env:
|
|
23
|
-
// - layer: "
|
|
24
|
-
//
|
|
21
|
+
// Pattern: {app}-{env}-{layer}-{component}
|
|
22
|
+
// - app: AUTH_CRAFT_APP_NAME if set, else "auth-craft" (base) / "auth-craft-{project}"
|
|
23
|
+
// - env: "dev", "staging", "prod"
|
|
24
|
+
// - layer: LAYER (default "auth") — namespaces this deploy within a larger app
|
|
25
|
+
// - component: per-resource suffix ("data" for the table, "api" for the Lambda)
|
|
26
|
+
// The {app}-{env}-{layer} base is the resourceName (AUTH_SYSTEM_NAME / JWT issuer);
|
|
27
|
+
// it carries no component so the iss claim stays stable across resources.
|
|
28
|
+
// Examples: snapshot-commerce-prod-auth-data, snapshot-commerce-prod-auth-api
|
|
25
29
|
|
|
26
|
-
const appName =
|
|
27
|
-
|
|
30
|
+
const appName = process.env.AUTH_CRAFT_APP_NAME || (project === 'default' ? 'auth-craft' : `auth-craft-${project}`);
|
|
31
|
+
|
|
32
|
+
// Base name shared by every resource (also AUTH_SYSTEM_NAME / JWT issuer).
|
|
33
|
+
const resourceBase = `${appName}-${stage}-${layer}`;
|
|
28
34
|
|
|
29
35
|
console.log(`🚀 Deploying Auth Craft to AWS`);
|
|
30
36
|
console.log(` App: ${appName}`);
|
|
31
37
|
console.log(` Stage: ${stage}`);
|
|
38
|
+
console.log(` Layer: ${layer}`);
|
|
32
39
|
console.log(` Region: ${env.region}`);
|
|
33
40
|
|
|
34
41
|
// Common tags for cost allocation
|
|
@@ -39,30 +46,29 @@ const costTags = {
|
|
|
39
46
|
ManagedBy: 'CDK',
|
|
40
47
|
};
|
|
41
48
|
|
|
42
|
-
// Stack naming helper: {app}-{env}-{layer}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
// Resource naming helper: {app}-{env}
|
|
46
|
-
const getResourceName = () => `${appName}-${stage}`;
|
|
49
|
+
// Stack / resource naming helper: {app}-{env}-{layer}-{component}
|
|
50
|
+
const nameFor = (component: string) => `${resourceBase}-${component}`;
|
|
47
51
|
|
|
48
|
-
// DynamoDB Stack (data
|
|
49
|
-
const dynamodbStack = new DynamoDBStack(app,
|
|
52
|
+
// DynamoDB Stack (data component)
|
|
53
|
+
const dynamodbStack = new DynamoDBStack(app, nameFor('data'), {
|
|
50
54
|
env,
|
|
51
|
-
stackName:
|
|
52
|
-
description: `Auth Craft DynamoDB (${
|
|
55
|
+
stackName: nameFor('data'),
|
|
56
|
+
description: `Auth Craft DynamoDB (${resourceBase})`,
|
|
53
57
|
tags: costTags,
|
|
54
|
-
|
|
58
|
+
tableName: nameFor('data'),
|
|
55
59
|
stage,
|
|
56
60
|
});
|
|
57
61
|
|
|
58
|
-
// Lambda Stack (api
|
|
59
|
-
const lambdaStack = new LambdaStack(app,
|
|
62
|
+
// Lambda Stack (api component)
|
|
63
|
+
const lambdaStack = new LambdaStack(app, nameFor('api'), {
|
|
60
64
|
env,
|
|
61
|
-
stackName:
|
|
62
|
-
description: `Auth Craft Lambda API (${
|
|
65
|
+
stackName: nameFor('api'),
|
|
66
|
+
description: `Auth Craft Lambda API (${resourceBase})`,
|
|
63
67
|
table: dynamodbStack.table,
|
|
64
68
|
tags: costTags,
|
|
65
|
-
|
|
69
|
+
functionName: nameFor('api'),
|
|
70
|
+
// Base {app}-{env}-{layer} — AUTH_SYSTEM_NAME / JWT issuer (no component).
|
|
71
|
+
resourceName: resourceBase,
|
|
66
72
|
stage,
|
|
67
73
|
});
|
|
68
74
|
|
|
@@ -3,7 +3,7 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
|
3
3
|
import type { Construct } from 'constructs';
|
|
4
4
|
|
|
5
5
|
interface DynamoDBStackProps extends cdk.StackProps {
|
|
6
|
-
|
|
6
|
+
tableName: string; // {app}-{env}-{layer}-data, e.g. "snapshot-commerce-prod-auth-data"
|
|
7
7
|
stage: string; // "dev" | "staging" | "prod" — passed from the app, single source of truth
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -13,12 +13,10 @@ export class DynamoDBStack extends cdk.Stack {
|
|
|
13
13
|
constructor(scope: Construct, id: string, props: DynamoDBStackProps) {
|
|
14
14
|
super(scope, id, props);
|
|
15
15
|
|
|
16
|
-
const {
|
|
16
|
+
const { tableName, stage } = props;
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
// Examples: auth-
|
|
20
|
-
// Multi-project: auth-craft-ecommerce-dev, auth-craft-saas-prod
|
|
21
|
-
const tableName = resourceName;
|
|
18
|
+
// Table naming: {app}-{env}-{layer}-data
|
|
19
|
+
// Examples: snapshot-commerce-prod-auth-data, auth-craft-dev-auth-data
|
|
22
20
|
|
|
23
21
|
this.table = new dynamodb.Table(this, 'AuthCraftTable', {
|
|
24
22
|
tableName,
|
package/cdk/lib/lambda-stack.ts
CHANGED
|
@@ -16,9 +16,82 @@ const __dirname = path.dirname(__filename);
|
|
|
16
16
|
// packages/aws-cf-stack/cdk/lib/lambda-stack.ts -> packages/aws-cf-stack/assets/lambda
|
|
17
17
|
const LAMBDA_ASSET_DIR = path.resolve(__dirname, '../../assets/lambda');
|
|
18
18
|
|
|
19
|
+
// CloudWatch Logs only accepts a fixed set of retention periods — an arbitrary day
|
|
20
|
+
// count is rejected at deploy time. Map the supported values so a typo fails fast at
|
|
21
|
+
// synth (with the valid list) instead of mid-deploy.
|
|
22
|
+
const RETENTION_BY_DAYS: Record<number, logs.RetentionDays> = {
|
|
23
|
+
1: logs.RetentionDays.ONE_DAY,
|
|
24
|
+
3: logs.RetentionDays.THREE_DAYS,
|
|
25
|
+
5: logs.RetentionDays.FIVE_DAYS,
|
|
26
|
+
7: logs.RetentionDays.ONE_WEEK,
|
|
27
|
+
14: logs.RetentionDays.TWO_WEEKS,
|
|
28
|
+
30: logs.RetentionDays.ONE_MONTH,
|
|
29
|
+
60: logs.RetentionDays.TWO_MONTHS,
|
|
30
|
+
90: logs.RetentionDays.THREE_MONTHS,
|
|
31
|
+
120: logs.RetentionDays.FOUR_MONTHS,
|
|
32
|
+
150: logs.RetentionDays.FIVE_MONTHS,
|
|
33
|
+
180: logs.RetentionDays.SIX_MONTHS,
|
|
34
|
+
365: logs.RetentionDays.ONE_YEAR,
|
|
35
|
+
400: logs.RetentionDays.THIRTEEN_MONTHS,
|
|
36
|
+
545: logs.RetentionDays.EIGHTEEN_MONTHS,
|
|
37
|
+
731: logs.RetentionDays.TWO_YEARS,
|
|
38
|
+
1096: logs.RetentionDays.THREE_YEARS,
|
|
39
|
+
1827: logs.RetentionDays.FIVE_YEARS,
|
|
40
|
+
2192: logs.RetentionDays.SIX_YEARS,
|
|
41
|
+
2557: logs.RetentionDays.SEVEN_YEARS,
|
|
42
|
+
2922: logs.RetentionDays.EIGHT_YEARS,
|
|
43
|
+
3288: logs.RetentionDays.NINE_YEARS,
|
|
44
|
+
3653: logs.RetentionDays.TEN_YEARS,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the CloudWatch log retention from LAMBDA_LOG_RETENTION_DAYS.
|
|
49
|
+
* Accepts a supported day count (e.g. "3") or 'infinite' / 'never' to keep logs
|
|
50
|
+
* forever. Unset → stage default: 3 days in dev (cheap), 1 month otherwise.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveLogRetention(raw: string | undefined, stage: string): logs.RetentionDays {
|
|
53
|
+
const value = raw?.trim().toLowerCase();
|
|
54
|
+
|
|
55
|
+
if (!value) {
|
|
56
|
+
return stage === 'dev' ? logs.RetentionDays.THREE_DAYS : logs.RetentionDays.ONE_MONTH;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (value === 'infinite' || value === 'never' || value === '0') {
|
|
60
|
+
return logs.RetentionDays.INFINITE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const days = Number(value);
|
|
64
|
+
const retention = Number.isInteger(days) ? RETENTION_BY_DAYS[days] : undefined;
|
|
65
|
+
if (retention === undefined) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Unsupported LAMBDA_LOG_RETENTION_DAYS: '${raw}'. ` +
|
|
68
|
+
`CloudWatch Logs accepts only: ${Object.keys(RETENTION_BY_DAYS).join(', ')} (or 'infinite').`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return retention;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve X-Ray tracing from LAMBDA_TRACING ('active' | 'disabled').
|
|
76
|
+
* Unset → disabled in dev (traces are billed per request), active otherwise.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveTracing(raw: string | undefined, stage: string): lambda.Tracing {
|
|
79
|
+
const value = raw?.trim().toLowerCase();
|
|
80
|
+
|
|
81
|
+
if (!value) {
|
|
82
|
+
return stage === 'dev' ? lambda.Tracing.DISABLED : lambda.Tracing.ACTIVE;
|
|
83
|
+
}
|
|
84
|
+
if (value === 'active' || value === 'true' || value === 'enabled') return lambda.Tracing.ACTIVE;
|
|
85
|
+
if (value === 'disabled' || value === 'false' || value === 'off') return lambda.Tracing.DISABLED;
|
|
86
|
+
if (value === 'pass_through' || value === 'passthrough') return lambda.Tracing.PASS_THROUGH;
|
|
87
|
+
|
|
88
|
+
throw new Error(`Unsupported LAMBDA_TRACING: '${raw}'. Use 'active', 'disabled', or 'pass_through'.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
19
91
|
interface LambdaStackProps extends cdk.StackProps {
|
|
20
92
|
table: dynamodb.ITable;
|
|
21
|
-
|
|
93
|
+
functionName: string; // {app}-{env}-{layer}-api, e.g. "snapshot-commerce-prod-auth-api"
|
|
94
|
+
resourceName: string; // base {app}-{env}-{layer} — AUTH_SYSTEM_NAME / JWT issuer (no component)
|
|
22
95
|
stage: string; // "dev" | "staging" | "prod" — passed from the app, single source of truth
|
|
23
96
|
}
|
|
24
97
|
|
|
@@ -30,7 +103,7 @@ export class LambdaStack extends cdk.Stack {
|
|
|
30
103
|
constructor(scope: Construct, id: string, props: LambdaStackProps) {
|
|
31
104
|
super(scope, id, props);
|
|
32
105
|
|
|
33
|
-
const { resourceName, stage } = props;
|
|
106
|
+
const { resourceName, functionName, stage } = props;
|
|
34
107
|
|
|
35
108
|
// API base path — obscures the Lambda route surface and is forwarded onto by
|
|
36
109
|
// the gateway worker (BACKEND_URL = FunctionUrl + this path). It MUST be stable
|
|
@@ -89,25 +162,36 @@ export class LambdaStack extends cdk.Stack {
|
|
|
89
162
|
}
|
|
90
163
|
}
|
|
91
164
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
165
|
+
// Naming: functionName = {app}-{env}-{layer}-api (the deployed resource);
|
|
166
|
+
// resourceName = {app}-{env}-{layer} base (AUTH_SYSTEM_NAME / JWT issuer).
|
|
167
|
+
|
|
168
|
+
// Log group for Lambda function (matches the function name).
|
|
169
|
+
// Retention drives CloudWatch Logs storage cost, so it is configurable:
|
|
170
|
+
// LAMBDA_LOG_RETENTION_DAYS accepts any retention CloudWatch supports, plus
|
|
171
|
+
// 'infinite' (never expire). Unset → a stage-aware default that keeps dev cheap
|
|
172
|
+
// (3 days) while staging/prod keep a useful history (1 month).
|
|
173
|
+
const retention = resolveLogRetention(process.env.LAMBDA_LOG_RETENTION_DAYS, stage);
|
|
94
174
|
|
|
95
|
-
// Log group for Lambda function
|
|
96
175
|
const logGroup = new logs.LogGroup(this, 'AuthLambdaLogGroup', {
|
|
97
|
-
logGroupName: `/aws/lambda/${
|
|
98
|
-
retention
|
|
176
|
+
logGroupName: `/aws/lambda/${functionName}`,
|
|
177
|
+
retention,
|
|
99
178
|
removalPolicy: stage === 'prod' ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
|
|
100
179
|
});
|
|
101
180
|
|
|
181
|
+
// X-Ray tracing is billed per trace and is rarely worth it in dev, so it is
|
|
182
|
+
// opt-out there: LAMBDA_TRACING = 'active' | 'disabled' (default: disabled in
|
|
183
|
+
// dev, active elsewhere).
|
|
184
|
+
const tracing = resolveTracing(process.env.LAMBDA_TRACING, stage);
|
|
185
|
+
|
|
102
186
|
// Lambda function from the PREBUILT bundle shipped in this package.
|
|
103
187
|
// Unlike the in-repo app stack (which uses NodejsFunction to build from
|
|
104
188
|
// src/index.ts at synth time), this self-contained package deploys the
|
|
105
189
|
// already-bundled assets/lambda/index.mjs — no monorepo / source needed.
|
|
106
190
|
// The app build (esbuild, ESM, @aws-sdk/* external) emits `export const handler`
|
|
107
191
|
// in index.mjs → handler 'index.handler'.
|
|
108
|
-
// Function name: {app}-{env}
|
|
192
|
+
// Function name: {app}-{env}-{layer}-api
|
|
109
193
|
this.lambdaFunction = new lambda.Function(this, 'AuthLambda', {
|
|
110
|
-
functionName
|
|
194
|
+
functionName,
|
|
111
195
|
code: lambda.Code.fromAsset(LAMBDA_ASSET_DIR),
|
|
112
196
|
handler: 'index.handler',
|
|
113
197
|
runtime: lambda.Runtime.NODEJS_24_X,
|
|
@@ -233,8 +317,8 @@ export class LambdaStack extends cdk.Stack {
|
|
|
233
317
|
}),
|
|
234
318
|
},
|
|
235
319
|
|
|
236
|
-
// Tracing
|
|
237
|
-
tracing
|
|
320
|
+
// Tracing (X-Ray) — see resolveTracing above; off by default in dev.
|
|
321
|
+
tracing,
|
|
238
322
|
});
|
|
239
323
|
|
|
240
324
|
// Grant DynamoDB permissions
|
|
@@ -290,6 +374,20 @@ export class LambdaStack extends cdk.Stack {
|
|
|
290
374
|
exportName: `${this.stackName}-ApiBasePath`,
|
|
291
375
|
});
|
|
292
376
|
|
|
377
|
+
// Cost-relevant knobs, echoed so `auth-craft-stack outputs` can show what was
|
|
378
|
+
// actually deployed (rather than what the caller thinks they set).
|
|
379
|
+
new cdk.CfnOutput(this, 'LogRetentionDays', {
|
|
380
|
+
value: String(retention),
|
|
381
|
+
description: 'CloudWatch log retention in days (0 = never expire)',
|
|
382
|
+
exportName: `${this.stackName}-LogRetentionDays`,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
new cdk.CfnOutput(this, 'TracingMode', {
|
|
386
|
+
value: tracing,
|
|
387
|
+
description: 'X-Ray tracing mode',
|
|
388
|
+
exportName: `${this.stackName}-TracingMode`,
|
|
389
|
+
});
|
|
390
|
+
|
|
293
391
|
// Output auth system name — used as JWT issuer by Workers to verify tokens
|
|
294
392
|
new cdk.CfnOutput(this, 'AuthSystemName', {
|
|
295
393
|
value: resourceName,
|
package/lib/common.sh
CHANGED
|
@@ -76,11 +76,18 @@ load_env_file() {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
# ── Stage / naming derivation (mirrors the CI workflow + CDK app.ts) ──────────
|
|
79
|
-
# Inputs: STAGE, PROJECT, optional AUTH_CRAFT_APP_NAME.
|
|
80
|
-
# Exports STAGE (normalized), PROJECT, APP_NAME, RESOURCE_NAME, RUNTIME_NODE_ENV.
|
|
79
|
+
# Inputs: STAGE, PROJECT, LAYER, optional AUTH_CRAFT_APP_NAME.
|
|
80
|
+
# Exports STAGE (normalized), PROJECT, APP_NAME, LAYER, RESOURCE_NAME, RUNTIME_NODE_ENV.
|
|
81
|
+
#
|
|
82
|
+
# Naming pattern: {app}-{env}-{layer}, with a per-resource {component} appended
|
|
83
|
+
# (data/api/system/…). So table = {app}-{env}-{layer}-data, lambda = …-api, etc.
|
|
84
|
+
# RESOURCE_NAME is the {app}-{env}-{layer} base (also the AUTH_SYSTEM_NAME / JWT
|
|
85
|
+
# issuer). LAYER (default "auth") namespaces this deploy within a larger app that
|
|
86
|
+
# has other layers (e.g. billing); a consumer sets --layer/LAYER to reuse the CLI.
|
|
81
87
|
derive_naming() {
|
|
82
88
|
STAGE="${STAGE:-dev}"
|
|
83
89
|
PROJECT="${PROJECT:-default}"
|
|
90
|
+
LAYER="${LAYER:-auth}"
|
|
84
91
|
|
|
85
92
|
# #2 Normalize common stage aliases so a consumer can pass either form.
|
|
86
93
|
# production -> prod, development -> dev, staging stays staging.
|
|
@@ -103,9 +110,12 @@ derive_naming() {
|
|
|
103
110
|
APP_NAME="auth-craft-${PROJECT}"
|
|
104
111
|
fi
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
# Base name shared by every resource: {app}-{env}-{layer}. Per-resource component
|
|
114
|
+
# (data/api/system/…) is appended at each call site. Also the AUTH_SYSTEM_NAME /
|
|
115
|
+
# JWT issuer, so it must NOT carry a component (keeps the iss claim stable).
|
|
116
|
+
RESOURCE_NAME="${APP_NAME}-${STAGE}-${LAYER}"
|
|
107
117
|
RUNTIME_NODE_ENV="$([[ "$STAGE" == "prod" ]] && echo production || echo development)"
|
|
108
|
-
export STAGE PROJECT APP_NAME RESOURCE_NAME RUNTIME_NODE_ENV
|
|
118
|
+
export STAGE PROJECT APP_NAME LAYER RESOURCE_NAME RUNTIME_NODE_ENV
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
# ── Gateway JWT key pair (#5: one source, derive the rest) ────────────────────
|
|
@@ -188,16 +198,16 @@ gateway_mode() {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
# Derive a gateway worker URL for a scope (custom domain > workers.dev > empty).
|
|
191
|
-
# In shared mode all scopes resolve to the single worker ${
|
|
201
|
+
# In shared mode all scopes resolve to the single worker ${RESOURCE_NAME}.
|
|
192
202
|
gateway_url_for() {
|
|
193
203
|
local scope="$1" upper dom_var custom worker_name
|
|
194
204
|
upper="$(echo "$scope" | tr '[:lower:]' '[:upper:]')"
|
|
195
205
|
dom_var="CF_WORKER_CUSTOM_DOMAIN_${upper}"
|
|
196
206
|
custom="${!dom_var:-}"
|
|
197
207
|
if [[ "$(gateway_mode)" == "shared" ]]; then
|
|
198
|
-
worker_name="${
|
|
208
|
+
worker_name="${RESOURCE_NAME}"
|
|
199
209
|
else
|
|
200
|
-
worker_name="${
|
|
210
|
+
worker_name="${RESOURCE_NAME}-${scope}"
|
|
201
211
|
fi
|
|
202
212
|
if [[ -n "$custom" ]]; then
|
|
203
213
|
echo "https://$custom"
|
|
@@ -221,7 +231,7 @@ gateway_url_for() {
|
|
|
221
231
|
print_outputs_json() {
|
|
222
232
|
require_cmd aws; require_cmd jq
|
|
223
233
|
require_aws_identity
|
|
224
|
-
local api_stack="${
|
|
234
|
+
local api_stack="${RESOURCE_NAME}-api"
|
|
225
235
|
local url base name table fn backend issuer
|
|
226
236
|
url="$(cfn_output "$api_stack" LambdaFunctionUrl)"
|
|
227
237
|
base="$(cfn_output "$api_stack" ApiBasePath)"
|
|
@@ -244,20 +254,28 @@ print_outputs_json() {
|
|
|
244
254
|
svcPub="$(lambda_env "$fn" INTERNAL_JWT_PUBLIC_KEY)"; svcPub="${svcPub:-${LAMBDA_INTERNAL_JWT_PUBLIC_KEY:-}}"
|
|
245
255
|
|
|
246
256
|
# Worker names per scope. In multi mode (default) each scope is its own worker
|
|
247
|
-
# (${
|
|
248
|
-
# ${
|
|
257
|
+
# (${RESOURCE_NAME}-${scope}); in shared mode all scopes are the single worker
|
|
258
|
+
# ${RESOURCE_NAME}. These are what a consumer's Cloudflare Pages project references
|
|
249
259
|
# in a Service Binding (`[[services]] service = "<worker name>"`) to front auth
|
|
250
260
|
# same-origin via @auth-craft/pages-auth-proxy. The binding lives in the consumer's
|
|
251
261
|
# Pages project, not here — `serviceBindings` is a ready-to-paste snippet.
|
|
262
|
+
# Cost knobs, read back from the deployed stack so `outputs` reports what is live
|
|
263
|
+
# rather than what the caller's env happens to say.
|
|
264
|
+
local logRetention tracingMode
|
|
265
|
+
logRetention="$(cfn_output "$api_stack" LogRetentionDays)"
|
|
266
|
+
tracingMode="$(cfn_output "$api_stack" TracingMode)"
|
|
267
|
+
[[ -n "$logRetention" && "$logRetention" != "None" ]] || logRetention=""
|
|
268
|
+
[[ -n "$tracingMode" && "$tracingMode" != "None" ]] || tracingMode=""
|
|
269
|
+
|
|
252
270
|
local mode wnSystem wnTenant wnCustomer bindingService
|
|
253
271
|
mode="$(gateway_mode)"
|
|
254
272
|
if [[ "$mode" == "shared" ]]; then
|
|
255
|
-
wnSystem="${
|
|
273
|
+
wnSystem="${RESOURCE_NAME}"; wnTenant="$wnSystem"; wnCustomer="$wnSystem"
|
|
256
274
|
bindingService="$wnSystem"
|
|
257
275
|
else
|
|
258
|
-
wnSystem="${
|
|
259
|
-
wnTenant="${
|
|
260
|
-
wnCustomer="${
|
|
276
|
+
wnSystem="${RESOURCE_NAME}-system"
|
|
277
|
+
wnTenant="${RESOURCE_NAME}-tenant"
|
|
278
|
+
wnCustomer="${RESOURCE_NAME}-customer"
|
|
261
279
|
bindingService="$wnCustomer"
|
|
262
280
|
fi
|
|
263
281
|
local bindingSnippet
|
|
@@ -278,11 +296,13 @@ print_outputs_json() {
|
|
|
278
296
|
--arg rpCustomer "${CF_WORKER_ROUTE_PREFIX_CUSTOMER:-/customer}" \
|
|
279
297
|
--arg wnSystem "$wnSystem" --arg wnTenant "$wnTenant" --arg wnCustomer "$wnCustomer" \
|
|
280
298
|
--arg bindingSnippet "$bindingSnippet" \
|
|
299
|
+
--arg logRetention "$logRetention" --arg tracingMode "$tracingMode" \
|
|
281
300
|
'{stage:$stage, project:$project, region:$region,
|
|
282
301
|
lambdaFunctionUrl:$url, apiBasePath:$base, authSystemName:$name,
|
|
283
302
|
dynamodbTable:$table, backendUrl:$backend,
|
|
284
303
|
jwtPublicKey:$jwtPub, jwtIssuer:$jwtIssuer, jwtAlgorithm:$jwtAlg,
|
|
285
304
|
serviceJwtPublicKey:$svcPub,
|
|
305
|
+
observability:{logRetentionDays:$logRetention, tracingMode:$tracingMode},
|
|
286
306
|
gatewayUrls:{system:$gwSystem, tenant:$gwTenant, customer:$gwCustomer},
|
|
287
307
|
workerNames:{system:$wnSystem, tenant:$wnTenant, customer:$wnCustomer},
|
|
288
308
|
routePrefixes:{system:$rpSystem, tenant:$rpTenant, customer:$rpCustomer},
|
package/lib/deploy-gateway.sh
CHANGED
|
@@ -295,7 +295,7 @@ deploy_gateway() {
|
|
|
295
295
|
# ── Shared worker: one worker serves all scopes; scope comes from the path's first
|
|
296
296
|
# segment (worker AUTH_SCOPE unset). CORS unions all scopes; KV uses the customer id.
|
|
297
297
|
local worker_name custom_domain allowed_origins origin_suffixes cors_kv_id
|
|
298
|
-
worker_name="${
|
|
298
|
+
worker_name="${RESOURCE_NAME}"
|
|
299
299
|
custom_domain="$(custom_domain_for shared)"
|
|
300
300
|
allowed_origins="$(merge_cors_for_shared allowed_origins_for)"
|
|
301
301
|
origin_suffixes="$(merge_cors_for_shared origin_suffixes_for)"
|
|
@@ -324,7 +324,7 @@ deploy_gateway() {
|
|
|
324
324
|
local scope worker_name route_prefix custom_domain
|
|
325
325
|
local allowed_origins origin_suffixes cors_kv_id
|
|
326
326
|
for scope in system tenant customer; do
|
|
327
|
-
worker_name="${
|
|
327
|
+
worker_name="${RESOURCE_NAME}-${scope}"
|
|
328
328
|
route_prefix="$(route_prefix_for "$scope")"
|
|
329
329
|
custom_domain="$(custom_domain_for "$scope")"
|
|
330
330
|
allowed_origins="$(allowed_origins_for "$scope")"
|
package/lib/deploy-lambda.sh
CHANGED
|
@@ -21,11 +21,11 @@ deploy_lambda() {
|
|
|
21
21
|
[[ -f "$LAMBDA_ASSET_DIR/index.mjs" ]] \
|
|
22
22
|
|| die "Prebuilt Lambda bundle missing: $LAMBDA_ASSET_DIR/index.mjs (was the package built?)"
|
|
23
23
|
|
|
24
|
-
local api_stack="${
|
|
24
|
+
local api_stack="${RESOURCE_NAME}-api"
|
|
25
25
|
OUTPUTS_FILE="${OUTPUTS_FILE:-$(mktemp -t auth-craft-cdk-outputs.XXXXXX.json)}"
|
|
26
26
|
export OUTPUTS_FILE
|
|
27
27
|
|
|
28
|
-
info "Deploying CDK stacks (${
|
|
28
|
+
info "Deploying CDK stacks (${RESOURCE_NAME}-data, ${api_stack})…"
|
|
29
29
|
(
|
|
30
30
|
cd "$CDK_DIR"
|
|
31
31
|
CDK_DEFAULT_ACCOUNT="$AWS_ACCOUNT_ID" \
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@auth-craft/aws-cf-stack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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": {
|
|
@@ -10,19 +10,23 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
12
|
"cdk",
|
|
13
|
+
"!cdk/cdk.out",
|
|
13
14
|
"assets",
|
|
14
15
|
"wrangler",
|
|
15
16
|
"README.md",
|
|
16
17
|
".env.example"
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"aws-cdk-lib": "^2.
|
|
20
|
-
"constructs": "^10.
|
|
20
|
+
"aws-cdk-lib": "^2.265.0",
|
|
21
|
+
"constructs": "^10.8.1"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
24
|
-
"tsx": "^4.
|
|
25
|
-
"typescript": "^
|
|
24
|
+
"@types/node": "^26.2.0",
|
|
25
|
+
"tsx": "^4.23.12",
|
|
26
|
+
"typescript": "^7.0.2",
|
|
27
|
+
"@auth-craft/auth-admin-cli": "2.0.0",
|
|
28
|
+
"@auth-craft/auth-hono-dynamodb-lambda": "1.0.0",
|
|
29
|
+
"@auth-craft/cloudflare-auth-gateway": "1.0.0"
|
|
26
30
|
},
|
|
27
31
|
"peerDependencies": {
|
|
28
32
|
"aws-cdk": "^2.1128.0",
|