@layers/amba-react-native 4.0.7 → 4.0.10
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/README.md +6 -3
- package/dist/index.cjs +470 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +112 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +470 -91
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -263,8 +263,23 @@ var IdentityStore = class IdentityStore {
|
|
|
263
263
|
sessionToken = null;
|
|
264
264
|
refreshToken = null;
|
|
265
265
|
expiresAt = null;
|
|
266
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Credentials an earlier SDK version left in the AsyncStorage record, with
|
|
268
|
+
* the user they belong to. They stay on disk until the vault holds the
|
|
269
|
+
* session, so a vault write that fails or is skipped (or a process killed
|
|
270
|
+
* between the two writes) never loses the only stored copy.
|
|
271
|
+
*/
|
|
272
|
+
plaintext;
|
|
273
|
+
/**
|
|
274
|
+
* Set while the vault could not be read. Writes are held back until a read
|
|
275
|
+
* succeeds, so a session this launch never loaded is not overwritten.
|
|
276
|
+
*/
|
|
277
|
+
vaultUnreadable;
|
|
278
|
+
constructor(persistence, seed, vault, plaintext, vaultUnreadable) {
|
|
267
279
|
this.persistence = persistence;
|
|
280
|
+
this.vault = vault;
|
|
281
|
+
this.plaintext = plaintext;
|
|
282
|
+
this.vaultUnreadable = vaultUnreadable;
|
|
268
283
|
this.anonymousId = seed.anonymous_id;
|
|
269
284
|
this.appUserId = seed.app_user_id;
|
|
270
285
|
this.sessionToken = seed.session_token;
|
|
@@ -272,13 +287,19 @@ var IdentityStore = class IdentityStore {
|
|
|
272
287
|
this.expiresAt = seed.expires_at;
|
|
273
288
|
}
|
|
274
289
|
/**
|
|
275
|
-
* Build a store, hydrating any prior identity from AsyncStorage
|
|
276
|
-
* If no prior identity exists, a fresh `anonymous_id`
|
|
277
|
-
* Expired session tokens are dropped at hydration (the refresh
|
|
278
|
-
* survives so callers can `auth.refresh()` to mint a new pair).
|
|
290
|
+
* Build a store, hydrating any prior identity from AsyncStorage and the
|
|
291
|
+
* credential vault. If no prior identity exists, a fresh `anonymous_id`
|
|
292
|
+
* is minted. Expired session tokens are dropped at hydration (the refresh
|
|
293
|
+
* token survives so callers can `auth.refresh()` to mint a new pair).
|
|
294
|
+
*
|
|
295
|
+
* With a vault, credentials an earlier SDK version left in the
|
|
296
|
+
* AsyncStorage record are used for this launch and moved into the vault;
|
|
297
|
+
* the AsyncStorage copy is removed once the vault write succeeds. A vault
|
|
298
|
+
* holding its own credentials wins over them.
|
|
279
299
|
*/
|
|
280
|
-
static async create(persistence) {
|
|
300
|
+
static async create(persistence, vault = null) {
|
|
281
301
|
const raw = await persistence.read(IDENTITY_KEY);
|
|
302
|
+
let recordPresent = false;
|
|
282
303
|
let seed;
|
|
283
304
|
if (raw) try {
|
|
284
305
|
const parsed = JSON.parse(raw);
|
|
@@ -289,16 +310,42 @@ var IdentityStore = class IdentityStore {
|
|
|
289
310
|
refresh_token: parsed.refresh_token ?? null,
|
|
290
311
|
expires_at: parsed.expires_at ?? null
|
|
291
312
|
};
|
|
313
|
+
recordPresent = typeof parsed.anonymous_id === "string";
|
|
292
314
|
} catch {
|
|
293
315
|
seed = freshSeed();
|
|
294
316
|
}
|
|
295
317
|
else seed = freshSeed();
|
|
318
|
+
const plaintext = vault && (seed.session_token !== null || seed.refresh_token !== null || seed.expires_at !== null) ? {
|
|
319
|
+
appUserId: seed.app_user_id,
|
|
320
|
+
slice: {
|
|
321
|
+
session_token: seed.session_token,
|
|
322
|
+
refresh_token: seed.refresh_token,
|
|
323
|
+
expires_at: seed.expires_at
|
|
324
|
+
}
|
|
325
|
+
} : null;
|
|
326
|
+
let vaultReadable = true;
|
|
327
|
+
if (vault) {
|
|
328
|
+
let fromVault = null;
|
|
329
|
+
try {
|
|
330
|
+
fromVault = await vault.read();
|
|
331
|
+
} catch {
|
|
332
|
+
vaultReadable = false;
|
|
333
|
+
}
|
|
334
|
+
const belongs = fromVault !== null && recordPresent && fromVault.anonymous_id === seed.anonymous_id;
|
|
335
|
+
if (fromVault && belongs) {
|
|
336
|
+
seed.session_token = fromVault.session_token;
|
|
337
|
+
seed.refresh_token = fromVault.refresh_token;
|
|
338
|
+
seed.expires_at = fromVault.expires_at;
|
|
339
|
+
if (fromVault.app_user_id != null) seed.app_user_id = fromVault.app_user_id;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (vaultReadable && seed.session_token === null && seed.refresh_token === null) seed.app_user_id = null;
|
|
296
343
|
if (isExpired(seed.expires_at)) {
|
|
297
344
|
seed.session_token = null;
|
|
298
345
|
seed.expires_at = null;
|
|
299
346
|
}
|
|
300
|
-
const store = new IdentityStore(persistence, seed);
|
|
301
|
-
await store.flush();
|
|
347
|
+
const store = new IdentityStore(persistence, seed, vault, plaintext, !vaultReadable);
|
|
348
|
+
await store.flush({ hydrating: true });
|
|
302
349
|
return store;
|
|
303
350
|
}
|
|
304
351
|
snapshot() {
|
|
@@ -338,17 +385,64 @@ var IdentityStore = class IdentityStore {
|
|
|
338
385
|
this.refreshToken = null;
|
|
339
386
|
this.expiresAt = null;
|
|
340
387
|
if (rotateAnonymousId) this.anonymousId = uuidv4();
|
|
341
|
-
await this.flush();
|
|
388
|
+
await this.flush({ signingOut: true });
|
|
342
389
|
}
|
|
343
|
-
|
|
344
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Persist the identity. `hydrating` marks the write that follows `create`:
|
|
392
|
+
* when the vault was unreadable and a retry now succeeds, the session it
|
|
393
|
+
* holds is adopted instead of overwritten. `signingOut` marks the write
|
|
394
|
+
* from `clearSession`: when the vault can't be emptied, the install moves
|
|
395
|
+
* to a new anonymous id so the entry left behind no longer belongs to it.
|
|
396
|
+
*/
|
|
397
|
+
async flush(opts) {
|
|
398
|
+
if (!this.vault) {
|
|
399
|
+
const payload = {
|
|
400
|
+
anonymous_id: this.anonymousId,
|
|
401
|
+
app_user_id: this.appUserId,
|
|
402
|
+
session_token: this.sessionToken,
|
|
403
|
+
refresh_token: this.refreshToken,
|
|
404
|
+
expires_at: this.expiresAt
|
|
405
|
+
};
|
|
406
|
+
await this.persistence.write(IDENTITY_KEY, JSON.stringify(payload));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let saved = false;
|
|
410
|
+
if (this.vaultUnreadable) try {
|
|
411
|
+
const stored = await this.vault.read();
|
|
412
|
+
this.vaultUnreadable = false;
|
|
413
|
+
if (opts?.hydrating && stored && (stored.session_token !== null || stored.refresh_token !== null) && stored.anonymous_id === this.anonymousId) {
|
|
414
|
+
this.sessionToken = isExpired(stored.expires_at) ? null : stored.session_token;
|
|
415
|
+
this.refreshToken = stored.refresh_token;
|
|
416
|
+
this.expiresAt = isExpired(stored.expires_at) ? null : stored.expires_at;
|
|
417
|
+
if (stored.app_user_id != null) this.appUserId = stored.app_user_id;
|
|
418
|
+
}
|
|
419
|
+
} catch {}
|
|
420
|
+
if (!this.vaultUnreadable) try {
|
|
421
|
+
await this.vault.write({
|
|
422
|
+
session_token: this.sessionToken,
|
|
423
|
+
refresh_token: this.refreshToken,
|
|
424
|
+
expires_at: this.expiresAt,
|
|
425
|
+
anonymous_id: this.anonymousId,
|
|
426
|
+
app_user_id: this.appUserId
|
|
427
|
+
});
|
|
428
|
+
saved = true;
|
|
429
|
+
} catch (err) {
|
|
430
|
+
if (typeof console !== "undefined") console.warn("[amba] could not save the session to the secure store", err);
|
|
431
|
+
}
|
|
432
|
+
if (opts?.signingOut && !saved) {
|
|
433
|
+
this.anonymousId = uuidv4();
|
|
434
|
+
if (typeof console !== "undefined") console.warn("[amba] could not clear the secure store at sign-out; starting a new anonymous id so the stored session can't be restored");
|
|
435
|
+
}
|
|
436
|
+
const keep = !saved && this.plaintext !== null && this.refreshToken !== null && this.plaintext.appUserId === this.appUserId && this.plaintext.slice.refresh_token === this.refreshToken ? this.plaintext.slice : null;
|
|
437
|
+
const record = {
|
|
345
438
|
anonymous_id: this.anonymousId,
|
|
346
439
|
app_user_id: this.appUserId,
|
|
347
|
-
session_token:
|
|
348
|
-
refresh_token:
|
|
349
|
-
expires_at:
|
|
440
|
+
session_token: keep?.session_token ?? null,
|
|
441
|
+
refresh_token: keep?.refresh_token ?? null,
|
|
442
|
+
expires_at: keep?.expires_at ?? null
|
|
350
443
|
};
|
|
351
|
-
await this.persistence.write(IDENTITY_KEY, JSON.stringify(
|
|
444
|
+
await this.persistence.write(IDENTITY_KEY, JSON.stringify(record));
|
|
445
|
+
if (!keep) this.plaintext = null;
|
|
352
446
|
}
|
|
353
447
|
};
|
|
354
448
|
function freshSeed() {
|
|
@@ -361,9 +455,167 @@ function freshSeed() {
|
|
|
361
455
|
};
|
|
362
456
|
}
|
|
363
457
|
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/token-storage.ts
|
|
460
|
+
const TOKEN_STORAGE_MODES = [
|
|
461
|
+
"secure",
|
|
462
|
+
"memory",
|
|
463
|
+
"asyncStorage"
|
|
464
|
+
];
|
|
465
|
+
function isEmpty(slice) {
|
|
466
|
+
return slice.session_token === null && slice.refresh_token === null && slice.expires_at === null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Memory-held credentials, keyed like the keystore entry. They last as long
|
|
470
|
+
* as the JS runtime, so a second `Amba.configure()` (Fast Refresh, a
|
|
471
|
+
* re-configure) keeps the session, and a relaunch starts signed out.
|
|
472
|
+
*/
|
|
473
|
+
const memorySlices = /* @__PURE__ */ new Map();
|
|
474
|
+
/** Credentials held in this JS runtime only. */
|
|
475
|
+
var MemoryTokenVault = class {
|
|
476
|
+
kind = "memory";
|
|
477
|
+
constructor(key) {
|
|
478
|
+
this.key = key;
|
|
479
|
+
}
|
|
480
|
+
async read() {
|
|
481
|
+
const slice = memorySlices.get(this.key);
|
|
482
|
+
return slice ? { ...slice } : null;
|
|
483
|
+
}
|
|
484
|
+
async write(slice) {
|
|
485
|
+
if (isEmpty(slice)) memorySlices.delete(this.key);
|
|
486
|
+
else memorySlices.set(this.key, { ...slice });
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Keystore key for a storage prefix. Kept to `[A-Za-z0-9_.-]` because some
|
|
491
|
+
* secure stores (for example `expo-secure-store`) reject other characters.
|
|
492
|
+
*/
|
|
493
|
+
function secureTokenKey(prefix) {
|
|
494
|
+
return `${prefix.replace(/[^A-Za-z0-9_.-]/g, "_")}_session_tokens_v1`;
|
|
495
|
+
}
|
|
496
|
+
/** Credentials in a keystore-backed {@link SecureTokenStorage}. */
|
|
497
|
+
var SecureTokenVault = class {
|
|
498
|
+
kind = "secure";
|
|
499
|
+
constructor(storage, key) {
|
|
500
|
+
this.storage = storage;
|
|
501
|
+
this.key = key;
|
|
502
|
+
}
|
|
503
|
+
async read() {
|
|
504
|
+
const raw = await this.storage.getItem(this.key);
|
|
505
|
+
if (!raw) return null;
|
|
506
|
+
try {
|
|
507
|
+
const parsed = JSON.parse(raw);
|
|
508
|
+
return {
|
|
509
|
+
session_token: parsed.session_token ?? null,
|
|
510
|
+
refresh_token: parsed.refresh_token ?? null,
|
|
511
|
+
expires_at: parsed.expires_at ?? null,
|
|
512
|
+
anonymous_id: parsed.anonymous_id ?? null,
|
|
513
|
+
app_user_id: parsed.app_user_id ?? null
|
|
514
|
+
};
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async write(slice) {
|
|
520
|
+
if (isEmpty(slice)) {
|
|
521
|
+
await this.storage.removeItem(this.key);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
await this.storage.setItem(this.key, JSON.stringify(slice));
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
function isKeychainLike(mod) {
|
|
528
|
+
const k = mod;
|
|
529
|
+
return typeof k === "object" && k !== null && typeof k.setGenericPassword === "function" && typeof k.getGenericPassword === "function" && typeof k.resetGenericPassword === "function";
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Adapt `react-native-keychain` to {@link SecureTokenStorage}. Items use the
|
|
533
|
+
* `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` class on iOS: readable after the
|
|
534
|
+
* first unlock (so background refreshes work) and left behind by backups
|
|
535
|
+
* restored onto another device.
|
|
536
|
+
*/
|
|
537
|
+
function keychainStorage(keychain) {
|
|
538
|
+
const accessible = keychain.ACCESSIBLE?.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY;
|
|
539
|
+
return {
|
|
540
|
+
async getItem(key) {
|
|
541
|
+
const entry = await keychain.getGenericPassword({ service: key });
|
|
542
|
+
return entry ? entry.password : null;
|
|
543
|
+
},
|
|
544
|
+
async setItem(key, value) {
|
|
545
|
+
if (await keychain.setGenericPassword("amba", value, {
|
|
546
|
+
service: key,
|
|
547
|
+
...accessible !== void 0 ? { accessible } : {}
|
|
548
|
+
}) === false) throw new Error("react-native-keychain refused to store the session");
|
|
549
|
+
},
|
|
550
|
+
async removeItem(key) {
|
|
551
|
+
if (await keychain.resetGenericPassword({ service: key }) === false && await keychain.getGenericPassword({ service: key })) throw new Error("react-native-keychain could not remove the session");
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
let cachedKeychain;
|
|
556
|
+
/**
|
|
557
|
+
* Load `react-native-keychain` when the app has it installed. Resolves null
|
|
558
|
+
* otherwise (module missing, or a build without its native half).
|
|
559
|
+
*/
|
|
560
|
+
async function loadKeychainStorage() {
|
|
561
|
+
if (cachedKeychain !== void 0) return cachedKeychain;
|
|
562
|
+
try {
|
|
563
|
+
const mod = await import("react-native-keychain");
|
|
564
|
+
const candidate = isKeychainLike(mod) ? mod : mod.default;
|
|
565
|
+
cachedKeychain = isKeychainLike(candidate) ? keychainStorage(candidate) : null;
|
|
566
|
+
} catch {
|
|
567
|
+
cachedKeychain = null;
|
|
568
|
+
}
|
|
569
|
+
return cachedKeychain;
|
|
570
|
+
}
|
|
571
|
+
let memoryFallbackWarned = false;
|
|
572
|
+
/**
|
|
573
|
+
* Pick the vault for `mode`. Resolves null for `"asyncStorage"`, which keeps
|
|
574
|
+
* the credentials inside the AsyncStorage identity record.
|
|
575
|
+
*/
|
|
576
|
+
async function resolveTokenVault(mode, prefix, secureStorage) {
|
|
577
|
+
if (mode === "asyncStorage") return null;
|
|
578
|
+
const key = secureTokenKey(prefix);
|
|
579
|
+
if (mode === "memory") return new MemoryTokenVault(key);
|
|
580
|
+
const storage = secureStorage ?? await loadKeychainStorage();
|
|
581
|
+
if (storage) return new SecureTokenVault(storage, key);
|
|
582
|
+
if (!memoryFallbackWarned && typeof console !== "undefined") {
|
|
583
|
+
memoryFallbackWarned = true;
|
|
584
|
+
console.warn("[amba] No secure token store found, so the session is kept in memory and users sign in again after every launch. Install react-native-keychain (or pass `secureStorage` to Amba.configure) to keep sessions across launches in the Keychain/Keystore.");
|
|
585
|
+
}
|
|
586
|
+
return new MemoryTokenVault(key);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/path-segment.ts
|
|
591
|
+
/**
|
|
592
|
+
* Encode one caller-supplied id, key, slug or name as a single URL path
|
|
593
|
+
* segment. `encodeURIComponent` escapes `/`, `?` and `#`, so the value stays
|
|
594
|
+
* inside its segment. `.` and `..` are refused because URL resolution treats
|
|
595
|
+
* them as steps to another route, and the empty string is refused because it
|
|
596
|
+
* collapses onto the parent route.
|
|
597
|
+
*/
|
|
598
|
+
function pathSegment(value) {
|
|
599
|
+
if (value === "" || value === "." || value === "..") throw new AmbaApiError("VALIDATION_ERROR", `path segment must not be ${value === "" ? "empty" : `'${value}'`}`);
|
|
600
|
+
return encodeURIComponent(value);
|
|
601
|
+
}
|
|
602
|
+
|
|
364
603
|
//#endregion
|
|
365
604
|
//#region src/resilience.ts
|
|
366
605
|
/**
|
|
606
|
+
* Whether `op` belongs to `current`. An op queued under a user id matches only
|
|
607
|
+
* that user id. An op queued before any session matches the same anonymous id,
|
|
608
|
+
* so a guest's offline write lands once that guest's session starts. An op
|
|
609
|
+
* with no recorded owner (queued by an SDK version before owners existed)
|
|
610
|
+
* matches nobody.
|
|
611
|
+
*/
|
|
612
|
+
function outboxOwnerMatches(op, current) {
|
|
613
|
+
const owner = op.owner;
|
|
614
|
+
if (!owner) return false;
|
|
615
|
+
if (owner.appUserId) return owner.appUserId === current.appUserId;
|
|
616
|
+
return owner.anonymousId !== null && owner.anonymousId === current.anonymousId;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
367
619
|
* Best-effort online probe. In a browser/RN runtime `navigator.onLine`
|
|
368
620
|
* exists; under Node / SSR it doesn't, so we treat "unknown" as online
|
|
369
621
|
* (the executor's own error handling then decides).
|
|
@@ -447,8 +699,18 @@ var OfflineQueue = class {
|
|
|
447
699
|
* SDK surface the loss (debug log) instead of silently discarding.
|
|
448
700
|
*/
|
|
449
701
|
onDropped;
|
|
702
|
+
/**
|
|
703
|
+
* The identity that is signed in right now. When set, enqueue stamps it on
|
|
704
|
+
* each op and flush/pending only touch ops it owns.
|
|
705
|
+
*/
|
|
706
|
+
currentOwner;
|
|
450
707
|
flushing = null;
|
|
451
708
|
/**
|
|
709
|
+
* Bumped by `purge()`. A replay that started before a purge sees the change
|
|
710
|
+
* and does not re-queue its op.
|
|
711
|
+
*/
|
|
712
|
+
purgeGeneration = 0;
|
|
713
|
+
/**
|
|
452
714
|
* Serializes every read-modify-write of the persisted outbox. Each mutation
|
|
453
715
|
* chains onto the previous one's promise, so concurrent `enqueue`s — and an
|
|
454
716
|
* `enqueue` racing the per-op removal inside `flush` — can never read a stale
|
|
@@ -471,6 +733,16 @@ var OfflineQueue = class {
|
|
|
471
733
|
this.executor = opts.executor;
|
|
472
734
|
this.storageKey = opts.storageKey ?? "offline_outbox";
|
|
473
735
|
this.onDropped = opts.onDropped;
|
|
736
|
+
this.currentOwner = opts.currentOwner;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The ops the current owner may see and replay. Without an owner source
|
|
740
|
+
* every op qualifies.
|
|
741
|
+
*/
|
|
742
|
+
visible(ops) {
|
|
743
|
+
if (!this.currentOwner) return ops;
|
|
744
|
+
const current = this.currentOwner();
|
|
745
|
+
return ops.filter((op) => outboxOwnerMatches(op, current));
|
|
474
746
|
}
|
|
475
747
|
/**
|
|
476
748
|
* Run `fn` exclusively against the persisted outbox: it chains after every
|
|
@@ -527,28 +799,55 @@ var OfflineQueue = class {
|
|
|
527
799
|
get isDisposed() {
|
|
528
800
|
return this.disposed;
|
|
529
801
|
}
|
|
530
|
-
/**
|
|
531
|
-
|
|
802
|
+
/**
|
|
803
|
+
* Capture the current owner and purge generation. Take it before a write's
|
|
804
|
+
* live attempt and pass it to {@link enqueue}.
|
|
805
|
+
*/
|
|
806
|
+
ticket() {
|
|
807
|
+
return {
|
|
808
|
+
owner: this.currentOwner?.(),
|
|
809
|
+
generation: this.purgeGeneration
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Append an op to the durable outbox. Returns the persisted op. With a
|
|
814
|
+
* `ticket`, the op belongs to the ticket's owner, and the enqueue rejects
|
|
815
|
+
* (code `OFFLINE_QUEUE_PURGED`) when the queue was purged after the ticket
|
|
816
|
+
* was taken, which is what a sign-out during the live attempt does.
|
|
817
|
+
*/
|
|
818
|
+
async enqueue(op, ticket) {
|
|
532
819
|
if (this.disposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
820
|
+
const owner = op.owner ?? (ticket ? ticket.owner : this.currentOwner?.());
|
|
533
821
|
const full = {
|
|
534
822
|
...op,
|
|
823
|
+
...owner ? { owner: { ...owner } } : {},
|
|
535
824
|
idempotencyKey: op.idempotencyKey ?? newIdempotencyKey(),
|
|
536
825
|
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
537
826
|
};
|
|
538
|
-
return this.mutate((ops) =>
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
827
|
+
return this.mutate((ops) => {
|
|
828
|
+
if (ticket && ticket.generation !== this.purgeGeneration) {
|
|
829
|
+
const err = /* @__PURE__ */ new Error("amba offline queue was cleared by a sign-out while this write was in flight");
|
|
830
|
+
err.code = "OFFLINE_QUEUE_PURGED";
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
next: [...ops, full],
|
|
835
|
+
result: full
|
|
836
|
+
};
|
|
837
|
+
}, { rejectIfDisposed: true });
|
|
542
838
|
}
|
|
543
|
-
/** Number of ops awaiting replay. */
|
|
839
|
+
/** Number of the current owner's ops awaiting replay. */
|
|
544
840
|
async pendingCount() {
|
|
545
|
-
return (await this.load()).length;
|
|
841
|
+
return this.visible(await this.load()).length;
|
|
546
842
|
}
|
|
547
|
-
/**
|
|
843
|
+
/**
|
|
844
|
+
* Snapshot the current owner's pending ops (a copy — mutating it does not
|
|
845
|
+
* affect the queue). Ops queued by another user stay out of the list.
|
|
846
|
+
*/
|
|
548
847
|
async pending() {
|
|
549
|
-
return [...await this.load()];
|
|
848
|
+
return [...this.visible(await this.load())];
|
|
550
849
|
}
|
|
551
|
-
/** Drop every queued op without replaying. */
|
|
850
|
+
/** Drop every queued op, whoever queued it, without replaying. */
|
|
552
851
|
async clear() {
|
|
553
852
|
await this.mutate(() => ({
|
|
554
853
|
next: [],
|
|
@@ -556,10 +855,23 @@ var OfflineQueue = class {
|
|
|
556
855
|
}));
|
|
557
856
|
}
|
|
558
857
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
|
|
858
|
+
* Drop every queued op, including one whose replay is in flight right now:
|
|
859
|
+
* that op is not re-queued when its replay fails. The SDKs call this on
|
|
860
|
+
* sign-out so nothing the signed-out user queued survives on the device.
|
|
861
|
+
*/
|
|
862
|
+
async purge() {
|
|
863
|
+
this.purgeGeneration += 1;
|
|
864
|
+
await this.clear();
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Replay the current owner's ops in FIFO order. Each op is removed only
|
|
868
|
+
* after it succeeds; the first failure stops the flush and retains the
|
|
869
|
+
* remainder (so order is preserved and nothing is dropped mid-batch). Ops
|
|
870
|
+
* another user queued stay held for that user. Ops that can never replay
|
|
871
|
+
* are dropped through `onDropped`: one with no recorded owner (code
|
|
872
|
+
* `OFFLINE_OWNER_UNKNOWN`), and one queued before any session under an
|
|
873
|
+
* anonymous id the install no longer has (code `OFFLINE_OWNER_GONE`).
|
|
874
|
+
* Concurrent calls share one in-flight flush.
|
|
563
875
|
*/
|
|
564
876
|
async flush() {
|
|
565
877
|
if (this.disposed) return {
|
|
@@ -578,8 +890,26 @@ var OfflineQueue = class {
|
|
|
578
890
|
let dropped = 0;
|
|
579
891
|
for (;;) {
|
|
580
892
|
if (this.disposed) break;
|
|
581
|
-
const
|
|
893
|
+
const all = await this.load();
|
|
894
|
+
if (this.currentOwner) {
|
|
895
|
+
const current = this.currentOwner();
|
|
896
|
+
const orphan = all.find((o) => !o.owner || o.owner.appUserId === null && current.anonymousId !== null && o.owner.anonymousId !== current.anonymousId);
|
|
897
|
+
if (orphan) {
|
|
898
|
+
if (!await this.removeOp(orphan)) break;
|
|
899
|
+
dropped += 1;
|
|
900
|
+
this.reportDropped(orphan, orphan.owner ? {
|
|
901
|
+
code: "OFFLINE_OWNER_GONE",
|
|
902
|
+
message: "queued before sign-in under an anonymous id this install no longer has; not replayed"
|
|
903
|
+
} : {
|
|
904
|
+
code: "OFFLINE_OWNER_UNKNOWN",
|
|
905
|
+
message: "queued by an earlier SDK version without an owner; not replayed"
|
|
906
|
+
});
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const head = this.visible(all)[0];
|
|
582
911
|
if (!head) break;
|
|
912
|
+
const generation = this.purgeGeneration;
|
|
583
913
|
let replayError = void 0;
|
|
584
914
|
let ok = false;
|
|
585
915
|
try {
|
|
@@ -591,32 +921,20 @@ var OfflineQueue = class {
|
|
|
591
921
|
if (!ok && !isDeterministicError(replayError)) {
|
|
592
922
|
await this.mutate((ops) => {
|
|
593
923
|
return {
|
|
594
|
-
next: ops.some((o) => o.idempotencyKey === head.idempotencyKey)
|
|
924
|
+
next: !ops.some((o) => o.idempotencyKey === head.idempotencyKey) && generation === this.purgeGeneration ? [head, ...ops] : ops,
|
|
595
925
|
result: void 0
|
|
596
926
|
};
|
|
597
927
|
});
|
|
598
928
|
break;
|
|
599
929
|
}
|
|
600
|
-
if (!await this.
|
|
601
|
-
const idx = ops.findIndex((o) => o.idempotencyKey === head.idempotencyKey);
|
|
602
|
-
if (idx === -1) return {
|
|
603
|
-
next: ops,
|
|
604
|
-
result: false
|
|
605
|
-
};
|
|
606
|
-
return {
|
|
607
|
-
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
608
|
-
result: true
|
|
609
|
-
};
|
|
610
|
-
})) break;
|
|
930
|
+
if (!await this.removeOp(head)) break;
|
|
611
931
|
if (ok) flushed += 1;
|
|
612
932
|
else {
|
|
613
933
|
dropped += 1;
|
|
614
|
-
|
|
615
|
-
this.onDropped?.(head, replayError);
|
|
616
|
-
} catch {}
|
|
934
|
+
this.reportDropped(head, replayError);
|
|
617
935
|
}
|
|
618
936
|
}
|
|
619
|
-
const remaining = (await this.load()).length;
|
|
937
|
+
const remaining = this.visible(await this.load()).length;
|
|
620
938
|
return {
|
|
621
939
|
flushed,
|
|
622
940
|
dropped,
|
|
@@ -624,6 +942,29 @@ var OfflineQueue = class {
|
|
|
624
942
|
};
|
|
625
943
|
}
|
|
626
944
|
/**
|
|
945
|
+
* Remove exactly `op` (by idempotency key) under the lock. Robust against a
|
|
946
|
+
* concurrent enqueue having shifted the array. Resolves false when the op
|
|
947
|
+
* is already gone.
|
|
948
|
+
*/
|
|
949
|
+
removeOp(op) {
|
|
950
|
+
return this.mutate((ops) => {
|
|
951
|
+
const idx = ops.findIndex((o) => o.idempotencyKey === op.idempotencyKey);
|
|
952
|
+
if (idx === -1) return {
|
|
953
|
+
next: ops,
|
|
954
|
+
result: false
|
|
955
|
+
};
|
|
956
|
+
return {
|
|
957
|
+
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
958
|
+
result: true
|
|
959
|
+
};
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
reportDropped(op, err) {
|
|
963
|
+
try {
|
|
964
|
+
this.onDropped?.(op, err);
|
|
965
|
+
} catch {}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
627
968
|
* Install an auto-flush listener on the runtime's `online` event so the
|
|
628
969
|
* queue drains the moment connectivity returns. Idempotent — a second
|
|
629
970
|
* call is a no-op. Returns a detach function.
|
|
@@ -1192,7 +1533,7 @@ var CollectionSubscription = class {
|
|
|
1192
1533
|
if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
1193
1534
|
const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
|
|
1194
1535
|
return openStream({
|
|
1195
|
-
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${
|
|
1536
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${pathSegment(this.name)}${qs}`,
|
|
1196
1537
|
authHeaders: this.ctx.authHeaders,
|
|
1197
1538
|
onChange: cb,
|
|
1198
1539
|
options,
|
|
@@ -1216,7 +1557,7 @@ var ConversationSubscription = class {
|
|
|
1216
1557
|
subscribe(cb, options = {}) {
|
|
1217
1558
|
if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
1218
1559
|
return openStream({
|
|
1219
|
-
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${
|
|
1560
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${pathSegment(this.conversationId)}`,
|
|
1220
1561
|
authHeaders: this.ctx.authHeaders,
|
|
1221
1562
|
onChange: cb,
|
|
1222
1563
|
options,
|
|
@@ -1476,7 +1817,7 @@ function delay(ms) {
|
|
|
1476
1817
|
//#endregion
|
|
1477
1818
|
//#region src/index.ts
|
|
1478
1819
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
1479
|
-
const SDK_VERSION = "4.0.
|
|
1820
|
+
const SDK_VERSION = "4.0.10";
|
|
1480
1821
|
/**
|
|
1481
1822
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
1482
1823
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -1717,8 +2058,20 @@ const P = {
|
|
|
1717
2058
|
deepLinks: "/v1/client/deep-links",
|
|
1718
2059
|
onboarding: "/v1/client/onboarding"
|
|
1719
2060
|
};
|
|
2061
|
+
/**
|
|
2062
|
+
* Encode one caller-supplied id, key or name as a single URL path segment.
|
|
2063
|
+
* `encodeURIComponent` escapes `/`, `?` and `#`; `.` and `..` are refused
|
|
2064
|
+
* because URL resolution treats them as steps to another route, and the
|
|
2065
|
+
* empty string is refused because it collapses onto the parent route.
|
|
2066
|
+
*/
|
|
1720
2067
|
function encodePathSegment(s) {
|
|
1721
|
-
return
|
|
2068
|
+
return pathSegment(s);
|
|
2069
|
+
}
|
|
2070
|
+
function nonceField(options) {
|
|
2071
|
+
const nonce = options?.nonce;
|
|
2072
|
+
if (nonce === void 0) return {};
|
|
2073
|
+
if (typeof nonce !== "string" || nonce.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "nonce must be a non-empty string");
|
|
2074
|
+
return { nonce };
|
|
1722
2075
|
}
|
|
1723
2076
|
/**
|
|
1724
2077
|
* Resolve the full invocation URL for a deployed function. Resolution checks
|
|
@@ -1728,8 +2081,8 @@ function encodePathSegment(s) {
|
|
|
1728
2081
|
* cached for subsequent invokes.
|
|
1729
2082
|
*/
|
|
1730
2083
|
async function resolveFunctionUrl(s, name) {
|
|
1731
|
-
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${
|
|
1732
|
-
return `https://${await resolveFunctionsProjectId(s)}.${DEFAULT_FUNCTIONS_HOST}/${
|
|
2084
|
+
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${pathSegment(name)}`;
|
|
2085
|
+
return `https://${await resolveFunctionsProjectId(s)}.${DEFAULT_FUNCTIONS_HOST}/${pathSegment(name)}`;
|
|
1733
2086
|
}
|
|
1734
2087
|
async function resolveFunctionsProjectId(s) {
|
|
1735
2088
|
if (s.resolvedFunctionsProjectId) return s.resolvedFunctionsProjectId;
|
|
@@ -1806,6 +2159,10 @@ function getOfflineQueue(s) {
|
|
|
1806
2159
|
store: offlineStore(s),
|
|
1807
2160
|
storageKey: `offline_outbox:${s.projectId}`,
|
|
1808
2161
|
executor: (op) => replayOutboxOp(s, op),
|
|
2162
|
+
currentOwner: () => ({
|
|
2163
|
+
appUserId: s.identity.getSessionToken() || s.identity.getRefreshToken() ? s.identity.getAppUserId() : null,
|
|
2164
|
+
anonymousId: s.identity.getAnonymousId()
|
|
2165
|
+
}),
|
|
1809
2166
|
onDropped: (op, err) => {
|
|
1810
2167
|
if (typeof console !== "undefined") console.warn(`[amba] dropped un-replayable offline ${op.kind} (${op.collection ?? op.event ?? "?"}):`, err);
|
|
1811
2168
|
}
|
|
@@ -1836,10 +2193,16 @@ const Amba = {
|
|
|
1836
2193
|
if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
|
|
1837
2194
|
if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
|
|
1838
2195
|
}
|
|
1839
|
-
const
|
|
2196
|
+
const tokenStorage = config.tokenStorage ?? "secure";
|
|
2197
|
+
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
2198
|
+
const secureStorage = config.secureStorage;
|
|
2199
|
+
if (secureStorage !== void 0 && (typeof secureStorage?.getItem !== "function" || typeof secureStorage?.setItem !== "function" || typeof secureStorage?.removeItem !== "function")) throw new Error("Amba.configure: secureStorage must provide getItem, setItem and removeItem");
|
|
2200
|
+
const storagePrefix = config.storagePrefix ?? "amba";
|
|
2201
|
+
const persistence = new AsyncStoragePersistence(storagePrefix);
|
|
1840
2202
|
if (config.events?.batching) await persistence.assertDurable();
|
|
1841
2203
|
await persistence.hydrate();
|
|
1842
|
-
const
|
|
2204
|
+
const vault = await resolveTokenVault(tokenStorage, storagePrefix, secureStorage);
|
|
2205
|
+
const identity = await IdentityStore.create(persistence, vault);
|
|
1843
2206
|
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
1844
2207
|
const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
|
|
1845
2208
|
const http = new AmbaHttpClient({
|
|
@@ -1940,6 +2303,7 @@ const Amba = {
|
|
|
1940
2303
|
timestamp: occurredAt
|
|
1941
2304
|
};
|
|
1942
2305
|
if (options?.telemetry) body["telemetry"] = true;
|
|
2306
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
1943
2307
|
try {
|
|
1944
2308
|
await s.http.post(P.events, body);
|
|
1945
2309
|
} catch (err) {
|
|
@@ -1950,7 +2314,7 @@ const Amba = {
|
|
|
1950
2314
|
properties,
|
|
1951
2315
|
telemetry: options?.telemetry,
|
|
1952
2316
|
occurredAt
|
|
1953
|
-
});
|
|
2317
|
+
}, outboxTicket);
|
|
1954
2318
|
return;
|
|
1955
2319
|
}
|
|
1956
2320
|
throw err;
|
|
@@ -1994,18 +2358,20 @@ const Amba = {
|
|
|
1994
2358
|
password
|
|
1995
2359
|
});
|
|
1996
2360
|
},
|
|
1997
|
-
signInWithApple: async (identityToken) => {
|
|
2361
|
+
signInWithApple: async (identityToken, options) => {
|
|
1998
2362
|
if (identityToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "identity_token must not be empty");
|
|
1999
2363
|
return postAuth(getState(), P.authSocial, {
|
|
2000
2364
|
provider: "apple",
|
|
2001
|
-
id_token: identityToken
|
|
2365
|
+
id_token: identityToken,
|
|
2366
|
+
...nonceField(options)
|
|
2002
2367
|
});
|
|
2003
2368
|
},
|
|
2004
|
-
signInWithGoogle: async (idToken) => {
|
|
2369
|
+
signInWithGoogle: async (idToken, options) => {
|
|
2005
2370
|
if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
|
|
2006
2371
|
return postAuth(getState(), P.authSocial, {
|
|
2007
2372
|
provider: "google",
|
|
2008
|
-
id_token: idToken
|
|
2373
|
+
id_token: idToken,
|
|
2374
|
+
...nonceField(options)
|
|
2009
2375
|
});
|
|
2010
2376
|
},
|
|
2011
2377
|
requestEmailOtp: async (email) => {
|
|
@@ -2040,7 +2406,7 @@ const Amba = {
|
|
|
2040
2406
|
if (token.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "token must not be empty");
|
|
2041
2407
|
return postAuth(getState(), P.authMagicLinkVerify, { token });
|
|
2042
2408
|
},
|
|
2043
|
-
linkAccount: async (provider, credential) => {
|
|
2409
|
+
linkAccount: async (provider, credential, options) => {
|
|
2044
2410
|
const providerTrimmed = provider?.trim() ?? "";
|
|
2045
2411
|
if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
|
|
2046
2412
|
if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
|
|
@@ -2050,7 +2416,8 @@ const Amba = {
|
|
|
2050
2416
|
return postAuth(s, P.authLink, {
|
|
2051
2417
|
provider: providerTrimmed,
|
|
2052
2418
|
token: credential,
|
|
2053
|
-
session_token: sessionToken
|
|
2419
|
+
session_token: sessionToken,
|
|
2420
|
+
...nonceField(options)
|
|
2054
2421
|
});
|
|
2055
2422
|
},
|
|
2056
2423
|
linkSmsOtp: async (phone, code) => {
|
|
@@ -2109,6 +2476,11 @@ const Amba = {
|
|
|
2109
2476
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
2110
2477
|
}
|
|
2111
2478
|
await s.identity.clearSession(rotateAnonymousId);
|
|
2479
|
+
try {
|
|
2480
|
+
await getOfflineQueue(s).purge();
|
|
2481
|
+
} catch (err) {
|
|
2482
|
+
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
2483
|
+
}
|
|
2112
2484
|
s.eventQueue?.flush();
|
|
2113
2485
|
notifyAuthSubscribers(null);
|
|
2114
2486
|
},
|
|
@@ -2224,6 +2596,7 @@ const Amba = {
|
|
|
2224
2596
|
const s = getState();
|
|
2225
2597
|
if (s.offlineEnabled) {
|
|
2226
2598
|
const idempotencyKey = (typeof row["idempotency_key"] === "string" ? row["idempotency_key"] : void 0) ?? newIdempotencyKey();
|
|
2599
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
2227
2600
|
try {
|
|
2228
2601
|
return (await s.http.post(`${P.collections}/${encodePathSegment(name)}`, {
|
|
2229
2602
|
...row,
|
|
@@ -2236,7 +2609,7 @@ const Amba = {
|
|
|
2236
2609
|
collection: name,
|
|
2237
2610
|
payload: row,
|
|
2238
2611
|
idempotencyKey
|
|
2239
|
-
});
|
|
2612
|
+
}, outboxTicket);
|
|
2240
2613
|
return {
|
|
2241
2614
|
...row,
|
|
2242
2615
|
_amba_pending: true
|
|
@@ -2249,40 +2622,46 @@ const Amba = {
|
|
|
2249
2622
|
},
|
|
2250
2623
|
update: async (name, id, set) => {
|
|
2251
2624
|
const s = getState();
|
|
2252
|
-
if (s.offlineEnabled)
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2625
|
+
if (s.offlineEnabled) {
|
|
2626
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
2627
|
+
try {
|
|
2628
|
+
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
2629
|
+
} catch (err) {
|
|
2630
|
+
if (isOfflineError(err)) {
|
|
2631
|
+
await getOfflineQueue(s).enqueue({
|
|
2632
|
+
kind: "collection.update",
|
|
2633
|
+
collection: name,
|
|
2634
|
+
id,
|
|
2635
|
+
payload: set
|
|
2636
|
+
}, outboxTicket);
|
|
2637
|
+
return {
|
|
2638
|
+
id,
|
|
2639
|
+
...set,
|
|
2640
|
+
_amba_pending: true
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
throw err;
|
|
2267
2644
|
}
|
|
2268
|
-
throw err;
|
|
2269
2645
|
}
|
|
2270
2646
|
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
2271
2647
|
},
|
|
2272
2648
|
delete: async (name, id) => {
|
|
2273
2649
|
const s = getState();
|
|
2274
|
-
if (s.offlineEnabled)
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2650
|
+
if (s.offlineEnabled) {
|
|
2651
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
2652
|
+
try {
|
|
2653
|
+
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
if (isOfflineError(err)) {
|
|
2656
|
+
await getOfflineQueue(s).enqueue({
|
|
2657
|
+
kind: "collection.delete",
|
|
2658
|
+
collection: name,
|
|
2659
|
+
id
|
|
2660
|
+
}, outboxTicket);
|
|
2661
|
+
return { data: { deleted: true } };
|
|
2662
|
+
}
|
|
2663
|
+
throw err;
|
|
2284
2664
|
}
|
|
2285
|
-
throw err;
|
|
2286
2665
|
}
|
|
2287
2666
|
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
2288
2667
|
},
|
|
@@ -2643,7 +3022,7 @@ const Amba = {
|
|
|
2643
3022
|
return (await getState().http.get(P.friends, { state: "accepted" })).data;
|
|
2644
3023
|
},
|
|
2645
3024
|
sendRequest: async (userId) => {
|
|
2646
|
-
return (await getState().http.post(`${P.friends}/request`, {
|
|
3025
|
+
return (await getState().http.post(`${P.friends}/request`, { addressee_id: userId })).data;
|
|
2647
3026
|
},
|
|
2648
3027
|
acceptRequest: async (friendshipId) => {
|
|
2649
3028
|
return (await getState().http.post(`${P.friends}/${encodePathSegment(friendshipId)}/accept`, {})).data;
|