@poly-x/core 0.2.0 → 0.3.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 +29 -3
- package/dist/index.cjs +20 -2
- package/dist/index.d.cts +18 -1
- package/dist/index.d.mts +18 -1
- package/dist/index.mjs +20 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,10 +16,36 @@ don't import this directly; reach for the framework package instead.
|
|
|
16
16
|
- **Transport** — a `Transport` seam with a fetch implementation (timeout +
|
|
17
17
|
bounded transient retries) and a `MockTransport` for tests.
|
|
18
18
|
- **Session engine** — the auth state machine (`resolving → authenticated ⇄
|
|
19
|
-
refreshing → signed-out`), single-flight refresh
|
|
20
|
-
|
|
19
|
+
refreshing → signed-out`), single-flight refresh, cross-tab sign-out, and
|
|
20
|
+
server-authoritative proactive refresh, over swappable seams.
|
|
21
21
|
- **Auth protocol client** — PKCE, authorize-URL building, the membership-outcome
|
|
22
|
-
parse, code exchange, refresh, logout.
|
|
22
|
+
parse, code exchange, refresh, logout, and the silent warm-hop probe.
|
|
23
|
+
|
|
24
|
+
### `completeAuthorize` refuses to run on a server
|
|
25
|
+
|
|
26
|
+
The warm hop asks the platform to recognise a browser it has seen before, and the platform
|
|
27
|
+
resolves that from a cookie on **its own** domain. A server-side request carries no browser
|
|
28
|
+
cookie, so it would be told "sign-in required" every single time — correctly, and
|
|
29
|
+
indistinguishably from someone who genuinely is signed out. Built that way, the feature looks
|
|
30
|
+
finished and never works.
|
|
31
|
+
|
|
32
|
+
So calling it from a server context throws `WARM_HOP_ON_SERVER` rather than returning that
|
|
33
|
+
plausible answer, and does not send the request at all. `@poly-x/next`'s `hop` routes prepare the
|
|
34
|
+
attempt for the browser to make; use those.
|
|
35
|
+
|
|
36
|
+
### What refresh does and does not protect against
|
|
37
|
+
|
|
38
|
+
Refresh is **single-flight** — concurrent callers coalesce onto one in-flight request, so a burst
|
|
39
|
+
of expired calls produces one refresh rather than a stampede.
|
|
40
|
+
|
|
41
|
+
It is **not** refresh-token rotation, and the SDK does not claim reuse detection. Verified against
|
|
42
|
+
a running platform (2026-07-29): PolyX returns the *same* refresh token on refresh, so a refresh
|
|
43
|
+
token is a long-lived credential that can be replayed for its lifetime, and its capture is not
|
|
44
|
+
detectable. Session expiry and revocation still apply.
|
|
45
|
+
|
|
46
|
+
**This is why custody matters more than it looks.** Under `@poly-x/next`'s BFF the refresh token
|
|
47
|
+
never reaches the browser at all. If you hold refresh tokens client-side in your own integration,
|
|
48
|
+
you are carrying that risk yourself.
|
|
23
49
|
|
|
24
50
|
```ts
|
|
25
51
|
import { createSessionEngine, AuthClient, resolveConfig } from "@poly-x/core";
|
package/dist/index.cjs
CHANGED
|
@@ -989,8 +989,26 @@ var AuthClient = class {
|
|
|
989
989
|
if (!this.config.signInUrl) return this.buildAuthorizeUrl(params);
|
|
990
990
|
return this.appendAuthorizeParams(new URL(this.config.signInUrl), params);
|
|
991
991
|
}
|
|
992
|
-
/**
|
|
992
|
+
/**
|
|
993
|
+
* Probe the warm hop (GET /idp/authorize) and return the typed outcome.
|
|
994
|
+
*
|
|
995
|
+
* **Browser only, and it refuses to run anywhere else** (FR-HOP-5/6).
|
|
996
|
+
*
|
|
997
|
+
* The platform resolves this from the *browser's* ecosystem cookie, set on the platform's
|
|
998
|
+
* own domain. A request issued from a server carries no browser cookie, so the platform
|
|
999
|
+
* would answer `login_required` — correctly, and every single time. The caller would see a
|
|
1000
|
+
* perfectly ordinary "this person must sign in", identical to the answer for someone who
|
|
1001
|
+
* genuinely is signed out. The whole feature would look implemented and never once work,
|
|
1002
|
+
* with nothing red anywhere to say so.
|
|
1003
|
+
*
|
|
1004
|
+
* That is why this throws instead of returning the plausible answer: a loud failure at the
|
|
1005
|
+
* one wrong call site is worth far more than a silent no-op nobody notices for a release.
|
|
1006
|
+
* A cross-origin `fetch(credentials:"include")` from the browser is the supported path
|
|
1007
|
+
* (`@poly-x/next`'s hop routes prepare it); it works because the ecosystem cookie is
|
|
1008
|
+
* attached by the browser to the platform host.
|
|
1009
|
+
*/
|
|
993
1010
|
async completeAuthorize(params) {
|
|
1011
|
+
if (this.config.context === "server") throw new PolyXConfigError("The warm hop cannot be attempted from a server. The platform reads the ecosystem session from the browser's cookie, which a server-side request does not carry — so this call would report 'sign-in required' every time, whether or not the person is signed in. Start the hop from the browser instead (see createAuthHandlers().hop).", { code: "WARM_HOP_ON_SERVER" });
|
|
994
1012
|
const response = await this.transport.request({
|
|
995
1013
|
method: "GET",
|
|
996
1014
|
url: this.buildAuthorizeUrl(params)
|
|
@@ -1288,7 +1306,7 @@ function conflictField(message) {
|
|
|
1288
1306
|
*/
|
|
1289
1307
|
const PACKAGE_NAME = "@poly-x/core";
|
|
1290
1308
|
/** The published version of this package, injected at build time. */
|
|
1291
|
-
const version = "0.
|
|
1309
|
+
const version = "0.3.0";
|
|
1292
1310
|
//#endregion
|
|
1293
1311
|
exports.AuthClient = AuthClient;
|
|
1294
1312
|
exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
package/dist/index.d.cts
CHANGED
|
@@ -658,7 +658,24 @@ declare class AuthClient {
|
|
|
658
658
|
* `signInUrl` is configured (deployments where the API host serves the page).
|
|
659
659
|
*/
|
|
660
660
|
buildSignInUrl(params: BuildAuthorizeUrlParams): string;
|
|
661
|
-
/**
|
|
661
|
+
/**
|
|
662
|
+
* Probe the warm hop (GET /idp/authorize) and return the typed outcome.
|
|
663
|
+
*
|
|
664
|
+
* **Browser only, and it refuses to run anywhere else** (FR-HOP-5/6).
|
|
665
|
+
*
|
|
666
|
+
* The platform resolves this from the *browser's* ecosystem cookie, set on the platform's
|
|
667
|
+
* own domain. A request issued from a server carries no browser cookie, so the platform
|
|
668
|
+
* would answer `login_required` — correctly, and every single time. The caller would see a
|
|
669
|
+
* perfectly ordinary "this person must sign in", identical to the answer for someone who
|
|
670
|
+
* genuinely is signed out. The whole feature would look implemented and never once work,
|
|
671
|
+
* with nothing red anywhere to say so.
|
|
672
|
+
*
|
|
673
|
+
* That is why this throws instead of returning the plausible answer: a loud failure at the
|
|
674
|
+
* one wrong call site is worth far more than a silent no-op nobody notices for a release.
|
|
675
|
+
* A cross-origin `fetch(credentials:"include")` from the browser is the supported path
|
|
676
|
+
* (`@poly-x/next`'s hop routes prepare it); it works because the ecosystem cookie is
|
|
677
|
+
* attached by the browser to the platform host.
|
|
678
|
+
*/
|
|
662
679
|
completeAuthorize(params: BuildAuthorizeUrlParams): Promise<AuthorizeOutcome>;
|
|
663
680
|
/**
|
|
664
681
|
* The embedded `<SignIn/>` credential flow (D2b): POST `/idp/authorize` with
|
package/dist/index.d.mts
CHANGED
|
@@ -658,7 +658,24 @@ declare class AuthClient {
|
|
|
658
658
|
* `signInUrl` is configured (deployments where the API host serves the page).
|
|
659
659
|
*/
|
|
660
660
|
buildSignInUrl(params: BuildAuthorizeUrlParams): string;
|
|
661
|
-
/**
|
|
661
|
+
/**
|
|
662
|
+
* Probe the warm hop (GET /idp/authorize) and return the typed outcome.
|
|
663
|
+
*
|
|
664
|
+
* **Browser only, and it refuses to run anywhere else** (FR-HOP-5/6).
|
|
665
|
+
*
|
|
666
|
+
* The platform resolves this from the *browser's* ecosystem cookie, set on the platform's
|
|
667
|
+
* own domain. A request issued from a server carries no browser cookie, so the platform
|
|
668
|
+
* would answer `login_required` — correctly, and every single time. The caller would see a
|
|
669
|
+
* perfectly ordinary "this person must sign in", identical to the answer for someone who
|
|
670
|
+
* genuinely is signed out. The whole feature would look implemented and never once work,
|
|
671
|
+
* with nothing red anywhere to say so.
|
|
672
|
+
*
|
|
673
|
+
* That is why this throws instead of returning the plausible answer: a loud failure at the
|
|
674
|
+
* one wrong call site is worth far more than a silent no-op nobody notices for a release.
|
|
675
|
+
* A cross-origin `fetch(credentials:"include")` from the browser is the supported path
|
|
676
|
+
* (`@poly-x/next`'s hop routes prepare it); it works because the ecosystem cookie is
|
|
677
|
+
* attached by the browser to the platform host.
|
|
678
|
+
*/
|
|
662
679
|
completeAuthorize(params: BuildAuthorizeUrlParams): Promise<AuthorizeOutcome>;
|
|
663
680
|
/**
|
|
664
681
|
* The embedded `<SignIn/>` credential flow (D2b): POST `/idp/authorize` with
|
package/dist/index.mjs
CHANGED
|
@@ -988,8 +988,26 @@ var AuthClient = class {
|
|
|
988
988
|
if (!this.config.signInUrl) return this.buildAuthorizeUrl(params);
|
|
989
989
|
return this.appendAuthorizeParams(new URL(this.config.signInUrl), params);
|
|
990
990
|
}
|
|
991
|
-
/**
|
|
991
|
+
/**
|
|
992
|
+
* Probe the warm hop (GET /idp/authorize) and return the typed outcome.
|
|
993
|
+
*
|
|
994
|
+
* **Browser only, and it refuses to run anywhere else** (FR-HOP-5/6).
|
|
995
|
+
*
|
|
996
|
+
* The platform resolves this from the *browser's* ecosystem cookie, set on the platform's
|
|
997
|
+
* own domain. A request issued from a server carries no browser cookie, so the platform
|
|
998
|
+
* would answer `login_required` — correctly, and every single time. The caller would see a
|
|
999
|
+
* perfectly ordinary "this person must sign in", identical to the answer for someone who
|
|
1000
|
+
* genuinely is signed out. The whole feature would look implemented and never once work,
|
|
1001
|
+
* with nothing red anywhere to say so.
|
|
1002
|
+
*
|
|
1003
|
+
* That is why this throws instead of returning the plausible answer: a loud failure at the
|
|
1004
|
+
* one wrong call site is worth far more than a silent no-op nobody notices for a release.
|
|
1005
|
+
* A cross-origin `fetch(credentials:"include")` from the browser is the supported path
|
|
1006
|
+
* (`@poly-x/next`'s hop routes prepare it); it works because the ecosystem cookie is
|
|
1007
|
+
* attached by the browser to the platform host.
|
|
1008
|
+
*/
|
|
992
1009
|
async completeAuthorize(params) {
|
|
1010
|
+
if (this.config.context === "server") throw new PolyXConfigError("The warm hop cannot be attempted from a server. The platform reads the ecosystem session from the browser's cookie, which a server-side request does not carry — so this call would report 'sign-in required' every time, whether or not the person is signed in. Start the hop from the browser instead (see createAuthHandlers().hop).", { code: "WARM_HOP_ON_SERVER" });
|
|
993
1011
|
const response = await this.transport.request({
|
|
994
1012
|
method: "GET",
|
|
995
1013
|
url: this.buildAuthorizeUrl(params)
|
|
@@ -1287,6 +1305,6 @@ function conflictField(message) {
|
|
|
1287
1305
|
*/
|
|
1288
1306
|
const PACKAGE_NAME = "@poly-x/core";
|
|
1289
1307
|
/** The published version of this package, injected at build time. */
|
|
1290
|
-
const version = "0.
|
|
1308
|
+
const version = "0.3.0";
|
|
1291
1309
|
//#endregion
|
|
1292
1310
|
export { AuthClient, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, FetchTransport, ForbiddenError, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, MAX_PROFILE_IMAGE_BYTES, MockTransport, PACKAGE_NAME, PROFILE_FIELDS, PolyXConfigError, PolyXError, RateLimitedError, SessionEngine, SessionExpiredError, SessionRevokedError, SystemClock, UnauthenticatedError, UpstreamError, ValidationError, buildProfileUpdate, computeChallenge, conflictField, createSessionEngine, decodeJwtExp, deriveDisplayClaims, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, validateProfileDraft, validateProfileImage, version };
|