@remit/backend 0.0.151 → 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 { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.151",
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"