@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
@@ -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,5 @@
1
+ import { type Document, OpenAPIBackend } from "openapi-backend";
2
+ export declare const OpenAPISpec: Document;
3
+ declare const api: OpenAPIBackend;
4
+ export { setLogger } from "@remit/logger";
5
+ export { api };
@@ -0,0 +1,73 @@
1
+ import spec from "@remit/api-openapi-spec/openapi.json" with { type: "json" };
2
+ import { redactCalendarFeedPath } from "@remit/calendar-service";
3
+ import { logger } from "@remit/logger";
4
+ import { OpenAPIBackend, } from "openapi-backend";
5
+ import { defaultErrorCode } from "./error.js";
6
+ import { postResponseHandler } from "./response.js";
7
+ const isDocument = (value) => typeof value === "object" &&
8
+ value !== null &&
9
+ "openapi" in value &&
10
+ typeof value.openapi === "string" &&
11
+ "paths" in value;
12
+ // Imported, not read from disk, so no host has to place openapi.json next to the
13
+ // entry point — a Worker has no filesystem to read it from.
14
+ if (!isDocument(spec)) {
15
+ throw new Error("@remit/api-openapi-spec is not an OpenAPI document");
16
+ }
17
+ // Typed as a Document so importers do not see the JSON literal type.
18
+ export const OpenAPISpec = spec;
19
+ const api = new OpenAPIBackend({
20
+ definition: OpenAPISpec,
21
+ quick: true,
22
+ // Coerce query/path params to their schema types. API Gateway delivers every
23
+ // query param as a string; handlers cast them to number/boolean (e.g. `limit`,
24
+ // `hasAttachment`) and forward them downstream. Without coercion a string
25
+ // `limit` reaches ElectroDB's `.go({ limit })` and DynamoDB rejects the
26
+ // numeric Limit field ("STRING_VALUE cannot be converted to Integer"), 500ing
27
+ // /addresses/search.
28
+ coerceTypes: true,
29
+ });
30
+ api.register("postResponseHandler", postResponseHandler);
31
+ /**
32
+ * The validator's findings, narrowed to the three fields `RequestValidationDetail`
33
+ * declares. Copied field by field rather than passed through: an AJV error also
34
+ * carries `params`, and under `verbose` the rejected `data` itself — which for a
35
+ * refused request body is whatever the caller sent, a password among the
36
+ * possibilities. Naming the fields is what keeps that off the wire.
37
+ */
38
+ const validationDetails = (errors) => (errors ?? []).map((error) => ({
39
+ path: error.instancePath,
40
+ rule: error.keyword,
41
+ message: error.message ?? "refused",
42
+ }));
43
+ // The errors and the operation, and nothing that came off the wire. A request
44
+ // that fails validation is logged at error level, and the request it carries
45
+ // holds the caller's headers and cookies — an Authorization bearer among them,
46
+ // and for a calendar feed the token in the path is itself the credential
47
+ // (issue #1065). The path is redacted the same way the request scope redacts it.
48
+ api.register("validationFail", (c) => {
49
+ logger.error({
50
+ errors: c.validation.errors,
51
+ path: redactCalendarFeedPath(c.request.path),
52
+ operation: c.operation?.operationId,
53
+ }, "Validation failed");
54
+ return {
55
+ statusCode: 400,
56
+ body: {
57
+ code: defaultErrorCode(400),
58
+ message: "Invalid request",
59
+ errors: validationDetails(c.validation.errors),
60
+ },
61
+ };
62
+ });
63
+ api.register("notFound", () => ({
64
+ statusCode: 404,
65
+ body: { code: defaultErrorCode(404), message: "Not found" },
66
+ }));
67
+ api.register("notImplemented", () => ({
68
+ statusCode: 501,
69
+ body: { code: defaultErrorCode(501), message: "Not implemented" },
70
+ }));
71
+ api.init();
72
+ export { setLogger } from "@remit/logger";
73
+ export { api };
@@ -0,0 +1,24 @@
1
+ import type { APIGatewayProxyEvent } from "aws-lambda";
2
+ import type { HandlerIdentity } from "./types.js";
3
+ /**
4
+ * The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
5
+ * for local dev and fixtures: with them set, an unauthenticated request is served
6
+ * as the pinned account. They must never reach a deployed environment. Refuse to
7
+ * start (throw at module load) if either is present outside a local NODE_ENV — the
8
+ * same `development`/`test` signal the data layer already uses to decide local vs
9
+ * deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
10
+ * boot, not exploited at runtime. In a local NODE_ENV the bypass is read here
11
+ * once, so a bypass set without LOCAL_EMAIL also fails at boot.
12
+ */
13
+ export declare const assertLocalBypassNotInDeployedEnv: (env: NodeJS.ProcessEnv) => void;
14
+ export declare const userFromClaims: (sub: string, email: string | undefined) => HandlerIdentity;
15
+ export declare const localIdentityFromEnv: (env: NodeJS.ProcessEnv) => HandlerIdentity;
16
+ /**
17
+ * The caller of an API Gateway event. Production reads the Cognito `sub` and
18
+ * `email` claims (verified by the authorizer, or by `jwt-auth.ts` on
19
+ * self-host) and derives the accountConfigId via UUIDv5. Local development
20
+ * honours `LOCAL_ACCOUNT_CONFIG_ID` (a pinned seeded config) and
21
+ * `LOCAL_COGNITO_SUB` (a pinned sub, derived like production), read once at
22
+ * boot into `localIdentity`.
23
+ */
24
+ export declare const getIdentityFromEvent: (event: APIGatewayProxyEvent, localIdentity: HandlerIdentity) => HandlerIdentity;
@@ -0,0 +1,81 @@
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. In a local NODE_ENV the bypass is read here
15
+ * once, so a bypass set without LOCAL_EMAIL also fails at boot.
16
+ */
17
+ export const assertLocalBypassNotInDeployedEnv = (env) => {
18
+ if (isLocalEnv(env)) {
19
+ localIdentityFromEnv(env);
20
+ return;
21
+ }
22
+ const leaked = LOCAL_BYPASS_VARS.filter((name) => {
23
+ const value = env[name];
24
+ return typeof value === "string" && value.length > 0;
25
+ });
26
+ if (leaked.length === 0)
27
+ return;
28
+ 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.`);
29
+ };
30
+ const readClaim = (event, name) => {
31
+ const value = event.requestContext?.authorizer?.claims?.[name];
32
+ if (typeof value === "string" && value.length > 0)
33
+ return value;
34
+ return undefined;
35
+ };
36
+ const ANONYMOUS = { kind: "anonymous" };
37
+ export const userFromClaims = (sub, email) => {
38
+ if (!email)
39
+ return ANONYMOUS;
40
+ return {
41
+ kind: "user",
42
+ accountConfigId: deriveAccountConfigId(sub),
43
+ sub,
44
+ email,
45
+ };
46
+ };
47
+ const pinnedLocalUser = (env) => {
48
+ const localAccountConfigId = env.LOCAL_ACCOUNT_CONFIG_ID;
49
+ if (localAccountConfigId) {
50
+ return { accountConfigId: localAccountConfigId, sub: localAccountConfigId };
51
+ }
52
+ const localSub = env.LOCAL_COGNITO_SUB;
53
+ if (localSub) {
54
+ return { accountConfigId: deriveAccountConfigId(localSub), sub: localSub };
55
+ }
56
+ return undefined;
57
+ };
58
+ export const localIdentityFromEnv = (env) => {
59
+ const pinned = pinnedLocalUser(env);
60
+ if (!pinned)
61
+ return ANONYMOUS;
62
+ const email = env.LOCAL_EMAIL;
63
+ if (!email) {
64
+ throw new Error("Refusing to start: the local auth bypass (LOCAL_ACCOUNT_CONFIG_ID or LOCAL_COGNITO_SUB) is set without LOCAL_EMAIL. The bypass serves a signed-in user, and every user carries an email; set LOCAL_EMAIL to the address that user signs in with.");
65
+ }
66
+ return { kind: "user", ...pinned, email };
67
+ };
68
+ /**
69
+ * The caller of an API Gateway event. Production reads the Cognito `sub` and
70
+ * `email` claims (verified by the authorizer, or by `jwt-auth.ts` on
71
+ * self-host) and derives the accountConfigId via UUIDv5. Local development
72
+ * honours `LOCAL_ACCOUNT_CONFIG_ID` (a pinned seeded config) and
73
+ * `LOCAL_COGNITO_SUB` (a pinned sub, derived like production), read once at
74
+ * boot into `localIdentity`.
75
+ */
76
+ export const getIdentityFromEvent = (event, localIdentity) => {
77
+ const sub = readClaim(event, "sub");
78
+ if (!sub)
79
+ return localIdentity;
80
+ return userFromClaims(sub, readClaim(event, "email"));
81
+ };
@@ -0,0 +1,20 @@
1
+ import type { MsOAuthConfig } from "./msoauth.js";
2
+ export type AuthConfig = {
3
+ mode: "BetterAuthJwt";
4
+ contentSigningSecret: string;
5
+ } | {
6
+ mode: "GatewayAuthorizer";
7
+ };
8
+ export type ResponseValidation = "Log" | "Skip";
9
+ export type SelfUpdate = "Enabled" | "Disabled";
10
+ export interface ApiConfig {
11
+ auth: AuthConfig;
12
+ corsAllowedOrigins: readonly string[];
13
+ publicOrigin: string;
14
+ contentDeliveryDomain: string;
15
+ msOAuth: MsOAuthConfig;
16
+ selfUpdate: SelfUpdate;
17
+ updateControlDir: string;
18
+ commit: string;
19
+ responseValidation: ResponseValidation;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ApiConfig } from "./api-config.js";
2
+ export declare const configFromEnv: (env: NodeJS.ProcessEnv) => ApiConfig;
@@ -0,0 +1,65 @@
1
+ import { msOAuthCredentialsFromSecret } from "./msoauth-secrets-manager.js";
2
+ const DEFAULT_UPDATE_CONTROL_DIR = "/data/control";
3
+ const optional = (env, key) => env[key] || undefined;
4
+ const required = (env, key, reason) => {
5
+ const value = optional(env, key);
6
+ if (!value)
7
+ throw new Error(`Refusing to start: ${key} is not set. ${reason}`);
8
+ return value;
9
+ };
10
+ const commaList = (raw) => (raw ?? "")
11
+ .split(",")
12
+ .map((origin) => origin.trim())
13
+ .filter((origin) => origin.length > 0);
14
+ const authFromEnv = (env) => env.DATA_BACKEND === "sqlite"
15
+ ? {
16
+ mode: "BetterAuthJwt",
17
+ contentSigningSecret: required(env, "BETTER_AUTH_SECRET", "The self-host SQL backend signs content URLs with it."),
18
+ }
19
+ : { mode: "GatewayAuthorizer" };
20
+ const contentDeliveryDomainFromEnv = (env, auth, publicOrigin) => {
21
+ if (auth.mode === "BetterAuthJwt") {
22
+ return optional(env, "CONTENT_DELIVERY_DOMAIN") ?? publicOrigin;
23
+ }
24
+ return required(env, "CONTENT_DELIVERY_DOMAIN", "BodyPartResponse.contentUrl is built from the CloudFront distribution domain (#299); wire it via the API Lambda env (see infra/bin/infra.ts).");
25
+ };
26
+ const msOAuthCredentialsFromEnv = (env) => {
27
+ const clientId = optional(env, "MSOAUTH_CLIENT_ID");
28
+ const clientSecret = optional(env, "MSOAUTH_CLIENT_SECRET");
29
+ if (clientId && clientSecret) {
30
+ return async () => ({ clientId, clientSecret });
31
+ }
32
+ const secretArn = optional(env, "MSOAUTH_SECRET_ARN");
33
+ if (secretArn)
34
+ return msOAuthCredentialsFromSecret(secretArn);
35
+ return undefined;
36
+ };
37
+ const msOAuthFromEnv = (env) => {
38
+ const credentials = msOAuthCredentialsFromEnv(env);
39
+ if (!credentials)
40
+ return { mode: "Disabled" };
41
+ const tokenEndpoint = optional(env, "MSOAUTH_TOKEN_ENDPOINT");
42
+ return {
43
+ mode: "Enabled",
44
+ redirectUri: required(env, "MSOAUTH_REDIRECT_URI", "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."),
45
+ overrides: tokenEndpoint ? { tokenEndpoint } : {},
46
+ credentials,
47
+ };
48
+ };
49
+ export const configFromEnv = (env) => {
50
+ const publicOrigin = required(env, "PUBLIC_ORIGIN", "It is the origin the web client is served from.");
51
+ const auth = authFromEnv(env);
52
+ return {
53
+ auth,
54
+ corsAllowedOrigins: commaList(env.CORS_ALLOWED_ORIGINS),
55
+ publicOrigin,
56
+ contentDeliveryDomain: contentDeliveryDomainFromEnv(env, auth, publicOrigin),
57
+ msOAuth: msOAuthFromEnv(env),
58
+ selfUpdate: optional(env, "REMIT_UPDATE_MANIFEST_URL")
59
+ ? "Enabled"
60
+ : "Disabled",
61
+ updateControlDir: optional(env, "REMIT_UPDATE_CONTROL_DIR") ?? DEFAULT_UPDATE_CONTROL_DIR,
62
+ commit: optional(env, "REMIT_WORKER_SHA") ?? optional(env, "GITHUB_SHA") ?? "dev",
63
+ responseValidation: env.STAGE_NAME === "dev" ? "Log" : "Skip",
64
+ };
65
+ };
@@ -0,0 +1,2 @@
1
+ import type { MsOAuthCredentials } from "./msoauth.js";
2
+ export declare const msOAuthCredentialsFromSecret: (secretArn: string) => () => Promise<MsOAuthCredentials>;
@@ -0,0 +1,28 @@
1
+ import { GetSecretValueCommand, SecretsManagerClient, } from "@aws-sdk/client-secrets-manager";
2
+ import { z } from "zod";
3
+ const MsOAuthSecret = z.object({
4
+ clientId: z.string().min(1),
5
+ clientSecret: z.string().min(1),
6
+ });
7
+ let client = null;
8
+ const secretsManager = () => {
9
+ if (!client)
10
+ client = new SecretsManagerClient({});
11
+ return client;
12
+ };
13
+ const parseSecret = (raw) => {
14
+ try {
15
+ return JSON.parse(raw);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ export const msOAuthCredentialsFromSecret = (secretArn) => async () => {
22
+ const result = await secretsManager().send(new GetSecretValueCommand({ SecretId: secretArn }));
23
+ const parsed = MsOAuthSecret.safeParse(parseSecret(result.SecretString ?? ""));
24
+ if (!parsed.success) {
25
+ throw new Error("The Microsoft OAuth secret named by MSOAUTH_SECRET_ARN does not hold a JSON object with a non-empty clientId and clientSecret");
26
+ }
27
+ return parsed.data;
28
+ };
@@ -0,0 +1,19 @@
1
+ import type { OAuthProviderConfig } from "@remit/mail-oauth-service";
2
+ export interface MsOAuthCredentials {
3
+ clientId: string;
4
+ clientSecret: string;
5
+ }
6
+ export interface MsOAuthEnabled {
7
+ mode: "Enabled";
8
+ redirectUri: string;
9
+ overrides: Partial<Pick<OAuthProviderConfig, "tokenEndpoint">>;
10
+ credentials: () => Promise<MsOAuthCredentials>;
11
+ }
12
+ export type MsOAuthConfig = {
13
+ mode: "Disabled";
14
+ } | MsOAuthEnabled;
15
+ export interface LoadedMsOAuth {
16
+ config: MsOAuthEnabled;
17
+ credentials: MsOAuthCredentials;
18
+ }
19
+ export declare const loadMsOAuth: (config: MsOAuthConfig) => Promise<LoadedMsOAuth>;
@@ -0,0 +1,6 @@
1
+ export const loadMsOAuth = async (config) => {
2
+ if (config.mode === "Disabled") {
3
+ throw new Error("No Microsoft OAuth credentials are configured on this instance: set MSOAUTH_CLIENT_ID and MSOAUTH_CLIENT_SECRET, or MSOAUTH_SECRET_ARN");
4
+ }
5
+ return { config, credentials: await config.credentials() };
6
+ };
@@ -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,40 @@
1
+ import { type SignedPathFailure } from "@remit/storage-service/signed-path";
2
+ import type { ApiConfig } from "../config/api-config.js";
3
+ /**
4
+ * Default validity window for a signed content URL. Long enough to cover a
5
+ * viewing session (react-query caches describeMessage for 30 min and inline
6
+ * images may load late), short enough to bound the replay window if a URL
7
+ * leaks.
8
+ */
9
+ export declare const CONTENT_URL_TTL_SECONDS = 3600;
10
+ export interface ContentSignature {
11
+ exp: number;
12
+ sig: string;
13
+ }
14
+ /**
15
+ * A function that signs a decoded, account-scoped storage path and returns the
16
+ * `exp`/`sig` query params to append to its content URL.
17
+ */
18
+ export type ContentSigner = (relativePath: string) => ContentSignature;
19
+ export declare const createContentSigner: (masterSecret: string, ttlSeconds?: number) => ContentSigner;
20
+ /**
21
+ * Build the content-URL signer for the current environment, or `undefined` when
22
+ * signing does not apply. Signing is enforced on the self-host SQL backend,
23
+ * which serves `/content/*` straight from the backend container; on AWS the
24
+ * Lambda@Edge JWT verifier guards `/content/*` and the URL stays unsigned so
25
+ * CloudFront/S3 behaviour is unchanged.
26
+ */
27
+ export declare const getContentSigner: (config: ApiConfig) => ContentSigner | undefined;
28
+ export type ContentSignatureFailure = SignedPathFailure;
29
+ export type ContentSignatureResult = {
30
+ valid: true;
31
+ } | {
32
+ valid: false;
33
+ reason: ContentSignatureFailure;
34
+ };
35
+ /**
36
+ * Verify a signed content request. Recomputes the HMAC over the requested path
37
+ * and the supplied expiry and constant-time compares it against the presented
38
+ * signature. Pure so the decision can be unit-tested without a live server.
39
+ */
40
+ export declare const verifyContentSignature: (relativePath: string, expRaw: string | undefined, sig: string | undefined, masterSecret: string, nowSeconds: number) => ContentSignatureResult;