@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
@@ -1,105 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { gzipSync } from "node:zlib";
4
- import {
5
- parseContentStorageKey,
6
- type ServeContentDeps,
7
- serveContent,
8
- } from "./content-handler.js";
9
-
10
- const PART_KEY = "accounts/cfg-1/acc-1/messages/msg-1/parts/1.2";
11
-
12
- const baseDeps = (
13
- over: Partial<ServeContentDeps> = {},
14
- ): { deps: ServeContentDeps; cues: Array<Record<string, unknown>> } => {
15
- const cues: Array<Record<string, unknown>> = [];
16
- const deps: ServeContentDeps = {
17
- readObject: async () => null,
18
- lookupMessage: async () => ({ mailboxId: "mbx-1", uid: 42 }),
19
- requestBodySync: async (input) => {
20
- cues.push(input);
21
- },
22
- ...over,
23
- };
24
- return { deps, cues };
25
- };
26
-
27
- describe("parseContentStorageKey", () => {
28
- it("extracts account + message ids from a part key", () => {
29
- assert.deepEqual(parseContentStorageKey(PART_KEY), {
30
- accountConfigId: "cfg-1",
31
- accountId: "acc-1",
32
- messageId: "msg-1",
33
- });
34
- });
35
-
36
- it("returns null for a non-matching key", () => {
37
- assert.equal(parseContentStorageKey("nope/whatever"), null);
38
- });
39
- });
40
-
41
- describe("serveContent", () => {
42
- it("returns 200 with decompressed bytes when the object is present", async () => {
43
- const { deps, cues } = baseDeps({
44
- readObject: async () => gzipSync(Buffer.from("hello body")),
45
- });
46
-
47
- const result = await serveContent(deps, {
48
- fullPath: "/x/parts/1.2",
49
- storageKey: PART_KEY,
50
- });
51
-
52
- assert.equal(result.status, 200);
53
- assert.equal(result.headers["content-type"], "application/octet-stream");
54
- assert.equal(result.body.toString(), "hello body");
55
- assert.equal(cues.length, 0);
56
- });
57
-
58
- it("returns 202 + Retry-After and re-arms the cue when the object is missing", async () => {
59
- const { deps, cues } = baseDeps({ readObject: async () => null });
60
-
61
- const result = await serveContent(deps, {
62
- fullPath: "/x/parts/1.2",
63
- storageKey: PART_KEY,
64
- });
65
-
66
- assert.equal(result.status, 202);
67
- assert.equal(result.headers["Retry-After"], "1");
68
- assert.equal(cues.length, 1, "cue re-armed exactly once");
69
- assert.deepEqual(cues[0], {
70
- accountId: "acc-1",
71
- mailboxId: "mbx-1",
72
- messageId: "msg-1",
73
- uid: 42,
74
- });
75
- });
76
-
77
- it("still answers 202 (no cue) when the message row is gone", async () => {
78
- const { deps, cues } = baseDeps({
79
- readObject: async () => null,
80
- lookupMessage: async () => null,
81
- });
82
-
83
- const result = await serveContent(deps, {
84
- fullPath: "/x/parts/1.2",
85
- storageKey: PART_KEY,
86
- });
87
-
88
- assert.equal(result.status, 202);
89
- assert.equal(cues.length, 0);
90
- });
91
-
92
- it("propagates a non-missing read error so the route 500s", async () => {
93
- const { deps } = baseDeps({
94
- readObject: async () => {
95
- throw new Error("EACCES");
96
- },
97
- });
98
-
99
- await assert.rejects(
100
- () =>
101
- serveContent(deps, { fullPath: "/x/parts/1.2", storageKey: PART_KEY }),
102
- /EACCES/,
103
- );
104
- });
105
- });
@@ -1,79 +0,0 @@
1
- import { gunzipSync } from "node:zlib";
2
- import { parseContentStorageKey } from "@remit/storage-service";
3
-
4
- // Re-exported for existing same-package consumers (server.ts,
5
- // content-handler.test.ts). The key-shape parser itself lives in
6
- // `@remit/storage-service` next to the `build*Key` functions it
7
- // inverts, so non-backend consumers (e.g. the content-selfheal worker) can
8
- // depend on it without pulling in the rest of `@remit/backend`.
9
- export { parseContentStorageKey };
10
-
11
- export interface ContentResult {
12
- status: number;
13
- headers: Record<string, string>;
14
- body: string | Buffer;
15
- }
16
-
17
- export interface ServeContentDeps {
18
- /**
19
- * Reads the stored bytes, or returns null when the object is genuinely
20
- * missing (ENOENT / NoSuchKey). Any other read error must throw so the route
21
- * 500s — a permission/infra failure is never masked as a missing body.
22
- */
23
- readObject(fullPath: string): Promise<Buffer | null>;
24
- /** Resolves the message's mailbox + uid so the cue can issue one FETCH. */
25
- lookupMessage(
26
- messageId: string,
27
- ): Promise<{ mailboxId: string; uid: number } | null>;
28
- requestBodySync(input: {
29
- accountId: string;
30
- mailboxId: string;
31
- messageId: string;
32
- uid?: number;
33
- }): Promise<void>;
34
- }
35
-
36
- /**
37
- * Local stand-in for the CloudFront `/content/*` behavior. Body-fetch outcomes:
38
- *
39
- * - Object present → 200 with the (decompressed) bytes.
40
- * - Object missing → 202 + `Retry-After: 1`, and re-arm the SYNC_MESSAGE_BODY
41
- * cue for that message so a retry succeeds once the worker stores the body.
42
- * - Any other read failure throws, so the route 500s loudly.
43
- */
44
- export const serveContent = async (
45
- deps: ServeContentDeps,
46
- args: { fullPath: string; storageKey: string },
47
- ): Promise<ContentResult> => {
48
- const raw = await deps.readObject(args.fullPath);
49
-
50
- if (raw) {
51
- // The filesystem/S3 backends gzip every part; decompress here since there
52
- // is no CloudFront to auto-decode locally.
53
- const buffer = raw[0] === 0x1f && raw[1] === 0x8b ? gunzipSync(raw) : raw;
54
- return {
55
- status: 200,
56
- headers: { "content-type": "application/octet-stream" },
57
- body: buffer,
58
- };
59
- }
60
-
61
- const ids = parseContentStorageKey(args.storageKey);
62
- if (ids) {
63
- const message = await deps.lookupMessage(ids.messageId);
64
- if (message) {
65
- await deps.requestBodySync({
66
- accountId: ids.accountId,
67
- mailboxId: message.mailboxId,
68
- messageId: ids.messageId,
69
- uid: message.uid,
70
- });
71
- }
72
- }
73
-
74
- return {
75
- status: 202,
76
- headers: { "Retry-After": "1" },
77
- body: "body not yet synced",
78
- };
79
- };
@@ -1,37 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { sep } from "node:path";
3
- import { describe, it } from "node:test";
4
- import { resolveContentPath } from "./content-path.js";
5
-
6
- describe("resolveContentPath — dev-server /content/* path safety (#310 review P1)", () => {
7
- const ROOT = "/x/storage";
8
-
9
- it("returns the absolute path for a normal nested storage key", () => {
10
- assert.equal(
11
- resolveContentPath(ROOT, "accounts/cfg/acc/messages/m/parts/1"),
12
- "/x/storage/accounts/cfg/acc/messages/m/parts/1",
13
- );
14
- });
15
-
16
- it("rejects a sibling-prefix escape (`startsWith` was the original bug)", () => {
17
- // `/x/storage-evil/y` shares the prefix `/x/storage` with the root,
18
- // so the naive `fullPath.startsWith(ROOT)` check would pass it.
19
- assert.equal(resolveContentPath(ROOT, `..${sep}storage-evil${sep}y`), null);
20
- });
21
-
22
- it("rejects parent-directory traversal via `..`", () => {
23
- assert.equal(resolveContentPath(ROOT, `..${sep}etc${sep}passwd`), null);
24
- assert.equal(
25
- resolveContentPath(ROOT, `accounts${sep}..${sep}..${sep}etc`),
26
- null,
27
- );
28
- });
29
-
30
- it("rejects an absolute key that resolves outside the root", () => {
31
- assert.equal(resolveContentPath(ROOT, "/etc/passwd"), null);
32
- });
33
-
34
- it("rejects an empty key (would expose the root listing)", () => {
35
- assert.equal(resolveContentPath(ROOT, ""), null);
36
- });
37
- });
@@ -1,27 +0,0 @@
1
- import { relative, resolve, sep } from "node:path";
2
-
3
- /**
4
- * Resolve a `/content/*` URL path to an absolute filesystem path under the
5
- * storage root, or return null if the resolved path would escape the root.
6
- *
7
- * The naive `fullPath.startsWith(STORAGE_BASE)` check fails on a sibling
8
- * directory whose name is a prefix of the base — e.g. `/x/storage` and
9
- * `/x/storage-evil` — so we use `path.relative` and reject any result that
10
- * starts with `..` or is itself absolute (#310 review P1). Empty keys are
11
- * also rejected so the root directory is never served.
12
- */
13
- export const resolveContentPath = (
14
- storageBase: string,
15
- storageKey: string,
16
- ): string | null => {
17
- const fullPath = resolve(storageBase, storageKey);
18
- const rel = relative(storageBase, fullPath);
19
- if (rel === "" || rel.startsWith("..") || rel.includes(`..${sep}`)) {
20
- return null;
21
- }
22
- if (resolve(rel) === rel) {
23
- // `rel` is absolute → escaped the root via an absolute storageKey.
24
- return null;
25
- }
26
- return fullPath;
27
- };
@@ -1,48 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { parseAllowedOrigins, resolveAllowOrigin } from "./cors.js";
4
-
5
- describe("parseAllowedOrigins", () => {
6
- it("splits, trims, and drops empties", () => {
7
- assert.deepEqual(
8
- parseAllowedOrigins("https://a.example, https://b.example ,"),
9
- ["https://a.example", "https://b.example"],
10
- );
11
- });
12
-
13
- it("returns an empty list for undefined or blank", () => {
14
- assert.deepEqual(parseAllowedOrigins(undefined), []);
15
- assert.deepEqual(parseAllowedOrigins(" "), []);
16
- });
17
- });
18
-
19
- describe("resolveAllowOrigin", () => {
20
- it("returns '*' when the allowlist contains a wildcard", () => {
21
- assert.equal(resolveAllowOrigin("https://any.example", ["*"]), "*");
22
- assert.equal(resolveAllowOrigin(undefined, ["*"]), "*");
23
- });
24
-
25
- it("reflects an allowlisted origin", () => {
26
- assert.equal(
27
- resolveAllowOrigin("https://app.example", [
28
- "https://app.example",
29
- "https://other.example",
30
- ]),
31
- "https://app.example",
32
- );
33
- });
34
-
35
- it("returns undefined for an origin outside the allowlist", () => {
36
- assert.equal(
37
- resolveAllowOrigin("https://evil.example", ["https://app.example"]),
38
- undefined,
39
- );
40
- });
41
-
42
- it("returns undefined when no origin is presented and no wildcard", () => {
43
- assert.equal(
44
- resolveAllowOrigin(undefined, ["https://app.example"]),
45
- undefined,
46
- );
47
- });
48
- });
@@ -1,25 +0,0 @@
1
- export const parseAllowedOrigins = (raw: string | undefined): string[] =>
2
- (raw ?? "")
3
- .split(",")
4
- .map((origin) => origin.trim())
5
- .filter((origin) => origin.length > 0);
6
-
7
- /**
8
- * Resolve the `Access-Control-Allow-Origin` value for a request against the
9
- * configured allowlist.
10
- *
11
- * - a `*` entry allows any origin (browsers reject `*` with credentials, so this
12
- * stays safe for a bearer-token API);
13
- * - otherwise the request `Origin` is reflected only when it is in the allowlist;
14
- * - an origin outside the allowlist yields `undefined` — no CORS header is sent.
15
- */
16
- export const resolveAllowOrigin = (
17
- requestOrigin: string | undefined,
18
- allowedOrigins: string[],
19
- ): string | undefined => {
20
- if (allowedOrigins.includes("*")) return "*";
21
- if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
22
- return requestOrigin;
23
- }
24
- return undefined;
25
- };
@@ -1,24 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import type { Server } from "node:http";
3
- import { after, before, it } from "node:test";
4
- import { UPSTREAM_KEEPALIVE_IDLE_TIMEOUT_SECONDS } from "@remit/apisix/route-table";
5
-
6
- let server: Server;
7
-
8
- before(async () => {
9
- process.env.BETTER_AUTH_SECRET = "a-signing-secret-of-at-least-32-characters";
10
- process.env.CORS_ALLOWED_ORIGINS = "https://mail.example.com";
11
- process.env.SERVER_PORT = "0";
12
- server = (await import("./server.js")).listener;
13
- });
14
-
15
- after(async () => {
16
- await new Promise<void>((resolve) => server.close(() => resolve()));
17
- });
18
-
19
- it("keeps an idle socket open longer than the APISIX upstream pool does", () => {
20
- assert.ok(
21
- server.keepAliveTimeout > UPSTREAM_KEEPALIVE_IDLE_TIMEOUT_SECONDS * 1_000,
22
- `backend keepAliveTimeout ${server.keepAliveTimeout}ms must exceed APISIX idle_timeout ${UPSTREAM_KEEPALIVE_IDLE_TIMEOUT_SECONDS}s`,
23
- );
24
- });
@@ -1,69 +0,0 @@
1
- import type {
2
- APIGatewayProxyEvent,
3
- APIGatewayProxyEventPathParameters,
4
- Context,
5
- } from "aws-lambda";
6
- import type { Request } from "express";
7
-
8
- export const createLambdaEvent = (req: Request): APIGatewayProxyEvent => ({
9
- httpMethod: req.method,
10
- path: req.path,
11
- pathParameters: (req.params as APIGatewayProxyEventPathParameters) || null,
12
- queryStringParameters: (req.query as { [name: string]: string }) || null,
13
- headers: (req.headers as { [name: string]: string }) || {},
14
- body: req.body ? JSON.stringify(req.body) : null,
15
- isBase64Encoded: false,
16
- requestContext: {
17
- requestId: `local-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
18
- stage: "local",
19
- httpMethod: req.method,
20
- path: req.path,
21
- protocol: "HTTP/1.1",
22
- requestTime: new Date().toISOString(),
23
- requestTimeEpoch: Date.now(),
24
- identity: {
25
- sourceIp: req.ip || "127.0.0.1",
26
- userAgent: req.get("User-Agent") || "local-test",
27
- accessKey: null,
28
- accountId: null,
29
- apiKey: null,
30
- apiKeyId: null,
31
- caller: null,
32
- cognitoAuthenticationProvider: null,
33
- cognitoAuthenticationType: null,
34
- cognitoIdentityId: null,
35
- cognitoIdentityPoolId: null,
36
- principalOrgId: null,
37
- user: null,
38
- userArn: null,
39
- clientCert: null,
40
- },
41
- accountId: "123456789012",
42
- apiId: "local",
43
- domainName: "localhost",
44
- domainPrefix: "local",
45
- resourceId: "local",
46
- resourcePath: req.path,
47
- authorizer: undefined,
48
- },
49
- resource: req.path,
50
- multiValueHeaders: {},
51
- multiValueQueryStringParameters: null,
52
- stageVariables: null,
53
- });
54
-
55
- export const createLambdaContext = (): Context => ({
56
- callbackWaitsForEmptyEventLoop: false,
57
- functionName: "remit-backend-local",
58
- functionVersion: "$LATEST",
59
- invokedFunctionArn:
60
- "arn:aws:lambda:local:123456789012:function:remit-backend-local",
61
- memoryLimitInMB: "128",
62
- awsRequestId: `local-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
63
- logGroupName: "/aws/lambda/remit-backend-local",
64
- logStreamName: `${new Date().toISOString().split("T")[0]}/[$LATEST]${Math.random().toString(36).substr(2, 9)}`,
65
- getRemainingTimeInMillis: () => 30000,
66
- done: () => {},
67
- fail: () => {},
68
- succeed: () => {},
69
- });
@@ -1,173 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { spawn } from "node:child_process";
3
- import { createServer } from "node:net";
4
- import { dirname, resolve } from "node:path";
5
- import { after, before, describe, it } from "node:test";
6
- import { fileURLToPath } from "node:url";
7
-
8
- // This file is the backend image's entrypoint, so what it writes IS the
9
- // container's log stream. deploy/vps/README.md ("Logs") promises one JSON object
10
- // per line, and a log-shipping pipeline written against that contract breaks on
11
- // the first line it cannot parse — which is why this runs the real server rather
12
- // than asserting against a mocked writer.
13
- //
14
- // Both streams are held to it. The container log driver merges stdout and
15
- // stderr into one log and `remit logs` shows both, so a raw line on stderr
16
- // breaks a Vector parser exactly like a raw line on stdout.
17
-
18
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
19
-
20
- const freePort = (): Promise<number> =>
21
- new Promise((resolveWith, reject) => {
22
- const probe = createServer();
23
- probe.on("error", reject);
24
- probe.listen(0, "127.0.0.1", () => {
25
- const address = probe.address();
26
- if (address === null || typeof address === "string") {
27
- probe.close();
28
- reject(new Error("could not reserve a port"));
29
- return;
30
- }
31
- probe.close(() => resolveWith(address.port));
32
- });
33
- });
34
-
35
- type Line = Record<string, unknown>;
36
- type Stream = "stdout" | "stderr";
37
-
38
- const captured: Record<Stream, string> = { stdout: "", stderr: "" };
39
- const listeners = new Set<() => void>();
40
- let port = 0;
41
-
42
- // The child's output reaches this process asynchronously, so a request that has
43
- // already been answered is not yet a line here. Wait for the line rather than
44
- // for a duration, and only once the stream ends on a newline, so no assertion
45
- // ever runs against half of one.
46
- const waitForStdout = (contains: string): Promise<void> =>
47
- new Promise((resolveWith, reject) => {
48
- const settled = () =>
49
- captured.stdout.includes(contains) && captured.stdout.endsWith("\n");
50
- if (settled()) {
51
- resolveWith();
52
- return;
53
- }
54
- const timer = setTimeout(() => {
55
- stopWaiting();
56
- reject(new Error(`stdout never carried ${contains}: ${captured.stdout}`));
57
- }, 10_000);
58
- const onData = () => {
59
- if (!settled()) return;
60
- stopWaiting();
61
- resolveWith();
62
- };
63
- const stopWaiting = () => {
64
- clearTimeout(timer);
65
- listeners.delete(onData);
66
- };
67
- listeners.add(onData);
68
- });
69
-
70
- const parsedLines = (stream: Stream): Line[] =>
71
- captured[stream]
72
- .split("\n")
73
- .filter((line) => line.trim().length > 0)
74
- .map((line, index) => {
75
- try {
76
- return JSON.parse(line) as Line;
77
- } catch {
78
- throw new assert.AssertionError({
79
- message: `${stream} line ${index + 1} is not JSON, so a log pipeline drops it: ${line}`,
80
- });
81
- }
82
- });
83
-
84
- describe("the backend entrypoint's log output", () => {
85
- let server: ReturnType<typeof spawn>;
86
-
87
- before(async () => {
88
- port = await freePort();
89
-
90
- server = spawn(
91
- process.execPath,
92
- ["--import", "tsx", "dev-server/server.ts"],
93
- {
94
- cwd: packageRoot,
95
- stdio: ["ignore", "pipe", "pipe", "ipc"],
96
- env: {
97
- ...process.env,
98
- SERVER_PORT: String(port),
99
- LOG_LEVEL: "debug",
100
- REMIT_SERVICE_NAME: "backend",
101
- // tsx's own loader warnings are an artifact of running the
102
- // TypeScript source; the image runs a bundle and never emits them.
103
- NODE_NO_WARNINGS: "1",
104
- NODE_OPTIONS: "",
105
- },
106
- },
107
- );
108
-
109
- for (const stream of ["stdout", "stderr"] as const) {
110
- server[stream]?.setEncoding("utf8");
111
- server[stream]?.on("data", (chunk: string) => {
112
- captured[stream] += chunk;
113
- for (const listener of [...listeners]) listener();
114
- });
115
- }
116
-
117
- await new Promise<void>((resolveWith, reject) => {
118
- server.on("message", (message) => {
119
- if (message === "ready") resolveWith();
120
- });
121
- // Carry the child's stderr into the failure: a server that cannot boot
122
- // otherwise reports only an exit code, and the reason is in that buffer.
123
- server.on("exit", (code) =>
124
- reject(
125
- new Error(
126
- `server exited before listening (code ${code})\n${captured.stderr}`,
127
- ),
128
- ),
129
- );
130
- });
131
-
132
- // A request the router answers without touching a data backend, so the
133
- // invocation's own lines — the ones withTelemetry writes around the
134
- // handler — are captured by the time this suite reads them.
135
- await fetch(`http://127.0.0.1:${port}/no-such-route`);
136
- await waitForStdout("Request received");
137
- });
138
-
139
- after(() => {
140
- server.kill("SIGKILL");
141
- });
142
-
143
- it("is JSON on every line of both streams", () => {
144
- const lines = [...parsedLines("stdout"), ...parsedLines("stderr")];
145
- assert.ok(lines.length > 0, "expected the server to have written a line");
146
- for (const line of lines) {
147
- assert.equal(typeof line.level, "string");
148
- assert.equal(typeof line.time, "string");
149
- assert.equal(line.service, "backend");
150
- assert.equal(typeof line.msg, "string");
151
- }
152
- });
153
-
154
- it("reports what it is listening on as fields, not a banner", () => {
155
- const listening = parsedLines("stdout").find(
156
- (line) => line.msg === "Backend listening",
157
- );
158
- assert.ok(listening, "expected a startup line");
159
- assert.equal(listening.port, port);
160
- assert.equal(listening.url, `http://localhost:${port}`);
161
- assert.equal(listening.level, "info");
162
- });
163
-
164
- it("correlates the whole invocation under one requestId", () => {
165
- const lines = parsedLines("stdout").filter(
166
- (line) => line.msg === "Lambda invocation started" || line.path,
167
- );
168
- assert.ok(lines.length >= 2, "expected the invocation to have logged");
169
- const requestIds = new Set(lines.map((line) => line.requestId));
170
- assert.equal(requestIds.size, 1);
171
- assert.equal([...requestIds][0] === undefined, false);
172
- });
173
- });
@@ -1,66 +0,0 @@
1
- /**
2
- * The OAuth callback through the dev-server's own app — the process a
3
- * self-host container runs.
4
- *
5
- * One hop further than the handler test: the app object imported from
6
- * `server.ts`, so what is asserted is the header express actually writes onto
7
- * the socket for Microsoft's returning browser. The handler answering 302 is
8
- * not the same claim as the browser being sent anywhere, and the difference
9
- * between the two was a blank page.
10
- *
11
- * The identity tier is left unmounted — this app serves the route either way,
12
- * and the callback's exemption from the session gate is pinned where that gate
13
- * runs, in `src/handlers/account-oauth-callback.test.ts`.
14
- */
15
-
16
- import assert from "node:assert/strict";
17
- import type { Server } from "node:http";
18
- import { after, before, describe, it } from "node:test";
19
- import {
20
- _resetForTest,
21
- type RemitClient,
22
- setClient,
23
- } from "../src/service/data-client.js";
24
-
25
- const WEB_ORIGIN = "https://mail.example.com";
26
-
27
- let server: Server;
28
- let port: number;
29
-
30
- before(async () => {
31
- // Microsoft's redirect carries no session, so the callback answers before it
32
- // reads anything: no store is touched on this path.
33
- setClient({} as unknown as RemitClient);
34
-
35
- process.env.BETTER_AUTH_SECRET = "a-signing-secret-of-at-least-32-characters";
36
- process.env.CORS_ALLOWED_ORIGINS = WEB_ORIGIN;
37
- process.env.SERVER_PORT = "0";
38
-
39
- const imported = await import("./server.js");
40
- server = imported.listener;
41
- if (!server.listening) {
42
- await new Promise<void>((resolve) => server.once("listening", resolve));
43
- }
44
- const address = server.address();
45
- port = typeof address === "object" && address ? address.port : 0;
46
- });
47
-
48
- after(async () => {
49
- await new Promise<void>((resolve) => server.close(() => resolve()));
50
- _resetForTest();
51
- });
52
-
53
- describe("GET /accounts/oauth/microsoft/callback through the dev-server's own app", () => {
54
- it("answers the browser with somewhere to go", async () => {
55
- const response = await fetch(
56
- `http://127.0.0.1:${port}/accounts/oauth/microsoft/callback?error=access_denied`,
57
- { redirect: "manual" },
58
- );
59
-
60
- assert.equal(response.status, 302);
61
- assert.equal(
62
- response.headers.get("location"),
63
- `${WEB_ORIGIN}/settings/accounts?oauthError=access_denied`,
64
- );
65
- });
66
- });