@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.
- 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 -521
- 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
package/src/index.test.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue #1054: the backend must log one structured access line per request
|
|
3
|
-
* (method, path, status, duration, correlation id) at a level that is on in
|
|
4
|
-
* production. The access log lives in `rawHandler` in `src/index.ts`, wrapping
|
|
5
|
-
* the authenticated request path — so a 401 denial from edge-auth also gets an
|
|
6
|
-
* access line (once it flows through `formatResponse` with the correlation id).
|
|
7
|
-
*
|
|
8
|
-
* The 401 response shape (correlation id header + CORS on the denial) is tested
|
|
9
|
-
* directly in `jwt-auth.test.ts` via `runWithRequestContext`; this file tests
|
|
10
|
-
* that the handler emits the access log line on every request by spying on the
|
|
11
|
-
* logger's info method.
|
|
12
|
-
*/
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { describe, it, mock } from "node:test";
|
|
15
|
-
import { logger } from "@remit/logger-lambda";
|
|
16
|
-
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
|
|
17
|
-
import { handler } from "./index.js";
|
|
18
|
-
|
|
19
|
-
const buildEvent = (
|
|
20
|
-
overrides: Partial<APIGatewayProxyEvent> = {},
|
|
21
|
-
): APIGatewayProxyEvent =>
|
|
22
|
-
({
|
|
23
|
-
path: "/api/test",
|
|
24
|
-
httpMethod: "GET",
|
|
25
|
-
pathParameters: {},
|
|
26
|
-
queryStringParameters: {},
|
|
27
|
-
headers: {},
|
|
28
|
-
requestContext: {},
|
|
29
|
-
...overrides,
|
|
30
|
-
}) as unknown as APIGatewayProxyEvent;
|
|
31
|
-
|
|
32
|
-
const buildContext = (correlationId = "ctx-test-1"): Context =>
|
|
33
|
-
({
|
|
34
|
-
awsRequestId: correlationId,
|
|
35
|
-
functionName: "test-function",
|
|
36
|
-
getRemainingTimeInMillis: () => 30000,
|
|
37
|
-
}) as unknown as Context;
|
|
38
|
-
|
|
39
|
-
describe("issue #1054: per-request access log", () => {
|
|
40
|
-
it('emits one "Request completed" line at info level with method, path, status, duration and correlation id', async () => {
|
|
41
|
-
delete process.env.LOCAL_ACCOUNT_CONFIG_ID;
|
|
42
|
-
|
|
43
|
-
// Spy on logger.info — the real logger passes (fields, message) in
|
|
44
|
-
// pino order, so the spy captures the structured fields and message.
|
|
45
|
-
const infoSpy = mock.method(logger, "info");
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
await handler(
|
|
49
|
-
buildEvent({ path: "/api/__nonexistent_for_test__" }),
|
|
50
|
-
buildContext("ctx-access-log"),
|
|
51
|
-
).catch(() => {
|
|
52
|
-
// 404s are expected; we only care about the log line.
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// `logger.info` is overloaded ((obj, msg?) and (msg, obj?)), so the
|
|
56
|
-
// mock types `call.arguments` by a single overload. Read the
|
|
57
|
-
// arguments positionally through an unknown[] view instead.
|
|
58
|
-
const accessCalls = infoSpy.mock.calls.filter((call) => {
|
|
59
|
-
// pino calls logger.info(fields, message)
|
|
60
|
-
const args = call.arguments as unknown[];
|
|
61
|
-
return args[1] === "Request completed";
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
assert.ok(accessCalls.length >= 1, "expected an access log line");
|
|
65
|
-
const fields = (accessCalls[0].arguments as unknown[])[0] as Record<
|
|
66
|
-
string,
|
|
67
|
-
unknown
|
|
68
|
-
>;
|
|
69
|
-
|
|
70
|
-
// infoSpy was called on logger.info, so the level is "info" by
|
|
71
|
-
// construction — pino's adapter routes .info() calls to the info
|
|
72
|
-
// level. No separate level field needed.
|
|
73
|
-
|
|
74
|
-
assert.equal(fields.method, "GET");
|
|
75
|
-
assert.equal(fields.path, "/api/__nonexistent_for_test__");
|
|
76
|
-
assert.equal(fields.correlationId, "ctx-access-log");
|
|
77
|
-
assert.equal(typeof fields.statusCode, "number");
|
|
78
|
-
assert.equal(typeof fields.durationMs, "number");
|
|
79
|
-
} finally {
|
|
80
|
-
infoSpy.mock.restore();
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { inspect } from "node:util";
|
|
5
|
-
import { redactCalendarFeedPath } from "@remit/calendar-service";
|
|
6
|
-
import { logger, withLogContext, withTelemetry } from "@remit/logger-lambda";
|
|
7
|
-
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
|
|
8
|
-
import {
|
|
9
|
-
type Document,
|
|
10
|
-
OpenAPIBackend,
|
|
11
|
-
type Context as OpenAPIContext,
|
|
12
|
-
type Request,
|
|
13
|
-
} from "openapi-backend";
|
|
14
|
-
import { assertLocalBypassNotInDeployedEnv } from "./auth.js";
|
|
15
|
-
import { usesBetterAuthJwt } from "./data-backend.js";
|
|
16
|
-
import { defaultErrorCode, handleError } from "./error.js";
|
|
17
|
-
import { handlers } from "./handlers/index.js";
|
|
18
|
-
import { authenticateSelfHostRequest } from "./jwt-auth.js";
|
|
19
|
-
import { normalizeRequest } from "./request.js";
|
|
20
|
-
import { runWithRequestContext } from "./request-context.js";
|
|
21
|
-
import { formatResponse, postResponseHandler } from "./response.js";
|
|
22
|
-
|
|
23
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
|
|
25
|
-
// Boot-time refusal: the local auth-bypass overrides must never be set in a
|
|
26
|
-
// deployed environment. Runs on module load so a leaked env var fails the
|
|
27
|
-
// process at startup rather than silently opening authentication at runtime.
|
|
28
|
-
assertLocalBypassNotInDeployedEnv();
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Last-resort net for a leaked background rejection.
|
|
32
|
-
*
|
|
33
|
-
* Every fire-and-forget side effect is meant to be contained at its source (see
|
|
34
|
-
* service/fire-and-forget.ts), so reaching here is a bug — an uncontained
|
|
35
|
-
* `void promise` somewhere. The API process (dev-server / warm Lambda container)
|
|
36
|
-
* shares one event loop across requests, so without this handler such a leak
|
|
37
|
-
* would surface as a spurious 500 on whatever request was in flight. We log it
|
|
38
|
-
* LOUDLY with a distinct alert so it is observable, and deliberately do NOT exit
|
|
39
|
-
* — a stray background rejection must never take the API down or fail a request.
|
|
40
|
-
* (The worker process intentionally has no such net: there each message is its
|
|
41
|
-
* own invocation and a rejection should crash it.)
|
|
42
|
-
*/
|
|
43
|
-
const unhandledRejectionRegistered = Symbol.for(
|
|
44
|
-
"remit.backend.unhandledRejectionNet",
|
|
45
|
-
);
|
|
46
|
-
const globalProcess = process as unknown as Record<symbol, boolean>;
|
|
47
|
-
if (!globalProcess[unhandledRejectionRegistered]) {
|
|
48
|
-
globalProcess[unhandledRejectionRegistered] = true;
|
|
49
|
-
process.on("unhandledRejection", (reason: unknown) => {
|
|
50
|
-
logger.error(
|
|
51
|
-
{
|
|
52
|
-
alert: "unhandled_rejection",
|
|
53
|
-
errorName: (reason as { name?: string })?.name,
|
|
54
|
-
errorCode:
|
|
55
|
-
(reason as { Code?: string })?.Code ??
|
|
56
|
-
(reason as { code?: string })?.code,
|
|
57
|
-
error: inspect(reason),
|
|
58
|
-
},
|
|
59
|
-
"Unhandled promise rejection leaked into the API event loop (contained, request not failed)",
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const loadOpenAPISpec = (): Document => {
|
|
65
|
-
// Bundled Lambda: infra's NodeJSArmFunction copies openapi.json into the
|
|
66
|
-
// bundle root next to the entrypoint via its `extraFiles` prop.
|
|
67
|
-
const bundledPath = join(__dirname, "openapi.json");
|
|
68
|
-
if (existsSync(bundledPath)) {
|
|
69
|
-
return JSON.parse(readFileSync(bundledPath, "utf-8"));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Dev (tsx / dev-server): read straight from the repo build tree.
|
|
73
|
-
const devPath = join(__dirname, "../../../build/remit-openapi3/openapi.json");
|
|
74
|
-
return JSON.parse(readFileSync(devPath, "utf-8"));
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const OpenAPISpec = loadOpenAPISpec();
|
|
78
|
-
|
|
79
|
-
const api = new OpenAPIBackend({
|
|
80
|
-
definition: OpenAPISpec,
|
|
81
|
-
quick: true,
|
|
82
|
-
// Coerce query/path params to their schema types. API Gateway delivers every
|
|
83
|
-
// query param as a string; handlers cast them to number/boolean (e.g. `limit`,
|
|
84
|
-
// `hasAttachment`) and forward them downstream. Without coercion a string
|
|
85
|
-
// `limit` reaches ElectroDB's `.go({ limit })` and DynamoDB rejects the
|
|
86
|
-
// numeric Limit field ("STRING_VALUE cannot be converted to Integer"), 500ing
|
|
87
|
-
// /addresses/search and /search/semantic.
|
|
88
|
-
coerceTypes: true,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
api.register("postResponseHandler", postResponseHandler);
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The validator's findings, narrowed to the three fields `RequestValidationDetail`
|
|
95
|
-
* declares. Copied field by field rather than passed through: an AJV error also
|
|
96
|
-
* carries `params`, and under `verbose` the rejected `data` itself — which for a
|
|
97
|
-
* refused request body is whatever the caller sent, a password among the
|
|
98
|
-
* possibilities. Naming the fields is what keeps that off the wire.
|
|
99
|
-
*/
|
|
100
|
-
const validationDetails = (
|
|
101
|
-
errors: OpenAPIContext["validation"]["errors"],
|
|
102
|
-
): Array<{ path: string; rule: string; message: string }> =>
|
|
103
|
-
(errors ?? []).map((error) => ({
|
|
104
|
-
path: error.instancePath,
|
|
105
|
-
rule: error.keyword,
|
|
106
|
-
message: error.message ?? "refused",
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
// The errors and the operation, and nothing that came off the wire. A request
|
|
110
|
-
// that fails validation is logged at error level, and the request it carries
|
|
111
|
-
// holds the caller's headers and cookies — an Authorization bearer among them,
|
|
112
|
-
// and for a calendar feed the token in the path is itself the credential
|
|
113
|
-
// (issue #1065). The path is redacted the same way the request scope redacts it.
|
|
114
|
-
api.register("validationFail", (c: OpenAPIContext, req: Request) => {
|
|
115
|
-
logger.error(
|
|
116
|
-
{
|
|
117
|
-
errors: c.validation.errors,
|
|
118
|
-
path: redactCalendarFeedPath(req.path),
|
|
119
|
-
operation: c.operation?.operationId,
|
|
120
|
-
},
|
|
121
|
-
"Validation failed",
|
|
122
|
-
);
|
|
123
|
-
return {
|
|
124
|
-
statusCode: 400,
|
|
125
|
-
body: {
|
|
126
|
-
code: defaultErrorCode(400),
|
|
127
|
-
message: "Invalid request",
|
|
128
|
-
errors: validationDetails(c.validation.errors),
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
api.register("notFound", () => ({
|
|
134
|
-
statusCode: 404,
|
|
135
|
-
body: { code: defaultErrorCode(404), message: "Not found" },
|
|
136
|
-
}));
|
|
137
|
-
|
|
138
|
-
api.register("notImplemented", async (c: OpenAPIContext) => {
|
|
139
|
-
const { status, mock } = api.mockResponseForOperation(
|
|
140
|
-
c.operation.operationId as string,
|
|
141
|
-
);
|
|
142
|
-
return formatResponse(mock as Record<string, unknown>, status);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
api.register(handlers);
|
|
146
|
-
|
|
147
|
-
api.init();
|
|
148
|
-
|
|
149
|
-
export { api, OpenAPISpec };
|
|
150
|
-
|
|
151
|
-
const readOriginHeader = (
|
|
152
|
-
headers: APIGatewayProxyEvent["headers"],
|
|
153
|
-
): string | undefined => {
|
|
154
|
-
if (!headers) return undefined;
|
|
155
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
156
|
-
if (key.toLowerCase() === "origin" && typeof value === "string") {
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return undefined;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// A scope, not `logger.setBindings`: this process serves requests concurrently,
|
|
164
|
-
// and bindings on the shared logger belong to whichever request wrote them last,
|
|
165
|
-
// so a line gets attributed to the wrong request. The scope follows the request
|
|
166
|
-
// through its own async continuations and nothing else. It nests inside the one
|
|
167
|
-
// `withTelemetry` opens, which is where `requestId` comes from.
|
|
168
|
-
const rawHandler = async (event: APIGatewayProxyEvent, context: Context) => {
|
|
169
|
-
// A calendar feed carries its credential in the path, so the raw path is a
|
|
170
|
-
// secret and cannot be a log field (issue #1065). Redacted once, here, where
|
|
171
|
-
// every line inside the request picks it up from the scope.
|
|
172
|
-
const path = redactCalendarFeedPath(event.path);
|
|
173
|
-
|
|
174
|
-
return withLogContext(
|
|
175
|
-
{
|
|
176
|
-
path,
|
|
177
|
-
method: event.httpMethod,
|
|
178
|
-
},
|
|
179
|
-
async () => {
|
|
180
|
-
logger.debug({ method: event.httpMethod, path }, "Request received");
|
|
181
|
-
|
|
182
|
-
const origin = readOriginHeader(event.headers);
|
|
183
|
-
const startedAt = Date.now();
|
|
184
|
-
// Capture the correlation id here: it is seeded by
|
|
185
|
-
// runWithRequestContext below (from context.awsRequestId), but that
|
|
186
|
-
// AsyncLocalStorage scope does not reliably survive the
|
|
187
|
-
// openapi-backend handleRequest call chain, so we read it from
|
|
188
|
-
// `context.awsRequestId` directly rather than via
|
|
189
|
-
// getRequestCorrelationId at log time.
|
|
190
|
-
const correlationId = context.awsRequestId;
|
|
191
|
-
|
|
192
|
-
const result = await runWithRequestContext(
|
|
193
|
-
{ origin, correlationId },
|
|
194
|
-
async () => {
|
|
195
|
-
if (usesBetterAuthJwt()) {
|
|
196
|
-
const denied = await authenticateSelfHostRequest(event);
|
|
197
|
-
if (denied) return denied;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return api
|
|
201
|
-
.handleRequest(normalizeRequest(event), event, context)
|
|
202
|
-
.catch(handleError);
|
|
203
|
-
},
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
// One structured access line per request: method, path, status, and
|
|
207
|
-
// duration. The correlation id is inlined as a top-level field so a bug
|
|
208
|
-
// report quoting the id can grep logs by it directly, and emitted at
|
|
209
|
-
// info so it is on by default in production (the debug "Request
|
|
210
|
-
// received" above already filters out under normal log levels).
|
|
211
|
-
// biome-ignore lint/plugin/no-logger-info: one access line per request is an audit-grade signal (issue #1054)
|
|
212
|
-
logger.info(
|
|
213
|
-
{
|
|
214
|
-
method: event.httpMethod,
|
|
215
|
-
path,
|
|
216
|
-
statusCode: result.statusCode,
|
|
217
|
-
durationMs: Date.now() - startedAt,
|
|
218
|
-
correlationId,
|
|
219
|
-
},
|
|
220
|
-
"Request completed",
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
return result;
|
|
224
|
-
},
|
|
225
|
-
);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
export const handler = withTelemetry(rawHandler);
|
package/src/jwt-auth.test.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, test } from "node:test";
|
|
3
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
4
|
-
import {
|
|
5
|
-
_setVerifierForTest,
|
|
6
|
-
authenticateSelfHostRequest,
|
|
7
|
-
} from "./jwt-auth.js";
|
|
8
|
-
import { runWithRequestContext } from "./request-context.js";
|
|
9
|
-
|
|
10
|
-
const buildEvent = (
|
|
11
|
-
overrides: Partial<APIGatewayProxyEvent> = {},
|
|
12
|
-
): APIGatewayProxyEvent =>
|
|
13
|
-
({
|
|
14
|
-
headers: {},
|
|
15
|
-
requestContext: {},
|
|
16
|
-
...overrides,
|
|
17
|
-
}) as APIGatewayProxyEvent;
|
|
18
|
-
|
|
19
|
-
let savedBypass: string | undefined;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
savedBypass = process.env.LOCAL_ACCOUNT_CONFIG_ID;
|
|
23
|
-
delete process.env.LOCAL_ACCOUNT_CONFIG_ID;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
_setVerifierForTest(null);
|
|
28
|
-
if (savedBypass === undefined) delete process.env.LOCAL_ACCOUNT_CONFIG_ID;
|
|
29
|
-
else process.env.LOCAL_ACCOUNT_CONFIG_ID = savedBypass;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("valid token injects verified sub into authorizer claims", async () => {
|
|
33
|
-
_setVerifierForTest(async () => ({ sub: "user-abc", email: "a@b.com" }));
|
|
34
|
-
const event = buildEvent({
|
|
35
|
-
headers: { Authorization: "Bearer good.token.here" },
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const result = await authenticateSelfHostRequest(event);
|
|
39
|
-
|
|
40
|
-
assert.equal(result, null);
|
|
41
|
-
assert.equal(event.requestContext.authorizer?.claims?.sub, "user-abc");
|
|
42
|
-
assert.equal(event.requestContext.authorizer?.claims?.email, "a@b.com");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("invalid token returns 401 with correlation id and CORS headers", async () => {
|
|
46
|
-
_setVerifierForTest(async () => {
|
|
47
|
-
throw new Error("bad signature");
|
|
48
|
-
});
|
|
49
|
-
const event = buildEvent({
|
|
50
|
-
headers: { authorization: "Bearer bad.token" },
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const result = await runWithRequestContext(
|
|
54
|
-
{ correlationId: "req-invalid-token" },
|
|
55
|
-
async () => authenticateSelfHostRequest(event),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
assert.equal(result?.statusCode, 401);
|
|
59
|
-
assert.equal(
|
|
60
|
-
result?.headers?.["x-correlation-id"],
|
|
61
|
-
"req-invalid-token",
|
|
62
|
-
"edge-auth 401 must carry the correlation id",
|
|
63
|
-
);
|
|
64
|
-
assert.equal(
|
|
65
|
-
result?.headers?.["Access-Control-Allow-Origin"],
|
|
66
|
-
"*",
|
|
67
|
-
"edge-auth 401 must carry CORS headers",
|
|
68
|
-
);
|
|
69
|
-
assert.equal(
|
|
70
|
-
result?.body,
|
|
71
|
-
JSON.stringify({
|
|
72
|
-
code: "unauthorized",
|
|
73
|
-
message: "Invalid or expired token",
|
|
74
|
-
}),
|
|
75
|
-
);
|
|
76
|
-
assert.equal(event.requestContext.authorizer, undefined);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("no token with a local bypass configured is allowed", async () => {
|
|
80
|
-
process.env.LOCAL_ACCOUNT_CONFIG_ID = "some-config-id";
|
|
81
|
-
const event = buildEvent();
|
|
82
|
-
|
|
83
|
-
const result = await authenticateSelfHostRequest(event);
|
|
84
|
-
|
|
85
|
-
assert.equal(result, null);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("no token and no bypass returns 401 with correlation id", async () => {
|
|
89
|
-
const event = buildEvent();
|
|
90
|
-
|
|
91
|
-
const result = await runWithRequestContext(
|
|
92
|
-
{ correlationId: "req-no-token" },
|
|
93
|
-
async () => authenticateSelfHostRequest(event),
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
assert.equal(result?.statusCode, 401);
|
|
97
|
-
assert.equal(
|
|
98
|
-
result?.headers?.["x-correlation-id"],
|
|
99
|
-
"req-no-token",
|
|
100
|
-
"edge-auth 401 must carry the correlation id",
|
|
101
|
-
);
|
|
102
|
-
assert.equal(
|
|
103
|
-
result?.headers?.["Access-Control-Allow-Origin"],
|
|
104
|
-
"*",
|
|
105
|
-
"edge-auth 401 must carry CORS headers",
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("a tokenless GET to the Microsoft OAuth callback is admitted", async () => {
|
|
110
|
-
const event = buildEvent({
|
|
111
|
-
httpMethod: "GET",
|
|
112
|
-
path: "/accounts/oauth/microsoft/callback",
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const result = await authenticateSelfHostRequest(event);
|
|
116
|
-
|
|
117
|
-
assert.equal(result, null);
|
|
118
|
-
assert.equal(event.requestContext.authorizer, undefined);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("the callback exemption covers that path and method only", async () => {
|
|
122
|
-
const neighbour = buildEvent({
|
|
123
|
-
httpMethod: "GET",
|
|
124
|
-
path: "/accounts/oauth/microsoft/start",
|
|
125
|
-
});
|
|
126
|
-
const otherMethod = buildEvent({
|
|
127
|
-
httpMethod: "POST",
|
|
128
|
-
path: "/accounts/oauth/microsoft/callback",
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
assert.equal((await authenticateSelfHostRequest(neighbour))?.statusCode, 401);
|
|
132
|
-
assert.equal(
|
|
133
|
-
(await authenticateSelfHostRequest(otherMethod))?.statusCode,
|
|
134
|
-
401,
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("pre-injected claims (edge tier) short-circuit verification", async () => {
|
|
139
|
-
_setVerifierForTest(async () => {
|
|
140
|
-
throw new Error("verifier must not be called");
|
|
141
|
-
});
|
|
142
|
-
const event = buildEvent({
|
|
143
|
-
requestContext: {
|
|
144
|
-
authorizer: { claims: { sub: "edge-user" } },
|
|
145
|
-
} as unknown as APIGatewayProxyEvent["requestContext"],
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const result = await authenticateSelfHostRequest(event);
|
|
149
|
-
|
|
150
|
-
assert.equal(result, null);
|
|
151
|
-
assert.equal(event.requestContext.authorizer?.claims?.sub, "edge-user");
|
|
152
|
-
});
|
package/src/jwt-auth.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createJwtVerifier,
|
|
3
|
-
extractBearerToken,
|
|
4
|
-
type JwtVerifier,
|
|
5
|
-
resolveVerifierConfig,
|
|
6
|
-
} from "@remit/auth-service/verifier";
|
|
7
|
-
import { readCalendarFeedToken } from "@remit/calendar-service";
|
|
8
|
-
import { logger } from "@remit/logger-lambda";
|
|
9
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
10
|
-
import { defaultErrorCode } from "./error.js";
|
|
11
|
-
import { formatResponse } from "./response.js";
|
|
12
|
-
|
|
13
|
-
let verifier: JwtVerifier | null = null;
|
|
14
|
-
|
|
15
|
-
const getVerifier = (): JwtVerifier => {
|
|
16
|
-
if (!verifier) verifier = createJwtVerifier(resolveVerifierConfig());
|
|
17
|
-
return verifier;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/** Test-only override for the memoized verifier. Pass null to reset. */
|
|
21
|
-
export const _setVerifierForTest = (v: JwtVerifier | null): void => {
|
|
22
|
-
verifier = v;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const readHeader = (
|
|
26
|
-
headers: APIGatewayProxyEvent["headers"],
|
|
27
|
-
name: string,
|
|
28
|
-
): string | undefined => {
|
|
29
|
-
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
30
|
-
if (key.toLowerCase() === name && typeof value === "string") return value;
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const unauthorized = (message: string): APIGatewayProxyResult =>
|
|
36
|
-
formatResponse({ code: defaultErrorCode(401), message }, 401);
|
|
37
|
-
|
|
38
|
-
const injectClaims = (
|
|
39
|
-
event: APIGatewayProxyEvent,
|
|
40
|
-
sub: string,
|
|
41
|
-
email: string | undefined,
|
|
42
|
-
): void => {
|
|
43
|
-
event.requestContext = {
|
|
44
|
-
...event.requestContext,
|
|
45
|
-
authorizer: {
|
|
46
|
-
...event.requestContext?.authorizer,
|
|
47
|
-
claims: {
|
|
48
|
-
sub,
|
|
49
|
-
email: email ?? "",
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const hasLocalBypass = (): boolean =>
|
|
56
|
-
Boolean(process.env.LOCAL_ACCOUNT_CONFIG_ID);
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The routes that run without a token.
|
|
60
|
-
*
|
|
61
|
-
* Microsoft redirects the browser here after consent, so the request carries no
|
|
62
|
-
* Authorization header and no session to derive one from. The handler takes its
|
|
63
|
-
* identity from the HMAC-signed `state` parameter and validates it there; that
|
|
64
|
-
* signature is the gate, not a JWT.
|
|
65
|
-
*
|
|
66
|
-
* A calendar feed carries its credential in the path for the same reason: a
|
|
67
|
-
* subscribed client polls a URL and has no session to present, so the token is
|
|
68
|
-
* the gate and the handler compares it against a stored hash.
|
|
69
|
-
*
|
|
70
|
-
* Hardcoded, matching the edge exemption in packages/apisix/src/route-table.ts.
|
|
71
|
-
* `@useAuth(NoAuth)` in the spec does not open a path here — a future public
|
|
72
|
-
* route is added by hand.
|
|
73
|
-
*/
|
|
74
|
-
const PUBLIC_ROUTES: readonly {
|
|
75
|
-
method: string;
|
|
76
|
-
matches: (path: string) => boolean;
|
|
77
|
-
}[] = [
|
|
78
|
-
{
|
|
79
|
-
method: "GET",
|
|
80
|
-
matches: (path) => path === "/accounts/oauth/microsoft/callback",
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
method: "GET",
|
|
84
|
-
matches: (path) => readCalendarFeedToken(path) !== null,
|
|
85
|
-
},
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
const isPublicRoute = (event: APIGatewayProxyEvent): boolean =>
|
|
89
|
-
PUBLIC_ROUTES.some(
|
|
90
|
-
(route) => route.method === event.httpMethod && route.matches(event.path),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Authenticate a self-host request from a better-auth RS256 JWT.
|
|
95
|
-
*
|
|
96
|
-
* On a valid token the verified `sub` is injected into the event's authorizer
|
|
97
|
-
* claims, exactly where the Cognito authorizer puts it, so every downstream
|
|
98
|
-
* handler reads identity through the unchanged `auth.ts` path. Returns a 401
|
|
99
|
-
* response (not null) when a token is present but fails verification, or when no
|
|
100
|
-
* token is supplied and no local test bypass is configured. Returns null to let
|
|
101
|
-
* the request proceed.
|
|
102
|
-
*
|
|
103
|
-
* When the edge tier (APISIX) has already verified and injected claims, this is
|
|
104
|
-
* a no-op — the backend trusts pre-populated claims and re-verifies otherwise.
|
|
105
|
-
*/
|
|
106
|
-
export const authenticateSelfHostRequest = async (
|
|
107
|
-
event: APIGatewayProxyEvent,
|
|
108
|
-
): Promise<APIGatewayProxyResult | null> => {
|
|
109
|
-
if (isPublicRoute(event)) return null;
|
|
110
|
-
|
|
111
|
-
const existingSub = event.requestContext?.authorizer?.claims?.sub;
|
|
112
|
-
if (typeof existingSub === "string" && existingSub.length > 0) return null;
|
|
113
|
-
|
|
114
|
-
const token = extractBearerToken(readHeader(event.headers, "authorization"));
|
|
115
|
-
|
|
116
|
-
if (token) {
|
|
117
|
-
const claims = await getVerifier()(token).catch((err: unknown) => {
|
|
118
|
-
logger.warn(
|
|
119
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
120
|
-
"better-auth JWT verification failed",
|
|
121
|
-
);
|
|
122
|
-
return null;
|
|
123
|
-
});
|
|
124
|
-
if (!claims) return unauthorized("Invalid or expired token");
|
|
125
|
-
injectClaims(event, claims.sub, claims.email);
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (hasLocalBypass()) return null;
|
|
130
|
-
|
|
131
|
-
return unauthorized("Authentication required");
|
|
132
|
-
};
|