@flowselections/core 1.0.14 → 1.0.16
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/server/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { getProviderToken } from "./provider-token.js";
|
|
1
|
+
export { getProviderToken, listProviderAccounts, revokeProviderAccount } from "./provider-token.js";
|
|
2
|
+
export type { ProviderAccount } from "./provider-token.js";
|
|
2
3
|
export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getProviderToken } from "./provider-token.js";
|
|
1
|
+
export { getProviderToken, listProviderAccounts, revokeProviderAccount } from "./provider-token.js";
|
|
2
2
|
export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
|
|
@@ -13,6 +13,13 @@ export declare class CentralAuthUnavailableError extends Error {
|
|
|
13
13
|
export declare class AmbiguousAccountError extends Error {
|
|
14
14
|
constructor(message: string);
|
|
15
15
|
}
|
|
16
|
+
export interface ProviderAccount {
|
|
17
|
+
accountKey: string;
|
|
18
|
+
environment: "staging" | "live";
|
|
19
|
+
status: string;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
}
|
|
16
23
|
export declare function getProviderToken(provider: string, opts?: {
|
|
17
24
|
environment?: "staging" | "live";
|
|
18
25
|
accountKey?: string;
|
|
@@ -20,4 +27,10 @@ export declare function getProviderToken(provider: string, opts?: {
|
|
|
20
27
|
accessToken: string;
|
|
21
28
|
expiresAt: Date;
|
|
22
29
|
}>;
|
|
30
|
+
export declare function listProviderAccounts(provider: string, opts?: {
|
|
31
|
+
environment?: "staging" | "live";
|
|
32
|
+
}): Promise<ProviderAccount[]>;
|
|
33
|
+
export declare function revokeProviderAccount(provider: string, accountKey: string, opts?: {
|
|
34
|
+
environment?: "staging" | "live";
|
|
35
|
+
}): Promise<void>;
|
|
23
36
|
//# sourceMappingURL=provider-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-token.d.ts","sourceRoot":"","sources":["../../src/core/server/provider-token.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;
|
|
1
|
+
{"version":3,"file":"provider-token.d.ts","sourceRoot":"","sources":["../../src/core/server/provider-token.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAOD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AA4LD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CA6BnD;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,eAAe,EAAE,CAAC,CA4C5B;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -30,6 +30,10 @@ export class AmbiguousAccountError extends Error {
|
|
|
30
30
|
}
|
|
31
31
|
const TIMEOUT_MS = 10000;
|
|
32
32
|
const EXPIRY_BUFFER_MS = 60000;
|
|
33
|
+
// Module-private by design: getProviderToken and revokeProviderAccount share
|
|
34
|
+
// this Map (revoke invalidates entries it never wrote itself), so it must
|
|
35
|
+
// never be exposed or passed around — the two functions' coupling through it
|
|
36
|
+
// is intentional, but only within this file.
|
|
33
37
|
const cache = new Map();
|
|
34
38
|
function isFresh(token) {
|
|
35
39
|
return token.expiresAt.getTime() - EXPIRY_BUFFER_MS > Date.now();
|
|
@@ -37,55 +41,24 @@ function isFresh(token) {
|
|
|
37
41
|
function logAuthFailure(provider, environment, status) {
|
|
38
42
|
console.error("[central-auth] provider token request failed", { provider, environment, status });
|
|
39
43
|
}
|
|
40
|
-
|
|
44
|
+
function getCentralAuthConfig() {
|
|
41
45
|
const baseUrl = process.env.CENTRAL_AUTH_URL;
|
|
42
46
|
const secret = process.env.CENTRAL_AUTH_SECRET;
|
|
43
47
|
if (!baseUrl || !secret) {
|
|
44
48
|
throw new CentralAuthConfigError("Missing CENTRAL_AUTH_URL or CENTRAL_AUTH_SECRET environment variable.");
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
response = await fetch(url, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: {
|
|
61
|
-
Authorization: `Bearer ${secret}`,
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify(requestBody),
|
|
65
|
-
signal: controller.signal,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
logAuthFailure(provider, environment, timedOut ? "timeout" : "network");
|
|
70
|
-
throw new CentralAuthUnavailableError(`Central auth request failed (provider=${provider}, environment=${environment}, reason=${timedOut ? "timeout" : "network"}).`);
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
}
|
|
75
|
-
if (response.ok) {
|
|
76
|
-
let body;
|
|
77
|
-
try {
|
|
78
|
-
body = (await response.json());
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
logAuthFailure(provider, environment, response.status);
|
|
82
|
-
throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (provider=${provider}, environment=${environment}, status=${response.status}).`);
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
accessToken: body.access_token,
|
|
86
|
-
expiresAt: new Date(body.expires_at),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
50
|
+
return { baseUrl, secret };
|
|
51
|
+
}
|
|
52
|
+
// segment is always one of the three literals above, never caller input —
|
|
53
|
+
// the URL path must never be built from anything a caller supplies.
|
|
54
|
+
function buildCentralUrl(baseUrl, segment, provider, query) {
|
|
55
|
+
const base = `${baseUrl.replace(/\/+$/, "")}/api/${segment}/${encodeURIComponent(provider)}`;
|
|
56
|
+
if (!query)
|
|
57
|
+
return base;
|
|
58
|
+
const qs = new URLSearchParams(query).toString();
|
|
59
|
+
return qs ? `${base}?${qs}` : base;
|
|
60
|
+
}
|
|
61
|
+
async function mapCentralError(response, provider, environment, allowAmbiguousAccount) {
|
|
89
62
|
let errorCode;
|
|
90
63
|
try {
|
|
91
64
|
const text = await response.text();
|
|
@@ -98,6 +71,14 @@ async function fetchProviderToken(provider, environment, accountKey) {
|
|
|
98
71
|
}
|
|
99
72
|
logAuthFailure(provider, environment, response.status);
|
|
100
73
|
if (response.status === 400 && errorCode === "ambiguous_account") {
|
|
74
|
+
if (!allowAmbiguousAccount) {
|
|
75
|
+
// Only /api/token/ can legitimately return ambiguous_account. Any other
|
|
76
|
+
// endpoint returning it is an unexpected/malformed response from
|
|
77
|
+
// central, not a real ambiguity the caller can resolve — surfacing it
|
|
78
|
+
// as AmbiguousAccountError here would hand the caller a message about
|
|
79
|
+
// "multiple accounts" for an endpoint that never asked to disambiguate.
|
|
80
|
+
throw new CentralAuthUnavailableError(`Central auth returned an unexpected ambiguous_account error (provider=${provider}, environment=${environment}, status=400).`);
|
|
81
|
+
}
|
|
101
82
|
throw new AmbiguousAccountError(`Central auth reports multiple connected accounts for provider "${provider}"; accountKey is required to disambiguate (provider=${provider}, environment=${environment}).`);
|
|
102
83
|
}
|
|
103
84
|
if (response.status === 401 || errorCode === "unauthorized") {
|
|
@@ -116,9 +97,62 @@ async function fetchProviderToken(provider, environment, accountKey) {
|
|
|
116
97
|
}
|
|
117
98
|
throw new CentralAuthUnavailableError(`Central auth unavailable (provider=${provider}, environment=${environment}, status=${response.status}).`);
|
|
118
99
|
}
|
|
100
|
+
async function requestCentral(segment, provider, environment, init, allowAmbiguousAccount) {
|
|
101
|
+
const { baseUrl, secret } = getCentralAuthConfig();
|
|
102
|
+
const url = buildCentralUrl(baseUrl, segment, provider, init.query);
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
let timedOut = false;
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
timedOut = true;
|
|
107
|
+
controller.abort();
|
|
108
|
+
}, TIMEOUT_MS);
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = await fetch(url, {
|
|
112
|
+
method: init.method,
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${secret}`,
|
|
115
|
+
...(init.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
116
|
+
},
|
|
117
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
logAuthFailure(provider, environment, timedOut ? "timeout" : "network");
|
|
123
|
+
throw new CentralAuthUnavailableError(`Central auth request failed (provider=${provider}, environment=${environment}, reason=${timedOut ? "timeout" : "network"}).`);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
clearTimeout(timeout);
|
|
127
|
+
}
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
await mapCentralError(response, provider, environment, allowAmbiguousAccount);
|
|
130
|
+
}
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
async function fetchProviderToken(provider, environment, accountKey) {
|
|
134
|
+
const requestBody = { environment };
|
|
135
|
+
if (accountKey !== undefined)
|
|
136
|
+
requestBody.account_key = accountKey;
|
|
137
|
+
const response = await requestCentral("token", provider, environment, { method: "POST", body: requestBody },
|
|
138
|
+
/* allowAmbiguousAccount */ true);
|
|
139
|
+
let body;
|
|
140
|
+
try {
|
|
141
|
+
body = (await response.json());
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
logAuthFailure(provider, environment, response.status);
|
|
145
|
+
throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (provider=${provider}, environment=${environment}, status=${response.status}).`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
accessToken: body.access_token,
|
|
149
|
+
expiresAt: new Date(body.expires_at),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
119
152
|
export async function getProviderToken(provider, opts) {
|
|
120
153
|
const environment = opts?.environment ?? "live";
|
|
121
|
-
|
|
154
|
+
// Moet central's normalisatie in oauth-authcode.server.ts spiegelen (trim + toLowerCase).
|
|
155
|
+
const accountKey = opts?.accountKey?.trim().toLowerCase();
|
|
122
156
|
// accountKey is part of the key so two mailboxes on the same provider never
|
|
123
157
|
// share a cached token — omitting it would leak mailbox A's token to caller B.
|
|
124
158
|
const key = `${provider}:${environment}:${accountKey ?? ""}`;
|
|
@@ -145,3 +179,55 @@ export async function getProviderToken(provider, opts) {
|
|
|
145
179
|
});
|
|
146
180
|
return promise;
|
|
147
181
|
}
|
|
182
|
+
export async function listProviderAccounts(provider, opts) {
|
|
183
|
+
// Filter: only sent to central when the caller explicitly asked for one
|
|
184
|
+
// environment. Omitting it must return accounts across all environments —
|
|
185
|
+
// defaulting it here would silently narrow every unfiltered call to "live".
|
|
186
|
+
const environmentFilter = opts?.environment;
|
|
187
|
+
// Context for error messages/logging only, never the query filter above.
|
|
188
|
+
// Defaults to "live" purely so log lines read "environment=live" instead of
|
|
189
|
+
// "environment=" when no filter was requested.
|
|
190
|
+
const logEnvironment = environmentFilter ?? "live";
|
|
191
|
+
const response = await requestCentral("accounts", provider, logEnvironment, { method: "GET", query: environmentFilter ? { environment: environmentFilter } : undefined },
|
|
192
|
+
/* allowAmbiguousAccount */ false);
|
|
193
|
+
let body;
|
|
194
|
+
try {
|
|
195
|
+
body = (await response.json());
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
logAuthFailure(provider, logEnvironment, response.status);
|
|
199
|
+
throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (provider=${provider}, environment=${logEnvironment}, status=${response.status}).`);
|
|
200
|
+
}
|
|
201
|
+
// "revoked" rows are intentionally kept — the caller decides what to do with them.
|
|
202
|
+
return body.accounts.map((account) => ({
|
|
203
|
+
accountKey: account.account_key,
|
|
204
|
+
environment: account.environment,
|
|
205
|
+
status: account.status,
|
|
206
|
+
createdAt: new Date(account.created_at),
|
|
207
|
+
updatedAt: new Date(account.updated_at),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
export async function revokeProviderAccount(provider, accountKey, opts) {
|
|
211
|
+
const environment = opts?.environment ?? "live";
|
|
212
|
+
// Same normalisation as getProviderToken — a caller revoking "INFO@X.nl"
|
|
213
|
+
// must invalidate the exact cache entries getProviderToken would have used.
|
|
214
|
+
const normalizedAccountKey = accountKey.trim().toLowerCase();
|
|
215
|
+
if (!normalizedAccountKey) {
|
|
216
|
+
throw new CentralAuthConfigError("revokeProviderAccount requires a non-empty accountKey.");
|
|
217
|
+
}
|
|
218
|
+
await requestCentral("revoke", provider, environment, { method: "POST", body: { account_key: normalizedAccountKey, environment } },
|
|
219
|
+
/* allowAmbiguousAccount */ false);
|
|
220
|
+
// Cache-invalidatie na een geslaagde revoke: verwijder ALLE entries met
|
|
221
|
+
// prefix `${provider}:${environment}:`, niet alleen die van dit accountKey.
|
|
222
|
+
// Reden: de entry zonder accountKey (`${provider}:${environment}:`)
|
|
223
|
+
// resolveert naar "de enige actieve mailbox" — revoke je die mailbox, dan
|
|
224
|
+
// blijft een geldig token voor een ingetrokken account tot een uur in de
|
|
225
|
+
// cache staan. Revoke verandert bovendien de ambiguity-resolutie voor
|
|
226
|
+
// toekomstige aanroepen zonder accountKey. Alleen uitvoeren als de
|
|
227
|
+
// revoke-call zelf slaagde (requestCentral gooit hierboven bij een fout).
|
|
228
|
+
const prefix = `${provider}:${environment}:`;
|
|
229
|
+
for (const key of cache.keys()) {
|
|
230
|
+
if (key.startsWith(prefix))
|
|
231
|
+
cache.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|