@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an Address row to the wire-format `VipSuggestionEntry` returned by
|
|
3
|
+
* `GET /me/vip-suggestions` (issue #234). Absent counters are normalised to
|
|
4
|
+
* zero so the UI can render `12 received · 4 sent · 2 replies` without a
|
|
5
|
+
* `?? 0` fallback at every render site.
|
|
6
|
+
*/
|
|
7
|
+
export const toVipSuggestionEntry = (item) => ({
|
|
8
|
+
addressId: item.addressId,
|
|
9
|
+
displayName: item.displayName,
|
|
10
|
+
normalizedEmail: item.normalizedEmail,
|
|
11
|
+
inboundCount: item.inboundCount ?? 0,
|
|
12
|
+
outboundCount: item.outboundCount ?? 0,
|
|
13
|
+
replyCount: item.replyCount ?? 0,
|
|
14
|
+
lastInboundAt: item.lastInboundAt,
|
|
15
|
+
lastReplyAt: item.lastReplyAt,
|
|
16
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
import { logger } from "@remit/logger";
|
|
3
|
+
import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
|
|
4
|
+
import { handleApiEvent } from "./api.js";
|
|
5
|
+
installNodeLogger();
|
|
6
|
+
export { api, handleApiEvent, OpenAPISpec } from "./api.js";
|
|
7
|
+
/**
|
|
8
|
+
* Last-resort net for a leaked background rejection.
|
|
9
|
+
*
|
|
10
|
+
* Every fire-and-forget side effect is meant to be contained at its source (see
|
|
11
|
+
* service/fire-and-forget.ts), so reaching here is a bug — an uncontained
|
|
12
|
+
* `void promise` somewhere. The API process (dev-server / warm Lambda container)
|
|
13
|
+
* shares one event loop across requests, so without this handler such a leak
|
|
14
|
+
* would surface as a spurious 500 on whatever request was in flight. We log it
|
|
15
|
+
* LOUDLY with a distinct alert so it is observable, and deliberately do NOT exit
|
|
16
|
+
* — a stray background rejection must never take the API down or fail a request.
|
|
17
|
+
* (The worker process intentionally has no such net: there each message is its
|
|
18
|
+
* own invocation and a rejection should crash it.)
|
|
19
|
+
*/
|
|
20
|
+
const unhandledRejectionRegistered = Symbol.for("remit.backend.unhandledRejectionNet");
|
|
21
|
+
const globalProcess = process;
|
|
22
|
+
if (!globalProcess[unhandledRejectionRegistered]) {
|
|
23
|
+
globalProcess[unhandledRejectionRegistered] = true;
|
|
24
|
+
process.on("unhandledRejection", (reason) => {
|
|
25
|
+
logger.error({
|
|
26
|
+
alert: "unhandled_rejection",
|
|
27
|
+
errorName: reason?.name,
|
|
28
|
+
errorCode: reason?.Code ??
|
|
29
|
+
reason?.code,
|
|
30
|
+
error: inspect(reason),
|
|
31
|
+
}, "Unhandled promise rejection leaked into the API event loop (contained, request not failed)");
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// The Lambda and Node entry point. `withTelemetry` installs the Node logger and
|
|
35
|
+
// its process metrics; a host without Node's process APIs imports
|
|
36
|
+
// `@remit/backend/api` instead and plugs in its own logger with `setLogger`.
|
|
37
|
+
export const handler = withTelemetry(handleApiEvent);
|
|
@@ -5,7 +5,4 @@
|
|
|
5
5
|
* absence (e.g. a third-party token, a dev-server response body) — never to
|
|
6
6
|
* soften an internal contract, which must stay a fatal uncaught `JSON.parse`.
|
|
7
7
|
*/
|
|
8
|
-
export const safeJsonParse
|
|
9
|
-
new Promise((resolve) => {
|
|
10
|
-
resolve(JSON.parse(raw));
|
|
11
|
-
});
|
|
8
|
+
export declare const safeJsonParse: <T>(raw: string) => Promise<T>;
|
package/dist/src/json.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse JSON in a Promise so a malformed string surfaces as a rejection the
|
|
3
|
+
* caller can `.catch()`, instead of a synchronous throw that would need a block
|
|
4
|
+
* try/catch. Use only where an unparseable value is an expected, recoverable
|
|
5
|
+
* absence (e.g. a third-party token, a dev-server response body) — never to
|
|
6
|
+
* soften an internal contract, which must stay a fatal uncaught `JSON.parse`.
|
|
7
|
+
*/
|
|
8
|
+
export const safeJsonParse = (raw) => new Promise((resolve) => {
|
|
9
|
+
resolve(JSON.parse(raw));
|
|
10
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type JwtVerifier } from "@remit/auth-service/verifier";
|
|
2
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
3
|
+
/** Test-only override for the memoized verifier. Pass null to reset. */
|
|
4
|
+
export declare const _setVerifierForTest: (v: JwtVerifier | null) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Authenticate a self-host request from a better-auth RS256 JWT.
|
|
7
|
+
*
|
|
8
|
+
* On a valid token the verified `sub` is injected into the event's authorizer
|
|
9
|
+
* claims, exactly where the Cognito authorizer puts it, so every downstream
|
|
10
|
+
* handler reads identity through the unchanged `auth.ts` path. Returns a 401
|
|
11
|
+
* response (not null) when a token is present but fails verification, or when no
|
|
12
|
+
* token is supplied and no local test bypass is configured. Returns null to let
|
|
13
|
+
* the request proceed.
|
|
14
|
+
*
|
|
15
|
+
* When the edge tier (APISIX) has already verified and injected claims, this is
|
|
16
|
+
* a no-op — the backend trusts pre-populated claims and re-verifies otherwise.
|
|
17
|
+
*/
|
|
18
|
+
export declare const authenticateSelfHostRequest: (event: APIGatewayProxyEvent) => Promise<APIGatewayProxyResult | null>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createJwtVerifier, extractBearerToken, resolveVerifierConfig, } from "@remit/auth-service/verifier";
|
|
2
|
+
import { readCalendarFeedToken } from "@remit/calendar-service";
|
|
3
|
+
import { logger } from "@remit/logger";
|
|
4
|
+
import { defaultErrorCode } from "./error.js";
|
|
5
|
+
import { formatResponse } from "./response.js";
|
|
6
|
+
let verifier = null;
|
|
7
|
+
const getVerifier = () => {
|
|
8
|
+
if (!verifier)
|
|
9
|
+
verifier = createJwtVerifier(resolveVerifierConfig());
|
|
10
|
+
return verifier;
|
|
11
|
+
};
|
|
12
|
+
/** Test-only override for the memoized verifier. Pass null to reset. */
|
|
13
|
+
export const _setVerifierForTest = (v) => {
|
|
14
|
+
verifier = v;
|
|
15
|
+
};
|
|
16
|
+
const readHeader = (headers, name) => {
|
|
17
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
18
|
+
if (key.toLowerCase() === name && typeof value === "string")
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
};
|
|
23
|
+
const unauthorized = (message) => formatResponse({ code: defaultErrorCode(401), message }, 401);
|
|
24
|
+
const injectClaims = (event, sub, email) => {
|
|
25
|
+
event.requestContext = {
|
|
26
|
+
...event.requestContext,
|
|
27
|
+
authorizer: {
|
|
28
|
+
...event.requestContext?.authorizer,
|
|
29
|
+
claims: {
|
|
30
|
+
sub,
|
|
31
|
+
email: email ?? "",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const hasLocalBypass = () => Boolean(process.env.LOCAL_ACCOUNT_CONFIG_ID);
|
|
37
|
+
/**
|
|
38
|
+
* The routes that run without a token.
|
|
39
|
+
*
|
|
40
|
+
* Microsoft redirects the browser here after consent, so the request carries no
|
|
41
|
+
* Authorization header and no session to derive one from. The handler takes its
|
|
42
|
+
* identity from the HMAC-signed `state` parameter and validates it there; that
|
|
43
|
+
* signature is the gate, not a JWT.
|
|
44
|
+
*
|
|
45
|
+
* A calendar feed carries its credential in the path for the same reason: a
|
|
46
|
+
* subscribed client polls a URL and has no session to present, so the token is
|
|
47
|
+
* the gate and the handler compares it against a stored hash.
|
|
48
|
+
*
|
|
49
|
+
* Hardcoded, matching the edge exemption in packages/apisix/src/route-table.ts.
|
|
50
|
+
* `@useAuth(NoAuth)` in the spec does not open a path here — a future public
|
|
51
|
+
* route is added by hand.
|
|
52
|
+
*/
|
|
53
|
+
const PUBLIC_ROUTES = [
|
|
54
|
+
{
|
|
55
|
+
method: "GET",
|
|
56
|
+
matches: (path) => path === "/accounts/oauth/microsoft/callback",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
method: "GET",
|
|
60
|
+
matches: (path) => readCalendarFeedToken(path) !== null,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
const isPublicRoute = (event) => PUBLIC_ROUTES.some((route) => route.method === event.httpMethod && route.matches(event.path));
|
|
64
|
+
/**
|
|
65
|
+
* Authenticate a self-host request from a better-auth RS256 JWT.
|
|
66
|
+
*
|
|
67
|
+
* On a valid token the verified `sub` is injected into the event's authorizer
|
|
68
|
+
* claims, exactly where the Cognito authorizer puts it, so every downstream
|
|
69
|
+
* handler reads identity through the unchanged `auth.ts` path. Returns a 401
|
|
70
|
+
* response (not null) when a token is present but fails verification, or when no
|
|
71
|
+
* token is supplied and no local test bypass is configured. Returns null to let
|
|
72
|
+
* the request proceed.
|
|
73
|
+
*
|
|
74
|
+
* When the edge tier (APISIX) has already verified and injected claims, this is
|
|
75
|
+
* a no-op — the backend trusts pre-populated claims and re-verifies otherwise.
|
|
76
|
+
*/
|
|
77
|
+
export const authenticateSelfHostRequest = async (event) => {
|
|
78
|
+
if (isPublicRoute(event))
|
|
79
|
+
return null;
|
|
80
|
+
const existingSub = event.requestContext?.authorizer?.claims?.sub;
|
|
81
|
+
if (typeof existingSub === "string" && existingSub.length > 0)
|
|
82
|
+
return null;
|
|
83
|
+
const token = extractBearerToken(readHeader(event.headers, "authorization"));
|
|
84
|
+
if (token) {
|
|
85
|
+
const claims = await getVerifier()(token).catch((err) => {
|
|
86
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, "better-auth JWT verification failed");
|
|
87
|
+
return null;
|
|
88
|
+
});
|
|
89
|
+
if (!claims)
|
|
90
|
+
return unauthorized("Invalid or expired token");
|
|
91
|
+
injectClaims(event, claims.sub, claims.email);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (hasLocalBypass())
|
|
95
|
+
return null;
|
|
96
|
+
return unauthorized("Authentication required");
|
|
97
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface RequestContext {
|
|
2
|
+
origin?: string;
|
|
3
|
+
correlationId?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const runWithRequestContext: <T>(ctx: RequestContext, fn: () => T) => T;
|
|
6
|
+
export declare const getRequestOrigin: () => string | undefined;
|
|
7
|
+
export declare const getRequestCorrelationId: () => string | undefined;
|
|
8
|
+
export declare const resolveAllowedOrigin: (origin: string | undefined) => string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const storage = new AsyncLocalStorage();
|
|
3
|
+
export const runWithRequestContext = (ctx, fn) => storage.run(ctx, fn);
|
|
4
|
+
export const getRequestOrigin = () => storage.getStore()?.origin;
|
|
5
|
+
export const getRequestCorrelationId = () => storage.getStore()?.correlationId;
|
|
6
|
+
const parseAllowedOrigins = () => {
|
|
7
|
+
const raw = process.env.CORS_ALLOWED_ORIGINS;
|
|
8
|
+
if (!raw)
|
|
9
|
+
return [];
|
|
10
|
+
return raw
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((s) => s.trim())
|
|
13
|
+
.filter((s) => s.length > 0);
|
|
14
|
+
};
|
|
15
|
+
export const resolveAllowedOrigin = (origin) => {
|
|
16
|
+
const allowed = parseAllowedOrigins();
|
|
17
|
+
if (allowed.length === 0)
|
|
18
|
+
return "*";
|
|
19
|
+
if (origin && allowed.includes(origin))
|
|
20
|
+
return origin;
|
|
21
|
+
return allowed[0];
|
|
22
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { APIGatewayProxyResult } from "aws-lambda";
|
|
2
|
+
import type { Context as OpenAPIContext } from "openapi-backend";
|
|
3
|
+
declare const RAW_API_RESPONSE: unique symbol;
|
|
4
|
+
/**
|
|
5
|
+
* A response the caller reads by something other than its JSON body — an
|
|
6
|
+
* iCalendar feed, an OAuth redirect's Location, and whatever later joins them.
|
|
7
|
+
*
|
|
8
|
+
* Every other handler returns a plain object and lets this module decide the
|
|
9
|
+
* status, the content type and the serialization. That is the right default and
|
|
10
|
+
* stays the default; a handler that has to own its own media type or headers
|
|
11
|
+
* says so with `rawApiResponse` rather than by returning a shape this one has
|
|
12
|
+
* to guess at.
|
|
13
|
+
* The CORS and correlation headers are still added here, so a raw response is
|
|
14
|
+
* not a way around them.
|
|
15
|
+
*/
|
|
16
|
+
export interface RawApiResponse {
|
|
17
|
+
statusCode: number;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
body: string;
|
|
20
|
+
}
|
|
21
|
+
type MarkedRawApiResponse = RawApiResponse & {
|
|
22
|
+
readonly [RAW_API_RESPONSE]: true;
|
|
23
|
+
};
|
|
24
|
+
export declare const rawApiResponse: (response: RawApiResponse) => MarkedRawApiResponse;
|
|
25
|
+
export declare const formatResponse: (body: Record<string, unknown>, statusCode?: number) => APIGatewayProxyResult;
|
|
26
|
+
export declare const postResponseHandler: (context: OpenAPIContext) => APIGatewayProxyResult;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { logger } from "@remit/logger";
|
|
2
|
+
import { getRequestCorrelationId, getRequestOrigin, resolveAllowedOrigin, } from "./request-context.js";
|
|
3
|
+
const RAW_API_RESPONSE = Symbol("remit.rawApiResponse");
|
|
4
|
+
export const rawApiResponse = (response) => Object.assign(response, { [RAW_API_RESPONSE]: true });
|
|
5
|
+
const isRawApiResponse = (body) => RAW_API_RESPONSE in body;
|
|
6
|
+
/**
|
|
7
|
+
* The headers every response carries whatever its body is: the CORS grant the
|
|
8
|
+
* browser needs, and the id the request's log lines are already tagged with.
|
|
9
|
+
* Returning that id is what makes a bug report's "correlation id" resolve to a
|
|
10
|
+
* server-side line; the browser can only read a non-safelisted header when it
|
|
11
|
+
* is exposed.
|
|
12
|
+
*/
|
|
13
|
+
const envelopeHeaders = () => {
|
|
14
|
+
const allowOrigin = resolveAllowedOrigin(getRequestOrigin());
|
|
15
|
+
const headers = {
|
|
16
|
+
"Access-Control-Allow-Origin": allowOrigin,
|
|
17
|
+
"Access-Control-Allow-Headers": "Authorization,Content-Type",
|
|
18
|
+
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
19
|
+
Vary: "Origin",
|
|
20
|
+
};
|
|
21
|
+
if (allowOrigin !== "*") {
|
|
22
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
23
|
+
}
|
|
24
|
+
const correlationId = getRequestCorrelationId();
|
|
25
|
+
if (correlationId) {
|
|
26
|
+
headers["x-correlation-id"] = correlationId;
|
|
27
|
+
headers["Access-Control-Expose-Headers"] = "x-correlation-id";
|
|
28
|
+
}
|
|
29
|
+
return headers;
|
|
30
|
+
};
|
|
31
|
+
export const formatResponse = (body, statusCode = 200) => {
|
|
32
|
+
if (body.statusCode && typeof body.statusCode === "number") {
|
|
33
|
+
statusCode = body.statusCode;
|
|
34
|
+
}
|
|
35
|
+
if (isRawApiResponse(body)) {
|
|
36
|
+
logger.debug({ statusCode: body.statusCode }, "response");
|
|
37
|
+
return {
|
|
38
|
+
statusCode: body.statusCode,
|
|
39
|
+
headers: { ...envelopeHeaders(), ...body.headers },
|
|
40
|
+
body: body.body,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if ("body" in body && body.body && typeof body.body === "object") {
|
|
44
|
+
body = body.body;
|
|
45
|
+
}
|
|
46
|
+
logger.debug({ statusCode }, "response");
|
|
47
|
+
return {
|
|
48
|
+
statusCode: statusCode,
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
...envelopeHeaders(),
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export const postResponseHandler = (context) => {
|
|
57
|
+
const { api, response, operation } = context;
|
|
58
|
+
if (response.statusCode)
|
|
59
|
+
return formatResponse(response, response.statusCode);
|
|
60
|
+
if (process.env.STAGE_NAME !== "dev")
|
|
61
|
+
return formatResponse(response, 200);
|
|
62
|
+
const { valid, errors } = api.validateResponse(response, operation);
|
|
63
|
+
if (valid)
|
|
64
|
+
return formatResponse(response, 200);
|
|
65
|
+
const { operationId } = operation;
|
|
66
|
+
const errorDetails = {
|
|
67
|
+
errors,
|
|
68
|
+
operationId,
|
|
69
|
+
responseKeys: Object.keys(response),
|
|
70
|
+
};
|
|
71
|
+
errors?.forEach((error) => {
|
|
72
|
+
if (error.instancePath) {
|
|
73
|
+
const pathParts = error.instancePath.split("/").filter(Boolean);
|
|
74
|
+
let value = response;
|
|
75
|
+
for (const part of pathParts) {
|
|
76
|
+
value = value?.[part];
|
|
77
|
+
}
|
|
78
|
+
errorDetails[`problematicValue_${error.instancePath}`] = {
|
|
79
|
+
path: error.instancePath,
|
|
80
|
+
value: JSON.stringify(value).substring(0, 500),
|
|
81
|
+
error: error.message,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
logger.error(errorDetails, "Response validation failed");
|
|
86
|
+
return formatResponse(response, 200);
|
|
87
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
import type { AccountItem, IAccountRepository, IConfigImportRepository } from "@remit/data-ports";
|
|
3
|
+
import type { Logger } from "@remit/logger";
|
|
4
|
+
export interface AccountDeletionDeps {
|
|
5
|
+
account: Pick<IAccountRepository, "update">;
|
|
6
|
+
sqsClient: SQSClient;
|
|
7
|
+
accountFanoutQueueUrl: string;
|
|
8
|
+
log: Logger;
|
|
9
|
+
}
|
|
10
|
+
export declare const deleteAccount: (deps: AccountDeletionDeps, account: Pick<AccountItem, "accountId" | "accountConfigId">, deletedAt: number) => Promise<void>;
|
|
11
|
+
export interface HalfDeletedAccountDeps extends AccountDeletionDeps {
|
|
12
|
+
account: Pick<IAccountRepository, "update" | "listAll">;
|
|
13
|
+
configImport: Pick<IConfigImportRepository, "listByAccountConfig">;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Before #1384 a delete stored `isActive=false` and dropped `deletedAt`. Of the
|
|
17
|
+
* writers in this repo only that delete and a config import leave an account
|
|
18
|
+
* inactive, and an import names each account it creates in its stored
|
|
19
|
+
* document. The public update endpoint accepts `isActive` as sent, so a caller
|
|
20
|
+
* outside the repo can also leave one inactive.
|
|
21
|
+
*/
|
|
22
|
+
export declare const finishHalfDeletedAccounts: (deps: HalfDeletedAccountDeps) => Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { readConfigDocument } from "@remit/config-format";
|
|
3
|
+
export const deleteAccount = async (deps, account, deletedAt) => {
|
|
4
|
+
const { accountId, accountConfigId } = account;
|
|
5
|
+
// Purge first: an account whose request never left still reads as not deleted, so it is found and asked for again.
|
|
6
|
+
await deps.sqsClient.send(new SendMessageCommand({
|
|
7
|
+
QueueUrl: deps.accountFanoutQueueUrl,
|
|
8
|
+
MessageBody: JSON.stringify({
|
|
9
|
+
type: "AccountDataPurge",
|
|
10
|
+
accountId,
|
|
11
|
+
accountConfigId,
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
await deps.account.update(accountId, { deletedAt, isActive: false });
|
|
15
|
+
deps.log.info({ accountId, accountConfigId }, "Account data purge initiated");
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Before #1384 a delete stored `isActive=false` and dropped `deletedAt`. Of the
|
|
19
|
+
* writers in this repo only that delete and a config import leave an account
|
|
20
|
+
* inactive, and an import names each account it creates in its stored
|
|
21
|
+
* document. The public update endpoint accepts `isActive` as sent, so a caller
|
|
22
|
+
* outside the repo can also leave one inactive.
|
|
23
|
+
*/
|
|
24
|
+
export const finishHalfDeletedAccounts = async (deps) => {
|
|
25
|
+
const imported = new Map();
|
|
26
|
+
for (const account of await deps.account.listAll()) {
|
|
27
|
+
if (account.isActive || account.deletedAt !== undefined)
|
|
28
|
+
continue;
|
|
29
|
+
let names = imported.get(account.accountConfigId);
|
|
30
|
+
if (!names) {
|
|
31
|
+
const imports = await deps.configImport.listByAccountConfig(account.accountConfigId);
|
|
32
|
+
names = new Set(imports.flatMap((row) => readConfigDocument(row.document).accounts.map((a) => a.accountId)));
|
|
33
|
+
imported.set(account.accountConfigId, names);
|
|
34
|
+
}
|
|
35
|
+
if (names.has(account.accountId))
|
|
36
|
+
continue;
|
|
37
|
+
await deleteAccount(deps, account, Date.now());
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createSqliteRepositories, openSqliteDatabases, sqliteDatabasePathsFromEnv, } from "@remit/drizzle-service";
|
|
2
|
+
import { buildSharedDeps, createRemitClient, } from "./create-remit-client.js";
|
|
3
|
+
// The SQLite adapter composition (RFC 036). Every repo runs on the database
|
|
4
|
+
// that holds its tables: app, tenant mail or tenant search (#1420).
|
|
5
|
+
export const buildSqliteClient = async () => {
|
|
6
|
+
const databases = await openSqliteDatabases(sqliteDatabasePathsFromEnv());
|
|
7
|
+
return createRemitClient({
|
|
8
|
+
repositories: createSqliteRepositories(databases),
|
|
9
|
+
...buildSharedDeps(),
|
|
10
|
+
});
|
|
11
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccountRepository, IAccountSettingRepository, IAddressRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarSuggestionRepository, IConfigImportRepository, IEnvelopeRepository, IFilterRepository, ILabelRepository, IMailboxLockRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageLabelRepository, IMessagePlacementMoveRepository, IMessageRepository, IOrganizeJobRequestRepository, IOutboxAttachmentRepository, IOutboxMessageRepository, IQuarantineRepository, ISenderSignerStandingRepository, ISyncWarningRepository, IThreadMessageRepository } from "@remit/data-ports";
|
|
2
|
+
import { BodySyncQueueService, BodySyncService, type FilterConfig, FlagQueueService, type IImapConnection, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, SpamReportService } from "@remit/mailbox-service";
|
|
3
|
+
import { type SecretsService } from "@remit/secrets-service";
|
|
4
|
+
import type { StorageService } from "@remit/storage-service";
|
|
5
|
+
export interface ConnectionScope {
|
|
6
|
+
getConnection: () => Promise<IImapConnection>;
|
|
7
|
+
disconnect: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface RemitClient {
|
|
10
|
+
accountConfig: IAccountConfigRepository;
|
|
11
|
+
account: IAccountRepository;
|
|
12
|
+
accountSetting: IAccountSettingRepository;
|
|
13
|
+
address: IAddressRepository;
|
|
14
|
+
mailbox: IMailboxRepository;
|
|
15
|
+
mailboxSpecialUse: IMailboxSpecialUseRepository;
|
|
16
|
+
mailboxLock: IMailboxLockRepository;
|
|
17
|
+
message: IMessageRepository;
|
|
18
|
+
messageFlag: IMessageFlagRepository;
|
|
19
|
+
outboxMessage: IOutboxMessageRepository;
|
|
20
|
+
threadMessage: IThreadMessageRepository;
|
|
21
|
+
envelope: IEnvelopeRepository;
|
|
22
|
+
accountExportRequest: IAccountExportRequestRepository;
|
|
23
|
+
configImport: IConfigImportRepository;
|
|
24
|
+
quarantine: IQuarantineRepository;
|
|
25
|
+
syncWarning: ISyncWarningRepository;
|
|
26
|
+
organizeJobRequest: IOrganizeJobRequestRepository;
|
|
27
|
+
filter: IFilterRepository;
|
|
28
|
+
label: ILabelRepository;
|
|
29
|
+
messageLabel: IMessageLabelRepository;
|
|
30
|
+
senderSignerStanding: ISenderSignerStandingRepository;
|
|
31
|
+
calendarCollection: ICalendarCollectionRepository;
|
|
32
|
+
calendarObject: ICalendarObjectRepository;
|
|
33
|
+
calendarEventIndex: ICalendarEventIndexRepository;
|
|
34
|
+
calendarFeedToken: ICalendarFeedTokenRepository;
|
|
35
|
+
calendarSuggestion: ICalendarSuggestionRepository;
|
|
36
|
+
calendarStore: ICalendarStore;
|
|
37
|
+
storage: StorageService;
|
|
38
|
+
secrets: SecretsService;
|
|
39
|
+
bodySync: BodySyncService;
|
|
40
|
+
bodySyncQueue?: BodySyncQueueService;
|
|
41
|
+
placementMove: IMessagePlacementMoveRepository;
|
|
42
|
+
flagPush: IMessageFlagPushRepository;
|
|
43
|
+
flagQueue: FlagQueueService;
|
|
44
|
+
mailboxQueue: MailboxQueueService;
|
|
45
|
+
messageMove: MessageMoveService;
|
|
46
|
+
outboxAttachment: OutboxAttachmentService;
|
|
47
|
+
outboxQueue: OutboxQueueService;
|
|
48
|
+
spamReport: SpamReportService;
|
|
49
|
+
createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
|
|
50
|
+
}
|
|
51
|
+
export interface RemitClientRepositories {
|
|
52
|
+
accountConfig: IAccountConfigRepository;
|
|
53
|
+
account: IAccountRepository;
|
|
54
|
+
accountSetting: IAccountSettingRepository;
|
|
55
|
+
address: IAddressRepository;
|
|
56
|
+
mailbox: IMailboxRepository;
|
|
57
|
+
mailboxSpecialUse: IMailboxSpecialUseRepository;
|
|
58
|
+
mailboxLock: IMailboxLockRepository;
|
|
59
|
+
message: IMessageRepository;
|
|
60
|
+
messageFlag: IMessageFlagRepository;
|
|
61
|
+
outboxMessage: IOutboxMessageRepository;
|
|
62
|
+
outboxAttachment: IOutboxAttachmentRepository;
|
|
63
|
+
threadMessage: IThreadMessageRepository;
|
|
64
|
+
envelope: IEnvelopeRepository;
|
|
65
|
+
accountExportRequest: IAccountExportRequestRepository;
|
|
66
|
+
configImport: IConfigImportRepository;
|
|
67
|
+
quarantine: IQuarantineRepository;
|
|
68
|
+
syncWarning: ISyncWarningRepository;
|
|
69
|
+
organizeJobRequest: IOrganizeJobRequestRepository;
|
|
70
|
+
placementMove: IMessagePlacementMoveRepository;
|
|
71
|
+
flagPush: IMessageFlagPushRepository;
|
|
72
|
+
filter: IFilterRepository;
|
|
73
|
+
label: ILabelRepository;
|
|
74
|
+
messageLabel: IMessageLabelRepository;
|
|
75
|
+
senderSignerStanding: ISenderSignerStandingRepository;
|
|
76
|
+
calendarCollection: ICalendarCollectionRepository;
|
|
77
|
+
calendarObject: ICalendarObjectRepository;
|
|
78
|
+
calendarEventIndex: ICalendarEventIndexRepository;
|
|
79
|
+
calendarFeedToken: ICalendarFeedTokenRepository;
|
|
80
|
+
calendarSuggestion: ICalendarSuggestionRepository;
|
|
81
|
+
calendarStore: ICalendarStore;
|
|
82
|
+
}
|
|
83
|
+
export interface RemitClientSharedDeps {
|
|
84
|
+
storage: StorageService;
|
|
85
|
+
secrets: SecretsService;
|
|
86
|
+
sqsQueueUrl: string;
|
|
87
|
+
sqsSmtpQueueUrl: string;
|
|
88
|
+
bodySyncQueue?: BodySyncQueueService;
|
|
89
|
+
}
|
|
90
|
+
export interface RemitClientDeps extends RemitClientSharedDeps {
|
|
91
|
+
repositories: RemitClientRepositories;
|
|
92
|
+
}
|
|
93
|
+
export declare const buildSharedDeps: () => RemitClientSharedDeps;
|
|
94
|
+
export declare const buildReadPathFilterConfig: (repositories: RemitClientRepositories) => FilterConfig | undefined;
|
|
95
|
+
export declare const createRemitClient: (deps: RemitClientDeps) => RemitClient;
|