@poly-x/core 0.1.0-alpha.9 → 0.1.1

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/dist/index.cjs CHANGED
@@ -193,7 +193,16 @@ function resolveConfig(input) {
193
193
  * against poly-auth/src/api/constants/auth-responses.ts (2026-07-06). Unknown
194
194
  * shapes become a typed UNRECOGNIZED_RESPONSE — never a guess.
195
195
  */
196
- const REVOKED_CODES = /* @__PURE__ */ new Set(["REFRESH_TOKEN_REUSE", "SESSION_REVOKED"]);
196
+ /**
197
+ * Terminal: the session is gone and no retry brings it back. The platform revokes every session
198
+ * for a user on permission change, suspension, and password reset — `SESSION_REVOKED` for the
199
+ * revocation itself, `SESSION_MISMATCH` when the presented token no longer matches its session.
200
+ */
201
+ const REVOKED_CODES = /* @__PURE__ */ new Set([
202
+ "REFRESH_TOKEN_REUSE",
203
+ "SESSION_REVOKED",
204
+ "SESSION_MISMATCH"
205
+ ]);
197
206
  const EXPIRED_CODES = /* @__PURE__ */ new Set([
198
207
  "SESSION_EXPIRED",
199
208
  "REFRESH_TOKEN_REQUIRED",
@@ -708,7 +717,21 @@ const ENDPOINTS = {
708
717
  authorize: "/v1/idp/authorize",
709
718
  token: "/v1/idp/token",
710
719
  refresh: "/v1/auth/refresh-token",
720
+ /**
721
+ * Ecosystem (SSO) logout. Resolves the session from the *browser's* ecosystem cookie,
722
+ * so it only revokes when called from a context that carries it — a top-level hop.
723
+ * A server-to-server call reaches it without the cookie and revokes nothing (it still
724
+ * answers 200), which is why BFF sign-out must use the bearer-authenticated pair below.
725
+ */
711
726
  logout: "/v1/idp/logout",
727
+ /**
728
+ * Bearer-authenticated revocation: the access token identifies the session, so these
729
+ * work from any context that holds one — notably the BFF, where the token is sealed in
730
+ * the session cookie and no browser cookie reaches poly-auth. `logout` revokes this
731
+ * session; `logoutAll` revokes every session for the user.
732
+ */
733
+ authLogout: "/v1/auth/logout",
734
+ authLogoutAll: "/v1/auth/logout-all",
712
735
  orgResolve: "/v1/idp/org-resolve",
713
736
  /**
714
737
  * Completes the admin temp-password flow: the user signed in with a temporary
@@ -818,7 +841,35 @@ function toTokenSet(accessToken, refreshToken, now) {
818
841
  function asRecord(value) {
819
842
  return value && typeof value === "object" ? value : {};
820
843
  }
821
- /** `POST /idp/token` → StoredSession. Claims mapping is minimal in v1 (userId + org); AUTHZ populates roles/permissions later. */
844
+ /**
845
+ * Display-claim caps (ADR-0006). These values are user-controlled and unbounded in the
846
+ * platform's schema, and they ride in a sealed ~4KB cookie whose failure mode is the browser
847
+ * silently dropping it. Cap at the boundary rather than trusting the input.
848
+ */
849
+ const MAX_DISPLAY_NAME = 128;
850
+ const MAX_AVATAR_URL = 512;
851
+ /** A non-blank string, trimmed and capped — otherwise undefined. The shim never guesses. */
852
+ function displayString(value, max) {
853
+ if (typeof value !== "string") return void 0;
854
+ const trimmed = value.trim();
855
+ if (trimmed.length === 0) return void 0;
856
+ return trimmed.slice(0, max);
857
+ }
858
+ /**
859
+ * The best human label the token response can give us: full name, else username, else email.
860
+ * Each part is checked independently so a user with only one of them never renders "undefined".
861
+ */
862
+ function deriveDisplayName(user) {
863
+ return displayString([displayString(user.firstName, MAX_DISPLAY_NAME), displayString(user.lastName, MAX_DISPLAY_NAME)].filter(Boolean).join(" "), MAX_DISPLAY_NAME) ?? displayString(user.username, MAX_DISPLAY_NAME) ?? displayString(user.email, MAX_DISPLAY_NAME);
864
+ }
865
+ /**
866
+ * `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
867
+ * (ADR-0006); AUTHZ populates roles/permissions later.
868
+ *
869
+ * `organizationId` comes from the exchange's `tenantId`, never from the user document: a
870
+ * cross-membership login resolves a tenant that is deliberately *not* the user's home
871
+ * organization, and reading it off the user would silently sign them into the wrong one.
872
+ */
822
873
  function normalizeExchange(body, now) {
823
874
  const data = asRecord(asRecord(body).data);
824
875
  const user = asRecord(data.user);
@@ -828,7 +879,10 @@ function normalizeExchange(body, now) {
828
879
  userId,
829
880
  organizationId: data.tenantId != null ? String(data.tenantId) : void 0,
830
881
  roles: [],
831
- permissions: []
882
+ permissions: [],
883
+ displayName: deriveDisplayName(user),
884
+ email: displayString(user.email, MAX_DISPLAY_NAME),
885
+ avatarUrl: displayString(user.profilePicture, MAX_AVATAR_URL)
832
886
  };
833
887
  return {
834
888
  tokens: toTokenSet(String(data.accessToken ?? ""), stringOrUndefined(data.refreshToken), now),
@@ -989,6 +1043,7 @@ var AuthClient = class {
989
1043
  const response = await this.transport.request({
990
1044
  method: "POST",
991
1045
  url: this.config.baseUrl + ENDPOINTS.token,
1046
+ ...params.forwardedHeaders ? { headers: params.forwardedHeaders } : {},
992
1047
  body: {
993
1048
  grant_type: "authorization_code",
994
1049
  code: params.code,
@@ -1033,6 +1088,11 @@ var AuthClient = class {
1033
1088
  return normalizeRefresh(response.body, current, this.clock.now());
1034
1089
  };
1035
1090
  }
1091
+ /**
1092
+ * Ecosystem logout — only revokes when the caller carries the browser's ecosystem
1093
+ * cookie (a top-level hop). From a server-to-server context it clears nothing and
1094
+ * still answers 200; use `revokeSession` there.
1095
+ */
1036
1096
  async logout(scope = "device") {
1037
1097
  const response = await this.transport.request({
1038
1098
  method: "POST",
@@ -1041,6 +1101,22 @@ var AuthClient = class {
1041
1101
  });
1042
1102
  if (response.status >= 400) throw this.toError(response);
1043
1103
  }
1104
+ /**
1105
+ * Revoke a session using the access token as proof — the only logout that works
1106
+ * server-side, where no browser cookie reaches poly-auth. `device` ends this session;
1107
+ * `everywhere` ends every session for the user (FR-SSO-5).
1108
+ *
1109
+ * The token goes in the Authorization header and nowhere else: repeating it in the
1110
+ * body would only widen where it can leak.
1111
+ */
1112
+ async revokeSession(accessToken, scope = "device") {
1113
+ const response = await this.transport.request({
1114
+ method: "POST",
1115
+ url: this.config.baseUrl + (scope === "everywhere" ? ENDPOINTS.authLogoutAll : ENDPOINTS.authLogout),
1116
+ headers: { Authorization: `Bearer ${accessToken}` }
1117
+ });
1118
+ if (response.status >= 400) throw this.toError(response);
1119
+ }
1044
1120
  async resolveOrg(email) {
1045
1121
  const response = await this.transport.request({
1046
1122
  method: "POST",
package/dist/index.d.cts CHANGED
@@ -203,11 +203,24 @@ interface TokenSet {
203
203
  /** Epoch milliseconds when the access token expires, per the server. */
204
204
  accessTokenExpiresAt: number;
205
205
  }
206
+ /**
207
+ * The verified identity a session carries. Per ADR-0006 this holds identity plus a **bounded
208
+ * display subset** — enough for a signed-in user surface — and never bulk authorization data
209
+ * (module graphs, team lists). Claims are sealed into a ~4KB cookie that fails by being
210
+ * silently dropped, so a field earns its place by being bounded and small, not by being useful.
211
+ *
212
+ * The display fields are optional: they are derived defensively from whatever the platform
213
+ * returns, and their absence is normal, not an error.
214
+ */
206
215
  interface SessionClaims {
207
216
  userId: string;
208
217
  organizationId?: string;
209
218
  roles: readonly string[];
210
219
  permissions: readonly string[];
220
+ /** Best available human label: full name → username → email. */
221
+ displayName?: string;
222
+ email?: string;
223
+ avatarUrl?: string;
211
224
  }
212
225
  interface Session {
213
226
  claims: SessionClaims;
@@ -390,7 +403,21 @@ declare const ENDPOINTS: {
390
403
  readonly authorize: "/v1/idp/authorize";
391
404
  readonly token: "/v1/idp/token";
392
405
  readonly refresh: "/v1/auth/refresh-token";
406
+ /**
407
+ * Ecosystem (SSO) logout. Resolves the session from the *browser's* ecosystem cookie,
408
+ * so it only revokes when called from a context that carries it — a top-level hop.
409
+ * A server-to-server call reaches it without the cookie and revokes nothing (it still
410
+ * answers 200), which is why BFF sign-out must use the bearer-authenticated pair below.
411
+ */
393
412
  readonly logout: "/v1/idp/logout";
413
+ /**
414
+ * Bearer-authenticated revocation: the access token identifies the session, so these
415
+ * work from any context that holds one — notably the BFF, where the token is sealed in
416
+ * the session cookie and no browser cookie reaches poly-auth. `logout` revokes this
417
+ * session; `logoutAll` revokes every session for the user.
418
+ */
419
+ readonly authLogout: "/v1/auth/logout";
420
+ readonly authLogoutAll: "/v1/auth/logout-all";
394
421
  readonly orgResolve: "/v1/idp/org-resolve";
395
422
  /**
396
423
  * Completes the admin temp-password flow: the user signed in with a temporary
@@ -476,7 +503,14 @@ declare function parseResetOutcome(status: number, body: unknown): PasswordReset
476
503
  //#endregion
477
504
  //#region src/auth/normalize.d.ts
478
505
  declare function decodeJwtExp(token: string): number | null;
479
- /** `POST /idp/token` → StoredSession. Claims mapping is minimal in v1 (userId + org); AUTHZ populates roles/permissions later. */
506
+ /**
507
+ * `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
508
+ * (ADR-0006); AUTHZ populates roles/permissions later.
509
+ *
510
+ * `organizationId` comes from the exchange's `tenantId`, never from the user document: a
511
+ * cross-membership login resolves a tenant that is deliberately *not* the user's home
512
+ * organization, and reading it off the user would silently sign them into the wrong one.
513
+ */
480
514
  declare function normalizeExchange(body: unknown, now: number): StoredSession;
481
515
  /** `POST /auth/refresh-token` → StoredSession. No user in the response, so claims carry forward. */
482
516
  declare function normalizeRefresh(body: unknown, current: StoredSession, now: number): StoredSession;
@@ -523,6 +557,16 @@ interface ExchangeCodeParams {
523
557
  code: string;
524
558
  codeVerifier: string;
525
559
  redirectUri: string;
560
+ /**
561
+ * Browser context to attach to the token exchange (the call that creates the
562
+ * session). In a BFF deployment this request originates from Node, so without
563
+ * these the platform records the server's user-agent/IP ("node"/Unknown) instead
564
+ * of the end user's device. The BFF forwards a curated allowlist of the browser's
565
+ * request headers (user-agent, accept-language, the sec-ch-ua* client hints, and
566
+ * the observed client IP as x-forwarded-for). Optional and additive — omitting it
567
+ * preserves the prior behavior.
568
+ */
569
+ forwardedHeaders?: Record<string, string>;
526
570
  }
527
571
  interface SetPasswordParams {
528
572
  userId: string;
@@ -615,7 +659,21 @@ declare class AuthClient {
615
659
  fetchBranding(params?: FetchBrandingParams): Promise<BrandingTokens>;
616
660
  /** The `RefreshTokens` function the SessionEngine (F003) injects. */
617
661
  createRefresher(): RefreshTokens;
662
+ /**
663
+ * Ecosystem logout — only revokes when the caller carries the browser's ecosystem
664
+ * cookie (a top-level hop). From a server-to-server context it clears nothing and
665
+ * still answers 200; use `revokeSession` there.
666
+ */
618
667
  logout(scope?: "device" | "everywhere"): Promise<void>;
668
+ /**
669
+ * Revoke a session using the access token as proof — the only logout that works
670
+ * server-side, where no browser cookie reaches poly-auth. `device` ends this session;
671
+ * `everywhere` ends every session for the user (FR-SSO-5).
672
+ *
673
+ * The token goes in the Authorization header and nowhere else: repeating it in the
674
+ * body would only widen where it can leak.
675
+ */
676
+ revokeSession(accessToken: string, scope?: "device" | "everywhere"): Promise<void>;
619
677
  resolveOrg(email: string): Promise<{
620
678
  resolved: boolean;
621
679
  organizationCode?: string;
package/dist/index.d.mts CHANGED
@@ -203,11 +203,24 @@ interface TokenSet {
203
203
  /** Epoch milliseconds when the access token expires, per the server. */
204
204
  accessTokenExpiresAt: number;
205
205
  }
206
+ /**
207
+ * The verified identity a session carries. Per ADR-0006 this holds identity plus a **bounded
208
+ * display subset** — enough for a signed-in user surface — and never bulk authorization data
209
+ * (module graphs, team lists). Claims are sealed into a ~4KB cookie that fails by being
210
+ * silently dropped, so a field earns its place by being bounded and small, not by being useful.
211
+ *
212
+ * The display fields are optional: they are derived defensively from whatever the platform
213
+ * returns, and their absence is normal, not an error.
214
+ */
206
215
  interface SessionClaims {
207
216
  userId: string;
208
217
  organizationId?: string;
209
218
  roles: readonly string[];
210
219
  permissions: readonly string[];
220
+ /** Best available human label: full name → username → email. */
221
+ displayName?: string;
222
+ email?: string;
223
+ avatarUrl?: string;
211
224
  }
212
225
  interface Session {
213
226
  claims: SessionClaims;
@@ -390,7 +403,21 @@ declare const ENDPOINTS: {
390
403
  readonly authorize: "/v1/idp/authorize";
391
404
  readonly token: "/v1/idp/token";
392
405
  readonly refresh: "/v1/auth/refresh-token";
406
+ /**
407
+ * Ecosystem (SSO) logout. Resolves the session from the *browser's* ecosystem cookie,
408
+ * so it only revokes when called from a context that carries it — a top-level hop.
409
+ * A server-to-server call reaches it without the cookie and revokes nothing (it still
410
+ * answers 200), which is why BFF sign-out must use the bearer-authenticated pair below.
411
+ */
393
412
  readonly logout: "/v1/idp/logout";
413
+ /**
414
+ * Bearer-authenticated revocation: the access token identifies the session, so these
415
+ * work from any context that holds one — notably the BFF, where the token is sealed in
416
+ * the session cookie and no browser cookie reaches poly-auth. `logout` revokes this
417
+ * session; `logoutAll` revokes every session for the user.
418
+ */
419
+ readonly authLogout: "/v1/auth/logout";
420
+ readonly authLogoutAll: "/v1/auth/logout-all";
394
421
  readonly orgResolve: "/v1/idp/org-resolve";
395
422
  /**
396
423
  * Completes the admin temp-password flow: the user signed in with a temporary
@@ -476,7 +503,14 @@ declare function parseResetOutcome(status: number, body: unknown): PasswordReset
476
503
  //#endregion
477
504
  //#region src/auth/normalize.d.ts
478
505
  declare function decodeJwtExp(token: string): number | null;
479
- /** `POST /idp/token` → StoredSession. Claims mapping is minimal in v1 (userId + org); AUTHZ populates roles/permissions later. */
506
+ /**
507
+ * `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
508
+ * (ADR-0006); AUTHZ populates roles/permissions later.
509
+ *
510
+ * `organizationId` comes from the exchange's `tenantId`, never from the user document: a
511
+ * cross-membership login resolves a tenant that is deliberately *not* the user's home
512
+ * organization, and reading it off the user would silently sign them into the wrong one.
513
+ */
480
514
  declare function normalizeExchange(body: unknown, now: number): StoredSession;
481
515
  /** `POST /auth/refresh-token` → StoredSession. No user in the response, so claims carry forward. */
482
516
  declare function normalizeRefresh(body: unknown, current: StoredSession, now: number): StoredSession;
@@ -523,6 +557,16 @@ interface ExchangeCodeParams {
523
557
  code: string;
524
558
  codeVerifier: string;
525
559
  redirectUri: string;
560
+ /**
561
+ * Browser context to attach to the token exchange (the call that creates the
562
+ * session). In a BFF deployment this request originates from Node, so without
563
+ * these the platform records the server's user-agent/IP ("node"/Unknown) instead
564
+ * of the end user's device. The BFF forwards a curated allowlist of the browser's
565
+ * request headers (user-agent, accept-language, the sec-ch-ua* client hints, and
566
+ * the observed client IP as x-forwarded-for). Optional and additive — omitting it
567
+ * preserves the prior behavior.
568
+ */
569
+ forwardedHeaders?: Record<string, string>;
526
570
  }
527
571
  interface SetPasswordParams {
528
572
  userId: string;
@@ -615,7 +659,21 @@ declare class AuthClient {
615
659
  fetchBranding(params?: FetchBrandingParams): Promise<BrandingTokens>;
616
660
  /** The `RefreshTokens` function the SessionEngine (F003) injects. */
617
661
  createRefresher(): RefreshTokens;
662
+ /**
663
+ * Ecosystem logout — only revokes when the caller carries the browser's ecosystem
664
+ * cookie (a top-level hop). From a server-to-server context it clears nothing and
665
+ * still answers 200; use `revokeSession` there.
666
+ */
618
667
  logout(scope?: "device" | "everywhere"): Promise<void>;
668
+ /**
669
+ * Revoke a session using the access token as proof — the only logout that works
670
+ * server-side, where no browser cookie reaches poly-auth. `device` ends this session;
671
+ * `everywhere` ends every session for the user (FR-SSO-5).
672
+ *
673
+ * The token goes in the Authorization header and nowhere else: repeating it in the
674
+ * body would only widen where it can leak.
675
+ */
676
+ revokeSession(accessToken: string, scope?: "device" | "everywhere"): Promise<void>;
619
677
  resolveOrg(email: string): Promise<{
620
678
  resolved: boolean;
621
679
  organizationCode?: string;
package/dist/index.mjs CHANGED
@@ -192,7 +192,16 @@ function resolveConfig(input) {
192
192
  * against poly-auth/src/api/constants/auth-responses.ts (2026-07-06). Unknown
193
193
  * shapes become a typed UNRECOGNIZED_RESPONSE — never a guess.
194
194
  */
195
- const REVOKED_CODES = /* @__PURE__ */ new Set(["REFRESH_TOKEN_REUSE", "SESSION_REVOKED"]);
195
+ /**
196
+ * Terminal: the session is gone and no retry brings it back. The platform revokes every session
197
+ * for a user on permission change, suspension, and password reset — `SESSION_REVOKED` for the
198
+ * revocation itself, `SESSION_MISMATCH` when the presented token no longer matches its session.
199
+ */
200
+ const REVOKED_CODES = /* @__PURE__ */ new Set([
201
+ "REFRESH_TOKEN_REUSE",
202
+ "SESSION_REVOKED",
203
+ "SESSION_MISMATCH"
204
+ ]);
196
205
  const EXPIRED_CODES = /* @__PURE__ */ new Set([
197
206
  "SESSION_EXPIRED",
198
207
  "REFRESH_TOKEN_REQUIRED",
@@ -707,7 +716,21 @@ const ENDPOINTS = {
707
716
  authorize: "/v1/idp/authorize",
708
717
  token: "/v1/idp/token",
709
718
  refresh: "/v1/auth/refresh-token",
719
+ /**
720
+ * Ecosystem (SSO) logout. Resolves the session from the *browser's* ecosystem cookie,
721
+ * so it only revokes when called from a context that carries it — a top-level hop.
722
+ * A server-to-server call reaches it without the cookie and revokes nothing (it still
723
+ * answers 200), which is why BFF sign-out must use the bearer-authenticated pair below.
724
+ */
710
725
  logout: "/v1/idp/logout",
726
+ /**
727
+ * Bearer-authenticated revocation: the access token identifies the session, so these
728
+ * work from any context that holds one — notably the BFF, where the token is sealed in
729
+ * the session cookie and no browser cookie reaches poly-auth. `logout` revokes this
730
+ * session; `logoutAll` revokes every session for the user.
731
+ */
732
+ authLogout: "/v1/auth/logout",
733
+ authLogoutAll: "/v1/auth/logout-all",
711
734
  orgResolve: "/v1/idp/org-resolve",
712
735
  /**
713
736
  * Completes the admin temp-password flow: the user signed in with a temporary
@@ -817,7 +840,35 @@ function toTokenSet(accessToken, refreshToken, now) {
817
840
  function asRecord(value) {
818
841
  return value && typeof value === "object" ? value : {};
819
842
  }
820
- /** `POST /idp/token` → StoredSession. Claims mapping is minimal in v1 (userId + org); AUTHZ populates roles/permissions later. */
843
+ /**
844
+ * Display-claim caps (ADR-0006). These values are user-controlled and unbounded in the
845
+ * platform's schema, and they ride in a sealed ~4KB cookie whose failure mode is the browser
846
+ * silently dropping it. Cap at the boundary rather than trusting the input.
847
+ */
848
+ const MAX_DISPLAY_NAME = 128;
849
+ const MAX_AVATAR_URL = 512;
850
+ /** A non-blank string, trimmed and capped — otherwise undefined. The shim never guesses. */
851
+ function displayString(value, max) {
852
+ if (typeof value !== "string") return void 0;
853
+ const trimmed = value.trim();
854
+ if (trimmed.length === 0) return void 0;
855
+ return trimmed.slice(0, max);
856
+ }
857
+ /**
858
+ * The best human label the token response can give us: full name, else username, else email.
859
+ * Each part is checked independently so a user with only one of them never renders "undefined".
860
+ */
861
+ function deriveDisplayName(user) {
862
+ return displayString([displayString(user.firstName, MAX_DISPLAY_NAME), displayString(user.lastName, MAX_DISPLAY_NAME)].filter(Boolean).join(" "), MAX_DISPLAY_NAME) ?? displayString(user.username, MAX_DISPLAY_NAME) ?? displayString(user.email, MAX_DISPLAY_NAME);
863
+ }
864
+ /**
865
+ * `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
866
+ * (ADR-0006); AUTHZ populates roles/permissions later.
867
+ *
868
+ * `organizationId` comes from the exchange's `tenantId`, never from the user document: a
869
+ * cross-membership login resolves a tenant that is deliberately *not* the user's home
870
+ * organization, and reading it off the user would silently sign them into the wrong one.
871
+ */
821
872
  function normalizeExchange(body, now) {
822
873
  const data = asRecord(asRecord(body).data);
823
874
  const user = asRecord(data.user);
@@ -827,7 +878,10 @@ function normalizeExchange(body, now) {
827
878
  userId,
828
879
  organizationId: data.tenantId != null ? String(data.tenantId) : void 0,
829
880
  roles: [],
830
- permissions: []
881
+ permissions: [],
882
+ displayName: deriveDisplayName(user),
883
+ email: displayString(user.email, MAX_DISPLAY_NAME),
884
+ avatarUrl: displayString(user.profilePicture, MAX_AVATAR_URL)
831
885
  };
832
886
  return {
833
887
  tokens: toTokenSet(String(data.accessToken ?? ""), stringOrUndefined(data.refreshToken), now),
@@ -988,6 +1042,7 @@ var AuthClient = class {
988
1042
  const response = await this.transport.request({
989
1043
  method: "POST",
990
1044
  url: this.config.baseUrl + ENDPOINTS.token,
1045
+ ...params.forwardedHeaders ? { headers: params.forwardedHeaders } : {},
991
1046
  body: {
992
1047
  grant_type: "authorization_code",
993
1048
  code: params.code,
@@ -1032,6 +1087,11 @@ var AuthClient = class {
1032
1087
  return normalizeRefresh(response.body, current, this.clock.now());
1033
1088
  };
1034
1089
  }
1090
+ /**
1091
+ * Ecosystem logout — only revokes when the caller carries the browser's ecosystem
1092
+ * cookie (a top-level hop). From a server-to-server context it clears nothing and
1093
+ * still answers 200; use `revokeSession` there.
1094
+ */
1035
1095
  async logout(scope = "device") {
1036
1096
  const response = await this.transport.request({
1037
1097
  method: "POST",
@@ -1040,6 +1100,22 @@ var AuthClient = class {
1040
1100
  });
1041
1101
  if (response.status >= 400) throw this.toError(response);
1042
1102
  }
1103
+ /**
1104
+ * Revoke a session using the access token as proof — the only logout that works
1105
+ * server-side, where no browser cookie reaches poly-auth. `device` ends this session;
1106
+ * `everywhere` ends every session for the user (FR-SSO-5).
1107
+ *
1108
+ * The token goes in the Authorization header and nowhere else: repeating it in the
1109
+ * body would only widen where it can leak.
1110
+ */
1111
+ async revokeSession(accessToken, scope = "device") {
1112
+ const response = await this.transport.request({
1113
+ method: "POST",
1114
+ url: this.config.baseUrl + (scope === "everywhere" ? ENDPOINTS.authLogoutAll : ENDPOINTS.authLogout),
1115
+ headers: { Authorization: `Bearer ${accessToken}` }
1116
+ });
1117
+ if (response.status >= 400) throw this.toError(response);
1118
+ }
1043
1119
  async resolveOrg(email) {
1044
1120
  const response = await this.transport.request({
1045
1121
  method: "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/core",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.1.1",
4
4
  "description": "Framework-agnostic core of the PolyX SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",