@layers/amba-web 4.0.2 → 4.0.4

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.
@@ -19,6 +19,16 @@ interface AmbaConfig {
19
19
  * Defaults to `"https://api.amba.dev"`.
20
20
  */
21
21
  apiUrl?: string;
22
+ /**
23
+ * Base URL for `Amba.functions.invoke()`. Set this to call your deployed
24
+ * functions through a custom domain or a non-prod environment, e.g.
25
+ * `"https://api.example.com"` or `"https://<id>.staging.fn.amba.host"`.
26
+ * `invoke(name, body)` POSTs to `"<functionsBaseUrl>/<name>"`.
27
+ *
28
+ * When omitted, the SDK resolves the default host automatically on first
29
+ * use (one cached round-trip) — no configuration needed for production.
30
+ */
31
+ functionsBaseUrl?: string;
22
32
  /**
23
33
  * If `true`, blocks all telemetry until `setConsent(granted)` is called.
24
34
  * Required for SOC2 / GDPR strict modes.
@@ -267,17 +277,23 @@ interface ChallengeProgress {
267
277
  claimed_at?: string | null;
268
278
  }
269
279
  interface CurrencyBalance {
270
- currency_id: string;
271
- key: string;
280
+ currency_code: string;
281
+ name: string;
282
+ is_premium: boolean;
272
283
  balance: number;
273
- updated_at: string;
284
+ lifetime_earned: number;
285
+ lifetime_spent: number;
274
286
  }
275
287
  interface CurrencyTransaction {
276
288
  id: string;
277
- currency_id: string;
278
- delta: number;
289
+ app_user_id?: string;
290
+ currency_code: string;
291
+ amount: number;
279
292
  balance_after: number;
280
- reason?: string | null;
293
+ transaction_type?: string;
294
+ source?: string;
295
+ reference_id?: string | null;
296
+ idempotency_key?: string | null;
281
297
  created_at: string;
282
298
  }
283
299
  interface InventoryItem {
@@ -505,6 +521,18 @@ interface MarkReadResult {
505
521
  app_user_id?: string | null;
506
522
  last_read_at?: string | null;
507
523
  }
524
+ /**
525
+ * A `conversation_participants` row — returned by
526
+ * `Amba.messaging.addParticipant`. All fields are optional so older
527
+ * servers that trim columns still decode.
528
+ */
529
+ interface ConversationParticipant {
530
+ id?: string | null;
531
+ conversation_id?: string | null;
532
+ app_user_id?: string | null;
533
+ joined_at?: string | null;
534
+ last_read_at?: string | null;
535
+ }
508
536
  interface Report {
509
537
  id: string;
510
538
  reporter_id: string;
@@ -676,6 +704,8 @@ interface LeagueCohortMember {
676
704
  display_name?: string | null;
677
705
  score: number;
678
706
  rank: number;
707
+ /** `true` for the requesting user's own row (server-derived; no id leak). */
708
+ is_me?: boolean;
679
709
  }
680
710
  interface LeagueMembership {
681
711
  cohort: LeagueCohort | null;
@@ -716,6 +746,104 @@ interface NearestResponse<T> {
716
746
  data: T[];
717
747
  }
718
748
  //#endregion
749
+ //#region src/realtime.d.ts
750
+ /**
751
+ * Live collection subscriptions for `@layers/amba-web` (PROTOTYPE — T13).
752
+ *
753
+ * Public surface is intentionally tiny and provider-agnostic:
754
+ *
755
+ * const unsub = Amba.collection("messages")
756
+ * .where("circle_id", circleId)
757
+ * .subscribe((change) => {
758
+ * // change.op is "INSERT" | "UPDATE" | "DELETE"
759
+ * // change.row is the affected row
760
+ * });
761
+ * unsub(); // stop listening
762
+ *
763
+ * The transport (a streamed HTTP response) and the server-side fan-out
764
+ * mechanism are deliberately NOT named here — customers only ever see
765
+ * `.subscribe()`. Internally this opens a long-lived streamed GET to the
766
+ * realtime endpoint and reconnects with backoff if the connection drops.
767
+ *
768
+ * Delivery mirrors `Amba.collection.find()` exactly: you receive only rows
769
+ * your session owns, optionally narrowed by `.where(...)` equality filters.
770
+ */
771
+ type RowChangeOp = "INSERT" | "UPDATE" | "DELETE";
772
+ interface RealtimeChange<T = unknown> {
773
+ /** Which kind of write happened. */
774
+ op: RowChangeOp;
775
+ /**
776
+ * The affected row. For deletes this is a partial row (at minimum `id`).
777
+ * If the row was too large to deliver inline, `truncated` is `true` and
778
+ * `row` carries just `id` — re-fetch with `Amba.collection.find()` /
779
+ * `findOne()` when you need the full body.
780
+ */
781
+ row: T;
782
+ truncated?: boolean;
783
+ }
784
+ interface SubscribeOptions {
785
+ /** Called once when the live connection is (re)established. */
786
+ onOpen?: () => void;
787
+ /** Called on a connection error before the SDK retries. */
788
+ onError?: (err: unknown) => void;
789
+ }
790
+ /** Unsubscribe handle returned by `.subscribe(...)`. Call it to stop. */
791
+ type Unsubscribe = () => void;
792
+ /**
793
+ * Resolved at call time so the latest session token is used. Returns the
794
+ * auth headers, or `null` when there's no live session (subscribe throws).
795
+ */
796
+ interface RealtimeContext {
797
+ baseUrl: string;
798
+ authHeaders: () => Record<string, string> | null;
799
+ }
800
+ /**
801
+ * Builder returned by `Amba.collection(name)`. Chain `.where(...)` to
802
+ * narrow, then `.subscribe(cb)` to start streaming.
803
+ */
804
+ declare class CollectionSubscription<T = unknown> {
805
+ private readonly ctx;
806
+ private readonly name;
807
+ private readonly filters;
808
+ constructor(ctx: RealtimeContext, name: string);
809
+ /** Add an equality filter. Chainable. All filters must match (AND). */
810
+ where(column: string, value: unknown): this;
811
+ /**
812
+ * Start listening. `cb` fires once per matching row change. Returns an
813
+ * unsubscribe handle — call it to close the connection.
814
+ */
815
+ subscribe(cb: (change: RealtimeChange<T>) => void, options?: SubscribeOptions): Unsubscribe;
816
+ }
817
+ /**
818
+ * A live message change in a conversation. `op` is "INSERT" for a new
819
+ * message; moderation removals arrive as "DELETE". For a delete (or when a
820
+ * message was too large to deliver inline) `row` may be partial — re-fetch
821
+ * with `Amba.messaging.listMessages(conversationId)` when you need the full
822
+ * body.
823
+ */
824
+ type MessageChange<T = unknown> = RealtimeChange<T>;
825
+ /**
826
+ * Builder returned by `Amba.messaging.conversation(id)`. Call `.subscribe(cb)`
827
+ * to receive new messages live instead of polling.
828
+ *
829
+ * const unsub = Amba.messaging
830
+ * .conversation(conversationId)
831
+ * .subscribe((change) => {
832
+ * // change.op === "INSERT"; change.row is the new message
833
+ * });
834
+ * unsub(); // stop listening
835
+ *
836
+ * Delivery is scoped to conversation participants — you receive messages for
837
+ * a conversation only if your session is a member of it. Reconnects with the
838
+ * same exponential backoff as collection subscriptions.
839
+ */
840
+ declare class ConversationSubscription<T = unknown> {
841
+ private readonly ctx;
842
+ private readonly conversationId;
843
+ constructor(ctx: RealtimeContext, conversationId: string);
844
+ subscribe(cb: (change: MessageChange<T>) => void, options?: SubscribeOptions): Unsubscribe;
845
+ }
846
+ //#endregion
719
847
  //#region src/error.d.ts
720
848
  /**
721
849
  * Typed error class for the amba SDK + helper that converts arbitrary
@@ -742,6 +870,13 @@ declare class AmbaApiError extends Error {
742
870
  }
743
871
  //#endregion
744
872
  //#region src/index.d.ts
873
+ /** Options for `Amba.functions.invoke`. */
874
+ interface InvokeOptions {
875
+ /** HTTP method. Defaults to `"POST"`. */
876
+ method?: "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
877
+ /** Extra headers merged on top of the injected auth headers. */
878
+ headers?: Record<string, string>;
879
+ }
745
880
  /**
746
881
  * Top-level SDK namespace.
747
882
  *
@@ -899,6 +1034,32 @@ declare const Amba: {
899
1034
  * code is `EMAIL_ALREADY_LINKED`.
900
1035
  */
901
1036
  linkEmailOtp: (email: string, code: string) => Promise<User>;
1037
+ /**
1038
+ * Upgrade an anonymous (or any current) session by attaching an
1039
+ * email + password credential — converting the current user in place
1040
+ * to an email-identified account so its data (streak, library,
1041
+ * progress, `anonymous_id`) is preserved rather than minting a new
1042
+ * user.
1043
+ *
1044
+ * Guarantees (symmetric to `linkEmailOtp`):
1045
+ *
1046
+ * - **Same `user_id`** before and after.
1047
+ * - **Session token unchanged.** No rotation; `onAuthStateChange`
1048
+ * does NOT fire.
1049
+ * - **Idempotent.** Re-linking the same email to the same user
1050
+ * succeeds.
1051
+ * - **Collision.** If the email is already owned by a different user,
1052
+ * throws an error with `code = "EMAIL_ALREADY_LINKED"` and
1053
+ * `details.conflicting_user_id` populated.
1054
+ *
1055
+ * After linking, the user can sign in with `signInWithEmail(email,
1056
+ * password)`. Requires an active session — throws `UNAUTHORIZED`
1057
+ * otherwise.
1058
+ *
1059
+ * @returns the updated user record (with `email` set and
1060
+ * `email_verified = true`).
1061
+ */
1062
+ linkEmailPassword: (email: string, password: string) => Promise<User>;
902
1063
  /**
903
1064
  * Detach the SMS-OTP identifier from the current session's user.
904
1065
  * Refused with `LAST_IDENTIFIER` if this would leave the account
@@ -983,6 +1144,23 @@ declare const Amba: {
983
1144
  */
984
1145
  revoke: (sessionId: string) => Promise<void>;
985
1146
  };
1147
+ /**
1148
+ * Live collection subscriptions (SDK 4.0). Returns a builder — chain
1149
+ * `.where(column, value)` to narrow, then `.subscribe(cb)` to start
1150
+ * receiving row changes. `subscribe` returns an unsubscribe handle.
1151
+ *
1152
+ * @example
1153
+ * const unsub = Amba.collection("messages")
1154
+ * .where("circle_id", circleId)
1155
+ * .subscribe((change) => {
1156
+ * // change.op: "INSERT" | "UPDATE" | "DELETE"; change.row: the row
1157
+ * });
1158
+ * // later: unsub();
1159
+ *
1160
+ * Delivery mirrors `Amba.collections.find()` — you only receive rows your
1161
+ * session owns. Requires an authenticated session.
1162
+ */
1163
+ collection: <T = unknown>(name: string) => CollectionSubscription<T>;
986
1164
  /** Typed collection CRUD with auto-RLS. */
987
1165
  collections: {
988
1166
  find: <T = unknown>(name: string, options?: FindOptions) => Promise<FindResponse<T>>;
@@ -1115,6 +1293,31 @@ declare const Amba: {
1115
1293
  };
1116
1294
  };
1117
1295
  };
1296
+ /**
1297
+ * Invoke a deployed function by name, with the project's client key (and
1298
+ * the live session token, if any) injected automatically.
1299
+ *
1300
+ * This is the authenticated way to call your functions — prefer it over
1301
+ * hand-rolling `fetch` against a function URL, which leaves the call
1302
+ * unauthenticated (a footgun when the function holds secrets).
1303
+ *
1304
+ * @example
1305
+ * const result = await Amba.functions.invoke<{ ok: boolean }>(
1306
+ * 'send_welcome_email',
1307
+ * { to: 'user@example.com' },
1308
+ * );
1309
+ *
1310
+ * @param name The deployed function name.
1311
+ * @param body JSON-serializable request body (omitted for GET).
1312
+ * @param options Method + extra headers.
1313
+ * @returns The parsed JSON response (or the raw text if not JSON).
1314
+ * @throws AmbaApiError on a network failure or a non-2xx response. When
1315
+ * the function / router returns the standard `{ error: { code, message } }`
1316
+ * envelope, those surface as the thrown error's `code` + `message`.
1317
+ */
1318
+ functions: {
1319
+ invoke: <T = unknown>(name: string, body?: unknown, options?: InvokeOptions) => Promise<T>;
1320
+ };
1118
1321
  /** Remote config bundle. */
1119
1322
  config: {
1120
1323
  fetch: () => Promise<ConfigBundle>;
@@ -1315,12 +1518,44 @@ declare const Amba: {
1315
1518
  * native SDKs were already trying to call.)
1316
1519
  */
1317
1520
  getMessage: (conversationId: string, messageId: string) => Promise<Message | null>;
1521
+ /**
1522
+ * `POST /v1/client/messaging/conversations/:conversationId/participants`
1523
+ * — add a participant to an existing conversation. The caller must
1524
+ * already be a participant (the server returns `403
1525
+ * NOT_A_PARTICIPANT` otherwise) — this is the peer-invite path.
1526
+ * Idempotent: re-adding an existing member returns the existing
1527
+ * participant row, not an error. Enables dynamic membership growth
1528
+ * (e.g. a circle whose members qualify over time). Server-side
1529
+ * membership management (admin/matchmaker) uses the admin
1530
+ * add-participant endpoint instead.
1531
+ */
1532
+ addParticipant: (conversationId: string, userId: string) => Promise<ConversationParticipant>;
1318
1533
  /**
1319
1534
  * `POST /v1/client/messaging/conversations/:conversationId/read`
1320
1535
  * — bump the caller's `last_read_at` for the conversation. Returns
1321
1536
  * the updated participant row.
1322
1537
  */
1323
1538
  markRead: (conversationId: string) => Promise<MarkReadResult>;
1539
+ /**
1540
+ * Live message subscriptions (SDK 4.0). Returns a builder — call
1541
+ * `.subscribe(cb)` to receive new messages in a conversation live
1542
+ * instead of polling `listMessages`. `subscribe` returns an
1543
+ * unsubscribe handle.
1544
+ *
1545
+ * @example
1546
+ * const unsub = Amba.messaging
1547
+ * .conversation(conversationId)
1548
+ * .subscribe((change) => {
1549
+ * // change.op === "INSERT"; change.row is the new message
1550
+ * appendMessageToThread(change.row);
1551
+ * });
1552
+ * // later: unsub();
1553
+ *
1554
+ * Delivery is scoped to conversation participants — you receive a
1555
+ * conversation's messages only if your session is a member of it.
1556
+ * Requires an authenticated session.
1557
+ */
1558
+ conversation: <T = Message>(conversationId: string) => ConversationSubscription<T>;
1324
1559
  };
1325
1560
  /** Report users / content for moderation. */
1326
1561
  moderation: {
@@ -1400,7 +1635,7 @@ declare function getCoreWasm(): AmbaCoreWasm;
1400
1635
  * `verify-sdk-version-coherence` GH action gates every publish on the
1401
1636
  * pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
1402
1637
  */
1403
- declare const SDK_VERSION = "4.0.2";
1638
+ declare const SDK_VERSION = "4.0.4";
1404
1639
  //#endregion
1405
- export { Achievement, AchievementProgress, AiMessage, AiMessageRequest, AiMessageResponse, AiUsage, Amba, AmbaApiError, type AmbaApiErrorCode, AmbaConfig, AndFilter, AppSession, AuthResult, CatalogItem, Challenge, ChallengeProgress, ConditionFilter, ConfigBundle, ConsumeRequest, ContentItem, Conversation, ConversationType, CreateConversationRequest, CurrencyBalance, CurrencyTransaction, DeepLink, DeepLinkCreate, DiagnosticsEnvironment, EntitlementSource, FeedItem, FeedResponse, Filter, FilterValue, FindOptions, FindResponse, FlagAssignment, Friendship, FriendshipState, Group, GroupCreate, GroupMember, GroupUpdate, InventoryItem, Leaderboard, LeaderboardEntry, LeagueCohort, LeagueCohortMember, LeagueCohortView, LeagueMembership, LeagueTier, LinkedIdentifier, ListMessagesOptions, MarkReadResult, MediaAsset, MediaListResponse, Message, MessageType, NearestOptions, NearestResponse, NotFilter, OnboardingStatus, OrFilter, OrderBy, PingResult, PresignData, PullChangesCursor, PullChangesResult, PurchaseOption, PurchaseRequest, PurchaseResult, PushChangesResult, PushPlatform, PushToken, ReferralClaim, ReferralCode, Report, ReportRequest, Review, ReviewCreate, ReviewUpdate, Role, SDK_VERSION, SendMessageRequest, Session, SocialProvider, Store, Streak, SyncChange, SyncConflict, User, UserEntitlement, XpBalance, XpTransaction, getCoreWasm };
1640
+ export { Achievement, AchievementProgress, AiMessage, AiMessageRequest, AiMessageResponse, AiUsage, Amba, AmbaApiError, type AmbaApiErrorCode, AmbaConfig, AndFilter, AppSession, AuthResult, CatalogItem, Challenge, ChallengeProgress, CollectionSubscription, ConditionFilter, ConfigBundle, ConsumeRequest, ContentItem, Conversation, ConversationParticipant, ConversationSubscription, ConversationType, CreateConversationRequest, CurrencyBalance, CurrencyTransaction, DeepLink, DeepLinkCreate, DiagnosticsEnvironment, EntitlementSource, FeedItem, FeedResponse, Filter, FilterValue, FindOptions, FindResponse, FlagAssignment, Friendship, FriendshipState, Group, GroupCreate, GroupMember, GroupUpdate, InventoryItem, InvokeOptions, Leaderboard, LeaderboardEntry, LeagueCohort, LeagueCohortMember, LeagueCohortView, LeagueMembership, LeagueTier, LinkedIdentifier, ListMessagesOptions, MarkReadResult, MediaAsset, MediaListResponse, Message, type MessageChange, MessageType, NearestOptions, NearestResponse, NotFilter, OnboardingStatus, OrFilter, OrderBy, PingResult, PresignData, PullChangesCursor, PullChangesResult, PurchaseOption, PurchaseRequest, PurchaseResult, PushChangesResult, PushPlatform, PushToken, type RealtimeChange, ReferralClaim, ReferralCode, Report, ReportRequest, Review, ReviewCreate, ReviewUpdate, Role, type RowChangeOp, SDK_VERSION, SendMessageRequest, Session, SocialProvider, Store, Streak, type SubscribeOptions, SyncChange, SyncConflict, type Unsubscribe, User, UserEntitlement, XpBalance, XpTransaction, getCoreWasm };
1406
1641
  //# sourceMappingURL=index.d.ts.map
package/dist-rn/index.js CHANGED
@@ -99,8 +99,173 @@ function toAmbaApiError(err) {
99
99
  return new AmbaApiError("UNKNOWN_ERROR", String(err));
100
100
  }
101
101
 
102
+ //#endregion
103
+ //#region src/realtime.ts
104
+ const INITIAL_BACKOFF_MS = 500;
105
+ const MAX_BACKOFF_MS = 15e3;
106
+ /**
107
+ * Builder returned by `Amba.collection(name)`. Chain `.where(...)` to
108
+ * narrow, then `.subscribe(cb)` to start streaming.
109
+ */
110
+ var CollectionSubscription = class {
111
+ filters = {};
112
+ constructor(ctx, name) {
113
+ this.ctx = ctx;
114
+ this.name = name;
115
+ }
116
+ /** Add an equality filter. Chainable. All filters must match (AND). */
117
+ where(column, value) {
118
+ this.filters[column] = value;
119
+ return this;
120
+ }
121
+ /**
122
+ * Start listening. `cb` fires once per matching row change. Returns an
123
+ * unsubscribe handle — call it to close the connection.
124
+ */
125
+ subscribe(cb, options = {}) {
126
+ if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
127
+ const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
128
+ return openStream({
129
+ url: `${this.ctx.baseUrl}/v1/client/realtime/collections/${encodeURIComponent(this.name)}${qs}`,
130
+ authHeaders: this.ctx.authHeaders,
131
+ onChange: cb,
132
+ options
133
+ });
134
+ }
135
+ };
136
+ /**
137
+ * Open a streamed GET to a realtime endpoint and dispatch row-change events
138
+ * to `onChange`. Reconnects with exponential backoff until the returned
139
+ * unsubscribe handle is invoked. Shared by both collection and conversation
140
+ * subscriptions — the only thing that differs between them is the URL.
141
+ *
142
+ * Uses `fetch` + a streamed response body (rather than the browser's
143
+ * native `EventSource`) because `EventSource` cannot set request headers —
144
+ * which would force the session token into the URL query string, where it
145
+ * would leak into access logs. Streaming `fetch` keeps auth in headers.
146
+ */
147
+ function openStream(args) {
148
+ let stopped = false;
149
+ let controller = null;
150
+ let backoff = INITIAL_BACKOFF_MS;
151
+ const run = async () => {
152
+ while (!stopped) {
153
+ controller = new AbortController();
154
+ try {
155
+ const headers = args.authHeaders();
156
+ if (!headers) throw new Error("no active session");
157
+ const resp = await fetch(args.url, {
158
+ method: "GET",
159
+ headers: {
160
+ ...headers,
161
+ Accept: "text/event-stream"
162
+ },
163
+ signal: controller.signal
164
+ });
165
+ if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
166
+ await consume(resp.body, args, () => {
167
+ backoff = INITIAL_BACKOFF_MS;
168
+ args.options.onOpen?.();
169
+ });
170
+ } catch (err) {
171
+ if (stopped) break;
172
+ args.options.onError?.(err);
173
+ }
174
+ if (stopped) break;
175
+ await delay(backoff);
176
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
177
+ }
178
+ };
179
+ run();
180
+ return () => {
181
+ stopped = true;
182
+ controller?.abort();
183
+ };
184
+ }
185
+ async function consume(body, args, onFirstFrame) {
186
+ const reader = body.getReader();
187
+ const decoder = new TextDecoder();
188
+ let buffer = "";
189
+ let sawFrame = false;
190
+ for (;;) {
191
+ const { done, value } = await reader.read();
192
+ if (done) return;
193
+ buffer += decoder.decode(value, { stream: true });
194
+ let idx;
195
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
196
+ const frame = buffer.slice(0, idx);
197
+ buffer = buffer.slice(idx + 2);
198
+ if (!sawFrame) {
199
+ sawFrame = true;
200
+ onFirstFrame();
201
+ }
202
+ dispatchFrame(frame, args.onChange);
203
+ }
204
+ }
205
+ }
206
+ function dispatchFrame(frame, onChange) {
207
+ let event = "";
208
+ const dataLines = [];
209
+ for (const line of frame.split("\n")) {
210
+ if (line.startsWith(":")) continue;
211
+ if (line.startsWith("event:")) event = line.slice(6).trim();
212
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
213
+ }
214
+ if (event !== "change") return;
215
+ try {
216
+ const payload = JSON.parse(dataLines.join("\n"));
217
+ onChange({
218
+ op: payload.op,
219
+ row: payload.row,
220
+ truncated: payload.truncated
221
+ });
222
+ } catch {}
223
+ }
224
+ function delay(ms) {
225
+ return new Promise((resolve) => setTimeout(resolve, ms));
226
+ }
227
+ /**
228
+ * Builder returned by `Amba.messaging.conversation(id)`. Call `.subscribe(cb)`
229
+ * to receive new messages live instead of polling.
230
+ *
231
+ * const unsub = Amba.messaging
232
+ * .conversation(conversationId)
233
+ * .subscribe((change) => {
234
+ * // change.op === "INSERT"; change.row is the new message
235
+ * });
236
+ * unsub(); // stop listening
237
+ *
238
+ * Delivery is scoped to conversation participants — you receive messages for
239
+ * a conversation only if your session is a member of it. Reconnects with the
240
+ * same exponential backoff as collection subscriptions.
241
+ */
242
+ var ConversationSubscription = class {
243
+ constructor(ctx, conversationId) {
244
+ this.ctx = ctx;
245
+ this.conversationId = conversationId;
246
+ }
247
+ subscribe(cb, options = {}) {
248
+ if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
249
+ return openStream({
250
+ url: `${this.ctx.baseUrl}/v1/client/realtime/conversations/${encodeURIComponent(this.conversationId)}`,
251
+ authHeaders: this.ctx.authHeaders,
252
+ onChange: cb,
253
+ options
254
+ });
255
+ }
256
+ };
257
+
102
258
  //#endregion
103
259
  //#region src/index.ts
260
+ const DEFAULT_API_URL = "https://api.amba.dev";
261
+ /**
262
+ * Default host that serves deployed functions. `Amba.functions.invoke(name)`
263
+ * targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
264
+ * `functionsBaseUrl` override is configured. The `<projectId>` label is the
265
+ * server-resolved id (from `diagnostics.ping`), which the function router
266
+ * accepts directly.
267
+ */
268
+ const DEFAULT_FUNCTIONS_HOST = "fn.amba.host";
104
269
  /**
105
270
  * Internal state — one instance per page. Created lazily by `Amba.configure()`.
106
271
  */
@@ -111,6 +276,26 @@ let coreInstance = null;
111
276
  * core finishes initializing.
112
277
  */
113
278
  let _configuredProjectId = null;
279
+ /**
280
+ * Realtime (`Amba.collection(name).subscribe`) needs the API base URL,
281
+ * client key, and current session token in the TS layer to open the
282
+ * streamed connection — the engine doesn't expose them back. We capture
283
+ * the base URL + client key at configure-time and the session token from
284
+ * every auth result (the engine rotates it internally on refresh; the
285
+ * subscribe connection picks up the latest at connect time).
286
+ */
287
+ let _configuredApiUrl = DEFAULT_API_URL;
288
+ let _configuredClientKey = null;
289
+ let _currentSessionToken = null;
290
+ /**
291
+ * Functions invocation state. `_configuredFunctionsBaseUrl` is the optional
292
+ * override from `Amba.configure({ functionsBaseUrl })`. When it's null, the
293
+ * default-host path resolves the project id once via `diagnostics.ping` and
294
+ * caches it in `_resolvedFunctionsProjectId` so subsequent invokes skip the
295
+ * round-trip. Both reset on every `configure()`.
296
+ */
297
+ let _configuredFunctionsBaseUrl = null;
298
+ let _resolvedFunctionsProjectId = null;
114
299
  function requireCore() {
115
300
  if (!coreInstance) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
116
301
  return coreInstance;
@@ -148,9 +333,40 @@ function notifyAuthSubscribers(session) {
148
333
  }
149
334
  }
150
335
  async function notifyFromAuthResult(result) {
336
+ if (result?.session_token) _currentSessionToken = result.session_token;
151
337
  notifyAuthSubscribers(snapshotSession(result.user));
152
338
  }
153
339
  /**
340
+ * Build the auth headers for a realtime subscribe connection, or `null`
341
+ * when there's no live session. Read lazily at connect-time so reconnects
342
+ * pick up a rotated session token.
343
+ */
344
+ function realtimeAuthHeaders() {
345
+ if (!_configuredClientKey || !_currentSessionToken) return null;
346
+ return {
347
+ "X-Api-Key": _configuredClientKey,
348
+ Authorization: `Bearer ${_currentSessionToken}`
349
+ };
350
+ }
351
+ /**
352
+ * Resolve the full invocation URL for a deployed function.
353
+ *
354
+ * With `functionsBaseUrl` configured we POST to `<base>/<name>` verbatim
355
+ * (custom domains / non-prod). Otherwise we resolve the server-side project
356
+ * id once (via `diagnostics.ping`, cached) and target
357
+ * `https://<projectId>.fn.amba.host/<name>` — the function router accepts the
358
+ * project id as the host label directly.
359
+ */
360
+ async function resolveFunctionUrl(name) {
361
+ if (_configuredFunctionsBaseUrl) return `${_configuredFunctionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
362
+ if (!_resolvedFunctionsProjectId) {
363
+ const ping = await Amba.diagnostics.ping();
364
+ if (!ping.ok || !ping.server_project_id) throw new AmbaApiError("FUNCTIONS_HOST_UNRESOLVED", "Could not resolve the functions host for this project. Pass `functionsBaseUrl` to Amba.configure() to invoke functions explicitly.", ping);
365
+ _resolvedFunctionsProjectId = ping.server_project_id;
366
+ }
367
+ return `https://${_resolvedFunctionsProjectId}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
368
+ }
369
+ /**
154
370
  * Top-level SDK namespace.
155
371
  *
156
372
  * Stateless from the JS side — all state lives in the SDK runtime. Calling
@@ -176,6 +392,10 @@ const Amba = {
176
392
  throw toAmbaApiError(err);
177
393
  }
178
394
  _configuredProjectId = config.projectId;
395
+ _configuredApiUrl = config.apiUrl ?? DEFAULT_API_URL;
396
+ _configuredClientKey = config.clientKey;
397
+ _configuredFunctionsBaseUrl = config.functionsBaseUrl ?? null;
398
+ _resolvedFunctionsProjectId = null;
179
399
  },
180
400
  get projectId() {
181
401
  if (!_configuredProjectId) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
@@ -257,6 +477,9 @@ const Amba = {
257
477
  linkEmailOtp: async (email, code) => {
258
478
  return await coreCall((c) => c.linkEmailOtp(email, code));
259
479
  },
480
+ linkEmailPassword: async (email, password) => {
481
+ return await coreCall((c) => c.linkEmailPassword(email, password));
482
+ },
260
483
  unlinkSmsOtp: async () => {
261
484
  return await coreCall((c) => c.unlinkSmsOtp());
262
485
  },
@@ -274,6 +497,7 @@ const Amba = {
274
497
  },
275
498
  signOut: async (rotateAnonymousId = false) => {
276
499
  await coreCall((c) => c.signOut(rotateAnonymousId));
500
+ _currentSessionToken = null;
277
501
  notifyAuthSubscribers(null);
278
502
  },
279
503
  refresh: async () => {
@@ -317,6 +541,10 @@ const Amba = {
317
541
  await coreCall((c) => c.sessionsRevoke(sessionId));
318
542
  }
319
543
  },
544
+ collection: (name) => new CollectionSubscription({
545
+ baseUrl: _configuredApiUrl,
546
+ authHeaders: realtimeAuthHeaders
547
+ }, name),
320
548
  collections: {
321
549
  find: async (name, options = {}) => {
322
550
  return await coreCall((c) => c.collectionsFind(name, encodeJson(options)));
@@ -474,6 +702,45 @@ const Amba = {
474
702
  ai: { anthropic: { messages: { create: async (request) => {
475
703
  return await coreCall((c) => c.aiAnthropicMessages(encodeJson(request)));
476
704
  } } } },
705
+ functions: { invoke: async (name, body, options) => {
706
+ requireCore();
707
+ if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before Amba.functions.invoke().");
708
+ if (!name || name.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.functions.invoke: a function name is required.");
709
+ const url = await resolveFunctionUrl(name);
710
+ const method = options?.method ?? "POST";
711
+ const headers = {
712
+ "X-Api-Key": _configuredClientKey,
713
+ ...options?.headers ?? {}
714
+ };
715
+ if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
716
+ let requestBody;
717
+ if (body !== void 0 && method !== "GET") {
718
+ headers["content-type"] = "application/json";
719
+ requestBody = JSON.stringify(body);
720
+ }
721
+ let resp;
722
+ try {
723
+ resp = await fetch(url, {
724
+ method,
725
+ headers,
726
+ body: requestBody
727
+ });
728
+ } catch (err) {
729
+ throw new AmbaApiError("NETWORK_ERROR", `Amba.functions.invoke("${name}") network error: ${String(err)}`);
730
+ }
731
+ const text = await resp.text();
732
+ let parsed = void 0;
733
+ if (text.length > 0) try {
734
+ parsed = JSON.parse(text);
735
+ } catch {
736
+ parsed = text;
737
+ }
738
+ if (!resp.ok) {
739
+ const envelope = parsed && typeof parsed === "object" ? parsed : void 0;
740
+ throw new AmbaApiError(typeof envelope?.error?.code === "string" ? envelope.error.code : `HTTP_${resp.status}`, typeof envelope?.error?.message === "string" ? envelope.error.message : typeof parsed === "string" && parsed.length > 0 ? parsed : `Amba.functions.invoke("${name}") failed with status ${resp.status}`, parsed);
741
+ }
742
+ return parsed;
743
+ } },
477
744
  config: { fetch: async () => {
478
745
  return await coreCall((c) => c.configFetch());
479
746
  } },
@@ -580,7 +847,12 @@ const Amba = {
580
847
  getMessage: async (conversationId, messageId) => {
581
848
  return await coreCall((c) => c.messagingGetMessage(conversationId, messageId));
582
849
  },
583
- markRead: async (conversationId) => await coreCall((c) => c.messagingMarkRead(conversationId))
850
+ addParticipant: async (conversationId, userId) => await coreCall((c) => c.messagingAddParticipant(conversationId, userId)),
851
+ markRead: async (conversationId) => await coreCall((c) => c.messagingMarkRead(conversationId)),
852
+ conversation: (conversationId) => new ConversationSubscription({
853
+ baseUrl: _configuredApiUrl,
854
+ authHeaders: realtimeAuthHeaders
855
+ }, conversationId)
584
856
  },
585
857
  moderation: {
586
858
  reportUser: async (request) => await coreCall((c) => c.moderationReportUser(encodeJson(request))),
@@ -638,8 +910,8 @@ function getCoreWasm() {
638
910
  * `verify-sdk-version-coherence` GH action gates every publish on the
639
911
  * pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
640
912
  */
641
- const SDK_VERSION = "4.0.2";
913
+ const SDK_VERSION = "4.0.4";
642
914
 
643
915
  //#endregion
644
- export { Amba, AmbaApiError, SDK_VERSION, getCoreWasm };
916
+ export { Amba, AmbaApiError, CollectionSubscription, ConversationSubscription, SDK_VERSION, getCoreWasm };
645
917
  //# sourceMappingURL=index.js.map