@daloyjs/core 0.4.1 → 0.5.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/README.md CHANGED
@@ -27,7 +27,7 @@ DaloyJS exists to be the framework you'd build if you took the best ideas from e
27
27
  | **Supply-chain-hardened installs and publishing** | [pnpm](https://pnpm.io/motivation) + hardened CI/CD | `ignore-scripts`, release-age cooldown, explicit build allowlist, SHA-pinned actions, isolated OIDC publish with provenance. |
28
28
 
29
29
  ```
30
- 228/228 framework tests passing · 100% line + function coverage · clean strict TypeScript 6
30
+ 269/269 framework tests passing · 100% line + function coverage · clean strict TypeScript 6
31
31
  runs on Node, Bun, Deno, Cloudflare, Vercel
32
32
  ~12.3M static-route ops/sec · ~1.5M dynamic-route ops/sec on M-class CPU
33
33
  ```
@@ -77,7 +77,7 @@ verify-store-integrity=true
77
77
  provenance=true
78
78
  ```
79
79
 
80
- These defaults block transitive lifecycle scripts, wait 24 hours before resolving freshly published versions, verify the pnpm store, and require provenance on publish. The few dependencies that truly need install-time builds are allowlisted in `package.json` under `pnpm.onlyBuiltDependencies`.
80
+ These defaults block transitive lifecycle scripts, wait 24 hours before resolving freshly published versions, verify the pnpm store, and require provenance on publish. The few dependencies that truly need install-time builds are allowlisted in `package.json` under `pnpm.onlyBuiltDependencies`, and CI runs `pnpm verify:lockfile` to reject git dependency sources and non-registry tarball URLs in `pnpm-lock.yaml`.
81
81
 
82
82
  Run `pnpm audit --prod` regularly (or `pnpm run audit` in this repo) — and `pnpm install --frozen-lockfile --ignore-scripts` in CI.
83
83
 
@@ -200,7 +200,7 @@ Mount at `/docs` and the UI is always contract-accurate — never stale.
200
200
  | **Method confusion** | Real **405** with `Allow` header, not a misleading 404. |
201
201
  | **CORS misconfig** | Explicit allowlist; never `*` with credentials. |
202
202
  | **Request correlation** | Cryptographic `randomId()` request ids on every response. |
203
- | **Supply chain** | pnpm `ignore-scripts=true`, `minimum-release-age=1440`, verified store, reproducible lockfile, provenance publishing, and CI/CD hardening against cache poisoning and OIDC token abuse. |
203
+ | **Supply chain** | pnpm `ignore-scripts=true`, `minimum-release-age=1440`, verified store, reproducible lockfile, lockfile source verification, provenance publishing, and CI/CD hardening against cache poisoning and OIDC token abuse. |
204
204
 
205
205
  The publish pipeline is also hardened: no `pull_request_target`, no GitHub Actions cache in CI, top-level `permissions: {}`, `step-security/harden-runner`, a separate protected `release.yml` workflow, npm trusted publishing with `--provenance`, CodeQL, OpenSSF Scorecard, zizmor workflow linting, Dependabot, and CODEOWNERS on workflow/package files. See [SECURITY.md](SECURITY.md) and the [supply-chain security docs](https://daloyjs.dev/docs/security/supply-chain).
206
206
 
@@ -301,6 +301,7 @@ Full, versioned plan: [ROADMAP.md](./ROADMAP.md).
301
301
  - [x] Mock mode
302
302
  - [x] Scalar + Swagger UI handlers
303
303
  - [x] **pnpm-first distribution with hardened `.npmrc`**
304
+ - [x] **Lockfile source verification for git/non-registry tarball dependencies**
304
305
  - [x] **100% line + function coverage** enforced by `pnpm coverage`
305
306
 
306
307
  **Current (`0.2.x` follow-up — see [ROADMAP.md](./ROADMAP.md) for the full plan):**
@@ -319,8 +320,14 @@ Full, versioned plan: [ROADMAP.md](./ROADMAP.md).
319
320
  - [ ] Release checklist and publishing docs cleanup
320
321
 
321
322
  **On deck (`0.3.x` and beyond):**
322
- Redis rate-limit store,
323
- CLI route and schema inspector, WebSockets, and HTTP/2 + HTTP/3 adapters.
323
+ WebSockets and HTTP/2 + HTTP/3 adapters.
324
+
325
+ **Shipped from `0.5.x` ("project ops") so far:**
326
+
327
+ - [x] Bun and Deno scaffolder templates (`bun-basic`, `deno-basic`)
328
+ - [x] `--minimal` flag that strips the bookstore demo and `/docs` + `/openapi.json` routes from any template
329
+ - [x] `daloy inspect` CLI: route table, schema summary, contract-test gate, OpenAPI dump, tag/method filters
330
+ - [x] Redis-backed `RateLimitStore` at `@daloyjs/core/rate-limit-redis` with `ioredisAdapter` / `nodeRedisAdapter` and a fail-open default for shared counters across replicas
324
331
 
325
332
  ## License
326
333
 
package/bin/daloy.mjs ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `daloy` CLI shim. Real logic lives in `dist/cli.js` (`src/cli.ts`).
4
+ *
5
+ * For TypeScript entry files we try to register `tsx` if it's installed
6
+ * in the consumer project; otherwise we surface a friendly error pointing
7
+ * users at `node --import tsx`.
8
+ */
9
+
10
+ import { pathToFileURL, fileURLToPath } from "node:url";
11
+ import { resolve, dirname } from "node:path";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { runCli } from "../dist/cli.js";
14
+
15
+ const PKG = JSON.parse(
16
+ readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")
17
+ );
18
+
19
+ const TS_EXT = /\.(ts|tsx|mts|cts)$/i;
20
+
21
+ let tsxRegistered = false;
22
+ async function ensureTsxIfNeeded(specifier) {
23
+ if (!TS_EXT.test(specifier) || tsxRegistered) return;
24
+ try {
25
+ const api = await import("tsx/esm/api");
26
+ api.register();
27
+ tsxRegistered = true;
28
+ } catch {
29
+ throw new Error(
30
+ `Loading TypeScript entry "${specifier}" requires tsx. Install it ` +
31
+ `(\`pnpm add -D tsx\`) or run: node --import tsx ./node_modules/.bin/daloy inspect ${specifier}`
32
+ );
33
+ }
34
+ }
35
+
36
+ async function importEntry(specifier) {
37
+ const abs = resolve(process.cwd(), specifier);
38
+ if (!existsSync(abs)) {
39
+ throw new Error(`Entry file not found: ${abs}`);
40
+ }
41
+ await ensureTsxIfNeeded(abs);
42
+ return import(pathToFileURL(abs).href);
43
+ }
44
+
45
+ const result = await runCli(process.argv.slice(2), {
46
+ stdout: (chunk) => process.stdout.write(chunk),
47
+ stderr: (chunk) => process.stderr.write(chunk),
48
+ importEntry,
49
+ version: PKG.version,
50
+ });
51
+
52
+ process.exit(result.exitCode);
package/dist/cli.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `daloy inspect` — CLI inspector.
3
+ *
4
+ * Loads a user's `App` instance from an entry file and prints its routes,
5
+ * schema summary, dead routes, missing operationIds, or the full OpenAPI
6
+ * 3.1 document.
7
+ *
8
+ * Pure logic lives in `runCli` so it can be unit-tested without spawning
9
+ * a child process. The thin shim in `bin/daloy.mjs` wires this up to
10
+ * `process.argv`, `process.stdout`, dynamic `import()`, and `process.exit`.
11
+ */
12
+ export interface CliIO {
13
+ stdout: (chunk: string) => void;
14
+ stderr: (chunk: string) => void;
15
+ /** Resolve a user-provided entry specifier to a module to import. */
16
+ importEntry: (specifier: string) => Promise<unknown>;
17
+ /** Version string surfaced by `--version`. */
18
+ version: string;
19
+ }
20
+ export interface CliResult {
21
+ exitCode: number;
22
+ }
23
+ export interface CliOptions {
24
+ json: boolean;
25
+ check: boolean;
26
+ schemas: boolean;
27
+ openapi: boolean;
28
+ tag?: string;
29
+ method?: string;
30
+ entry?: string;
31
+ help: boolean;
32
+ version: boolean;
33
+ }
34
+ export declare function parseArgs(argv: readonly string[]): {
35
+ command: string;
36
+ opts: CliOptions;
37
+ };
38
+ export declare function runCli(argv: readonly string[], io: CliIO): Promise<CliResult>;
39
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qEAAqE;IACrE,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAkCD,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAmDxF;AAUD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,CAyDnF"}
package/dist/cli.js ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * `daloy inspect` — CLI inspector.
3
+ *
4
+ * Loads a user's `App` instance from an entry file and prints its routes,
5
+ * schema summary, dead routes, missing operationIds, or the full OpenAPI
6
+ * 3.1 document.
7
+ *
8
+ * Pure logic lives in `runCli` so it can be unit-tested without spawning
9
+ * a child process. The thin shim in `bin/daloy.mjs` wires this up to
10
+ * `process.argv`, `process.stdout`, dynamic `import()`, and `process.exit`.
11
+ */
12
+ import { runContractTests } from "./contract.js";
13
+ import { generateOpenAPI } from "./openapi.js";
14
+ const HELP = `daloy — DaloyJS CLI
15
+
16
+ Usage:
17
+ daloy <command> [options] [entry]
18
+
19
+ Commands:
20
+ inspect [entry] Load an App and print its routes (default command).
21
+
22
+ Options:
23
+ --json Print machine-readable JSON instead of a table.
24
+ --check Run the contract test suite; exit 1 on errors.
25
+ --schemas Include per-route schema presence (body/query/...).
26
+ --openapi Print the OpenAPI 3.1 document for the App.
27
+ --tag <tag> Only show routes that declare this tag.
28
+ --method <method> Only show routes for this HTTP method.
29
+ -h, --help Show this help.
30
+ -v, --version Print the @daloyjs/core version this CLI ships from.
31
+
32
+ Entry:
33
+ A path to a JS or TS file that exports an App instance, either as the
34
+ default export or as a named export called "app". Defaults to
35
+ ./src/app.ts, then ./src/app.js, then ./app.ts, then ./app.js.
36
+
37
+ Examples:
38
+ daloy inspect
39
+ daloy inspect --json src/server.ts
40
+ daloy inspect --check
41
+ daloy inspect --openapi > openapi.json
42
+ `;
43
+ const DEFAULT_ENTRIES = ["src/app.ts", "src/app.js", "app.ts", "app.js"];
44
+ export function parseArgs(argv) {
45
+ const opts = {
46
+ json: false,
47
+ check: false,
48
+ schemas: false,
49
+ openapi: false,
50
+ help: false,
51
+ version: false,
52
+ };
53
+ let command = "inspect";
54
+ let i = argv[0] === "inspect" ? 1 : 0;
55
+ // Treat the first positional that isn't a known command as the entry.
56
+ if (argv[0] === "help") {
57
+ command = "help";
58
+ i = 1;
59
+ }
60
+ for (; i < argv.length; i++) {
61
+ const a = argv[i];
62
+ if (a === undefined)
63
+ continue;
64
+ switch (a) {
65
+ case "--json":
66
+ opts.json = true;
67
+ break;
68
+ case "--check":
69
+ opts.check = true;
70
+ break;
71
+ case "--schemas":
72
+ opts.schemas = true;
73
+ break;
74
+ case "--openapi":
75
+ opts.openapi = true;
76
+ break;
77
+ case "--tag":
78
+ opts.tag = readFlagValue(argv, ++i, "--tag");
79
+ break;
80
+ case "--method":
81
+ opts.method = readFlagValue(argv, ++i, "--method").toUpperCase();
82
+ break;
83
+ case "-h":
84
+ case "--help":
85
+ opts.help = true;
86
+ break;
87
+ case "-v":
88
+ case "--version":
89
+ opts.version = true;
90
+ break;
91
+ default:
92
+ if (a.startsWith("-")) {
93
+ throw new Error(`Unknown flag: ${a}`);
94
+ }
95
+ opts.entry = a;
96
+ }
97
+ }
98
+ return { command, opts };
99
+ }
100
+ function readFlagValue(argv, index, flag) {
101
+ const value = argv[index];
102
+ if (!value || value.startsWith("-")) {
103
+ throw new Error(`${flag} requires a value`);
104
+ }
105
+ return value;
106
+ }
107
+ export async function runCli(argv, io) {
108
+ let parsed;
109
+ try {
110
+ parsed = parseArgs(argv);
111
+ }
112
+ catch (err) {
113
+ io.stderr(`${err.message}\n\n${HELP}`);
114
+ return { exitCode: 2 };
115
+ }
116
+ const { command, opts } = parsed;
117
+ if (opts.help || command === "help") {
118
+ io.stdout(HELP);
119
+ return { exitCode: 0 };
120
+ }
121
+ if (opts.version) {
122
+ io.stdout(`${io.version}\n`);
123
+ return { exitCode: 0 };
124
+ }
125
+ if (command !== "inspect") {
126
+ io.stderr(`Unknown command: ${command}\n\n${HELP}`);
127
+ return { exitCode: 2 };
128
+ }
129
+ let app;
130
+ try {
131
+ app = await loadApp(opts.entry, io);
132
+ }
133
+ catch (err) {
134
+ io.stderr(`${err.message}\n`);
135
+ return { exitCode: 1 };
136
+ }
137
+ if (opts.openapi) {
138
+ const doc = generateOpenAPI(app, {
139
+ info: { title: "App", version: "0.0.0" },
140
+ });
141
+ io.stdout(`${JSON.stringify(doc, null, opts.json ? 0 : 2)}\n`);
142
+ return { exitCode: 0 };
143
+ }
144
+ const all = app.introspect();
145
+ const routes = filterRoutes(all, opts);
146
+ const issues = opts.check ? await runContractTests(app) : undefined;
147
+ if (opts.json) {
148
+ const payload = { routes };
149
+ if (issues)
150
+ payload.contract = issues;
151
+ io.stdout(`${JSON.stringify(payload, null, 2)}\n`);
152
+ return { exitCode: issues && !issues.ok ? 1 : 0 };
153
+ }
154
+ io.stdout(formatTable(routes, opts.schemas));
155
+ if (issues) {
156
+ io.stdout(`\n${formatContract(issues)}`);
157
+ if (!issues.ok)
158
+ return { exitCode: 1 };
159
+ }
160
+ return { exitCode: 0 };
161
+ }
162
+ function filterRoutes(routes, opts) {
163
+ return routes.filter((r) => (!opts.method || r.method === opts.method) &&
164
+ (!opts.tag || Boolean(r.tags?.includes(opts.tag))));
165
+ }
166
+ function formatTable(routes, includeSchemas) {
167
+ if (routes.length === 0) {
168
+ return "No routes registered (or none matched the filter).\n";
169
+ }
170
+ const header = includeSchemas
171
+ ? ["METHOD", "PATH", "OPERATION ID", "B/Q/P/H", "RESPONSES", "TAGS"]
172
+ : ["METHOD", "PATH", "OPERATION ID", "RESPONSES", "TAGS"];
173
+ const rows = [header];
174
+ for (const r of routes) {
175
+ const opId = r.operationId ?? "-";
176
+ const tags = r.tags?.join(",") ?? "-";
177
+ const responses = r.responses.length === 0 ? "-" : r.responses.sort((a, b) => a - b).join(",");
178
+ if (includeSchemas) {
179
+ const flags = `${r.hasBody ? "B" : "-"}${r.hasQuery ? "Q" : "-"}${r.hasParams ? "P" : "-"}${r.hasHeaders ? "H" : "-"}`;
180
+ rows.push([r.method, r.path, opId, flags, responses, tags]);
181
+ }
182
+ else {
183
+ rows.push([r.method, r.path, opId, responses, tags]);
184
+ }
185
+ }
186
+ const widths = header.map((_, col) => Math.max(...rows.map((row) => (row[col] ?? "").length)));
187
+ const out = [];
188
+ for (let i = 0; i < rows.length; i++) {
189
+ const row = rows[i] ?? [];
190
+ const line = row.map((cell, col) => cell.padEnd(widths[col] ?? 0)).join(" ");
191
+ out.push(line.trimEnd());
192
+ if (i === 0)
193
+ out.push(widths.map((w) => "-".repeat(w)).join(" "));
194
+ }
195
+ out.push("");
196
+ out.push(`${routes.length} route${routes.length === 1 ? "" : "s"}.`);
197
+ return `${out.join("\n")}\n`;
198
+ }
199
+ function formatContract(report) {
200
+ const out = [];
201
+ const errors = report.issues.filter((i) => i.level === "error");
202
+ const warnings = report.issues.filter((i) => i.level === "warning");
203
+ out.push(`Contract checks: ${report.checked} route${report.checked === 1 ? "" : "s"} · ` +
204
+ `${errors.length} error${errors.length === 1 ? "" : "s"} · ` +
205
+ `${warnings.length} warning${warnings.length === 1 ? "" : "s"}`);
206
+ for (const issue of report.issues) {
207
+ out.push(` [${issue.level}] ${issue.route}: ${issue.message}`);
208
+ }
209
+ if (report.ok)
210
+ out.push("OK.");
211
+ else
212
+ out.push("FAIL.");
213
+ return `${out.join("\n")}\n`;
214
+ }
215
+ async function loadApp(entry, io) {
216
+ const candidates = entry ? [entry] : DEFAULT_ENTRIES.slice();
217
+ let lastErr;
218
+ for (const candidate of candidates) {
219
+ try {
220
+ const mod = (await io.importEntry(candidate));
221
+ const app = pickApp(mod);
222
+ if (app)
223
+ return app;
224
+ lastErr = new Error(`Loaded "${candidate}" but it did not export an App instance ` +
225
+ `(expected default export or "app" named export).`);
226
+ }
227
+ catch (err) {
228
+ lastErr = err;
229
+ }
230
+ }
231
+ if (entry) {
232
+ throw new Error(`Could not load App from "${entry}": ${lastErr?.message ?? String(lastErr)}`);
233
+ }
234
+ throw new Error(`Could not find an App entry. Tried: ${DEFAULT_ENTRIES.join(", ")}.\n` +
235
+ `Pass an explicit path: daloy inspect ./path/to/app.ts`);
236
+ }
237
+ function pickApp(mod) {
238
+ for (const key of ["default", "app", "default_app"]) {
239
+ const candidate = mod[key];
240
+ if (isApp(candidate))
241
+ return candidate;
242
+ }
243
+ // Fallback: scan all named exports.
244
+ for (const value of Object.values(mod)) {
245
+ if (isApp(value))
246
+ return value;
247
+ }
248
+ return undefined;
249
+ }
250
+ function isApp(value) {
251
+ return (typeof value === "object" &&
252
+ value !== null &&
253
+ Array.isArray(value.routes) &&
254
+ typeof value.introspect === "function" &&
255
+ typeof value.fetch === "function");
256
+ }
257
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Redis-backed {@link RateLimitStore} for {@link rateLimit}.
3
+ *
4
+ * The default in-process `MemoryStore` is per-instance and therefore unsafe
5
+ * behind more than one server replica. This store keeps the same token-bucket
6
+ * semantics (fixed window of `windowMs`) but stores the counter in Redis so
7
+ * every replica observes the same value.
8
+ *
9
+ * We avoid taking a hard dependency on any specific Redis client. Instead the
10
+ * store accepts a tiny {@link RedisCommands} contract: a single `eval`
11
+ * method. It also ships small adapters for the two most common clients
12
+ * ({@link ioredisAdapter}, {@link nodeRedisAdapter}). That keeps installs
13
+ * lightweight and means new clients can be plugged in with ~5 lines of glue
14
+ * code, which matches DaloyJS's "no magic, no global patching" rule.
15
+ *
16
+ * @example Using ioredis
17
+ * ```ts
18
+ * import IORedis from "ioredis";
19
+ * import { rateLimit } from "@daloyjs/core";
20
+ * import { redisRateLimitStore, ioredisAdapter } from "@daloyjs/core/rate-limit-redis";
21
+ *
22
+ * const redis = new IORedis(process.env.REDIS_URL!);
23
+ * app.use(rateLimit({
24
+ * windowMs: 60_000,
25
+ * max: 120,
26
+ * store: redisRateLimitStore({ client: ioredisAdapter(redis) }),
27
+ * }));
28
+ * ```
29
+ *
30
+ * @example Using node-redis v4+
31
+ * ```ts
32
+ * import { createClient } from "redis";
33
+ * import { redisRateLimitStore, nodeRedisAdapter } from "@daloyjs/core/rate-limit-redis";
34
+ *
35
+ * const redis = createClient({ url: process.env.REDIS_URL });
36
+ * await redis.connect();
37
+ * app.use(rateLimit({
38
+ * windowMs: 60_000,
39
+ * max: 120,
40
+ * store: redisRateLimitStore({ client: nodeRedisAdapter(redis) }),
41
+ * }));
42
+ * ```
43
+ */
44
+ import type { RateLimitStore } from "./middleware.js";
45
+ /**
46
+ * Minimal Redis transport contract used by {@link redisRateLimitStore}.
47
+ *
48
+ * `eval` must execute the supplied Lua script atomically on the server and
49
+ * return whatever Redis returns. Returning the array `[count, ttlMs]` is
50
+ * required by the bundled script.
51
+ */
52
+ export interface RedisCommands {
53
+ eval(script: string, keys: string[], args: string[]): Promise<unknown>;
54
+ }
55
+ /** Options accepted by {@link redisRateLimitStore}. */
56
+ export interface RedisRateLimitStoreOptions {
57
+ client: RedisCommands;
58
+ /**
59
+ * Optional namespace prefix for every Redis key. Defaults to `"daloy:rl:"`.
60
+ * Use a unique prefix per app/environment to avoid key collisions on a
61
+ * shared Redis.
62
+ */
63
+ prefix?: string;
64
+ /**
65
+ * Called when the underlying Redis call throws. The default behavior is
66
+ * fail-open, which allows the request and reports it as the first hit in a
67
+ * fresh local window. Override to fail-closed or to wire into your
68
+ * structured logger.
69
+ */
70
+ onError?: (err: unknown) => "fail-open" | "fail-closed";
71
+ }
72
+ /**
73
+ * Build a {@link RateLimitStore} that persists counters in Redis.
74
+ *
75
+ * The returned store is safe to share between requests and replicas. Errors
76
+ * from Redis are fail-open by default (see {@link RedisRateLimitStoreOptions.onError});
77
+ * pass a custom handler to fail-closed (return `"fail-closed"`).
78
+ */
79
+ export declare function redisRateLimitStore(opts: RedisRateLimitStoreOptions): RateLimitStore;
80
+ /**
81
+ * Shape of an `ioredis` client we care about. Only the `eval` overload that
82
+ * takes `(script, numKeys, ...keysAndArgs)` is used.
83
+ */
84
+ export interface IoredisLike {
85
+ eval(script: string, numKeys: number, ...keysAndArgs: string[]): Promise<unknown>;
86
+ }
87
+ /** Wrap an [`ioredis`](https://github.com/redis/ioredis) client. */
88
+ export declare function ioredisAdapter(client: IoredisLike): RedisCommands;
89
+ /**
90
+ * Shape of a `node-redis` v4+ client we care about. The v4 `eval` takes an
91
+ * options object instead of variadic arguments.
92
+ */
93
+ export interface NodeRedisLike {
94
+ eval(script: string, options: {
95
+ keys: string[];
96
+ arguments: string[];
97
+ }): Promise<unknown>;
98
+ }
99
+ /** Wrap a [`node-redis`](https://github.com/redis/node-redis) v4+ client. */
100
+ export declare function nodeRedisAdapter(client: NodeRedisLike): RedisCommands;
101
+ //# sourceMappingURL=rate-limit-redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-redis.d.ts","sourceRoot":"","sources":["../src/rate-limit-redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE;AAED,uDAAuD;AACvD,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,WAAW,GAAG,aAAa,CAAC;CACzD;AA+BD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,cAAc,CAoBpF;AAID;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnF;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAMjE;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CACF,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,GAC/C,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAMrE"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Redis-backed {@link RateLimitStore} for {@link rateLimit}.
3
+ *
4
+ * The default in-process `MemoryStore` is per-instance and therefore unsafe
5
+ * behind more than one server replica. This store keeps the same token-bucket
6
+ * semantics (fixed window of `windowMs`) but stores the counter in Redis so
7
+ * every replica observes the same value.
8
+ *
9
+ * We avoid taking a hard dependency on any specific Redis client. Instead the
10
+ * store accepts a tiny {@link RedisCommands} contract: a single `eval`
11
+ * method. It also ships small adapters for the two most common clients
12
+ * ({@link ioredisAdapter}, {@link nodeRedisAdapter}). That keeps installs
13
+ * lightweight and means new clients can be plugged in with ~5 lines of glue
14
+ * code, which matches DaloyJS's "no magic, no global patching" rule.
15
+ *
16
+ * @example Using ioredis
17
+ * ```ts
18
+ * import IORedis from "ioredis";
19
+ * import { rateLimit } from "@daloyjs/core";
20
+ * import { redisRateLimitStore, ioredisAdapter } from "@daloyjs/core/rate-limit-redis";
21
+ *
22
+ * const redis = new IORedis(process.env.REDIS_URL!);
23
+ * app.use(rateLimit({
24
+ * windowMs: 60_000,
25
+ * max: 120,
26
+ * store: redisRateLimitStore({ client: ioredisAdapter(redis) }),
27
+ * }));
28
+ * ```
29
+ *
30
+ * @example Using node-redis v4+
31
+ * ```ts
32
+ * import { createClient } from "redis";
33
+ * import { redisRateLimitStore, nodeRedisAdapter } from "@daloyjs/core/rate-limit-redis";
34
+ *
35
+ * const redis = createClient({ url: process.env.REDIS_URL });
36
+ * await redis.connect();
37
+ * app.use(rateLimit({
38
+ * windowMs: 60_000,
39
+ * max: 120,
40
+ * store: redisRateLimitStore({ client: nodeRedisAdapter(redis) }),
41
+ * }));
42
+ * ```
43
+ */
44
+ /**
45
+ * Atomic INCR + PEXPIRE script.
46
+ *
47
+ * Returns `{count, ttlMs}` so the caller can compute `resetMs` without a
48
+ * second round trip. We set the TTL only on the very first INCR so a busy
49
+ * key keeps its original window and is not perpetually extended.
50
+ */
51
+ const SCRIPT = `local current = redis.call('INCR', KEYS[1])
52
+ if current == 1 then
53
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
54
+ return {current, tonumber(ARGV[1])}
55
+ end
56
+ local ttl = redis.call('PTTL', KEYS[1])
57
+ if ttl < 0 then
58
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
59
+ ttl = tonumber(ARGV[1])
60
+ end
61
+ return {current, ttl}`;
62
+ function toNumber(value) {
63
+ if (typeof value === "number")
64
+ return value;
65
+ if (typeof value === "bigint")
66
+ return Number(value);
67
+ if (typeof value === "string") {
68
+ const n = Number(value);
69
+ return Number.isFinite(n) ? n : 0;
70
+ }
71
+ return 0;
72
+ }
73
+ /**
74
+ * Build a {@link RateLimitStore} that persists counters in Redis.
75
+ *
76
+ * The returned store is safe to share between requests and replicas. Errors
77
+ * from Redis are fail-open by default (see {@link RedisRateLimitStoreOptions.onError});
78
+ * pass a custom handler to fail-closed (return `"fail-closed"`).
79
+ */
80
+ export function redisRateLimitStore(opts) {
81
+ const prefix = opts.prefix ?? "daloy:rl:";
82
+ const onError = opts.onError;
83
+ return {
84
+ async hit(key, windowMs) {
85
+ const fullKey = prefix + key;
86
+ try {
87
+ const result = (await opts.client.eval(SCRIPT, [fullKey], [String(windowMs)]));
88
+ const count = toNumber(result?.[0]);
89
+ const ttl = toNumber(result?.[1]);
90
+ return { count, resetMs: Date.now() + ttl };
91
+ }
92
+ catch (err) {
93
+ const decision = onError ? onError(err) : "fail-open";
94
+ if (decision === "fail-closed")
95
+ throw err;
96
+ return { count: 1, resetMs: Date.now() + windowMs };
97
+ }
98
+ },
99
+ };
100
+ }
101
+ /** Wrap an [`ioredis`](https://github.com/redis/ioredis) client. */
102
+ export function ioredisAdapter(client) {
103
+ return {
104
+ eval(script, keys, args) {
105
+ return client.eval(script, keys.length, ...keys, ...args);
106
+ },
107
+ };
108
+ }
109
+ /** Wrap a [`node-redis`](https://github.com/redis/node-redis) v4+ client. */
110
+ export function nodeRedisAdapter(client) {
111
+ return {
112
+ eval(script, keys, args) {
113
+ return client.eval(script, { keys, arguments: args });
114
+ },
115
+ };
116
+ }
117
+ //# sourceMappingURL=rate-limit-redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-redis.js","sourceRoot":"","sources":["../src/rate-limit-redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAiCH;;;;;;GAMG;AACH,MAAM,MAAM,GAAG;;;;;;;;;;sBAUO,CAAC;AAEvB,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAgC;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC7B,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,QAAgB;YACrC,MAAM,OAAO,GAAG,MAAM,GAAG,GAAG,CAAC;YAC7B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAEvD,CAAC;gBACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;gBACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC;YAC9C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;gBACtD,IAAI,QAAQ,KAAK,aAAa;oBAAE,MAAM,GAAG,CAAC;gBAC1C,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YACtD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAYD,oEAAoE;AACpE,MAAM,UAAU,cAAc,CAAC,MAAmB;IAChD,OAAO;QACL,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI;YACrB,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;QAC5D,CAAC;KACF,CAAC;AACJ,CAAC;AAaD,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,OAAO;QACL,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI;YACrB,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -17,6 +17,9 @@
17
17
  "author": "DaloyJS",
18
18
  "main": "./dist/index.js",
19
19
  "types": "./dist/index.d.ts",
20
+ "bin": {
21
+ "daloy": "bin/daloy.mjs"
22
+ },
20
23
  "engines": {
21
24
  "node": ">=20.10.0",
22
25
  "pnpm": ">=9.0.0"
@@ -73,10 +76,19 @@
73
76
  "./multipart": {
74
77
  "types": "./dist/multipart.d.ts",
75
78
  "import": "./dist/multipart.js"
79
+ },
80
+ "./rate-limit-redis": {
81
+ "types": "./dist/rate-limit-redis.d.ts",
82
+ "import": "./dist/rate-limit-redis.js"
83
+ },
84
+ "./cli": {
85
+ "types": "./dist/cli.d.ts",
86
+ "import": "./dist/cli.js"
76
87
  }
77
88
  },
78
89
  "files": [
79
90
  "dist",
91
+ "bin",
80
92
  "README.md"
81
93
  ],
82
94
  "keywords": [
@@ -114,6 +126,7 @@
114
126
  "gen:openapi": "node --import tsx scripts/dump-openapi.ts",
115
127
  "gen:client": "openapi-ts",
116
128
  "gen": "pnpm gen:openapi && pnpm gen:client",
129
+ "verify:lockfile": "node --import tsx scripts/verify-lockfile-sources.ts",
117
130
  "audit": "pnpm audit --prod"
118
131
  }
119
132
  }