@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,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,5 @@
1
+ export { OpenAPISpec } from "./api.js";
2
+ export { configFromEnv } from "./config/from-env.js";
3
+ export { createApiEventHandler } from "./lambda.js";
4
+ export declare const handleApiEvent: import("./lambda.js").ApiEventHandler;
5
+ export declare const handler: (event: import("aws-lambda").APIGatewayProxyEvent, context: import("aws-lambda").Context) => Promise<import("aws-lambda").APIGatewayProxyResult>;
@@ -0,0 +1,53 @@
1
+ import { inspect } from "node:util";
2
+ import { logger } from "@remit/logger";
3
+ import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
4
+ import { assertLocalBypassNotInDeployedEnv, localIdentityFromEnv, } from "./auth.js";
5
+ import { configFromEnv } from "./config/from-env.js";
6
+ import { handlers, publicHandlers } from "./handlers/index.js";
7
+ import { createApiEventHandler } from "./lambda.js";
8
+ import { serveApi } from "./serve.js";
9
+ import { getClient } from "./service/data-client.js";
10
+ installNodeLogger();
11
+ // Boot-time refusal: the local auth-bypass overrides must never be set in a
12
+ // deployed environment, so a leaked env var fails the process at startup
13
+ // rather than silently opening authentication at runtime.
14
+ assertLocalBypassNotInDeployedEnv(process.env);
15
+ export { OpenAPISpec } from "./api.js";
16
+ export { configFromEnv } from "./config/from-env.js";
17
+ export { createApiEventHandler } from "./lambda.js";
18
+ export const handleApiEvent = createApiEventHandler(serveApi({ handlers, publicHandlers }), {
19
+ config: configFromEnv(process.env),
20
+ localIdentity: localIdentityFromEnv(process.env),
21
+ client: getClient,
22
+ });
23
+ /**
24
+ * Last-resort net for a leaked background rejection.
25
+ *
26
+ * Every fire-and-forget side effect is meant to be contained at its source (see
27
+ * service/fire-and-forget.ts), so reaching here is a bug — an uncontained
28
+ * `void promise` somewhere. The API process (dev-server / warm Lambda container)
29
+ * shares one event loop across requests, so without this handler such a leak
30
+ * would surface as a spurious 500 on whatever request was in flight. We log it
31
+ * LOUDLY with a distinct alert so it is observable, and deliberately do NOT exit
32
+ * — a stray background rejection must never take the API down or fail a request.
33
+ * (The worker process intentionally has no such net: there each message is its
34
+ * own invocation and a rejection should crash it.)
35
+ */
36
+ const unhandledRejectionRegistered = Symbol.for("remit.backend.unhandledRejectionNet");
37
+ const globalProcess = process;
38
+ if (!globalProcess[unhandledRejectionRegistered]) {
39
+ globalProcess[unhandledRejectionRegistered] = true;
40
+ process.on("unhandledRejection", (reason) => {
41
+ logger.error({
42
+ alert: "unhandled_rejection",
43
+ errorName: reason?.name,
44
+ errorCode: reason?.Code ??
45
+ reason?.code,
46
+ error: inspect(reason),
47
+ }, "Unhandled promise rejection leaked into the API event loop (contained, request not failed)");
48
+ });
49
+ }
50
+ // The Lambda and Node entry point. `withTelemetry` installs the Node logger and
51
+ // its process metrics; a host without Node's process APIs imports
52
+ // `@remit/backend/api` instead and plugs in its own logger with `setLogger`.
53
+ 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,15 @@
1
+ import { type JwtVerifier } from "@remit/auth-service/verifier";
2
+ import type { APIGatewayProxyEvent } from "aws-lambda";
3
+ import type { HandlerIdentity } from "./types.js";
4
+ /** Test-only override for the memoized verifier. Pass null to reset. */
5
+ export declare const _setVerifierForTest: (v: JwtVerifier | null) => void;
6
+ /**
7
+ * The caller of a self-host request, from a better-auth RS256 JWT.
8
+ *
9
+ * Claims the edge tier (APISIX) already verified and injected are trusted as
10
+ * they stand. Otherwise a bearer token is verified here; a token that fails
11
+ * verification is an anonymous caller, so the operation table answers 401. With
12
+ * no token the local bypass, when one is configured, is the caller. Only an
13
+ * operation that needs a caller resolves this, so a public route never does.
14
+ */
15
+ export declare const identityFromSelfHostRequest: (event: APIGatewayProxyEvent, localIdentity: HandlerIdentity) => Promise<HandlerIdentity>;
@@ -0,0 +1,45 @@
1
+ import { createJwtVerifier, extractBearerToken, resolveVerifierConfig, } from "@remit/auth-service/verifier";
2
+ import { logger } from "@remit/logger";
3
+ import { getIdentityFromEvent, userFromClaims } from "./auth.js";
4
+ let verifier = null;
5
+ const getVerifier = () => {
6
+ if (!verifier)
7
+ verifier = createJwtVerifier(resolveVerifierConfig());
8
+ return verifier;
9
+ };
10
+ /** Test-only override for the memoized verifier. Pass null to reset. */
11
+ export const _setVerifierForTest = (v) => {
12
+ verifier = v;
13
+ };
14
+ const readHeader = (headers, name) => {
15
+ for (const [key, value] of Object.entries(headers ?? {})) {
16
+ if (key.toLowerCase() === name && typeof value === "string")
17
+ return value;
18
+ }
19
+ return undefined;
20
+ };
21
+ /**
22
+ * The caller of a self-host request, from a better-auth RS256 JWT.
23
+ *
24
+ * Claims the edge tier (APISIX) already verified and injected are trusted as
25
+ * they stand. Otherwise a bearer token is verified here; a token that fails
26
+ * verification is an anonymous caller, so the operation table answers 401. With
27
+ * no token the local bypass, when one is configured, is the caller. Only an
28
+ * operation that needs a caller resolves this, so a public route never does.
29
+ */
30
+ export const identityFromSelfHostRequest = async (event, localIdentity) => {
31
+ const existingSub = event.requestContext?.authorizer?.claims?.sub;
32
+ if (typeof existingSub === "string" && existingSub.length > 0) {
33
+ return getIdentityFromEvent(event, localIdentity);
34
+ }
35
+ const token = extractBearerToken(readHeader(event.headers, "authorization"));
36
+ if (!token)
37
+ return localIdentity;
38
+ const claims = await getVerifier()(token).catch((err) => {
39
+ logger.warn({ err: err instanceof Error ? err.message : String(err) }, "better-auth JWT verification failed");
40
+ return null;
41
+ });
42
+ if (!claims)
43
+ return { kind: "anonymous" };
44
+ return userFromClaims(claims.sub, claims.email);
45
+ };
@@ -0,0 +1,13 @@
1
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";
2
+ import type { ApiConfig } from "./config/api-config.js";
3
+ import type { ServedApi } from "./serve.js";
4
+ import type { RemitClient } from "./service/data-client.js";
5
+ import type { QueueName } from "./service/queues.js";
6
+ import type { HandlerIdentity } from "./types.js";
7
+ export interface ApiEventHandlerOptions<K extends QueueName> {
8
+ config: ApiConfig;
9
+ localIdentity: HandlerIdentity;
10
+ client: () => Promise<RemitClient<K>>;
11
+ }
12
+ export type ApiEventHandler = (event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>;
13
+ export declare const createApiEventHandler: <K extends QueueName>(served: ServedApi<K>, { config, localIdentity, client }: ApiEventHandlerOptions<NoInfer<K>>) => ApiEventHandler;
@@ -0,0 +1,33 @@
1
+ import { getIdentityFromEvent } from "./auth.js";
2
+ import { identityFromSelfHostRequest } from "./jwt-auth.js";
3
+ import { normalizeRequest } from "./request.js";
4
+ const queryOf = (event) => {
5
+ if (event.multiValueQueryStringParameters) {
6
+ return event.multiValueQueryStringParameters;
7
+ }
8
+ return Object.fromEntries(Object.entries(event.queryStringParameters ?? {}).map(([key, value]) => [
9
+ key,
10
+ value === undefined ? undefined : [value],
11
+ ]));
12
+ };
13
+ const headersOf = (event) => ({
14
+ ...event.headers,
15
+ ...Object.fromEntries(Object.entries(event.multiValueHeaders ?? {}).map(([name, values]) => [
16
+ name,
17
+ values?.join(", "),
18
+ ])),
19
+ });
20
+ const normalizeApiGatewayEvent = (event) => normalizeRequest({
21
+ method: event.httpMethod,
22
+ path: event.path,
23
+ query: queryOf(event),
24
+ headers: headersOf(event),
25
+ body: event.body,
26
+ });
27
+ export const createApiEventHandler = (served, { config, localIdentity, client }) => (event, context) => served.dispatch(normalizeApiGatewayEvent(event), {
28
+ config,
29
+ client,
30
+ identity: async () => config.auth.mode === "BetterAuthJwt"
31
+ ? identityFromSelfHostRequest(event, localIdentity)
32
+ : getIdentityFromEvent(event, localIdentity),
33
+ }, context.awsRequestId);
@@ -0,0 +1,9 @@
1
+ import type { Request } from "openapi-backend";
2
+ export interface HostRequest {
3
+ method: string;
4
+ path: string;
5
+ query: Readonly<Record<string, readonly string[] | undefined>> | null;
6
+ headers: Readonly<Record<string, string | undefined>> | null;
7
+ body: string | null;
8
+ }
9
+ export declare const normalizeRequest: (request: HostRequest) => Request;
@@ -0,0 +1,17 @@
1
+ const definedHeaders = (map) => Object.fromEntries(Object.entries(map ?? {}).filter((entry) => entry[1] !== undefined));
2
+ const collapsedQuery = (map) => {
3
+ const query = {};
4
+ for (const [key, values] of Object.entries(map ?? {})) {
5
+ if (!values || values.length === 0)
6
+ continue;
7
+ query[key] = values.length === 1 ? values[0] : [...values];
8
+ }
9
+ return query;
10
+ };
11
+ export const normalizeRequest = (request) => ({
12
+ method: request.method,
13
+ path: request.path,
14
+ query: collapsedQuery(request.query),
15
+ body: request.body === "" ? null : request.body,
16
+ headers: definedHeaders(request.headers),
17
+ });
@@ -0,0 +1,44 @@
1
+ import type { APIGatewayProxyResult } from "aws-lambda";
2
+ import type { Context as OpenAPIContext } from "openapi-backend";
3
+ import type { ApiConfig } from "./config/api-config.js";
4
+ declare const RAW_API_RESPONSE: unique symbol;
5
+ /**
6
+ * A response the caller reads by something other than its JSON body — an
7
+ * iCalendar feed, an OAuth redirect's Location, and whatever later joins them.
8
+ *
9
+ * Every other handler returns a plain object and lets this module decide the
10
+ * status, the content type and the serialization. That is the right default and
11
+ * stays the default; a handler that has to own its own media type or headers
12
+ * says so with `rawApiResponse` rather than by returning a shape this one has
13
+ * to guess at.
14
+ * `dispatch` still adds the envelope headers, so a raw response is not a way
15
+ * around them.
16
+ */
17
+ export interface RawApiResponse {
18
+ statusCode: number;
19
+ headers: Record<string, string>;
20
+ body: string;
21
+ }
22
+ type MarkedRawApiResponse = RawApiResponse & {
23
+ readonly [RAW_API_RESPONSE]: true;
24
+ };
25
+ export declare const rawApiResponse: (response: RawApiResponse) => MarkedRawApiResponse;
26
+ /**
27
+ * The headers every response carries whatever its body is: the CORS grant the
28
+ * browser needs, and the id the request's log lines are already tagged with.
29
+ * Returning that id is what makes a bug report's "correlation id" resolve to a
30
+ * server-side line; the browser can only read a non-safelisted header when it
31
+ * is exposed.
32
+ */
33
+ export interface EnvelopeInput {
34
+ origin: string | undefined;
35
+ allowedOrigins: readonly string[];
36
+ correlationId: string;
37
+ }
38
+ export declare const resolveAllowedOrigin: (origin: string | undefined, allowed: readonly string[]) => string;
39
+ export declare const envelopeHeaders: ({ origin, allowedOrigins, correlationId, }: EnvelopeInput) => Record<string, string>;
40
+ export declare const formatResponse: (body: Record<string, unknown>, statusCode?: number) => APIGatewayProxyResult;
41
+ export declare const postResponseHandler: (context: OpenAPIContext, scope: {
42
+ config: ApiConfig;
43
+ }) => APIGatewayProxyResult;
44
+ export {};
@@ -0,0 +1,80 @@
1
+ import { logger } from "@remit/logger";
2
+ const RAW_API_RESPONSE = Symbol("remit.rawApiResponse");
3
+ export const rawApiResponse = (response) => Object.assign(response, { [RAW_API_RESPONSE]: true });
4
+ const isRawApiResponse = (body) => RAW_API_RESPONSE in body;
5
+ export const resolveAllowedOrigin = (origin, allowed) => {
6
+ if (allowed.length === 0)
7
+ return "*";
8
+ if (origin && allowed.includes(origin))
9
+ return origin;
10
+ return allowed[0];
11
+ };
12
+ export const envelopeHeaders = ({ origin, allowedOrigins, correlationId, }) => {
13
+ const allowOrigin = resolveAllowedOrigin(origin, allowedOrigins);
14
+ const headers = {
15
+ "Access-Control-Allow-Origin": allowOrigin,
16
+ "Access-Control-Allow-Headers": "Authorization,Content-Type",
17
+ "Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
18
+ Vary: "Origin",
19
+ };
20
+ if (allowOrigin !== "*") {
21
+ headers["Access-Control-Allow-Credentials"] = "true";
22
+ }
23
+ headers["x-correlation-id"] = correlationId;
24
+ headers["Access-Control-Expose-Headers"] = "x-correlation-id";
25
+ return headers;
26
+ };
27
+ export const formatResponse = (body, statusCode = 200) => {
28
+ if (body.statusCode && typeof body.statusCode === "number") {
29
+ statusCode = body.statusCode;
30
+ }
31
+ if (isRawApiResponse(body)) {
32
+ logger.debug({ statusCode: body.statusCode }, "response");
33
+ return {
34
+ statusCode: body.statusCode,
35
+ headers: body.headers,
36
+ body: body.body,
37
+ };
38
+ }
39
+ if ("body" in body && body.body && typeof body.body === "object") {
40
+ body = body.body;
41
+ }
42
+ logger.debug({ statusCode }, "response");
43
+ return {
44
+ statusCode: statusCode,
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify(body),
47
+ };
48
+ };
49
+ export const postResponseHandler = (context, scope) => {
50
+ const { api, response, operation } = context;
51
+ if (response.statusCode)
52
+ return formatResponse(response, response.statusCode);
53
+ if (scope.config.responseValidation === "Skip")
54
+ return formatResponse(response, 200);
55
+ const { valid, errors } = api.validateResponse(response, operation);
56
+ if (valid)
57
+ return formatResponse(response, 200);
58
+ const { operationId } = operation;
59
+ const errorDetails = {
60
+ errors,
61
+ operationId,
62
+ responseKeys: Object.keys(response),
63
+ };
64
+ errors?.forEach((error) => {
65
+ if (error.instancePath) {
66
+ const pathParts = error.instancePath.split("/").filter(Boolean);
67
+ let value = response;
68
+ for (const part of pathParts) {
69
+ value = value?.[part];
70
+ }
71
+ errorDetails[`problematicValue_${error.instancePath}`] = {
72
+ path: error.instancePath,
73
+ value: JSON.stringify(value).substring(0, 500),
74
+ error: error.message,
75
+ };
76
+ }
77
+ });
78
+ logger.error(errorDetails, "Response validation failed");
79
+ return formatResponse(response, 200);
80
+ };
@@ -0,0 +1,31 @@
1
+ import type { APIGatewayProxyResult } from "aws-lambda";
2
+ import type { Request as OpenAPIRequest } from "openapi-backend";
3
+ import type { ApiConfig } from "./config/api-config.js";
4
+ import type { RemitClient } from "./service/data-client.js";
5
+ import type { QueueName } from "./service/queues.js";
6
+ import type { HandlerIdentity, OperationHandler, OperationIds, PublicOperationHandler, PublicOperationIds } from "./types.js";
7
+ export type { ApiConfig } from "./config/api-config.js";
8
+ export type { HandlerContext, HandlerIdentity, PublicHandlerContext, UserIdentity, } from "./types.js";
9
+ export interface RequestHost<K extends QueueName> {
10
+ config: ApiConfig;
11
+ client: () => Promise<RemitClient<K>>;
12
+ identity: () => Promise<HandlerIdentity>;
13
+ }
14
+ /**
15
+ * The operations a host serves, each typed by the queues it sends to. An
16
+ * operation the host leaves out answers 501.
17
+ */
18
+ export interface ApiRegistry<K extends QueueName> {
19
+ handlers: Partial<Record<Exclude<OperationIds, PublicOperationIds>, OperationHandler<OperationIds, K>>>;
20
+ publicHandlers: Partial<Record<PublicOperationIds, PublicOperationHandler<K>>>;
21
+ }
22
+ export interface ServedApi<K extends QueueName> {
23
+ dispatch: (request: OpenAPIRequest, host: RequestHost<K>, correlationId: string) => Promise<APIGatewayProxyResult>;
24
+ handleFetch: (request: Request, host: RequestHost<K>, correlationId: string) => Promise<Response>;
25
+ }
26
+ /**
27
+ * Register a host's operations and return the two ways in: a normalised
28
+ * request, or a Fetch `Request`. Each request's host supplies the client, and
29
+ * the client's queues must cover every operation in the registry.
30
+ */
31
+ export declare const serveApi: <K extends QueueName>(registry: ApiRegistry<NoInfer<K>>) => ServedApi<K>;
@@ -0,0 +1,117 @@
1
+ import { redactCalendarFeedPath } from "@remit/calendar-service";
2
+ import { logger, withLogContext } from "@remit/logger";
3
+ import { api } from "./api.js";
4
+ import { defaultErrorCode, handleError } from "./error.js";
5
+ import { normalizeRequest } from "./request.js";
6
+ import { envelopeHeaders } from "./response.js";
7
+ const unauthenticated = () => ({
8
+ statusCode: 401,
9
+ body: { code: defaultErrorCode(401), message: "Unauthorized" },
10
+ });
11
+ const isDefined = (value) => value !== undefined;
12
+ const entriesOf = (record) => Object.entries(record).flatMap(([key, value]) => isDefined(value) ? [[key, value]] : []);
13
+ /**
14
+ * Register a host's operations and return the two ways in: a normalised
15
+ * request, or a Fetch `Request`. Each request's host supplies the client, and
16
+ * the client's queues must cover every operation in the registry.
17
+ */
18
+ export const serveApi = (registry) => {
19
+ api.register({
20
+ ...Object.fromEntries(entriesOf(registry.handlers).map(([operationId, handler]) => [
21
+ operationId,
22
+ async (c, host) => {
23
+ const identity = await host.identity();
24
+ if (identity.kind === "anonymous")
25
+ return unauthenticated();
26
+ return handler(c, {
27
+ client: await host.client(),
28
+ identity,
29
+ config: host.config,
30
+ });
31
+ },
32
+ ])),
33
+ ...Object.fromEntries(entriesOf(registry.publicHandlers).map(([operationId, handler]) => [
34
+ operationId,
35
+ async (c, host) => handler(c, { client: await host.client(), config: host.config }),
36
+ ])),
37
+ });
38
+ return { dispatch, handleFetch };
39
+ };
40
+ const readHeader = (headers, name) => {
41
+ for (const [key, value] of Object.entries(headers ?? {})) {
42
+ if (key.toLowerCase() === name && typeof value === "string")
43
+ return value;
44
+ }
45
+ return undefined;
46
+ };
47
+ const dispatch = async (request, host, correlationId) => {
48
+ // A calendar feed carries its credential in the path, so the raw path is a
49
+ // secret and cannot be a log field (issue #1065). Redacted once, here, where
50
+ // every line inside the request picks it up from the scope.
51
+ const path = redactCalendarFeedPath(request.path);
52
+ const { method } = request;
53
+ return withLogContext({ path, method }, async () => {
54
+ logger.debug({ method, path }, "Request received");
55
+ const startedAt = Date.now();
56
+ const answered = await api
57
+ .handleRequest(request, host)
58
+ .catch(handleError);
59
+ const result = {
60
+ ...answered,
61
+ headers: {
62
+ ...envelopeHeaders({
63
+ origin: readHeader(request.headers, "origin"),
64
+ allowedOrigins: host.config.corsAllowedOrigins,
65
+ correlationId,
66
+ }),
67
+ ...answered.headers,
68
+ },
69
+ };
70
+ // One structured access line per request: method, path, status, and
71
+ // duration. The correlation id is inlined as a top-level field so a bug
72
+ // report quoting the id can grep logs by it directly, and emitted at
73
+ // info so it is on by default in production (the debug "Request
74
+ // received" above already filters out under normal log levels).
75
+ // biome-ignore lint/plugin/no-logger-info: one access line per request is an audit-grade signal (issue #1054)
76
+ logger.info({
77
+ method,
78
+ path,
79
+ statusCode: result.statusCode,
80
+ durationMs: Date.now() - startedAt,
81
+ correlationId,
82
+ }, "Request completed");
83
+ return result;
84
+ });
85
+ };
86
+ const queryOf = (params) => {
87
+ const query = {};
88
+ for (const [key, value] of params) {
89
+ query[key] = [...(query[key] ?? []), value];
90
+ }
91
+ return query;
92
+ };
93
+ const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
94
+ const toFetchResponse = (result) => {
95
+ const headers = new Headers();
96
+ for (const [name, value] of Object.entries(result.headers ?? {})) {
97
+ headers.set(name, String(value));
98
+ }
99
+ for (const [name, values] of Object.entries(result.multiValueHeaders ?? {})) {
100
+ for (const value of values)
101
+ headers.append(name, String(value));
102
+ }
103
+ const body = NULL_BODY_STATUSES.has(result.statusCode) ? null : result.body;
104
+ return new Response(body, { status: result.statusCode, headers });
105
+ };
106
+ const handleFetch = async (request, host, correlationId) => {
107
+ const url = new URL(request.url);
108
+ const body = request.body === null ? null : await request.text();
109
+ const result = await dispatch(normalizeRequest({
110
+ method: request.method,
111
+ path: url.pathname,
112
+ query: queryOf(url.searchParams),
113
+ headers: Object.fromEntries(request.headers),
114
+ body,
115
+ }), host, correlationId);
116
+ return toFetchResponse(result);
117
+ };
@@ -0,0 +1,20 @@
1
+ import type { AccountDataPurgeEvent, AccountItem, IAccountRepository, IConfigImportRepository, QueueProducer } from "@remit/data-ports";
2
+ import type { Logger } from "@remit/logger";
3
+ export interface AccountDeletionDeps {
4
+ account: Pick<IAccountRepository, "update">;
5
+ queue: QueueProducer<AccountDataPurgeEvent>;
6
+ log: Logger;
7
+ }
8
+ export declare const deleteAccount: (deps: AccountDeletionDeps, account: Pick<AccountItem, "accountId" | "accountConfigId">, deletedAt: number) => Promise<void>;
9
+ export interface HalfDeletedAccountDeps extends AccountDeletionDeps {
10
+ account: Pick<IAccountRepository, "update" | "listAll">;
11
+ configImport: Pick<IConfigImportRepository, "listByAccountConfig">;
12
+ }
13
+ /**
14
+ * Before #1384 a delete stored `isActive=false` and dropped `deletedAt`. Of the
15
+ * writers in this repo only that delete and a config import leave an account
16
+ * inactive, and an import names each account it creates in its stored
17
+ * document. The public update endpoint accepts `isActive` as sent, so a caller
18
+ * outside the repo can also leave one inactive.
19
+ */
20
+ export declare const finishHalfDeletedAccounts: (deps: HalfDeletedAccountDeps) => Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { readConfigDocument } from "@remit/config-format";
2
+ export const deleteAccount = async (deps, account, deletedAt) => {
3
+ const { accountId, accountConfigId } = account;
4
+ // Purge first: an account whose request never left still reads as not deleted, so it is found and asked for again.
5
+ await deps.queue.send({
6
+ body: { type: "AccountDataPurge", accountId, accountConfigId },
7
+ });
8
+ await deps.account.update(accountId, { deletedAt, isActive: false });
9
+ deps.log.info({ accountId, accountConfigId }, "Account data purge initiated");
10
+ };
11
+ /**
12
+ * Before #1384 a delete stored `isActive=false` and dropped `deletedAt`. Of the
13
+ * writers in this repo only that delete and a config import leave an account
14
+ * inactive, and an import names each account it creates in its stored
15
+ * document. The public update endpoint accepts `isActive` as sent, so a caller
16
+ * outside the repo can also leave one inactive.
17
+ */
18
+ export const finishHalfDeletedAccounts = async (deps) => {
19
+ const imported = new Map();
20
+ for (const account of await deps.account.listAll()) {
21
+ if (account.isActive || account.deletedAt !== undefined)
22
+ continue;
23
+ let names = imported.get(account.accountConfigId);
24
+ if (!names) {
25
+ const imports = await deps.configImport.listByAccountConfig(account.accountConfigId);
26
+ names = new Set(imports.flatMap((row) => readConfigDocument(row.document).accounts.map((a) => a.accountId)));
27
+ imported.set(account.accountConfigId, names);
28
+ }
29
+ if (names.has(account.accountId))
30
+ continue;
31
+ await deleteAccount(deps, account, Date.now());
32
+ }
33
+ };
@@ -0,0 +1,3 @@
1
+ import { type RemitClient, type RemitClientSharedDeps } from "./create-remit-client.js";
2
+ export declare const composeSqliteClient: (shared: RemitClientSharedDeps) => Promise<RemitClient>;
3
+ export declare const buildSqliteClient: () => Promise<RemitClient>;
@@ -0,0 +1,13 @@
1
+ import { createSqliteRepositories, openSqliteDatabases, sqliteDatabasePathsFromEnv, } from "@remit/drizzle-service";
2
+ import { createRemitClient, } from "./create-remit-client.js";
3
+ import { buildSharedDeps, sqsQueues, sqsQueueUrlsFromEnv, } from "./shared-deps.js";
4
+ // The SQLite adapter composition (RFC 036). Every repo runs on the database
5
+ // that holds its tables: app, tenant mail or tenant search (#1420).
6
+ export const composeSqliteClient = async (shared) => {
7
+ const databases = await openSqliteDatabases(sqliteDatabasePathsFromEnv());
8
+ return createRemitClient({
9
+ repositories: createSqliteRepositories(databases),
10
+ ...shared,
11
+ });
12
+ };
13
+ export const buildSqliteClient = () => composeSqliteClient(buildSharedDeps(sqsQueues(sqsQueueUrlsFromEnv())));