@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,16 @@
1
+ /**
2
+ * Map an Address row to the wire-format `VipSuggestionEntry` returned by
3
+ * `GET /me/vip-suggestions` (issue #234). Absent counters are normalised to
4
+ * zero so the UI can render `12 received · 4 sent · 2 replies` without a
5
+ * `?? 0` fallback at every render site.
6
+ */
7
+ export const toVipSuggestionEntry = (item) => ({
8
+ addressId: item.addressId,
9
+ displayName: item.displayName,
10
+ normalizedEmail: item.normalizedEmail,
11
+ inboundCount: item.inboundCount ?? 0,
12
+ outboundCount: item.outboundCount ?? 0,
13
+ replyCount: item.replyCount ?? 0,
14
+ lastInboundAt: item.lastInboundAt,
15
+ lastReplyAt: item.lastReplyAt,
16
+ });
@@ -0,0 +1,2 @@
1
+ export { api, handleApiEvent, OpenAPISpec } from "./api.js";
2
+ export declare const handler: (event: import("aws-lambda").APIGatewayProxyEvent, context: import("aws-lambda").Context) => Promise<import("aws-lambda").APIGatewayProxyResult>;
@@ -0,0 +1,37 @@
1
+ import { inspect } from "node:util";
2
+ import { logger } from "@remit/logger";
3
+ import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
4
+ import { handleApiEvent } from "./api.js";
5
+ installNodeLogger();
6
+ export { api, handleApiEvent, OpenAPISpec } from "./api.js";
7
+ /**
8
+ * Last-resort net for a leaked background rejection.
9
+ *
10
+ * Every fire-and-forget side effect is meant to be contained at its source (see
11
+ * service/fire-and-forget.ts), so reaching here is a bug — an uncontained
12
+ * `void promise` somewhere. The API process (dev-server / warm Lambda container)
13
+ * shares one event loop across requests, so without this handler such a leak
14
+ * would surface as a spurious 500 on whatever request was in flight. We log it
15
+ * LOUDLY with a distinct alert so it is observable, and deliberately do NOT exit
16
+ * — a stray background rejection must never take the API down or fail a request.
17
+ * (The worker process intentionally has no such net: there each message is its
18
+ * own invocation and a rejection should crash it.)
19
+ */
20
+ const unhandledRejectionRegistered = Symbol.for("remit.backend.unhandledRejectionNet");
21
+ const globalProcess = process;
22
+ if (!globalProcess[unhandledRejectionRegistered]) {
23
+ globalProcess[unhandledRejectionRegistered] = true;
24
+ process.on("unhandledRejection", (reason) => {
25
+ logger.error({
26
+ alert: "unhandled_rejection",
27
+ errorName: reason?.name,
28
+ errorCode: reason?.Code ??
29
+ reason?.code,
30
+ error: inspect(reason),
31
+ }, "Unhandled promise rejection leaked into the API event loop (contained, request not failed)");
32
+ });
33
+ }
34
+ // The Lambda and Node entry point. `withTelemetry` installs the Node logger and
35
+ // its process metrics; a host without Node's process APIs imports
36
+ // `@remit/backend/api` instead and plugs in its own logger with `setLogger`.
37
+ export const handler = withTelemetry(handleApiEvent);
@@ -5,7 +5,4 @@
5
5
  * absence (e.g. a third-party token, a dev-server response body) — never to
6
6
  * soften an internal contract, which must stay a fatal uncaught `JSON.parse`.
7
7
  */
8
- export const safeJsonParse = <T>(raw: string): Promise<T> =>
9
- new Promise((resolve) => {
10
- resolve(JSON.parse(raw));
11
- });
8
+ export declare const safeJsonParse: <T>(raw: string) => Promise<T>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Parse JSON in a Promise so a malformed string surfaces as a rejection the
3
+ * caller can `.catch()`, instead of a synchronous throw that would need a block
4
+ * try/catch. Use only where an unparseable value is an expected, recoverable
5
+ * absence (e.g. a third-party token, a dev-server response body) — never to
6
+ * soften an internal contract, which must stay a fatal uncaught `JSON.parse`.
7
+ */
8
+ export const safeJsonParse = (raw) => new Promise((resolve) => {
9
+ resolve(JSON.parse(raw));
10
+ });
@@ -0,0 +1,18 @@
1
+ import { type JwtVerifier } from "@remit/auth-service/verifier";
2
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
3
+ /** Test-only override for the memoized verifier. Pass null to reset. */
4
+ export declare const _setVerifierForTest: (v: JwtVerifier | null) => void;
5
+ /**
6
+ * Authenticate a self-host request from a better-auth RS256 JWT.
7
+ *
8
+ * On a valid token the verified `sub` is injected into the event's authorizer
9
+ * claims, exactly where the Cognito authorizer puts it, so every downstream
10
+ * handler reads identity through the unchanged `auth.ts` path. Returns a 401
11
+ * response (not null) when a token is present but fails verification, or when no
12
+ * token is supplied and no local test bypass is configured. Returns null to let
13
+ * the request proceed.
14
+ *
15
+ * When the edge tier (APISIX) has already verified and injected claims, this is
16
+ * a no-op — the backend trusts pre-populated claims and re-verifies otherwise.
17
+ */
18
+ export declare const authenticateSelfHostRequest: (event: APIGatewayProxyEvent) => Promise<APIGatewayProxyResult | null>;
@@ -0,0 +1,97 @@
1
+ import { createJwtVerifier, extractBearerToken, resolveVerifierConfig, } from "@remit/auth-service/verifier";
2
+ import { readCalendarFeedToken } from "@remit/calendar-service";
3
+ import { logger } from "@remit/logger";
4
+ import { defaultErrorCode } from "./error.js";
5
+ import { formatResponse } from "./response.js";
6
+ let verifier = null;
7
+ const getVerifier = () => {
8
+ if (!verifier)
9
+ verifier = createJwtVerifier(resolveVerifierConfig());
10
+ return verifier;
11
+ };
12
+ /** Test-only override for the memoized verifier. Pass null to reset. */
13
+ export const _setVerifierForTest = (v) => {
14
+ verifier = v;
15
+ };
16
+ const readHeader = (headers, name) => {
17
+ for (const [key, value] of Object.entries(headers ?? {})) {
18
+ if (key.toLowerCase() === name && typeof value === "string")
19
+ return value;
20
+ }
21
+ return undefined;
22
+ };
23
+ const unauthorized = (message) => formatResponse({ code: defaultErrorCode(401), message }, 401);
24
+ const injectClaims = (event, sub, email) => {
25
+ event.requestContext = {
26
+ ...event.requestContext,
27
+ authorizer: {
28
+ ...event.requestContext?.authorizer,
29
+ claims: {
30
+ sub,
31
+ email: email ?? "",
32
+ },
33
+ },
34
+ };
35
+ };
36
+ const hasLocalBypass = () => Boolean(process.env.LOCAL_ACCOUNT_CONFIG_ID);
37
+ /**
38
+ * The routes that run without a token.
39
+ *
40
+ * Microsoft redirects the browser here after consent, so the request carries no
41
+ * Authorization header and no session to derive one from. The handler takes its
42
+ * identity from the HMAC-signed `state` parameter and validates it there; that
43
+ * signature is the gate, not a JWT.
44
+ *
45
+ * A calendar feed carries its credential in the path for the same reason: a
46
+ * subscribed client polls a URL and has no session to present, so the token is
47
+ * the gate and the handler compares it against a stored hash.
48
+ *
49
+ * Hardcoded, matching the edge exemption in packages/apisix/src/route-table.ts.
50
+ * `@useAuth(NoAuth)` in the spec does not open a path here — a future public
51
+ * route is added by hand.
52
+ */
53
+ const PUBLIC_ROUTES = [
54
+ {
55
+ method: "GET",
56
+ matches: (path) => path === "/accounts/oauth/microsoft/callback",
57
+ },
58
+ {
59
+ method: "GET",
60
+ matches: (path) => readCalendarFeedToken(path) !== null,
61
+ },
62
+ ];
63
+ const isPublicRoute = (event) => PUBLIC_ROUTES.some((route) => route.method === event.httpMethod && route.matches(event.path));
64
+ /**
65
+ * Authenticate a self-host request from a better-auth RS256 JWT.
66
+ *
67
+ * On a valid token the verified `sub` is injected into the event's authorizer
68
+ * claims, exactly where the Cognito authorizer puts it, so every downstream
69
+ * handler reads identity through the unchanged `auth.ts` path. Returns a 401
70
+ * response (not null) when a token is present but fails verification, or when no
71
+ * token is supplied and no local test bypass is configured. Returns null to let
72
+ * the request proceed.
73
+ *
74
+ * When the edge tier (APISIX) has already verified and injected claims, this is
75
+ * a no-op — the backend trusts pre-populated claims and re-verifies otherwise.
76
+ */
77
+ export const authenticateSelfHostRequest = async (event) => {
78
+ if (isPublicRoute(event))
79
+ return null;
80
+ const existingSub = event.requestContext?.authorizer?.claims?.sub;
81
+ if (typeof existingSub === "string" && existingSub.length > 0)
82
+ return null;
83
+ const token = extractBearerToken(readHeader(event.headers, "authorization"));
84
+ if (token) {
85
+ const claims = await getVerifier()(token).catch((err) => {
86
+ logger.warn({ err: err instanceof Error ? err.message : String(err) }, "better-auth JWT verification failed");
87
+ return null;
88
+ });
89
+ if (!claims)
90
+ return unauthorized("Invalid or expired token");
91
+ injectClaims(event, claims.sub, claims.email);
92
+ return null;
93
+ }
94
+ if (hasLocalBypass())
95
+ return null;
96
+ return unauthorized("Authentication required");
97
+ };
@@ -0,0 +1,9 @@
1
+ interface RequestContext {
2
+ origin?: string;
3
+ correlationId?: string;
4
+ }
5
+ export declare const runWithRequestContext: <T>(ctx: RequestContext, fn: () => T) => T;
6
+ export declare const getRequestOrigin: () => string | undefined;
7
+ export declare const getRequestCorrelationId: () => string | undefined;
8
+ export declare const resolveAllowedOrigin: (origin: string | undefined) => string;
9
+ export {};
@@ -0,0 +1,22 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const storage = new AsyncLocalStorage();
3
+ export const runWithRequestContext = (ctx, fn) => storage.run(ctx, fn);
4
+ export const getRequestOrigin = () => storage.getStore()?.origin;
5
+ export const getRequestCorrelationId = () => storage.getStore()?.correlationId;
6
+ const parseAllowedOrigins = () => {
7
+ const raw = process.env.CORS_ALLOWED_ORIGINS;
8
+ if (!raw)
9
+ return [];
10
+ return raw
11
+ .split(",")
12
+ .map((s) => s.trim())
13
+ .filter((s) => s.length > 0);
14
+ };
15
+ export const resolveAllowedOrigin = (origin) => {
16
+ const allowed = parseAllowedOrigins();
17
+ if (allowed.length === 0)
18
+ return "*";
19
+ if (origin && allowed.includes(origin))
20
+ return origin;
21
+ return allowed[0];
22
+ };
@@ -0,0 +1,3 @@
1
+ import type { APIGatewayProxyEvent } from "aws-lambda";
2
+ import type { Request } from "openapi-backend";
3
+ export declare const normalizeRequest: (event: APIGatewayProxyEvent) => Request;
@@ -0,0 +1,7 @@
1
+ export const normalizeRequest = (event) => ({
2
+ method: event.httpMethod,
3
+ path: event.path,
4
+ query: (event.queryStringParameters ?? {}),
5
+ body: event.body,
6
+ headers: (event.headers ?? {}),
7
+ });
@@ -0,0 +1,27 @@
1
+ import type { APIGatewayProxyResult } from "aws-lambda";
2
+ import type { Context as OpenAPIContext } from "openapi-backend";
3
+ declare const RAW_API_RESPONSE: unique symbol;
4
+ /**
5
+ * A response the caller reads by something other than its JSON body — an
6
+ * iCalendar feed, an OAuth redirect's Location, and whatever later joins them.
7
+ *
8
+ * Every other handler returns a plain object and lets this module decide the
9
+ * status, the content type and the serialization. That is the right default and
10
+ * stays the default; a handler that has to own its own media type or headers
11
+ * says so with `rawApiResponse` rather than by returning a shape this one has
12
+ * to guess at.
13
+ * The CORS and correlation headers are still added here, so a raw response is
14
+ * not a way around them.
15
+ */
16
+ export interface RawApiResponse {
17
+ statusCode: number;
18
+ headers: Record<string, string>;
19
+ body: string;
20
+ }
21
+ type MarkedRawApiResponse = RawApiResponse & {
22
+ readonly [RAW_API_RESPONSE]: true;
23
+ };
24
+ export declare const rawApiResponse: (response: RawApiResponse) => MarkedRawApiResponse;
25
+ export declare const formatResponse: (body: Record<string, unknown>, statusCode?: number) => APIGatewayProxyResult;
26
+ export declare const postResponseHandler: (context: OpenAPIContext) => APIGatewayProxyResult;
27
+ export {};
@@ -0,0 +1,87 @@
1
+ import { logger } from "@remit/logger";
2
+ import { getRequestCorrelationId, getRequestOrigin, resolveAllowedOrigin, } from "./request-context.js";
3
+ const RAW_API_RESPONSE = Symbol("remit.rawApiResponse");
4
+ export const rawApiResponse = (response) => Object.assign(response, { [RAW_API_RESPONSE]: true });
5
+ const isRawApiResponse = (body) => RAW_API_RESPONSE in body;
6
+ /**
7
+ * The headers every response carries whatever its body is: the CORS grant the
8
+ * browser needs, and the id the request's log lines are already tagged with.
9
+ * Returning that id is what makes a bug report's "correlation id" resolve to a
10
+ * server-side line; the browser can only read a non-safelisted header when it
11
+ * is exposed.
12
+ */
13
+ const envelopeHeaders = () => {
14
+ const allowOrigin = resolveAllowedOrigin(getRequestOrigin());
15
+ const headers = {
16
+ "Access-Control-Allow-Origin": allowOrigin,
17
+ "Access-Control-Allow-Headers": "Authorization,Content-Type",
18
+ "Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
19
+ Vary: "Origin",
20
+ };
21
+ if (allowOrigin !== "*") {
22
+ headers["Access-Control-Allow-Credentials"] = "true";
23
+ }
24
+ const correlationId = getRequestCorrelationId();
25
+ if (correlationId) {
26
+ headers["x-correlation-id"] = correlationId;
27
+ headers["Access-Control-Expose-Headers"] = "x-correlation-id";
28
+ }
29
+ return headers;
30
+ };
31
+ export const formatResponse = (body, statusCode = 200) => {
32
+ if (body.statusCode && typeof body.statusCode === "number") {
33
+ statusCode = body.statusCode;
34
+ }
35
+ if (isRawApiResponse(body)) {
36
+ logger.debug({ statusCode: body.statusCode }, "response");
37
+ return {
38
+ statusCode: body.statusCode,
39
+ headers: { ...envelopeHeaders(), ...body.headers },
40
+ body: body.body,
41
+ };
42
+ }
43
+ if ("body" in body && body.body && typeof body.body === "object") {
44
+ body = body.body;
45
+ }
46
+ logger.debug({ statusCode }, "response");
47
+ return {
48
+ statusCode: statusCode,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ ...envelopeHeaders(),
52
+ },
53
+ body: JSON.stringify(body),
54
+ };
55
+ };
56
+ export const postResponseHandler = (context) => {
57
+ const { api, response, operation } = context;
58
+ if (response.statusCode)
59
+ return formatResponse(response, response.statusCode);
60
+ if (process.env.STAGE_NAME !== "dev")
61
+ return formatResponse(response, 200);
62
+ const { valid, errors } = api.validateResponse(response, operation);
63
+ if (valid)
64
+ return formatResponse(response, 200);
65
+ const { operationId } = operation;
66
+ const errorDetails = {
67
+ errors,
68
+ operationId,
69
+ responseKeys: Object.keys(response),
70
+ };
71
+ errors?.forEach((error) => {
72
+ if (error.instancePath) {
73
+ const pathParts = error.instancePath.split("/").filter(Boolean);
74
+ let value = response;
75
+ for (const part of pathParts) {
76
+ value = value?.[part];
77
+ }
78
+ errorDetails[`problematicValue_${error.instancePath}`] = {
79
+ path: error.instancePath,
80
+ value: JSON.stringify(value).substring(0, 500),
81
+ error: error.message,
82
+ };
83
+ }
84
+ });
85
+ logger.error(errorDetails, "Response validation failed");
86
+ return formatResponse(response, 200);
87
+ };
@@ -0,0 +1,22 @@
1
+ import { type SQSClient } from "@aws-sdk/client-sqs";
2
+ import type { AccountItem, IAccountRepository, IConfigImportRepository } from "@remit/data-ports";
3
+ import type { Logger } from "@remit/logger";
4
+ export interface AccountDeletionDeps {
5
+ account: Pick<IAccountRepository, "update">;
6
+ sqsClient: SQSClient;
7
+ accountFanoutQueueUrl: string;
8
+ log: Logger;
9
+ }
10
+ export declare const deleteAccount: (deps: AccountDeletionDeps, account: Pick<AccountItem, "accountId" | "accountConfigId">, deletedAt: number) => Promise<void>;
11
+ export interface HalfDeletedAccountDeps extends AccountDeletionDeps {
12
+ account: Pick<IAccountRepository, "update" | "listAll">;
13
+ configImport: Pick<IConfigImportRepository, "listByAccountConfig">;
14
+ }
15
+ /**
16
+ * Before #1384 a delete stored `isActive=false` and dropped `deletedAt`. Of the
17
+ * writers in this repo only that delete and a config import leave an account
18
+ * inactive, and an import names each account it creates in its stored
19
+ * document. The public update endpoint accepts `isActive` as sent, so a caller
20
+ * outside the repo can also leave one inactive.
21
+ */
22
+ export declare const finishHalfDeletedAccounts: (deps: HalfDeletedAccountDeps) => Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { SendMessageCommand } from "@aws-sdk/client-sqs";
2
+ import { readConfigDocument } from "@remit/config-format";
3
+ export const deleteAccount = async (deps, account, deletedAt) => {
4
+ const { accountId, accountConfigId } = account;
5
+ // Purge first: an account whose request never left still reads as not deleted, so it is found and asked for again.
6
+ await deps.sqsClient.send(new SendMessageCommand({
7
+ QueueUrl: deps.accountFanoutQueueUrl,
8
+ MessageBody: JSON.stringify({
9
+ type: "AccountDataPurge",
10
+ accountId,
11
+ accountConfigId,
12
+ }),
13
+ }));
14
+ await deps.account.update(accountId, { deletedAt, isActive: false });
15
+ deps.log.info({ accountId, accountConfigId }, "Account data purge initiated");
16
+ };
17
+ /**
18
+ * Before #1384 a delete stored `isActive=false` and dropped `deletedAt`. Of the
19
+ * writers in this repo only that delete and a config import leave an account
20
+ * inactive, and an import names each account it creates in its stored
21
+ * document. The public update endpoint accepts `isActive` as sent, so a caller
22
+ * outside the repo can also leave one inactive.
23
+ */
24
+ export const finishHalfDeletedAccounts = async (deps) => {
25
+ const imported = new Map();
26
+ for (const account of await deps.account.listAll()) {
27
+ if (account.isActive || account.deletedAt !== undefined)
28
+ continue;
29
+ let names = imported.get(account.accountConfigId);
30
+ if (!names) {
31
+ const imports = await deps.configImport.listByAccountConfig(account.accountConfigId);
32
+ names = new Set(imports.flatMap((row) => readConfigDocument(row.document).accounts.map((a) => a.accountId)));
33
+ imported.set(account.accountConfigId, names);
34
+ }
35
+ if (names.has(account.accountId))
36
+ continue;
37
+ await deleteAccount(deps, account, Date.now());
38
+ }
39
+ };
@@ -0,0 +1,2 @@
1
+ import { type RemitClient } from "./create-remit-client.js";
2
+ export declare const buildSqliteClient: () => Promise<RemitClient>;
@@ -0,0 +1,11 @@
1
+ import { createSqliteRepositories, openSqliteDatabases, sqliteDatabasePathsFromEnv, } from "@remit/drizzle-service";
2
+ import { buildSharedDeps, createRemitClient, } from "./create-remit-client.js";
3
+ // The SQLite adapter composition (RFC 036). Every repo runs on the database
4
+ // that holds its tables: app, tenant mail or tenant search (#1420).
5
+ export const buildSqliteClient = async () => {
6
+ const databases = await openSqliteDatabases(sqliteDatabasePathsFromEnv());
7
+ return createRemitClient({
8
+ repositories: createSqliteRepositories(databases),
9
+ ...buildSharedDeps(),
10
+ });
11
+ };
@@ -0,0 +1,95 @@
1
+ import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccountRepository, IAccountSettingRepository, IAddressRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarSuggestionRepository, IConfigImportRepository, IEnvelopeRepository, IFilterRepository, ILabelRepository, IMailboxLockRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageLabelRepository, IMessagePlacementMoveRepository, IMessageRepository, IOrganizeJobRequestRepository, IOutboxAttachmentRepository, IOutboxMessageRepository, IQuarantineRepository, ISenderSignerStandingRepository, ISyncWarningRepository, IThreadMessageRepository } from "@remit/data-ports";
2
+ import { BodySyncQueueService, BodySyncService, type FilterConfig, FlagQueueService, type IImapConnection, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, SpamReportService } from "@remit/mailbox-service";
3
+ import { type SecretsService } from "@remit/secrets-service";
4
+ import type { StorageService } from "@remit/storage-service";
5
+ export interface ConnectionScope {
6
+ getConnection: () => Promise<IImapConnection>;
7
+ disconnect: () => Promise<void>;
8
+ }
9
+ export interface RemitClient {
10
+ accountConfig: IAccountConfigRepository;
11
+ account: IAccountRepository;
12
+ accountSetting: IAccountSettingRepository;
13
+ address: IAddressRepository;
14
+ mailbox: IMailboxRepository;
15
+ mailboxSpecialUse: IMailboxSpecialUseRepository;
16
+ mailboxLock: IMailboxLockRepository;
17
+ message: IMessageRepository;
18
+ messageFlag: IMessageFlagRepository;
19
+ outboxMessage: IOutboxMessageRepository;
20
+ threadMessage: IThreadMessageRepository;
21
+ envelope: IEnvelopeRepository;
22
+ accountExportRequest: IAccountExportRequestRepository;
23
+ configImport: IConfigImportRepository;
24
+ quarantine: IQuarantineRepository;
25
+ syncWarning: ISyncWarningRepository;
26
+ organizeJobRequest: IOrganizeJobRequestRepository;
27
+ filter: IFilterRepository;
28
+ label: ILabelRepository;
29
+ messageLabel: IMessageLabelRepository;
30
+ senderSignerStanding: ISenderSignerStandingRepository;
31
+ calendarCollection: ICalendarCollectionRepository;
32
+ calendarObject: ICalendarObjectRepository;
33
+ calendarEventIndex: ICalendarEventIndexRepository;
34
+ calendarFeedToken: ICalendarFeedTokenRepository;
35
+ calendarSuggestion: ICalendarSuggestionRepository;
36
+ calendarStore: ICalendarStore;
37
+ storage: StorageService;
38
+ secrets: SecretsService;
39
+ bodySync: BodySyncService;
40
+ bodySyncQueue?: BodySyncQueueService;
41
+ placementMove: IMessagePlacementMoveRepository;
42
+ flagPush: IMessageFlagPushRepository;
43
+ flagQueue: FlagQueueService;
44
+ mailboxQueue: MailboxQueueService;
45
+ messageMove: MessageMoveService;
46
+ outboxAttachment: OutboxAttachmentService;
47
+ outboxQueue: OutboxQueueService;
48
+ spamReport: SpamReportService;
49
+ createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
50
+ }
51
+ export interface RemitClientRepositories {
52
+ accountConfig: IAccountConfigRepository;
53
+ account: IAccountRepository;
54
+ accountSetting: IAccountSettingRepository;
55
+ address: IAddressRepository;
56
+ mailbox: IMailboxRepository;
57
+ mailboxSpecialUse: IMailboxSpecialUseRepository;
58
+ mailboxLock: IMailboxLockRepository;
59
+ message: IMessageRepository;
60
+ messageFlag: IMessageFlagRepository;
61
+ outboxMessage: IOutboxMessageRepository;
62
+ outboxAttachment: IOutboxAttachmentRepository;
63
+ threadMessage: IThreadMessageRepository;
64
+ envelope: IEnvelopeRepository;
65
+ accountExportRequest: IAccountExportRequestRepository;
66
+ configImport: IConfigImportRepository;
67
+ quarantine: IQuarantineRepository;
68
+ syncWarning: ISyncWarningRepository;
69
+ organizeJobRequest: IOrganizeJobRequestRepository;
70
+ placementMove: IMessagePlacementMoveRepository;
71
+ flagPush: IMessageFlagPushRepository;
72
+ filter: IFilterRepository;
73
+ label: ILabelRepository;
74
+ messageLabel: IMessageLabelRepository;
75
+ senderSignerStanding: ISenderSignerStandingRepository;
76
+ calendarCollection: ICalendarCollectionRepository;
77
+ calendarObject: ICalendarObjectRepository;
78
+ calendarEventIndex: ICalendarEventIndexRepository;
79
+ calendarFeedToken: ICalendarFeedTokenRepository;
80
+ calendarSuggestion: ICalendarSuggestionRepository;
81
+ calendarStore: ICalendarStore;
82
+ }
83
+ export interface RemitClientSharedDeps {
84
+ storage: StorageService;
85
+ secrets: SecretsService;
86
+ sqsQueueUrl: string;
87
+ sqsSmtpQueueUrl: string;
88
+ bodySyncQueue?: BodySyncQueueService;
89
+ }
90
+ export interface RemitClientDeps extends RemitClientSharedDeps {
91
+ repositories: RemitClientRepositories;
92
+ }
93
+ export declare const buildSharedDeps: () => RemitClientSharedDeps;
94
+ export declare const buildReadPathFilterConfig: (repositories: RemitClientRepositories) => FilterConfig | undefined;
95
+ export declare const createRemitClient: (deps: RemitClientDeps) => RemitClient;