@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,38 @@
|
|
|
1
|
+
import { parseContentStorageKey } from "@remit/storage-service";
|
|
2
|
+
export { parseContentStorageKey };
|
|
3
|
+
export interface ContentResult {
|
|
4
|
+
status: number;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body: string | Buffer;
|
|
7
|
+
}
|
|
8
|
+
export interface ServeContentDeps {
|
|
9
|
+
/**
|
|
10
|
+
* Reads the stored bytes, or returns null when the object is genuinely
|
|
11
|
+
* missing (ENOENT / NoSuchKey). Any other read error must throw so the route
|
|
12
|
+
* 500s — a permission/infra failure is never masked as a missing body.
|
|
13
|
+
*/
|
|
14
|
+
readObject(fullPath: string): Promise<Buffer | null>;
|
|
15
|
+
/** Resolves the message's mailbox + uid so the cue can issue one FETCH. */
|
|
16
|
+
lookupMessage(messageId: string): Promise<{
|
|
17
|
+
mailboxId: string;
|
|
18
|
+
uid: number;
|
|
19
|
+
} | null>;
|
|
20
|
+
requestBodySync(input: {
|
|
21
|
+
accountId: string;
|
|
22
|
+
mailboxId: string;
|
|
23
|
+
messageId: string;
|
|
24
|
+
uid?: number;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Local stand-in for the CloudFront `/content/*` behavior. Body-fetch outcomes:
|
|
29
|
+
*
|
|
30
|
+
* - Object present → 200 with the (decompressed) bytes.
|
|
31
|
+
* - Object missing → 202 + `Retry-After: 1`, and re-arm the SYNC_MESSAGE_BODY
|
|
32
|
+
* cue for that message so a retry succeeds once the worker stores the body.
|
|
33
|
+
* - Any other read failure throws, so the route 500s loudly.
|
|
34
|
+
*/
|
|
35
|
+
export declare const serveContent: (deps: ServeContentDeps, args: {
|
|
36
|
+
fullPath: string;
|
|
37
|
+
storageKey: string;
|
|
38
|
+
}) => Promise<ContentResult>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
import { parseContentStorageKey } from "@remit/storage-service";
|
|
3
|
+
// Re-exported for existing same-package consumers (server.ts,
|
|
4
|
+
// content-handler.test.ts). The key-shape parser itself lives in
|
|
5
|
+
// `@remit/storage-service` next to the `build*Key` functions it
|
|
6
|
+
// inverts, so non-backend consumers (e.g. the content-selfheal worker) can
|
|
7
|
+
// depend on it without pulling in the rest of `@remit/backend`.
|
|
8
|
+
export { parseContentStorageKey };
|
|
9
|
+
/**
|
|
10
|
+
* Local stand-in for the CloudFront `/content/*` behavior. Body-fetch outcomes:
|
|
11
|
+
*
|
|
12
|
+
* - Object present → 200 with the (decompressed) bytes.
|
|
13
|
+
* - Object missing → 202 + `Retry-After: 1`, and re-arm the SYNC_MESSAGE_BODY
|
|
14
|
+
* cue for that message so a retry succeeds once the worker stores the body.
|
|
15
|
+
* - Any other read failure throws, so the route 500s loudly.
|
|
16
|
+
*/
|
|
17
|
+
export const serveContent = async (deps, args) => {
|
|
18
|
+
const raw = await deps.readObject(args.fullPath);
|
|
19
|
+
if (raw) {
|
|
20
|
+
// The filesystem/S3 backends gzip every part; decompress here since there
|
|
21
|
+
// is no CloudFront to auto-decode locally.
|
|
22
|
+
const buffer = raw[0] === 0x1f && raw[1] === 0x8b ? gunzipSync(raw) : raw;
|
|
23
|
+
return {
|
|
24
|
+
status: 200,
|
|
25
|
+
headers: { "content-type": "application/octet-stream" },
|
|
26
|
+
body: buffer,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const ids = parseContentStorageKey(args.storageKey);
|
|
30
|
+
if (ids) {
|
|
31
|
+
const message = await deps.lookupMessage(ids.messageId);
|
|
32
|
+
if (message) {
|
|
33
|
+
await deps.requestBodySync({
|
|
34
|
+
accountId: ids.accountId,
|
|
35
|
+
mailboxId: message.mailboxId,
|
|
36
|
+
messageId: ids.messageId,
|
|
37
|
+
uid: message.uid,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
status: 202,
|
|
43
|
+
headers: { "Retry-After": "1" },
|
|
44
|
+
body: "body not yet synced",
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";
|
|
2
|
+
import { type Document, OpenAPIBackend } from "openapi-backend";
|
|
3
|
+
export declare const OpenAPISpec: Document;
|
|
4
|
+
declare const api: OpenAPIBackend;
|
|
5
|
+
export { setLogger } from "@remit/logger";
|
|
6
|
+
export { api };
|
|
7
|
+
export declare const handleApiEvent: (event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>;
|
package/dist/src/api.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import spec from "@remit/api-openapi-spec/openapi.json" with { type: "json" };
|
|
2
|
+
import { redactCalendarFeedPath } from "@remit/calendar-service";
|
|
3
|
+
import { logger, withLogContext } from "@remit/logger";
|
|
4
|
+
import { OpenAPIBackend, } from "openapi-backend";
|
|
5
|
+
import { assertLocalBypassNotInDeployedEnv } from "./auth.js";
|
|
6
|
+
import { usesBetterAuthJwt } from "./data-backend.js";
|
|
7
|
+
import { defaultErrorCode, handleError } from "./error.js";
|
|
8
|
+
import { handlers } from "./handlers/index.js";
|
|
9
|
+
import { authenticateSelfHostRequest } from "./jwt-auth.js";
|
|
10
|
+
import { normalizeRequest } from "./request.js";
|
|
11
|
+
import { runWithRequestContext } from "./request-context.js";
|
|
12
|
+
import { formatResponse, postResponseHandler } from "./response.js";
|
|
13
|
+
// Boot-time refusal: the local auth-bypass overrides must never be set in a
|
|
14
|
+
// deployed environment. Runs on module load so a leaked env var fails the
|
|
15
|
+
// process at startup rather than silently opening authentication at runtime.
|
|
16
|
+
assertLocalBypassNotInDeployedEnv();
|
|
17
|
+
const isDocument = (value) => typeof value === "object" &&
|
|
18
|
+
value !== null &&
|
|
19
|
+
"openapi" in value &&
|
|
20
|
+
typeof value.openapi === "string" &&
|
|
21
|
+
"paths" in value;
|
|
22
|
+
// Imported, not read from disk, so no host has to place openapi.json next to the
|
|
23
|
+
// entry point — a Worker has no filesystem to read it from.
|
|
24
|
+
if (!isDocument(spec)) {
|
|
25
|
+
throw new Error("@remit/api-openapi-spec is not an OpenAPI document");
|
|
26
|
+
}
|
|
27
|
+
// Typed as a Document so importers do not see the JSON literal type.
|
|
28
|
+
export const OpenAPISpec = spec;
|
|
29
|
+
const api = new OpenAPIBackend({
|
|
30
|
+
definition: OpenAPISpec,
|
|
31
|
+
quick: true,
|
|
32
|
+
// Coerce query/path params to their schema types. API Gateway delivers every
|
|
33
|
+
// query param as a string; handlers cast them to number/boolean (e.g. `limit`,
|
|
34
|
+
// `hasAttachment`) and forward them downstream. Without coercion a string
|
|
35
|
+
// `limit` reaches ElectroDB's `.go({ limit })` and DynamoDB rejects the
|
|
36
|
+
// numeric Limit field ("STRING_VALUE cannot be converted to Integer"), 500ing
|
|
37
|
+
// /addresses/search.
|
|
38
|
+
coerceTypes: true,
|
|
39
|
+
});
|
|
40
|
+
api.register("postResponseHandler", postResponseHandler);
|
|
41
|
+
/**
|
|
42
|
+
* The validator's findings, narrowed to the three fields `RequestValidationDetail`
|
|
43
|
+
* declares. Copied field by field rather than passed through: an AJV error also
|
|
44
|
+
* carries `params`, and under `verbose` the rejected `data` itself — which for a
|
|
45
|
+
* refused request body is whatever the caller sent, a password among the
|
|
46
|
+
* possibilities. Naming the fields is what keeps that off the wire.
|
|
47
|
+
*/
|
|
48
|
+
const validationDetails = (errors) => (errors ?? []).map((error) => ({
|
|
49
|
+
path: error.instancePath,
|
|
50
|
+
rule: error.keyword,
|
|
51
|
+
message: error.message ?? "refused",
|
|
52
|
+
}));
|
|
53
|
+
// The errors and the operation, and nothing that came off the wire. A request
|
|
54
|
+
// that fails validation is logged at error level, and the request it carries
|
|
55
|
+
// holds the caller's headers and cookies — an Authorization bearer among them,
|
|
56
|
+
// and for a calendar feed the token in the path is itself the credential
|
|
57
|
+
// (issue #1065). The path is redacted the same way the request scope redacts it.
|
|
58
|
+
api.register("validationFail", (c, req) => {
|
|
59
|
+
logger.error({
|
|
60
|
+
errors: c.validation.errors,
|
|
61
|
+
path: redactCalendarFeedPath(req.path),
|
|
62
|
+
operation: c.operation?.operationId,
|
|
63
|
+
}, "Validation failed");
|
|
64
|
+
return {
|
|
65
|
+
statusCode: 400,
|
|
66
|
+
body: {
|
|
67
|
+
code: defaultErrorCode(400),
|
|
68
|
+
message: "Invalid request",
|
|
69
|
+
errors: validationDetails(c.validation.errors),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
api.register("notFound", () => ({
|
|
74
|
+
statusCode: 404,
|
|
75
|
+
body: { code: defaultErrorCode(404), message: "Not found" },
|
|
76
|
+
}));
|
|
77
|
+
api.register("notImplemented", async (c) => {
|
|
78
|
+
const { status, mock } = api.mockResponseForOperation(c.operation.operationId);
|
|
79
|
+
return formatResponse(mock, status);
|
|
80
|
+
});
|
|
81
|
+
api.register(handlers);
|
|
82
|
+
api.init();
|
|
83
|
+
export { setLogger } from "@remit/logger";
|
|
84
|
+
export { api };
|
|
85
|
+
const readOriginHeader = (headers) => {
|
|
86
|
+
if (!headers)
|
|
87
|
+
return undefined;
|
|
88
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
89
|
+
if (key.toLowerCase() === "origin" && typeof value === "string") {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
// A scope, not `logger.setBindings`: this process serves requests concurrently,
|
|
96
|
+
// and bindings on the shared logger belong to whichever request wrote them last,
|
|
97
|
+
// so a line gets attributed to the wrong request. The scope follows the request
|
|
98
|
+
// through its own async continuations and nothing else. It nests inside the one
|
|
99
|
+
// `withTelemetry` opens, which is where `requestId` comes from.
|
|
100
|
+
export const handleApiEvent = async (event, context) => {
|
|
101
|
+
// A calendar feed carries its credential in the path, so the raw path is a
|
|
102
|
+
// secret and cannot be a log field (issue #1065). Redacted once, here, where
|
|
103
|
+
// every line inside the request picks it up from the scope.
|
|
104
|
+
const path = redactCalendarFeedPath(event.path);
|
|
105
|
+
return withLogContext({
|
|
106
|
+
path,
|
|
107
|
+
method: event.httpMethod,
|
|
108
|
+
}, async () => {
|
|
109
|
+
logger.debug({ method: event.httpMethod, path }, "Request received");
|
|
110
|
+
const origin = readOriginHeader(event.headers);
|
|
111
|
+
const startedAt = Date.now();
|
|
112
|
+
// Capture the correlation id here: it is seeded by
|
|
113
|
+
// runWithRequestContext below (from context.awsRequestId), but that
|
|
114
|
+
// AsyncLocalStorage scope does not reliably survive the
|
|
115
|
+
// openapi-backend handleRequest call chain, so we read it from
|
|
116
|
+
// `context.awsRequestId` directly rather than via
|
|
117
|
+
// getRequestCorrelationId at log time.
|
|
118
|
+
const correlationId = context.awsRequestId;
|
|
119
|
+
const result = await runWithRequestContext({ origin, correlationId }, async () => {
|
|
120
|
+
if (usesBetterAuthJwt()) {
|
|
121
|
+
const denied = await authenticateSelfHostRequest(event);
|
|
122
|
+
if (denied)
|
|
123
|
+
return denied;
|
|
124
|
+
}
|
|
125
|
+
return api
|
|
126
|
+
.handleRequest(normalizeRequest(event), event, context)
|
|
127
|
+
.catch(handleError);
|
|
128
|
+
});
|
|
129
|
+
// One structured access line per request: method, path, status, and
|
|
130
|
+
// duration. The correlation id is inlined as a top-level field so a bug
|
|
131
|
+
// report quoting the id can grep logs by it directly, and emitted at
|
|
132
|
+
// info so it is on by default in production (the debug "Request
|
|
133
|
+
// received" above already filters out under normal log levels).
|
|
134
|
+
// biome-ignore lint/plugin/no-logger-info: one access line per request is an audit-grade signal (issue #1054)
|
|
135
|
+
logger.info({
|
|
136
|
+
method: event.httpMethod,
|
|
137
|
+
path,
|
|
138
|
+
statusCode: result.statusCode,
|
|
139
|
+
durationMs: Date.now() - startedAt,
|
|
140
|
+
correlationId,
|
|
141
|
+
}, "Request completed");
|
|
142
|
+
return result;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
2
|
+
/**
|
|
3
|
+
* The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
|
|
4
|
+
* for local dev and fixtures: with them set, an unauthenticated request is served
|
|
5
|
+
* as the pinned account. They must never reach a deployed environment. Refuse to
|
|
6
|
+
* start (throw at module load) if either is present outside a local NODE_ENV — the
|
|
7
|
+
* same `development`/`test` signal the data layer already uses to decide local vs
|
|
8
|
+
* deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
|
|
9
|
+
* boot, not exploited at runtime.
|
|
10
|
+
*/
|
|
11
|
+
export declare const assertLocalBypassNotInDeployedEnv: (env?: NodeJS.ProcessEnv) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Return the Cognito `sub` for the current request, preferring the JWT claim
|
|
14
|
+
* and falling back to `LOCAL_COGNITO_SUB` for local development. Returns
|
|
15
|
+
* `undefined` when neither is available (e.g. seeded fixtures that only set
|
|
16
|
+
* `LOCAL_ACCOUNT_CONFIG_ID`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const getSubFromEvent: (event: APIGatewayProxyEvent) => string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Extract the caller's accountConfigId from an API Gateway event.
|
|
21
|
+
*
|
|
22
|
+
* Production: reads the Cognito `sub` claim (verified by the API Gateway
|
|
23
|
+
* Cognito authorizer) and derives the accountConfigId via UUIDv5.
|
|
24
|
+
*
|
|
25
|
+
* Local development: honours two environment overrides so the dev server and
|
|
26
|
+
* playwright fixtures can run without a real Cognito token:
|
|
27
|
+
* - `LOCAL_ACCOUNT_CONFIG_ID` — pin a specific seeded accountConfigId
|
|
28
|
+
* - `LOCAL_COGNITO_SUB` — pin a Cognito `sub` value; derived like production
|
|
29
|
+
*/
|
|
30
|
+
export declare const getAccountConfigIdFromEvent: (event: APIGatewayProxyEvent) => string;
|
package/dist/src/auth.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { deriveAccountConfigId } from "@remit/data-ports/id";
|
|
2
|
+
const LOCAL_BYPASS_VARS = [
|
|
3
|
+
"LOCAL_ACCOUNT_CONFIG_ID",
|
|
4
|
+
"LOCAL_COGNITO_SUB",
|
|
5
|
+
];
|
|
6
|
+
const isLocalEnv = (env) => env.NODE_ENV === "development" || env.NODE_ENV === "test";
|
|
7
|
+
/**
|
|
8
|
+
* The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
|
|
9
|
+
* for local dev and fixtures: with them set, an unauthenticated request is served
|
|
10
|
+
* as the pinned account. They must never reach a deployed environment. Refuse to
|
|
11
|
+
* start (throw at module load) if either is present outside a local NODE_ENV — the
|
|
12
|
+
* same `development`/`test` signal the data layer already uses to decide local vs
|
|
13
|
+
* deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
|
|
14
|
+
* boot, not exploited at runtime.
|
|
15
|
+
*/
|
|
16
|
+
export const assertLocalBypassNotInDeployedEnv = (env = process.env) => {
|
|
17
|
+
if (isLocalEnv(env))
|
|
18
|
+
return;
|
|
19
|
+
const leaked = LOCAL_BYPASS_VARS.filter((name) => {
|
|
20
|
+
const value = env[name];
|
|
21
|
+
return typeof value === "string" && value.length > 0;
|
|
22
|
+
});
|
|
23
|
+
if (leaked.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
throw new Error(`Refusing to start: local auth bypass ${leaked.join(", ")} is set while NODE_ENV=${env.NODE_ENV ?? "(unset)"}. These overrides bypass authentication and must never be present in a deployed environment.`);
|
|
26
|
+
};
|
|
27
|
+
const getSubFromClaims = (event) => {
|
|
28
|
+
const claims = event.requestContext?.authorizer?.claims;
|
|
29
|
+
if (!claims)
|
|
30
|
+
return undefined;
|
|
31
|
+
const sub = claims.sub;
|
|
32
|
+
if (typeof sub === "string" && sub.length > 0)
|
|
33
|
+
return sub;
|
|
34
|
+
return undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Return the Cognito `sub` for the current request, preferring the JWT claim
|
|
38
|
+
* and falling back to `LOCAL_COGNITO_SUB` for local development. Returns
|
|
39
|
+
* `undefined` when neither is available (e.g. seeded fixtures that only set
|
|
40
|
+
* `LOCAL_ACCOUNT_CONFIG_ID`).
|
|
41
|
+
*/
|
|
42
|
+
export const getSubFromEvent = (event) => {
|
|
43
|
+
const sub = getSubFromClaims(event);
|
|
44
|
+
if (sub)
|
|
45
|
+
return sub;
|
|
46
|
+
const localSub = process.env.LOCAL_COGNITO_SUB;
|
|
47
|
+
if (localSub && localSub.length > 0)
|
|
48
|
+
return localSub;
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Extract the caller's accountConfigId from an API Gateway event.
|
|
53
|
+
*
|
|
54
|
+
* Production: reads the Cognito `sub` claim (verified by the API Gateway
|
|
55
|
+
* Cognito authorizer) and derives the accountConfigId via UUIDv5.
|
|
56
|
+
*
|
|
57
|
+
* Local development: honours two environment overrides so the dev server and
|
|
58
|
+
* playwright fixtures can run without a real Cognito token:
|
|
59
|
+
* - `LOCAL_ACCOUNT_CONFIG_ID` — pin a specific seeded accountConfigId
|
|
60
|
+
* - `LOCAL_COGNITO_SUB` — pin a Cognito `sub` value; derived like production
|
|
61
|
+
*/
|
|
62
|
+
export const getAccountConfigIdFromEvent = (event) => {
|
|
63
|
+
const sub = getSubFromClaims(event);
|
|
64
|
+
if (sub)
|
|
65
|
+
return deriveAccountConfigId(sub);
|
|
66
|
+
const localAccountConfigId = process.env.LOCAL_ACCOUNT_CONFIG_ID;
|
|
67
|
+
if (localAccountConfigId)
|
|
68
|
+
return localAccountConfigId;
|
|
69
|
+
const localSub = process.env.LOCAL_COGNITO_SUB;
|
|
70
|
+
if (localSub)
|
|
71
|
+
return deriveAccountConfigId(localSub);
|
|
72
|
+
throw new Error("Missing accountConfigId: no Cognito `sub` claim on the request, and neither LOCAL_ACCOUNT_CONFIG_ID nor LOCAL_COGNITO_SUB env var is set");
|
|
73
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed reads for Microsoft OAuth (Entra) environment variables.
|
|
3
|
+
*
|
|
4
|
+
* `MSOAUTH_REDIRECT_URI` is derived from `PUBLIC_ORIGIN` by the compose file
|
|
5
|
+
* (deploy/vps/docker-compose.sqlite.yml) and must match the redirect URI
|
|
6
|
+
* registered on the Entra app registration.
|
|
7
|
+
*
|
|
8
|
+
* Credentials come either from `MSOAUTH_CLIENT_ID` / `MSOAUTH_CLIENT_SECRET`
|
|
9
|
+
* in the deploy env file, or from Secrets Manager via `MSOAUTH_SECRET_ARN`.
|
|
10
|
+
*
|
|
11
|
+
* The Microsoft authorization and token endpoints are fixed in
|
|
12
|
+
* `@remit/mail-oauth-service`; no authority URL is read from the environment.
|
|
13
|
+
*/
|
|
14
|
+
export interface MsOAuthConfig {
|
|
15
|
+
/** Secrets Manager ARN — an alternative to the client id/secret pair. */
|
|
16
|
+
readonly secretArn: string | undefined;
|
|
17
|
+
/** OAuth redirect URI registered in the Entra app. */
|
|
18
|
+
readonly redirectUri: string;
|
|
19
|
+
/** Client ID — set directly to bypass Secrets Manager. */
|
|
20
|
+
readonly clientId: string | undefined;
|
|
21
|
+
/** Client secret — set directly to bypass Secrets Manager. */
|
|
22
|
+
readonly clientSecret: string | undefined;
|
|
23
|
+
/** Token endpoint override — used for local stubbing (bypasses default OIDC discovery). */
|
|
24
|
+
readonly tokenEndpoint: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read Microsoft OAuth configuration from the environment.
|
|
28
|
+
* Throws when `MSOAUTH_REDIRECT_URI` is absent.
|
|
29
|
+
*/
|
|
30
|
+
export declare const getMsOAuthConfig: () => MsOAuthConfig;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed reads for Microsoft OAuth (Entra) environment variables.
|
|
3
|
+
*
|
|
4
|
+
* `MSOAUTH_REDIRECT_URI` is derived from `PUBLIC_ORIGIN` by the compose file
|
|
5
|
+
* (deploy/vps/docker-compose.sqlite.yml) and must match the redirect URI
|
|
6
|
+
* registered on the Entra app registration.
|
|
7
|
+
*
|
|
8
|
+
* Credentials come either from `MSOAUTH_CLIENT_ID` / `MSOAUTH_CLIENT_SECRET`
|
|
9
|
+
* in the deploy env file, or from Secrets Manager via `MSOAUTH_SECRET_ARN`.
|
|
10
|
+
*
|
|
11
|
+
* The Microsoft authorization and token endpoints are fixed in
|
|
12
|
+
* `@remit/mail-oauth-service`; no authority URL is read from the environment.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Read Microsoft OAuth configuration from the environment.
|
|
16
|
+
* Throws when `MSOAUTH_REDIRECT_URI` is absent.
|
|
17
|
+
*/
|
|
18
|
+
export const getMsOAuthConfig = () => {
|
|
19
|
+
const redirectUri = process.env.MSOAUTH_REDIRECT_URI;
|
|
20
|
+
if (!redirectUri) {
|
|
21
|
+
throw new Error("MSOAUTH_REDIRECT_URI is not set. It is derived from PUBLIC_ORIGIN — set PUBLIC_ORIGIN in the deploy env file (deploy/vps/.env) and restart, or set MSOAUTH_REDIRECT_URI directly.");
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
secretArn: process.env.MSOAUTH_SECRET_ARN,
|
|
25
|
+
redirectUri,
|
|
26
|
+
clientId: process.env.MSOAUTH_CLIENT_ID,
|
|
27
|
+
clientSecret: process.env.MSOAUTH_CLIENT_SECRET,
|
|
28
|
+
tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
* The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
|
|
3
3
|
* path.
|
|
4
4
|
*/
|
|
5
|
-
export const isSelfHostSqlBackend
|
|
6
|
-
process.env.DATA_BACKEND === "sqlite";
|
|
7
|
-
|
|
5
|
+
export declare const isSelfHostSqlBackend: () => boolean;
|
|
8
6
|
/**
|
|
9
7
|
* The self-host SQL backend authenticates requests and signs content URLs with
|
|
10
8
|
* a better-auth RS256 JWT verified at the edge and re-verified in-process, no
|
|
11
9
|
* Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
|
|
12
10
|
* content guard) and is deliberately excluded.
|
|
13
11
|
*/
|
|
14
|
-
export const usesBetterAuthJwt
|
|
12
|
+
export declare const usesBetterAuthJwt: () => boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
|
|
3
|
+
* path.
|
|
4
|
+
*/
|
|
5
|
+
export const isSelfHostSqlBackend = () => process.env.DATA_BACKEND === "sqlite";
|
|
6
|
+
/**
|
|
7
|
+
* The self-host SQL backend authenticates requests and signs content URLs with
|
|
8
|
+
* a better-auth RS256 JWT verified at the edge and re-verified in-process, no
|
|
9
|
+
* Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
|
|
10
|
+
* content guard) and is deliberately excluded.
|
|
11
|
+
*/
|
|
12
|
+
export const usesBetterAuthJwt = () => isSelfHostSqlBackend();
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { AutoMovedInfo } from "@remit/api-openapi-types";
|
|
2
2
|
import type { MessageItem } from "@remit/data-ports";
|
|
3
|
-
import { PlacementConfidence } from "@remit/domain-enums";
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Project a Message's internal auto-move state to the public `AutoMovedInfo`
|
|
7
5
|
* shape. Present only for a real, in-effect auto-move; absent otherwise, so a
|
|
@@ -27,30 +25,4 @@ import { PlacementConfidence } from "@remit/domain-enums";
|
|
|
27
25
|
* The client derives everything else (whether the move is still in effect, the
|
|
28
26
|
* undo target) from these fields plus current placement.
|
|
29
27
|
*/
|
|
30
|
-
export const deriveAutoMoved
|
|
31
|
-
message: Pick<
|
|
32
|
-
MessageItem,
|
|
33
|
-
"movedByRemit" | "placementVerdict" | "filterMove"
|
|
34
|
-
>,
|
|
35
|
-
): AutoMovedInfo | undefined => {
|
|
36
|
-
if (message.movedByRemit !== true) return undefined;
|
|
37
|
-
|
|
38
|
-
const filterMove = message.filterMove;
|
|
39
|
-
if (filterMove) {
|
|
40
|
-
return {
|
|
41
|
-
fromMailboxId: filterMove.sourceMailboxId,
|
|
42
|
-
destinationMailboxId: filterMove.destinationMailboxId,
|
|
43
|
-
filterId: filterMove.filterId,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const verdict = message.placementVerdict;
|
|
48
|
-
if (!verdict) return undefined;
|
|
49
|
-
if (verdict.confidence !== PlacementConfidence.Confident) return undefined;
|
|
50
|
-
if (verdict.dryRun === true) return undefined;
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
action: verdict.action,
|
|
54
|
-
fromPlacement: verdict.fromPlacement,
|
|
55
|
-
};
|
|
56
|
-
};
|
|
28
|
+
export declare const deriveAutoMoved: (message: Pick<MessageItem, "movedByRemit" | "placementVerdict" | "filterMove">) => AutoMovedInfo | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PlacementConfidence } from "@remit/domain-enums";
|
|
2
|
+
/**
|
|
3
|
+
* Project a Message's internal auto-move state to the public `AutoMovedInfo`
|
|
4
|
+
* shape. Present only for a real, in-effect auto-move; absent otherwise, so a
|
|
5
|
+
* message synced before either mechanism existed (no `filterMove`, no
|
|
6
|
+
* `placementVerdict`) projects nothing and renders no badge.
|
|
7
|
+
*
|
|
8
|
+
* Two mechanisms move mail and both raise `movedByRemit`:
|
|
9
|
+
*
|
|
10
|
+
* 1. A standing filter (RFC 034) records a `filterMove` marker naming the
|
|
11
|
+
* source folder, the destination folder, and the filter. It has no
|
|
12
|
+
* classifier direction/role, so the projection carries mailbox ids
|
|
13
|
+
* (`fromMailboxId`/`destinationMailboxId`) and the `filterId` for the
|
|
14
|
+
* Settings link — an arbitrary destination the Inbox/Junk-shaped `action`
|
|
15
|
+
* cannot name.
|
|
16
|
+
* 2. The Tier 0 classifier records a `placementVerdict`. Its `action`
|
|
17
|
+
* (MoveToInbox/MoveToJunk) and `fromPlacement` (inbox/junk/other) are the
|
|
18
|
+
* only fields that cross to the wire; confidence, dryRun, decidedAt and
|
|
19
|
+
* reasons stay internal.
|
|
20
|
+
*
|
|
21
|
+
* The filter marker outranks the verdict check: a matched filter's move is
|
|
22
|
+
* exclusive and outranks the classifier's placement move at index time (RFC 034
|
|
23
|
+
* Decision 3.1), so a message carrying both was ultimately moved by the filter.
|
|
24
|
+
* The client derives everything else (whether the move is still in effect, the
|
|
25
|
+
* undo target) from these fields plus current placement.
|
|
26
|
+
*/
|
|
27
|
+
export const deriveAutoMoved = (message) => {
|
|
28
|
+
if (message.movedByRemit !== true)
|
|
29
|
+
return undefined;
|
|
30
|
+
const filterMove = message.filterMove;
|
|
31
|
+
if (filterMove) {
|
|
32
|
+
return {
|
|
33
|
+
fromMailboxId: filterMove.sourceMailboxId,
|
|
34
|
+
destinationMailboxId: filterMove.destinationMailboxId,
|
|
35
|
+
filterId: filterMove.filterId,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const verdict = message.placementVerdict;
|
|
39
|
+
if (!verdict)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (verdict.confidence !== PlacementConfidence.Confident)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (verdict.dryRun === true)
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
action: verdict.action,
|
|
47
|
+
fromPlacement: verdict.fromPlacement,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SignedPathFailure } from "@remit/storage-service/signed-path";
|
|
2
|
+
/**
|
|
3
|
+
* Default validity window for a signed content URL. Long enough to cover a
|
|
4
|
+
* viewing session (react-query caches describeMessage for 30 min and inline
|
|
5
|
+
* images may load late), short enough to bound the replay window if a URL
|
|
6
|
+
* leaks.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CONTENT_URL_TTL_SECONDS = 3600;
|
|
9
|
+
export interface ContentSignature {
|
|
10
|
+
exp: number;
|
|
11
|
+
sig: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A function that signs a decoded, account-scoped storage path and returns the
|
|
15
|
+
* `exp`/`sig` query params to append to its content URL.
|
|
16
|
+
*/
|
|
17
|
+
export type ContentSigner = (relativePath: string) => ContentSignature;
|
|
18
|
+
export declare const createContentSigner: (masterSecret: string, ttlSeconds?: number) => ContentSigner;
|
|
19
|
+
/**
|
|
20
|
+
* Build the content-URL signer for the current environment, or `undefined` when
|
|
21
|
+
* signing does not apply. Signing is enforced on the self-host SQL backend,
|
|
22
|
+
* which serves `/content/*` straight from the backend container; on AWS the
|
|
23
|
+
* Lambda@Edge JWT verifier guards `/content/*` and the URL stays unsigned so
|
|
24
|
+
* CloudFront/S3 behaviour is unchanged. Throws on the self-host backend when the
|
|
25
|
+
* master secret is missing, so a misconfigured deploy fails loud rather than
|
|
26
|
+
* shipping unsigned (unauthenticated) content URLs.
|
|
27
|
+
*/
|
|
28
|
+
export declare const getContentSigner: () => ContentSigner | undefined;
|
|
29
|
+
export type ContentSignatureFailure = SignedPathFailure;
|
|
30
|
+
export type ContentSignatureResult = {
|
|
31
|
+
valid: true;
|
|
32
|
+
} | {
|
|
33
|
+
valid: false;
|
|
34
|
+
reason: ContentSignatureFailure;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Verify a signed content request. Recomputes the HMAC over the requested path
|
|
38
|
+
* and the supplied expiry and constant-time compares it against the presented
|
|
39
|
+
* signature. Pure so the decision can be unit-tested without a live server.
|
|
40
|
+
*/
|
|
41
|
+
export declare const verifyContentSignature: (relativePath: string, expRaw: string | undefined, sig: string | undefined, masterSecret: string, nowSeconds: number) => ContentSignatureResult;
|