@rdlabo/workers-hono-kit 0.10.6 → 0.11.1-beta.pr46.sha8ccc8da384f0

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/docs/cli.md ADDED
@@ -0,0 +1,33 @@
1
+ # CLI
2
+
3
+ The package ships `bin` commands that can be run with `npx` or wired into npm scripts in the consuming app. They are operational helpers: AWS credential sync, subrequest fan-out gating, and brownfield database baselining.
4
+
5
+ | Command | Use |
6
+ | --- | --- |
7
+ | `workers-hono-kit-sync-dev-aws <wrangler-args…>` | Launch `wrangler` with AWS credentials injected as `--var`, resolved from the active AWS profile (honors `AWS_PROFILE`, supports short-lived SSO/temporary creds). Nothing is written to disk — replaces `.dev.vars`. Wire it as the `dev` script, e.g. `AWS_PROFILE=<p> workers-hono-kit-sync-dev-aws dev --var APP_ENV:development`. |
8
+ | `workers-hono-kit-check-subrequest-fanout [dir…]` | CI gate that greps for per-item external-call fan-outs (`runWithConcurrency(` / `PromisePool` / `.withConcurrency(`) that would eventually exceed the Workers subrequest cap. Annotate a genuinely-safe site with `subrequest-ok`. Scans `src` by default; exits 1 on an un-annotated marker. |
9
+ | `workers-hono-kit-db-baseline [--migrations ./drizzle]` | Brownfield first-deploy helper: record the baseline `0000` migration as *already applied* on an existing MySQL DB without running its DDL (the CLI wrapper around `baselineMigrations` / `readBaselineEntry`). Reads DB credentials from `DB_SECRET` (AWS RDS managed secret) or the individual `DB_*` env vars. |
10
+
11
+ ## `workers-hono-kit-sync-dev-aws`
12
+
13
+ Use this in the `dev` npm script when you want AWS credentials from the active profile to be available inside `wrangler dev` without committing them to `.dev.vars`. It resolves short-lived SSO or temporary credentials and passes them as `--var` arguments. Nothing is written to disk.
14
+
15
+ ```bash
16
+ AWS_PROFILE=my-sso-profile workers-hono-kit-sync-dev-aws dev --var APP_ENV:development
17
+ ```
18
+
19
+ ## `workers-hono-kit-check-subrequest-fanout`
20
+
21
+ Run this in CI to catch per-item external call patterns (for example `runWithConcurrency`, `PromisePool`, or `.withConcurrency`) that could fan out beyond the Workers subrequest cap. If a call site is safe, annotate it with `subrequest-ok`. The command scans `src` by default and exits with `1` if it finds an un-annotated marker.
22
+
23
+ ```bash
24
+ workers-hono-kit-check-subrequest-fanout src
25
+ ```
26
+
27
+ ## `workers-hono-kit-db-baseline`
28
+
29
+ Use this for a brownfield first deploy against a MySQL database that already matches the schema in your first migration (`0000_*`). It records that migration as already applied without running its DDL, so later migrations can apply normally. Database credentials are read from `DB_SECRET` (an AWS RDS managed-secret JSON string) or from individual `DB_*` environment variables.
30
+
31
+ ```bash
32
+ workers-hono-kit-db-baseline --migrations ./drizzle
33
+ ```
@@ -0,0 +1,54 @@
1
+ Import database helpers from `@rdlabo/workers-hono-kit/db`. This entry point requires `drizzle-orm` and `mysql2`.
2
+
3
+ ## Hyperdrive database
4
+
5
+ `createHyperdriveDatabase()` lazily opens primary and replica connections from Hyperdrive bindings. `read()` uses the replica query runner; `query()` provides an explicit raw primary SELECT for read-after-write consistency; writes and transactions use the primary Drizzle instance. `readTransaction()` runs Drizzle and raw reads against one primary repeatable-read snapshot. Read transactions are serialized on one separately cached connection so their boundaries cannot mix with each other or with ordinary primary operations. After a fatal mysql2 connection error, a single read or the complete read-only transaction opens a fresh connection and repeats at most once. Writes and write transactions are not repeated because their commit state may be ambiguous. Workers owns connection cleanup at invocation end.
6
+
7
+ ```ts
8
+ import { createHyperdriveDatabase } from '@rdlabo/workers-hono-kit/db';
9
+ import { drizzle } from 'drizzle-orm/mysql2';
10
+
11
+ const db = createHyperdriveDatabase({
12
+ primaryHyperdrive: env.DB_PRIMARY,
13
+ replicaHyperdrive: env.DB_REPLICA,
14
+ createOrm: (primary) => drizzle(primary, { schema }),
15
+ });
16
+
17
+ const rows = await db.read<Item>('SELECT * FROM items WHERE id = ?', [id]);
18
+ const freshRows = await db.query<Item[]>('SELECT * FROM items WHERE id = ?', [id]);
19
+ await db.write((dz) => dz.insert(items).values(input));
20
+ await db.transaction((tx) => tx.insert(items).values(input));
21
+
22
+ const snapshot = await db.readTransaction(async ({ orm, query }) => ({
23
+ items: await orm.select().from(items),
24
+ count: await query<{ count: number }[]>('SELECT COUNT(*) count FROM items'),
25
+ }));
26
+ ```
27
+
28
+ MySQL enforces `READ ONLY` for every transaction attempt. Drizzle does not provide a distinct read-only transaction type, so applications can wrap `orm` in a SELECT-only facade when they also want compile-time enforcement.
29
+
30
+ Do not call `readTransaction()` recursively from inside its callback. Calls share one serialized snapshot lane, so a nested call would wait for its own outer transaction to finish. Consumers that expose nested snapshot helpers should reuse the outer reader instead.
31
+
32
+ Use `hyperdriveConnectionOptions()` when constructing lower-level mysql2 connections. The default JavaScript date conversion timezone is `+09:00`; it does not change the MySQL session timezone.
33
+
34
+ ## Writes and retries
35
+
36
+ - `retryWhenDeadlock()` retries `ER_LOCK_DEADLOCK` with exponential backoff.
37
+ - `insertIdOf()`, `affectedRowsOf()`, and `insertedIdsOf()` normalize mysql2 write results.
38
+ - `withMysqlConnections()` opens primary and replica connections in parallel for a scoped operation.
39
+
40
+ ## Drizzle and JST helpers
41
+
42
+ Use `jstTimestamp`, `jstDatetime`, and `jstDate` for shared date behavior. Pair update timestamps with `jstOnUpdateNow()` because custom timestamp types do not expose Drizzle's `.onUpdateNow()`. For decimal columns, use Drizzle's `decimal(name, { precision, scale, mode: 'number' })` directly.
43
+
44
+ The `/business-time` entry point converts instants and business dates in the JST business timezone:
45
+
46
+ ```ts
47
+ import { addBusinessDays, toBusinessDateTime } from '@rdlabo/workers-hono-kit/business-time';
48
+
49
+ toBusinessDateTime(new Date('2026-07-05T21:00:00Z'));
50
+ // '2026-07-06 06:00:00'
51
+
52
+ addBusinessDays('2026-07-06', 3);
53
+ // '2026-07-09'
54
+ ```
@@ -0,0 +1,32 @@
1
+ # Development
2
+
3
+ These commands are used when working on the package itself:
4
+
5
+ ```bash
6
+ npm install
7
+ npm run typecheck # tsc --noEmit
8
+ npm run lint # eslint
9
+ npm test # vitest
10
+ npm run build # tsc -p tsconfig.build.json → dist/
11
+ ```
12
+
13
+ ## Local development / linking
14
+
15
+ If you consume this package via a local path (e.g. `"@rdlabo/workers-hono-kit": "../../hono-kit"`) rather than from npm, TypeScript and esbuild resolve the package's bare imports from *its own* `node_modules`, which can create a second `zod` instance. That breaks types where your zod-inferred values flow into other libraries (e.g. Drizzle inserts). Dedupe with tsconfig `paths`:
16
+
17
+ ```jsonc
18
+ {
19
+ "compilerOptions": {
20
+ "baseUrl": ".",
21
+ "paths": {
22
+ "zod": ["node_modules/zod"],
23
+ "zod/*": ["node_modules/zod/*"],
24
+ "@hono/zod-validator": ["node_modules/@hono/zod-validator"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ When installed from npm normally, package managers dedupe `zod` to a single copy and this is not needed.
31
+
32
+ When developing against the kit via a direct `file:` link, run `npm install` in the kit repo itself to satisfy its peers (do not add `overrides` on the consumer side).
@@ -0,0 +1,36 @@
1
+ ## Validation
2
+
3
+ `validate(target, schema, options?)` adapts a Zod schema to Hono and returns a NestJS `ValidationPipe`-shaped `400` response. Use `createValidate({ sentry })` to bind optional reporting once.
4
+
5
+ ```ts
6
+ import { createValidate, zNumOptional } from '@rdlabo/workers-hono-kit';
7
+ import { z } from 'zod';
8
+
9
+ const validate = createValidate({ sentry });
10
+ const querySchema = z.object({ page: zNumOptional() });
11
+
12
+ app.get('/items', validate('query', querySchema), async (c) => {
13
+ const query = c.req.valid('query');
14
+ return c.json(await listItems(query.page));
15
+ });
16
+ ```
17
+
18
+ ## Authentication
19
+
20
+ `createAuthMiddleware()` reads a token header, verifies a Firebase ID token, optionally resolves the application user ID, and stores the result on the Hono context. Use `createRemoteFirebaseVerifier(projectId)` for cached remote JWKS verification or `createServiceAccountVerifier()` when Identity Toolkit `getUser` and `deleteUser` operations are required.
21
+
22
+ Keep identity, reauthentication, and feature credential failures distinct with the stable auth-failure body helpers.
23
+
24
+ ## Error and routing contracts
25
+
26
+ - `createAppErrorHandler()` composes query failure classification, generic mysql2 classification, and optional reporting.
27
+ - `createHttpErrorHandler()` maps `HTTPException` to the shared JSON error body.
28
+ - `notFoundHandler()` returns `Cannot METHOD path` with a 404 status.
29
+ - `normalizeTrailingSlash()` removes trailing slashes without redirecting, preserving request bodies.
30
+ - `finalizeResponse()` adds weak ETags and handles matching `If-None-Match` requests.
31
+
32
+ Mount `createMaintenanceMiddleware()` after CORS and before container or database middleware so maintenance responses do not initialize expensive infrastructure.
33
+
34
+ ## Deferred work and observability
35
+
36
+ `createWaitUntilDefer(ctx)` registers background work through `waitUntil` and logs rejected work. `perfLog()` emits per-request application latency, colo, cold/warm state, route, and status to Workers Logs and optionally Analytics Engine.
@@ -0,0 +1,27 @@
1
+ ## Durable Object realtime
2
+
3
+ The root and `/realtime` entry points expose the same focused realtime primitives:
4
+
5
+ - `configureHibernationAutoResponse()` configures runtime ping/pong without waking JavaScript.
6
+ - `upgradeHibernationWebSocket()` attaches state before accepting the socket.
7
+ - `broadcastHibernationWebSockets()` broadcasts through sockets restored by `getWebSockets()`.
8
+ - `acknowledgeHibernationWebSocketClose()` and `closeHibernationWebSocket()` normalize close handling.
9
+ - `retryDurableObjectOperation()` retries only errors marked `retryable` and not `overloaded`. Create a fresh stub inside the operation for every attempt.
10
+ - `invokeDurableObjectFetch()` preserves the structured response/error contract for DO calls.
11
+
12
+ WebSocket protocol parsers validate offered subprotocols before upgrade.
13
+
14
+ ## Offline replica contracts
15
+
16
+ `@rdlabo/workers-hono-kit/offline` is table-agnostic. Product schemas, Zod objects, public-column allowlists, schema hashes, and domain policy stay in the application.
17
+
18
+ `defineRestDbMethodConverter()` types a pure REST method ↔ table converter. Every represented table and column is required, including nullable/default columns. Omit an auto-increment `id` from the product-owned table scheme when a create method intentionally does not own it.
19
+
20
+ Wire helpers canonicalize values:
21
+
22
+ - `toReplicaIsoDatetime()` → UTC ISO-8601
23
+ - `toReplicaDateOnly()` → `YYYY-MM-DD` or `null`
24
+ - `toTinyIntFlag()` / `fromTinyIntFlag()` → boolean/tinyint conversion
25
+ - `replicaNowIso(clock?)` → injectable current time
26
+
27
+ Journal helpers enforce cursor coverage, retention, mutation transactions, and rebaseline behavior. Wire compatibility helpers let an application accept explicit previous fingerprints while maintaining a canonical current fingerprint.
@@ -0,0 +1,48 @@
1
+ # Storage-agnostic role policies
2
+
3
+ `createRolePolicy` builds pure RBAC checks without coupling the policy to a database schema. The application can resolve roles from a membership table, a `users.role` column, token claims, or any other source.
4
+
5
+ ```ts
6
+ import { createRolePolicy } from '@rdlabo/workers-hono-kit';
7
+
8
+ type Role = 'owner' | 'admin' | 'member' | 'read';
9
+ type Permission = 'organization.manage' | 'resource.write' | 'resource.read';
10
+
11
+ const policy = createRolePolicy<Role, Permission>({
12
+ permissions: {
13
+ owner: ['organization.manage', 'resource.write', 'resource.read'],
14
+ admin: ['resource.write', 'resource.read'],
15
+ member: ['resource.write', 'resource.read'],
16
+ read: ['resource.read'],
17
+ },
18
+ assignableRoles: {
19
+ owner: ['admin', 'member', 'read'],
20
+ admin: ['member', 'read'],
21
+ member: [],
22
+ read: [],
23
+ },
24
+ manageableRoles: {
25
+ owner: ['admin', 'member', 'read'],
26
+ admin: ['member', 'read'],
27
+ member: [],
28
+ read: [],
29
+ },
30
+ });
31
+ ```
32
+
33
+ ## Policy fields
34
+
35
+ - `permissions` maps a role to the set of permissions it grants.
36
+ - `assignableRoles` defines which roles an actor may grant to another subject.
37
+ - `manageableRoles` defines which existing subject roles an actor may manage.
38
+
39
+ The resulting `RolePolicy` has four pure checks:
40
+
41
+ ```ts
42
+ policy.hasPermission('member', 'resource.write'); // true
43
+ policy.canAssignRole('admin', 'member'); // true
44
+ policy.canManageRole('owner', 'admin'); // true
45
+ policy.canChangeRole('owner', 'admin', 'member'); // true
46
+ ```
47
+
48
+ `canChangeRole(actor, current, next)` is a combination: the actor must be able to manage the subject's current role and also be allowed to assign the next role. Keeping role lookup and policy checks separate means the same policy can be reused no matter where roles are stored.
@@ -0,0 +1,25 @@
1
+ ## Testing entry point
2
+
3
+ `@rdlabo/workers-hono-kit/testing` requires the database peers and is never loaded by production code.
4
+
5
+ | Helper | Use |
6
+ | --------------------------------------------------------------- | --------------------------------------------------------------------- |
7
+ | `createTestDb()` | Build a Drizzle-migration-backed test database. |
8
+ | `FakeFirebaseVerifier` | Verify registered in-memory Firebase tokens. |
9
+ | `createPoolDatabase()` / `createNoopDatabase()` | Provide database implementations for tests. |
10
+ | `authHeaders()` / `registerFirebaseToken()` / `provisionUser()` | Prepare authenticated route tests. |
11
+ | `configurableFake()` | Create a partial fake that fails explicitly for unconfigured members. |
12
+ | `fakeKv()` / `fakeQueue()` | Use in-memory Workers binding fakes. |
13
+ | Stripe fixture factories | Create typed events, sessions, subscriptions, prices, and intents. |
14
+
15
+ ## Queues
16
+
17
+ `sendInChunks()` bounds queue sends under Workers subrequest limits. `processBatch()` handles a message batch sequentially, bounding concurrent subrequests to one; errors explicitly marked with `queueDisposition: 'discard'` are acknowledged, while other failures retry. `createQueueErrorHandler()` adds logging and optional final-attempt reporting.
18
+
19
+ ## Operational CLI
20
+
21
+ The package publishes commands for synchronizing development AWS credentials, checking subrequest fanout, creating database baselines, checking realtime bundles, and querying Durable Object metrics. Run the exact CLI shipped with the installed package version and review its `--help` before changing infrastructure.
22
+
23
+ ## Trust boundaries
24
+
25
+ AWS, Firebase, AI Gateway, Stripe, and database clients are configured by the consuming application. Do not place domain-specific credentials, schemas, or authorization policy inside the shared kit. Use `createRolePolicy()` only for storage-agnostic role and relation mapping; the application still owns its roles and permissions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.10.6",
3
+ "version": "0.11.1-beta.pr46.sha8ccc8da384f0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -11,12 +11,12 @@
11
11
  "license": "MIT",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/rdlabo-team/workers-hono-kit.git"
14
+ "url": "git+https://github.com/rdlabo-dev/workers-hono-kit.git"
15
15
  },
16
16
  "bugs": {
17
- "url": "https://github.com/rdlabo-team/workers-hono-kit/issues"
17
+ "url": "https://github.com/rdlabo-dev/workers-hono-kit/issues"
18
18
  },
19
- "homepage": "https://github.com/rdlabo-team/workers-hono-kit#readme",
19
+ "homepage": "https://docs.rdlabo.dev/projects/workers-hono-kit",
20
20
  "keywords": [
21
21
  "hono",
22
22
  "cloudflare-workers",
@@ -43,6 +43,7 @@
43
43
  "files": [
44
44
  "dist",
45
45
  "scripts",
46
+ "docs",
46
47
  "!src/**/*.spec.ts"
47
48
  ],
48
49
  "bin": {
File without changes
File without changes
@@ -1,35 +0,0 @@
1
- /**
2
- * Drizzle `customType` params for a MySQL `DECIMAL` column.
3
- *
4
- * @remarks
5
- * - **Reads (SELECT)**: `fromDriver` unifies the driver value (`number` / `string` / `null`) to a JS
6
- * `number | null`. Combined with the connection's `decimalNumbers: true`
7
- * ({@link hyperdriveConnectionOptions} default), it aligns values to numbers even on the Drizzle
8
- * builder path when strings like `"0"` / `"100.00"` slip in, without dropping `0`.
9
- * - **Writes (INSERT/UPDATE)**: `toDriver` binds the number to mysql2 as-is (no `String()` conversion).
10
- * - Raw-SQL `db.read` relies on the connection's `decimalNumbers: true`; the column's `fromDriver` is
11
- * for the Drizzle `select` path.
12
- */
13
- export interface DecimalNumberConfig {
14
- precision: number;
15
- scale: number;
16
- }
17
- /**
18
- * Normalize a DECIMAL value coming from mysql2 / Drizzle to a JS `number | null`.
19
- * `0` is preserved as-is so it is not dropped as falsy.
20
- *
21
- * @param value - the raw driver value (`number` / `string` / `bigint` / nullish).
22
- * @returns the coerced finite number, or `null` when it cannot be resolved.
23
- */
24
- export declare function coerceDecimalNumber(value: unknown): number | null;
25
- /**
26
- * Params for a `customType`. For advanced use; the {@link decimalNumber} column helper is usually enough.
27
- *
28
- * @param config - the DECIMAL `precision` / `scale`.
29
- * @returns the `customType` params (`dataType` / `fromDriver` / `toDriver`).
30
- */
31
- export declare const decimalNumberParams: (config: DecimalNumberConfig) => {
32
- dataType: () => string;
33
- fromDriver: (value: unknown) => number | null;
34
- toDriver: (value: number | string | null) => number | string | null;
35
- };
@@ -1,58 +0,0 @@
1
- /**
2
- * Drizzle `customType` params for a MySQL `DECIMAL` column.
3
- *
4
- * @remarks
5
- * - **Reads (SELECT)**: `fromDriver` unifies the driver value (`number` / `string` / `null`) to a JS
6
- * `number | null`. Combined with the connection's `decimalNumbers: true`
7
- * ({@link hyperdriveConnectionOptions} default), it aligns values to numbers even on the Drizzle
8
- * builder path when strings like `"0"` / `"100.00"` slip in, without dropping `0`.
9
- * - **Writes (INSERT/UPDATE)**: `toDriver` binds the number to mysql2 as-is (no `String()` conversion).
10
- * - Raw-SQL `db.read` relies on the connection's `decimalNumbers: true`; the column's `fromDriver` is
11
- * for the Drizzle `select` path.
12
- */
13
- /**
14
- * Normalize a DECIMAL value coming from mysql2 / Drizzle to a JS `number | null`.
15
- * `0` is preserved as-is so it is not dropped as falsy.
16
- *
17
- * @param value - the raw driver value (`number` / `string` / `bigint` / nullish).
18
- * @returns the coerced finite number, or `null` when it cannot be resolved.
19
- */
20
- export function coerceDecimalNumber(value) {
21
- if (value === null || value === undefined) {
22
- return null;
23
- }
24
- if (typeof value === 'number') {
25
- return Number.isFinite(value) ? value : null;
26
- }
27
- if (typeof value === 'string') {
28
- const trimmed = value.trim();
29
- if (trimmed === '') {
30
- return null;
31
- }
32
- const n = Number(trimmed);
33
- return Number.isFinite(n) ? n : null;
34
- }
35
- if (typeof value === 'bigint') {
36
- return Number(value);
37
- }
38
- return null;
39
- }
40
- /**
41
- * Params for a `customType`. For advanced use; the {@link decimalNumber} column helper is usually enough.
42
- *
43
- * @param config - the DECIMAL `precision` / `scale`.
44
- * @returns the `customType` params (`dataType` / `fromDriver` / `toDriver`).
45
- */
46
- export const decimalNumberParams = (config) => ({
47
- dataType: () => `decimal(${config.precision},${config.scale})`,
48
- fromDriver: (value) => coerceDecimalNumber(value),
49
- toDriver: (value) => {
50
- if (value === null) {
51
- return null;
52
- }
53
- if (typeof value === 'number') {
54
- return value;
55
- }
56
- return coerceDecimalNumber(value);
57
- },
58
- });