@geekmidas/constructs 10.0.0-alpha.101 → 10.0.0-alpha.103

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 (100) hide show
  1. package/dist/{Endpoint-BG4UJ9oA.mjs → Endpoint-BW-nFdL_.mjs} +67 -3
  2. package/dist/Endpoint-BW-nFdL_.mjs.map +1 -0
  3. package/dist/{Endpoint-aCvBMPoH.d.cts → Endpoint-BWyKCa2x.d.cts} +47 -9
  4. package/dist/Endpoint-BWyKCa2x.d.cts.map +1 -0
  5. package/dist/{Endpoint-BoUg8ocM.cjs → Endpoint-B_UHlLxv.cjs} +78 -2
  6. package/dist/Endpoint-B_UHlLxv.cjs.map +1 -0
  7. package/dist/{Endpoint-Du-NwPX2.d.mts → Endpoint-C3r2OzOD.d.mts} +47 -9
  8. package/dist/Endpoint-C3r2OzOD.d.mts.map +1 -0
  9. package/dist/{EndpointFactory-BDtsrEvJ.mjs → EndpointFactory-Bbmx4vXc.mjs} +7 -2
  10. package/dist/EndpointFactory-Bbmx4vXc.mjs.map +1 -0
  11. package/dist/{EndpointFactory-I7s1co3Y.d.cts → EndpointFactory-C1-IBwm4.d.cts} +9 -4
  12. package/dist/EndpointFactory-C1-IBwm4.d.cts.map +1 -0
  13. package/dist/{EndpointFactory-DMYdJCOj.d.mts → EndpointFactory-C9eVhDKF.d.mts} +9 -4
  14. package/dist/EndpointFactory-C9eVhDKF.d.mts.map +1 -0
  15. package/dist/{EndpointFactory-DJ7gXLpR.cjs → EndpointFactory-DeZTvDjU.cjs} +7 -2
  16. package/dist/{EndpointFactory-BDtsrEvJ.mjs.map → EndpointFactory-DeZTvDjU.cjs.map} +1 -1
  17. package/dist/{HonoEndpointAdaptor-PtPlbeKY.mjs → HonoEndpointAdaptor-RSvRnXSl.mjs} +38 -14
  18. package/dist/HonoEndpointAdaptor-RSvRnXSl.mjs.map +1 -0
  19. package/dist/{HonoEndpointAdaptor-zVzVLJEO.cjs → HonoEndpointAdaptor-eCKyEOl_.cjs} +41 -17
  20. package/dist/HonoEndpointAdaptor-eCKyEOl_.cjs.map +1 -0
  21. package/dist/adaptors/aws.cjs +66 -27
  22. package/dist/adaptors/aws.cjs.map +1 -1
  23. package/dist/adaptors/aws.d.cts +23 -10
  24. package/dist/adaptors/aws.d.cts.map +1 -1
  25. package/dist/adaptors/aws.d.mts +23 -10
  26. package/dist/adaptors/aws.d.mts.map +1 -1
  27. package/dist/adaptors/aws.mjs +63 -24
  28. package/dist/adaptors/aws.mjs.map +1 -1
  29. package/dist/adaptors/hono.cjs +1 -1
  30. package/dist/adaptors/hono.d.cts +1 -1
  31. package/dist/adaptors/hono.d.cts.map +1 -1
  32. package/dist/adaptors/hono.d.mts +1 -1
  33. package/dist/adaptors/hono.d.mts.map +1 -1
  34. package/dist/adaptors/hono.mjs +1 -1
  35. package/dist/adaptors/testing.cjs +26 -13
  36. package/dist/adaptors/testing.cjs.map +1 -1
  37. package/dist/adaptors/testing.d.cts +2 -2
  38. package/dist/adaptors/testing.d.cts.map +1 -1
  39. package/dist/adaptors/testing.d.mts +2 -2
  40. package/dist/adaptors/testing.d.mts.map +1 -1
  41. package/dist/adaptors/testing.mjs +24 -11
  42. package/dist/adaptors/testing.mjs.map +1 -1
  43. package/dist/credential-DYymbp_7.cjs +257 -0
  44. package/dist/credential-DYymbp_7.cjs.map +1 -0
  45. package/dist/credential-DfSAOOdY.mjs +222 -0
  46. package/dist/credential-DfSAOOdY.mjs.map +1 -0
  47. package/dist/credential.cjs +7 -155
  48. package/dist/credential.d.cts +2 -86
  49. package/dist/credential.d.mts +2 -86
  50. package/dist/credential.mjs +2 -152
  51. package/dist/endpoints/index.cjs +15 -9
  52. package/dist/endpoints/index.d.cts +57 -3
  53. package/dist/endpoints/index.d.cts.map +1 -1
  54. package/dist/endpoints/index.d.mts +57 -3
  55. package/dist/endpoints/index.d.mts.map +1 -1
  56. package/dist/endpoints/index.mjs +4 -4
  57. package/dist/external-api.cjs +3 -39
  58. package/dist/external-api.cjs.map +1 -1
  59. package/dist/external-api.d.cts +2 -2
  60. package/dist/external-api.d.mts +2 -2
  61. package/dist/external-api.mjs +1 -37
  62. package/dist/external-api.mjs.map +1 -1
  63. package/dist/fake-BcHTPz7N.d.mts +201 -0
  64. package/dist/fake-BcHTPz7N.d.mts.map +1 -0
  65. package/dist/fake-Dvion_Fh.d.cts +201 -0
  66. package/dist/fake-Dvion_Fh.d.cts.map +1 -0
  67. package/dist/{processAudits-DasOZjPl.cjs → rateLimit-BwHcMECR.cjs} +26 -5
  68. package/dist/rateLimit-BwHcMECR.cjs.map +1 -0
  69. package/dist/{processAudits-BRRVVHUB.mjs → rateLimit-DbOTsP01.mjs} +23 -2
  70. package/dist/rateLimit-DbOTsP01.mjs.map +1 -0
  71. package/dist/{lazyAccessors-CFanua0O.mjs → readRequestBody-BTIl3k-D.mjs} +99 -2
  72. package/dist/readRequestBody-BTIl3k-D.mjs.map +1 -0
  73. package/dist/{lazyAccessors-hJKGv8Ay.cjs → readRequestBody-Dej_L3c4.cjs} +122 -1
  74. package/dist/readRequestBody-Dej_L3c4.cjs.map +1 -0
  75. package/dist/rest-api.cjs +1 -1
  76. package/dist/rest-api.d.cts +2 -2
  77. package/dist/rest-api.d.mts +2 -2
  78. package/dist/rest-api.mjs +1 -1
  79. package/package.json +31 -31
  80. package/dist/Endpoint-BG4UJ9oA.mjs.map +0 -1
  81. package/dist/Endpoint-BoUg8ocM.cjs.map +0 -1
  82. package/dist/Endpoint-Du-NwPX2.d.mts.map +0 -1
  83. package/dist/Endpoint-aCvBMPoH.d.cts.map +0 -1
  84. package/dist/EndpointFactory-DJ7gXLpR.cjs.map +0 -1
  85. package/dist/EndpointFactory-DMYdJCOj.d.mts.map +0 -1
  86. package/dist/EndpointFactory-I7s1co3Y.d.cts.map +0 -1
  87. package/dist/HonoEndpointAdaptor-PtPlbeKY.mjs.map +0 -1
  88. package/dist/HonoEndpointAdaptor-zVzVLJEO.cjs.map +0 -1
  89. package/dist/credential.cjs.map +0 -1
  90. package/dist/credential.d.cts.map +0 -1
  91. package/dist/credential.d.mts.map +0 -1
  92. package/dist/credential.mjs.map +0 -1
  93. package/dist/external-api-MJ8id87V.d.mts +0 -105
  94. package/dist/external-api-MJ8id87V.d.mts.map +0 -1
  95. package/dist/external-api-O107yzZJ.d.cts +0 -105
  96. package/dist/external-api-O107yzZJ.d.cts.map +0 -1
  97. package/dist/lazyAccessors-CFanua0O.mjs.map +0 -1
  98. package/dist/lazyAccessors-hJKGv8Ay.cjs.map +0 -1
  99. package/dist/processAudits-BRRVVHUB.mjs.map +0 -1
  100. package/dist/processAudits-DasOZjPl.cjs.map +0 -1
@@ -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, "executeWithAuditTransaction", {
156
+ Object.defineProperty(exports, "enforceRateLimit", {
136
157
  enumerable: true,
137
158
  get: function() {
138
- return executeWithAuditTransaction;
159
+ return enforceRateLimit;
139
160
  }
140
161
  });
141
- Object.defineProperty(exports, "loadRateLimit", {
162
+ Object.defineProperty(exports, "executeWithAuditTransaction", {
142
163
  enumerable: true,
143
164
  get: function() {
144
- return loadRateLimit;
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=processAudits-DasOZjPl.cjs.map
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
- export { loadRls as i, executeWithAuditTransaction as n, loadRateLimit as r, createAuditContext as t };
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=processAudits-BRRVVHUB.mjs.map
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"}
@@ -1,3 +1,4 @@
1
+ import { HttpError } from "@geekmidas/errors";
1
2
  import { getCookie } from "hono/cookie";
2
3
  //#region src/endpoints/lazyAccessors.ts
3
4
  /**
@@ -169,6 +170,102 @@ function createNoopCookies() {
169
170
  });
170
171
  }
171
172
  //#endregion
172
- export { createHonoHeaders as a, createObjectHeaders as c, createHonoCookies as i, createApiGatewayHeaders as n, createNoopCookies as o, createCookieHeaderAccessor as r, createNoopHeaders as s, createApiGatewayCookies as t };
173
+ //#region src/endpoints/readRequestBody.ts
174
+ /**
175
+ * The media types an endpoint's `.body()` schema can be handed — as written
176
+ * in the 415 a request in any other type is answered with.
177
+ */
178
+ const SUPPORTED_BODY_TYPES = [
179
+ "application/json",
180
+ "application/x-www-form-urlencoded",
181
+ "multipart/form-data",
182
+ "text/*"
183
+ ];
184
+ /**
185
+ * A request whose body is in a media type no endpoint reads — XML, a bare
186
+ * octet stream. Answered with a 415, before the body schema runs.
187
+ */
188
+ var UnsupportedRequestContentType = class extends HttpError {
189
+ contentType;
190
+ constructor(contentType) {
191
+ super(415, `A body sent as '${contentType}' can't be read. Send it as ${SUPPORTED_BODY_TYPES.join(", ")}.`, {
192
+ statusMessage: "Unsupported Media Type",
193
+ details: {
194
+ contentType,
195
+ supported: [...SUPPORTED_BODY_TYPES]
196
+ }
197
+ });
198
+ this.contentType = contentType;
199
+ this.name = "UnsupportedRequestContentType";
200
+ }
201
+ };
202
+ /**
203
+ * A body that says it is JSON and is not. Answered with a 400 — the request
204
+ * is broken, not merely invalid against the schema.
205
+ */
206
+ var MalformedRequestBody = class extends HttpError {
207
+ contentType;
208
+ constructor(contentType, cause) {
209
+ super(400, `The body says it is '${contentType}' but can't be parsed as that. Send a well-formed body, with the Content-Type it is written in.`, { cause });
210
+ this.contentType = contentType;
211
+ this.name = "MalformedRequestBody";
212
+ }
213
+ };
214
+ /**
215
+ * A request body, read by its Content-Type into what an endpoint's `.body()`
216
+ * schema validates — the one place every adaptor reads a body:
217
+ *
218
+ * - `application/json` (and `application/*+json`) — the parsed JSON.
219
+ * - `application/x-www-form-urlencoded` and `multipart/form-data` — a plain
220
+ * object of fields, as an HTML form posts them. A repeated field, or one
221
+ * named `field[]`, is an array; a file part is a `File`.
222
+ * - `text/*` — the text.
223
+ *
224
+ * Anything else is an {@link UnsupportedRequestContentType}.
225
+ */
226
+ async function readRequestBody(source, contentType, options) {
227
+ const mediaType = contentType?.split(";")[0]?.trim().toLowerCase();
228
+ if (!mediaType) {
229
+ if (options.untyped === "empty") return {};
230
+ return readJson(source, "application/json");
231
+ }
232
+ if (isJson(mediaType)) return readJson(source, mediaType);
233
+ if (mediaType === "application/x-www-form-urlencoded" || mediaType === "multipart/form-data") return formFields(await readForm(source, mediaType));
234
+ if (mediaType.startsWith("text/")) return source.text();
235
+ throw new UnsupportedRequestContentType(mediaType);
236
+ }
237
+ function isJson(mediaType) {
238
+ return mediaType === "application/json" || mediaType.startsWith("application/") && mediaType.endsWith("+json");
239
+ }
240
+ async function readJson(source, mediaType) {
241
+ try {
242
+ return await source.json();
243
+ } catch (error) {
244
+ throw new MalformedRequestBody(mediaType, error);
245
+ }
246
+ }
247
+ async function readForm(source, mediaType) {
248
+ try {
249
+ return await source.formData();
250
+ } catch (error) {
251
+ throw new MalformedRequestBody(mediaType, error);
252
+ }
253
+ }
254
+ /**
255
+ * A form's fields as an object, the way Hono's `parseBody({ all: true })`
256
+ * reads them. Without a prototype, so a field named `__proto__` is a field.
257
+ */
258
+ function formFields(form) {
259
+ const fields = Object.create(null);
260
+ for (const [key, value] of form.entries()) {
261
+ const existing = fields[key];
262
+ if (existing === void 0) fields[key] = key.endsWith("[]") ? [value] : value;
263
+ else if (Array.isArray(existing)) existing.push(value);
264
+ else fields[key] = [existing, value];
265
+ }
266
+ return fields;
267
+ }
268
+ //#endregion
269
+ export { createApiGatewayCookies as a, createHonoCookies as c, createNoopHeaders as d, createObjectHeaders as f, readRequestBody as i, createHonoHeaders as l, SUPPORTED_BODY_TYPES as n, createApiGatewayHeaders as o, UnsupportedRequestContentType as r, createCookieHeaderAccessor as s, MalformedRequestBody as t, createNoopCookies as u };
173
270
 
174
- //# sourceMappingURL=lazyAccessors-CFanua0O.mjs.map
271
+ //# sourceMappingURL=readRequestBody-BTIl3k-D.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readRequestBody-BTIl3k-D.mjs","names":[],"sources":["../src/endpoints/lazyAccessors.ts","../src/endpoints/readRequestBody.ts"],"sourcesContent":["/**\n * Lazy header and cookie accessors for different adaptors.\n *\n * Instead of parsing all headers/cookies upfront, these accessors\n * use native adaptor methods for single lookups and only parse\n * everything when `header()` or `cookie()` is called without arguments.\n */\nimport type { Context } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport type { CookieFn, HeaderFn } from './Endpoint';\n\n/**\n * Create a lazy header accessor for Hono.\n * Uses `c.req.header(name)` for single lookups (native, case-insensitive).\n * Only calls `c.req.header()` to get all headers when needed.\n */\nexport function createHonoHeaders(c: Context): HeaderFn {\n\tlet allHeaders: Record<string, string> | null = null;\n\n\treturn ((key?: string) => {\n\t\tif (key !== undefined) {\n\t\t\t// Single header lookup - use native Hono method (case-insensitive)\n\t\t\treturn c.req.header(key);\n\t\t}\n\t\t// Get all headers - cache the result\n\t\tif (!allHeaders) {\n\t\t\tallHeaders = c.req.header();\n\t\t}\n\t\treturn allHeaders;\n\t}) as HeaderFn;\n}\n\n/**\n * Create a lazy cookie accessor for Hono.\n * Uses `getCookie(c, name)` for single lookups (native).\n * Only parses the cookie header when all cookies are requested.\n */\nexport function createHonoCookies(c: Context): CookieFn {\n\tlet allCookies: Record<string, string> | null = null;\n\n\treturn ((name?: string) => {\n\t\tif (name !== undefined) {\n\t\t\t// Single cookie lookup - use native Hono method\n\t\t\treturn getCookie(c, name);\n\t\t}\n\t\t// Get all cookies - parse and cache\n\t\tif (!allCookies) {\n\t\t\tallCookies = {};\n\t\t\tconst cookieHeader = c.req.header('cookie');\n\t\t\tif (cookieHeader) {\n\t\t\t\tfor (const part of cookieHeader.split(';')) {\n\t\t\t\t\tconst trimmed = part.trim();\n\t\t\t\t\tconst eqIndex = trimmed.indexOf('=');\n\t\t\t\t\tif (eqIndex > 0) {\n\t\t\t\t\t\tconst key = trimmed.slice(0, eqIndex);\n\t\t\t\t\t\tconst value = trimmed.slice(eqIndex + 1);\n\t\t\t\t\t\tallCookies[key] = value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn allCookies;\n\t}) as CookieFn;\n}\n\n/**\n * Create a lazy header accessor for API Gateway events.\n * Handles case-insensitive lookups.\n */\nexport function createApiGatewayHeaders(\n\theaders: Record<string, string | undefined> | null | undefined,\n): HeaderFn {\n\tlet normalizedHeaders: Record<string, string> | null = null;\n\n\tconst normalize = () => {\n\t\tif (!normalizedHeaders) {\n\t\t\tnormalizedHeaders = {};\n\t\t\tif (headers) {\n\t\t\t\tfor (const [k, v] of Object.entries(headers)) {\n\t\t\t\t\tif (v !== undefined) {\n\t\t\t\t\t\tnormalizedHeaders[k.toLowerCase()] = v;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn normalizedHeaders;\n\t};\n\n\treturn ((key?: string) => {\n\t\tif (key !== undefined) {\n\t\t\t// Try direct lookup first (common case: headers already lowercase)\n\t\t\tif (headers) {\n\t\t\t\tconst direct = headers[key] ?? headers[key.toLowerCase()];\n\t\t\t\tif (direct !== undefined) return direct;\n\t\t\t\t// Fall back to normalized lookup\n\t\t\t\treturn normalize()[key.toLowerCase()];\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\t\treturn normalize();\n\t}) as HeaderFn;\n}\n\n/**\n * Create a lazy cookie accessor for API Gateway events.\n * Decodes URL-encoded values to match browser behavior.\n */\nexport function createApiGatewayCookies(\n\tcookies: string[] | undefined,\n\tcookieHeader: string | undefined,\n): CookieFn {\n\tlet parsed: Record<string, string> | null = null;\n\n\tconst parse = () => {\n\t\tif (!parsed) {\n\t\t\tparsed = {};\n\t\t\t// API Gateway v2 provides cookies as array\n\t\t\tif (cookies) {\n\t\t\t\tfor (const cookie of cookies) {\n\t\t\t\t\tconst eqIndex = cookie.indexOf('=');\n\t\t\t\t\tif (eqIndex > 0) {\n\t\t\t\t\t\tconst key = cookie.slice(0, eqIndex);\n\t\t\t\t\t\tconst value = cookie.slice(eqIndex + 1);\n\t\t\t\t\t\tparsed[key] = decodeURIComponent(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (cookieHeader) {\n\t\t\t\t// Fall back to parsing cookie header\n\t\t\t\tfor (const part of cookieHeader.split(';')) {\n\t\t\t\t\tconst trimmed = part.trim();\n\t\t\t\t\tconst eqIndex = trimmed.indexOf('=');\n\t\t\t\t\tif (eqIndex > 0) {\n\t\t\t\t\t\tconst key = trimmed.slice(0, eqIndex);\n\t\t\t\t\t\tconst value = trimmed.slice(eqIndex + 1);\n\t\t\t\t\t\tparsed[key] = decodeURIComponent(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn parsed;\n\t};\n\n\treturn ((name?: string) => {\n\t\tif (name !== undefined) {\n\t\t\treturn parse()[name];\n\t\t}\n\t\treturn parse();\n\t}) as CookieFn;\n}\n\n/**\n * Create a lazy header accessor for plain object headers (test adaptor, etc).\n * Headers are already available as an object, just provide lazy access.\n */\nexport function createObjectHeaders(\n\theaders: Record<string, string> | undefined,\n): HeaderFn {\n\tlet normalized: Record<string, string> | null = null;\n\n\treturn ((key?: string) => {\n\t\tif (key !== undefined) {\n\t\t\t// Try direct lookup first (common case)\n\t\t\tif (headers) {\n\t\t\t\tconst direct = headers[key] ?? headers[key.toLowerCase()];\n\t\t\t\tif (direct !== undefined) return direct;\n\t\t\t}\n\t\t\t// Fall back to normalized lookup for case-insensitivity\n\t\t\tif (!normalized && headers) {\n\t\t\t\tnormalized = {};\n\t\t\t\tfor (const [k, v] of Object.entries(headers)) {\n\t\t\t\t\tnormalized[k.toLowerCase()] = v;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn normalized?.[key.toLowerCase()];\n\t\t}\n\t\t// Return all headers\n\t\tif (!normalized && headers) {\n\t\t\tnormalized = {};\n\t\t\tfor (const [k, v] of Object.entries(headers)) {\n\t\t\t\tnormalized[k.toLowerCase()] = v;\n\t\t\t}\n\t\t}\n\t\treturn normalized ?? {};\n\t}) as HeaderFn;\n}\n\n/**\n * Create a lazy cookie accessor from a cookie header string.\n * Decodes URL-encoded values to match browser behavior.\n */\nexport function createCookieHeaderAccessor(\n\tcookieHeader: string | undefined,\n): CookieFn {\n\tlet parsed: Record<string, string> | null = null;\n\n\tconst parse = () => {\n\t\tif (!parsed) {\n\t\t\tparsed = {};\n\t\t\tif (cookieHeader) {\n\t\t\t\tfor (const part of cookieHeader.split(';')) {\n\t\t\t\t\tconst trimmed = part.trim();\n\t\t\t\t\tconst eqIndex = trimmed.indexOf('=');\n\t\t\t\t\tif (eqIndex > 0) {\n\t\t\t\t\t\tconst key = trimmed.slice(0, eqIndex);\n\t\t\t\t\t\tconst value = trimmed.slice(eqIndex + 1);\n\t\t\t\t\t\t// Decode URL-encoded values to match Endpoint.createCookies behavior\n\t\t\t\t\t\tparsed[key] = decodeURIComponent(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn parsed;\n\t};\n\n\treturn ((name?: string) => {\n\t\tif (name !== undefined) {\n\t\t\treturn parse()[name];\n\t\t}\n\t\treturn parse();\n\t}) as CookieFn;\n}\n\n/**\n * Create a no-op header accessor (for minimal endpoints that don't use headers).\n */\nexport function createNoopHeaders(): HeaderFn {\n\treturn ((key?: string) => {\n\t\tif (key !== undefined) return undefined;\n\t\treturn {};\n\t}) as HeaderFn;\n}\n\n/**\n * Create a no-op cookie accessor (for minimal endpoints that don't use cookies).\n */\nexport function createNoopCookies(): CookieFn {\n\treturn ((name?: string) => {\n\t\tif (name !== undefined) return undefined;\n\t\treturn {};\n\t}) as CookieFn;\n}\n","import { HttpError } from '@geekmidas/errors';\n\n/**\n * The media types an endpoint's `.body()` schema can be handed — as written\n * in the 415 a request in any other type is answered with.\n */\nexport const SUPPORTED_BODY_TYPES = [\n\t'application/json',\n\t'application/x-www-form-urlencoded',\n\t'multipart/form-data',\n\t'text/*',\n] as const;\n\n/**\n * A request whose body is in a media type no endpoint reads — XML, a bare\n * octet stream. Answered with a 415, before the body schema runs.\n */\nexport class UnsupportedRequestContentType extends HttpError {\n\tconstructor(readonly contentType: string) {\n\t\tsuper(\n\t\t\t415,\n\t\t\t`A body sent as '${contentType}' can't be read. Send it as ${SUPPORTED_BODY_TYPES.join(', ')}.`,\n\t\t\t{\n\t\t\t\tstatusMessage: 'Unsupported Media Type',\n\t\t\t\tdetails: { contentType, supported: [...SUPPORTED_BODY_TYPES] },\n\t\t\t},\n\t\t);\n\t\tthis.name = 'UnsupportedRequestContentType';\n\t}\n}\n\n/**\n * A body that says it is JSON and is not. Answered with a 400 — the request\n * is broken, not merely invalid against the schema.\n */\nexport class MalformedRequestBody extends HttpError {\n\tconstructor(\n\t\treadonly contentType: string,\n\t\tcause?: Error,\n\t) {\n\t\tsuper(\n\t\t\t400,\n\t\t\t`The body says it is '${contentType}' but can't be parsed as that. Send a well-formed body, with the Content-Type it is written in.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'MalformedRequestBody';\n\t}\n}\n\n/**\n * What a body is read from: a `Request`, or Hono's request — which caches\n * what it read, so a later reader is not handed a spent stream.\n */\nexport interface RequestBodySource {\n\tjson(): Promise<unknown>;\n\ttext(): Promise<string>;\n\tformData(): Promise<FormData>;\n}\n\nexport interface ReadRequestBodyOptions {\n\t/**\n\t * A body with no Content-Type: read as JSON (`'json'`, what API Gateway\n\t * has always done), or not read at all and handed to the schema as `{}`\n\t * (`'empty'`, what a Hono server has always done).\n\t */\n\tuntyped: 'json' | 'empty';\n}\n\n/**\n * A request body, read by its Content-Type into what an endpoint's `.body()`\n * schema validates — the one place every adaptor reads a body:\n *\n * - `application/json` (and `application/*+json`) — the parsed JSON.\n * - `application/x-www-form-urlencoded` and `multipart/form-data` — a plain\n * object of fields, as an HTML form posts them. A repeated field, or one\n * named `field[]`, is an array; a file part is a `File`.\n * - `text/*` — the text.\n *\n * Anything else is an {@link UnsupportedRequestContentType}.\n */\nexport async function readRequestBody(\n\tsource: RequestBodySource,\n\tcontentType: string | null | undefined,\n\toptions: ReadRequestBodyOptions,\n): Promise<unknown> {\n\tconst mediaType = contentType?.split(';')[0]?.trim().toLowerCase();\n\n\tif (!mediaType) {\n\t\tif (options.untyped === 'empty') return {};\n\t\treturn readJson(source, 'application/json');\n\t}\n\n\tif (isJson(mediaType)) return readJson(source, mediaType);\n\n\tif (\n\t\tmediaType === 'application/x-www-form-urlencoded' ||\n\t\tmediaType === 'multipart/form-data'\n\t) {\n\t\treturn formFields(await readForm(source, mediaType));\n\t}\n\n\tif (mediaType.startsWith('text/')) return source.text();\n\n\tthrow new UnsupportedRequestContentType(mediaType);\n}\n\nfunction isJson(mediaType: string): boolean {\n\treturn (\n\t\tmediaType === 'application/json' ||\n\t\t(mediaType.startsWith('application/') && mediaType.endsWith('+json'))\n\t);\n}\n\nasync function readJson(\n\tsource: RequestBodySource,\n\tmediaType: string,\n): Promise<unknown> {\n\ttry {\n\t\treturn await source.json();\n\t} catch (error) {\n\t\tthrow new MalformedRequestBody(mediaType, error as Error);\n\t}\n}\n\nasync function readForm(\n\tsource: RequestBodySource,\n\tmediaType: string,\n): Promise<FormData> {\n\ttry {\n\t\treturn await source.formData();\n\t} catch (error) {\n\t\tthrow new MalformedRequestBody(mediaType, error as Error);\n\t}\n}\n\n/** A form field's value: its text, or a `File` for a file part. */\ntype FormValue = NonNullable<ReturnType<FormData['get']>>;\n\n/**\n * A form's fields as an object, the way Hono's `parseBody({ all: true })`\n * reads them. Without a prototype, so a field named `__proto__` is a field.\n */\nfunction formFields(form: FormData): Record<string, FormValue | FormValue[]> {\n\tconst fields: Record<string, FormValue | FormValue[]> = Object.create(null);\n\n\tfor (const [key, value] of form.entries()) {\n\t\tconst existing = fields[key];\n\t\tif (existing === undefined) {\n\t\t\tfields[key] = key.endsWith('[]') ? [value] : value;\n\t\t} else if (Array.isArray(existing)) {\n\t\t\texisting.push(value);\n\t\t} else {\n\t\t\tfields[key] = [existing, value];\n\t\t}\n\t}\n\n\treturn fields;\n}\n"],"mappings":"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"}