@remit/backend 0.0.143 → 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 -522
  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
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
- };