@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,132 +0,0 @@
1
- import type { Readable } from "node:stream";
2
- import {
3
- authorizeUploadRequest,
4
- parseOutboxAttachmentKey,
5
- type StorageService,
6
- } from "@remit/storage-service";
7
-
8
- /**
9
- * The self-hosted receiver for an attachment upload.
10
- *
11
- * A hosted deployment mints a presigned PUT and none of this runs: the browser
12
- * writes to block storage and the size is bound by the storage service itself.
13
- * Where there is no storage service to presign against, this route is the
14
- * equivalent, and it has to bind the same things — one attachment, one size,
15
- * for a while — on its own.
16
- */
17
-
18
- export type CollectedBody =
19
- | { readonly outcome: "Collected"; readonly content: Buffer }
20
- | { readonly outcome: "TooLarge" };
21
-
22
- /**
23
- * Read a request body, refusing it the moment it passes what was reserved.
24
- *
25
- * The count is checked per chunk and the stream destroyed on the one that
26
- * crosses the line, so a client that reserved a megabyte and started sending a
27
- * hundred is cut off after the first megabyte rather than after all of it.
28
- */
29
- export const collectLimitedBody = async (
30
- stream: Readable,
31
- maxBytes: number,
32
- ): Promise<CollectedBody> => {
33
- const chunks: Buffer[] = [];
34
- let received = 0;
35
-
36
- for await (const chunk of stream) {
37
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
38
- received += buffer.length;
39
- if (received > maxBytes) {
40
- // Stop reading, but leave the socket alone: destroying it here races the
41
- // 413 the caller is about to write, and the client sees a connection
42
- // reset instead of the reason it was refused.
43
- stream.pause();
44
- return { outcome: "TooLarge" };
45
- }
46
- chunks.push(buffer);
47
- }
48
-
49
- return { outcome: "Collected", content: Buffer.concat(chunks) };
50
- };
51
-
52
- export interface ReceiveUploadInput {
53
- /** Decoded storage key, the `/outbox-upload/` prefix already stripped. */
54
- storageKey: string;
55
- exp: string | undefined;
56
- max: string | undefined;
57
- sig: string | undefined;
58
- body: Readable;
59
- nowSeconds: number;
60
- secret: string | undefined;
61
- /**
62
- * Whether the database still holds a live reservation for this attachment.
63
- * Injected rather than reached for, so the receiver stays a function of its
64
- * request and the storage it writes to.
65
- */
66
- findLiveReservation: (
67
- accountConfigId: string,
68
- outboxAttachmentId: string,
69
- nowSeconds: number,
70
- ) => Promise<boolean>;
71
- }
72
-
73
- export interface UploadResult {
74
- status: number;
75
- reason: string;
76
- }
77
-
78
- export const receiveUpload = async (
79
- storage: StorageService,
80
- input: ReceiveUploadInput,
81
- ): Promise<UploadResult> => {
82
- const target = parseOutboxAttachmentKey(input.storageKey);
83
- if (!target) return { status: 404, reason: "not-an-attachment-key" };
84
-
85
- const auth = authorizeUploadRequest({
86
- secret: input.secret,
87
- relativePath: input.storageKey,
88
- exp: input.exp,
89
- max: input.max,
90
- sig: input.sig,
91
- nowSeconds: input.nowSeconds,
92
- });
93
- if (!auth.authorized) {
94
- return { status: auth.status, reason: auth.reason };
95
- }
96
-
97
- // A signed URL outlives the draft it was minted for, so possession of one is
98
- // not enough: the database has to still hold a live reservation for this
99
- // attachment. Without this a URL minted before a discard writes bytes back
100
- // under a draft that no longer exists. The hosted shape cannot make this
101
- // check — block storage receives the PUT directly — which is what the sweep
102
- // exists for.
103
- const reserved = await input.findLiveReservation(
104
- target.accountConfigId,
105
- target.outboxAttachmentId,
106
- input.nowSeconds,
107
- );
108
- if (!reserved) return { status: 409, reason: "no-live-reservation" };
109
-
110
- const collected = await collectLimitedBody(input.body, auth.maxBytes);
111
- if (collected.outcome === "TooLarge") {
112
- return { status: 413, reason: "over-reserved-size" };
113
- }
114
-
115
- // Exactly the reserved size, not merely under it — the same thing S3 binds
116
- // when it recomputes a presigned PUT's signature over the content-length it
117
- // was sent. A short body is a failed upload, and the completion would refuse
118
- // it anyway; refusing here means no partial object is left to refuse.
119
- if (collected.content.length !== auth.maxBytes) {
120
- return { status: 400, reason: "size-mismatch" };
121
- }
122
-
123
- await storage.storeOutboxAttachment({
124
- accountConfigId: target.accountConfigId,
125
- accountId: target.accountId,
126
- outboxMessageId: target.outboxMessageId,
127
- outboxAttachmentId: target.outboxAttachmentId,
128
- content: collected.content,
129
- });
130
-
131
- return { status: 204, reason: "stored" };
132
- };
@@ -1,196 +0,0 @@
1
- /**
2
- * Issue #679: the upload route through the real dev-server app.
3
- *
4
- * Not a copy of its middleware order — the app object itself, imported from
5
- * `server.ts`. A copy is what let a JSON-typed upload silently read zero bytes:
6
- * `express.json` drains the stream before any route below it sees the body, and
7
- * a test that rebuilds the stack stays green when the real one is reordered.
8
- */
9
-
10
- import assert from "node:assert/strict";
11
- import { mkdtemp, rm } from "node:fs/promises";
12
- import type { Server } from "node:http";
13
- import { join } from "node:path";
14
- import { after, before, describe, it } from "node:test";
15
- import type {
16
- IOutboxAttachmentRepository,
17
- OutboxAttachmentItem,
18
- } from "@remit/data-ports";
19
- import { NotFoundError } from "@remit/data-ports/errors";
20
- import { OutboxAttachmentService } from "@remit/mailbox-service";
21
- import {
22
- type StorageService,
23
- UPLOAD_ROUTE_PREFIX,
24
- } from "@remit/storage-service";
25
- import { createFilesystemStorageService } from "@remit/storage-service/filesystem";
26
- import {
27
- _resetForTest,
28
- type RemitClient,
29
- setClient,
30
- } from "../src/service/data-client.js";
31
-
32
- const SECRET = "a-signing-secret-of-at-least-32-characters";
33
- const CFG = "cfg-upload";
34
- const ACC = "acc-upload";
35
- const DRAFT = "draft-upload";
36
-
37
- let storage: StorageService;
38
- let basePath: string;
39
- let rows: Map<string, OutboxAttachmentItem>;
40
- let server: Server;
41
- let port: number;
42
-
43
- const repository = (): IOutboxAttachmentRepository =>
44
- ({
45
- get: async (accountConfigId: string, outboxAttachmentId: string) => {
46
- const row = rows.get(outboxAttachmentId);
47
- if (!row || row.accountConfigId !== accountConfigId) {
48
- throw new NotFoundError("gone");
49
- }
50
- return row;
51
- },
52
- listByOutboxMessage: async () => [...rows.values()],
53
- }) as unknown as IOutboxAttachmentRepository;
54
-
55
- const reserve = async (
56
- outboxAttachmentId: string,
57
- sizeBytes: number,
58
- ): Promise<string> => {
59
- const expiresAt = Math.floor(Date.now() / 1000) + 900;
60
- rows.set(outboxAttachmentId, {
61
- outboxAttachmentId,
62
- outboxMessageId: DRAFT,
63
- accountId: ACC,
64
- accountConfigId: CFG,
65
- filename: `${outboxAttachmentId}.bin`,
66
- contentType: "application/octet-stream",
67
- sizeBytes,
68
- state: "Pending",
69
- storageKey: "",
70
- reservationExpiresAt: expiresAt,
71
- createdAt: 0,
72
- updatedAt: 0,
73
- });
74
- const { uploadUrl } = await storage.createOutboxAttachmentUploadUrl({
75
- accountConfigId: CFG,
76
- accountId: ACC,
77
- outboxMessageId: DRAFT,
78
- outboxAttachmentId,
79
- sizeBytes,
80
- expiresAt,
81
- });
82
- const url = new URL(uploadUrl);
83
- return `http://127.0.0.1:${port}${url.pathname}${url.search}`;
84
- };
85
-
86
- before(async () => {
87
- // Repo-local scratch, not the machine's shared temp directory.
88
- basePath = await mkdtemp(join(process.cwd(), ".tmp-upload-route-"));
89
- storage = createFilesystemStorageService(basePath, {
90
- origin: "http://127.0.0.1",
91
- signingSecret: SECRET,
92
- });
93
- rows = new Map();
94
-
95
- setClient({
96
- storage,
97
- outboxAttachment: new OutboxAttachmentService({
98
- outboxMessageService: {} as never,
99
- outboxAttachmentService: repository(),
100
- storage,
101
- }),
102
- } as unknown as RemitClient);
103
-
104
- process.env.BETTER_AUTH_SECRET = SECRET;
105
- // Port 0: the app binds whatever is free and tells us which.
106
- process.env.SERVER_PORT = "0";
107
- process.env.CORS_ALLOWED_ORIGINS = "*";
108
-
109
- const imported = await import("./server.js");
110
- server = imported.listener;
111
- if (!server.listening) {
112
- await new Promise<void>((resolve) => server.once("listening", resolve));
113
- }
114
- const address = server.address();
115
- port = typeof address === "object" && address ? address.port : 0;
116
- });
117
-
118
- after(async () => {
119
- await new Promise<void>((resolve) => server.close(() => resolve()));
120
- await rm(basePath, { recursive: true, force: true });
121
- _resetForTest();
122
- });
123
-
124
- describe("PUT /outbox-upload through the dev-server's own app", () => {
125
- it("stores every byte of a file the browser labelled application/json", async () => {
126
- const content = Buffer.from(JSON.stringify({ note: "x".repeat(100) }));
127
- const url = await reserve("attjson", content.length);
128
-
129
- const response = await fetch(url, {
130
- method: "PUT",
131
- body: content,
132
- // What `fetch(url, { body: file })` sends for a .json file: File.type.
133
- headers: { "content-type": "application/json" },
134
- });
135
-
136
- assert.equal(response.status, 204);
137
- assert.equal(
138
- (await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attjson"))
139
- ?.sizeBytes,
140
- content.length,
141
- );
142
- });
143
-
144
- it("stores every byte of a form-urlencoded-looking upload too", async () => {
145
- const content = Buffer.from(`a=1&b=2&c=${"z".repeat(200)}`);
146
- const url = await reserve("attform", content.length);
147
-
148
- const response = await fetch(url, {
149
- method: "PUT",
150
- body: content,
151
- headers: { "content-type": "application/x-www-form-urlencoded" },
152
- });
153
-
154
- assert.equal(response.status, 204);
155
- assert.equal(
156
- (await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attform"))
157
- ?.sizeBytes,
158
- content.length,
159
- );
160
- });
161
-
162
- it("stores binary unchanged", async () => {
163
- const content = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x10]);
164
- const url = await reserve("attpng", content.length);
165
-
166
- const response = await fetch(url, {
167
- method: "PUT",
168
- body: content,
169
- headers: { "content-type": "image/png" },
170
- });
171
-
172
- assert.equal(response.status, 204);
173
- assert.equal(
174
- (await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attpng"))
175
- ?.sizeBytes,
176
- content.length,
177
- );
178
- });
179
-
180
- it("refuses a PUT once the draft no longer has a live reservation", async () => {
181
- const url = await reserve("attgone", 8);
182
- rows.delete("attgone");
183
-
184
- const response = await fetch(url, {
185
- method: "PUT",
186
- body: Buffer.alloc(8),
187
- headers: { "content-type": "application/octet-stream" },
188
- });
189
-
190
- assert.equal(response.status, 409);
191
- assert.equal(
192
- await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attgone"),
193
- null,
194
- );
195
- });
196
- });
@@ -1,91 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
- import { logger } from "@remit/logger-lambda";
5
- import {
6
- backfillClassifications,
7
- type ClassificationBackfillCheckpoint,
8
- type ClassificationBackfillCheckpointStore,
9
- } from "@remit/mailbox-service";
10
- import { getClient } from "../src/service/data-client.js";
11
-
12
- /**
13
- * One-time, full-corpus classification backfill over the stored-body cohort
14
- * the classifier never examined (issue #1197): rows whose bodies were synced
15
- * before the classifier reached them, which carry `NotExamined` on
16
- * `Message.classificationState` and so stay `uncategorized` in the list. The
17
- * same alternate-entrypoint shape `backfill-list-id.mjs` uses: baked into the
18
- * backend image, never wired into a compose one-shot, run by hand once per
19
- * install:
20
- *
21
- * docker compose -f docker-compose.sqlite.yml run --rm backend \
22
- * node backfill-classification.mjs
23
- *
24
- * Safe to interrupt: progress is checkpointed to disk after every page and
25
- * picked back up on the next run, every already-examined row reads as
26
- * `alreadyExamined` on a rerun, and a row whose category was already decided
27
- * is recorded without re-deriving it, so a partial pass resumes rather than
28
- * repeats.
29
- */
30
-
31
- const CHECKPOINT_PATH =
32
- process.env.CLASSIFICATION_BACKFILL_CHECKPOINT_PATH ??
33
- "/data/sqlite/classification-backfill-checkpoint.json";
34
-
35
- const isCheckpoint = (
36
- value: unknown,
37
- ): value is ClassificationBackfillCheckpoint => {
38
- if (typeof value !== "object" || value === null) return false;
39
- if (!("accountConfigId" in value)) return false;
40
- const candidate = value as Record<string, unknown>;
41
- return (
42
- typeof candidate.accountConfigId === "string" &&
43
- (candidate.continuationToken === undefined ||
44
- typeof candidate.continuationToken === "string")
45
- );
46
- };
47
-
48
- const fileCheckpointStore: ClassificationBackfillCheckpointStore = {
49
- load: async () => {
50
- if (!existsSync(CHECKPOINT_PATH)) return undefined;
51
- const raw = await readFile(CHECKPOINT_PATH, "utf8");
52
- const parsed: unknown = JSON.parse(raw);
53
- return isCheckpoint(parsed) ? parsed : undefined;
54
- },
55
- save: async (checkpoint) => {
56
- await mkdir(dirname(CHECKPOINT_PATH), { recursive: true });
57
- const staging = `${CHECKPOINT_PATH}.writing`;
58
- await writeFile(staging, JSON.stringify(checkpoint));
59
- await rename(staging, CHECKPOINT_PATH);
60
- },
61
- clear: async () => {
62
- await rm(CHECKPOINT_PATH, { force: true });
63
- },
64
- };
65
-
66
- const run = async (): Promise<void> => {
67
- const client = await getClient();
68
-
69
- const result = await backfillClassifications(
70
- {
71
- accountConfigService: client.accountConfig,
72
- accountService: client.account,
73
- mailboxSpecialUseService: client.mailboxSpecialUse,
74
- addressService: client.address,
75
- threadMessageService: client.threadMessage,
76
- messageService: client.message,
77
- storageService: client.storage,
78
- },
79
- { checkpointStore: fileCheckpointStore, logger },
80
- );
81
-
82
- // biome-ignore lint/plugin/no-logger-info: a completed full-corpus backfill is an audit-grade signal
83
- logger.info({ result }, "backfill done");
84
- };
85
-
86
- run()
87
- .then(() => process.exit(0))
88
- .catch((error: unknown) => {
89
- logger.error({ error }, "backfill failed");
90
- process.exit(1);
91
- });
@@ -1,83 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
- import { logger } from "@remit/logger-lambda";
5
- import {
6
- backfillListIds,
7
- type ListIdBackfillCheckpoint,
8
- type ListIdBackfillCheckpointStore,
9
- } from "@remit/mailbox-service";
10
- import { getClient } from "../src/service/data-client.js";
11
-
12
- /**
13
- * One-time, full-corpus `ThreadMessage.listId` backfill (issue #263). Ships as
14
- * an alternate entrypoint baked into the backend image — "the backend image
15
- * with a command", the same shape `migrate.mjs` uses — rather than a service
16
- * of its own. Unlike `migrate`, it is never wired into a compose one-shot: a
17
- * full pass runs once per install, not on every restart. Invoke it by hand:
18
- *
19
- * docker compose -f docker-compose.sqlite.yml run --rm backend \
20
- * node backfill-list-id.mjs
21
- *
22
- * Safe to interrupt: progress is checkpointed to disk after every page and
23
- * picked back up on the next run, and every row it touches is read-only
24
- * against the stored message, so a rerun after a partial pass is a no-op for
25
- * everything already done.
26
- */
27
-
28
- const CHECKPOINT_PATH =
29
- process.env.LIST_ID_BACKFILL_CHECKPOINT_PATH ??
30
- "/data/sqlite/list-id-backfill-checkpoint.json";
31
-
32
- const isCheckpoint = (value: unknown): value is ListIdBackfillCheckpoint => {
33
- if (typeof value !== "object" || value === null) return false;
34
- if (!("accountConfigId" in value)) return false;
35
- const candidate = value as Record<string, unknown>;
36
- return (
37
- typeof candidate.accountConfigId === "string" &&
38
- (candidate.continuationToken === undefined ||
39
- typeof candidate.continuationToken === "string")
40
- );
41
- };
42
-
43
- const fileCheckpointStore: ListIdBackfillCheckpointStore = {
44
- load: async () => {
45
- if (!existsSync(CHECKPOINT_PATH)) return undefined;
46
- const raw = await readFile(CHECKPOINT_PATH, "utf8");
47
- const parsed: unknown = JSON.parse(raw);
48
- return isCheckpoint(parsed) ? parsed : undefined;
49
- },
50
- save: async (checkpoint) => {
51
- await mkdir(dirname(CHECKPOINT_PATH), { recursive: true });
52
- const staging = `${CHECKPOINT_PATH}.writing`;
53
- await writeFile(staging, JSON.stringify(checkpoint));
54
- await rename(staging, CHECKPOINT_PATH);
55
- },
56
- clear: async () => {
57
- await rm(CHECKPOINT_PATH, { force: true });
58
- },
59
- };
60
-
61
- const run = async (): Promise<void> => {
62
- const client = await getClient();
63
-
64
- const result = await backfillListIds(
65
- {
66
- accountConfigService: client.accountConfig,
67
- threadMessageService: client.threadMessage,
68
- messageService: client.message,
69
- storageService: client.storage,
70
- },
71
- { checkpointStore: fileCheckpointStore, logger },
72
- );
73
-
74
- // biome-ignore lint/plugin/no-logger-info: a completed full-corpus backfill is an audit-grade signal
75
- logger.info({ result }, "backfill done");
76
- };
77
-
78
- run()
79
- .then(() => process.exit(0))
80
- .catch((error: unknown) => {
81
- logger.error({ error }, "backfill failed");
82
- process.exit(1);
83
- });
@@ -1,128 +0,0 @@
1
- import { readConfigForExport } from "@remit/config-transfer";
2
- import { env } from "expect-env";
3
- // Reached by module path rather than through either package's entry point, the
4
- // same way the migrate entrypoint reaches its repairs: this file is bundled by
5
- // esbuild, and a barrel import would drag every repository and the native
6
- // driver behind it into a script that reads one table.
7
- import { auth_user } from "../../auth-service/src/schema/auth-schema-sqlite.js";
8
- import { createSqliteDatabase } from "../../drizzle-service/src/sqlite-client.js";
9
- import { deriveAccountConfigId } from "../src/auth.js";
10
- import { getClient } from "../src/service/data-client.js";
11
- import { exportIdentity } from "../src/service/export-identity.js";
12
-
13
- /**
14
- * `remit config save` (issue #1021). Writes one configuration out as a
15
- * versioned JSON document on stdout, over the same reader the export endpoint
16
- * uses. Ships as an alternate entrypoint in the backend image — "the backend
17
- * image with a command", the shape `migrate.mjs` and `backfill-list-id.mjs`
18
- * already use — because the operator runs it before a migration drops the
19
- * database, with no browser and no session to authenticate.
20
- *
21
- * The document goes to stdout and never to a path inside the container: the
22
- * wrapper redirects it to a host file, so the file lands where the operator can
23
- * see it and with their ownership, rather than root-owned inside a volume.
24
- */
25
-
26
- const USAGE = `Usage: node config-save.mjs [--user <email>]
27
-
28
- Writes the configuration as a versioned JSON document to stdout. Contains no
29
- credential: each account records which one it will need back instead.
30
-
31
- --user <email> Which sign-in to export. Optional on an instance that holds
32
- exactly one configuration.
33
- `;
34
-
35
- interface Options {
36
- user: string | undefined;
37
- }
38
-
39
- const parseArguments = (argv: readonly string[]): Options => {
40
- const options: Options = { user: undefined };
41
- for (let index = 0; index < argv.length; index += 1) {
42
- const argument = argv[index];
43
- if (argument === "--user") {
44
- const value = argv[index + 1];
45
- if (value === undefined || value.startsWith("--")) {
46
- throw new Error("--user needs an email address");
47
- }
48
- options.user = value;
49
- index += 1;
50
- continue;
51
- }
52
- throw new Error(`unknown option '${argument}'`);
53
- }
54
- return options;
55
- };
56
-
57
- /**
58
- * The configuration behind a sign-in. Identity lives in better-auth's own
59
- * tables in the same database file, and the configuration id derives from the
60
- * user id, so the email on the sign-in screen is the one thing an operator can
61
- * be expected to know.
62
- */
63
- const accountConfigIdForUser = async (email: string): Promise<string> => {
64
- const { db, close } = await createSqliteDatabase(
65
- { auth_user },
66
- { filename: env.SQLITE_DB_PATH },
67
- );
68
- try {
69
- const users = await db
70
- .select({ id: auth_user.id, email: auth_user.email })
71
- .from(auth_user);
72
- // Matched in JS rather than in the query: an address is
73
- // case-insensitive on the part that matters, and which collation the
74
- // column happens to carry is not something an operator should have to
75
- // know before their own email matches.
76
- const wanted = email.toLowerCase();
77
- const user = users.find((row) => row.email.toLowerCase() === wanted);
78
- if (!user) throw new Error(`no user signs in as ${email}`);
79
- return deriveAccountConfigId(user.id);
80
- } finally {
81
- await close();
82
- }
83
- };
84
-
85
- const soleAccountConfigId = async (
86
- listAll: () => Promise<Array<{ accountConfigId: string }>>,
87
- ): Promise<string> => {
88
- const configs = await listAll();
89
- const only = configs[0];
90
- if (!only) throw new Error("this instance holds no configuration to export");
91
- if (configs.length > 1) {
92
- throw new Error(
93
- `this instance holds ${configs.length} configurations — name one with --user <email>`,
94
- );
95
- }
96
- return only.accountConfigId;
97
- };
98
-
99
- const run = async (): Promise<void> => {
100
- const argv = process.argv.slice(2);
101
- if (argv.includes("--help") || argv.includes("-h")) {
102
- process.stdout.write(USAGE);
103
- return;
104
- }
105
-
106
- const options = parseArguments(argv);
107
- const client = await getClient();
108
- const accountConfigId = options.user
109
- ? await accountConfigIdForUser(options.user)
110
- : await soleAccountConfigId(() => client.accountConfig.listAll());
111
-
112
- const document = await readConfigForExport(
113
- client,
114
- accountConfigId,
115
- exportIdentity(),
116
- );
117
- process.stdout.write(`${JSON.stringify(document, null, "\t")}\n`);
118
- };
119
-
120
- // No `process.exit` on the way out: the document is written to stdout, and
121
- // stdout is a pipe under `compose run`, so exiting drops whatever write is
122
- // still pending. Setting the code and letting the process end delivers it.
123
- await run().catch((error: unknown) => {
124
- process.stderr.write(
125
- `config save: ${error instanceof Error ? error.message : String(error)}\n`,
126
- );
127
- process.exitCode = 1;
128
- });
package/src/auth.test.ts DELETED
@@ -1,12 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, test } from "node:test";
3
- import { deriveAccountConfigId } from "./auth.js";
4
-
5
- describe("derived ids are stored primary keys: a changed value orphans every row already written and needs a migration, never a new expectation here", () => {
6
- test("deriveAccountConfigId pins the account a Cognito subject resolves to", () => {
7
- assert.equal(
8
- deriveAccountConfigId("golden-cognito-sub"),
9
- "97h2fhd0c32ocenk6ju12oxto",
10
- );
11
- });
12
- });