@rdlabo/workers-hono-kit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) 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 +15 -0
  42. package/dist/index.js +14 -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/queue/consumer.d.ts +112 -0
  52. package/dist/queue/consumer.js +80 -0
  53. package/dist/queue/send.d.ts +90 -0
  54. package/dist/queue/send.js +85 -0
  55. package/dist/stripe/client.d.ts +46 -9
  56. package/dist/stripe/client.js +41 -3
  57. package/dist/testing/auth.d.ts +58 -10
  58. package/dist/testing/auth.js +58 -10
  59. package/dist/testing/configurable-fake.d.ts +20 -9
  60. package/dist/testing/configurable-fake.js +23 -11
  61. package/dist/testing/db.d.ts +81 -12
  62. package/dist/testing/db.js +23 -1
  63. package/dist/testing/fakes.d.ts +77 -9
  64. package/dist/testing/fakes.js +69 -7
  65. package/dist/testing/index.d.ts +7 -0
  66. package/dist/testing/index.js +10 -5
  67. package/dist/testing/stripe-fixtures.d.ts +93 -3
  68. package/dist/testing/stripe-fixtures.js +93 -3
  69. package/package.json +3 -2
  70. package/scripts/check-subrequest-fanout.mjs +86 -0
  71. package/src/ai/gateway.ts +66 -27
  72. package/src/aws/cloudfront.ts +46 -6
  73. package/src/aws/secrets-manager.ts +56 -7
  74. package/src/cache/kv-cache.ts +194 -12
  75. package/src/db/connection.ts +56 -14
  76. package/src/db/database.ts +160 -24
  77. package/src/db/index.ts +11 -2
  78. package/src/db/jst.ts +89 -23
  79. package/src/db/orm-config.ts +61 -19
  80. package/src/db/retry.ts +25 -3
  81. package/src/db/write-result.ts +27 -4
  82. package/src/firebase/firebase-verifier.ts +53 -4
  83. package/src/firebase/identity-toolkit.ts +57 -5
  84. package/src/firebase/jose-firebase-verifier.ts +79 -9
  85. package/src/firebase/remote-verifier.ts +58 -9
  86. package/src/http/app-env.ts +41 -8
  87. package/src/http/app-info.ts +25 -3
  88. package/src/http/http-status.ts +12 -3
  89. package/src/http/nest-error.ts +106 -37
  90. package/src/http/user-protocol.ts +23 -3
  91. package/src/index.ts +17 -3
  92. package/src/middleware/auth.ts +77 -15
  93. package/src/middleware/finalize-response.ts +41 -12
  94. package/src/middleware/validation.ts +89 -15
  95. package/src/middleware/zod-coerce.ts +68 -9
  96. package/src/queue/consumer.ts +146 -0
  97. package/src/queue/send.ts +129 -0
  98. package/src/stripe/client.ts +46 -9
  99. package/src/testing/auth.ts +58 -10
  100. package/src/testing/configurable-fake.ts +23 -11
  101. package/src/testing/db.ts +82 -13
  102. package/src/testing/fakes.ts +77 -9
  103. package/src/testing/index.ts +10 -5
  104. package/src/testing/stripe-fixtures.ts +93 -3
@@ -1,47 +1,89 @@
1
1
  /**
2
- * Drizzle の列名 casing をフリートで一元管理する(標準: snake_case を「config」と「runtime」の両方で固定)。
2
+ * Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
3
+ * config and the runtime ORM.
3
4
  *
4
- * casing は2か所にあり別物:
5
- * drizzle.config.ts top-level `casing` `db:generate` が **作る列名** を決める(→ honoDrizzleConfig)
6
- * ② database.ts の `drizzle(conn, { …casing })` … **実行時の書き込みビルダが参照する列名** を決める(→ DRIZZLE_ORM_OPTIONS)
5
+ * @remarks
6
+ * Casing is configured in two distinct places:
7
7
  *
8
- * この2つが食い違うと、明示列名を書き忘れた camelCase 複数単語列で generate と実行時がズレて実行時
9
- * `Unknown column` になる(typecheck もマイグレーションも正常に見えるので発覚が遅い)。両方ここから取れば
10
- * ズレが構造的に起きない。明示列名がある列では casing は無視されるので、既存挙動は変えない純粋な安全網。
8
+ * 1. The top-level `casing` in `drizzle.config.ts` decides the column names that `db:generate`
9
+ * **creates** (see {@link honoDrizzleConfig}).
10
+ * 2. The `drizzle(conn, { …casing })` call decides the column names the **runtime write builder**
11
+ * resolves to (see {@link DRIZZLE_ORM_OPTIONS}).
11
12
  *
12
- * 注: runtime `drizzle()` 呼び出し自体は **消費側 repo が自分の drizzle-orm で行う**(kit drizzle()
13
- * 呼ぶと kit repo drizzle-orm が別コピーになり型同一性が壊れるため)。kit は「値」だけを提供する。
13
+ * If these two disagree, a multi-word camelCase column without an explicit column name will be
14
+ * generated with one name but queried with another, producing a runtime `Unknown column` error —
15
+ * something neither the type-check nor the migration surface, so it is caught late. Sourcing both
16
+ * from here makes the mismatch structurally impossible. Casing is ignored for columns that declare
17
+ * an explicit name, so this is a pure safety net that does not change existing behavior.
18
+ *
19
+ * The runtime `drizzle()` call itself is made by the consuming app with its own `drizzle-orm`; the
20
+ * kit only ever provides values, never the ORM instance, to avoid splitting `drizzle-orm` into two
21
+ * copies and breaking type identity.
14
22
  */
15
23
  /**
16
- * runtime 用。消費側 repo database.ts `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` と spread して使う。
17
- * mode/casing を kit が固定し、書き込みビルダの列名解決を snake_case に揃える。
24
+ * Runtime ORM options shared by the consuming app's `drizzle()` call.
25
+ *
26
+ * Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
27
+ * builder resolves column names as `snake_case`, matching what `db:generate` creates.
28
+ *
29
+ * @remarks
30
+ * Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
31
+ * the same casing must be used by both the config and the runtime ORM.
18
32
  */
19
33
  export declare const DRIZZLE_ORM_OPTIONS: {
20
34
  readonly mode: "default";
21
35
  readonly casing: "snake_case";
22
36
  };
37
+ /**
38
+ * Options for {@link honoDrizzleConfig}.
39
+ */
23
40
  export interface HonoDrizzleConfigOptions {
24
- /** drizzle-kit dbCredentials.database(localConnectionString とは別)。 */
41
+ /** drizzle-kit `dbCredentials.database` the database name to connect to. */
25
42
  database: string;
43
+ /** Database host; defaults to `process.env.DB_HOST` then `127.0.0.1`. */
26
44
  host?: string;
45
+ /** Database port; defaults to `process.env.DB_PORT` then `3306`. */
27
46
  port?: number;
47
+ /** Database user; defaults to `process.env.DB_USER` then `root`. */
28
48
  user?: string;
49
+ /** Database password; defaults to `process.env.DB_PASSWORD` then `root`. */
29
50
  password?: string;
30
- /** 既定 './src/db/schemes' */
51
+ /** Path to the schema directory; defaults to `'./src/db/schemes'`. */
31
52
  schema?: string;
32
- /** 既定 './drizzle' */
53
+ /** Output directory for generated migrations; defaults to `'./drizzle'`. */
33
54
  out?: string;
34
- /** /api と DB を共有する repo は schema 由来テーブルに限定する(省略可)。 */
55
+ /**
56
+ * Optional table allow-list. Use this to restrict drizzle-kit to the schema's own tables when the
57
+ * database is shared with another application.
58
+ */
35
59
  tablesFilter?: string[];
36
- /** db:introspect(DB→JS)の casing。生成方向の `casing:'snake_case'` とは別軸(省略可)。 */
60
+ /**
61
+ * Optional `db:introspect` (DB → JS) casing. This is an independent axis from the generation-side
62
+ * `casing: 'snake_case'` and only affects introspection output.
63
+ */
37
64
  introspect?: {
38
65
  casing: 'camel' | 'preserve';
39
66
  };
40
67
  }
41
68
  /**
42
- * drizzle.config.ts 用ファクトリ。`export default honoDrizzleConfig({ database })` で使う。
43
- * casing:'snake_case'・schema/out・dbCredentials(env 既定)を kit が owner として固定する。
44
- * drizzle-kit kit の依存にしないため plain object を返す(drizzle-kit CLI default export を読むだけ)。
69
+ * Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
70
+ *
71
+ * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
72
+ * defaults), while leaving `tablesFilter` and `introspect` opt-in.
73
+ *
74
+ * @remarks
75
+ * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
76
+ * dependency of the kit; the drizzle-kit CLI only reads the default export.
77
+ *
78
+ * @param options - configuration overrides; only `database` is required.
79
+ * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
80
+ * @example
81
+ * ```ts
82
+ * // drizzle.config.ts
83
+ * import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
84
+ *
85
+ * export default honoDrizzleConfig({ database: 'app' });
86
+ * ```
45
87
  */
46
88
  export declare function honoDrizzleConfig(options: HonoDrizzleConfigOptions): {
47
89
  dbCredentials: {
@@ -1,26 +1,55 @@
1
1
  /**
2
- * Drizzle の列名 casing をフリートで一元管理する(標準: snake_case を「config」と「runtime」の両方で固定)。
2
+ * Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
3
+ * config and the runtime ORM.
3
4
  *
4
- * casing は2か所にあり別物:
5
- * drizzle.config.ts top-level `casing` `db:generate` が **作る列名** を決める(→ honoDrizzleConfig)
6
- * ② database.ts の `drizzle(conn, { …casing })` … **実行時の書き込みビルダが参照する列名** を決める(→ DRIZZLE_ORM_OPTIONS)
5
+ * @remarks
6
+ * Casing is configured in two distinct places:
7
7
  *
8
- * この2つが食い違うと、明示列名を書き忘れた camelCase 複数単語列で generate と実行時がズレて実行時
9
- * `Unknown column` になる(typecheck もマイグレーションも正常に見えるので発覚が遅い)。両方ここから取れば
10
- * ズレが構造的に起きない。明示列名がある列では casing は無視されるので、既存挙動は変えない純粋な安全網。
8
+ * 1. The top-level `casing` in `drizzle.config.ts` decides the column names that `db:generate`
9
+ * **creates** (see {@link honoDrizzleConfig}).
10
+ * 2. The `drizzle(conn, { …casing })` call decides the column names the **runtime write builder**
11
+ * resolves to (see {@link DRIZZLE_ORM_OPTIONS}).
11
12
  *
12
- * 注: runtime `drizzle()` 呼び出し自体は **消費側 repo が自分の drizzle-orm で行う**(kit drizzle()
13
- * 呼ぶと kit repo drizzle-orm が別コピーになり型同一性が壊れるため)。kit は「値」だけを提供する。
13
+ * If these two disagree, a multi-word camelCase column without an explicit column name will be
14
+ * generated with one name but queried with another, producing a runtime `Unknown column` error —
15
+ * something neither the type-check nor the migration surface, so it is caught late. Sourcing both
16
+ * from here makes the mismatch structurally impossible. Casing is ignored for columns that declare
17
+ * an explicit name, so this is a pure safety net that does not change existing behavior.
18
+ *
19
+ * The runtime `drizzle()` call itself is made by the consuming app with its own `drizzle-orm`; the
20
+ * kit only ever provides values, never the ORM instance, to avoid splitting `drizzle-orm` into two
21
+ * copies and breaking type identity.
14
22
  */
15
23
  /**
16
- * runtime 用。消費側 repo database.ts `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` と spread して使う。
17
- * mode/casing を kit が固定し、書き込みビルダの列名解決を snake_case に揃える。
24
+ * Runtime ORM options shared by the consuming app's `drizzle()` call.
25
+ *
26
+ * Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
27
+ * builder resolves column names as `snake_case`, matching what `db:generate` creates.
28
+ *
29
+ * @remarks
30
+ * Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
31
+ * the same casing must be used by both the config and the runtime ORM.
18
32
  */
19
33
  export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' };
20
34
  /**
21
- * drizzle.config.ts 用ファクトリ。`export default honoDrizzleConfig({ database })` で使う。
22
- * casing:'snake_case'・schema/out・dbCredentials(env 既定)を kit が owner として固定する。
23
- * drizzle-kit kit の依存にしないため plain object を返す(drizzle-kit CLI default export を読むだけ)。
35
+ * Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
36
+ *
37
+ * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
38
+ * defaults), while leaving `tablesFilter` and `introspect` opt-in.
39
+ *
40
+ * @remarks
41
+ * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
42
+ * dependency of the kit; the drizzle-kit CLI only reads the default export.
43
+ *
44
+ * @param options - configuration overrides; only `database` is required.
45
+ * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
46
+ * @example
47
+ * ```ts
48
+ * // drizzle.config.ts
49
+ * import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
50
+ *
51
+ * export default honoDrizzleConfig({ database: 'app' });
52
+ * ```
24
53
  */
25
54
  export function honoDrizzleConfig(options) {
26
55
  const { database, host, port, user, password, schema = './src/db/schemes', out = './drizzle', tablesFilter, introspect, } = options;
@@ -1,6 +1,28 @@
1
1
  /**
2
- * ER_LOCK_DEADLOCK を指数バックオフで retry する(NestJS/TypeORM retryWhenDeadlock 相当)。
3
- * MySQL はデッドロック時にトランザクション全体をロールバックするため、同じ作業単位の再実行は安全。
4
- * `fn` は単一文(write)またはトランザクション全体(transaction)であること retry `fn` 全体を再実行する。
2
+ * Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
3
+ *
4
+ * Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
5
+ * `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
6
+ * non-deadlock error is rethrown immediately without retrying.
7
+ *
8
+ * @remarks
9
+ * MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
10
+ * of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
11
+ * transaction — because the whole `fn` is re-executed on each retry.
12
+ *
13
+ * @typeParam T - resolved value produced by `fn`.
14
+ * @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
15
+ * @param retries - maximum number of attempts (default `3`).
16
+ * @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
17
+ * @returns the value resolved by the first successful call to `fn`.
18
+ * @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
19
+ * the first occurrence.
20
+ * @example
21
+ * ```ts
22
+ * await retryWhenDeadlock(() => db.transaction(async (tx) => {
23
+ * await tx.insert(orders).values(order);
24
+ * await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
25
+ * }));
26
+ * ```
5
27
  */
6
28
  export declare function retryWhenDeadlock<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
package/dist/db/retry.js CHANGED
@@ -1,7 +1,29 @@
1
1
  /**
2
- * ER_LOCK_DEADLOCK を指数バックオフで retry する(NestJS/TypeORM retryWhenDeadlock 相当)。
3
- * MySQL はデッドロック時にトランザクション全体をロールバックするため、同じ作業単位の再実行は安全。
4
- * `fn` は単一文(write)またはトランザクション全体(transaction)であること retry `fn` 全体を再実行する。
2
+ * Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
3
+ *
4
+ * Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
5
+ * `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
6
+ * non-deadlock error is rethrown immediately without retrying.
7
+ *
8
+ * @remarks
9
+ * MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
10
+ * of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
11
+ * transaction — because the whole `fn` is re-executed on each retry.
12
+ *
13
+ * @typeParam T - resolved value produced by `fn`.
14
+ * @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
15
+ * @param retries - maximum number of attempts (default `3`).
16
+ * @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
17
+ * @returns the value resolved by the first successful call to `fn`.
18
+ * @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
19
+ * the first occurrence.
20
+ * @example
21
+ * ```ts
22
+ * await retryWhenDeadlock(() => db.transaction(async (tx) => {
23
+ * await tx.insert(orders).values(order);
24
+ * await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
25
+ * }));
26
+ * ```
5
27
  */
6
28
  export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
7
29
  for (let attempt = 0; attempt < retries; attempt++) {
@@ -1,16 +1,39 @@
1
1
  /**
2
- * Drizzle(mysql2) write 結果から insertId / affectedRows を型安全に取り出すヘルパ。
3
- * 生の builder/ResultSetHeader を repo 側に晒さずに、よく使う値だけを取り出す。
2
+ * Shape of a Drizzle (mysql2) write result, narrowed to the fields callers actually read.
4
3
  *
5
- * mysql2 の INSERT/UPDATE/DELETE 結果は `[ResultSetHeader, FieldPacket[]]` 形。
4
+ * @remarks
5
+ * A mysql2 INSERT/UPDATE/DELETE result is the tuple `[ResultSetHeader, FieldPacket[]]`. Typing the
6
+ * result this way lets repositories extract the common values without exposing the raw query
7
+ * builder or the full `ResultSetHeader` to the rest of the codebase.
6
8
  */
7
9
  export type DzWriteResult = readonly [{
8
10
  insertId: number;
9
11
  affectedRows: number;
10
12
  }, ...unknown[]];
13
+ /**
14
+ * Extract the auto-increment `insertId` from a write result.
15
+ *
16
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
17
+ * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
18
+ */
11
19
  export declare function insertIdOf(result: DzWriteResult): number;
20
+ /**
21
+ * Extract the number of affected rows from a write result.
22
+ *
23
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
24
+ * @returns the `affectedRows` count reported by mysql2.
25
+ */
12
26
  export declare function affectedRowsOf(result: DzWriteResult): number;
13
27
  /**
14
- * 一括 INSERT で連番採番された行の id 群を返す(mysql2 は先頭 insertId のみ返すため count 分を生成)。
28
+ * Reconstruct the auto-increment ids assigned by a bulk INSERT.
29
+ *
30
+ * @remarks
31
+ * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
32
+ * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
33
+ * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
34
+ *
35
+ * @param result - the result of a bulk INSERT.
36
+ * @param count - the number of rows that were inserted.
37
+ * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
15
38
  */
16
39
  export declare function insertedIdsOf(result: DzWriteResult, count: number): number[];
@@ -1,11 +1,32 @@
1
+ /**
2
+ * Extract the auto-increment `insertId` from a write result.
3
+ *
4
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
5
+ * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
6
+ */
1
7
  export function insertIdOf(result) {
2
8
  return result[0].insertId;
3
9
  }
10
+ /**
11
+ * Extract the number of affected rows from a write result.
12
+ *
13
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
14
+ * @returns the `affectedRows` count reported by mysql2.
15
+ */
4
16
  export function affectedRowsOf(result) {
5
17
  return result[0].affectedRows;
6
18
  }
7
19
  /**
8
- * 一括 INSERT で連番採番された行の id 群を返す(mysql2 は先頭 insertId のみ返すため count 分を生成)。
20
+ * Reconstruct the auto-increment ids assigned by a bulk INSERT.
21
+ *
22
+ * @remarks
23
+ * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
24
+ * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
25
+ * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
26
+ *
27
+ * @param result - the result of a bulk INSERT.
28
+ * @param count - the number of rows that were inserted.
29
+ * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
9
30
  */
10
31
  export function insertedIdsOf(result, count) {
11
32
  const base = result[0].insertId;
@@ -1,17 +1,66 @@
1
- /** Firebase boundary replacing the firebase-admin Auth surface used by `/api`. */
1
+ /**
2
+ * Decoded Firebase ID token payload.
3
+ *
4
+ * Shaped to match the subset of `firebase-admin`'s `DecodedIdToken` that consumers
5
+ * typically rely on: a stable `uid` plus an optional `email`. The index signature keeps
6
+ * every other JWT claim (e.g. `name`, `picture`, custom claims) accessible without
7
+ * enumerating them here.
8
+ *
9
+ * @remarks
10
+ * This is the return type of {@link FirebaseVerifier.verifyIdToken}. The `uid` is derived
11
+ * from the token's `sub` claim.
12
+ */
2
13
  export interface DecodedIdToken {
14
+ /** The authenticated user's unique id, taken from the token's `sub` claim. */
3
15
  uid: string;
16
+ /** The user's email address, when present on the token. */
4
17
  email?: string;
18
+ /** Any additional JWT claim carried by the token (custom claims, `name`, `picture`, ...). */
5
19
  [claim: string]: unknown;
6
20
  }
21
+ /**
22
+ * Abstract authentication boundary that replaces the `firebase-admin` Auth surface
23
+ * (`verifyIdToken` / `getUser` / `deleteUser`) for environments where the Node SDK cannot
24
+ * run, such as Cloudflare Workers.
25
+ *
26
+ * @remarks
27
+ * Implementations verify Firebase ID tokens and look up or delete accounts without the
28
+ * `firebase-admin` Node dependency. See `JoseFirebaseVerifier` for the `jose`-based
29
+ * implementation and the `createRemoteFirebaseVerifier` / `createServiceAccountVerifier`
30
+ * factories for ready-made instances.
31
+ */
7
32
  export interface FirebaseVerifier {
8
- /** Mirrors firebase-admin getAuth().verifyIdToken(). Throws on invalid token. */
33
+ /**
34
+ * Verify a Firebase ID token and return its decoded payload.
35
+ *
36
+ * Mirrors `firebase-admin` `getAuth().verifyIdToken()`.
37
+ *
38
+ * @param idToken - The raw Firebase ID token (JWT) to verify.
39
+ * @returns The decoded token payload.
40
+ * @throws If the token signature, issuer, audience, expiry, or other required claims are invalid.
41
+ */
9
42
  verifyIdToken(idToken: string): Promise<DecodedIdToken>;
10
- /** Mirrors getAuth().getUser(); returns null when the user is absent. */
43
+ /**
44
+ * Look up a user record by uid.
45
+ *
46
+ * Mirrors `firebase-admin` `getAuth().getUser()`.
47
+ *
48
+ * @param uid - The user's unique id.
49
+ * @returns The user's `uid` and optional `email`, or `null` when the user does not exist.
50
+ * @throws If the backing user-management service is not configured or the lookup fails.
51
+ */
11
52
  getUser(uid: string): Promise<{
12
53
  uid: string;
13
54
  email?: string;
14
55
  } | null>;
15
- /** Mirrors getAuth().deleteUser(). */
56
+ /**
57
+ * Delete a user by uid.
58
+ *
59
+ * Mirrors `firebase-admin` `getAuth().deleteUser()`.
60
+ *
61
+ * @param uid - The user's unique id.
62
+ * @returns A promise that resolves once the user has been deleted.
63
+ * @throws If the backing user-management service is not configured or the deletion fails.
64
+ */
16
65
  deleteUser(uid: string): Promise<void>;
17
66
  }
@@ -1,24 +1,73 @@
1
1
  /**
2
- * Minimal Google Identity Toolkit client for the operations firebase-admin performed
3
- * that aren't token verification: accounts:lookup (getUser) and accounts:delete
4
- * (deleteUser). Replaces the firebase-admin Node SDK, which won't run on workerd.
2
+ * Minimal service-account credential consumed by {@link IdentityToolkit}.
5
3
  *
6
- * Auth: sign a JWT assertion with the service-account private key (jose), exchange it for
7
- * an OAuth2 access token, then call the REST API. Tokens are cached in-process.
4
+ * @remarks
5
+ * Corresponds to the relevant fields of a Google service-account JSON key file.
8
6
  */
9
7
  export interface ServiceAccount {
8
+ /** The service account's email, used as the JWT assertion issuer and subject. */
10
9
  client_email: string;
10
+ /** The PEM-encoded PKCS#8 RSA private key used to sign the OAuth2 assertion. */
11
11
  private_key: string;
12
+ /** The Google/Firebase project id the Identity Toolkit calls target. */
12
13
  project_id: string;
13
14
  }
15
+ /**
16
+ * Minimal Google Identity Toolkit REST client for the user-management operations that token
17
+ * verification does not cover: `accounts:lookup` (getUser) and `accounts:delete` (deleteUser).
18
+ *
19
+ * This replaces the parts of the `firebase-admin` Node SDK that cannot run on Cloudflare
20
+ * Workers (workerd).
21
+ *
22
+ * @remarks
23
+ * Authentication follows the JWT-bearer flow: a JWT assertion is signed with the service
24
+ * account's private key (via `jose`), exchanged at the OAuth2 token endpoint for an access
25
+ * token, and that token is then used to call the REST API. Access tokens are cached in-process
26
+ * and reused until shortly before they expire.
27
+ */
14
28
  export declare class IdentityToolkit {
15
29
  private readonly sa;
30
+ /** Cached OAuth2 access token and its absolute expiry (Unix seconds), or `null` when none. */
16
31
  private accessToken;
32
+ /**
33
+ * Create a client bound to a single service account.
34
+ *
35
+ * @param sa - The service-account credential used to authenticate REST calls.
36
+ */
17
37
  constructor(sa: ServiceAccount);
38
+ /**
39
+ * Return a valid OAuth2 access token, minting a new one when the cache is empty or expiring.
40
+ *
41
+ * Signs a short-lived JWT assertion with the service-account key and exchanges it at the
42
+ * Google OAuth2 token endpoint. The result is cached and reused while it remains valid
43
+ * (with a 60-second safety margin).
44
+ *
45
+ * @param nowSeconds - The current Unix time in seconds, used for cache validity and JWT timestamps.
46
+ * @returns A bearer access token for the Identity Toolkit API.
47
+ * @throws If the token exchange request fails.
48
+ * @internal
49
+ */
18
50
  private getAccessToken;
51
+ /**
52
+ * Look up a user record by uid via the `accounts:lookup` endpoint.
53
+ *
54
+ * @param uid - The user's unique id (`localId`).
55
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
56
+ * @returns The user's `uid` and optional `email`, or `null` when the request is unsuccessful
57
+ * or no matching user is returned.
58
+ * @throws If acquiring an access token fails.
59
+ */
19
60
  lookup(uid: string, nowSeconds: number): Promise<{
20
61
  uid: string;
21
62
  email?: string;
22
63
  } | null>;
64
+ /**
65
+ * Delete a user by uid via the `accounts:delete` endpoint.
66
+ *
67
+ * @param uid - The user's unique id (`localId`).
68
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
69
+ * @returns A promise that resolves once the user has been deleted.
70
+ * @throws If acquiring an access token fails or the delete request is unsuccessful.
71
+ */
23
72
  remove(uid: string, nowSeconds: number): Promise<void>;
24
73
  }
@@ -1,13 +1,47 @@
1
1
  import { SignJWT, importPKCS8 } from 'jose';
2
+ /** Google OAuth2 token endpoint used to exchange a signed JWT assertion for an access token. */
2
3
  const TOKEN_URL = 'https://oauth2.googleapis.com/token';
4
+ /** Base URL of the Google Identity Toolkit v1 REST API. */
3
5
  const IDENTITY_TOOLKIT = 'https://identitytoolkit.googleapis.com/v1';
6
+ /** OAuth2 scopes required for Identity Toolkit account lookup and deletion. */
4
7
  const SCOPE = 'https://www.googleapis.com/auth/identitytoolkit https://www.googleapis.com/auth/firebase';
8
+ /**
9
+ * Minimal Google Identity Toolkit REST client for the user-management operations that token
10
+ * verification does not cover: `accounts:lookup` (getUser) and `accounts:delete` (deleteUser).
11
+ *
12
+ * This replaces the parts of the `firebase-admin` Node SDK that cannot run on Cloudflare
13
+ * Workers (workerd).
14
+ *
15
+ * @remarks
16
+ * Authentication follows the JWT-bearer flow: a JWT assertion is signed with the service
17
+ * account's private key (via `jose`), exchanged at the OAuth2 token endpoint for an access
18
+ * token, and that token is then used to call the REST API. Access tokens are cached in-process
19
+ * and reused until shortly before they expire.
20
+ */
5
21
  export class IdentityToolkit {
6
22
  sa;
23
+ /** Cached OAuth2 access token and its absolute expiry (Unix seconds), or `null` when none. */
7
24
  accessToken = null;
25
+ /**
26
+ * Create a client bound to a single service account.
27
+ *
28
+ * @param sa - The service-account credential used to authenticate REST calls.
29
+ */
8
30
  constructor(sa) {
9
31
  this.sa = sa;
10
32
  }
33
+ /**
34
+ * Return a valid OAuth2 access token, minting a new one when the cache is empty or expiring.
35
+ *
36
+ * Signs a short-lived JWT assertion with the service-account key and exchanges it at the
37
+ * Google OAuth2 token endpoint. The result is cached and reused while it remains valid
38
+ * (with a 60-second safety margin).
39
+ *
40
+ * @param nowSeconds - The current Unix time in seconds, used for cache validity and JWT timestamps.
41
+ * @returns A bearer access token for the Identity Toolkit API.
42
+ * @throws If the token exchange request fails.
43
+ * @internal
44
+ */
11
45
  async getAccessToken(nowSeconds) {
12
46
  if (this.accessToken && this.accessToken.expiresAt > nowSeconds + 60) {
13
47
  return this.accessToken.value;
@@ -36,6 +70,15 @@ export class IdentityToolkit {
36
70
  this.accessToken = { value: json.access_token, expiresAt: nowSeconds + json.expires_in };
37
71
  return json.access_token;
38
72
  }
73
+ /**
74
+ * Look up a user record by uid via the `accounts:lookup` endpoint.
75
+ *
76
+ * @param uid - The user's unique id (`localId`).
77
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
78
+ * @returns The user's `uid` and optional `email`, or `null` when the request is unsuccessful
79
+ * or no matching user is returned.
80
+ * @throws If acquiring an access token fails.
81
+ */
39
82
  async lookup(uid, nowSeconds) {
40
83
  const token = await this.getAccessToken(nowSeconds);
41
84
  const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:lookup`, {
@@ -50,6 +93,14 @@ export class IdentityToolkit {
50
93
  const user = json.users?.[0];
51
94
  return user ? { uid: user.localId, email: user.email } : null;
52
95
  }
96
+ /**
97
+ * Delete a user by uid via the `accounts:delete` endpoint.
98
+ *
99
+ * @param uid - The user's unique id (`localId`).
100
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
101
+ * @returns A promise that resolves once the user has been deleted.
102
+ * @throws If acquiring an access token fails or the delete request is unsuccessful.
103
+ */
53
104
  async remove(uid, nowSeconds) {
54
105
  const token = await this.getAccessToken(nowSeconds);
55
106
  const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:delete`, {