@layers/amba-react-native 3.0.3 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,13 +1,39 @@
1
1
 
2
2
  //#region src/async-storage-adapter.ts
3
- let cachedStorage = null;
3
+ let cachedStorage = void 0;
4
+ /**
5
+ * Late-imports `@react-native-async-storage/async-storage` and returns
6
+ * it ONLY if the resolved module exposes the three methods we use
7
+ * (`getItem` / `setItem` / `removeItem`). Otherwise returns null and
8
+ * the persistence layer falls back to memory-only.
9
+ *
10
+ * Why the shape check: pnpm with auto-install-peers (default since
11
+ * pnpm 7) installs declared peer deps even in non-RN scratch
12
+ * directories (e.g., this SDK's docs-smoke harness). The installed
13
+ * package's Node entry point doesn't expose AsyncStorage's methods
14
+ * — it's a React Native native module — so the `await import(...)`
15
+ * succeeds (no catch fires) but the returned object has no
16
+ * `getItem`. Calling it throws `TypeError: storage.getItem is not a
17
+ * function` from inside `read()` (verified at PR #246 docs-react-native
18
+ * / docs-expo run on 547a1ff4). Checking method presence here turns
19
+ * that into the same fall-through as "peer dep not installed at all."
20
+ */
21
+ function isAsyncStorageLike(mod) {
22
+ return typeof mod === "object" && mod !== null && typeof mod.getItem === "function" && typeof mod.setItem === "function" && typeof mod.removeItem === "function";
23
+ }
4
24
  async function getStorage() {
5
- if (cachedStorage !== null) return cachedStorage;
25
+ if (cachedStorage !== void 0) return cachedStorage;
6
26
  try {
7
27
  const mod = await import("@react-native-async-storage/async-storage");
8
- cachedStorage = mod.default ?? mod;
28
+ const candidate = mod.default ?? mod;
29
+ if (!isAsyncStorageLike(candidate)) {
30
+ cachedStorage = null;
31
+ return null;
32
+ }
33
+ cachedStorage = candidate;
9
34
  return cachedStorage;
10
35
  } catch {
36
+ cachedStorage = null;
11
37
  return null;
12
38
  }
13
39
  }
@@ -325,7 +351,7 @@ function freshSeed() {
325
351
  //#endregion
326
352
  //#region src/index.ts
327
353
  /** SDK semver — burned in at build time. Bump alongside package.json. */
328
- const SDK_VERSION = "3.0.0";
354
+ const SDK_VERSION = "4.0.1";
329
355
  let state = null;
330
356
  function getState() {
331
357
  if (!state) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start.");
@@ -369,6 +395,24 @@ async function postAuth(state_, path, body) {
369
395
  await notifyFromAuthResult(result);
370
396
  return result;
371
397
  }
398
+ /**
399
+ * POST helper for endpoints that return `{ data: { user } }` WITHOUT
400
+ * minting new tokens. Used by `linkSmsOtp` / `linkEmailOtp` / `unlink*`
401
+ * — these are "decorate the current user," not "re-authenticate."
402
+ * Existing session tokens stay untouched.
403
+ */
404
+ async function postUserNoRotate(state_, path, body) {
405
+ return (await state_.http.post(path, body)).data.user;
406
+ }
407
+ async function unlinkProvider(provider) {
408
+ const s = getState();
409
+ const sessionToken = s.identity.getSessionToken();
410
+ if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
411
+ return postUserNoRotate(s, P.authUnlink, {
412
+ provider,
413
+ session_token: sessionToken
414
+ });
415
+ }
372
416
  function requireEmail(email) {
373
417
  const trimmed = email.trim();
374
418
  if (trimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "email must not be empty");
@@ -377,15 +421,36 @@ function requireEmail(email) {
377
421
  function requirePassword(password) {
378
422
  if (password.length < 8) throw new AmbaApiError("VALIDATION_ERROR", "password must be at least 8 characters");
379
423
  }
424
+ function requireOtpCode(code) {
425
+ if (code.length !== 6 || !/^[0-9]{6}$/.test(code)) throw new AmbaApiError("VALIDATION_ERROR", "code must be 6 digits");
426
+ }
427
+ const E164_RE = /^\+[1-9][0-9]{6,14}$/;
428
+ function requireE164Phone(phone) {
429
+ if (!E164_RE.test(phone)) throw new AmbaApiError("VALIDATION_ERROR", "phone must be E.164 (starts with +, 8–15 digits)");
430
+ }
380
431
  const P = {
381
432
  authAnonymous: "/v1/client/auth/anonymous",
382
433
  authEmailLogin: "/v1/client/auth/email/login",
383
434
  authEmailSignup: "/v1/client/auth/email/signup",
435
+ authEmailOtpRequest: "/v1/client/auth/email-otp/request",
436
+ authSmsOtpRequest: "/v1/client/auth/sms-otp/request",
437
+ authSmsOtpVerify: "/v1/client/auth/sms-otp/verify",
438
+ authEmailOtpVerify: "/v1/client/auth/email-otp/verify",
384
439
  authSocial: "/v1/client/auth/social",
440
+ authMagicLinkRequest: "/v1/client/auth/magic-link/request",
441
+ authMagicLinkVerify: "/v1/client/auth/magic-link/verify",
442
+ authLink: "/v1/client/auth/link",
443
+ authUnlink: "/v1/client/auth/unlink",
444
+ authIdentifiers: "/v1/client/auth/identifiers",
385
445
  authRefresh: "/v1/client/auth/refresh",
386
446
  authLogout: "/v1/client/auth/logout",
387
447
  authMe: "/v1/client/auth/me",
388
448
  events: "/v1/client/events",
449
+ users: "/v1/client/users",
450
+ usersMe: "/v1/client/users/me",
451
+ sessions: "/v1/client/sessions",
452
+ sync: "/v1/client/sync",
453
+ leagues: "/v1/client/leagues",
389
454
  collections: "/v1/client/collections",
390
455
  storage: "/v1/client/media",
391
456
  storagePresign: "/v1/client/media/presign",
@@ -400,6 +465,7 @@ const P = {
400
465
  aiAnthropic: "/v1/client/ai/anthropic/messages",
401
466
  config: "/v1/client/config",
402
467
  flags: "/v1/client/flags",
468
+ diagnosticsPing: "/v1/client/diagnostics/ping",
403
469
  achievements: "/v1/client/achievements",
404
470
  challenges: "/v1/client/challenges",
405
471
  catalog: "/v1/client/catalog",
@@ -468,14 +534,16 @@ const Amba = {
468
534
  setDebug: (enabled) => {
469
535
  getState().debug = enabled;
470
536
  },
471
- events: { track: async (event, properties) => {
537
+ events: { track: async (event, properties, options) => {
472
538
  const s = getState();
473
- await s.http.post(P.events, {
474
- name: event,
539
+ const body = {
540
+ event,
475
541
  properties: properties ?? void 0,
476
542
  anonymous_id: s.identity.getAnonymousId(),
477
543
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
478
- });
544
+ };
545
+ if (options?.telemetry) body["telemetry"] = true;
546
+ await s.http.post(P.events, body);
479
547
  } },
480
548
  auth: {
481
549
  signInAnonymously: async () => {
@@ -498,13 +566,100 @@ const Amba = {
498
566
  password
499
567
  });
500
568
  },
501
- signInWithSocial: async (provider, idToken) => {
569
+ signInWithApple: async (identityToken) => {
570
+ if (identityToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "identity_token must not be empty");
571
+ return postAuth(getState(), P.authSocial, {
572
+ provider: "apple",
573
+ id_token: identityToken
574
+ });
575
+ },
576
+ signInWithGoogle: async (idToken) => {
502
577
  if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
503
578
  return postAuth(getState(), P.authSocial, {
504
- provider,
579
+ provider: "google",
505
580
  id_token: idToken
506
581
  });
507
582
  },
583
+ requestEmailOtp: async (email) => {
584
+ requireEmail(email);
585
+ await getState().http.post(P.authEmailOtpRequest, { email });
586
+ },
587
+ verifyEmailOtp: async (email, code) => {
588
+ requireEmail(email);
589
+ requireOtpCode(code);
590
+ return postAuth(getState(), P.authEmailOtpVerify, {
591
+ email,
592
+ code
593
+ });
594
+ },
595
+ requestSmsOtp: async (phone) => {
596
+ requireE164Phone(phone);
597
+ await getState().http.post(P.authSmsOtpRequest, { phone });
598
+ },
599
+ verifySmsOtp: async (phone, code) => {
600
+ requireE164Phone(phone);
601
+ requireOtpCode(code);
602
+ return postAuth(getState(), P.authSmsOtpVerify, {
603
+ phone,
604
+ code
605
+ });
606
+ },
607
+ requestMagicLink: async (email) => {
608
+ requireEmail(email);
609
+ await getState().http.post(P.authMagicLinkRequest, { email });
610
+ },
611
+ verifyMagicLink: async (token) => {
612
+ if (token.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "token must not be empty");
613
+ return postAuth(getState(), P.authMagicLinkVerify, { token });
614
+ },
615
+ linkAccount: async (provider, credential) => {
616
+ const providerTrimmed = provider?.trim() ?? "";
617
+ if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
618
+ if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
619
+ const s = getState();
620
+ const sessionToken = s.identity.getSessionToken();
621
+ if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
622
+ return postAuth(s, P.authLink, {
623
+ provider: providerTrimmed,
624
+ token: credential,
625
+ session_token: sessionToken
626
+ });
627
+ },
628
+ linkSmsOtp: async (phone, code) => {
629
+ requireE164Phone(phone);
630
+ requireOtpCode(code);
631
+ const s = getState();
632
+ const sessionToken = s.identity.getSessionToken();
633
+ if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
634
+ return postUserNoRotate(s, P.authLink, {
635
+ provider: "sms_otp",
636
+ phone,
637
+ code,
638
+ session_token: sessionToken
639
+ });
640
+ },
641
+ linkEmailOtp: async (email, code) => {
642
+ requireEmail(email);
643
+ requireOtpCode(code);
644
+ const s = getState();
645
+ const sessionToken = s.identity.getSessionToken();
646
+ if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
647
+ return postUserNoRotate(s, P.authLink, {
648
+ provider: "email_otp",
649
+ email,
650
+ code,
651
+ session_token: sessionToken
652
+ });
653
+ },
654
+ unlinkSmsOtp: async () => unlinkProvider("sms_otp"),
655
+ unlinkEmailOtp: async () => unlinkProvider("email_otp"),
656
+ unlinkApple: async () => unlinkProvider("apple"),
657
+ unlinkGoogle: async () => unlinkProvider("google"),
658
+ getCurrentIdentifiers: async () => {
659
+ const s = getState();
660
+ if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
661
+ return (await s.http.get(P.authIdentifiers)).data.identifiers;
662
+ },
508
663
  signOut: async (rotateAnonymousId = false) => {
509
664
  const s = getState();
510
665
  if (s.identity.getSessionToken()) try {
@@ -541,6 +696,41 @@ const Amba = {
541
696
  };
542
697
  }
543
698
  },
699
+ users: {
700
+ get: async (userId) => {
701
+ const s = getState();
702
+ if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
703
+ const path = userId === void 0 || userId === null || userId === "" ? P.usersMe : `${P.users}/${encodePathSegment(userId)}`;
704
+ return (await s.http.get(path)).data;
705
+ },
706
+ update: ((arg1, arg2) => {
707
+ const s = getState();
708
+ if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
709
+ const isById = typeof arg1 === "string";
710
+ const patch = isById ? arg2 ?? {} : arg1;
711
+ const path = isById ? `${P.users}/${encodePathSegment(arg1)}` : `${P.users}/me`;
712
+ return s.http.patch(path, patch).then((env) => env.data);
713
+ })
714
+ },
715
+ sessions: {
716
+ list: async () => {
717
+ return (await getState().http.get(P.sessions)).data;
718
+ },
719
+ revoke: async (sessionId) => {
720
+ await getState().http.post(`${P.sessions}/${encodePathSegment(sessionId)}/end`, {});
721
+ }
722
+ },
723
+ sync: {
724
+ pushChanges: async (changes) => {
725
+ return (await getState().http.post(P.sync, { changes })).data;
726
+ },
727
+ pullChanges: async (since) => {
728
+ if (!since.entity_type || since.entity_type.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "pullChanges requires a non-empty entity_type");
729
+ const query = { entity_type: since.entity_type };
730
+ if (since.checkpoint_token !== void 0) query["checkpoint_token"] = since.checkpoint_token;
731
+ return (await getState().http.get(P.sync, query)).data;
732
+ }
733
+ },
544
734
  collections: {
545
735
  find: async (name, options = {}) => {
546
736
  return getState().http.post(`${P.collections}/${encodePathSegment(name)}/find`, options);
@@ -548,6 +738,20 @@ const Amba = {
548
738
  findOne: async (name, id) => {
549
739
  return (await getState().http.get(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`)).data;
550
740
  },
741
+ findNearest: async (name, vector, k, where) => {
742
+ const s = getState();
743
+ const body = {
744
+ to_vector: vector,
745
+ k
746
+ };
747
+ if (where !== void 0) body.where = where;
748
+ return (await s.http.post(`${P.collections}/${encodePathSegment(name)}/find-nearest`, body)).data;
749
+ },
750
+ count: async (name, where) => {
751
+ const s = getState();
752
+ const query = where !== void 0 ? { query: JSON.stringify({ where }) } : void 0;
753
+ return (await s.http.get(`${P.collections}/${encodePathSegment(name)}/count`, query)).data.count;
754
+ },
551
755
  insert: async (name, row) => {
552
756
  return (await getState().http.post(`${P.collections}/${encodePathSegment(name)}`, row)).data;
553
757
  },
@@ -636,6 +840,33 @@ const Amba = {
636
840
  upload_id: uploadId,
637
841
  asset_id: assetId
638
842
  })).data;
843
+ },
844
+ list: async (prefix) => {
845
+ const env = await getState().http.get(P.storage, prefix !== void 0 ? { prefix } : void 0);
846
+ return {
847
+ data: env.data ?? [],
848
+ next_cursor: env.next_cursor ?? null
849
+ };
850
+ },
851
+ delete: async (assetId) => {
852
+ await getState().http.delete(`${P.storage}/${encodePathSegment(assetId)}`);
853
+ },
854
+ download: async (assetId) => {
855
+ const assetEnv = await getState().http.get(`${P.storage}/${encodePathSegment(assetId)}`);
856
+ const cdnUrl = assetEnv.data.url ?? assetEnv.data.storage_url ?? "";
857
+ if (!cdnUrl) throw new AmbaApiError("NOT_FOUND", "asset has no resolvable CDN url");
858
+ let response;
859
+ try {
860
+ response = await (globalThis.fetch ?? fetch)(cdnUrl, { method: "GET" });
861
+ } catch (err) {
862
+ throw new AmbaApiError("NETWORK_ERROR", `Network error: ${err instanceof Error ? err.message : String(err)}`, err);
863
+ }
864
+ if (!response.ok) {
865
+ const text = await response.text();
866
+ const ct = response.headers.get("content-type") ?? "";
867
+ throw mapHttpError(response, text, ct.includes("application/json"));
868
+ }
869
+ return response.blob();
639
870
  }
640
871
  },
641
872
  push: {
@@ -706,8 +937,21 @@ const Amba = {
706
937
  values: parsed.data ?? {}
707
938
  };
708
939
  } },
709
- flags: { fetch: async () => {
710
- return (await getState().http.get(P.flags)).data;
940
+ flags: {
941
+ fetch: async () => {
942
+ return (await getState().http.get(P.flags)).data;
943
+ },
944
+ get: async (key) => {
945
+ try {
946
+ return (await getState().http.get(`${P.flags}/${encodePathSegment(key)}`)).data;
947
+ } catch (err) {
948
+ if (err instanceof AmbaApiError && err.code === "NOT_FOUND") return null;
949
+ throw err;
950
+ }
951
+ }
952
+ },
953
+ diagnostics: { ping: async () => {
954
+ return await getState().http.post(P.diagnosticsPing, {});
711
955
  } },
712
956
  achievements: {
713
957
  getAll: async () => {
@@ -765,6 +1009,14 @@ const Amba = {
765
1009
  return (await getState().http.get(`${P.leaderboards}/${encodePathSegment(key)}/me`)).data;
766
1010
  }
767
1011
  },
1012
+ leagues: {
1013
+ me: async () => {
1014
+ return (await getState().http.get(`${P.leagues}/me`)).data;
1015
+ },
1016
+ cohort: async () => {
1017
+ return (await getState().http.get(`${P.leagues}/me/cohort`)).data;
1018
+ }
1019
+ },
768
1020
  stores: {
769
1021
  list: async () => {
770
1022
  return (await getState().http.get(P.stores)).data;
@@ -809,6 +1061,15 @@ const Amba = {
809
1061
  getFriends: async () => {
810
1062
  return (await getState().http.get(P.friends, { state: "accepted" })).data;
811
1063
  },
1064
+ sendRequest: async (userId) => {
1065
+ return (await getState().http.post(`${P.friends}/request`, { user_id: userId })).data;
1066
+ },
1067
+ acceptRequest: async (friendshipId) => {
1068
+ return (await getState().http.post(`${P.friends}/${encodePathSegment(friendshipId)}/accept`, {})).data;
1069
+ },
1070
+ declineRequest: async (friendshipId) => {
1071
+ await getState().http.post(`${P.friends}/${encodePathSegment(friendshipId)}/reject`, {});
1072
+ },
812
1073
  blockUser: async (userId) => {
813
1074
  return (await getState().http.post(`${P.friends}/block`, { user_id: userId })).data;
814
1075
  },
@@ -817,6 +1078,9 @@ const Amba = {
817
1078
  },
818
1079
  removeBlock: async (friendshipId) => {
819
1080
  await getState().http.delete(`${P.friends}/${encodePathSegment(friendshipId)}`);
1081
+ },
1082
+ removeFriend: async (userId) => {
1083
+ await getState().http.delete(`${P.friends}/by-user/${encodePathSegment(userId)}`);
820
1084
  }
821
1085
  },
822
1086
  groups: {
@@ -849,10 +1113,7 @@ const Amba = {
849
1113
  createConversation: async (request) => {
850
1114
  return (await getState().http.post(`${P.messaging}/conversations`, request)).data;
851
1115
  },
852
- getConversations: async () => {
853
- return (await getState().http.get(`${P.messaging}/conversations`)).data;
854
- },
855
- listConversations: async () => {
1116
+ conversations: async () => {
856
1117
  return (await getState().http.get(`${P.messaging}/conversations`)).data;
857
1118
  },
858
1119
  sendMessage: async (conversationId, request) => {
@@ -866,6 +1127,24 @@ const Amba = {
866
1127
  },
867
1128
  markRead: async (conversationId) => {
868
1129
  return (await getState().http.post(`${P.messaging}/conversations/${encodePathSegment(conversationId)}/read`, {})).data;
1130
+ },
1131
+ getMessage: async (conversationId, messageId) => {
1132
+ const PAGE_SIZE = 100;
1133
+ const MAX_PAGES = 50;
1134
+ const s = getState();
1135
+ const path = `${P.messaging}/conversations/${encodePathSegment(conversationId)}/messages`;
1136
+ let offset = 0;
1137
+ for (let i = 0; i < MAX_PAGES; i++) {
1138
+ const page = (await s.http.get(path, {
1139
+ limit: PAGE_SIZE,
1140
+ offset
1141
+ })).data;
1142
+ const hit = page.find((m) => m.id === messageId);
1143
+ if (hit) return hit;
1144
+ if (page.length < PAGE_SIZE) return null;
1145
+ offset += PAGE_SIZE;
1146
+ }
1147
+ return null;
869
1148
  }
870
1149
  },
871
1150
  moderation: {
@@ -918,20 +1197,25 @@ const Amba = {
918
1197
  return (await getState().http.post(P.referrals, body)).data;
919
1198
  }
920
1199
  },
921
- catalog: { list: async () => {
922
- return (await getState().http.get(P.catalog)).data;
923
- } },
1200
+ catalog: {
1201
+ list: async () => {
1202
+ return (await getState().http.get(P.catalog)).data;
1203
+ },
1204
+ get: async (id) => {
1205
+ return (await getState().http.get(`${P.catalog}/${encodePathSegment(id)}`)).data;
1206
+ }
1207
+ },
924
1208
  content: {
925
- getToday: async (channel = "default") => {
1209
+ today: async (channel = "default") => {
926
1210
  return (await getState().http.get(`${P.content}/today/${encodePathSegment(channel)}`)).data ?? null;
927
1211
  },
928
- getLibrary: async (channel = "default", options) => {
1212
+ library: async (channel = "default", options) => {
929
1213
  return (await getState().http.get(`${P.content}/library/${encodePathSegment(channel)}`, {
930
1214
  limit: options?.limit,
931
1215
  cursor: options?.cursor
932
1216
  })).data;
933
1217
  },
934
- getItem: async (id) => {
1218
+ item: async (id) => {
935
1219
  return (await getState().http.get(`${P.content}/${encodePathSegment(id)}`)).data;
936
1220
  },
937
1221
  updateItem: async (id, state_) => {