@palbase/web 1.0.0 → 1.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.
@@ -1,454 +0,0 @@
1
- import { HttpClient, TokenManager, Session } from '@palbase/core';
2
- import { AuthClient } from '@palbase/auth';
3
- import { S as SessionStorageAdapter } from './storage-BPaeSG8K.cjs';
4
- import { FlagValue } from '@palbase/flags';
5
-
6
- interface PalbeOAuthConfig {
7
- google?: {
8
- enabled?: boolean;
9
- clientId?: string;
10
- };
11
- apple?: {
12
- enabled?: boolean;
13
- };
14
- }
15
- /** Baked into palbe.gen.ts by `palbase web link` — the generated web config. */
16
- interface PalbeConfig {
17
- url: string;
18
- apiKey: string;
19
- /** Informational — url+apiKey are already branch-specific (endpointRef embeds the branch slug); gen bakes all three consistently. */
20
- branch?: string;
21
- oauth?: PalbeOAuthConfig;
22
- /** Refresh-token persistence. Default: endpoint-scoped localStorage in browsers, memory elsewhere. */
23
- storage?: SessionStorageAdapter;
24
- /** Extra headers on every request. */
25
- headers?: Record<string, string>;
26
- }
27
-
28
- /** Frozen view of all cached flag values (what `all()` returns / `changes()` yields). */
29
- type FlagsView = Readonly<Record<string, FlagValue>>;
30
- /**
31
- * `pb.flags` — iOS-parity facade over `FlagsPool` (cache + delta polling +
32
- * auth binding) and `FlagsClient` (stateless transport).
33
- *
34
- * Start semantics: the facade is constructed lazily (first `pb.flags` touch).
35
- * - Browser: the pool starts right here in the constructor — cold snapshot +
36
- * 30s delta polling + visibility pause. "First use starts the machinery"
37
- * without per-method start checks.
38
- * - Server (no `document`): NEVER auto-starts. `ready()`/`refresh()` are
39
- * one-shot snapshot fetches with zero timers and zero storage access, so a
40
- * pbServer request that reads flags pays exactly one HTTP call.
41
- *
42
- * localStorage persistence is scoped per endpoint ref (`palbe.flags.<ref>`),
43
- * mirroring the session-storage key convention.
44
- */
45
- declare class PalbeFlags {
46
- private readonly transport;
47
- private readonly pool;
48
- constructor(rt: PalbeRuntime);
49
- /** Resolves once the first snapshot (or persisted hydrate) is available. Auto-starts the pool. */
50
- ready(): Promise<void>;
51
- /** Force an immediate re-snapshot. */
52
- refresh(): Promise<void>;
53
- /** Frozen snapshot of all cached values (identity-stable until a change). */
54
- all(): FlagsView;
55
- /** Raw cached value for `key`, or `undefined` when not in the cache. */
56
- get(key: string): FlagValue | undefined;
57
- /** `true` only when the cached value is strictly `true`; `fallback` when the key is absent. */
58
- isEnabled(key: string, fallback?: boolean): boolean;
59
- /** Alias of {@link isEnabled} (iOS parity). */
60
- bool(key: string, fallback?: boolean): boolean;
61
- /** Cached value when it is a string, else `fallback`. */
62
- getString(key: string, fallback: string): string;
63
- /** Cached value when it is an integer number, else `fallback`. */
64
- getInt(key: string, fallback: number): number;
65
- /** Cached value when it is a number (integers included), else `fallback`. */
66
- getDouble(key: string, fallback: number): number;
67
- /**
68
- * Resolve the multivariate variant for `key` — the variant name, or `null`
69
- * when the flag has no variant (or the read fails).
70
- *
71
- * Wire failures (network errors, 404, etc.) resolve to `null`.
72
- * Invalid flag names throw `BackendError('validation', { code: 'invalid_flag_name' })`.
73
- *
74
- * DEVIATION from iOS sync-cache parity: the platform does not propagate
75
- * variant metadata into the user-flags snapshot/delta cache, so this is an
76
- * ASYNC transport read (`GET /v1/flags/{key}/variant`), not a cache lookup.
77
- */
78
- getVariant(key: string): Promise<string | null>;
79
- /** Subscribe to any change in the cached flag set. */
80
- onChange(callback: () => void): Unsubscribe;
81
- /**
82
- * Observe ONE key: fires only when that key's value actually changes
83
- * (per {@link sameFlagValue} — structural compare for objects), with the
84
- * new value (`undefined` = deleted). P5 React-hook substrate.
85
- */
86
- subscribeKey(key: string, callback: (value: FlagValue | undefined) => void): Unsubscribe;
87
- /**
88
- * Async iteration over flag changes: yields the new {@link all} view on
89
- * every pool change notification. The listener is detached when the
90
- * consumer `break`s/`return`s/`throw`s — including while a `next()` is
91
- * still pending (it resolves `{ done: true }` instead of hanging, which a
92
- * plain async-generator `finally` would not guarantee).
93
- */
94
- changes(): AsyncIterableIterator<FlagsView>;
95
- /** Stop polling and detach all listeners (auth + visibility + subscribers). */
96
- destroy(): void;
97
- }
98
-
99
- /**
100
- * The connection state of the shared realtime WebSocket (iOS enum parity), plus
101
- * `'error'` — surfaced when a channel could not be recovered after a
102
- * token-expiry close exhausted its rejoin attempts (the socket itself may still
103
- * be up; the app should stop expecting events on the dead channel).
104
- */
105
- type RealtimeConnectionState = 'idle' | 'connected' | 'reconnecting' | 'error';
106
-
107
- /** An inbound broadcast payload (the inner `payload` of the Phoenix envelope). */
108
- type RealtimePayload = Record<string, unknown>;
109
- type RealtimeHandler = (payload: RealtimePayload) => void;
110
- /** A cancellable realtime subscription. `cancel()` removes the handler and,
111
- * when it was the channel's last, leaves the channel on the shared socket.
112
- * Cancelling twice is a no-op. */
113
- interface RealtimeSubscription {
114
- cancel(): void;
115
- }
116
- interface RealtimeStatusSnapshot {
117
- state: RealtimeConnectionState;
118
- lastEventAt: Date | null;
119
- }
120
- /**
121
- * The observable connection status (`pb.realtime.status`) — the plain-JS
122
- * analogue of the iOS `@Observable RealtimeStatusStore`. `state` drives a
123
- * live "connected" badge; `lastEventAt` proves a push actually arrived.
124
- * `onChange` fires on state transitions and on every recorded event.
125
- */
126
- interface RealtimeStatus {
127
- readonly state: RealtimeConnectionState;
128
- readonly lastEventAt: Date | null;
129
- onChange(callback: (status: RealtimeStatusSnapshot) => void): Unsubscribe;
130
- }
131
- /**
132
- * A subscription handle for one realtime channel (returned by
133
- * `pb.realtime.channel(name)` — the SAME instance per name). A channel is
134
- * joined on the shared socket when its first handler is added (or on the
135
- * first `send`) and left when its last handler cancels.
136
- */
137
- declare class RealtimeChannel {
138
- /** The app-defined channel name (bare sub-topic, no "realtime:" prefix). */
139
- readonly name: string;
140
- private readonly owner;
141
- /** @internal — obtain channels via `pb.realtime.channel(name)`. */
142
- constructor(name: string, owner: PalbeRealtime);
143
- /**
144
- * Subscribe `handler` to `event` on this channel. Fire-and-forget: the
145
- * join rides the shared socket asynchronously. Hold the returned
146
- * subscription and `cancel()` it to stop.
147
- */
148
- on(event: string, handler: RealtimeHandler): RealtimeSubscription;
149
- /**
150
- * Broadcast `payload` to the channel's other subscribers (web addition —
151
- * iOS is receive-only). Joins the channel if it isn't already; queued
152
- * until the join is live on the socket.
153
- */
154
- send(event: string, payload?: RealtimePayload): void;
155
- }
156
- declare class PalbeRealtime {
157
- private readonly rt;
158
- private socket;
159
- private readonly channels;
160
- private readonly statusStore;
161
- private readonly handlers;
162
- private readonly topicRefcount;
163
- constructor(rt: PalbeRuntime);
164
- /**
165
- * Get the handle for an app-defined channel (e.g. "room:42"). The same
166
- * name returns the SAME instance. Throws a guided error on server-side
167
- * hosts (SSR/RSC/Node) — realtime is client-only.
168
- */
169
- channel(name: string): RealtimeChannel;
170
- /**
171
- * Tear down the shared socket and clear all channel state. Called by
172
- * `__configure` and `__reset` when the runtime is replaced, so old sockets
173
- * are not left open and heartbeat timers do not leak.
174
- */
175
- destroy(): void;
176
- /** The observable connection status. Safe to read anywhere (reports `idle` until a socket exists). */
177
- get status(): RealtimeStatus;
178
- /** @internal */
179
- subscribe(topic: string, event: string, handler: RealtimeHandler): RealtimeSubscription;
180
- /** @internal */
181
- send(topic: string, event: string, payload: RealtimePayload): void;
182
- /** Lazily build + start the shared socket on first subscription/send. */
183
- private ensureSocket;
184
- private route;
185
- /**
186
- * A channel died for good (token-expiry rejoin exhausted N attempts). Drop its
187
- * handlers + refcount so the app stops expecting events on it, and flip the
188
- * status observable to `'error'` (the React `useChannel` hook reports this).
189
- * A later `.on()` for the same topic re-joins from scratch (refcount 1).
190
- */
191
- private handleChannelError;
192
- }
193
-
194
- interface PalbeRuntime {
195
- config: PalbeConfig;
196
- http: HttpClient;
197
- tokenManager: TokenManager;
198
- authClient: AuthClient;
199
- auth: PalbeAuth;
200
- flags: PalbeFlags;
201
- realtime: PalbeRealtime;
202
- analytics: PalbeAnalytics;
203
- storage: SessionStorageAdapter;
204
- /**
205
- * Destroy the realtime facade if it was already constructed (no-op otherwise).
206
- * Called by `__configure` / `__reset` when the runtime is replaced so the old
207
- * socket is closed and heartbeat timers do not leak.
208
- */
209
- destroyRealtime(): void;
210
- }
211
- declare function buildRuntime(config: PalbeConfig): PalbeRuntime;
212
-
213
- type MagicLinkResult = ({
214
- status: 'signedIn';
215
- } & AuthSuccess) | {
216
- status: 'mfaRequired';
217
- mfaToken: string;
218
- factors: string[];
219
- };
220
- type OAuthExchangeResult = ({
221
- status: 'signedIn';
222
- } & AuthSuccess) | {
223
- status: 'mfaRequired';
224
- mfaToken: string;
225
- factors: string[];
226
- };
227
- interface AuthUser {
228
- id: string;
229
- /** null for phone-only users (the server omits email). */
230
- email: string | null;
231
- emailVerified: boolean;
232
- createdAt: string;
233
- }
234
- interface AuthSuccess {
235
- user: AuthUser;
236
- session: Session;
237
- }
238
- type AuthState = {
239
- status: 'signedIn';
240
- user: AuthUser;
241
- } | {
242
- status: 'signedOut';
243
- };
244
- type AuthChangeEvent = {
245
- type: 'signedIn';
246
- user: AuthUser;
247
- } | {
248
- type: 'signedOut';
249
- reason: 'userInitiated' | 'sessionExpired';
250
- } | {
251
- type: 'tokenRefreshed';
252
- };
253
- type Unsubscribe = () => void;
254
- declare class PalbeAuth {
255
- private readonly rt;
256
- private cachedUser;
257
- private signingOut;
258
- private signingIn;
259
- private signedInState;
260
- private readonly stateListeners;
261
- private readonly eventListeners;
262
- private readonly userListeners;
263
- constructor(rt: PalbeRuntime);
264
- get currentUser(): AuthUser | null;
265
- get isSignedIn(): boolean;
266
- signUp(params: {
267
- email: string;
268
- password: string;
269
- }): Promise<AuthSuccess>;
270
- signIn(params: {
271
- email: string;
272
- password: string;
273
- }): Promise<AuthSuccess>;
274
- signOut(): Promise<void>;
275
- getUser(): Promise<AuthUser>;
276
- refreshUser(): Promise<AuthUser>;
277
- /**
278
- * Subscribe to signed-in/signed-out state. Fires immediately with the
279
- * current snapshot (iOS parity). NOTE: a restored session (page reload) has
280
- * no cached user yet, so the immediate snapshot reports signedOut even when
281
- * `isSignedIn` is true — call `refreshUser()` on boot to populate the user
282
- * and rely on `isSignedIn` for the session truth.
283
- */
284
- onAuthStateChange(callback: (state: AuthState) => void): Unsubscribe;
285
- onAuthEvent(callback: (event: AuthChangeEvent) => void): Unsubscribe;
286
- /** Subscribe to user-profile changes. Replays the cached user on subscribe (iOS parity). */
287
- onUserChange(callback: (user: AuthUser) => void): Unsubscribe;
288
- private snapshotState;
289
- /**
290
- * Listener exception isolation: a throwing consumer callback must never
291
- * break delivery to other listeners nor propagate into the emit caller
292
- * (tokenManager.clearSession callers, pb.call paths, signOut, ...).
293
- */
294
- private safeInvoke;
295
- private emitState;
296
- private emitEvent;
297
- /** Cache the user + announce sign-in. Session was already set by AuthClient. */
298
- private adopt;
299
- /** Adopt a raw palauth AuthResult wire shape: wire tokens, cache user, announce. */
300
- private adoptWire;
301
- /** Run a sign-in flow under the signingIn flag to suppress spurious tokenRefreshed. */
302
- private withSigningIn;
303
- /**
304
- * Defensively decode a 200 union body (AuthResult | mfa-required) and
305
- * complete the flow. The wire contract promises one of the two shapes, but
306
- * a literal-`null` (or otherwise malformed) JSON body must surface as
307
- * BackendError('decode') — never a raw TypeError from probing a non-object.
308
- * The mfa branch validates its fields: `mfa_token` is required; the factor
309
- * list (named `factors` by magic-link verify, `mfa_factors` by the OAuth
310
- * callback — extraction-verified against palauth) tolerates a missing or
311
- * malformed value as []. The signedIn branch validates the FULL AuthResult
312
- * shape (asWireAuthResult) — an incomplete body falls through to decode,
313
- * never half-adopts.
314
- */
315
- private completeAuthUnion;
316
- private decodeError;
317
- signInWithOTP(params: {
318
- phone: string;
319
- }): Promise<void>;
320
- verifyOTP(params: {
321
- phone: string;
322
- token: string;
323
- }): Promise<AuthSuccess>;
324
- resetPassword(email: string): Promise<void>;
325
- confirmPasswordReset(params: {
326
- token: string;
327
- newPassword: string;
328
- }): Promise<void>;
329
- updatePassword(params: {
330
- currentPassword: string;
331
- newPassword: string;
332
- }): Promise<void>;
333
- verifyEmail(params: {
334
- token: string;
335
- } | {
336
- code: string;
337
- email: string;
338
- }): Promise<void>;
339
- resendVerification(email: string): Promise<void>;
340
- signInWithMagicLink(email: string): Promise<void>;
341
- verifyMagicLink(token: string): Promise<MagicLinkResult>;
342
- signInWithCredential(params: {
343
- provider: string;
344
- credential: string;
345
- }): Promise<AuthSuccess>;
346
- signInWithOAuth(params: {
347
- provider: string;
348
- redirectTo?: string;
349
- /** Set false to skip the browser redirect (popup/manual flows). Default true. */
350
- redirect?: boolean;
351
- }): Promise<{
352
- url: string;
353
- }>;
354
- /**
355
- * Complete the OAuth redirect flow: trade the provider's `code` + `state`
356
- * (from the app's redirect_uri query) for a session. PKCE verifier + state
357
- * live server-side in palauth — the client only relays the two values.
358
- */
359
- exchangeCodeForSession(params: {
360
- provider: string;
361
- code: string;
362
- state: string;
363
- }): Promise<OAuthExchangeResult>;
364
- /** Config-as-code gate for the zero-arg provider sugar. */
365
- private requireProvider;
366
- signInWithGoogle(opts?: {
367
- redirectTo?: string;
368
- redirect?: boolean;
369
- }): Promise<{
370
- url: string;
371
- }>;
372
- signInWithApple(opts?: {
373
- redirectTo?: string;
374
- redirect?: boolean;
375
- }): Promise<{
376
- url: string;
377
- }>;
378
- }
379
-
380
- /** Free-form event property bag (JSON object on the wire). */
381
- type AnalyticsProperties = Record<string, unknown>;
382
- /**
383
- * Runtime-scoped analytics identity — constructed EAGERLY in `buildRuntime`
384
- * (cheap: listener wiring only, no I/O) because the X-Distinct-Id header
385
- * interceptor must stamp every request from the first one, even when
386
- * `pb.analytics` is never touched (iOS: the resolver is wired at
387
- * PalBackend init). The facade wraps this state lazily.
388
- *
389
- * Auth events: while no facade exists, the state maintains identity only
390
- * (user id for the header, anon rotation on user-initiated sign-out). Once
391
- * the facade attaches, it takes over wholesale and adds the ingest
392
- * side-effects (flush + identify / flush + reset).
393
- */
394
- declare class AnalyticsState {
395
- identifiedUserId: string | null;
396
- /** Set by PalbeAnalytics on construction — delegates auth events to it. */
397
- facadeAuthHandler: ((event: AuthChangeEvent) => void) | null;
398
- private anonId;
399
- private readonly store;
400
- private readonly idKey;
401
- private readonly optOutKey;
402
- constructor(rt: PalbeRuntime);
403
- /** User id when identified, else the stable anon id. NEVER empty —
404
- * this is what `identify()` links, so stamping it on every request lets
405
- * the trace pipeline stitch pre-login activity to the user. */
406
- distinctId(): string;
407
- anonymousId(): string;
408
- rotateAnonymousId(): string;
409
- isOptedOut(): boolean;
410
- setOptOut(on: boolean): void;
411
- }
412
- declare class PalbeAnalytics {
413
- private readonly rt;
414
- private readonly state;
415
- private readonly buffer;
416
- private flushTimer;
417
- /** Browser → buffer + size/timer flush. Server (no document) → immediate
418
- * per-call POST, zero timers (nothing leaks into RSC/route handlers). */
419
- private readonly browser;
420
- constructor(rt: PalbeRuntime, state: AnalyticsState);
421
- /** Record an event. Invalid names are dropped (one warn per process). */
422
- capture(event: string, properties?: AnalyticsProperties): void;
423
- /** Record a screen view — server-canonical `$screen` + `screen_name`.
424
- * Validates non-empty only (the live wire at /v1/analytics/screen requires
425
- * non-empty screen_name; the event-name regex does NOT apply here). */
426
- screen(name: string, properties?: AnalyticsProperties): void;
427
- /** Link the current anon id to `userId` and adopt it as the distinct id.
428
- * Immediate POST (not buffered). Re-identifying a DIFFERENT user
429
- * auto-resets first (iOS K10) so one anon id never links two users. */
430
- identify(userId: string, traits?: AnalyticsProperties): void;
431
- /** Merge distinct id `from` into `to` (identity stitching). Immediate POST. */
432
- alias(from: string, to: string): void;
433
- /** Drop the buffer, clear the identified user and rotate the anon id
434
- * (sign-out hygiene). Callers wanting pending events delivered flush first
435
- * — the auth binding does. */
436
- reset(): void;
437
- /** Persisted GDPR opt-out. Opting out drops anything pending so nothing
438
- * already-buffered leaks after the user said stop. */
439
- setOptOut(on: boolean): void;
440
- /** Drain the buffer to /v1/analytics/batch (≤100 per request, sequential).
441
- * Resolves when delivery finished; NEVER rejects — failed slices are
442
- * dropped with one warn per flush. 207 partial-reject is also warned once
443
- * (fire-and-forget: no retry for already-delivered batches). */
444
- flush(): Promise<void>;
445
- private ingest;
446
- /** iOS PalBackend auth-binding parity: signedIn → flush + identify;
447
- * userInitiated signedOut → flush + reset; sessionExpired → flush only. */
448
- private handleAuthEvent;
449
- private startTimer;
450
- private cancelTimer;
451
- private post;
452
- }
453
-
454
- export { type AnalyticsProperties as A, type FlagsView as F, type MagicLinkResult as M, type OAuthExchangeResult as O, PalbeAnalytics as P, RealtimeChannel as R, type Unsubscribe as U, type AuthChangeEvent as a, type AuthState as b, type AuthSuccess as c, type AuthUser as d, PalbeAuth as e, type PalbeConfig as f, PalbeFlags as g, type PalbeOAuthConfig as h, PalbeRealtime as i, type RealtimeConnectionState as j, type RealtimeHandler as k, type RealtimePayload as l, type RealtimeStatus as m, type RealtimeStatusSnapshot as n, type RealtimeSubscription as o, type PalbeRuntime as p, buildRuntime as q };