@tinycloud/node-sdk 3.8.0-beta.0 → 3.8.0-beta.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.
@@ -1,6 +1,6 @@
1
1
  import { NamedKeyClient } from '@tinycloud/sdk-services/encryption/named-keys';
2
2
  export { DecryptManyResult, KeyCallOptions, KeyDescriptor, KeyEncryptOptions, NamedKeyClient, NamedKeyEnvelope, NamedKeyError, NamedKeyErrorCode, PublicKeyDescriptor, buildKeyAdminFacts, buildKeyDescribeFacts } from '@tinycloud/sdk-services/encryption/named-keys';
3
- import { T as TinyCloudNode } from './TinyCloudNode-ggmlnSfI.cjs';
3
+ import { T as TinyCloudNode } from './TinyCloudNode-CVO41O12.cjs';
4
4
  import '@tinycloud/sdk-core';
5
5
  import '@tinycloud/sdk-services/kv/replication';
6
6
  import '@tinycloud/sdk-services';
@@ -1,6 +1,6 @@
1
1
  import { NamedKeyClient } from '@tinycloud/sdk-services/encryption/named-keys';
2
2
  export { DecryptManyResult, KeyCallOptions, KeyDescriptor, KeyEncryptOptions, NamedKeyClient, NamedKeyEnvelope, NamedKeyError, NamedKeyErrorCode, PublicKeyDescriptor, buildKeyAdminFacts, buildKeyDescribeFacts } from '@tinycloud/sdk-services/encryption/named-keys';
3
- import { T as TinyCloudNode } from './TinyCloudNode-BtKzLDJX.js';
3
+ import { T as TinyCloudNode } from './TinyCloudNode-B6VOBrCb.js';
4
4
  import '@tinycloud/sdk-core';
5
5
  import '@tinycloud/sdk-services/kv/replication';
6
6
  import '@tinycloud/sdk-services';
package/dist/index.cjs CHANGED
@@ -17053,7 +17053,7 @@ function augmentSignInEntriesWithReplication(input) {
17053
17053
  }
17054
17054
  eligiblePaths.push(path);
17055
17055
  }
17056
- if (eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
17056
+ if (replication.syncScope !== "prefixes" && eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
17057
17057
  return [{
17058
17058
  service: KV_SERVICE,
17059
17059
  space: input.primarySpaceId,
@@ -17086,6 +17086,9 @@ var init_authority_sign_in = __esm({
17086
17086
 
17087
17087
  // src/replication/config.ts
17088
17088
  function assertValidReplicationConfig(replication) {
17089
+ if (replication?.syncScope !== void 0 && replication.syncScope !== "prefixes") {
17090
+ throw new TypeError('replication.syncScope must be "prefixes"');
17091
+ }
17089
17092
  if (replication === void 0 || !replication.enabled) return;
17090
17093
  if (replication.storage === void 0 || replication.storage === null) {
17091
17094
  throw new TypeError("replication.enabled requires replication.storage");
@@ -17411,6 +17414,7 @@ __export(authority_exports, {
17411
17414
  assertValidReplicationConfig: () => assertValidReplicationConfig,
17412
17415
  augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17413
17416
  createReplicationAuthority: () => createReplicationAuthority,
17417
+ eraseReplicaLocalCopy: () => eraseReplicaLocalCopy,
17414
17418
  establishReplicaSessionStamp: () => establishReplicaSessionStamp,
17415
17419
  hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17416
17420
  mintNodeReplicationGrant: () => mintNodeReplicationGrant,
@@ -17441,6 +17445,38 @@ function reportReplicaDenial(result, reason, onEvent, context) {
17441
17445
  } catch {
17442
17446
  }
17443
17447
  }
17448
+ async function eraseReplicaLocalCopy(lifecycle, trigger, denyStatus, handlers) {
17449
+ if (lifecycle?.kind !== "indexeddb" || lifecycle.eraseLocalCopy === void 0) return void 0;
17450
+ let outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
17451
+ for (let attempt = 0; attempt <= ERASE_RETRY_DELAYS_MS.length; attempt++) {
17452
+ if (attempt > 0) await new Promise((resolve2) => setTimeout(resolve2, ERASE_RETRY_DELAYS_MS[attempt - 1]));
17453
+ try {
17454
+ outcome = await lifecycle.eraseLocalCopy({ signal: AbortSignal.timeout(ERASE_ATTEMPT_MS) });
17455
+ } catch {
17456
+ outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
17457
+ }
17458
+ if (outcome.status !== "failed") break;
17459
+ }
17460
+ const keysKept = denyStatus === "marker" || denyStatus === "unguarded";
17461
+ const purge = outcome.status === "erased" ? { ok: true } : { ok: false, retryNow: outcome.status === "failed" };
17462
+ if (outcome.status === "erased" || outcome.status === "refused" && !keysKept) return { outcome, purge };
17463
+ const logoutPending = outcome.status === "failed" && outcome.marker;
17464
+ const event = {
17465
+ type: "replica.purge.failed",
17466
+ trigger,
17467
+ reason: logoutPending ? "logout_pending" : "gate_unavailable",
17468
+ code: outcome.code,
17469
+ retryNow: outcome.status === "failed"
17470
+ };
17471
+ for (const handler of handlers) {
17472
+ try {
17473
+ handler(event);
17474
+ } catch (error) {
17475
+ console.error('Error in "replica.purge.failed" handler:', error);
17476
+ }
17477
+ }
17478
+ return logoutPending ? { outcome, purge, logoutPending } : { outcome, purge };
17479
+ }
17444
17480
  function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
17445
17481
  if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
17446
17482
  try {
@@ -17699,7 +17735,7 @@ function createReplicationAuthority(host) {
17699
17735
  };
17700
17736
  return authority;
17701
17737
  }
17702
- var import_siwe3, import_sdk_core12, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17738
+ var import_siwe3, import_sdk_core12, import_replica, parseUcanGrant, ERASE_RETRY_DELAYS_MS, ERASE_ATTEMPT_MS, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17703
17739
  var init_authority = __esm({
17704
17740
  "src/replication/authority.ts"() {
17705
17741
  "use strict";
@@ -17709,6 +17745,8 @@ var init_authority = __esm({
17709
17745
  init_config();
17710
17746
  init_authority_sign_in();
17711
17747
  parseUcanGrant = import_replica.parseUcanGrant;
17748
+ ERASE_RETRY_DELAYS_MS = [150, 450];
17749
+ ERASE_ATTEMPT_MS = 3e3;
17712
17750
  KV_GET2 = import_sdk_core12.KV.GET;
17713
17751
  KV_SYNC2 = import_sdk_core12.KV.SYNC;
17714
17752
  KV_SERVICE2 = "tinycloud.kv";
@@ -21562,6 +21600,7 @@ function replicaRestoreRefusal(error) {
21562
21600
  case "purge_pending":
21563
21601
  case "logout_pending":
21564
21602
  case "gate_unavailable":
21603
+ case "wallet_provider_required":
21565
21604
  return error.gate;
21566
21605
  }
21567
21606
  }
@@ -21596,6 +21635,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21596
21635
  this.auth = null;
21597
21636
  this.tc = null;
21598
21637
  this._chainId = 1;
21638
+ this.replicaPurgeFailedHandlers = /* @__PURE__ */ new Set();
21599
21639
  /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21600
21640
  this.establishReplicaStamp = async (session) => {
21601
21641
  const replication = this.config.replication;
@@ -21802,6 +21842,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21802
21842
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
21803
21843
  },
21804
21844
  clearPending: async () => (await runtime).control.clearPending(),
21845
+ purgeLocalCopy: async () => (await this.eraseReplicaLocalCopy("purge_local_copy"))?.purge ?? { ok: false, retryNow: false },
21805
21846
  close: async () => (await runtime).control.close()
21806
21847
  };
21807
21848
  }
@@ -21856,18 +21897,14 @@ var _TinyCloudNode = class _TinyCloudNode {
21856
21897
  get freshEncryptionNetworkCreated() {
21857
21898
  return this._freshEncryptionNetworkCreated;
21858
21899
  }
21859
- /**
21860
- * Subscribe to `storage.full`. It fires on the first write any service had
21861
- * rejected because the owner's TinyCloud storage is full
21862
- * (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`), so an app can show
21863
- * one read-only banner. Reads keep working. For the signed-in account's own
21864
- * storage it fires once per episode: the episode ends only when a write to
21865
- * that storage succeeds, or on sign-out. A `storage.status()` read does not
21866
- * end it. A rejection on another owner's delegated space fires once per
21867
- * owner, until that owner's `storage.status()` reports space again. Returns
21868
- * the unsubscribe function.
21869
- */
21870
21900
  on(event, handler) {
21901
+ if (event === "replica.purge.failed") {
21902
+ const listener = handler;
21903
+ this.replicaPurgeFailedHandlers.add(listener);
21904
+ return () => {
21905
+ this.replicaPurgeFailedHandlers.delete(listener);
21906
+ };
21907
+ }
21871
21908
  if (event !== "storage.full") {
21872
21909
  throw new Error(`Unknown TinyCloud event: ${String(event)}`);
21873
21910
  }
@@ -22087,7 +22124,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22087
22124
  replication: config.replication?.enabled === true ? {
22088
22125
  enabled: true,
22089
22126
  prefixes: config.replication.prefixes,
22090
- allowSecrets: config.replication.allowSecrets === true
22127
+ allowSecrets: config.replication.allowSecrets === true,
22128
+ syncScope: config.replication.syncScope
22091
22129
  } : void 0
22092
22130
  });
22093
22131
  this.tc = new import_sdk_core10.TinyCloud(this.auth, {
@@ -22347,6 +22385,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22347
22385
  const leavingAuth = this.auth;
22348
22386
  const leavingContext = this._serviceContext;
22349
22387
  let denied;
22388
+ let erased;
22350
22389
  try {
22351
22390
  this.teardownSession();
22352
22391
  } finally {
@@ -22357,6 +22396,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22357
22396
  try {
22358
22397
  await this.accountRegistryTail;
22359
22398
  await this.quiesceReplicaRuntime();
22399
+ if (denied) erased = await this.eraseReplicaLocalCopy("sign_out", denied.status);
22360
22400
  } finally {
22361
22401
  if (leavingAddress) {
22362
22402
  await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
@@ -22364,8 +22404,37 @@ var _TinyCloudNode = class _TinyCloudNode {
22364
22404
  }
22365
22405
  }
22366
22406
  }
22367
- if (denied?.status === "unguarded") {
22368
- throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
22407
+ if (denied?.status === "unguarded" && erased?.outcome.status !== "erased") {
22408
+ throw Object.assign(
22409
+ new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`),
22410
+ {
22411
+ code: "REPLICA_PURGE_FAILED",
22412
+ reason: "gate_unavailable",
22413
+ retryNow: erased?.outcome.status === "failed"
22414
+ }
22415
+ );
22416
+ }
22417
+ }
22418
+ /**
22419
+ * Erases the browser local copy after a deny (retry, event and next-load marker live in the lazy
22420
+ * replication authority). Never rejects; `undefined` when replication is off or the storage keeps
22421
+ * no browser database.
22422
+ */
22423
+ async eraseReplicaLocalCopy(trigger, denyStatus) {
22424
+ const replication = this.config.replication;
22425
+ if (!replication?.enabled) return void 0;
22426
+ try {
22427
+ const { eraseReplicaLocalCopy: eraseReplicaLocalCopy2 } = await getNodeReplicationLoaders().authority();
22428
+ const erased = await eraseReplicaLocalCopy2(
22429
+ replication.storage.lifecycle,
22430
+ trigger,
22431
+ denyStatus,
22432
+ this.replicaPurgeFailedHandlers
22433
+ );
22434
+ if (erased?.logoutPending) this.replicaRestoreGate = "logout_pending";
22435
+ return erased;
22436
+ } catch {
22437
+ return void 0;
22369
22438
  }
22370
22439
  }
22371
22440
  /**
@@ -22529,6 +22598,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22529
22598
  const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core10.pkhDid)(this.address, this._chainId) : void 0);
22530
22599
  if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22531
22600
  const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22601
+ if (denial) {
22602
+ await this.quiesceReplicaRuntime();
22603
+ await this.eraseReplicaLocalCopy("account_switch", denial.status);
22604
+ }
22532
22605
  this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22533
22606
  }
22534
22607
  if (address !== this._address || chainId !== this._chainId) {
@@ -24509,7 +24582,8 @@ var _TinyCloudNode = class _TinyCloudNode {
24509
24582
  replication: this.config.replication?.enabled === true ? {
24510
24583
  enabled: true,
24511
24584
  prefixes: this.config.replication.prefixes,
24512
- allowSecrets: this.config.replication.allowSecrets === true
24585
+ allowSecrets: this.config.replication.allowSecrets === true,
24586
+ syncScope: this.config.replication.syncScope
24513
24587
  } : void 0
24514
24588
  });
24515
24589
  this.tc = new import_sdk_core10.TinyCloud(this.auth, {
@@ -24571,7 +24645,8 @@ var _TinyCloudNode = class _TinyCloudNode {
24571
24645
  replication: this.config.replication?.enabled === true ? {
24572
24646
  enabled: true,
24573
24647
  prefixes: this.config.replication.prefixes,
24574
- allowSecrets: this.config.replication.allowSecrets === true
24648
+ allowSecrets: this.config.replication.allowSecrets === true,
24649
+ syncScope: this.config.replication.syncScope
24575
24650
  } : void 0
24576
24651
  });
24577
24652
  this.tc = new import_sdk_core10.TinyCloud(this.auth, {