@layers/amba-react-native 1.0.1 → 2.1.0

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