@remit/backend 0.0.143 → 0.0.145

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 (279) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +5 -0
  4. package/dist/src/api.js +73 -0
  5. package/dist/src/auth.d.ts +24 -0
  6. package/dist/src/auth.js +81 -0
  7. package/dist/src/config/api-config.d.ts +20 -0
  8. package/dist/src/config/api-config.js +1 -0
  9. package/dist/src/config/from-env.d.ts +2 -0
  10. package/dist/src/config/from-env.js +65 -0
  11. package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
  12. package/dist/src/config/msoauth-secrets-manager.js +28 -0
  13. package/dist/src/config/msoauth.d.ts +19 -0
  14. package/dist/src/config/msoauth.js +6 -0
  15. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  16. package/dist/src/derive/autoMoved.js +49 -0
  17. package/dist/src/derive/contentSignature.d.ts +40 -0
  18. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
  19. package/dist/src/derive/contentUrl.d.ts +33 -0
  20. package/dist/src/derive/contentUrl.js +27 -0
  21. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  22. package/dist/src/derive/deriveMuted.js +7 -0
  23. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  24. package/dist/src/derive/enrichThreadRows.js +151 -0
  25. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  26. package/dist/src/derive/filterThreadCriteria.js +24 -0
  27. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  28. package/dist/src/derive/pendingMoveCounts.js +53 -0
  29. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  30. package/dist/src/derive/senderTrust.js +19 -0
  31. package/dist/src/error.d.ts +21 -0
  32. package/dist/src/error.js +85 -0
  33. package/dist/src/handlers/account-guards.d.ts +36 -0
  34. package/dist/src/handlers/account-guards.js +107 -0
  35. package/dist/src/handlers/account-oauth.d.ts +15 -0
  36. package/dist/src/handlers/account-oauth.js +334 -0
  37. package/dist/src/handlers/account-overrides.d.ts +53 -0
  38. package/dist/src/handlers/account-overrides.js +233 -0
  39. package/dist/src/handlers/account-signature.d.ts +28 -0
  40. package/dist/src/handlers/account-signature.js +85 -0
  41. package/dist/src/handlers/account.d.ts +7 -0
  42. package/dist/src/handlers/account.js +337 -0
  43. package/dist/src/handlers/address.d.ts +35 -0
  44. package/dist/src/handlers/address.js +133 -0
  45. package/dist/src/handlers/calendar-event.d.ts +70 -0
  46. package/dist/src/handlers/calendar-event.js +417 -0
  47. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  48. package/dist/src/handlers/calendar-feed.js +184 -0
  49. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  50. package/dist/src/handlers/calendar-suggestion.js +204 -0
  51. package/dist/src/handlers/calendar.d.ts +116 -0
  52. package/dist/src/handlers/calendar.js +323 -0
  53. package/dist/src/handlers/config.d.ts +43 -0
  54. package/dist/src/handlers/config.js +164 -0
  55. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  56. package/dist/src/handlers/ensure-account-config.js +27 -0
  57. package/dist/src/handlers/filter.d.ts +3 -0
  58. package/dist/src/handlers/filter.js +88 -0
  59. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  60. package/dist/src/handlers/folder-role-appointments.js +168 -0
  61. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  62. package/dist/src/handlers/folder-role-labels.js +40 -0
  63. package/dist/src/handlers/folder-role.d.ts +9 -0
  64. package/dist/src/handlers/folder-role.js +37 -0
  65. package/dist/src/handlers/index.d.ts +88 -0
  66. package/dist/src/handlers/index.js +62 -0
  67. package/dist/src/handlers/label.d.ts +43 -0
  68. package/dist/src/handlers/label.js +107 -0
  69. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  70. package/dist/src/handlers/mailbox-bindings.js +61 -0
  71. package/dist/src/handlers/mailbox.d.ts +23 -0
  72. package/dist/src/handlers/mailbox.js +211 -0
  73. package/dist/src/handlers/me.d.ts +2 -0
  74. package/dist/src/handlers/me.js +117 -0
  75. package/dist/src/handlers/message.d.ts +82 -0
  76. package/dist/src/handlers/message.js +595 -0
  77. package/dist/src/handlers/organize.d.ts +10 -0
  78. package/dist/src/handlers/organize.js +103 -0
  79. package/dist/src/handlers/outbox-attachment.d.ts +25 -0
  80. package/dist/src/handlers/outbox-attachment.js +77 -0
  81. package/dist/src/handlers/outbox.d.ts +4 -0
  82. package/dist/src/handlers/outbox.js +136 -0
  83. package/dist/src/handlers/sync-progress.d.ts +46 -0
  84. package/dist/src/handlers/sync-progress.js +63 -0
  85. package/dist/src/handlers/sync.d.ts +28 -0
  86. package/dist/src/handlers/sync.js +73 -0
  87. package/dist/src/handlers/system-update.d.ts +2 -0
  88. package/dist/src/handlers/system-update.js +159 -0
  89. package/dist/src/handlers/thread.d.ts +148 -0
  90. package/dist/src/handlers/thread.js +228 -0
  91. package/dist/src/handlers/unified-threads.d.ts +247 -0
  92. package/dist/src/handlers/unified-threads.js +406 -0
  93. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  94. package/dist/src/handlers/vip-suggestions.js +16 -0
  95. package/dist/src/index.d.ts +5 -0
  96. package/dist/src/index.js +53 -0
  97. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  98. package/dist/src/json.js +10 -0
  99. package/dist/src/jwt-auth.d.ts +15 -0
  100. package/dist/src/jwt-auth.js +45 -0
  101. package/dist/src/lambda.d.ts +13 -0
  102. package/dist/src/lambda.js +33 -0
  103. package/dist/src/request.d.ts +9 -0
  104. package/dist/src/request.js +17 -0
  105. package/dist/src/response.d.ts +44 -0
  106. package/dist/src/response.js +80 -0
  107. package/dist/src/serve.d.ts +31 -0
  108. package/dist/src/serve.js +117 -0
  109. package/dist/src/service/account-deletion.d.ts +20 -0
  110. package/dist/src/service/account-deletion.js +33 -0
  111. package/dist/src/service/compose-sqlite.d.ts +3 -0
  112. package/dist/src/service/compose-sqlite.js +13 -0
  113. package/dist/src/service/create-remit-client.d.ts +109 -0
  114. package/dist/src/service/create-remit-client.js +188 -0
  115. package/dist/src/service/data-client.d.ts +12 -0
  116. package/dist/src/service/data-client.js +42 -0
  117. package/dist/src/service/export-identity.d.ts +4 -0
  118. package/dist/src/service/export-identity.js +33 -0
  119. package/dist/src/service/fire-and-forget.d.ts +54 -0
  120. package/dist/src/service/fire-and-forget.js +37 -0
  121. package/dist/src/service/organize.d.ts +181 -0
  122. package/dist/src/service/organize.js +367 -0
  123. package/dist/src/service/queues.d.ts +12 -0
  124. package/dist/src/service/queues.js +1 -0
  125. package/dist/src/service/refusal.d.ts +4 -0
  126. package/dist/src/service/refusal.js +20 -0
  127. package/dist/src/service/sender-category.d.ts +51 -0
  128. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  129. package/dist/src/service/shared-deps.d.ts +6 -0
  130. package/dist/src/service/shared-deps.js +24 -0
  131. package/dist/src/service/trigger-sync.d.ts +57 -0
  132. package/dist/src/service/trigger-sync.js +31 -0
  133. package/dist/src/types.d.ts +72 -0
  134. package/dist/src/types.js +1 -0
  135. package/package.json +125 -98
  136. package/.env.test +0 -4
  137. package/dev-server/content-auth.test.ts +0 -113
  138. package/dev-server/content-auth.ts +0 -54
  139. package/dev-server/content-handler.test.ts +0 -105
  140. package/dev-server/content-handler.ts +0 -79
  141. package/dev-server/content-path.test.ts +0 -37
  142. package/dev-server/content-path.ts +0 -27
  143. package/dev-server/cors.test.ts +0 -48
  144. package/dev-server/cors.ts +0 -25
  145. package/dev-server/keep-alive.test.ts +0 -24
  146. package/dev-server/lambda-helpers.ts +0 -69
  147. package/dev-server/log-output.test.ts +0 -173
  148. package/dev-server/oauth-callback-route.test.ts +0 -66
  149. package/dev-server/relational-health.test.ts +0 -83
  150. package/dev-server/relational-health.ts +0 -39
  151. package/dev-server/server.ts +0 -384
  152. package/dev-server/sync-age.test.ts +0 -91
  153. package/dev-server/sync-age.ts +0 -55
  154. package/dev-server/upload-handler.ts +0 -132
  155. package/dev-server/upload-route.test.ts +0 -196
  156. package/scripts/backfill-classification.ts +0 -91
  157. package/scripts/backfill-list-id.ts +0 -83
  158. package/scripts/config-save.ts +0 -128
  159. package/src/auth.test.ts +0 -12
  160. package/src/auth.ts +0 -92
  161. package/src/config/msoauth.test.ts +0 -48
  162. package/src/config/msoauth.ts +0 -48
  163. package/src/data-backend.test.ts +0 -23
  164. package/src/data-backend.ts +0 -14
  165. package/src/derive/autoMoved.test.ts +0 -104
  166. package/src/derive/contentSignature.test.ts +0 -161
  167. package/src/derive/contentUrl.test.ts +0 -172
  168. package/src/derive/contentUrl.ts +0 -86
  169. package/src/derive/deriveMuted.test.ts +0 -34
  170. package/src/derive/enrichThreadRows.test.ts +0 -360
  171. package/src/derive/enrichThreadRows.ts +0 -234
  172. package/src/derive/filterThreadCriteria.test.ts +0 -126
  173. package/src/derive/filterThreadCriteria.ts +0 -51
  174. package/src/derive/senderTrust.test.ts +0 -67
  175. package/src/error.test.ts +0 -201
  176. package/src/error.ts +0 -123
  177. package/src/handlers/account-guards.test.ts +0 -67
  178. package/src/handlers/account-guards.ts +0 -169
  179. package/src/handlers/account-oauth-callback.test.ts +0 -155
  180. package/src/handlers/account-oauth-consent.test.ts +0 -336
  181. package/src/handlers/account-oauth-start.test.ts +0 -191
  182. package/src/handlers/account-oauth.test.ts +0 -439
  183. package/src/handlers/account-oauth.ts +0 -635
  184. package/src/handlers/account-overrides.test.ts +0 -88
  185. package/src/handlers/account-overrides.ts +0 -323
  186. package/src/handlers/account-ownership.ts +0 -25
  187. package/src/handlers/account-signature.ts +0 -117
  188. package/src/handlers/account-synced-services.test.ts +0 -245
  189. package/src/handlers/account.ts +0 -552
  190. package/src/handlers/address.test.ts +0 -381
  191. package/src/handlers/address.ts +0 -200
  192. package/src/handlers/calendar-event.test.ts +0 -685
  193. package/src/handlers/calendar-event.ts +0 -738
  194. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  195. package/src/handlers/calendar-feed.test.ts +0 -439
  196. package/src/handlers/calendar-feed.ts +0 -291
  197. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  198. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  199. package/src/handlers/calendar-suggestion.ts +0 -425
  200. package/src/handlers/calendar.test.ts +0 -1080
  201. package/src/handlers/calendar.ts +0 -517
  202. package/src/handlers/config.test.ts +0 -245
  203. package/src/handlers/config.ts +0 -332
  204. package/src/handlers/ensure-account-config.ts +0 -30
  205. package/src/handlers/filter.test.ts +0 -644
  206. package/src/handlers/filter.ts +0 -518
  207. package/src/handlers/folder-role-appointments.test.ts +0 -438
  208. package/src/handlers/folder-role-appointments.ts +0 -249
  209. package/src/handlers/folder-role-labels.ts +0 -63
  210. package/src/handlers/folder-role.test.ts +0 -209
  211. package/src/handlers/folder-role.ts +0 -80
  212. package/src/handlers/index.ts +0 -83
  213. package/src/handlers/label.test.ts +0 -112
  214. package/src/handlers/label.ts +0 -210
  215. package/src/handlers/mailbox-bindings.ts +0 -111
  216. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  217. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  218. package/src/handlers/mailbox.test.ts +0 -280
  219. package/src/handlers/mailbox.ts +0 -581
  220. package/src/handlers/me.test.ts +0 -81
  221. package/src/handlers/me.ts +0 -203
  222. package/src/handlers/message-read-backfill.test.ts +0 -103
  223. package/src/handlers/message.test.ts +0 -131
  224. package/src/handlers/message.ts +0 -1131
  225. package/src/handlers/organize.test.ts +0 -277
  226. package/src/handlers/organize.ts +0 -197
  227. package/src/handlers/outbox-attachment.test.ts +0 -765
  228. package/src/handlers/outbox-attachment.ts +0 -126
  229. package/src/handlers/outbox-list.test.ts +0 -179
  230. package/src/handlers/outbox.test.ts +0 -772
  231. package/src/handlers/outbox.ts +0 -258
  232. package/src/handlers/search.test.ts +0 -161
  233. package/src/handlers/search.ts +0 -105
  234. package/src/handlers/sync-progress.test.ts +0 -205
  235. package/src/handlers/sync-progress.ts +0 -99
  236. package/src/handlers/sync.test.ts +0 -168
  237. package/src/handlers/sync.ts +0 -118
  238. package/src/handlers/system-update.test.ts +0 -497
  239. package/src/handlers/system-update.ts +0 -230
  240. package/src/handlers/thread.test.ts +0 -396
  241. package/src/handlers/thread.ts +0 -427
  242. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  243. package/src/handlers/unified-threads.test.ts +0 -785
  244. package/src/handlers/unified-threads.ts +0 -669
  245. package/src/index.test.ts +0 -83
  246. package/src/index.ts +0 -228
  247. package/src/jwt-auth.test.ts +0 -152
  248. package/src/jwt-auth.ts +0 -132
  249. package/src/log-redaction.test.ts +0 -144
  250. package/src/openapi-spec.test.ts +0 -118
  251. package/src/request-context.test.ts +0 -61
  252. package/src/request-context.ts +0 -33
  253. package/src/request-pipeline.test.ts +0 -88
  254. package/src/request.ts +0 -10
  255. package/src/response.test.ts +0 -130
  256. package/src/response.ts +0 -144
  257. package/src/service/compose-sqlite.ts +0 -98
  258. package/src/service/config-import.ts +0 -39
  259. package/src/service/create-remit-client.ts +0 -522
  260. package/src/service/data-client.test.ts +0 -97
  261. package/src/service/data-client.ts +0 -58
  262. package/src/service/export-identity.ts +0 -39
  263. package/src/service/filter.ts +0 -40
  264. package/src/service/fire-and-forget.test.ts +0 -126
  265. package/src/service/fire-and-forget.ts +0 -79
  266. package/src/service/localhost-env-config.test.ts +0 -36
  267. package/src/service/organize-queue.ts +0 -98
  268. package/src/service/organize.test.ts +0 -1451
  269. package/src/service/organize.ts +0 -1025
  270. package/src/service/read-path-filter-config.test.ts +0 -243
  271. package/src/service/semantic-capability.test.ts +0 -78
  272. package/src/service/semantic-capability.ts +0 -95
  273. package/src/service/sender-category.test.ts +0 -235
  274. package/src/service/sqs.ts +0 -4
  275. package/src/service/trigger-sync.test.ts +0 -205
  276. package/src/service/trigger-sync.ts +0 -100
  277. package/src/stdout-capture-fixture.ts +0 -40
  278. package/src/types.ts +0 -271
  279. package/tsconfig.json +0 -7
@@ -1,144 +0,0 @@
1
- /**
2
- * Issue #1065: what a request is allowed to leave behind in the log.
3
- *
4
- * A calendar feed's token is in its path and is the whole credential, so the
5
- * path is a secret; a validation failure is logged at error level and the
6
- * request it carries holds the caller's Authorization header. Both are asserted
7
- * against captured stdout rather than by reading the call sites, because the
8
- * leak was a field bound two frames away from the handler that logged it.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import { after, before, describe, it } from "node:test";
13
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
14
- import { captureStdout, restoreStdout } from "./stdout-capture-fixture.js";
15
-
16
- process.env.LOG_LEVEL = "trace";
17
-
18
- const { mintCalendarFeedToken } = await import("@remit/calendar-service");
19
- const { _resetForTest, setClient } = await import("./service/data-client.js");
20
- const { createCalendarSqliteClient } = await import(
21
- "./handlers/calendar-sqlite-fixture.js"
22
- );
23
-
24
- let handler: (
25
- event: APIGatewayProxyEvent,
26
- context: unknown,
27
- ) => Promise<APIGatewayProxyResult>;
28
- let cleanup: () => void;
29
-
30
- const send = (
31
- event: Partial<APIGatewayProxyEvent> & { httpMethod: string; path: string },
32
- ): Promise<APIGatewayProxyResult> =>
33
- handler(
34
- {
35
- headers: {},
36
- queryStringParameters: null,
37
- body: null,
38
- requestContext: {},
39
- ...event,
40
- } as unknown as APIGatewayProxyEvent,
41
- { awsRequestId: "log-redaction", functionName: "test" },
42
- );
43
-
44
- before(async () => {
45
- _resetForTest();
46
- process.env.DATA_BACKEND = "sqlite";
47
- const fixture = await createCalendarSqliteClient();
48
- cleanup = fixture.cleanup;
49
- setClient(fixture.client);
50
- ({ handler } = (await import("./index.js")) as unknown as {
51
- handler: typeof handler;
52
- });
53
- });
54
-
55
- after(() => {
56
- _resetForTest();
57
- restoreStdout();
58
- cleanup();
59
- });
60
-
61
- describe("what a request leaves in the log", () => {
62
- it("never writes the feed token a subscriber polls with", async () => {
63
- const { token } = mintCalendarFeedToken();
64
-
65
- const { result: response, logged } = await captureStdout(() =>
66
- send({ httpMethod: "GET", path: `/feeds/calendar/${token}.ics` }),
67
- );
68
-
69
- assert.equal(
70
- response.statusCode,
71
- 404,
72
- "an unknown token, but still routed",
73
- );
74
- assert.ok(logged.length > 0, "the request has to have logged something");
75
- assert.equal(
76
- logged.includes(token),
77
- false,
78
- "the token in the path is the credential, so the path cannot be a log field",
79
- );
80
- assert.ok(
81
- logged.includes("/feeds/calendar/<redacted>.ics"),
82
- "which route was served is what an operator reads the log for",
83
- );
84
- });
85
-
86
- it("never writes the address a calendar subscribes to", async () => {
87
- const secret = "private-7c0ffee5eed";
88
- const { result: response, logged } = await captureStdout(() =>
89
- send({
90
- httpMethod: "POST",
91
- path: "/calendars",
92
- headers: { "Content-Type": "application/json" },
93
- body: JSON.stringify({
94
- urlSegment: "harbour",
95
- displayName: "Harbour",
96
- subscriptionUrl: `http://127.0.0.1:9/calendar/ical/${secret}/basic.ics`,
97
- }),
98
- requestContext: {
99
- authorizer: { claims: { sub: "log-redaction-subscriber" } },
100
- } as unknown as APIGatewayProxyEvent["requestContext"],
101
- }),
102
- );
103
-
104
- assert.equal(response.statusCode, 400, response.body);
105
- assert.match(response.body, /subscription_unreachable/);
106
- assert.equal(response.body.includes(secret), false);
107
- assert.ok(logged.length > 0, "the request has to have logged something");
108
- assert.equal(
109
- logged.includes(secret),
110
- false,
111
- "for a Google secret address the URL is the whole credential",
112
- );
113
- });
114
-
115
- it("never writes the credentials a failing request arrived with", async () => {
116
- // A session is already established, so the bearer is never verified and the
117
- // request reaches validation — where the whole parsed request, headers and
118
- // all, used to be logged. The delete refuses because it declares
119
- // `calendarId` as a required query parameter and none was sent.
120
- const { result: response, logged } = await captureStdout(() =>
121
- send({
122
- httpMethod: "DELETE",
123
- path: "/calendar-events/8f14e45f-ceea-467a-9c9e-9c9e9c9e9c9e",
124
- headers: {
125
- Authorization: "Bearer x",
126
- Cookie: "session=secret-cookie-value",
127
- },
128
- requestContext: {
129
- authorizer: { claims: { sub: "log-redaction-sub" } },
130
- } as unknown as APIGatewayProxyEvent["requestContext"],
131
- }),
132
- );
133
-
134
- assert.equal(response.statusCode, 400, response.body);
135
- assert.ok(logged.includes("Validation failed"));
136
- assert.equal(logged.includes("Bearer x"), false);
137
- assert.equal(logged.includes("secret-cookie-value"), false);
138
- assert.equal(
139
- logged.includes("parsedRequest"),
140
- false,
141
- "the request that failed is not a field; its errors are",
142
- );
143
- });
144
- });
@@ -1,118 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { OpenAPISpec } from "./index.js";
4
-
5
- type Parameter = {
6
- name: string;
7
- in: string;
8
- explode?: boolean;
9
- };
10
-
11
- const queryParameters = (): Array<{
12
- operationId: string;
13
- param: Parameter;
14
- }> => {
15
- const found: Array<{ operationId: string; param: Parameter }> = [];
16
- for (const item of Object.values(OpenAPISpec.paths ?? {})) {
17
- for (const operation of Object.values(item ?? {})) {
18
- const { operationId, parameters } = operation as {
19
- operationId?: string;
20
- parameters?: Parameter[];
21
- };
22
- if (!operationId || !parameters) continue;
23
- for (const param of parameters) {
24
- if (param.in === "query") found.push({ operationId, param });
25
- }
26
- }
27
- }
28
- return found;
29
- };
30
-
31
- type Schema = {
32
- $ref?: string;
33
- type?: string;
34
- required?: string[];
35
- properties?: Record<string, unknown>;
36
- };
37
-
38
- const resolve = (schema: Schema | undefined): Schema | undefined => {
39
- if (!schema?.$ref) return schema;
40
- const name = schema.$ref.replace("#/components/schemas/", "");
41
- return (OpenAPISpec.components?.schemas?.[name] ?? undefined) as
42
- | Schema
43
- | undefined;
44
- };
45
-
46
- const errorBodySchemas = (): Array<{
47
- operationId: string;
48
- status: string;
49
- schema: Schema | undefined;
50
- }> => {
51
- const found: Array<{
52
- operationId: string;
53
- status: string;
54
- schema: Schema | undefined;
55
- }> = [];
56
- for (const item of Object.values(OpenAPISpec.paths ?? {})) {
57
- for (const operation of Object.values(item ?? {})) {
58
- const { operationId, responses } = operation as {
59
- operationId?: string;
60
- responses?: Record<
61
- string,
62
- { content?: Record<string, { schema?: Schema }> }
63
- >;
64
- };
65
- if (!operationId || !responses) continue;
66
- for (const [status, response] of Object.entries(responses)) {
67
- if (Number(status) < 400) continue;
68
- const schema = response.content?.["application/json"]?.schema;
69
- if (!schema) continue;
70
- found.push({ operationId, status, schema: resolve(schema) });
71
- }
72
- }
73
- }
74
- return found;
75
- };
76
-
77
- // Issue #371: one error shape for the whole API. Every JSON refusal body is
78
- // flat — `code` and `message` at the top level, no `error` envelope — and
79
- // `code` is required, because it is the only field a client may branch on.
80
- describe("the declared shape of an error response", () => {
81
- it("is a flat object carrying a required code and message", () => {
82
- const offenders = errorBodySchemas()
83
- .filter(({ schema }) => {
84
- const required = schema?.required ?? [];
85
- return (
86
- schema?.type !== "object" ||
87
- !required.includes("code") ||
88
- !required.includes("message")
89
- );
90
- })
91
- .map(({ operationId, status }) => `${operationId}.${status}`);
92
-
93
- assert.deepEqual(offenders, []);
94
- });
95
-
96
- it("never wraps the body in an envelope property", () => {
97
- const offenders = errorBodySchemas()
98
- .filter(({ schema }) => schema?.properties?.error !== undefined)
99
- .map(({ operationId, status }) => `${operationId}.${status}`);
100
-
101
- assert.deepEqual(offenders, []);
102
- });
103
- });
104
-
105
- describe("the built OpenAPI spec", () => {
106
- it("declares every query parameter exploded", () => {
107
- // openapi-backend splits a non-exploded query value on commas before it
108
- // validates, so `?refresh=true` arrives at the validator as `["true"]` and
109
- // is refused as "must be boolean" — the handler never runs. Exploded is the
110
- // wire-identical form for a scalar, so every `@query` in main.tsp carries
111
- // `#{ explode: true }`.
112
- const offenders = queryParameters()
113
- .filter(({ param }) => param.explode === false)
114
- .map(({ operationId, param }) => `${operationId}.${param.name}`);
115
-
116
- assert.deepEqual(offenders, []);
117
- });
118
- });
@@ -1,61 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import {
4
- getRequestOrigin,
5
- resolveAllowedOrigin,
6
- runWithRequestContext,
7
- } from "./request-context.js";
8
-
9
- describe("request-context", () => {
10
- const originalEnv = process.env.CORS_ALLOWED_ORIGINS;
11
-
12
- afterEach(() => {
13
- if (originalEnv === undefined) {
14
- process.env.CORS_ALLOWED_ORIGINS = undefined;
15
- process.env.CORS_ALLOWED_ORIGINS = "";
16
- delete process.env.CORS_ALLOWED_ORIGINS;
17
- } else {
18
- process.env.CORS_ALLOWED_ORIGINS = originalEnv;
19
- }
20
- });
21
-
22
- it("stores and retrieves the request origin", () => {
23
- runWithRequestContext({ origin: "https://example.com" }, () => {
24
- assert.equal(getRequestOrigin(), "https://example.com");
25
- });
26
- });
27
-
28
- it("returns undefined when no origin is set", () => {
29
- runWithRequestContext({}, () => {
30
- assert.equal(getRequestOrigin(), undefined);
31
- });
32
- });
33
-
34
- it("returns star when no allow-list is configured", () => {
35
- delete process.env.CORS_ALLOWED_ORIGINS;
36
- assert.equal(resolveAllowedOrigin("https://any.com"), "*");
37
- });
38
-
39
- it("reflects the request origin when it is allow-listed", () => {
40
- process.env.CORS_ALLOWED_ORIGINS =
41
- "https://app.example.com,http://localhost:5173";
42
- assert.equal(
43
- resolveAllowedOrigin("http://localhost:5173"),
44
- "http://localhost:5173",
45
- );
46
- });
47
-
48
- it("falls back to the first allowed origin when the request origin is not allow-listed", () => {
49
- process.env.CORS_ALLOWED_ORIGINS =
50
- "https://app.example.com,http://localhost:5173";
51
- assert.equal(
52
- resolveAllowedOrigin("https://evil.example.com"),
53
- "https://app.example.com",
54
- );
55
- });
56
-
57
- it("falls back to the first allowed origin when no request origin is provided", () => {
58
- process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com";
59
- assert.equal(resolveAllowedOrigin(undefined), "https://app.example.com");
60
- });
61
- });
@@ -1,33 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- interface RequestContext {
4
- origin?: string;
5
- correlationId?: string;
6
- }
7
-
8
- const storage = new AsyncLocalStorage<RequestContext>();
9
-
10
- export const runWithRequestContext = <T>(ctx: RequestContext, fn: () => T): T =>
11
- storage.run(ctx, fn);
12
-
13
- export const getRequestOrigin = (): string | undefined =>
14
- storage.getStore()?.origin;
15
-
16
- export const getRequestCorrelationId = (): string | undefined =>
17
- storage.getStore()?.correlationId;
18
-
19
- const parseAllowedOrigins = (): readonly string[] => {
20
- const raw = process.env.CORS_ALLOWED_ORIGINS;
21
- if (!raw) return [];
22
- return raw
23
- .split(",")
24
- .map((s) => s.trim())
25
- .filter((s) => s.length > 0);
26
- };
27
-
28
- export const resolveAllowedOrigin = (origin: string | undefined): string => {
29
- const allowed = parseAllowedOrigins();
30
- if (allowed.length === 0) return "*";
31
- if (origin && allowed.includes(origin)) return origin;
32
- return allowed[0];
33
- };
@@ -1,88 +0,0 @@
1
- /**
2
- * The two refusals no handler ever sees: a path the spec does not route, and a
3
- * request the spec refuses before routing. Both are answered by the framework,
4
- * and both used to leave the one shape the API promises (issue #371) — the
5
- * unmatched route with no body at all, the refused request with no `code` and
6
- * with the validator's raw findings beside it.
7
- */
8
- import assert from "node:assert/strict";
9
- import { describe, it } from "node:test";
10
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
11
- import { api } from "./index.js";
12
- import { normalizeRequest } from "./request.js";
13
-
14
- const SUB = "cognito-sub-pipeline";
15
-
16
- const send = async (
17
- method: string,
18
- path: string,
19
- body?: string,
20
- ): Promise<APIGatewayProxyResult> => {
21
- const event = {
22
- httpMethod: method,
23
- path,
24
- body: body ?? null,
25
- queryStringParameters: null,
26
- headers: body ? { "content-type": "application/json" } : {},
27
- requestContext: { authorizer: { claims: { sub: SUB } } },
28
- } as unknown as APIGatewayProxyEvent;
29
-
30
- return (await api.handleRequest(
31
- normalizeRequest(event),
32
- event,
33
- {} as never,
34
- )) as APIGatewayProxyResult;
35
- };
36
-
37
- describe("a path the spec does not route", () => {
38
- it("answers 404 with a body, not an empty one", async () => {
39
- const response = await send("GET", "/no-such-resource");
40
-
41
- assert.equal(response.statusCode, 404);
42
- assert.deepEqual(JSON.parse(response.body), {
43
- code: "not_found",
44
- message: "Not found",
45
- });
46
- });
47
- });
48
-
49
- describe("a request the spec refuses before any handler runs", () => {
50
- it("answers 400 with a code and the fields it refused", async () => {
51
- const response = await send(
52
- "POST",
53
- "/calendars",
54
- JSON.stringify({ displayName: 42 }),
55
- );
56
-
57
- assert.equal(response.statusCode, 400);
58
- const body = JSON.parse(response.body) as {
59
- code: string;
60
- message: string;
61
- errors: { path: string; rule: string; message: string }[];
62
- };
63
-
64
- assert.equal(body.code, "invalid_request");
65
- assert.equal(body.message, "Invalid request");
66
- assert.ok(body.errors.length > 0);
67
- for (const detail of body.errors) {
68
- assert.deepEqual(Object.keys(detail).sort(), ["message", "path", "rule"]);
69
- assert.equal(typeof detail.path, "string");
70
- assert.equal(typeof detail.rule, "string");
71
- assert.equal(typeof detail.message, "string");
72
- }
73
- });
74
-
75
- // An AJV error also carries `params`, and under `verbose` the rejected value
76
- // itself. A refused create body is whatever the caller sent — a password
77
- // among the possibilities — so nothing but the three named fields may travel.
78
- it("never echoes the value it refused", async () => {
79
- const response = await send(
80
- "POST",
81
- "/accounts",
82
- JSON.stringify({ password: "hunter2", imapPort: "not-a-number" }),
83
- );
84
-
85
- assert.equal(response.statusCode, 400);
86
- assert.ok(!response.body.includes("hunter2"));
87
- });
88
- });
package/src/request.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { APIGatewayProxyEvent } from "aws-lambda";
2
- import type { Request } from "openapi-backend";
3
-
4
- export const normalizeRequest = (event: APIGatewayProxyEvent): Request => ({
5
- method: event.httpMethod,
6
- path: event.path,
7
- query: (event.queryStringParameters ?? {}) as Record<string, string>,
8
- body: event.body,
9
- headers: (event.headers ?? {}) as Record<string, string>,
10
- });
@@ -1,130 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it } from "node:test";
3
- import type { Context as OpenAPIContext } from "openapi-backend";
4
- import { runWithRequestContext } from "./request-context.js";
5
- import { formatResponse, postResponseHandler } from "./response.js";
6
-
7
- type ValidateResponseFn = (
8
- response: unknown,
9
- operation: unknown,
10
- ) => { valid: boolean; errors?: { instancePath?: string; message?: string }[] };
11
-
12
- const makeContext = (
13
- response: Record<string, unknown>,
14
- validateResponse: ValidateResponseFn,
15
- ): OpenAPIContext => {
16
- return {
17
- api: { validateResponse },
18
- response,
19
- operation: { operationId: "testOp" },
20
- } as unknown as OpenAPIContext;
21
- };
22
-
23
- describe("postResponseHandler validation gating", () => {
24
- const originalStage = process.env.STAGE_NAME;
25
-
26
- beforeEach(() => {
27
- delete process.env.STAGE_NAME;
28
- });
29
-
30
- afterEach(() => {
31
- if (originalStage === undefined) {
32
- delete process.env.STAGE_NAME;
33
- } else {
34
- process.env.STAGE_NAME = originalStage;
35
- }
36
- });
37
-
38
- it("runs validateResponse when STAGE_NAME is 'dev'", () => {
39
- process.env.STAGE_NAME = "dev";
40
- let called = false;
41
- const validate: ValidateResponseFn = () => {
42
- called = true;
43
- return { valid: true };
44
- };
45
- const result = postResponseHandler(makeContext({ items: [] }, validate));
46
- assert.equal(called, true);
47
- assert.equal(result.statusCode, 200);
48
- });
49
-
50
- it("skips validateResponse when STAGE_NAME is 'prod'", () => {
51
- process.env.STAGE_NAME = "prod";
52
- let called = false;
53
- const validate: ValidateResponseFn = () => {
54
- called = true;
55
- return { valid: true };
56
- };
57
- const result = postResponseHandler(makeContext({ items: [] }, validate));
58
- assert.equal(called, false);
59
- assert.equal(result.statusCode, 200);
60
- });
61
-
62
- it("skips validateResponse when STAGE_NAME is 'beta'", () => {
63
- process.env.STAGE_NAME = "beta";
64
- let called = false;
65
- const validate: ValidateResponseFn = () => {
66
- called = true;
67
- return { valid: true };
68
- };
69
- postResponseHandler(makeContext({ items: [] }, validate));
70
- assert.equal(called, false);
71
- });
72
-
73
- it("skips validateResponse when STAGE_NAME is unset", () => {
74
- let called = false;
75
- const validate: ValidateResponseFn = () => {
76
- called = true;
77
- return { valid: true };
78
- };
79
- postResponseHandler(makeContext({ items: [] }, validate));
80
- assert.equal(called, false);
81
- });
82
-
83
- it("returns the formatted error response when statusCode is set, regardless of stage", () => {
84
- process.env.STAGE_NAME = "dev";
85
- let called = false;
86
- const validate: ValidateResponseFn = () => {
87
- called = true;
88
- return { valid: true };
89
- };
90
- const result = postResponseHandler(
91
- makeContext({ statusCode: 404, message: "Not Found" }, validate),
92
- );
93
- assert.equal(called, false);
94
- assert.equal(result.statusCode, 404);
95
- });
96
-
97
- it("returns 200 with the response body when validation fails in dev", () => {
98
- process.env.STAGE_NAME = "dev";
99
- const validate: ValidateResponseFn = () => ({
100
- valid: false,
101
- errors: [{ instancePath: "/items/0/id", message: "must be string" }],
102
- });
103
- const result = postResponseHandler(
104
- makeContext({ items: [{ id: 42 }] }, validate),
105
- );
106
- assert.equal(result.statusCode, 200);
107
- });
108
- });
109
-
110
- // A bug report quotes the correlation id off the failing response. Without the
111
- // header it reads "(none)" and there is nothing to grep the server logs for.
112
- describe("the correlation id travels back on the response", () => {
113
- it("carries the request's id, and exposes the header to the browser", () => {
114
- const result = runWithRequestContext({ correlationId: "req-604" }, () =>
115
- formatResponse({ message: "Conflict" }, 409),
116
- );
117
-
118
- assert.equal(result.headers?.["x-correlation-id"], "req-604");
119
- assert.match(
120
- String(result.headers?.["Access-Control-Expose-Headers"]),
121
- /x-correlation-id/,
122
- );
123
- });
124
-
125
- it("omits the header when the request carried no id", () => {
126
- const result = formatResponse({ message: "Conflict" }, 409);
127
-
128
- assert.equal(result.headers?.["x-correlation-id"], undefined);
129
- });
130
- });