@poly-x/core 0.1.0-alpha.0 → 0.1.0-alpha.2

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
@@ -167,11 +167,21 @@ function resolveConfig(input) {
167
167
  }
168
168
  baseUrl = normalizeBaseUrl(input.baseUrl);
169
169
  } else baseUrl = `https://${normalizeBaseUrl(key.host)}`;
170
+ let signInUrl;
171
+ if (input.signInUrl !== void 0) {
172
+ try {
173
+ new URL(input.signInUrl);
174
+ } catch {
175
+ throw new PolyXConfigError("The signInUrl is not a valid URL.", { code: "SIGN_IN_URL_INVALID" });
176
+ }
177
+ signInUrl = normalizeBaseUrl(input.signInUrl);
178
+ }
170
179
  return {
171
180
  key,
172
181
  kind: key.kind,
173
182
  env: key.env,
174
183
  baseUrl,
184
+ signInUrl,
175
185
  context
176
186
  };
177
187
  }
@@ -737,6 +747,27 @@ function parseAuthorizeOutcome(status, body) {
737
747
  if (statusField === "login_required" || b.code === "LOGIN_REQUIRED") return { type: "login_required" };
738
748
  return { type: "login_required" };
739
749
  }
750
+ function parsePasswordOutcome(status, body) {
751
+ const b = asRecord$1(body);
752
+ const statusField = typeof b.status === "string" ? b.status : void 0;
753
+ if (statusField === "authorized" && typeof b.code === "string") return {
754
+ type: "authorized",
755
+ code: b.code,
756
+ state: typeof b.state === "string" ? b.state : "",
757
+ redirectUri: typeof b.redirectUri === "string" ? b.redirectUri : void 0
758
+ };
759
+ if (statusField === "select_tenant") return {
760
+ type: "select_tenant",
761
+ state: typeof b.state === "string" ? b.state : "",
762
+ tenants: parseTenants(b.tenants)
763
+ };
764
+ if (status === 403 && b.userPasswordSet === true && typeof b.userId === "string") return {
765
+ type: "password_change_required",
766
+ userId: b.userId
767
+ };
768
+ if (status === 403) return { type: "no_access" };
769
+ return { type: "invalid_credentials" };
770
+ }
740
771
  //#endregion
741
772
  //#region src/auth/normalize.ts
742
773
  const FALLBACK_TTL_MS = 5 * 6e4;
@@ -815,10 +846,9 @@ var AuthClient = class {
815
846
  this.transport = options.transport;
816
847
  this.clock = options.clock ?? new SystemClock();
817
848
  }
818
- buildAuthorizeUrl(params) {
819
- const url = new URL(this.config.baseUrl + ENDPOINTS.authorize);
849
+ appendAuthorizeParams(url, params) {
820
850
  url.searchParams.set("response_type", "code");
821
- url.searchParams.set("client", this.config.key.raw);
851
+ url.searchParams.set("client_id", this.config.key.raw);
822
852
  url.searchParams.set("redirect_uri", params.redirectUri);
823
853
  url.searchParams.set("state", params.state);
824
854
  url.searchParams.set("code_challenge", params.challenge);
@@ -827,6 +857,20 @@ var AuthClient = class {
827
857
  if (params.prompt) url.searchParams.set("prompt", params.prompt);
828
858
  return url.toString();
829
859
  }
860
+ /** The API `GET /v1/idp/authorize` URL — used for the silent warm-hop probe. */
861
+ buildAuthorizeUrl(params) {
862
+ return this.appendAuthorizeParams(new URL(this.config.baseUrl + ENDPOINTS.authorize), params);
863
+ }
864
+ /**
865
+ * The URL the interactive sign-in popup/redirect opens. Targets the HOSTED login
866
+ * PAGE (`config.signInUrl`) — which renders the form, POSTs the API, and redirects
867
+ * to `redirect_uri` with the code. Falls back to the API authorize URL when no
868
+ * `signInUrl` is configured (deployments where the API host serves the page).
869
+ */
870
+ buildSignInUrl(params) {
871
+ if (!this.config.signInUrl) return this.buildAuthorizeUrl(params);
872
+ return this.appendAuthorizeParams(new URL(this.config.signInUrl), params);
873
+ }
830
874
  /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
831
875
  async completeAuthorize(params) {
832
876
  const response = await this.transport.request({
@@ -836,6 +880,34 @@ var AuthClient = class {
836
880
  if (response.status >= 500) throw this.toError(response);
837
881
  return parseAuthorizeOutcome(response.status, response.body);
838
882
  }
883
+ /**
884
+ * The embedded `<SignIn/>` credential flow (D2b): POST `/idp/authorize` with
885
+ * email/password + PKCE. On `authorized` the caller exchanges the returned
886
+ * `code` (server-side, via the BFF) for a session — no redirect to a hosted
887
+ * login page. Creds live only for the duration of this request; the SDK never
888
+ * stores them. 5xx surfaces as a typed error; every other status is a typed
889
+ * outcome the UI branches on.
890
+ */
891
+ async authorizeWithPassword(params) {
892
+ const response = await this.transport.request({
893
+ method: "POST",
894
+ url: this.config.baseUrl + ENDPOINTS.authorize,
895
+ body: {
896
+ response_type: "code",
897
+ client_id: this.config.key.raw,
898
+ redirect_uri: params.redirectUri,
899
+ state: params.state,
900
+ code_challenge: params.challenge,
901
+ code_challenge_method: "S256",
902
+ email: params.email,
903
+ password: params.password,
904
+ ...params.organizationCode ? { organizationCode: params.organizationCode } : {},
905
+ ...params.tenantId ? { tenantId: params.tenantId } : {}
906
+ }
907
+ });
908
+ if (response.status >= 500) throw this.toError(response);
909
+ return parsePasswordOutcome(response.status, response.body);
910
+ }
839
911
  async exchangeCode(params) {
840
912
  const response = await this.transport.request({
841
913
  method: "POST",
@@ -845,7 +917,7 @@ var AuthClient = class {
845
917
  code: params.code,
846
918
  code_verifier: params.codeVerifier,
847
919
  redirect_uri: params.redirectUri,
848
- client: this.config.key.raw
920
+ client_id: this.config.key.raw
849
921
  }
850
922
  });
851
923
  if (response.status !== 200) throw this.toError(response);
@@ -932,6 +1004,7 @@ exports.mapPlatformError = mapPlatformError;
932
1004
  exports.normalizeExchange = normalizeExchange;
933
1005
  exports.normalizeRefresh = normalizeRefresh;
934
1006
  exports.parseAuthorizeOutcome = parseAuthorizeOutcome;
1007
+ exports.parsePasswordOutcome = parsePasswordOutcome;
935
1008
  exports.parsePolyXKey = parsePolyXKey;
936
1009
  exports.randomState = randomState;
937
1010
  exports.redactSensitive = redactSensitive;
package/dist/index.d.cts CHANGED
@@ -27,6 +27,14 @@ interface ResolveConfigInput {
27
27
  key: string;
28
28
  /** Wins over the key-embedded host. */
29
29
  baseUrl?: string;
30
+ /**
31
+ * URL of the HOSTED sign-in page (the account portal that renders the login
32
+ * form) — distinct from the API `baseUrl` the key encodes. The interactive
33
+ * sign-in popup/redirect targets this; the silent warm-hop probe still hits the
34
+ * API. When unset, sign-in falls back to the API's `/v1/idp/authorize` (for
35
+ * deployments where the API host also serves the login page).
36
+ */
37
+ signInUrl?: string;
30
38
  /** Defaults to runtime auto-detection. */
31
39
  context?: RuntimeContext;
32
40
  }
@@ -35,6 +43,8 @@ interface ResolvedConfig {
35
43
  kind: KeyKind;
36
44
  env: KeyEnv;
37
45
  baseUrl: string;
46
+ /** Hosted sign-in page URL (see ResolveConfigInput.signInUrl); undefined = use the API. */
47
+ signInUrl?: string;
38
48
  context: RuntimeContext;
39
49
  }
40
50
  declare function resolveConfig(input: ResolveConfigInput): ResolvedConfig;
@@ -411,6 +421,30 @@ type AuthorizeOutcome = {
411
421
  type: "login_required";
412
422
  };
413
423
  declare function parseAuthorizeOutcome(status: number, body: unknown): AuthorizeOutcome;
424
+ /**
425
+ * Outcome of a PASSWORD authorize (`POST /idp/authorize` with credentials) — the
426
+ * embedded `<SignIn/>` flow. Distinguishes invalid-credentials and the
427
+ * must-set-password step from the membership outcomes (the GET warm-hop parser
428
+ * collapses those into login_required).
429
+ */
430
+ type PasswordAuthOutcome = {
431
+ type: "authorized";
432
+ code: string;
433
+ state: string;
434
+ redirectUri?: string;
435
+ } | {
436
+ type: "select_tenant";
437
+ state: string;
438
+ tenants: TenantOption[];
439
+ } | {
440
+ type: "invalid_credentials";
441
+ } | {
442
+ type: "password_change_required";
443
+ userId: string;
444
+ } | {
445
+ type: "no_access";
446
+ };
447
+ declare function parsePasswordOutcome(status: number, body: unknown): PasswordAuthOutcome;
414
448
  //#endregion
415
449
  //#region src/auth/normalize.d.ts
416
450
  declare function decodeJwtExp(token: string): number | null;
@@ -462,14 +496,42 @@ interface ExchangeCodeParams {
462
496
  codeVerifier: string;
463
497
  redirectUri: string;
464
498
  }
499
+ interface AuthorizeWithPasswordParams {
500
+ email: string;
501
+ password: string;
502
+ redirectUri: string;
503
+ state: string;
504
+ challenge: string;
505
+ organizationCode?: string;
506
+ /** Set on the re-attempt after the user picks a workspace (select_tenant). */
507
+ tenantId?: string;
508
+ }
465
509
  declare class AuthClient {
466
510
  private readonly config;
467
511
  private readonly transport;
468
512
  private readonly clock;
469
513
  constructor(options: AuthClientOptions);
514
+ private appendAuthorizeParams;
515
+ /** The API `GET /v1/idp/authorize` URL — used for the silent warm-hop probe. */
470
516
  buildAuthorizeUrl(params: BuildAuthorizeUrlParams): string;
517
+ /**
518
+ * The URL the interactive sign-in popup/redirect opens. Targets the HOSTED login
519
+ * PAGE (`config.signInUrl`) — which renders the form, POSTs the API, and redirects
520
+ * to `redirect_uri` with the code. Falls back to the API authorize URL when no
521
+ * `signInUrl` is configured (deployments where the API host serves the page).
522
+ */
523
+ buildSignInUrl(params: BuildAuthorizeUrlParams): string;
471
524
  /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
472
525
  completeAuthorize(params: BuildAuthorizeUrlParams): Promise<AuthorizeOutcome>;
526
+ /**
527
+ * The embedded `<SignIn/>` credential flow (D2b): POST `/idp/authorize` with
528
+ * email/password + PKCE. On `authorized` the caller exchanges the returned
529
+ * `code` (server-side, via the BFF) for a session — no redirect to a hosted
530
+ * login page. Creds live only for the duration of this request; the SDK never
531
+ * stores them. 5xx surfaces as a typed error; every other status is a typed
532
+ * outcome the UI branches on.
533
+ */
534
+ authorizeWithPassword(params: AuthorizeWithPasswordParams): Promise<PasswordAuthOutcome>;
473
535
  exchangeCode(params: ExchangeCodeParams): Promise<StoredSession>;
474
536
  /** The `RefreshTokens` function the SessionEngine (F003) injects. */
475
537
  createRefresher(): RefreshTokens;
@@ -488,4 +550,4 @@ declare class AuthClient {
488
550
  declare const PACKAGE_NAME = "@poly-x/core";
489
551
  declare const version = "0.0.0";
490
552
  //#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 };
553
+ export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type AuthorizeWithPasswordParams, 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 PasswordAuthOutcome, 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, parsePasswordOutcome, parsePolyXKey, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
package/dist/index.d.mts CHANGED
@@ -27,6 +27,14 @@ interface ResolveConfigInput {
27
27
  key: string;
28
28
  /** Wins over the key-embedded host. */
29
29
  baseUrl?: string;
30
+ /**
31
+ * URL of the HOSTED sign-in page (the account portal that renders the login
32
+ * form) — distinct from the API `baseUrl` the key encodes. The interactive
33
+ * sign-in popup/redirect targets this; the silent warm-hop probe still hits the
34
+ * API. When unset, sign-in falls back to the API's `/v1/idp/authorize` (for
35
+ * deployments where the API host also serves the login page).
36
+ */
37
+ signInUrl?: string;
30
38
  /** Defaults to runtime auto-detection. */
31
39
  context?: RuntimeContext;
32
40
  }
@@ -35,6 +43,8 @@ interface ResolvedConfig {
35
43
  kind: KeyKind;
36
44
  env: KeyEnv;
37
45
  baseUrl: string;
46
+ /** Hosted sign-in page URL (see ResolveConfigInput.signInUrl); undefined = use the API. */
47
+ signInUrl?: string;
38
48
  context: RuntimeContext;
39
49
  }
40
50
  declare function resolveConfig(input: ResolveConfigInput): ResolvedConfig;
@@ -411,6 +421,30 @@ type AuthorizeOutcome = {
411
421
  type: "login_required";
412
422
  };
413
423
  declare function parseAuthorizeOutcome(status: number, body: unknown): AuthorizeOutcome;
424
+ /**
425
+ * Outcome of a PASSWORD authorize (`POST /idp/authorize` with credentials) — the
426
+ * embedded `<SignIn/>` flow. Distinguishes invalid-credentials and the
427
+ * must-set-password step from the membership outcomes (the GET warm-hop parser
428
+ * collapses those into login_required).
429
+ */
430
+ type PasswordAuthOutcome = {
431
+ type: "authorized";
432
+ code: string;
433
+ state: string;
434
+ redirectUri?: string;
435
+ } | {
436
+ type: "select_tenant";
437
+ state: string;
438
+ tenants: TenantOption[];
439
+ } | {
440
+ type: "invalid_credentials";
441
+ } | {
442
+ type: "password_change_required";
443
+ userId: string;
444
+ } | {
445
+ type: "no_access";
446
+ };
447
+ declare function parsePasswordOutcome(status: number, body: unknown): PasswordAuthOutcome;
414
448
  //#endregion
415
449
  //#region src/auth/normalize.d.ts
416
450
  declare function decodeJwtExp(token: string): number | null;
@@ -462,14 +496,42 @@ interface ExchangeCodeParams {
462
496
  codeVerifier: string;
463
497
  redirectUri: string;
464
498
  }
499
+ interface AuthorizeWithPasswordParams {
500
+ email: string;
501
+ password: string;
502
+ redirectUri: string;
503
+ state: string;
504
+ challenge: string;
505
+ organizationCode?: string;
506
+ /** Set on the re-attempt after the user picks a workspace (select_tenant). */
507
+ tenantId?: string;
508
+ }
465
509
  declare class AuthClient {
466
510
  private readonly config;
467
511
  private readonly transport;
468
512
  private readonly clock;
469
513
  constructor(options: AuthClientOptions);
514
+ private appendAuthorizeParams;
515
+ /** The API `GET /v1/idp/authorize` URL — used for the silent warm-hop probe. */
470
516
  buildAuthorizeUrl(params: BuildAuthorizeUrlParams): string;
517
+ /**
518
+ * The URL the interactive sign-in popup/redirect opens. Targets the HOSTED login
519
+ * PAGE (`config.signInUrl`) — which renders the form, POSTs the API, and redirects
520
+ * to `redirect_uri` with the code. Falls back to the API authorize URL when no
521
+ * `signInUrl` is configured (deployments where the API host serves the page).
522
+ */
523
+ buildSignInUrl(params: BuildAuthorizeUrlParams): string;
471
524
  /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
472
525
  completeAuthorize(params: BuildAuthorizeUrlParams): Promise<AuthorizeOutcome>;
526
+ /**
527
+ * The embedded `<SignIn/>` credential flow (D2b): POST `/idp/authorize` with
528
+ * email/password + PKCE. On `authorized` the caller exchanges the returned
529
+ * `code` (server-side, via the BFF) for a session — no redirect to a hosted
530
+ * login page. Creds live only for the duration of this request; the SDK never
531
+ * stores them. 5xx surfaces as a typed error; every other status is a typed
532
+ * outcome the UI branches on.
533
+ */
534
+ authorizeWithPassword(params: AuthorizeWithPasswordParams): Promise<PasswordAuthOutcome>;
473
535
  exchangeCode(params: ExchangeCodeParams): Promise<StoredSession>;
474
536
  /** The `RefreshTokens` function the SessionEngine (F003) injects. */
475
537
  createRefresher(): RefreshTokens;
@@ -488,4 +550,4 @@ declare class AuthClient {
488
550
  declare const PACKAGE_NAME = "@poly-x/core";
489
551
  declare const version = "0.0.0";
490
552
  //#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 };
553
+ export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type AuthorizeWithPasswordParams, 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 PasswordAuthOutcome, 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, parsePasswordOutcome, parsePolyXKey, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
package/dist/index.mjs CHANGED
@@ -166,11 +166,21 @@ function resolveConfig(input) {
166
166
  }
167
167
  baseUrl = normalizeBaseUrl(input.baseUrl);
168
168
  } else baseUrl = `https://${normalizeBaseUrl(key.host)}`;
169
+ let signInUrl;
170
+ if (input.signInUrl !== void 0) {
171
+ try {
172
+ new URL(input.signInUrl);
173
+ } catch {
174
+ throw new PolyXConfigError("The signInUrl is not a valid URL.", { code: "SIGN_IN_URL_INVALID" });
175
+ }
176
+ signInUrl = normalizeBaseUrl(input.signInUrl);
177
+ }
169
178
  return {
170
179
  key,
171
180
  kind: key.kind,
172
181
  env: key.env,
173
182
  baseUrl,
183
+ signInUrl,
174
184
  context
175
185
  };
176
186
  }
@@ -736,6 +746,27 @@ function parseAuthorizeOutcome(status, body) {
736
746
  if (statusField === "login_required" || b.code === "LOGIN_REQUIRED") return { type: "login_required" };
737
747
  return { type: "login_required" };
738
748
  }
749
+ function parsePasswordOutcome(status, body) {
750
+ const b = asRecord$1(body);
751
+ const statusField = typeof b.status === "string" ? b.status : void 0;
752
+ if (statusField === "authorized" && typeof b.code === "string") return {
753
+ type: "authorized",
754
+ code: b.code,
755
+ state: typeof b.state === "string" ? b.state : "",
756
+ redirectUri: typeof b.redirectUri === "string" ? b.redirectUri : void 0
757
+ };
758
+ if (statusField === "select_tenant") return {
759
+ type: "select_tenant",
760
+ state: typeof b.state === "string" ? b.state : "",
761
+ tenants: parseTenants(b.tenants)
762
+ };
763
+ if (status === 403 && b.userPasswordSet === true && typeof b.userId === "string") return {
764
+ type: "password_change_required",
765
+ userId: b.userId
766
+ };
767
+ if (status === 403) return { type: "no_access" };
768
+ return { type: "invalid_credentials" };
769
+ }
739
770
  //#endregion
740
771
  //#region src/auth/normalize.ts
741
772
  const FALLBACK_TTL_MS = 5 * 6e4;
@@ -814,10 +845,9 @@ var AuthClient = class {
814
845
  this.transport = options.transport;
815
846
  this.clock = options.clock ?? new SystemClock();
816
847
  }
817
- buildAuthorizeUrl(params) {
818
- const url = new URL(this.config.baseUrl + ENDPOINTS.authorize);
848
+ appendAuthorizeParams(url, params) {
819
849
  url.searchParams.set("response_type", "code");
820
- url.searchParams.set("client", this.config.key.raw);
850
+ url.searchParams.set("client_id", this.config.key.raw);
821
851
  url.searchParams.set("redirect_uri", params.redirectUri);
822
852
  url.searchParams.set("state", params.state);
823
853
  url.searchParams.set("code_challenge", params.challenge);
@@ -826,6 +856,20 @@ var AuthClient = class {
826
856
  if (params.prompt) url.searchParams.set("prompt", params.prompt);
827
857
  return url.toString();
828
858
  }
859
+ /** The API `GET /v1/idp/authorize` URL — used for the silent warm-hop probe. */
860
+ buildAuthorizeUrl(params) {
861
+ return this.appendAuthorizeParams(new URL(this.config.baseUrl + ENDPOINTS.authorize), params);
862
+ }
863
+ /**
864
+ * The URL the interactive sign-in popup/redirect opens. Targets the HOSTED login
865
+ * PAGE (`config.signInUrl`) — which renders the form, POSTs the API, and redirects
866
+ * to `redirect_uri` with the code. Falls back to the API authorize URL when no
867
+ * `signInUrl` is configured (deployments where the API host serves the page).
868
+ */
869
+ buildSignInUrl(params) {
870
+ if (!this.config.signInUrl) return this.buildAuthorizeUrl(params);
871
+ return this.appendAuthorizeParams(new URL(this.config.signInUrl), params);
872
+ }
829
873
  /** Probe the warm hop (GET /idp/authorize) and return the typed outcome. */
830
874
  async completeAuthorize(params) {
831
875
  const response = await this.transport.request({
@@ -835,6 +879,34 @@ var AuthClient = class {
835
879
  if (response.status >= 500) throw this.toError(response);
836
880
  return parseAuthorizeOutcome(response.status, response.body);
837
881
  }
882
+ /**
883
+ * The embedded `<SignIn/>` credential flow (D2b): POST `/idp/authorize` with
884
+ * email/password + PKCE. On `authorized` the caller exchanges the returned
885
+ * `code` (server-side, via the BFF) for a session — no redirect to a hosted
886
+ * login page. Creds live only for the duration of this request; the SDK never
887
+ * stores them. 5xx surfaces as a typed error; every other status is a typed
888
+ * outcome the UI branches on.
889
+ */
890
+ async authorizeWithPassword(params) {
891
+ const response = await this.transport.request({
892
+ method: "POST",
893
+ url: this.config.baseUrl + ENDPOINTS.authorize,
894
+ body: {
895
+ response_type: "code",
896
+ client_id: this.config.key.raw,
897
+ redirect_uri: params.redirectUri,
898
+ state: params.state,
899
+ code_challenge: params.challenge,
900
+ code_challenge_method: "S256",
901
+ email: params.email,
902
+ password: params.password,
903
+ ...params.organizationCode ? { organizationCode: params.organizationCode } : {},
904
+ ...params.tenantId ? { tenantId: params.tenantId } : {}
905
+ }
906
+ });
907
+ if (response.status >= 500) throw this.toError(response);
908
+ return parsePasswordOutcome(response.status, response.body);
909
+ }
838
910
  async exchangeCode(params) {
839
911
  const response = await this.transport.request({
840
912
  method: "POST",
@@ -844,7 +916,7 @@ var AuthClient = class {
844
916
  code: params.code,
845
917
  code_verifier: params.codeVerifier,
846
918
  redirect_uri: params.redirectUri,
847
- client: this.config.key.raw
919
+ client_id: this.config.key.raw
848
920
  }
849
921
  });
850
922
  if (response.status !== 200) throw this.toError(response);
@@ -899,4 +971,4 @@ var AuthClient = class {
899
971
  const PACKAGE_NAME = "@poly-x/core";
900
972
  const version = "0.0.0";
901
973
  //#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 };
974
+ 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, parsePasswordOutcome, parsePolyXKey, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/core",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Framework-agnostic core of the PolyX SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",