@flowselections/core 1.0.13 → 1.0.14

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,3 +1,3 @@
1
1
  export { getProviderToken } from "./provider-token.js";
2
- export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, } from "./provider-token.js";
2
+ export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
3
3
  //# 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;AACvD,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC"}
@@ -1,2 +1,2 @@
1
1
  export { getProviderToken } from "./provider-token.js";
2
- export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, } from "./provider-token.js";
2
+ export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
@@ -10,8 +10,12 @@ export declare class ProviderUpstreamError extends Error {
10
10
  export declare class CentralAuthUnavailableError extends Error {
11
11
  constructor(message: string);
12
12
  }
13
+ export declare class AmbiguousAccountError extends Error {
14
+ constructor(message: string);
15
+ }
13
16
  export declare function getProviderToken(provider: string, opts?: {
14
17
  environment?: "staging" | "live";
18
+ accountKey?: string;
15
19
  }): Promise<{
16
20
  accessToken: string;
17
21
  expiresAt: Date;
@@ -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;AAwGD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAuBnD"}
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;AAkID,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,CA4BnD"}
@@ -22,6 +22,12 @@ export class CentralAuthUnavailableError extends Error {
22
22
  this.name = "CentralAuthUnavailableError";
23
23
  }
24
24
  }
25
+ export class AmbiguousAccountError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = "AmbiguousAccountError";
29
+ }
30
+ }
25
31
  const TIMEOUT_MS = 10000;
26
32
  const EXPIRY_BUFFER_MS = 60000;
27
33
  const cache = new Map();
@@ -31,13 +37,16 @@ function isFresh(token) {
31
37
  function logAuthFailure(provider, environment, status) {
32
38
  console.error("[central-auth] provider token request failed", { provider, environment, status });
33
39
  }
34
- async function fetchProviderToken(provider, environment) {
40
+ async function fetchProviderToken(provider, environment, accountKey) {
35
41
  const baseUrl = process.env.CENTRAL_AUTH_URL;
36
42
  const secret = process.env.CENTRAL_AUTH_SECRET;
37
43
  if (!baseUrl || !secret) {
38
44
  throw new CentralAuthConfigError("Missing CENTRAL_AUTH_URL or CENTRAL_AUTH_SECRET environment variable.");
39
45
  }
40
46
  const url = `${baseUrl.replace(/\/+$/, "")}/api/token/${encodeURIComponent(provider)}`;
47
+ const requestBody = { environment };
48
+ if (accountKey !== undefined)
49
+ requestBody.account_key = accountKey;
41
50
  const controller = new AbortController();
42
51
  let timedOut = false;
43
52
  const timeout = setTimeout(() => {
@@ -52,7 +61,7 @@ async function fetchProviderToken(provider, environment) {
52
61
  Authorization: `Bearer ${secret}`,
53
62
  "Content-Type": "application/json",
54
63
  },
55
- body: JSON.stringify({ environment }),
64
+ body: JSON.stringify(requestBody),
56
65
  signal: controller.signal,
57
66
  });
58
67
  }
@@ -77,23 +86,42 @@ async function fetchProviderToken(provider, environment) {
77
86
  expiresAt: new Date(body.expires_at),
78
87
  };
79
88
  }
89
+ let errorCode;
90
+ try {
91
+ const text = await response.text();
92
+ const parsed = JSON.parse(text);
93
+ if (typeof parsed?.error === "string")
94
+ errorCode = parsed.error;
95
+ }
96
+ catch {
97
+ // body not readable or not valid JSON — fall back to status-only mapping below
98
+ }
80
99
  logAuthFailure(provider, environment, response.status);
81
- switch (response.status) {
82
- case 401:
83
- throw new CentralAuthConfigError(`Central auth rejected credentials (provider=${provider}, environment=${environment}, status=401).`);
84
- case 409:
85
- throw new ProviderNotConnectedError(`Provider not connected (provider=${provider}, environment=${environment}, status=409).`);
86
- case 400:
87
- throw new CentralAuthConfigError(`Invalid request to central auth (provider=${provider}, environment=${environment}, status=400).`);
88
- case 502:
89
- throw new ProviderUpstreamError(`Upstream error from central auth (provider=${provider}, environment=${environment}, status=502).`);
90
- default:
91
- throw new CentralAuthUnavailableError(`Central auth unavailable (provider=${provider}, environment=${environment}, status=${response.status}).`);
100
+ if (response.status === 400 && errorCode === "ambiguous_account") {
101
+ throw new AmbiguousAccountError(`Central auth reports multiple connected accounts for provider "${provider}"; accountKey is required to disambiguate (provider=${provider}, environment=${environment}).`);
102
+ }
103
+ if (response.status === 401 || errorCode === "unauthorized") {
104
+ throw new CentralAuthConfigError(`Central auth rejected credentials (provider=${provider}, environment=${environment}, status=${response.status}).`);
105
+ }
106
+ if (response.status === 409 || errorCode === "not_connected") {
107
+ throw new ProviderNotConnectedError(`Provider not connected (provider=${provider}, environment=${environment}, status=${response.status}).`);
108
+ }
109
+ if (response.status === 400) {
110
+ // errorCode is "invalid_request", missing, or unrecognized — same
111
+ // build-time-error classification the status-only mapping already used.
112
+ throw new CentralAuthConfigError(`Invalid request to central auth (provider=${provider}, environment=${environment}, status=400).`);
113
+ }
114
+ if (response.status === 502 || errorCode === "upstream_error") {
115
+ throw new ProviderUpstreamError(`Upstream error from central auth (provider=${provider}, environment=${environment}, status=${response.status}).`);
92
116
  }
117
+ throw new CentralAuthUnavailableError(`Central auth unavailable (provider=${provider}, environment=${environment}, status=${response.status}).`);
93
118
  }
94
119
  export async function getProviderToken(provider, opts) {
95
120
  const environment = opts?.environment ?? "live";
96
- const key = `${provider}:${environment}`;
121
+ const accountKey = opts?.accountKey;
122
+ // accountKey is part of the key so two mailboxes on the same provider never
123
+ // share a cached token — omitting it would leak mailbox A's token to caller B.
124
+ const key = `${provider}:${environment}:${accountKey ?? ""}`;
97
125
  const cached = cache.get(key);
98
126
  if (cached) {
99
127
  const token = await cached;
@@ -107,8 +135,10 @@ export async function getProviderToken(provider, opts) {
107
135
  }
108
136
  // No `await` above this line: a synchronous burst of callers must all see
109
137
  // this Map entry before any of them proceeds to fetch, so they share one fetch.
110
- const promise = fetchProviderToken(provider, environment);
138
+ const promise = fetchProviderToken(provider, environment, accountKey);
111
139
  cache.set(key, promise);
140
+ // Generic, error-type-agnostic: this also evicts an AmbiguousAccountError
141
+ // rejection — an ambiguous-account failure must never be served from cache.
112
142
  promise.catch(() => {
113
143
  if (cache.get(key) === promise)
114
144
  cache.delete(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/core",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",