@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
@@ -0,0 +1,38 @@
1
+ import { parseContentStorageKey } from "@remit/storage-service";
2
+ export { parseContentStorageKey };
3
+ export interface ContentResult {
4
+ status: number;
5
+ headers: Record<string, string>;
6
+ body: string | Buffer;
7
+ }
8
+ export interface ServeContentDeps {
9
+ /**
10
+ * Reads the stored bytes, or returns null when the object is genuinely
11
+ * missing (ENOENT / NoSuchKey). Any other read error must throw so the route
12
+ * 500s — a permission/infra failure is never masked as a missing body.
13
+ */
14
+ readObject(fullPath: string): Promise<Buffer | null>;
15
+ /** Resolves the message's mailbox + uid so the cue can issue one FETCH. */
16
+ lookupMessage(messageId: string): Promise<{
17
+ mailboxId: string;
18
+ uid: number;
19
+ } | null>;
20
+ requestBodySync(input: {
21
+ accountId: string;
22
+ mailboxId: string;
23
+ messageId: string;
24
+ uid?: number;
25
+ }): Promise<void>;
26
+ }
27
+ /**
28
+ * Local stand-in for the CloudFront `/content/*` behavior. Body-fetch outcomes:
29
+ *
30
+ * - Object present → 200 with the (decompressed) bytes.
31
+ * - Object missing → 202 + `Retry-After: 1`, and re-arm the SYNC_MESSAGE_BODY
32
+ * cue for that message so a retry succeeds once the worker stores the body.
33
+ * - Any other read failure throws, so the route 500s loudly.
34
+ */
35
+ export declare const serveContent: (deps: ServeContentDeps, args: {
36
+ fullPath: string;
37
+ storageKey: string;
38
+ }) => Promise<ContentResult>;
@@ -0,0 +1,46 @@
1
+ import { gunzipSync } from "node:zlib";
2
+ import { parseContentStorageKey } from "@remit/storage-service";
3
+ // Re-exported for existing same-package consumers (server.ts,
4
+ // content-handler.test.ts). The key-shape parser itself lives in
5
+ // `@remit/storage-service` next to the `build*Key` functions it
6
+ // inverts, so non-backend consumers (e.g. the content-selfheal worker) can
7
+ // depend on it without pulling in the rest of `@remit/backend`.
8
+ export { parseContentStorageKey };
9
+ /**
10
+ * Local stand-in for the CloudFront `/content/*` behavior. Body-fetch outcomes:
11
+ *
12
+ * - Object present → 200 with the (decompressed) bytes.
13
+ * - Object missing → 202 + `Retry-After: 1`, and re-arm the SYNC_MESSAGE_BODY
14
+ * cue for that message so a retry succeeds once the worker stores the body.
15
+ * - Any other read failure throws, so the route 500s loudly.
16
+ */
17
+ export const serveContent = async (deps, args) => {
18
+ const raw = await deps.readObject(args.fullPath);
19
+ if (raw) {
20
+ // The filesystem/S3 backends gzip every part; decompress here since there
21
+ // is no CloudFront to auto-decode locally.
22
+ const buffer = raw[0] === 0x1f && raw[1] === 0x8b ? gunzipSync(raw) : raw;
23
+ return {
24
+ status: 200,
25
+ headers: { "content-type": "application/octet-stream" },
26
+ body: buffer,
27
+ };
28
+ }
29
+ const ids = parseContentStorageKey(args.storageKey);
30
+ if (ids) {
31
+ const message = await deps.lookupMessage(ids.messageId);
32
+ if (message) {
33
+ await deps.requestBodySync({
34
+ accountId: ids.accountId,
35
+ mailboxId: message.mailboxId,
36
+ messageId: ids.messageId,
37
+ uid: message.uid,
38
+ });
39
+ }
40
+ }
41
+ return {
42
+ status: 202,
43
+ headers: { "Retry-After": "1" },
44
+ body: "body not yet synced",
45
+ };
46
+ };
@@ -0,0 +1,7 @@
1
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";
2
+ import { type Document, OpenAPIBackend } from "openapi-backend";
3
+ export declare const OpenAPISpec: Document;
4
+ declare const api: OpenAPIBackend;
5
+ export { setLogger } from "@remit/logger";
6
+ export { api };
7
+ export declare const handleApiEvent: (event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>;
@@ -0,0 +1,144 @@
1
+ import spec from "@remit/api-openapi-spec/openapi.json" with { type: "json" };
2
+ import { redactCalendarFeedPath } from "@remit/calendar-service";
3
+ import { logger, withLogContext } from "@remit/logger";
4
+ import { OpenAPIBackend, } from "openapi-backend";
5
+ import { assertLocalBypassNotInDeployedEnv } from "./auth.js";
6
+ import { usesBetterAuthJwt } from "./data-backend.js";
7
+ import { defaultErrorCode, handleError } from "./error.js";
8
+ import { handlers } from "./handlers/index.js";
9
+ import { authenticateSelfHostRequest } from "./jwt-auth.js";
10
+ import { normalizeRequest } from "./request.js";
11
+ import { runWithRequestContext } from "./request-context.js";
12
+ import { formatResponse, postResponseHandler } from "./response.js";
13
+ // Boot-time refusal: the local auth-bypass overrides must never be set in a
14
+ // deployed environment. Runs on module load so a leaked env var fails the
15
+ // process at startup rather than silently opening authentication at runtime.
16
+ assertLocalBypassNotInDeployedEnv();
17
+ const isDocument = (value) => typeof value === "object" &&
18
+ value !== null &&
19
+ "openapi" in value &&
20
+ typeof value.openapi === "string" &&
21
+ "paths" in value;
22
+ // Imported, not read from disk, so no host has to place openapi.json next to the
23
+ // entry point — a Worker has no filesystem to read it from.
24
+ if (!isDocument(spec)) {
25
+ throw new Error("@remit/api-openapi-spec is not an OpenAPI document");
26
+ }
27
+ // Typed as a Document so importers do not see the JSON literal type.
28
+ export const OpenAPISpec = spec;
29
+ const api = new OpenAPIBackend({
30
+ definition: OpenAPISpec,
31
+ quick: true,
32
+ // Coerce query/path params to their schema types. API Gateway delivers every
33
+ // query param as a string; handlers cast them to number/boolean (e.g. `limit`,
34
+ // `hasAttachment`) and forward them downstream. Without coercion a string
35
+ // `limit` reaches ElectroDB's `.go({ limit })` and DynamoDB rejects the
36
+ // numeric Limit field ("STRING_VALUE cannot be converted to Integer"), 500ing
37
+ // /addresses/search.
38
+ coerceTypes: true,
39
+ });
40
+ api.register("postResponseHandler", postResponseHandler);
41
+ /**
42
+ * The validator's findings, narrowed to the three fields `RequestValidationDetail`
43
+ * declares. Copied field by field rather than passed through: an AJV error also
44
+ * carries `params`, and under `verbose` the rejected `data` itself — which for a
45
+ * refused request body is whatever the caller sent, a password among the
46
+ * possibilities. Naming the fields is what keeps that off the wire.
47
+ */
48
+ const validationDetails = (errors) => (errors ?? []).map((error) => ({
49
+ path: error.instancePath,
50
+ rule: error.keyword,
51
+ message: error.message ?? "refused",
52
+ }));
53
+ // The errors and the operation, and nothing that came off the wire. A request
54
+ // that fails validation is logged at error level, and the request it carries
55
+ // holds the caller's headers and cookies — an Authorization bearer among them,
56
+ // and for a calendar feed the token in the path is itself the credential
57
+ // (issue #1065). The path is redacted the same way the request scope redacts it.
58
+ api.register("validationFail", (c, req) => {
59
+ logger.error({
60
+ errors: c.validation.errors,
61
+ path: redactCalendarFeedPath(req.path),
62
+ operation: c.operation?.operationId,
63
+ }, "Validation failed");
64
+ return {
65
+ statusCode: 400,
66
+ body: {
67
+ code: defaultErrorCode(400),
68
+ message: "Invalid request",
69
+ errors: validationDetails(c.validation.errors),
70
+ },
71
+ };
72
+ });
73
+ api.register("notFound", () => ({
74
+ statusCode: 404,
75
+ body: { code: defaultErrorCode(404), message: "Not found" },
76
+ }));
77
+ api.register("notImplemented", async (c) => {
78
+ const { status, mock } = api.mockResponseForOperation(c.operation.operationId);
79
+ return formatResponse(mock, status);
80
+ });
81
+ api.register(handlers);
82
+ api.init();
83
+ export { setLogger } from "@remit/logger";
84
+ export { api };
85
+ const readOriginHeader = (headers) => {
86
+ if (!headers)
87
+ return undefined;
88
+ for (const [key, value] of Object.entries(headers)) {
89
+ if (key.toLowerCase() === "origin" && typeof value === "string") {
90
+ return value;
91
+ }
92
+ }
93
+ return undefined;
94
+ };
95
+ // A scope, not `logger.setBindings`: this process serves requests concurrently,
96
+ // and bindings on the shared logger belong to whichever request wrote them last,
97
+ // so a line gets attributed to the wrong request. The scope follows the request
98
+ // through its own async continuations and nothing else. It nests inside the one
99
+ // `withTelemetry` opens, which is where `requestId` comes from.
100
+ export const handleApiEvent = async (event, context) => {
101
+ // A calendar feed carries its credential in the path, so the raw path is a
102
+ // secret and cannot be a log field (issue #1065). Redacted once, here, where
103
+ // every line inside the request picks it up from the scope.
104
+ const path = redactCalendarFeedPath(event.path);
105
+ return withLogContext({
106
+ path,
107
+ method: event.httpMethod,
108
+ }, async () => {
109
+ logger.debug({ method: event.httpMethod, path }, "Request received");
110
+ const origin = readOriginHeader(event.headers);
111
+ const startedAt = Date.now();
112
+ // Capture the correlation id here: it is seeded by
113
+ // runWithRequestContext below (from context.awsRequestId), but that
114
+ // AsyncLocalStorage scope does not reliably survive the
115
+ // openapi-backend handleRequest call chain, so we read it from
116
+ // `context.awsRequestId` directly rather than via
117
+ // getRequestCorrelationId at log time.
118
+ const correlationId = context.awsRequestId;
119
+ const result = await runWithRequestContext({ origin, correlationId }, async () => {
120
+ if (usesBetterAuthJwt()) {
121
+ const denied = await authenticateSelfHostRequest(event);
122
+ if (denied)
123
+ return denied;
124
+ }
125
+ return api
126
+ .handleRequest(normalizeRequest(event), event, context)
127
+ .catch(handleError);
128
+ });
129
+ // One structured access line per request: method, path, status, and
130
+ // duration. The correlation id is inlined as a top-level field so a bug
131
+ // report quoting the id can grep logs by it directly, and emitted at
132
+ // info so it is on by default in production (the debug "Request
133
+ // received" above already filters out under normal log levels).
134
+ // biome-ignore lint/plugin/no-logger-info: one access line per request is an audit-grade signal (issue #1054)
135
+ logger.info({
136
+ method: event.httpMethod,
137
+ path,
138
+ statusCode: result.statusCode,
139
+ durationMs: Date.now() - startedAt,
140
+ correlationId,
141
+ }, "Request completed");
142
+ return result;
143
+ });
144
+ };
@@ -0,0 +1,30 @@
1
+ import type { APIGatewayProxyEvent } from "aws-lambda";
2
+ /**
3
+ * The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
4
+ * for local dev and fixtures: with them set, an unauthenticated request is served
5
+ * as the pinned account. They must never reach a deployed environment. Refuse to
6
+ * start (throw at module load) if either is present outside a local NODE_ENV — the
7
+ * same `development`/`test` signal the data layer already uses to decide local vs
8
+ * deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
9
+ * boot, not exploited at runtime.
10
+ */
11
+ export declare const assertLocalBypassNotInDeployedEnv: (env?: NodeJS.ProcessEnv) => void;
12
+ /**
13
+ * Return the Cognito `sub` for the current request, preferring the JWT claim
14
+ * and falling back to `LOCAL_COGNITO_SUB` for local development. Returns
15
+ * `undefined` when neither is available (e.g. seeded fixtures that only set
16
+ * `LOCAL_ACCOUNT_CONFIG_ID`).
17
+ */
18
+ export declare const getSubFromEvent: (event: APIGatewayProxyEvent) => string | undefined;
19
+ /**
20
+ * Extract the caller's accountConfigId from an API Gateway event.
21
+ *
22
+ * Production: reads the Cognito `sub` claim (verified by the API Gateway
23
+ * Cognito authorizer) and derives the accountConfigId via UUIDv5.
24
+ *
25
+ * Local development: honours two environment overrides so the dev server and
26
+ * playwright fixtures can run without a real Cognito token:
27
+ * - `LOCAL_ACCOUNT_CONFIG_ID` — pin a specific seeded accountConfigId
28
+ * - `LOCAL_COGNITO_SUB` — pin a Cognito `sub` value; derived like production
29
+ */
30
+ export declare const getAccountConfigIdFromEvent: (event: APIGatewayProxyEvent) => string;
@@ -0,0 +1,73 @@
1
+ import { deriveAccountConfigId } from "@remit/data-ports/id";
2
+ const LOCAL_BYPASS_VARS = [
3
+ "LOCAL_ACCOUNT_CONFIG_ID",
4
+ "LOCAL_COGNITO_SUB",
5
+ ];
6
+ const isLocalEnv = (env) => env.NODE_ENV === "development" || env.NODE_ENV === "test";
7
+ /**
8
+ * The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
9
+ * for local dev and fixtures: with them set, an unauthenticated request is served
10
+ * as the pinned account. They must never reach a deployed environment. Refuse to
11
+ * start (throw at module load) if either is present outside a local NODE_ENV — the
12
+ * same `development`/`test` signal the data layer already uses to decide local vs
13
+ * deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
14
+ * boot, not exploited at runtime.
15
+ */
16
+ export const assertLocalBypassNotInDeployedEnv = (env = process.env) => {
17
+ if (isLocalEnv(env))
18
+ return;
19
+ const leaked = LOCAL_BYPASS_VARS.filter((name) => {
20
+ const value = env[name];
21
+ return typeof value === "string" && value.length > 0;
22
+ });
23
+ if (leaked.length === 0)
24
+ return;
25
+ throw new Error(`Refusing to start: local auth bypass ${leaked.join(", ")} is set while NODE_ENV=${env.NODE_ENV ?? "(unset)"}. These overrides bypass authentication and must never be present in a deployed environment.`);
26
+ };
27
+ const getSubFromClaims = (event) => {
28
+ const claims = event.requestContext?.authorizer?.claims;
29
+ if (!claims)
30
+ return undefined;
31
+ const sub = claims.sub;
32
+ if (typeof sub === "string" && sub.length > 0)
33
+ return sub;
34
+ return undefined;
35
+ };
36
+ /**
37
+ * Return the Cognito `sub` for the current request, preferring the JWT claim
38
+ * and falling back to `LOCAL_COGNITO_SUB` for local development. Returns
39
+ * `undefined` when neither is available (e.g. seeded fixtures that only set
40
+ * `LOCAL_ACCOUNT_CONFIG_ID`).
41
+ */
42
+ export const getSubFromEvent = (event) => {
43
+ const sub = getSubFromClaims(event);
44
+ if (sub)
45
+ return sub;
46
+ const localSub = process.env.LOCAL_COGNITO_SUB;
47
+ if (localSub && localSub.length > 0)
48
+ return localSub;
49
+ return undefined;
50
+ };
51
+ /**
52
+ * Extract the caller's accountConfigId from an API Gateway event.
53
+ *
54
+ * Production: reads the Cognito `sub` claim (verified by the API Gateway
55
+ * Cognito authorizer) and derives the accountConfigId via UUIDv5.
56
+ *
57
+ * Local development: honours two environment overrides so the dev server and
58
+ * playwright fixtures can run without a real Cognito token:
59
+ * - `LOCAL_ACCOUNT_CONFIG_ID` — pin a specific seeded accountConfigId
60
+ * - `LOCAL_COGNITO_SUB` — pin a Cognito `sub` value; derived like production
61
+ */
62
+ export const getAccountConfigIdFromEvent = (event) => {
63
+ const sub = getSubFromClaims(event);
64
+ if (sub)
65
+ return deriveAccountConfigId(sub);
66
+ const localAccountConfigId = process.env.LOCAL_ACCOUNT_CONFIG_ID;
67
+ if (localAccountConfigId)
68
+ return localAccountConfigId;
69
+ const localSub = process.env.LOCAL_COGNITO_SUB;
70
+ if (localSub)
71
+ return deriveAccountConfigId(localSub);
72
+ throw new Error("Missing accountConfigId: no Cognito `sub` claim on the request, and neither LOCAL_ACCOUNT_CONFIG_ID nor LOCAL_COGNITO_SUB env var is set");
73
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Typed reads for Microsoft OAuth (Entra) environment variables.
3
+ *
4
+ * `MSOAUTH_REDIRECT_URI` is derived from `PUBLIC_ORIGIN` by the compose file
5
+ * (deploy/vps/docker-compose.sqlite.yml) and must match the redirect URI
6
+ * registered on the Entra app registration.
7
+ *
8
+ * Credentials come either from `MSOAUTH_CLIENT_ID` / `MSOAUTH_CLIENT_SECRET`
9
+ * in the deploy env file, or from Secrets Manager via `MSOAUTH_SECRET_ARN`.
10
+ *
11
+ * The Microsoft authorization and token endpoints are fixed in
12
+ * `@remit/mail-oauth-service`; no authority URL is read from the environment.
13
+ */
14
+ export interface MsOAuthConfig {
15
+ /** Secrets Manager ARN — an alternative to the client id/secret pair. */
16
+ readonly secretArn: string | undefined;
17
+ /** OAuth redirect URI registered in the Entra app. */
18
+ readonly redirectUri: string;
19
+ /** Client ID — set directly to bypass Secrets Manager. */
20
+ readonly clientId: string | undefined;
21
+ /** Client secret — set directly to bypass Secrets Manager. */
22
+ readonly clientSecret: string | undefined;
23
+ /** Token endpoint override — used for local stubbing (bypasses default OIDC discovery). */
24
+ readonly tokenEndpoint: string | undefined;
25
+ }
26
+ /**
27
+ * Read Microsoft OAuth configuration from the environment.
28
+ * Throws when `MSOAUTH_REDIRECT_URI` is absent.
29
+ */
30
+ export declare const getMsOAuthConfig: () => MsOAuthConfig;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Typed reads for Microsoft OAuth (Entra) environment variables.
3
+ *
4
+ * `MSOAUTH_REDIRECT_URI` is derived from `PUBLIC_ORIGIN` by the compose file
5
+ * (deploy/vps/docker-compose.sqlite.yml) and must match the redirect URI
6
+ * registered on the Entra app registration.
7
+ *
8
+ * Credentials come either from `MSOAUTH_CLIENT_ID` / `MSOAUTH_CLIENT_SECRET`
9
+ * in the deploy env file, or from Secrets Manager via `MSOAUTH_SECRET_ARN`.
10
+ *
11
+ * The Microsoft authorization and token endpoints are fixed in
12
+ * `@remit/mail-oauth-service`; no authority URL is read from the environment.
13
+ */
14
+ /**
15
+ * Read Microsoft OAuth configuration from the environment.
16
+ * Throws when `MSOAUTH_REDIRECT_URI` is absent.
17
+ */
18
+ export const getMsOAuthConfig = () => {
19
+ const redirectUri = process.env.MSOAUTH_REDIRECT_URI;
20
+ if (!redirectUri) {
21
+ throw new Error("MSOAUTH_REDIRECT_URI is not set. It is derived from PUBLIC_ORIGIN — set PUBLIC_ORIGIN in the deploy env file (deploy/vps/.env) and restart, or set MSOAUTH_REDIRECT_URI directly.");
22
+ }
23
+ return {
24
+ secretArn: process.env.MSOAUTH_SECRET_ARN,
25
+ redirectUri,
26
+ clientId: process.env.MSOAUTH_CLIENT_ID,
27
+ clientSecret: process.env.MSOAUTH_CLIENT_SECRET,
28
+ tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT,
29
+ };
30
+ };
@@ -2,13 +2,11 @@
2
2
  * The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
3
3
  * path.
4
4
  */
5
- export const isSelfHostSqlBackend = (): boolean =>
6
- process.env.DATA_BACKEND === "sqlite";
7
-
5
+ export declare const isSelfHostSqlBackend: () => boolean;
8
6
  /**
9
7
  * The self-host SQL backend authenticates requests and signs content URLs with
10
8
  * a better-auth RS256 JWT verified at the edge and re-verified in-process, no
11
9
  * Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
12
10
  * content guard) and is deliberately excluded.
13
11
  */
14
- export const usesBetterAuthJwt = (): boolean => isSelfHostSqlBackend();
12
+ export declare const usesBetterAuthJwt: () => boolean;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
3
+ * path.
4
+ */
5
+ export const isSelfHostSqlBackend = () => process.env.DATA_BACKEND === "sqlite";
6
+ /**
7
+ * The self-host SQL backend authenticates requests and signs content URLs with
8
+ * a better-auth RS256 JWT verified at the edge and re-verified in-process, no
9
+ * Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
10
+ * content guard) and is deliberately excluded.
11
+ */
12
+ export const usesBetterAuthJwt = () => isSelfHostSqlBackend();
@@ -1,7 +1,5 @@
1
1
  import type { AutoMovedInfo } from "@remit/api-openapi-types";
2
2
  import type { MessageItem } from "@remit/data-ports";
3
- import { PlacementConfidence } from "@remit/domain-enums";
4
-
5
3
  /**
6
4
  * Project a Message's internal auto-move state to the public `AutoMovedInfo`
7
5
  * shape. Present only for a real, in-effect auto-move; absent otherwise, so a
@@ -27,30 +25,4 @@ import { PlacementConfidence } from "@remit/domain-enums";
27
25
  * The client derives everything else (whether the move is still in effect, the
28
26
  * undo target) from these fields plus current placement.
29
27
  */
30
- export const deriveAutoMoved = (
31
- message: Pick<
32
- MessageItem,
33
- "movedByRemit" | "placementVerdict" | "filterMove"
34
- >,
35
- ): AutoMovedInfo | undefined => {
36
- if (message.movedByRemit !== true) return undefined;
37
-
38
- const filterMove = message.filterMove;
39
- if (filterMove) {
40
- return {
41
- fromMailboxId: filterMove.sourceMailboxId,
42
- destinationMailboxId: filterMove.destinationMailboxId,
43
- filterId: filterMove.filterId,
44
- };
45
- }
46
-
47
- const verdict = message.placementVerdict;
48
- if (!verdict) return undefined;
49
- if (verdict.confidence !== PlacementConfidence.Confident) return undefined;
50
- if (verdict.dryRun === true) return undefined;
51
-
52
- return {
53
- action: verdict.action,
54
- fromPlacement: verdict.fromPlacement,
55
- };
56
- };
28
+ export declare const deriveAutoMoved: (message: Pick<MessageItem, "movedByRemit" | "placementVerdict" | "filterMove">) => AutoMovedInfo | undefined;
@@ -0,0 +1,49 @@
1
+ import { PlacementConfidence } from "@remit/domain-enums";
2
+ /**
3
+ * Project a Message's internal auto-move state to the public `AutoMovedInfo`
4
+ * shape. Present only for a real, in-effect auto-move; absent otherwise, so a
5
+ * message synced before either mechanism existed (no `filterMove`, no
6
+ * `placementVerdict`) projects nothing and renders no badge.
7
+ *
8
+ * Two mechanisms move mail and both raise `movedByRemit`:
9
+ *
10
+ * 1. A standing filter (RFC 034) records a `filterMove` marker naming the
11
+ * source folder, the destination folder, and the filter. It has no
12
+ * classifier direction/role, so the projection carries mailbox ids
13
+ * (`fromMailboxId`/`destinationMailboxId`) and the `filterId` for the
14
+ * Settings link — an arbitrary destination the Inbox/Junk-shaped `action`
15
+ * cannot name.
16
+ * 2. The Tier 0 classifier records a `placementVerdict`. Its `action`
17
+ * (MoveToInbox/MoveToJunk) and `fromPlacement` (inbox/junk/other) are the
18
+ * only fields that cross to the wire; confidence, dryRun, decidedAt and
19
+ * reasons stay internal.
20
+ *
21
+ * The filter marker outranks the verdict check: a matched filter's move is
22
+ * exclusive and outranks the classifier's placement move at index time (RFC 034
23
+ * Decision 3.1), so a message carrying both was ultimately moved by the filter.
24
+ * The client derives everything else (whether the move is still in effect, the
25
+ * undo target) from these fields plus current placement.
26
+ */
27
+ export const deriveAutoMoved = (message) => {
28
+ if (message.movedByRemit !== true)
29
+ return undefined;
30
+ const filterMove = message.filterMove;
31
+ if (filterMove) {
32
+ return {
33
+ fromMailboxId: filterMove.sourceMailboxId,
34
+ destinationMailboxId: filterMove.destinationMailboxId,
35
+ filterId: filterMove.filterId,
36
+ };
37
+ }
38
+ const verdict = message.placementVerdict;
39
+ if (!verdict)
40
+ return undefined;
41
+ if (verdict.confidence !== PlacementConfidence.Confident)
42
+ return undefined;
43
+ if (verdict.dryRun === true)
44
+ return undefined;
45
+ return {
46
+ action: verdict.action,
47
+ fromPlacement: verdict.fromPlacement,
48
+ };
49
+ };
@@ -0,0 +1,41 @@
1
+ import { type SignedPathFailure } from "@remit/storage-service/signed-path";
2
+ /**
3
+ * Default validity window for a signed content URL. Long enough to cover a
4
+ * viewing session (react-query caches describeMessage for 30 min and inline
5
+ * images may load late), short enough to bound the replay window if a URL
6
+ * leaks.
7
+ */
8
+ export declare const CONTENT_URL_TTL_SECONDS = 3600;
9
+ export interface ContentSignature {
10
+ exp: number;
11
+ sig: string;
12
+ }
13
+ /**
14
+ * A function that signs a decoded, account-scoped storage path and returns the
15
+ * `exp`/`sig` query params to append to its content URL.
16
+ */
17
+ export type ContentSigner = (relativePath: string) => ContentSignature;
18
+ export declare const createContentSigner: (masterSecret: string, ttlSeconds?: number) => ContentSigner;
19
+ /**
20
+ * Build the content-URL signer for the current environment, or `undefined` when
21
+ * signing does not apply. Signing is enforced on the self-host SQL backend,
22
+ * which serves `/content/*` straight from the backend container; on AWS the
23
+ * Lambda@Edge JWT verifier guards `/content/*` and the URL stays unsigned so
24
+ * CloudFront/S3 behaviour is unchanged. Throws on the self-host backend when the
25
+ * master secret is missing, so a misconfigured deploy fails loud rather than
26
+ * shipping unsigned (unauthenticated) content URLs.
27
+ */
28
+ export declare const getContentSigner: () => ContentSigner | undefined;
29
+ export type ContentSignatureFailure = SignedPathFailure;
30
+ export type ContentSignatureResult = {
31
+ valid: true;
32
+ } | {
33
+ valid: false;
34
+ reason: ContentSignatureFailure;
35
+ };
36
+ /**
37
+ * Verify a signed content request. Recomputes the HMAC over the requested path
38
+ * and the supplied expiry and constant-time compares it against the presented
39
+ * signature. Pure so the decision can be unit-tested without a live server.
40
+ */
41
+ export declare const verifyContentSignature: (relativePath: string, expRaw: string | undefined, sig: string | undefined, masterSecret: string, nowSeconds: number) => ContentSignatureResult;