@tinycloud/node-sdk 3.2.1-beta.0 → 3.2.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -18494,6 +18494,11 @@ var NodeUserAuthorization = class {
18494
18494
  this._nodeFeatures = [];
18495
18495
  this._nodeFeaturesKnown = false;
18496
18496
  this._lastActivationSkippedSpaceIds = [];
18497
+ /**
18498
+ * Spaces the sign-in activation reported hosted. Only the first POST of a
18499
+ * delegation lists them: the node answers a repeat with empty lists.
18500
+ */
18501
+ this._lastActivationActivatedSpaceIds = [];
18497
18502
  this.wasm = config.wasmBindings;
18498
18503
  this.signer = config.signer;
18499
18504
  this.signStrategy = config.signStrategy ?? defaultSignStrategy;
@@ -18623,6 +18628,7 @@ var NodeUserAuthorization = class {
18623
18628
  this._nodeInfoIdentity = void 0;
18624
18629
  this.tinycloudHosts = hosts && hosts.length > 0 ? [...hosts] : void 0;
18625
18630
  this._lastActivationSkippedSpaceIds = [];
18631
+ this._lastActivationActivatedSpaceIds = [];
18626
18632
  if (!session) {
18627
18633
  this._session = void 0;
18628
18634
  this._tinyCloudSession = void 0;
@@ -18717,6 +18723,9 @@ var NodeUserAuthorization = class {
18717
18723
  get lastActivationSkippedSpaceIds() {
18718
18724
  return [...this._lastActivationSkippedSpaceIds];
18719
18725
  }
18726
+ get lastActivationActivatedSpaceIds() {
18727
+ return [...this._lastActivationActivatedSpaceIds];
18728
+ }
18720
18729
  /**
18721
18730
  * Compute the `abilities` map the WASM `prepareSession` call should
18722
18731
  * see at sign-in time.
@@ -19212,8 +19221,10 @@ var NodeUserAuthorization = class {
19212
19221
  recordActivationSkippedSpaces(result, primarySpaceId) {
19213
19222
  if (result.success) {
19214
19223
  this._lastActivationSkippedSpaceIds = result.skipped ?? [];
19224
+ this._lastActivationActivatedSpaceIds = result.activated ?? [];
19215
19225
  return;
19216
19226
  }
19227
+ this._lastActivationActivatedSpaceIds = [];
19217
19228
  this._lastActivationSkippedSpaceIds = result.status === 404 ? [primarySpaceId] : [];
19218
19229
  }
19219
19230
  async createBootstrapSession(options) {
@@ -20943,10 +20954,26 @@ var _TinyCloudNode = class _TinyCloudNode {
20943
20954
  * hosting is confirmed against the session that was active at the time.
20944
20955
  */
20945
20956
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
20957
+ /**
20958
+ * Owned space ids known to be hosted for the current session: the sign-in
20959
+ * activation listed them as `activated`, or this session hosted them. Unlike
20960
+ * {@link confirmedHostedSpaceIds}, nothing here is inferred from a space being
20961
+ * merely absent from `skipped`. Read by {@link ensureOwnedSpaceHosted}.
20962
+ */
20963
+ this.hostedOwnedSpaceIds = /* @__PURE__ */ new Set();
20964
+ /**
20965
+ * In-flight host delegations, so concurrent callers share one approval per
20966
+ * space. `waiters` counts callers still waiting; the request is cancelled
20967
+ * once every caller with a signal has aborted and none waits without one.
20968
+ */
20969
+ this.ownedSpaceHosts = /* @__PURE__ */ new Map();
20946
20970
  /** Serializes account-registry writes for the active node instance. */
20947
20971
  this.accountRegistryTail = Promise.resolve();
20948
- /** Bounds best-effort registry work on the sign-in critical path. */
20949
- this.accountRegistryDeadlineMs = 4e3;
20972
+ /**
20973
+ * Hang guard for the background registry sync. It is off the sign-in path,
20974
+ * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
20975
+ */
20976
+ this.accountRegistryDeadlineMs = 3e4;
20950
20977
  /**
20951
20978
  * True when the last signIn() detected an interactive signer and skipped
20952
20979
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -21541,6 +21568,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21541
21568
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
21542
21569
  );
21543
21570
  }
21571
+ this.pendingAccountRegistrySync?.controller.abort();
21544
21572
  await this.accountRegistryTail;
21545
21573
  await this.wasmBindings.ensureInitialized?.();
21546
21574
  const address = (0, import_sdk_core9.canonicalizeAddress)(await this.signer.getAddress());
@@ -21561,8 +21589,13 @@ var _TinyCloudNode = class _TinyCloudNode {
21561
21589
  this._serviceContext = void 0;
21562
21590
  this.runtimePermissionGrants = [];
21563
21591
  this.confirmedHostedSpaceIds.clear();
21592
+ this.hostedOwnedSpaceIds.clear();
21564
21593
  await this.tc.signIn(options);
21594
+ this.cancelOwnedSpaceHosts();
21565
21595
  this.syncResolvedHostFromAuth();
21596
+ for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
21597
+ this.hostedOwnedSpaceIds.add(spaceId);
21598
+ }
21566
21599
  this.replicationDelegateSession = void 0;
21567
21600
  this.replicationScopeError = void 0;
21568
21601
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
@@ -21587,10 +21620,20 @@ var _TinyCloudNode = class _TinyCloudNode {
21587
21620
  }
21588
21621
  if (!bootstrapped) {
21589
21622
  this.scheduleAccountRegistrySync();
21590
- await this.accountRegistryTail;
21591
21623
  }
21592
21624
  this.notificationHandler.success("Successfully signed in");
21593
21625
  }
21626
+ /**
21627
+ * Wait for the account-registry sync that {@link signIn} starts in the
21628
+ * background (account index, this app's manifest record, space list).
21629
+ * Resolves once it has finished, failed or hit its hang guard; it never
21630
+ * rejects, and failures are logged as warnings. `signIn()` does not wait for
21631
+ * it, so call this only when you need the registry written, or before a first
21632
+ * write against a SQLite-backed node older than 1.20.2 (TC-899).
21633
+ */
21634
+ async waitForAccountRegistrySync() {
21635
+ await this.accountRegistryTail;
21636
+ }
21594
21637
  ownedSpaceId(name) {
21595
21638
  if (!this._address) {
21596
21639
  throw new Error("Cannot resolve owned space before sign-in");
@@ -22196,17 +22239,17 @@ var _TinyCloudNode = class _TinyCloudNode {
22196
22239
  clearTimeout(deadline);
22197
22240
  }
22198
22241
  });
22199
- this.pendingAccountRegistrySync = { session, promise };
22200
- const clearPendingSync = () => {
22242
+ this.pendingAccountRegistrySync = { session, promise, controller };
22243
+ const settle = () => {
22244
+ clearTimeout(deadline);
22201
22245
  if (this.pendingAccountRegistrySync?.promise === promise) {
22202
22246
  this.pendingAccountRegistrySync = void 0;
22203
22247
  }
22204
22248
  };
22205
- void promise.then(clearPendingSync, clearPendingSync);
22206
- void promise.then(
22207
- () => clearTimeout(deadline),
22208
- () => clearTimeout(deadline)
22209
- );
22249
+ void promise.then(settle, (error) => {
22250
+ settle();
22251
+ console.warn("TinyCloud account registry sync failed", error);
22252
+ });
22210
22253
  }
22211
22254
  enqueueAccountRegistryOperation(session, task) {
22212
22255
  const operation = this.accountRegistryTail.then(async () => {
@@ -22244,10 +22287,15 @@ var _TinyCloudNode = class _TinyCloudNode {
22244
22287
  }
22245
22288
  async withAccountRegistryRetry(task, signal) {
22246
22289
  const delays = [250, 1e3, 3e3];
22290
+ const stopped = (detail) => {
22291
+ if (signal.reason instanceof DOMException && signal.reason.name === "TimeoutError") {
22292
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", detail);
22293
+ }
22294
+ };
22247
22295
  let lastError;
22248
22296
  for (let attempt = 0; attempt < delays.length; attempt += 1) {
22249
22297
  if (signal.aborted) {
22250
- console.warn("TinyCloud account registry sync stopped: deadline exceeded", signal.reason);
22298
+ stopped(signal.reason);
22251
22299
  return;
22252
22300
  }
22253
22301
  try {
@@ -22255,7 +22303,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22255
22303
  return;
22256
22304
  } catch (error) {
22257
22305
  if (signal.aborted) {
22258
- console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
22306
+ stopped(error);
22259
22307
  return;
22260
22308
  }
22261
22309
  const verdict = (0, import_sdk_core9.authorizationVerdictOf)(error);
@@ -22348,7 +22396,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22348
22396
  if (!session) {
22349
22397
  throw new Error("Owned space hosting requires an active session");
22350
22398
  }
22351
- if (this.confirmedHostedSpaceIds.has(spaceId)) {
22399
+ if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
22352
22400
  return;
22353
22401
  }
22354
22402
  const host = this.hosts[0] ?? this.config.host;
@@ -22366,8 +22414,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22366
22414
  { cause: activation }
22367
22415
  );
22368
22416
  }
22369
- const authorization = this.auth;
22370
- const created = signal ? await authorization.hostOwnedSpaceResult(spaceId, void 0, signal) : await authorization.hostOwnedSpaceResult(spaceId);
22417
+ const created = await this.hostOwnedSpaceOnce(spaceId, signal);
22371
22418
  if (!created.success) {
22372
22419
  throw Object.assign(
22373
22420
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
@@ -22384,7 +22431,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22384
22431
  { cause: retry }
22385
22432
  );
22386
22433
  }
22387
- this.confirmedHostedSpaceIds.add(spaceId);
22434
+ if (this.auth.tinyCloudSession === session) {
22435
+ this.confirmedHostedSpaceIds.add(spaceId);
22436
+ this.hostedOwnedSpaceIds.add(spaceId);
22437
+ }
22388
22438
  }
22389
22439
  /**
22390
22440
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -22416,7 +22466,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22416
22466
  if (!host) {
22417
22467
  throw new Error("Owned space hosting requires a TinyCloud host");
22418
22468
  }
22419
- const hosted = await this.auth.hostOwnedSpaceResult(spaceId);
22469
+ const hosted = await this.hostOwnedSpaceOnce(spaceId);
22420
22470
  if (!hosted.success) {
22421
22471
  throw Object.assign(
22422
22472
  new Error(`Failed to host owned space ${spaceId}: ${describeHostFailure2(hosted)}`),
@@ -22435,6 +22485,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22435
22485
  { cause: activation }
22436
22486
  );
22437
22487
  }
22488
+ if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
22438
22489
  try {
22439
22490
  await this.enqueueAccountRegistryOperation(session, async () => {
22440
22491
  await this.account.spaces.register({
@@ -22462,18 +22513,19 @@ var _TinyCloudNode = class _TinyCloudNode {
22462
22513
  * registered on the node.
22463
22514
  *
22464
22515
  * Calling this resolves `name` to the owner's owned-space URI
22465
- * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It first consults the
22466
- * account-space spaces registry (`account/spaces/{space_id}`, the canonical
22467
- * KV source of truth, fronted by a best-effort SQLite index): if the space is
22468
- * already registered/hosted it returns the URI WITHOUT submitting a host
22469
- * delegation, avoiding a redundant host-SIWE signature prompt for owners who
22470
- * already use the space. Only when the space is absent — or the registry
22471
- * check fails for any reason (e.g. a cold SQLite index reporting
22472
- * `no such table: spaces`) — does it fall through to {@link hostOwnedSpace}.
22516
+ * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It returns WITHOUT
22517
+ * submitting a host delegation when the space is known to be hosted: the
22518
+ * sign-in activation listed it as `activated` (any space the session's recap
22519
+ * covers), this session already hosted it, or the account-space spaces
22520
+ * registry (`account/spaces/{space_id}`, the canonical KV source of truth,
22521
+ * fronted by a best-effort SQLite index) has its record. Otherwise it falls
22522
+ * through to {@link hostOwnedSpace}, which asks for a signature; a host
22523
+ * already in flight for the same space (e.g. from the background registry
22524
+ * sync) is shared rather than asked for twice.
22473
22525
  *
22474
- * The registry check is purely an optimization: any failure falls back to
22475
- * hosting, and the host SIWE is idempotent server-side, so re-hosting an
22476
- * existing space remains a safe no-op. Must be called after {@link signIn}.
22526
+ * Hosting is still needed when the space really isn't hosted, or when the
22527
+ * session's recap doesn't cover it and the registry has no record; the host
22528
+ * SIWE is idempotent server-side. Must be called after {@link signIn}.
22477
22529
  *
22478
22530
  * @param name - The owned space name (e.g. `"secrets"`).
22479
22531
  * @returns The hosted owned-space URI.
@@ -22482,13 +22534,57 @@ var _TinyCloudNode = class _TinyCloudNode {
22482
22534
  if (!this.auth || !this.auth.tinyCloudSession) {
22483
22535
  throw new Error("Not signed in. Call signIn() first.");
22484
22536
  }
22485
- await this.accountRegistryTail;
22486
22537
  const spaceId = this.ownedSpaceId(name);
22487
- if (await this.isOwnedSpaceRegistered(spaceId)) {
22538
+ if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
22488
22539
  return spaceId;
22489
22540
  }
22490
22541
  return this.hostOwnedSpace(name);
22491
22542
  }
22543
+ /**
22544
+ * Submit the host delegation for an owned space, sharing one in-flight
22545
+ * request (and so one approval) between concurrent callers in this session,
22546
+ * such as the background registry sync and {@link ensureOwnedSpaceHosted}.
22547
+ * Each caller's `signal` releases only its own wait; the request itself is
22548
+ * cancelled once no caller is left waiting, or when the session is replaced
22549
+ * ({@link cancelOwnedSpaceHosts}), so a late approval is discarded.
22550
+ */
22551
+ hostOwnedSpaceOnce(spaceId, signal) {
22552
+ if (signal?.aborted) return Promise.reject(signal.reason);
22553
+ let entry = this.ownedSpaceHosts.get(spaceId);
22554
+ if (!entry) {
22555
+ const controller = new AbortController();
22556
+ const promise = this.auth.hostOwnedSpaceResult(spaceId, void 0, controller.signal);
22557
+ const created = { promise, controller, waiters: 0 };
22558
+ this.ownedSpaceHosts.set(spaceId, created);
22559
+ const forget = () => {
22560
+ if (this.ownedSpaceHosts.get(spaceId) === created) this.ownedSpaceHosts.delete(spaceId);
22561
+ };
22562
+ promise.then(forget, forget);
22563
+ entry = created;
22564
+ }
22565
+ if (!signal) {
22566
+ entry.waiters = Number.POSITIVE_INFINITY;
22567
+ return entry.promise;
22568
+ }
22569
+ entry.waiters += 1;
22570
+ const shared = entry;
22571
+ return new Promise((resolve, reject2) => {
22572
+ const onAbort = () => {
22573
+ reject2(signal.reason);
22574
+ shared.waiters -= 1;
22575
+ if (shared.waiters !== 0) return;
22576
+ shared.controller.abort(signal.reason);
22577
+ if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
22578
+ };
22579
+ signal.addEventListener("abort", onAbort, { once: true });
22580
+ shared.promise.then(resolve, reject2).finally(() => signal.removeEventListener("abort", onAbort));
22581
+ });
22582
+ }
22583
+ /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
22584
+ cancelOwnedSpaceHosts() {
22585
+ for (const { controller } of this.ownedSpaceHosts.values()) controller.abort();
22586
+ this.ownedSpaceHosts.clear();
22587
+ }
22492
22588
  /** Resolve and authenticate the owner encoded by an owned-space URI. */
22493
22589
  credentialSpaceOwnerDid(spaceId) {
22494
22590
  const ownerDid = this.ownerDidFromSpaceId(spaceId);
@@ -22731,6 +22827,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22731
22827
  sessionExpiry: stagedSessionExpiry,
22732
22828
  recap: stagedRecap
22733
22829
  });
22830
+ this.pendingAccountRegistrySync?.controller.abort();
22831
+ await this.accountRegistryTail;
22734
22832
  const oldGraph = this._serviceGraph;
22735
22833
  const oldCore = this.tc;
22736
22834
  this.sessionManager = stagedManager;
@@ -22764,6 +22862,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22764
22862
  this._recapOperationsCache = stagedPrimary.cache;
22765
22863
  this.runtimePermissionGrants = stagedPrimary.grants;
22766
22864
  this.confirmedHostedSpaceIds.clear();
22865
+ this.hostedOwnedSpaceIds.clear();
22866
+ this.cancelOwnedSpaceHosts();
22767
22867
  this._restoredNodeFeatures = restoredNodeFeatures;
22768
22868
  if (this.auth) {
22769
22869
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost], restoredNodeFeatures);