@remit/backend 0.0.143 → 0.0.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +73 -0
- package/dist/src/auth.d.ts +24 -0
- package/dist/src/auth.js +81 -0
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +19 -0
- package/dist/src/config/msoauth.js +6 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +40 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
- package/dist/src/derive/contentUrl.d.ts +33 -0
- package/dist/src/derive/contentUrl.js +27 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +15 -0
- package/dist/src/handlers/account-oauth.js +334 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +7 -0
- package/dist/src/handlers/account.js +337 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +133 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +417 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +184 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +204 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +323 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +164 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +88 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +37 -0
- package/dist/src/handlers/index.d.ts +88 -0
- package/dist/src/handlers/index.js +62 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +107 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +211 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +117 -0
- package/dist/src/handlers/message.d.ts +82 -0
- package/dist/src/handlers/message.js +595 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +103 -0
- package/dist/src/handlers/outbox-attachment.d.ts +25 -0
- package/dist/src/handlers/outbox-attachment.js +77 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +136 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +73 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +159 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +228 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +406 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +53 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +15 -0
- package/dist/src/jwt-auth.js +45 -0
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +9 -0
- package/dist/src/request.js +17 -0
- package/dist/src/response.d.ts +44 -0
- package/dist/src/response.js +80 -0
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +20 -0
- package/dist/src/service/account-deletion.js +33 -0
- package/dist/src/service/compose-sqlite.d.ts +3 -0
- package/dist/src/service/compose-sqlite.js +13 -0
- package/dist/src/service/create-remit-client.d.ts +109 -0
- package/dist/src/service/create-remit-client.js +188 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +42 -0
- package/dist/src/service/export-identity.d.ts +4 -0
- package/dist/src/service/export-identity.js +33 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +37 -0
- package/dist/src/service/organize.d.ts +181 -0
- package/dist/src/service/organize.js +367 -0
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +57 -0
- package/dist/src/service/trigger-sync.js +31 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +1 -0
- package/package.json +125 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/data-backend.ts +0 -14
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/sqs.ts +0 -4
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
package/src/auth.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { base36uuidv5, REMIT_NAMESPACE } from "@remit/data-ports/id";
|
|
2
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
3
|
-
|
|
4
|
-
const LOCAL_BYPASS_VARS = [
|
|
5
|
-
"LOCAL_ACCOUNT_CONFIG_ID",
|
|
6
|
-
"LOCAL_COGNITO_SUB",
|
|
7
|
-
] as const;
|
|
8
|
-
|
|
9
|
-
const isLocalEnv = (env: NodeJS.ProcessEnv): boolean =>
|
|
10
|
-
env.NODE_ENV === "development" || env.NODE_ENV === "test";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The LOCAL_ACCOUNT_CONFIG_ID / LOCAL_COGNITO_SUB overrides bypass authentication
|
|
14
|
-
* for local dev and fixtures: with them set, an unauthenticated request is served
|
|
15
|
-
* as the pinned account. They must never reach a deployed environment. Refuse to
|
|
16
|
-
* start (throw at module load) if either is present outside a local NODE_ENV — the
|
|
17
|
-
* same `development`/`test` signal the data layer already uses to decide local vs
|
|
18
|
-
* deployed. Fail loud rather than silently ignore, so a leaked env var is caught at
|
|
19
|
-
* boot, not exploited at runtime.
|
|
20
|
-
*/
|
|
21
|
-
export const assertLocalBypassNotInDeployedEnv = (
|
|
22
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
23
|
-
): void => {
|
|
24
|
-
if (isLocalEnv(env)) return;
|
|
25
|
-
const leaked = LOCAL_BYPASS_VARS.filter((name) => {
|
|
26
|
-
const value = env[name];
|
|
27
|
-
return typeof value === "string" && value.length > 0;
|
|
28
|
-
});
|
|
29
|
-
if (leaked.length === 0) return;
|
|
30
|
-
throw new Error(
|
|
31
|
-
`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.`,
|
|
32
|
-
);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Derive a deterministic accountConfigId from a Cognito subject (`sub`) claim.
|
|
37
|
-
* One Cognito user maps to exactly one account config.
|
|
38
|
-
*/
|
|
39
|
-
export const deriveAccountConfigId = (sub: string): string =>
|
|
40
|
-
base36uuidv5(`account:${sub}`, REMIT_NAMESPACE);
|
|
41
|
-
|
|
42
|
-
const getSubFromClaims = (event: APIGatewayProxyEvent): string | undefined => {
|
|
43
|
-
const claims = event.requestContext?.authorizer?.claims;
|
|
44
|
-
if (!claims) return undefined;
|
|
45
|
-
const sub = claims.sub;
|
|
46
|
-
if (typeof sub === "string" && sub.length > 0) return sub;
|
|
47
|
-
return undefined;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Return the Cognito `sub` for the current request, preferring the JWT claim
|
|
52
|
-
* and falling back to `LOCAL_COGNITO_SUB` for local development. Returns
|
|
53
|
-
* `undefined` when neither is available (e.g. seeded fixtures that only set
|
|
54
|
-
* `LOCAL_ACCOUNT_CONFIG_ID`).
|
|
55
|
-
*/
|
|
56
|
-
export const getSubFromEvent = (
|
|
57
|
-
event: APIGatewayProxyEvent,
|
|
58
|
-
): string | undefined => {
|
|
59
|
-
const sub = getSubFromClaims(event);
|
|
60
|
-
if (sub) return sub;
|
|
61
|
-
const localSub = process.env.LOCAL_COGNITO_SUB;
|
|
62
|
-
if (localSub && localSub.length > 0) return localSub;
|
|
63
|
-
return undefined;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Extract the caller's accountConfigId from an API Gateway event.
|
|
68
|
-
*
|
|
69
|
-
* Production: reads the Cognito `sub` claim (verified by the API Gateway
|
|
70
|
-
* Cognito authorizer) and derives the accountConfigId via UUIDv5.
|
|
71
|
-
*
|
|
72
|
-
* Local development: honours two environment overrides so the dev server and
|
|
73
|
-
* playwright fixtures can run without a real Cognito token:
|
|
74
|
-
* - `LOCAL_ACCOUNT_CONFIG_ID` — pin a specific seeded accountConfigId
|
|
75
|
-
* - `LOCAL_COGNITO_SUB` — pin a Cognito `sub` value; derived like production
|
|
76
|
-
*/
|
|
77
|
-
export const getAccountConfigIdFromEvent = (
|
|
78
|
-
event: APIGatewayProxyEvent,
|
|
79
|
-
): string => {
|
|
80
|
-
const sub = getSubFromClaims(event);
|
|
81
|
-
if (sub) return deriveAccountConfigId(sub);
|
|
82
|
-
|
|
83
|
-
const localAccountConfigId = process.env.LOCAL_ACCOUNT_CONFIG_ID;
|
|
84
|
-
if (localAccountConfigId) return localAccountConfigId;
|
|
85
|
-
|
|
86
|
-
const localSub = process.env.LOCAL_COGNITO_SUB;
|
|
87
|
-
if (localSub) return deriveAccountConfigId(localSub);
|
|
88
|
-
|
|
89
|
-
throw new Error(
|
|
90
|
-
"Missing accountConfigId: no Cognito `sub` claim on the request, and neither LOCAL_ACCOUNT_CONFIG_ID nor LOCAL_COGNITO_SUB env var is set",
|
|
91
|
-
);
|
|
92
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import { getMsOAuthConfig } from "./msoauth.js";
|
|
4
|
-
|
|
5
|
-
const MSOAUTH_KEYS = [
|
|
6
|
-
"MSOAUTH_AUTHORITY",
|
|
7
|
-
"MSOAUTH_REDIRECT_URI",
|
|
8
|
-
"MSOAUTH_SECRET_ARN",
|
|
9
|
-
"MSOAUTH_CLIENT_ID",
|
|
10
|
-
"MSOAUTH_CLIENT_SECRET",
|
|
11
|
-
"MSOAUTH_TOKEN_ENDPOINT",
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
|
-
describe("getMsOAuthConfig", () => {
|
|
15
|
-
const original = new Map(
|
|
16
|
-
MSOAUTH_KEYS.map((key) => [key, process.env[key]] as const),
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
for (const [key, value] of original) {
|
|
21
|
-
if (value === undefined) delete process.env[key];
|
|
22
|
-
else process.env[key] = value;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("resolves on a self-host deployment, where no authority is wired", () => {
|
|
27
|
-
for (const key of MSOAUTH_KEYS) delete process.env[key];
|
|
28
|
-
process.env.MSOAUTH_REDIRECT_URI =
|
|
29
|
-
"https://mail.example.com/api/accounts/oauth/microsoft/callback";
|
|
30
|
-
process.env.MSOAUTH_CLIENT_ID = "client-id";
|
|
31
|
-
process.env.MSOAUTH_CLIENT_SECRET = "client-secret";
|
|
32
|
-
|
|
33
|
-
assert.deepEqual(getMsOAuthConfig(), {
|
|
34
|
-
secretArn: undefined,
|
|
35
|
-
redirectUri:
|
|
36
|
-
"https://mail.example.com/api/accounts/oauth/microsoft/callback",
|
|
37
|
-
clientId: "client-id",
|
|
38
|
-
clientSecret: "client-secret",
|
|
39
|
-
tokenEndpoint: undefined,
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("names PUBLIC_ORIGIN when the redirect URI is missing", () => {
|
|
44
|
-
for (const key of MSOAUTH_KEYS) delete process.env[key];
|
|
45
|
-
|
|
46
|
-
assert.throws(getMsOAuthConfig, /MSOAUTH_REDIRECT_URI.*PUBLIC_ORIGIN/s);
|
|
47
|
-
});
|
|
48
|
-
});
|
package/src/config/msoauth.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
export interface MsOAuthConfig {
|
|
16
|
-
/** Secrets Manager ARN — an alternative to the client id/secret pair. */
|
|
17
|
-
readonly secretArn: string | undefined;
|
|
18
|
-
/** OAuth redirect URI registered in the Entra app. */
|
|
19
|
-
readonly redirectUri: string;
|
|
20
|
-
/** Client ID — set directly to bypass Secrets Manager. */
|
|
21
|
-
readonly clientId: string | undefined;
|
|
22
|
-
/** Client secret — set directly to bypass Secrets Manager. */
|
|
23
|
-
readonly clientSecret: string | undefined;
|
|
24
|
-
/** Token endpoint override — used for local stubbing (bypasses default OIDC discovery). */
|
|
25
|
-
readonly tokenEndpoint: string | undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Read Microsoft OAuth configuration from the environment.
|
|
30
|
-
* Throws when `MSOAUTH_REDIRECT_URI` is absent.
|
|
31
|
-
*/
|
|
32
|
-
export const getMsOAuthConfig = (): MsOAuthConfig => {
|
|
33
|
-
const redirectUri = process.env.MSOAUTH_REDIRECT_URI;
|
|
34
|
-
|
|
35
|
-
if (!redirectUri) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
"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.",
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
secretArn: process.env.MSOAUTH_SECRET_ARN,
|
|
43
|
-
redirectUri,
|
|
44
|
-
clientId: process.env.MSOAUTH_CLIENT_ID,
|
|
45
|
-
clientSecret: process.env.MSOAUTH_CLIENT_SECRET,
|
|
46
|
-
tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT,
|
|
47
|
-
};
|
|
48
|
-
};
|
package/src/data-backend.test.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import { usesBetterAuthJwt } from "./data-backend.js";
|
|
4
|
-
|
|
5
|
-
describe("usesBetterAuthJwt", () => {
|
|
6
|
-
const ORIGINAL = process.env.DATA_BACKEND;
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
if (ORIGINAL === undefined) delete process.env.DATA_BACKEND;
|
|
9
|
-
else process.env.DATA_BACKEND = ORIGINAL;
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("is true for the self-host SQL backend", () => {
|
|
13
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
14
|
-
assert.equal(usesBetterAuthJwt(), true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("is false for the AWS DynamoDB path and when unset", () => {
|
|
18
|
-
process.env.DATA_BACKEND = "dynamodb";
|
|
19
|
-
assert.equal(usesBetterAuthJwt(), false);
|
|
20
|
-
delete process.env.DATA_BACKEND;
|
|
21
|
-
assert.equal(usesBetterAuthJwt(), false);
|
|
22
|
-
});
|
|
23
|
-
});
|
package/src/data-backend.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
|
|
3
|
-
* path.
|
|
4
|
-
*/
|
|
5
|
-
export const isSelfHostSqlBackend = (): boolean =>
|
|
6
|
-
process.env.DATA_BACKEND === "sqlite";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The self-host SQL backend authenticates requests and signs content URLs with
|
|
10
|
-
* a better-auth RS256 JWT verified at the edge and re-verified in-process, no
|
|
11
|
-
* Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
|
|
12
|
-
* content guard) and is deliberately excluded.
|
|
13
|
-
*/
|
|
14
|
-
export const usesBetterAuthJwt = (): boolean => isSelfHostSqlBackend();
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { MessageItem } from "@remit/data-ports";
|
|
4
|
-
import { PlacementAction, PlacementConfidence } from "@remit/domain-enums";
|
|
5
|
-
import { deriveAutoMoved } from "./autoMoved.js";
|
|
6
|
-
|
|
7
|
-
type AutoMovedInput = Pick<
|
|
8
|
-
MessageItem,
|
|
9
|
-
"movedByRemit" | "placementVerdict" | "filterMove"
|
|
10
|
-
>;
|
|
11
|
-
|
|
12
|
-
const confidentVerdict = {
|
|
13
|
-
action: PlacementAction.MoveToInbox,
|
|
14
|
-
confidence: PlacementConfidence.Confident,
|
|
15
|
-
fromPlacement: "junk",
|
|
16
|
-
reasons: ["provider=spam", "dmarc=pass"],
|
|
17
|
-
dryRun: false,
|
|
18
|
-
decidedAt: 1_700_000_000_000,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const filterMove = {
|
|
22
|
-
filterId: "flt-1",
|
|
23
|
-
sourceMailboxId: "mb-inbox",
|
|
24
|
-
destinationMailboxId: "mb-travel",
|
|
25
|
-
decidedAt: 1_700_000_000_000,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
describe("deriveAutoMoved", () => {
|
|
29
|
-
it("returns undefined when the message was not moved by Remit", () => {
|
|
30
|
-
const message: AutoMovedInput = {
|
|
31
|
-
movedByRemit: false,
|
|
32
|
-
placementVerdict: confidentVerdict,
|
|
33
|
-
};
|
|
34
|
-
assert.equal(deriveAutoMoved(message), undefined);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("projects a standing-filter move to its mailbox ids and filter id", () => {
|
|
38
|
-
const message: AutoMovedInput = { movedByRemit: true, filterMove };
|
|
39
|
-
assert.deepEqual(deriveAutoMoved(message), {
|
|
40
|
-
fromMailboxId: "mb-inbox",
|
|
41
|
-
destinationMailboxId: "mb-travel",
|
|
42
|
-
filterId: "flt-1",
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("never leaks the classifier action/fromPlacement for a filter move", () => {
|
|
47
|
-
const result = deriveAutoMoved({ movedByRemit: true, filterMove });
|
|
48
|
-
assert.equal(result?.action, undefined);
|
|
49
|
-
assert.equal(result?.fromPlacement, undefined);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("a filter move outranks a co-present placement verdict", () => {
|
|
53
|
-
// Both a filter move and a classifier verdict can be recorded; the filter's
|
|
54
|
-
// exclusive move is what ultimately placed the message (RFC 034 Dec. 3.1).
|
|
55
|
-
const message: AutoMovedInput = {
|
|
56
|
-
movedByRemit: true,
|
|
57
|
-
filterMove,
|
|
58
|
-
placementVerdict: confidentVerdict,
|
|
59
|
-
};
|
|
60
|
-
assert.deepEqual(deriveAutoMoved(message), {
|
|
61
|
-
fromMailboxId: "mb-inbox",
|
|
62
|
-
destinationMailboxId: "mb-travel",
|
|
63
|
-
filterId: "flt-1",
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("projects a confident classifier move to action + fromPlacement", () => {
|
|
68
|
-
const message: AutoMovedInput = {
|
|
69
|
-
movedByRemit: true,
|
|
70
|
-
placementVerdict: confidentVerdict,
|
|
71
|
-
};
|
|
72
|
-
assert.deepEqual(deriveAutoMoved(message), {
|
|
73
|
-
action: PlacementAction.MoveToInbox,
|
|
74
|
-
fromPlacement: "junk",
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("drops an unsure classifier verdict", () => {
|
|
79
|
-
const message: AutoMovedInput = {
|
|
80
|
-
movedByRemit: true,
|
|
81
|
-
placementVerdict: {
|
|
82
|
-
...confidentVerdict,
|
|
83
|
-
confidence: PlacementConfidence.Unsure,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
assert.equal(deriveAutoMoved(message), undefined);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("drops a dry-run classifier verdict", () => {
|
|
90
|
-
const message: AutoMovedInput = {
|
|
91
|
-
movedByRemit: true,
|
|
92
|
-
placementVerdict: { ...confidentVerdict, dryRun: true },
|
|
93
|
-
};
|
|
94
|
-
assert.equal(deriveAutoMoved(message), undefined);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("does not crash on legacy verdict-less, filter-less moved data", () => {
|
|
98
|
-
// A message moved before either the verdict or the filter marker existed:
|
|
99
|
-
// movedByRemit is set but nothing is derivable. Must return undefined, not
|
|
100
|
-
// throw (issue #223 back-compat).
|
|
101
|
-
const message: AutoMovedInput = { movedByRemit: true };
|
|
102
|
-
assert.equal(deriveAutoMoved(message), undefined);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
CONTENT_URL_TTL_SECONDS,
|
|
5
|
-
createContentSigner,
|
|
6
|
-
getContentSigner,
|
|
7
|
-
verifyContentSignature,
|
|
8
|
-
} from "./contentSignature.js";
|
|
9
|
-
|
|
10
|
-
const SECRET = "test-master-secret-at-least-32-chars-long";
|
|
11
|
-
const PATH_A = "accounts/cfg-alice/acc-alice/messages/msg-1/parts/1.2";
|
|
12
|
-
const PATH_B = "accounts/cfg-bob/acc-bob/messages/msg-9/parts/1.2";
|
|
13
|
-
|
|
14
|
-
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
|
15
|
-
|
|
16
|
-
describe("content signature: sign + verify round trip", () => {
|
|
17
|
-
it("a freshly signed URL verifies", () => {
|
|
18
|
-
const { exp, sig } = createContentSigner(SECRET)(PATH_A);
|
|
19
|
-
const result = verifyContentSignature(
|
|
20
|
-
PATH_A,
|
|
21
|
-
String(exp),
|
|
22
|
-
sig,
|
|
23
|
-
SECRET,
|
|
24
|
-
nowSeconds(),
|
|
25
|
-
);
|
|
26
|
-
assert.deepEqual(result, { valid: true });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("rejects a signature minted for a different account's path (cross-account IDOR defense)", () => {
|
|
30
|
-
const { exp, sig } = createContentSigner(SECRET)(PATH_A);
|
|
31
|
-
// Present account A's signature against account B's path.
|
|
32
|
-
const result = verifyContentSignature(
|
|
33
|
-
PATH_B,
|
|
34
|
-
String(exp),
|
|
35
|
-
sig,
|
|
36
|
-
SECRET,
|
|
37
|
-
nowSeconds(),
|
|
38
|
-
);
|
|
39
|
-
assert.deepEqual(result, { valid: false, reason: "bad-signature" });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("rejects a tampered signature", () => {
|
|
43
|
-
const { exp, sig } = createContentSigner(SECRET)(PATH_A);
|
|
44
|
-
const tampered = `${sig.slice(0, -1)}${sig.endsWith("A") ? "B" : "A"}`;
|
|
45
|
-
const result = verifyContentSignature(
|
|
46
|
-
PATH_A,
|
|
47
|
-
String(exp),
|
|
48
|
-
tampered,
|
|
49
|
-
SECRET,
|
|
50
|
-
nowSeconds(),
|
|
51
|
-
);
|
|
52
|
-
assert.equal(result.valid, false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("rejects a valid signature verified with the wrong secret", () => {
|
|
56
|
-
const { exp, sig } = createContentSigner(SECRET)(PATH_A);
|
|
57
|
-
const result = verifyContentSignature(
|
|
58
|
-
PATH_A,
|
|
59
|
-
String(exp),
|
|
60
|
-
sig,
|
|
61
|
-
"a-different-master-secret-also-32-chars",
|
|
62
|
-
nowSeconds(),
|
|
63
|
-
);
|
|
64
|
-
assert.deepEqual(result, { valid: false, reason: "bad-signature" });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("rejects an expired signature", () => {
|
|
68
|
-
// Negative TTL mints a signature whose expiry is already in the past.
|
|
69
|
-
const { exp, sig } = createContentSigner(SECRET, -20)(PATH_A);
|
|
70
|
-
assert.ok(exp < nowSeconds());
|
|
71
|
-
const result = verifyContentSignature(
|
|
72
|
-
PATH_A,
|
|
73
|
-
String(exp),
|
|
74
|
-
sig,
|
|
75
|
-
SECRET,
|
|
76
|
-
nowSeconds(),
|
|
77
|
-
);
|
|
78
|
-
assert.deepEqual(result, { valid: false, reason: "expired" });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("reports missing exp/sig distinctly from a bad signature", () => {
|
|
82
|
-
assert.deepEqual(
|
|
83
|
-
verifyContentSignature(
|
|
84
|
-
PATH_A,
|
|
85
|
-
undefined,
|
|
86
|
-
undefined,
|
|
87
|
-
SECRET,
|
|
88
|
-
nowSeconds(),
|
|
89
|
-
),
|
|
90
|
-
{ valid: false, reason: "missing" },
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("rejects a non-integer exp as malformed", () => {
|
|
95
|
-
const { sig } = createContentSigner(SECRET)(PATH_A);
|
|
96
|
-
assert.deepEqual(
|
|
97
|
-
verifyContentSignature(PATH_A, "not-a-number", sig, SECRET, nowSeconds()),
|
|
98
|
-
{ valid: false, reason: "malformed" },
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("getContentSigner", () => {
|
|
104
|
-
const ORIGINAL_BACKEND = process.env.DATA_BACKEND;
|
|
105
|
-
const ORIGINAL_SECRET = process.env.BETTER_AUTH_SECRET;
|
|
106
|
-
|
|
107
|
-
afterEach(() => {
|
|
108
|
-
if (ORIGINAL_BACKEND === undefined) delete process.env.DATA_BACKEND;
|
|
109
|
-
else process.env.DATA_BACKEND = ORIGINAL_BACKEND;
|
|
110
|
-
if (ORIGINAL_SECRET === undefined) delete process.env.BETTER_AUTH_SECRET;
|
|
111
|
-
else process.env.BETTER_AUTH_SECRET = ORIGINAL_SECRET;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("returns undefined on the AWS path (unsigned URLs)", () => {
|
|
115
|
-
delete process.env.DATA_BACKEND;
|
|
116
|
-
assert.equal(getContentSigner(), undefined);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("returns a working signer in SQLite mode", () => {
|
|
120
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
121
|
-
process.env.BETTER_AUTH_SECRET = SECRET;
|
|
122
|
-
const signer = getContentSigner();
|
|
123
|
-
assert.ok(signer);
|
|
124
|
-
const { exp, sig } = signer(PATH_A);
|
|
125
|
-
assert.deepEqual(
|
|
126
|
-
verifyContentSignature(PATH_A, String(exp), sig, SECRET, nowSeconds()),
|
|
127
|
-
{ valid: true },
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("throws in SQLite mode when the master secret is missing (fail loud)", () => {
|
|
132
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
133
|
-
delete process.env.BETTER_AUTH_SECRET;
|
|
134
|
-
assert.throws(() => getContentSigner(), /BETTER_AUTH_SECRET/);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe("the wire format is pinned, not merely self-consistent", () => {
|
|
139
|
-
/**
|
|
140
|
-
* Every other test here round-trips through this module, so a change to the
|
|
141
|
-
* canonical message or the key derivation passes them all while breaking every
|
|
142
|
-
* `/content` URL already in a browser. This vector was computed once, by hand,
|
|
143
|
-
* from the scheme as shipped: HMAC-SHA256 with a subkey derived from the
|
|
144
|
-
* master secret under `remit-content-url-signing-v1`, over `path\nexp`,
|
|
145
|
-
* base64url. If it fails, the format moved and in-flight URLs moved with it.
|
|
146
|
-
*/
|
|
147
|
-
it("still produces the signature it produced when the scheme shipped", () => {
|
|
148
|
-
const signer = createContentSigner(
|
|
149
|
-
"a-signing-secret-of-at-least-32-characters",
|
|
150
|
-
);
|
|
151
|
-
const originalNow = Date.now;
|
|
152
|
-
Date.now = () => 1_800_000_000_000 - CONTENT_URL_TTL_SECONDS * 1000;
|
|
153
|
-
try {
|
|
154
|
-
const { exp, sig } = signer("accounts/cfg1/acc1/messages/msg1/parts/1.2");
|
|
155
|
-
assert.equal(exp, 1_800_000_000);
|
|
156
|
-
assert.equal(sig, "eFKwOYf42a6DWwaZahIm7aVtWuqMOfhOAPoyxtpFybo");
|
|
157
|
-
} finally {
|
|
158
|
-
Date.now = originalNow;
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
createContentSigner,
|
|
5
|
-
verifyContentSignature,
|
|
6
|
-
} from "./contentSignature.js";
|
|
7
|
-
import {
|
|
8
|
-
buildContentUrl,
|
|
9
|
-
buildStorageContentUrl,
|
|
10
|
-
getContentDeliveryDomain,
|
|
11
|
-
} from "./contentUrl.js";
|
|
12
|
-
|
|
13
|
-
describe("buildContentUrl", () => {
|
|
14
|
-
it("produces the /content/accounts/{cfg}/{acc}/messages/{msg}/parts/{part} layout the Lambda@Edge expects", () => {
|
|
15
|
-
const url = buildContentUrl({
|
|
16
|
-
domain: "https://abc123.cloudfront.net",
|
|
17
|
-
accountConfigId: "cfg-alice",
|
|
18
|
-
accountId: "acc-alice",
|
|
19
|
-
messageId: "msg-1",
|
|
20
|
-
partPath: "1.2",
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
assert.equal(
|
|
24
|
-
url,
|
|
25
|
-
"https://abc123.cloudfront.net/content/accounts/cfg-alice/acc-alice/messages/msg-1/parts/1.2",
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("prepends https:// when the domain is given as a bare hostname", () => {
|
|
30
|
-
const url = buildContentUrl({
|
|
31
|
-
domain: "abc123.cloudfront.net",
|
|
32
|
-
accountConfigId: "cfg-alice",
|
|
33
|
-
accountId: "acc-alice",
|
|
34
|
-
messageId: "msg-1",
|
|
35
|
-
partPath: "1",
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
assert.equal(url.startsWith("https://abc123.cloudfront.net/"), true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("strips a trailing slash on the domain so the path doesn't double up", () => {
|
|
42
|
-
const url = buildContentUrl({
|
|
43
|
-
domain: "https://abc123.cloudfront.net/",
|
|
44
|
-
accountConfigId: "cfg-alice",
|
|
45
|
-
accountId: "acc-alice",
|
|
46
|
-
messageId: "msg-1",
|
|
47
|
-
partPath: "1",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
assert.equal(
|
|
51
|
-
url,
|
|
52
|
-
"https://abc123.cloudfront.net/content/accounts/cfg-alice/acc-alice/messages/msg-1/parts/1",
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("encodes each segment of partPath defensively (forward-slash kept as separator)", () => {
|
|
57
|
-
const url = buildContentUrl({
|
|
58
|
-
domain: "https://cdn.test",
|
|
59
|
-
accountConfigId: "cfg",
|
|
60
|
-
accountId: "acc",
|
|
61
|
-
messageId: "msg",
|
|
62
|
-
partPath: "1/strange path/3",
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
assert.equal(
|
|
66
|
-
url,
|
|
67
|
-
"https://cdn.test/content/accounts/cfg/acc/messages/msg/parts/1/strange%20path/3",
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("addresses an outbox attachment by its storage key", () => {
|
|
72
|
-
const url = buildStorageContentUrl({
|
|
73
|
-
domain: "https://cdn.test",
|
|
74
|
-
storageKey: "accounts/cfg/acc/outbox/ob-1/attachments/att-1",
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
assert.equal(
|
|
78
|
-
url,
|
|
79
|
-
"https://cdn.test/content/accounts/cfg/acc/outbox/ob-1/attachments/att-1",
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("leaves the URL unsigned when no signer is provided (AWS/Lambda@Edge path)", () => {
|
|
84
|
-
const url = buildContentUrl({
|
|
85
|
-
domain: "https://abc123.cloudfront.net",
|
|
86
|
-
accountConfigId: "cfg-alice",
|
|
87
|
-
accountId: "acc-alice",
|
|
88
|
-
messageId: "msg-1",
|
|
89
|
-
partPath: "1",
|
|
90
|
-
});
|
|
91
|
-
assert.equal(url.includes("?"), false);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("appends an exp/sig signature over the decoded storage path when a signer is provided", () => {
|
|
95
|
-
const secret = "test-master-secret-at-least-32-chars-long";
|
|
96
|
-
const url = buildContentUrl({
|
|
97
|
-
domain: "https://api.example.test",
|
|
98
|
-
accountConfigId: "cfg-alice",
|
|
99
|
-
accountId: "acc-alice",
|
|
100
|
-
messageId: "msg-1",
|
|
101
|
-
partPath: "1.2",
|
|
102
|
-
sign: createContentSigner(secret),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const parsed = new URL(url);
|
|
106
|
-
const exp = parsed.searchParams.get("exp");
|
|
107
|
-
const sig = parsed.searchParams.get("sig");
|
|
108
|
-
assert.ok(exp);
|
|
109
|
-
assert.ok(sig);
|
|
110
|
-
// The signature must verify against the same decoded storage path the
|
|
111
|
-
// `/content` route recomputes from `req.path`.
|
|
112
|
-
const relativePath = parsed.pathname.replace(/^\/content\//, "");
|
|
113
|
-
const result = verifyContentSignature(
|
|
114
|
-
relativePath,
|
|
115
|
-
exp,
|
|
116
|
-
sig,
|
|
117
|
-
secret,
|
|
118
|
-
Math.floor(Date.now() / 1000),
|
|
119
|
-
);
|
|
120
|
-
assert.deepEqual(result, { valid: true });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("nests accountConfigId before accountId so the Lambda@Edge prefix check matches", () => {
|
|
124
|
-
const url = buildContentUrl({
|
|
125
|
-
domain: "https://cdn.test",
|
|
126
|
-
accountConfigId: "CFG",
|
|
127
|
-
accountId: "ACC",
|
|
128
|
-
messageId: "MSG",
|
|
129
|
-
partPath: "1",
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const segments = new URL(url).pathname.split("/");
|
|
133
|
-
// ["", "content", "accounts", "CFG", "ACC", "messages", "MSG", "parts", "1"]
|
|
134
|
-
assert.equal(segments[1], "content");
|
|
135
|
-
assert.equal(segments[2], "accounts");
|
|
136
|
-
assert.equal(segments[3], "CFG");
|
|
137
|
-
assert.equal(segments[4], "ACC");
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe("getContentDeliveryDomain", () => {
|
|
142
|
-
const ORIGINAL = process.env.CONTENT_DELIVERY_DOMAIN;
|
|
143
|
-
|
|
144
|
-
beforeEach(() => {
|
|
145
|
-
delete process.env.CONTENT_DELIVERY_DOMAIN;
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
afterEach(() => {
|
|
149
|
-
if (ORIGINAL === undefined) delete process.env.CONTENT_DELIVERY_DOMAIN;
|
|
150
|
-
else process.env.CONTENT_DELIVERY_DOMAIN = ORIGINAL;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("throws when CONTENT_DELIVERY_DOMAIN is unset (fail loud over silent placeholder, #299)", () => {
|
|
154
|
-
assert.throws(
|
|
155
|
-
() => getContentDeliveryDomain(),
|
|
156
|
-
/CONTENT_DELIVERY_DOMAIN is not set/,
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("throws when CONTENT_DELIVERY_DOMAIN is empty", () => {
|
|
161
|
-
process.env.CONTENT_DELIVERY_DOMAIN = "";
|
|
162
|
-
assert.throws(
|
|
163
|
-
() => getContentDeliveryDomain(),
|
|
164
|
-
/CONTENT_DELIVERY_DOMAIN is not set/,
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("returns the env var value when set", () => {
|
|
169
|
-
process.env.CONTENT_DELIVERY_DOMAIN = "https://abc.cloudfront.net";
|
|
170
|
-
assert.equal(getContentDeliveryDomain(), "https://abc.cloudfront.net");
|
|
171
|
-
});
|
|
172
|
-
});
|