@remit/backend 0.0.142 → 0.0.144

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 (269) 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 +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -521
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. 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
- });