@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.
@@ -0,0 +1,491 @@
1
+ //#region src/keys.d.ts
2
+ /**
3
+ * PolyX key parsing (FR-CFG-3/4). Format draft v1 — the cross-repo contract
4
+ * poly-x v4 (FR-KEY-2) implements; test vectors live in test/fixtures/keys.ts
5
+ * and are mirrored into poly-x's tests:
6
+ *
7
+ * {kind}_{env}_{base64url("v1|" + deploymentHost + "|" + random)}
8
+ *
9
+ * The deployment host is the issuing PolyX deployment's authority plus optional
10
+ * path prefix, no scheme (e.g. "poly.fintra.ai/poly-x") — there is no single
11
+ * SaaS PolyX, so the key locates its own backend (discovery D8).
12
+ */
13
+ type KeyKind = "pk" | "sk";
14
+ type KeyEnv = "test" | "live";
15
+ interface ParsedKey {
16
+ kind: KeyKind;
17
+ env: KeyEnv;
18
+ host: string;
19
+ formatVersion: string;
20
+ raw: string;
21
+ }
22
+ declare function parsePolyXKey(raw: string): ParsedKey;
23
+ //#endregion
24
+ //#region src/config.d.ts
25
+ type RuntimeContext = "browser" | "server";
26
+ interface ResolveConfigInput {
27
+ key: string;
28
+ /** Wins over the key-embedded host. */
29
+ baseUrl?: string;
30
+ /** Defaults to runtime auto-detection. */
31
+ context?: RuntimeContext;
32
+ }
33
+ interface ResolvedConfig {
34
+ key: ParsedKey;
35
+ kind: KeyKind;
36
+ env: KeyEnv;
37
+ baseUrl: string;
38
+ context: RuntimeContext;
39
+ }
40
+ declare function resolveConfig(input: ResolveConfigInput): ResolvedConfig;
41
+ //#endregion
42
+ //#region src/errors.d.ts
43
+ /**
44
+ * The typed error taxonomy shared by every @poly-x package (FR-DX-5).
45
+ * Every failure a consumer can see is one of these classes — branchable by
46
+ * `instanceof` or `code`, and safe to serialize: `toJSON()` redacts key
47
+ * material and tokens (FR-DX-3).
48
+ */
49
+ declare function redactSensitive(text: string): string;
50
+ interface PolyXErrorOptions {
51
+ code: string;
52
+ requestId?: string;
53
+ /** Arbitrary diagnostic context; redacted on serialization. */
54
+ meta?: Record<string, unknown>;
55
+ cause?: unknown;
56
+ }
57
+ declare class PolyXError extends Error {
58
+ readonly code: string;
59
+ readonly requestId?: string;
60
+ readonly meta?: Record<string, unknown>;
61
+ constructor(message: string, options: PolyXErrorOptions);
62
+ toJSON(): Record<string, unknown>;
63
+ }
64
+ declare class PolyXConfigError extends PolyXError {
65
+ constructor(message: string, options?: Partial<PolyXErrorOptions>);
66
+ }
67
+ /** Not signed in. `refreshable: true` means a silent refresh may recover it. */
68
+ declare class UnauthenticatedError extends PolyXError {
69
+ readonly refreshable: boolean;
70
+ constructor(message: string, options: PolyXErrorOptions & {
71
+ refreshable: boolean;
72
+ });
73
+ }
74
+ /** Terminal session end (expiry / invalid refresh) — re-authentication required. */
75
+ declare class SessionExpiredError extends PolyXError {
76
+ readonly shouldRelogin = true;
77
+ constructor(message: string, options: PolyXErrorOptions);
78
+ }
79
+ /** Session (or its whole family, on refresh-token reuse) was revoked. */
80
+ declare class SessionRevokedError extends PolyXError {
81
+ readonly shouldRelogin = true;
82
+ constructor(message: string, options: PolyXErrorOptions);
83
+ }
84
+ declare class ForbiddenError extends PolyXError {}
85
+ interface FieldError {
86
+ field: string;
87
+ message: string;
88
+ }
89
+ declare class ValidationError extends PolyXError {
90
+ readonly errors: FieldError[];
91
+ constructor(message: string, options: Partial<PolyXErrorOptions> & {
92
+ errors: FieldError[];
93
+ });
94
+ }
95
+ declare class RateLimitedError extends PolyXError {
96
+ readonly retryAfterSeconds?: number;
97
+ constructor(message: string, options: Partial<PolyXErrorOptions> & {
98
+ retryAfterSeconds?: number;
99
+ });
100
+ }
101
+ /** Platform unreachable, 5xx, timeout, or an unrecognizable response. */
102
+ declare class UpstreamError extends PolyXError {}
103
+ //#endregion
104
+ //#region src/contract/error-mapping.d.ts
105
+ interface PlatformResponseLike {
106
+ status: number;
107
+ body: unknown;
108
+ headers?: Record<string, string>;
109
+ requestId?: string;
110
+ }
111
+ declare function mapPlatformError(response: PlatformResponseLike): PolyXError;
112
+ //#endregion
113
+ //#region src/transport/index.d.ts
114
+ /**
115
+ * The transport seam (FR-DX-4): everything in the SDK talks HTTP through this
116
+ * interface, so mock mode swaps the transport — not the code under test.
117
+ *
118
+ * Contract: `request` resolves with a TransportResponse for ANY HTTP outcome
119
+ * (mapping to taxonomy errors is the caller's job, via mapPlatformError) and
120
+ * rejects with UpstreamError only for network-level failures (unreachable,
121
+ * timeout) after bounded transient retries.
122
+ */
123
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
124
+ interface TransportRequest {
125
+ method: HttpMethod;
126
+ url: string;
127
+ headers?: Record<string, string>;
128
+ body?: unknown;
129
+ timeoutMs?: number;
130
+ }
131
+ interface TransportResponse {
132
+ status: number;
133
+ headers: Record<string, string>;
134
+ body: unknown;
135
+ }
136
+ interface Transport {
137
+ request(request: TransportRequest): Promise<TransportResponse>;
138
+ }
139
+ interface RetryPolicy {
140
+ /** Total attempts including the first (default 3). */
141
+ maxAttempts: number;
142
+ /** Base backoff between attempts, doubled each retry (default 250ms). */
143
+ retryDelayMs: number;
144
+ }
145
+ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
146
+ declare const DEFAULT_TIMEOUT_MS = 15000;
147
+ declare function isTransientStatus(status: number): boolean;
148
+ //#endregion
149
+ //#region src/transport/fetch.d.ts
150
+ /** Default transport: global fetch + timeout + bounded transient-only retries. */
151
+ declare class FetchTransport implements Transport {
152
+ private readonly retry;
153
+ constructor(retry?: Partial<RetryPolicy>);
154
+ request(request: TransportRequest): Promise<TransportResponse>;
155
+ }
156
+ //#endregion
157
+ //#region src/transport/mock.d.ts
158
+ interface MockRoute {
159
+ method: HttpMethod;
160
+ /** Matched against the request URL's pathname. */
161
+ path: string;
162
+ response?: {
163
+ status: number;
164
+ body: unknown;
165
+ headers?: Record<string, string>;
166
+ };
167
+ /** Simulate an unreachable deployment instead of an HTTP response. */
168
+ networkError?: boolean;
169
+ /** Consume this route at most N times (default: unlimited). */
170
+ times?: number;
171
+ }
172
+ /**
173
+ * Fixture-driven transport with the exact contract of FetchTransport — the
174
+ * seam mock mode (FR-DX-4) plugs in. Unmatched requests fail loudly so fixture
175
+ * bugs surface instead of hanging tests.
176
+ */
177
+ declare class MockTransport implements Transport {
178
+ private readonly routes;
179
+ constructor(routes: MockRoute[]);
180
+ request(request: TransportRequest): Promise<TransportResponse>;
181
+ }
182
+ //#endregion
183
+ //#region src/session/types.d.ts
184
+ /**
185
+ * Session value types (F003). Claims are the *verified* identity/permission
186
+ * data the authorization surface (a later feature) reads; tokens are custody
187
+ * material held by the SessionStore seam, never exposed to consumer UI.
188
+ */
189
+ interface TokenSet {
190
+ accessToken: string;
191
+ /** May be absent when custody keeps it httpOnly / server-side only. */
192
+ refreshToken?: string;
193
+ /** Epoch milliseconds when the access token expires, per the server. */
194
+ accessTokenExpiresAt: number;
195
+ }
196
+ interface SessionClaims {
197
+ userId: string;
198
+ organizationId?: string;
199
+ roles: readonly string[];
200
+ permissions: readonly string[];
201
+ }
202
+ interface Session {
203
+ claims: SessionClaims;
204
+ }
205
+ /** What the SessionStore persists: custody material + the derived session. */
206
+ interface StoredSession {
207
+ tokens: TokenSet;
208
+ session: Session;
209
+ }
210
+ //#endregion
211
+ //#region src/session/state.d.ts
212
+ type SignedOutReason = "expired" | "revoked" | "signed-out";
213
+ type SessionState = {
214
+ status: "resolving";
215
+ } | {
216
+ status: "authenticated";
217
+ session: Session;
218
+ } | {
219
+ status: "refreshing";
220
+ session: Session;
221
+ } | {
222
+ status: "signed-out";
223
+ reason: SignedOutReason;
224
+ };
225
+ type SessionEvent = {
226
+ type: "hydrated";
227
+ session: Session | null;
228
+ } | {
229
+ type: "established";
230
+ session: Session;
231
+ } | {
232
+ type: "refresh-started";
233
+ } | {
234
+ type: "refresh-succeeded";
235
+ session: Session;
236
+ } | {
237
+ type: "refresh-failed-transient";
238
+ } | {
239
+ type: "expired";
240
+ } | {
241
+ type: "revoked";
242
+ } | {
243
+ type: "signed-out";
244
+ };
245
+ declare const INITIAL_STATE: SessionState;
246
+ declare function reduce(state: SessionState, event: SessionEvent): SessionState;
247
+ //#endregion
248
+ //#region src/session/seams.d.ts
249
+ /** Token + session custody. */
250
+ interface SessionStore {
251
+ load(): Promise<StoredSession | null>;
252
+ save(stored: StoredSession): Promise<void>;
253
+ clear(): Promise<void>;
254
+ }
255
+ /**
256
+ * Cross-tab coordination. v1 broadcasts sign-out/expiry/revocation only —
257
+ * FR-SES-7 is "logout or hard expiry in one tab reflects everywhere" (login
258
+ * propagation needs shared custody and is out of scope for core).
259
+ */
260
+ interface SessionChannelEvent {
261
+ type: "signed-out";
262
+ reason: SignedOutReason;
263
+ }
264
+ interface SessionChannel {
265
+ publish(event: SessionChannelEvent): void;
266
+ subscribe(listener: (event: SessionChannelEvent) => void): () => void;
267
+ }
268
+ /** Exclusive section for single-flight refresh coordination. */
269
+ interface Lock {
270
+ runExclusive<T>(key: string, fn: () => Promise<T>): Promise<T>;
271
+ }
272
+ /** Opaque timer handle — the engine holds it and hands it back, never inspects it. */
273
+ type TimerHandle = unknown;
274
+ interface Clock {
275
+ now(): number;
276
+ setTimer(fn: () => void, delayMs: number): TimerHandle;
277
+ clearTimer(handle: TimerHandle): void;
278
+ }
279
+ declare class InMemorySessionStore implements SessionStore {
280
+ private stored;
281
+ load(): Promise<StoredSession | null>;
282
+ save(stored: StoredSession): Promise<void>;
283
+ clear(): Promise<void>;
284
+ }
285
+ /**
286
+ * A shared in-process bus. Two engines given the same instance simulate two
287
+ * tabs on one channel. Delivers to every subscriber including the publisher;
288
+ * the engine's transitions are idempotent so an echo is a harmless no-op.
289
+ */
290
+ declare class InMemorySessionChannel implements SessionChannel {
291
+ private readonly listeners;
292
+ publish(event: SessionChannelEvent): void;
293
+ subscribe(listener: (event: SessionChannelEvent) => void): () => void;
294
+ }
295
+ /** Per-key promise chain: fn runs after the prior holder settles (either way). */
296
+ declare class InMemoryLock implements Lock {
297
+ private readonly chains;
298
+ runExclusive<T>(key: string, fn: () => Promise<T>): Promise<T>;
299
+ }
300
+ declare class SystemClock implements Clock {
301
+ now(): number;
302
+ setTimer(fn: () => void, delayMs: number): TimerHandle;
303
+ clearTimer(handle: TimerHandle): void;
304
+ }
305
+ //#endregion
306
+ //#region src/session/engine.d.ts
307
+ type RefreshTokens = (current: StoredSession) => Promise<StoredSession>;
308
+ interface SessionEngineOptions {
309
+ refreshTokens: RefreshTokens;
310
+ store?: SessionStore;
311
+ channel?: SessionChannel;
312
+ lock?: Lock;
313
+ clock?: Clock;
314
+ /** Refresh this many ms before the access token's expiry. Default 60s. */
315
+ refreshSkewMs?: number;
316
+ /** Invoked on every sign-out transition (consumer customizes the UX). */
317
+ onSignOut?: (reason: SignedOutReason) => void;
318
+ }
319
+ declare class SessionEngine {
320
+ private state;
321
+ private readonly subscribers;
322
+ private readonly store;
323
+ private readonly channel;
324
+ private readonly lock;
325
+ private readonly clock;
326
+ private readonly refreshTokens;
327
+ private readonly skewMs;
328
+ private readonly onSignOut?;
329
+ private readonly unsubscribeChannel;
330
+ private refreshTimer;
331
+ /** Bumped by establish/sign-out so an in-flight refresh can detect supersession. */
332
+ private generation;
333
+ constructor(options: SessionEngineOptions);
334
+ getState(): SessionState;
335
+ subscribe(listener: (state: SessionState) => void): () => void;
336
+ /** Restore a persisted session (or resolve to signed-out). Call once at startup. */
337
+ hydrate(): Promise<void>;
338
+ /** Establish a session from freshly-issued or externally-exchanged tokens (FR-SES-1/2). */
339
+ establishSession(stored: StoredSession): Promise<void>;
340
+ /** Current access token, refreshing first if it is at/near expiry (FR-SES-4). */
341
+ getAccessToken(): Promise<string>;
342
+ signOut(): Promise<void>;
343
+ /** Release the cross-tab subscription (call on teardown). */
344
+ dispose(): void;
345
+ private isStale;
346
+ private refresh;
347
+ private handleRefreshError;
348
+ private applySignOut;
349
+ private scheduleRefresh;
350
+ private cancelScheduledRefresh;
351
+ private dispatch;
352
+ }
353
+ declare function createSessionEngine(options: SessionEngineOptions): SessionEngine;
354
+ //#endregion
355
+ //#region src/auth/pkce.d.ts
356
+ /**
357
+ * PKCE (RFC 7636) via Web Crypto — `crypto.subtle` / `crypto.getRandomValues`
358
+ * are universal (browser + Node ≥20), so core needs no runtime-specific import.
359
+ */
360
+ interface PkcePair {
361
+ verifier: string;
362
+ challenge: string;
363
+ method: "S256";
364
+ }
365
+ /** S256 code challenge for a given verifier (exported for RFC test-vector coverage). */
366
+ declare function computeChallenge(verifier: string): Promise<string>;
367
+ declare function generatePkce(): Promise<PkcePair>;
368
+ declare function randomState(): string;
369
+ //#endregion
370
+ //#region src/auth/endpoints.d.ts
371
+ /**
372
+ * Every PolyX auth endpoint path in one place — the cross-repo contract surface
373
+ * with poly-x. Paths are relative to the deployment base URL (which already
374
+ * carries any deployment path prefix, e.g. `/poly-x`).
375
+ *
376
+ * Verified against poly-auth routes 2026-07-07: the IdP has no refresh grant;
377
+ * refresh is the legacy `/auth/refresh-token`.
378
+ */
379
+ declare const ENDPOINTS: {
380
+ readonly authorize: "/v1/idp/authorize";
381
+ readonly token: "/v1/idp/token";
382
+ readonly refresh: "/v1/auth/refresh-token";
383
+ readonly logout: "/v1/idp/logout";
384
+ readonly orgResolve: "/v1/idp/org-resolve";
385
+ };
386
+ //#endregion
387
+ //#region src/auth/outcomes.d.ts
388
+ /**
389
+ * The three membership outcomes (+ login-required) as one typed union
390
+ * (FR-SSO-3): consumers never branch on raw platform codes. Parsed from the
391
+ * `GET /idp/authorize` response.
392
+ */
393
+ interface TenantOption {
394
+ tenantId: string;
395
+ organizationCode?: string;
396
+ slug?: string;
397
+ name?: string;
398
+ }
399
+ type AuthorizeOutcome = {
400
+ type: "authorized";
401
+ code: string;
402
+ state: string;
403
+ redirectUri?: string;
404
+ } | {
405
+ type: "select_tenant";
406
+ state: string;
407
+ tenants: TenantOption[];
408
+ } | {
409
+ type: "no_access";
410
+ } | {
411
+ type: "login_required";
412
+ };
413
+ declare function parseAuthorizeOutcome(status: number, body: unknown): AuthorizeOutcome;
414
+ //#endregion
415
+ //#region src/auth/normalize.d.ts
416
+ declare function decodeJwtExp(token: string): number | null;
417
+ /** `POST /idp/token` → StoredSession. Claims mapping is minimal in v1 (userId + org); AUTHZ populates roles/permissions later. */
418
+ declare function normalizeExchange(body: unknown, now: number): StoredSession;
419
+ /** `POST /auth/refresh-token` → StoredSession. No user in the response, so claims carry forward. */
420
+ declare function normalizeRefresh(body: unknown, current: StoredSession, now: number): StoredSession;
421
+ //#endregion
422
+ //#region src/auth/pkce-store.d.ts
423
+ /**
424
+ * PKCE-transaction custody: the verifier + redirect must survive the round-trip
425
+ * to the PolyX login and back to the callback, keyed by `state`. In-memory
426
+ * default here; `@poly-x/react` supplies a `sessionStorage`-backed store (F005)
427
+ * so the transaction survives a full-page redirect.
428
+ */
429
+ interface PkceEntry {
430
+ state: string;
431
+ verifier: string;
432
+ redirectUri: string;
433
+ createdAt: number;
434
+ }
435
+ interface PkceStore {
436
+ save(entry: PkceEntry): Promise<void>;
437
+ /** Retrieve and remove (single-use) the entry for a state value. */
438
+ take(state: string): Promise<PkceEntry | null>;
439
+ }
440
+ declare class InMemoryPkceStore implements PkceStore {
441
+ private readonly entries;
442
+ save(entry: PkceEntry): Promise<void>;
443
+ take(state: string): Promise<PkceEntry | null>;
444
+ }
445
+ //#endregion
446
+ //#region src/auth/client.d.ts
447
+ interface AuthClientOptions {
448
+ config: ResolvedConfig;
449
+ transport: Transport;
450
+ clock?: Clock;
451
+ }
452
+ interface BuildAuthorizeUrlParams {
453
+ redirectUri: string;
454
+ state: string;
455
+ challenge: string;
456
+ organizationCode?: string;
457
+ /** `none` = silent warm-hop probe (no login UI). */
458
+ prompt?: "none";
459
+ }
460
+ interface ExchangeCodeParams {
461
+ code: string;
462
+ codeVerifier: string;
463
+ redirectUri: string;
464
+ }
465
+ declare class AuthClient {
466
+ private readonly config;
467
+ private readonly transport;
468
+ private readonly clock;
469
+ constructor(options: AuthClientOptions);
470
+ buildAuthorizeUrl(params: BuildAuthorizeUrlParams): string;
471
+ /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
472
+ completeAuthorize(params: BuildAuthorizeUrlParams): Promise<AuthorizeOutcome>;
473
+ exchangeCode(params: ExchangeCodeParams): Promise<StoredSession>;
474
+ /** The `RefreshTokens` function the SessionEngine (F003) injects. */
475
+ createRefresher(): RefreshTokens;
476
+ logout(scope?: "device" | "everywhere"): Promise<void>;
477
+ resolveOrg(email: string): Promise<{
478
+ resolved: boolean;
479
+ organizationCode?: string;
480
+ }>;
481
+ private toError;
482
+ }
483
+ //#endregion
484
+ //#region src/index.d.ts
485
+ /**
486
+ * @poly-x/core — framework-agnostic guts of the PolyX SDK.
487
+ */
488
+ declare const PACKAGE_NAME = "@poly-x/core";
489
+ declare const version = "0.0.0";
490
+ //#endregion
491
+ export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type BuildAuthorizeUrlParams, type Clock, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, type ExchangeCodeParams, FetchTransport, type FieldError, ForbiddenError, type HttpMethod, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, type KeyEnv, type KeyKind, type Lock, type MockRoute, MockTransport, PACKAGE_NAME, type ParsedKey, type PkceEntry, type PkcePair, type PkceStore, type PlatformResponseLike, PolyXConfigError, PolyXError, type PolyXErrorOptions, RateLimitedError, type RefreshTokens, type ResolveConfigInput, type ResolvedConfig, type RetryPolicy, type RuntimeContext, type Session, type SessionChannel, type SessionChannelEvent, type SessionClaims, SessionEngine, type SessionEngineOptions, type SessionEvent, SessionExpiredError, SessionRevokedError, type SessionState, type SessionStore, type SignedOutReason, type StoredSession, SystemClock, type TenantOption, type TimerHandle, type TokenSet, type Transport, type TransportRequest, type TransportResponse, UnauthenticatedError, UpstreamError, ValidationError, computeChallenge, createSessionEngine, decodeJwtExp, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePolyXKey, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };