@poly-x/core 0.1.0-alpha.0
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/README.md +34 -0
- package/dist/index.cjs +940 -0
- package/dist/index.d.cts +491 -0
- package/dist/index.d.mts +491 -0
- package/dist/index.mjs +902 -0
- package/package.json +41 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* The typed error taxonomy shared by every @poly-x package (FR-DX-5).
|
|
4
|
+
* Every failure a consumer can see is one of these classes — branchable by
|
|
5
|
+
* `instanceof` or `code`, and safe to serialize: `toJSON()` redacts key
|
|
6
|
+
* material and tokens (FR-DX-3).
|
|
7
|
+
*/
|
|
8
|
+
const KEY_PATTERN = /\b(?:pk|sk)_(?:test|live)_[A-Za-z0-9_-]{6,}/g;
|
|
9
|
+
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
10
|
+
const JWT_PATTERN = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
11
|
+
function redactSensitive(text) {
|
|
12
|
+
return text.replace(KEY_PATTERN, "[REDACTED]").replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(JWT_PATTERN, "[REDACTED]");
|
|
13
|
+
}
|
|
14
|
+
function redactDeep(value) {
|
|
15
|
+
if (typeof value === "string") return redactSensitive(value);
|
|
16
|
+
if (Array.isArray(value)) return value.map(redactDeep);
|
|
17
|
+
if (value && typeof value === "object") {
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const [key, entry] of Object.entries(value)) result[key] = redactDeep(entry);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
var PolyXError = class extends Error {
|
|
25
|
+
code;
|
|
26
|
+
requestId;
|
|
27
|
+
meta;
|
|
28
|
+
constructor(message, options) {
|
|
29
|
+
super(redactSensitive(message), { cause: options.cause });
|
|
30
|
+
this.name = new.target.name;
|
|
31
|
+
this.code = options.code;
|
|
32
|
+
this.requestId = options.requestId;
|
|
33
|
+
this.meta = options.meta;
|
|
34
|
+
}
|
|
35
|
+
toJSON() {
|
|
36
|
+
return {
|
|
37
|
+
name: this.name,
|
|
38
|
+
code: this.code,
|
|
39
|
+
message: this.message,
|
|
40
|
+
...this.requestId ? { requestId: this.requestId } : {},
|
|
41
|
+
...this.meta ? { meta: redactDeep(this.meta) } : {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var PolyXConfigError = class extends PolyXError {
|
|
46
|
+
constructor(message, options) {
|
|
47
|
+
super(message, {
|
|
48
|
+
code: options?.code ?? "CONFIG_ERROR",
|
|
49
|
+
...options
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/** Not signed in. `refreshable: true` means a silent refresh may recover it. */
|
|
54
|
+
var UnauthenticatedError = class extends PolyXError {
|
|
55
|
+
refreshable;
|
|
56
|
+
constructor(message, options) {
|
|
57
|
+
super(message, options);
|
|
58
|
+
this.refreshable = options.refreshable;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Terminal session end (expiry / invalid refresh) — re-authentication required. */
|
|
62
|
+
var SessionExpiredError = class extends PolyXError {
|
|
63
|
+
shouldRelogin = true;
|
|
64
|
+
constructor(message, options) {
|
|
65
|
+
super(message, options);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** Session (or its whole family, on refresh-token reuse) was revoked. */
|
|
69
|
+
var SessionRevokedError = class extends PolyXError {
|
|
70
|
+
shouldRelogin = true;
|
|
71
|
+
constructor(message, options) {
|
|
72
|
+
super(message, options);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var ForbiddenError = class extends PolyXError {};
|
|
76
|
+
var ValidationError = class extends PolyXError {
|
|
77
|
+
errors;
|
|
78
|
+
constructor(message, options) {
|
|
79
|
+
super(message, {
|
|
80
|
+
code: options.code ?? "VALIDATION_ERROR",
|
|
81
|
+
...options
|
|
82
|
+
});
|
|
83
|
+
this.errors = options.errors;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var RateLimitedError = class extends PolyXError {
|
|
87
|
+
retryAfterSeconds;
|
|
88
|
+
constructor(message, options) {
|
|
89
|
+
super(message, {
|
|
90
|
+
code: options.code ?? "RATE_LIMITED",
|
|
91
|
+
...options
|
|
92
|
+
});
|
|
93
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/** Platform unreachable, 5xx, timeout, or an unrecognizable response. */
|
|
97
|
+
var UpstreamError = class extends PolyXError {};
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/keys.ts
|
|
100
|
+
/**
|
|
101
|
+
* PolyX key parsing (FR-CFG-3/4). Format draft v1 — the cross-repo contract
|
|
102
|
+
* poly-x v4 (FR-KEY-2) implements; test vectors live in test/fixtures/keys.ts
|
|
103
|
+
* and are mirrored into poly-x's tests:
|
|
104
|
+
*
|
|
105
|
+
* {kind}_{env}_{base64url("v1|" + deploymentHost + "|" + random)}
|
|
106
|
+
*
|
|
107
|
+
* The deployment host is the issuing PolyX deployment's authority plus optional
|
|
108
|
+
* path prefix, no scheme (e.g. "poly.fintra.ai/poly-x") — there is no single
|
|
109
|
+
* SaaS PolyX, so the key locates its own backend (discovery D8).
|
|
110
|
+
*/
|
|
111
|
+
const KEY_SHAPE = /^(pk|sk)_(test|live)_([A-Za-z0-9_-]+)$/;
|
|
112
|
+
const SUPPORTED_FORMAT_VERSIONS = /* @__PURE__ */ new Set(["v1"]);
|
|
113
|
+
function decodeBase64Url(payload) {
|
|
114
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
115
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
116
|
+
try {
|
|
117
|
+
return atob(padded);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new PolyXConfigError("PolyX key payload is not valid base64url.", { code: "KEY_MALFORMED" });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function parsePolyXKey(raw) {
|
|
123
|
+
if (typeof raw !== "string" || raw.length === 0) throw new PolyXConfigError("PolyX key is missing. Provide the key from your App instance.", { code: "KEY_MISSING" });
|
|
124
|
+
const match = KEY_SHAPE.exec(raw);
|
|
125
|
+
if (!match) throw new PolyXConfigError("PolyX key has an unrecognized prefix. Expected a key starting with pk_test_, pk_live_, sk_test_ or sk_live_.", { code: "KEY_MALFORMED" });
|
|
126
|
+
const [, kind, env, payload] = match;
|
|
127
|
+
const segments = decodeBase64Url(payload).split("|");
|
|
128
|
+
const formatVersion = segments[0] ?? "";
|
|
129
|
+
if (!SUPPORTED_FORMAT_VERSIONS.has(formatVersion)) throw new PolyXConfigError(`PolyX key has an unsupported format version ("${formatVersion || "none"}"). Upgrade @poly-x/core or re-issue the key.`, { code: "KEY_UNSUPPORTED_VERSION" });
|
|
130
|
+
if (segments.length !== 3) throw new PolyXConfigError("PolyX key payload is incomplete.", { code: "KEY_MALFORMED" });
|
|
131
|
+
const host = segments[1] ?? "";
|
|
132
|
+
if (host.length === 0) throw new PolyXConfigError("PolyX key does not carry an issuing deployment host.", { code: "KEY_MALFORMED" });
|
|
133
|
+
return {
|
|
134
|
+
kind,
|
|
135
|
+
env,
|
|
136
|
+
host,
|
|
137
|
+
formatVersion,
|
|
138
|
+
raw
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/config.ts
|
|
143
|
+
/**
|
|
144
|
+
* Effective SDK configuration (FR-CFG-4/5/6): the key locates its deployment;
|
|
145
|
+
* an explicit baseUrl override wins (proxies, container-internal hostnames,
|
|
146
|
+
* deployment migrations); secret keys are refused in browser contexts
|
|
147
|
+
* (FR-CFG-1/2).
|
|
148
|
+
*/
|
|
149
|
+
function detectContext() {
|
|
150
|
+
const globals = globalThis;
|
|
151
|
+
return globals.window !== void 0 && globals.document !== void 0 ? "browser" : "server";
|
|
152
|
+
}
|
|
153
|
+
function normalizeBaseUrl(url) {
|
|
154
|
+
return url.replace(/\/+$/, "");
|
|
155
|
+
}
|
|
156
|
+
function resolveConfig(input) {
|
|
157
|
+
const context = input.context ?? detectContext();
|
|
158
|
+
const key = parsePolyXKey(input.key);
|
|
159
|
+
if (key.kind === "sk" && context === "browser") throw new PolyXConfigError("A PolyX secret key was passed to a browser-side package. Secret keys belong on your server only — use the publishable key (pk_…) in the browser.", { code: "SECRET_KEY_IN_BROWSER" });
|
|
160
|
+
let baseUrl;
|
|
161
|
+
if (input.baseUrl !== void 0) {
|
|
162
|
+
try {
|
|
163
|
+
new URL(input.baseUrl);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new PolyXConfigError("The baseUrl override is not a valid URL.", { code: "BASE_URL_INVALID" });
|
|
166
|
+
}
|
|
167
|
+
baseUrl = normalizeBaseUrl(input.baseUrl);
|
|
168
|
+
} else baseUrl = `https://${normalizeBaseUrl(key.host)}`;
|
|
169
|
+
return {
|
|
170
|
+
key,
|
|
171
|
+
kind: key.kind,
|
|
172
|
+
env: key.env,
|
|
173
|
+
baseUrl,
|
|
174
|
+
context
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/contract/error-mapping.ts
|
|
179
|
+
/**
|
|
180
|
+
* Maps poly-auth's standardized error contract onto the SDK taxonomy — the
|
|
181
|
+
* backward-compat shim's error half (FR-DX-2/3). Discriminator codes verified
|
|
182
|
+
* against poly-auth/src/api/constants/auth-responses.ts (2026-07-06). Unknown
|
|
183
|
+
* shapes become a typed UNRECOGNIZED_RESPONSE — never a guess.
|
|
184
|
+
*/
|
|
185
|
+
const REVOKED_CODES = /* @__PURE__ */ new Set(["REFRESH_TOKEN_REUSE", "SESSION_REVOKED"]);
|
|
186
|
+
const EXPIRED_CODES = /* @__PURE__ */ new Set([
|
|
187
|
+
"SESSION_EXPIRED",
|
|
188
|
+
"REFRESH_TOKEN_REQUIRED",
|
|
189
|
+
"REFRESH_TOKEN_INVALID",
|
|
190
|
+
"REFRESH_TOKEN_EXPIRED"
|
|
191
|
+
]);
|
|
192
|
+
const FORBIDDEN_CODES = /* @__PURE__ */ new Set([
|
|
193
|
+
"TENANT_MEMBERSHIP_DENIED",
|
|
194
|
+
"ACCOUNT_ARCHIVED",
|
|
195
|
+
"NO_ACCESS"
|
|
196
|
+
]);
|
|
197
|
+
function asPlatformBody(body) {
|
|
198
|
+
return body && typeof body === "object" ? body : {};
|
|
199
|
+
}
|
|
200
|
+
function mapPlatformError(response) {
|
|
201
|
+
const { status, requestId } = response;
|
|
202
|
+
const body = asPlatformBody(response.body);
|
|
203
|
+
const message = typeof body.message === "string" ? body.message : `PolyX request failed (${status})`;
|
|
204
|
+
const code = typeof body.code === "string" ? body.code : void 0;
|
|
205
|
+
const common = { requestId };
|
|
206
|
+
if (code && REVOKED_CODES.has(code)) return new SessionRevokedError(message, {
|
|
207
|
+
code,
|
|
208
|
+
...common
|
|
209
|
+
});
|
|
210
|
+
if (code && EXPIRED_CODES.has(code)) return new SessionExpiredError(message, {
|
|
211
|
+
code,
|
|
212
|
+
...common
|
|
213
|
+
});
|
|
214
|
+
if (code === "ACCESS_TOKEN_EXPIRED") return new UnauthenticatedError(message, {
|
|
215
|
+
code,
|
|
216
|
+
refreshable: true,
|
|
217
|
+
...common
|
|
218
|
+
});
|
|
219
|
+
if (code === "TOKEN_REQUIRED") return new UnauthenticatedError(message, {
|
|
220
|
+
code,
|
|
221
|
+
refreshable: false,
|
|
222
|
+
...common
|
|
223
|
+
});
|
|
224
|
+
if (code && FORBIDDEN_CODES.has(code)) return new ForbiddenError(message, {
|
|
225
|
+
code,
|
|
226
|
+
...common
|
|
227
|
+
});
|
|
228
|
+
if (status === 400 || status === 422) return new ValidationError(message, {
|
|
229
|
+
code: code ?? "BAD_REQUEST",
|
|
230
|
+
errors: Array.isArray(body.errors) ? body.errors : [],
|
|
231
|
+
...common
|
|
232
|
+
});
|
|
233
|
+
if (status === 429) {
|
|
234
|
+
const header = response.headers?.["retry-after"];
|
|
235
|
+
return new RateLimitedError(message, {
|
|
236
|
+
retryAfterSeconds: header !== void 0 && /^\d+$/.test(header) ? Number(header) : void 0,
|
|
237
|
+
...common
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (status === 401) return new UnauthenticatedError(message, {
|
|
241
|
+
code: code ?? "UNAUTHENTICATED",
|
|
242
|
+
refreshable: false,
|
|
243
|
+
...common
|
|
244
|
+
});
|
|
245
|
+
if (status === 403) return new ForbiddenError(message, {
|
|
246
|
+
code: code ?? "FORBIDDEN",
|
|
247
|
+
...common
|
|
248
|
+
});
|
|
249
|
+
if (status >= 500) return new UpstreamError(message, {
|
|
250
|
+
code: code ?? "UPSTREAM_ERROR",
|
|
251
|
+
meta: { status },
|
|
252
|
+
...common
|
|
253
|
+
});
|
|
254
|
+
return new UpstreamError("PolyX returned a response this SDK version does not recognize.", {
|
|
255
|
+
code: "UNRECOGNIZED_RESPONSE",
|
|
256
|
+
meta: { status },
|
|
257
|
+
...common
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/transport/index.ts
|
|
262
|
+
const DEFAULT_RETRY_POLICY = {
|
|
263
|
+
maxAttempts: 3,
|
|
264
|
+
retryDelayMs: 250
|
|
265
|
+
};
|
|
266
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
267
|
+
function isTransientStatus(status) {
|
|
268
|
+
return status >= 500;
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/transport/fetch.ts
|
|
272
|
+
function sleep(ms) {
|
|
273
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
274
|
+
}
|
|
275
|
+
async function parseBody(response) {
|
|
276
|
+
const text = await response.text();
|
|
277
|
+
if (text.length === 0) return void 0;
|
|
278
|
+
try {
|
|
279
|
+
return JSON.parse(text);
|
|
280
|
+
} catch {
|
|
281
|
+
return text;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function headersToRecord(headers) {
|
|
285
|
+
const record = {};
|
|
286
|
+
headers.forEach((value, key) => {
|
|
287
|
+
record[key.toLowerCase()] = value;
|
|
288
|
+
});
|
|
289
|
+
return record;
|
|
290
|
+
}
|
|
291
|
+
/** Default transport: global fetch + timeout + bounded transient-only retries. */
|
|
292
|
+
var FetchTransport = class {
|
|
293
|
+
retry;
|
|
294
|
+
constructor(retry = {}) {
|
|
295
|
+
this.retry = {
|
|
296
|
+
...DEFAULT_RETRY_POLICY,
|
|
297
|
+
...retry
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async request(request) {
|
|
301
|
+
const timeoutMs = request.timeoutMs ?? 15e3;
|
|
302
|
+
let lastNetworkError;
|
|
303
|
+
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt += 1) {
|
|
304
|
+
let response;
|
|
305
|
+
try {
|
|
306
|
+
response = await fetch(request.url, {
|
|
307
|
+
method: request.method,
|
|
308
|
+
headers: {
|
|
309
|
+
...request.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
310
|
+
...request.headers
|
|
311
|
+
},
|
|
312
|
+
body: request.body !== void 0 ? JSON.stringify(request.body) : void 0,
|
|
313
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
lastNetworkError = error;
|
|
317
|
+
if (attempt < this.retry.maxAttempts) {
|
|
318
|
+
await sleep(this.retry.retryDelayMs * 2 ** (attempt - 1));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
throw new UpstreamError("PolyX deployment is unreachable.", {
|
|
322
|
+
code: "NETWORK_ERROR",
|
|
323
|
+
meta: {
|
|
324
|
+
url: request.url,
|
|
325
|
+
attempts: attempt
|
|
326
|
+
},
|
|
327
|
+
cause: error
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const result = {
|
|
331
|
+
status: response.status,
|
|
332
|
+
headers: headersToRecord(response.headers),
|
|
333
|
+
body: await parseBody(response)
|
|
334
|
+
};
|
|
335
|
+
if (isTransientStatus(result.status) && attempt < this.retry.maxAttempts) {
|
|
336
|
+
await sleep(this.retry.retryDelayMs * 2 ** (attempt - 1));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
throw new UpstreamError("PolyX request could not be completed.", {
|
|
342
|
+
code: "NETWORK_ERROR",
|
|
343
|
+
cause: lastNetworkError
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/transport/mock.ts
|
|
349
|
+
/**
|
|
350
|
+
* Fixture-driven transport with the exact contract of FetchTransport — the
|
|
351
|
+
* seam mock mode (FR-DX-4) plugs in. Unmatched requests fail loudly so fixture
|
|
352
|
+
* bugs surface instead of hanging tests.
|
|
353
|
+
*/
|
|
354
|
+
var MockTransport = class {
|
|
355
|
+
routes;
|
|
356
|
+
constructor(routes) {
|
|
357
|
+
this.routes = routes.map((route) => ({
|
|
358
|
+
...route,
|
|
359
|
+
used: 0
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
request(request) {
|
|
363
|
+
const pathname = new URL(request.url).pathname;
|
|
364
|
+
const route = this.routes.find((candidate) => candidate.method === request.method && candidate.path === pathname && (candidate.times === void 0 || candidate.used < candidate.times));
|
|
365
|
+
if (!route) return Promise.reject(/* @__PURE__ */ new Error(`MockTransport: no mock route for ${request.method} ${pathname}`));
|
|
366
|
+
route.used += 1;
|
|
367
|
+
if (route.networkError) return Promise.reject(new UpstreamError("PolyX deployment is unreachable.", {
|
|
368
|
+
code: "NETWORK_ERROR",
|
|
369
|
+
meta: {
|
|
370
|
+
url: request.url,
|
|
371
|
+
mock: true
|
|
372
|
+
}
|
|
373
|
+
}));
|
|
374
|
+
const response = route.response ?? {
|
|
375
|
+
status: 200,
|
|
376
|
+
body: void 0
|
|
377
|
+
};
|
|
378
|
+
return Promise.resolve({
|
|
379
|
+
status: response.status,
|
|
380
|
+
headers: Object.fromEntries(Object.entries(response.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value])),
|
|
381
|
+
body: response.body
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/session/state.ts
|
|
387
|
+
const INITIAL_STATE = { status: "resolving" };
|
|
388
|
+
function reduce(state, event) {
|
|
389
|
+
switch (event.type) {
|
|
390
|
+
case "hydrated":
|
|
391
|
+
if (state.status !== "resolving") return state;
|
|
392
|
+
return event.session ? {
|
|
393
|
+
status: "authenticated",
|
|
394
|
+
session: event.session
|
|
395
|
+
} : {
|
|
396
|
+
status: "signed-out",
|
|
397
|
+
reason: "signed-out"
|
|
398
|
+
};
|
|
399
|
+
case "established": return {
|
|
400
|
+
status: "authenticated",
|
|
401
|
+
session: event.session
|
|
402
|
+
};
|
|
403
|
+
case "refresh-started":
|
|
404
|
+
if (state.status !== "authenticated") return state;
|
|
405
|
+
return {
|
|
406
|
+
status: "refreshing",
|
|
407
|
+
session: state.session
|
|
408
|
+
};
|
|
409
|
+
case "refresh-succeeded":
|
|
410
|
+
if (state.status !== "refreshing") return state;
|
|
411
|
+
return {
|
|
412
|
+
status: "authenticated",
|
|
413
|
+
session: event.session
|
|
414
|
+
};
|
|
415
|
+
case "refresh-failed-transient":
|
|
416
|
+
if (state.status !== "refreshing") return state;
|
|
417
|
+
return {
|
|
418
|
+
status: "authenticated",
|
|
419
|
+
session: state.session
|
|
420
|
+
};
|
|
421
|
+
case "expired":
|
|
422
|
+
if (state.status !== "authenticated" && state.status !== "refreshing") return state;
|
|
423
|
+
return {
|
|
424
|
+
status: "signed-out",
|
|
425
|
+
reason: "expired"
|
|
426
|
+
};
|
|
427
|
+
case "revoked":
|
|
428
|
+
if (state.status !== "authenticated" && state.status !== "refreshing") return state;
|
|
429
|
+
return {
|
|
430
|
+
status: "signed-out",
|
|
431
|
+
reason: "revoked"
|
|
432
|
+
};
|
|
433
|
+
case "signed-out": return {
|
|
434
|
+
status: "signed-out",
|
|
435
|
+
reason: "signed-out"
|
|
436
|
+
};
|
|
437
|
+
default: return state;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/session/seams.ts
|
|
442
|
+
var InMemorySessionStore = class {
|
|
443
|
+
stored = null;
|
|
444
|
+
load() {
|
|
445
|
+
return Promise.resolve(this.stored);
|
|
446
|
+
}
|
|
447
|
+
save(stored) {
|
|
448
|
+
this.stored = stored;
|
|
449
|
+
return Promise.resolve();
|
|
450
|
+
}
|
|
451
|
+
clear() {
|
|
452
|
+
this.stored = null;
|
|
453
|
+
return Promise.resolve();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* A shared in-process bus. Two engines given the same instance simulate two
|
|
458
|
+
* tabs on one channel. Delivers to every subscriber including the publisher;
|
|
459
|
+
* the engine's transitions are idempotent so an echo is a harmless no-op.
|
|
460
|
+
*/
|
|
461
|
+
var InMemorySessionChannel = class {
|
|
462
|
+
listeners = /* @__PURE__ */ new Set();
|
|
463
|
+
publish(event) {
|
|
464
|
+
for (const listener of [...this.listeners]) listener(event);
|
|
465
|
+
}
|
|
466
|
+
subscribe(listener) {
|
|
467
|
+
this.listeners.add(listener);
|
|
468
|
+
return () => this.listeners.delete(listener);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
/** Per-key promise chain: fn runs after the prior holder settles (either way). */
|
|
472
|
+
var InMemoryLock = class {
|
|
473
|
+
chains = /* @__PURE__ */ new Map();
|
|
474
|
+
runExclusive(key, fn) {
|
|
475
|
+
const run = (this.chains.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
476
|
+
this.chains.set(key, run.then(() => void 0, () => void 0));
|
|
477
|
+
return run;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var SystemClock = class {
|
|
481
|
+
now() {
|
|
482
|
+
return Date.now();
|
|
483
|
+
}
|
|
484
|
+
setTimer(fn, delayMs) {
|
|
485
|
+
return setTimeout(fn, delayMs);
|
|
486
|
+
}
|
|
487
|
+
clearTimer(handle) {
|
|
488
|
+
clearTimeout(handle);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/session/engine.ts
|
|
493
|
+
/**
|
|
494
|
+
* SessionEngine (F003): the one owner of session lifecycle — hydrate,
|
|
495
|
+
* establish, single-flight refresh, sign-out, cross-tab mirroring, and
|
|
496
|
+
* server-authoritative proactive-refresh scheduling. Network calls are
|
|
497
|
+
* injected (`refreshTokens`) so core stays endpoint-agnostic; custody,
|
|
498
|
+
* cross-tab, lock, and clock are seams (in-memory defaults provided).
|
|
499
|
+
*/
|
|
500
|
+
const REFRESH_LOCK_KEY = "session.refresh";
|
|
501
|
+
const DEFAULT_SKEW_MS = 6e4;
|
|
502
|
+
var SessionEngine = class {
|
|
503
|
+
state = INITIAL_STATE;
|
|
504
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
505
|
+
store;
|
|
506
|
+
channel;
|
|
507
|
+
lock;
|
|
508
|
+
clock;
|
|
509
|
+
refreshTokens;
|
|
510
|
+
skewMs;
|
|
511
|
+
onSignOut;
|
|
512
|
+
unsubscribeChannel;
|
|
513
|
+
refreshTimer = null;
|
|
514
|
+
/** Bumped by establish/sign-out so an in-flight refresh can detect supersession. */
|
|
515
|
+
generation = 0;
|
|
516
|
+
constructor(options) {
|
|
517
|
+
this.store = options.store ?? new InMemorySessionStore();
|
|
518
|
+
this.channel = options.channel ?? new InMemorySessionChannel();
|
|
519
|
+
this.lock = options.lock ?? new InMemoryLock();
|
|
520
|
+
this.clock = options.clock ?? new SystemClock();
|
|
521
|
+
this.refreshTokens = options.refreshTokens;
|
|
522
|
+
this.skewMs = options.refreshSkewMs ?? DEFAULT_SKEW_MS;
|
|
523
|
+
this.onSignOut = options.onSignOut;
|
|
524
|
+
this.unsubscribeChannel = this.channel.subscribe((event) => {
|
|
525
|
+
this.applySignOut(event.reason, false);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
getState() {
|
|
529
|
+
return this.state;
|
|
530
|
+
}
|
|
531
|
+
subscribe(listener) {
|
|
532
|
+
this.subscribers.add(listener);
|
|
533
|
+
return () => this.subscribers.delete(listener);
|
|
534
|
+
}
|
|
535
|
+
/** Restore a persisted session (or resolve to signed-out). Call once at startup. */
|
|
536
|
+
async hydrate() {
|
|
537
|
+
const stored = await this.store.load();
|
|
538
|
+
this.dispatch({
|
|
539
|
+
type: "hydrated",
|
|
540
|
+
session: stored?.session ?? null
|
|
541
|
+
});
|
|
542
|
+
if (stored) this.scheduleRefresh(stored);
|
|
543
|
+
}
|
|
544
|
+
/** Establish a session from freshly-issued or externally-exchanged tokens (FR-SES-1/2). */
|
|
545
|
+
async establishSession(stored) {
|
|
546
|
+
this.generation += 1;
|
|
547
|
+
await this.store.save(stored);
|
|
548
|
+
this.dispatch({
|
|
549
|
+
type: "established",
|
|
550
|
+
session: stored.session
|
|
551
|
+
});
|
|
552
|
+
this.scheduleRefresh(stored);
|
|
553
|
+
}
|
|
554
|
+
/** Current access token, refreshing first if it is at/near expiry (FR-SES-4). */
|
|
555
|
+
async getAccessToken() {
|
|
556
|
+
const stored = await this.store.load();
|
|
557
|
+
if (stored && !this.isStale(stored.tokens)) return stored.tokens.accessToken;
|
|
558
|
+
return (await this.refresh()).accessToken;
|
|
559
|
+
}
|
|
560
|
+
async signOut() {
|
|
561
|
+
await this.applySignOut("signed-out", true);
|
|
562
|
+
}
|
|
563
|
+
/** Release the cross-tab subscription (call on teardown). */
|
|
564
|
+
dispose() {
|
|
565
|
+
this.unsubscribeChannel();
|
|
566
|
+
this.cancelScheduledRefresh();
|
|
567
|
+
}
|
|
568
|
+
isStale(tokens) {
|
|
569
|
+
return this.clock.now() >= tokens.accessTokenExpiresAt - this.skewMs;
|
|
570
|
+
}
|
|
571
|
+
refresh() {
|
|
572
|
+
return this.lock.runExclusive(REFRESH_LOCK_KEY, async () => {
|
|
573
|
+
const gen = this.generation;
|
|
574
|
+
const current = await this.store.load();
|
|
575
|
+
if (!current) throw new UnauthenticatedError("No session to refresh.", {
|
|
576
|
+
code: "UNAUTHENTICATED",
|
|
577
|
+
refreshable: false
|
|
578
|
+
});
|
|
579
|
+
if (!this.isStale(current.tokens)) return current.tokens;
|
|
580
|
+
this.dispatch({ type: "refresh-started" });
|
|
581
|
+
let next;
|
|
582
|
+
try {
|
|
583
|
+
next = await this.refreshTokens(current);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (this.generation === gen) this.handleRefreshError(error);
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
if (this.generation !== gen) {
|
|
589
|
+
if (this.state.status === "signed-out") throw new UnauthenticatedError("Session ended during refresh.", {
|
|
590
|
+
code: "UNAUTHENTICATED",
|
|
591
|
+
refreshable: false
|
|
592
|
+
});
|
|
593
|
+
return (await this.store.load())?.tokens ?? next.tokens;
|
|
594
|
+
}
|
|
595
|
+
await this.store.save(next);
|
|
596
|
+
this.dispatch({
|
|
597
|
+
type: "refresh-succeeded",
|
|
598
|
+
session: next.session
|
|
599
|
+
});
|
|
600
|
+
this.scheduleRefresh(next);
|
|
601
|
+
return next.tokens;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
handleRefreshError(error) {
|
|
605
|
+
if (error instanceof SessionRevokedError) {
|
|
606
|
+
this.applySignOut("revoked", true);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (error instanceof SessionExpiredError) {
|
|
610
|
+
this.applySignOut("expired", true);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (error instanceof UpstreamError || error instanceof Error) this.dispatch({ type: "refresh-failed-transient" });
|
|
614
|
+
}
|
|
615
|
+
async applySignOut(reason, broadcast) {
|
|
616
|
+
if (this.state.status === "signed-out") return;
|
|
617
|
+
this.generation += 1;
|
|
618
|
+
this.cancelScheduledRefresh();
|
|
619
|
+
await this.store.clear();
|
|
620
|
+
this.dispatch(signOutEvent(reason));
|
|
621
|
+
if (broadcast) this.channel.publish({
|
|
622
|
+
type: "signed-out",
|
|
623
|
+
reason
|
|
624
|
+
});
|
|
625
|
+
this.onSignOut?.(reason);
|
|
626
|
+
}
|
|
627
|
+
scheduleRefresh(stored) {
|
|
628
|
+
this.cancelScheduledRefresh();
|
|
629
|
+
const delay = stored.tokens.accessTokenExpiresAt - this.skewMs - this.clock.now();
|
|
630
|
+
this.refreshTimer = this.clock.setTimer(() => {
|
|
631
|
+
this.refresh().catch(() => void 0);
|
|
632
|
+
}, Math.max(0, delay));
|
|
633
|
+
}
|
|
634
|
+
cancelScheduledRefresh() {
|
|
635
|
+
if (this.refreshTimer !== null) {
|
|
636
|
+
this.clock.clearTimer(this.refreshTimer);
|
|
637
|
+
this.refreshTimer = null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
dispatch(event) {
|
|
641
|
+
const next = reduce(this.state, event);
|
|
642
|
+
if (next === this.state) return;
|
|
643
|
+
this.state = next;
|
|
644
|
+
for (const listener of [...this.subscribers]) listener(next);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
function signOutEvent(reason) {
|
|
648
|
+
if (reason === "expired") return { type: "expired" };
|
|
649
|
+
if (reason === "revoked") return { type: "revoked" };
|
|
650
|
+
return { type: "signed-out" };
|
|
651
|
+
}
|
|
652
|
+
function createSessionEngine(options) {
|
|
653
|
+
return new SessionEngine(options);
|
|
654
|
+
}
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/auth/pkce.ts
|
|
657
|
+
/**
|
|
658
|
+
* PKCE (RFC 7636) via Web Crypto — `crypto.subtle` / `crypto.getRandomValues`
|
|
659
|
+
* are universal (browser + Node ≥20), so core needs no runtime-specific import.
|
|
660
|
+
*/
|
|
661
|
+
function base64UrlEncode(bytes) {
|
|
662
|
+
let binary = "";
|
|
663
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
664
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
665
|
+
}
|
|
666
|
+
/** S256 code challenge for a given verifier (exported for RFC test-vector coverage). */
|
|
667
|
+
async function computeChallenge(verifier) {
|
|
668
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
669
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
670
|
+
}
|
|
671
|
+
async function generatePkce() {
|
|
672
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
673
|
+
crypto.getRandomValues(bytes);
|
|
674
|
+
const verifier = base64UrlEncode(bytes);
|
|
675
|
+
return {
|
|
676
|
+
verifier,
|
|
677
|
+
challenge: await computeChallenge(verifier),
|
|
678
|
+
method: "S256"
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function randomState() {
|
|
682
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
683
|
+
crypto.getRandomValues(bytes);
|
|
684
|
+
return base64UrlEncode(bytes);
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/auth/endpoints.ts
|
|
688
|
+
/**
|
|
689
|
+
* Every PolyX auth endpoint path in one place — the cross-repo contract surface
|
|
690
|
+
* with poly-x. Paths are relative to the deployment base URL (which already
|
|
691
|
+
* carries any deployment path prefix, e.g. `/poly-x`).
|
|
692
|
+
*
|
|
693
|
+
* Verified against poly-auth routes 2026-07-07: the IdP has no refresh grant;
|
|
694
|
+
* refresh is the legacy `/auth/refresh-token`.
|
|
695
|
+
*/
|
|
696
|
+
const ENDPOINTS = {
|
|
697
|
+
authorize: "/v1/idp/authorize",
|
|
698
|
+
token: "/v1/idp/token",
|
|
699
|
+
refresh: "/v1/auth/refresh-token",
|
|
700
|
+
logout: "/v1/idp/logout",
|
|
701
|
+
orgResolve: "/v1/idp/org-resolve"
|
|
702
|
+
};
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/auth/outcomes.ts
|
|
705
|
+
function asRecord$1(value) {
|
|
706
|
+
return value && typeof value === "object" ? value : {};
|
|
707
|
+
}
|
|
708
|
+
function parseTenants(value) {
|
|
709
|
+
if (!Array.isArray(value)) return [];
|
|
710
|
+
return value.map((raw) => {
|
|
711
|
+
const t = asRecord$1(raw);
|
|
712
|
+
return {
|
|
713
|
+
tenantId: String(t.tenantId ?? ""),
|
|
714
|
+
organizationCode: typeof t.organizationCode === "string" ? t.organizationCode : void 0,
|
|
715
|
+
slug: typeof t.slug === "string" ? t.slug : void 0,
|
|
716
|
+
name: typeof t.name === "string" ? t.name : void 0
|
|
717
|
+
};
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function parseAuthorizeOutcome(status, body) {
|
|
721
|
+
const b = asRecord$1(body);
|
|
722
|
+
const statusField = typeof b.status === "string" ? b.status : void 0;
|
|
723
|
+
if (status === 403) return { type: "no_access" };
|
|
724
|
+
if (status === 401) return { type: "login_required" };
|
|
725
|
+
if (statusField === "authorized" && typeof b.code === "string") return {
|
|
726
|
+
type: "authorized",
|
|
727
|
+
code: b.code,
|
|
728
|
+
state: typeof b.state === "string" ? b.state : "",
|
|
729
|
+
redirectUri: typeof b.redirectUri === "string" ? b.redirectUri : void 0
|
|
730
|
+
};
|
|
731
|
+
if (statusField === "select_tenant") return {
|
|
732
|
+
type: "select_tenant",
|
|
733
|
+
state: typeof b.state === "string" ? b.state : "",
|
|
734
|
+
tenants: parseTenants(b.tenants)
|
|
735
|
+
};
|
|
736
|
+
if (statusField === "login_required" || b.code === "LOGIN_REQUIRED") return { type: "login_required" };
|
|
737
|
+
return { type: "login_required" };
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/auth/normalize.ts
|
|
741
|
+
const FALLBACK_TTL_MS = 5 * 6e4;
|
|
742
|
+
function decodeJwtExp(token) {
|
|
743
|
+
const parts = token.split(".");
|
|
744
|
+
if (parts.length < 2 || !parts[1]) return null;
|
|
745
|
+
try {
|
|
746
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
747
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
748
|
+
const payload = JSON.parse(atob(padded));
|
|
749
|
+
return typeof payload.exp === "number" ? payload.exp * 1e3 : null;
|
|
750
|
+
} catch {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function toTokenSet(accessToken, refreshToken, now) {
|
|
755
|
+
return {
|
|
756
|
+
accessToken,
|
|
757
|
+
refreshToken,
|
|
758
|
+
accessTokenExpiresAt: decodeJwtExp(accessToken) ?? now + FALLBACK_TTL_MS
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function asRecord(value) {
|
|
762
|
+
return value && typeof value === "object" ? value : {};
|
|
763
|
+
}
|
|
764
|
+
/** `POST /idp/token` → StoredSession. Claims mapping is minimal in v1 (userId + org); AUTHZ populates roles/permissions later. */
|
|
765
|
+
function normalizeExchange(body, now) {
|
|
766
|
+
const data = asRecord(asRecord(body).data);
|
|
767
|
+
const user = asRecord(data.user);
|
|
768
|
+
const userId = String(user._id ?? user.id ?? "");
|
|
769
|
+
if (userId.length === 0) throw new Error("PolyX token response is missing a user identity.");
|
|
770
|
+
const claims = {
|
|
771
|
+
userId,
|
|
772
|
+
organizationId: data.tenantId != null ? String(data.tenantId) : void 0,
|
|
773
|
+
roles: [],
|
|
774
|
+
permissions: []
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
tokens: toTokenSet(String(data.accessToken ?? ""), stringOrUndefined(data.refreshToken), now),
|
|
778
|
+
session: { claims }
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/** `POST /auth/refresh-token` → StoredSession. No user in the response, so claims carry forward. */
|
|
782
|
+
function normalizeRefresh(body, current, now) {
|
|
783
|
+
const b = asRecord(body);
|
|
784
|
+
return {
|
|
785
|
+
tokens: toTokenSet(String(b.accessToken ?? ""), stringOrUndefined(b.refreshToken) ?? current.tokens.refreshToken, now),
|
|
786
|
+
session: current.session
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function stringOrUndefined(value) {
|
|
790
|
+
return typeof value === "string" ? value : void 0;
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/auth/pkce-store.ts
|
|
794
|
+
var InMemoryPkceStore = class {
|
|
795
|
+
entries = /* @__PURE__ */ new Map();
|
|
796
|
+
save(entry) {
|
|
797
|
+
this.entries.set(entry.state, entry);
|
|
798
|
+
return Promise.resolve();
|
|
799
|
+
}
|
|
800
|
+
take(state) {
|
|
801
|
+
const entry = this.entries.get(state) ?? null;
|
|
802
|
+
if (entry) this.entries.delete(state);
|
|
803
|
+
return Promise.resolve(entry);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region src/auth/client.ts
|
|
808
|
+
var AuthClient = class {
|
|
809
|
+
config;
|
|
810
|
+
transport;
|
|
811
|
+
clock;
|
|
812
|
+
constructor(options) {
|
|
813
|
+
this.config = options.config;
|
|
814
|
+
this.transport = options.transport;
|
|
815
|
+
this.clock = options.clock ?? new SystemClock();
|
|
816
|
+
}
|
|
817
|
+
buildAuthorizeUrl(params) {
|
|
818
|
+
const url = new URL(this.config.baseUrl + ENDPOINTS.authorize);
|
|
819
|
+
url.searchParams.set("response_type", "code");
|
|
820
|
+
url.searchParams.set("client", this.config.key.raw);
|
|
821
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
822
|
+
url.searchParams.set("state", params.state);
|
|
823
|
+
url.searchParams.set("code_challenge", params.challenge);
|
|
824
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
825
|
+
if (params.organizationCode) url.searchParams.set("organizationCode", params.organizationCode);
|
|
826
|
+
if (params.prompt) url.searchParams.set("prompt", params.prompt);
|
|
827
|
+
return url.toString();
|
|
828
|
+
}
|
|
829
|
+
/** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
|
|
830
|
+
async completeAuthorize(params) {
|
|
831
|
+
const response = await this.transport.request({
|
|
832
|
+
method: "GET",
|
|
833
|
+
url: this.buildAuthorizeUrl(params)
|
|
834
|
+
});
|
|
835
|
+
if (response.status >= 500) throw this.toError(response);
|
|
836
|
+
return parseAuthorizeOutcome(response.status, response.body);
|
|
837
|
+
}
|
|
838
|
+
async exchangeCode(params) {
|
|
839
|
+
const response = await this.transport.request({
|
|
840
|
+
method: "POST",
|
|
841
|
+
url: this.config.baseUrl + ENDPOINTS.token,
|
|
842
|
+
body: {
|
|
843
|
+
grant_type: "authorization_code",
|
|
844
|
+
code: params.code,
|
|
845
|
+
code_verifier: params.codeVerifier,
|
|
846
|
+
redirect_uri: params.redirectUri,
|
|
847
|
+
client: this.config.key.raw
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
if (response.status !== 200) throw this.toError(response);
|
|
851
|
+
return normalizeExchange(response.body, this.clock.now());
|
|
852
|
+
}
|
|
853
|
+
/** The `RefreshTokens` function the SessionEngine (F003) injects. */
|
|
854
|
+
createRefresher() {
|
|
855
|
+
return async (current) => {
|
|
856
|
+
const response = await this.transport.request({
|
|
857
|
+
method: "POST",
|
|
858
|
+
url: this.config.baseUrl + ENDPOINTS.refresh,
|
|
859
|
+
body: current.tokens.refreshToken ? { refreshToken: current.tokens.refreshToken } : {}
|
|
860
|
+
});
|
|
861
|
+
if (response.status !== 200) throw this.toError(response);
|
|
862
|
+
return normalizeRefresh(response.body, current, this.clock.now());
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
async logout(scope = "device") {
|
|
866
|
+
const response = await this.transport.request({
|
|
867
|
+
method: "POST",
|
|
868
|
+
url: this.config.baseUrl + ENDPOINTS.logout,
|
|
869
|
+
body: { scope }
|
|
870
|
+
});
|
|
871
|
+
if (response.status >= 400) throw this.toError(response);
|
|
872
|
+
}
|
|
873
|
+
async resolveOrg(email) {
|
|
874
|
+
const response = await this.transport.request({
|
|
875
|
+
method: "POST",
|
|
876
|
+
url: this.config.baseUrl + ENDPOINTS.orgResolve,
|
|
877
|
+
body: { email }
|
|
878
|
+
});
|
|
879
|
+
if (response.status !== 200) throw this.toError(response);
|
|
880
|
+
const body = response.body ?? {};
|
|
881
|
+
return {
|
|
882
|
+
resolved: body.resolved === true,
|
|
883
|
+
organizationCode: typeof body.organizationCode === "string" ? body.organizationCode : void 0
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
toError(response) {
|
|
887
|
+
return mapPlatformError({
|
|
888
|
+
status: response.status,
|
|
889
|
+
body: response.body,
|
|
890
|
+
headers: response.headers
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region src/index.ts
|
|
896
|
+
/**
|
|
897
|
+
* @poly-x/core — framework-agnostic guts of the PolyX SDK.
|
|
898
|
+
*/
|
|
899
|
+
const PACKAGE_NAME = "@poly-x/core";
|
|
900
|
+
const version = "0.0.0";
|
|
901
|
+
//#endregion
|
|
902
|
+
export { AuthClient, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, FetchTransport, ForbiddenError, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, MockTransport, PACKAGE_NAME, PolyXConfigError, PolyXError, RateLimitedError, SessionEngine, SessionExpiredError, SessionRevokedError, SystemClock, UnauthenticatedError, UpstreamError, ValidationError, computeChallenge, createSessionEngine, decodeJwtExp, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePolyXKey, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
|