@remit/backend 0.0.142 → 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 -521
  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
@@ -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,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
- });
@@ -1,86 +0,0 @@
1
- /**
2
- * Build the public CloudFront URL that fetches a single body part for a
3
- * message. The path layout is what the Lambda@Edge JWT verifier expects
4
- * (PR 1 of #224): `/content/accounts/{accountConfigId}/{accountId}/messages/
5
- * {messageId}/parts/{partPath}`.
6
- *
7
- * The function is intentionally pure — domain comes from configuration so
8
- * tests can pin the output without env vars. `partPath` is passed through
9
- * `encodeURIComponent` because IMAP section paths are dot-separated digits
10
- * (e.g. `1`, `1.2`, `2.1.3`) but a defensive encode keeps the builder safe
11
- * if the path ever carries unexpected characters.
12
- */
13
- import type { ContentSigner } from "./contentSignature.js";
14
-
15
- export interface BuildContentUrlInput {
16
- domain: string;
17
- accountConfigId: string;
18
- accountId: string;
19
- messageId: string;
20
- partPath: string;
21
- /**
22
- * When present (self-host stack), append an `exp`/`sig` signature scoped to
23
- * this account's storage path. Absent on AWS, where Lambda@Edge guards the
24
- * URL and the path stays unsigned. See `contentSignature.ts`.
25
- */
26
- sign?: ContentSigner;
27
- }
28
-
29
- export interface BuildStorageContentUrlInput {
30
- domain: string;
31
- storageKey: string;
32
- sign?: ContentSigner;
33
- }
34
-
35
- export const buildStorageContentUrl = (
36
- input: BuildStorageContentUrlInput,
37
- ): string => {
38
- const { domain, storageKey, sign } = input;
39
- const normalizedDomain = domain.replace(/\/+$/, "");
40
- const base = normalizedDomain.startsWith("http")
41
- ? normalizedDomain
42
- : `https://${normalizedDomain}`;
43
- // The signed message is the decoded, account-scoped storage path — the same
44
- // value the `/content` route recomputes from `req.path`. Keep it in lockstep
45
- // with the verifier's `req.path.replace(/^\/content\//, "")`.
46
- const safeKey = storageKey
47
- .split("/")
48
- .map((segment) => encodeURIComponent(segment))
49
- .join("/");
50
- const url = `${base}/content/${safeKey}`;
51
- if (!sign) return url;
52
- const { exp, sig } = sign(storageKey);
53
- return `${url}?exp=${exp}&sig=${encodeURIComponent(sig)}`;
54
- };
55
-
56
- export const buildContentUrl = (input: BuildContentUrlInput): string => {
57
- const { domain, accountConfigId, accountId, messageId, partPath, sign } =
58
- input;
59
- return buildStorageContentUrl({
60
- domain,
61
- storageKey: `accounts/${accountConfigId}/${accountId}/messages/${messageId}/parts/${partPath}`,
62
- sign,
63
- });
64
- };
65
-
66
- /**
67
- * Read the CloudFront distribution domain from the environment. The CDK
68
- * wires `CONTENT_DELIVERY_DOMAIN` onto the API Lambda from the
69
- * ContentDelivery stack's published SSM parameter (`/{stage}/Remit/frontendUrl`).
70
- *
71
- * Throws when the env var is missing or empty. `BodyPartResponse.contentUrl`
72
- * is required + URL-formatted in the API contract, so silently emitting a
73
- * placeholder string would ship a contract lie to clients. Failing loud here
74
- * surfaces the missing wire-up as a 500 the deploy team can act on (#299).
75
- * Production synth hard-codes a syntactically-valid placeholder URL via
76
- * `infra/bin/infra.ts` so this only fires when the env is actually unset.
77
- */
78
- export const getContentDeliveryDomain = (): string => {
79
- const value = process.env.CONTENT_DELIVERY_DOMAIN;
80
- if (!value || value.length === 0) {
81
- throw new Error(
82
- "CONTENT_DELIVERY_DOMAIN is not set; BodyPartResponse.contentUrl cannot be built. Wire the CloudFront distribution domain via the API Lambda env (see infra/bin/infra.ts).",
83
- );
84
- }
85
- return value;
86
- };
@@ -1,34 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { AddressFlags } from "@remit/api-openapi-types";
4
- import { deriveMuted } from "./deriveMuted.js";
5
-
6
- const SET_AT = 1_700_000_000_000;
7
-
8
- describe("deriveMuted", () => {
9
- it("returns false when flags is undefined", () => {
10
- assert.equal(deriveMuted(undefined), false);
11
- });
12
-
13
- it("returns false for an empty flags object", () => {
14
- assert.equal(deriveMuted({}), false);
15
- });
16
-
17
- it("returns false when muted.value is false", () => {
18
- const flags: AddressFlags = { muted: { value: false, setAt: SET_AT } };
19
- assert.equal(deriveMuted(flags), false);
20
- });
21
-
22
- it("returns true when muted.value is true", () => {
23
- const flags: AddressFlags = { muted: { value: true, setAt: SET_AT } };
24
- assert.equal(deriveMuted(flags), true);
25
- });
26
-
27
- it("ignores other flags (orthogonal axis)", () => {
28
- const flags: AddressFlags = {
29
- vip: { value: true, setAt: SET_AT },
30
- wellknown: { value: true, setAt: SET_AT },
31
- };
32
- assert.equal(deriveMuted(flags), false);
33
- });
34
- });