@geekmidas/constructs 10.0.0-alpha.102 → 10.0.0-alpha.104
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/dist/{HonoEndpointAdaptor-Cvhfj4gd.mjs → HonoEndpointAdaptor-RSvRnXSl.mjs} +17 -8
- package/dist/HonoEndpointAdaptor-RSvRnXSl.mjs.map +1 -0
- package/dist/{HonoEndpointAdaptor-C_FbrwlA.cjs → HonoEndpointAdaptor-eCKyEOl_.cjs} +19 -10
- package/dist/HonoEndpointAdaptor-eCKyEOl_.cjs.map +1 -0
- package/dist/adaptors/aws.cjs +31 -6
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts +8 -0
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts +8 -0
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +29 -4
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +1 -1
- package/dist/adaptors/hono.d.cts.map +1 -1
- package/dist/adaptors/hono.d.mts.map +1 -1
- package/dist/adaptors/hono.mjs +1 -1
- package/dist/adaptors/testing.cjs +15 -4
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +1 -1
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +1 -1
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +14 -3
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/credential-DYymbp_7.cjs +257 -0
- package/dist/credential-DYymbp_7.cjs.map +1 -0
- package/dist/credential-DfSAOOdY.mjs +222 -0
- package/dist/credential-DfSAOOdY.mjs.map +1 -0
- package/dist/credential.cjs +7 -155
- package/dist/credential.d.cts +2 -86
- package/dist/credential.d.mts +2 -86
- package/dist/credential.mjs +2 -152
- package/dist/external-api.cjs +3 -39
- package/dist/external-api.cjs.map +1 -1
- package/dist/external-api.d.cts +2 -2
- package/dist/external-api.d.mts +2 -2
- package/dist/external-api.mjs +1 -37
- package/dist/external-api.mjs.map +1 -1
- package/dist/fake-BcHTPz7N.d.mts +201 -0
- package/dist/fake-BcHTPz7N.d.mts.map +1 -0
- package/dist/fake-Dvion_Fh.d.cts +201 -0
- package/dist/fake-Dvion_Fh.d.cts.map +1 -0
- package/dist/{processAudits-DasOZjPl.cjs → rateLimit-BwHcMECR.cjs} +26 -5
- package/dist/rateLimit-BwHcMECR.cjs.map +1 -0
- package/dist/{processAudits-BRRVVHUB.mjs → rateLimit-DbOTsP01.mjs} +23 -2
- package/dist/rateLimit-DbOTsP01.mjs.map +1 -0
- package/package.json +31 -31
- package/dist/HonoEndpointAdaptor-C_FbrwlA.cjs.map +0 -1
- package/dist/HonoEndpointAdaptor-Cvhfj4gd.mjs.map +0 -1
- package/dist/credential.cjs.map +0 -1
- package/dist/credential.d.cts.map +0 -1
- package/dist/credential.d.mts.map +0 -1
- package/dist/credential.mjs.map +0 -1
- package/dist/external-api-MJ8id87V.d.mts +0 -105
- package/dist/external-api-MJ8id87V.d.mts.map +0 -1
- package/dist/external-api-O107yzZJ.d.cts +0 -105
- package/dist/external-api-O107yzZJ.d.cts.map +0 -1
- package/dist/processAudits-BRRVVHUB.mjs.map +0 -1
- package/dist/processAudits-DasOZjPl.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-api.mjs","names":[],"sources":["../src/external-api.ts"],"sourcesContent":["/**\n * `ExternalApi` — an HTTP API somebody else runs.\n *\n * Polar, Stripe, a payment gateway: nothing to provision, but an address that\n * changes by stage and credentials the provider issued for each one. Without a\n * construct every integration writes that switching by hand — an optional\n * `POLAR_URL`, a fake started somewhere, a client built in a service — and the\n * target cannot see any of it.\n *\n * ```ts\n * // constructs/polar.ts\n * export const polar = new ExternalApi('Polar', {\n * url: 'https://www.polaraccesslink.com',\n * credentials: z.object({ clientId: z.string(), clientSecret: z.string() }),\n * client: ({ url, credentials }) => new PolarClient(url, credentials),\n * });\n *\n * // test/fakes/polar.ts\n * export default fake.app(new Hono().post('/v3/oauth2/token', …), {\n * credentials: { clientId: 'fake', clientSecret: 'fake' },\n * });\n *\n * // in a handler — the real API deployed, the fake locally and in tests\n * .dependsOn([polar])\n * .handle(({ services }) => services.polar.exchangeCode(code))\n * ```\n *\n * Deployed, the URL is `url` — one for every stage, or one per stage name with\n * a `default` — and the credentials are the stage's `<ID>_CREDENTIALS`, one\n * JSON value read the way a `Credential`'s is. Locally and in tests both come\n * from the fake.\n *\n * **The construct never names its fake.** It is found by convention, at\n * `test/fakes/<id>.ts`, and read only by gkm and the generated test harness —\n * so a fake, and the recorded responses it answers from, cannot end up in a\n * deployed bundle however the bundler follows imports.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { Consumable } from './construct-interface';\nimport { readCredentials } from './credential';\nimport type { DeployedStage, LocalStage } from './stages';\nimport { traceClient } from './tracing';\n\n/** What a credentials schema yields. */\ntype Value<TSchema extends StandardSchemaV1> =\n\tStandardSchemaV1.InferOutput<TSchema>;\n\n/**\n * Where an external API answers: one URL for every stage, or one per stage\n * name with `default` for any stage not listed.\n *\n * Keyed by the stages `gkm.config.ts` declares once `.gkm/stages.d.ts` exists,\n * so a key that names no stage — `prodution` — is a type error rather than a\n * URL nothing reads. Without the file any key goes, as before.\n */\nexport type ExternalApiUrl =\n\t| string\n\t| (string extends LocalStage | DeployedStage\n\t\t\t? Readonly<Record<string, string>>\n\t\t\t: { readonly [S in LocalStage | DeployedStage]?: string } & {\n\t\t\t\t\treadonly default?: string;\n\t\t\t\t});\n\nexport interface ExternalApiOptions<TSchema extends StandardSchemaV1, TClient> {\n\t/**\n\t * Where it answers deployed: one URL for every stage, or one per stage\n\t * name, with `default` for any stage not listed.\n\t */\n\turl: ExternalApiUrl;\n\t/** What the stage's `<ID>_CREDENTIALS` must look like. */\n\tcredentials: TSchema;\n\t/** What a handler is handed, built from where it answers and the credentials. */\n\tclient: (connection: {\n\t\turl: string;\n\t\tcredentials: Value<TSchema>;\n\t}) => TClient | Promise<TClient>;\n}\n\nexport class ExternalApi<\n\tTName extends string = string,\n\tTSchema extends StandardSchemaV1 = StandardSchemaV1,\n\tTClient = unknown,\n> implements Consumable<TName, TClient>\n{\n\t// `Consumable` for the reason `Credential` gives: a client generic in\n\t// `TClient` cannot satisfy `Construct`'s conditional service type.\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, TClient>;\n\n\t/** Read by both `declare()` and `connect()`, so the two cannot drift. */\n\tprivate readonly keys: { url: string; credentials: string };\n\n\t/** The promise, so concurrent registrations share one client. */\n\tprivate connected?: Promise<TClient>;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: ExternalApiOptions<TSchema, TClient>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.keys = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\tcredentials: provideKey(canonical, 'credentials'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => {\n\t\t\t\tthis.connected ??= this.connect(registerOptions);\n\t\t\t\treturn this.connected;\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * What the stage's `<ID>_CREDENTIALS` must look like — the schema the value\n\t * is validated against where it is read. Public so a tool setting the value\n\t * (`gkm secrets:add`) can check it, and describe its fields, before it is\n\t * stored rather than at the first request.\n\t */\n\tget credentialsSchema(): TSchema {\n\t\treturn this.options.credentials;\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'external-api',\n\t\t\t\tid: this.id,\n\t\t\t\t// The stage-keyed map is a record of URLs to the manifest, which\n\t\t\t\t// reads it by whatever stage it is resolving.\n\t\t\t\turl: this.options.url as Readonly<Record<string, string>> | string,\n\t\t\t\tprovides: [this.keys.url, this.keys.credentials],\n\t\t\t},\n\t\t];\n\t}\n\n\tprivate async connect(options: ServiceRegisterOptions): Promise<TClient> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\t\tconst credentials = await readCredentials(\n\t\t\tthis.id,\n\t\t\tthis.keys.credentials,\n\t\t\tthis.options.credentials,\n\t\t\toptions.envParser,\n\t\t);\n\n\t\tconst client = await this.options.client({ url, credentials });\n\t\t// Each call a span carrying this API's name — the parent of the fetch\n\t\t// it makes, which is traced already.\n\t\treturn traceClient(this.id, client, {\n\t\t\t'server.address': hostOf(url),\n\t\t});\n\t}\n}\n\nfunction hostOf(url: string): string {\n\ttry {\n\t\treturn new URL(url).host;\n\t} catch {\n\t\treturn url;\n\t}\n}\n\n/** Anything that answers a request — a Hono app, or a bare `{ fetch }`. */\nexport interface FetchHandler {\n\tfetch(request: Request): Response | Promise<Response>;\n}\n\n/**\n * Marks a fake. A registered symbol, so a fake built by a second copy of this\n * package — a linked workspace, two versions in a lockfile — is still one.\n */\nconst FAKE = Symbol.for('@geekmidas/constructs/fake');\n\n/** What an external API's credentials look like going in, for a fake to supply. */\ntype CredentialsOf<TApi> =\n\tTApi extends ExternalApi<string, infer TSchema, unknown>\n\t\t? StandardSchemaV1.InferInput<TSchema>\n\t\t: unknown;\n\n/**\n * An external API's stand-in, as `test/fakes/<id>.ts` default-exports it.\n *\n * `credentials` are what the fake accepts, and what a local or test stage is\n * handed as `<ID>_CREDENTIALS` in place of the stage's own.\n */\nexport type Fake = {\n\treadonly [FAKE]: true;\n\treadonly credentials: unknown;\n} & (\n\t| {\n\t\t\treadonly kind: 'app';\n\t\t\t/** Served by gkm: in-process in a test, on its own port in `gkm dev`. */\n\t\t\treadonly handler: FetchHandler;\n\t }\n\t| {\n\t\t\treadonly kind: 'image';\n\t\t\t/** The provider's own local server, run as a container. */\n\t\t\treadonly image: string;\n\t\t\t/** The port it listens on inside the container. */\n\t\t\treadonly port: number;\n\t }\n);\n\n/**\n * Build the fake `test/fakes/<id>.ts` default-exports.\n *\n * Pass the API's type to have the credentials checked against its schema —\n * `fake.app<typeof polar>(…)`.\n */\nexport const fake = {\n\t/** A working implementation of the API, kept beside the tests that use it. */\n\tapp<TApi = unknown>(\n\t\thandler: FetchHandler,\n\t\toptions: { credentials: CredentialsOf<TApi> },\n\t): Fake {\n\t\treturn {\n\t\t\t[FAKE]: true,\n\t\t\tkind: 'app',\n\t\t\thandler,\n\t\t\tcredentials: options.credentials,\n\t\t};\n\t},\n\n\t/** A local server the provider publishes — `stripe/stripe-mock`. */\n\timage<TApi = unknown>(\n\t\timage: string,\n\t\toptions: { port: number; credentials: CredentialsOf<TApi> },\n\t): Fake {\n\t\treturn {\n\t\t\t[FAKE]: true,\n\t\t\tkind: 'image',\n\t\t\timage,\n\t\t\tport: options.port,\n\t\t\tcredentials: options.credentials,\n\t\t};\n\t},\n};\n\n/** Whether a value is a fake — what gkm asks of a `test/fakes` default export. */\nexport function isFake(value: unknown): value is Fake {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t(value as { [FAKE]?: unknown })[FAKE] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFA,IAAa,cAAb,MAKA;CAcmB;CAXlB;CACA;;CAGA;;CAGA;CAEA,YACC,IACA,SACC;EADgB,KAAA,UAAA;EAEjB,MAAM,YAAY,YAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAO;GACX,KAAK,WAAW,WAAW,KAAK;GAChC,aAAa,WAAW,WAAW,aAAa;EACjD;EAGA,KAAK,UAAU;GACd,aAAa,WAAW,SAAS;GACjC,WAAW,oBAAoB;IAC9B,KAAK,cAAc,KAAK,QAAQ,eAAe;IAC/C,OAAO,KAAK;GACb;EACD;CACD;;;;;;;CAQA,IAAI,oBAA6B;EAChC,OAAO,KAAK,QAAQ;CACrB;CAEA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GAGT,KAAK,KAAK,QAAQ;GAClB,UAAU,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,WAAW;EAChD,CACD;CACD;CAEA,MAAc,QAAQ,SAAmD;EACxE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACvD,MAAM;EACR,MAAM,cAAc,MAAM,gBACzB,KAAK,IACL,KAAK,KAAK,aACV,KAAK,QAAQ,aACb,QAAQ,SACT;EAEA,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO;GAAE;GAAK;EAAY,CAAC;EAG7D,OAAO,YAAY,KAAK,IAAI,QAAQ,EACnC,kBAAkB,OAAO,GAAG,EAC7B,CAAC;CACF;AACD;AAEA,SAAS,OAAO,KAAqB;CACpC,IAAI;EACH,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACrB,QAAQ;EACP,OAAO;CACR;AACD;;;;;AAWA,MAAM,OAAO,OAAO,IAAI,4BAA4B;;;;;;;AAsCpD,MAAa,OAAO;;CAEnB,IACC,SACA,SACO;EACP,OAAO;IACL,OAAO;GACR,MAAM;GACN;GACA,aAAa,QAAQ;EACtB;CACD;;CAGA,MACC,OACA,SACO;EACP,OAAO;IACL,OAAO;GACR,MAAM;GACN;GACA,MAAM,QAAQ;GACd,aAAa,QAAQ;EACtB;CACD;AACD;;AAGA,SAAgB,OAAO,OAA+B;CACrD,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAA+B,UAAU;AAE5C"}
|
|
1
|
+
{"version":3,"file":"external-api.mjs","names":[],"sources":["../src/external-api.ts"],"sourcesContent":["/**\n * `ExternalApi` — an HTTP API somebody else runs.\n *\n * Polar, Stripe, a payment gateway: nothing to provision, but an address that\n * changes by stage and credentials the provider issued for each one. Without a\n * construct every integration writes that switching by hand — an optional\n * `POLAR_URL`, a fake started somewhere, a client built in a service — and the\n * target cannot see any of it.\n *\n * ```ts\n * // constructs/polar.ts\n * export const polar = new ExternalApi('Polar', {\n * url: 'https://www.polaraccesslink.com',\n * credentials: z.object({ clientId: z.string(), clientSecret: z.string() }),\n * client: ({ url, credentials }) => new PolarClient(url, credentials),\n * });\n *\n * // test/fakes/polar.ts\n * export default fake.app(new Hono().post('/v3/oauth2/token', …), {\n * credentials: { clientId: 'fake', clientSecret: 'fake' },\n * });\n *\n * // in a handler — the real API deployed, the fake locally and in tests\n * .dependsOn([polar])\n * .handle(({ services }) => services.polar.exchangeCode(code))\n * ```\n *\n * Deployed, the URL is `url` — one for every stage, or one per stage name with\n * a `default` — and the credentials are the stage's `<ID>_CREDENTIALS`, one\n * JSON value read the way a `Credential`'s is. Locally and in tests both come\n * from the fake.\n *\n * **The construct never names its fake.** It is found by convention, at\n * `test/fakes/<id>.ts`, and read only by gkm and the generated test harness —\n * so a fake, and the recorded responses it answers from, cannot end up in a\n * deployed bundle however the bundler follows imports.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { Consumable } from './construct-interface';\nimport { readCredentials } from './credential';\nimport type { DeployedStage, LocalStage } from './stages';\nimport { traceClient } from './tracing';\n\n/** What a credentials schema yields. */\ntype Value<TSchema extends StandardSchemaV1> =\n\tStandardSchemaV1.InferOutput<TSchema>;\n\n/**\n * Where an external API answers: one URL for every stage, or one per stage\n * name with `default` for any stage not listed.\n *\n * Keyed by the stages `gkm.config.ts` declares once `.gkm/stages.d.ts` exists,\n * so a key that names no stage — `prodution` — is a type error rather than a\n * URL nothing reads. Without the file any key goes, as before.\n */\nexport type ExternalApiUrl =\n\t| string\n\t| (string extends LocalStage | DeployedStage\n\t\t\t? Readonly<Record<string, string>>\n\t\t\t: { readonly [S in LocalStage | DeployedStage]?: string } & {\n\t\t\t\t\treadonly default?: string;\n\t\t\t\t});\n\nexport interface ExternalApiOptions<TSchema extends StandardSchemaV1, TClient> {\n\t/**\n\t * Where it answers deployed: one URL for every stage, or one per stage\n\t * name, with `default` for any stage not listed.\n\t */\n\turl: ExternalApiUrl;\n\t/** What the stage's `<ID>_CREDENTIALS` must look like. */\n\tcredentials: TSchema;\n\t/** What a handler is handed, built from where it answers and the credentials. */\n\tclient: (connection: {\n\t\turl: string;\n\t\tcredentials: Value<TSchema>;\n\t}) => TClient | Promise<TClient>;\n}\n\nexport class ExternalApi<\n\tTName extends string = string,\n\tTSchema extends StandardSchemaV1 = StandardSchemaV1,\n\tTClient = unknown,\n> implements Consumable<TName, TClient>\n{\n\t// `Consumable` for the reason `Credential` gives: a client generic in\n\t// `TClient` cannot satisfy `Construct`'s conditional service type.\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, TClient>;\n\n\t/** Read by both `declare()` and `connect()`, so the two cannot drift. */\n\tprivate readonly keys: { url: string; credentials: string };\n\n\t/** The promise, so concurrent registrations share one client. */\n\tprivate connected?: Promise<TClient>;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: ExternalApiOptions<TSchema, TClient>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.keys = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\tcredentials: provideKey(canonical, 'credentials'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => {\n\t\t\t\tthis.connected ??= this.connect(registerOptions);\n\t\t\t\treturn this.connected;\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * What the stage's `<ID>_CREDENTIALS` must look like — the schema the value\n\t * is validated against where it is read. Public so a tool setting the value\n\t * (`gkm secrets:add`) can check it, and describe its fields, before it is\n\t * stored rather than at the first request.\n\t */\n\tget credentialsSchema(): TSchema {\n\t\treturn this.options.credentials;\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'external-api',\n\t\t\t\tid: this.id,\n\t\t\t\t// The stage-keyed map is a record of URLs to the manifest, which\n\t\t\t\t// reads it by whatever stage it is resolving.\n\t\t\t\turl: this.options.url as Readonly<Record<string, string>> | string,\n\t\t\t\tprovides: [this.keys.url, this.keys.credentials],\n\t\t\t},\n\t\t];\n\t}\n\n\tprivate async connect(options: ServiceRegisterOptions): Promise<TClient> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\t\tconst credentials = await readCredentials(\n\t\t\tthis.id,\n\t\t\tthis.keys.credentials,\n\t\t\tthis.options.credentials,\n\t\t\toptions.envParser,\n\t\t);\n\n\t\tconst client = await this.options.client({ url, credentials });\n\t\t// Each call a span carrying this API's name — the parent of the fetch\n\t\t// it makes, which is traced already.\n\t\treturn traceClient(this.id, client, {\n\t\t\t'server.address': hostOf(url),\n\t\t});\n\t}\n}\n\nfunction hostOf(url: string): string {\n\ttry {\n\t\treturn new URL(url).host;\n\t} catch {\n\t\treturn url;\n\t}\n}\n\nexport {\n\ttype Fake,\n\ttype FetchHandler,\n\tfake,\n\tisFake,\n} from './fake';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFA,IAAa,cAAb,MAKA;CAcmB;CAXlB;CACA;;CAGA;;CAGA;CAEA,YACC,IACA,SACC;EADgB,KAAA,UAAA;EAEjB,MAAM,YAAY,YAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAO;GACX,KAAK,WAAW,WAAW,KAAK;GAChC,aAAa,WAAW,WAAW,aAAa;EACjD;EAGA,KAAK,UAAU;GACd,aAAa,WAAW,SAAS;GACjC,WAAW,oBAAoB;IAC9B,KAAK,cAAc,KAAK,QAAQ,eAAe;IAC/C,OAAO,KAAK;GACb;EACD;CACD;;;;;;;CAQA,IAAI,oBAA6B;EAChC,OAAO,KAAK,QAAQ;CACrB;CAEA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GAGT,KAAK,KAAK,QAAQ;GAClB,UAAU,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,WAAW;EAChD,CACD;CACD;CAEA,MAAc,QAAQ,SAAmD;EACxE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACvD,MAAM;EACR,MAAM,cAAc,MAAM,gBACzB,KAAK,IACL,KAAK,KAAK,aACV,KAAK,QAAQ,aACb,QAAQ,SACT;EAEA,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO;GAAE;GAAK;EAAY,CAAC;EAG7D,OAAO,YAAY,KAAK,IAAI,QAAQ,EACnC,kBAAkB,OAAO,GAAG,EAC7B,CAAC;CACF;AACD;AAEA,SAAS,OAAO,KAAqB;CACpC,IAAI;EACH,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACrB,QAAQ;EACP,OAAO;CACR;AACD"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { t as StandardSchemaV1 } from "./index-CeEtxDmK.mjs";
|
|
2
|
+
import { r as Consumable } from "./construct-interface-DluK3_da.mjs";
|
|
3
|
+
import { n as DeployedStage, r as LocalStage } from "./stages-BSCF19o2.mjs";
|
|
4
|
+
import { ConstructName, Declaration } from "@geekmidas/manifest";
|
|
5
|
+
import { Service, ServiceRegisterOptions } from "@geekmidas/services";
|
|
6
|
+
//#region src/credential.d.ts
|
|
7
|
+
/** What a credential's schema yields. */
|
|
8
|
+
type Value$1<TSchema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<TSchema>;
|
|
9
|
+
interface CredentialOptions<TSchema extends StandardSchemaV1> {
|
|
10
|
+
/**
|
|
11
|
+
* What the credential must look like.
|
|
12
|
+
*
|
|
13
|
+
* Validated at registration, so a malformed or half-set credential fails
|
|
14
|
+
* when the process starts rather than on the first request that needs the
|
|
15
|
+
* one field somebody forgot.
|
|
16
|
+
*/
|
|
17
|
+
schema: TSchema;
|
|
18
|
+
/**
|
|
19
|
+
* Re-resolve on every registration instead of once per process.
|
|
20
|
+
*
|
|
21
|
+
* Off by default: a credential is resolved once and reused, because the
|
|
22
|
+
* alternative is a fetch on every request. The cost of that default is that
|
|
23
|
+
* a rotated credential needs a restart, which is the right trade for a value
|
|
24
|
+
* that changes a few times a year — and this is the escape hatch for when it
|
|
25
|
+
* is not.
|
|
26
|
+
*/
|
|
27
|
+
refresh?: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare class Credential<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> implements Consumable<TName, Value$1<TSchema>> {
|
|
30
|
+
private readonly options;
|
|
31
|
+
readonly id: TName;
|
|
32
|
+
readonly service: Service<Uncapitalize<TName>, Value$1<TSchema>>;
|
|
33
|
+
/**
|
|
34
|
+
* Declared once and read by both `declare()` and `connect()`, so the key the
|
|
35
|
+
* target publishes and the key the client reads cannot drift.
|
|
36
|
+
*/
|
|
37
|
+
private readonly keys;
|
|
38
|
+
/**
|
|
39
|
+
* The resolved value, held as the *promise* rather than the value.
|
|
40
|
+
*
|
|
41
|
+
* Caching the promise is what makes two concurrent registrations share one
|
|
42
|
+
* resolution: caching the value would let both start before either finished,
|
|
43
|
+
* which is a duplicated fetch every cold start rather than never.
|
|
44
|
+
*/
|
|
45
|
+
private resolved?;
|
|
46
|
+
constructor(id: ConstructName<TName>, options: CredentialOptions<TSchema>);
|
|
47
|
+
/**
|
|
48
|
+
* What the stage's `<ID>_CREDENTIALS` must look like — the schema the value
|
|
49
|
+
* is validated against where it is read. Public so a tool setting the value
|
|
50
|
+
* (`gkm secrets:add`) can check it, and describe its fields, before it is
|
|
51
|
+
* stored rather than at the first request.
|
|
52
|
+
*/
|
|
53
|
+
get credentialsSchema(): TSchema;
|
|
54
|
+
declare(): Declaration[];
|
|
55
|
+
private connect;
|
|
56
|
+
private resolve;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A credentials key, read and validated against its schema.
|
|
60
|
+
*
|
|
61
|
+
* Shared with every construct that holds a third party's credentials — an
|
|
62
|
+
* `ExternalApi` reads its own the same way — so the shape a stage stores and
|
|
63
|
+
* the error a bad value raises are the same everywhere.
|
|
64
|
+
*
|
|
65
|
+
* @throws {MalformedCredential} when the value is not what the schema says.
|
|
66
|
+
*/
|
|
67
|
+
declare function readCredentials<TSchema extends StandardSchemaV1>(id: string, key: string, schema: TSchema, envParser: ServiceRegisterOptions['envParser']): Promise<Value$1<TSchema>>;
|
|
68
|
+
/**
|
|
69
|
+
* The raw environment value as the thing the schema should see.
|
|
70
|
+
*
|
|
71
|
+
* JSON when it is JSON, and the string itself when it is not — a credential
|
|
72
|
+
* with a single opaque field is ordinary, and requiring `"…"` around it would
|
|
73
|
+
* be ceremony that every operator setting one by hand would get wrong once.
|
|
74
|
+
*
|
|
75
|
+
* Exported so a tool checking a value before it is stored reads it exactly as
|
|
76
|
+
* the process that uses it will.
|
|
77
|
+
*/
|
|
78
|
+
declare function decodeCredentials(raw: string): unknown;
|
|
79
|
+
/** A credential was set, and is not what its schema says it should be. */
|
|
80
|
+
declare class MalformedCredential extends Error {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly key: string;
|
|
83
|
+
readonly issues: unknown;
|
|
84
|
+
constructor(id: string, key: string, issues: unknown);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/external-api.d.ts
|
|
88
|
+
/** What a credentials schema yields. */
|
|
89
|
+
type Value<TSchema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<TSchema>;
|
|
90
|
+
/**
|
|
91
|
+
* Where an external API answers: one URL for every stage, or one per stage
|
|
92
|
+
* name with `default` for any stage not listed.
|
|
93
|
+
*
|
|
94
|
+
* Keyed by the stages `gkm.config.ts` declares once `.gkm/stages.d.ts` exists,
|
|
95
|
+
* so a key that names no stage — `prodution` — is a type error rather than a
|
|
96
|
+
* URL nothing reads. Without the file any key goes, as before.
|
|
97
|
+
*/
|
|
98
|
+
type ExternalApiUrl = string | (string extends LocalStage | DeployedStage ? Readonly<Record<string, string>> : { readonly [S in LocalStage | DeployedStage]?: string; } & {
|
|
99
|
+
readonly default?: string;
|
|
100
|
+
});
|
|
101
|
+
interface ExternalApiOptions<TSchema extends StandardSchemaV1, TClient> {
|
|
102
|
+
/**
|
|
103
|
+
* Where it answers deployed: one URL for every stage, or one per stage
|
|
104
|
+
* name, with `default` for any stage not listed.
|
|
105
|
+
*/
|
|
106
|
+
url: ExternalApiUrl;
|
|
107
|
+
/** What the stage's `<ID>_CREDENTIALS` must look like. */
|
|
108
|
+
credentials: TSchema;
|
|
109
|
+
/** What a handler is handed, built from where it answers and the credentials. */
|
|
110
|
+
client: (connection: {
|
|
111
|
+
url: string;
|
|
112
|
+
credentials: Value<TSchema>;
|
|
113
|
+
}) => TClient | Promise<TClient>;
|
|
114
|
+
}
|
|
115
|
+
declare class ExternalApi<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1, TClient = unknown> implements Consumable<TName, TClient> {
|
|
116
|
+
private readonly options;
|
|
117
|
+
readonly id: TName;
|
|
118
|
+
readonly service: Service<Uncapitalize<TName>, TClient>;
|
|
119
|
+
/** Read by both `declare()` and `connect()`, so the two cannot drift. */
|
|
120
|
+
private readonly keys;
|
|
121
|
+
/** The promise, so concurrent registrations share one client. */
|
|
122
|
+
private connected?;
|
|
123
|
+
constructor(id: ConstructName<TName>, options: ExternalApiOptions<TSchema, TClient>);
|
|
124
|
+
/**
|
|
125
|
+
* What the stage's `<ID>_CREDENTIALS` must look like — the schema the value
|
|
126
|
+
* is validated against where it is read. Public so a tool setting the value
|
|
127
|
+
* (`gkm secrets:add`) can check it, and describe its fields, before it is
|
|
128
|
+
* stored rather than at the first request.
|
|
129
|
+
*/
|
|
130
|
+
get credentialsSchema(): TSchema;
|
|
131
|
+
declare(): Declaration[];
|
|
132
|
+
private connect;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/fake.d.ts
|
|
136
|
+
/**
|
|
137
|
+
* Marks a fake. A registered symbol, so a fake built by a second copy of this
|
|
138
|
+
* package — a linked workspace, two versions in a lockfile — is still one.
|
|
139
|
+
*/
|
|
140
|
+
declare const FAKE: unique symbol;
|
|
141
|
+
/** Anything that answers a request — a Hono app, or a bare `{ fetch }`. */
|
|
142
|
+
interface FetchHandler {
|
|
143
|
+
fetch(request: Request): Response | Promise<Response>;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* What a construct's credentials look like going in, for a fake to supply —
|
|
147
|
+
* an external API's `credentials` schema, or a credential's `schema`.
|
|
148
|
+
*/
|
|
149
|
+
type CredentialsOf<TConstruct> = TConstruct extends ExternalApi<string, infer TSchema, unknown> ? StandardSchemaV1.InferInput<TSchema> : TConstruct extends Credential<string, infer TSchema> ? StandardSchemaV1.InferInput<TSchema> : unknown;
|
|
150
|
+
/**
|
|
151
|
+
* A third party's stand-in, as `test/fakes/<id>.ts` default-exports it.
|
|
152
|
+
*
|
|
153
|
+
* `credentials` are what a local or test stage is handed as `<ID>_CREDENTIALS`
|
|
154
|
+
* in place of the stage's own: what an external API's fake accepts, or a
|
|
155
|
+
* credential's test value.
|
|
156
|
+
*/
|
|
157
|
+
type Fake = {
|
|
158
|
+
readonly [FAKE]: true;
|
|
159
|
+
readonly credentials: unknown;
|
|
160
|
+
} & ({
|
|
161
|
+
readonly kind: 'app';
|
|
162
|
+
/** Served by gkm: in-process in a test, on its own port in `gkm dev`. */
|
|
163
|
+
readonly handler: FetchHandler;
|
|
164
|
+
} | {
|
|
165
|
+
readonly kind: 'image';
|
|
166
|
+
/** The provider's own local server, run as a container. */
|
|
167
|
+
readonly image: string;
|
|
168
|
+
/** The port it listens on inside the container. */
|
|
169
|
+
readonly port: number;
|
|
170
|
+
} | {
|
|
171
|
+
/** A `Credential`'s test value — nothing to serve. */
|
|
172
|
+
readonly kind: 'credential';
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Build the fake `test/fakes/<id>.ts` default-exports.
|
|
176
|
+
*
|
|
177
|
+
* Pass the construct's type to have the value checked against its schema —
|
|
178
|
+
* `fake.app<typeof polar>(…)`, `fake.credential<typeof stripe>(…)`.
|
|
179
|
+
*/
|
|
180
|
+
declare const fake: {
|
|
181
|
+
/** A working implementation of an external API, kept beside the tests that use it. */
|
|
182
|
+
app<TApi = unknown>(handler: FetchHandler, options: {
|
|
183
|
+
credentials: CredentialsOf<TApi>;
|
|
184
|
+
}): Fake;
|
|
185
|
+
/** A local server an external API's provider publishes — `stripe/stripe-mock`. */
|
|
186
|
+
image<TApi = unknown>(image: string, options: {
|
|
187
|
+
port: number;
|
|
188
|
+
credentials: CredentialsOf<TApi>;
|
|
189
|
+
}): Fake;
|
|
190
|
+
/**
|
|
191
|
+
* A `Credential`'s value on the test stage and under `gkm dev --fake`, as
|
|
192
|
+
* its schema takes it — a string, or the object a JSON credential holds.
|
|
193
|
+
* It is validated where it is read, like the real one.
|
|
194
|
+
*/
|
|
195
|
+
credential<TCredential = unknown>(credentials: CredentialsOf<TCredential>): Fake;
|
|
196
|
+
};
|
|
197
|
+
/** Whether a value is a fake — what gkm asks of a `test/fakes` default export. */
|
|
198
|
+
declare function isFake(value: unknown): value is Fake;
|
|
199
|
+
//#endregion
|
|
200
|
+
export { ExternalApi as a, Credential as c, decodeCredentials as d, readCredentials as f, isFake as i, CredentialOptions as l, FetchHandler as n, ExternalApiOptions as o, fake as r, ExternalApiUrl as s, Fake as t, MalformedCredential as u };
|
|
201
|
+
//# sourceMappingURL=fake-BcHTPz7N.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-BcHTPz7N.d.mts","names":["Value"],"sources":["../src/credential.ts","../src/external-api.ts","../src/fake.ts"],"mappings":";;;;;;;KAiEKA,QAAM,gBAAgB,oBAC1B,iBAAiB,YAAY;UAEb,kBAAkB,gBAAgB;;;;;;;;EAQlD,QAAQ;;;;;;;;;;EAUR;;cAGY,WACZ,+BACA,gBAAgB,mBAAmB,6BACvB,WAAW,OAAOA,QAAM;mBA6BlB;WApBT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQA,QAAM;;;;;mBAMpC;;;;;;;;UAST;EAER,YACC,IAAI,cAAc,QACD,SAAS,kBAAkB;;;;;;;MAoBzC,qBAAqB;EAIzB,WAAW;UAUH;UAQA;;;;;;;;;;;iBAmBa,gBAAgB,gBAAgB,kBACrD,YACA,aACA,QAAQ,SACR,WAAW,sCACT,QAAQA,QAAM;;;;;;;;;;;iBAsBD,kBAAkB;;cAoBrB,4BAA4B;WAE9B;WACA;WACA;EAHV,YACU,YACA,aACA;;;;;KCpLN,MAAM,gBAAgB,oBAC1B,iBAAiB,YAAY;;;;;;;;;KAUlB,0CAEO,aAAa,gBAC3B,SAAS,sCACG,KAAK,aAAa;WACrB;;UAGG,mBAAmB,gBAAgB,kBAAkB;;;;;EAKrE,KAAK;;EAEL,aAAa;;EAEb,SAAS;IACR;IACA,aAAa,MAAM;QACd,UAAU,QAAQ;;cAGZ,YACZ,+BACA,gBAAgB,mBAAmB,kBACnC,8BACY,WAAW,OAAO;mBAeZ;WAXT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;;mBAG9B;;UAGT;EAER,YACC,IAAI,cAAc,QACD,SAAS,mBAAmB,SAAS;;;;;;;MA0BnD,qBAAqB;EAIzB,WAAW;UAaG;;;;;;;;cCjHT;;UAGW;EAChB,MAAM,SAAS,UAAU,WAAW,QAAQ;;;;;;KAOxC,cAAc,cAClB,mBAAmB,0BAA0B,oBAC1C,iBAAiB,WAAW,WAC5B,mBAAmB,yBAAyB,WAC3C,iBAAiB,WAAW;;;;;;;;KAUrB;YACD;WACD;;WAGE;;WAEA,SAAS;;WAGT;;WAEA;;WAEA;;;WAIA;;;;;;;;cAUC;;EAEZ,IAAI,gBAAc,SACR,cAAY;IACV,aAAa,cAAc;MACpC;;EAUH,MAAM,gBAAc,eACN;IACF;IAAc,aAAa,cAAc;MAClD;;;;;;EAeH,WAAW,uBAAqB,aAClB,cAAc,eACzB;;;iBAMY,OAAO,iBAAiB,SAAS"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { t as StandardSchemaV1 } from "./index-CeEtxDmK.cjs";
|
|
2
|
+
import { r as Consumable } from "./construct-interface-B1AfqRl4.cjs";
|
|
3
|
+
import { n as DeployedStage, r as LocalStage } from "./stages-BSCF19o2.cjs";
|
|
4
|
+
import { Service, ServiceRegisterOptions } from "@geekmidas/services";
|
|
5
|
+
import { ConstructName, Declaration } from "@geekmidas/manifest";
|
|
6
|
+
//#region src/credential.d.ts
|
|
7
|
+
/** What a credential's schema yields. */
|
|
8
|
+
type Value$1<TSchema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<TSchema>;
|
|
9
|
+
interface CredentialOptions<TSchema extends StandardSchemaV1> {
|
|
10
|
+
/**
|
|
11
|
+
* What the credential must look like.
|
|
12
|
+
*
|
|
13
|
+
* Validated at registration, so a malformed or half-set credential fails
|
|
14
|
+
* when the process starts rather than on the first request that needs the
|
|
15
|
+
* one field somebody forgot.
|
|
16
|
+
*/
|
|
17
|
+
schema: TSchema;
|
|
18
|
+
/**
|
|
19
|
+
* Re-resolve on every registration instead of once per process.
|
|
20
|
+
*
|
|
21
|
+
* Off by default: a credential is resolved once and reused, because the
|
|
22
|
+
* alternative is a fetch on every request. The cost of that default is that
|
|
23
|
+
* a rotated credential needs a restart, which is the right trade for a value
|
|
24
|
+
* that changes a few times a year — and this is the escape hatch for when it
|
|
25
|
+
* is not.
|
|
26
|
+
*/
|
|
27
|
+
refresh?: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare class Credential<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> implements Consumable<TName, Value$1<TSchema>> {
|
|
30
|
+
private readonly options;
|
|
31
|
+
readonly id: TName;
|
|
32
|
+
readonly service: Service<Uncapitalize<TName>, Value$1<TSchema>>;
|
|
33
|
+
/**
|
|
34
|
+
* Declared once and read by both `declare()` and `connect()`, so the key the
|
|
35
|
+
* target publishes and the key the client reads cannot drift.
|
|
36
|
+
*/
|
|
37
|
+
private readonly keys;
|
|
38
|
+
/**
|
|
39
|
+
* The resolved value, held as the *promise* rather than the value.
|
|
40
|
+
*
|
|
41
|
+
* Caching the promise is what makes two concurrent registrations share one
|
|
42
|
+
* resolution: caching the value would let both start before either finished,
|
|
43
|
+
* which is a duplicated fetch every cold start rather than never.
|
|
44
|
+
*/
|
|
45
|
+
private resolved?;
|
|
46
|
+
constructor(id: ConstructName<TName>, options: CredentialOptions<TSchema>);
|
|
47
|
+
/**
|
|
48
|
+
* What the stage's `<ID>_CREDENTIALS` must look like — the schema the value
|
|
49
|
+
* is validated against where it is read. Public so a tool setting the value
|
|
50
|
+
* (`gkm secrets:add`) can check it, and describe its fields, before it is
|
|
51
|
+
* stored rather than at the first request.
|
|
52
|
+
*/
|
|
53
|
+
get credentialsSchema(): TSchema;
|
|
54
|
+
declare(): Declaration[];
|
|
55
|
+
private connect;
|
|
56
|
+
private resolve;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A credentials key, read and validated against its schema.
|
|
60
|
+
*
|
|
61
|
+
* Shared with every construct that holds a third party's credentials — an
|
|
62
|
+
* `ExternalApi` reads its own the same way — so the shape a stage stores and
|
|
63
|
+
* the error a bad value raises are the same everywhere.
|
|
64
|
+
*
|
|
65
|
+
* @throws {MalformedCredential} when the value is not what the schema says.
|
|
66
|
+
*/
|
|
67
|
+
declare function readCredentials<TSchema extends StandardSchemaV1>(id: string, key: string, schema: TSchema, envParser: ServiceRegisterOptions['envParser']): Promise<Value$1<TSchema>>;
|
|
68
|
+
/**
|
|
69
|
+
* The raw environment value as the thing the schema should see.
|
|
70
|
+
*
|
|
71
|
+
* JSON when it is JSON, and the string itself when it is not — a credential
|
|
72
|
+
* with a single opaque field is ordinary, and requiring `"…"` around it would
|
|
73
|
+
* be ceremony that every operator setting one by hand would get wrong once.
|
|
74
|
+
*
|
|
75
|
+
* Exported so a tool checking a value before it is stored reads it exactly as
|
|
76
|
+
* the process that uses it will.
|
|
77
|
+
*/
|
|
78
|
+
declare function decodeCredentials(raw: string): unknown;
|
|
79
|
+
/** A credential was set, and is not what its schema says it should be. */
|
|
80
|
+
declare class MalformedCredential extends Error {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly key: string;
|
|
83
|
+
readonly issues: unknown;
|
|
84
|
+
constructor(id: string, key: string, issues: unknown);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/external-api.d.ts
|
|
88
|
+
/** What a credentials schema yields. */
|
|
89
|
+
type Value<TSchema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<TSchema>;
|
|
90
|
+
/**
|
|
91
|
+
* Where an external API answers: one URL for every stage, or one per stage
|
|
92
|
+
* name with `default` for any stage not listed.
|
|
93
|
+
*
|
|
94
|
+
* Keyed by the stages `gkm.config.ts` declares once `.gkm/stages.d.ts` exists,
|
|
95
|
+
* so a key that names no stage — `prodution` — is a type error rather than a
|
|
96
|
+
* URL nothing reads. Without the file any key goes, as before.
|
|
97
|
+
*/
|
|
98
|
+
type ExternalApiUrl = string | (string extends LocalStage | DeployedStage ? Readonly<Record<string, string>> : { readonly [S in LocalStage | DeployedStage]?: string; } & {
|
|
99
|
+
readonly default?: string;
|
|
100
|
+
});
|
|
101
|
+
interface ExternalApiOptions<TSchema extends StandardSchemaV1, TClient> {
|
|
102
|
+
/**
|
|
103
|
+
* Where it answers deployed: one URL for every stage, or one per stage
|
|
104
|
+
* name, with `default` for any stage not listed.
|
|
105
|
+
*/
|
|
106
|
+
url: ExternalApiUrl;
|
|
107
|
+
/** What the stage's `<ID>_CREDENTIALS` must look like. */
|
|
108
|
+
credentials: TSchema;
|
|
109
|
+
/** What a handler is handed, built from where it answers and the credentials. */
|
|
110
|
+
client: (connection: {
|
|
111
|
+
url: string;
|
|
112
|
+
credentials: Value<TSchema>;
|
|
113
|
+
}) => TClient | Promise<TClient>;
|
|
114
|
+
}
|
|
115
|
+
declare class ExternalApi<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1, TClient = unknown> implements Consumable<TName, TClient> {
|
|
116
|
+
private readonly options;
|
|
117
|
+
readonly id: TName;
|
|
118
|
+
readonly service: Service<Uncapitalize<TName>, TClient>;
|
|
119
|
+
/** Read by both `declare()` and `connect()`, so the two cannot drift. */
|
|
120
|
+
private readonly keys;
|
|
121
|
+
/** The promise, so concurrent registrations share one client. */
|
|
122
|
+
private connected?;
|
|
123
|
+
constructor(id: ConstructName<TName>, options: ExternalApiOptions<TSchema, TClient>);
|
|
124
|
+
/**
|
|
125
|
+
* What the stage's `<ID>_CREDENTIALS` must look like — the schema the value
|
|
126
|
+
* is validated against where it is read. Public so a tool setting the value
|
|
127
|
+
* (`gkm secrets:add`) can check it, and describe its fields, before it is
|
|
128
|
+
* stored rather than at the first request.
|
|
129
|
+
*/
|
|
130
|
+
get credentialsSchema(): TSchema;
|
|
131
|
+
declare(): Declaration[];
|
|
132
|
+
private connect;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/fake.d.ts
|
|
136
|
+
/**
|
|
137
|
+
* Marks a fake. A registered symbol, so a fake built by a second copy of this
|
|
138
|
+
* package — a linked workspace, two versions in a lockfile — is still one.
|
|
139
|
+
*/
|
|
140
|
+
declare const FAKE: unique symbol;
|
|
141
|
+
/** Anything that answers a request — a Hono app, or a bare `{ fetch }`. */
|
|
142
|
+
interface FetchHandler {
|
|
143
|
+
fetch(request: Request): Response | Promise<Response>;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* What a construct's credentials look like going in, for a fake to supply —
|
|
147
|
+
* an external API's `credentials` schema, or a credential's `schema`.
|
|
148
|
+
*/
|
|
149
|
+
type CredentialsOf<TConstruct> = TConstruct extends ExternalApi<string, infer TSchema, unknown> ? StandardSchemaV1.InferInput<TSchema> : TConstruct extends Credential<string, infer TSchema> ? StandardSchemaV1.InferInput<TSchema> : unknown;
|
|
150
|
+
/**
|
|
151
|
+
* A third party's stand-in, as `test/fakes/<id>.ts` default-exports it.
|
|
152
|
+
*
|
|
153
|
+
* `credentials` are what a local or test stage is handed as `<ID>_CREDENTIALS`
|
|
154
|
+
* in place of the stage's own: what an external API's fake accepts, or a
|
|
155
|
+
* credential's test value.
|
|
156
|
+
*/
|
|
157
|
+
type Fake = {
|
|
158
|
+
readonly [FAKE]: true;
|
|
159
|
+
readonly credentials: unknown;
|
|
160
|
+
} & ({
|
|
161
|
+
readonly kind: 'app';
|
|
162
|
+
/** Served by gkm: in-process in a test, on its own port in `gkm dev`. */
|
|
163
|
+
readonly handler: FetchHandler;
|
|
164
|
+
} | {
|
|
165
|
+
readonly kind: 'image';
|
|
166
|
+
/** The provider's own local server, run as a container. */
|
|
167
|
+
readonly image: string;
|
|
168
|
+
/** The port it listens on inside the container. */
|
|
169
|
+
readonly port: number;
|
|
170
|
+
} | {
|
|
171
|
+
/** A `Credential`'s test value — nothing to serve. */
|
|
172
|
+
readonly kind: 'credential';
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Build the fake `test/fakes/<id>.ts` default-exports.
|
|
176
|
+
*
|
|
177
|
+
* Pass the construct's type to have the value checked against its schema —
|
|
178
|
+
* `fake.app<typeof polar>(…)`, `fake.credential<typeof stripe>(…)`.
|
|
179
|
+
*/
|
|
180
|
+
declare const fake: {
|
|
181
|
+
/** A working implementation of an external API, kept beside the tests that use it. */
|
|
182
|
+
app<TApi = unknown>(handler: FetchHandler, options: {
|
|
183
|
+
credentials: CredentialsOf<TApi>;
|
|
184
|
+
}): Fake;
|
|
185
|
+
/** A local server an external API's provider publishes — `stripe/stripe-mock`. */
|
|
186
|
+
image<TApi = unknown>(image: string, options: {
|
|
187
|
+
port: number;
|
|
188
|
+
credentials: CredentialsOf<TApi>;
|
|
189
|
+
}): Fake;
|
|
190
|
+
/**
|
|
191
|
+
* A `Credential`'s value on the test stage and under `gkm dev --fake`, as
|
|
192
|
+
* its schema takes it — a string, or the object a JSON credential holds.
|
|
193
|
+
* It is validated where it is read, like the real one.
|
|
194
|
+
*/
|
|
195
|
+
credential<TCredential = unknown>(credentials: CredentialsOf<TCredential>): Fake;
|
|
196
|
+
};
|
|
197
|
+
/** Whether a value is a fake — what gkm asks of a `test/fakes` default export. */
|
|
198
|
+
declare function isFake(value: unknown): value is Fake;
|
|
199
|
+
//#endregion
|
|
200
|
+
export { ExternalApi as a, Credential as c, decodeCredentials as d, readCredentials as f, isFake as i, CredentialOptions as l, FetchHandler as n, ExternalApiOptions as o, fake as r, ExternalApiUrl as s, Fake as t, MalformedCredential as u };
|
|
201
|
+
//# sourceMappingURL=fake-Dvion_Fh.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-Dvion_Fh.d.cts","names":["Value"],"sources":["../src/credential.ts","../src/external-api.ts","../src/fake.ts"],"mappings":";;;;;;;KAiEKA,QAAM,gBAAgB,oBAC1B,iBAAiB,YAAY;UAEb,kBAAkB,gBAAgB;;;;;;;;EAQlD,QAAQ;;;;;;;;;;EAUR;;cAGY,WACZ,+BACA,gBAAgB,mBAAmB,6BACvB,WAAW,OAAOA,QAAM;mBA6BlB;WApBT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQA,QAAM;;;;;mBAMpC;;;;;;;;UAST;EAER,YACC,IAAI,cAAc,QACD,SAAS,kBAAkB;;;;;;;MAoBzC,qBAAqB;EAIzB,WAAW;UAUH;UAQA;;;;;;;;;;;iBAmBa,gBAAgB,gBAAgB,kBACrD,YACA,aACA,QAAQ,SACR,WAAW,sCACT,QAAQA,QAAM;;;;;;;;;;;iBAsBD,kBAAkB;;cAoBrB,4BAA4B;WAE9B;WACA;WACA;EAHV,YACU,YACA,aACA;;;;;KCpLN,MAAM,gBAAgB,oBAC1B,iBAAiB,YAAY;;;;;;;;;KAUlB,0CAEO,aAAa,gBAC3B,SAAS,sCACG,KAAK,aAAa;WACrB;;UAGG,mBAAmB,gBAAgB,kBAAkB;;;;;EAKrE,KAAK;;EAEL,aAAa;;EAEb,SAAS;IACR;IACA,aAAa,MAAM;QACd,UAAU,QAAQ;;cAGZ,YACZ,+BACA,gBAAgB,mBAAmB,kBACnC,8BACY,WAAW,OAAO;mBAeZ;WAXT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;;mBAG9B;;UAGT;EAER,YACC,IAAI,cAAc,QACD,SAAS,mBAAmB,SAAS;;;;;;;MA0BnD,qBAAqB;EAIzB,WAAW;UAaG;;;;;;;;cCjHT;;UAGW;EAChB,MAAM,SAAS,UAAU,WAAW,QAAQ;;;;;;KAOxC,cAAc,cAClB,mBAAmB,0BAA0B,oBAC1C,iBAAiB,WAAW,WAC5B,mBAAmB,yBAAyB,WAC3C,iBAAiB,WAAW;;;;;;;;KAUrB;YACD;WACD;;WAGE;;WAEA,SAAS;;WAGT;;WAEA;;WAEA;;;WAIA;;;;;;;;cAUC;;EAEZ,IAAI,gBAAc,SACR,cAAY;IACV,aAAa,cAAc;MACpC;;EAUH,MAAM,gBAAc,eACN;IACF;IAAc,aAAa,cAAc;MAClD;;;;;;EAeH,WAAW,uBAAqB,aAClB,cAAc,eACzB;;;iBAMY,OAAO,iBAAiB,SAAS"}
|
|
@@ -126,22 +126,43 @@ async function executeWithAuditTransaction(auditContext, handler, onComplete, op
|
|
|
126
126
|
return response;
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
|
+
//#region src/endpoints/rateLimit.ts
|
|
130
|
+
/**
|
|
131
|
+
* An endpoint's `.rateLimit()`, checked the one way every adaptor checks it —
|
|
132
|
+
* Hono, Lambda and the test adaptor — after the request is authorized.
|
|
133
|
+
*
|
|
134
|
+
* Under the limit, it answers the headers the response carries:
|
|
135
|
+
* `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (and
|
|
136
|
+
* `Retry-After` on the last request the window allows).
|
|
137
|
+
*
|
|
138
|
+
* @throws {TooManyRequestsError} over the limit — `@geekmidas/errors`' 429,
|
|
139
|
+
* whose `headers` hold `Retry-After` (seconds until the window resets) and the
|
|
140
|
+
* same `X-RateLimit-*`, which every adaptor sets on the error response.
|
|
141
|
+
*/
|
|
142
|
+
async function enforceRateLimit(config, ctx) {
|
|
143
|
+
const { checkRateLimit, getRateLimitHeaders } = await loadRateLimit();
|
|
144
|
+
const info = await checkRateLimit(config, ctx);
|
|
145
|
+
const headers = {};
|
|
146
|
+
for (const [key, value] of Object.entries(getRateLimitHeaders(info, config))) if (value !== void 0) headers[key] = value;
|
|
147
|
+
return headers;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
129
150
|
Object.defineProperty(exports, "createAuditContext", {
|
|
130
151
|
enumerable: true,
|
|
131
152
|
get: function() {
|
|
132
153
|
return createAuditContext;
|
|
133
154
|
}
|
|
134
155
|
});
|
|
135
|
-
Object.defineProperty(exports, "
|
|
156
|
+
Object.defineProperty(exports, "enforceRateLimit", {
|
|
136
157
|
enumerable: true,
|
|
137
158
|
get: function() {
|
|
138
|
-
return
|
|
159
|
+
return enforceRateLimit;
|
|
139
160
|
}
|
|
140
161
|
});
|
|
141
|
-
Object.defineProperty(exports, "
|
|
162
|
+
Object.defineProperty(exports, "executeWithAuditTransaction", {
|
|
142
163
|
enumerable: true,
|
|
143
164
|
get: function() {
|
|
144
|
-
return
|
|
165
|
+
return executeWithAuditTransaction;
|
|
145
166
|
}
|
|
146
167
|
});
|
|
147
168
|
Object.defineProperty(exports, "loadRls", {
|
|
@@ -151,4 +172,4 @@ Object.defineProperty(exports, "loadRls", {
|
|
|
151
172
|
}
|
|
152
173
|
});
|
|
153
174
|
|
|
154
|
-
//# sourceMappingURL=
|
|
175
|
+
//# sourceMappingURL=rateLimit-BwHcMECR.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimit-BwHcMECR.cjs","names":[],"sources":["../src/endpoints/optionalPeers.ts","../src/endpoints/processAudits.ts","../src/endpoints/rateLimit.ts"],"sourcesContent":["/**\n * The optional peers an endpoint needs only for a feature it opted into.\n *\n * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional\n * peers of this package: an app that audits nothing, limits nothing and sets\n * no row-level security need not install them. The adaptor every endpoint is\n * served by used to import them at the top, so serving any endpoint needed\n * them all — and a production bundle of an app without them failed to build.\n * They are loaded here, when an endpoint that uses one is served, which is\n * also when the app declaring that endpoint has installed it: an auditor's\n * storage, a rate limit's store and an RLS policy's database come from these\n * packages.\n *\n * The `try` is load-bearing beyond the error it names: a bundler leaves an\n * import it cannot resolve to run time when the import is guarded, and fails\n * the build when it is not.\n */\n\n/** An endpoint uses a feature whose optional peer is not installed. */\nexport class OptionalPeerMissing extends Error {\n\tconstructor(\n\t\treadonly packageName: string,\n\t\treadonly feature: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${feature} needs '${packageName}', which could not be loaded. ` +\n\t\t\t\t`Add it to the dependencies of the app that serves this endpoint.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'OptionalPeerMissing';\n\t}\n}\n\n/** `@geekmidas/audit`, for an endpoint with an auditor. */\nexport async function loadAudit(): Promise<typeof import('@geekmidas/audit')> {\n\ttry {\n\t\treturn await import('@geekmidas/audit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/audit',\n\t\t\t'An endpoint with an auditor',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */\nexport async function loadRateLimit(): Promise<\n\ttypeof import('@geekmidas/rate-limit')\n> {\n\ttry {\n\t\treturn await import('@geekmidas/rate-limit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/rate-limit',\n\t\t\t'An endpoint with a rate limit',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/db/rls`, for an endpoint with row-level security. */\nexport async function loadRls(): Promise<typeof import('@geekmidas/db/rls')> {\n\ttry {\n\t\treturn await import('@geekmidas/db/rls');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/db',\n\t\t\t'An endpoint with row-level security',\n\t\t\tcause,\n\t\t);\n\t}\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\nimport { loadAudit } from './optionalPeers';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { DefaultAuditor } = await loadAudit();\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst { DefaultAuditor } = await loadAudit();\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n","import type { RateLimitConfig, RateLimitContext } from '@geekmidas/rate-limit';\nimport { loadRateLimit } from './optionalPeers';\n\n/**\n * An endpoint's `.rateLimit()`, checked the one way every adaptor checks it —\n * Hono, Lambda and the test adaptor — after the request is authorized.\n *\n * Under the limit, it answers the headers the response carries:\n * `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (and\n * `Retry-After` on the last request the window allows).\n *\n * @throws {TooManyRequestsError} over the limit — `@geekmidas/errors`' 429,\n * whose `headers` hold `Retry-After` (seconds until the window resets) and the\n * same `X-RateLimit-*`, which every adaptor sets on the error response.\n */\nexport async function enforceRateLimit(\n\tconfig: RateLimitConfig,\n\tctx: RateLimitContext<any, any, any>,\n): Promise<Record<string, string>> {\n\tconst { checkRateLimit, getRateLimitHeaders } = await loadRateLimit();\n\tconst info = await checkRateLimit(config, ctx);\n\n\tconst headers: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(\n\t\tgetRateLimitHeaders(info, config),\n\t)) {\n\t\tif (value !== undefined) headers[key] = value;\n\t}\n\treturn headers;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,cAAyC,MAAM;CAEpC;CACA;CAFV,YACC,aACA,SACA,OACC;EACD,MACC,GAAG,QAAQ,UAAU,YAAY,iGAEjC,EAAE,MAAM,CACT;EARS,KAAA,cAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,eAAsB,YAAwD;CAC7E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,oBACA,+BACA,KACD;CACD;AACD;;AAGA,eAAsB,gBAEpB;CACD,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,yBACA,iCACA,KACD;CACD;AACD;;AAGA,eAAsB,UAAuD;CAC5E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,iBACA,uCACA,KACD;CACD;AACD;;;;;;;;;;;;;ACwIA,eAAsB,mBAarB,UAcA,kBACA,QACA,KAM2D;CAC3D,IAAI,CAAC,SAAS,uBACb;CAMD,MAAM,WAAU,MAHO,iBAAiB,SAAS,CAChD,SAAS,qBACV,CAAC,EAAA,CAEA,SAAS,sBAAsB;CAIhC,IAAI,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAS;CACvD,IAAI,SAAS,gBACZ,IAAI;EACH,QAAQ,MACP,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACD,CAAC;CACF,SAAS,OAAO;EACf,OAAO,MAAM,OAAgB,oCAAoC;CAClE;CAGD,MAAM,EAAE,mBAAmB,MAAM,UAAU;CAU3C,OAAO;EAAE,SAAA,IATW,eAA6B;GAChD;GACA;GACA,UAAU;IACT,UAAU,SAAS;IACnB,QAAQ,SAAS;GAClB;EACD,CAEe;EAAG;CAAQ;AAC3B;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,4BAOrB,cACA,SACA,YACA,SACa;CAEb,IAAI,CAAC,cACJ,OAAO,QAAQ,KAAA,CAAS;CAGzB,MAAM,EAAE,SAAS,YAAY;CAG7B,IAAI,QAAQ,iBAIX,OAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAM,WAAW,MAAM,QAAQ,OAAO;EAGtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;EAGnC,OAAO;CACR,GACA,SAAS,EACV;CAID,MAAM,WAAW,MAAM,QAAQ,OAAO;CAEtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;CAInC,MAAM,QAAQ,MAAM;CAEpB,OAAO;AACR;;;;;;;;;;;;;;;ACjWA,eAAsB,iBACrB,QACA,KACkC;CAClC,MAAM,EAAE,gBAAgB,wBAAwB,MAAM,cAAc;CACpE,MAAM,OAAO,MAAM,eAAe,QAAQ,GAAG;CAE7C,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QACjC,oBAAoB,MAAM,MAAM,CACjC,GACC,IAAI,UAAU,KAAA,GAAW,QAAQ,OAAO;CAEzC,OAAO;AACR"}
|
|
@@ -126,6 +126,27 @@ async function executeWithAuditTransaction(auditContext, handler, onComplete, op
|
|
|
126
126
|
return response;
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
|
-
|
|
129
|
+
//#region src/endpoints/rateLimit.ts
|
|
130
|
+
/**
|
|
131
|
+
* An endpoint's `.rateLimit()`, checked the one way every adaptor checks it —
|
|
132
|
+
* Hono, Lambda and the test adaptor — after the request is authorized.
|
|
133
|
+
*
|
|
134
|
+
* Under the limit, it answers the headers the response carries:
|
|
135
|
+
* `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (and
|
|
136
|
+
* `Retry-After` on the last request the window allows).
|
|
137
|
+
*
|
|
138
|
+
* @throws {TooManyRequestsError} over the limit — `@geekmidas/errors`' 429,
|
|
139
|
+
* whose `headers` hold `Retry-After` (seconds until the window resets) and the
|
|
140
|
+
* same `X-RateLimit-*`, which every adaptor sets on the error response.
|
|
141
|
+
*/
|
|
142
|
+
async function enforceRateLimit(config, ctx) {
|
|
143
|
+
const { checkRateLimit, getRateLimitHeaders } = await loadRateLimit();
|
|
144
|
+
const info = await checkRateLimit(config, ctx);
|
|
145
|
+
const headers = {};
|
|
146
|
+
for (const [key, value] of Object.entries(getRateLimitHeaders(info, config))) if (value !== void 0) headers[key] = value;
|
|
147
|
+
return headers;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { loadRls as i, createAuditContext as n, executeWithAuditTransaction as r, enforceRateLimit as t };
|
|
130
151
|
|
|
131
|
-
//# sourceMappingURL=
|
|
152
|
+
//# sourceMappingURL=rateLimit-DbOTsP01.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimit-DbOTsP01.mjs","names":[],"sources":["../src/endpoints/optionalPeers.ts","../src/endpoints/processAudits.ts","../src/endpoints/rateLimit.ts"],"sourcesContent":["/**\n * The optional peers an endpoint needs only for a feature it opted into.\n *\n * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional\n * peers of this package: an app that audits nothing, limits nothing and sets\n * no row-level security need not install them. The adaptor every endpoint is\n * served by used to import them at the top, so serving any endpoint needed\n * them all — and a production bundle of an app without them failed to build.\n * They are loaded here, when an endpoint that uses one is served, which is\n * also when the app declaring that endpoint has installed it: an auditor's\n * storage, a rate limit's store and an RLS policy's database come from these\n * packages.\n *\n * The `try` is load-bearing beyond the error it names: a bundler leaves an\n * import it cannot resolve to run time when the import is guarded, and fails\n * the build when it is not.\n */\n\n/** An endpoint uses a feature whose optional peer is not installed. */\nexport class OptionalPeerMissing extends Error {\n\tconstructor(\n\t\treadonly packageName: string,\n\t\treadonly feature: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${feature} needs '${packageName}', which could not be loaded. ` +\n\t\t\t\t`Add it to the dependencies of the app that serves this endpoint.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'OptionalPeerMissing';\n\t}\n}\n\n/** `@geekmidas/audit`, for an endpoint with an auditor. */\nexport async function loadAudit(): Promise<typeof import('@geekmidas/audit')> {\n\ttry {\n\t\treturn await import('@geekmidas/audit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/audit',\n\t\t\t'An endpoint with an auditor',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */\nexport async function loadRateLimit(): Promise<\n\ttypeof import('@geekmidas/rate-limit')\n> {\n\ttry {\n\t\treturn await import('@geekmidas/rate-limit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/rate-limit',\n\t\t\t'An endpoint with a rate limit',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/db/rls`, for an endpoint with row-level security. */\nexport async function loadRls(): Promise<typeof import('@geekmidas/db/rls')> {\n\ttry {\n\t\treturn await import('@geekmidas/db/rls');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/db',\n\t\t\t'An endpoint with row-level security',\n\t\t\tcause,\n\t\t);\n\t}\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\nimport { loadAudit } from './optionalPeers';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { DefaultAuditor } = await loadAudit();\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst { DefaultAuditor } = await loadAudit();\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n","import type { RateLimitConfig, RateLimitContext } from '@geekmidas/rate-limit';\nimport { loadRateLimit } from './optionalPeers';\n\n/**\n * An endpoint's `.rateLimit()`, checked the one way every adaptor checks it —\n * Hono, Lambda and the test adaptor — after the request is authorized.\n *\n * Under the limit, it answers the headers the response carries:\n * `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (and\n * `Retry-After` on the last request the window allows).\n *\n * @throws {TooManyRequestsError} over the limit — `@geekmidas/errors`' 429,\n * whose `headers` hold `Retry-After` (seconds until the window resets) and the\n * same `X-RateLimit-*`, which every adaptor sets on the error response.\n */\nexport async function enforceRateLimit(\n\tconfig: RateLimitConfig,\n\tctx: RateLimitContext<any, any, any>,\n): Promise<Record<string, string>> {\n\tconst { checkRateLimit, getRateLimitHeaders } = await loadRateLimit();\n\tconst info = await checkRateLimit(config, ctx);\n\n\tconst headers: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(\n\t\tgetRateLimitHeaders(info, config),\n\t)) {\n\t\tif (value !== undefined) headers[key] = value;\n\t}\n\treturn headers;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,cAAyC,MAAM;CAEpC;CACA;CAFV,YACC,aACA,SACA,OACC;EACD,MACC,GAAG,QAAQ,UAAU,YAAY,iGAEjC,EAAE,MAAM,CACT;EARS,KAAA,cAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,eAAsB,YAAwD;CAC7E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,oBACA,+BACA,KACD;CACD;AACD;;AAGA,eAAsB,gBAEpB;CACD,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,yBACA,iCACA,KACD;CACD;AACD;;AAGA,eAAsB,UAAuD;CAC5E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,iBACA,uCACA,KACD;CACD;AACD;;;;;;;;;;;;;ACwIA,eAAsB,mBAarB,UAcA,kBACA,QACA,KAM2D;CAC3D,IAAI,CAAC,SAAS,uBACb;CAMD,MAAM,WAAU,MAHO,iBAAiB,SAAS,CAChD,SAAS,qBACV,CAAC,EAAA,CAEA,SAAS,sBAAsB;CAIhC,IAAI,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAS;CACvD,IAAI,SAAS,gBACZ,IAAI;EACH,QAAQ,MACP,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACD,CAAC;CACF,SAAS,OAAO;EACf,OAAO,MAAM,OAAgB,oCAAoC;CAClE;CAGD,MAAM,EAAE,mBAAmB,MAAM,UAAU;CAU3C,OAAO;EAAE,SAAA,IATW,eAA6B;GAChD;GACA;GACA,UAAU;IACT,UAAU,SAAS;IACnB,QAAQ,SAAS;GAClB;EACD,CAEe;EAAG;CAAQ;AAC3B;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,4BAOrB,cACA,SACA,YACA,SACa;CAEb,IAAI,CAAC,cACJ,OAAO,QAAQ,KAAA,CAAS;CAGzB,MAAM,EAAE,SAAS,YAAY;CAG7B,IAAI,QAAQ,iBAIX,OAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAM,WAAW,MAAM,QAAQ,OAAO;EAGtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;EAGnC,OAAO;CACR,GACA,SAAS,EACV;CAID,MAAM,WAAW,MAAM,QAAQ,OAAO;CAEtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;CAInC,MAAM,QAAQ,MAAM;CAEpB,OAAO;AACR;;;;;;;;;;;;;;;ACjWA,eAAsB,iBACrB,QACA,KACkC;CAClC,MAAM,EAAE,gBAAgB,wBAAwB,MAAM,cAAc;CACpE,MAAM,OAAO,MAAM,eAAe,QAAQ,GAAG;CAE7C,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QACjC,oBAAoB,MAAM,MAAM,CACjC,GACC,IAAI,UAAU,KAAA,GAAW,QAAQ,OAAO;CAEzC,OAAO;AACR"}
|