@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.
- package/dist/index.d.ts +243 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +275 -3
- package/dist/index.js.map +1 -1
- package/dist-rn/index.d.ts +243 -8
- package/dist-rn/index.js +275 -3
- package/package.json +2 -2
package/dist-rn/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
271
|
-
|
|
280
|
+
currency_code: string;
|
|
281
|
+
name: string;
|
|
282
|
+
is_premium: boolean;
|
|
272
283
|
balance: number;
|
|
273
|
-
|
|
284
|
+
lifetime_earned: number;
|
|
285
|
+
lifetime_spent: number;
|
|
274
286
|
}
|
|
275
287
|
interface CurrencyTransaction {
|
|
276
288
|
id: string;
|
|
277
|
-
|
|
278
|
-
|
|
289
|
+
app_user_id?: string;
|
|
290
|
+
currency_code: string;
|
|
291
|
+
amount: number;
|
|
279
292
|
balance_after: number;
|
|
280
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|