@remit/backend 0.0.143 → 0.0.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -522
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. package/tsconfig.json +0 -7
@@ -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
- });
@@ -1,83 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { chmodSync, mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { afterEach, describe, it } from "node:test";
6
- import Database from "better-sqlite3";
7
- import { checkRelationalStore } from "./relational-health.js";
8
-
9
- describe("checkRelationalStore", () => {
10
- const ORIGINAL_DATA_BACKEND = process.env.DATA_BACKEND;
11
- const ORIGINAL_SQLITE_DB_PATH = process.env.SQLITE_DB_PATH;
12
- let dir: string | undefined;
13
-
14
- afterEach(() => {
15
- if (ORIGINAL_DATA_BACKEND === undefined) delete process.env.DATA_BACKEND;
16
- else process.env.DATA_BACKEND = ORIGINAL_DATA_BACKEND;
17
- if (ORIGINAL_SQLITE_DB_PATH === undefined) {
18
- delete process.env.SQLITE_DB_PATH;
19
- } else {
20
- process.env.SQLITE_DB_PATH = ORIGINAL_SQLITE_DB_PATH;
21
- }
22
- if (dir) {
23
- rmSync(dir, { recursive: true, force: true });
24
- dir = undefined;
25
- }
26
- });
27
-
28
- it("is true without touching disk on the AWS/DynamoDB path", async () => {
29
- delete process.env.DATA_BACKEND;
30
- delete process.env.SQLITE_DB_PATH;
31
- assert.equal(await checkRelationalStore(), true);
32
-
33
- process.env.DATA_BACKEND = "dynamodb";
34
- assert.equal(await checkRelationalStore(), true);
35
- });
36
-
37
- it("is true when the SQLite file is reachable", async () => {
38
- dir = mkdtempSync(join(tmpdir(), "remit-health-"));
39
- const dbPath = join(dir, "remit.db");
40
- new Database(dbPath).close();
41
-
42
- process.env.DATA_BACKEND = "sqlite";
43
- process.env.SQLITE_DB_PATH = dbPath;
44
-
45
- assert.equal(await checkRelationalStore(), true);
46
- });
47
-
48
- it("is false when SQLITE_DB_PATH points at a directory that does not exist", async () => {
49
- process.env.DATA_BACKEND = "sqlite";
50
- process.env.SQLITE_DB_PATH = "/nonexistent/remit.db";
51
-
52
- assert.equal(await checkRelationalStore(), false);
53
- });
54
-
55
- it("is false when the SQLite file exists but is unreadable", async () => {
56
- dir = mkdtempSync(join(tmpdir(), "remit-health-"));
57
- const dbPath = join(dir, "remit.db");
58
- new Database(dbPath).close();
59
- chmodSync(dbPath, 0o000);
60
-
61
- process.env.DATA_BACKEND = "sqlite";
62
- process.env.SQLITE_DB_PATH = dbPath;
63
-
64
- assert.equal(await checkRelationalStore(), false);
65
- });
66
-
67
- it("is false when the file is readable but no longer writable", async () => {
68
- dir = mkdtempSync(join(tmpdir(), "remit-health-"));
69
- const dbPath = join(dir, "remit.db");
70
- new Database(dbPath).close();
71
-
72
- process.env.DATA_BACKEND = "sqlite";
73
- process.env.SQLITE_DB_PATH = dbPath;
74
- assert.equal(await checkRelationalStore(), true);
75
-
76
- // Readable, not writable: the read-only SELECT ping still opens and
77
- // succeeds, so without `access(W_OK)` this reports healthy on a store the
78
- // backend can no longer write to. This is the case the writability check
79
- // exists to catch — dropping `access()` from pingSqlite fails this test.
80
- chmodSync(dbPath, 0o400);
81
- assert.equal(await checkRelationalStore(), false);
82
- });
83
- });
@@ -1,39 +0,0 @@
1
- import { access, constants } from "node:fs/promises";
2
- import { isSelfHostSqlBackend } from "../src/data-backend.js";
3
-
4
- // The SELECT ping opens a read-only connection, so it keeps succeeding on a
5
- // file that can still be read but no longer written — permissions revoked to
6
- // read-only, or the volume remounted read-only under the process. The backend
7
- // must write (better-auth persists sessions to this same file, RFC 036 D3), so
8
- // a readable-but-unwritable store has to report unhealthy. `access(R_OK|W_OK)`
9
- // checks both bits in one fresh syscall; the read-only query never covers
10
- // writability. The query after it still exercises the SQL engine itself.
11
- const pingSqlite = async (): Promise<void> => {
12
- const path = process.env.SQLITE_DB_PATH ?? "";
13
- await access(path, constants.R_OK | constants.W_OK);
14
-
15
- const { default: Database } = await import("better-sqlite3");
16
- const db = new Database(path, { readonly: true });
17
- try {
18
- db.prepare("SELECT 1").get();
19
- } finally {
20
- db.close();
21
- }
22
- };
23
-
24
- /**
25
- * A trivial read against the relational store — the dependency `/health` must
26
- * actually exercise (RFC 037 D5, R9) so a backend that
27
- * booted with an unusable database reports unhealthy instead of a bare 200. The
28
- * AWS/DynamoDB path has no relational store and is never checked.
29
- */
30
- export const checkRelationalStore = async (): Promise<boolean> => {
31
- if (!isSelfHostSqlBackend()) return true;
32
-
33
- try {
34
- await pingSqlite();
35
- return true;
36
- } catch {
37
- return false;
38
- }
39
- };