@layers/amba-web 1.0.1 → 1.0.2

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.
@@ -0,0 +1,488 @@
1
+ import init, { AmbaCoreWasm } from "@layers/amba-core";
2
+
3
+ //#region src/storage-adapter.ts
4
+ /**
5
+ * Browser localStorage adapter for SDK identity persistence.
6
+ *
7
+ * The SDK runtime currently re-seeds an `anonymous_id` on every page load.
8
+ * This adapter is the documented home for storage-prefix handling so
9
+ * customers can override the localStorage key prefix used by the
10
+ * (eventual) identity cache.
11
+ */
12
+ var LocalStoragePersistence = class {
13
+ constructor(prefix) {
14
+ this.prefix = prefix;
15
+ }
16
+ /** Read everything we'd want to hydrate at init. No-op until wired through. */
17
+ hydrate() {}
18
+ read(key) {
19
+ if (typeof localStorage === "undefined") return null;
20
+ return localStorage.getItem(`${this.prefix}:${key}`);
21
+ }
22
+ write(key, value) {
23
+ if (typeof localStorage === "undefined") return;
24
+ localStorage.setItem(`${this.prefix}:${key}`, value);
25
+ }
26
+ delete(key) {
27
+ if (typeof localStorage === "undefined") return;
28
+ localStorage.removeItem(`${this.prefix}:${key}`);
29
+ }
30
+ };
31
+
32
+ //#endregion
33
+ //#region src/json.ts
34
+ /**
35
+ * JSON encode helper — guarantees stable serialization across browser
36
+ * runtimes and provides a single hook for future schema validation.
37
+ */
38
+ function encodeJson(value) {
39
+ return JSON.stringify(value);
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/error.ts
44
+ var AmbaApiError = class extends Error {
45
+ code;
46
+ details;
47
+ constructor(code, message, details) {
48
+ super(message);
49
+ this.name = "AmbaApiError";
50
+ this.code = code;
51
+ this.details = details;
52
+ Object.setPrototypeOf(this, new.target.prototype);
53
+ }
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";
80
+ }
81
+ /**
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. runtime-emitted error) → infer code from message prefix.
90
+ * 4. Anything else → stringify and tag UNKNOWN_ERROR.
91
+ */
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));
100
+ }
101
+
102
+ //#endregion
103
+ //#region src/index.ts
104
+ /**
105
+ * Internal state — one instance per page. Created lazily by `Amba.configure()`.
106
+ */
107
+ let coreInstance = null;
108
+ function requireCore() {
109
+ if (!coreInstance) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ apiKey }) at app start before any other API.");
110
+ return coreInstance;
111
+ }
112
+ /**
113
+ * Run a single SDK-runtime call and rewrap any thrown value into a
114
+ * stable `AmbaApiError`. Every method on `Amba` funnels through this
115
+ * so callers see one error shape regardless of which layer failed
116
+ * (network, validation, etc.). Sync `requireCore()` accessors
117
+ * (e.g. the `anonymousId` getter) already throw the typed shape on
118
+ * their own.
119
+ */
120
+ async function coreCall(fn) {
121
+ try {
122
+ return await fn(requireCore());
123
+ } catch (err) {
124
+ throw toAmbaApiError(err);
125
+ }
126
+ }
127
+ const authSubscribers = /* @__PURE__ */ new Set();
128
+ function snapshotSession(user) {
129
+ if (!user) return null;
130
+ return {
131
+ sessionToken: "",
132
+ refreshToken: "",
133
+ user,
134
+ expiresAt: ""
135
+ };
136
+ }
137
+ function notifyAuthSubscribers(session) {
138
+ for (const cb of authSubscribers) try {
139
+ cb(session);
140
+ } catch (err) {
141
+ if (typeof console !== "undefined") console.error("[amba] onAuthStateChange callback threw", err);
142
+ }
143
+ }
144
+ async function notifyFromAuthResult(result) {
145
+ notifyAuthSubscribers(snapshotSession(result.user));
146
+ }
147
+ /**
148
+ * Top-level SDK namespace.
149
+ *
150
+ * Stateless from the JS side — all state lives in the SDK runtime. Calling
151
+ * `Amba.configure()` multiple times replaces the previous instance.
152
+ */
153
+ const Amba = {
154
+ configure: async (config) => {
155
+ if (!config.apiKey || config.apiKey.trim() === "") throw new Error("Amba.configure: apiKey is required");
156
+ await init(); // Hermes-stripped Node-init block (DX-15) — see scripts/build-rn.mjs
157
+ new LocalStoragePersistence(config.storagePrefix ?? "amba").hydrate();
158
+ coreInstance = AmbaCoreWasm.init(encodeJson({
159
+ api_key: config.apiKey,
160
+ base_url: config.baseUrl,
161
+ sdk_platform: "web",
162
+ sdk_wrapper_version: `amba-web/${SDK_VERSION}`,
163
+ consent_required: config.consentRequired,
164
+ debug: config.debug
165
+ }), globalThis.fetch.bind(globalThis));
166
+ },
167
+ get anonymousId() {
168
+ return requireCore().anonymousId();
169
+ },
170
+ get appUserId() {
171
+ return requireCore().appUserId() ?? void 0;
172
+ },
173
+ get isAuthenticated() {
174
+ return requireCore().isAuthenticated();
175
+ },
176
+ setDebug: (enabled) => {
177
+ requireCore().setDebug(enabled);
178
+ },
179
+ events: { track: async (event, properties) => {
180
+ await coreCall((c) => c.track(event, properties ? encodeJson(properties) : void 0));
181
+ } },
182
+ auth: {
183
+ signInAnonymously: async () => {
184
+ const r = await coreCall((c) => c.signInAnonymously());
185
+ await notifyFromAuthResult(r);
186
+ return r;
187
+ },
188
+ signInWithEmail: async (email, password) => {
189
+ const r = await coreCall((c) => c.signInWithEmail(email, password));
190
+ await notifyFromAuthResult(r);
191
+ return r;
192
+ },
193
+ signUpWithEmail: async (email, password) => {
194
+ const r = await coreCall((c) => c.signUpWithEmail(email, password));
195
+ await notifyFromAuthResult(r);
196
+ return r;
197
+ },
198
+ signInWithSocial: async (provider, idToken) => {
199
+ const r = await coreCall((c) => c.signInWithSocial(provider, idToken));
200
+ await notifyFromAuthResult(r);
201
+ return r;
202
+ },
203
+ signOut: async (rotateAnonymousId = false) => {
204
+ await coreCall((c) => c.signOut(rotateAnonymousId));
205
+ notifyAuthSubscribers(null);
206
+ },
207
+ refresh: async () => {
208
+ const r = await coreCall((c) => c.refreshSession());
209
+ await notifyFromAuthResult(r);
210
+ return r;
211
+ },
212
+ me: async () => {
213
+ return await coreCall((c) => c.me());
214
+ },
215
+ getSession: async () => {
216
+ if (!requireCore().isAuthenticated()) return null;
217
+ return snapshotSession(await coreCall((c) => c.me()));
218
+ },
219
+ getAnonymousId: async () => {
220
+ return requireCore().anonymousId();
221
+ },
222
+ onAuthStateChange: (cb) => {
223
+ authSubscribers.add(cb);
224
+ return () => {
225
+ authSubscribers.delete(cb);
226
+ };
227
+ }
228
+ },
229
+ collections: {
230
+ find: async (name, options = {}) => {
231
+ return await coreCall((c) => c.collectionsFind(name, encodeJson(options)));
232
+ },
233
+ findOne: async (name, id) => {
234
+ return await coreCall((c) => c.collectionsFindOne(name, id));
235
+ },
236
+ insert: async (name, row) => {
237
+ return await coreCall((c) => c.collectionsInsert(name, encodeJson(row)));
238
+ },
239
+ update: async (name, id, set) => {
240
+ return await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set)));
241
+ },
242
+ delete: async (name, id) => {
243
+ return await coreCall((c) => c.collectionsDelete(name, id));
244
+ },
245
+ where: {
246
+ eq: (column, value) => ({
247
+ column,
248
+ op: "eq",
249
+ value
250
+ }),
251
+ ne: (column, value) => ({
252
+ column,
253
+ op: "ne",
254
+ value
255
+ }),
256
+ gt: (column, value) => ({
257
+ column,
258
+ op: "gt",
259
+ value
260
+ }),
261
+ gte: (column, value) => ({
262
+ column,
263
+ op: "gte",
264
+ value
265
+ }),
266
+ lt: (column, value) => ({
267
+ column,
268
+ op: "lt",
269
+ value
270
+ }),
271
+ lte: (column, value) => ({
272
+ column,
273
+ op: "lte",
274
+ value
275
+ }),
276
+ in: (column, values) => ({
277
+ column,
278
+ op: "in",
279
+ value: values
280
+ }),
281
+ notIn: (column, values) => ({
282
+ column,
283
+ op: "not_in",
284
+ value: values
285
+ }),
286
+ like: (column, pattern) => ({
287
+ column,
288
+ op: "like",
289
+ value: pattern
290
+ }),
291
+ ilike: (column, pattern) => ({
292
+ column,
293
+ op: "ilike",
294
+ value: pattern
295
+ }),
296
+ isNull: (column) => ({
297
+ column,
298
+ op: "is_null"
299
+ }),
300
+ isNotNull: (column) => ({
301
+ column,
302
+ op: "is_not_null"
303
+ }),
304
+ and: (...filters) => ({ and: filters }),
305
+ or: (...filters) => ({ or: filters }),
306
+ not: (filter) => ({ not: filter })
307
+ }
308
+ },
309
+ storage: {
310
+ presign: async (params) => {
311
+ return await coreCall((c) => c.storagePresign(params.bucket, params.filename, params.mimeType, params.sizeBytes, params.retentionDays ?? void 0));
312
+ },
313
+ upload: async (params) => {
314
+ const filename = params.filename ?? ("name" in params.file ? params.file.name : "upload");
315
+ const mimeType = params.file.type || "application/octet-stream";
316
+ const presign = await Amba.storage.presign({
317
+ bucket: params.bucket,
318
+ filename,
319
+ mimeType,
320
+ sizeBytes: params.file.size,
321
+ retentionDays: params.retentionDays
322
+ });
323
+ const uploadHeaders = {};
324
+ for (const [k, v] of presign.upload_headers) uploadHeaders[k] = v;
325
+ const putResp = await fetch(presign.upload_url, {
326
+ method: "PUT",
327
+ headers: uploadHeaders,
328
+ body: params.file
329
+ });
330
+ if (!putResp.ok) throw new Error(`upload to R2 failed: status ${putResp.status}`);
331
+ return await coreCall((c) => c.storageCommit(presign.upload_id, presign.asset_id));
332
+ }
333
+ },
334
+ push: {
335
+ register: async (token, platform, bundleId) => {
336
+ return await coreCall((c) => c.pushRegister(token, platform, bundleId ?? void 0));
337
+ },
338
+ unregister: async (token) => {
339
+ await coreCall((c) => c.pushUnregister(token));
340
+ },
341
+ getTokens: async () => {
342
+ return await coreCall((c) => c.pushGetTokens());
343
+ },
344
+ subscribe: async (topic) => {
345
+ await coreCall((c) => c.pushSubscribe(topic));
346
+ },
347
+ unsubscribe: async (topic) => {
348
+ await coreCall((c) => c.pushUnsubscribe(topic));
349
+ }
350
+ },
351
+ entitlements: {
352
+ list: async () => {
353
+ return await coreCall((c) => c.entitlementsList());
354
+ },
355
+ has: async (name) => {
356
+ return await coreCall((c) => c.entitlementsHas(name));
357
+ }
358
+ },
359
+ ai: { anthropic: { messages: { create: async (request) => {
360
+ return await coreCall((c) => c.aiAnthropicMessages(encodeJson(request)));
361
+ } } } },
362
+ config: { fetch: async () => {
363
+ return await coreCall((c) => c.configFetch());
364
+ } },
365
+ flags: { fetch: async () => {
366
+ return await coreCall((c) => c.flagsFetch());
367
+ } },
368
+ achievements: {
369
+ getAll: async () => await coreCall((c) => c.achievementsGetAll()),
370
+ getProgress: async () => await coreCall((c) => c.achievementsGetProgress())
371
+ },
372
+ challenges: {
373
+ getActive: async () => await coreCall((c) => c.challengesGetActive()),
374
+ get: async (id) => await coreCall((c) => c.challengesGet(id)),
375
+ getProgress: async (id) => await coreCall((c) => c.challengesGetProgress(id)),
376
+ claim: async (id) => await coreCall((c) => c.challengesClaim(id))
377
+ },
378
+ currencies: {
379
+ getBalance: async () => await coreCall((c) => c.currenciesGetBalance()),
380
+ getTransactions: async (currencyKey) => await coreCall((c) => c.currenciesGetTransactions(currencyKey))
381
+ },
382
+ inventory: {
383
+ getItems: async () => await coreCall((c) => c.inventoryGetItems()),
384
+ getItem: async (id) => await coreCall((c) => c.inventoryGetItem(id)),
385
+ purchase: async (request) => await coreCall((c) => c.inventoryPurchase(encodeJson(request))),
386
+ consume: async (request) => await coreCall((c) => c.inventoryConsume(encodeJson(request)))
387
+ },
388
+ leaderboards: {
389
+ get: async (key) => await coreCall((c) => c.leaderboardsGet(key)),
390
+ getEntries: async (key, limit) => await coreCall((c) => c.leaderboardsGetEntries(key, limit ?? void 0)),
391
+ getMyRank: async (key) => await coreCall((c) => c.leaderboardsGetMyRank(key))
392
+ },
393
+ stores: {
394
+ list: async () => await coreCall((c) => c.storesList()),
395
+ getPurchaseOptions: async (storeKey) => await coreCall((c) => c.storesGetPurchaseOptions(storeKey)),
396
+ purchase: async (storeKey, purchaseOptionId, receipt) => await coreCall((c) => c.storesPurchase(storeKey, purchaseOptionId, encodeJson(receipt)))
397
+ },
398
+ xp: {
399
+ getBalance: async () => await coreCall((c) => c.xpGetBalance()),
400
+ getHistory: async (limit) => await coreCall((c) => c.xpGetHistory(limit ?? void 0)),
401
+ claim: async (grantKey) => await coreCall((c) => c.xpClaim(grantKey))
402
+ },
403
+ streaks: {
404
+ getAll: async () => await coreCall((c) => c.streaksGetAll()),
405
+ qualify: async (streakKey) => await coreCall((c) => c.streaksQualify(streakKey))
406
+ },
407
+ feeds: { getActivity: async (feed, cursor) => await coreCall((c) => c.feedsGetActivity(feed ?? void 0, cursor ?? void 0)) },
408
+ friends: {
409
+ getList: async () => await coreCall((c) => c.friendsGetList()),
410
+ getFriends: async () => await coreCall((c) => c.friendsGetFriends()),
411
+ blockUser: async (userId) => await coreCall((c) => c.friendsBlockUser(userId)),
412
+ unblockUser: async (userId) => {
413
+ await coreCall((c) => c.friendsUnblockUser(userId));
414
+ },
415
+ removeBlock: async (friendshipId) => {
416
+ await coreCall((c) => c.friendsRemoveBlock(friendshipId));
417
+ }
418
+ },
419
+ groups: {
420
+ create: async (params) => await coreCall((c) => c.groupsCreate(encodeJson(params))),
421
+ get: async (id) => await coreCall((c) => c.groupsGet(id)),
422
+ update: async (id, patch) => await coreCall((c) => c.groupsUpdate(id, encodeJson(patch))),
423
+ delete: async (id) => {
424
+ await coreCall((c) => c.groupsDelete(id));
425
+ },
426
+ getMembers: async (id) => await coreCall((c) => c.groupsGetMembers(id)),
427
+ join: async (id) => await coreCall((c) => c.groupsJoin(id)),
428
+ leave: async (id) => {
429
+ await coreCall((c) => c.groupsLeave(id));
430
+ },
431
+ invite: async (id, userId) => await coreCall((c) => c.groupsInvite(id, userId))
432
+ },
433
+ messaging: {
434
+ getConversations: async () => await coreCall((c) => c.messagingGetConversations()),
435
+ getMessage: async (id) => await coreCall((c) => c.messagingGetMessage(id)),
436
+ sendMessage: async (request) => await coreCall((c) => c.messagingSendMessage(encodeJson(request)))
437
+ },
438
+ moderation: {
439
+ reportUser: async (request) => await coreCall((c) => c.moderationReportUser(encodeJson(request))),
440
+ reportContent: async (request) => await coreCall((c) => c.moderationReportContent(encodeJson(request))),
441
+ getReportStatus: async (id) => await coreCall((c) => c.moderationGetReportStatus(id))
442
+ },
443
+ reviews: {
444
+ list: async (targetType, targetId) => await coreCall((c) => c.reviewsList(targetType, targetId)),
445
+ create: async (params) => await coreCall((c) => c.reviewsCreate(encodeJson(params))),
446
+ update: async (id, patch) => await coreCall((c) => c.reviewsUpdate(id, encodeJson(patch))),
447
+ delete: async (id) => {
448
+ await coreCall((c) => c.reviewsDelete(id));
449
+ }
450
+ },
451
+ roles: {
452
+ getMyRoles: async () => await coreCall((c) => c.rolesGetMyRoles()),
453
+ hasPermission: async (permission) => await coreCall((c) => c.rolesHasPermission(permission))
454
+ },
455
+ referrals: {
456
+ getReferralCode: async () => await coreCall((c) => c.referralsGetReferralCode()),
457
+ claimReferral: async (code) => await coreCall((c) => c.referralsClaimReferral(code)),
458
+ create: async (code, maxUses) => await coreCall((c) => c.referralsCreate(code ?? void 0, maxUses ?? void 0))
459
+ },
460
+ catalog: { list: async () => await coreCall((c) => c.catalogList()) },
461
+ content: {
462
+ getToday: async (channel = "default") => await coreCall((c) => c.contentGetToday(channel)),
463
+ getLibrary: async (channel = "default", options) => await coreCall((c) => c.contentGetLibrary(channel, options?.limit ?? void 0, options?.cursor ?? void 0)),
464
+ getItem: async (id) => await coreCall((c) => c.contentGetItem(id)),
465
+ updateItem: async (id, state) => await coreCall((c) => c.contentUpdateItem(id, encodeJson(state))),
466
+ createItem: async (channel, item) => await coreCall((c) => c.contentCreateItem(channel, encodeJson(item)))
467
+ },
468
+ deepLinks: {
469
+ get: async (shortCode) => await coreCall((c) => c.deepLinksGet(shortCode)),
470
+ create: async (params) => await coreCall((c) => c.deepLinksCreate(encodeJson(params)))
471
+ },
472
+ onboarding: {
473
+ getStatus: async () => await coreCall((c) => c.onboardingGetStatus()),
474
+ nextStep: async (payload) => await coreCall((c) => c.onboardingNextStep(encodeJson(payload))),
475
+ skipStep: async () => await coreCall((c) => c.onboardingSkipStep()),
476
+ complete: async () => await coreCall((c) => c.onboardingComplete())
477
+ }
478
+ };
479
+ /** Escape hatch — the raw SDK runtime handle, for namespaces not yet wrapped. */
480
+ function getCoreWasm() {
481
+ return requireCore();
482
+ }
483
+ /** Package version stamped at build time. */
484
+ const SDK_VERSION = "0.1.0";
485
+
486
+ //#endregion
487
+ export { Amba, AmbaApiError, SDK_VERSION, getCoreWasm };
488
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba-web",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "amba SDK for browser apps — full 25-namespace surface (auth, collections, storage, events, push, entitlements, ai, config, flags, achievements, challenges, currencies, friends, groups, leaderboards, messaging, and more).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,11 +9,16 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
12
+ "react-native": "./dist-rn/index.js",
13
+ "browser": "./dist/index.js",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
13
16
  }
14
17
  },
15
18
  "files": [
16
19
  "dist",
20
+ "dist-rn/index.js",
21
+ "dist-rn/index.d.ts",
17
22
  "README.md"
18
23
  ],
19
24
  "license": "MIT",
@@ -27,7 +32,7 @@
27
32
  },
28
33
  "sideEffects": false,
29
34
  "dependencies": {
30
- "@layers/amba-core": "^1.0.0"
35
+ "@layers/amba-core": "^1.0.3"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@types/node": "^22.13.14",
@@ -45,7 +50,10 @@
45
50
  "last 2 edge versions"
46
51
  ],
47
52
  "scripts": {
48
- "build": "tsdown",
53
+ "build": "tsdown && node ./scripts/build-rn.mjs",
54
+ "build:web": "tsdown",
55
+ "build:rn": "node ./scripts/build-rn.mjs",
56
+ "verify:pkg": "node ./scripts/verify-pkg.mjs",
49
57
  "test": "vitest run",
50
58
  "test:watch": "vitest"
51
59
  }