@remit/backend 0.0.152 → 0.0.154
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/config/msoauth.d.ts +20 -2
- package/dist/src/config/msoauth.js +23 -0
- package/dist/src/handlers/account-oauth.js +34 -22
- package/dist/src/handlers/me.js +5 -1
- package/dist/src/oauth-state.d.ts +4 -0
- package/dist/src/oauth-state.js +8 -0
- package/dist/src/service/compose-sqlite.js +5 -2
- package/package.json +5 -1
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MailOAuthService, type OAuthProviderConfig } from "@remit/mail-oauth-service";
|
|
2
2
|
export interface MsOAuthCredentials {
|
|
3
3
|
clientId: string;
|
|
4
4
|
clientSecret: string;
|
|
5
5
|
}
|
|
6
|
+
/** Where a connected Outlook account's mail server is reached instead of Microsoft's. */
|
|
7
|
+
export interface MailServerOverride {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
tls: boolean;
|
|
11
|
+
startTls: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface MsOAuthOverrides extends Partial<Pick<OAuthProviderConfig, "tokenEndpoint">> {
|
|
14
|
+
imapServer?: MailServerOverride;
|
|
15
|
+
smtpServer?: MailServerOverride;
|
|
16
|
+
}
|
|
6
17
|
export interface MsOAuthEnabled {
|
|
7
18
|
mode: "Enabled";
|
|
8
19
|
redirectUri: string;
|
|
9
|
-
overrides:
|
|
20
|
+
overrides: MsOAuthOverrides;
|
|
10
21
|
credentials: () => Promise<MsOAuthCredentials>;
|
|
11
22
|
}
|
|
12
23
|
export type MsOAuthConfig = {
|
|
@@ -17,3 +28,10 @@ export interface LoadedMsOAuth {
|
|
|
17
28
|
credentials: MsOAuthCredentials;
|
|
18
29
|
}
|
|
19
30
|
export declare const loadMsOAuth: (config: MsOAuthConfig) => Promise<LoadedMsOAuth>;
|
|
31
|
+
export type MicrosoftTokenService = Pick<MailOAuthService, "getAccessToken">;
|
|
32
|
+
/**
|
|
33
|
+
* The access-token minting a connected Outlook account syncs and sends with.
|
|
34
|
+
* The app's credentials load on the first token asked for, and once loaded the
|
|
35
|
+
* service keeps its token cache for every account after it.
|
|
36
|
+
*/
|
|
37
|
+
export declare const microsoftTokenService: (msOAuth: MsOAuthConfig) => MicrosoftTokenService;
|
|
@@ -1,6 +1,29 @@
|
|
|
1
|
+
import { createMailOAuthService, microsoftProviderConfig, } from "@remit/mail-oauth-service";
|
|
1
2
|
export const loadMsOAuth = async (config) => {
|
|
2
3
|
if (config.mode === "Disabled") {
|
|
3
4
|
throw new Error("No Microsoft OAuth credentials are configured on this instance: set MSOAUTH_CLIENT_ID and MSOAUTH_CLIENT_SECRET, or MSOAUTH_SECRET_ARN");
|
|
4
5
|
}
|
|
5
6
|
return { config, credentials: await config.credentials() };
|
|
6
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* The access-token minting a connected Outlook account syncs and sends with.
|
|
10
|
+
* The app's credentials load on the first token asked for, and once loaded the
|
|
11
|
+
* service keeps its token cache for every account after it.
|
|
12
|
+
*/
|
|
13
|
+
export const microsoftTokenService = (msOAuth) => {
|
|
14
|
+
let service;
|
|
15
|
+
const load = () => loadMsOAuth(msOAuth).then(({ config, credentials }) => createMailOAuthService(microsoftProviderConfig({
|
|
16
|
+
clientId: credentials.clientId,
|
|
17
|
+
clientSecret: credentials.clientSecret,
|
|
18
|
+
overrides: config.overrides,
|
|
19
|
+
})));
|
|
20
|
+
return {
|
|
21
|
+
getAccessToken: (cacheKey, refreshToken) => {
|
|
22
|
+
service ??= load().catch((error) => {
|
|
23
|
+
service = undefined;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
return service.then((loaded) => loaded.getAccessToken(cacheKey, refreshToken));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -6,11 +6,22 @@ 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
|
+
import { ACCOUNT_SETTINGS_ROUTE, oauthRefusalUrl, signState, verifyState, } from "../oauth-state.js";
|
|
10
10
|
import { rawApiResponse } from "../response.js";
|
|
11
11
|
import { triggerAccountSyncSafe } from "./account.js";
|
|
12
12
|
import { findActiveDuplicateMailbox } from "./account-guards.js";
|
|
13
|
-
const
|
|
13
|
+
const OUTLOOK_IMAP = {
|
|
14
|
+
host: "outlook.office365.com",
|
|
15
|
+
port: 993,
|
|
16
|
+
tls: true,
|
|
17
|
+
startTls: false,
|
|
18
|
+
};
|
|
19
|
+
const OUTLOOK_SMTP = {
|
|
20
|
+
host: "smtp.office365.com",
|
|
21
|
+
port: 587,
|
|
22
|
+
tls: false,
|
|
23
|
+
startTls: true,
|
|
24
|
+
};
|
|
14
25
|
// ─── JWT claims parsing ──────────────────────────────────────────────────────
|
|
15
26
|
export async function parseJwtClaims(token) {
|
|
16
27
|
const parts = token.split(".");
|
|
@@ -25,7 +36,7 @@ export function getWebOrigin(origins) {
|
|
|
25
36
|
return httpsOrigin ?? origins[0] ?? "https://localhost:3000";
|
|
26
37
|
}
|
|
27
38
|
const scopeRefusalUrl = (webOrigin, email, missingServices) => {
|
|
28
|
-
const url = new URL(`${webOrigin}
|
|
39
|
+
const url = new URL(`${webOrigin}${ACCOUNT_SETTINGS_ROUTE}`);
|
|
29
40
|
url.searchParams.set("oauthError", "scope_not_granted");
|
|
30
41
|
url.searchParams.set("oauthEmail", email);
|
|
31
42
|
url.searchParams.set("missingServices", missingServices.join(","));
|
|
@@ -73,13 +84,12 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
73
84
|
});
|
|
74
85
|
// If Microsoft returned an error, pass it through to the frontend
|
|
75
86
|
if (qs.error) {
|
|
76
|
-
|
|
77
|
-
return redirect(`${webOrigin}/settings/accounts?oauthError=${errorCode}`);
|
|
87
|
+
return redirect(oauthRefusalUrl(webOrigin, qs.error));
|
|
78
88
|
}
|
|
79
89
|
const code = qs.code;
|
|
80
90
|
const state = qs.state;
|
|
81
91
|
if (!code || !state) {
|
|
82
|
-
return redirect(
|
|
92
|
+
return redirect(oauthRefusalUrl(webOrigin, "missing_params"));
|
|
83
93
|
}
|
|
84
94
|
const loaded = await loadMsOAuth(ctx.config.msOAuth).catch((err) => {
|
|
85
95
|
logger.error({
|
|
@@ -92,7 +102,7 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
92
102
|
return null;
|
|
93
103
|
});
|
|
94
104
|
if (loaded === null)
|
|
95
|
-
return redirect(
|
|
105
|
+
return redirect(oauthRefusalUrl(webOrigin, "config_error"));
|
|
96
106
|
const { config, credentials } = loaded;
|
|
97
107
|
// Verify the HMAC-signed state
|
|
98
108
|
// A bad/expired/forged state is a security-relevant signal (CSRF
|
|
@@ -108,7 +118,7 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
108
118
|
return null;
|
|
109
119
|
});
|
|
110
120
|
if (statePayload === null)
|
|
111
|
-
return redirect(
|
|
121
|
+
return redirect(oauthRefusalUrl(webOrigin, "invalid_state"));
|
|
112
122
|
const { accountConfigId } = statePayload;
|
|
113
123
|
const [firstService] = orderServices(statePayload.services);
|
|
114
124
|
const serviceFor = (service) => createMailOAuthService(microsoftProviderConfig({
|
|
@@ -125,10 +135,10 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
125
135
|
return null;
|
|
126
136
|
});
|
|
127
137
|
if (tokenSet === null)
|
|
128
|
-
return redirect(
|
|
138
|
+
return redirect(oauthRefusalUrl(webOrigin, "exchange_failed"));
|
|
129
139
|
if (!tokenSet.refreshToken) {
|
|
130
140
|
logger.error({ accountConfigId }, "MS OAuth token exchange returned no refresh_token");
|
|
131
|
-
return redirect(
|
|
141
|
+
return redirect(oauthRefusalUrl(webOrigin, "exchange_failed"));
|
|
132
142
|
}
|
|
133
143
|
// Extract the user's email from the ID token returned alongside the access
|
|
134
144
|
// token when openid+email scopes are requested.
|
|
@@ -160,7 +170,7 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
160
170
|
: undefined;
|
|
161
171
|
if (!email) {
|
|
162
172
|
logger.error({ accountConfigId }, "MS OAuth: could not extract email from token claims");
|
|
163
|
-
return redirect(
|
|
173
|
+
return redirect(oauthRefusalUrl(webOrigin, "missing_email"));
|
|
164
174
|
}
|
|
165
175
|
const { account, accountConfig, secrets } = ctx.client;
|
|
166
176
|
// Reconnect when an active OAuth account already onboards this mailbox.
|
|
@@ -168,8 +178,10 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
168
178
|
// the existing account (token refresh) rather than rejecting, because a
|
|
169
179
|
// re-auth is the expected, idempotent path for OAuth.
|
|
170
180
|
const existingAccounts = (await account.listAllByAccountConfig(accountConfigId)).filter((a) => a.authType === AccountAuthType.OauthMicrosoft);
|
|
181
|
+
const imapServer = config.overrides.imapServer ?? OUTLOOK_IMAP;
|
|
182
|
+
const smtpServer = config.overrides.smtpServer ?? OUTLOOK_SMTP;
|
|
171
183
|
const existing = findActiveDuplicateMailbox(existingAccounts, {
|
|
172
|
-
imapHost:
|
|
184
|
+
imapHost: imapServer.host,
|
|
173
185
|
username: email,
|
|
174
186
|
});
|
|
175
187
|
const services = orderServices([
|
|
@@ -181,7 +193,7 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
181
193
|
redemption.reason === "scope_not_granted")
|
|
182
194
|
return redirect(scopeRefusalUrl(webOrigin, email, [redemption.service]));
|
|
183
195
|
if (redemption.kind === "refused")
|
|
184
|
-
return redirect(
|
|
196
|
+
return redirect(oauthRefusalUrl(webOrigin, redemption.reason));
|
|
185
197
|
const grantedScopes = [
|
|
186
198
|
...new Set([...tokenSet.grantedScopes, ...redemption.grantedScopes]),
|
|
187
199
|
];
|
|
@@ -228,15 +240,15 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
228
240
|
grantedScopes,
|
|
229
241
|
oauthRefreshTokenHash,
|
|
230
242
|
oauthTokenUpdatedAt,
|
|
231
|
-
imapHost:
|
|
232
|
-
imapPort:
|
|
233
|
-
imapTls:
|
|
234
|
-
imapStartTls:
|
|
243
|
+
imapHost: imapServer.host,
|
|
244
|
+
imapPort: imapServer.port,
|
|
245
|
+
imapTls: imapServer.tls,
|
|
246
|
+
imapStartTls: imapServer.startTls,
|
|
235
247
|
smtpEnabled: true,
|
|
236
|
-
smtpHost:
|
|
237
|
-
smtpPort:
|
|
238
|
-
smtpTls:
|
|
239
|
-
smtpStartTls:
|
|
248
|
+
smtpHost: smtpServer.host,
|
|
249
|
+
smtpPort: smtpServer.port,
|
|
250
|
+
smtpTls: smtpServer.tls,
|
|
251
|
+
smtpStartTls: smtpServer.startTls,
|
|
240
252
|
isActive: true,
|
|
241
253
|
connectionState: ConnectionState.NotAuthenticated,
|
|
242
254
|
});
|
|
@@ -247,7 +259,7 @@ export const microsoftOAuthCallback = async (context, ctx) => {
|
|
|
247
259
|
if (services.includes(AccountService.Mail)) {
|
|
248
260
|
await triggerAccountSyncSafe(ctx.client, accountId);
|
|
249
261
|
}
|
|
250
|
-
return redirect(`${webOrigin}
|
|
262
|
+
return redirect(`${webOrigin}${ACCOUNT_SETTINGS_ROUTE}?connected=${encodeURIComponent(accountId)}`);
|
|
251
263
|
};
|
|
252
264
|
/**
|
|
253
265
|
* An instance with no Microsoft app answers the start of a sign-in with a
|
package/dist/src/handlers/me.js
CHANGED
|
@@ -55,7 +55,11 @@ export const MeOperations = {
|
|
|
55
55
|
throw err;
|
|
56
56
|
}
|
|
57
57
|
await ctx.client.queues.accountFanout.send({
|
|
58
|
-
body: {
|
|
58
|
+
body: {
|
|
59
|
+
type: "AccountDelete",
|
|
60
|
+
accountConfigId,
|
|
61
|
+
userId: ctx.identity.sub,
|
|
62
|
+
},
|
|
59
63
|
});
|
|
60
64
|
// biome-ignore lint/plugin/no-logger-info: account deletion is an audit-grade signal
|
|
61
65
|
logger.info({ accountConfigId }, "Account deletion initiated");
|
|
@@ -8,3 +8,7 @@ export interface OAuthState {
|
|
|
8
8
|
export declare const STATE_TTL_MS: number;
|
|
9
9
|
export declare function signState(payload: OAuthState, clientSecret: string): Promise<string>;
|
|
10
10
|
export declare function verifyState(state: string, clientSecret: string): Promise<OAuthState>;
|
|
11
|
+
/** The web client's account settings, where every Microsoft sign-in lands. */
|
|
12
|
+
export declare const ACCOUNT_SETTINGS_ROUTE = "/settings/accounts";
|
|
13
|
+
/** Where a refused Microsoft sign-in sends the browser back to, carrying why. */
|
|
14
|
+
export declare const oauthRefusalUrl: (webOrigin: string, oauthError: string) => string;
|
package/dist/src/oauth-state.js
CHANGED
|
@@ -53,3 +53,11 @@ export async function verifyState(state, clientSecret) {
|
|
|
53
53
|
}
|
|
54
54
|
return payload;
|
|
55
55
|
}
|
|
56
|
+
/** The web client's account settings, where every Microsoft sign-in lands. */
|
|
57
|
+
export const ACCOUNT_SETTINGS_ROUTE = "/settings/accounts";
|
|
58
|
+
/** Where a refused Microsoft sign-in sends the browser back to, carrying why. */
|
|
59
|
+
export const oauthRefusalUrl = (webOrigin, oauthError) => {
|
|
60
|
+
const url = new URL(`${webOrigin}${ACCOUNT_SETTINGS_ROUTE}`);
|
|
61
|
+
url.searchParams.set("oauthError", oauthError);
|
|
62
|
+
return url.toString();
|
|
63
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BETTER_SQLITE3_LIMITS, createSqliteRepositories, openSqliteDatabases, sqliteDatabasePathsFromEnv, } from "@remit/drizzle-service";
|
|
1
|
+
import { BETTER_SQLITE3_LIMITS, createSqliteRepositories, DEFAULT_MESSAGE_OBJECT_DELETE_CONCURRENCY, openSqliteDatabases, sqliteDatabasePathsFromEnv, } from "@remit/drizzle-service";
|
|
2
2
|
import { createRemitClient, } from "./create-remit-client.js";
|
|
3
3
|
import { buildSharedDeps, sqsQueues, sqsQueueUrlsFromEnv, } from "./shared-deps.js";
|
|
4
4
|
// The SQLite adapter composition (RFC 036). Every repo runs on the database
|
|
@@ -6,7 +6,10 @@ import { buildSharedDeps, sqsQueues, sqsQueueUrlsFromEnv, } from "./shared-deps.
|
|
|
6
6
|
export const composeSqliteClient = async (shared) => {
|
|
7
7
|
const databases = await openSqliteDatabases(sqliteDatabasePathsFromEnv());
|
|
8
8
|
return createRemitClient({
|
|
9
|
-
repositories: createSqliteRepositories(databases, BETTER_SQLITE3_LIMITS
|
|
9
|
+
repositories: createSqliteRepositories(databases, BETTER_SQLITE3_LIMITS, {
|
|
10
|
+
storage: shared.storage,
|
|
11
|
+
messageObjectDeleteConcurrency: DEFAULT_MESSAGE_OBJECT_DELETE_CONCURRENCY,
|
|
12
|
+
}),
|
|
10
13
|
...shared,
|
|
11
14
|
});
|
|
12
15
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/backend",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.154",
|
|
4
4
|
"description": "Remit Mail Inspector API backend",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "",
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
"types": "./dist/src/oauth-state.d.ts",
|
|
68
68
|
"default": "./dist/src/oauth-state.js"
|
|
69
69
|
},
|
|
70
|
+
"./msoauth": {
|
|
71
|
+
"types": "./dist/src/config/msoauth.d.ts",
|
|
72
|
+
"default": "./dist/src/config/msoauth.js"
|
|
73
|
+
},
|
|
70
74
|
"./content-handler": {
|
|
71
75
|
"types": "./dist/dev-server/content-handler.d.ts",
|
|
72
76
|
"default": "./dist/dev-server/content-handler.js"
|