@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,765 +0,0 @@
1
- /**
2
- * Issue #679, phase 1: reserving room for a file, uploading it, and confirming
3
- * it landed.
4
- *
5
- * Driven through the real handlers, the real OutboxAttachmentService, a real
6
- * storage backend and the real error funnel, so what is asserted is the status
7
- * code and body the browser receives. The upload leg goes through the real
8
- * receiver against a URL the storage backend actually minted — nothing here
9
- * hand-builds a signature.
10
- */
11
-
12
- import assert from "node:assert/strict";
13
- import { mkdtemp, rm } from "node:fs/promises";
14
- import { join } from "node:path";
15
- import { Readable } from "node:stream";
16
- import { afterEach, describe, it } from "node:test";
17
- import type {
18
- CreateOutboxMessageInput,
19
- IOutboxAttachmentRepository,
20
- IOutboxMessageRepository,
21
- OutboxAttachmentItem,
22
- OutboxMessageItem,
23
- UpdateOutboxMessageInput,
24
- } from "@remit/data-ports";
25
- import { holdsRoom } from "@remit/data-ports";
26
- import { ForbiddenError, NotFoundError } from "@remit/data-ports/errors";
27
- import {
28
- OutboxAttachmentRejectionReason,
29
- OutboxMessageStatus,
30
- } from "@remit/domain-enums";
31
- import {
32
- OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES,
33
- OutboxAttachmentService,
34
- } from "@remit/mailbox-service";
35
- import {
36
- type StorageService,
37
- UPLOAD_ROUTE_PREFIX,
38
- } from "@remit/storage-service";
39
- import { createFilesystemStorageService } from "@remit/storage-service/filesystem";
40
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
41
- import type { Context } from "openapi-backend";
42
- import { receiveUpload } from "../../dev-server/upload-handler.js";
43
- import { deriveAccountConfigId } from "../auth.js";
44
- import { handleError } from "../error.js";
45
- import { formatResponse } from "../response.js";
46
- import {
47
- _resetForTest,
48
- type RemitClient,
49
- setClient,
50
- } from "../service/data-client.js";
51
- import {
52
- completeOutboxAttachment,
53
- mintOutboxAttachment,
54
- } from "./outbox-attachment.js";
55
-
56
- process.env.CONTENT_DELIVERY_DOMAIN = "https://content.test";
57
-
58
- const SUB = "cognito-sub-679";
59
- const ACCOUNT_CONFIG_ID = deriveAccountConfigId(SUB);
60
- const OTHER_ACCOUNT_CONFIG_ID = deriveAccountConfigId("cognito-sub-stranger");
61
- const ACCOUNT_ID = "acc-679";
62
- const DRAFT_ID = "a1b2c3d4e5f6g7h8i9j0k1l2m";
63
- const ORIGIN = "https://mail.example.test";
64
- const SECRET = "a-signing-secret-of-at-least-32-characters";
65
-
66
- const createOutboxRepository = (
67
- rows: Map<string, OutboxMessageItem>,
68
- ): IOutboxMessageRepository =>
69
- ({
70
- get: async (
71
- accountConfigId: string,
72
- outboxMessageId: string,
73
- mode?: "read" | "act",
74
- ) => {
75
- const row = rows.get(outboxMessageId);
76
- if (!row) throw new NotFoundError(`No outbox message ${outboxMessageId}`);
77
- if (row.accountConfigId !== accountConfigId) {
78
- // Mirrors the real repository: an action verb denies, a read feigns
79
- // absence.
80
- if (mode === "act") {
81
- throw new ForbiddenError(
82
- `Outbox message ${outboxMessageId} not in account config`,
83
- );
84
- }
85
- throw new NotFoundError(`No outbox message ${outboxMessageId}`);
86
- }
87
- return row;
88
- },
89
- create: async (input: CreateOutboxMessageInput) =>
90
- input as OutboxMessageItem,
91
- update: async (
92
- _accountConfigId: string,
93
- outboxMessageId: string,
94
- input: UpdateOutboxMessageInput,
95
- ) => ({ ...rows.get(outboxMessageId), ...input }) as OutboxMessageItem,
96
- }) as unknown as IOutboxMessageRepository;
97
-
98
- interface Installed {
99
- storage: StorageService;
100
- basePath: string;
101
- rows: Map<string, OutboxAttachmentItem>;
102
- }
103
-
104
- /** The row store the cap and the sweep both read. */
105
- const createAttachmentRepository = (
106
- rows: Map<string, OutboxAttachmentItem>,
107
- ): IOutboxAttachmentRepository => {
108
- const forDraft = (accountConfigId: string, outboxMessageId: string) =>
109
- [...rows.values()].filter(
110
- (row) =>
111
- row.accountConfigId === accountConfigId &&
112
- row.outboxMessageId === outboxMessageId,
113
- );
114
-
115
- return {
116
- reserve: async (input, cap) => {
117
- const live = forDraft(
118
- input.accountConfigId,
119
- input.outboxMessageId,
120
- ).filter((row) => holdsRoom(row, cap.nowSeconds));
121
- const usedBytes = live.reduce((total, row) => total + row.sizeBytes, 0);
122
- if (live.length >= cap.maxCount) {
123
- return { outcome: "OverCountCap", usedBytes };
124
- }
125
- if (usedBytes + input.sizeBytes > cap.maxTotalBytes) {
126
- return { outcome: "OverByteCap", usedBytes };
127
- }
128
- const item = {
129
- ...input,
130
- state: "Pending",
131
- createdAt: 0,
132
- updatedAt: 0,
133
- } as OutboxAttachmentItem;
134
- rows.set(item.outboxAttachmentId, item);
135
- return { outcome: "Reserved", item };
136
- },
137
- get: async (accountConfigId, outboxAttachmentId) => {
138
- const row = rows.get(outboxAttachmentId);
139
- if (!row || row.accountConfigId !== accountConfigId) {
140
- throw new NotFoundError("gone");
141
- }
142
- return row;
143
- },
144
- listByOutboxMessage: async (accountConfigId, outboxMessageId) =>
145
- forDraft(accountConfigId, outboxMessageId),
146
- markStored: async (_accountConfigId, outboxAttachmentId, sizeBytes) => {
147
- const row = rows.get(outboxAttachmentId);
148
- if (!row || row.state !== "Pending") return null;
149
- const next = {
150
- ...row,
151
- state: "Stored",
152
- sizeBytes,
153
- reservationExpiresAt: 0,
154
- } as OutboxAttachmentItem;
155
- rows.set(outboxAttachmentId, next);
156
- return next;
157
- },
158
- deleteLapsedReservations: async (
159
- accountConfigId: string,
160
- outboxMessageId: string,
161
- nowSeconds: number,
162
- ) => {
163
- const gone: string[] = [];
164
- for (const row of [...rows.values()]) {
165
- if (
166
- row.accountConfigId === accountConfigId &&
167
- row.outboxMessageId === outboxMessageId &&
168
- row.state === "Pending" &&
169
- row.reservationExpiresAt < nowSeconds
170
- ) {
171
- rows.delete(row.outboxAttachmentId);
172
- gone.push(row.outboxAttachmentId);
173
- }
174
- }
175
- return gone;
176
- },
177
- deleteMany: async (_accountConfigId, ids) => {
178
- for (const id of ids) rows.delete(id);
179
- },
180
- deleteByOutboxMessage: async (accountConfigId, outboxMessageId) => {
181
- for (const row of forDraft(accountConfigId, outboxMessageId)) {
182
- rows.delete(row.outboxAttachmentId);
183
- }
184
- },
185
- };
186
- };
187
-
188
- const temporaryRoots: string[] = [];
189
-
190
- const install = async (
191
- overrides: Partial<OutboxMessageItem> = {},
192
- accountConfigId = ACCOUNT_CONFIG_ID,
193
- ): Promise<Installed> => {
194
- const basePath = await mkdtemp(join(process.cwd(), ".tmp-attachments-"));
195
- temporaryRoots.push(basePath);
196
-
197
- const rows = new Map<string, OutboxMessageItem>([
198
- [
199
- DRAFT_ID,
200
- {
201
- outboxMessageId: DRAFT_ID,
202
- accountId: ACCOUNT_ID,
203
- accountConfigId,
204
- status: OutboxMessageStatus.draft,
205
- ...overrides,
206
- } as OutboxMessageItem,
207
- ],
208
- ]);
209
- const outboxMessage = createOutboxRepository(rows);
210
- const storage = createFilesystemStorageService(basePath, {
211
- origin: ORIGIN,
212
- signingSecret: SECRET,
213
- });
214
-
215
- const attachmentRows = new Map<string, OutboxAttachmentItem>();
216
- setClient({
217
- outboxMessage,
218
- storage,
219
- outboxAttachment: new OutboxAttachmentService({
220
- outboxMessageService: outboxMessage,
221
- outboxAttachmentService: createAttachmentRepository(attachmentRows),
222
- storage,
223
- }),
224
- } as unknown as RemitClient);
225
-
226
- return { storage, basePath, rows: attachmentRows };
227
- };
228
-
229
- const authorizedEvent = (): APIGatewayProxyEvent =>
230
- ({
231
- requestContext: { authorizer: { claims: { sub: SUB } } },
232
- }) as unknown as APIGatewayProxyEvent;
233
-
234
- const mintContext = (
235
- body: Record<string, unknown>,
236
- outboxMessageId = DRAFT_ID,
237
- ): Context =>
238
- ({
239
- request: { params: { outboxMessageId }, requestBody: body },
240
- }) as unknown as Context;
241
-
242
- const completeContext = (
243
- outboxAttachmentId: string,
244
- outboxMessageId = DRAFT_ID,
245
- ): Context =>
246
- ({
247
- request: { params: { outboxMessageId, outboxAttachmentId } },
248
- }) as unknown as Context;
249
-
250
- /** The response the browser would receive, error funnel included. */
251
- const respond = async (
252
- run: () => Promise<unknown>,
253
- ): Promise<APIGatewayProxyResult> => {
254
- const outcome = await run().then(
255
- (body) => ({ ok: true as const, body }),
256
- (error: unknown) => ({ ok: false as const, error }),
257
- );
258
- if (!outcome.ok) return handleError(outcome.error);
259
- return formatResponse(outcome.body as Record<string, unknown>);
260
- };
261
-
262
- const parse = (response: APIGatewayProxyResult): Record<string, unknown> =>
263
- JSON.parse(response.body) as Record<string, unknown>;
264
-
265
- const mint = (
266
- body: Record<string, unknown>,
267
- outboxMessageId = DRAFT_ID,
268
- ): Promise<APIGatewayProxyResult> =>
269
- respond(() =>
270
- mintOutboxAttachment(mintContext(body, outboxMessageId), authorizedEvent()),
271
- );
272
-
273
- const complete = (
274
- outboxAttachmentId: string,
275
- outboxMessageId = DRAFT_ID,
276
- ): Promise<APIGatewayProxyResult> =>
277
- respond(() =>
278
- completeOutboxAttachment(
279
- completeContext(outboxAttachmentId, outboxMessageId),
280
- authorizedEvent(),
281
- ),
282
- );
283
-
284
- /** PUT bytes to a minted URL through the real receiver. */
285
- const putTo = (
286
- storage: StorageService,
287
- uploadUrl: string,
288
- content: Buffer,
289
- nowSeconds = Math.floor(Date.now() / 1000),
290
- ) => {
291
- const url = new URL(uploadUrl);
292
- return receiveUpload(storage, {
293
- storageKey: url.pathname.slice(UPLOAD_ROUTE_PREFIX.length),
294
- exp: url.searchParams.get("exp") ?? undefined,
295
- max: url.searchParams.get("max") ?? undefined,
296
- sig: url.searchParams.get("sig") ?? undefined,
297
- body: Readable.from(content),
298
- nowSeconds,
299
- secret: SECRET,
300
- findLiveReservation: async () => true,
301
- });
302
- };
303
-
304
- const mintedBody = (
305
- response: APIGatewayProxyResult,
306
- ): { outboxAttachmentId: string; uploadUrl: string } => {
307
- assert.equal(response.statusCode, 200);
308
- const body = parse(response);
309
- return {
310
- outboxAttachmentId: String(body.outboxAttachmentId),
311
- uploadUrl: String(body.uploadUrl),
312
- };
313
- };
314
-
315
- afterEach(async () => {
316
- _resetForTest();
317
- for (const root of temporaryRoots.splice(0)) {
318
- await rm(root, { recursive: true, force: true });
319
- }
320
- });
321
-
322
- describe("reserving room on a draft (#679)", () => {
323
- it("answers with an upload URL scoped to one attachment on this draft", async () => {
324
- await install();
325
-
326
- const response = await mint({
327
- filename: "invoice.pdf",
328
- contentType: "application/pdf",
329
- sizeBytes: 2048,
330
- });
331
-
332
- assert.equal(response.statusCode, 200);
333
- const body = parse(response);
334
- assert.equal(body.outboxMessageId, DRAFT_ID);
335
- assert.equal(body.filename, "invoice.pdf");
336
- assert.equal(body.contentType, "application/pdf");
337
- assert.equal(body.sizeBytes, 2048);
338
- assert.ok(Number(body.uploadExpiresAt) > Math.floor(Date.now() / 1000));
339
-
340
- const url = new URL(String(body.uploadUrl));
341
- assert.equal(url.origin, ORIGIN);
342
- assert.ok(url.pathname.includes(String(body.outboxAttachmentId)));
343
- assert.ok(url.pathname.includes(DRAFT_ID));
344
- assert.equal(url.searchParams.get("max"), "2048");
345
- assert.ok((url.searchParams.get("sig") ?? "").length > 0);
346
- });
347
-
348
- it("sanitizes the filename it will put on the message", async () => {
349
- await install();
350
-
351
- const response = await mint({
352
- filename: "../../../etc/pa\u202Edwssap",
353
- contentType: "definitely not a media type",
354
- sizeBytes: 10,
355
- });
356
-
357
- assert.equal(response.statusCode, 200);
358
- const body = parse(response);
359
- assert.equal(body.filename, "padwssap");
360
- assert.equal(body.contentType, "application/octet-stream");
361
- });
362
-
363
- it("refuses a filename that is nothing but separators", async () => {
364
- await install();
365
-
366
- const response = await mint({
367
- filename: "../..",
368
- contentType: "text/plain",
369
- sizeBytes: 10,
370
- });
371
-
372
- assert.equal(response.statusCode, 400);
373
- assert.equal(
374
- parse(response).reason,
375
- OutboxAttachmentRejectionReason.UnusableFilename,
376
- );
377
- });
378
-
379
- it("refuses a declared size over the cap with 413, before any bytes move", async () => {
380
- await install();
381
-
382
- const response = await mint({
383
- filename: "huge.bin",
384
- contentType: "application/octet-stream",
385
- sizeBytes: OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES + 1,
386
- });
387
-
388
- assert.equal(response.statusCode, 413);
389
- const body = parse(response);
390
- assert.equal(body.reason, OutboxAttachmentRejectionReason.FileTooLarge);
391
- assert.equal(body.limitBytes, OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES);
392
- assert.match(String(body.message), /huge\.bin/);
393
- });
394
-
395
- it("counts a reservation nobody has uploaded against yet", async () => {
396
- await install();
397
-
398
- const first = await mint({
399
- filename: "half.bin",
400
- contentType: "application/octet-stream",
401
- sizeBytes: OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES - 1024,
402
- });
403
- assert.equal(first.statusCode, 200);
404
-
405
- const second = await mint({
406
- filename: "one-too-many.bin",
407
- contentType: "application/octet-stream",
408
- sizeBytes: 2048,
409
- });
410
-
411
- assert.equal(second.statusCode, 413);
412
- const body = parse(second);
413
- assert.equal(body.reason, OutboxAttachmentRejectionReason.MessageTooLarge);
414
- assert.equal(body.usedBytes, OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES - 1024);
415
- });
416
-
417
- it("refuses an empty file", async () => {
418
- await install();
419
-
420
- const response = await mint({
421
- filename: "empty.txt",
422
- contentType: "text/plain",
423
- sizeBytes: 0,
424
- });
425
-
426
- assert.equal(response.statusCode, 400);
427
- assert.equal(
428
- parse(response).reason,
429
- OutboxAttachmentRejectionReason.EmptyFile,
430
- );
431
- });
432
-
433
- it("denies a mint against someone else's draft, and reserves nothing", async () => {
434
- const { storage } = await install({}, OTHER_ACCOUNT_CONFIG_ID);
435
-
436
- const response = await mint({
437
- filename: "trespass.txt",
438
- contentType: "text/plain",
439
- sizeBytes: 10,
440
- });
441
-
442
- assert.equal(response.statusCode, 403);
443
- assert.deepEqual(
444
- await storage.listOutboxAttachments(
445
- OTHER_ACCOUNT_CONFIG_ID,
446
- ACCOUNT_ID,
447
- DRAFT_ID,
448
- ),
449
- [],
450
- );
451
- });
452
-
453
- it("answers 404 for a draft that does not exist", async () => {
454
- await install();
455
-
456
- const response = await mint(
457
- { filename: "orphan.txt", contentType: "text/plain", sizeBytes: 10 },
458
- "z9y8x7w6v5u4t3s2r1q0p9o8n",
459
- );
460
-
461
- assert.equal(response.statusCode, 404);
462
- });
463
-
464
- it("refuses to reserve on a message that has already left draft", async () => {
465
- await install({ status: OutboxMessageStatus.sent });
466
-
467
- const response = await mint({
468
- filename: "late.txt",
469
- contentType: "text/plain",
470
- sizeBytes: 10,
471
- });
472
-
473
- assert.equal(response.statusCode, 409);
474
- assert.match(
475
- String(parse(response).message),
476
- /no longer take an attachment/,
477
- );
478
- });
479
- });
480
-
481
- describe("uploading to a minted URL (#679)", () => {
482
- it("stores the bytes, and the completion answers with the size storage holds", async () => {
483
- const { storage } = await install();
484
- const content = Buffer.from("%PDF-1.7 a small invoice");
485
- const { outboxAttachmentId, uploadUrl } = mintedBody(
486
- await mint({
487
- filename: "invoice.pdf",
488
- contentType: "application/pdf",
489
- sizeBytes: content.length,
490
- }),
491
- );
492
-
493
- const upload = await putTo(storage, uploadUrl, content);
494
- assert.equal(upload.status, 204);
495
-
496
- const response = await complete(outboxAttachmentId);
497
- assert.equal(response.statusCode, 200);
498
- const body = parse(response);
499
- assert.equal(body.outboxAttachmentId, outboxAttachmentId);
500
- assert.equal(body.sizeBytes, content.length);
501
-
502
- const stat = await storage.statOutboxAttachment(
503
- ACCOUNT_CONFIG_ID,
504
- ACCOUNT_ID,
505
- DRAFT_ID,
506
- outboxAttachmentId,
507
- );
508
- assert.equal(stat?.sizeBytes, content.length);
509
- });
510
-
511
- it("refuses a forged signature and writes nothing", async () => {
512
- const { storage } = await install();
513
- const { outboxAttachmentId, uploadUrl } = mintedBody(
514
- await mint({
515
- filename: "forged.bin",
516
- contentType: "application/octet-stream",
517
- sizeBytes: 4,
518
- }),
519
- );
520
-
521
- const tampered = new URL(uploadUrl);
522
- tampered.searchParams.set("sig", "not-the-signature");
523
-
524
- const upload = await putTo(storage, tampered.toString(), Buffer.alloc(4));
525
- assert.equal(upload.status, 403);
526
- assert.equal(
527
- await storage.statOutboxAttachment(
528
- ACCOUNT_CONFIG_ID,
529
- ACCOUNT_ID,
530
- DRAFT_ID,
531
- outboxAttachmentId,
532
- ),
533
- null,
534
- );
535
- });
536
-
537
- it("refuses a URL whose size was raised after it was minted", async () => {
538
- const { storage } = await install();
539
- const { uploadUrl } = mintedBody(
540
- await mint({
541
- filename: "grown.bin",
542
- contentType: "application/octet-stream",
543
- sizeBytes: 4,
544
- }),
545
- );
546
-
547
- // The byte count is part of the signed message, so raising it invalidates
548
- // the URL rather than raising the allowance.
549
- const tampered = new URL(uploadUrl);
550
- tampered.searchParams.set("max", "40000");
551
-
552
- const upload = await putTo(
553
- storage,
554
- tampered.toString(),
555
- Buffer.alloc(4000),
556
- );
557
- assert.equal(upload.status, 403);
558
- });
559
-
560
- it("refuses a URL pointed at a different attachment", async () => {
561
- const { storage } = await install();
562
- const first = mintedBody(
563
- await mint({
564
- filename: "mine.bin",
565
- contentType: "application/octet-stream",
566
- sizeBytes: 4,
567
- }),
568
- );
569
- const second = mintedBody(
570
- await mint({
571
- filename: "theirs.bin",
572
- contentType: "application/octet-stream",
573
- sizeBytes: 4,
574
- }),
575
- );
576
-
577
- const swapped = new URL(first.uploadUrl);
578
- swapped.pathname = new URL(second.uploadUrl).pathname;
579
-
580
- const upload = await putTo(storage, swapped.toString(), Buffer.alloc(4));
581
- assert.equal(upload.status, 403);
582
- assert.equal(
583
- await storage.statOutboxAttachment(
584
- ACCOUNT_CONFIG_ID,
585
- ACCOUNT_ID,
586
- DRAFT_ID,
587
- second.outboxAttachmentId,
588
- ),
589
- null,
590
- );
591
- });
592
-
593
- it("refuses an expired URL", async () => {
594
- const { storage } = await install();
595
- const { outboxAttachmentId, uploadUrl } = mintedBody(
596
- await mint({
597
- filename: "stale.bin",
598
- contentType: "application/octet-stream",
599
- sizeBytes: 4,
600
- }),
601
- );
602
-
603
- const upload = await putTo(
604
- storage,
605
- uploadUrl,
606
- Buffer.alloc(4),
607
- Math.floor(Date.now() / 1000) + 100_000,
608
- );
609
- assert.equal(upload.status, 403);
610
- assert.equal(upload.reason, "expired");
611
- assert.equal(
612
- await storage.statOutboxAttachment(
613
- ACCOUNT_CONFIG_ID,
614
- ACCOUNT_ID,
615
- DRAFT_ID,
616
- outboxAttachmentId,
617
- ),
618
- null,
619
- );
620
- });
621
-
622
- it("cuts off a body larger than was reserved, and stores none of it", async () => {
623
- const { storage } = await install();
624
- const { outboxAttachmentId, uploadUrl } = mintedBody(
625
- await mint({
626
- filename: "sneaky.bin",
627
- contentType: "application/octet-stream",
628
- sizeBytes: 1024,
629
- }),
630
- );
631
-
632
- const upload = await putTo(storage, uploadUrl, Buffer.alloc(64 * 1024));
633
- assert.equal(upload.status, 413);
634
- assert.equal(
635
- await storage.statOutboxAttachment(
636
- ACCOUNT_CONFIG_ID,
637
- ACCOUNT_ID,
638
- DRAFT_ID,
639
- outboxAttachmentId,
640
- ),
641
- null,
642
- );
643
- });
644
-
645
- it("refuses a body shorter than was reserved", async () => {
646
- const { storage } = await install();
647
- const { uploadUrl } = mintedBody(
648
- await mint({
649
- filename: "short.bin",
650
- contentType: "application/octet-stream",
651
- sizeBytes: 1024,
652
- }),
653
- );
654
-
655
- const upload = await putTo(storage, uploadUrl, Buffer.alloc(16));
656
- assert.equal(upload.status, 400);
657
- assert.equal(upload.reason, "size-mismatch");
658
- });
659
- });
660
-
661
- describe("completing an attachment (#679)", () => {
662
- it("refuses to complete when nothing was ever uploaded", async () => {
663
- await install();
664
- const { outboxAttachmentId } = mintedBody(
665
- await mint({
666
- filename: "never-sent.bin",
667
- contentType: "application/octet-stream",
668
- sizeBytes: 8,
669
- }),
670
- );
671
-
672
- const response = await complete(outboxAttachmentId);
673
-
674
- assert.equal(response.statusCode, 400);
675
- assert.equal(
676
- parse(response).reason,
677
- OutboxAttachmentRejectionReason.UploadMissing,
678
- );
679
- });
680
-
681
- it("refuses to complete an attachment that was never reserved", async () => {
682
- await install();
683
-
684
- const response = await complete("never-minted");
685
-
686
- assert.equal(response.statusCode, 400);
687
- assert.equal(
688
- parse(response).reason,
689
- OutboxAttachmentRejectionReason.ReservationExpired,
690
- );
691
- });
692
-
693
- it("refuses, and removes, an object that is not the size reserved for it", async () => {
694
- const { storage } = await install();
695
- const { outboxAttachmentId } = mintedBody(
696
- await mint({
697
- filename: "mismatched.bin",
698
- contentType: "application/octet-stream",
699
- sizeBytes: 4096,
700
- }),
701
- );
702
-
703
- // Write past the upload route, the way a hosted deployment's block storage
704
- // could end up holding something other than what was announced.
705
- await storage.storeOutboxAttachment({
706
- accountConfigId: ACCOUNT_CONFIG_ID,
707
- accountId: ACCOUNT_ID,
708
- outboxMessageId: DRAFT_ID,
709
- outboxAttachmentId,
710
- content: Buffer.alloc(9),
711
- });
712
-
713
- const response = await complete(outboxAttachmentId);
714
-
715
- assert.equal(response.statusCode, 400);
716
- assert.equal(
717
- parse(response).reason,
718
- OutboxAttachmentRejectionReason.SizeMismatch,
719
- );
720
- assert.equal(
721
- await storage.statOutboxAttachment(
722
- ACCOUNT_CONFIG_ID,
723
- ACCOUNT_ID,
724
- DRAFT_ID,
725
- outboxAttachmentId,
726
- ),
727
- null,
728
- );
729
- });
730
- });
731
-
732
- describe("the mint route through the whole request pipeline", () => {
733
- it("reaches its handler with the real built spec in front of it", async () => {
734
- await install();
735
- const { api } = await import("../index.js");
736
-
737
- const body = JSON.stringify({
738
- filename: "routed.txt",
739
- contentType: "text/plain",
740
- sizeBytes: 18,
741
- });
742
- const event = {
743
- httpMethod: "POST",
744
- path: `/outbox/${DRAFT_ID}/attachments`,
745
- body,
746
- headers: { "content-type": "application/json" },
747
- requestContext: { authorizer: { claims: { sub: SUB } } },
748
- } as unknown as APIGatewayProxyEvent;
749
-
750
- const result = (await api.handleRequest(
751
- {
752
- method: "POST",
753
- path: event.path,
754
- query: {},
755
- body,
756
- headers: { "content-type": "application/json" },
757
- },
758
- event,
759
- {} as never,
760
- )) as APIGatewayProxyResult;
761
-
762
- assert.equal(result.statusCode, 200);
763
- assert.equal(parse(result).filename, "routed.txt");
764
- });
765
- });