@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.
- package/dist/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +73 -0
- package/dist/src/auth.d.ts +24 -0
- package/dist/src/auth.js +81 -0
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +19 -0
- package/dist/src/config/msoauth.js +6 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +40 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
- package/dist/src/derive/contentUrl.d.ts +33 -0
- package/dist/src/derive/contentUrl.js +27 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +15 -0
- package/dist/src/handlers/account-oauth.js +334 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +7 -0
- package/dist/src/handlers/account.js +337 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +133 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +417 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +184 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +204 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +323 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +164 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +88 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +37 -0
- package/dist/src/handlers/index.d.ts +88 -0
- package/dist/src/handlers/index.js +62 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +107 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +211 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +117 -0
- package/dist/src/handlers/message.d.ts +82 -0
- package/dist/src/handlers/message.js +595 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +103 -0
- package/dist/src/handlers/outbox-attachment.d.ts +25 -0
- package/dist/src/handlers/outbox-attachment.js +77 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +136 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +73 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +159 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +228 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +406 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +53 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +15 -0
- package/dist/src/jwt-auth.js +45 -0
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +9 -0
- package/dist/src/request.js +17 -0
- package/dist/src/response.d.ts +44 -0
- package/dist/src/response.js +80 -0
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +20 -0
- package/dist/src/service/account-deletion.js +33 -0
- package/dist/src/service/compose-sqlite.d.ts +3 -0
- package/dist/src/service/compose-sqlite.js +13 -0
- package/dist/src/service/create-remit-client.d.ts +109 -0
- package/dist/src/service/create-remit-client.js +188 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +42 -0
- package/dist/src/service/export-identity.d.ts +4 -0
- package/dist/src/service/export-identity.js +33 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +37 -0
- package/dist/src/service/organize.d.ts +181 -0
- package/dist/src/service/organize.js +367 -0
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +57 -0
- package/dist/src/service/trigger-sync.js +31 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +1 -0
- package/package.json +125 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/data-backend.ts +0 -14
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/sqs.ts +0 -4
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { createServer, type Server } from "node:http";
|
|
4
|
-
import type { AddressInfo } from "node:net";
|
|
5
|
-
import { after, before, beforeEach, describe, it } from "node:test";
|
|
6
|
-
import type {
|
|
7
|
-
AccountResponse,
|
|
8
|
-
AccountService as AccountServiceName,
|
|
9
|
-
} from "@remit/api-openapi-types";
|
|
10
|
-
import { AccountService } from "@remit/domain-enums";
|
|
11
|
-
import { MICROSOFT_SERVICE_SCOPES } from "@remit/mail-oauth-service";
|
|
12
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
13
|
-
import { deriveAccountConfigId } from "../auth.js";
|
|
14
|
-
import {
|
|
15
|
-
_resetForTest,
|
|
16
|
-
type RemitClient,
|
|
17
|
-
setClient,
|
|
18
|
-
} from "../service/data-client.js";
|
|
19
|
-
import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
|
|
20
|
-
|
|
21
|
-
const CLIENT_SECRET = "consent-test-client-secret";
|
|
22
|
-
const WEB_ORIGIN = "https://mail.example.com";
|
|
23
|
-
const GRAPH = "https://graph.microsoft.com/";
|
|
24
|
-
const MAIL_SCOPES = [...MICROSOFT_SERVICE_SCOPES[AccountService.Mail]];
|
|
25
|
-
const CALENDAR_SCOPES = [...MICROSOFT_SERVICE_SCOPES[AccountService.Calendar]];
|
|
26
|
-
|
|
27
|
-
let handler: (
|
|
28
|
-
event: APIGatewayProxyEvent,
|
|
29
|
-
context: unknown,
|
|
30
|
-
) => Promise<APIGatewayProxyResult>;
|
|
31
|
-
let client: RemitClient;
|
|
32
|
-
let cleanup: () => void;
|
|
33
|
-
let tokenEndpoint: Server;
|
|
34
|
-
let withheld: Set<string>;
|
|
35
|
-
let redemptions: string[][];
|
|
36
|
-
|
|
37
|
-
const jwt = (claims: Record<string, unknown>): string =>
|
|
38
|
-
[
|
|
39
|
-
Buffer.from(JSON.stringify({ alg: "RS256" })).toString("base64url"),
|
|
40
|
-
Buffer.from(JSON.stringify(claims), "utf8").toString("base64url"),
|
|
41
|
-
"signature-not-checked-here",
|
|
42
|
-
].join(".");
|
|
43
|
-
|
|
44
|
-
const readBody = async (request: AsyncIterable<Buffer>): Promise<string> => {
|
|
45
|
-
const chunks: Buffer[] = [];
|
|
46
|
-
for await (const chunk of request) chunks.push(chunk);
|
|
47
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const startTokenEndpoint = async (): Promise<string> => {
|
|
51
|
-
tokenEndpoint = createServer(async (request, response) => {
|
|
52
|
-
const form = new URLSearchParams(await readBody(request));
|
|
53
|
-
const requested = (form.get("scope") ?? "").split(" ").filter(Boolean);
|
|
54
|
-
redemptions.push(requested);
|
|
55
|
-
const granted = requested.filter((scope) => !withheld.has(scope));
|
|
56
|
-
const resourceScopes = granted.filter((scope) => scope.includes("://"));
|
|
57
|
-
if (resourceScopes.length === 0) {
|
|
58
|
-
response.writeHead(400, { "Content-Type": "application/json" });
|
|
59
|
-
response.end(
|
|
60
|
-
JSON.stringify({ error: "invalid_grant", error_codes: [65001] }),
|
|
61
|
-
);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
response.writeHead(200, { "Content-Type": "application/json" });
|
|
65
|
-
response.end(
|
|
66
|
-
JSON.stringify({
|
|
67
|
-
access_token: "access-token",
|
|
68
|
-
refresh_token: `refresh-token-${redemptions.length}`,
|
|
69
|
-
id_token: jwt({ preferred_username: "person@example.com" }),
|
|
70
|
-
expires_in: 3600,
|
|
71
|
-
scope: granted
|
|
72
|
-
.map((scope) =>
|
|
73
|
-
scope.startsWith(GRAPH) ? scope.slice(GRAPH.length) : scope,
|
|
74
|
-
)
|
|
75
|
-
.join(" "),
|
|
76
|
-
}),
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
await new Promise<void>((resolve) => tokenEndpoint.listen(0, resolve));
|
|
80
|
-
const { port } = tokenEndpoint.address() as AddressInfo;
|
|
81
|
-
return `http://127.0.0.1:${port}/token`;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const headerOf = (
|
|
85
|
-
response: APIGatewayProxyResult,
|
|
86
|
-
name: string,
|
|
87
|
-
): string | undefined => {
|
|
88
|
-
for (const [key, value] of Object.entries(response.headers ?? {})) {
|
|
89
|
-
if (key.toLowerCase() === name) return String(value);
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const request = (
|
|
95
|
-
sub: string,
|
|
96
|
-
httpMethod: string,
|
|
97
|
-
path: string,
|
|
98
|
-
body: unknown,
|
|
99
|
-
queryStringParameters: Record<string, string> | null = null,
|
|
100
|
-
): Promise<APIGatewayProxyResult> =>
|
|
101
|
-
handler(
|
|
102
|
-
{
|
|
103
|
-
httpMethod,
|
|
104
|
-
path,
|
|
105
|
-
headers: { "content-type": "application/json" },
|
|
106
|
-
queryStringParameters,
|
|
107
|
-
body: body === null ? null : JSON.stringify(body),
|
|
108
|
-
requestContext: { authorizer: { claims: { sub } } },
|
|
109
|
-
} as unknown as APIGatewayProxyEvent,
|
|
110
|
-
{ awsRequestId: `req-${randomUUID()}`, functionName: "test" },
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const startConsent = async (
|
|
114
|
-
sub: string,
|
|
115
|
-
services: AccountServiceName[] | undefined,
|
|
116
|
-
): Promise<URL> => {
|
|
117
|
-
const response = await request(
|
|
118
|
-
sub,
|
|
119
|
-
"POST",
|
|
120
|
-
"/accounts/oauth/microsoft/start",
|
|
121
|
-
{
|
|
122
|
-
email: "person@example.com",
|
|
123
|
-
services,
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
assert.equal(response.statusCode, 200, response.body);
|
|
127
|
-
return new URL(
|
|
128
|
-
(JSON.parse(response.body) as { authorizationUrl: string })
|
|
129
|
-
.authorizationUrl,
|
|
130
|
-
);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const consent = async (
|
|
134
|
-
sub: string,
|
|
135
|
-
services: AccountServiceName[] | undefined,
|
|
136
|
-
): Promise<string> => {
|
|
137
|
-
const authorizationUrl = await startConsent(sub, services);
|
|
138
|
-
const state = authorizationUrl.searchParams.get("state");
|
|
139
|
-
assert.ok(state, "the authorization URL carries the signed state");
|
|
140
|
-
const response = await request(
|
|
141
|
-
sub,
|
|
142
|
-
"GET",
|
|
143
|
-
"/accounts/oauth/microsoft/callback",
|
|
144
|
-
null,
|
|
145
|
-
{ code: "auth-code", state },
|
|
146
|
-
);
|
|
147
|
-
assert.equal(response.statusCode, 302, response.body);
|
|
148
|
-
const location = headerOf(response, "location");
|
|
149
|
-
assert.ok(location, "the callback answers with a Location");
|
|
150
|
-
return location;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const onlyAccount = async (sub: string) => {
|
|
154
|
-
const accounts = await client.account.listAllByAccountConfig(
|
|
155
|
-
deriveAccountConfigId(sub),
|
|
156
|
-
);
|
|
157
|
-
assert.equal(accounts.length, 1, "the consent wrote exactly one account");
|
|
158
|
-
return accounts[0];
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const patchServices = (
|
|
162
|
-
sub: string,
|
|
163
|
-
accountId: string,
|
|
164
|
-
syncedServices: AccountServiceName[],
|
|
165
|
-
): Promise<APIGatewayProxyResult> =>
|
|
166
|
-
request(sub, "PATCH", `/accounts/${accountId}`, { syncedServices });
|
|
167
|
-
|
|
168
|
-
before(async () => {
|
|
169
|
-
_resetForTest();
|
|
170
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
171
|
-
process.env.CORS_ALLOWED_ORIGINS = WEB_ORIGIN;
|
|
172
|
-
process.env.MSOAUTH_REDIRECT_URI = `${WEB_ORIGIN}/accounts/oauth/microsoft/callback`;
|
|
173
|
-
process.env.MSOAUTH_CLIENT_ID = "consent-test-client-id";
|
|
174
|
-
process.env.MSOAUTH_CLIENT_SECRET = CLIENT_SECRET;
|
|
175
|
-
process.env.MSOAUTH_TOKEN_ENDPOINT = await startTokenEndpoint();
|
|
176
|
-
process.env.FAKE_KMS_DATAKEY = "0".repeat(64);
|
|
177
|
-
process.env.AWS_REGION = "eu-west-1";
|
|
178
|
-
process.env.AWS_ACCESS_KEY_ID = "test";
|
|
179
|
-
process.env.AWS_SECRET_ACCESS_KEY = "test";
|
|
180
|
-
({ client, cleanup } = await createCalendarSqliteClient());
|
|
181
|
-
setClient(client);
|
|
182
|
-
({ handler } = (await import("../index.js")) as unknown as {
|
|
183
|
-
handler: typeof handler;
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
beforeEach(() => {
|
|
188
|
-
withheld = new Set();
|
|
189
|
-
redemptions = [];
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
after(async () => {
|
|
193
|
-
_resetForTest();
|
|
194
|
-
cleanup();
|
|
195
|
-
await new Promise<void>((resolve) => tokenEndpoint.close(() => resolve()));
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("the Microsoft consent for the services an account syncs", () => {
|
|
199
|
-
it("asks for the calendar scope only when the start names Calendar", async () => {
|
|
200
|
-
const sub = randomUUID();
|
|
201
|
-
|
|
202
|
-
const mailOnly = (
|
|
203
|
-
await startConsent(sub, [AccountService.Mail])
|
|
204
|
-
).searchParams
|
|
205
|
-
.get("scope")
|
|
206
|
-
?.split(" ");
|
|
207
|
-
const calendarOnly = (
|
|
208
|
-
await startConsent(sub, [AccountService.Calendar])
|
|
209
|
-
).searchParams
|
|
210
|
-
.get("scope")
|
|
211
|
-
?.split(" ");
|
|
212
|
-
|
|
213
|
-
assert.ok(mailOnly);
|
|
214
|
-
assert.ok(calendarOnly);
|
|
215
|
-
assert.deepEqual(
|
|
216
|
-
mailOnly.filter((scope) => scope.includes("://")),
|
|
217
|
-
MAIL_SCOPES,
|
|
218
|
-
);
|
|
219
|
-
assert.deepEqual(
|
|
220
|
-
calendarOnly.filter((scope) => scope.includes("://")),
|
|
221
|
-
CALENDAR_SCOPES,
|
|
222
|
-
);
|
|
223
|
-
assert.ok(calendarOnly.includes("offline_access"));
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("creates a calendar-only account holding the calendar grant", async () => {
|
|
227
|
-
const sub = randomUUID();
|
|
228
|
-
|
|
229
|
-
const location = await consent(sub, [AccountService.Calendar]);
|
|
230
|
-
|
|
231
|
-
const account = await onlyAccount(sub);
|
|
232
|
-
assert.equal(
|
|
233
|
-
location,
|
|
234
|
-
`${WEB_ORIGIN}/settings/accounts?connected=${encodeURIComponent(account.accountId)}`,
|
|
235
|
-
);
|
|
236
|
-
assert.deepEqual(account.syncedServices, [AccountService.Calendar]);
|
|
237
|
-
assert.deepEqual(account.grantedScopes, CALENDAR_SCOPES);
|
|
238
|
-
assert.deepEqual(redemptions, [
|
|
239
|
-
[...CALENDAR_SCOPES, "offline_access", "openid", "email"],
|
|
240
|
-
]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("refuses a consent that comes back without the calendar scope and writes nothing", async () => {
|
|
244
|
-
const sub = randomUUID();
|
|
245
|
-
withheld = new Set(CALENDAR_SCOPES);
|
|
246
|
-
|
|
247
|
-
const location = await consent(sub, [
|
|
248
|
-
AccountService.Mail,
|
|
249
|
-
AccountService.Calendar,
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
|
-
assert.equal(
|
|
253
|
-
location,
|
|
254
|
-
`${WEB_ORIGIN}/settings/accounts?oauthError=scope_not_granted&oauthEmail=person%40example.com&missingServices=Calendar`,
|
|
255
|
-
);
|
|
256
|
-
assert.deepEqual(
|
|
257
|
-
await client.account.listAllByAccountConfig(deriveAccountConfigId(sub)),
|
|
258
|
-
[],
|
|
259
|
-
);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("keeps calendar when a mail and calendar account reconnects naming only its email", async () => {
|
|
263
|
-
const sub = randomUUID();
|
|
264
|
-
await consent(sub, [AccountService.Mail, AccountService.Calendar]);
|
|
265
|
-
const connected = await onlyAccount(sub);
|
|
266
|
-
|
|
267
|
-
redemptions = [];
|
|
268
|
-
const location = await consent(sub, undefined);
|
|
269
|
-
|
|
270
|
-
const reconnected = await onlyAccount(sub);
|
|
271
|
-
assert.equal(
|
|
272
|
-
location,
|
|
273
|
-
`${WEB_ORIGIN}/settings/accounts?connected=${encodeURIComponent(connected.accountId)}`,
|
|
274
|
-
);
|
|
275
|
-
assert.deepEqual(reconnected.syncedServices, [
|
|
276
|
-
AccountService.Mail,
|
|
277
|
-
AccountService.Calendar,
|
|
278
|
-
]);
|
|
279
|
-
assert.deepEqual(reconnected.grantedScopes, [
|
|
280
|
-
...MAIL_SCOPES,
|
|
281
|
-
...CALENDAR_SCOPES,
|
|
282
|
-
]);
|
|
283
|
-
assert.deepEqual(
|
|
284
|
-
redemptions.map((scopes) => scopes.filter((s) => s.includes("://"))),
|
|
285
|
-
[MAIL_SCOPES, CALENDAR_SCOPES],
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("adds calendar to a mail account through a second consent", async () => {
|
|
290
|
-
const sub = randomUUID();
|
|
291
|
-
await consent(sub, [AccountService.Mail]);
|
|
292
|
-
const mailAccount = await onlyAccount(sub);
|
|
293
|
-
assert.deepEqual(mailAccount.grantedScopes, MAIL_SCOPES);
|
|
294
|
-
|
|
295
|
-
const refused = await patchServices(sub, mailAccount.accountId, [
|
|
296
|
-
AccountService.Mail,
|
|
297
|
-
AccountService.Calendar,
|
|
298
|
-
]);
|
|
299
|
-
assert.equal(refused.statusCode, 400, refused.body);
|
|
300
|
-
const { message } = JSON.parse(refused.body) as { message: string };
|
|
301
|
-
assert.match(message, /granted scopes lack Calendar/);
|
|
302
|
-
assert.match(message, /POST \/accounts\/oauth\/microsoft\/start/);
|
|
303
|
-
|
|
304
|
-
redemptions = [];
|
|
305
|
-
await consent(sub, [AccountService.Mail, AccountService.Calendar]);
|
|
306
|
-
|
|
307
|
-
const reconnected = await onlyAccount(sub);
|
|
308
|
-
assert.equal(reconnected.accountId, mailAccount.accountId);
|
|
309
|
-
assert.deepEqual(reconnected.syncedServices, [
|
|
310
|
-
AccountService.Mail,
|
|
311
|
-
AccountService.Calendar,
|
|
312
|
-
]);
|
|
313
|
-
assert.deepEqual(reconnected.grantedScopes, [
|
|
314
|
-
...MAIL_SCOPES,
|
|
315
|
-
...CALENDAR_SCOPES,
|
|
316
|
-
]);
|
|
317
|
-
assert.deepEqual(
|
|
318
|
-
redemptions.map((scopes) => scopes.filter((s) => s.includes("://"))),
|
|
319
|
-
[MAIL_SCOPES, CALENDAR_SCOPES],
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
const mailAgain = await patchServices(sub, mailAccount.accountId, [
|
|
323
|
-
AccountService.Mail,
|
|
324
|
-
]);
|
|
325
|
-
assert.equal(mailAgain.statusCode, 200, mailAgain.body);
|
|
326
|
-
const both = await patchServices(sub, mailAccount.accountId, [
|
|
327
|
-
AccountService.Calendar,
|
|
328
|
-
AccountService.Mail,
|
|
329
|
-
]);
|
|
330
|
-
assert.equal(both.statusCode, 200, both.body);
|
|
331
|
-
assert.deepEqual(
|
|
332
|
-
(JSON.parse(both.body) as AccountResponse).syncedServices,
|
|
333
|
-
[AccountService.Mail, AccountService.Calendar],
|
|
334
|
-
);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What an operator can tell from the log when the Microsoft OAuth start fails.
|
|
3
|
-
*
|
|
4
|
-
* Every credential failure answers the same 500 with the same body, so the
|
|
5
|
-
* backend log for that request is the only place the four are told apart: an
|
|
6
|
-
* instance with no Microsoft app registered must not read like a live outage.
|
|
7
|
-
* Driven through the real handler so what is asserted is the line a deployment
|
|
8
|
-
* actually writes, correlation id and all.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
-
import { after, before, describe, it } from "node:test";
|
|
14
|
-
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
15
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
16
|
-
import { captureStdout, restoreStdout } from "../stdout-capture-fixture.js";
|
|
17
|
-
|
|
18
|
-
const WEB_ORIGIN = "https://mail.example.com";
|
|
19
|
-
const START_PATH = "/accounts/oauth/microsoft/start";
|
|
20
|
-
const SECRET_ARN =
|
|
21
|
-
"arn:aws:secretsmanager:eu-west-1:000000000000:secret:msoauth-AbCdEf";
|
|
22
|
-
|
|
23
|
-
let handler: (
|
|
24
|
-
event: APIGatewayProxyEvent,
|
|
25
|
-
context: unknown,
|
|
26
|
-
) => Promise<APIGatewayProxyResult>;
|
|
27
|
-
|
|
28
|
-
const originalSend = SecretsManagerClient.prototype.send;
|
|
29
|
-
let secretString: string | undefined;
|
|
30
|
-
|
|
31
|
-
interface Outcome {
|
|
32
|
-
response: APIGatewayProxyResult;
|
|
33
|
-
log: string;
|
|
34
|
-
logged: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const headerOf = (
|
|
38
|
-
response: APIGatewayProxyResult,
|
|
39
|
-
name: string,
|
|
40
|
-
): string | undefined => {
|
|
41
|
-
for (const [key, value] of Object.entries(response.headers ?? {})) {
|
|
42
|
-
if (key.toLowerCase() === name) return String(value);
|
|
43
|
-
}
|
|
44
|
-
return undefined;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const start = async (): Promise<Outcome> => {
|
|
48
|
-
const awsRequestId = `req-${randomUUID()}`;
|
|
49
|
-
const { result: response, logged } = await captureStdout(() =>
|
|
50
|
-
handler(
|
|
51
|
-
{
|
|
52
|
-
httpMethod: "POST",
|
|
53
|
-
path: START_PATH,
|
|
54
|
-
headers: {},
|
|
55
|
-
queryStringParameters: null,
|
|
56
|
-
body: JSON.stringify({ email: "matthijs@example.com" }),
|
|
57
|
-
requestContext: { authorizer: { claims: { sub: "cognito-sub" } } },
|
|
58
|
-
} as unknown as APIGatewayProxyEvent,
|
|
59
|
-
{ awsRequestId, functionName: "test" },
|
|
60
|
-
),
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
assert.equal(
|
|
64
|
-
headerOf(response, "x-correlation-id"),
|
|
65
|
-
awsRequestId,
|
|
66
|
-
"the id the client reports has to be the id the log lines carry",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const forThisRequest = logged
|
|
70
|
-
.split("\n")
|
|
71
|
-
.filter((line) => line.length > 0)
|
|
72
|
-
.map((line) => JSON.parse(line) as Record<string, unknown>)
|
|
73
|
-
.filter((line) => line.requestId === awsRequestId);
|
|
74
|
-
|
|
75
|
-
assert.ok(
|
|
76
|
-
forThisRequest.length > 0,
|
|
77
|
-
"the failure left no line carrying the correlation id the client was handed",
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
return { response, log: JSON.stringify(forThisRequest), logged };
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/** The name each condition, and only that condition, puts in the log. */
|
|
84
|
-
const NO_APP_REGISTERED = "MicrosoftOAuthNotConfigured";
|
|
85
|
-
const SECRET_HAS_NO_STRING = "MicrosoftOAuthSecretEmpty";
|
|
86
|
-
const SECRET_MISSING_FIELDS = "MicrosoftOAuthSecretIncomplete";
|
|
87
|
-
const SECRET_IS_NOT_JSON = "MicrosoftOAuthSecretUnparseable";
|
|
88
|
-
|
|
89
|
-
const CONDITIONS = [
|
|
90
|
-
NO_APP_REGISTERED,
|
|
91
|
-
SECRET_HAS_NO_STRING,
|
|
92
|
-
SECRET_MISSING_FIELDS,
|
|
93
|
-
SECRET_IS_NOT_JSON,
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
const assertNamesOnly = (log: string, condition: string): void => {
|
|
97
|
-
assert.ok(
|
|
98
|
-
log.includes(condition),
|
|
99
|
-
`the log for this request never names the condition ${condition}: ${log}`,
|
|
100
|
-
);
|
|
101
|
-
for (const other of CONDITIONS) {
|
|
102
|
-
if (other === condition) continue;
|
|
103
|
-
assert.ok(
|
|
104
|
-
!log.includes(other),
|
|
105
|
-
`the log also names ${other}, so the two conditions are indistinguishable`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const assertGenericFiveHundred = (response: APIGatewayProxyResult): void => {
|
|
111
|
-
assert.equal(response.statusCode, 500);
|
|
112
|
-
assert.deepEqual(JSON.parse(response.body), {
|
|
113
|
-
code: "internal_error",
|
|
114
|
-
message: "Internal server error",
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
before(async () => {
|
|
119
|
-
process.env.LOG_LEVEL = "error";
|
|
120
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
121
|
-
process.env.CORS_ALLOWED_ORIGINS = WEB_ORIGIN;
|
|
122
|
-
process.env.MSOAUTH_REDIRECT_URI = `${WEB_ORIGIN}/accounts/oauth/microsoft/callback`;
|
|
123
|
-
|
|
124
|
-
SecretsManagerClient.prototype.send = (async () => ({
|
|
125
|
-
SecretString: secretString,
|
|
126
|
-
})) as typeof SecretsManagerClient.prototype.send;
|
|
127
|
-
|
|
128
|
-
({ handler } = (await import("../index.js")) as unknown as {
|
|
129
|
-
handler: typeof handler;
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
after(() => {
|
|
134
|
-
SecretsManagerClient.prototype.send = originalSend;
|
|
135
|
-
restoreStdout();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe("a Microsoft OAuth start that cannot load its credentials", () => {
|
|
139
|
-
it("names an instance with no Microsoft app registered", async () => {
|
|
140
|
-
delete process.env.MSOAUTH_CLIENT_ID;
|
|
141
|
-
delete process.env.MSOAUTH_CLIENT_SECRET;
|
|
142
|
-
delete process.env.MSOAUTH_SECRET_ARN;
|
|
143
|
-
|
|
144
|
-
const { response, log } = await start();
|
|
145
|
-
|
|
146
|
-
assertGenericFiveHundred(response);
|
|
147
|
-
assertNamesOnly(log, NO_APP_REGISTERED);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("names a secret that carries no string value", async () => {
|
|
151
|
-
delete process.env.MSOAUTH_CLIENT_ID;
|
|
152
|
-
delete process.env.MSOAUTH_CLIENT_SECRET;
|
|
153
|
-
process.env.MSOAUTH_SECRET_ARN = SECRET_ARN;
|
|
154
|
-
secretString = undefined;
|
|
155
|
-
|
|
156
|
-
const { response, log } = await start();
|
|
157
|
-
|
|
158
|
-
assertGenericFiveHundred(response);
|
|
159
|
-
assertNamesOnly(log, SECRET_HAS_NO_STRING);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("names a secret whose credential fields are absent", async () => {
|
|
163
|
-
delete process.env.MSOAUTH_CLIENT_ID;
|
|
164
|
-
delete process.env.MSOAUTH_CLIENT_SECRET;
|
|
165
|
-
process.env.MSOAUTH_SECRET_ARN = SECRET_ARN;
|
|
166
|
-
secretString = JSON.stringify({ clientId: "an-app-id" });
|
|
167
|
-
|
|
168
|
-
const { response, log } = await start();
|
|
169
|
-
|
|
170
|
-
assertGenericFiveHundred(response);
|
|
171
|
-
assertNamesOnly(log, SECRET_MISSING_FIELDS);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("names a secret that is not JSON without quoting what it holds", async () => {
|
|
175
|
-
const secretMaterial = "s3cr3t-not-json-9d41c0b7";
|
|
176
|
-
delete process.env.MSOAUTH_CLIENT_ID;
|
|
177
|
-
delete process.env.MSOAUTH_CLIENT_SECRET;
|
|
178
|
-
process.env.MSOAUTH_SECRET_ARN = SECRET_ARN;
|
|
179
|
-
secretString = secretMaterial;
|
|
180
|
-
|
|
181
|
-
const { response, log, logged } = await start();
|
|
182
|
-
|
|
183
|
-
assertGenericFiveHundred(response);
|
|
184
|
-
assertNamesOnly(log, SECRET_IS_NOT_JSON);
|
|
185
|
-
assert.equal(
|
|
186
|
-
logged.includes(secretMaterial),
|
|
187
|
-
false,
|
|
188
|
-
"a parse failure quoting the secret into the log turns a misconfiguration into a leak",
|
|
189
|
-
);
|
|
190
|
-
});
|
|
191
|
-
});
|