@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
package/src/auth.ts DELETED
@@ -1,92 +0,0 @@
1
- import { base36uuidv5, REMIT_NAMESPACE } from "@remit/data-ports/id";
2
- import type { APIGatewayProxyEvent } from "aws-lambda";
3
-
4
- const LOCAL_BYPASS_VARS = [
5
- "LOCAL_ACCOUNT_CONFIG_ID",
6
- "LOCAL_COGNITO_SUB",
7
- ] as const;
8
-
9
- const isLocalEnv = (env: NodeJS.ProcessEnv): boolean =>
10
- env.NODE_ENV === "development" || env.NODE_ENV === "test";
11
-
12
- /**
13
- * The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
14
- * for local dev and fixtures: with them set, an unauthenticated request is served
15
- * as the pinned account. They must never reach a deployed environment. Refuse to
16
- * start (throw at module load) if either is present outside a local NODE_ENV — the
17
- * same `development`/`test` signal the data layer already uses to decide local vs
18
- * deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
19
- * boot, not exploited at runtime.
20
- */
21
- export const assertLocalBypassNotInDeployedEnv = (
22
- env: NodeJS.ProcessEnv = process.env,
23
- ): void => {
24
- if (isLocalEnv(env)) return;
25
- const leaked = LOCAL_BYPASS_VARS.filter((name) => {
26
- const value = env[name];
27
- return typeof value === "string" && value.length > 0;
28
- });
29
- if (leaked.length === 0) return;
30
- throw new Error(
31
- `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.`,
32
- );
33
- };
34
-
35
- /**
36
- * Derive a deterministic accountConfigId from a Cognito subject (`sub`) claim.
37
- * One Cognito user maps to exactly one account config.
38
- */
39
- export const deriveAccountConfigId = (sub: string): string =>
40
- base36uuidv5(`account:${sub}`, REMIT_NAMESPACE);
41
-
42
- const getSubFromClaims = (event: APIGatewayProxyEvent): string | undefined => {
43
- const claims = event.requestContext?.authorizer?.claims;
44
- if (!claims) return undefined;
45
- const sub = claims.sub;
46
- if (typeof sub === "string" && sub.length > 0) return sub;
47
- return undefined;
48
- };
49
-
50
- /**
51
- * Return the Cognito `sub` for the current request, preferring the JWT claim
52
- * and falling back to `LOCAL_COGNITO_SUB` for local development. Returns
53
- * `undefined` when neither is available (e.g. seeded fixtures that only set
54
- * `LOCAL_ACCOUNT_CONFIG_ID`).
55
- */
56
- export const getSubFromEvent = (
57
- event: APIGatewayProxyEvent,
58
- ): string | undefined => {
59
- const sub = getSubFromClaims(event);
60
- if (sub) return sub;
61
- const localSub = process.env.LOCAL_COGNITO_SUB;
62
- if (localSub && localSub.length > 0) return localSub;
63
- return undefined;
64
- };
65
-
66
- /**
67
- * Extract the caller's accountConfigId from an API Gateway event.
68
- *
69
- * Production: reads the Cognito `sub` claim (verified by the API Gateway
70
- * Cognito authorizer) and derives the accountConfigId via UUIDv5.
71
- *
72
- * Local development: honours two environment overrides so the dev server and
73
- * playwright fixtures can run without a real Cognito token:
74
- * - `LOCAL_ACCOUNT_CONFIG_ID` — pin a specific seeded accountConfigId
75
- * - `LOCAL_COGNITO_SUB` — pin a Cognito `sub` value; derived like production
76
- */
77
- export const getAccountConfigIdFromEvent = (
78
- event: APIGatewayProxyEvent,
79
- ): string => {
80
- const sub = getSubFromClaims(event);
81
- if (sub) return deriveAccountConfigId(sub);
82
-
83
- const localAccountConfigId = process.env.LOCAL_ACCOUNT_CONFIG_ID;
84
- if (localAccountConfigId) return localAccountConfigId;
85
-
86
- const localSub = process.env.LOCAL_COGNITO_SUB;
87
- if (localSub) return deriveAccountConfigId(localSub);
88
-
89
- throw new Error(
90
- "Missing accountConfigId: no Cognito `sub` claim on the request, and neither LOCAL_ACCOUNT_CONFIG_ID nor LOCAL_COGNITO_SUB env var is set",
91
- );
92
- };
@@ -1,48 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { getMsOAuthConfig } from "./msoauth.js";
4
-
5
- const MSOAUTH_KEYS = [
6
- "MSOAUTH_AUTHORITY",
7
- "MSOAUTH_REDIRECT_URI",
8
- "MSOAUTH_SECRET_ARN",
9
- "MSOAUTH_CLIENT_ID",
10
- "MSOAUTH_CLIENT_SECRET",
11
- "MSOAUTH_TOKEN_ENDPOINT",
12
- ] as const;
13
-
14
- describe("getMsOAuthConfig", () => {
15
- const original = new Map(
16
- MSOAUTH_KEYS.map((key) => [key, process.env[key]] as const),
17
- );
18
-
19
- afterEach(() => {
20
- for (const [key, value] of original) {
21
- if (value === undefined) delete process.env[key];
22
- else process.env[key] = value;
23
- }
24
- });
25
-
26
- it("resolves on a self-host deployment, where no authority is wired", () => {
27
- for (const key of MSOAUTH_KEYS) delete process.env[key];
28
- process.env.MSOAUTH_REDIRECT_URI =
29
- "https://mail.example.com/api/accounts/oauth/microsoft/callback";
30
- process.env.MSOAUTH_CLIENT_ID = "client-id";
31
- process.env.MSOAUTH_CLIENT_SECRET = "client-secret";
32
-
33
- assert.deepEqual(getMsOAuthConfig(), {
34
- secretArn: undefined,
35
- redirectUri:
36
- "https://mail.example.com/api/accounts/oauth/microsoft/callback",
37
- clientId: "client-id",
38
- clientSecret: "client-secret",
39
- tokenEndpoint: undefined,
40
- });
41
- });
42
-
43
- it("names PUBLIC_ORIGIN when the redirect URI is missing", () => {
44
- for (const key of MSOAUTH_KEYS) delete process.env[key];
45
-
46
- assert.throws(getMsOAuthConfig, /MSOAUTH_REDIRECT_URI.*PUBLIC_ORIGIN/s);
47
- });
48
- });
@@ -1,48 +0,0 @@
1
- /**
2
- * Typed reads for Microsoft OAuth (Entra) environment variables.
3
- *
4
- * `MSOAUTH_REDIRECT_URI` is derived from `PUBLIC_ORIGIN` by the compose file
5
- * (deploy/vps/docker-compose.sqlite.yml) and must match the redirect URI
6
- * registered on the Entra app registration.
7
- *
8
- * Credentials come either from `MSOAUTH_CLIENT_ID` / `MSOAUTH_CLIENT_SECRET`
9
- * in the deploy env file, or from Secrets Manager via `MSOAUTH_SECRET_ARN`.
10
- *
11
- * The Microsoft authorization and token endpoints are fixed in
12
- * `@remit/mail-oauth-service`; no authority URL is read from the environment.
13
- */
14
-
15
- export interface MsOAuthConfig {
16
- /** Secrets Manager ARN — an alternative to the client id/secret pair. */
17
- readonly secretArn: string | undefined;
18
- /** OAuth redirect URI registered in the Entra app. */
19
- readonly redirectUri: string;
20
- /** Client ID — set directly to bypass Secrets Manager. */
21
- readonly clientId: string | undefined;
22
- /** Client secret — set directly to bypass Secrets Manager. */
23
- readonly clientSecret: string | undefined;
24
- /** Token endpoint override — used for local stubbing (bypasses default OIDC discovery). */
25
- readonly tokenEndpoint: string | undefined;
26
- }
27
-
28
- /**
29
- * Read Microsoft OAuth configuration from the environment.
30
- * Throws when `MSOAUTH_REDIRECT_URI` is absent.
31
- */
32
- export const getMsOAuthConfig = (): MsOAuthConfig => {
33
- const redirectUri = process.env.MSOAUTH_REDIRECT_URI;
34
-
35
- if (!redirectUri) {
36
- throw new Error(
37
- "MSOAUTH_REDIRECT_URI is not set. It is derived from PUBLIC_ORIGIN — set PUBLIC_ORIGIN in the deploy env file (deploy/vps/.env) and restart, or set MSOAUTH_REDIRECT_URI directly.",
38
- );
39
- }
40
-
41
- return {
42
- secretArn: process.env.MSOAUTH_SECRET_ARN,
43
- redirectUri,
44
- clientId: process.env.MSOAUTH_CLIENT_ID,
45
- clientSecret: process.env.MSOAUTH_CLIENT_SECRET,
46
- tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT,
47
- };
48
- };
@@ -1,23 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { usesBetterAuthJwt } from "./data-backend.js";
4
-
5
- describe("usesBetterAuthJwt", () => {
6
- const ORIGINAL = process.env.DATA_BACKEND;
7
- afterEach(() => {
8
- if (ORIGINAL === undefined) delete process.env.DATA_BACKEND;
9
- else process.env.DATA_BACKEND = ORIGINAL;
10
- });
11
-
12
- it("is true for the self-host SQL backend", () => {
13
- process.env.DATA_BACKEND = "sqlite";
14
- assert.equal(usesBetterAuthJwt(), true);
15
- });
16
-
17
- it("is false for the AWS DynamoDB path and when unset", () => {
18
- process.env.DATA_BACKEND = "dynamodb";
19
- assert.equal(usesBetterAuthJwt(), false);
20
- delete process.env.DATA_BACKEND;
21
- assert.equal(usesBetterAuthJwt(), false);
22
- });
23
- });
@@ -1,14 +0,0 @@
1
- /**
2
- * The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
3
- * path.
4
- */
5
- export const isSelfHostSqlBackend = (): boolean =>
6
- process.env.DATA_BACKEND === "sqlite";
7
-
8
- /**
9
- * The self-host SQL backend authenticates requests and signs content URLs with
10
- * a better-auth RS256 JWT verified at the edge and re-verified in-process, no
11
- * Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
12
- * content guard) and is deliberately excluded.
13
- */
14
- export const usesBetterAuthJwt = (): boolean => isSelfHostSqlBackend();
@@ -1,104 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { MessageItem } from "@remit/data-ports";
4
- import { PlacementAction, PlacementConfidence } from "@remit/domain-enums";
5
- import { deriveAutoMoved } from "./autoMoved.js";
6
-
7
- type AutoMovedInput = Pick<
8
- MessageItem,
9
- "movedByRemit" | "placementVerdict" | "filterMove"
10
- >;
11
-
12
- const confidentVerdict = {
13
- action: PlacementAction.MoveToInbox,
14
- confidence: PlacementConfidence.Confident,
15
- fromPlacement: "junk",
16
- reasons: ["provider=spam", "dmarc=pass"],
17
- dryRun: false,
18
- decidedAt: 1_700_000_000_000,
19
- };
20
-
21
- const filterMove = {
22
- filterId: "flt-1",
23
- sourceMailboxId: "mb-inbox",
24
- destinationMailboxId: "mb-travel",
25
- decidedAt: 1_700_000_000_000,
26
- };
27
-
28
- describe("deriveAutoMoved", () => {
29
- it("returns undefined when the message was not moved by Remit", () => {
30
- const message: AutoMovedInput = {
31
- movedByRemit: false,
32
- placementVerdict: confidentVerdict,
33
- };
34
- assert.equal(deriveAutoMoved(message), undefined);
35
- });
36
-
37
- it("projects a standing-filter move to its mailbox ids and filter id", () => {
38
- const message: AutoMovedInput = { movedByRemit: true, filterMove };
39
- assert.deepEqual(deriveAutoMoved(message), {
40
- fromMailboxId: "mb-inbox",
41
- destinationMailboxId: "mb-travel",
42
- filterId: "flt-1",
43
- });
44
- });
45
-
46
- it("never leaks the classifier action/fromPlacement for a filter move", () => {
47
- const result = deriveAutoMoved({ movedByRemit: true, filterMove });
48
- assert.equal(result?.action, undefined);
49
- assert.equal(result?.fromPlacement, undefined);
50
- });
51
-
52
- it("a filter move outranks a co-present placement verdict", () => {
53
- // Both a filter move and a classifier verdict can be recorded; the filter's
54
- // exclusive move is what ultimately placed the message (RFC 034 Dec. 3.1).
55
- const message: AutoMovedInput = {
56
- movedByRemit: true,
57
- filterMove,
58
- placementVerdict: confidentVerdict,
59
- };
60
- assert.deepEqual(deriveAutoMoved(message), {
61
- fromMailboxId: "mb-inbox",
62
- destinationMailboxId: "mb-travel",
63
- filterId: "flt-1",
64
- });
65
- });
66
-
67
- it("projects a confident classifier move to action + fromPlacement", () => {
68
- const message: AutoMovedInput = {
69
- movedByRemit: true,
70
- placementVerdict: confidentVerdict,
71
- };
72
- assert.deepEqual(deriveAutoMoved(message), {
73
- action: PlacementAction.MoveToInbox,
74
- fromPlacement: "junk",
75
- });
76
- });
77
-
78
- it("drops an unsure classifier verdict", () => {
79
- const message: AutoMovedInput = {
80
- movedByRemit: true,
81
- placementVerdict: {
82
- ...confidentVerdict,
83
- confidence: PlacementConfidence.Unsure,
84
- },
85
- };
86
- assert.equal(deriveAutoMoved(message), undefined);
87
- });
88
-
89
- it("drops a dry-run classifier verdict", () => {
90
- const message: AutoMovedInput = {
91
- movedByRemit: true,
92
- placementVerdict: { ...confidentVerdict, dryRun: true },
93
- };
94
- assert.equal(deriveAutoMoved(message), undefined);
95
- });
96
-
97
- it("does not crash on legacy verdict-less, filter-less moved data", () => {
98
- // A message moved before either the verdict or the filter marker existed:
99
- // movedByRemit is set but nothing is derivable. Must return undefined, not
100
- // throw (issue #223 back-compat).
101
- const message: AutoMovedInput = { movedByRemit: true };
102
- assert.equal(deriveAutoMoved(message), undefined);
103
- });
104
- });
@@ -1,161 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import {
4
- CONTENT_URL_TTL_SECONDS,
5
- createContentSigner,
6
- getContentSigner,
7
- verifyContentSignature,
8
- } from "./contentSignature.js";
9
-
10
- const SECRET = "test-master-secret-at-least-32-chars-long";
11
- const PATH_A = "accounts/cfg-alice/acc-alice/messages/msg-1/parts/1.2";
12
- const PATH_B = "accounts/cfg-bob/acc-bob/messages/msg-9/parts/1.2";
13
-
14
- const nowSeconds = () => Math.floor(Date.now() / 1000);
15
-
16
- describe("content signature: sign + verify round trip", () => {
17
- it("a freshly signed URL verifies", () => {
18
- const { exp, sig } = createContentSigner(SECRET)(PATH_A);
19
- const result = verifyContentSignature(
20
- PATH_A,
21
- String(exp),
22
- sig,
23
- SECRET,
24
- nowSeconds(),
25
- );
26
- assert.deepEqual(result, { valid: true });
27
- });
28
-
29
- it("rejects a signature minted for a different account's path (cross-account IDOR defense)", () => {
30
- const { exp, sig } = createContentSigner(SECRET)(PATH_A);
31
- // Present account A's signature against account B's path.
32
- const result = verifyContentSignature(
33
- PATH_B,
34
- String(exp),
35
- sig,
36
- SECRET,
37
- nowSeconds(),
38
- );
39
- assert.deepEqual(result, { valid: false, reason: "bad-signature" });
40
- });
41
-
42
- it("rejects a tampered signature", () => {
43
- const { exp, sig } = createContentSigner(SECRET)(PATH_A);
44
- const tampered = `${sig.slice(0, -1)}${sig.endsWith("A") ? "B" : "A"}`;
45
- const result = verifyContentSignature(
46
- PATH_A,
47
- String(exp),
48
- tampered,
49
- SECRET,
50
- nowSeconds(),
51
- );
52
- assert.equal(result.valid, false);
53
- });
54
-
55
- it("rejects a valid signature verified with the wrong secret", () => {
56
- const { exp, sig } = createContentSigner(SECRET)(PATH_A);
57
- const result = verifyContentSignature(
58
- PATH_A,
59
- String(exp),
60
- sig,
61
- "a-different-master-secret-also-32-chars",
62
- nowSeconds(),
63
- );
64
- assert.deepEqual(result, { valid: false, reason: "bad-signature" });
65
- });
66
-
67
- it("rejects an expired signature", () => {
68
- // Negative TTL mints a signature whose expiry is already in the past.
69
- const { exp, sig } = createContentSigner(SECRET, -20)(PATH_A);
70
- assert.ok(exp < nowSeconds());
71
- const result = verifyContentSignature(
72
- PATH_A,
73
- String(exp),
74
- sig,
75
- SECRET,
76
- nowSeconds(),
77
- );
78
- assert.deepEqual(result, { valid: false, reason: "expired" });
79
- });
80
-
81
- it("reports missing exp/sig distinctly from a bad signature", () => {
82
- assert.deepEqual(
83
- verifyContentSignature(
84
- PATH_A,
85
- undefined,
86
- undefined,
87
- SECRET,
88
- nowSeconds(),
89
- ),
90
- { valid: false, reason: "missing" },
91
- );
92
- });
93
-
94
- it("rejects a non-integer exp as malformed", () => {
95
- const { sig } = createContentSigner(SECRET)(PATH_A);
96
- assert.deepEqual(
97
- verifyContentSignature(PATH_A, "not-a-number", sig, SECRET, nowSeconds()),
98
- { valid: false, reason: "malformed" },
99
- );
100
- });
101
- });
102
-
103
- describe("getContentSigner", () => {
104
- const ORIGINAL_BACKEND = process.env.DATA_BACKEND;
105
- const ORIGINAL_SECRET = process.env.BETTER_AUTH_SECRET;
106
-
107
- afterEach(() => {
108
- if (ORIGINAL_BACKEND === undefined) delete process.env.DATA_BACKEND;
109
- else process.env.DATA_BACKEND = ORIGINAL_BACKEND;
110
- if (ORIGINAL_SECRET === undefined) delete process.env.BETTER_AUTH_SECRET;
111
- else process.env.BETTER_AUTH_SECRET = ORIGINAL_SECRET;
112
- });
113
-
114
- it("returns undefined on the AWS path (unsigned URLs)", () => {
115
- delete process.env.DATA_BACKEND;
116
- assert.equal(getContentSigner(), undefined);
117
- });
118
-
119
- it("returns a working signer in SQLite mode", () => {
120
- process.env.DATA_BACKEND = "sqlite";
121
- process.env.BETTER_AUTH_SECRET = SECRET;
122
- const signer = getContentSigner();
123
- assert.ok(signer);
124
- const { exp, sig } = signer(PATH_A);
125
- assert.deepEqual(
126
- verifyContentSignature(PATH_A, String(exp), sig, SECRET, nowSeconds()),
127
- { valid: true },
128
- );
129
- });
130
-
131
- it("throws in SQLite mode when the master secret is missing (fail loud)", () => {
132
- process.env.DATA_BACKEND = "sqlite";
133
- delete process.env.BETTER_AUTH_SECRET;
134
- assert.throws(() => getContentSigner(), /BETTER_AUTH_SECRET/);
135
- });
136
- });
137
-
138
- describe("the wire format is pinned, not merely self-consistent", () => {
139
- /**
140
- * Every other test here round-trips through this module, so a change to the
141
- * canonical message or the key derivation passes them all while breaking every
142
- * `/content` URL already in a browser. This vector was computed once, by hand,
143
- * from the scheme as shipped: HMAC-SHA256 with a subkey derived from the
144
- * master secret under `remit-content-url-signing-v1`, over `path\nexp`,
145
- * base64url. If it fails, the format moved and in-flight URLs moved with it.
146
- */
147
- it("still produces the signature it produced when the scheme shipped", () => {
148
- const signer = createContentSigner(
149
- "a-signing-secret-of-at-least-32-characters",
150
- );
151
- const originalNow = Date.now;
152
- Date.now = () => 1_800_000_000_000 - CONTENT_URL_TTL_SECONDS * 1000;
153
- try {
154
- const { exp, sig } = signer("accounts/cfg1/acc1/messages/msg1/parts/1.2");
155
- assert.equal(exp, 1_800_000_000);
156
- assert.equal(sig, "eFKwOYf42a6DWwaZahIm7aVtWuqMOfhOAPoyxtpFybo");
157
- } finally {
158
- Date.now = originalNow;
159
- }
160
- });
161
- });
@@ -1,172 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it } from "node:test";
3
- import {
4
- createContentSigner,
5
- verifyContentSignature,
6
- } from "./contentSignature.js";
7
- import {
8
- buildContentUrl,
9
- buildStorageContentUrl,
10
- getContentDeliveryDomain,
11
- } from "./contentUrl.js";
12
-
13
- describe("buildContentUrl", () => {
14
- it("produces the /content/accounts/{cfg}/{acc}/messages/{msg}/parts/{part} layout the Lambda@Edge expects", () => {
15
- const url = buildContentUrl({
16
- domain: "https://abc123.cloudfront.net",
17
- accountConfigId: "cfg-alice",
18
- accountId: "acc-alice",
19
- messageId: "msg-1",
20
- partPath: "1.2",
21
- });
22
-
23
- assert.equal(
24
- url,
25
- "https://abc123.cloudfront.net/content/accounts/cfg-alice/acc-alice/messages/msg-1/parts/1.2",
26
- );
27
- });
28
-
29
- it("prepends https:// when the domain is given as a bare hostname", () => {
30
- const url = buildContentUrl({
31
- domain: "abc123.cloudfront.net",
32
- accountConfigId: "cfg-alice",
33
- accountId: "acc-alice",
34
- messageId: "msg-1",
35
- partPath: "1",
36
- });
37
-
38
- assert.equal(url.startsWith("https://abc123.cloudfront.net/"), true);
39
- });
40
-
41
- it("strips a trailing slash on the domain so the path doesn't double up", () => {
42
- const url = buildContentUrl({
43
- domain: "https://abc123.cloudfront.net/",
44
- accountConfigId: "cfg-alice",
45
- accountId: "acc-alice",
46
- messageId: "msg-1",
47
- partPath: "1",
48
- });
49
-
50
- assert.equal(
51
- url,
52
- "https://abc123.cloudfront.net/content/accounts/cfg-alice/acc-alice/messages/msg-1/parts/1",
53
- );
54
- });
55
-
56
- it("encodes each segment of partPath defensively (forward-slash kept as separator)", () => {
57
- const url = buildContentUrl({
58
- domain: "https://cdn.test",
59
- accountConfigId: "cfg",
60
- accountId: "acc",
61
- messageId: "msg",
62
- partPath: "1/strange path/3",
63
- });
64
-
65
- assert.equal(
66
- url,
67
- "https://cdn.test/content/accounts/cfg/acc/messages/msg/parts/1/strange%20path/3",
68
- );
69
- });
70
-
71
- it("addresses an outbox attachment by its storage key", () => {
72
- const url = buildStorageContentUrl({
73
- domain: "https://cdn.test",
74
- storageKey: "accounts/cfg/acc/outbox/ob-1/attachments/att-1",
75
- });
76
-
77
- assert.equal(
78
- url,
79
- "https://cdn.test/content/accounts/cfg/acc/outbox/ob-1/attachments/att-1",
80
- );
81
- });
82
-
83
- it("leaves the URL unsigned when no signer is provided (AWS/Lambda@Edge path)", () => {
84
- const url = buildContentUrl({
85
- domain: "https://abc123.cloudfront.net",
86
- accountConfigId: "cfg-alice",
87
- accountId: "acc-alice",
88
- messageId: "msg-1",
89
- partPath: "1",
90
- });
91
- assert.equal(url.includes("?"), false);
92
- });
93
-
94
- it("appends an exp/sig signature over the decoded storage path when a signer is provided", () => {
95
- const secret = "test-master-secret-at-least-32-chars-long";
96
- const url = buildContentUrl({
97
- domain: "https://api.example.test",
98
- accountConfigId: "cfg-alice",
99
- accountId: "acc-alice",
100
- messageId: "msg-1",
101
- partPath: "1.2",
102
- sign: createContentSigner(secret),
103
- });
104
-
105
- const parsed = new URL(url);
106
- const exp = parsed.searchParams.get("exp");
107
- const sig = parsed.searchParams.get("sig");
108
- assert.ok(exp);
109
- assert.ok(sig);
110
- // The signature must verify against the same decoded storage path the
111
- // `/content` route recomputes from `req.path`.
112
- const relativePath = parsed.pathname.replace(/^\/content\//, "");
113
- const result = verifyContentSignature(
114
- relativePath,
115
- exp,
116
- sig,
117
- secret,
118
- Math.floor(Date.now() / 1000),
119
- );
120
- assert.deepEqual(result, { valid: true });
121
- });
122
-
123
- it("nests accountConfigId before accountId so the Lambda@Edge prefix check matches", () => {
124
- const url = buildContentUrl({
125
- domain: "https://cdn.test",
126
- accountConfigId: "CFG",
127
- accountId: "ACC",
128
- messageId: "MSG",
129
- partPath: "1",
130
- });
131
-
132
- const segments = new URL(url).pathname.split("/");
133
- // ["", "content", "accounts", "CFG", "ACC", "messages", "MSG", "parts", "1"]
134
- assert.equal(segments[1], "content");
135
- assert.equal(segments[2], "accounts");
136
- assert.equal(segments[3], "CFG");
137
- assert.equal(segments[4], "ACC");
138
- });
139
- });
140
-
141
- describe("getContentDeliveryDomain", () => {
142
- const ORIGINAL = process.env.CONTENT_DELIVERY_DOMAIN;
143
-
144
- beforeEach(() => {
145
- delete process.env.CONTENT_DELIVERY_DOMAIN;
146
- });
147
-
148
- afterEach(() => {
149
- if (ORIGINAL === undefined) delete process.env.CONTENT_DELIVERY_DOMAIN;
150
- else process.env.CONTENT_DELIVERY_DOMAIN = ORIGINAL;
151
- });
152
-
153
- it("throws when CONTENT_DELIVERY_DOMAIN is unset (fail loud over silent placeholder, #299)", () => {
154
- assert.throws(
155
- () => getContentDeliveryDomain(),
156
- /CONTENT_DELIVERY_DOMAIN is not set/,
157
- );
158
- });
159
-
160
- it("throws when CONTENT_DELIVERY_DOMAIN is empty", () => {
161
- process.env.CONTENT_DELIVERY_DOMAIN = "";
162
- assert.throws(
163
- () => getContentDeliveryDomain(),
164
- /CONTENT_DELIVERY_DOMAIN is not set/,
165
- );
166
- });
167
-
168
- it("returns the env var value when set", () => {
169
- process.env.CONTENT_DELIVERY_DOMAIN = "https://abc.cloudfront.net";
170
- assert.equal(getContentDeliveryDomain(), "https://abc.cloudfront.net");
171
- });
172
- });