@remit/backend 0.0.144 → 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/src/api.d.ts +0 -2
- package/dist/src/api.js +9 -80
- package/dist/src/auth.d.ts +13 -19
- package/dist/src/auth.js +51 -43
- 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 +18 -29
- package/dist/src/config/msoauth.js +4 -28
- package/dist/src/derive/contentSignature.d.ts +3 -4
- package/dist/src/derive/contentSignature.js +4 -13
- package/dist/src/derive/contentUrl.d.ts +0 -13
- package/dist/src/derive/contentUrl.js +0 -19
- package/dist/src/handlers/account-oauth.d.ts +4 -3
- package/dist/src/handlers/account-oauth.js +197 -250
- package/dist/src/handlers/account.d.ts +4 -3
- package/dist/src/handlers/account.js +26 -39
- package/dist/src/handlers/address.d.ts +2 -2
- package/dist/src/handlers/address.js +11 -19
- package/dist/src/handlers/calendar-event.js +18 -26
- package/dist/src/handlers/calendar-feed.d.ts +2 -2
- package/dist/src/handlers/calendar-feed.js +24 -32
- package/dist/src/handlers/calendar-suggestion.js +18 -26
- package/dist/src/handlers/calendar.d.ts +1 -1
- package/dist/src/handlers/calendar.js +15 -22
- package/dist/src/handlers/config.d.ts +5 -5
- package/dist/src/handlers/config.js +23 -35
- package/dist/src/handlers/filter.d.ts +2 -2
- package/dist/src/handlers/filter.js +17 -32
- package/dist/src/handlers/folder-role.js +3 -6
- package/dist/src/handlers/index.d.ts +88 -2
- package/dist/src/handlers/index.js +6 -4
- package/dist/src/handlers/label.js +15 -22
- package/dist/src/handlers/mailbox.d.ts +3 -3
- package/dist/src/handlers/mailbox.js +18 -26
- package/dist/src/handlers/me.d.ts +1 -1
- package/dist/src/handlers/me.js +26 -47
- package/dist/src/handlers/message.d.ts +3 -5
- package/dist/src/handlers/message.js +34 -46
- package/dist/src/handlers/organize.d.ts +2 -2
- package/dist/src/handlers/organize.js +11 -20
- package/dist/src/handlers/outbox-attachment.d.ts +5 -3
- package/dist/src/handlers/outbox-attachment.js +11 -15
- package/dist/src/handlers/outbox.d.ts +3 -3
- package/dist/src/handlers/outbox.js +27 -35
- package/dist/src/handlers/sync.d.ts +5 -5
- package/dist/src/handlers/sync.js +13 -22
- package/dist/src/handlers/system-update.js +14 -27
- package/dist/src/handlers/thread.js +9 -14
- package/dist/src/handlers/unified-threads.js +3 -6
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +18 -2
- package/dist/src/jwt-auth.d.ts +9 -12
- package/dist/src/jwt-auth.js +19 -71
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +8 -2
- package/dist/src/request.js +16 -6
- package/dist/src/response.d.ts +20 -3
- package/dist/src/response.js +15 -22
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +2 -4
- package/dist/src/service/account-deletion.js +3 -9
- package/dist/src/service/compose-sqlite.d.ts +2 -1
- package/dist/src/service/compose-sqlite.js +5 -3
- package/dist/src/service/create-remit-client.d.ts +25 -11
- package/dist/src/service/create-remit-client.js +96 -115
- package/dist/src/service/data-client.js +3 -5
- package/dist/src/service/export-identity.d.ts +2 -1
- package/dist/src/service/export-identity.js +4 -6
- package/dist/src/service/fire-and-forget.js +1 -2
- package/dist/src/service/organize.d.ts +7 -10
- package/dist/src/service/organize.js +10 -27
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- 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 +11 -5
- package/dist/src/service/trigger-sync.js +10 -18
- package/dist/src/types.d.ts +24 -1
- package/package.json +20 -8
- package/dist/src/data-backend.d.ts +0 -12
- package/dist/src/data-backend.js +0 -12
- package/dist/src/request-context.d.ts +0 -9
- package/dist/src/request-context.js +0 -22
- package/dist/src/service/sqs.d.ts +0 -1
- package/dist/src/service/sqs.js +0 -3
|
@@ -2,15 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ConflictError, HTTPError } from "@remit/data-ports/errors";
|
|
5
|
-
import { getSubFromEvent } from "../auth.js";
|
|
6
5
|
import { defaultErrorCode } from "../error.js";
|
|
7
6
|
class TargetVersionMismatchError extends HTTPError {
|
|
8
7
|
name = "TargetVersionMismatchError";
|
|
9
8
|
statusCode = 422;
|
|
10
9
|
}
|
|
11
|
-
const DEFAULT_CONTROL_DIR = "/data/control";
|
|
12
|
-
const controlDir = () => process.env.REMIT_UPDATE_CONTROL_DIR ?? DEFAULT_CONTROL_DIR;
|
|
13
|
-
const manifestUrl = () => process.env.REMIT_UPDATE_MANIFEST_URL;
|
|
14
10
|
/**
|
|
15
11
|
* The running version is a fact the updater owns: it reads the tag from `.env`
|
|
16
12
|
* and writes it into `state.json`, which is the only honest source across an
|
|
@@ -33,24 +29,23 @@ const refuse = (statusCode, message) => ({
|
|
|
33
29
|
body: { code: defaultErrorCode(statusCode), message },
|
|
34
30
|
});
|
|
35
31
|
const notFound = () => refuse(404, "Not found");
|
|
36
|
-
const unauthorized = () => refuse(401, "Unauthorized");
|
|
37
32
|
/**
|
|
38
33
|
* The self-update seam is off unless a manifest URL is configured (RFC 037 D8),
|
|
39
34
|
* which the hosted deployment leaves unset. Both endpoints answer 404 in that
|
|
40
35
|
* case. Where it is on, any authenticated caller may use it — the account list
|
|
41
36
|
* is the trust boundary of a self-hosted instance.
|
|
42
37
|
*/
|
|
43
|
-
const guardManifestConfigured = () =>
|
|
38
|
+
const guardManifestConfigured = (config) => config.selfUpdate === "Enabled" ? null : notFound();
|
|
44
39
|
/**
|
|
45
40
|
* Read `state.json` off the control volume. A missing file is an instance that
|
|
46
41
|
* has never checked or updated (RFC 037 D8), not an error, so it reads as
|
|
47
42
|
* `null`; the caller renders the disabled/empty resource. Any other read or
|
|
48
43
|
* parse failure propagates.
|
|
49
44
|
*/
|
|
50
|
-
const readState = () => {
|
|
45
|
+
const readState = (controlDir) => {
|
|
51
46
|
let raw;
|
|
52
47
|
try {
|
|
53
|
-
raw = readFileSync(join(controlDir
|
|
48
|
+
raw = readFileSync(join(controlDir, "state.json"), "utf8");
|
|
54
49
|
}
|
|
55
50
|
catch (error) {
|
|
56
51
|
if (error.code === "ENOENT")
|
|
@@ -78,8 +73,7 @@ const checkedVersion = (state) => state?.check.status === "ok" ? state.check.lat
|
|
|
78
73
|
* so the backend never writes one. The temp file sits in the same directory as
|
|
79
74
|
* the target for the rename to be atomic on one filesystem.
|
|
80
75
|
*/
|
|
81
|
-
const writeRequest = (request) => {
|
|
82
|
-
const dir = controlDir();
|
|
76
|
+
const writeRequest = (dir, request) => {
|
|
83
77
|
const tmp = join(dir, `.request.json.${request.runId}.tmp`);
|
|
84
78
|
writeFileSync(tmp, JSON.stringify(request), { mode: 0o644 });
|
|
85
79
|
renameSync(tmp, join(dir, "request.json"));
|
|
@@ -91,8 +85,7 @@ const writeRequest = (request) => {
|
|
|
91
85
|
* request.json it is written atomically and carries no authority: its presence
|
|
92
86
|
* is the whole message, so it is empty.
|
|
93
87
|
*/
|
|
94
|
-
const writeCheckRequest = () => {
|
|
95
|
-
const dir = controlDir();
|
|
88
|
+
const writeCheckRequest = (dir) => {
|
|
96
89
|
const tmp = join(dir, `.check-request.json.tmp`);
|
|
97
90
|
writeFileSync(tmp, JSON.stringify({}), { mode: 0o644 });
|
|
98
91
|
renameSync(tmp, join(dir, "check-request.json"));
|
|
@@ -123,13 +116,10 @@ const requestedResource = (state, runId, targetVersion, requestedAt) => {
|
|
|
123
116
|
};
|
|
124
117
|
};
|
|
125
118
|
export const SystemOperations = {
|
|
126
|
-
SystemOperations_getSystemUpdate: async (context,
|
|
127
|
-
const offSurface = guardManifestConfigured();
|
|
119
|
+
SystemOperations_getSystemUpdate: async (context, ctx) => {
|
|
120
|
+
const offSurface = guardManifestConfigured(ctx.config);
|
|
128
121
|
if (offSurface)
|
|
129
122
|
return offSurface;
|
|
130
|
-
const event = args[0];
|
|
131
|
-
if (!getSubFromEvent(event))
|
|
132
|
-
return unauthorized();
|
|
133
123
|
// A refresh asks for a fresh answer, and only the updater can fetch one, so
|
|
134
124
|
// the request goes on the control seam for its watch loop to pick up (#599).
|
|
135
125
|
// The answer is the stored state as it stands, `lastCheckedAt` included: the
|
|
@@ -137,19 +127,16 @@ export const SystemOperations = {
|
|
|
137
127
|
// response never has to claim a verdict the updater has not reached.
|
|
138
128
|
const { refresh } = context.request.query;
|
|
139
129
|
if (refresh === true)
|
|
140
|
-
writeCheckRequest();
|
|
141
|
-
return readState() ?? emptyResource();
|
|
130
|
+
writeCheckRequest(ctx.config.updateControlDir);
|
|
131
|
+
return readState(ctx.config.updateControlDir) ?? emptyResource();
|
|
142
132
|
},
|
|
143
|
-
SystemOperations_applySystemUpdate: async (context,
|
|
144
|
-
const offSurface = guardManifestConfigured();
|
|
133
|
+
SystemOperations_applySystemUpdate: async (context, ctx) => {
|
|
134
|
+
const offSurface = guardManifestConfigured(ctx.config);
|
|
145
135
|
if (offSurface)
|
|
146
136
|
return offSurface;
|
|
147
|
-
const
|
|
148
|
-
const sub = getSubFromEvent(event);
|
|
149
|
-
if (!sub)
|
|
150
|
-
return unauthorized();
|
|
137
|
+
const sub = ctx.identity.sub;
|
|
151
138
|
const { targetVersion } = context.request.requestBody;
|
|
152
|
-
const state = readState();
|
|
139
|
+
const state = readState(ctx.config.updateControlDir);
|
|
153
140
|
if (isRunInFlight(state?.run)) {
|
|
154
141
|
throw new ConflictError("An update is already in progress.");
|
|
155
142
|
}
|
|
@@ -158,7 +145,7 @@ export const SystemOperations = {
|
|
|
158
145
|
}
|
|
159
146
|
const runId = randomUUID();
|
|
160
147
|
const requestedAt = new Date().toISOString();
|
|
161
|
-
writeRequest({
|
|
148
|
+
writeRequest(ctx.config.updateControlDir, {
|
|
162
149
|
runId,
|
|
163
150
|
targetVersion,
|
|
164
151
|
requestedAt,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { NotFoundError } from "@remit/data-ports/errors";
|
|
2
2
|
import { isValidMessageId } from "@remit/data-ports/id";
|
|
3
|
-
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
4
3
|
import { enrichThreadRows, } from "../derive/enrichThreadRows.js";
|
|
5
4
|
import { filterByOffRowCriteria, hasOffRowCriteria, } from "../derive/filterThreadCriteria.js";
|
|
6
|
-
import { getClient } from "../service/data-client.js";
|
|
7
5
|
const DEFAULT_THREADS_PAGE_SIZE = 50;
|
|
8
6
|
// Project to fields used by ListThreadsResponse — keep in sync with handler mapping.
|
|
9
7
|
// Includes ThreadMessage table key fields (accountConfigId, threadMessageId) and
|
|
@@ -183,12 +181,11 @@ export const dedupeThreadMessages = (rows) => {
|
|
|
183
181
|
});
|
|
184
182
|
};
|
|
185
183
|
export const ThreadOperations = {
|
|
186
|
-
ThreadOperations_listThreads: async (context,
|
|
187
|
-
const
|
|
188
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
184
|
+
ThreadOperations_listThreads: async (context, ctx) => {
|
|
185
|
+
const accountConfigId = ctx.identity.accountConfigId;
|
|
189
186
|
const { mailboxId } = context.request.params;
|
|
190
187
|
const { continuationToken, order } = context.request.query;
|
|
191
|
-
const client =
|
|
188
|
+
const client = ctx.client;
|
|
192
189
|
const result = await client.threadMessage.listByMailbox(accountConfigId, mailboxId, buildListThreadsOptions({ continuationToken, order }));
|
|
193
190
|
const items = await enrichThreadRows(result.items, client, accountConfigId);
|
|
194
191
|
return {
|
|
@@ -196,12 +193,11 @@ export const ThreadOperations = {
|
|
|
196
193
|
continuationToken: result.continuationToken,
|
|
197
194
|
};
|
|
198
195
|
},
|
|
199
|
-
ThreadOperations_searchThreads: async (context,
|
|
200
|
-
const
|
|
201
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
196
|
+
ThreadOperations_searchThreads: async (context, ctx) => {
|
|
197
|
+
const accountConfigId = ctx.identity.accountConfigId;
|
|
202
198
|
const { mailboxId } = context.request.params;
|
|
203
199
|
const raw = context.request.query;
|
|
204
|
-
return executeThreadSearch(
|
|
200
|
+
return executeThreadSearch(ctx.client, accountConfigId, mailboxId, {
|
|
205
201
|
...raw,
|
|
206
202
|
senderTrust: toArray(raw.senderTrust),
|
|
207
203
|
category: toArray(raw.category),
|
|
@@ -209,12 +205,11 @@ export const ThreadOperations = {
|
|
|
209
205
|
},
|
|
210
206
|
};
|
|
211
207
|
export const ThreadDetailOperations = {
|
|
212
|
-
ThreadDetailOperations_listThreadMessages: async (context,
|
|
213
|
-
const
|
|
214
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
208
|
+
ThreadDetailOperations_listThreadMessages: async (context, ctx) => {
|
|
209
|
+
const accountConfigId = ctx.identity.accountConfigId;
|
|
215
210
|
const { threadId } = context.request.params;
|
|
216
211
|
const { order } = context.request.query;
|
|
217
|
-
const client =
|
|
212
|
+
const client = ctx.client;
|
|
218
213
|
const result = await client.threadMessage.listByThread(threadId, accountConfigId, buildListThreadMessagesOptions({ order }));
|
|
219
214
|
// Defense-in-depth: the query is already scoped to accountConfigId; assert
|
|
220
215
|
// no foreign row slips into the response. `read`-class → 404, no existence
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { isVirtualCopyMailbox } from "@remit/data-ports/virtual-copy";
|
|
2
2
|
import { MailboxSpecialUse } from "@remit/domain-enums";
|
|
3
3
|
import pMap from "p-map";
|
|
4
|
-
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
5
4
|
import { enrichThreadRows, } from "../derive/enrichThreadRows.js";
|
|
6
|
-
import { getClient } from "../service/data-client.js";
|
|
7
5
|
import { groupAccountOverrides, groupMailboxOverrides, } from "./account-overrides.js";
|
|
8
6
|
const DEFAULT_UNIFIED_THREADS_PAGE_SIZE = 50;
|
|
9
7
|
const MAILBOX_LIST_CONCURRENCY = 5;
|
|
@@ -374,9 +372,8 @@ const toBoolean = (value) => {
|
|
|
374
372
|
return undefined;
|
|
375
373
|
};
|
|
376
374
|
export const UnifiedThreadOperations = {
|
|
377
|
-
UnifiedThreadOperations_listAllThreads: async (context,
|
|
378
|
-
const
|
|
379
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
375
|
+
UnifiedThreadOperations_listAllThreads: async (context, ctx) => {
|
|
376
|
+
const accountConfigId = ctx.identity.accountConfigId;
|
|
380
377
|
const { continuationToken, order, limit, starred, query, from, subject, accountId, mailboxId, category, unread, attachments, muted, count, results, } = context.request.query;
|
|
381
378
|
// Whitespace-only text is not a search: it would widen the scope to every
|
|
382
379
|
// folder while matching nothing in particular. The two field parameters
|
|
@@ -384,7 +381,7 @@ export const UnifiedThreadOperations = {
|
|
|
384
381
|
const searchText = query?.trim();
|
|
385
382
|
const fromText = from?.trim();
|
|
386
383
|
const subjectText = subject?.trim();
|
|
387
|
-
return executeUnifiedThreadListing(
|
|
384
|
+
return executeUnifiedThreadListing(ctx.client, accountConfigId, {
|
|
388
385
|
continuationToken,
|
|
389
386
|
order,
|
|
390
387
|
limit,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { OpenAPISpec } from "./api.js";
|
|
2
|
+
export { configFromEnv } from "./config/from-env.js";
|
|
3
|
+
export { createApiEventHandler } from "./lambda.js";
|
|
4
|
+
export declare const handleApiEvent: import("./lambda.js").ApiEventHandler;
|
|
2
5
|
export declare const handler: (event: import("aws-lambda").APIGatewayProxyEvent, context: import("aws-lambda").Context) => Promise<import("aws-lambda").APIGatewayProxyResult>;
|
package/dist/src/index.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { inspect } from "node:util";
|
|
2
2
|
import { logger } from "@remit/logger";
|
|
3
3
|
import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
|
|
4
|
-
import {
|
|
4
|
+
import { assertLocalBypassNotInDeployedEnv, localIdentityFromEnv, } from "./auth.js";
|
|
5
|
+
import { configFromEnv } from "./config/from-env.js";
|
|
6
|
+
import { handlers, publicHandlers } from "./handlers/index.js";
|
|
7
|
+
import { createApiEventHandler } from "./lambda.js";
|
|
8
|
+
import { serveApi } from "./serve.js";
|
|
9
|
+
import { getClient } from "./service/data-client.js";
|
|
5
10
|
installNodeLogger();
|
|
6
|
-
|
|
11
|
+
// Boot-time refusal: the local auth-bypass overrides must never be set in a
|
|
12
|
+
// deployed environment, so a leaked env var fails the process at startup
|
|
13
|
+
// rather than silently opening authentication at runtime.
|
|
14
|
+
assertLocalBypassNotInDeployedEnv(process.env);
|
|
15
|
+
export { OpenAPISpec } from "./api.js";
|
|
16
|
+
export { configFromEnv } from "./config/from-env.js";
|
|
17
|
+
export { createApiEventHandler } from "./lambda.js";
|
|
18
|
+
export const handleApiEvent = createApiEventHandler(serveApi({ handlers, publicHandlers }), {
|
|
19
|
+
config: configFromEnv(process.env),
|
|
20
|
+
localIdentity: localIdentityFromEnv(process.env),
|
|
21
|
+
client: getClient,
|
|
22
|
+
});
|
|
7
23
|
/**
|
|
8
24
|
* Last-resort net for a leaked background rejection.
|
|
9
25
|
*
|
package/dist/src/jwt-auth.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { type JwtVerifier } from "@remit/auth-service/verifier";
|
|
2
|
-
import type { APIGatewayProxyEvent
|
|
2
|
+
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
3
|
+
import type { HandlerIdentity } from "./types.js";
|
|
3
4
|
/** Test-only override for the memoized verifier. Pass null to reset. */
|
|
4
5
|
export declare const _setVerifierForTest: (v: JwtVerifier | null) => void;
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* The caller of a self-host request, from a better-auth RS256 JWT.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
9
|
+
* Claims the edge tier (APISIX) already verified and injected are trusted as
|
|
10
|
+
* they stand. Otherwise a bearer token is verified here; a token that fails
|
|
11
|
+
* verification is an anonymous caller, so the operation table answers 401. With
|
|
12
|
+
* no token the local bypass, when one is configured, is the caller. Only an
|
|
13
|
+
* operation that needs a caller resolves this, so a public route never does.
|
|
17
14
|
*/
|
|
18
|
-
export declare const
|
|
15
|
+
export declare const identityFromSelfHostRequest: (event: APIGatewayProxyEvent, localIdentity: HandlerIdentity) => Promise<HandlerIdentity>;
|
package/dist/src/jwt-auth.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createJwtVerifier, extractBearerToken, resolveVerifierConfig, } from "@remit/auth-service/verifier";
|
|
2
|
-
import { readCalendarFeedToken } from "@remit/calendar-service";
|
|
3
2
|
import { logger } from "@remit/logger";
|
|
4
|
-
import {
|
|
5
|
-
import { formatResponse } from "./response.js";
|
|
3
|
+
import { getIdentityFromEvent, userFromClaims } from "./auth.js";
|
|
6
4
|
let verifier = null;
|
|
7
5
|
const getVerifier = () => {
|
|
8
6
|
if (!verifier)
|
|
@@ -20,78 +18,28 @@ const readHeader = (headers, name) => {
|
|
|
20
18
|
}
|
|
21
19
|
return undefined;
|
|
22
20
|
};
|
|
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
21
|
/**
|
|
65
|
-
*
|
|
22
|
+
* The caller of a self-host request, from a better-auth RS256 JWT.
|
|
66
23
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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.
|
|
24
|
+
* Claims the edge tier (APISIX) already verified and injected are trusted as
|
|
25
|
+
* they stand. Otherwise a bearer token is verified here; a token that fails
|
|
26
|
+
* verification is an anonymous caller, so the operation table answers 401. With
|
|
27
|
+
* no token the local bypass, when one is configured, is the caller. Only an
|
|
28
|
+
* operation that needs a caller resolves this, so a public route never does.
|
|
76
29
|
*/
|
|
77
|
-
export const
|
|
78
|
-
if (isPublicRoute(event))
|
|
79
|
-
return null;
|
|
30
|
+
export const identityFromSelfHostRequest = async (event, localIdentity) => {
|
|
80
31
|
const existingSub = event.requestContext?.authorizer?.claims?.sub;
|
|
81
|
-
if (typeof existingSub === "string" && existingSub.length > 0)
|
|
82
|
-
return
|
|
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;
|
|
32
|
+
if (typeof existingSub === "string" && existingSub.length > 0) {
|
|
33
|
+
return getIdentityFromEvent(event, localIdentity);
|
|
93
34
|
}
|
|
94
|
-
|
|
35
|
+
const token = extractBearerToken(readHeader(event.headers, "authorization"));
|
|
36
|
+
if (!token)
|
|
37
|
+
return localIdentity;
|
|
38
|
+
const claims = await getVerifier()(token).catch((err) => {
|
|
39
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, "better-auth JWT verification failed");
|
|
95
40
|
return null;
|
|
96
|
-
|
|
41
|
+
});
|
|
42
|
+
if (!claims)
|
|
43
|
+
return { kind: "anonymous" };
|
|
44
|
+
return userFromClaims(claims.sub, claims.email);
|
|
97
45
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";
|
|
2
|
+
import type { ApiConfig } from "./config/api-config.js";
|
|
3
|
+
import type { ServedApi } from "./serve.js";
|
|
4
|
+
import type { RemitClient } from "./service/data-client.js";
|
|
5
|
+
import type { QueueName } from "./service/queues.js";
|
|
6
|
+
import type { HandlerIdentity } from "./types.js";
|
|
7
|
+
export interface ApiEventHandlerOptions<K extends QueueName> {
|
|
8
|
+
config: ApiConfig;
|
|
9
|
+
localIdentity: HandlerIdentity;
|
|
10
|
+
client: () => Promise<RemitClient<K>>;
|
|
11
|
+
}
|
|
12
|
+
export type ApiEventHandler = (event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>;
|
|
13
|
+
export declare const createApiEventHandler: <K extends QueueName>(served: ServedApi<K>, { config, localIdentity, client }: ApiEventHandlerOptions<NoInfer<K>>) => ApiEventHandler;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getIdentityFromEvent } from "./auth.js";
|
|
2
|
+
import { identityFromSelfHostRequest } from "./jwt-auth.js";
|
|
3
|
+
import { normalizeRequest } from "./request.js";
|
|
4
|
+
const queryOf = (event) => {
|
|
5
|
+
if (event.multiValueQueryStringParameters) {
|
|
6
|
+
return event.multiValueQueryStringParameters;
|
|
7
|
+
}
|
|
8
|
+
return Object.fromEntries(Object.entries(event.queryStringParameters ?? {}).map(([key, value]) => [
|
|
9
|
+
key,
|
|
10
|
+
value === undefined ? undefined : [value],
|
|
11
|
+
]));
|
|
12
|
+
};
|
|
13
|
+
const headersOf = (event) => ({
|
|
14
|
+
...event.headers,
|
|
15
|
+
...Object.fromEntries(Object.entries(event.multiValueHeaders ?? {}).map(([name, values]) => [
|
|
16
|
+
name,
|
|
17
|
+
values?.join(", "),
|
|
18
|
+
])),
|
|
19
|
+
});
|
|
20
|
+
const normalizeApiGatewayEvent = (event) => normalizeRequest({
|
|
21
|
+
method: event.httpMethod,
|
|
22
|
+
path: event.path,
|
|
23
|
+
query: queryOf(event),
|
|
24
|
+
headers: headersOf(event),
|
|
25
|
+
body: event.body,
|
|
26
|
+
});
|
|
27
|
+
export const createApiEventHandler = (served, { config, localIdentity, client }) => (event, context) => served.dispatch(normalizeApiGatewayEvent(event), {
|
|
28
|
+
config,
|
|
29
|
+
client,
|
|
30
|
+
identity: async () => config.auth.mode === "BetterAuthJwt"
|
|
31
|
+
? identityFromSelfHostRequest(event, localIdentity)
|
|
32
|
+
: getIdentityFromEvent(event, localIdentity),
|
|
33
|
+
}, context.awsRequestId);
|
package/dist/src/request.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
2
1
|
import type { Request } from "openapi-backend";
|
|
3
|
-
export
|
|
2
|
+
export interface HostRequest {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
query: Readonly<Record<string, readonly string[] | undefined>> | null;
|
|
6
|
+
headers: Readonly<Record<string, string | undefined>> | null;
|
|
7
|
+
body: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare const normalizeRequest: (request: HostRequest) => Request;
|
package/dist/src/request.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
const definedHeaders = (map) => Object.fromEntries(Object.entries(map ?? {}).filter((entry) => entry[1] !== undefined));
|
|
2
|
+
const collapsedQuery = (map) => {
|
|
3
|
+
const query = {};
|
|
4
|
+
for (const [key, values] of Object.entries(map ?? {})) {
|
|
5
|
+
if (!values || values.length === 0)
|
|
6
|
+
continue;
|
|
7
|
+
query[key] = values.length === 1 ? values[0] : [...values];
|
|
8
|
+
}
|
|
9
|
+
return query;
|
|
10
|
+
};
|
|
11
|
+
export const normalizeRequest = (request) => ({
|
|
12
|
+
method: request.method,
|
|
13
|
+
path: request.path,
|
|
14
|
+
query: collapsedQuery(request.query),
|
|
15
|
+
body: request.body === "" ? null : request.body,
|
|
16
|
+
headers: definedHeaders(request.headers),
|
|
7
17
|
});
|
package/dist/src/response.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { APIGatewayProxyResult } from "aws-lambda";
|
|
2
2
|
import type { Context as OpenAPIContext } from "openapi-backend";
|
|
3
|
+
import type { ApiConfig } from "./config/api-config.js";
|
|
3
4
|
declare const RAW_API_RESPONSE: unique symbol;
|
|
4
5
|
/**
|
|
5
6
|
* A response the caller reads by something other than its JSON body — an
|
|
@@ -10,8 +11,8 @@ declare const RAW_API_RESPONSE: unique symbol;
|
|
|
10
11
|
* stays the default; a handler that has to own its own media type or headers
|
|
11
12
|
* says so with `rawApiResponse` rather than by returning a shape this one has
|
|
12
13
|
* to guess at.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* `dispatch` still adds the envelope headers, so a raw response is not a way
|
|
15
|
+
* around them.
|
|
15
16
|
*/
|
|
16
17
|
export interface RawApiResponse {
|
|
17
18
|
statusCode: number;
|
|
@@ -22,6 +23,22 @@ type MarkedRawApiResponse = RawApiResponse & {
|
|
|
22
23
|
readonly [RAW_API_RESPONSE]: true;
|
|
23
24
|
};
|
|
24
25
|
export declare const rawApiResponse: (response: RawApiResponse) => MarkedRawApiResponse;
|
|
26
|
+
/**
|
|
27
|
+
* The headers every response carries whatever its body is: the CORS grant the
|
|
28
|
+
* browser needs, and the id the request's log lines are already tagged with.
|
|
29
|
+
* Returning that id is what makes a bug report's "correlation id" resolve to a
|
|
30
|
+
* server-side line; the browser can only read a non-safelisted header when it
|
|
31
|
+
* is exposed.
|
|
32
|
+
*/
|
|
33
|
+
export interface EnvelopeInput {
|
|
34
|
+
origin: string | undefined;
|
|
35
|
+
allowedOrigins: readonly string[];
|
|
36
|
+
correlationId: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const resolveAllowedOrigin: (origin: string | undefined, allowed: readonly string[]) => string;
|
|
39
|
+
export declare const envelopeHeaders: ({ origin, allowedOrigins, correlationId, }: EnvelopeInput) => Record<string, string>;
|
|
25
40
|
export declare const formatResponse: (body: Record<string, unknown>, statusCode?: number) => APIGatewayProxyResult;
|
|
26
|
-
export declare const postResponseHandler: (context: OpenAPIContext
|
|
41
|
+
export declare const postResponseHandler: (context: OpenAPIContext, scope: {
|
|
42
|
+
config: ApiConfig;
|
|
43
|
+
}) => APIGatewayProxyResult;
|
|
27
44
|
export {};
|
package/dist/src/response.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { logger } from "@remit/logger";
|
|
2
|
-
import { getRequestCorrelationId, getRequestOrigin, resolveAllowedOrigin, } from "./request-context.js";
|
|
3
2
|
const RAW_API_RESPONSE = Symbol("remit.rawApiResponse");
|
|
4
3
|
export const rawApiResponse = (response) => Object.assign(response, { [RAW_API_RESPONSE]: true });
|
|
5
4
|
const isRawApiResponse = (body) => RAW_API_RESPONSE in body;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const envelopeHeaders = () => {
|
|
14
|
-
const allowOrigin = resolveAllowedOrigin(
|
|
5
|
+
export const resolveAllowedOrigin = (origin, allowed) => {
|
|
6
|
+
if (allowed.length === 0)
|
|
7
|
+
return "*";
|
|
8
|
+
if (origin && allowed.includes(origin))
|
|
9
|
+
return origin;
|
|
10
|
+
return allowed[0];
|
|
11
|
+
};
|
|
12
|
+
export const envelopeHeaders = ({ origin, allowedOrigins, correlationId, }) => {
|
|
13
|
+
const allowOrigin = resolveAllowedOrigin(origin, allowedOrigins);
|
|
15
14
|
const headers = {
|
|
16
15
|
"Access-Control-Allow-Origin": allowOrigin,
|
|
17
16
|
"Access-Control-Allow-Headers": "Authorization,Content-Type",
|
|
@@ -21,11 +20,8 @@ const envelopeHeaders = () => {
|
|
|
21
20
|
if (allowOrigin !== "*") {
|
|
22
21
|
headers["Access-Control-Allow-Credentials"] = "true";
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
headers["x-correlation-id"] = correlationId;
|
|
27
|
-
headers["Access-Control-Expose-Headers"] = "x-correlation-id";
|
|
28
|
-
}
|
|
23
|
+
headers["x-correlation-id"] = correlationId;
|
|
24
|
+
headers["Access-Control-Expose-Headers"] = "x-correlation-id";
|
|
29
25
|
return headers;
|
|
30
26
|
};
|
|
31
27
|
export const formatResponse = (body, statusCode = 200) => {
|
|
@@ -36,7 +32,7 @@ export const formatResponse = (body, statusCode = 200) => {
|
|
|
36
32
|
logger.debug({ statusCode: body.statusCode }, "response");
|
|
37
33
|
return {
|
|
38
34
|
statusCode: body.statusCode,
|
|
39
|
-
headers:
|
|
35
|
+
headers: body.headers,
|
|
40
36
|
body: body.body,
|
|
41
37
|
};
|
|
42
38
|
}
|
|
@@ -46,18 +42,15 @@ export const formatResponse = (body, statusCode = 200) => {
|
|
|
46
42
|
logger.debug({ statusCode }, "response");
|
|
47
43
|
return {
|
|
48
44
|
statusCode: statusCode,
|
|
49
|
-
headers: {
|
|
50
|
-
"Content-Type": "application/json",
|
|
51
|
-
...envelopeHeaders(),
|
|
52
|
-
},
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
53
46
|
body: JSON.stringify(body),
|
|
54
47
|
};
|
|
55
48
|
};
|
|
56
|
-
export const postResponseHandler = (context) => {
|
|
49
|
+
export const postResponseHandler = (context, scope) => {
|
|
57
50
|
const { api, response, operation } = context;
|
|
58
51
|
if (response.statusCode)
|
|
59
52
|
return formatResponse(response, response.statusCode);
|
|
60
|
-
if (
|
|
53
|
+
if (scope.config.responseValidation === "Skip")
|
|
61
54
|
return formatResponse(response, 200);
|
|
62
55
|
const { valid, errors } = api.validateResponse(response, operation);
|
|
63
56
|
if (valid)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { APIGatewayProxyResult } from "aws-lambda";
|
|
2
|
+
import type { Request as OpenAPIRequest } from "openapi-backend";
|
|
3
|
+
import type { ApiConfig } from "./config/api-config.js";
|
|
4
|
+
import type { RemitClient } from "./service/data-client.js";
|
|
5
|
+
import type { QueueName } from "./service/queues.js";
|
|
6
|
+
import type { HandlerIdentity, OperationHandler, OperationIds, PublicOperationHandler, PublicOperationIds } from "./types.js";
|
|
7
|
+
export type { ApiConfig } from "./config/api-config.js";
|
|
8
|
+
export type { HandlerContext, HandlerIdentity, PublicHandlerContext, UserIdentity, } from "./types.js";
|
|
9
|
+
export interface RequestHost<K extends QueueName> {
|
|
10
|
+
config: ApiConfig;
|
|
11
|
+
client: () => Promise<RemitClient<K>>;
|
|
12
|
+
identity: () => Promise<HandlerIdentity>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The operations a host serves, each typed by the queues it sends to. An
|
|
16
|
+
* operation the host leaves out answers 501.
|
|
17
|
+
*/
|
|
18
|
+
export interface ApiRegistry<K extends QueueName> {
|
|
19
|
+
handlers: Partial<Record<Exclude<OperationIds, PublicOperationIds>, OperationHandler<OperationIds, K>>>;
|
|
20
|
+
publicHandlers: Partial<Record<PublicOperationIds, PublicOperationHandler<K>>>;
|
|
21
|
+
}
|
|
22
|
+
export interface ServedApi<K extends QueueName> {
|
|
23
|
+
dispatch: (request: OpenAPIRequest, host: RequestHost<K>, correlationId: string) => Promise<APIGatewayProxyResult>;
|
|
24
|
+
handleFetch: (request: Request, host: RequestHost<K>, correlationId: string) => Promise<Response>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Register a host's operations and return the two ways in: a normalised
|
|
28
|
+
* request, or a Fetch `Request`. Each request's host supplies the client, and
|
|
29
|
+
* the client's queues must cover every operation in the registry.
|
|
30
|
+
*/
|
|
31
|
+
export declare const serveApi: <K extends QueueName>(registry: ApiRegistry<NoInfer<K>>) => ServedApi<K>;
|