@remit/backend 0.0.133 → 0.0.135
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.
- package/package.json +1 -1
- package/src/derive/contentUrl.test.ts +17 -1
- package/src/derive/contentUrl.ts +23 -7
- package/src/handlers/calendar-suggestion.test.ts +42 -1
- package/src/handlers/calendar-suggestion.ts +24 -3
- package/src/handlers/outbox-attachment.test.ts +2 -0
- package/src/handlers/outbox-attachment.ts +25 -1
- package/src/handlers/outbox.test.ts +28 -0
- package/src/handlers/outbox.ts +2 -12
package/package.json
CHANGED
|
@@ -4,7 +4,11 @@ import {
|
|
|
4
4
|
createContentSigner,
|
|
5
5
|
verifyContentSignature,
|
|
6
6
|
} from "./contentSignature.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildContentUrl,
|
|
9
|
+
buildStorageContentUrl,
|
|
10
|
+
getContentDeliveryDomain,
|
|
11
|
+
} from "./contentUrl.js";
|
|
8
12
|
|
|
9
13
|
describe("buildContentUrl", () => {
|
|
10
14
|
it("produces the /content/accounts/{cfg}/{acc}/messages/{msg}/parts/{part} layout the Lambda@Edge expects", () => {
|
|
@@ -64,6 +68,18 @@ describe("buildContentUrl", () => {
|
|
|
64
68
|
);
|
|
65
69
|
});
|
|
66
70
|
|
|
71
|
+
it("addresses an outbox attachment by its storage key", () => {
|
|
72
|
+
const url = buildStorageContentUrl({
|
|
73
|
+
domain: "https://cdn.test",
|
|
74
|
+
storageKey: "accounts/cfg/acc/outbox/ob-1/attachments/att-1",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(
|
|
78
|
+
url,
|
|
79
|
+
"https://cdn.test/content/accounts/cfg/acc/outbox/ob-1/attachments/att-1",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
67
83
|
it("leaves the URL unsigned when no signer is provided (AWS/Lambda@Edge path)", () => {
|
|
68
84
|
const url = buildContentUrl({
|
|
69
85
|
domain: "https://abc123.cloudfront.net",
|
package/src/derive/contentUrl.ts
CHANGED
|
@@ -26,9 +26,16 @@ export interface BuildContentUrlInput {
|
|
|
26
26
|
sign?: ContentSigner;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
export interface BuildStorageContentUrlInput {
|
|
30
|
+
domain: string;
|
|
31
|
+
storageKey: string;
|
|
32
|
+
sign?: ContentSigner;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const buildStorageContentUrl = (
|
|
36
|
+
input: BuildStorageContentUrlInput,
|
|
37
|
+
): string => {
|
|
38
|
+
const { domain, storageKey, sign } = input;
|
|
32
39
|
const normalizedDomain = domain.replace(/\/+$/, "");
|
|
33
40
|
const base = normalizedDomain.startsWith("http")
|
|
34
41
|
? normalizedDomain
|
|
@@ -36,17 +43,26 @@ export const buildContentUrl = (input: BuildContentUrlInput): string => {
|
|
|
36
43
|
// The signed message is the decoded, account-scoped storage path — the same
|
|
37
44
|
// value the `/content` route recomputes from `req.path`. Keep it in lockstep
|
|
38
45
|
// with the verifier's `req.path.replace(/^\/content\//, "")`.
|
|
39
|
-
const
|
|
40
|
-
const safePart = partPath
|
|
46
|
+
const safeKey = storageKey
|
|
41
47
|
.split("/")
|
|
42
48
|
.map((segment) => encodeURIComponent(segment))
|
|
43
49
|
.join("/");
|
|
44
|
-
const url = `${base}/content
|
|
50
|
+
const url = `${base}/content/${safeKey}`;
|
|
45
51
|
if (!sign) return url;
|
|
46
|
-
const { exp, sig } = sign(
|
|
52
|
+
const { exp, sig } = sign(storageKey);
|
|
47
53
|
return `${url}?exp=${exp}&sig=${encodeURIComponent(sig)}`;
|
|
48
54
|
};
|
|
49
55
|
|
|
56
|
+
export const buildContentUrl = (input: BuildContentUrlInput): string => {
|
|
57
|
+
const { domain, accountConfigId, accountId, messageId, partPath, sign } =
|
|
58
|
+
input;
|
|
59
|
+
return buildStorageContentUrl({
|
|
60
|
+
domain,
|
|
61
|
+
storageKey: `accounts/${accountConfigId}/${accountId}/messages/${messageId}/parts/${partPath}`,
|
|
62
|
+
sign,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
50
66
|
/**
|
|
51
67
|
* Read the CloudFront distribution domain from the environment. The CDK
|
|
52
68
|
* wires `CONTENT_DELIVERY_DOMAIN` onto the API Lambda from the
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SettleCalendarSuggestionInput,
|
|
11
11
|
} from "@remit/data-ports";
|
|
12
12
|
import {
|
|
13
|
+
CalendarInviteAnswer,
|
|
13
14
|
CalendarInviteMethod,
|
|
14
15
|
CalendarSuggestionSource,
|
|
15
16
|
CalendarSuggestionState,
|
|
@@ -354,6 +355,7 @@ const INVITATION = [
|
|
|
354
355
|
const putSuggestion = (
|
|
355
356
|
accountConfigId: string,
|
|
356
357
|
messageId: string,
|
|
358
|
+
method: CalendarSuggestionItem["method"] = CalendarInviteMethod.Request,
|
|
357
359
|
): Promise<CalendarSuggestionItem> =>
|
|
358
360
|
client.calendarSuggestion.put({
|
|
359
361
|
accountConfigId,
|
|
@@ -361,7 +363,7 @@ const putSuggestion = (
|
|
|
361
363
|
bodyPartId: "part-1",
|
|
362
364
|
icalUid: "invite@example.test",
|
|
363
365
|
sequence: 0,
|
|
364
|
-
method
|
|
366
|
+
method,
|
|
365
367
|
source: CalendarSuggestionSource.IcalendarPart,
|
|
366
368
|
summary: "Quarterly review",
|
|
367
369
|
dtStart: "2026-09-01T10:00:00+02:00",
|
|
@@ -506,6 +508,45 @@ describe("POST /calendar-suggestions/{suggestionId}/accept", () => {
|
|
|
506
508
|
assert.equal(untouched.state, CalendarSuggestionState.Pending);
|
|
507
509
|
assert.equal(untouched.acceptedCalendarObjectId, "");
|
|
508
510
|
});
|
|
511
|
+
|
|
512
|
+
test("refuses a maybe to a cancellation, before writing anything", async () => {
|
|
513
|
+
const { accountConfigId, event } = anAccount();
|
|
514
|
+
const calendar = await client.calendarCollection.create({
|
|
515
|
+
accountConfigId,
|
|
516
|
+
urlSegment: "default",
|
|
517
|
+
displayName: "Calendar",
|
|
518
|
+
});
|
|
519
|
+
const card = await putSuggestion(
|
|
520
|
+
accountConfigId,
|
|
521
|
+
"msg-cancel-maybe",
|
|
522
|
+
CalendarInviteMethod.Cancel,
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
await assert.rejects(
|
|
526
|
+
() =>
|
|
527
|
+
acceptSuggestion(
|
|
528
|
+
contextOf({
|
|
529
|
+
params: { suggestionId: card.suggestionId },
|
|
530
|
+
requestBody: {
|
|
531
|
+
calendarId: calendar.calendarId,
|
|
532
|
+
answer: CalendarInviteAnswer.Tentative,
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
event,
|
|
536
|
+
),
|
|
537
|
+
(error: unknown) => (error as { statusCode?: number }).statusCode === 400,
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
assert.deepEqual(
|
|
541
|
+
await client.calendarObject.listByCalendar(calendar.calendarId),
|
|
542
|
+
[],
|
|
543
|
+
);
|
|
544
|
+
const untouched = await client.calendarSuggestion.get(
|
|
545
|
+
accountConfigId,
|
|
546
|
+
card.suggestionId,
|
|
547
|
+
);
|
|
548
|
+
assert.equal(untouched.state, CalendarSuggestionState.Pending);
|
|
549
|
+
});
|
|
509
550
|
});
|
|
510
551
|
|
|
511
552
|
describe("POST /calendar-suggestions/{suggestionId}/dismiss", () => {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CalendarInviteAnswer as CalendarInviteAnswerValue,
|
|
3
|
+
CalendarSuggestionResponse,
|
|
4
|
+
} from "@remit/api-openapi-types";
|
|
2
5
|
import { acceptCalendarSuggestion } from "@remit/calendar-service";
|
|
3
6
|
import {
|
|
4
7
|
type CalendarSuggestionItem,
|
|
@@ -9,6 +12,8 @@ import {
|
|
|
9
12
|
} from "@remit/data-ports";
|
|
10
13
|
import { BadRequestError } from "@remit/data-ports/errors";
|
|
11
14
|
import {
|
|
15
|
+
CalendarInviteAnswer,
|
|
16
|
+
CalendarInviteMethod,
|
|
12
17
|
CalendarSource,
|
|
13
18
|
CalendarSuggestionState,
|
|
14
19
|
FilterClauseField,
|
|
@@ -211,8 +216,10 @@ export const CalendarSuggestionActionOperations: Record<
|
|
|
211
216
|
const { suggestionId } = context.request.params as {
|
|
212
217
|
suggestionId: string;
|
|
213
218
|
};
|
|
214
|
-
const { calendarId
|
|
219
|
+
const { calendarId, answer = CalendarInviteAnswer.Accepted } = context
|
|
220
|
+
.request.requestBody as {
|
|
215
221
|
calendarId: string;
|
|
222
|
+
answer?: CalendarInviteAnswerValue;
|
|
216
223
|
};
|
|
217
224
|
|
|
218
225
|
const client = await getClient();
|
|
@@ -220,7 +227,20 @@ export const CalendarSuggestionActionOperations: Record<
|
|
|
220
227
|
accountConfigId,
|
|
221
228
|
suggestionId,
|
|
222
229
|
);
|
|
223
|
-
|
|
230
|
+
if (
|
|
231
|
+
answer === CalendarInviteAnswer.Tentative &&
|
|
232
|
+
suggestion.method === CalendarInviteMethod.Cancel
|
|
233
|
+
) {
|
|
234
|
+
throw new BadRequestError(
|
|
235
|
+
"This is a cancellation, so there is nothing to answer maybe to.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
assertSettleable(
|
|
239
|
+
suggestion,
|
|
240
|
+
answer === CalendarInviteAnswer.Tentative
|
|
241
|
+
? CalendarSuggestionState.Tentative
|
|
242
|
+
: CalendarSuggestionState.Accepted,
|
|
243
|
+
);
|
|
224
244
|
|
|
225
245
|
// Reads the collection through the caller's own account config, so a
|
|
226
246
|
// calendarId naming somebody else's collection is a 404 before anything
|
|
@@ -240,6 +260,7 @@ export const CalendarSuggestionActionOperations: Record<
|
|
|
240
260
|
calendarId,
|
|
241
261
|
suggestion,
|
|
242
262
|
attendee: await accountEmailForMessage(client, suggestion.messageId),
|
|
263
|
+
answer,
|
|
243
264
|
});
|
|
244
265
|
if (!accepted.ok) {
|
|
245
266
|
throw new BadRequestError(accepted.error.message);
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
mintOutboxAttachment,
|
|
54
54
|
} from "./outbox-attachment.js";
|
|
55
55
|
|
|
56
|
+
process.env.CONTENT_DELIVERY_DOMAIN = "https://content.test";
|
|
57
|
+
|
|
56
58
|
const SUB = "cognito-sub-679";
|
|
57
59
|
const ACCOUNT_CONFIG_ID = deriveAccountConfigId(SUB);
|
|
58
60
|
const OTHER_ACCOUNT_CONFIG_ID = deriveAccountConfigId("cognito-sub-stranger");
|
|
@@ -4,14 +4,21 @@ import type {
|
|
|
4
4
|
OutboxAttachmentRejection,
|
|
5
5
|
OutboxAttachmentResponse,
|
|
6
6
|
} from "@remit/api-openapi-types";
|
|
7
|
+
import type { OutboxAttachmentItem } from "@remit/data-ports";
|
|
7
8
|
import { OutboxAttachmentRejectionReason } from "@remit/domain-enums";
|
|
8
9
|
import type {
|
|
9
10
|
OutboxAttachmentRejectionDetail,
|
|
10
11
|
OutboxAttachmentRejectionReasonValue,
|
|
11
12
|
} from "@remit/mailbox-service";
|
|
13
|
+
import { outboxAttachmentContentId } from "@remit/mailbox-service/outbox-attachment-content";
|
|
12
14
|
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
13
15
|
import type { Context } from "openapi-backend";
|
|
14
16
|
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
17
|
+
import { getContentSigner } from "../derive/contentSignature.js";
|
|
18
|
+
import {
|
|
19
|
+
buildStorageContentUrl,
|
|
20
|
+
getContentDeliveryDomain,
|
|
21
|
+
} from "../derive/contentUrl.js";
|
|
15
22
|
import { getClient } from "../service/data-client.js";
|
|
16
23
|
|
|
17
24
|
// Every error body the API emits carries a `code` (issue #371). A refusal here
|
|
@@ -24,6 +31,23 @@ const TOO_LARGE: ReadonlySet<OutboxAttachmentRejectionReasonValue> = new Set([
|
|
|
24
31
|
OutboxAttachmentRejectionReason.TooManyAttachments,
|
|
25
32
|
]);
|
|
26
33
|
|
|
34
|
+
export const toOutboxAttachmentResponse = (
|
|
35
|
+
item: OutboxAttachmentItem,
|
|
36
|
+
): OutboxAttachmentResponse => ({
|
|
37
|
+
outboxAttachmentId: item.outboxAttachmentId,
|
|
38
|
+
outboxMessageId: item.outboxMessageId,
|
|
39
|
+
filename: item.filename,
|
|
40
|
+
contentType: item.contentType,
|
|
41
|
+
sizeBytes: item.sizeBytes,
|
|
42
|
+
state: item.state,
|
|
43
|
+
contentId: outboxAttachmentContentId(item.outboxAttachmentId),
|
|
44
|
+
contentUrl: buildStorageContentUrl({
|
|
45
|
+
domain: getContentDeliveryDomain(),
|
|
46
|
+
storageKey: item.storageKey,
|
|
47
|
+
sign: getContentSigner(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
27
51
|
interface RejectionResponse {
|
|
28
52
|
statusCode: number;
|
|
29
53
|
body: OutboxAttachmentRejection;
|
|
@@ -98,5 +122,5 @@ export const completeOutboxAttachment = async (
|
|
|
98
122
|
|
|
99
123
|
if (result.outcome === "Rejected") return refuse(result.rejection);
|
|
100
124
|
|
|
101
|
-
return result.attachment;
|
|
125
|
+
return toOutboxAttachmentResponse(result.attachment);
|
|
102
126
|
};
|
|
@@ -50,6 +50,9 @@ import {
|
|
|
50
50
|
} from "../service/data-client.js";
|
|
51
51
|
import { OutboxDetailOperations, OutboxOperations } from "./outbox.js";
|
|
52
52
|
|
|
53
|
+
const CONTENT_DOMAIN = "https://content.test";
|
|
54
|
+
process.env.CONTENT_DELIVERY_DOMAIN = CONTENT_DOMAIN;
|
|
55
|
+
|
|
53
56
|
const SUB = "cognito-sub-604";
|
|
54
57
|
const ACCOUNT_CONFIG_ID = deriveAccountConfigId(SUB);
|
|
55
58
|
const ACCOUNT_ID = "acc-604";
|
|
@@ -616,6 +619,31 @@ describe("attachmentIds on a draft update (#679)", () => {
|
|
|
616
619
|
assert.equal(attachmentRows.size, 2);
|
|
617
620
|
});
|
|
618
621
|
|
|
622
|
+
it("names each file by its content-id and where its bytes are read from", async () => {
|
|
623
|
+
installClient();
|
|
624
|
+
const { outboxMessageId, ids } = await seed();
|
|
625
|
+
|
|
626
|
+
const response = await patch(outboxMessageId, { subject: "still typing" });
|
|
627
|
+
|
|
628
|
+
const body = JSON.parse(response.body) as {
|
|
629
|
+
attachments: {
|
|
630
|
+
outboxAttachmentId: string;
|
|
631
|
+
contentId: string;
|
|
632
|
+
contentUrl: string;
|
|
633
|
+
}[];
|
|
634
|
+
};
|
|
635
|
+
const [first] = body.attachments.filter(
|
|
636
|
+
(item) => item.outboxAttachmentId === ids[0],
|
|
637
|
+
);
|
|
638
|
+
assert.equal(first.contentId, `${ids[0]}@remit`);
|
|
639
|
+
assert.ok(
|
|
640
|
+
first.contentUrl.startsWith(
|
|
641
|
+
`${CONTENT_DOMAIN}/content/${attachmentRows.get(ids[0])?.storageKey}`,
|
|
642
|
+
),
|
|
643
|
+
first.contentUrl,
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
619
647
|
it("present and empty removes every file", async () => {
|
|
620
648
|
installClient();
|
|
621
649
|
const { outboxMessageId } = await seed();
|
package/src/handlers/outbox.ts
CHANGED
|
@@ -21,24 +21,14 @@ import type {
|
|
|
21
21
|
import {
|
|
22
22
|
completeOutboxAttachment,
|
|
23
23
|
mintOutboxAttachment,
|
|
24
|
+
toOutboxAttachmentResponse,
|
|
24
25
|
} from "./outbox-attachment.js";
|
|
25
26
|
|
|
26
|
-
const toAttachmentResponse = (
|
|
27
|
-
item: OutboxAttachmentItem,
|
|
28
|
-
): OutboxMessageResponse["attachments"][number] => ({
|
|
29
|
-
outboxAttachmentId: item.outboxAttachmentId,
|
|
30
|
-
outboxMessageId: item.outboxMessageId,
|
|
31
|
-
filename: item.filename,
|
|
32
|
-
contentType: item.contentType,
|
|
33
|
-
sizeBytes: item.sizeBytes,
|
|
34
|
-
state: item.state,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
27
|
const toOutboxMessageResponse = (
|
|
38
28
|
item: OutboxMessageItem,
|
|
39
29
|
attachments: OutboxAttachmentItem[] = [],
|
|
40
30
|
): OutboxMessageResponse => ({
|
|
41
|
-
attachments: attachments.map(
|
|
31
|
+
attachments: attachments.map(toOutboxAttachmentResponse),
|
|
42
32
|
outboxMessageId: item.outboxMessageId,
|
|
43
33
|
accountId: item.accountId,
|
|
44
34
|
fromAddress: item.fromAddress,
|