@flowselections/core 1.0.12 → 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.
@@ -0,0 +1,3 @@
1
+ export { getProviderToken } from "./provider-token.js";
2
+ export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { getProviderToken } from "./provider-token.js";
2
+ export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
@@ -0,0 +1,23 @@
1
+ export declare class CentralAuthConfigError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class ProviderNotConnectedError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ export declare class ProviderUpstreamError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ export declare class CentralAuthUnavailableError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ export declare class AmbiguousAccountError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ export declare function getProviderToken(provider: string, opts?: {
17
+ environment?: "staging" | "live";
18
+ accountKey?: string;
19
+ }): Promise<{
20
+ accessToken: string;
21
+ expiresAt: Date;
22
+ }>;
23
+ //# sourceMappingURL=provider-token.d.ts.map
@@ -0,0 +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;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"}
@@ -0,0 +1,147 @@
1
+ export class CentralAuthConfigError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "CentralAuthConfigError";
5
+ }
6
+ }
7
+ export class ProviderNotConnectedError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "ProviderNotConnectedError";
11
+ }
12
+ }
13
+ export class ProviderUpstreamError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "ProviderUpstreamError";
17
+ }
18
+ }
19
+ export class CentralAuthUnavailableError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "CentralAuthUnavailableError";
23
+ }
24
+ }
25
+ export class AmbiguousAccountError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = "AmbiguousAccountError";
29
+ }
30
+ }
31
+ const TIMEOUT_MS = 10000;
32
+ const EXPIRY_BUFFER_MS = 60000;
33
+ const cache = new Map();
34
+ function isFresh(token) {
35
+ return token.expiresAt.getTime() - EXPIRY_BUFFER_MS > Date.now();
36
+ }
37
+ function logAuthFailure(provider, environment, status) {
38
+ console.error("[central-auth] provider token request failed", { provider, environment, status });
39
+ }
40
+ async function fetchProviderToken(provider, environment, accountKey) {
41
+ const baseUrl = process.env.CENTRAL_AUTH_URL;
42
+ const secret = process.env.CENTRAL_AUTH_SECRET;
43
+ if (!baseUrl || !secret) {
44
+ throw new CentralAuthConfigError("Missing CENTRAL_AUTH_URL or CENTRAL_AUTH_SECRET environment variable.");
45
+ }
46
+ const url = `${baseUrl.replace(/\/+$/, "")}/api/token/${encodeURIComponent(provider)}`;
47
+ const requestBody = { environment };
48
+ if (accountKey !== undefined)
49
+ requestBody.account_key = accountKey;
50
+ const controller = new AbortController();
51
+ let timedOut = false;
52
+ const timeout = setTimeout(() => {
53
+ timedOut = true;
54
+ controller.abort();
55
+ }, TIMEOUT_MS);
56
+ let response;
57
+ try {
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
+ }
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
+ }
99
+ logAuthFailure(provider, environment, 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}).`);
116
+ }
117
+ throw new CentralAuthUnavailableError(`Central auth unavailable (provider=${provider}, environment=${environment}, status=${response.status}).`);
118
+ }
119
+ export async function getProviderToken(provider, opts) {
120
+ const environment = opts?.environment ?? "live";
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 ?? ""}`;
125
+ const cached = cache.get(key);
126
+ if (cached) {
127
+ const token = await cached;
128
+ if (isFresh(token))
129
+ return token;
130
+ // Only the caller that observes the still-current stale entry evicts it —
131
+ // a concurrent caller may have already replaced it with a fresh in-flight fetch.
132
+ if (cache.get(key) === cached)
133
+ cache.delete(key);
134
+ return getProviderToken(provider, opts);
135
+ }
136
+ // No `await` above this line: a synchronous burst of callers must all see
137
+ // this Map entry before any of them proceeds to fetch, so they share one fetch.
138
+ const promise = fetchProviderToken(provider, environment, accountKey);
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.
142
+ promise.catch(() => {
143
+ if (cache.get(key) === promise)
144
+ cache.delete(key);
145
+ });
146
+ return promise;
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/core",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -10,6 +10,10 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./server": {
15
+ "types": "./dist/server/index.d.ts",
16
+ "default": "./dist/server/index.js"
13
17
  }
14
18
  },
15
19
  "files": ["dist"],