create-daloy 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/bun-basic/AGENTS.md +2 -1
- package/templates/bun-basic/_agents/skills/daloyjs-best-practices/SKILL.md +44 -1
- package/templates/bun-basic/package.json +1 -1
- package/templates/cloudflare-worker/AGENTS.md +3 -3
- package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +45 -1
- package/templates/cloudflare-worker/package.json +1 -1
- package/templates/deno-basic/AGENTS.md +2 -1
- package/templates/deno-basic/_agents/skills/daloyjs-best-practices/SKILL.md +44 -1
- package/templates/deno-basic/deno.json +5 -5
- package/templates/node-basic/AGENTS.md +2 -1
- package/templates/node-basic/_agents/skills/daloyjs-best-practices/SKILL.md +44 -1
- package/templates/node-basic/package.json +1 -1
- package/templates/vercel/AGENTS.md +4 -3
- package/templates/vercel/_agents/skills/daloyjs-best-practices/SKILL.md +45 -2
- package/templates/vercel/package.json +1 -1
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:334286a9-ce6f-53b5-9ece-5e69ac3b7aac",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-08-
|
|
7
|
+
"timestamp": "2026-08-28T11:08:56.276Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.3.0"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "library",
|
|
18
|
-
"bom-ref": "pkg:npm/create-daloy@1.
|
|
18
|
+
"bom-ref": "pkg:npm/create-daloy@1.3.0",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "1.
|
|
20
|
+
"version": "1.3.0",
|
|
21
21
|
"description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
|
|
22
|
-
"purl": "pkg:npm/create-daloy@1.
|
|
22
|
+
"purl": "pkg:npm/create-daloy@1.3.0",
|
|
23
23
|
"licenses": [
|
|
24
24
|
{
|
|
25
25
|
"license": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
}
|
|
43
43
|
],
|
|
44
44
|
"swid": {
|
|
45
|
-
"tagId": "swidtag-create-daloy-1.
|
|
45
|
+
"tagId": "swidtag-create-daloy-1.3.0",
|
|
46
46
|
"name": "create-daloy",
|
|
47
|
-
"version": "1.
|
|
47
|
+
"version": "1.3.0",
|
|
48
48
|
"tagVersion": 0,
|
|
49
49
|
"patch": false
|
|
50
50
|
}
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"components": [],
|
|
54
54
|
"dependencies": [
|
|
55
55
|
{
|
|
56
|
-
"ref": "pkg:npm/create-daloy@1.
|
|
56
|
+
"ref": "pkg:npm/create-daloy@1.3.0",
|
|
57
57
|
"dependsOn": []
|
|
58
58
|
}
|
|
59
59
|
]
|
package/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "create-daloy-1.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.
|
|
5
|
+
"name": "create-daloy-1.3.0",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.3.0-334286a9-ce6f-53b5-9ece-5e69ac3b7aac",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-08-
|
|
8
|
+
"created": "2026-08-28T11:08:56.276Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package-create-daloy",
|
|
18
18
|
"name": "create-daloy",
|
|
19
|
-
"versionInfo": "1.
|
|
19
|
+
"versionInfo": "1.3.0",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/create-daloy@1.
|
|
30
|
+
"referenceLocator": "pkg:npm/create-daloy@1.3.0"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
|
@@ -53,6 +53,7 @@ Do not write `.js` here — that's the Node NodeNext convention and will fail to
|
|
|
53
53
|
6. Keep operation IDs stable and examples schema-valid; `bun run contract` must pass after route, metadata, or OpenAPI-facing changes.
|
|
54
54
|
7. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
55
55
|
8. After any route change: `bun run gen:openapi && bun run gen:client && bun run contract && bun run typecheck && bun test`.
|
|
56
|
+
9. Side effects that must outlive a request: enqueue a job (`app.useJobs` + idempotency key); pattern in the skill.
|
|
56
57
|
|
|
57
58
|
## Secure-by-default (do not let an AI strip these)
|
|
58
59
|
|
|
@@ -74,4 +75,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
|
|
|
74
75
|
- Bug fixes include a regression test.
|
|
75
76
|
- Never bypass safety checks without a clear reason.
|
|
76
77
|
|
|
77
|
-
For the full workflow —
|
|
78
|
+
For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -4,7 +4,8 @@ description: >-
|
|
|
4
4
|
Build, test, and harden this DaloyJS REST API on Bun. Use when the user
|
|
5
5
|
asks to add or change an endpoint, route, Zod/Standard Schema,
|
|
6
6
|
middleware, error handling, OpenAPI spec, typed Hey API client, contract
|
|
7
|
-
gate, auth, rate limit, or security default. Also use
|
|
7
|
+
gate, auth, rate limit, background jobs, or security default. Also use
|
|
8
|
+
for phrasing like
|
|
8
9
|
"add GET /...", "new route", "regenerate the client", "bun test", or
|
|
9
10
|
"fix the 401". Do not use for frontend UI, infra-only work, or unrelated
|
|
10
11
|
docs.
|
|
@@ -217,6 +218,48 @@ app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
|
217
218
|
|
|
218
219
|
Add CORS only when needed, with an explicit `origin` allowlist.
|
|
219
220
|
|
|
221
|
+
## Background jobs
|
|
222
|
+
|
|
223
|
+
Side effects that must outlive the HTTP request (welcome emails, webhook
|
|
224
|
+
fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
|
|
225
|
+
inline in the handler and not in a fire-and-forget promise.
|
|
226
|
+
|
|
227
|
+
- `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
|
|
228
|
+
optional in-process worker) into the app lifecycle, including the
|
|
229
|
+
graceful-shutdown drain.
|
|
230
|
+
- Enqueue **after** the DB commit, always with an idempotency key:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { jobIdempotencyKey } from "@daloyjs/core";
|
|
234
|
+
|
|
235
|
+
await app.jobs!.enqueue({
|
|
236
|
+
name: "email.welcome",
|
|
237
|
+
payload: { userId: user.id, to: user.email }, // ids, never file bytes
|
|
238
|
+
idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- Delivery is **at-least-once**: handlers must be idempotent. Pass a key
|
|
243
|
+
through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
|
|
244
|
+
duplicate run would move money or send email.
|
|
245
|
+
- Throw `JobFatalError` for permanent failures (no retry); any other throw
|
|
246
|
+
retries with full-jitter backoff, then dead-letters.
|
|
247
|
+
- `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
|
|
248
|
+
idempotent enqueue. Use it for side effects that must run once
|
|
249
|
+
cluster-wide; keep plain `app.cron()` for process-local maintenance
|
|
250
|
+
(other replicas have their own memory to sweep).
|
|
251
|
+
- Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
|
|
252
|
+
never file contents.
|
|
253
|
+
- `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
|
|
254
|
+
dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
|
|
255
|
+
in app code; `useJobs` warns on Memory in production, and
|
|
256
|
+
`strictProduction: true` refuses to boot.
|
|
257
|
+
- `startWorker: true` in the same process is fine for small deployments;
|
|
258
|
+
split a dedicated worker process (same app, `useJobs` with
|
|
259
|
+
`startWorker`, no public ingress) as you grow.
|
|
260
|
+
|
|
261
|
+
Full reference: <https://daloyjs.dev/docs/jobs>
|
|
262
|
+
|
|
220
263
|
## Testing best practices
|
|
221
264
|
|
|
222
265
|
Tests run with `bun test`. Use **in-process** requests through
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so
|
|
3
|
+
A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so bundle cost is negligible; drop `docs` (and the `openapi` block) for the smallest bundle.
|
|
4
4
|
|
|
5
5
|
- Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
|
|
6
6
|
- Runtime: Cloudflare Workers (Web Standard `Request`/`Response`).
|
|
@@ -37,7 +37,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **
|
|
|
37
37
|
5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. The in-memory limiter resets per isolate.
|
|
38
38
|
6. Stay on the Workers runtime: only Web Standards APIs + Cloudflare bindings. No `node:` modules unless you explicitly add `nodejs_compat` and require it.
|
|
39
39
|
7. Bindings flow through `env`. Read KV/D1/R2/secrets from the `env` argument; never read them via globals.
|
|
40
|
-
8. Long-running work
|
|
40
|
+
8. Long-running work: `ctx.waitUntil(...)` or a remote-store job via `app.useJobs` (enqueue only, no worker loop on an isolate); never block the response.
|
|
41
41
|
9. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
|
|
42
42
|
10. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
43
43
|
|
|
@@ -64,4 +64,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
|
|
|
64
64
|
- For deploys, ensure the user has run `wrangler login`; do not authenticate on their behalf.
|
|
65
65
|
- Never bypass safety checks without a clear reason.
|
|
66
66
|
|
|
67
|
-
For the full workflow —
|
|
67
|
+
For the full workflow — routes, background jobs, bindings, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -4,7 +4,8 @@ description: >-
|
|
|
4
4
|
Build, test, and harden this DaloyJS REST API on Cloudflare Workers. Use
|
|
5
5
|
when the user asks to add or change an endpoint, route, Zod/Standard
|
|
6
6
|
Schema, middleware, error handling, Worker binding (KV, D1, R2, Queues,
|
|
7
|
-
env), contract gate, auth, or
|
|
7
|
+
env), contract gate, auth, rate limit, or background jobs. Also use for
|
|
8
|
+
phrasing like
|
|
8
9
|
"add GET /...", "new route", "wrangler", "fix the 401", or "add a KV
|
|
9
10
|
binding". Do not use for frontend UI, infra-only work, or unrelated
|
|
10
11
|
docs.
|
|
@@ -226,6 +227,49 @@ Add CORS only when needed, with an explicit `origin` allowlist.
|
|
|
226
227
|
4. Store secrets via `wrangler secret put` — they appear on `env` but
|
|
227
228
|
are not committed to `wrangler.toml`.
|
|
228
229
|
|
|
230
|
+
## Background jobs
|
|
231
|
+
|
|
232
|
+
Side effects that must outlive the HTTP request (welcome emails, webhook
|
|
233
|
+
fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
|
|
234
|
+
inline in the handler and not in a fire-and-forget promise.
|
|
235
|
+
|
|
236
|
+
- `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
|
|
237
|
+
optional in-process worker) into the app lifecycle, including the
|
|
238
|
+
graceful-shutdown drain.
|
|
239
|
+
- Enqueue **after** the DB commit, always with an idempotency key:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { jobIdempotencyKey } from "@daloyjs/core";
|
|
243
|
+
|
|
244
|
+
await app.jobs!.enqueue({
|
|
245
|
+
name: "email.welcome",
|
|
246
|
+
payload: { userId: user.id, to: user.email }, // ids, never file bytes
|
|
247
|
+
idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- Delivery is **at-least-once**: handlers must be idempotent. Pass a key
|
|
252
|
+
through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
|
|
253
|
+
duplicate run would move money or send email.
|
|
254
|
+
- Throw `JobFatalError` for permanent failures (no retry); any other throw
|
|
255
|
+
retries with full-jitter backoff, then dead-letters.
|
|
256
|
+
- `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
|
|
257
|
+
idempotent enqueue. Use it for side effects that must run once
|
|
258
|
+
cluster-wide; keep plain `app.cron()` for process-local maintenance
|
|
259
|
+
(other replicas have their own memory to sweep).
|
|
260
|
+
- Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
|
|
261
|
+
never file contents.
|
|
262
|
+
- `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
|
|
263
|
+
dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
|
|
264
|
+
in app code; `useJobs` warns on Memory in production, and
|
|
265
|
+
`strictProduction: true` refuses to boot.
|
|
266
|
+
- A Workers isolate **enqueues only**: never `startWorker: true` here —
|
|
267
|
+
the isolate is torn down once the response flushes. Run the worker as a
|
|
268
|
+
separate long-lived Node service (same app code, `useJobs` with
|
|
269
|
+
`startWorker`) against the same remote store.
|
|
270
|
+
|
|
271
|
+
Full reference: <https://daloyjs.dev/docs/jobs>
|
|
272
|
+
|
|
229
273
|
## Testing best practices
|
|
230
274
|
|
|
231
275
|
Two patterns:
|
|
@@ -42,6 +42,7 @@ The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint
|
|
|
42
42
|
7. Keep operation IDs stable and examples schema-valid; `deno task contract` must pass after route, metadata, or OpenAPI-facing changes.
|
|
43
43
|
8. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
44
44
|
9. After any route change: `deno task gen:openapi && deno task contract && deno task typecheck && deno task test`.
|
|
45
|
+
10. Side effects that must outlive a request: enqueue a job (`app.useJobs` + idempotency key); pattern in the skill.
|
|
45
46
|
|
|
46
47
|
## Secure-by-default (do not let an AI strip these)
|
|
47
48
|
|
|
@@ -63,4 +64,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
|
|
|
63
64
|
- Bug fixes include a regression test.
|
|
64
65
|
- Use `deno task ...`, not `npm`/`pnpm`. There is no `package.json` here.
|
|
65
66
|
|
|
66
|
-
For the full workflow —
|
|
67
|
+
For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -4,7 +4,8 @@ description: >-
|
|
|
4
4
|
Build, test, and harden this DaloyJS REST API on Deno. Use when the user
|
|
5
5
|
asks to add or change an endpoint, route, Zod/Standard Schema,
|
|
6
6
|
middleware, error handling, OpenAPI spec, contract gate, Deno
|
|
7
|
-
permission, deno.json import, auth,
|
|
7
|
+
permission, deno.json import, auth, rate limit, or background jobs.
|
|
8
|
+
Also use for phrasing
|
|
8
9
|
like "add GET /...", "new route", "deno task", "fix the 401", or "allow
|
|
9
10
|
net". Do not use for frontend UI, npm/pnpm package.json work, or
|
|
10
11
|
unrelated docs.
|
|
@@ -214,6 +215,48 @@ app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
|
214
215
|
|
|
215
216
|
Add CORS only when needed, with an explicit `origin` allowlist.
|
|
216
217
|
|
|
218
|
+
## Background jobs
|
|
219
|
+
|
|
220
|
+
Side effects that must outlive the HTTP request (welcome emails, webhook
|
|
221
|
+
fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
|
|
222
|
+
inline in the handler and not in a fire-and-forget promise.
|
|
223
|
+
|
|
224
|
+
- `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
|
|
225
|
+
optional in-process worker) into the app lifecycle, including the
|
|
226
|
+
graceful-shutdown drain.
|
|
227
|
+
- Enqueue **after** the DB commit, always with an idempotency key:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { jobIdempotencyKey } from "@daloyjs/core";
|
|
231
|
+
|
|
232
|
+
await app.jobs!.enqueue({
|
|
233
|
+
name: "email.welcome",
|
|
234
|
+
payload: { userId: user.id, to: user.email }, // ids, never file bytes
|
|
235
|
+
idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- Delivery is **at-least-once**: handlers must be idempotent. Pass a key
|
|
240
|
+
through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
|
|
241
|
+
duplicate run would move money or send email.
|
|
242
|
+
- Throw `JobFatalError` for permanent failures (no retry); any other throw
|
|
243
|
+
retries with full-jitter backoff, then dead-letters.
|
|
244
|
+
- `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
|
|
245
|
+
idempotent enqueue. Use it for side effects that must run once
|
|
246
|
+
cluster-wide; keep plain `app.cron()` for process-local maintenance
|
|
247
|
+
(other replicas have their own memory to sweep).
|
|
248
|
+
- Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
|
|
249
|
+
never file contents.
|
|
250
|
+
- `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
|
|
251
|
+
dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
|
|
252
|
+
in app code; `useJobs` warns on Memory in production, and
|
|
253
|
+
`strictProduction: true` refuses to boot.
|
|
254
|
+
- `startWorker: true` in the same process is fine for small deployments;
|
|
255
|
+
split a dedicated worker process (same app, `useJobs` with
|
|
256
|
+
`startWorker`, no public ingress) as you grow.
|
|
257
|
+
|
|
258
|
+
Full reference: <https://daloyjs.dev/docs/jobs>
|
|
259
|
+
|
|
217
260
|
## Testing best practices
|
|
218
261
|
|
|
219
262
|
Tests run with `deno test`. Use **in-process** `app.request()` — no port
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
11
11
|
},
|
|
12
12
|
"imports": {
|
|
13
|
-
"@daloyjs/core": "jsr:@daloyjs/daloy@^1.
|
|
14
|
-
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.
|
|
15
|
-
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.
|
|
16
|
-
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.
|
|
17
|
-
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.
|
|
13
|
+
"@daloyjs/core": "jsr:@daloyjs/daloy@^1.3.0",
|
|
14
|
+
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.3.0/banner",
|
|
15
|
+
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.3.0/contract",
|
|
16
|
+
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.3.0/deno",
|
|
17
|
+
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.3.0/openapi",
|
|
18
18
|
"zod": "npm:zod@^4.4.3"
|
|
19
19
|
},
|
|
20
20
|
"compilerOptions": {
|
|
@@ -56,6 +56,7 @@ You import the file you see. On `pnpm build`, TypeScript rewrites the `.ts` spec
|
|
|
56
56
|
6. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
|
|
57
57
|
7. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
58
58
|
8. After any route change: `pnpm gen && pnpm contract && pnpm typecheck && pnpm test`.
|
|
59
|
+
9. Side effects that must outlive a request: enqueue a job (`app.useJobs` + idempotency key); pattern in the skill.
|
|
59
60
|
|
|
60
61
|
## Secure-by-default (do not let an AI strip these)
|
|
61
62
|
|
|
@@ -74,4 +75,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
|
|
|
74
75
|
|
|
75
76
|
- Bug fixes include a regression test.
|
|
76
77
|
|
|
77
|
-
For the full workflow —
|
|
78
|
+
For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -4,7 +4,8 @@ description: >-
|
|
|
4
4
|
Build, test, and harden this DaloyJS REST API on Node.js. Use when the
|
|
5
5
|
user asks to add or change an endpoint, route, Zod/Standard Schema,
|
|
6
6
|
middleware, error handling, OpenAPI spec, typed Hey API client, contract
|
|
7
|
-
gate, auth, rate limit, or security default. Also use
|
|
7
|
+
gate, auth, rate limit, background jobs, or security default. Also use
|
|
8
|
+
for phrasing like
|
|
8
9
|
"add GET /...", "new route", "regenerate the client", or "fix the 401".
|
|
9
10
|
Do not use for frontend UI, infra-only work, or unrelated docs.
|
|
10
11
|
license: MIT
|
|
@@ -256,6 +257,48 @@ Custom middleware should be small, well-typed, and call `await next()`
|
|
|
256
257
|
exactly once. Wrap it in `try { await next() } finally { ... }` if it
|
|
257
258
|
needs to run code after the handler.
|
|
258
259
|
|
|
260
|
+
## Background jobs
|
|
261
|
+
|
|
262
|
+
Side effects that must outlive the HTTP request (welcome emails, webhook
|
|
263
|
+
fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
|
|
264
|
+
inline in the handler and not in a fire-and-forget promise.
|
|
265
|
+
|
|
266
|
+
- `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
|
|
267
|
+
optional in-process worker) into the app lifecycle, including the
|
|
268
|
+
graceful-shutdown drain.
|
|
269
|
+
- Enqueue **after** the DB commit, always with an idempotency key:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { jobIdempotencyKey } from "@daloyjs/core";
|
|
273
|
+
|
|
274
|
+
await app.jobs!.enqueue({
|
|
275
|
+
name: "email.welcome",
|
|
276
|
+
payload: { userId: user.id, to: user.email }, // ids, never file bytes
|
|
277
|
+
idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- Delivery is **at-least-once**: handlers must be idempotent. Pass a key
|
|
282
|
+
through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
|
|
283
|
+
duplicate run would move money or send email.
|
|
284
|
+
- Throw `JobFatalError` for permanent failures (no retry); any other throw
|
|
285
|
+
retries with full-jitter backoff, then dead-letters.
|
|
286
|
+
- `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
|
|
287
|
+
idempotent enqueue. Use it for side effects that must run once
|
|
288
|
+
cluster-wide; keep plain `app.cron()` for process-local maintenance
|
|
289
|
+
(other replicas have their own memory to sweep).
|
|
290
|
+
- Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
|
|
291
|
+
never file contents.
|
|
292
|
+
- `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
|
|
293
|
+
dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
|
|
294
|
+
in app code; `useJobs` warns on Memory in production, and
|
|
295
|
+
`strictProduction: true` refuses to boot.
|
|
296
|
+
- `startWorker: true` in the same process is fine for small deployments;
|
|
297
|
+
split a dedicated worker process (same app, `useJobs` with
|
|
298
|
+
`startWorker`, no public ingress) as you grow.
|
|
299
|
+
|
|
300
|
+
Full reference: <https://daloyjs.dev/docs/jobs>
|
|
301
|
+
|
|
259
302
|
## Testing best practices
|
|
260
303
|
|
|
261
304
|
Tests live under `tests/` and run with `node --test` (Node's built-in
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.js runtime**. **Contract-first**: routes use validation schemas (Zod here; DaloyJS also supports Standard Schema-compatible validators) and generate OpenAPI 3.1.
|
|
3
|
+
A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.js runtime**. **Contract-first**: routes use validation schemas (Zod here; DaloyJS also supports Standard Schema-compatible validators) and generate OpenAPI 3.1. `docs: true` auto-mounts `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
|
|
4
4
|
|
|
5
5
|
- Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
|
|
6
6
|
- Runtime: Vercel Node.js Functions on Fluid Compute (Web Standard `Request`/`Response`).
|
|
@@ -14,7 +14,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.j
|
|
|
14
14
|
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
17
|
-
- `pnpm dev` — local Node dev server (`src/dev.ts`) on http://localhost:3000 (no `vercel dev`
|
|
17
|
+
- `pnpm dev` — local Node dev server (`src/dev.ts`) on http://localhost:3000 (no `vercel dev` needed)
|
|
18
18
|
- `pnpm typecheck` — `tsc --noEmit`
|
|
19
19
|
- `pnpm test` — run test suite
|
|
20
20
|
- `pnpm contract` — run `daloy inspect --check api/index.ts`
|
|
@@ -51,6 +51,7 @@ You import the file you see. Vercel resolves `.ts` at deploy time; Node runs it
|
|
|
51
51
|
7. Keep a single `api/index.ts` entry and the `vercel.json` `/(.*)` → `/api` rewrite so DaloyJS handles all routing at the site root.
|
|
52
52
|
8. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
|
|
53
53
|
9. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
54
|
+
10. Side effects that must outlive a request: `app.useJobs` + remote store; enqueue only, no worker loop in the Function.
|
|
54
55
|
|
|
55
56
|
## Secure-by-default (do not let an AI strip these)
|
|
56
57
|
|
|
@@ -73,4 +74,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
|
|
|
73
74
|
- For deploys, ensure the user has run `vercel login`; do not authenticate on their behalf.
|
|
74
75
|
- Never bypass safety checks without a clear reason.
|
|
75
76
|
|
|
76
|
-
For the full workflow —
|
|
77
|
+
For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -4,8 +4,8 @@ description: >-
|
|
|
4
4
|
Build, test, and harden this DaloyJS REST API on Vercel (Node.js
|
|
5
5
|
runtime). Use when the user asks to add or change an endpoint, route,
|
|
6
6
|
Zod/Standard Schema, middleware, error handling, OpenAPI spec, typed
|
|
7
|
-
client, contract gate, auth, rate limit, or the
|
|
8
|
-
entrypoint / rewrite. Also use for phrasing like "add GET /...", "new
|
|
7
|
+
client, contract gate, auth, rate limit, background jobs, or the
|
|
8
|
+
single Functions entrypoint / rewrite. Also use for phrasing like "add GET /...", "new
|
|
9
9
|
route", "fix the 401", or "deploy this API". Do not use for Next.js App
|
|
10
10
|
Router work, frontend UI, or unrelated docs.
|
|
11
11
|
license: MIT
|
|
@@ -201,6 +201,49 @@ Register middleware **before** route definitions. Order matters.
|
|
|
201
201
|
Keep the secure baseline (`requestId`, `secureHeaders`, `rateLimit`).
|
|
202
202
|
Add CORS only when needed, with an explicit `origin` allowlist.
|
|
203
203
|
|
|
204
|
+
## Background jobs
|
|
205
|
+
|
|
206
|
+
Side effects that must outlive the HTTP request (welcome emails, webhook
|
|
207
|
+
fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
|
|
208
|
+
inline in the handler and not in a fire-and-forget promise.
|
|
209
|
+
|
|
210
|
+
- `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
|
|
211
|
+
optional in-process worker) into the app lifecycle, including the
|
|
212
|
+
graceful-shutdown drain.
|
|
213
|
+
- Enqueue **after** the DB commit, always with an idempotency key:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { jobIdempotencyKey } from "@daloyjs/core";
|
|
217
|
+
|
|
218
|
+
await app.jobs!.enqueue({
|
|
219
|
+
name: "email.welcome",
|
|
220
|
+
payload: { userId: user.id, to: user.email }, // ids, never file bytes
|
|
221
|
+
idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- Delivery is **at-least-once**: handlers must be idempotent. Pass a key
|
|
226
|
+
through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
|
|
227
|
+
duplicate run would move money or send email.
|
|
228
|
+
- Throw `JobFatalError` for permanent failures (no retry); any other throw
|
|
229
|
+
retries with full-jitter backoff, then dead-letters.
|
|
230
|
+
- `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
|
|
231
|
+
idempotent enqueue. Use it for side effects that must run once
|
|
232
|
+
cluster-wide; keep plain `app.cron()` for process-local maintenance
|
|
233
|
+
(other replicas have their own memory to sweep).
|
|
234
|
+
- Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
|
|
235
|
+
never file contents.
|
|
236
|
+
- `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
|
|
237
|
+
dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
|
|
238
|
+
in app code; `useJobs` warns on Memory in production, and
|
|
239
|
+
`strictProduction: true` refuses to boot.
|
|
240
|
+
- Vercel Functions **enqueue only**: never `startWorker: true` here — a
|
|
241
|
+
Function is frozen once the response flushes. Run the worker as a
|
|
242
|
+
separate long-lived Node service (same app code, `useJobs` with
|
|
243
|
+
`startWorker`) against the same remote store.
|
|
244
|
+
|
|
245
|
+
Full reference: <https://daloyjs.dev/docs/jobs>
|
|
246
|
+
|
|
204
247
|
## Testing best practices
|
|
205
248
|
|
|
206
249
|
Tests use in-process `app.request(...)` — no port, no Vercel runtime
|