@remit/backend 0.0.143 → 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.
- 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 +7 -0
- package/dist/src/api.js +144 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +73 -0
- package/dist/src/config/msoauth.d.ts +30 -0
- package/dist/src/config/msoauth.js +30 -0
- package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
- package/dist/src/data-backend.js +12 -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 +41 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
- package/dist/src/derive/contentUrl.d.ts +46 -0
- package/dist/src/derive/contentUrl.js +46 -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 +14 -0
- package/dist/src/handlers/account-oauth.js +387 -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 +6 -0
- package/dist/src/handlers/account.js +350 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +141 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +425 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +192 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +212 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +330 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +176 -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 +103 -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 +40 -0
- package/dist/src/handlers/index.d.ts +2 -0
- package/dist/src/handlers/index.js +60 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +114 -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 +219 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +138 -0
- package/dist/src/handlers/message.d.ts +84 -0
- package/dist/src/handlers/message.js +607 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +112 -0
- package/dist/src/handlers/outbox-attachment.d.ts +23 -0
- package/dist/src/handlers/outbox-attachment.js +81 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +144 -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 +82 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +172 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +233 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +409 -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 +2 -0
- package/dist/src/index.js +37 -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 +18 -0
- package/dist/src/jwt-auth.js +97 -0
- package/dist/src/request-context.d.ts +9 -0
- package/dist/src/request-context.js +22 -0
- package/dist/src/request.d.ts +3 -0
- package/dist/src/request.js +7 -0
- package/dist/src/response.d.ts +27 -0
- package/dist/src/response.js +87 -0
- package/dist/src/service/account-deletion.d.ts +22 -0
- package/dist/src/service/account-deletion.js +39 -0
- package/dist/src/service/compose-sqlite.d.ts +2 -0
- package/dist/src/service/compose-sqlite.js +11 -0
- package/dist/src/service/create-remit-client.d.ts +95 -0
- package/dist/src/service/create-remit-client.js +207 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +44 -0
- package/dist/src/service/export-identity.d.ts +3 -0
- package/dist/src/service/export-identity.js +35 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +38 -0
- package/dist/src/service/organize.d.ts +184 -0
- package/dist/src/service/organize.js +384 -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/sqs.d.ts +1 -0
- package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
- package/dist/src/service/trigger-sync.d.ts +51 -0
- package/dist/src/service/trigger-sync.js +39 -0
- package/dist/src/types.d.ts +49 -0
- package/dist/src/types.js +1 -0
- package/package.json +113 -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/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/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,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue #1065: what a request is allowed to leave behind in the log.
|
|
3
|
-
*
|
|
4
|
-
* A calendar feed's token is in its path and is the whole credential, so the
|
|
5
|
-
* path is a secret; a validation failure is logged at error level and the
|
|
6
|
-
* request it carries holds the caller's Authorization header. Both are asserted
|
|
7
|
-
* against captured stdout rather than by reading the call sites, because the
|
|
8
|
-
* leak was a field bound two frames away from the handler that logged it.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { after, before, describe, it } from "node:test";
|
|
13
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
14
|
-
import { captureStdout, restoreStdout } from "./stdout-capture-fixture.js";
|
|
15
|
-
|
|
16
|
-
process.env.LOG_LEVEL = "trace";
|
|
17
|
-
|
|
18
|
-
const { mintCalendarFeedToken } = await import("@remit/calendar-service");
|
|
19
|
-
const { _resetForTest, setClient } = await import("./service/data-client.js");
|
|
20
|
-
const { createCalendarSqliteClient } = await import(
|
|
21
|
-
"./handlers/calendar-sqlite-fixture.js"
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
let handler: (
|
|
25
|
-
event: APIGatewayProxyEvent,
|
|
26
|
-
context: unknown,
|
|
27
|
-
) => Promise<APIGatewayProxyResult>;
|
|
28
|
-
let cleanup: () => void;
|
|
29
|
-
|
|
30
|
-
const send = (
|
|
31
|
-
event: Partial<APIGatewayProxyEvent> & { httpMethod: string; path: string },
|
|
32
|
-
): Promise<APIGatewayProxyResult> =>
|
|
33
|
-
handler(
|
|
34
|
-
{
|
|
35
|
-
headers: {},
|
|
36
|
-
queryStringParameters: null,
|
|
37
|
-
body: null,
|
|
38
|
-
requestContext: {},
|
|
39
|
-
...event,
|
|
40
|
-
} as unknown as APIGatewayProxyEvent,
|
|
41
|
-
{ awsRequestId: "log-redaction", functionName: "test" },
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
before(async () => {
|
|
45
|
-
_resetForTest();
|
|
46
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
47
|
-
const fixture = await createCalendarSqliteClient();
|
|
48
|
-
cleanup = fixture.cleanup;
|
|
49
|
-
setClient(fixture.client);
|
|
50
|
-
({ handler } = (await import("./index.js")) as unknown as {
|
|
51
|
-
handler: typeof handler;
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
after(() => {
|
|
56
|
-
_resetForTest();
|
|
57
|
-
restoreStdout();
|
|
58
|
-
cleanup();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe("what a request leaves in the log", () => {
|
|
62
|
-
it("never writes the feed token a subscriber polls with", async () => {
|
|
63
|
-
const { token } = mintCalendarFeedToken();
|
|
64
|
-
|
|
65
|
-
const { result: response, logged } = await captureStdout(() =>
|
|
66
|
-
send({ httpMethod: "GET", path: `/feeds/calendar/${token}.ics` }),
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
assert.equal(
|
|
70
|
-
response.statusCode,
|
|
71
|
-
404,
|
|
72
|
-
"an unknown token, but still routed",
|
|
73
|
-
);
|
|
74
|
-
assert.ok(logged.length > 0, "the request has to have logged something");
|
|
75
|
-
assert.equal(
|
|
76
|
-
logged.includes(token),
|
|
77
|
-
false,
|
|
78
|
-
"the token in the path is the credential, so the path cannot be a log field",
|
|
79
|
-
);
|
|
80
|
-
assert.ok(
|
|
81
|
-
logged.includes("/feeds/calendar/<redacted>.ics"),
|
|
82
|
-
"which route was served is what an operator reads the log for",
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("never writes the address a calendar subscribes to", async () => {
|
|
87
|
-
const secret = "private-7c0ffee5eed";
|
|
88
|
-
const { result: response, logged } = await captureStdout(() =>
|
|
89
|
-
send({
|
|
90
|
-
httpMethod: "POST",
|
|
91
|
-
path: "/calendars",
|
|
92
|
-
headers: { "Content-Type": "application/json" },
|
|
93
|
-
body: JSON.stringify({
|
|
94
|
-
urlSegment: "harbour",
|
|
95
|
-
displayName: "Harbour",
|
|
96
|
-
subscriptionUrl: `http://127.0.0.1:9/calendar/ical/${secret}/basic.ics`,
|
|
97
|
-
}),
|
|
98
|
-
requestContext: {
|
|
99
|
-
authorizer: { claims: { sub: "log-redaction-subscriber" } },
|
|
100
|
-
} as unknown as APIGatewayProxyEvent["requestContext"],
|
|
101
|
-
}),
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
assert.equal(response.statusCode, 400, response.body);
|
|
105
|
-
assert.match(response.body, /subscription_unreachable/);
|
|
106
|
-
assert.equal(response.body.includes(secret), false);
|
|
107
|
-
assert.ok(logged.length > 0, "the request has to have logged something");
|
|
108
|
-
assert.equal(
|
|
109
|
-
logged.includes(secret),
|
|
110
|
-
false,
|
|
111
|
-
"for a Google secret address the URL is the whole credential",
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("never writes the credentials a failing request arrived with", async () => {
|
|
116
|
-
// A session is already established, so the bearer is never verified and the
|
|
117
|
-
// request reaches validation — where the whole parsed request, headers and
|
|
118
|
-
// all, used to be logged. The delete refuses because it declares
|
|
119
|
-
// `calendarId` as a required query parameter and none was sent.
|
|
120
|
-
const { result: response, logged } = await captureStdout(() =>
|
|
121
|
-
send({
|
|
122
|
-
httpMethod: "DELETE",
|
|
123
|
-
path: "/calendar-events/8f14e45f-ceea-467a-9c9e-9c9e9c9e9c9e",
|
|
124
|
-
headers: {
|
|
125
|
-
Authorization: "Bearer x",
|
|
126
|
-
Cookie: "session=secret-cookie-value",
|
|
127
|
-
},
|
|
128
|
-
requestContext: {
|
|
129
|
-
authorizer: { claims: { sub: "log-redaction-sub" } },
|
|
130
|
-
} as unknown as APIGatewayProxyEvent["requestContext"],
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
assert.equal(response.statusCode, 400, response.body);
|
|
135
|
-
assert.ok(logged.includes("Validation failed"));
|
|
136
|
-
assert.equal(logged.includes("Bearer x"), false);
|
|
137
|
-
assert.equal(logged.includes("secret-cookie-value"), false);
|
|
138
|
-
assert.equal(
|
|
139
|
-
logged.includes("parsedRequest"),
|
|
140
|
-
false,
|
|
141
|
-
"the request that failed is not a field; its errors are",
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
});
|
package/src/openapi-spec.test.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { OpenAPISpec } from "./index.js";
|
|
4
|
-
|
|
5
|
-
type Parameter = {
|
|
6
|
-
name: string;
|
|
7
|
-
in: string;
|
|
8
|
-
explode?: boolean;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const queryParameters = (): Array<{
|
|
12
|
-
operationId: string;
|
|
13
|
-
param: Parameter;
|
|
14
|
-
}> => {
|
|
15
|
-
const found: Array<{ operationId: string; param: Parameter }> = [];
|
|
16
|
-
for (const item of Object.values(OpenAPISpec.paths ?? {})) {
|
|
17
|
-
for (const operation of Object.values(item ?? {})) {
|
|
18
|
-
const { operationId, parameters } = operation as {
|
|
19
|
-
operationId?: string;
|
|
20
|
-
parameters?: Parameter[];
|
|
21
|
-
};
|
|
22
|
-
if (!operationId || !parameters) continue;
|
|
23
|
-
for (const param of parameters) {
|
|
24
|
-
if (param.in === "query") found.push({ operationId, param });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return found;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type Schema = {
|
|
32
|
-
$ref?: string;
|
|
33
|
-
type?: string;
|
|
34
|
-
required?: string[];
|
|
35
|
-
properties?: Record<string, unknown>;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const resolve = (schema: Schema | undefined): Schema | undefined => {
|
|
39
|
-
if (!schema?.$ref) return schema;
|
|
40
|
-
const name = schema.$ref.replace("#/components/schemas/", "");
|
|
41
|
-
return (OpenAPISpec.components?.schemas?.[name] ?? undefined) as
|
|
42
|
-
| Schema
|
|
43
|
-
| undefined;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const errorBodySchemas = (): Array<{
|
|
47
|
-
operationId: string;
|
|
48
|
-
status: string;
|
|
49
|
-
schema: Schema | undefined;
|
|
50
|
-
}> => {
|
|
51
|
-
const found: Array<{
|
|
52
|
-
operationId: string;
|
|
53
|
-
status: string;
|
|
54
|
-
schema: Schema | undefined;
|
|
55
|
-
}> = [];
|
|
56
|
-
for (const item of Object.values(OpenAPISpec.paths ?? {})) {
|
|
57
|
-
for (const operation of Object.values(item ?? {})) {
|
|
58
|
-
const { operationId, responses } = operation as {
|
|
59
|
-
operationId?: string;
|
|
60
|
-
responses?: Record<
|
|
61
|
-
string,
|
|
62
|
-
{ content?: Record<string, { schema?: Schema }> }
|
|
63
|
-
>;
|
|
64
|
-
};
|
|
65
|
-
if (!operationId || !responses) continue;
|
|
66
|
-
for (const [status, response] of Object.entries(responses)) {
|
|
67
|
-
if (Number(status) < 400) continue;
|
|
68
|
-
const schema = response.content?.["application/json"]?.schema;
|
|
69
|
-
if (!schema) continue;
|
|
70
|
-
found.push({ operationId, status, schema: resolve(schema) });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return found;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Issue #371: one error shape for the whole API. Every JSON refusal body is
|
|
78
|
-
// flat — `code` and `message` at the top level, no `error` envelope — and
|
|
79
|
-
// `code` is required, because it is the only field a client may branch on.
|
|
80
|
-
describe("the declared shape of an error response", () => {
|
|
81
|
-
it("is a flat object carrying a required code and message", () => {
|
|
82
|
-
const offenders = errorBodySchemas()
|
|
83
|
-
.filter(({ schema }) => {
|
|
84
|
-
const required = schema?.required ?? [];
|
|
85
|
-
return (
|
|
86
|
-
schema?.type !== "object" ||
|
|
87
|
-
!required.includes("code") ||
|
|
88
|
-
!required.includes("message")
|
|
89
|
-
);
|
|
90
|
-
})
|
|
91
|
-
.map(({ operationId, status }) => `${operationId}.${status}`);
|
|
92
|
-
|
|
93
|
-
assert.deepEqual(offenders, []);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("never wraps the body in an envelope property", () => {
|
|
97
|
-
const offenders = errorBodySchemas()
|
|
98
|
-
.filter(({ schema }) => schema?.properties?.error !== undefined)
|
|
99
|
-
.map(({ operationId, status }) => `${operationId}.${status}`);
|
|
100
|
-
|
|
101
|
-
assert.deepEqual(offenders, []);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("the built OpenAPI spec", () => {
|
|
106
|
-
it("declares every query parameter exploded", () => {
|
|
107
|
-
// openapi-backend splits a non-exploded query value on commas before it
|
|
108
|
-
// validates, so `?refresh=true` arrives at the validator as `["true"]` and
|
|
109
|
-
// is refused as "must be boolean" — the handler never runs. Exploded is the
|
|
110
|
-
// wire-identical form for a scalar, so every `@query` in main.tsp carries
|
|
111
|
-
// `#{ explode: true }`.
|
|
112
|
-
const offenders = queryParameters()
|
|
113
|
-
.filter(({ param }) => param.explode === false)
|
|
114
|
-
.map(({ operationId, param }) => `${operationId}.${param.name}`);
|
|
115
|
-
|
|
116
|
-
assert.deepEqual(offenders, []);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
getRequestOrigin,
|
|
5
|
-
resolveAllowedOrigin,
|
|
6
|
-
runWithRequestContext,
|
|
7
|
-
} from "./request-context.js";
|
|
8
|
-
|
|
9
|
-
describe("request-context", () => {
|
|
10
|
-
const originalEnv = process.env.CORS_ALLOWED_ORIGINS;
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
if (originalEnv === undefined) {
|
|
14
|
-
process.env.CORS_ALLOWED_ORIGINS = undefined;
|
|
15
|
-
process.env.CORS_ALLOWED_ORIGINS = "";
|
|
16
|
-
delete process.env.CORS_ALLOWED_ORIGINS;
|
|
17
|
-
} else {
|
|
18
|
-
process.env.CORS_ALLOWED_ORIGINS = originalEnv;
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("stores and retrieves the request origin", () => {
|
|
23
|
-
runWithRequestContext({ origin: "https://example.com" }, () => {
|
|
24
|
-
assert.equal(getRequestOrigin(), "https://example.com");
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("returns undefined when no origin is set", () => {
|
|
29
|
-
runWithRequestContext({}, () => {
|
|
30
|
-
assert.equal(getRequestOrigin(), undefined);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("returns star when no allow-list is configured", () => {
|
|
35
|
-
delete process.env.CORS_ALLOWED_ORIGINS;
|
|
36
|
-
assert.equal(resolveAllowedOrigin("https://any.com"), "*");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("reflects the request origin when it is allow-listed", () => {
|
|
40
|
-
process.env.CORS_ALLOWED_ORIGINS =
|
|
41
|
-
"https://app.example.com,http://localhost:5173";
|
|
42
|
-
assert.equal(
|
|
43
|
-
resolveAllowedOrigin("http://localhost:5173"),
|
|
44
|
-
"http://localhost:5173",
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("falls back to the first allowed origin when the request origin is not allow-listed", () => {
|
|
49
|
-
process.env.CORS_ALLOWED_ORIGINS =
|
|
50
|
-
"https://app.example.com,http://localhost:5173";
|
|
51
|
-
assert.equal(
|
|
52
|
-
resolveAllowedOrigin("https://evil.example.com"),
|
|
53
|
-
"https://app.example.com",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("falls back to the first allowed origin when no request origin is provided", () => {
|
|
58
|
-
process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com";
|
|
59
|
-
assert.equal(resolveAllowedOrigin(undefined), "https://app.example.com");
|
|
60
|
-
});
|
|
61
|
-
});
|
package/src/request-context.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
|
|
3
|
-
interface RequestContext {
|
|
4
|
-
origin?: string;
|
|
5
|
-
correlationId?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const storage = new AsyncLocalStorage<RequestContext>();
|
|
9
|
-
|
|
10
|
-
export const runWithRequestContext = <T>(ctx: RequestContext, fn: () => T): T =>
|
|
11
|
-
storage.run(ctx, fn);
|
|
12
|
-
|
|
13
|
-
export const getRequestOrigin = (): string | undefined =>
|
|
14
|
-
storage.getStore()?.origin;
|
|
15
|
-
|
|
16
|
-
export const getRequestCorrelationId = (): string | undefined =>
|
|
17
|
-
storage.getStore()?.correlationId;
|
|
18
|
-
|
|
19
|
-
const parseAllowedOrigins = (): readonly string[] => {
|
|
20
|
-
const raw = process.env.CORS_ALLOWED_ORIGINS;
|
|
21
|
-
if (!raw) return [];
|
|
22
|
-
return raw
|
|
23
|
-
.split(",")
|
|
24
|
-
.map((s) => s.trim())
|
|
25
|
-
.filter((s) => s.length > 0);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const resolveAllowedOrigin = (origin: string | undefined): string => {
|
|
29
|
-
const allowed = parseAllowedOrigins();
|
|
30
|
-
if (allowed.length === 0) return "*";
|
|
31
|
-
if (origin && allowed.includes(origin)) return origin;
|
|
32
|
-
return allowed[0];
|
|
33
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two refusals no handler ever sees: a path the spec does not route, and a
|
|
3
|
-
* request the spec refuses before routing. Both are answered by the framework,
|
|
4
|
-
* and both used to leave the one shape the API promises (issue #371) — the
|
|
5
|
-
* unmatched route with no body at all, the refused request with no `code` and
|
|
6
|
-
* with the validator's raw findings beside it.
|
|
7
|
-
*/
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { describe, it } from "node:test";
|
|
10
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
11
|
-
import { api } from "./index.js";
|
|
12
|
-
import { normalizeRequest } from "./request.js";
|
|
13
|
-
|
|
14
|
-
const SUB = "cognito-sub-pipeline";
|
|
15
|
-
|
|
16
|
-
const send = async (
|
|
17
|
-
method: string,
|
|
18
|
-
path: string,
|
|
19
|
-
body?: string,
|
|
20
|
-
): Promise<APIGatewayProxyResult> => {
|
|
21
|
-
const event = {
|
|
22
|
-
httpMethod: method,
|
|
23
|
-
path,
|
|
24
|
-
body: body ?? null,
|
|
25
|
-
queryStringParameters: null,
|
|
26
|
-
headers: body ? { "content-type": "application/json" } : {},
|
|
27
|
-
requestContext: { authorizer: { claims: { sub: SUB } } },
|
|
28
|
-
} as unknown as APIGatewayProxyEvent;
|
|
29
|
-
|
|
30
|
-
return (await api.handleRequest(
|
|
31
|
-
normalizeRequest(event),
|
|
32
|
-
event,
|
|
33
|
-
{} as never,
|
|
34
|
-
)) as APIGatewayProxyResult;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
describe("a path the spec does not route", () => {
|
|
38
|
-
it("answers 404 with a body, not an empty one", async () => {
|
|
39
|
-
const response = await send("GET", "/no-such-resource");
|
|
40
|
-
|
|
41
|
-
assert.equal(response.statusCode, 404);
|
|
42
|
-
assert.deepEqual(JSON.parse(response.body), {
|
|
43
|
-
code: "not_found",
|
|
44
|
-
message: "Not found",
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("a request the spec refuses before any handler runs", () => {
|
|
50
|
-
it("answers 400 with a code and the fields it refused", async () => {
|
|
51
|
-
const response = await send(
|
|
52
|
-
"POST",
|
|
53
|
-
"/calendars",
|
|
54
|
-
JSON.stringify({ displayName: 42 }),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
assert.equal(response.statusCode, 400);
|
|
58
|
-
const body = JSON.parse(response.body) as {
|
|
59
|
-
code: string;
|
|
60
|
-
message: string;
|
|
61
|
-
errors: { path: string; rule: string; message: string }[];
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
assert.equal(body.code, "invalid_request");
|
|
65
|
-
assert.equal(body.message, "Invalid request");
|
|
66
|
-
assert.ok(body.errors.length > 0);
|
|
67
|
-
for (const detail of body.errors) {
|
|
68
|
-
assert.deepEqual(Object.keys(detail).sort(), ["message", "path", "rule"]);
|
|
69
|
-
assert.equal(typeof detail.path, "string");
|
|
70
|
-
assert.equal(typeof detail.rule, "string");
|
|
71
|
-
assert.equal(typeof detail.message, "string");
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// An AJV error also carries `params`, and under `verbose` the rejected value
|
|
76
|
-
// itself. A refused create body is whatever the caller sent — a password
|
|
77
|
-
// among the possibilities — so nothing but the three named fields may travel.
|
|
78
|
-
it("never echoes the value it refused", async () => {
|
|
79
|
-
const response = await send(
|
|
80
|
-
"POST",
|
|
81
|
-
"/accounts",
|
|
82
|
-
JSON.stringify({ password: "hunter2", imapPort: "not-a-number" }),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
assert.equal(response.statusCode, 400);
|
|
86
|
-
assert.ok(!response.body.includes("hunter2"));
|
|
87
|
-
});
|
|
88
|
-
});
|
package/src/request.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
2
|
-
import type { Request } from "openapi-backend";
|
|
3
|
-
|
|
4
|
-
export const normalizeRequest = (event: APIGatewayProxyEvent): Request => ({
|
|
5
|
-
method: event.httpMethod,
|
|
6
|
-
path: event.path,
|
|
7
|
-
query: (event.queryStringParameters ?? {}) as Record<string, string>,
|
|
8
|
-
body: event.body,
|
|
9
|
-
headers: (event.headers ?? {}) as Record<string, string>,
|
|
10
|
-
});
|
package/src/response.test.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import type { Context as OpenAPIContext } from "openapi-backend";
|
|
4
|
-
import { runWithRequestContext } from "./request-context.js";
|
|
5
|
-
import { formatResponse, postResponseHandler } from "./response.js";
|
|
6
|
-
|
|
7
|
-
type ValidateResponseFn = (
|
|
8
|
-
response: unknown,
|
|
9
|
-
operation: unknown,
|
|
10
|
-
) => { valid: boolean; errors?: { instancePath?: string; message?: string }[] };
|
|
11
|
-
|
|
12
|
-
const makeContext = (
|
|
13
|
-
response: Record<string, unknown>,
|
|
14
|
-
validateResponse: ValidateResponseFn,
|
|
15
|
-
): OpenAPIContext => {
|
|
16
|
-
return {
|
|
17
|
-
api: { validateResponse },
|
|
18
|
-
response,
|
|
19
|
-
operation: { operationId: "testOp" },
|
|
20
|
-
} as unknown as OpenAPIContext;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
describe("postResponseHandler validation gating", () => {
|
|
24
|
-
const originalStage = process.env.STAGE_NAME;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
delete process.env.STAGE_NAME;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
if (originalStage === undefined) {
|
|
32
|
-
delete process.env.STAGE_NAME;
|
|
33
|
-
} else {
|
|
34
|
-
process.env.STAGE_NAME = originalStage;
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("runs validateResponse when STAGE_NAME is 'dev'", () => {
|
|
39
|
-
process.env.STAGE_NAME = "dev";
|
|
40
|
-
let called = false;
|
|
41
|
-
const validate: ValidateResponseFn = () => {
|
|
42
|
-
called = true;
|
|
43
|
-
return { valid: true };
|
|
44
|
-
};
|
|
45
|
-
const result = postResponseHandler(makeContext({ items: [] }, validate));
|
|
46
|
-
assert.equal(called, true);
|
|
47
|
-
assert.equal(result.statusCode, 200);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("skips validateResponse when STAGE_NAME is 'prod'", () => {
|
|
51
|
-
process.env.STAGE_NAME = "prod";
|
|
52
|
-
let called = false;
|
|
53
|
-
const validate: ValidateResponseFn = () => {
|
|
54
|
-
called = true;
|
|
55
|
-
return { valid: true };
|
|
56
|
-
};
|
|
57
|
-
const result = postResponseHandler(makeContext({ items: [] }, validate));
|
|
58
|
-
assert.equal(called, false);
|
|
59
|
-
assert.equal(result.statusCode, 200);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("skips validateResponse when STAGE_NAME is 'beta'", () => {
|
|
63
|
-
process.env.STAGE_NAME = "beta";
|
|
64
|
-
let called = false;
|
|
65
|
-
const validate: ValidateResponseFn = () => {
|
|
66
|
-
called = true;
|
|
67
|
-
return { valid: true };
|
|
68
|
-
};
|
|
69
|
-
postResponseHandler(makeContext({ items: [] }, validate));
|
|
70
|
-
assert.equal(called, false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("skips validateResponse when STAGE_NAME is unset", () => {
|
|
74
|
-
let called = false;
|
|
75
|
-
const validate: ValidateResponseFn = () => {
|
|
76
|
-
called = true;
|
|
77
|
-
return { valid: true };
|
|
78
|
-
};
|
|
79
|
-
postResponseHandler(makeContext({ items: [] }, validate));
|
|
80
|
-
assert.equal(called, false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("returns the formatted error response when statusCode is set, regardless of stage", () => {
|
|
84
|
-
process.env.STAGE_NAME = "dev";
|
|
85
|
-
let called = false;
|
|
86
|
-
const validate: ValidateResponseFn = () => {
|
|
87
|
-
called = true;
|
|
88
|
-
return { valid: true };
|
|
89
|
-
};
|
|
90
|
-
const result = postResponseHandler(
|
|
91
|
-
makeContext({ statusCode: 404, message: "Not Found" }, validate),
|
|
92
|
-
);
|
|
93
|
-
assert.equal(called, false);
|
|
94
|
-
assert.equal(result.statusCode, 404);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("returns 200 with the response body when validation fails in dev", () => {
|
|
98
|
-
process.env.STAGE_NAME = "dev";
|
|
99
|
-
const validate: ValidateResponseFn = () => ({
|
|
100
|
-
valid: false,
|
|
101
|
-
errors: [{ instancePath: "/items/0/id", message: "must be string" }],
|
|
102
|
-
});
|
|
103
|
-
const result = postResponseHandler(
|
|
104
|
-
makeContext({ items: [{ id: 42 }] }, validate),
|
|
105
|
-
);
|
|
106
|
-
assert.equal(result.statusCode, 200);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// A bug report quotes the correlation id off the failing response. Without the
|
|
111
|
-
// header it reads "(none)" and there is nothing to grep the server logs for.
|
|
112
|
-
describe("the correlation id travels back on the response", () => {
|
|
113
|
-
it("carries the request's id, and exposes the header to the browser", () => {
|
|
114
|
-
const result = runWithRequestContext({ correlationId: "req-604" }, () =>
|
|
115
|
-
formatResponse({ message: "Conflict" }, 409),
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
assert.equal(result.headers?.["x-correlation-id"], "req-604");
|
|
119
|
-
assert.match(
|
|
120
|
-
String(result.headers?.["Access-Control-Expose-Headers"]),
|
|
121
|
-
/x-correlation-id/,
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("omits the header when the request carried no id", () => {
|
|
126
|
-
const result = formatResponse({ message: "Conflict" }, 409);
|
|
127
|
-
|
|
128
|
-
assert.equal(result.headers?.["x-correlation-id"], undefined);
|
|
129
|
-
});
|
|
130
|
-
});
|