@remit/backend 0.0.150 → 0.0.152
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.
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import type { AccountService as AccountServiceName } from "@remit/api-openapi-types";
|
|
2
1
|
import type { Context } from "openapi-backend";
|
|
3
2
|
import type { HandlerContext, PublicOperationHandler } from "../types.js";
|
|
4
|
-
export interface OAuthState {
|
|
5
|
-
accountConfigId: string;
|
|
6
|
-
nonce: string;
|
|
7
|
-
timestamp: number;
|
|
8
|
-
services: AccountServiceName[];
|
|
9
|
-
}
|
|
10
|
-
export declare const STATE_TTL_MS: number;
|
|
11
|
-
export declare function signState(payload: OAuthState, clientSecret: string): Promise<string>;
|
|
12
|
-
export declare function verifyState(state: string, clientSecret: string): Promise<OAuthState>;
|
|
13
3
|
export declare function parseJwtClaims(token: string): Promise<Record<string, unknown> | null>;
|
|
14
4
|
export declare function getWebOrigin(origins: readonly string[]): string;
|
|
15
5
|
export declare const microsoftOAuthCallback: PublicOperationHandler<"sync">;
|
|
6
|
+
export interface SignInNotConfigured {
|
|
7
|
+
statusCode: 501;
|
|
8
|
+
body: {
|
|
9
|
+
code: "not_configured";
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
16
13
|
export declare const MicrosoftOAuthOperations: {
|
|
17
14
|
MicrosoftOAuthOperations_microsoftOAuthStart: (context: Context, ctx: HandlerContext) => Promise<{
|
|
18
15
|
authorizationUrl: string;
|
|
19
|
-
}>;
|
|
16
|
+
} | SignInNotConfigured>;
|
|
20
17
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { ensureAccountConfig } from "@remit/data-ports/account-config";
|
|
3
3
|
import { AccountAuthType, AccountService, ConnectionState, } from "@remit/domain-enums";
|
|
4
4
|
import { logger } from "@remit/logger";
|
|
@@ -6,63 +6,11 @@ import { createMailOAuthService, microsoftProviderConfig, microsoftServicesGrant
|
|
|
6
6
|
import { serializeEncryptedPayload } from "@remit/secrets-service";
|
|
7
7
|
import { loadMsOAuth } from "../config/msoauth.js";
|
|
8
8
|
import { safeJsonParse } from "../json.js";
|
|
9
|
+
import { signState, verifyState } from "../oauth-state.js";
|
|
9
10
|
import { rawApiResponse } from "../response.js";
|
|
10
11
|
import { triggerAccountSyncSafe } from "./account.js";
|
|
11
12
|
import { findActiveDuplicateMailbox } from "./account-guards.js";
|
|
12
13
|
const OUTLOOK_IMAP_HOST = "outlook.office365.com";
|
|
13
|
-
const ACCOUNT_SERVICES = Object.values(AccountService);
|
|
14
|
-
const isServiceList = (value) => Array.isArray(value) &&
|
|
15
|
-
value.length > 0 &&
|
|
16
|
-
value.every((service) => typeof service === "string" && ACCOUNT_SERVICES.includes(service));
|
|
17
|
-
export const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
18
|
-
/**
|
|
19
|
-
* Derive a signing key from the client secret, namespaced to avoid reuse.
|
|
20
|
-
* The key is HMAC-SHA256("oauth-state-signing:" + clientSecret).
|
|
21
|
-
*/
|
|
22
|
-
function deriveSigningKey(clientSecret) {
|
|
23
|
-
return createHmac("sha256", `oauth-state-signing:${clientSecret}`)
|
|
24
|
-
.update("")
|
|
25
|
-
.digest();
|
|
26
|
-
}
|
|
27
|
-
function base64urlEncode(buf) {
|
|
28
|
-
return buf.toString("base64url");
|
|
29
|
-
}
|
|
30
|
-
function base64urlDecode(str) {
|
|
31
|
-
return Buffer.from(str, "base64url");
|
|
32
|
-
}
|
|
33
|
-
export async function signState(payload, clientSecret) {
|
|
34
|
-
const key = deriveSigningKey(clientSecret);
|
|
35
|
-
const payloadBuf = Buffer.from(JSON.stringify(payload), "utf8");
|
|
36
|
-
const payloadEncoded = base64urlEncode(payloadBuf);
|
|
37
|
-
const sig = createHmac("sha256", key).update(payloadEncoded).digest();
|
|
38
|
-
const sigEncoded = base64urlEncode(sig);
|
|
39
|
-
return `${payloadEncoded}.${sigEncoded}`;
|
|
40
|
-
}
|
|
41
|
-
export async function verifyState(state, clientSecret) {
|
|
42
|
-
const dotIndex = state.lastIndexOf(".");
|
|
43
|
-
if (dotIndex < 0)
|
|
44
|
-
throw new Error("Malformed state: missing dot separator");
|
|
45
|
-
const payloadEncoded = state.slice(0, dotIndex);
|
|
46
|
-
const sigEncoded = state.slice(dotIndex + 1);
|
|
47
|
-
const key = deriveSigningKey(clientSecret);
|
|
48
|
-
const expectedSig = createHmac("sha256", key).update(payloadEncoded).digest();
|
|
49
|
-
const actualSig = base64urlDecode(sigEncoded);
|
|
50
|
-
if (actualSig.length !== expectedSig.length ||
|
|
51
|
-
!timingSafeEqual(actualSig, expectedSig)) {
|
|
52
|
-
throw new Error("State signature verification failed");
|
|
53
|
-
}
|
|
54
|
-
const payload = JSON.parse(base64urlDecode(payloadEncoded).toString("utf8"));
|
|
55
|
-
if (Date.now() - payload.timestamp > STATE_TTL_MS) {
|
|
56
|
-
throw new Error("State has expired");
|
|
57
|
-
}
|
|
58
|
-
if (payload.services === undefined) {
|
|
59
|
-
return { ...payload, services: [AccountService.Mail] };
|
|
60
|
-
}
|
|
61
|
-
if (!isServiceList(payload.services)) {
|
|
62
|
-
throw new Error("State names no requested services");
|
|
63
|
-
}
|
|
64
|
-
return payload;
|
|
65
|
-
}
|
|
66
14
|
// ─── JWT claims parsing ──────────────────────────────────────────────────────
|
|
67
15
|
export async function parseJwtClaims(token) {
|
|
68
16
|
const parts = token.split(".");
|
|
@@ -301,8 +249,22 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
301
249
|
}
|
|
302
250
|
return redirect(`${webOrigin}/settings/accounts?connected=${encodeURIComponent(accountId)}`);
|
|
303
251
|
};
|
|
252
|
+
/**
|
|
253
|
+
* An instance with no Microsoft app answers the start of a sign-in with a
|
|
254
|
+
* refusal the onboarding wizard can show, rather than the 500 the config load
|
|
255
|
+
* would otherwise throw.
|
|
256
|
+
*/
|
|
257
|
+
const signInNotConfigured = () => ({
|
|
258
|
+
statusCode: 501,
|
|
259
|
+
body: {
|
|
260
|
+
code: "not_configured",
|
|
261
|
+
message: "Microsoft sign-in is not configured on this instance; connect the account with an app password instead.",
|
|
262
|
+
},
|
|
263
|
+
});
|
|
304
264
|
export const MicrosoftOAuthOperations = {
|
|
305
265
|
MicrosoftOAuthOperations_microsoftOAuthStart: async (context, ctx) => {
|
|
266
|
+
if (ctx.config.msOAuth.mode === "Disabled")
|
|
267
|
+
return signInNotConfigured();
|
|
306
268
|
const accountConfigId = ctx.identity.accountConfigId;
|
|
307
269
|
const input = context.request.requestBody;
|
|
308
270
|
const email = typeof input.email === "string" ? input.email : undefined;
|
|
@@ -162,7 +162,7 @@ export declare const handlers: {
|
|
|
162
162
|
MailboxOperations_createMailbox: (context: import("openapi-backend").Context<any, any, any, any, any, import("openapi-backend").Document>, ctx: import("../types.js").HandlerContext<"sync">) => Promise<import("@remit/api-openapi-types").Components.Schemas.MailboxResponse>;
|
|
163
163
|
MicrosoftOAuthOperations_microsoftOAuthStart: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext) => Promise<{
|
|
164
164
|
authorizationUrl: string;
|
|
165
|
-
}>;
|
|
165
|
+
} | import("./account-oauth.js").SignInNotConfigured>;
|
|
166
166
|
AccountDetailOperations_updateAccount: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext) => Promise<import("@remit/api-openapi-types").AccountResponse>;
|
|
167
167
|
AccountDetailOperations_deleteAccount: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext<"accountFanout">) => Promise<import("@remit/api-openapi-types").DeleteAccountResponse>;
|
|
168
168
|
AccountOperations_createAccount: (context: import("openapi-backend").Context, ctx: import("../types.js").HandlerContext<"sync">) => Promise<import("@remit/api-openapi-types").AccountResponse>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AccountService as AccountServiceName } from "@remit/api-openapi-types";
|
|
2
|
+
export interface OAuthState {
|
|
3
|
+
accountConfigId: string;
|
|
4
|
+
nonce: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
services: AccountServiceName[];
|
|
7
|
+
}
|
|
8
|
+
export declare const STATE_TTL_MS: number;
|
|
9
|
+
export declare function signState(payload: OAuthState, clientSecret: string): Promise<string>;
|
|
10
|
+
export declare function verifyState(state: string, clientSecret: string): Promise<OAuthState>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { AccountService } from "@remit/domain-enums";
|
|
3
|
+
const ACCOUNT_SERVICES = Object.values(AccountService);
|
|
4
|
+
const isServiceList = (value) => Array.isArray(value) &&
|
|
5
|
+
value.length > 0 &&
|
|
6
|
+
value.every((service) => typeof service === "string" && ACCOUNT_SERVICES.includes(service));
|
|
7
|
+
export const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
8
|
+
/**
|
|
9
|
+
* Derive a signing key from the client secret, namespaced to avoid reuse.
|
|
10
|
+
* The key is HMAC-SHA256("oauth-state-signing:" + clientSecret).
|
|
11
|
+
*/
|
|
12
|
+
function deriveSigningKey(clientSecret) {
|
|
13
|
+
return createHmac("sha256", `oauth-state-signing:${clientSecret}`)
|
|
14
|
+
.update("")
|
|
15
|
+
.digest();
|
|
16
|
+
}
|
|
17
|
+
function base64urlEncode(buf) {
|
|
18
|
+
return buf.toString("base64url");
|
|
19
|
+
}
|
|
20
|
+
function base64urlDecode(str) {
|
|
21
|
+
return Buffer.from(str, "base64url");
|
|
22
|
+
}
|
|
23
|
+
export async function signState(payload, clientSecret) {
|
|
24
|
+
const key = deriveSigningKey(clientSecret);
|
|
25
|
+
const payloadBuf = Buffer.from(JSON.stringify(payload), "utf8");
|
|
26
|
+
const payloadEncoded = base64urlEncode(payloadBuf);
|
|
27
|
+
const sig = createHmac("sha256", key).update(payloadEncoded).digest();
|
|
28
|
+
const sigEncoded = base64urlEncode(sig);
|
|
29
|
+
return `${payloadEncoded}.${sigEncoded}`;
|
|
30
|
+
}
|
|
31
|
+
export async function verifyState(state, clientSecret) {
|
|
32
|
+
const dotIndex = state.lastIndexOf(".");
|
|
33
|
+
if (dotIndex < 0)
|
|
34
|
+
throw new Error("Malformed state: missing dot separator");
|
|
35
|
+
const payloadEncoded = state.slice(0, dotIndex);
|
|
36
|
+
const sigEncoded = state.slice(dotIndex + 1);
|
|
37
|
+
const key = deriveSigningKey(clientSecret);
|
|
38
|
+
const expectedSig = createHmac("sha256", key).update(payloadEncoded).digest();
|
|
39
|
+
const actualSig = base64urlDecode(sigEncoded);
|
|
40
|
+
if (actualSig.length !== expectedSig.length ||
|
|
41
|
+
!timingSafeEqual(actualSig, expectedSig)) {
|
|
42
|
+
throw new Error("State signature verification failed");
|
|
43
|
+
}
|
|
44
|
+
const payload = JSON.parse(base64urlDecode(payloadEncoded).toString("utf8"));
|
|
45
|
+
if (Date.now() - payload.timestamp > STATE_TTL_MS) {
|
|
46
|
+
throw new Error("State has expired");
|
|
47
|
+
}
|
|
48
|
+
if (payload.services === undefined) {
|
|
49
|
+
return { ...payload, services: [AccountService.Mail] };
|
|
50
|
+
}
|
|
51
|
+
if (!isServiceList(payload.services)) {
|
|
52
|
+
throw new Error("State names no requested services");
|
|
53
|
+
}
|
|
54
|
+
return payload;
|
|
55
|
+
}
|
|
@@ -339,6 +339,16 @@ export const backApplyFilter = async (deps, accountConfigId, filterId) => {
|
|
|
339
339
|
...nothing,
|
|
340
340
|
matched: match.messageIds.length,
|
|
341
341
|
};
|
|
342
|
+
const active = await client.filter.listByAccountAndState(accountConfigId, FilterState.Active);
|
|
343
|
+
const preparedByAccount = new Map();
|
|
344
|
+
const preparedFor = (accountId) => {
|
|
345
|
+
const known = preparedByAccount.get(accountId);
|
|
346
|
+
if (known)
|
|
347
|
+
return known;
|
|
348
|
+
const prepared = pipeline.prepare(accountConfigId, accountId, active);
|
|
349
|
+
preparedByAccount.set(accountId, prepared);
|
|
350
|
+
return prepared;
|
|
351
|
+
};
|
|
342
352
|
for (const messageId of match.messageIds) {
|
|
343
353
|
const message = await filterMessageOf(client, accountConfigId, messageId);
|
|
344
354
|
if (!message)
|
|
@@ -349,7 +359,7 @@ export const backApplyFilter = async (deps, accountConfigId, filterId) => {
|
|
|
349
359
|
if (folderAccountId !== undefined && folderAccountId !== accountId) {
|
|
350
360
|
continue;
|
|
351
361
|
}
|
|
352
|
-
const decision =
|
|
362
|
+
const decision = pipeline.decide(await preparedFor(accountId), message);
|
|
353
363
|
const move = (decision.move?.filterId ?? filterId) === filterId;
|
|
354
364
|
if (!move && filter.actionLabelId === NO_ACTION)
|
|
355
365
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/backend",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.152",
|
|
4
4
|
"description": "Remit Mail Inspector API backend",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "",
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
"types": "./dist/src/service/trigger-sync.d.ts",
|
|
64
64
|
"default": "./dist/src/service/trigger-sync.js"
|
|
65
65
|
},
|
|
66
|
+
"./oauth-state": {
|
|
67
|
+
"types": "./dist/src/oauth-state.d.ts",
|
|
68
|
+
"default": "./dist/src/oauth-state.js"
|
|
69
|
+
},
|
|
66
70
|
"./content-handler": {
|
|
67
71
|
"types": "./dist/dev-server/content-handler.d.ts",
|
|
68
72
|
"default": "./dist/dev-server/content-handler.js"
|