@jaypie/mcp 0.7.37 → 0.7.39
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/dist/suites/docs/index.js +1 -1
- package/package.json +1 -1
- package/release-notes/express/1.2.16.md +17 -0
- package/release-notes/jaypie/1.2.22.md +12 -0
- package/release-notes/kit/1.2.5.md +16 -0
- package/release-notes/mcp/0.7.38.md +19 -0
- package/release-notes/mcp/0.7.39.md +12 -0
- package/skills/apikey.md +21 -1
- package/skills/aws.md +8 -0
- package/skills/cdk.md +80 -1
- package/skills/datadog.md +7 -0
- package/skills/dns.md +4 -0
- package/skills/dynamodb.md +332 -83
- package/skills/errors.md +6 -0
- package/skills/index.md +11 -0
- package/skills/logs.md +6 -0
- package/skills/mocks.md +5 -0
- package/skills/models.md +6 -0
- package/skills/recipe-api-server.md +205 -0
- package/skills/secrets.md +50 -2
- package/skills/services.md +6 -0
- package/skills/skills.md +1 -0
- package/skills/style.md +6 -0
- package/skills/variables.md +19 -1
|
@@ -9,7 +9,7 @@ import { gt } from 'semver';
|
|
|
9
9
|
/**
|
|
10
10
|
* Docs Suite - Documentation services (skill, version, release_notes)
|
|
11
11
|
*/
|
|
12
|
-
const BUILD_VERSION_STRING = "@jaypie/mcp@0.7.
|
|
12
|
+
const BUILD_VERSION_STRING = "@jaypie/mcp@0.7.39#e7380f8a"
|
|
13
13
|
;
|
|
14
14
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname$1 = path.dirname(__filename$1);
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 1.2.16
|
|
3
|
+
date: 2026-03-14
|
|
4
|
+
summary: Fix setup array mutation causing duplicate execution on warm Lambda containers
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @jaypie/express 1.2.16
|
|
8
|
+
|
|
9
|
+
## Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **Setup array mutation on warm containers**: `expressHandler` and `expressStreamHandler` mutated the shared `setup` array via `unshift`/`push` on every request. On warm Lambda containers, this caused `loadEnvSecrets` and other setup functions to accumulate and execute multiple times per invoke. Now builds a request-local setup array each invocation, leaving the shared array untouched.
|
|
12
|
+
|
|
13
|
+
## Impact
|
|
14
|
+
|
|
15
|
+
- Eliminates duplicate `loadEnvSecrets` execution per request
|
|
16
|
+
- Eliminates increasing duplication over time on warm containers
|
|
17
|
+
- Reduces unnecessary Secrets Manager traffic and latency
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 1.2.22
|
|
3
|
+
date: 2026-03-14
|
|
4
|
+
summary: Bump @jaypie/express and @jaypie/kit for warm container and logging fixes
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# jaypie 1.2.22
|
|
8
|
+
|
|
9
|
+
## Dependencies
|
|
10
|
+
|
|
11
|
+
- `@jaypie/express` 1.2.15 -> 1.2.16: Fix setup array mutation causing duplicate execution on warm Lambda containers
|
|
12
|
+
- `@jaypie/kit` 1.2.4 -> 1.2.5: Use shared logger singleton so invoke tags propagate to kit lifecycle logs
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 1.2.5
|
|
3
|
+
date: 2026-03-14
|
|
4
|
+
summary: Use shared logger singleton so invoke/shortInvoke tags propagate to kit lifecycle logs
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @jaypie/kit 1.2.5
|
|
8
|
+
|
|
9
|
+
## Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **Logger invoke tags not propagating**: `@jaypie/kit` created its own logger instance via `createLogger()` instead of using the shared `@jaypie/logger` singleton. Tags like `invoke` and `shortInvoke` set by upstream adapters (e.g., `@jaypie/express`) never reached kit lifecycle logs (`[jaypie] Handler init`, `[handler] Setup`, etc.). Now uses the shared singleton so all tags propagate across packages.
|
|
12
|
+
|
|
13
|
+
## Impact
|
|
14
|
+
|
|
15
|
+
- Datadog queries on `@invoke:<uuid>` now include `@jaypie/kit` lifecycle logs
|
|
16
|
+
- Full correlation across middleware/framework layers in a single invoke
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 0.7.38
|
|
3
|
+
date: 2026-03-14
|
|
4
|
+
summary: Unify skill documentation with cross-linking, recipe guides, and @jaypie/dynamodb runtime docs
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Changes
|
|
8
|
+
|
|
9
|
+
- Expanded `dynamodb` skill with full `@jaypie/dynamodb` runtime package documentation (entity operations, GSI queries, scope hierarchy, seed/export, key builders)
|
|
10
|
+
- Added GSI guidance: start with zero, add one per deploy, CLI recommended for production
|
|
11
|
+
- Clarified key naming conventions: `model`/`id` (Jaypie default) vs `pk`/`sk` (generic DynamoDB)
|
|
12
|
+
- Added `Lambda with DynamoDB Tables` and `Lambda with Secrets` sections to `cdk` skill
|
|
13
|
+
- Documented personal environment secret consumer gotcha in `secrets` skill with `consumer: false` fix
|
|
14
|
+
- Added version note for `seed` support and DynamoDB hash storage pattern to `apikey` skill
|
|
15
|
+
- Added missing environment variables to `variables` skill (PROJECT_SALT, PROJECT_ADMIN_SEED, CDK_ENV_PERSONAL, DYNAMODB_TABLE_NAME, BASE_URL, PROJECT_BASE_URL)
|
|
16
|
+
- Created `recipe-api-server` skill: end-to-end guide for API server with DynamoDB + API keys
|
|
17
|
+
- Added `## See Also` sections to 11 skills for consistent cross-linking
|
|
18
|
+
- Added category taxonomy to `index` skill
|
|
19
|
+
- Added `recipes` category to skill index
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 0.7.39
|
|
3
|
+
date: 2026-03-14
|
|
4
|
+
summary: Add release notes for @jaypie/express 1.2.16 and jaypie 1.2.22
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @jaypie/mcp 0.7.39
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
- Added release notes for `@jaypie/express` 1.2.16 (setup array mutation fix)
|
|
12
|
+
- Added release notes for `jaypie` 1.2.22 (dependency bump)
|
package/skills/apikey.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: API key generation, validation, and hashing with Jaypie keys
|
|
3
|
-
related: secrets, style, tests
|
|
3
|
+
related: dynamodb, secrets, style, tests
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# API Keys
|
|
@@ -43,6 +43,8 @@ generateJaypieKey({ prefix: "", issuer: "jaypie" });
|
|
|
43
43
|
|
|
44
44
|
### With Seed
|
|
45
45
|
|
|
46
|
+
> **Version note:** `seed` support requires `jaypie >= 1.2.17` / `@jaypie/kit >= 1.2.2`.
|
|
47
|
+
|
|
46
48
|
Pass `seed` to derive a deterministic key from a secret. Uses HMAC-SHA256 with the `issuer` (defaulting to `"jaypie"`) as the HMAC message:
|
|
47
49
|
|
|
48
50
|
```typescript
|
|
@@ -211,3 +213,21 @@ new JaypieEnvSecret(this, "ProjectAdminSeed", {
|
|
|
211
213
|
```
|
|
212
214
|
|
|
213
215
|
See `~secrets` for the full secrets management pattern.
|
|
216
|
+
|
|
217
|
+
## DynamoDB Storage Pattern
|
|
218
|
+
|
|
219
|
+
Store hashed API keys in DynamoDB for direct lookup without a GSI:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// model = "apikey", id = hash — enables direct get-item lookup
|
|
223
|
+
{ model: "apikey", id: hashJaypieKey(key), ownerId: "user_123", createdAt: "..." }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This uses the `JaypieDynamoDb` default key convention (`model`/`id`). See `skill("dynamodb")` for table setup and query patterns.
|
|
227
|
+
|
|
228
|
+
## See Also
|
|
229
|
+
|
|
230
|
+
- **`skill("cdk")`** - CDK constructs for Lambda with secrets and tables
|
|
231
|
+
- **`skill("dynamodb")`** - DynamoDB key conventions and query patterns
|
|
232
|
+
- **`skill("secrets")`** - Generated secrets for PROJECT_SALT and PROJECT_ADMIN_SEED
|
|
233
|
+
- **`skill("recipe-api-server")`** - End-to-end guide for API server with DynamoDB + API keys
|
package/skills/aws.md
CHANGED
|
@@ -277,3 +277,11 @@ describe("Handler", () => {
|
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
279
|
```
|
|
280
|
+
|
|
281
|
+
## See Also
|
|
282
|
+
|
|
283
|
+
- **`skill("cdk")`** - CDK constructs for deploying AWS infrastructure
|
|
284
|
+
- **`skill("dynamodb")`** - DynamoDB key design and query patterns
|
|
285
|
+
- **`skill("lambda")`** - Lambda handler wrappers with secrets loading
|
|
286
|
+
- **`skill("secrets")`** - Secret management with JaypieEnvSecret
|
|
287
|
+
- **`skill("tools-aws")`** - Interactive AWS MCP tools
|
package/skills/cdk.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: CDK constructs and deployment patterns
|
|
3
|
-
related: aws, cicd, dynamodb, streaming, websockets
|
|
3
|
+
related: apikey, aws, cicd, dynamodb, express, lambda, secrets, streaming, websockets
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# CDK Constructs
|
|
@@ -120,6 +120,79 @@ PROJECT_ENV=sandbox PROJECT_NONCE=dev cdk deploy
|
|
|
120
120
|
PROJECT_ENV=production PROJECT_NONCE=prod cdk deploy
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## Lambda with DynamoDB Tables
|
|
124
|
+
|
|
125
|
+
Pass tables to `JaypieLambda` for automatic permissions and environment wiring:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { JaypieDynamoDb, JaypieLambda } from "@jaypie/constructs";
|
|
129
|
+
|
|
130
|
+
const table = new JaypieDynamoDb(this, "myApp");
|
|
131
|
+
|
|
132
|
+
const handler = new JaypieLambda(this, "ApiHandler", {
|
|
133
|
+
entry: "src/handler.ts",
|
|
134
|
+
handler: "handler",
|
|
135
|
+
tables: [table],
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Behavior:**
|
|
140
|
+
|
|
141
|
+
| Tables Count | Permissions | Environment |
|
|
142
|
+
|-------------|-------------|-------------|
|
|
143
|
+
| 1 table | `grantReadWriteData()` | `DYNAMODB_TABLE_NAME` set automatically |
|
|
144
|
+
| 2+ tables | `grantReadWriteData()` for each | No auto env var — set `CDK_ENV_TABLE` manually |
|
|
145
|
+
|
|
146
|
+
Single table example — at runtime, use `process.env.DYNAMODB_TABLE_NAME`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const table = new JaypieDynamoDb(this, "myApp");
|
|
150
|
+
new JaypieLambda(this, "Handler", {
|
|
151
|
+
tables: [table],
|
|
152
|
+
// DYNAMODB_TABLE_NAME is set automatically
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Multi-table example — set environment variables explicitly:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const usersTable = new JaypieDynamoDb(this, "users");
|
|
160
|
+
const ordersTable = new JaypieDynamoDb(this, "orders");
|
|
161
|
+
|
|
162
|
+
new JaypieLambda(this, "Handler", {
|
|
163
|
+
tables: [usersTable, ordersTable],
|
|
164
|
+
environment: {
|
|
165
|
+
USERS_TABLE: usersTable.tableName,
|
|
166
|
+
ORDERS_TABLE: ordersTable.tableName,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
See `skill("dynamodb")` for table key conventions and query patterns.
|
|
172
|
+
|
|
173
|
+
## Lambda with Secrets
|
|
174
|
+
|
|
175
|
+
Pass secrets as strings (auto-creates `JaypieEnvSecret`) or as construct instances:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// String shorthand — creates JaypieEnvSecret from env vars at deploy time
|
|
179
|
+
new JaypieLambda(this, "Handler", {
|
|
180
|
+
secrets: ["MONGODB_URI", "ANTHROPIC_API_KEY"],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Construct instances — for generated secrets or custom config
|
|
184
|
+
const salt = new JaypieEnvSecret(this, "ProjectSalt", {
|
|
185
|
+
envKey: "PROJECT_SALT",
|
|
186
|
+
generateSecretString: { excludePunctuation: true, passwordLength: 64 },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
new JaypieLambda(this, "Handler", {
|
|
190
|
+
secrets: [salt, "ANTHROPIC_API_KEY"],
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
See `skill("secrets")` for the full secrets pattern including generated secrets and personal environment gotchas.
|
|
195
|
+
|
|
123
196
|
## Environment Variables
|
|
124
197
|
|
|
125
198
|
Pass configuration to Lambda via environment variables:
|
|
@@ -207,5 +280,11 @@ new JaypieDistribution(this, "Dist", {
|
|
|
207
280
|
|
|
208
281
|
## See Also
|
|
209
282
|
|
|
283
|
+
- **`skill("apikey")`** - API key generation, validation, and hashing
|
|
284
|
+
- **`skill("dynamodb")`** - DynamoDB key design and query patterns
|
|
285
|
+
- **`skill("express")`** - Express handler and Lambda adapter
|
|
286
|
+
- **`skill("lambda")`** - Lambda handler wrappers and lifecycle
|
|
287
|
+
- **`skill("secrets")`** - Secret management with JaypieEnvSecret
|
|
210
288
|
- **`skill("streaming")`** - JaypieDistribution and JaypieNextJs streaming configuration
|
|
289
|
+
- **`skill("variables")`** - Environment variables reference
|
|
211
290
|
- **`skill("websockets")`** - JaypieWebSocket and JaypieWebSocketTable constructs
|
package/skills/datadog.md
CHANGED
|
@@ -192,3 +192,10 @@ describe("CheckoutService", () => {
|
|
|
192
192
|
});
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## See Also
|
|
196
|
+
|
|
197
|
+
- **`skill("cdk")`** - CDK Lambda construct with Datadog integration
|
|
198
|
+
- **`skill("logs")`** - Logging patterns for structured observability
|
|
199
|
+
- **`skill("tools-datadog")`** - Interactive Datadog MCP tools
|
|
200
|
+
- **`skill("variables")`** - Datadog and environment variables reference
|
|
201
|
+
|
package/skills/dns.md
CHANGED
package/skills/dynamodb.md
CHANGED
|
@@ -1,108 +1,342 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: DynamoDB
|
|
3
|
-
related: aws, cdk, models, tools-dynamodb
|
|
2
|
+
description: DynamoDB runtime package, key design, entity operations, and queries
|
|
3
|
+
related: apikey, aws, cdk, models, tools-dynamodb
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# DynamoDB Patterns
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Jaypie provides `@jaypie/dynamodb` for single-table DynamoDB with entity operations, GSI-based queries, hierarchical scoping, and soft delete. Access through the main `jaypie` package or directly.
|
|
9
9
|
|
|
10
10
|
## MCP Tools
|
|
11
11
|
|
|
12
12
|
For interactive DynamoDB tools (query, scan, get-item, describe-table), see **tools-dynamodb**.
|
|
13
13
|
|
|
14
|
-
## Key
|
|
14
|
+
## Key Naming Conventions
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Jaypie uses two key naming patterns. Understanding when to use each avoids confusion.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### Jaypie Default: `model` / `id`
|
|
19
|
+
|
|
20
|
+
`JaypieDynamoDb` creates tables with `model` (partition key) and `id` (sort key) by default. This is the **recommended convention for new Jaypie tables** and the convention used by `@jaypie/dynamodb`:
|
|
19
21
|
|
|
20
22
|
```typescript
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
sk: "PROFILE",
|
|
25
|
-
name: "John Doe",
|
|
26
|
-
email: "john@example.com"
|
|
27
|
-
}
|
|
23
|
+
const table = new JaypieDynamoDb(this, "myApp");
|
|
24
|
+
// Creates table with: model (HASH), id (RANGE)
|
|
25
|
+
```
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
status: "completed"
|
|
35
|
-
}
|
|
27
|
+
Items look like:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
{ model: "user", id: "u_abc123", name: "John", email: "john@example.com" }
|
|
31
|
+
{ model: "apikey", id: "a1b2c3d4...", ownerId: "u_abc123" }
|
|
36
32
|
```
|
|
37
33
|
|
|
38
|
-
###
|
|
34
|
+
### Generic DynamoDB: `pk` / `sk`
|
|
35
|
+
|
|
36
|
+
The `pk` / `sk` pattern is the standard DynamoDB convention used in broader DynamoDB literature. Jaypie uses it in local development scripts and educational examples. It's functionally equivalent — just different attribute names.
|
|
37
|
+
|
|
38
|
+
### Entity Prefixing (Value Pattern)
|
|
39
|
+
|
|
40
|
+
Entity prefixes like `USER#123` are a **value-level pattern** (how you structure the data stored in keys), not an attribute naming convention. You can use entity prefixes with either `model`/`id` or `pk`/`sk` attribute names.
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|---------------|---------------|
|
|
42
|
-
| Get user profile | pk = USER#123, sk = PROFILE |
|
|
43
|
-
| List user orders | pk = USER#123, sk begins_with ORDER# |
|
|
44
|
-
| Get specific order | pk = USER#123, sk = ORDER#2024-01-15#abc |
|
|
42
|
+
## @jaypie/dynamodb Package
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
The runtime package provides entity operations, GSI-based queries, key builders, and client management.
|
|
47
45
|
|
|
48
46
|
```typescript
|
|
49
|
-
import {
|
|
50
|
-
|
|
47
|
+
import {
|
|
48
|
+
APEX,
|
|
49
|
+
initClient,
|
|
50
|
+
putEntity,
|
|
51
|
+
getEntity,
|
|
52
|
+
deleteEntity,
|
|
53
|
+
queryByScope,
|
|
54
|
+
queryByCategory,
|
|
55
|
+
} from "@jaypie/dynamodb";
|
|
56
|
+
```
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
Or through the main package:
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
|
|
57
|
-
ExpressionAttributeValues: {
|
|
58
|
-
":pk": "USER#123",
|
|
59
|
-
":prefix": "ORDER#",
|
|
60
|
-
},
|
|
61
|
-
ScanIndexForward: false, // Newest first
|
|
62
|
-
Limit: 10,
|
|
63
|
-
}));
|
|
60
|
+
```typescript
|
|
61
|
+
import { APEX, initClient, putEntity, queryByScope } from "jaypie";
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
### Client Initialization
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
Call `initClient()` once at startup. In Lambda, use the handler's `setup` option:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { initClient } from "@jaypie/dynamodb";
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
// Production: uses DYNAMODB_TABLE_NAME and AWS_REGION env vars
|
|
72
|
+
initClient();
|
|
73
|
+
|
|
74
|
+
// Local development: explicit config
|
|
75
|
+
initClient({
|
|
76
|
+
endpoint: "http://127.0.0.1:8000",
|
|
77
|
+
tableName: "jaypie-local",
|
|
78
|
+
credentials: { accessKeyId: "local", secretAccessKey: "local" },
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Function | Description |
|
|
83
|
+
|----------|-------------|
|
|
84
|
+
| `initClient(config?)` | Initialize the DynamoDB client (call once) |
|
|
85
|
+
| `getDocClient()` | Get the initialized Document Client |
|
|
86
|
+
| `getTableName()` | Get the configured table name |
|
|
87
|
+
| `isInitialized()` | Check if client is initialized |
|
|
88
|
+
| `resetClient()` | Reset client state (for testing) |
|
|
89
|
+
|
|
90
|
+
### Constants
|
|
91
|
+
|
|
92
|
+
| Constant | Value | Description |
|
|
93
|
+
|----------|-------|-------------|
|
|
94
|
+
| `APEX` | `"@"` | Root-level scope marker (DynamoDB prohibits empty strings) |
|
|
95
|
+
| `SEPARATOR` | `"#"` | Composite key separator |
|
|
96
|
+
| `ARCHIVED_SUFFIX` | `"#archived"` | Suffix for archived entity GSI keys |
|
|
97
|
+
| `DELETED_SUFFIX` | `"#deleted"` | Suffix for soft-deleted entity GSI keys |
|
|
98
|
+
|
|
99
|
+
### StorableEntity Type
|
|
100
|
+
|
|
101
|
+
All entities follow this shape:
|
|
71
102
|
|
|
72
103
|
```typescript
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
interface StorableEntity {
|
|
105
|
+
// Primary Key
|
|
106
|
+
model: string; // Partition key (e.g., "record", "message")
|
|
107
|
+
id: string; // Sort key (UUID)
|
|
108
|
+
|
|
109
|
+
// Required
|
|
110
|
+
name: string;
|
|
111
|
+
scope: string; // APEX ("@") or "{parent.model}#{parent.id}"
|
|
112
|
+
sequence: number; // Date.now() for chronological ordering
|
|
113
|
+
|
|
114
|
+
// Optional (trigger GSI index population when present)
|
|
115
|
+
alias?: string; // Human-friendly slug
|
|
116
|
+
category?: string; // Category for filtering
|
|
117
|
+
type?: string; // Type for filtering
|
|
118
|
+
xid?: string; // External ID for cross-system lookup
|
|
119
|
+
|
|
120
|
+
// GSI Keys (auto-populated by putEntity/updateEntity)
|
|
121
|
+
indexAlias?: string;
|
|
122
|
+
indexCategory?: string;
|
|
123
|
+
indexScope?: string;
|
|
124
|
+
indexType?: string;
|
|
125
|
+
indexXid?: string;
|
|
126
|
+
|
|
127
|
+
// Timestamps (ISO 8601)
|
|
128
|
+
createdAt: string;
|
|
129
|
+
updatedAt: string;
|
|
130
|
+
archivedAt?: string;
|
|
131
|
+
deletedAt?: string;
|
|
79
132
|
}
|
|
80
133
|
```
|
|
81
134
|
|
|
82
|
-
|
|
135
|
+
### Entity Operations
|
|
83
136
|
|
|
84
137
|
```typescript
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
import { APEX, putEntity, getEntity, updateEntity, deleteEntity, archiveEntity, destroyEntity } from "@jaypie/dynamodb";
|
|
139
|
+
|
|
140
|
+
const now = new Date().toISOString();
|
|
141
|
+
|
|
142
|
+
// Create entity — auto-populates GSI keys
|
|
143
|
+
const record = await putEntity({
|
|
144
|
+
entity: {
|
|
145
|
+
model: "record",
|
|
146
|
+
id: crypto.randomUUID(),
|
|
147
|
+
name: "Daily Log",
|
|
148
|
+
scope: APEX,
|
|
149
|
+
sequence: Date.now(),
|
|
150
|
+
alias: "2026-01-07",
|
|
151
|
+
category: "memory",
|
|
152
|
+
createdAt: now,
|
|
153
|
+
updatedAt: now,
|
|
91
154
|
},
|
|
92
|
-
})
|
|
155
|
+
});
|
|
156
|
+
// indexScope: "@#record" (auto-populated)
|
|
157
|
+
// indexAlias: "@#record#2026-01-07" (auto-populated)
|
|
158
|
+
// indexCategory: "@#record#memory" (auto-populated)
|
|
159
|
+
|
|
160
|
+
// Get by primary key
|
|
161
|
+
const item = await getEntity({ id: "abc-123", model: "record" });
|
|
162
|
+
|
|
163
|
+
// Update — sets updatedAt, re-indexes
|
|
164
|
+
await updateEntity({ entity: { ...item, name: "Updated Name" } });
|
|
165
|
+
|
|
166
|
+
// Soft delete — sets deletedAt, re-indexes with #deleted suffix
|
|
167
|
+
await deleteEntity({ id: "abc-123", model: "record" });
|
|
168
|
+
|
|
169
|
+
// Archive — sets archivedAt, re-indexes with #archived suffix
|
|
170
|
+
await archiveEntity({ id: "abc-123", model: "record" });
|
|
171
|
+
|
|
172
|
+
// Hard delete — permanently removes
|
|
173
|
+
await destroyEntity({ id: "abc-123", model: "record" });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Function | Description |
|
|
177
|
+
|----------|-------------|
|
|
178
|
+
| `putEntity({ entity })` | Create or replace (auto-indexes GSI keys) |
|
|
179
|
+
| `getEntity({ id, model })` | Get by primary key |
|
|
180
|
+
| `updateEntity({ entity })` | Update (sets `updatedAt`, re-indexes) |
|
|
181
|
+
| `deleteEntity({ id, model })` | Soft delete (`deletedAt`, `#deleted` suffix) |
|
|
182
|
+
| `archiveEntity({ id, model })` | Archive (`archivedAt`, `#archived` suffix) |
|
|
183
|
+
| `destroyEntity({ id, model })` | Hard delete (permanent) |
|
|
184
|
+
|
|
185
|
+
### Scope and Hierarchy
|
|
186
|
+
|
|
187
|
+
The `scope` field enables parent-child relationships:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { APEX, calculateScope, putEntity, queryByScope } from "@jaypie/dynamodb";
|
|
191
|
+
|
|
192
|
+
// Root-level entity: scope = APEX ("@")
|
|
193
|
+
await putEntity({ entity: { model: "chat", scope: APEX, ... } });
|
|
194
|
+
|
|
195
|
+
// Child entity: scope = "{parent.model}#{parent.id}"
|
|
196
|
+
const chat = { model: "chat", id: "abc-123" };
|
|
197
|
+
const messageScope = calculateScope(chat); // "chat#abc-123"
|
|
198
|
+
|
|
199
|
+
await putEntity({
|
|
200
|
+
entity: { model: "message", scope: messageScope, ... },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Query all messages in a chat
|
|
204
|
+
const { items } = await queryByScope({ model: "message", scope: messageScope });
|
|
205
|
+
|
|
206
|
+
// Query all root-level chats
|
|
207
|
+
const { items: chats } = await queryByScope({ model: "chat", scope: APEX });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### GSI Schema
|
|
211
|
+
|
|
212
|
+
Jaypie defines five GSI patterns, but **do not create all five upfront**. Start with zero GSIs and add only what your access patterns require. The most common first GSI is `indexScope` for hierarchical queries.
|
|
213
|
+
|
|
214
|
+
**Important:** DynamoDB allows only **one GSI to be added per deployment**. If you need multiple GSIs, add them sequentially across separate deploys. For production tables, the AWS CLI is often better suited for adding GSIs than CDK (which may try to replace the table).
|
|
215
|
+
|
|
216
|
+
All GSIs use `sequence` (Number) as the sort key for chronological ordering.
|
|
217
|
+
|
|
218
|
+
| GSI Name | Partition Key Pattern | Purpose | Add When |
|
|
219
|
+
|----------|----------------------|---------|----------|
|
|
220
|
+
| `indexScope` | `{scope}#{model}` | List entities by parent | You need hierarchical queries |
|
|
221
|
+
| `indexAlias` | `{scope}#{model}#{alias}` | Human-friendly slug lookup | You need slug-based lookups |
|
|
222
|
+
| `indexCategory` | `{scope}#{model}#{category}` | Category filtering | You need to filter by category |
|
|
223
|
+
| `indexType` | `{scope}#{model}#{type}` | Type filtering | You need to filter by type |
|
|
224
|
+
| `indexXid` | `{scope}#{model}#{xid}` | External ID lookup | You need cross-system ID lookups |
|
|
225
|
+
|
|
226
|
+
### Query Functions
|
|
227
|
+
|
|
228
|
+
All queries return `{ items, lastEvaluatedKey }` and support pagination:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { APEX, queryByScope, queryByAlias, queryByCategory, queryByType, queryByXid } from "@jaypie/dynamodb";
|
|
232
|
+
|
|
233
|
+
// List by parent scope (most common)
|
|
234
|
+
const { items } = await queryByScope({ model: "record", scope: APEX });
|
|
235
|
+
|
|
236
|
+
// Filter by category
|
|
237
|
+
const { items: memories } = await queryByCategory({
|
|
238
|
+
model: "record", scope: APEX, category: "memory",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Lookup by alias (returns single or null)
|
|
242
|
+
const item = await queryByAlias({ model: "record", scope: APEX, alias: "2026-01-07" });
|
|
243
|
+
|
|
244
|
+
// Filter by type
|
|
245
|
+
const { items: drafts } = await queryByType({
|
|
246
|
+
model: "record", scope: APEX, type: "draft",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Lookup by external ID (returns single or null)
|
|
250
|
+
const item = await queryByXid({ model: "user", scope: APEX, xid: "github:12345" });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Query Options
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const { items, lastEvaluatedKey } = await queryByScope({
|
|
257
|
+
model: "record",
|
|
258
|
+
scope: APEX,
|
|
259
|
+
limit: 10, // Max items
|
|
260
|
+
ascending: true, // Oldest first (default: false, newest first)
|
|
261
|
+
deleted: true, // Query soft-deleted entities
|
|
262
|
+
archived: true, // Query archived entities
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Pagination
|
|
266
|
+
const nextPage = await queryByScope({
|
|
267
|
+
model: "record",
|
|
268
|
+
scope: APEX,
|
|
269
|
+
startKey: lastEvaluatedKey,
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Seed and Export
|
|
274
|
+
|
|
275
|
+
Idempotent seeding for bootstrapping data and export for migrations:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { APEX, seedEntities, seedEntityIfNotExists, exportEntities } from "@jaypie/dynamodb";
|
|
279
|
+
|
|
280
|
+
// Seed a single entity if it doesn't exist (checks by alias)
|
|
281
|
+
const created = await seedEntityIfNotExists({
|
|
282
|
+
alias: "config-main", model: "config", name: "Main Config", scope: APEX,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Seed multiple entities (idempotent)
|
|
286
|
+
const result = await seedEntities([
|
|
287
|
+
{ alias: "vocab-en", model: "vocabulary", name: "English", scope: APEX },
|
|
288
|
+
{ alias: "vocab-es", model: "vocabulary", name: "Spanish", scope: APEX },
|
|
289
|
+
]);
|
|
290
|
+
// result.created: ["vocab-en"] — aliases that were created
|
|
291
|
+
// result.skipped: ["vocab-es"] — aliases that already existed
|
|
292
|
+
// result.errors: [] — any failures
|
|
293
|
+
|
|
294
|
+
// Dry run
|
|
295
|
+
await seedEntities(entities, { dryRun: true });
|
|
296
|
+
|
|
297
|
+
// Replace existing
|
|
298
|
+
await seedEntities(entities, { replace: true });
|
|
299
|
+
|
|
300
|
+
// Export entities
|
|
301
|
+
const { entities, count } = await exportEntities("vocabulary", APEX);
|
|
302
|
+
|
|
303
|
+
// Export as JSON
|
|
304
|
+
const json = await exportEntitiesToJson("vocabulary", APEX);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Key Builders
|
|
308
|
+
|
|
309
|
+
Build composite GSI keys manually when needed:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { buildIndexScope, buildIndexAlias, buildIndexCategory, calculateScope } from "@jaypie/dynamodb";
|
|
313
|
+
|
|
314
|
+
buildIndexScope(APEX, "record"); // "@#record"
|
|
315
|
+
buildIndexAlias(APEX, "record", "daily-log"); // "@#record#daily-log"
|
|
316
|
+
buildIndexCategory(APEX, "record", "memory"); // "@#record#memory"
|
|
317
|
+
calculateScope({ model: "chat", id: "abc-123" }); // "chat#abc-123"
|
|
93
318
|
```
|
|
94
319
|
|
|
95
320
|
## CDK Table Definition
|
|
96
321
|
|
|
97
|
-
|
|
322
|
+
`JaypieDynamoDb` provides these defaults:
|
|
323
|
+
|
|
324
|
+
| Setting | Default | Source |
|
|
325
|
+
|---------|---------|--------|
|
|
326
|
+
| Partition key | `model` (String) | Jaypie construct |
|
|
327
|
+
| Sort key | `id` (String) | Jaypie construct |
|
|
328
|
+
| Billing mode | PAY_PER_REQUEST | Jaypie construct |
|
|
329
|
+
| Removal policy | DESTROY (non-production), RETAIN (production) | Jaypie construct |
|
|
330
|
+
| Point-in-time recovery | Enabled | Jaypie construct |
|
|
331
|
+
| Encryption | AWS-owned key | CDK default |
|
|
98
332
|
|
|
99
333
|
```typescript
|
|
100
334
|
import { JaypieDynamoDb } from "@jaypie/constructs";
|
|
101
335
|
|
|
102
|
-
// Recommended: start with no indexes
|
|
336
|
+
// Recommended: start with no indexes (uses model/id keys)
|
|
103
337
|
const table = new JaypieDynamoDb(this, "myApp");
|
|
104
338
|
|
|
105
|
-
// Add indexes
|
|
339
|
+
// Add indexes when driven by real access patterns
|
|
106
340
|
const table = new JaypieDynamoDb(this, "myApp", {
|
|
107
341
|
indexes: [
|
|
108
342
|
{ pk: ["scope", "model"], sk: ["sequence"] },
|
|
@@ -110,6 +344,8 @@ const table = new JaypieDynamoDb(this, "myApp", {
|
|
|
110
344
|
});
|
|
111
345
|
```
|
|
112
346
|
|
|
347
|
+
Wire tables to Lambda using the `tables` prop — see `skill("cdk")` for details.
|
|
348
|
+
|
|
113
349
|
## Local Development
|
|
114
350
|
|
|
115
351
|
Use docker-compose for local DynamoDB. The `@jaypie/dynamodb` MCP tool can generate a `docker-compose.yml` with custom ports.
|
|
@@ -128,35 +364,43 @@ Use docker-compose for local DynamoDB. The `@jaypie/dynamodb` MCP tool can gener
|
|
|
128
364
|
}
|
|
129
365
|
```
|
|
130
366
|
|
|
131
|
-
|
|
132
|
-
|--------|-------------|
|
|
133
|
-
| `dynamo:init` | Start containers and create the default table |
|
|
134
|
-
| `dynamo:create-table` | Create the local table (idempotent) |
|
|
135
|
-
| `dynamo:remove` | Stop containers and delete data volumes |
|
|
136
|
-
| `dynamo:start` | Start containers (data persists) |
|
|
137
|
-
| `dynamo:stop` | Stop containers (data persists) |
|
|
367
|
+
## Raw SDK Patterns
|
|
138
368
|
|
|
139
|
-
|
|
369
|
+
For cases where you need direct SDK access instead of `@jaypie/dynamodb`:
|
|
140
370
|
|
|
141
|
-
|
|
371
|
+
```typescript
|
|
372
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
373
|
+
import { QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
142
374
|
|
|
143
|
-
|
|
144
|
-
# Start local DynamoDB
|
|
145
|
-
docker run -p 8000:8000 amazon/dynamodb-local
|
|
375
|
+
const client = new DynamoDBClient({});
|
|
146
376
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
377
|
+
const result = await client.send(new QueryCommand({
|
|
378
|
+
TableName: process.env.CDK_ENV_TABLE,
|
|
379
|
+
KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
|
|
380
|
+
ExpressionAttributeValues: {
|
|
381
|
+
":pk": "USER#123",
|
|
382
|
+
":prefix": "ORDER#",
|
|
383
|
+
},
|
|
384
|
+
ScanIndexForward: false,
|
|
385
|
+
Limit: 10,
|
|
386
|
+
}));
|
|
155
387
|
```
|
|
156
388
|
|
|
157
389
|
## Testing
|
|
158
390
|
|
|
159
|
-
Mock
|
|
391
|
+
Mock `@jaypie/dynamodb` via testkit. Key builders and `indexEntity` work correctly in tests:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import {
|
|
395
|
+
APEX,
|
|
396
|
+
indexEntity,
|
|
397
|
+
putEntity,
|
|
398
|
+
queryByScope,
|
|
399
|
+
seedEntities,
|
|
400
|
+
} from "@jaypie/testkit/mock";
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
For raw SDK mocking:
|
|
160
404
|
|
|
161
405
|
```typescript
|
|
162
406
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
@@ -167,11 +411,10 @@ vi.mock("@aws-sdk/client-dynamodb");
|
|
|
167
411
|
describe("OrderService", () => {
|
|
168
412
|
it("queries user orders", async () => {
|
|
169
413
|
vi.mocked(DynamoDBClient.prototype.send).mockResolvedValue({
|
|
170
|
-
Items: [{
|
|
414
|
+
Items: [{ model: "order", id: "order-abc" }],
|
|
171
415
|
});
|
|
172
416
|
|
|
173
417
|
const orders = await getOrders("123");
|
|
174
|
-
|
|
175
418
|
expect(orders).toHaveLength(1);
|
|
176
419
|
});
|
|
177
420
|
});
|
|
@@ -193,3 +436,9 @@ Version 0.4.0 renamed `class` → `category` and `indexClass` → `indexCategory
|
|
|
193
436
|
| `INDEX_CLASS` | `INDEX_CATEGORY` |
|
|
194
437
|
| `queryByClass()` | `queryByCategory()` |
|
|
195
438
|
|
|
439
|
+
## See Also
|
|
440
|
+
|
|
441
|
+
- **`skill("apikey")`** - API key hash storage in DynamoDB
|
|
442
|
+
- **`skill("cdk")`** - JaypieDynamoDb construct and Lambda table wiring
|
|
443
|
+
- **`skill("models")`** - Data model and type definition patterns
|
|
444
|
+
- **`skill("tools-dynamodb")`** - Interactive DynamoDB MCP tools
|
package/skills/errors.md
CHANGED
|
@@ -140,3 +140,9 @@ it("includes context in error", async () => {
|
|
|
140
140
|
});
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
## See Also
|
|
144
|
+
|
|
145
|
+
- **`skill("handlers")`** - Handler lifecycle and automatic error formatting
|
|
146
|
+
- **`skill("logs")`** - Logging patterns for error context
|
|
147
|
+
- **`skill("tests")`** - Testing error types with Vitest
|
|
148
|
+
|
package/skills/index.md
CHANGED
|
@@ -5,3 +5,14 @@ description: Skill directory listing
|
|
|
5
5
|
# Jaypie Skills
|
|
6
6
|
|
|
7
7
|
Query the Jaypie MCP `skill` tool with one of the following alias keywords `mcp__jaypie__skill(alias: String)`:
|
|
8
|
+
|
|
9
|
+
## Categories
|
|
10
|
+
|
|
11
|
+
| Category | Skills |
|
|
12
|
+
|----------|--------|
|
|
13
|
+
| contents | index, releasenotes |
|
|
14
|
+
| development | apikey, documentation, errors, llm, logs, mocks, monorepo, style, subpackages, tests |
|
|
15
|
+
| infrastructure | aws, cdk, cicd, datadog, dns, dynamodb, express, lambda, secrets, streaming, variables, websockets |
|
|
16
|
+
| patterns | fabric, handlers, models, services, vocabulary |
|
|
17
|
+
| recipes | recipe-api-server |
|
|
18
|
+
| meta | issues, jaypie, skills, tools |
|
package/skills/logs.md
CHANGED
|
@@ -158,3 +158,9 @@ export const handler = lambdaHandler(async (event) => {
|
|
|
158
158
|
});
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
+
## See Also
|
|
162
|
+
|
|
163
|
+
- **`skill("datadog")`** - Datadog integration and log forwarding
|
|
164
|
+
- **`skill("handlers")`** - Handler lifecycle with automatic log context
|
|
165
|
+
- **`skill("variables")`** - LOG_LEVEL and other environment variables
|
|
166
|
+
|
package/skills/mocks.md
CHANGED
package/skills/models.md
CHANGED
|
@@ -176,6 +176,12 @@ export interface PaginatedResponse<T> {
|
|
|
176
176
|
4. **Document fields** - Add JSDoc comments for complex types
|
|
177
177
|
5. **Prefer interfaces** - Use `interface` over `type` for objects
|
|
178
178
|
|
|
179
|
+
## See Also
|
|
180
|
+
|
|
181
|
+
- **`skill("dynamodb")`** - DynamoDB key conventions and table patterns
|
|
182
|
+
- **`skill("fabric")`** - Fabric service input schema definitions
|
|
183
|
+
- **`skill("services")`** - Service layer architecture patterns
|
|
184
|
+
|
|
179
185
|
## Type Documentation
|
|
180
186
|
|
|
181
187
|
```typescript
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Task-oriented guide for building a Jaypie API server with DynamoDB and API keys
|
|
3
|
+
related: apikey, cdk, dynamodb, express, secrets, variables
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recipe: API Server with DynamoDB + API Keys
|
|
7
|
+
|
|
8
|
+
Step-by-step guide for creating a Jaypie API server stack with DynamoDB storage, generated secrets, and API key authentication.
|
|
9
|
+
|
|
10
|
+
## 1. Create the DynamoDB Table
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { JaypieDynamoDb } from "@jaypie/constructs";
|
|
14
|
+
|
|
15
|
+
const table = new JaypieDynamoDb(this, "myApi");
|
|
16
|
+
// Default keys: model (partition), id (sort)
|
|
17
|
+
// Billing: PAY_PER_REQUEST
|
|
18
|
+
// Removal: DESTROY (non-prod), RETAIN (prod)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
No indexes needed initially. API key hashes are stored with `model: "apikey"` and `id: <hash>`, giving direct lookup by hash without a GSI.
|
|
22
|
+
|
|
23
|
+
See `skill("dynamodb")` for key conventions and query patterns.
|
|
24
|
+
|
|
25
|
+
## 2. Create Generated Secrets
|
|
26
|
+
|
|
27
|
+
Two secrets support API key workflows:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { JaypieEnvSecret } from "@jaypie/constructs";
|
|
31
|
+
import { isProductionEnv } from "@jaypie/kit";
|
|
32
|
+
|
|
33
|
+
// PROJECT_SALT — HMAC salt for hashing keys before storage
|
|
34
|
+
// If lost, all stored key hashes become unverifiable
|
|
35
|
+
const salt = new JaypieEnvSecret(this, "ProjectSalt", {
|
|
36
|
+
envKey: "PROJECT_SALT",
|
|
37
|
+
generateSecretString: {
|
|
38
|
+
excludePunctuation: true,
|
|
39
|
+
includeSpace: false,
|
|
40
|
+
passwordLength: 64,
|
|
41
|
+
},
|
|
42
|
+
removalPolicy: isProductionEnv(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// PROJECT_ADMIN_SEED — derives the bootstrap owner key deterministically
|
|
46
|
+
const adminSeed = new JaypieEnvSecret(this, "ProjectAdminSeed", {
|
|
47
|
+
envKey: "PROJECT_ADMIN_SEED",
|
|
48
|
+
generateSecretString: {
|
|
49
|
+
excludePunctuation: true,
|
|
50
|
+
includeSpace: false,
|
|
51
|
+
passwordLength: 64,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Personal environment note:** If deploying to a personal environment (`CDK_ENV_PERSONAL=true`), these generated secrets auto-detect as consumers and try to import from sandbox. Since they don't exist there, the deploy fails with `No export named env-sandbox-...`. Fix by adding `consumer: false`:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
new JaypieEnvSecret(this, "ProjectSalt", {
|
|
60
|
+
envKey: "PROJECT_SALT",
|
|
61
|
+
consumer: false, // Create in this stack, don't import
|
|
62
|
+
generateSecretString: { /* ... */ },
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See `skill("secrets")` for the full provider/consumer pattern.
|
|
67
|
+
|
|
68
|
+
## 3. Create the Lambda
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { JaypieExpressLambda } from "@jaypie/constructs";
|
|
72
|
+
|
|
73
|
+
new JaypieExpressLambda(this, "ApiLambda", {
|
|
74
|
+
code: "../api/dist",
|
|
75
|
+
handler: "index.handler",
|
|
76
|
+
tables: [table], // Auto-grants read/write, sets DYNAMODB_TABLE_NAME
|
|
77
|
+
secrets: [salt, adminSeed, "ANTHROPIC_API_KEY"],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The `tables` prop:
|
|
82
|
+
- Calls `grantReadWriteData()` automatically
|
|
83
|
+
- Sets `DYNAMODB_TABLE_NAME` when exactly 1 table is passed
|
|
84
|
+
|
|
85
|
+
The `secrets` prop accepts both construct instances and string env var names.
|
|
86
|
+
|
|
87
|
+
See `skill("cdk")` for full Lambda configuration options.
|
|
88
|
+
|
|
89
|
+
## 4. Express Routes
|
|
90
|
+
|
|
91
|
+
### Key Generation Endpoint
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { expressHandler, generateJaypieKey, hashJaypieKey } from "jaypie";
|
|
95
|
+
|
|
96
|
+
app.post("/api/keys", expressHandler(async (req, res) => {
|
|
97
|
+
const key = generateJaypieKey({ issuer: "myapi" });
|
|
98
|
+
const hash = hashJaypieKey(key); // Uses process.env.PROJECT_SALT
|
|
99
|
+
|
|
100
|
+
// Store hash in DynamoDB
|
|
101
|
+
await dynamoClient.send(new PutItemCommand({
|
|
102
|
+
TableName: process.env.DYNAMODB_TABLE_NAME,
|
|
103
|
+
Item: {
|
|
104
|
+
model: { S: "apikey" },
|
|
105
|
+
id: { S: hash },
|
|
106
|
+
ownerId: { S: req.userId },
|
|
107
|
+
createdAt: { S: new Date().toISOString() },
|
|
108
|
+
},
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Return plaintext key to user (only time it's visible)
|
|
112
|
+
return { key };
|
|
113
|
+
}));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Key Validation Middleware
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { validateJaypieKey, hashJaypieKey, ForbiddenError } from "jaypie";
|
|
120
|
+
|
|
121
|
+
async function authenticateApiKey(req, res, next) {
|
|
122
|
+
const key = req.headers["x-api-key"];
|
|
123
|
+
|
|
124
|
+
if (!key || !validateJaypieKey(key, { issuer: "myapi" })) {
|
|
125
|
+
throw new ForbiddenError("Invalid API key");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hash = hashJaypieKey(key);
|
|
129
|
+
const result = await dynamoClient.send(new GetItemCommand({
|
|
130
|
+
TableName: process.env.DYNAMODB_TABLE_NAME,
|
|
131
|
+
Key: { model: { S: "apikey" }, id: { S: hash } },
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
if (!result.Item) {
|
|
135
|
+
throw new ForbiddenError("API key not found");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
req.apiKeyOwner = result.Item.ownerId.S;
|
|
139
|
+
next();
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Seed-Derived Bootstrap Key
|
|
144
|
+
|
|
145
|
+
On first startup, derive the admin key from the seed:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { expressHandler, generateJaypieKey, hashJaypieKey, loadEnvSecrets } from "jaypie";
|
|
149
|
+
|
|
150
|
+
export default expressHandler(handler, {
|
|
151
|
+
secrets: ["PROJECT_SALT", "PROJECT_ADMIN_SEED"],
|
|
152
|
+
setup: async () => {
|
|
153
|
+
// Derive the bootstrap key (deterministic — same every time)
|
|
154
|
+
const adminKey = generateJaypieKey({
|
|
155
|
+
seed: process.env.PROJECT_ADMIN_SEED,
|
|
156
|
+
issuer: "myapi",
|
|
157
|
+
});
|
|
158
|
+
const hash = hashJaypieKey(adminKey);
|
|
159
|
+
|
|
160
|
+
// Auto-provision if not exists
|
|
161
|
+
await dynamoClient.send(new PutItemCommand({
|
|
162
|
+
TableName: process.env.DYNAMODB_TABLE_NAME,
|
|
163
|
+
Item: {
|
|
164
|
+
model: { S: "apikey" },
|
|
165
|
+
id: { S: hash },
|
|
166
|
+
ownerId: { S: "admin" },
|
|
167
|
+
createdAt: { S: new Date().toISOString() },
|
|
168
|
+
},
|
|
169
|
+
ConditionExpression: "attribute_not_exists(id)",
|
|
170
|
+
})).catch(() => {}); // Ignore if already exists
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
See `skill("apikey")` for full key generation/validation/hashing documentation.
|
|
176
|
+
|
|
177
|
+
## 5. Deploy
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# First deploy creates secrets and table
|
|
181
|
+
PROJECT_ENV=sandbox PROJECT_NONCE=dev npx cdk deploy
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
No workflow secrets needed for generated values — CloudFormation creates them on first deploy and preserves them on subsequent deploys.
|
|
185
|
+
|
|
186
|
+
## Jaypie Defaults vs CDK Defaults
|
|
187
|
+
|
|
188
|
+
| Setting | Value | Source |
|
|
189
|
+
|---------|-------|--------|
|
|
190
|
+
| DynamoDB keys: `model`/`id` | Jaypie construct | Jaypie |
|
|
191
|
+
| DynamoDB billing: PAY_PER_REQUEST | Jaypie construct | Jaypie |
|
|
192
|
+
| DynamoDB removal: env-aware | Jaypie construct | Jaypie |
|
|
193
|
+
| DynamoDB PITR: enabled | Jaypie construct | Jaypie |
|
|
194
|
+
| DynamoDB encryption: AWS-owned | CDK default | CDK |
|
|
195
|
+
| Secret consumer auto-detect | Jaypie construct | Jaypie |
|
|
196
|
+
| Lambda `DYNAMODB_TABLE_NAME` | Jaypie construct (1 table) | Jaypie |
|
|
197
|
+
|
|
198
|
+
## See Also
|
|
199
|
+
|
|
200
|
+
- **`skill("apikey")`** - Key generation, validation, hashing, and seed options
|
|
201
|
+
- **`skill("cdk")`** - CDK constructs, tables, and secrets integration
|
|
202
|
+
- **`skill("dynamodb")`** - Key conventions, queries, and local development
|
|
203
|
+
- **`skill("express")`** - Express handler and Lambda adapter
|
|
204
|
+
- **`skill("secrets")`** - Secret management and personal environment gotchas
|
|
205
|
+
- **`skill("variables")`** - Environment variables reference
|
package/skills/secrets.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Secret management with AWS Secrets Manager
|
|
3
|
-
related: aws, cdk, variables
|
|
3
|
+
related: apikey, aws, cdk, variables
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Secret Management
|
|
@@ -88,7 +88,49 @@ new JaypieEnvSecret(this, "SHARED_API_KEY", { provider: true });
|
|
|
88
88
|
new JaypieEnvSecret(this, "SHARED_API_KEY"); // consumer auto-detected
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
### Auto-Detection
|
|
92
|
+
|
|
93
|
+
The construct auto-detects consumer mode when any of these conditions is true:
|
|
94
|
+
|
|
95
|
+
| Condition | Trigger |
|
|
96
|
+
|-----------|---------|
|
|
97
|
+
| `PROJECT_ENV=personal` | Personal environment |
|
|
98
|
+
| `CDK_ENV_PERSONAL=true` | Personal environment flag |
|
|
99
|
+
| `CDK_ENV_EPHEMERAL=true` | Legacy ephemeral flag |
|
|
100
|
+
|
|
101
|
+
When consumer mode is active, the construct **imports** a secret from the sandbox stack via CloudFormation exports instead of creating a new one. The export name follows the pattern: `env-sandbox-{PROJECT_KEY}-{SecretName}`.
|
|
102
|
+
|
|
103
|
+
### Personal Environment Gotcha
|
|
104
|
+
|
|
105
|
+
**When adding new generated secrets in a personal environment**, the consumer auto-detection can cause deploy failures:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
No export named env-sandbox-myproject-ProjectSalt found
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This happens because the personal stack tries to import from sandbox, but sandbox has no such export yet (the secret is new).
|
|
112
|
+
|
|
113
|
+
**Fix**: Set `consumer: false` for secrets that should be created per-stack:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
new JaypieEnvSecret(this, "ProjectSalt", {
|
|
117
|
+
envKey: "PROJECT_SALT",
|
|
118
|
+
consumer: false, // Create in this stack, don't import from sandbox
|
|
119
|
+
generateSecretString: {
|
|
120
|
+
excludePunctuation: true,
|
|
121
|
+
includeSpace: false,
|
|
122
|
+
passwordLength: 64,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**When to use `consumer: false`:**
|
|
128
|
+
- Generated secrets (`generateSecretString`) that don't need to be shared
|
|
129
|
+
- Secrets unique to each environment (salts, seeds)
|
|
130
|
+
|
|
131
|
+
**When to let consumer auto-detect (default):**
|
|
132
|
+
- Shared vendor credentials (API keys, database URIs) that exist in sandbox
|
|
133
|
+
- Secrets that must be identical across sandbox and personal stacks
|
|
92
134
|
|
|
93
135
|
## Generated Secrets
|
|
94
136
|
|
|
@@ -274,6 +316,12 @@ secret.grantRead(lambdaFunction);
|
|
|
274
316
|
|
|
275
317
|
Or use the `secrets` array on `JaypieLambda`, which handles permissions automatically.
|
|
276
318
|
|
|
319
|
+
## See Also
|
|
320
|
+
|
|
321
|
+
- **`skill("apikey")`** - API key infrastructure using PROJECT_SALT and PROJECT_ADMIN_SEED
|
|
322
|
+
- **`skill("cdk")`** - CDK constructs including JaypieLambda secrets integration
|
|
323
|
+
- **`skill("variables")`** - Environment variables reference
|
|
324
|
+
|
|
277
325
|
## Testing
|
|
278
326
|
|
|
279
327
|
Mock secret functions in tests:
|
package/skills/services.md
CHANGED
|
@@ -173,3 +173,9 @@ describe("getUser", () => {
|
|
|
173
173
|
});
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
## See Also
|
|
177
|
+
|
|
178
|
+
- **`skill("fabric")`** - Fabric service pattern for multi-platform deployment
|
|
179
|
+
- **`skill("handlers")`** - Handler lifecycle and integration with services
|
|
180
|
+
- **`skill("models")`** - Data model and type definitions
|
|
181
|
+
|
package/skills/skills.md
CHANGED
|
@@ -19,4 +19,5 @@ Look up skills by alias: `mcp__jaypie__skill(alias)`
|
|
|
19
19
|
| development | apikey, documentation, errors, llm, logs, mocks, monorepo, style, subpackages, tests |
|
|
20
20
|
| infrastructure | aws, cdk, cicd, datadog, dns, dynamodb, express, lambda, secrets, streaming, variables, websockets |
|
|
21
21
|
| patterns | fabric, handlers, models, services, vocabulary |
|
|
22
|
+
| recipes | recipe-api-server |
|
|
22
23
|
| meta | issues, jaypie, skills, tools |
|
package/skills/style.md
CHANGED
|
@@ -165,3 +165,9 @@ export default [...jaypie];
|
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
Always run `npm run format` before committing.
|
|
168
|
+
|
|
169
|
+
## See Also
|
|
170
|
+
|
|
171
|
+
- **`skill("documentation")`** - Writing style for Jaypie docs
|
|
172
|
+
- **`skill("errors")`** - Error handling conventions
|
|
173
|
+
- **`skill("tests")`** - Testing patterns and conventions
|
package/skills/variables.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Environment variables reference
|
|
3
|
-
related:
|
|
3
|
+
related: apikey, cdk, datadog, secrets
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Environment Variables
|
|
@@ -15,6 +15,8 @@ Configuration variables used in Jaypie applications.
|
|
|
15
15
|
| `PROJECT_KEY` | Project name for logging | e.g., my-api |
|
|
16
16
|
| `PROJECT_NONCE` | Unique resource suffix | e.g., dev, staging, prod |
|
|
17
17
|
| `PROJECT_CHAOS` | Chaos engineering mode | none, partial, full |
|
|
18
|
+
| `PROJECT_SALT` | Secret salt for HMAC hashing (API keys) | Generated by JaypieEnvSecret |
|
|
19
|
+
| `PROJECT_ADMIN_SEED` | Secret seed for deterministic key derivation | Generated by JaypieEnvSecret |
|
|
18
20
|
|
|
19
21
|
### Usage
|
|
20
22
|
|
|
@@ -52,6 +54,8 @@ LOG_LEVEL=debug npm run dev
|
|
|
52
54
|
| `CDK_ENV_SNS_TOPIC_ARN` | SNS topic ARN |
|
|
53
55
|
| `CDK_ENV_DATADOG_API_KEY_ARN` | Datadog API key ARN |
|
|
54
56
|
| `CDK_ENV_TABLE` | DynamoDB table name |
|
|
57
|
+
| `CDK_ENV_PERSONAL` | Personal environment flag (triggers consumer mode for secrets) |
|
|
58
|
+
| `DYNAMODB_TABLE_NAME` | Auto-set by JaypieLambda when exactly 1 table is passed |
|
|
55
59
|
|
|
56
60
|
### Passing to Lambda
|
|
57
61
|
|
|
@@ -113,6 +117,13 @@ environment: {
|
|
|
113
117
|
}
|
|
114
118
|
```
|
|
115
119
|
|
|
120
|
+
## Application Variables
|
|
121
|
+
|
|
122
|
+
| Variable | Description |
|
|
123
|
+
|----------|-------------|
|
|
124
|
+
| `BASE_URL` | Application base URL (used by CORS) |
|
|
125
|
+
| `PROJECT_BASE_URL` | Project base URL (used by CORS) |
|
|
126
|
+
|
|
116
127
|
## Local Development
|
|
117
128
|
|
|
118
129
|
### .env Files
|
|
@@ -144,3 +155,10 @@ const isLocal = process.env.PROJECT_ENV === "local";
|
|
|
144
155
|
const isDevelopment = !isProduction;
|
|
145
156
|
```
|
|
146
157
|
|
|
158
|
+
## See Also
|
|
159
|
+
|
|
160
|
+
- **`skill("apikey")`** - Uses PROJECT_SALT and PROJECT_ADMIN_SEED
|
|
161
|
+
- **`skill("cdk")`** - CDK constructs that set infrastructure variables
|
|
162
|
+
- **`skill("cors")`** - Uses BASE_URL and PROJECT_BASE_URL
|
|
163
|
+
- **`skill("secrets")`** - Secret management and CDK_ENV_PERSONAL behavior
|
|
164
|
+
|