@layers/amba-react-native 1.0.1 → 2.0.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.
package/dist/index.cjs CHANGED
@@ -53,70 +53,278 @@ var AmbaApiError = class extends Error {
53
53
  Object.setPrototypeOf(this, new.target.prototype);
54
54
  }
55
55
  };
56
- /**
57
- * Best-effort: pull a stable error code out of a Rust `AmbaError`
58
- * Display string. Used by `toAmbaApiError` when the underlying thrown
59
- * value is a generic `Error` produced by `wasm-bindgen`'s `JsError::new`.
60
- *
61
- * Keep in sync with `sdks/core/src/error.rs::AmbaError`'s `#[error]`
62
- * format strings — but the parser falls back to UNKNOWN_ERROR for any
63
- * pattern it doesn't recognize, so a stale match list isn't a footgun,
64
- * just a quality-of-life regression.
65
- */
66
- function codeFromMessage(message) {
67
- if (message.startsWith("Unauthorized")) return "UNAUTHORIZED";
68
- if (message.startsWith("Forbidden")) return "FORBIDDEN";
69
- if (message.startsWith("Not found")) return "NOT_FOUND";
70
- if (message.startsWith("Conflict")) return "CONFLICT";
71
- if (message.startsWith("Rate limited")) return "RATE_LIMITED";
72
- if (message.startsWith("Validation error")) return "VALIDATION_ERROR";
73
- if (message.startsWith("Network error")) return "NETWORK_ERROR";
74
- if (message.startsWith("HTTP error")) return "HTTP_ERROR";
75
- if (message.startsWith("Circuit breaker")) return "CIRCUIT_OPEN";
76
- if (message.startsWith("Consent not granted")) return "CONSENT_NOT_GRANTED";
77
- if (message.includes("not initialized")) return "NOT_INITIALIZED";
78
- if (message.startsWith("Invalid configuration")) return "INVALID_CONFIG";
79
- if (message.startsWith("Invalid argument")) return "INVALID_ARGUMENT";
80
- return "UNKNOWN_ERROR";
56
+
57
+ //#endregion
58
+ //#region src/http-client.ts
59
+ /** Default amba API endpoint. */
60
+ const DEFAULT_BASE_URL = "https://api.amba.dev";
61
+ var AmbaHttpClient = class {
62
+ apiKey;
63
+ baseUrl;
64
+ sdkPlatform;
65
+ sdkVersionHeader;
66
+ identity;
67
+ fetchImpl;
68
+ constructor(opts) {
69
+ this.apiKey = opts.apiKey;
70
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
71
+ this.sdkPlatform = opts.sdkPlatform;
72
+ this.sdkVersionHeader = opts.sdkVersionHeader;
73
+ this.identity = opts.identity;
74
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
75
+ }
76
+ /** Build the absolute URL for an API path, optionally with a query string. */
77
+ url(path, query) {
78
+ const base = this.baseUrl + path;
79
+ if (!query) return base;
80
+ const parts = [];
81
+ for (const k of Object.keys(query)) {
82
+ const v = query[k];
83
+ if (v === void 0 || v === null) continue;
84
+ parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
85
+ }
86
+ if (parts.length === 0) return base;
87
+ return `${base}?${parts.join("&")}`;
88
+ }
89
+ /** Build the standard request headers for the current identity state. */
90
+ buildHeaders(extra) {
91
+ const h = {
92
+ "X-Api-Key": this.apiKey,
93
+ "X-SDK-Platform": this.sdkPlatform,
94
+ "X-SDK-Version": this.sdkVersionHeader,
95
+ "Content-Type": "application/json",
96
+ Accept: "application/json",
97
+ ...extra
98
+ };
99
+ const tok = this.identity.getSessionToken();
100
+ if (tok) h.Authorization = `Bearer ${tok}`;
101
+ return h;
102
+ }
103
+ async get(path, query) {
104
+ return this.request("GET", path, void 0, query);
105
+ }
106
+ async post(path, body, query) {
107
+ return this.request("POST", path, body, query);
108
+ }
109
+ async patch(path, body) {
110
+ return this.request("PATCH", path, body);
111
+ }
112
+ async delete(path) {
113
+ return this.request("DELETE", path);
114
+ }
115
+ async request(method, path, body, query) {
116
+ const url = this.url(path, query);
117
+ const init = {
118
+ method,
119
+ headers: this.buildHeaders(),
120
+ body: body === void 0 ? void 0 : JSON.stringify(body ?? {})
121
+ };
122
+ let response;
123
+ try {
124
+ response = await this.fetchImpl(url, init);
125
+ } catch (err) {
126
+ throw new AmbaApiError("NETWORK_ERROR", `Network error: ${err instanceof Error ? err.message : typeof err === "string" ? err : "fetch failed"}`, err);
127
+ }
128
+ return parseResponse(response);
129
+ }
130
+ };
131
+ async function parseResponse(response) {
132
+ if (response.status === 204) return null;
133
+ const text = await response.text();
134
+ const isJson = (response.headers.get("content-type") ?? "").includes("application/json");
135
+ if (!response.ok) throw mapHttpError(response, text, isJson);
136
+ if (text === "") return null;
137
+ if (isJson) try {
138
+ return JSON.parse(text);
139
+ } catch (err) {
140
+ throw new AmbaApiError("HTTP_ERROR", `Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}`, { body: text });
141
+ }
142
+ return text;
143
+ }
144
+ function mapHttpError(response, rawBody, isJson) {
145
+ let parsed = void 0;
146
+ let message = rawBody || `HTTP ${response.status}`;
147
+ let code;
148
+ if (isJson && rawBody) {
149
+ try {
150
+ parsed = JSON.parse(rawBody);
151
+ } catch {
152
+ parsed = void 0;
153
+ }
154
+ const innerErr = parsed?.error;
155
+ if (innerErr && typeof innerErr === "object") {
156
+ if (typeof innerErr.code === "string") code = innerErr.code;
157
+ if (typeof innerErr.message === "string") message = innerErr.message;
158
+ }
159
+ }
160
+ switch (response.status) {
161
+ case 401: return new AmbaApiError(code ?? "UNAUTHORIZED", message, parsed);
162
+ case 403: return new AmbaApiError(code ?? "FORBIDDEN", message, parsed);
163
+ case 404: return new AmbaApiError(code ?? "NOT_FOUND", message, parsed);
164
+ case 409: return new AmbaApiError(code ?? "CONFLICT", message, parsed);
165
+ case 422: return new AmbaApiError(code ?? "VALIDATION_ERROR", message, parsed);
166
+ case 429: {
167
+ const retryAfter = response.headers.get("retry-after") ?? "unspecified";
168
+ const serverGaveMessage = message !== rawBody && message !== `HTTP ${response.status}`;
169
+ return new AmbaApiError(code ?? "RATE_LIMITED", serverGaveMessage ? message : `Rate limited (retry after ${retryAfter})`, parsed);
170
+ }
171
+ default: return new AmbaApiError(code ?? "HTTP_ERROR", message, parsed ?? {
172
+ status: response.status,
173
+ body: rawBody
174
+ });
175
+ }
176
+ }
177
+
178
+ //#endregion
179
+ //#region src/uuid.ts
180
+ function getRandomBytes(n) {
181
+ const bytes = new Uint8Array(n);
182
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
183
+ globalThis.crypto.getRandomValues(bytes);
184
+ return bytes;
185
+ }
186
+ for (let i = 0; i < n; i++) bytes[i] = Math.floor(Math.random() * 256);
187
+ return bytes;
81
188
  }
82
189
  /**
83
- * Coerce any thrown value into an `AmbaApiError`. Idempotent — passing
84
- * an existing `AmbaApiError` returns it unchanged so wrappers can stack
85
- * without re-wrapping.
86
- *
87
- * Heuristics:
88
- * 1. Existing `AmbaApiError` → returned as-is.
89
- * 2. Object with `.code` → use that code + `.message` + `.details`.
90
- * 3. `Error` (e.g. WASM JsError) → infer code from message prefix.
91
- * 4. Anything else → stringify and tag UNKNOWN_ERROR.
190
+ * Generate a UUID v4 (random) per RFC 4122. Wire shape:
191
+ * `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` where `y ∈ {8,9,a,b}`.
92
192
  */
93
- function toAmbaApiError(err) {
94
- if (err instanceof AmbaApiError) return err;
95
- if (err && typeof err === "object") {
96
- const obj = err;
97
- if (typeof obj.code === "string") return new AmbaApiError(obj.code, typeof obj.message === "string" ? obj.message : "unknown", obj.details);
98
- }
99
- if (err instanceof Error) return new AmbaApiError(codeFromMessage(err.message), err.message);
100
- return new AmbaApiError("UNKNOWN_ERROR", String(err));
193
+ function uuidv4() {
194
+ const b = getRandomBytes(16);
195
+ b[6] = b[6] & 15 | 64;
196
+ b[8] = b[8] & 63 | 128;
197
+ const h = Array.from(b, (v) => v.toString(16).padStart(2, "0"));
198
+ return h.slice(0, 4).join("") + "-" + h.slice(4, 6).join("") + "-" + h.slice(6, 8).join("") + "-" + h.slice(8, 10).join("") + "-" + h.slice(10, 16).join("");
101
199
  }
102
200
 
103
201
  //#endregion
104
- //#region src/index.ts
105
- let coreInstance = null;
106
- function requireCore() {
107
- if (!coreInstance) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ apiKey }) at app start.");
108
- return coreInstance;
202
+ //#region src/identity.ts
203
+ /** Storage key under the configured prefix. Bumped when shape changes. */
204
+ const IDENTITY_KEY = "identity_v1";
205
+ function isExpired(expiresAt) {
206
+ if (!expiresAt) return false;
207
+ const t = Date.parse(expiresAt);
208
+ if (Number.isNaN(t)) return false;
209
+ return t <= Date.now();
109
210
  }
110
211
  /**
111
- * Run a single WASM-core call and rewrap any thrown value into a
112
- * stable `AmbaApiError`. Mirrors the helper in `@layers/amba-web`.
212
+ * The SDK's identity state — held in memory after hydration, persisted
213
+ * to AsyncStorage on every mutation. All methods are sync after
214
+ * `hydrate()` resolves.
113
215
  */
114
- async function coreCall(fn) {
115
- try {
116
- return await fn(requireCore());
117
- } catch (err) {
118
- throw toAmbaApiError(err);
216
+ var IdentityStore = class IdentityStore {
217
+ anonymousId;
218
+ appUserId = null;
219
+ sessionToken = null;
220
+ refreshToken = null;
221
+ expiresAt = null;
222
+ constructor(persistence, seed) {
223
+ this.persistence = persistence;
224
+ this.anonymousId = seed.anonymous_id;
225
+ this.appUserId = seed.app_user_id;
226
+ this.sessionToken = seed.session_token;
227
+ this.refreshToken = seed.refresh_token;
228
+ this.expiresAt = seed.expires_at;
229
+ }
230
+ /**
231
+ * Build a store, hydrating any prior identity from AsyncStorage.
232
+ * If no prior identity exists, a fresh `anonymous_id` is minted.
233
+ * Expired session tokens are dropped at hydration (the refresh token
234
+ * survives so callers can `auth.refresh()` to mint a new pair).
235
+ */
236
+ static async create(persistence) {
237
+ const raw = await persistence.read(IDENTITY_KEY);
238
+ let seed;
239
+ if (raw) try {
240
+ const parsed = JSON.parse(raw);
241
+ seed = {
242
+ anonymous_id: parsed.anonymous_id ?? uuidv4(),
243
+ app_user_id: parsed.app_user_id ?? null,
244
+ session_token: parsed.session_token ?? null,
245
+ refresh_token: parsed.refresh_token ?? null,
246
+ expires_at: parsed.expires_at ?? null
247
+ };
248
+ } catch {
249
+ seed = freshSeed();
250
+ }
251
+ else seed = freshSeed();
252
+ if (isExpired(seed.expires_at)) {
253
+ seed.session_token = null;
254
+ seed.expires_at = null;
255
+ }
256
+ const store = new IdentityStore(persistence, seed);
257
+ await store.flush();
258
+ return store;
119
259
  }
260
+ snapshot() {
261
+ return {
262
+ anonymousId: this.anonymousId,
263
+ appUserId: this.appUserId,
264
+ sessionToken: this.sessionToken,
265
+ refreshToken: this.refreshToken,
266
+ expiresAt: this.expiresAt
267
+ };
268
+ }
269
+ getAnonymousId() {
270
+ return this.anonymousId;
271
+ }
272
+ getAppUserId() {
273
+ return this.appUserId;
274
+ }
275
+ getSessionToken() {
276
+ return this.sessionToken;
277
+ }
278
+ getRefreshToken() {
279
+ return this.refreshToken;
280
+ }
281
+ isAuthenticated() {
282
+ return this.sessionToken !== null;
283
+ }
284
+ async setSession(args) {
285
+ this.appUserId = args.appUserId;
286
+ this.sessionToken = args.sessionToken;
287
+ this.refreshToken = args.refreshToken;
288
+ this.expiresAt = args.expiresAt;
289
+ await this.flush();
290
+ }
291
+ async clearSession(rotateAnonymousId) {
292
+ this.appUserId = null;
293
+ this.sessionToken = null;
294
+ this.refreshToken = null;
295
+ this.expiresAt = null;
296
+ if (rotateAnonymousId) this.anonymousId = uuidv4();
297
+ await this.flush();
298
+ }
299
+ async flush() {
300
+ const payload = {
301
+ anonymous_id: this.anonymousId,
302
+ app_user_id: this.appUserId,
303
+ session_token: this.sessionToken,
304
+ refresh_token: this.refreshToken,
305
+ expires_at: this.expiresAt
306
+ };
307
+ await this.persistence.write(IDENTITY_KEY, JSON.stringify(payload));
308
+ }
309
+ };
310
+ function freshSeed() {
311
+ return {
312
+ anonymous_id: uuidv4(),
313
+ app_user_id: null,
314
+ session_token: null,
315
+ refresh_token: null,
316
+ expires_at: null
317
+ };
318
+ }
319
+
320
+ //#endregion
321
+ //#region src/index.ts
322
+ /** SDK semver — burned in at build time. Bump alongside package.json. */
323
+ const SDK_VERSION = "2.0.0";
324
+ let state = null;
325
+ function getState() {
326
+ if (!state) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ apiKey }) at app start.");
327
+ return state;
120
328
  }
121
329
  const authSubscribers = /* @__PURE__ */ new Set();
122
330
  function snapshotSession(user) {
@@ -138,82 +346,183 @@ function notifyAuthSubscribers(session) {
138
346
  async function notifyFromAuthResult(result) {
139
347
  notifyAuthSubscribers(snapshotSession(result.user));
140
348
  }
349
+ function expiresAtFromResult(result) {
350
+ if (result.expires_at && typeof result.expires_at === "string") return result.expires_at;
351
+ return new Date(Date.now() + 3600 * 1e3).toISOString();
352
+ }
353
+ async function persistAuthResult(state_, result) {
354
+ await state_.identity.setSession({
355
+ appUserId: result.user.id,
356
+ sessionToken: result.session_token,
357
+ refreshToken: result.refresh_token,
358
+ expiresAt: expiresAtFromResult(result)
359
+ });
360
+ }
361
+ async function postAuth(state_, path, body) {
362
+ const result = (await state_.http.post(path, body)).data;
363
+ await persistAuthResult(state_, result);
364
+ await notifyFromAuthResult(result);
365
+ return result;
366
+ }
367
+ function requireEmail(email) {
368
+ const trimmed = email.trim();
369
+ if (trimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "email must not be empty");
370
+ if (!trimmed.includes("@") || !trimmed.includes(".")) throw new AmbaApiError("VALIDATION_ERROR", `email '${email}' is not a valid email address`);
371
+ }
372
+ function requirePassword(password) {
373
+ if (password.length < 8) throw new AmbaApiError("VALIDATION_ERROR", "password must be at least 8 characters");
374
+ }
375
+ const P = {
376
+ authAnonymous: "/v1/client/auth/anonymous",
377
+ authEmailLogin: "/v1/client/auth/email/login",
378
+ authEmailSignup: "/v1/client/auth/email/signup",
379
+ authSocial: "/v1/client/auth/social",
380
+ authRefresh: "/v1/client/auth/refresh",
381
+ authLogout: "/v1/client/auth/logout",
382
+ authMe: "/v1/client/auth/me",
383
+ events: "/v1/client/events",
384
+ collections: "/v1/client/collections",
385
+ storage: "/v1/client/media",
386
+ storagePresign: "/v1/client/media/presign",
387
+ storageCommit: "/v1/client/media/commit",
388
+ push: "/v1/client/push",
389
+ pushRegister: "/v1/client/push/register",
390
+ pushUnregister: "/v1/client/push/unregister",
391
+ pushTokens: "/v1/client/push/tokens",
392
+ pushSubscribe: "/v1/client/push/subscribe",
393
+ pushUnsubscribe: "/v1/client/push/unsubscribe",
394
+ entitlements: "/v1/client/entitlements",
395
+ aiAnthropic: "/v1/client/ai/anthropic/messages",
396
+ config: "/v1/client/config",
397
+ flags: "/v1/client/flags",
398
+ achievements: "/v1/client/achievements",
399
+ challenges: "/v1/client/challenges",
400
+ catalog: "/v1/client/catalog",
401
+ currencies: "/v1/client/currencies",
402
+ inventory: "/v1/client/inventory",
403
+ leaderboards: "/v1/client/leaderboards",
404
+ stores: "/v1/client/stores",
405
+ xp: "/v1/client/xp",
406
+ streaks: "/v1/client/streaks",
407
+ feeds: "/v1/client/feeds",
408
+ friends: "/v1/client/friends",
409
+ groups: "/v1/client/groups",
410
+ messaging: "/v1/client/messaging",
411
+ moderation: "/v1/client/moderation",
412
+ reviews: "/v1/client/reviews",
413
+ roles: "/v1/client/roles",
414
+ referrals: "/v1/client/referrals",
415
+ content: "/v1/client/content",
416
+ deepLinks: "/v1/client/deeplinks",
417
+ onboarding: "/v1/client/onboarding"
418
+ };
419
+ function encodePathSegment(s) {
420
+ return encodeURIComponent(s);
421
+ }
141
422
  /**
142
423
  * Top-level SDK namespace for React Native.
143
- *
144
- * Differences from `@layers/amba-web`:
145
- * - `configure` is async (waits for WASM init AND AsyncStorage hydration).
146
- * - Persistence is AsyncStorage-backed — identity survives app restarts.
147
424
  */
148
425
  const Amba = {
149
426
  configure: async (config) => {
150
427
  if (!config.apiKey || config.apiKey.trim() === "") throw new Error("Amba.configure: apiKey is required");
151
- const coreWasm = await import("@layers/amba-core");
152
- const init = coreWasm.default;
153
- const AmbaCoreWasm = coreWasm.AmbaCoreWasm;
154
- await init();
155
- await new AsyncStoragePersistence(config.storagePrefix ?? "amba").hydrate();
156
- coreInstance = AmbaCoreWasm.init(JSON.stringify({
157
- api_key: config.apiKey,
158
- base_url: config.baseUrl,
159
- sdk_platform: "react-native",
160
- sdk_wrapper_version: `amba-react-native/${SDK_VERSION}`,
161
- consent_required: config.consentRequired,
162
- debug: config.debug
163
- }), globalThis.fetch.bind(globalThis));
428
+ const persistence = new AsyncStoragePersistence(config.storagePrefix ?? "amba");
429
+ await persistence.hydrate();
430
+ const identity = await IdentityStore.create(persistence);
431
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
432
+ const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
433
+ state = {
434
+ http: new AmbaHttpClient({
435
+ apiKey: config.apiKey,
436
+ baseUrl,
437
+ sdkPlatform: "react-native",
438
+ sdkVersionHeader,
439
+ identity
440
+ }),
441
+ identity,
442
+ debug: config.debug ?? false
443
+ };
444
+ },
445
+ _reset: () => {
446
+ state = null;
447
+ authSubscribers.clear();
164
448
  },
165
449
  get anonymousId() {
166
- return requireCore().anonymousId();
450
+ return getState().identity.getAnonymousId();
167
451
  },
168
452
  get appUserId() {
169
- return requireCore().appUserId() ?? void 0;
453
+ return getState().identity.getAppUserId() ?? void 0;
170
454
  },
171
455
  get isAuthenticated() {
172
- return requireCore().isAuthenticated();
456
+ return getState().identity.isAuthenticated();
173
457
  },
174
458
  setDebug: (enabled) => {
175
- requireCore().setDebug(enabled);
459
+ getState().debug = enabled;
176
460
  },
177
461
  events: { track: async (event, properties) => {
178
- await coreCall((c) => c.track(event, properties ? JSON.stringify(properties) : void 0));
462
+ const s = getState();
463
+ await s.http.post(P.events, {
464
+ name: event,
465
+ properties: properties ?? void 0,
466
+ anonymous_id: s.identity.getAnonymousId(),
467
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
468
+ });
179
469
  } },
180
470
  auth: {
181
471
  signInAnonymously: async () => {
182
- const r = await coreCall((c) => c.signInAnonymously());
183
- await notifyFromAuthResult(r);
184
- return r;
472
+ const s = getState();
473
+ return postAuth(s, P.authAnonymous, { anonymous_id: s.identity.getAnonymousId() });
185
474
  },
186
475
  signInWithEmail: async (email, password) => {
187
- const r = await coreCall((c) => c.signInWithEmail(email, password));
188
- await notifyFromAuthResult(r);
189
- return r;
476
+ requireEmail(email);
477
+ if (password === "") throw new AmbaApiError("VALIDATION_ERROR", "password must not be empty");
478
+ return postAuth(getState(), P.authEmailLogin, {
479
+ email,
480
+ password
481
+ });
190
482
  },
191
483
  signUpWithEmail: async (email, password) => {
192
- const r = await coreCall((c) => c.signUpWithEmail(email, password));
193
- await notifyFromAuthResult(r);
194
- return r;
484
+ requireEmail(email);
485
+ requirePassword(password);
486
+ return postAuth(getState(), P.authEmailSignup, {
487
+ email,
488
+ password
489
+ });
195
490
  },
196
491
  signInWithSocial: async (provider, idToken) => {
197
- const r = await coreCall((c) => c.signInWithSocial(provider, idToken));
198
- await notifyFromAuthResult(r);
199
- return r;
492
+ if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
493
+ return postAuth(getState(), P.authSocial, {
494
+ provider,
495
+ id_token: idToken
496
+ });
200
497
  },
201
498
  signOut: async (rotateAnonymousId = false) => {
202
- await coreCall((c) => c.signOut(rotateAnonymousId));
499
+ const s = getState();
500
+ if (s.identity.getSessionToken()) try {
501
+ await s.http.post(P.authLogout, {});
502
+ } catch (err) {
503
+ if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
504
+ }
505
+ await s.identity.clearSession(rotateAnonymousId);
203
506
  notifyAuthSubscribers(null);
204
507
  },
205
508
  refresh: async () => {
206
- const r = await coreCall((c) => c.refreshSession());
207
- await notifyFromAuthResult(r);
208
- return r;
509
+ const s = getState();
510
+ const refreshToken = s.identity.getRefreshToken();
511
+ if (!refreshToken) throw new AmbaApiError("UNAUTHORIZED", "no refresh token available — call signIn* first");
512
+ return postAuth(s, P.authRefresh, { refresh_token: refreshToken });
513
+ },
514
+ me: async () => {
515
+ const s = getState();
516
+ if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
517
+ return (await s.http.get(P.authMe)).data;
209
518
  },
210
- me: async () => await coreCall((c) => c.me()),
211
519
  getSession: async () => {
212
- if (!requireCore().isAuthenticated()) return null;
213
- return snapshotSession(await coreCall((c) => c.me()));
520
+ const s = getState();
521
+ if (!s.identity.isAuthenticated()) return null;
522
+ return snapshotSession((await s.http.get(P.authMe)).data);
214
523
  },
215
524
  getAnonymousId: async () => {
216
- return requireCore().anonymousId();
525
+ return getState().identity.getAnonymousId();
217
526
  },
218
527
  onAuthStateChange: (cb) => {
219
528
  authSubscribers.add(cb);
@@ -223,11 +532,21 @@ const Amba = {
223
532
  }
224
533
  },
225
534
  collections: {
226
- find: async (name, options = {}) => await coreCall((c) => c.collectionsFind(name, JSON.stringify(options))),
227
- findOne: async (name, id) => await coreCall((c) => c.collectionsFindOne(name, id)),
228
- insert: async (name, row) => await coreCall((c) => c.collectionsInsert(name, JSON.stringify(row))),
229
- update: async (name, id, set) => await coreCall((c) => c.collectionsUpdate(name, id, JSON.stringify(set))),
230
- delete: async (name, id) => await coreCall((c) => c.collectionsDelete(name, id)),
535
+ find: async (name, options = {}) => {
536
+ return getState().http.post(`${P.collections}/${encodePathSegment(name)}/find`, options);
537
+ },
538
+ findOne: async (name, id) => {
539
+ return (await getState().http.get(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`)).data;
540
+ },
541
+ insert: async (name, row) => {
542
+ return (await getState().http.post(`${P.collections}/${encodePathSegment(name)}`, row)).data;
543
+ },
544
+ update: async (name, id, set) => {
545
+ return (await getState().http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
546
+ },
547
+ delete: async (name, id) => {
548
+ return await getState().http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
549
+ },
231
550
  where: {
232
551
  eq: (column, value) => ({
233
552
  column,
@@ -293,141 +612,335 @@ const Amba = {
293
612
  }
294
613
  },
295
614
  storage: {
296
- presign: async (params) => await coreCall((c) => c.storagePresign(params.bucket, params.filename, params.mimeType, params.sizeBytes, params.retentionDays)),
297
- commit: async (uploadId, assetId) => await coreCall((c) => c.storageCommit(uploadId, assetId))
615
+ presign: async (params) => {
616
+ return (await getState().http.post(P.storagePresign, {
617
+ bucket: params.bucket,
618
+ filename: params.filename,
619
+ mime_type: params.mimeType,
620
+ size_bytes: params.sizeBytes,
621
+ retention_days: params.retentionDays
622
+ })).data;
623
+ },
624
+ commit: async (uploadId, assetId) => {
625
+ return (await getState().http.post(P.storageCommit, {
626
+ upload_id: uploadId,
627
+ asset_id: assetId
628
+ })).data;
629
+ }
298
630
  },
299
631
  push: {
300
- register: async (token, platform, bundleId) => await coreCall((c) => c.pushRegister(token, platform, bundleId)),
632
+ register: async (token, platform, bundleId) => {
633
+ return (await getState().http.post(P.pushRegister, {
634
+ token,
635
+ platform,
636
+ bundle_id: bundleId
637
+ })).data;
638
+ },
301
639
  unregister: async (token) => {
302
- await coreCall((c) => c.pushUnregister(token));
640
+ await getState().http.post(P.pushUnregister, { token });
641
+ },
642
+ getTokens: async () => {
643
+ return (await getState().http.get(P.pushTokens)).data;
303
644
  },
304
- getTokens: async () => await coreCall((c) => c.pushGetTokens()),
305
645
  subscribe: async (topic) => {
306
- await coreCall((c) => c.pushSubscribe(topic));
646
+ await getState().http.post(P.pushSubscribe, { topic });
307
647
  },
308
648
  unsubscribe: async (topic) => {
309
- await coreCall((c) => c.pushUnsubscribe(topic));
649
+ await getState().http.post(P.pushUnsubscribe, { topic });
310
650
  }
311
651
  },
312
652
  entitlements: {
313
- list: async () => await coreCall((c) => c.entitlementsList()),
314
- has: async (name) => await coreCall((c) => c.entitlementsHas(name))
653
+ list: async () => {
654
+ return (await getState().http.get(P.entitlements)).data;
655
+ },
656
+ has: async (name) => {
657
+ return (await getState().http.get(P.entitlements, { active_only: "true" })).data.some((e) => e.name === name && e.is_active);
658
+ }
315
659
  },
316
- ai: { anthropic: { messages: { create: async (request) => await coreCall((c) => c.aiAnthropicMessages(JSON.stringify(request))) } } },
317
- config: { fetch: async () => await coreCall((c) => c.configFetch()) },
318
- flags: { fetch: async () => await coreCall((c) => c.flagsFetch()) },
660
+ ai: { anthropic: { messages: { create: async (request) => {
661
+ return (await getState().http.post(P.aiAnthropic, request)).data;
662
+ } } } },
663
+ config: { fetch: async () => {
664
+ const s = getState();
665
+ const url = s.http.url(P.config);
666
+ const headers = s.http.buildHeaders();
667
+ let response;
668
+ try {
669
+ response = await (globalThis.fetch ?? fetch)(url, {
670
+ method: "GET",
671
+ headers
672
+ });
673
+ } catch (err) {
674
+ throw new AmbaApiError("NETWORK_ERROR", `Network error: ${err instanceof Error ? err.message : String(err)}`, err);
675
+ }
676
+ const text = await response.text();
677
+ if (!response.ok) {
678
+ const ct = response.headers.get("content-type") ?? "";
679
+ throw mapHttpError(response, text, ct.includes("application/json"));
680
+ }
681
+ let parsed = {};
682
+ if (text) try {
683
+ parsed = JSON.parse(text);
684
+ } catch {
685
+ parsed = {};
686
+ }
687
+ const etag = response.headers.get("etag");
688
+ let version = null;
689
+ if (etag) {
690
+ const cleaned = etag.replace(/^"|"$/g, "");
691
+ const m = cleaned.match(/^(v\d+)/);
692
+ version = m ? m[1] ?? null : cleaned || null;
693
+ }
694
+ return {
695
+ version,
696
+ values: parsed.data ?? {}
697
+ };
698
+ } },
699
+ flags: { fetch: async () => {
700
+ return (await getState().http.get(P.flags)).data;
701
+ } },
319
702
  achievements: {
320
- getAll: async () => await coreCall((c) => c.achievementsGetAll()),
321
- getProgress: async () => await coreCall((c) => c.achievementsGetProgress())
703
+ getAll: async () => {
704
+ return (await getState().http.get(P.achievements)).data;
705
+ },
706
+ getProgress: async () => {
707
+ return (await getState().http.get(`${P.achievements}/progress`)).data;
708
+ }
322
709
  },
323
710
  challenges: {
324
- getActive: async () => await coreCall((c) => c.challengesGetActive()),
325
- get: async (id) => await coreCall((c) => c.challengesGet(id)),
326
- getProgress: async (id) => await coreCall((c) => c.challengesGetProgress(id)),
327
- claim: async (id) => await coreCall((c) => c.challengesClaim(id))
711
+ getActive: async () => {
712
+ return (await getState().http.get(`${P.challenges}/active`)).data;
713
+ },
714
+ get: async (id) => {
715
+ return (await getState().http.get(`${P.challenges}/${encodePathSegment(id)}`)).data;
716
+ },
717
+ getProgress: async (id) => {
718
+ return (await getState().http.get(`${P.challenges}/${encodePathSegment(id)}/progress`)).data;
719
+ },
720
+ claim: async (id) => {
721
+ return (await getState().http.post(`${P.challenges}/${encodePathSegment(id)}/claim`, {})).data;
722
+ }
328
723
  },
329
724
  currencies: {
330
- getBalance: async () => await coreCall((c) => c.currenciesGetBalance()),
331
- getTransactions: async (currencyKey) => await coreCall((c) => c.currenciesGetTransactions(currencyKey))
725
+ getBalance: async () => {
726
+ return (await getState().http.get(P.currencies)).data;
727
+ },
728
+ getTransactions: async (currencyKey) => {
729
+ return (await getState().http.get(`${P.currencies}/${encodePathSegment(currencyKey)}/transactions`)).data;
730
+ }
332
731
  },
333
732
  inventory: {
334
- getItems: async () => await coreCall((c) => c.inventoryGetItems()),
335
- getItem: async (id) => await coreCall((c) => c.inventoryGetItem(id)),
336
- purchase: async (request) => await coreCall((c) => c.inventoryPurchase(JSON.stringify(request))),
337
- consume: async (request) => await coreCall((c) => c.inventoryConsume(JSON.stringify(request)))
733
+ getItems: async () => {
734
+ return (await getState().http.get(P.inventory)).data;
735
+ },
736
+ getItem: async (id) => {
737
+ return (await getState().http.get(`${P.inventory}/${encodePathSegment(id)}`)).data;
738
+ },
739
+ purchase: async (request) => {
740
+ return (await getState().http.post(`${P.inventory}/purchase`, request)).data;
741
+ },
742
+ consume: async (request) => {
743
+ return (await getState().http.post(`${P.inventory}/consume`, request)).data;
744
+ }
338
745
  },
339
746
  leaderboards: {
340
- get: async (key) => await coreCall((c) => c.leaderboardsGet(key)),
341
- getEntries: async (key, limit) => await coreCall((c) => c.leaderboardsGetEntries(key, limit ?? void 0)),
342
- getMyRank: async (key) => await coreCall((c) => c.leaderboardsGetMyRank(key))
747
+ get: async (key) => {
748
+ return (await getState().http.get(`${P.leaderboards}/${encodePathSegment(key)}`)).data;
749
+ },
750
+ getEntries: async (key, limit) => {
751
+ return (await getState().http.get(`${P.leaderboards}/${encodePathSegment(key)}/entries`, limit !== void 0 ? { limit } : void 0)).data;
752
+ },
753
+ getMyRank: async (key) => {
754
+ return (await getState().http.get(`${P.leaderboards}/${encodePathSegment(key)}/me`)).data;
755
+ }
343
756
  },
344
757
  stores: {
345
- list: async () => await coreCall((c) => c.storesList()),
346
- getPurchaseOptions: async (storeKey) => await coreCall((c) => c.storesGetPurchaseOptions(storeKey)),
347
- purchase: async (storeKey, purchaseOptionId, receipt) => await coreCall((c) => c.storesPurchase(storeKey, purchaseOptionId, JSON.stringify(receipt)))
758
+ list: async () => {
759
+ return (await getState().http.get(P.stores)).data;
760
+ },
761
+ getPurchaseOptions: async (storeKey) => {
762
+ return (await getState().http.get(`${P.stores}/${encodePathSegment(storeKey)}/options`)).data;
763
+ },
764
+ purchase: async (storeKey, purchaseOptionId, receipt) => {
765
+ return (await getState().http.post(`${P.stores}/${encodePathSegment(storeKey)}/purchase`, {
766
+ purchase_option_id: purchaseOptionId,
767
+ receipt
768
+ })).data;
769
+ }
348
770
  },
349
771
  xp: {
350
- getBalance: async () => await coreCall((c) => c.xpGetBalance()),
351
- getHistory: async (limit) => await coreCall((c) => c.xpGetHistory(limit ?? void 0)),
352
- claim: async (grantKey) => await coreCall((c) => c.xpClaim(grantKey))
772
+ getBalance: async () => {
773
+ return (await getState().http.get(P.xp)).data;
774
+ },
775
+ getHistory: async (limit) => {
776
+ return (await getState().http.get(`${P.xp}/history`, limit !== void 0 ? { limit } : void 0)).data;
777
+ },
778
+ claim: async (grantKey) => {
779
+ return (await getState().http.post(`${P.xp}/claim`, { grant_key: grantKey })).data;
780
+ }
353
781
  },
354
782
  streaks: {
355
- getAll: async () => await coreCall((c) => c.streaksGetAll()),
356
- qualify: async (streakKey) => await coreCall((c) => c.streaksQualify(streakKey))
783
+ getAll: async () => {
784
+ return (await getState().http.get(P.streaks)).data;
785
+ },
786
+ qualify: async (streakKey) => {
787
+ return (await getState().http.post(`${P.streaks}/${encodePathSegment(streakKey)}/qualify`, {})).data;
788
+ }
357
789
  },
358
- feeds: { getActivity: async (feed, cursor) => await coreCall((c) => c.feedsGetActivity(feed ?? void 0, cursor ?? void 0)) },
790
+ feeds: { getActivity: async (feed, cursor) => {
791
+ const f = feed ?? "default";
792
+ return getState().http.get(`${P.feeds}/${encodePathSegment(f)}`, cursor ? { cursor } : void 0);
793
+ } },
359
794
  friends: {
360
- getList: async () => await coreCall((c) => c.friendsGetList()),
361
- getFriends: async () => await coreCall((c) => c.friendsGetFriends()),
362
- blockUser: async (userId) => await coreCall((c) => c.friendsBlockUser(userId)),
795
+ getList: async () => {
796
+ return (await getState().http.get(P.friends)).data;
797
+ },
798
+ getFriends: async () => {
799
+ return (await getState().http.get(P.friends, { state: "accepted" })).data;
800
+ },
801
+ blockUser: async (userId) => {
802
+ return (await getState().http.post(`${P.friends}/block`, { user_id: userId })).data;
803
+ },
363
804
  unblockUser: async (userId) => {
364
- await coreCall((c) => c.friendsUnblockUser(userId));
805
+ await getState().http.post(`${P.friends}/unblock`, { user_id: userId });
365
806
  },
366
807
  removeBlock: async (friendshipId) => {
367
- await coreCall((c) => c.friendsRemoveBlock(friendshipId));
808
+ await getState().http.delete(`${P.friends}/${encodePathSegment(friendshipId)}`);
368
809
  }
369
810
  },
370
811
  groups: {
371
- create: async (params) => await coreCall((c) => c.groupsCreate(JSON.stringify(params))),
372
- get: async (id) => await coreCall((c) => c.groupsGet(id)),
373
- update: async (id, patch) => await coreCall((c) => c.groupsUpdate(id, JSON.stringify(patch))),
812
+ create: async (params) => {
813
+ return (await getState().http.post(P.groups, params)).data;
814
+ },
815
+ get: async (id) => {
816
+ return (await getState().http.get(`${P.groups}/${encodePathSegment(id)}`)).data;
817
+ },
818
+ update: async (id, patch) => {
819
+ return (await getState().http.patch(`${P.groups}/${encodePathSegment(id)}`, patch)).data;
820
+ },
374
821
  delete: async (id) => {
375
- await coreCall((c) => c.groupsDelete(id));
822
+ await getState().http.delete(`${P.groups}/${encodePathSegment(id)}`);
823
+ },
824
+ getMembers: async (id) => {
825
+ return (await getState().http.get(`${P.groups}/${encodePathSegment(id)}/members`)).data;
826
+ },
827
+ join: async (id) => {
828
+ return (await getState().http.post(`${P.groups}/${encodePathSegment(id)}/join`, {})).data;
376
829
  },
377
- getMembers: async (id) => await coreCall((c) => c.groupsGetMembers(id)),
378
- join: async (id) => await coreCall((c) => c.groupsJoin(id)),
379
830
  leave: async (id) => {
380
- await coreCall((c) => c.groupsLeave(id));
831
+ await getState().http.post(`${P.groups}/${encodePathSegment(id)}/leave`, {});
381
832
  },
382
- invite: async (id, userId) => await coreCall((c) => c.groupsInvite(id, userId))
833
+ invite: async (id, userId) => {
834
+ return (await getState().http.post(`${P.groups}/${encodePathSegment(id)}/invite`, { user_id: userId })).data;
835
+ }
383
836
  },
384
837
  messaging: {
385
- getConversations: async () => await coreCall((c) => c.messagingGetConversations()),
386
- getMessage: async (id) => await coreCall((c) => c.messagingGetMessage(id)),
387
- sendMessage: async (request) => await coreCall((c) => c.messagingSendMessage(JSON.stringify(request)))
838
+ getConversations: async () => {
839
+ return (await getState().http.get(`${P.messaging}/conversations`)).data;
840
+ },
841
+ getMessage: async (id) => {
842
+ return (await getState().http.get(`${P.messaging}/messages/${encodePathSegment(id)}`)).data;
843
+ },
844
+ sendMessage: async (request) => {
845
+ return (await getState().http.post(`${P.messaging}/messages`, request)).data;
846
+ }
388
847
  },
389
848
  moderation: {
390
- reportUser: async (request) => await coreCall((c) => c.moderationReportUser(JSON.stringify(request))),
391
- reportContent: async (request) => await coreCall((c) => c.moderationReportContent(JSON.stringify(request))),
392
- getReportStatus: async (id) => await coreCall((c) => c.moderationGetReportStatus(id))
849
+ reportUser: async (request) => {
850
+ return (await getState().http.post(`${P.moderation}/reports/users`, request)).data;
851
+ },
852
+ reportContent: async (request) => {
853
+ return (await getState().http.post(`${P.moderation}/reports/content`, request)).data;
854
+ },
855
+ getReportStatus: async (id) => {
856
+ return (await getState().http.get(`${P.moderation}/reports/${encodePathSegment(id)}`)).data;
857
+ }
393
858
  },
394
859
  reviews: {
395
- list: async (targetType, targetId) => await coreCall((c) => c.reviewsList(targetType, targetId)),
396
- create: async (params) => await coreCall((c) => c.reviewsCreate(JSON.stringify(params))),
397
- update: async (id, patch) => await coreCall((c) => c.reviewsUpdate(id, JSON.stringify(patch))),
860
+ list: async (targetType, targetId) => {
861
+ return (await getState().http.get(P.reviews, {
862
+ target_type: targetType,
863
+ target_id: targetId
864
+ })).data;
865
+ },
866
+ create: async (params) => {
867
+ return (await getState().http.post(P.reviews, params)).data;
868
+ },
869
+ update: async (id, patch) => {
870
+ return (await getState().http.patch(`${P.reviews}/${encodePathSegment(id)}`, patch)).data;
871
+ },
398
872
  delete: async (id) => {
399
- await coreCall((c) => c.reviewsDelete(id));
873
+ await getState().http.delete(`${P.reviews}/${encodePathSegment(id)}`);
400
874
  }
401
875
  },
402
876
  roles: {
403
- getMyRoles: async () => await coreCall((c) => c.rolesGetMyRoles()),
404
- hasPermission: async (permission) => await coreCall((c) => c.rolesHasPermission(permission))
877
+ getMyRoles: async () => {
878
+ return (await getState().http.get(`${P.roles}/me`)).data;
879
+ },
880
+ hasPermission: async (permission) => {
881
+ return (await getState().http.get(`${P.roles}/check`, { permission })).data.allowed === true;
882
+ }
405
883
  },
406
884
  referrals: {
407
- getReferralCode: async () => await coreCall((c) => c.referralsGetReferralCode()),
408
- claimReferral: async (code) => await coreCall((c) => c.referralsClaimReferral(code)),
409
- create: async (code, maxUses) => await coreCall((c) => c.referralsCreate(code ?? void 0, maxUses ?? void 0))
885
+ getReferralCode: async () => {
886
+ return (await getState().http.get(`${P.referrals}/me`)).data;
887
+ },
888
+ claimReferral: async (code) => {
889
+ return (await getState().http.post(`${P.referrals}/claim`, { code })).data;
890
+ },
891
+ create: async (code, maxUses) => {
892
+ const body = {};
893
+ if (code !== void 0) body.code = code;
894
+ if (maxUses !== void 0) body.max_uses = maxUses;
895
+ return (await getState().http.post(P.referrals, body)).data;
896
+ }
410
897
  },
411
- catalog: { list: async () => await coreCall((c) => c.catalogList()) },
898
+ catalog: { list: async () => {
899
+ return (await getState().http.get(P.catalog)).data;
900
+ } },
412
901
  content: {
413
- getToday: async (channel = "default") => await coreCall((c) => c.contentGetToday(channel)),
414
- getLibrary: async (channel = "default", options) => await coreCall((c) => c.contentGetLibrary(channel, options?.limit ?? void 0, options?.cursor ?? void 0)),
415
- getItem: async (id) => await coreCall((c) => c.contentGetItem(id)),
416
- updateItem: async (id, state) => await coreCall((c) => c.contentUpdateItem(id, JSON.stringify(state))),
417
- createItem: async (channel, item) => await coreCall((c) => c.contentCreateItem(channel, JSON.stringify(item)))
902
+ getToday: async (channel = "default") => {
903
+ return (await getState().http.get(`${P.content}/today/${encodePathSegment(channel)}`)).data ?? null;
904
+ },
905
+ getLibrary: async (channel = "default", options) => {
906
+ return (await getState().http.get(`${P.content}/library/${encodePathSegment(channel)}`, {
907
+ limit: options?.limit,
908
+ cursor: options?.cursor
909
+ })).data;
910
+ },
911
+ getItem: async (id) => {
912
+ return (await getState().http.get(`${P.content}/${encodePathSegment(id)}`)).data;
913
+ },
914
+ updateItem: async (id, state_) => {
915
+ return (await getState().http.post(`${P.content}/${encodePathSegment(id)}/state`, state_)).data;
916
+ },
917
+ createItem: async (channel, item) => {
918
+ return (await getState().http.post(`${P.content}/${encodePathSegment(channel)}`, item)).data;
919
+ }
418
920
  },
419
921
  deepLinks: {
420
- get: async (shortCode) => await coreCall((c) => c.deepLinksGet(shortCode)),
421
- create: async (params) => await coreCall((c) => c.deepLinksCreate(JSON.stringify(params)))
922
+ get: async (shortCode) => {
923
+ return (await getState().http.get(`${P.deepLinks}/${encodePathSegment(shortCode)}`)).data;
924
+ },
925
+ create: async (params) => {
926
+ return (await getState().http.post(P.deepLinks, params)).data;
927
+ }
422
928
  },
423
929
  onboarding: {
424
- getStatus: async () => await coreCall((c) => c.onboardingGetStatus()),
425
- nextStep: async (payload) => await coreCall((c) => c.onboardingNextStep(JSON.stringify(payload))),
426
- skipStep: async () => await coreCall((c) => c.onboardingSkipStep()),
427
- complete: async () => await coreCall((c) => c.onboardingComplete())
930
+ getStatus: async () => {
931
+ return (await getState().http.get(P.onboarding)).data;
932
+ },
933
+ nextStep: async (payload) => {
934
+ return (await getState().http.post(`${P.onboarding}/next`, payload)).data;
935
+ },
936
+ skipStep: async () => {
937
+ return (await getState().http.post(`${P.onboarding}/skip`, {})).data;
938
+ },
939
+ complete: async () => {
940
+ return (await getState().http.post(`${P.onboarding}/complete`, {})).data;
941
+ }
428
942
  }
429
943
  };
430
- const SDK_VERSION = "0.1.0";
431
944
 
432
945
  //#endregion
433
946
  exports.Amba = Amba;