@rdlabo/workers-hono-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/ai/gateway.d.ts +40 -0
- package/dist/ai/gateway.js +36 -0
- package/dist/aws/cloudfront.d.ts +9 -0
- package/dist/aws/cloudfront.js +43 -0
- package/dist/aws/secrets-manager.d.ts +14 -0
- package/dist/aws/secrets-manager.js +43 -0
- package/dist/cache/kv-cache.d.ts +51 -0
- package/dist/cache/kv-cache.js +88 -0
- package/dist/db/connection.d.ts +41 -0
- package/dist/db/connection.js +48 -0
- package/dist/db/database.d.ts +60 -0
- package/dist/db/database.js +63 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +8 -0
- package/dist/db/jst.d.ts +19 -0
- package/dist/db/jst.js +48 -0
- package/dist/db/orm-config.d.ts +62 -0
- package/dist/db/orm-config.js +42 -0
- package/dist/db/retry.d.ts +6 -0
- package/dist/db/retry.js +22 -0
- package/dist/db/write-result.d.ts +16 -0
- package/dist/db/write-result.js +13 -0
- package/dist/firebase/firebase-verifier.d.ts +17 -0
- package/dist/firebase/firebase-verifier.js +1 -0
- package/dist/firebase/identity-toolkit.d.ts +24 -0
- package/dist/firebase/identity-toolkit.js +64 -0
- package/dist/firebase/jose-firebase-verifier.d.ts +32 -0
- package/dist/firebase/jose-firebase-verifier.js +52 -0
- package/dist/firebase/remote-verifier.d.ts +9 -0
- package/dist/firebase/remote-verifier.js +44 -0
- package/dist/http/app-env.d.ts +19 -0
- package/dist/http/app-env.js +16 -0
- package/dist/http/app-info.d.ts +11 -0
- package/dist/http/app-info.js +8 -0
- package/dist/http/http-status.d.ts +62 -0
- package/dist/http/http-status.js +63 -0
- package/dist/http/nest-error.d.ts +72 -0
- package/dist/http/nest-error.js +65 -0
- package/dist/http/user-protocol.d.ts +11 -0
- package/dist/http/user-protocol.js +8 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +28 -0
- package/dist/middleware/auth.d.ts +36 -0
- package/dist/middleware/auth.js +30 -0
- package/dist/middleware/finalize-response.d.ts +2 -0
- package/dist/middleware/finalize-response.js +53 -0
- package/dist/middleware/validation.d.ts +77 -0
- package/dist/middleware/validation.js +53 -0
- package/dist/middleware/zod-coerce.d.ts +9 -0
- package/dist/middleware/zod-coerce.js +50 -0
- package/dist/stripe/client.d.ts +19 -0
- package/dist/stripe/client.js +23 -0
- package/dist/testing/auth.d.ts +36 -0
- package/dist/testing/auth.js +42 -0
- package/dist/testing/configurable-fake.d.ts +14 -0
- package/dist/testing/configurable-fake.js +33 -0
- package/dist/testing/db.d.ts +37 -0
- package/dist/testing/db.js +74 -0
- package/dist/testing/fakes.d.ts +35 -0
- package/dist/testing/fakes.js +56 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/stripe-fixtures.d.ts +13 -0
- package/dist/testing/stripe-fixtures.js +76 -0
- package/package.json +113 -0
- package/scripts/sync-dev-aws.mjs +59 -0
- package/src/ai/gateway.ts +81 -0
- package/src/aws/cloudfront.ts +65 -0
- package/src/aws/secrets-manager.ts +63 -0
- package/src/cache/kv-cache.ts +134 -0
- package/src/db/connection.ts +73 -0
- package/src/db/database.ts +133 -0
- package/src/db/index.ts +27 -0
- package/src/db/jst.ts +56 -0
- package/src/db/orm-config.ts +71 -0
- package/src/db/retry.ts +21 -0
- package/src/db/write-result.ts +23 -0
- package/src/firebase/firebase-verifier.ts +15 -0
- package/src/firebase/identity-toolkit.ts +82 -0
- package/src/firebase/jose-firebase-verifier.ts +71 -0
- package/src/firebase/remote-verifier.ts +49 -0
- package/src/http/app-env.ts +20 -0
- package/src/http/app-info.ts +16 -0
- package/src/http/http-status.ts +62 -0
- package/src/http/nest-error.ts +138 -0
- package/src/http/user-protocol.ts +16 -0
- package/src/index.ts +55 -0
- package/src/middleware/auth.ts +67 -0
- package/src/middleware/finalize-response.ts +61 -0
- package/src/middleware/validation.ts +84 -0
- package/src/middleware/zod-coerce.ts +65 -0
- package/src/stripe/client.ts +48 -0
- package/src/testing/auth.ts +62 -0
- package/src/testing/configurable-fake.ts +33 -0
- package/src/testing/db.ts +125 -0
- package/src/testing/fakes.ts +75 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/stripe-fixtures.ts +85 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createConnection } from 'mysql2/promise';
|
|
2
|
+
/**
|
|
3
|
+
* Hyperdrive バインディングから mysql2 の createConnection 用オプションを作る。
|
|
4
|
+
* `disableEval: true`(Workers で eval 不可)は既定で付与。`extra` で timezone 等を上書き/追加。
|
|
5
|
+
*
|
|
6
|
+
* `decimalNumbers: true`: DECIMAL/NEWDECIMAL を文字列でなく JS number で返す。Drizzle の
|
|
7
|
+
* `$inferSelect`(decimal→string)と生 SQL reads の戻り値を、各 repo の数値ドメイン型
|
|
8
|
+
* (nutrition の number 等)に揃えるため既定で有効化。precision/scale が JS の安全整数域
|
|
9
|
+
* (decimal(15,2) 程度まで)を超える列が無いことが前提。
|
|
10
|
+
*
|
|
11
|
+
* `timezone: '+09:00'`: フリートの接続先 RDB は session time_zone=Asia/Tokyo。mysql2 の driver
|
|
12
|
+
* `timezone` 既定は `'local'`=Workers では UTC で、揃わないと `datetime/timestamp` の生 Date 読みが
|
|
13
|
+
* +9h・生 Date 書きが −9h ズレる(NestJS は JST 実行で一致=移植で顕在化する潜在バグ)。driver を
|
|
14
|
+
* 固定すれば round-trip の観測値は DB の session tz に非依存(内部格納 UTC 値だけ変わるが app 不可視)。
|
|
15
|
+
* 非 JST repo は `extra: { timezone: '...' }` で上書き可。
|
|
16
|
+
*/
|
|
17
|
+
export function hyperdriveConnectionOptions(hyperdrive, extra) {
|
|
18
|
+
return {
|
|
19
|
+
host: hyperdrive.host,
|
|
20
|
+
user: hyperdrive.user,
|
|
21
|
+
password: hyperdrive.password,
|
|
22
|
+
database: hyperdrive.database,
|
|
23
|
+
port: hyperdrive.port,
|
|
24
|
+
disableEval: true,
|
|
25
|
+
decimalNumbers: true,
|
|
26
|
+
timezone: '+09:00',
|
|
27
|
+
...extra,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* primary/replica の接続を開いて `fn` を実行し、finally で `ctx.waitUntil` 越しに閉じる
|
|
32
|
+
* (receptray/tipsys の worker entry の接続ライフサイクル相当)。
|
|
33
|
+
*/
|
|
34
|
+
export async function withMysqlConnections(hyperdrives, ctx, fn, connectionOptions) {
|
|
35
|
+
let primary;
|
|
36
|
+
let replica;
|
|
37
|
+
try {
|
|
38
|
+
primary = await createConnection(hyperdriveConnectionOptions(hyperdrives.primary, connectionOptions));
|
|
39
|
+
replica = await createConnection(hyperdriveConnectionOptions(hyperdrives.replica, connectionOptions));
|
|
40
|
+
return await fn({ primary, replica });
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
const closing = [primary, replica].filter((c) => c !== undefined).map((c) => c.end());
|
|
44
|
+
if (closing.length > 0) {
|
|
45
|
+
ctx.waitUntil(Promise.allSettled(closing));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Connection, Pool } from 'mysql2/promise';
|
|
2
|
+
import type { HyperdriveLike } from './connection';
|
|
3
|
+
/**
|
|
4
|
+
* フリート共通のデータ層(NestJS/TypeORM の master/slave + retryWhenDeadlock 置換)。
|
|
5
|
+
* - reads → replica。透明性のため生 SQL を維持(旧 helper.slave().query 相当)。
|
|
6
|
+
* - writes → primary。型安全のため Drizzle 経由。ただし `write(fn)`/`transaction(fn)` 経由のみで、
|
|
7
|
+
* 生 builder は公開しない。builder はここで await される(Drizzle builder は lazy thenable のため
|
|
8
|
+
* `return builder` が黙って no-op になるフットガンを排除)。両者とも ER_LOCK_DEADLOCK を retry。
|
|
9
|
+
*
|
|
10
|
+
* 重要: kit は `drizzle-orm` の **型同一性に依存しない**。各 repo が自分の drizzle-orm で作った orm を
|
|
11
|
+
* 渡す(`Database` は orm 型 `TDrizzle` にジェネリック)。これにより kit と repo で drizzle-orm の
|
|
12
|
+
* コピーが分かれても(symlink 構成)`MySqlTable`/`SQL` のブランド衝突が起きない。
|
|
13
|
+
*/
|
|
14
|
+
/** reads 用の最小接続インターフェース(mysql2 Connection / Pool が構造的に満たす)。 */
|
|
15
|
+
export interface QueryRunner {
|
|
16
|
+
query(sql: string, params?: unknown[]): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
/** drizzle インスタンスの `.transaction(cb)` が受け取る tx ハンドルの型を取り出す。 */
|
|
19
|
+
export type TxOf<TDrizzle> = TDrizzle extends {
|
|
20
|
+
transaction(cb: (tx: infer Tx) => Promise<unknown>): Promise<unknown>;
|
|
21
|
+
} ? Tx : unknown;
|
|
22
|
+
export interface Database<TDrizzle, TTx = TxOf<TDrizzle>> {
|
|
23
|
+
read<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
24
|
+
/** 単一 INSERT/UPDATE/DELETE。primary で await + deadlock retry。 */
|
|
25
|
+
write<T>(fn: (dz: TDrizzle) => Promise<T>): Promise<T>;
|
|
26
|
+
/** 複数 write を 1 トランザクションで。deadlock 時は全体を retry。 */
|
|
27
|
+
transaction<T>(fn: (tx: TTx) => Promise<T>): Promise<T>;
|
|
28
|
+
}
|
|
29
|
+
/** dispose() を持つ Database(接続をモジュール内で開く版=Hyperdrive / Pool 背面)。 */
|
|
30
|
+
export interface DisposableDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends Database<TDrizzle, TTx> {
|
|
31
|
+
dispose(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export interface CreateMysqlDatabaseOptions<TDrizzle> {
|
|
34
|
+
/** 消費側が自分の drizzle-orm で `drizzle(primary, { schema, ... })` を作って渡す(writes 用)。 */
|
|
35
|
+
orm: TDrizzle;
|
|
36
|
+
/** reads(生 SQL)用の接続。 */
|
|
37
|
+
replica: QueryRunner;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 接続済みの orm/replica を受け取り Database を組み立てる(receptray/tipsys の MysqlDatabase 相当)。
|
|
41
|
+
* 接続・orm の生成と接続の破棄は呼び出し側(worker entry)が担う。
|
|
42
|
+
*/
|
|
43
|
+
export declare function createMysqlDatabase<TDrizzle>(options: CreateMysqlDatabaseOptions<TDrizzle>): Database<TDrizzle>;
|
|
44
|
+
export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
|
|
45
|
+
primaryHyperdrive: HyperdriveLike;
|
|
46
|
+
replicaHyperdrive: HyperdriveLike;
|
|
47
|
+
/** 消費側の drizzle-orm で primary 接続から orm を作る factory(writes 用)。 */
|
|
48
|
+
createOrm: (primary: Connection) => TDrizzle;
|
|
49
|
+
/** createConnection に渡す追加オプション(timezone など)。disableEval:true は既定で付与。 */
|
|
50
|
+
connectionOptions?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Hyperdrive バインディングから接続を遅延生成する Database(foodlabel の MysqlDatabase 相当)。
|
|
54
|
+
* リクエスト毎に new し、レスポンス後 `dispose()` で接続を閉じる。read/write/transaction の面は同一。
|
|
55
|
+
*/
|
|
56
|
+
export declare function createHyperdriveDatabase<TDrizzle>(options: CreateHyperdriveDatabaseOptions<TDrizzle>): DisposableDatabase<TDrizzle>;
|
|
57
|
+
/** orm と replica 接続から Database を組み立てる内部ヘルパ。 */
|
|
58
|
+
export declare function databaseFrom<TDrizzle>(orm: TDrizzle, replica: QueryRunner): Database<TDrizzle>;
|
|
59
|
+
/** Pool/Connection は mysql2 の型。kit の QueryRunner には構造的に代入可能。 */
|
|
60
|
+
export type { Connection, Pool };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createConnection } from 'mysql2/promise';
|
|
2
|
+
import { hyperdriveConnectionOptions } from './connection';
|
|
3
|
+
import { retryWhenDeadlock } from './retry';
|
|
4
|
+
/**
|
|
5
|
+
* 接続済みの orm/replica を受け取り Database を組み立てる(receptray/tipsys の MysqlDatabase 相当)。
|
|
6
|
+
* 接続・orm の生成と接続の破棄は呼び出し側(worker entry)が担う。
|
|
7
|
+
*/
|
|
8
|
+
export function createMysqlDatabase(options) {
|
|
9
|
+
return databaseFrom(options.orm, options.replica);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Hyperdrive バインディングから接続を遅延生成する Database(foodlabel の MysqlDatabase 相当)。
|
|
13
|
+
* リクエスト毎に new し、レスポンス後 `dispose()` で接続を閉じる。read/write/transaction の面は同一。
|
|
14
|
+
*/
|
|
15
|
+
export function createHyperdriveDatabase(options) {
|
|
16
|
+
const { primaryHyperdrive, replicaHyperdrive, createOrm, connectionOptions } = options;
|
|
17
|
+
let primaryConn;
|
|
18
|
+
let replicaConn;
|
|
19
|
+
let orm;
|
|
20
|
+
const primary = () => (primaryConn ??= connect(primaryHyperdrive, connectionOptions));
|
|
21
|
+
const replica = () => (replicaConn ??= connect(replicaHyperdrive, connectionOptions));
|
|
22
|
+
const ormFor = async () => (orm ??= createOrm(await primary()));
|
|
23
|
+
return {
|
|
24
|
+
read(sql, params = []) {
|
|
25
|
+
return retryWhenDeadlock(async () => {
|
|
26
|
+
const [rows] = (await (await replica()).query(sql, params));
|
|
27
|
+
return rows;
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
async write(fn) {
|
|
31
|
+
const dz = await ormFor();
|
|
32
|
+
return retryWhenDeadlock(() => fn(dz));
|
|
33
|
+
},
|
|
34
|
+
async transaction(fn) {
|
|
35
|
+
const dz = (await ormFor());
|
|
36
|
+
return retryWhenDeadlock(() => dz.transaction(fn));
|
|
37
|
+
},
|
|
38
|
+
async dispose() {
|
|
39
|
+
await Promise.all([primaryConn?.then((c) => c.end()), replicaConn?.then((c) => c.end())]);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** orm と replica 接続から Database を組み立てる内部ヘルパ。 */
|
|
44
|
+
export function databaseFrom(orm, replica) {
|
|
45
|
+
const drizzleLike = orm;
|
|
46
|
+
return {
|
|
47
|
+
read(sql, params = []) {
|
|
48
|
+
return retryWhenDeadlock(async () => {
|
|
49
|
+
const [rows] = (await replica.query(sql, params));
|
|
50
|
+
return rows;
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
write(fn) {
|
|
54
|
+
return retryWhenDeadlock(() => fn(orm));
|
|
55
|
+
},
|
|
56
|
+
transaction(fn) {
|
|
57
|
+
return retryWhenDeadlock(() => drizzleLike.transaction(fn));
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function connect(hyperdrive, extra) {
|
|
62
|
+
return createConnection(hyperdriveConnectionOptions(hyperdrive, extra));
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { retryWhenDeadlock } from './retry';
|
|
2
|
+
export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database';
|
|
3
|
+
export type { Database, DisposableDatabase, QueryRunner, TxOf, CreateMysqlDatabaseOptions, CreateHyperdriveDatabaseOptions, Connection, Pool, } from './database';
|
|
4
|
+
export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result';
|
|
5
|
+
export type { DzWriteResult } from './write-result';
|
|
6
|
+
export { hyperdriveConnectionOptions, withMysqlConnections } from './connection';
|
|
7
|
+
export type { HyperdriveLike, ExecutionContextLike } from './connection';
|
|
8
|
+
export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst';
|
|
9
|
+
export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig } from './orm-config';
|
|
10
|
+
export type { HonoDrizzleConfigOptions } from './orm-config';
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @rdlabo/workers-hono-kit/db — mysql2 依存のデータ層ヘルパ(ルート `.` は web 標準のみのため別サブパス)。
|
|
2
|
+
// drizzle-orm の型同一性には依存しない(orm は消費側が渡す)。
|
|
3
|
+
export { retryWhenDeadlock } from './retry';
|
|
4
|
+
export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database';
|
|
5
|
+
export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result';
|
|
6
|
+
export { hyperdriveConnectionOptions, withMysqlConnections } from './connection';
|
|
7
|
+
export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst';
|
|
8
|
+
export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig } from './orm-config';
|
package/dist/db/jst.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* クライアント送出の日付(ISO 8601 `...Z` / `YYYY-MM-DD` / 空文字)を MySQL DATE 用の
|
|
3
|
+
* `YYYY-MM-DD`(JST)へ正規化。nullish/空/解釈不能は null。MySQL DATE は ISO を弾く
|
|
4
|
+
* (ER_TRUNCATED_WRONG_VALUE)ため driver では代替できず、列の toDriver に必要。
|
|
5
|
+
*/
|
|
6
|
+
export declare function toJstDate(value: string | null | undefined): string | null;
|
|
7
|
+
/** `customType` に渡す params: `timestamp(fsp)` 列(created_at 群)。Date 素通し(pass-through)。 */
|
|
8
|
+
export declare const jstTimestampParams: (fsp?: number) => {
|
|
9
|
+
dataType: () => string;
|
|
10
|
+
};
|
|
11
|
+
/** `customType` に渡す params: `datetime` 列(payment.limit_at 等)。Date 素通し(pass-through)。 */
|
|
12
|
+
export declare const jstDatetimeParams: (fsp?: number) => {
|
|
13
|
+
dataType: () => string;
|
|
14
|
+
};
|
|
15
|
+
/** `customType` に渡す params: `date` 列(expiry_date 等)。toJstDate で文字列を正規化。 */
|
|
16
|
+
export declare const jstDateParams: () => {
|
|
17
|
+
dataType: () => string;
|
|
18
|
+
toDriver: (value: string | null) => string | null;
|
|
19
|
+
};
|
package/dist/db/jst.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// JST 日時の整形を「DB 書き込みの副作用」として列の境界に寄せるための共有部品。
|
|
2
|
+
// drizzle-orm の値・型は import しない(kit の脱・型同一性方針)。kit で完成した customType 列を
|
|
3
|
+
// export すると、消費側と drizzle-orm のコピーが分かれた場合に `.default(sql\`…\`)` 等で `SQL` の
|
|
4
|
+
// private プロパティ(shouldInlineParams)が nominal 不一致になり型衝突する(実測で確認)。そのため
|
|
5
|
+
// 列生成は消費側の `customType` に委ね、kit は params とヘルパだけ供給する:
|
|
6
|
+
//
|
|
7
|
+
// import { customType } from 'drizzle-orm/mysql-core';
|
|
8
|
+
// import { jstTimestampParams, jstDateParams } from '@rdlabo/workers-hono-kit/db';
|
|
9
|
+
// export const jstTimestamp = (name: string, opts?: { fsp?: number }) =>
|
|
10
|
+
// customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams(opts?.fsp))(name);
|
|
11
|
+
// export const jstDate = (name: string) =>
|
|
12
|
+
// customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
|
|
13
|
+
//
|
|
14
|
+
// timestamp/datetime は toDriver を置かず Date を素通し → 接続の `timezone:'+09:00'`(hyperdrive
|
|
15
|
+
// 既定)で mysql2 が JST 整形、整形済み文字列も素通し。Drizzle ネイティブ `mode:'date'` は Date を
|
|
16
|
+
// tz 層より前に UTC 文字列化して −9h で壊れるため使わない(customType pass-through が唯一クリーン)。
|
|
17
|
+
// date 列はクライアントの ISO/空文字を MySQL DATE が弾く+JST 日跨ぎ正規化が要るので toJstDate を残す。
|
|
18
|
+
const JST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
|
19
|
+
/**
|
|
20
|
+
* クライアント送出の日付(ISO 8601 `...Z` / `YYYY-MM-DD` / 空文字)を MySQL DATE 用の
|
|
21
|
+
* `YYYY-MM-DD`(JST)へ正規化。nullish/空/解釈不能は null。MySQL DATE は ISO を弾く
|
|
22
|
+
* (ER_TRUNCATED_WRONG_VALUE)ため driver では代替できず、列の toDriver に必要。
|
|
23
|
+
*/
|
|
24
|
+
export function toJstDate(value) {
|
|
25
|
+
if (!value) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const ms = new Date(value).getTime();
|
|
29
|
+
if (Number.isNaN(ms)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const jst = new Date(ms + JST_OFFSET_MS);
|
|
33
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
34
|
+
return `${jst.getUTCFullYear()}-${p(jst.getUTCMonth() + 1)}-${p(jst.getUTCDate())}`;
|
|
35
|
+
}
|
|
36
|
+
/** `customType` に渡す params: `timestamp(fsp)` 列(created_at 群)。Date 素通し(pass-through)。 */
|
|
37
|
+
export const jstTimestampParams = (fsp) => ({
|
|
38
|
+
dataType: () => (fsp != null ? `timestamp(${fsp})` : 'timestamp'),
|
|
39
|
+
});
|
|
40
|
+
/** `customType` に渡す params: `datetime` 列(payment.limit_at 等)。Date 素通し(pass-through)。 */
|
|
41
|
+
export const jstDatetimeParams = (fsp) => ({
|
|
42
|
+
dataType: () => (fsp != null ? `datetime(${fsp})` : 'datetime'),
|
|
43
|
+
});
|
|
44
|
+
/** `customType` に渡す params: `date` 列(expiry_date 等)。toJstDate で文字列を正規化。 */
|
|
45
|
+
export const jstDateParams = () => ({
|
|
46
|
+
dataType: () => 'date',
|
|
47
|
+
toDriver: (value) => toJstDate(value),
|
|
48
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle の列名 casing をフリートで一元管理する(標準: snake_case を「config」と「runtime」の両方で固定)。
|
|
3
|
+
*
|
|
4
|
+
* casing は2か所にあり別物:
|
|
5
|
+
* ① drizzle.config.ts の top-level `casing` … `db:generate` が **作る列名** を決める(→ honoDrizzleConfig)
|
|
6
|
+
* ② database.ts の `drizzle(conn, { …casing })` … **実行時の書き込みビルダが参照する列名** を決める(→ DRIZZLE_ORM_OPTIONS)
|
|
7
|
+
*
|
|
8
|
+
* この2つが食い違うと、明示列名を書き忘れた camelCase 複数単語列で generate と実行時がズレて実行時
|
|
9
|
+
* `Unknown column` になる(typecheck もマイグレーションも正常に見えるので発覚が遅い)。両方ここから取れば
|
|
10
|
+
* ズレが構造的に起きない。明示列名がある列では casing は無視されるので、既存挙動は変えない純粋な安全網。
|
|
11
|
+
*
|
|
12
|
+
* 注: runtime の `drizzle()` 呼び出し自体は **消費側 repo が自分の drizzle-orm で行う**(kit が drizzle() を
|
|
13
|
+
* 呼ぶと kit と repo で drizzle-orm が別コピーになり型同一性が壊れるため)。kit は「値」だけを提供する。
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* runtime 用。消費側 repo の database.ts で `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` と spread して使う。
|
|
17
|
+
* mode/casing を kit が固定し、書き込みビルダの列名解決を snake_case に揃える。
|
|
18
|
+
*/
|
|
19
|
+
export declare const DRIZZLE_ORM_OPTIONS: {
|
|
20
|
+
readonly mode: "default";
|
|
21
|
+
readonly casing: "snake_case";
|
|
22
|
+
};
|
|
23
|
+
export interface HonoDrizzleConfigOptions {
|
|
24
|
+
/** drizzle-kit の dbCredentials.database(localConnectionString とは別)。 */
|
|
25
|
+
database: string;
|
|
26
|
+
host?: string;
|
|
27
|
+
port?: number;
|
|
28
|
+
user?: string;
|
|
29
|
+
password?: string;
|
|
30
|
+
/** 既定 './src/db/schemes'。 */
|
|
31
|
+
schema?: string;
|
|
32
|
+
/** 既定 './drizzle'。 */
|
|
33
|
+
out?: string;
|
|
34
|
+
/** /api と DB を共有する repo は schema 由来テーブルに限定する(省略可)。 */
|
|
35
|
+
tablesFilter?: string[];
|
|
36
|
+
/** db:introspect(DB→JS)の casing。生成方向の `casing:'snake_case'` とは別軸(省略可)。 */
|
|
37
|
+
introspect?: {
|
|
38
|
+
casing: 'camel' | 'preserve';
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
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 を読むだけ)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function honoDrizzleConfig(options: HonoDrizzleConfigOptions): {
|
|
47
|
+
dbCredentials: {
|
|
48
|
+
host: string;
|
|
49
|
+
port: number;
|
|
50
|
+
user: string;
|
|
51
|
+
password: string;
|
|
52
|
+
database: string;
|
|
53
|
+
};
|
|
54
|
+
introspect?: {
|
|
55
|
+
casing: "camel" | "preserve";
|
|
56
|
+
} | undefined;
|
|
57
|
+
tablesFilter?: string[] | undefined;
|
|
58
|
+
dialect: "mysql";
|
|
59
|
+
schema: string;
|
|
60
|
+
out: string;
|
|
61
|
+
casing: "snake_case";
|
|
62
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle の列名 casing をフリートで一元管理する(標準: snake_case を「config」と「runtime」の両方で固定)。
|
|
3
|
+
*
|
|
4
|
+
* casing は2か所にあり別物:
|
|
5
|
+
* ① drizzle.config.ts の top-level `casing` … `db:generate` が **作る列名** を決める(→ honoDrizzleConfig)
|
|
6
|
+
* ② database.ts の `drizzle(conn, { …casing })` … **実行時の書き込みビルダが参照する列名** を決める(→ DRIZZLE_ORM_OPTIONS)
|
|
7
|
+
*
|
|
8
|
+
* この2つが食い違うと、明示列名を書き忘れた camelCase 複数単語列で generate と実行時がズレて実行時
|
|
9
|
+
* `Unknown column` になる(typecheck もマイグレーションも正常に見えるので発覚が遅い)。両方ここから取れば
|
|
10
|
+
* ズレが構造的に起きない。明示列名がある列では casing は無視されるので、既存挙動は変えない純粋な安全網。
|
|
11
|
+
*
|
|
12
|
+
* 注: runtime の `drizzle()` 呼び出し自体は **消費側 repo が自分の drizzle-orm で行う**(kit が drizzle() を
|
|
13
|
+
* 呼ぶと kit と repo で drizzle-orm が別コピーになり型同一性が壊れるため)。kit は「値」だけを提供する。
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* runtime 用。消費側 repo の database.ts で `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` と spread して使う。
|
|
17
|
+
* mode/casing を kit が固定し、書き込みビルダの列名解決を snake_case に揃える。
|
|
18
|
+
*/
|
|
19
|
+
export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' };
|
|
20
|
+
/**
|
|
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 を読むだけ)。
|
|
24
|
+
*/
|
|
25
|
+
export function honoDrizzleConfig(options) {
|
|
26
|
+
const { database, host, port, user, password, schema = './src/db/schemes', out = './drizzle', tablesFilter, introspect, } = options;
|
|
27
|
+
return {
|
|
28
|
+
dialect: 'mysql',
|
|
29
|
+
schema,
|
|
30
|
+
out,
|
|
31
|
+
casing: 'snake_case',
|
|
32
|
+
...(tablesFilter ? { tablesFilter } : {}),
|
|
33
|
+
...(introspect ? { introspect } : {}),
|
|
34
|
+
dbCredentials: {
|
|
35
|
+
host: host ?? process.env.DB_HOST ?? '127.0.0.1',
|
|
36
|
+
port: port ?? Number(process.env.DB_PORT ?? 3306),
|
|
37
|
+
user: user ?? process.env.DB_USER ?? 'root',
|
|
38
|
+
password: password ?? process.env.DB_PASSWORD ?? 'root',
|
|
39
|
+
database,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ER_LOCK_DEADLOCK を指数バックオフで retry する(NestJS/TypeORM の retryWhenDeadlock 相当)。
|
|
3
|
+
* MySQL はデッドロック時にトランザクション全体をロールバックするため、同じ作業単位の再実行は安全。
|
|
4
|
+
* `fn` は単一文(write)またはトランザクション全体(transaction)であること — retry は `fn` 全体を再実行する。
|
|
5
|
+
*/
|
|
6
|
+
export declare function retryWhenDeadlock<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
|
package/dist/db/retry.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ER_LOCK_DEADLOCK を指数バックオフで retry する(NestJS/TypeORM の retryWhenDeadlock 相当)。
|
|
3
|
+
* MySQL はデッドロック時にトランザクション全体をロールバックするため、同じ作業単位の再実行は安全。
|
|
4
|
+
* `fn` は単一文(write)またはトランザクション全体(transaction)であること — retry は `fn` 全体を再実行する。
|
|
5
|
+
*/
|
|
6
|
+
export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
|
|
7
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
8
|
+
try {
|
|
9
|
+
return await fn();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const code = error.code;
|
|
13
|
+
if (code === 'ER_LOCK_DEADLOCK' && attempt < retries - 1) {
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Unreachable: the loop returns on success and throws on the final failed attempt.
|
|
21
|
+
throw new Error('retryWhenDeadlock: exhausted retries');
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle(mysql2) の write 結果から insertId / affectedRows を型安全に取り出すヘルパ。
|
|
3
|
+
* 生の builder/ResultSetHeader を repo 側に晒さずに、よく使う値だけを取り出す。
|
|
4
|
+
*
|
|
5
|
+
* mysql2 の INSERT/UPDATE/DELETE 結果は `[ResultSetHeader, FieldPacket[]]` 形。
|
|
6
|
+
*/
|
|
7
|
+
export type DzWriteResult = readonly [{
|
|
8
|
+
insertId: number;
|
|
9
|
+
affectedRows: number;
|
|
10
|
+
}, ...unknown[]];
|
|
11
|
+
export declare function insertIdOf(result: DzWriteResult): number;
|
|
12
|
+
export declare function affectedRowsOf(result: DzWriteResult): number;
|
|
13
|
+
/**
|
|
14
|
+
* 一括 INSERT で連番採番された行の id 群を返す(mysql2 は先頭 insertId のみ返すため count 分を生成)。
|
|
15
|
+
*/
|
|
16
|
+
export declare function insertedIdsOf(result: DzWriteResult, count: number): number[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function insertIdOf(result) {
|
|
2
|
+
return result[0].insertId;
|
|
3
|
+
}
|
|
4
|
+
export function affectedRowsOf(result) {
|
|
5
|
+
return result[0].affectedRows;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 一括 INSERT で連番採番された行の id 群を返す(mysql2 は先頭 insertId のみ返すため count 分を生成)。
|
|
9
|
+
*/
|
|
10
|
+
export function insertedIdsOf(result, count) {
|
|
11
|
+
const base = result[0].insertId;
|
|
12
|
+
return Array.from({ length: count }, (_, i) => base + i);
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Firebase boundary replacing the firebase-admin Auth surface used by `/api`. */
|
|
2
|
+
export interface DecodedIdToken {
|
|
3
|
+
uid: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
[claim: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface FirebaseVerifier {
|
|
8
|
+
/** Mirrors firebase-admin getAuth().verifyIdToken(). Throws on invalid token. */
|
|
9
|
+
verifyIdToken(idToken: string): Promise<DecodedIdToken>;
|
|
10
|
+
/** Mirrors getAuth().getUser(); returns null when the user is absent. */
|
|
11
|
+
getUser(uid: string): Promise<{
|
|
12
|
+
uid: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
} | null>;
|
|
15
|
+
/** Mirrors getAuth().deleteUser(). */
|
|
16
|
+
deleteUser(uid: string): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
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.
|
|
5
|
+
*
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
export interface ServiceAccount {
|
|
10
|
+
client_email: string;
|
|
11
|
+
private_key: string;
|
|
12
|
+
project_id: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class IdentityToolkit {
|
|
15
|
+
private readonly sa;
|
|
16
|
+
private accessToken;
|
|
17
|
+
constructor(sa: ServiceAccount);
|
|
18
|
+
private getAccessToken;
|
|
19
|
+
lookup(uid: string, nowSeconds: number): Promise<{
|
|
20
|
+
uid: string;
|
|
21
|
+
email?: string;
|
|
22
|
+
} | null>;
|
|
23
|
+
remove(uid: string, nowSeconds: number): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SignJWT, importPKCS8 } from 'jose';
|
|
2
|
+
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
3
|
+
const IDENTITY_TOOLKIT = 'https://identitytoolkit.googleapis.com/v1';
|
|
4
|
+
const SCOPE = 'https://www.googleapis.com/auth/identitytoolkit https://www.googleapis.com/auth/firebase';
|
|
5
|
+
export class IdentityToolkit {
|
|
6
|
+
sa;
|
|
7
|
+
accessToken = null;
|
|
8
|
+
constructor(sa) {
|
|
9
|
+
this.sa = sa;
|
|
10
|
+
}
|
|
11
|
+
async getAccessToken(nowSeconds) {
|
|
12
|
+
if (this.accessToken && this.accessToken.expiresAt > nowSeconds + 60) {
|
|
13
|
+
return this.accessToken.value;
|
|
14
|
+
}
|
|
15
|
+
const key = await importPKCS8(this.sa.private_key, 'RS256');
|
|
16
|
+
const assertion = await new SignJWT({ scope: SCOPE })
|
|
17
|
+
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
|
|
18
|
+
.setIssuer(this.sa.client_email)
|
|
19
|
+
.setSubject(this.sa.client_email)
|
|
20
|
+
.setAudience(TOKEN_URL)
|
|
21
|
+
.setIssuedAt(nowSeconds)
|
|
22
|
+
.setExpirationTime(nowSeconds + 3600)
|
|
23
|
+
.sign(key);
|
|
24
|
+
const res = await fetch(TOKEN_URL, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
27
|
+
body: new URLSearchParams({
|
|
28
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
29
|
+
assertion,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`Identity Toolkit token exchange failed: ${res.status}`);
|
|
34
|
+
}
|
|
35
|
+
const json = (await res.json());
|
|
36
|
+
this.accessToken = { value: json.access_token, expiresAt: nowSeconds + json.expires_in };
|
|
37
|
+
return json.access_token;
|
|
38
|
+
}
|
|
39
|
+
async lookup(uid, nowSeconds) {
|
|
40
|
+
const token = await this.getAccessToken(nowSeconds);
|
|
41
|
+
const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:lookup`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ localId: [uid] }),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const json = (await res.json());
|
|
50
|
+
const user = json.users?.[0];
|
|
51
|
+
return user ? { uid: user.localId, email: user.email } : null;
|
|
52
|
+
}
|
|
53
|
+
async remove(uid, nowSeconds) {
|
|
54
|
+
const token = await this.getAccessToken(nowSeconds);
|
|
55
|
+
const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:delete`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ localId: uid }),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`Identity Toolkit delete failed: ${res.status}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CryptoKey, JWK, JWTVerifyGetKey, KeyObject } from 'jose';
|
|
2
|
+
import type { DecodedIdToken, FirebaseVerifier } from './firebase-verifier';
|
|
3
|
+
import type { IdentityToolkit } from './identity-toolkit';
|
|
4
|
+
type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array | JWTVerifyGetKey;
|
|
5
|
+
/**
|
|
6
|
+
* Replaces firebase-admin getAuth().verifyIdToken() with jose RS256 verification against
|
|
7
|
+
* Google's securetoken JWKS. Mirrors the admin SDK's checks: issuer/audience = projectId,
|
|
8
|
+
* RS256, a non-empty subject (the uid), and a valid auth_time.
|
|
9
|
+
*
|
|
10
|
+
* - prod: keyResolver = createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL)).
|
|
11
|
+
* - test: keyResolver = the generated public key (offline, no network).
|
|
12
|
+
*
|
|
13
|
+
* getUser/deleteUser delegate to Identity Toolkit REST (network); absent it throws.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SECURETOKEN_JWK_URL = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
|
|
16
|
+
export declare class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
17
|
+
private readonly opts;
|
|
18
|
+
constructor(opts: {
|
|
19
|
+
projectId: string;
|
|
20
|
+
keyResolver: KeyInput;
|
|
21
|
+
identity?: IdentityToolkit;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
});
|
|
24
|
+
verifyIdToken(idToken: string): Promise<DecodedIdToken>;
|
|
25
|
+
getUser(uid: string): Promise<{
|
|
26
|
+
uid: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
} | null>;
|
|
29
|
+
deleteUser(uid: string): Promise<void>;
|
|
30
|
+
private nowSeconds;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jwtVerify } from 'jose';
|
|
2
|
+
/**
|
|
3
|
+
* Replaces firebase-admin getAuth().verifyIdToken() with jose RS256 verification against
|
|
4
|
+
* Google's securetoken JWKS. Mirrors the admin SDK's checks: issuer/audience = projectId,
|
|
5
|
+
* RS256, a non-empty subject (the uid), and a valid auth_time.
|
|
6
|
+
*
|
|
7
|
+
* - prod: keyResolver = createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL)).
|
|
8
|
+
* - test: keyResolver = the generated public key (offline, no network).
|
|
9
|
+
*
|
|
10
|
+
* getUser/deleteUser delegate to Identity Toolkit REST (network); absent it throws.
|
|
11
|
+
*/
|
|
12
|
+
export const SECURETOKEN_JWK_URL = 'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com';
|
|
13
|
+
export class JoseFirebaseVerifier {
|
|
14
|
+
opts;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.opts = opts;
|
|
17
|
+
}
|
|
18
|
+
async verifyIdToken(idToken) {
|
|
19
|
+
const options = {
|
|
20
|
+
issuer: `https://securetoken.google.com/${this.opts.projectId}`,
|
|
21
|
+
audience: this.opts.projectId,
|
|
22
|
+
algorithms: ['RS256'],
|
|
23
|
+
};
|
|
24
|
+
// Branch so each call matches a single jwtVerify overload (static key vs getKey fn).
|
|
25
|
+
const key = this.opts.keyResolver;
|
|
26
|
+
const { payload } = typeof key === 'function' ? await jwtVerify(idToken, key, options) : await jwtVerify(idToken, key, options);
|
|
27
|
+
// Mirror firebase-admin's extra checks beyond signature/iss/aud/exp:
|
|
28
|
+
if (!payload.sub || typeof payload.sub !== 'string' || payload.sub.length > 128) {
|
|
29
|
+
throw new Error('Firebase ID token has an invalid subject');
|
|
30
|
+
}
|
|
31
|
+
const authTime = payload.auth_time;
|
|
32
|
+
if (typeof authTime !== 'number' || authTime > this.nowSeconds()) {
|
|
33
|
+
throw new Error('Firebase ID token has an invalid auth_time');
|
|
34
|
+
}
|
|
35
|
+
return { ...payload, uid: payload.sub, email: payload.email };
|
|
36
|
+
}
|
|
37
|
+
async getUser(uid) {
|
|
38
|
+
if (!this.opts.identity) {
|
|
39
|
+
throw new Error('Identity Toolkit not configured');
|
|
40
|
+
}
|
|
41
|
+
return this.opts.identity.lookup(uid, this.nowSeconds());
|
|
42
|
+
}
|
|
43
|
+
async deleteUser(uid) {
|
|
44
|
+
if (!this.opts.identity) {
|
|
45
|
+
throw new Error('Identity Toolkit not configured');
|
|
46
|
+
}
|
|
47
|
+
await this.opts.identity.remove(uid, this.nowSeconds());
|
|
48
|
+
}
|
|
49
|
+
nowSeconds() {
|
|
50
|
+
return this.opts.now ? this.opts.now() : Math.floor(Date.now() / 1000);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { JoseFirebaseVerifier } from './jose-firebase-verifier';
|
|
2
|
+
export declare function createRemoteFirebaseVerifier(projectId: string): JoseFirebaseVerifier;
|
|
3
|
+
/**
|
|
4
|
+
* サービスアカウント JSON から検証器を作る便宜ファクトリ(receptray/tipsys hono の `firebaseFor` 相当)。
|
|
5
|
+
* `getUser`/`deleteUser` のため `IdentityToolkit` を内包する点が `createRemoteFirebaseVerifier` との違い。
|
|
6
|
+
* SA JSON 文字列をキーに isolate 内で 1 つだけキャッシュ(秘密が変わったときだけ再生成)し、
|
|
7
|
+
* JWKS は `createRemoteFirebaseVerifier` と共有する。
|
|
8
|
+
*/
|
|
9
|
+
export declare function createServiceAccountVerifier(serviceAccountJson: string): JoseFirebaseVerifier;
|