@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
package/src/index.test.ts DELETED
@@ -1,83 +0,0 @@
1
- /**
2
- * Issue #1054: the backend must log one structured access line per request
3
- * (method, path, status, duration, correlation id) at a level that is on in
4
- * production. The access log lives in `rawHandler` in `src/index.ts`, wrapping
5
- * the authenticated request path — so a 401 denial from edge-auth also gets an
6
- * access line (once it flows through `formatResponse` with the correlation id).
7
- *
8
- * The 401 response shape (correlation id header + CORS on the denial) is tested
9
- * directly in `jwt-auth.test.ts` via `runWithRequestContext`; this file tests
10
- * that the handler emits the access log line on every request by spying on the
11
- * logger's info method.
12
- */
13
- import assert from "node:assert/strict";
14
- import { describe, it, mock } from "node:test";
15
- import { logger } from "@remit/logger-lambda";
16
- import type { APIGatewayProxyEvent, Context } from "aws-lambda";
17
- import { handler } from "./index.js";
18
-
19
- const buildEvent = (
20
- overrides: Partial<APIGatewayProxyEvent> = {},
21
- ): APIGatewayProxyEvent =>
22
- ({
23
- path: "/api/test",
24
- httpMethod: "GET",
25
- pathParameters: {},
26
- queryStringParameters: {},
27
- headers: {},
28
- requestContext: {},
29
- ...overrides,
30
- }) as unknown as APIGatewayProxyEvent;
31
-
32
- const buildContext = (correlationId = "ctx-test-1"): Context =>
33
- ({
34
- awsRequestId: correlationId,
35
- functionName: "test-function",
36
- getRemainingTimeInMillis: () => 30000,
37
- }) as unknown as Context;
38
-
39
- describe("issue #1054: per-request access log", () => {
40
- it('emits one "Request completed" line at info level with method, path, status, duration and correlation id', async () => {
41
- delete process.env.LOCAL_ACCOUNT_CONFIG_ID;
42
-
43
- // Spy on logger.info — the real logger passes (fields, message) in
44
- // pino order, so the spy captures the structured fields and message.
45
- const infoSpy = mock.method(logger, "info");
46
-
47
- try {
48
- await handler(
49
- buildEvent({ path: "/api/__nonexistent_for_test__" }),
50
- buildContext("ctx-access-log"),
51
- ).catch(() => {
52
- // 404s are expected; we only care about the log line.
53
- });
54
-
55
- // `logger.info` is overloaded ((obj, msg?) and (msg, obj?)), so the
56
- // mock types `call.arguments` by a single overload. Read the
57
- // arguments positionally through an unknown[] view instead.
58
- const accessCalls = infoSpy.mock.calls.filter((call) => {
59
- // pino calls logger.info(fields, message)
60
- const args = call.arguments as unknown[];
61
- return args[1] === "Request completed";
62
- });
63
-
64
- assert.ok(accessCalls.length >= 1, "expected an access log line");
65
- const fields = (accessCalls[0].arguments as unknown[])[0] as Record<
66
- string,
67
- unknown
68
- >;
69
-
70
- // infoSpy was called on logger.info, so the level is "info" by
71
- // construction — pino's adapter routes .info() calls to the info
72
- // level. No separate level field needed.
73
-
74
- assert.equal(fields.method, "GET");
75
- assert.equal(fields.path, "/api/__nonexistent_for_test__");
76
- assert.equal(fields.correlationId, "ctx-access-log");
77
- assert.equal(typeof fields.statusCode, "number");
78
- assert.equal(typeof fields.durationMs, "number");
79
- } finally {
80
- infoSpy.mock.restore();
81
- }
82
- });
83
- });
package/src/index.ts DELETED
@@ -1,228 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { inspect } from "node:util";
5
- import { redactCalendarFeedPath } from "@remit/calendar-service";
6
- import { logger, withLogContext, withTelemetry } from "@remit/logger-lambda";
7
- import type { APIGatewayProxyEvent, Context } from "aws-lambda";
8
- import {
9
- type Document,
10
- OpenAPIBackend,
11
- type Context as OpenAPIContext,
12
- type Request,
13
- } from "openapi-backend";
14
- import { assertLocalBypassNotInDeployedEnv } from "./auth.js";
15
- import { usesBetterAuthJwt } from "./data-backend.js";
16
- import { defaultErrorCode, handleError } from "./error.js";
17
- import { handlers } from "./handlers/index.js";
18
- import { authenticateSelfHostRequest } from "./jwt-auth.js";
19
- import { normalizeRequest } from "./request.js";
20
- import { runWithRequestContext } from "./request-context.js";
21
- import { formatResponse, postResponseHandler } from "./response.js";
22
-
23
- const __dirname = dirname(fileURLToPath(import.meta.url));
24
-
25
- // Boot-time refusal: the local auth-bypass overrides must never be set in a
26
- // deployed environment. Runs on module load so a leaked env var fails the
27
- // process at startup rather than silently opening authentication at runtime.
28
- assertLocalBypassNotInDeployedEnv();
29
-
30
- /**
31
- * Last-resort net for a leaked background rejection.
32
- *
33
- * Every fire-and-forget side effect is meant to be contained at its source (see
34
- * service/fire-and-forget.ts), so reaching here is a bug — an uncontained
35
- * `void promise` somewhere. The API process (dev-server / warm Lambda container)
36
- * shares one event loop across requests, so without this handler such a leak
37
- * would surface as a spurious 500 on whatever request was in flight. We log it
38
- * LOUDLY with a distinct alert so it is observable, and deliberately do NOT exit
39
- * — a stray background rejection must never take the API down or fail a request.
40
- * (The worker process intentionally has no such net: there each message is its
41
- * own invocation and a rejection should crash it.)
42
- */
43
- const unhandledRejectionRegistered = Symbol.for(
44
- "remit.backend.unhandledRejectionNet",
45
- );
46
- const globalProcess = process as unknown as Record<symbol, boolean>;
47
- if (!globalProcess[unhandledRejectionRegistered]) {
48
- globalProcess[unhandledRejectionRegistered] = true;
49
- process.on("unhandledRejection", (reason: unknown) => {
50
- logger.error(
51
- {
52
- alert: "unhandled_rejection",
53
- errorName: (reason as { name?: string })?.name,
54
- errorCode:
55
- (reason as { Code?: string })?.Code ??
56
- (reason as { code?: string })?.code,
57
- error: inspect(reason),
58
- },
59
- "Unhandled promise rejection leaked into the API event loop (contained, request not failed)",
60
- );
61
- });
62
- }
63
-
64
- const loadOpenAPISpec = (): Document => {
65
- // Bundled Lambda: infra's NodeJSArmFunction copies openapi.json into the
66
- // bundle root next to the entrypoint via its `extraFiles` prop.
67
- const bundledPath = join(__dirname, "openapi.json");
68
- if (existsSync(bundledPath)) {
69
- return JSON.parse(readFileSync(bundledPath, "utf-8"));
70
- }
71
-
72
- // Dev (tsx / dev-server): read straight from the repo build tree.
73
- const devPath = join(__dirname, "../../../build/remit-openapi3/openapi.json");
74
- return JSON.parse(readFileSync(devPath, "utf-8"));
75
- };
76
-
77
- const OpenAPISpec = loadOpenAPISpec();
78
-
79
- const api = new OpenAPIBackend({
80
- definition: OpenAPISpec,
81
- quick: true,
82
- // Coerce query/path params to their schema types. API Gateway delivers every
83
- // query param as a string; handlers cast them to number/boolean (e.g. `limit`,
84
- // `hasAttachment`) and forward them downstream. Without coercion a string
85
- // `limit` reaches ElectroDB's `.go({ limit })` and DynamoDB rejects the
86
- // numeric Limit field ("STRING_VALUE cannot be converted to Integer"), 500ing
87
- // /addresses/search and /search/semantic.
88
- coerceTypes: true,
89
- });
90
-
91
- api.register("postResponseHandler", postResponseHandler);
92
-
93
- /**
94
- * The validator's findings, narrowed to the three fields `RequestValidationDetail`
95
- * declares. Copied field by field rather than passed through: an AJV error also
96
- * carries `params`, and under `verbose` the rejected `data` itself — which for a
97
- * refused request body is whatever the caller sent, a password among the
98
- * possibilities. Naming the fields is what keeps that off the wire.
99
- */
100
- const validationDetails = (
101
- errors: OpenAPIContext["validation"]["errors"],
102
- ): Array<{ path: string; rule: string; message: string }> =>
103
- (errors ?? []).map((error) => ({
104
- path: error.instancePath,
105
- rule: error.keyword,
106
- message: error.message ?? "refused",
107
- }));
108
-
109
- // The errors and the operation, and nothing that came off the wire. A request
110
- // that fails validation is logged at error level, and the request it carries
111
- // holds the caller's headers and cookies — an Authorization bearer among them,
112
- // and for a calendar feed the token in the path is itself the credential
113
- // (issue #1065). The path is redacted the same way the request scope redacts it.
114
- api.register("validationFail", (c: OpenAPIContext, req: Request) => {
115
- logger.error(
116
- {
117
- errors: c.validation.errors,
118
- path: redactCalendarFeedPath(req.path),
119
- operation: c.operation?.operationId,
120
- },
121
- "Validation failed",
122
- );
123
- return {
124
- statusCode: 400,
125
- body: {
126
- code: defaultErrorCode(400),
127
- message: "Invalid request",
128
- errors: validationDetails(c.validation.errors),
129
- },
130
- };
131
- });
132
-
133
- api.register("notFound", () => ({
134
- statusCode: 404,
135
- body: { code: defaultErrorCode(404), message: "Not found" },
136
- }));
137
-
138
- api.register("notImplemented", async (c: OpenAPIContext) => {
139
- const { status, mock } = api.mockResponseForOperation(
140
- c.operation.operationId as string,
141
- );
142
- return formatResponse(mock as Record<string, unknown>, status);
143
- });
144
-
145
- api.register(handlers);
146
-
147
- api.init();
148
-
149
- export { api, OpenAPISpec };
150
-
151
- const readOriginHeader = (
152
- headers: APIGatewayProxyEvent["headers"],
153
- ): string | undefined => {
154
- if (!headers) return undefined;
155
- for (const [key, value] of Object.entries(headers)) {
156
- if (key.toLowerCase() === "origin" && typeof value === "string") {
157
- return value;
158
- }
159
- }
160
- return undefined;
161
- };
162
-
163
- // A scope, not `logger.setBindings`: this process serves requests concurrently,
164
- // and bindings on the shared logger belong to whichever request wrote them last,
165
- // so a line gets attributed to the wrong request. The scope follows the request
166
- // through its own async continuations and nothing else. It nests inside the one
167
- // `withTelemetry` opens, which is where `requestId` comes from.
168
- const rawHandler = async (event: APIGatewayProxyEvent, context: Context) => {
169
- // A calendar feed carries its credential in the path, so the raw path is a
170
- // secret and cannot be a log field (issue #1065). Redacted once, here, where
171
- // every line inside the request picks it up from the scope.
172
- const path = redactCalendarFeedPath(event.path);
173
-
174
- return withLogContext(
175
- {
176
- path,
177
- method: event.httpMethod,
178
- },
179
- async () => {
180
- logger.debug({ method: event.httpMethod, path }, "Request received");
181
-
182
- const origin = readOriginHeader(event.headers);
183
- const startedAt = Date.now();
184
- // Capture the correlation id here: it is seeded by
185
- // runWithRequestContext below (from context.awsRequestId), but that
186
- // AsyncLocalStorage scope does not reliably survive the
187
- // openapi-backend handleRequest call chain, so we read it from
188
- // `context.awsRequestId` directly rather than via
189
- // getRequestCorrelationId at log time.
190
- const correlationId = context.awsRequestId;
191
-
192
- const result = await runWithRequestContext(
193
- { origin, correlationId },
194
- async () => {
195
- if (usesBetterAuthJwt()) {
196
- const denied = await authenticateSelfHostRequest(event);
197
- if (denied) return denied;
198
- }
199
-
200
- return api
201
- .handleRequest(normalizeRequest(event), event, context)
202
- .catch(handleError);
203
- },
204
- );
205
-
206
- // One structured access line per request: method, path, status, and
207
- // duration. The correlation id is inlined as a top-level field so a bug
208
- // report quoting the id can grep logs by it directly, and emitted at
209
- // info so it is on by default in production (the debug "Request
210
- // received" above already filters out under normal log levels).
211
- // biome-ignore lint/plugin/no-logger-info: one access line per request is an audit-grade signal (issue #1054)
212
- logger.info(
213
- {
214
- method: event.httpMethod,
215
- path,
216
- statusCode: result.statusCode,
217
- durationMs: Date.now() - startedAt,
218
- correlationId,
219
- },
220
- "Request completed",
221
- );
222
-
223
- return result;
224
- },
225
- );
226
- };
227
-
228
- export const handler = withTelemetry(rawHandler);
@@ -1,152 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, test } from "node:test";
3
- import type { APIGatewayProxyEvent } from "aws-lambda";
4
- import {
5
- _setVerifierForTest,
6
- authenticateSelfHostRequest,
7
- } from "./jwt-auth.js";
8
- import { runWithRequestContext } from "./request-context.js";
9
-
10
- const buildEvent = (
11
- overrides: Partial<APIGatewayProxyEvent> = {},
12
- ): APIGatewayProxyEvent =>
13
- ({
14
- headers: {},
15
- requestContext: {},
16
- ...overrides,
17
- }) as APIGatewayProxyEvent;
18
-
19
- let savedBypass: string | undefined;
20
-
21
- beforeEach(() => {
22
- savedBypass = process.env.LOCAL_ACCOUNT_CONFIG_ID;
23
- delete process.env.LOCAL_ACCOUNT_CONFIG_ID;
24
- });
25
-
26
- afterEach(() => {
27
- _setVerifierForTest(null);
28
- if (savedBypass === undefined) delete process.env.LOCAL_ACCOUNT_CONFIG_ID;
29
- else process.env.LOCAL_ACCOUNT_CONFIG_ID = savedBypass;
30
- });
31
-
32
- test("valid token injects verified sub into authorizer claims", async () => {
33
- _setVerifierForTest(async () => ({ sub: "user-abc", email: "a@b.com" }));
34
- const event = buildEvent({
35
- headers: { Authorization: "Bearer good.token.here" },
36
- });
37
-
38
- const result = await authenticateSelfHostRequest(event);
39
-
40
- assert.equal(result, null);
41
- assert.equal(event.requestContext.authorizer?.claims?.sub, "user-abc");
42
- assert.equal(event.requestContext.authorizer?.claims?.email, "a@b.com");
43
- });
44
-
45
- test("invalid token returns 401 with correlation id and CORS headers", async () => {
46
- _setVerifierForTest(async () => {
47
- throw new Error("bad signature");
48
- });
49
- const event = buildEvent({
50
- headers: { authorization: "Bearer bad.token" },
51
- });
52
-
53
- const result = await runWithRequestContext(
54
- { correlationId: "req-invalid-token" },
55
- async () => authenticateSelfHostRequest(event),
56
- );
57
-
58
- assert.equal(result?.statusCode, 401);
59
- assert.equal(
60
- result?.headers?.["x-correlation-id"],
61
- "req-invalid-token",
62
- "edge-auth 401 must carry the correlation id",
63
- );
64
- assert.equal(
65
- result?.headers?.["Access-Control-Allow-Origin"],
66
- "*",
67
- "edge-auth 401 must carry CORS headers",
68
- );
69
- assert.equal(
70
- result?.body,
71
- JSON.stringify({
72
- code: "unauthorized",
73
- message: "Invalid or expired token",
74
- }),
75
- );
76
- assert.equal(event.requestContext.authorizer, undefined);
77
- });
78
-
79
- test("no token with a local bypass configured is allowed", async () => {
80
- process.env.LOCAL_ACCOUNT_CONFIG_ID = "some-config-id";
81
- const event = buildEvent();
82
-
83
- const result = await authenticateSelfHostRequest(event);
84
-
85
- assert.equal(result, null);
86
- });
87
-
88
- test("no token and no bypass returns 401 with correlation id", async () => {
89
- const event = buildEvent();
90
-
91
- const result = await runWithRequestContext(
92
- { correlationId: "req-no-token" },
93
- async () => authenticateSelfHostRequest(event),
94
- );
95
-
96
- assert.equal(result?.statusCode, 401);
97
- assert.equal(
98
- result?.headers?.["x-correlation-id"],
99
- "req-no-token",
100
- "edge-auth 401 must carry the correlation id",
101
- );
102
- assert.equal(
103
- result?.headers?.["Access-Control-Allow-Origin"],
104
- "*",
105
- "edge-auth 401 must carry CORS headers",
106
- );
107
- });
108
-
109
- test("a tokenless GET to the Microsoft OAuth callback is admitted", async () => {
110
- const event = buildEvent({
111
- httpMethod: "GET",
112
- path: "/accounts/oauth/microsoft/callback",
113
- });
114
-
115
- const result = await authenticateSelfHostRequest(event);
116
-
117
- assert.equal(result, null);
118
- assert.equal(event.requestContext.authorizer, undefined);
119
- });
120
-
121
- test("the callback exemption covers that path and method only", async () => {
122
- const neighbour = buildEvent({
123
- httpMethod: "GET",
124
- path: "/accounts/oauth/microsoft/start",
125
- });
126
- const otherMethod = buildEvent({
127
- httpMethod: "POST",
128
- path: "/accounts/oauth/microsoft/callback",
129
- });
130
-
131
- assert.equal((await authenticateSelfHostRequest(neighbour))?.statusCode, 401);
132
- assert.equal(
133
- (await authenticateSelfHostRequest(otherMethod))?.statusCode,
134
- 401,
135
- );
136
- });
137
-
138
- test("pre-injected claims (edge tier) short-circuit verification", async () => {
139
- _setVerifierForTest(async () => {
140
- throw new Error("verifier must not be called");
141
- });
142
- const event = buildEvent({
143
- requestContext: {
144
- authorizer: { claims: { sub: "edge-user" } },
145
- } as unknown as APIGatewayProxyEvent["requestContext"],
146
- });
147
-
148
- const result = await authenticateSelfHostRequest(event);
149
-
150
- assert.equal(result, null);
151
- assert.equal(event.requestContext.authorizer?.claims?.sub, "edge-user");
152
- });
package/src/jwt-auth.ts DELETED
@@ -1,132 +0,0 @@
1
- import {
2
- createJwtVerifier,
3
- extractBearerToken,
4
- type JwtVerifier,
5
- resolveVerifierConfig,
6
- } from "@remit/auth-service/verifier";
7
- import { readCalendarFeedToken } from "@remit/calendar-service";
8
- import { logger } from "@remit/logger-lambda";
9
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
10
- import { defaultErrorCode } from "./error.js";
11
- import { formatResponse } from "./response.js";
12
-
13
- let verifier: JwtVerifier | null = null;
14
-
15
- const getVerifier = (): JwtVerifier => {
16
- if (!verifier) verifier = createJwtVerifier(resolveVerifierConfig());
17
- return verifier;
18
- };
19
-
20
- /** Test-only override for the memoized verifier. Pass null to reset. */
21
- export const _setVerifierForTest = (v: JwtVerifier | null): void => {
22
- verifier = v;
23
- };
24
-
25
- const readHeader = (
26
- headers: APIGatewayProxyEvent["headers"],
27
- name: string,
28
- ): string | undefined => {
29
- for (const [key, value] of Object.entries(headers ?? {})) {
30
- if (key.toLowerCase() === name && typeof value === "string") return value;
31
- }
32
- return undefined;
33
- };
34
-
35
- const unauthorized = (message: string): APIGatewayProxyResult =>
36
- formatResponse({ code: defaultErrorCode(401), message }, 401);
37
-
38
- const injectClaims = (
39
- event: APIGatewayProxyEvent,
40
- sub: string,
41
- email: string | undefined,
42
- ): void => {
43
- event.requestContext = {
44
- ...event.requestContext,
45
- authorizer: {
46
- ...event.requestContext?.authorizer,
47
- claims: {
48
- sub,
49
- email: email ?? "",
50
- },
51
- },
52
- };
53
- };
54
-
55
- const hasLocalBypass = (): boolean =>
56
- Boolean(process.env.LOCAL_ACCOUNT_CONFIG_ID);
57
-
58
- /**
59
- * The routes that run without a token.
60
- *
61
- * Microsoft redirects the browser here after consent, so the request carries no
62
- * Authorization header and no session to derive one from. The handler takes its
63
- * identity from the HMAC-signed `state` parameter and validates it there; that
64
- * signature is the gate, not a JWT.
65
- *
66
- * A calendar feed carries its credential in the path for the same reason: a
67
- * subscribed client polls a URL and has no session to present, so the token is
68
- * the gate and the handler compares it against a stored hash.
69
- *
70
- * Hardcoded, matching the edge exemption in packages/apisix/src/route-table.ts.
71
- * `@useAuth(NoAuth)` in the spec does not open a path here — a future public
72
- * route is added by hand.
73
- */
74
- const PUBLIC_ROUTES: readonly {
75
- method: string;
76
- matches: (path: string) => boolean;
77
- }[] = [
78
- {
79
- method: "GET",
80
- matches: (path) => path === "/accounts/oauth/microsoft/callback",
81
- },
82
- {
83
- method: "GET",
84
- matches: (path) => readCalendarFeedToken(path) !== null,
85
- },
86
- ];
87
-
88
- const isPublicRoute = (event: APIGatewayProxyEvent): boolean =>
89
- PUBLIC_ROUTES.some(
90
- (route) => route.method === event.httpMethod && route.matches(event.path),
91
- );
92
-
93
- /**
94
- * Authenticate a self-host request from a better-auth RS256 JWT.
95
- *
96
- * On a valid token the verified `sub` is injected into the event's authorizer
97
- * claims, exactly where the Cognito authorizer puts it, so every downstream
98
- * handler reads identity through the unchanged `auth.ts` path. Returns a 401
99
- * response (not null) when a token is present but fails verification, or when no
100
- * token is supplied and no local test bypass is configured. Returns null to let
101
- * the request proceed.
102
- *
103
- * When the edge tier (APISIX) has already verified and injected claims, this is
104
- * a no-op — the backend trusts pre-populated claims and re-verifies otherwise.
105
- */
106
- export const authenticateSelfHostRequest = async (
107
- event: APIGatewayProxyEvent,
108
- ): Promise<APIGatewayProxyResult | null> => {
109
- if (isPublicRoute(event)) return null;
110
-
111
- const existingSub = event.requestContext?.authorizer?.claims?.sub;
112
- if (typeof existingSub === "string" && existingSub.length > 0) return null;
113
-
114
- const token = extractBearerToken(readHeader(event.headers, "authorization"));
115
-
116
- if (token) {
117
- const claims = await getVerifier()(token).catch((err: unknown) => {
118
- logger.warn(
119
- { err: err instanceof Error ? err.message : String(err) },
120
- "better-auth JWT verification failed",
121
- );
122
- return null;
123
- });
124
- if (!claims) return unauthorized("Invalid or expired token");
125
- injectClaims(event, claims.sub, claims.email);
126
- return null;
127
- }
128
-
129
- if (hasLocalBypass()) return null;
130
-
131
- return unauthorized("Authentication required");
132
- };