@rdlabo/workers-hono-kit 0.2.0 → 0.2.1

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.
Files changed (97) hide show
  1. package/README.md +126 -11
  2. package/dist/ai/gateway.d.ts +54 -16
  3. package/dist/ai/gateway.js +37 -12
  4. package/dist/aws/cloudfront.d.ts +23 -5
  5. package/dist/aws/cloudfront.js +45 -6
  6. package/dist/aws/secrets-manager.d.ts +38 -4
  7. package/dist/aws/secrets-manager.js +48 -3
  8. package/dist/cache/kv-cache.d.ts +173 -10
  9. package/dist/cache/kv-cache.js +139 -7
  10. package/dist/db/connection.d.ts +56 -14
  11. package/dist/db/connection.js +39 -13
  12. package/dist/db/database.d.ts +159 -23
  13. package/dist/db/database.js +49 -5
  14. package/dist/db/index.d.ts +11 -0
  15. package/dist/db/index.js +11 -2
  16. package/dist/db/jst.d.ts +89 -6
  17. package/dist/db/jst.js +89 -23
  18. package/dist/db/orm-config.d.ts +61 -19
  19. package/dist/db/orm-config.js +43 -14
  20. package/dist/db/retry.d.ts +25 -3
  21. package/dist/db/retry.js +25 -3
  22. package/dist/db/write-result.d.ts +27 -4
  23. package/dist/db/write-result.js +22 -1
  24. package/dist/firebase/firebase-verifier.d.ts +53 -4
  25. package/dist/firebase/identity-toolkit.d.ts +54 -5
  26. package/dist/firebase/identity-toolkit.js +51 -0
  27. package/dist/firebase/jose-firebase-verifier.d.ts +79 -7
  28. package/dist/firebase/jose-firebase-verifier.js +68 -7
  29. package/dist/firebase/remote-verifier.d.ts +42 -4
  30. package/dist/firebase/remote-verifier.js +58 -9
  31. package/dist/http/app-env.d.ts +41 -8
  32. package/dist/http/app-env.js +38 -8
  33. package/dist/http/app-info.d.ts +25 -3
  34. package/dist/http/app-info.js +16 -2
  35. package/dist/http/http-status.d.ts +12 -3
  36. package/dist/http/http-status.js +12 -3
  37. package/dist/http/nest-error.d.ts +90 -29
  38. package/dist/http/nest-error.js +59 -18
  39. package/dist/http/user-protocol.d.ts +23 -3
  40. package/dist/http/user-protocol.js +14 -2
  41. package/dist/index.d.ts +11 -0
  42. package/dist/index.js +11 -3
  43. package/dist/middleware/auth.d.ts +74 -13
  44. package/dist/middleware/auth.js +30 -6
  45. package/dist/middleware/finalize-response.d.ts +30 -0
  46. package/dist/middleware/finalize-response.js +41 -12
  47. package/dist/middleware/validation.d.ts +83 -9
  48. package/dist/middleware/validation.js +52 -9
  49. package/dist/middleware/zod-coerce.d.ts +56 -2
  50. package/dist/middleware/zod-coerce.js +68 -9
  51. package/dist/stripe/client.d.ts +46 -9
  52. package/dist/stripe/client.js +41 -3
  53. package/dist/testing/auth.d.ts +58 -10
  54. package/dist/testing/auth.js +58 -10
  55. package/dist/testing/configurable-fake.d.ts +20 -9
  56. package/dist/testing/configurable-fake.js +23 -11
  57. package/dist/testing/db.d.ts +81 -12
  58. package/dist/testing/db.js +23 -1
  59. package/dist/testing/fakes.d.ts +77 -9
  60. package/dist/testing/fakes.js +69 -7
  61. package/dist/testing/index.d.ts +7 -0
  62. package/dist/testing/index.js +10 -5
  63. package/dist/testing/stripe-fixtures.d.ts +93 -3
  64. package/dist/testing/stripe-fixtures.js +93 -3
  65. package/package.json +1 -1
  66. package/src/ai/gateway.ts +66 -27
  67. package/src/aws/cloudfront.ts +46 -6
  68. package/src/aws/secrets-manager.ts +56 -7
  69. package/src/cache/kv-cache.ts +194 -12
  70. package/src/db/connection.ts +56 -14
  71. package/src/db/database.ts +160 -24
  72. package/src/db/index.ts +11 -2
  73. package/src/db/jst.ts +89 -23
  74. package/src/db/orm-config.ts +61 -19
  75. package/src/db/retry.ts +25 -3
  76. package/src/db/write-result.ts +27 -4
  77. package/src/firebase/firebase-verifier.ts +53 -4
  78. package/src/firebase/identity-toolkit.ts +57 -5
  79. package/src/firebase/jose-firebase-verifier.ts +79 -9
  80. package/src/firebase/remote-verifier.ts +58 -9
  81. package/src/http/app-env.ts +41 -8
  82. package/src/http/app-info.ts +25 -3
  83. package/src/http/http-status.ts +12 -3
  84. package/src/http/nest-error.ts +106 -37
  85. package/src/http/user-protocol.ts +23 -3
  86. package/src/index.ts +11 -3
  87. package/src/middleware/auth.ts +77 -15
  88. package/src/middleware/finalize-response.ts +41 -12
  89. package/src/middleware/validation.ts +89 -15
  90. package/src/middleware/zod-coerce.ts +68 -9
  91. package/src/stripe/client.ts +46 -9
  92. package/src/testing/auth.ts +58 -10
  93. package/src/testing/configurable-fake.ts +23 -11
  94. package/src/testing/db.ts +82 -13
  95. package/src/testing/fakes.ts +77 -9
  96. package/src/testing/index.ts +10 -5
  97. package/src/testing/stripe-fixtures.ts +93 -3
package/README.md CHANGED
@@ -7,8 +7,11 @@ It provides the building blocks a NestJS-style API needs but that don't run on `
7
7
  - **Firebase ID-token verification** on Workers via [`jose`](https://github.com/panva/jose) (RS256 against Google's securetoken JWKS), with optional Identity Toolkit REST for `getUser` / `deleteUser`.
8
8
  - **AWS Secrets Manager** via SigV4-signed `fetch` ([`aws4fetch`](https://github.com/mhart/aws4fetch)) — no AWS SDK.
9
9
  - **Middleware**: `finalizeResponse` (Express-compatible weak ETag + JSON charset), `validate` (NestJS `ValidationPipe`-shaped 400), and zod number-coercion helpers.
10
- - **Deadlock retry** (`ER_LOCK_DEADLOCK` exponential backoff).
11
- - **HTTP helpers**: `getUserProtocol`, `getAppInfo`, `HttpStatus`.
10
+ - **NestJS-shaped errors**: `createNestErrorHandler` / `nestNotFoundHandler` / `HttpStatus`.
11
+ - **Deadlock retry** (`ER_LOCK_DEADLOCK` exponential backoff) and an optional **MySQL data layer** (`@rdlabo/workers-hono-kit/db`) for Hyperdrive + Drizzle.
12
+ - **AI Gateway**: route `@ai-sdk` models through the Cloudflare AI Gateway.
13
+ - **Stripe** Workers-native client + async webhook verification.
14
+ - **Testing helpers** (`@rdlabo/workers-hono-kit/testing`): a Drizzle-migration-backed test database, in-memory Firebase fake, configurable test doubles, and Stripe fixtures.
12
15
 
13
16
  ## Install
14
17
 
@@ -19,17 +22,36 @@ npm install @rdlabo/workers-hono-kit
19
22
  Peer dependencies — install the ones you use:
20
23
 
21
24
  ```bash
25
+ # Core (root export)
22
26
  npm install hono zod @hono/zod-validator jose aws4fetch
27
+
28
+ # Optional, only if you use the corresponding feature:
29
+ npm install drizzle-orm mysql2 # ./db and ./testing
30
+ npm install ai ai-gateway-provider # createAiGatewayProvider
23
31
  ```
24
32
 
25
- > **TypeScript sources, no build step.** The package is published as `.ts` via the `exports` field and is meant to be consumed by a bundler that compiles TypeScript — wrangler/esbuild, Vite, etc. targeting `workerd` or another edge runtime. It relies only on Web-standard APIs (`fetch`, `crypto.subtle`, `Response`) available on Cloudflare Workers.
33
+ `stripe` is bundled as a direct dependency, so the Stripe helpers work without an extra install.
34
+
35
+ > **Compiled ESM, with types.** The package is published as compiled ES modules (`./dist/*.js`) plus
36
+ > declaration files (`./dist/*.d.ts`) via the `exports` field. It depends only on Web-standard APIs
37
+ > (`fetch`, `crypto.subtle`, `Response`) available on Cloudflare Workers (`workerd`) and other edge
38
+ > runtimes, and requires Node.js ≥ 20 for tooling. Three entry points are exposed:
39
+ >
40
+ > | Subpath | Import | Use |
41
+ > | --- | --- | --- |
42
+ > | `.` | `@rdlabo/workers-hono-kit` | Web-standard helpers (middleware, HTTP, Firebase, AWS, AI, Stripe, KV). |
43
+ > | `./db` | `@rdlabo/workers-hono-kit/db` | MySQL data layer (mysql2 + Drizzle). |
44
+ > | `./testing` | `@rdlabo/workers-hono-kit/testing` | Test helpers (mysql2 + Drizzle + fakes/fixtures). |
26
45
 
27
46
  ## API
28
47
 
48
+ ### Root — `@rdlabo/workers-hono-kit`
49
+
29
50
  | Export | Description |
30
51
  | --- | --- |
31
52
  | `finalizeResponse()` | Middleware that adds an Express-compatible weak `ETag` and JSON `charset=utf-8`. |
32
53
  | `validate(target, schema, options?)` | Zod validator → NestJS `ValidationPipe`-shaped `400` (`{ statusCode, message[], error }`). `options.onValidationError(err, c)` to report (e.g. Sentry). |
54
+ | `createSentryValidate(sentry)` | Returns a `validate` variant that reports validation failures to an injected Sentry-like client (tags + context), avoiding a hard `@sentry/cloudflare` dependency. |
33
55
  | `zNum` / `zNumWithDefault` / `zNumOptional` / `zNumNullable` | Number-coercion zod schemas (mirror class-transformer `@Transform`). |
34
56
  | `getAuthenticationSecret<T>(options, secretId)` / `AwsSecretsOptions` | Fetch a secret from AWS Secrets Manager (SigV4 `fetch`, per-isolate cache). |
35
57
  | `getCloudFrontSignedUrl(url, privateKeyPem, keyPairId, dateLessThan)` | CloudFront signed URL (canned policy, RSA-SHA1, URL-safe base64) — Web Crypto reimpl of `@aws-sdk/cloudfront-signer`, byte-identical query order. |
@@ -40,14 +62,48 @@ npm install hono zod @hono/zod-validator jose aws4fetch
40
62
  | `retryWhenDeadlock(fn, retries?, delay?)` | Retry on MySQL `ER_LOCK_DEADLOCK` with exponential backoff. |
41
63
  | `getUserProtocol(c)` / `IUserProtocol` | Read client IP / UA (`CF-Connecting-IP` → `X-Forwarded-For`). |
42
64
  | `getAppInfo(c)` / `AppInfo` | Read `x-amz-meta-version` / `x-amz-meta-uuid`. |
65
+ | `resolveAppEnv(env)` / `isProductionEnv(env)` / `AppEnv` | Resolve `'development'` / `'production'` from `env.APP_ENV` (defaults to `'production'` for safety). |
43
66
  | `HttpStatus` | HTTP status enum identical to NestJS `@nestjs/common`. |
44
67
  | `createNestErrorHandler(options?)` / `NestErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to the NestJS exception-filter body (`{ statusCode, message, error? }`; `401` omits `error`). Configurable field order, reason phrases, error predicate, and unhandled-error report hook. |
45
68
  | `nestNotFoundHandler(c)` | `app.notFound()` handler with the Express/Nest default `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
46
69
  | `NEST_REASON_PHRASES` | `{ 400, 401, 403, 404 }` → NestJS reason phrases. |
47
70
  | `createAuthMiddleware(options)` / `AuthMiddlewareOptions` | Factory for a Firebase-token auth middleware: reads the token header, verifies, resolves the DB user id, and stashes the result on the context. Omit `resolveUserId` for a token-only (login) guard. |
48
- | `ErrorReporter` / `ErrorReportContext` | Types for a `container.reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
49
- | `KVCache` / `KVNamespace` / `KVCacheOptions` | Workers-KV cache-aside helper (key `appName+version+table_type_column`, sha256 for string ids, TTL clamped ≥60s). `appName`/`version` per repo. |
50
- | `createStripeClient(secret, opts?)` / `verifyStripeWebhook(...)` / `CreateStripeClientOptions` | Workers-native Stripe client (fetch transport) + async webhook verification (SubtleCrypto). `apiVersion` optional (pin per `/api` parity). |
71
+ | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
72
+ | `createAiGatewayProvider(config)` / `AiGatewayConfig` / `AiGatewayProvider` | Route `@ai-sdk` models through the Cloudflare AI Gateway, via either a Workers `AI` binding or REST credentials (`accountId` / `gateway` / `token`). |
73
+ | `KVCache` / `KVNamespace` / `KVCacheOptions` | Workers-KV cache-aside helper (key `appName+version+table_type_column`, sha256 for string ids, TTL clamped ≥60s). Set `appName` / `version` per application. |
74
+ | `createStripeClient(secret, opts?)` / `verifyStripeWebhook(...)` / `CreateStripeClientOptions` | Workers-native Stripe client (fetch transport) + async webhook verification (SubtleCrypto). `apiVersion` optional (pin to a fixed Stripe API version). |
75
+
76
+ ### Data layer — `@rdlabo/workers-hono-kit/db`
77
+
78
+ Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via raw SQL; writes/transactions run against the primary through the Drizzle ORM with deadlock retry. The kit deliberately does not depend on the ORM's type identity — you pass the Drizzle instance in.
79
+
80
+ | Export | Description |
81
+ | --- | --- |
82
+ | `createHyperdriveDatabase(options)` | `DisposableDatabase` that lazily opens primary/replica connections from Hyperdrive bindings per request; `dispose()` closes them. |
83
+ | `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
84
+ | `Database` / `DisposableDatabase` / `QueryRunner` / `TxOf` | The `read` / `write` / `transaction` API and its supporting types. |
85
+ | `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). |
86
+ | `withMysqlConnections(...)` | Open primary/replica connections, run a function, close them in `finally` (via `ctx.waitUntil`). |
87
+ | `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
88
+ | `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
89
+ | `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization applied at the Drizzle `customType` column boundary. |
90
+ | `DRIZZLE_ORM_OPTIONS` / `honoDrizzleConfig(options)` / `HonoDrizzleConfigOptions` | Shared Drizzle casing (`snake_case`) for both the runtime `drizzle()` call and `drizzle.config.ts`, keeping config ↔ runtime in sync. |
91
+
92
+ ### Testing — `@rdlabo/workers-hono-kit/testing`
93
+
94
+ Requires the `drizzle-orm` and `mysql2` peers. Consolidates duplicated test boilerplate.
95
+
96
+ | Export | Description |
97
+ | --- | --- |
98
+ | `createTestDb(options)` / `TestDb` / `CreateTestDbOptions` / `TestDbConnection` | Test database built from committed Drizzle migrations as the single source of truth: `resetSchema` / `createTestPool` / `truncateAll` / `seed` / `mysqlReachable`. |
99
+ | `FakeFirebaseVerifier` | In-memory `FirebaseVerifier` for offline route tests (`register` / `verifyIdToken` / `getUser` / `deleteUser`). |
100
+ | `createPoolDatabase(options)` / `CreatePoolDatabaseOptions` | A `Database` backed by a single pool used as both primary and replica. |
101
+ | `createNoopDatabase()` | A `Database` stub that throws on `write` / `transaction` to catch accidental DB use in DB-less routes. |
102
+ | `authHeaders(token, opts?)` | Build interceptor-compatible auth headers for requests. |
103
+ | `registerFirebaseToken(firebase, uid, record?, token?)` | Register a token in a `FakeFirebaseVerifier` (no DB). |
104
+ | `provisionUser(pool, firebase, opts)` | Register a token and provision a conventional `users(id, firebase_uid, agree)` row; returns the user id (idempotent). |
105
+ | `configurableFake(impl, name?)` | Build a test double from a partial implementation; un-stubbed members throw `"${name}.${method} not configured"`. |
106
+ | `fakeApiList` / `fakePaymentIntent` / `fakeStripeEvent` / `fakeCheckoutSession` / `fakeCustomer` / `fakePrice` / `fakeSubscription` | Stripe object fixtures with sensible defaults, overridable per test. |
51
107
 
52
108
  ## Usage
53
109
 
@@ -144,8 +200,8 @@ return c.json(body, HttpStatus.CREATED);
144
200
 
145
201
  `createNestErrorHandler()` renders a thrown `HTTPException` as the NestJS exception-filter
146
202
  body, and `nestNotFoundHandler` gives the Express/Nest default 404. The defaults match the
147
- NestJS canonical shape (`{ statusCode, message, error? }`, `401` omits `error`); each repo
148
- keeps its own byte-parity via options.
203
+ NestJS canonical shape (`{ statusCode, message, error? }`, `401` omits `error`); the options
204
+ let you reproduce any byte-for-byte variation an existing API expects.
149
205
 
150
206
  ```ts
151
207
  import { createNestErrorHandler, nestNotFoundHandler } from '@rdlabo/workers-hono-kit';
@@ -153,7 +209,7 @@ import { createNestErrorHandler, nestNotFoundHandler } from '@rdlabo/workers-hon
153
209
  app.notFound(nestNotFoundHandler);
154
210
  app.onError(createNestErrorHandler());
155
211
 
156
- // Repo-specific parity deltas:
212
+ // Application-specific parity deltas:
157
213
  app.onError(
158
214
  createNestErrorHandler({
159
215
  fieldOrder: 'message-first', // emit { message, error, statusCode } instead of statusCode-first
@@ -166,7 +222,7 @@ app.onError(
166
222
  ### Auth middleware
167
223
 
168
224
  Encodes the shared skeleton (read token header → verify → `getAppInfo` → resolve user id →
169
- set context, with `console.error` + a configurable failure on error). Inject the repo's
225
+ set context, with `console.error` + a configurable failure on error). Inject your own
170
226
  verify/resolver, context-variable names, and failure mode.
171
227
 
172
228
  `createAuthMiddleware<Env, Verified, Id>` is generic over your Hono `Env`, so `c.set(...)` in
@@ -195,12 +251,55 @@ const tokenAuth = createAuthMiddleware<AppEnv, UserRecord>({
195
251
  });
196
252
  ```
197
253
 
254
+ ### AI Gateway
255
+
256
+ Route `@ai-sdk` models through the Cloudflare AI Gateway — either with a Workers `AI` binding
257
+ (production / `wrangler dev`) or with REST credentials (non-Workers contexts).
258
+
259
+ ```ts
260
+ import { createAiGatewayProvider } from '@rdlabo/workers-hono-kit';
261
+ import { openai } from '@ai-sdk/openai';
262
+
263
+ // Binding form (Workers):
264
+ const provider = createAiGatewayProvider({ binding: env.AI.gateway('my-gateway') });
265
+
266
+ // REST form (anywhere):
267
+ const rest = createAiGatewayProvider({
268
+ accountId: env.CF_ACCOUNT_ID,
269
+ gateway: 'my-gateway',
270
+ token: env.CF_AIG_TOKEN,
271
+ });
272
+
273
+ const model = provider.aigateway(openai('gpt-4o-mini'));
274
+ ```
275
+
276
+ ### MySQL data layer (Hyperdrive + Drizzle)
277
+
278
+ ```ts
279
+ import { createHyperdriveDatabase, hyperdriveConnectionOptions } from '@rdlabo/workers-hono-kit/db';
280
+ import { drizzle } from 'drizzle-orm/mysql2';
281
+ import { DRIZZLE_ORM_OPTIONS } from '@rdlabo/workers-hono-kit/db';
282
+
283
+ const db = createHyperdriveDatabase({
284
+ primaryHyperdrive: env.HYPERDRIVE,
285
+ replicaHyperdrive: env.HYPERDRIVE_REPLICA,
286
+ createOrm: (conn) => drizzle(conn, { ...DRIZZLE_ORM_OPTIONS, schema }),
287
+ });
288
+
289
+ try {
290
+ const rows = await db.read('SELECT * FROM users WHERE id = ?', [id]); // replica, raw SQL
291
+ await db.write((dz) => dz.insert(users).values({ name })); // primary, deadlock-retried
292
+ } finally {
293
+ await db.dispose();
294
+ }
295
+ ```
296
+
198
297
  ### KV cache
199
298
 
200
299
  ```ts
201
300
  import { KVCache } from '@rdlabo/workers-hono-kit';
202
301
 
203
- const cache = new KVCache(env.CACHE, { appName: 'myapp' }); // version defaults to 'v8_'
302
+ const cache = new KVCache(env.CACHE, { appName: 'myapp' }); // version prefix defaults to 'v8_'
204
303
  await cache.set('users', 'byId', userId, user, 600);
205
304
  const hit = await cache.get<User>('users', 'byId', userId);
206
305
  ```
@@ -214,6 +313,21 @@ const stripe = createStripeClient(secret); // or { apiVersion: '2024-04-10' } to
214
313
  const event = await verifyStripeWebhook(secret, webhookSecret, rawBody, c.req.header('stripe-signature') ?? '');
215
314
  ```
216
315
 
316
+ ### Testing
317
+
318
+ ```ts
319
+ import { createTestDb, FakeFirebaseVerifier, configurableFake } from '@rdlabo/workers-hono-kit/testing';
320
+
321
+ const testDb = createTestDb({ dbName: 'myapp_test', migrationsFolder: './drizzle' });
322
+ await testDb.resetSchema();
323
+ const pool = testDb.createTestPool();
324
+
325
+ const firebase = new FakeFirebaseVerifier();
326
+ firebase.register('uid-1', { email: 'a@example.com' });
327
+
328
+ const gateway = configurableFake<PaymentGateway>({ charge: async () => ({ ok: true }) }, 'PaymentGateway');
329
+ ```
330
+
217
331
  ## Local development / linking
218
332
 
219
333
  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`:
@@ -240,6 +354,7 @@ npm install
240
354
  npm run typecheck # tsc --noEmit
241
355
  npm run lint # eslint
242
356
  npm test # vitest
357
+ npm run build # tsc -p tsconfig.build.json → dist/
243
358
  ```
244
359
 
245
360
  ## License
@@ -1,40 +1,78 @@
1
1
  import type { AiGateway, AiGatewayBindingSettings, AiGatewayOptions } from 'ai-gateway-provider';
2
2
  export type { AiGateway, AiGatewayOptions } from 'ai-gateway-provider';
3
- /** Workers の AI binding(`env.AI.gateway(name)`)の最小形。Cloudflare の `AiGateway` が構造的に適合する。 */
3
+ /**
4
+ * Minimal shape of a Workers AI binding (`env.AI.gateway(name)`).
5
+ *
6
+ * @remarks
7
+ * Cloudflare's runtime `AiGateway` type is structurally compatible with this binding shape.
8
+ */
4
9
  export type AiGatewayBinding = AiGatewayBindingSettings['binding'];
5
10
  /**
6
- * AI Gateway 設定。2 系統:
7
- * - binding 形: Workers ランタイム(本番 / `wrangler dev`)。`env.AI.gateway(name)` を渡す。
8
- * binding 経由は同一アカウント内で事前認証されるため Gateway トークン不要。
9
- * - REST 形: Workers 外(Node eval ハーネス等、binding 不可)。accountId + gateway + token で REST。
11
+ * Configuration for the AI Gateway provider. This is a union with two mutually exclusive forms.
12
+ *
13
+ * @remarks
14
+ * - **Binding form** for the Workers runtime (production and `wrangler dev`). Pass the
15
+ * `env.AI.gateway(name)` binding. Requests through a binding are pre-authenticated within the same
16
+ * Cloudflare account, so no Gateway token is required.
17
+ * - **REST form** — for non-Workers contexts where a binding is unavailable (e.g. a Node evaluation
18
+ * harness). Supply `accountId`, `gateway`, and (for authenticated Gateways) `token` to reach the
19
+ * Gateway over REST.
10
20
  */
11
21
  export type AiGatewayConfig = {
12
- /** `env.AI.gateway(name)` 等の AI Gateway binding。 */
22
+ /** The AI Gateway binding, typically obtained via `env.AI.gateway(name)`. */
13
23
  binding: AiGatewayBinding;
14
- /** キャッシュ / リトライ / メタデータ等の Gateway オプション(任意)。 */
24
+ /** Optional Gateway options such as caching, retries, and request metadata. */
15
25
  options?: AiGatewayOptions;
16
26
  } | {
17
- /** Cloudflare アカウント ID */
27
+ /** Cloudflare account ID that owns the Gateway. */
18
28
  accountId: string;
19
- /** AI Gateway 名。 */
29
+ /** AI Gateway name. */
20
30
  gateway: string;
21
31
  /**
22
- * `cf-aig-authorization` に載せる Gateway 認証トークン。Authenticated Gateway のときだけ必要。
23
- * unauthenticated Gateway では省略可(プロバイダの API キーではなく Gateway 自体への認証)。
32
+ * Gateway authentication token sent in the `cf-aig-authorization` header. Required only for an
33
+ * Authenticated Gateway; omit it for an unauthenticated Gateway. This authenticates the request to
34
+ * the Gateway itself and is distinct from any provider API key.
24
35
  */
25
36
  token?: string;
26
- /** キャッシュ / リトライ / メタデータ等の Gateway オプション(任意)。 */
37
+ /** Optional Gateway options such as caching, retries, and request metadata. */
27
38
  options?: AiGatewayOptions;
28
39
  };
40
+ /** Provider object exposing the AI Gateway model wrapper. */
29
41
  export interface AiGatewayProvider {
30
42
  /**
31
- * `@ai-sdk/*` のモデルを包んで AI Gateway 経由にする。
32
- * 例: `aigateway(createAnthropic({ apiKey }).('claude-...'))`。
33
- * 配列を渡すとフォールバック(先頭から順に試行)になる。
43
+ * Wraps an `@ai-sdk/*` model so its requests are routed through the AI Gateway.
44
+ *
45
+ * @remarks
46
+ * Example invocation: `aigateway(createAnthropic({ apiKey })('claude-...'))`. Passing an array of
47
+ * models enables fallback behavior — each model is attempted in order from the start of the array.
34
48
  */
35
49
  aigateway: AiGateway;
36
50
  }
37
51
  /**
38
- * AI Gateway 用のプロバイダを生成する。binding / REST 形のどちらでも可(欠落時は fail-fast)。
52
+ * Create an AI Gateway provider from either the binding form or the REST form of the configuration.
53
+ *
54
+ * @param config - The Gateway configuration; either the binding form or the REST form.
55
+ * @returns A provider whose `aigateway` wrapper routes models through the AI Gateway.
56
+ * @throws Error When the REST form is used and `accountId` or `gateway` is missing (fail-fast).
57
+ * @example
58
+ * ```ts
59
+ * // Binding form (Workers runtime: production / wrangler dev)
60
+ * import { createAnthropic } from '@ai-sdk/anthropic';
61
+ *
62
+ * const { aigateway } = createAiGatewayProvider({ binding: env.AI.gateway('my-gateway') });
63
+ * const model = aigateway(createAnthropic({ apiKey: env.ANTHROPIC_API_KEY })('claude-3-5-sonnet-latest'));
64
+ * ```
65
+ * @example
66
+ * ```ts
67
+ * // REST form (non-Workers context, e.g. a Node evaluation harness)
68
+ * import { createOpenAI } from '@ai-sdk/openai';
69
+ *
70
+ * const { aigateway } = createAiGatewayProvider({
71
+ * accountId: process.env.CF_ACCOUNT_ID!,
72
+ * gateway: 'my-gateway',
73
+ * token: process.env.CF_AIG_TOKEN, // only for an Authenticated Gateway
74
+ * });
75
+ * const model = aigateway(createOpenAI({ apiKey: process.env.OPENAI_API_KEY })('gpt-4o'));
76
+ * ```
39
77
  */
40
78
  export declare function createAiGatewayProvider(config: AiGatewayConfig): AiGatewayProvider;
@@ -1,30 +1,55 @@
1
1
  /**
2
- * Cloudflare AI Gateway のプロバイダ生成(`ai` SDK + `ai-gateway-provider`)。
3
- * フリート共通 = foodlabel / winecode / receptray hono の AI 呼び出しを必ず Gateway 経由にする。
2
+ * Cloudflare AI Gateway provider factory built on the Vercel AI SDK and `ai-gateway-provider`.
4
3
  *
5
- * `createAiGateway` が返す wrapper `@ai-sdk/*` のモデルを包むと、SDK が組み立てた
6
- * プロバイダ宛リクエスト(api.openai.com / api.anthropic.com / *-aiplatform.googleapis.com 等)を
7
- * AI Gateway Universal Endpoint 経由に差し替える。OpenAI / Anthropic / Google Vertex(SA) の
8
- * いずれも同じ `aigateway(model)` で透過的にルーティングされる(Vertex も対応)。
4
+ * Routes OpenAI, Anthropic, and Google Vertex `@ai-sdk/*` models through the AI Gateway Universal
5
+ * Endpoint. The wrapper returned by `createAiGateway` intercepts the provider-bound requests the SDK
6
+ * assembles (`api.openai.com`, `api.anthropic.com`, `*-aiplatform.googleapis.com`, etc.) and redirects
7
+ * them through the Gateway. Every provider is routed transparently via the same `aigateway(model)` call.
9
8
  *
10
- * ここはインフラ層(Gateway 識別子と認証トークンの注入だけ)。プロバイダの API キーや
11
- * Vertex SA 認証情報は各 repo 側でモデル生成時に渡す(pass-through)。
9
+ * @remarks
10
+ * This module is purely the infrastructure layer: it injects only the Gateway identifier and (optionally)
11
+ * the Gateway authentication token. Provider API keys and Vertex service-account credentials are supplied
12
+ * by the caller at model-construction time and passed through untouched.
12
13
  */
13
14
  import { createAiGateway } from 'ai-gateway-provider';
14
15
  /**
15
- * AI Gateway 用のプロバイダを生成する。binding / REST 形のどちらでも可(欠落時は fail-fast)。
16
+ * Create an AI Gateway provider from either the binding form or the REST form of the configuration.
17
+ *
18
+ * @param config - The Gateway configuration; either the binding form or the REST form.
19
+ * @returns A provider whose `aigateway` wrapper routes models through the AI Gateway.
20
+ * @throws Error When the REST form is used and `accountId` or `gateway` is missing (fail-fast).
21
+ * @example
22
+ * ```ts
23
+ * // Binding form (Workers runtime: production / wrangler dev)
24
+ * import { createAnthropic } from '@ai-sdk/anthropic';
25
+ *
26
+ * const { aigateway } = createAiGatewayProvider({ binding: env.AI.gateway('my-gateway') });
27
+ * const model = aigateway(createAnthropic({ apiKey: env.ANTHROPIC_API_KEY })('claude-3-5-sonnet-latest'));
28
+ * ```
29
+ * @example
30
+ * ```ts
31
+ * // REST form (non-Workers context, e.g. a Node evaluation harness)
32
+ * import { createOpenAI } from '@ai-sdk/openai';
33
+ *
34
+ * const { aigateway } = createAiGatewayProvider({
35
+ * accountId: process.env.CF_ACCOUNT_ID!,
36
+ * gateway: 'my-gateway',
37
+ * token: process.env.CF_AIG_TOKEN, // only for an Authenticated Gateway
38
+ * });
39
+ * const model = aigateway(createOpenAI({ apiKey: process.env.OPENAI_API_KEY })('gpt-4o'));
40
+ * ```
16
41
  */
17
42
  export function createAiGatewayProvider(config) {
18
43
  if ('binding' in config) {
19
44
  return { aigateway: createAiGateway({ binding: config.binding, options: config.options }) };
20
45
  }
21
46
  if (!config.accountId) {
22
- throw new Error('AI Gateway: accountId が未設定です');
47
+ throw new Error('AI Gateway: accountId is not set');
23
48
  }
24
49
  if (!config.gateway) {
25
- throw new Error('AI Gateway: gateway 名が未設定です');
50
+ throw new Error('AI Gateway: gateway name is not set');
26
51
  }
27
- // token Authenticated Gateway のときだけ apiKey として送る。unauthenticated では undefined で可。
52
+ // The token is sent as apiKey only for an Authenticated Gateway; undefined is fine when unauthenticated.
28
53
  return {
29
54
  aigateway: createAiGateway({
30
55
  accountId: config.accountId,
@@ -1,9 +1,27 @@
1
1
  /**
2
- * CloudFront 署名付き URL 生成(`@aws-sdk/cloudfront-signer` getSignedUrl Web Crypto で再実装)。
3
- * Cloudflare Workers ネイティブ(aws-sdk 不要)。フリート共通 = tipsys/winecode hono。
2
+ * Generate a CloudFront signed URL using a canned policy, implemented natively for Cloudflare Workers.
4
3
  *
5
- * canned policy RSASSA-PKCS1-v1_5 + SHA-1 で署名し、AWS URL-safe base64 変換
6
- * '+' -> '-' , '/' -> '~' , '=' -> '_'
7
- * を施して `Expires` / `Key-Pair-Id` / `Signature` の順でクエリを付与する(aws-sdk の出力とバイト一致)。
4
+ * Reimplements `getSignedUrl` from `@aws-sdk/cloudfront-signer` on top of the Web Crypto API, so no
5
+ * `@aws-sdk` dependency is required. The canned policy is signed with RSASSA-PKCS1-v1_5 and SHA-1, the
6
+ * signature is converted to AWS URL-safe base64 (`+` -> `-`, `/` -> `~`, `=` -> `_`), and the query
7
+ * parameters are appended in the order `Expires`, `Key-Pair-Id`, `Signature`.
8
+ *
9
+ * @remarks
10
+ * The output is byte-for-byte identical to that of `@aws-sdk/cloudfront-signer`.
11
+ *
12
+ * @param url - The resource URL to sign.
13
+ * @param privateKeyPem - The CloudFront key group private key in PKCS#8 PEM format.
14
+ * @param keyPairId - The CloudFront public key (key pair) ID associated with the private key.
15
+ * @param dateLessThan - Expiry time, accepted as a `Date`, epoch-millisecond number, or date string.
16
+ * @returns The signed URL with the `Expires`, `Key-Pair-Id`, and `Signature` query parameters appended.
17
+ * @example
18
+ * ```ts
19
+ * const signedUrl = await getCloudFrontSignedUrl(
20
+ * 'https://cdn.example.com/private/video.mp4',
21
+ * env.CLOUDFRONT_PRIVATE_KEY,
22
+ * env.CLOUDFRONT_KEY_PAIR_ID,
23
+ * Date.now() + 60 * 60 * 1000, // valid for one hour
24
+ * );
25
+ * ```
8
26
  */
9
27
  export declare function getCloudFrontSignedUrl(url: string, privateKeyPem: string, keyPairId: string, dateLessThan: string | number | Date): Promise<string>;
@@ -1,10 +1,28 @@
1
1
  /**
2
- * CloudFront 署名付き URL 生成(`@aws-sdk/cloudfront-signer` getSignedUrl Web Crypto で再実装)。
3
- * Cloudflare Workers ネイティブ(aws-sdk 不要)。フリート共通 = tipsys/winecode hono。
2
+ * Generate a CloudFront signed URL using a canned policy, implemented natively for Cloudflare Workers.
4
3
  *
5
- * canned policy RSASSA-PKCS1-v1_5 + SHA-1 で署名し、AWS URL-safe base64 変換
6
- * '+' -> '-' , '/' -> '~' , '=' -> '_'
7
- * を施して `Expires` / `Key-Pair-Id` / `Signature` の順でクエリを付与する(aws-sdk の出力とバイト一致)。
4
+ * Reimplements `getSignedUrl` from `@aws-sdk/cloudfront-signer` on top of the Web Crypto API, so no
5
+ * `@aws-sdk` dependency is required. The canned policy is signed with RSASSA-PKCS1-v1_5 and SHA-1, the
6
+ * signature is converted to AWS URL-safe base64 (`+` -> `-`, `/` -> `~`, `=` -> `_`), and the query
7
+ * parameters are appended in the order `Expires`, `Key-Pair-Id`, `Signature`.
8
+ *
9
+ * @remarks
10
+ * The output is byte-for-byte identical to that of `@aws-sdk/cloudfront-signer`.
11
+ *
12
+ * @param url - The resource URL to sign.
13
+ * @param privateKeyPem - The CloudFront key group private key in PKCS#8 PEM format.
14
+ * @param keyPairId - The CloudFront public key (key pair) ID associated with the private key.
15
+ * @param dateLessThan - Expiry time, accepted as a `Date`, epoch-millisecond number, or date string.
16
+ * @returns The signed URL with the `Expires`, `Key-Pair-Id`, and `Signature` query parameters appended.
17
+ * @example
18
+ * ```ts
19
+ * const signedUrl = await getCloudFrontSignedUrl(
20
+ * 'https://cdn.example.com/private/video.mp4',
21
+ * env.CLOUDFRONT_PRIVATE_KEY,
22
+ * env.CLOUDFRONT_KEY_PAIR_ID,
23
+ * Date.now() + 60 * 60 * 1000, // valid for one hour
24
+ * );
25
+ * ```
8
26
  */
9
27
  export async function getCloudFrontSignedUrl(url, privateKeyPem, keyPairId, dateLessThan) {
10
28
  const epochSeconds = Math.round(new Date(dateLessThan).getTime() / 1000);
@@ -15,12 +33,26 @@ export async function getCloudFrontSignedUrl(url, privateKeyPem, keyPairId, date
15
33
  const signatureBuffer = await crypto.subtle.sign({ name: 'RSASSA-PKCS1-v1_5' }, key, new TextEncoder().encode(policy));
16
34
  const signature = toUrlSafeBase64(arrayBufferToBase64(signatureBuffer));
17
35
  const separator = url.includes('?') ? '&' : '?';
18
- // @aws-sdk/cloudfront-signer のクエリ順: Expires -> Key-Pair-Id -> Signature
36
+ // Query order used by @aws-sdk/cloudfront-signer: Expires -> Key-Pair-Id -> Signature
19
37
  return `${url}${separator}Expires=${epochSeconds}&Key-Pair-Id=${keyPairId}&Signature=${signature}`;
20
38
  }
39
+ /**
40
+ * Convert standard base64 to the URL-safe alphabet expected in CloudFront signatures.
41
+ *
42
+ * @param value - A standard base64 string.
43
+ * @returns The base64 string with `+` -> `-`, `=` -> `_`, and `/` -> `~`.
44
+ * @internal
45
+ */
21
46
  function toUrlSafeBase64(value) {
22
47
  return value.replace(/\+/g, '-').replace(/=/g, '_').replace(/\//g, '~');
23
48
  }
49
+ /**
50
+ * Encode an `ArrayBuffer` to standard base64.
51
+ *
52
+ * @param buffer - The raw bytes to encode.
53
+ * @returns The standard base64 representation of the buffer.
54
+ * @internal
55
+ */
24
56
  function arrayBufferToBase64(buffer) {
25
57
  const bytes = new Uint8Array(buffer);
26
58
  let binary = '';
@@ -29,6 +61,13 @@ function arrayBufferToBase64(buffer) {
29
61
  }
30
62
  return btoa(binary);
31
63
  }
64
+ /**
65
+ * Decode a PKCS#8 PEM private key into its DER `ArrayBuffer`.
66
+ *
67
+ * @param pem - The PEM-encoded key, including the BEGIN/END armor.
68
+ * @returns The decoded DER bytes, suitable for `crypto.subtle.importKey('pkcs8', ...)`.
69
+ * @internal
70
+ */
32
71
  function pemToDer(pem) {
33
72
  const base64 = pem
34
73
  .replace(/-----BEGIN [^-]+-----/, '')
@@ -1,14 +1,48 @@
1
1
  /**
2
- * AWS Secrets Manager GetSecretValue aws4fetch(SigV4 署名 fetch)で叩く汎用ヘルパ。
3
- * Cloudflare Workers には AWS SDK も IAM ロールも無いため、AWS の静的キーを Workers secrets として
4
- * 渡して署名する(移植元 `api/src/secrets-manager.ts` 相当)。DB 認証情報は Hyperdrive 側に持つので対象外。
2
+ * AWS credentials used to sign Secrets Manager requests.
5
3
  *
6
- * Secret の中身(スキーマ)と secretId は repo ごとに異なるため、`<T>` と `secretId` を呼び出し側が渡す。
4
+ * @remarks
5
+ * Cloudflare Workers have neither the AWS SDK nor IAM role credentials, so static AWS keys are supplied
6
+ * as Workers secrets and used to produce a SigV4 signature.
7
7
  */
8
8
  export interface AwsSecretsOptions {
9
+ /** AWS access key ID. */
9
10
  accessKeyId: string;
11
+ /** AWS secret access key. */
10
12
  secretAccessKey: string;
13
+ /** Optional STS session token, required when using temporary credentials. */
11
14
  sessionToken?: string;
15
+ /** AWS region of the Secrets Manager endpoint, e.g. `ap-northeast-1`. */
12
16
  region: string;
13
17
  }
18
+ /**
19
+ * Fetch and parse a secret from AWS Secrets Manager, caching the result per isolate.
20
+ *
21
+ * Issues a `GetSecretValue` call to Secrets Manager via a SigV4-signed `fetch` (using aws4fetch), with no
22
+ * AWS SDK involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
23
+ * secret ID; concurrent first-time callers share one in-flight request, and a rejected fetch clears the
24
+ * cache entry so the next call retries.
25
+ *
26
+ * @typeParam T - The shape of the JSON-parsed secret payload, supplied by the caller.
27
+ * @param options - AWS credentials and region used to sign the request.
28
+ * @param secretId - The Secrets Manager secret ID or ARN to retrieve.
29
+ * @returns The parsed secret value cast to `T`.
30
+ * @throws Error When the Secrets Manager response is not OK, or when it contains no `SecretString`.
31
+ * @example
32
+ * ```ts
33
+ * interface DbSecret {
34
+ * username: string;
35
+ * password: string;
36
+ * }
37
+ *
38
+ * const secret = await getAuthenticationSecret<DbSecret>(
39
+ * {
40
+ * accessKeyId: env.AWS_ACCESS_KEY_ID,
41
+ * secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
42
+ * region: 'ap-northeast-1',
43
+ * },
44
+ * 'prod/db/credentials',
45
+ * );
46
+ * ```
47
+ */
14
48
  export declare function getAuthenticationSecret<T>(options: AwsSecretsOptions, secretId: string): Promise<T>;
@@ -1,10 +1,46 @@
1
1
  import { AwsClient } from 'aws4fetch';
2
2
  /**
3
- * Per-isolate cache: Secrets Manager isolate ごと 1 回だけ叩く。region+accessKeyId+secretId を
4
- * キーにし、資格情報ローテーション時は再取得する。promise をキャッシュして同時初回リクエストが 1 回の
5
- * 呼び出しを共有する。reject 時はキャッシュをクリアして retry を許す。
3
+ * Per-isolate cache for the fetched secret.
4
+ *
5
+ * @remarks
6
+ * Secrets Manager is queried at most once per isolate. The entry is keyed by
7
+ * `region:accessKeyId:secretId`, so rotating credentials triggers a fresh fetch. The in-flight promise
8
+ * itself is cached so that concurrent first-time callers share a single request. On rejection the cache
9
+ * is cleared so a failed fetch can be retried.
10
+ *
11
+ * @internal
6
12
  */
7
13
  let cache = null;
14
+ /**
15
+ * Fetch and parse a secret from AWS Secrets Manager, caching the result per isolate.
16
+ *
17
+ * Issues a `GetSecretValue` call to Secrets Manager via a SigV4-signed `fetch` (using aws4fetch), with no
18
+ * AWS SDK involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
19
+ * secret ID; concurrent first-time callers share one in-flight request, and a rejected fetch clears the
20
+ * cache entry so the next call retries.
21
+ *
22
+ * @typeParam T - The shape of the JSON-parsed secret payload, supplied by the caller.
23
+ * @param options - AWS credentials and region used to sign the request.
24
+ * @param secretId - The Secrets Manager secret ID or ARN to retrieve.
25
+ * @returns The parsed secret value cast to `T`.
26
+ * @throws Error When the Secrets Manager response is not OK, or when it contains no `SecretString`.
27
+ * @example
28
+ * ```ts
29
+ * interface DbSecret {
30
+ * username: string;
31
+ * password: string;
32
+ * }
33
+ *
34
+ * const secret = await getAuthenticationSecret<DbSecret>(
35
+ * {
36
+ * accessKeyId: env.AWS_ACCESS_KEY_ID,
37
+ * secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
38
+ * region: 'ap-northeast-1',
39
+ * },
40
+ * 'prod/db/credentials',
41
+ * );
42
+ * ```
43
+ */
8
44
  export function getAuthenticationSecret(options, secretId) {
9
45
  const key = `${options.region}:${options.accessKeyId}:${secretId}`;
10
46
  if (cache?.key !== key) {
@@ -16,6 +52,15 @@ export function getAuthenticationSecret(options, secretId) {
16
52
  }
17
53
  return cache.value;
18
54
  }
55
+ /**
56
+ * Perform the SigV4-signed `GetSecretValue` request and parse the returned `SecretString`.
57
+ *
58
+ * @param options - AWS credentials and region used to sign the request.
59
+ * @param secretId - The Secrets Manager secret ID or ARN to retrieve.
60
+ * @returns The JSON-parsed secret payload.
61
+ * @throws Error When the response is not OK, or when it contains no `SecretString`.
62
+ * @internal
63
+ */
19
64
  async function fetchSecret(options, secretId) {
20
65
  const aws = new AwsClient({
21
66
  accessKeyId: options.accessKeyId,