@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 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 with reuse→revoke, cross-tab
20
- sign-out, and server-authoritative proactive refresh, over swappable seams.
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
- /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
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.2.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
- /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
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
- /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
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
- /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
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.2.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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Framework-agnostic core of the PolyX SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",