@palbase/web 6.0.0 → 7.1.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.
Files changed (37) hide show
  1. package/dist/{analytics-facade-Cp3d4QI-.d.ts → analytics-facade-CZIwbsvG.d.ts} +65 -1
  2. package/dist/{analytics-facade-CaluA4bW.d.cts → analytics-facade-esr6cOBN.d.cts} +65 -1
  3. package/dist/{chunk-BNAGRUIQ.js → chunk-ACCJV6FV.js} +32 -2
  4. package/dist/chunk-ACCJV6FV.js.map +1 -0
  5. package/dist/{chunk-KOJ4OAAR.js → chunk-P6TWUWZC.js} +209 -3
  6. package/dist/chunk-P6TWUWZC.js.map +1 -0
  7. package/dist/{chunk-OWLTZG2V.js → chunk-ZC6Q3GPX.js} +2 -2
  8. package/dist/index.cjs +207 -1
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +3 -3
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +2 -2
  13. package/dist/internal.cjs +238 -2
  14. package/dist/internal.cjs.map +1 -1
  15. package/dist/internal.d.cts +3 -3
  16. package/dist/internal.d.ts +3 -3
  17. package/dist/internal.js +2 -2
  18. package/dist/next/client.cjs +238 -2
  19. package/dist/next/client.cjs.map +1 -1
  20. package/dist/next/client.js +2 -2
  21. package/dist/next/index.cjs +238 -2
  22. package/dist/next/index.cjs.map +1 -1
  23. package/dist/next/index.d.cts +2 -2
  24. package/dist/next/index.d.ts +2 -2
  25. package/dist/next/index.js +3 -3
  26. package/dist/next/proxy.js +2 -2
  27. package/dist/{pb-DUK5qy81.d.ts → pb-AWwslm5m.d.ts} +1 -1
  28. package/dist/{pb-DvV6ftmf.d.cts → pb-BT-vKA4j.d.cts} +1 -1
  29. package/dist/react/index.cjs +1 -1
  30. package/dist/react/index.cjs.map +1 -1
  31. package/dist/react/index.d.cts +1 -1
  32. package/dist/react/index.d.ts +1 -1
  33. package/dist/react/index.js +2 -2
  34. package/package.json +1 -1
  35. package/dist/chunk-BNAGRUIQ.js.map +0 -1
  36. package/dist/chunk-KOJ4OAAR.js.map +0 -1
  37. /package/dist/{chunk-OWLTZG2V.js.map → chunk-ZC6Q3GPX.js.map} +0 -0
@@ -15,6 +15,15 @@ interface PalbaseResponse$1<T> {
15
15
  status: number;
16
16
  }
17
17
  interface HttpClientOptions$1 {
18
+ /**
19
+ * The host application's own version, sent as `X-Palbase-Client-Version` and
20
+ * readable in flag targeting as `client.app_version`.
21
+ *
22
+ * Opt-in because the SDK cannot discover it on the web — there is no bundle
23
+ * to read a version out of. iOS fills the same header automatically from
24
+ * CFBundleShortVersionString.
25
+ */
26
+ appVersion?: string;
18
27
  url?: string;
19
28
  headers?: Record<string, string>;
20
29
  }
@@ -104,6 +113,15 @@ interface PalbaseResponse<T> {
104
113
  status: number;
105
114
  }
106
115
  interface HttpClientOptions {
116
+ /**
117
+ * The host application's own version, sent as `X-Palbase-Client-Version` and
118
+ * readable in flag targeting as `client.app_version`.
119
+ *
120
+ * Opt-in because the SDK cannot discover it on the web — there is no bundle
121
+ * to read a version out of. iOS fills the same header automatically from
122
+ * CFBundleShortVersionString.
123
+ */
124
+ appVersion?: string;
107
125
  url?: string;
108
126
  headers?: Record<string, string>;
109
127
  }
@@ -1648,6 +1666,19 @@ interface AuthUser {
1648
1666
  emailVerified: boolean;
1649
1667
  createdAt: string;
1650
1668
  }
1669
+ /** What `pb.auth.getSignInMethods()` reports about an account's credentials. */
1670
+ interface SignInMethods {
1671
+ /** A local password exists — offer "change password", not "set password". */
1672
+ password: boolean;
1673
+ /** An MFA factor is enrolled. */
1674
+ mfa: boolean;
1675
+ /**
1676
+ * Linked OAuth providers, e.g. `['google']`. `[]` means none are linked;
1677
+ * `null` means the server could not determine it (social login is not
1678
+ * configured, or the lookup failed) — treat null as unknown, not as none.
1679
+ */
1680
+ providers: string[] | null;
1681
+ }
1651
1682
  interface AuthSuccess {
1652
1683
  user: AuthUser;
1653
1684
  session: Session$1;
@@ -1716,6 +1747,16 @@ declare class PalbeAuth {
1716
1747
  erasureId: string;
1717
1748
  }>;
1718
1749
  getUser(): Promise<AuthUser>;
1750
+ /**
1751
+ * Which credentials this account actually has.
1752
+ *
1753
+ * An account reached through Google has no password, so showing it a
1754
+ * "current password" field gives the user something they can never fill —
1755
+ * and `getUser()` cannot tell you, because GET /auth/user carries no
1756
+ * credential information. Read this before rendering any re-auth or
1757
+ * change-password form.
1758
+ */
1759
+ getSignInMethods(): Promise<SignInMethods>;
1719
1760
  refreshUser(): Promise<AuthUser>;
1720
1761
  /**
1721
1762
  * Restore the user after a session was rehydrated from storage (page reload).
@@ -1798,6 +1839,29 @@ declare class PalbeAuth {
1798
1839
  provider: string;
1799
1840
  credential: string;
1800
1841
  }): Promise<AuthSuccess>;
1842
+ /**
1843
+ * Sign in with a passkey stored for this Environment. No identifier is typed:
1844
+ * the browser lists the accounts it holds and the user picks one.
1845
+ *
1846
+ * Throws when the browser cannot run a ceremony — check `passkeysSupported`
1847
+ * first and keep a password or e-mail path beside the button.
1848
+ */
1849
+ signInWithPasskey(): Promise<AuthSuccess>;
1850
+ /**
1851
+ * Create an account whose only credential is a passkey — the e-mail is the
1852
+ * identifier, the passkey is the credential, and no password ever exists.
1853
+ */
1854
+ signUpWithPasskey(email: string, displayName?: string): Promise<AuthSuccess>;
1855
+ /**
1856
+ * Add a passkey to the signed-in account, so the next sign-in is one tap.
1857
+ *
1858
+ * Rejects with a 403 `reauthentication_required` when the session is not
1859
+ * recent: a passkey outlives a password reset, so enrolling one takes more
1860
+ * than a valid session.
1861
+ */
1862
+ registerPasskey(name?: string): Promise<void>;
1863
+ /** Whether this browser can run a passkey ceremony at all. */
1864
+ get passkeysSupported(): boolean;
1801
1865
  signInWithOAuth(params: {
1802
1866
  provider: string;
1803
1867
  redirectTo?: string;
@@ -1906,4 +1970,4 @@ declare class PalbeAnalytics {
1906
1970
  private post;
1907
1971
  }
1908
1972
 
1909
- export { type AuthUser as A, PalbeMessaging as B, Chat as C, type PalbeOAuthConfig as D, PalbeRealtime as E, type FlagsView as F, type PresenceState as G, RealtimeChannel as H, type RealtimeHandler as I, type RealtimeStatus as J, type RealtimeStatusSnapshot as K, type RealtimeSubscription as L, type MagicLinkResult as M, type NotifyScope as N, type OAuthExchangeResult as O, type PalbeConfig as P, type ResolvedMention as Q, type RealtimeConnectionState as R, type ResolvedReply as S, type SentReceipt as T, type UnreadView as U, type Unsubscribe as V, PalbePerf as W, type RealtimePayload as a, type ChatMember as b, type ChatMessage as c, type PalbeRuntime as d, buildRuntime as e, type AnalyticsProperties as f, type AuthChangeEvent as g, type AuthState as h, type AuthSuccess as i, Call as j, type CallChangeCallback as k, type CallParticipant as l, type CallState as m, type ChatBackend as n, type ChatDraft as o, type ChatKind as p, type ChatMessageKind as q, type ChatRole as r, type ChatState as s, type CommsPrefs as t, type MentionRange as u, type MessageDirection as v, PalbeAnalytics as w, PalbeAuth as x, PalbeCalls as y, PalbeFlags as z };
1973
+ export { type AuthUser as A, PalbeMessaging as B, Chat as C, type PalbeOAuthConfig as D, PalbeRealtime as E, type FlagsView as F, type PresenceState as G, RealtimeChannel as H, type RealtimeHandler as I, type RealtimeStatus as J, type RealtimeStatusSnapshot as K, type RealtimeSubscription as L, type MagicLinkResult as M, type NotifyScope as N, type OAuthExchangeResult as O, type PalbeConfig as P, type ResolvedMention as Q, type RealtimeConnectionState as R, type ResolvedReply as S, type SentReceipt as T, type SignInMethods as U, type UnreadView as V, type Unsubscribe as W, PalbePerf as X, type RealtimePayload as a, type ChatMember as b, type ChatMessage as c, type PalbeRuntime as d, buildRuntime as e, type AnalyticsProperties as f, type AuthChangeEvent as g, type AuthState as h, type AuthSuccess as i, Call as j, type CallChangeCallback as k, type CallParticipant as l, type CallState as m, type ChatBackend as n, type ChatDraft as o, type ChatKind as p, type ChatMessageKind as q, type ChatRole as r, type ChatState as s, type CommsPrefs as t, type MentionRange as u, type MessageDirection as v, PalbeAnalytics as w, PalbeAuth as x, PalbeCalls as y, PalbeFlags as z };
@@ -15,6 +15,15 @@ interface PalbaseResponse$1<T> {
15
15
  status: number;
16
16
  }
17
17
  interface HttpClientOptions$1 {
18
+ /**
19
+ * The host application's own version, sent as `X-Palbase-Client-Version` and
20
+ * readable in flag targeting as `client.app_version`.
21
+ *
22
+ * Opt-in because the SDK cannot discover it on the web — there is no bundle
23
+ * to read a version out of. iOS fills the same header automatically from
24
+ * CFBundleShortVersionString.
25
+ */
26
+ appVersion?: string;
18
27
  url?: string;
19
28
  headers?: Record<string, string>;
20
29
  }
@@ -104,6 +113,15 @@ interface PalbaseResponse<T> {
104
113
  status: number;
105
114
  }
106
115
  interface HttpClientOptions {
116
+ /**
117
+ * The host application's own version, sent as `X-Palbase-Client-Version` and
118
+ * readable in flag targeting as `client.app_version`.
119
+ *
120
+ * Opt-in because the SDK cannot discover it on the web — there is no bundle
121
+ * to read a version out of. iOS fills the same header automatically from
122
+ * CFBundleShortVersionString.
123
+ */
124
+ appVersion?: string;
107
125
  url?: string;
108
126
  headers?: Record<string, string>;
109
127
  }
@@ -1648,6 +1666,19 @@ interface AuthUser {
1648
1666
  emailVerified: boolean;
1649
1667
  createdAt: string;
1650
1668
  }
1669
+ /** What `pb.auth.getSignInMethods()` reports about an account's credentials. */
1670
+ interface SignInMethods {
1671
+ /** A local password exists — offer "change password", not "set password". */
1672
+ password: boolean;
1673
+ /** An MFA factor is enrolled. */
1674
+ mfa: boolean;
1675
+ /**
1676
+ * Linked OAuth providers, e.g. `['google']`. `[]` means none are linked;
1677
+ * `null` means the server could not determine it (social login is not
1678
+ * configured, or the lookup failed) — treat null as unknown, not as none.
1679
+ */
1680
+ providers: string[] | null;
1681
+ }
1651
1682
  interface AuthSuccess {
1652
1683
  user: AuthUser;
1653
1684
  session: Session$1;
@@ -1716,6 +1747,16 @@ declare class PalbeAuth {
1716
1747
  erasureId: string;
1717
1748
  }>;
1718
1749
  getUser(): Promise<AuthUser>;
1750
+ /**
1751
+ * Which credentials this account actually has.
1752
+ *
1753
+ * An account reached through Google has no password, so showing it a
1754
+ * "current password" field gives the user something they can never fill —
1755
+ * and `getUser()` cannot tell you, because GET /auth/user carries no
1756
+ * credential information. Read this before rendering any re-auth or
1757
+ * change-password form.
1758
+ */
1759
+ getSignInMethods(): Promise<SignInMethods>;
1719
1760
  refreshUser(): Promise<AuthUser>;
1720
1761
  /**
1721
1762
  * Restore the user after a session was rehydrated from storage (page reload).
@@ -1798,6 +1839,29 @@ declare class PalbeAuth {
1798
1839
  provider: string;
1799
1840
  credential: string;
1800
1841
  }): Promise<AuthSuccess>;
1842
+ /**
1843
+ * Sign in with a passkey stored for this Environment. No identifier is typed:
1844
+ * the browser lists the accounts it holds and the user picks one.
1845
+ *
1846
+ * Throws when the browser cannot run a ceremony — check `passkeysSupported`
1847
+ * first and keep a password or e-mail path beside the button.
1848
+ */
1849
+ signInWithPasskey(): Promise<AuthSuccess>;
1850
+ /**
1851
+ * Create an account whose only credential is a passkey — the e-mail is the
1852
+ * identifier, the passkey is the credential, and no password ever exists.
1853
+ */
1854
+ signUpWithPasskey(email: string, displayName?: string): Promise<AuthSuccess>;
1855
+ /**
1856
+ * Add a passkey to the signed-in account, so the next sign-in is one tap.
1857
+ *
1858
+ * Rejects with a 403 `reauthentication_required` when the session is not
1859
+ * recent: a passkey outlives a password reset, so enrolling one takes more
1860
+ * than a valid session.
1861
+ */
1862
+ registerPasskey(name?: string): Promise<void>;
1863
+ /** Whether this browser can run a passkey ceremony at all. */
1864
+ get passkeysSupported(): boolean;
1801
1865
  signInWithOAuth(params: {
1802
1866
  provider: string;
1803
1867
  redirectTo?: string;
@@ -1906,4 +1970,4 @@ declare class PalbeAnalytics {
1906
1970
  private post;
1907
1971
  }
1908
1972
 
1909
- export { type AuthUser as A, PalbeMessaging as B, Chat as C, type PalbeOAuthConfig as D, PalbeRealtime as E, type FlagsView as F, type PresenceState as G, RealtimeChannel as H, type RealtimeHandler as I, type RealtimeStatus as J, type RealtimeStatusSnapshot as K, type RealtimeSubscription as L, type MagicLinkResult as M, type NotifyScope as N, type OAuthExchangeResult as O, type PalbeConfig as P, type ResolvedMention as Q, type RealtimeConnectionState as R, type ResolvedReply as S, type SentReceipt as T, type UnreadView as U, type Unsubscribe as V, PalbePerf as W, type RealtimePayload as a, type ChatMember as b, type ChatMessage as c, type PalbeRuntime as d, buildRuntime as e, type AnalyticsProperties as f, type AuthChangeEvent as g, type AuthState as h, type AuthSuccess as i, Call as j, type CallChangeCallback as k, type CallParticipant as l, type CallState as m, type ChatBackend as n, type ChatDraft as o, type ChatKind as p, type ChatMessageKind as q, type ChatRole as r, type ChatState as s, type CommsPrefs as t, type MentionRange as u, type MessageDirection as v, PalbeAnalytics as w, PalbeAuth as x, PalbeCalls as y, PalbeFlags as z };
1973
+ export { type AuthUser as A, PalbeMessaging as B, Chat as C, type PalbeOAuthConfig as D, PalbeRealtime as E, type FlagsView as F, type PresenceState as G, RealtimeChannel as H, type RealtimeHandler as I, type RealtimeStatus as J, type RealtimeStatusSnapshot as K, type RealtimeSubscription as L, type MagicLinkResult as M, type NotifyScope as N, type OAuthExchangeResult as O, type PalbeConfig as P, type ResolvedMention as Q, type RealtimeConnectionState as R, type ResolvedReply as S, type SentReceipt as T, type SignInMethods as U, type UnreadView as V, type Unsubscribe as W, PalbePerf as X, type RealtimePayload as a, type ChatMember as b, type ChatMessage as c, type PalbeRuntime as d, buildRuntime as e, type AnalyticsProperties as f, type AuthChangeEvent as g, type AuthState as h, type AuthSuccess as i, Call as j, type CallChangeCallback as k, type CallParticipant as l, type CallState as m, type ChatBackend as n, type ChatDraft as o, type ChatKind as p, type ChatMessageKind as q, type ChatRole as r, type ChatState as s, type CommsPrefs as t, type MentionRange as u, type MessageDirection as v, PalbeAnalytics as w, PalbeAuth as x, PalbeCalls as y, PalbeFlags as z };
@@ -12,6 +12,27 @@ var PalbaseError = class extends Error {
12
12
  this.details = details;
13
13
  }
14
14
  };
15
+ function detectPlatform() {
16
+ if (typeof Deno !== "undefined") {
17
+ return "deno";
18
+ }
19
+ if (process?.versions) {
20
+ if ("bun" in process.versions) {
21
+ return "bun";
22
+ }
23
+ if ("node" in process.versions) {
24
+ return "node";
25
+ }
26
+ }
27
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
28
+ return "react-native";
29
+ }
30
+ return "browser";
31
+ }
32
+ function wirePlatform() {
33
+ const host = detectPlatform();
34
+ return host === "browser" ? "web" : host;
35
+ }
15
36
  var PALBASE_DEFAULT_HOST = "api.palbase.studio";
16
37
  var API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20}$/;
17
38
  function parseEnvironmentRef(apiKey) {
@@ -97,8 +118,17 @@ var HttpClient = class _HttpClient {
97
118
  }
98
119
  buildHeaders(options) {
99
120
  const headers = {
100
- "Content-Type": "application/json"
121
+ "Content-Type": "application/json",
122
+ // Client identity, the web counterpart of the iOS SDK's
123
+ // ClientInfo.augment(). The server reads these to resolve flag targeting
124
+ // conditions and to label telemetry, so an app declares nothing and calls
125
+ // nothing — whatever the SDK can know, it sends.
126
+ "X-Platform": wirePlatform()
101
127
  };
128
+ const appVersion = this.options?.appVersion?.trim();
129
+ if (appVersion) {
130
+ headers["X-Palbase-Client-Version"] = appVersion;
131
+ }
102
132
  const effectiveKey = this.apiKey;
103
133
  if (effectiveKey) {
104
134
  headers["apikey"] = effectiveKey;
@@ -378,4 +408,4 @@ export {
378
408
  asPalbaseError,
379
409
  unwrap
380
410
  };
381
- //# sourceMappingURL=chunk-BNAGRUIQ.js.map
411
+ //# sourceMappingURL=chunk-ACCJV6FV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/config.ts","../../core/src/errors.ts","../../core/src/platform.ts","../../core/src/http.ts","../../core/src/token.ts","../src/errors.ts"],"sourcesContent":["import type { HttpClient } from './http.js';\nimport type { ProjectConfig } from './types.js';\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\nexport class ConfigFetcher {\n protected readonly httpClient: HttpClient;\n private cachedConfig: ProjectConfig | null = null;\n private cacheTimestamp = 0;\n\n constructor(httpClient: HttpClient) {\n this.httpClient = httpClient;\n }\n\n async getConfig(): Promise<ProjectConfig | null> {\n const now = Date.now();\n\n if (this.cachedConfig && now - this.cacheTimestamp < CACHE_TTL_MS) {\n return this.cachedConfig;\n }\n\n try {\n const response = await this.httpClient.request<ProjectConfig>('GET', '/v1/config');\n\n if (response.error || !response.data) {\n return null;\n }\n\n this.cachedConfig = response.data;\n this.cacheTimestamp = now;\n\n return this.cachedConfig;\n } catch {\n return null;\n }\n }\n}\n","export class PalbaseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly details?: unknown;\n\n constructor(code: string, message: string, status: number, details?: unknown) {\n super(message);\n this.name = 'PalbaseError';\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n","export type Platform = 'browser' | 'node' | 'react-native' | 'deno' | 'bun';\n\ndeclare const Deno: unknown;\ndeclare const process: { versions: Record<string, string> } | undefined;\n\nexport function detectPlatform(): Platform {\n if (typeof Deno !== 'undefined') {\n return 'deno';\n }\n\n if (process?.versions) {\n if ('bun' in process.versions) {\n return 'bun';\n }\n if ('node' in process.versions) {\n return 'node';\n }\n }\n\n if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {\n return 'react-native';\n }\n\n return 'browser';\n}\n\n/**\n * The platform word this SDK puts on the wire (`X-Platform`), which the server\n * reads to target flags and to label telemetry.\n *\n * It is NOT `detectPlatform()`'s value verbatim: that reports the JS host\n * (\"browser\"), while the wire wants the platform. iOS sends \"ios\", not the name\n * of its runtime, and a condition author writes `client.platform == 'web'` —\n * the word every other flag vendor uses too. Server hosts keep their own names,\n * where the distinction is the useful part.\n */\nexport function wirePlatform(): string {\n const host = detectPlatform();\n return host === 'browser' ? 'web' : host;\n}\n","import { PalbaseError } from './errors.js';\nimport { wirePlatform } from './platform.js';\nimport type { TokenManager } from './token.js';\nimport type { HttpClientOptions, PalbaseResponse, RequestOptions } from './types.js';\n\n/**\n * Default production host. Dev / staging / local callers override via\n * `options.url`. Apex-style routing is the only supported production path;\n * Kong resolves Environment identity from the API key.\n */\nconst PALBASE_DEFAULT_HOST = 'api.palbase.studio';\n\n/**\n * Parse the Environment ref from a Palbase API key.\n *\n * Canonical shape: `pb_{environment_ref}_c{random}`, where the Environment\n * ref is 4-24 lowercase ASCII alphanumeric characters and random is 20 base62\n * chars. See\n * docs/MODULE_HEADER_CONTRACT.md §\"API key format\" (palbase repo) for\n * the full spec.\n *\n * Returns the Environment ref on match; `null` otherwise.\n */\nconst API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20}$/;\n\nfunction parseEnvironmentRef(apiKey: string): string | null {\n return API_KEY_RE.exec(apiKey)?.[1] ?? null;\n}\nconst MAX_RETRIES = 3;\nconst INITIAL_BACKOFF_MS = 200;\n/**\n * Upper bound on a single 429 retry sleep. A server may return a long\n * Retry-After (a locked account can send minutes/hours); honoring it verbatim\n * would HANG the request for that whole window. Cap each retry at 10s — after\n * MAX_RETRIES the 429 envelope surfaces to the caller (fail fast, don't sleep\n * minutes). The clamp never skips a retry; it only bounds how long each waits.\n */\nconst MAX_RETRY_DELAY_MS = 10_000;\n\n/**\n * Request interceptor. Runs before every HTTP request.\n * Can modify headers, body, or reject the request.\n */\nexport type RequestInterceptor = (request: {\n headers: Record<string, string>;\n method: string;\n path: string;\n}) => void | Promise<void>;\n\nexport class HttpClient {\n protected readonly apiKey: string;\n protected readonly options?: HttpClientOptions;\n\n tokenManager: TokenManager | null = null;\n\n /**\n * Admin JWT used for platform admin endpoints (/admin/*).\n * When set, takes precedence over tokenManager access token in the\n * Authorization header.\n */\n adminToken: string | null = null;\n\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(apiKey: string, options?: HttpClientOptions) {\n this.apiKey = apiKey;\n this.options = options;\n }\n\n /** Set (or clear) the admin JWT used on admin endpoints. */\n setAdminToken(token: string | null): void {\n this.adminToken = token;\n }\n\n /**\n * Create a scoped HttpClient that adds the given extra headers to every\n * request. The returned client shares the admin token and token manager\n * with the parent at runtime — later changes on the parent propagate to\n * the scope and vice versa.\n *\n * Typical use: adding an Environment-routing header for an admin call.\n */\n withHeaders(extra: Record<string, string>): HttpClient {\n const mergedHeaders = { ...(this.options?.headers ?? {}), ...extra };\n\n const scoped: HttpClient = new HttpClient(this.apiKey, {\n ...this.options,\n headers: mergedHeaders,\n });\n scoped.tokenManager = this.tokenManager;\n // Delegate adminToken reads + writes to the parent so the scope always\n // sees the latest token, and setAdminToken on the scope affects the parent.\n Object.defineProperty(scoped, 'adminToken', {\n get: () => this.adminToken,\n set: (v: string | null) => {\n this.adminToken = v;\n },\n configurable: true,\n });\n return scoped;\n }\n\n /** Add a request interceptor. Runs before every request. */\n addInterceptor(interceptor: RequestInterceptor): void {\n this.interceptors.push(interceptor);\n }\n\n async request<T>(\n method: string,\n path: string,\n options?: RequestOptions,\n ): Promise<PalbaseResponse<T>> {\n // If token is expired and refresh is available, refresh before making the request\n if (\n this.tokenManager?.isExpired() &&\n this.tokenManager.getRefreshToken() &&\n this.tokenManager.refreshFunction\n ) {\n try {\n await this.tokenManager.refreshSession();\n } catch (e) {\n const status = e instanceof PalbaseError ? e.status : 0;\n if (status === 400 || status === 401 || status === 403) {\n // Terminal: the refresh token is dead (revoked/expired/forbidden).\n // Clear the session (listeners persist the sign-out) and proceed\n // unauthenticated — the endpoint will 401 into the normal error\n // envelope instead of bricking every subsequent call including\n // the recovery sign-in.\n this.tokenManager.clearSession();\n } else {\n throw e; // network/5xx: transient, stay loud\n }\n }\n }\n\n return this.executeWithRetry<T>(method, path, options, 0);\n }\n\n private getBaseUrl(): string {\n // Explicit URL always wins (local dev, staging, test rigs).\n if (this.options?.url) {\n return this.options.url;\n }\n\n // Validate the key shape up front so apex-routed callers still\n // fail loud on a malformed key instead of hitting the gateway\n // with bad credentials.\n if (this.apiKey && parseEnvironmentRef(this.apiKey) === null) {\n throw new PalbaseError(\n 'invalid_api_key',\n 'Invalid API key format. Expected pb_{environment_ref}_c{20_base62_chars}. For dev/staging pass `url: \"https://api.dev.palbase.studio\"` via options.',\n 0,\n );\n }\n\n return `https://${PALBASE_DEFAULT_HOST}`;\n }\n\n private buildHeaders(options?: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n // Client identity, the web counterpart of the iOS SDK's\n // ClientInfo.augment(). The server reads these to resolve flag targeting\n // conditions and to label telemetry, so an app declares nothing and calls\n // nothing — whatever the SDK can know, it sends.\n 'X-Platform': wirePlatform(),\n };\n // The host app's own version is not knowable on the web (no bundle to read\n // it from), so it is opt-in; when given it fills the same header iOS fills\n // from CFBundleShortVersionString.\n const appVersion = this.options?.appVersion?.trim();\n if (appVersion) {\n headers['X-Palbase-Client-Version'] = appVersion;\n }\n\n // Palbase Environment keys live in the `apikey` header — never in\n // `Authorization` — because Kong's key-auth resolves them on that\n // header and the gateway's pre-function plugin stamps the downstream\n // identity.\n const effectiveKey = this.apiKey;\n if (effectiveKey) {\n headers['apikey'] = effectiveKey;\n }\n\n // User session token, if any. Kong's pre-function plugin strips\n // Authorization on /v1/* routes anyway (PostgREST has no JWT\n // secret and would crash on a Bearer it can't decode), but\n // sending it preserves the contract for /auth/* endpoints that\n // do consume the bearer (e.g. session refresh).\n const token = this.tokenManager?.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n // adminToken (platform admin JWT) takes precedence — used by the\n // @palbase/admin internal flows that hit /admin/* routes; those\n // routes verify the bearer themselves and aren't subject to the\n // /v1/* Authorization-strip rule.\n if (this.adminToken) {\n headers['Authorization'] = `Bearer ${this.adminToken}`;\n }\n\n // Merge global custom headers\n if (this.options?.headers) {\n Object.assign(headers, this.options.headers);\n }\n\n // Merge per-request headers\n if (options?.headers) {\n Object.assign(headers, options.headers);\n }\n\n return headers;\n }\n\n private async executeWithRetry<T>(\n method: string,\n path: string,\n options: RequestOptions | undefined,\n attempt: number,\n ): Promise<PalbaseResponse<T>> {\n const url = `${this.getBaseUrl()}${path}`;\n const headers = this.buildHeaders(options);\n\n // Run interceptors\n for (const interceptor of this.interceptors) {\n await interceptor({ headers, method, path });\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: options?.signal,\n };\n\n if (options?.body !== undefined) {\n fetchOptions.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, fetchOptions);\n } catch (error) {\n // Network error — retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;\n await this.delay(backoff);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n\n // All retries exhausted — throw PalbaseError\n throw new PalbaseError(\n 'network_error',\n error instanceof Error ? error.message : 'Network request failed',\n 0,\n );\n }\n\n // Handle 429 Too Many Requests — retry with Retry-After or backoff;\n // if retries exhausted, fall through to normal error response handling below\n if (response.status === 429) {\n if (attempt < MAX_RETRIES - 1) {\n const retryAfter = response.headers.get('Retry-After');\n const parsed = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN;\n // Clamp the server-requested wait: a long Retry-After (locked account)\n // must not hang the request — cap each sleep, exhaust MAX_RETRIES, then\n // fall through to surface the 429 envelope below.\n const delayMs = Number.isNaN(parsed)\n ? INITIAL_BACKOFF_MS * 2 ** attempt\n : Math.min(parsed * 1000, MAX_RETRY_DELAY_MS);\n await this.delay(delayMs);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n }\n\n // Parse response body\n let data: T | null = null;\n let errorBody: { error?: string; error_description?: string; status?: number } | undefined;\n\n // HEAD responses have no body by spec — skip parsing.\n const contentType = response.headers.get('Content-Type');\n if (method !== 'HEAD' && contentType?.includes('json')) {\n const body = (await response.json()) as Record<string, unknown>;\n if (response.ok) {\n data = body as T;\n } else {\n errorBody = body as typeof errorBody;\n }\n }\n\n if (!response.ok) {\n return {\n data: null,\n error: new PalbaseError(\n errorBody?.error ?? 'unknown_error',\n errorBody?.error_description ?? response.statusText,\n response.status,\n errorBody,\n ),\n status: response.status,\n };\n }\n\n // Parse PostgREST Content-Range for count queries (e.g. \"0-9/42\" or \"*/42\").\n const contentRange = response.headers.get('Content-Range');\n let count: number | undefined;\n if (contentRange) {\n const slash = contentRange.lastIndexOf('/');\n if (slash >= 0) {\n const totalPart = contentRange.slice(slash + 1);\n if (totalPart !== '*') {\n const parsed = Number.parseInt(totalPart, 10);\n if (!Number.isNaN(parsed)) {\n count = parsed;\n }\n }\n }\n }\n\n return {\n data,\n error: null,\n status: response.status,\n ...(count !== undefined ? { count } : {}),\n };\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","import type { AuthStateCallback, Session, Unsubscribe } from './types.js';\n\nexport class TokenManager {\n private session: Session | null = null;\n private listeners: Set<AuthStateCallback> = new Set();\n private refreshPromise: Promise<void> | null = null;\n private refreshing = false;\n\n refreshFunction: ((refreshToken: string) => Promise<Session>) | null = null;\n\n setSession(session: Session): void {\n this.session = session;\n this.notify('SESSION_SET', session);\n }\n\n getAccessToken(): string | null {\n return this.session?.accessToken ?? null;\n }\n\n getRefreshToken(): string | null {\n return this.session?.refreshToken ?? null;\n }\n\n clearSession(): void {\n this.session = null;\n this.notify('SESSION_CLEARED', null);\n }\n\n isExpired(): boolean {\n if (!this.session) return true;\n return Date.now() >= this.session.expiresAt;\n }\n\n async refreshSession(): Promise<void> {\n if (!this.session?.refreshToken || !this.refreshFunction) {\n return;\n }\n\n // Collapse concurrent refresh calls into a single request\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n // Re-entrancy guard: the wired refreshFunction issues its own HTTP request\n // (POST /auth/token/refresh) through HttpClient, whose pre-flight calls\n // refreshSession() again SYNCHRONOUSLY — before `refreshPromise` below is\n // assigned (the whole chain runs before the first real await). Without\n // this flag that recursion is unbounded (stack overflow). Returning early\n // lets the refresh request itself proceed unauthenticated — it carries\n // the refresh token in its body, not the Bearer header.\n if (this.refreshing) {\n return;\n }\n\n this.refreshing = true;\n this.refreshPromise = this.executeRefresh(this.session.refreshToken);\n\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = null;\n this.refreshing = false;\n }\n }\n\n onAuthStateChange(callback: AuthStateCallback): Unsubscribe {\n this.listeners.add(callback);\n return () => {\n this.listeners.delete(callback);\n };\n }\n\n private async executeRefresh(refreshToken: string): Promise<void> {\n if (!this.refreshFunction) return;\n const newSession = await this.refreshFunction(refreshToken);\n this.setSession(newSession);\n }\n\n private notify(event: 'SESSION_SET' | 'SESSION_CLEARED', session: Session | null): void {\n for (const listener of this.listeners) {\n listener(event, session);\n }\n }\n}\n","import { PalbaseError, type PalbaseResponse } from '@palbase/core';\n\nexport interface FieldError {\n field: string;\n message: string;\n}\n\nexport type BackendErrorKind =\n | 'notConfigured'\n | 'validation'\n | 'unauthorized'\n | 'rateLimited'\n | 'server'\n | 'network'\n | 'decode';\n\ninterface BackendErrorParams {\n code: string;\n message: string;\n status?: number;\n requestId?: string;\n fields?: FieldError[];\n retryAfter?: number;\n data?: unknown;\n}\n\nexport class BackendError extends Error {\n readonly kind: BackendErrorKind;\n readonly code: string;\n readonly status: number;\n readonly requestId?: string;\n readonly fields?: FieldError[];\n readonly retryAfter?: number;\n readonly data?: unknown;\n\n constructor(kind: BackendErrorKind, params: BackendErrorParams) {\n super(params.message);\n this.name = 'BackendError';\n this.kind = kind;\n this.code = params.code;\n this.status = params.status ?? 0;\n this.requestId = params.requestId;\n this.fields = params.fields;\n this.retryAfter = params.retryAfter;\n this.data = params.data;\n }\n\n static notConfigured(): BackendError {\n return new BackendError('notConfigured', {\n code: 'not_configured',\n message:\n \"Palbe is not configured. Run 'palbase web link' in your project and make sure palbe.gen.ts is imported once at app startup.\",\n });\n }\n}\n\nfunction isFieldErrorArray(value: unknown): value is FieldError[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every(\n (v) =>\n typeof v === 'object' &&\n v !== null &&\n typeof (v as Record<string, unknown>).field === 'string' &&\n typeof (v as Record<string, unknown>).message === 'string',\n )\n );\n}\n\nfunction pickField(value: unknown, key: string): unknown {\n if (typeof value === 'object' && value !== null) {\n return (value as Record<string, unknown>)[key];\n }\n return undefined;\n}\n\nfunction pickNumber(value: unknown, key: string): number | undefined {\n const n = pickField(value, key);\n return typeof n === 'number' ? n : undefined;\n}\n\nfunction pickString(value: unknown, key: string): string | undefined {\n const s = pickField(value, key);\n return typeof s === 'string' ? s : undefined;\n}\n\nexport function fromPalbaseError(err: PalbaseError): BackendError {\n // HttpClient stores the WHOLE wire envelope as err.details:\n // { error, error_description, status, request_id, retry_after?, details?, data? }\n const base: BackendErrorParams = {\n code: err.code,\n message: err.message,\n status: err.status,\n requestId: pickString(err.details, 'request_id'),\n data: pickField(err.details, 'data'),\n };\n if (err.code === 'network_error') return new BackendError('network', base);\n if (err.status === 401) return new BackendError('unauthorized', base);\n if (err.status === 429)\n return new BackendError('rateLimited', {\n ...base,\n retryAfter: pickNumber(err.details, 'retry_after'),\n });\n // Field-error array lives at the envelope's nested `details` key.\n const nested = pickField(err.details, 'details');\n if (err.status === 400 && isFieldErrorArray(nested))\n return new BackendError('validation', { ...base, fields: nested });\n // Fallthrough also covers status-0 non-network errors (e.g. auth client's\n // synthetic 'no_refresh_token' with status 0) — those intentionally map to 'server'.\n return new BackendError('server', base);\n}\n\n/** Decode a raw wire body (used by paths that bypass HttpClient, e.g. upload). */\nexport function fromEnvelope(status: number, body: unknown): BackendError {\n const code = pickString(body, 'error') ?? 'http_error';\n const message = pickString(body, 'error_description') ?? `HTTP ${status}`;\n const requestId = pickString(body, 'request_id');\n const details = pickField(body, 'details');\n const params: BackendErrorParams = {\n code,\n message,\n status,\n requestId,\n data: pickField(body, 'data'),\n };\n if (status === 401) return new BackendError('unauthorized', params);\n if (status === 429)\n return new BackendError('rateLimited', {\n ...params,\n // Real 429 wire body has TOP-LEVEL retry_after; nested details is a fallback.\n retryAfter: pickNumber(body, 'retry_after') ?? pickNumber(details, 'retry_after'),\n });\n if (status === 400 && isFieldErrorArray(details))\n return new BackendError('validation', { ...params, fields: details });\n return new BackendError('server', params);\n}\n\n/**\n * Type guard for BackendError that survives module-identity splits.\n * This package ships dual ESM + CJS builds; if both end up loaded (or the\n * package is installed twice), two distinct BackendError classes coexist and\n * `instanceof` fails for errors thrown by \"the other\" copy. Falls back to a\n * structural check on `name` + `kind`.\n */\nexport function isBackendError(e: unknown): e is BackendError {\n return (\n e instanceof BackendError ||\n (typeof e === 'object' &&\n e !== null &&\n (e as Record<string, unknown>).name === 'BackendError' &&\n typeof (e as Record<string, unknown>).kind === 'string')\n );\n}\n\n/**\n * Structural fallback after instanceof for PalbaseError — same dual ESM+CJS\n * identity-split rationale as isBackendError above: two loaded PalbaseError\n * classes break `instanceof` across copies; the name check bridges that.\n * Returns the error as PalbaseError, or null when it isn't one.\n */\nexport function asPalbaseError(e: unknown): PalbaseError | null {\n if (e instanceof PalbaseError) return e;\n if (e instanceof Error && e.name === 'PalbaseError') return e as PalbaseError;\n return null;\n}\n\n/** Convert an internal {data,error} envelope into data-or-throw. */\nexport function unwrap<T>(res: PalbaseResponse<T>): T {\n if (res.error) throw fromPalbaseError(res.error);\n return res.data as T;\n}\n"],"mappings":"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","names":[]}
@@ -8,7 +8,7 @@ import {
8
8
  fromPalbaseError,
9
9
  isBackendError,
10
10
  unwrap
11
- } from "./chunk-BNAGRUIQ.js";
11
+ } from "./chunk-ACCJV6FV.js";
12
12
  import {
13
13
  environmentRefFromPublishableApiKey
14
14
  } from "./chunk-CFDU23TB.js";
@@ -1048,6 +1048,162 @@ function asWireAuthResult(raw) {
1048
1048
  return raw;
1049
1049
  }
1050
1050
 
1051
+ // src/passkey.ts
1052
+ function base64UrlToBytes(value) {
1053
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
1054
+ const binary = atob(padded.padEnd(padded.length + (4 - padded.length % 4) % 4, "="));
1055
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
1056
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
1057
+ return bytes;
1058
+ }
1059
+ function bytesToBase64Url(buffer) {
1060
+ const bytes = new Uint8Array(buffer);
1061
+ let binary = "";
1062
+ for (const b of bytes) binary += String.fromCharCode(b);
1063
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1064
+ }
1065
+ function requireString(source, key) {
1066
+ const value = source[key];
1067
+ if (typeof value !== "string" || value === "") {
1068
+ throw new Error(`passkey options are missing ${key}`);
1069
+ }
1070
+ return value;
1071
+ }
1072
+ function toCreationOptions(publicKey) {
1073
+ const rp = publicKey.rp;
1074
+ const user = publicKey.user;
1075
+ if (!user?.id) {
1076
+ throw new Error("passkey options carry no user id \u2014 the credential would be unusable");
1077
+ }
1078
+ const excludeCredentials = Array.isArray(publicKey.excludeCredentials) ? publicKey.excludeCredentials.map((c) => ({
1079
+ id: base64UrlToBytes(c.id),
1080
+ type: "public-key"
1081
+ })) : void 0;
1082
+ return {
1083
+ challenge: base64UrlToBytes(requireString(publicKey, "challenge")),
1084
+ rp: { id: rp?.id, name: rp?.name ?? rp?.id ?? "" },
1085
+ user: {
1086
+ id: base64UrlToBytes(user.id),
1087
+ name: user.name ?? "",
1088
+ displayName: user.displayName ?? user.name ?? ""
1089
+ },
1090
+ pubKeyCredParams: publicKey.pubKeyCredParams ?? [],
1091
+ authenticatorSelection: publicKey.authenticatorSelection,
1092
+ timeout: publicKey.timeout,
1093
+ attestation: publicKey.attestation,
1094
+ excludeCredentials
1095
+ };
1096
+ }
1097
+ function toRequestOptions(publicKey) {
1098
+ const allowCredentials = Array.isArray(publicKey.allowCredentials) ? publicKey.allowCredentials.map((c) => ({
1099
+ id: base64UrlToBytes(c.id),
1100
+ type: "public-key"
1101
+ })) : void 0;
1102
+ return {
1103
+ challenge: base64UrlToBytes(requireString(publicKey, "challenge")),
1104
+ rpId: publicKey.rpId,
1105
+ timeout: publicKey.timeout,
1106
+ userVerification: publicKey.userVerification,
1107
+ allowCredentials
1108
+ };
1109
+ }
1110
+ function attestationToJSON(credential) {
1111
+ const response = credential.response;
1112
+ return {
1113
+ id: credential.id,
1114
+ rawId: bytesToBase64Url(credential.rawId),
1115
+ type: credential.type,
1116
+ response: {
1117
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
1118
+ attestationObject: bytesToBase64Url(response.attestationObject)
1119
+ }
1120
+ };
1121
+ }
1122
+ function assertionToJSON(credential) {
1123
+ const response = credential.response;
1124
+ return {
1125
+ id: credential.id,
1126
+ rawId: bytesToBase64Url(credential.rawId),
1127
+ type: credential.type,
1128
+ response: {
1129
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
1130
+ authenticatorData: bytesToBase64Url(response.authenticatorData),
1131
+ signature: bytesToBase64Url(response.signature),
1132
+ // The user handle is how palauth resolves WHO signed in on a discoverable
1133
+ // login — there is no identifier in the request.
1134
+ userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null
1135
+ }
1136
+ };
1137
+ }
1138
+ function isPasskeySupported() {
1139
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && navigator.credentials !== void 0;
1140
+ }
1141
+ function ensureSupported() {
1142
+ if (!isPasskeySupported()) {
1143
+ throw new Error("This browser cannot use passkeys \u2014 offer a password or e-mail sign-in");
1144
+ }
1145
+ }
1146
+ async function createCredential(publicKey) {
1147
+ const credential = await navigator.credentials.create({
1148
+ publicKey: toCreationOptions(publicKey)
1149
+ });
1150
+ if (credential === null) {
1151
+ throw new Error("the browser returned no credential");
1152
+ }
1153
+ return credential;
1154
+ }
1155
+ async function getCredential(publicKey) {
1156
+ const credential = await navigator.credentials.get({
1157
+ publicKey: toRequestOptions(publicKey)
1158
+ });
1159
+ if (credential === null) {
1160
+ throw new Error("the browser returned no credential");
1161
+ }
1162
+ return credential;
1163
+ }
1164
+ async function passkeySignIn(rt) {
1165
+ ensureSupported();
1166
+ const envelope = await palbeRequest(rt, "POST", "/auth/webauthn/login/begin", {
1167
+ body: {}
1168
+ });
1169
+ const credential = await getCredential(envelope.options.publicKey);
1170
+ const raw = await palbeRequest(rt, "POST", "/auth/webauthn/login/finish", {
1171
+ body: assertionToJSON(credential)
1172
+ });
1173
+ const result = asWireAuthResult(raw);
1174
+ if (result === null) {
1175
+ throw new Error("passkey sign-in returned an unusable session");
1176
+ }
1177
+ return result;
1178
+ }
1179
+ async function passkeySignUp(rt, email, displayName) {
1180
+ ensureSupported();
1181
+ const begun = await palbeRequest(rt, "POST", "/auth/webauthn/signup/begin", {
1182
+ body: { email, user_name: displayName }
1183
+ });
1184
+ const credential = await createCredential(begun.options.publicKey);
1185
+ await palbeRequest(
1186
+ rt,
1187
+ "POST",
1188
+ `/auth/webauthn/signup/finish?user_id=${encodeURIComponent(begun.user_id)}`,
1189
+ { body: attestationToJSON(credential) }
1190
+ );
1191
+ return await passkeySignIn(rt);
1192
+ }
1193
+ async function passkeyRegister(rt, name) {
1194
+ ensureSupported();
1195
+ const envelope = await palbeRequest(
1196
+ rt,
1197
+ "POST",
1198
+ "/auth/webauthn/register/begin",
1199
+ { body: { name } }
1200
+ );
1201
+ const credential = await createCredential(envelope.options.publicKey);
1202
+ await palbeRequest(rt, "POST", "/auth/webauthn/register/finish", {
1203
+ body: attestationToJSON(credential)
1204
+ });
1205
+ }
1206
+
1051
1207
  // src/auth-facade.ts
1052
1208
  function mapWireUser(raw) {
1053
1209
  return {
@@ -1184,6 +1340,23 @@ var PalbeAuth = class {
1184
1340
  const raw = await palbeRequest(this.rt, "GET", "/auth/user");
1185
1341
  return mapWireUser(raw);
1186
1342
  }
1343
+ /**
1344
+ * Which credentials this account actually has.
1345
+ *
1346
+ * An account reached through Google has no password, so showing it a
1347
+ * "current password" field gives the user something they can never fill —
1348
+ * and `getUser()` cannot tell you, because GET /auth/user carries no
1349
+ * credential information. Read this before rendering any re-auth or
1350
+ * change-password form.
1351
+ */
1352
+ async getSignInMethods() {
1353
+ const raw = await palbeRequest(this.rt, "GET", "/auth/me");
1354
+ return {
1355
+ password: raw.has_password,
1356
+ mfa: raw.has_mfa,
1357
+ providers: raw.providers ?? null
1358
+ };
1359
+ }
1187
1360
  async refreshUser() {
1188
1361
  const user = await this.getUser();
1189
1362
  const changed = !usersEqual(user, this.cachedUser);
@@ -1385,6 +1558,39 @@ var PalbeAuth = class {
1385
1558
  return this.adopt(data);
1386
1559
  });
1387
1560
  }
1561
+ /**
1562
+ * Sign in with a passkey stored for this Environment. No identifier is typed:
1563
+ * the browser lists the accounts it holds and the user picks one.
1564
+ *
1565
+ * Throws when the browser cannot run a ceremony — check `passkeysSupported`
1566
+ * first and keep a password or e-mail path beside the button.
1567
+ */
1568
+ async signInWithPasskey() {
1569
+ return this.withSigningIn(async () => this.adoptWire(await passkeySignIn(this.rt)));
1570
+ }
1571
+ /**
1572
+ * Create an account whose only credential is a passkey — the e-mail is the
1573
+ * identifier, the passkey is the credential, and no password ever exists.
1574
+ */
1575
+ async signUpWithPasskey(email, displayName) {
1576
+ return this.withSigningIn(
1577
+ async () => this.adoptWire(await passkeySignUp(this.rt, email, displayName))
1578
+ );
1579
+ }
1580
+ /**
1581
+ * Add a passkey to the signed-in account, so the next sign-in is one tap.
1582
+ *
1583
+ * Rejects with a 403 `reauthentication_required` when the session is not
1584
+ * recent: a passkey outlives a password reset, so enrolling one takes more
1585
+ * than a valid session.
1586
+ */
1587
+ async registerPasskey(name) {
1588
+ await passkeyRegister(this.rt, name);
1589
+ }
1590
+ /** Whether this browser can run a passkey ceremony at all. */
1591
+ get passkeysSupported() {
1592
+ return isPasskeySupported();
1593
+ }
1388
1594
  async signInWithOAuth(params) {
1389
1595
  const { redirect = true, ...opts } = params;
1390
1596
  const { url } = unwrap(await this.rt.authClient.getOAuthURL(opts));
@@ -8633,7 +8839,7 @@ function defaultSessionStorage(key) {
8633
8839
  }
8634
8840
 
8635
8841
  // src/version.ts
8636
- var VERSION = "6.0.0";
8842
+ var VERSION = "7.1.0";
8637
8843
 
8638
8844
  // src/runtime.ts
8639
8845
  function buildRuntime(config) {
@@ -9058,4 +9264,4 @@ export {
9058
9264
  pb,
9059
9265
  createBoundClient
9060
9266
  };
9061
- //# sourceMappingURL=chunk-KOJ4OAAR.js.map
9267
+ //# sourceMappingURL=chunk-P6TWUWZC.js.map