@rdlabo/workers-hono-kit 0.3.7 → 0.4.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 (52) hide show
  1. package/README.md +33 -1
  2. package/dist/business-time/index.d.ts +49 -0
  3. package/dist/business-time/index.js +149 -0
  4. package/dist/business-time/types.d.ts +9 -0
  5. package/dist/business-time/types.js +5 -0
  6. package/dist/db/columns.d.ts +46 -0
  7. package/dist/db/columns.js +36 -0
  8. package/dist/db/connection.js +2 -1
  9. package/dist/db/decimal.d.ts +27 -0
  10. package/dist/db/decimal.js +50 -0
  11. package/dist/db/index.d.ts +4 -1
  12. package/dist/db/index.js +3 -1
  13. package/dist/db/jst.d.ts +10 -72
  14. package/dist/db/jst.js +10 -82
  15. package/package.json +7 -3
  16. package/src/ai/gateway.ts +0 -120
  17. package/src/aws/cloudfront.ts +0 -105
  18. package/src/aws/secrets-manager.ts +0 -112
  19. package/src/cache/kv-cache.ts +0 -316
  20. package/src/db/connection.ts +0 -107
  21. package/src/db/database.ts +0 -269
  22. package/src/db/index.ts +0 -39
  23. package/src/db/jst.ts +0 -122
  24. package/src/db/migrate.ts +0 -155
  25. package/src/db/orm-config.ts +0 -171
  26. package/src/db/retry.ts +0 -43
  27. package/src/db/write-result.ts +0 -46
  28. package/src/firebase/firebase-verifier.ts +0 -76
  29. package/src/firebase/identity-toolkit.ts +0 -179
  30. package/src/firebase/jose-firebase-verifier.ts +0 -159
  31. package/src/firebase/remote-verifier.ts +0 -98
  32. package/src/http/app-env.ts +0 -53
  33. package/src/http/app-info.ts +0 -38
  34. package/src/http/execution-context.ts +0 -11
  35. package/src/http/http-status.ts +0 -71
  36. package/src/http/nest-error.ts +0 -207
  37. package/src/http/trailing-slash.ts +0 -28
  38. package/src/http/user-protocol.ts +0 -36
  39. package/src/index.ts +0 -77
  40. package/src/middleware/auth.ts +0 -129
  41. package/src/middleware/finalize-response.ts +0 -90
  42. package/src/middleware/validation.ts +0 -158
  43. package/src/middleware/zod-coerce.ts +0 -124
  44. package/src/queue/consumer.ts +0 -146
  45. package/src/queue/send.ts +0 -129
  46. package/src/stripe/client.ts +0 -85
  47. package/src/testing/auth.ts +0 -110
  48. package/src/testing/configurable-fake.ts +0 -45
  49. package/src/testing/db.ts +0 -194
  50. package/src/testing/fakes.ts +0 -153
  51. package/src/testing/index.ts +0 -31
  52. package/src/testing/stripe-fixtures.ts +0 -175
package/dist/db/jst.js CHANGED
@@ -1,72 +1,24 @@
1
1
  /**
2
- * Shared building blocks for normalizing JST (Asia/Tokyo) date/time values.
2
+ * MySQL / Drizzle 向け JST ワイヤ変換と DATE 列正規化。
3
3
  *
4
4
  * @remarks
5
- * JST normalization is applied at the column boundary as a write-time side effect, and neither
6
- * values nor types from `drizzle-orm` are imported here on purpose. Exporting a fully-built
7
- * `customType` column from the kit would cause type collisions when the kit and the consumer
8
- * resolve separate copies of `drizzle-orm` (the private `SQL` brand stops being nominally
9
- * compatible). Instead the kit ships only the params and helpers, and the consumer builds the
10
- * column with its own `customType`:
11
- *
12
- * ```ts
13
- * import { customType } from 'drizzle-orm/mysql-core';
14
- * import { jstTimestampParams, jstDateParams } from '@rdlabo/workers-hono-kit/db';
15
- *
16
- * export const jstTimestamp = (name: string, opts?: { fsp?: number }) =>
17
- * customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams(opts?.fsp))(name);
18
- * export const jstDate = (name: string) =>
19
- * customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
20
- * ```
21
- *
22
- * `timestamp`/`datetime` columns omit `toDriver` and pass `Date` values straight through, so the
23
- * connection's `timezone: '+09:00'` default makes mysql2 format them as JST; pre-formatted strings
24
- * also pass through. Drizzle's native `mode: 'date'` is avoided because it stringifies `Date` to
25
- * UTC before the timezone layer, shifting values by -9h. `date` columns keep `toJstDate` because
26
- * MySQL `DATE` rejects ISO/empty strings and a JST day-boundary normalization is required.
5
+ * 業務時刻の意味論は {@link ../business-time/index.js | business-time} に集約する。
6
+ * このモジュールは「MySQL 接続既定」「DATE 列の toDriver」、列 `customType` params のみを担う。
27
7
  */
28
- const JST_OFFSET_MS = 9 * 60 * 60 * 1000;
8
+ import { normalizeBusinessDate } from '../business-time/index.js';
9
+ /** mysql2 接続 `timezone` 既定(既存 JST DB 運用)。 */
10
+ export const MYSQL_TIMEZONE = '+09:00';
29
11
  /**
30
- * Normalize a client-supplied date to the `YYYY-MM-DD` (JST) form accepted by a MySQL `DATE` column.
31
- *
32
- * Accepts ISO 8601 (`...Z`), `YYYY-MM-DD`, or an empty string. Nullish, empty, or unparseable input
33
- * resolves to `null`.
34
- *
35
- * @remarks
36
- * MySQL `DATE` rejects ISO strings with `ER_TRUNCATED_WRONG_VALUE`, so this cannot be handled by the
37
- * driver alone; it is needed as the `toDriver` transform for a `date` column.
38
- *
39
- * @param value - the raw date string from the client (ISO 8601, `YYYY-MM-DD`, or empty), or nullish.
40
- * @returns the JST calendar date as `YYYY-MM-DD`, or `null` when the input is empty or unparseable.
12
+ * クライアント入力を MySQL `DATE` 列向け `YYYY-MM-DD`(JST 業務暦日)へ正規化。
13
+ * ISO 8601 / `YYYY-MM-DD` / 空文字を受け付ける。`YYYY-MM-DD` は Date 化せずそのまま渡す。
41
14
  */
42
15
  export function toJstDate(value) {
43
- if (!value) {
44
- return null;
45
- }
46
- const ms = new Date(value).getTime();
47
- if (Number.isNaN(ms)) {
48
- return null;
49
- }
50
- const jst = new Date(ms + JST_OFFSET_MS);
51
- const p = (n) => String(n).padStart(2, '0');
52
- return `${jst.getUTCFullYear()}-${p(jst.getUTCMonth() + 1)}-${p(jst.getUTCDate())}`;
16
+ return normalizeBusinessDate(value ?? null);
53
17
  }
54
18
  /**
55
19
  * Build the params for a `customType` backing a MySQL `timestamp` column with `Date` pass-through.
56
20
  *
57
- * The column omits `toDriver`, so `Date` values flow straight to mysql2 and are formatted as JST by
58
- * the connection's `timezone: '+09:00'` default.
59
- *
60
21
  * @param fsp - optional fractional-seconds precision; when provided, emits `timestamp(fsp)`.
61
- * @returns the `customType` params object exposing the column's `dataType`.
62
- * @example
63
- * ```ts
64
- * import { customType } from 'drizzle-orm/mysql-core';
65
- * import { jstTimestampParams } from '@rdlabo/workers-hono-kit/db';
66
- *
67
- * const jstTimestamp = (name: string) =>
68
- * customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams())(name);
69
- * ```
70
22
  */
71
23
  export const jstTimestampParams = (fsp) => ({
72
24
  dataType: () => (fsp != null ? `timestamp(${fsp})` : 'timestamp'),
@@ -74,19 +26,7 @@ export const jstTimestampParams = (fsp) => ({
74
26
  /**
75
27
  * Build the params for a `customType` backing a MySQL `datetime` column with `Date` pass-through.
76
28
  *
77
- * Behaves like {@link jstTimestampParams} but emits a `datetime` data type; `Date` values pass
78
- * through and are formatted as JST by the connection's `timezone: '+09:00'` default.
79
- *
80
29
  * @param fsp - optional fractional-seconds precision; when provided, emits `datetime(fsp)`.
81
- * @returns the `customType` params object exposing the column's `dataType`.
82
- * @example
83
- * ```ts
84
- * import { customType } from 'drizzle-orm/mysql-core';
85
- * import { jstDatetimeParams } from '@rdlabo/workers-hono-kit/db';
86
- *
87
- * const jstDatetime = (name: string) =>
88
- * customType<{ data: string | Date; driverData: string | Date }>(jstDatetimeParams())(name);
89
- * ```
90
30
  */
91
31
  export const jstDatetimeParams = (fsp) => ({
92
32
  dataType: () => (fsp != null ? `datetime(${fsp})` : 'datetime'),
@@ -94,19 +34,7 @@ export const jstDatetimeParams = (fsp) => ({
94
34
  /**
95
35
  * Build the params for a `customType` backing a MySQL `date` column with JST normalization.
96
36
  *
97
- * Unlike the timestamp/datetime params, this defines a `toDriver` transform that runs
98
- * {@link toJstDate} so client-supplied ISO/empty strings are normalized to a JST `YYYY-MM-DD` value
99
- * the column accepts.
100
- *
101
- * @returns the `customType` params object exposing the column's `dataType` and `toDriver`.
102
- * @example
103
- * ```ts
104
- * import { customType } from 'drizzle-orm/mysql-core';
105
- * import { jstDateParams } from '@rdlabo/workers-hono-kit/db';
106
- *
107
- * const jstDate = (name: string) =>
108
- * customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
109
- * ```
37
+ * @returns params with `toDriver` running {@link toJstDate}.
110
38
  */
111
39
  export const jstDateParams = () => ({
112
40
  dataType: () => 'date',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,6 @@
35
35
  },
36
36
  "files": [
37
37
  "dist",
38
- "src",
39
38
  "scripts",
40
39
  "!src/**/*.spec.ts"
41
40
  ],
@@ -55,6 +54,11 @@
55
54
  "import": "./dist/db/index.js",
56
55
  "default": "./dist/db/index.js"
57
56
  },
57
+ "./business-time": {
58
+ "types": "./dist/business-time/index.d.ts",
59
+ "import": "./dist/business-time/index.js",
60
+ "default": "./dist/business-time/index.js"
61
+ },
58
62
  "./testing": {
59
63
  "types": "./dist/testing/index.d.ts",
60
64
  "import": "./dist/testing/index.js",
@@ -110,7 +114,7 @@
110
114
  "ai": "^6.0.204",
111
115
  "ai-gateway-provider": "^3.1.3",
112
116
  "aws4fetch": "^1.0.20",
113
- "drizzle-orm": "0.45.2",
117
+ "drizzle-orm": "^0.45.2",
114
118
  "eslint": "^9.39.4",
115
119
  "hono": "^4.6.0",
116
120
  "jose": "^6.2.3",
package/src/ai/gateway.ts DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * Cloudflare AI Gateway provider factory built on the Vercel AI SDK and `ai-gateway-provider`.
3
- *
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.
8
- *
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.
13
- */
14
- import { createAiGateway } from 'ai-gateway-provider';
15
- import type { AiGateway, AiGatewayBindingSettings, AiGatewayOptions } from 'ai-gateway-provider';
16
-
17
- export type { AiGateway, AiGatewayOptions } from 'ai-gateway-provider';
18
-
19
- /**
20
- * Minimal shape of a Workers AI binding (`env.AI.gateway(name)`).
21
- *
22
- * @remarks
23
- * Cloudflare's runtime `AiGateway` type is structurally compatible with this binding shape.
24
- */
25
- export type AiGatewayBinding = AiGatewayBindingSettings['binding'];
26
-
27
- /**
28
- * Configuration for the AI Gateway provider. This is a union with two mutually exclusive forms.
29
- *
30
- * @remarks
31
- * - **Binding form** — for the Workers runtime (production and `wrangler dev`). Pass the
32
- * `env.AI.gateway(name)` binding. Requests through a binding are pre-authenticated within the same
33
- * Cloudflare account, so no Gateway token is required.
34
- * - **REST form** — for non-Workers contexts where a binding is unavailable (e.g. a Node evaluation
35
- * harness). Supply `accountId`, `gateway`, and (for authenticated Gateways) `token` to reach the
36
- * Gateway over REST.
37
- */
38
- export type AiGatewayConfig =
39
- | {
40
- /** The AI Gateway binding, typically obtained via `env.AI.gateway(name)`. */
41
- binding: AiGatewayBinding;
42
- /** Optional Gateway options such as caching, retries, and request metadata. */
43
- options?: AiGatewayOptions;
44
- }
45
- | {
46
- /** Cloudflare account ID that owns the Gateway. */
47
- accountId: string;
48
- /** AI Gateway name. */
49
- gateway: string;
50
- /**
51
- * Gateway authentication token sent in the `cf-aig-authorization` header. Required only for an
52
- * Authenticated Gateway; omit it for an unauthenticated Gateway. This authenticates the request to
53
- * the Gateway itself and is distinct from any provider API key.
54
- */
55
- token?: string;
56
- /** Optional Gateway options such as caching, retries, and request metadata. */
57
- options?: AiGatewayOptions;
58
- };
59
-
60
- /** Provider object exposing the AI Gateway model wrapper. */
61
- export interface AiGatewayProvider {
62
- /**
63
- * Wraps an `@ai-sdk/*` model so its requests are routed through the AI Gateway.
64
- *
65
- * @remarks
66
- * Example invocation: `aigateway(createAnthropic({ apiKey })('claude-...'))`. Passing an array of
67
- * models enables fallback behavior — each model is attempted in order from the start of the array.
68
- */
69
- aigateway: AiGateway;
70
- }
71
-
72
- /**
73
- * Create an AI Gateway provider from either the binding form or the REST form of the configuration.
74
- *
75
- * @param config - The Gateway configuration; either the binding form or the REST form.
76
- * @returns A provider whose `aigateway` wrapper routes models through the AI Gateway.
77
- * @throws Error When the REST form is used and `accountId` or `gateway` is missing (fail-fast).
78
- * @example
79
- * ```ts
80
- * // Binding form (Workers runtime: production / wrangler dev)
81
- * import { createAnthropic } from '@ai-sdk/anthropic';
82
- *
83
- * const { aigateway } = createAiGatewayProvider({ binding: env.AI.gateway('my-gateway') });
84
- * const model = aigateway(createAnthropic({ apiKey: env.ANTHROPIC_API_KEY })('claude-3-5-sonnet-latest'));
85
- * ```
86
- * @example
87
- * ```ts
88
- * // REST form (non-Workers context, e.g. a Node evaluation harness)
89
- * import { createOpenAI } from '@ai-sdk/openai';
90
- *
91
- * const { aigateway } = createAiGatewayProvider({
92
- * accountId: process.env.CF_ACCOUNT_ID!,
93
- * gateway: 'my-gateway',
94
- * token: process.env.CF_AIG_TOKEN, // only for an Authenticated Gateway
95
- * });
96
- * const model = aigateway(createOpenAI({ apiKey: process.env.OPENAI_API_KEY })('gpt-4o'));
97
- * ```
98
- */
99
- export function createAiGatewayProvider(config: AiGatewayConfig): AiGatewayProvider {
100
- if ('binding' in config) {
101
- return { aigateway: createAiGateway({ binding: config.binding, options: config.options }) };
102
- }
103
-
104
- if (!config.accountId) {
105
- throw new Error('AI Gateway: accountId is not set');
106
- }
107
- if (!config.gateway) {
108
- throw new Error('AI Gateway: gateway name is not set');
109
- }
110
-
111
- // The token is sent as apiKey only for an Authenticated Gateway; undefined is fine when unauthenticated.
112
- return {
113
- aigateway: createAiGateway({
114
- accountId: config.accountId,
115
- gateway: config.gateway,
116
- apiKey: config.token,
117
- options: config.options,
118
- }),
119
- };
120
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * Generate a CloudFront signed URL using a canned policy, implemented natively for Cloudflare Workers.
3
- *
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
- * ```
26
- */
27
- export async function getCloudFrontSignedUrl(
28
- url: string,
29
- privateKeyPem: string,
30
- keyPairId: string,
31
- dateLessThan: string | number | Date,
32
- ): Promise<string> {
33
- const epochSeconds = Math.round(new Date(dateLessThan).getTime() / 1000);
34
-
35
- const policy = JSON.stringify({
36
- Statement: [{ Resource: url, Condition: { DateLessThan: { 'AWS:EpochTime': epochSeconds } } }],
37
- });
38
-
39
- const key = await crypto.subtle.importKey(
40
- 'pkcs8',
41
- pemToDer(privateKeyPem),
42
- { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-1' },
43
- false,
44
- ['sign'],
45
- );
46
- const signatureBuffer = await crypto.subtle.sign(
47
- { name: 'RSASSA-PKCS1-v1_5' },
48
- key,
49
- new TextEncoder().encode(policy),
50
- );
51
-
52
- const signature = toUrlSafeBase64(arrayBufferToBase64(signatureBuffer));
53
- const separator = url.includes('?') ? '&' : '?';
54
-
55
- // Query order used by @aws-sdk/cloudfront-signer: Expires -> Key-Pair-Id -> Signature
56
- return `${url}${separator}Expires=${epochSeconds}&Key-Pair-Id=${keyPairId}&Signature=${signature}`;
57
- }
58
-
59
- /**
60
- * Convert standard base64 to the URL-safe alphabet expected in CloudFront signatures.
61
- *
62
- * @param value - A standard base64 string.
63
- * @returns The base64 string with `+` -> `-`, `=` -> `_`, and `/` -> `~`.
64
- * @internal
65
- */
66
-
67
- function toUrlSafeBase64(value: string): string {
68
- return value.replace(/\+/g, '-').replace(/=/g, '_').replace(/\//g, '~');
69
- }
70
-
71
- /**
72
- * Encode an `ArrayBuffer` to standard base64.
73
- *
74
- * @param buffer - The raw bytes to encode.
75
- * @returns The standard base64 representation of the buffer.
76
- * @internal
77
- */
78
- function arrayBufferToBase64(buffer: ArrayBuffer): string {
79
- const bytes = new Uint8Array(buffer);
80
- let binary = '';
81
- for (const b of bytes) {
82
- binary += String.fromCharCode(b);
83
- }
84
- return btoa(binary);
85
- }
86
-
87
- /**
88
- * Decode a PKCS#8 PEM private key into its DER `ArrayBuffer`.
89
- *
90
- * @param pem - The PEM-encoded key, including the BEGIN/END armor.
91
- * @returns The decoded DER bytes, suitable for `crypto.subtle.importKey('pkcs8', ...)`.
92
- * @internal
93
- */
94
- function pemToDer(pem: string): ArrayBuffer {
95
- const base64 = pem
96
- .replace(/-----BEGIN [^-]+-----/, '')
97
- .replace(/-----END [^-]+-----/, '')
98
- .replace(/\s+/g, '');
99
- const binary = atob(base64);
100
- const bytes = new Uint8Array(binary.length);
101
- for (let i = 0; i < binary.length; i++) {
102
- bytes[i] = binary.charCodeAt(i);
103
- }
104
- return bytes.buffer;
105
- }
@@ -1,112 +0,0 @@
1
- import { AwsClient } from 'aws4fetch';
2
-
3
- /**
4
- * AWS credentials used to sign Secrets Manager requests.
5
- *
6
- * @remarks
7
- * Cloudflare Workers have neither the AWS SDK nor IAM role credentials, so static AWS keys are supplied
8
- * as Workers secrets and used to produce a SigV4 signature.
9
- */
10
- export interface AwsSecretsOptions {
11
- /** AWS access key ID. */
12
- accessKeyId: string;
13
- /** AWS secret access key. */
14
- secretAccessKey: string;
15
- /** Optional STS session token, required when using temporary credentials. */
16
- sessionToken?: string;
17
- /** AWS region of the Secrets Manager endpoint, e.g. `ap-northeast-1`. */
18
- region: string;
19
- }
20
-
21
- /**
22
- * Per-isolate cache for the fetched secret.
23
- *
24
- * @remarks
25
- * Secrets Manager is queried at most once per isolate. The entry is keyed by
26
- * `region:accessKeyId:secretId`, so rotating credentials triggers a fresh fetch. The in-flight promise
27
- * itself is cached so that concurrent first-time callers share a single request. On rejection the cache
28
- * is cleared so a failed fetch can be retried.
29
- *
30
- * @internal
31
- */
32
- let cache: { key: string; value: Promise<unknown> } | null = null;
33
-
34
- /**
35
- * Fetch and parse a secret from AWS Secrets Manager, caching the result per isolate.
36
- *
37
- * Issues a `GetSecretValue` call to Secrets Manager via a SigV4-signed `fetch` (using aws4fetch), with no
38
- * AWS SDK involved. The parsed `SecretString` is cached per isolate keyed by region, access key ID, and
39
- * secret ID; concurrent first-time callers share one in-flight request, and a rejected fetch clears the
40
- * cache entry so the next call retries.
41
- *
42
- * @typeParam T - The shape of the JSON-parsed secret payload, supplied by the caller.
43
- * @param options - AWS credentials and region used to sign the request.
44
- * @param secretId - The Secrets Manager secret ID or ARN to retrieve.
45
- * @returns The parsed secret value cast to `T`.
46
- * @throws Error When the Secrets Manager response is not OK, or when it contains no `SecretString`.
47
- * @example
48
- * ```ts
49
- * interface DbSecret {
50
- * username: string;
51
- * password: string;
52
- * }
53
- *
54
- * const secret = await getAuthenticationSecret<DbSecret>(
55
- * {
56
- * accessKeyId: env.AWS_ACCESS_KEY_ID,
57
- * secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
58
- * region: 'ap-northeast-1',
59
- * },
60
- * 'prod/db/credentials',
61
- * );
62
- * ```
63
- */
64
- export function getAuthenticationSecret<T>(options: AwsSecretsOptions, secretId: string): Promise<T> {
65
- const key = `${options.region}:${options.accessKeyId}:${secretId}`;
66
- if (cache?.key !== key) {
67
- const value = fetchSecret(options, secretId).catch((error: unknown) => {
68
- cache = null;
69
- throw error;
70
- });
71
- cache = { key, value };
72
- }
73
- return cache.value as Promise<T>;
74
- }
75
-
76
- /**
77
- * Perform the SigV4-signed `GetSecretValue` request and parse the returned `SecretString`.
78
- *
79
- * @param options - AWS credentials and region used to sign the request.
80
- * @param secretId - The Secrets Manager secret ID or ARN to retrieve.
81
- * @returns The JSON-parsed secret payload.
82
- * @throws Error When the response is not OK, or when it contains no `SecretString`.
83
- * @internal
84
- */
85
- async function fetchSecret(options: AwsSecretsOptions, secretId: string): Promise<unknown> {
86
- const aws = new AwsClient({
87
- accessKeyId: options.accessKeyId,
88
- secretAccessKey: options.secretAccessKey,
89
- sessionToken: options.sessionToken,
90
- service: 'secretsmanager',
91
- region: options.region,
92
- });
93
-
94
- const response = await aws.fetch(`https://secretsmanager.${options.region}.amazonaws.com/`, {
95
- method: 'POST',
96
- headers: {
97
- 'Content-Type': 'application/x-amz-json-1.1',
98
- 'X-Amz-Target': 'secretsmanager.GetSecretValue',
99
- },
100
- body: JSON.stringify({ SecretId: secretId, VersionStage: 'AWSCURRENT' }),
101
- });
102
-
103
- if (!response.ok) {
104
- throw new Error(`Secrets Manager GetSecretValue failed: ${response.status} ${await response.text()}`);
105
- }
106
-
107
- const body = (await response.json()) as { SecretString?: string };
108
- if (!body.SecretString) {
109
- throw new Error('Secrets Manager GetSecretValue returned no SecretString');
110
- }
111
- return JSON.parse(body.SecretString);
112
- }