@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.js
CHANGED
|
@@ -269,8 +269,23 @@ var IdentityStore = class IdentityStore {
|
|
|
269
269
|
sessionToken = null;
|
|
270
270
|
refreshToken = null;
|
|
271
271
|
expiresAt = null;
|
|
272
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Credentials an earlier SDK version left in the AsyncStorage record, with
|
|
274
|
+
* the user they belong to. They stay on disk until the vault holds the
|
|
275
|
+
* session, so a vault write that fails or is skipped (or a process killed
|
|
276
|
+
* between the two writes) never loses the only stored copy.
|
|
277
|
+
*/
|
|
278
|
+
plaintext;
|
|
279
|
+
/**
|
|
280
|
+
* Set while the vault could not be read. Writes are held back until a read
|
|
281
|
+
* succeeds, so a session this launch never loaded is not overwritten.
|
|
282
|
+
*/
|
|
283
|
+
vaultUnreadable;
|
|
284
|
+
constructor(persistence, seed, vault, plaintext, vaultUnreadable) {
|
|
273
285
|
this.persistence = persistence;
|
|
286
|
+
this.vault = vault;
|
|
287
|
+
this.plaintext = plaintext;
|
|
288
|
+
this.vaultUnreadable = vaultUnreadable;
|
|
274
289
|
this.anonymousId = seed.anonymous_id;
|
|
275
290
|
this.appUserId = seed.app_user_id;
|
|
276
291
|
this.sessionToken = seed.session_token;
|
|
@@ -278,13 +293,19 @@ var IdentityStore = class IdentityStore {
|
|
|
278
293
|
this.expiresAt = seed.expires_at;
|
|
279
294
|
}
|
|
280
295
|
/**
|
|
281
|
-
* Build a store, hydrating any prior identity from AsyncStorage
|
|
282
|
-
* If no prior identity exists, a fresh `anonymous_id`
|
|
283
|
-
* Expired session tokens are dropped at hydration (the refresh
|
|
284
|
-
* survives so callers can `auth.refresh()` to mint a new pair).
|
|
296
|
+
* Build a store, hydrating any prior identity from AsyncStorage and the
|
|
297
|
+
* credential vault. If no prior identity exists, a fresh `anonymous_id`
|
|
298
|
+
* is minted. Expired session tokens are dropped at hydration (the refresh
|
|
299
|
+
* token survives so callers can `auth.refresh()` to mint a new pair).
|
|
300
|
+
*
|
|
301
|
+
* With a vault, credentials an earlier SDK version left in the
|
|
302
|
+
* AsyncStorage record are used for this launch and moved into the vault;
|
|
303
|
+
* the AsyncStorage copy is removed once the vault write succeeds. A vault
|
|
304
|
+
* holding its own credentials wins over them.
|
|
285
305
|
*/
|
|
286
|
-
static async create(persistence) {
|
|
306
|
+
static async create(persistence, vault = null) {
|
|
287
307
|
const raw = await persistence.read(IDENTITY_KEY);
|
|
308
|
+
let recordPresent = false;
|
|
288
309
|
let seed;
|
|
289
310
|
if (raw) try {
|
|
290
311
|
const parsed = JSON.parse(raw);
|
|
@@ -295,16 +316,42 @@ var IdentityStore = class IdentityStore {
|
|
|
295
316
|
refresh_token: parsed.refresh_token ?? null,
|
|
296
317
|
expires_at: parsed.expires_at ?? null
|
|
297
318
|
};
|
|
319
|
+
recordPresent = typeof parsed.anonymous_id === "string";
|
|
298
320
|
} catch {
|
|
299
321
|
seed = freshSeed();
|
|
300
322
|
}
|
|
301
323
|
else seed = freshSeed();
|
|
324
|
+
const plaintext = vault && (seed.session_token !== null || seed.refresh_token !== null || seed.expires_at !== null) ? {
|
|
325
|
+
appUserId: seed.app_user_id,
|
|
326
|
+
slice: {
|
|
327
|
+
session_token: seed.session_token,
|
|
328
|
+
refresh_token: seed.refresh_token,
|
|
329
|
+
expires_at: seed.expires_at
|
|
330
|
+
}
|
|
331
|
+
} : null;
|
|
332
|
+
let vaultReadable = true;
|
|
333
|
+
if (vault) {
|
|
334
|
+
let fromVault = null;
|
|
335
|
+
try {
|
|
336
|
+
fromVault = await vault.read();
|
|
337
|
+
} catch {
|
|
338
|
+
vaultReadable = false;
|
|
339
|
+
}
|
|
340
|
+
const belongs = fromVault !== null && recordPresent && fromVault.anonymous_id === seed.anonymous_id;
|
|
341
|
+
if (fromVault && belongs) {
|
|
342
|
+
seed.session_token = fromVault.session_token;
|
|
343
|
+
seed.refresh_token = fromVault.refresh_token;
|
|
344
|
+
seed.expires_at = fromVault.expires_at;
|
|
345
|
+
if (fromVault.app_user_id != null) seed.app_user_id = fromVault.app_user_id;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (vaultReadable && seed.session_token === null && seed.refresh_token === null) seed.app_user_id = null;
|
|
302
349
|
if (isExpired(seed.expires_at)) {
|
|
303
350
|
seed.session_token = null;
|
|
304
351
|
seed.expires_at = null;
|
|
305
352
|
}
|
|
306
|
-
const store = new IdentityStore(persistence, seed);
|
|
307
|
-
await store.flush();
|
|
353
|
+
const store = new IdentityStore(persistence, seed, vault, plaintext, !vaultReadable);
|
|
354
|
+
await store.flush({ hydrating: true });
|
|
308
355
|
return store;
|
|
309
356
|
}
|
|
310
357
|
snapshot() {
|
|
@@ -344,17 +391,64 @@ var IdentityStore = class IdentityStore {
|
|
|
344
391
|
this.refreshToken = null;
|
|
345
392
|
this.expiresAt = null;
|
|
346
393
|
if (rotateAnonymousId) this.anonymousId = uuidv4();
|
|
347
|
-
await this.flush();
|
|
394
|
+
await this.flush({ signingOut: true });
|
|
348
395
|
}
|
|
349
|
-
|
|
350
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Persist the identity. `hydrating` marks the write that follows `create`:
|
|
398
|
+
* when the vault was unreadable and a retry now succeeds, the session it
|
|
399
|
+
* holds is adopted instead of overwritten. `signingOut` marks the write
|
|
400
|
+
* from `clearSession`: when the vault can't be emptied, the install moves
|
|
401
|
+
* to a new anonymous id so the entry left behind no longer belongs to it.
|
|
402
|
+
*/
|
|
403
|
+
async flush(opts) {
|
|
404
|
+
if (!this.vault) {
|
|
405
|
+
const payload = {
|
|
406
|
+
anonymous_id: this.anonymousId,
|
|
407
|
+
app_user_id: this.appUserId,
|
|
408
|
+
session_token: this.sessionToken,
|
|
409
|
+
refresh_token: this.refreshToken,
|
|
410
|
+
expires_at: this.expiresAt
|
|
411
|
+
};
|
|
412
|
+
await this.persistence.write(IDENTITY_KEY, JSON.stringify(payload));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
let saved = false;
|
|
416
|
+
if (this.vaultUnreadable) try {
|
|
417
|
+
const stored = await this.vault.read();
|
|
418
|
+
this.vaultUnreadable = false;
|
|
419
|
+
if (opts?.hydrating && stored && (stored.session_token !== null || stored.refresh_token !== null) && stored.anonymous_id === this.anonymousId) {
|
|
420
|
+
this.sessionToken = isExpired(stored.expires_at) ? null : stored.session_token;
|
|
421
|
+
this.refreshToken = stored.refresh_token;
|
|
422
|
+
this.expiresAt = isExpired(stored.expires_at) ? null : stored.expires_at;
|
|
423
|
+
if (stored.app_user_id != null) this.appUserId = stored.app_user_id;
|
|
424
|
+
}
|
|
425
|
+
} catch {}
|
|
426
|
+
if (!this.vaultUnreadable) try {
|
|
427
|
+
await this.vault.write({
|
|
428
|
+
session_token: this.sessionToken,
|
|
429
|
+
refresh_token: this.refreshToken,
|
|
430
|
+
expires_at: this.expiresAt,
|
|
431
|
+
anonymous_id: this.anonymousId,
|
|
432
|
+
app_user_id: this.appUserId
|
|
433
|
+
});
|
|
434
|
+
saved = true;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
if (typeof console !== "undefined") console.warn("[amba] could not save the session to the secure store", err);
|
|
437
|
+
}
|
|
438
|
+
if (opts?.signingOut && !saved) {
|
|
439
|
+
this.anonymousId = uuidv4();
|
|
440
|
+
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");
|
|
441
|
+
}
|
|
442
|
+
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;
|
|
443
|
+
const record = {
|
|
351
444
|
anonymous_id: this.anonymousId,
|
|
352
445
|
app_user_id: this.appUserId,
|
|
353
|
-
session_token:
|
|
354
|
-
refresh_token:
|
|
355
|
-
expires_at:
|
|
446
|
+
session_token: keep?.session_token ?? null,
|
|
447
|
+
refresh_token: keep?.refresh_token ?? null,
|
|
448
|
+
expires_at: keep?.expires_at ?? null
|
|
356
449
|
};
|
|
357
|
-
await this.persistence.write(IDENTITY_KEY, JSON.stringify(
|
|
450
|
+
await this.persistence.write(IDENTITY_KEY, JSON.stringify(record));
|
|
451
|
+
if (!keep) this.plaintext = null;
|
|
358
452
|
}
|
|
359
453
|
};
|
|
360
454
|
function freshSeed() {
|
|
@@ -367,9 +461,167 @@ function freshSeed() {
|
|
|
367
461
|
};
|
|
368
462
|
}
|
|
369
463
|
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/token-storage.ts
|
|
466
|
+
const TOKEN_STORAGE_MODES = [
|
|
467
|
+
"secure",
|
|
468
|
+
"memory",
|
|
469
|
+
"asyncStorage"
|
|
470
|
+
];
|
|
471
|
+
function isEmpty(slice) {
|
|
472
|
+
return slice.session_token === null && slice.refresh_token === null && slice.expires_at === null;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Memory-held credentials, keyed like the keystore entry. They last as long
|
|
476
|
+
* as the JS runtime, so a second `Amba.configure()` (Fast Refresh, a
|
|
477
|
+
* re-configure) keeps the session, and a relaunch starts signed out.
|
|
478
|
+
*/
|
|
479
|
+
const memorySlices = /* @__PURE__ */ new Map();
|
|
480
|
+
/** Credentials held in this JS runtime only. */
|
|
481
|
+
var MemoryTokenVault = class {
|
|
482
|
+
kind = "memory";
|
|
483
|
+
constructor(key) {
|
|
484
|
+
this.key = key;
|
|
485
|
+
}
|
|
486
|
+
async read() {
|
|
487
|
+
const slice = memorySlices.get(this.key);
|
|
488
|
+
return slice ? { ...slice } : null;
|
|
489
|
+
}
|
|
490
|
+
async write(slice) {
|
|
491
|
+
if (isEmpty(slice)) memorySlices.delete(this.key);
|
|
492
|
+
else memorySlices.set(this.key, { ...slice });
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* Keystore key for a storage prefix. Kept to `[A-Za-z0-9_.-]` because some
|
|
497
|
+
* secure stores (for example `expo-secure-store`) reject other characters.
|
|
498
|
+
*/
|
|
499
|
+
function secureTokenKey(prefix) {
|
|
500
|
+
return `${prefix.replace(/[^A-Za-z0-9_.-]/g, "_")}_session_tokens_v1`;
|
|
501
|
+
}
|
|
502
|
+
/** Credentials in a keystore-backed {@link SecureTokenStorage}. */
|
|
503
|
+
var SecureTokenVault = class {
|
|
504
|
+
kind = "secure";
|
|
505
|
+
constructor(storage, key) {
|
|
506
|
+
this.storage = storage;
|
|
507
|
+
this.key = key;
|
|
508
|
+
}
|
|
509
|
+
async read() {
|
|
510
|
+
const raw = await this.storage.getItem(this.key);
|
|
511
|
+
if (!raw) return null;
|
|
512
|
+
try {
|
|
513
|
+
const parsed = JSON.parse(raw);
|
|
514
|
+
return {
|
|
515
|
+
session_token: parsed.session_token ?? null,
|
|
516
|
+
refresh_token: parsed.refresh_token ?? null,
|
|
517
|
+
expires_at: parsed.expires_at ?? null,
|
|
518
|
+
anonymous_id: parsed.anonymous_id ?? null,
|
|
519
|
+
app_user_id: parsed.app_user_id ?? null
|
|
520
|
+
};
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async write(slice) {
|
|
526
|
+
if (isEmpty(slice)) {
|
|
527
|
+
await this.storage.removeItem(this.key);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
await this.storage.setItem(this.key, JSON.stringify(slice));
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
function isKeychainLike(mod) {
|
|
534
|
+
const k = mod;
|
|
535
|
+
return typeof k === "object" && k !== null && typeof k.setGenericPassword === "function" && typeof k.getGenericPassword === "function" && typeof k.resetGenericPassword === "function";
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Adapt `react-native-keychain` to {@link SecureTokenStorage}. Items use the
|
|
539
|
+
* `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` class on iOS: readable after the
|
|
540
|
+
* first unlock (so background refreshes work) and left behind by backups
|
|
541
|
+
* restored onto another device.
|
|
542
|
+
*/
|
|
543
|
+
function keychainStorage(keychain) {
|
|
544
|
+
const accessible = keychain.ACCESSIBLE?.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY;
|
|
545
|
+
return {
|
|
546
|
+
async getItem(key) {
|
|
547
|
+
const entry = await keychain.getGenericPassword({ service: key });
|
|
548
|
+
return entry ? entry.password : null;
|
|
549
|
+
},
|
|
550
|
+
async setItem(key, value) {
|
|
551
|
+
if (await keychain.setGenericPassword("amba", value, {
|
|
552
|
+
service: key,
|
|
553
|
+
...accessible !== void 0 ? { accessible } : {}
|
|
554
|
+
}) === false) throw new Error("react-native-keychain refused to store the session");
|
|
555
|
+
},
|
|
556
|
+
async removeItem(key) {
|
|
557
|
+
if (await keychain.resetGenericPassword({ service: key }) === false && await keychain.getGenericPassword({ service: key })) throw new Error("react-native-keychain could not remove the session");
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
let cachedKeychain;
|
|
562
|
+
/**
|
|
563
|
+
* Load `react-native-keychain` when the app has it installed. Resolves null
|
|
564
|
+
* otherwise (module missing, or a build without its native half).
|
|
565
|
+
*/
|
|
566
|
+
async function loadKeychainStorage() {
|
|
567
|
+
if (cachedKeychain !== void 0) return cachedKeychain;
|
|
568
|
+
try {
|
|
569
|
+
const mod = await import("react-native-keychain");
|
|
570
|
+
const candidate = isKeychainLike(mod) ? mod : mod.default;
|
|
571
|
+
cachedKeychain = isKeychainLike(candidate) ? keychainStorage(candidate) : null;
|
|
572
|
+
} catch {
|
|
573
|
+
cachedKeychain = null;
|
|
574
|
+
}
|
|
575
|
+
return cachedKeychain;
|
|
576
|
+
}
|
|
577
|
+
let memoryFallbackWarned = false;
|
|
578
|
+
/**
|
|
579
|
+
* Pick the vault for `mode`. Resolves null for `"asyncStorage"`, which keeps
|
|
580
|
+
* the credentials inside the AsyncStorage identity record.
|
|
581
|
+
*/
|
|
582
|
+
async function resolveTokenVault(mode, prefix, secureStorage) {
|
|
583
|
+
if (mode === "asyncStorage") return null;
|
|
584
|
+
const key = secureTokenKey(prefix);
|
|
585
|
+
if (mode === "memory") return new MemoryTokenVault(key);
|
|
586
|
+
const storage = secureStorage ?? await loadKeychainStorage();
|
|
587
|
+
if (storage) return new SecureTokenVault(storage, key);
|
|
588
|
+
if (!memoryFallbackWarned && typeof console !== "undefined") {
|
|
589
|
+
memoryFallbackWarned = true;
|
|
590
|
+
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.");
|
|
591
|
+
}
|
|
592
|
+
return new MemoryTokenVault(key);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/path-segment.ts
|
|
597
|
+
/**
|
|
598
|
+
* Encode one caller-supplied id, key, slug or name as a single URL path
|
|
599
|
+
* segment. `encodeURIComponent` escapes `/`, `?` and `#`, so the value stays
|
|
600
|
+
* inside its segment. `.` and `..` are refused because URL resolution treats
|
|
601
|
+
* them as steps to another route, and the empty string is refused because it
|
|
602
|
+
* collapses onto the parent route.
|
|
603
|
+
*/
|
|
604
|
+
function pathSegment(value) {
|
|
605
|
+
if (value === "" || value === "." || value === "..") throw new AmbaApiError("VALIDATION_ERROR", `path segment must not be ${value === "" ? "empty" : `'${value}'`}`);
|
|
606
|
+
return encodeURIComponent(value);
|
|
607
|
+
}
|
|
608
|
+
|
|
370
609
|
//#endregion
|
|
371
610
|
//#region src/resilience.ts
|
|
372
611
|
/**
|
|
612
|
+
* Whether `op` belongs to `current`. An op queued under a user id matches only
|
|
613
|
+
* that user id. An op queued before any session matches the same anonymous id,
|
|
614
|
+
* so a guest's offline write lands once that guest's session starts. An op
|
|
615
|
+
* with no recorded owner (queued by an SDK version before owners existed)
|
|
616
|
+
* matches nobody.
|
|
617
|
+
*/
|
|
618
|
+
function outboxOwnerMatches(op, current) {
|
|
619
|
+
const owner = op.owner;
|
|
620
|
+
if (!owner) return false;
|
|
621
|
+
if (owner.appUserId) return owner.appUserId === current.appUserId;
|
|
622
|
+
return owner.anonymousId !== null && owner.anonymousId === current.anonymousId;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
373
625
|
* Best-effort online probe. In a browser/RN runtime `navigator.onLine`
|
|
374
626
|
* exists; under Node / SSR it doesn't, so we treat "unknown" as online
|
|
375
627
|
* (the executor's own error handling then decides).
|
|
@@ -453,8 +705,18 @@ var OfflineQueue = class {
|
|
|
453
705
|
* SDK surface the loss (debug log) instead of silently discarding.
|
|
454
706
|
*/
|
|
455
707
|
onDropped;
|
|
708
|
+
/**
|
|
709
|
+
* The identity that is signed in right now. When set, enqueue stamps it on
|
|
710
|
+
* each op and flush/pending only touch ops it owns.
|
|
711
|
+
*/
|
|
712
|
+
currentOwner;
|
|
456
713
|
flushing = null;
|
|
457
714
|
/**
|
|
715
|
+
* Bumped by `purge()`. A replay that started before a purge sees the change
|
|
716
|
+
* and does not re-queue its op.
|
|
717
|
+
*/
|
|
718
|
+
purgeGeneration = 0;
|
|
719
|
+
/**
|
|
458
720
|
* Serializes every read-modify-write of the persisted outbox. Each mutation
|
|
459
721
|
* chains onto the previous one's promise, so concurrent `enqueue`s — and an
|
|
460
722
|
* `enqueue` racing the per-op removal inside `flush` — can never read a stale
|
|
@@ -477,6 +739,16 @@ var OfflineQueue = class {
|
|
|
477
739
|
this.executor = opts.executor;
|
|
478
740
|
this.storageKey = opts.storageKey ?? "offline_outbox";
|
|
479
741
|
this.onDropped = opts.onDropped;
|
|
742
|
+
this.currentOwner = opts.currentOwner;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* The ops the current owner may see and replay. Without an owner source
|
|
746
|
+
* every op qualifies.
|
|
747
|
+
*/
|
|
748
|
+
visible(ops) {
|
|
749
|
+
if (!this.currentOwner) return ops;
|
|
750
|
+
const current = this.currentOwner();
|
|
751
|
+
return ops.filter((op) => outboxOwnerMatches(op, current));
|
|
480
752
|
}
|
|
481
753
|
/**
|
|
482
754
|
* Run `fn` exclusively against the persisted outbox: it chains after every
|
|
@@ -533,28 +805,55 @@ var OfflineQueue = class {
|
|
|
533
805
|
get isDisposed() {
|
|
534
806
|
return this.disposed;
|
|
535
807
|
}
|
|
536
|
-
/**
|
|
537
|
-
|
|
808
|
+
/**
|
|
809
|
+
* Capture the current owner and purge generation. Take it before a write's
|
|
810
|
+
* live attempt and pass it to {@link enqueue}.
|
|
811
|
+
*/
|
|
812
|
+
ticket() {
|
|
813
|
+
return {
|
|
814
|
+
owner: this.currentOwner?.(),
|
|
815
|
+
generation: this.purgeGeneration
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Append an op to the durable outbox. Returns the persisted op. With a
|
|
820
|
+
* `ticket`, the op belongs to the ticket's owner, and the enqueue rejects
|
|
821
|
+
* (code `OFFLINE_QUEUE_PURGED`) when the queue was purged after the ticket
|
|
822
|
+
* was taken, which is what a sign-out during the live attempt does.
|
|
823
|
+
*/
|
|
824
|
+
async enqueue(op, ticket) {
|
|
538
825
|
if (this.disposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
826
|
+
const owner = op.owner ?? (ticket ? ticket.owner : this.currentOwner?.());
|
|
539
827
|
const full = {
|
|
540
828
|
...op,
|
|
829
|
+
...owner ? { owner: { ...owner } } : {},
|
|
541
830
|
idempotencyKey: op.idempotencyKey ?? newIdempotencyKey(),
|
|
542
831
|
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
543
832
|
};
|
|
544
|
-
return this.mutate((ops) =>
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
833
|
+
return this.mutate((ops) => {
|
|
834
|
+
if (ticket && ticket.generation !== this.purgeGeneration) {
|
|
835
|
+
const err = /* @__PURE__ */ new Error("amba offline queue was cleared by a sign-out while this write was in flight");
|
|
836
|
+
err.code = "OFFLINE_QUEUE_PURGED";
|
|
837
|
+
throw err;
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
next: [...ops, full],
|
|
841
|
+
result: full
|
|
842
|
+
};
|
|
843
|
+
}, { rejectIfDisposed: true });
|
|
548
844
|
}
|
|
549
|
-
/** Number of ops awaiting replay. */
|
|
845
|
+
/** Number of the current owner's ops awaiting replay. */
|
|
550
846
|
async pendingCount() {
|
|
551
|
-
return (await this.load()).length;
|
|
847
|
+
return this.visible(await this.load()).length;
|
|
552
848
|
}
|
|
553
|
-
/**
|
|
849
|
+
/**
|
|
850
|
+
* Snapshot the current owner's pending ops (a copy — mutating it does not
|
|
851
|
+
* affect the queue). Ops queued by another user stay out of the list.
|
|
852
|
+
*/
|
|
554
853
|
async pending() {
|
|
555
|
-
return [...await this.load()];
|
|
854
|
+
return [...this.visible(await this.load())];
|
|
556
855
|
}
|
|
557
|
-
/** Drop every queued op without replaying. */
|
|
856
|
+
/** Drop every queued op, whoever queued it, without replaying. */
|
|
558
857
|
async clear() {
|
|
559
858
|
await this.mutate(() => ({
|
|
560
859
|
next: [],
|
|
@@ -562,10 +861,23 @@ var OfflineQueue = class {
|
|
|
562
861
|
}));
|
|
563
862
|
}
|
|
564
863
|
/**
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
|
|
864
|
+
* Drop every queued op, including one whose replay is in flight right now:
|
|
865
|
+
* that op is not re-queued when its replay fails. The SDKs call this on
|
|
866
|
+
* sign-out so nothing the signed-out user queued survives on the device.
|
|
867
|
+
*/
|
|
868
|
+
async purge() {
|
|
869
|
+
this.purgeGeneration += 1;
|
|
870
|
+
await this.clear();
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Replay the current owner's ops in FIFO order. Each op is removed only
|
|
874
|
+
* after it succeeds; the first failure stops the flush and retains the
|
|
875
|
+
* remainder (so order is preserved and nothing is dropped mid-batch). Ops
|
|
876
|
+
* another user queued stay held for that user. Ops that can never replay
|
|
877
|
+
* are dropped through `onDropped`: one with no recorded owner (code
|
|
878
|
+
* `OFFLINE_OWNER_UNKNOWN`), and one queued before any session under an
|
|
879
|
+
* anonymous id the install no longer has (code `OFFLINE_OWNER_GONE`).
|
|
880
|
+
* Concurrent calls share one in-flight flush.
|
|
569
881
|
*/
|
|
570
882
|
async flush() {
|
|
571
883
|
if (this.disposed) return {
|
|
@@ -584,8 +896,26 @@ var OfflineQueue = class {
|
|
|
584
896
|
let dropped = 0;
|
|
585
897
|
for (;;) {
|
|
586
898
|
if (this.disposed) break;
|
|
587
|
-
const
|
|
899
|
+
const all = await this.load();
|
|
900
|
+
if (this.currentOwner) {
|
|
901
|
+
const current = this.currentOwner();
|
|
902
|
+
const orphan = all.find((o) => !o.owner || o.owner.appUserId === null && current.anonymousId !== null && o.owner.anonymousId !== current.anonymousId);
|
|
903
|
+
if (orphan) {
|
|
904
|
+
if (!await this.removeOp(orphan)) break;
|
|
905
|
+
dropped += 1;
|
|
906
|
+
this.reportDropped(orphan, orphan.owner ? {
|
|
907
|
+
code: "OFFLINE_OWNER_GONE",
|
|
908
|
+
message: "queued before sign-in under an anonymous id this install no longer has; not replayed"
|
|
909
|
+
} : {
|
|
910
|
+
code: "OFFLINE_OWNER_UNKNOWN",
|
|
911
|
+
message: "queued by an earlier SDK version without an owner; not replayed"
|
|
912
|
+
});
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const head = this.visible(all)[0];
|
|
588
917
|
if (!head) break;
|
|
918
|
+
const generation = this.purgeGeneration;
|
|
589
919
|
let replayError = void 0;
|
|
590
920
|
let ok = false;
|
|
591
921
|
try {
|
|
@@ -597,32 +927,20 @@ var OfflineQueue = class {
|
|
|
597
927
|
if (!ok && !isDeterministicError(replayError)) {
|
|
598
928
|
await this.mutate((ops) => {
|
|
599
929
|
return {
|
|
600
|
-
next: ops.some((o) => o.idempotencyKey === head.idempotencyKey)
|
|
930
|
+
next: !ops.some((o) => o.idempotencyKey === head.idempotencyKey) && generation === this.purgeGeneration ? [head, ...ops] : ops,
|
|
601
931
|
result: void 0
|
|
602
932
|
};
|
|
603
933
|
});
|
|
604
934
|
break;
|
|
605
935
|
}
|
|
606
|
-
if (!await this.
|
|
607
|
-
const idx = ops.findIndex((o) => o.idempotencyKey === head.idempotencyKey);
|
|
608
|
-
if (idx === -1) return {
|
|
609
|
-
next: ops,
|
|
610
|
-
result: false
|
|
611
|
-
};
|
|
612
|
-
return {
|
|
613
|
-
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
614
|
-
result: true
|
|
615
|
-
};
|
|
616
|
-
})) break;
|
|
936
|
+
if (!await this.removeOp(head)) break;
|
|
617
937
|
if (ok) flushed += 1;
|
|
618
938
|
else {
|
|
619
939
|
dropped += 1;
|
|
620
|
-
|
|
621
|
-
this.onDropped?.(head, replayError);
|
|
622
|
-
} catch {}
|
|
940
|
+
this.reportDropped(head, replayError);
|
|
623
941
|
}
|
|
624
942
|
}
|
|
625
|
-
const remaining = (await this.load()).length;
|
|
943
|
+
const remaining = this.visible(await this.load()).length;
|
|
626
944
|
return {
|
|
627
945
|
flushed,
|
|
628
946
|
dropped,
|
|
@@ -630,6 +948,29 @@ var OfflineQueue = class {
|
|
|
630
948
|
};
|
|
631
949
|
}
|
|
632
950
|
/**
|
|
951
|
+
* Remove exactly `op` (by idempotency key) under the lock. Robust against a
|
|
952
|
+
* concurrent enqueue having shifted the array. Resolves false when the op
|
|
953
|
+
* is already gone.
|
|
954
|
+
*/
|
|
955
|
+
removeOp(op) {
|
|
956
|
+
return this.mutate((ops) => {
|
|
957
|
+
const idx = ops.findIndex((o) => o.idempotencyKey === op.idempotencyKey);
|
|
958
|
+
if (idx === -1) return {
|
|
959
|
+
next: ops,
|
|
960
|
+
result: false
|
|
961
|
+
};
|
|
962
|
+
return {
|
|
963
|
+
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
964
|
+
result: true
|
|
965
|
+
};
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
reportDropped(op, err) {
|
|
969
|
+
try {
|
|
970
|
+
this.onDropped?.(op, err);
|
|
971
|
+
} catch {}
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
633
974
|
* Install an auto-flush listener on the runtime's `online` event so the
|
|
634
975
|
* queue drains the moment connectivity returns. Idempotent — a second
|
|
635
976
|
* call is a no-op. Returns a detach function.
|
|
@@ -1198,7 +1539,7 @@ var CollectionSubscription = class {
|
|
|
1198
1539
|
if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
1199
1540
|
const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
|
|
1200
1541
|
return openStream({
|
|
1201
|
-
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${
|
|
1542
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${pathSegment(this.name)}${qs}`,
|
|
1202
1543
|
authHeaders: this.ctx.authHeaders,
|
|
1203
1544
|
onChange: cb,
|
|
1204
1545
|
options,
|
|
@@ -1222,7 +1563,7 @@ var ConversationSubscription = class {
|
|
|
1222
1563
|
subscribe(cb, options = {}) {
|
|
1223
1564
|
if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
1224
1565
|
return openStream({
|
|
1225
|
-
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${
|
|
1566
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${pathSegment(this.conversationId)}`,
|
|
1226
1567
|
authHeaders: this.ctx.authHeaders,
|
|
1227
1568
|
onChange: cb,
|
|
1228
1569
|
options,
|
|
@@ -1482,7 +1823,7 @@ function delay(ms) {
|
|
|
1482
1823
|
//#endregion
|
|
1483
1824
|
//#region src/index.ts
|
|
1484
1825
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
1485
|
-
const SDK_VERSION = "4.0.
|
|
1826
|
+
const SDK_VERSION = "4.0.10";
|
|
1486
1827
|
/**
|
|
1487
1828
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
1488
1829
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -1723,8 +2064,20 @@ const P = {
|
|
|
1723
2064
|
deepLinks: "/v1/client/deep-links",
|
|
1724
2065
|
onboarding: "/v1/client/onboarding"
|
|
1725
2066
|
};
|
|
2067
|
+
/**
|
|
2068
|
+
* Encode one caller-supplied id, key or name as a single URL path segment.
|
|
2069
|
+
* `encodeURIComponent` escapes `/`, `?` and `#`; `.` and `..` are refused
|
|
2070
|
+
* because URL resolution treats them as steps to another route, and the
|
|
2071
|
+
* empty string is refused because it collapses onto the parent route.
|
|
2072
|
+
*/
|
|
1726
2073
|
function encodePathSegment(s) {
|
|
1727
|
-
return
|
|
2074
|
+
return pathSegment(s);
|
|
2075
|
+
}
|
|
2076
|
+
function nonceField(options) {
|
|
2077
|
+
const nonce = options?.nonce;
|
|
2078
|
+
if (nonce === void 0) return {};
|
|
2079
|
+
if (typeof nonce !== "string" || nonce.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "nonce must be a non-empty string");
|
|
2080
|
+
return { nonce };
|
|
1728
2081
|
}
|
|
1729
2082
|
/**
|
|
1730
2083
|
* Resolve the full invocation URL for a deployed function. Resolution checks
|
|
@@ -1734,8 +2087,8 @@ function encodePathSegment(s) {
|
|
|
1734
2087
|
* cached for subsequent invokes.
|
|
1735
2088
|
*/
|
|
1736
2089
|
async function resolveFunctionUrl(s, name) {
|
|
1737
|
-
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${
|
|
1738
|
-
return `https://${await resolveFunctionsProjectId(s)}.${DEFAULT_FUNCTIONS_HOST}/${
|
|
2090
|
+
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${pathSegment(name)}`;
|
|
2091
|
+
return `https://${await resolveFunctionsProjectId(s)}.${DEFAULT_FUNCTIONS_HOST}/${pathSegment(name)}`;
|
|
1739
2092
|
}
|
|
1740
2093
|
async function resolveFunctionsProjectId(s) {
|
|
1741
2094
|
if (s.resolvedFunctionsProjectId) return s.resolvedFunctionsProjectId;
|
|
@@ -1812,6 +2165,10 @@ function getOfflineQueue(s) {
|
|
|
1812
2165
|
store: offlineStore(s),
|
|
1813
2166
|
storageKey: `offline_outbox:${s.projectId}`,
|
|
1814
2167
|
executor: (op) => replayOutboxOp(s, op),
|
|
2168
|
+
currentOwner: () => ({
|
|
2169
|
+
appUserId: s.identity.getSessionToken() || s.identity.getRefreshToken() ? s.identity.getAppUserId() : null,
|
|
2170
|
+
anonymousId: s.identity.getAnonymousId()
|
|
2171
|
+
}),
|
|
1815
2172
|
onDropped: (op, err) => {
|
|
1816
2173
|
if (typeof console !== "undefined") console.warn(`[amba] dropped un-replayable offline ${op.kind} (${op.collection ?? op.event ?? "?"}):`, err);
|
|
1817
2174
|
}
|
|
@@ -1842,10 +2199,16 @@ const Amba = {
|
|
|
1842
2199
|
if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
|
|
1843
2200
|
if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
|
|
1844
2201
|
}
|
|
1845
|
-
const
|
|
2202
|
+
const tokenStorage = config.tokenStorage ?? "secure";
|
|
2203
|
+
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
2204
|
+
const secureStorage = config.secureStorage;
|
|
2205
|
+
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");
|
|
2206
|
+
const storagePrefix = config.storagePrefix ?? "amba";
|
|
2207
|
+
const persistence = new AsyncStoragePersistence(storagePrefix);
|
|
1846
2208
|
if (config.events?.batching) await persistence.assertDurable();
|
|
1847
2209
|
await persistence.hydrate();
|
|
1848
|
-
const
|
|
2210
|
+
const vault = await resolveTokenVault(tokenStorage, storagePrefix, secureStorage);
|
|
2211
|
+
const identity = await IdentityStore.create(persistence, vault);
|
|
1849
2212
|
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
1850
2213
|
const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
|
|
1851
2214
|
const http = new AmbaHttpClient({
|
|
@@ -1946,6 +2309,7 @@ const Amba = {
|
|
|
1946
2309
|
timestamp: occurredAt
|
|
1947
2310
|
};
|
|
1948
2311
|
if (options?.telemetry) body["telemetry"] = true;
|
|
2312
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
1949
2313
|
try {
|
|
1950
2314
|
await s.http.post(P.events, body);
|
|
1951
2315
|
} catch (err) {
|
|
@@ -1956,7 +2320,7 @@ const Amba = {
|
|
|
1956
2320
|
properties,
|
|
1957
2321
|
telemetry: options?.telemetry,
|
|
1958
2322
|
occurredAt
|
|
1959
|
-
});
|
|
2323
|
+
}, outboxTicket);
|
|
1960
2324
|
return;
|
|
1961
2325
|
}
|
|
1962
2326
|
throw err;
|
|
@@ -2000,18 +2364,20 @@ const Amba = {
|
|
|
2000
2364
|
password
|
|
2001
2365
|
});
|
|
2002
2366
|
},
|
|
2003
|
-
signInWithApple: async (identityToken) => {
|
|
2367
|
+
signInWithApple: async (identityToken, options) => {
|
|
2004
2368
|
if (identityToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "identity_token must not be empty");
|
|
2005
2369
|
return postAuth(getState(), P.authSocial, {
|
|
2006
2370
|
provider: "apple",
|
|
2007
|
-
id_token: identityToken
|
|
2371
|
+
id_token: identityToken,
|
|
2372
|
+
...nonceField(options)
|
|
2008
2373
|
});
|
|
2009
2374
|
},
|
|
2010
|
-
signInWithGoogle: async (idToken) => {
|
|
2375
|
+
signInWithGoogle: async (idToken, options) => {
|
|
2011
2376
|
if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
|
|
2012
2377
|
return postAuth(getState(), P.authSocial, {
|
|
2013
2378
|
provider: "google",
|
|
2014
|
-
id_token: idToken
|
|
2379
|
+
id_token: idToken,
|
|
2380
|
+
...nonceField(options)
|
|
2015
2381
|
});
|
|
2016
2382
|
},
|
|
2017
2383
|
requestEmailOtp: async (email) => {
|
|
@@ -2046,7 +2412,7 @@ const Amba = {
|
|
|
2046
2412
|
if (token.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "token must not be empty");
|
|
2047
2413
|
return postAuth(getState(), P.authMagicLinkVerify, { token });
|
|
2048
2414
|
},
|
|
2049
|
-
linkAccount: async (provider, credential) => {
|
|
2415
|
+
linkAccount: async (provider, credential, options) => {
|
|
2050
2416
|
const providerTrimmed = provider?.trim() ?? "";
|
|
2051
2417
|
if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
|
|
2052
2418
|
if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
|
|
@@ -2056,7 +2422,8 @@ const Amba = {
|
|
|
2056
2422
|
return postAuth(s, P.authLink, {
|
|
2057
2423
|
provider: providerTrimmed,
|
|
2058
2424
|
token: credential,
|
|
2059
|
-
session_token: sessionToken
|
|
2425
|
+
session_token: sessionToken,
|
|
2426
|
+
...nonceField(options)
|
|
2060
2427
|
});
|
|
2061
2428
|
},
|
|
2062
2429
|
linkSmsOtp: async (phone, code) => {
|
|
@@ -2115,6 +2482,11 @@ const Amba = {
|
|
|
2115
2482
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
2116
2483
|
}
|
|
2117
2484
|
await s.identity.clearSession(rotateAnonymousId);
|
|
2485
|
+
try {
|
|
2486
|
+
await getOfflineQueue(s).purge();
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
2489
|
+
}
|
|
2118
2490
|
s.eventQueue?.flush();
|
|
2119
2491
|
notifyAuthSubscribers(null);
|
|
2120
2492
|
},
|
|
@@ -2230,6 +2602,7 @@ const Amba = {
|
|
|
2230
2602
|
const s = getState();
|
|
2231
2603
|
if (s.offlineEnabled) {
|
|
2232
2604
|
const idempotencyKey = (typeof row["idempotency_key"] === "string" ? row["idempotency_key"] : void 0) ?? newIdempotencyKey();
|
|
2605
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
2233
2606
|
try {
|
|
2234
2607
|
return (await s.http.post(`${P.collections}/${encodePathSegment(name)}`, {
|
|
2235
2608
|
...row,
|
|
@@ -2242,7 +2615,7 @@ const Amba = {
|
|
|
2242
2615
|
collection: name,
|
|
2243
2616
|
payload: row,
|
|
2244
2617
|
idempotencyKey
|
|
2245
|
-
});
|
|
2618
|
+
}, outboxTicket);
|
|
2246
2619
|
return {
|
|
2247
2620
|
...row,
|
|
2248
2621
|
_amba_pending: true
|
|
@@ -2255,40 +2628,46 @@ const Amba = {
|
|
|
2255
2628
|
},
|
|
2256
2629
|
update: async (name, id, set) => {
|
|
2257
2630
|
const s = getState();
|
|
2258
|
-
if (s.offlineEnabled)
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2631
|
+
if (s.offlineEnabled) {
|
|
2632
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
2633
|
+
try {
|
|
2634
|
+
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
2635
|
+
} catch (err) {
|
|
2636
|
+
if (isOfflineError(err)) {
|
|
2637
|
+
await getOfflineQueue(s).enqueue({
|
|
2638
|
+
kind: "collection.update",
|
|
2639
|
+
collection: name,
|
|
2640
|
+
id,
|
|
2641
|
+
payload: set
|
|
2642
|
+
}, outboxTicket);
|
|
2643
|
+
return {
|
|
2644
|
+
id,
|
|
2645
|
+
...set,
|
|
2646
|
+
_amba_pending: true
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
throw err;
|
|
2273
2650
|
}
|
|
2274
|
-
throw err;
|
|
2275
2651
|
}
|
|
2276
2652
|
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
2277
2653
|
},
|
|
2278
2654
|
delete: async (name, id) => {
|
|
2279
2655
|
const s = getState();
|
|
2280
|
-
if (s.offlineEnabled)
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2656
|
+
if (s.offlineEnabled) {
|
|
2657
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
2658
|
+
try {
|
|
2659
|
+
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
if (isOfflineError(err)) {
|
|
2662
|
+
await getOfflineQueue(s).enqueue({
|
|
2663
|
+
kind: "collection.delete",
|
|
2664
|
+
collection: name,
|
|
2665
|
+
id
|
|
2666
|
+
}, outboxTicket);
|
|
2667
|
+
return { data: { deleted: true } };
|
|
2668
|
+
}
|
|
2669
|
+
throw err;
|
|
2290
2670
|
}
|
|
2291
|
-
throw err;
|
|
2292
2671
|
}
|
|
2293
2672
|
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
2294
2673
|
},
|
|
@@ -2649,7 +3028,7 @@ const Amba = {
|
|
|
2649
3028
|
return (await getState().http.get(P.friends, { state: "accepted" })).data;
|
|
2650
3029
|
},
|
|
2651
3030
|
sendRequest: async (userId) => {
|
|
2652
|
-
return (await getState().http.post(`${P.friends}/request`, {
|
|
3031
|
+
return (await getState().http.post(`${P.friends}/request`, { addressee_id: userId })).data;
|
|
2653
3032
|
},
|
|
2654
3033
|
acceptRequest: async (friendshipId) => {
|
|
2655
3034
|
return (await getState().http.post(`${P.friends}/${encodePathSegment(friendshipId)}/accept`, {})).data;
|