@remit/backend 0.0.142 → 0.0.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -521
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. package/tsconfig.json +0 -7
@@ -1,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
- });