@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.
@@ -337,6 +337,11 @@ declare class NodeUserAuthorization implements IUserAuthorization {
337
337
  */
338
338
  private _nodeInfoIdentity?;
339
339
  private _lastActivationSkippedSpaceIds;
340
+ /**
341
+ * Spaces the sign-in activation reported hosted. Only the first POST of a
342
+ * delegation lists them: the node answers a repeat with empty lists.
343
+ */
344
+ private _lastActivationActivatedSpaceIds;
340
345
  constructor(config: NodeUserAuthorizationConfig);
341
346
  /**
342
347
  * Return the manifest currently driving sign-in behavior, or
@@ -414,6 +419,7 @@ declare class NodeUserAuthorization implements IUserAuthorization {
414
419
  */
415
420
  nodeIdForHost(host: string): string | undefined;
416
421
  get lastActivationSkippedSpaceIds(): string[];
422
+ get lastActivationActivatedSpaceIds(): string[];
417
423
  /**
418
424
  * Compute the `abilities` map the WASM `prepareSession` call should
419
425
  * see at sign-in time.
@@ -1479,9 +1485,25 @@ declare class TinyCloudNode {
1479
1485
  * hosting is confirmed against the session that was active at the time.
1480
1486
  */
1481
1487
  private readonly confirmedHostedSpaceIds;
1488
+ /**
1489
+ * Owned space ids known to be hosted for the current session: the sign-in
1490
+ * activation listed them as `activated`, or this session hosted them. Unlike
1491
+ * {@link confirmedHostedSpaceIds}, nothing here is inferred from a space being
1492
+ * merely absent from `skipped`. Read by {@link ensureOwnedSpaceHosted}.
1493
+ */
1494
+ private readonly hostedOwnedSpaceIds;
1495
+ /**
1496
+ * In-flight host delegations, so concurrent callers share one approval per
1497
+ * space. `waiters` counts callers still waiting; the request is cancelled
1498
+ * once every caller with a signal has aborted and none waits without one.
1499
+ */
1500
+ private readonly ownedSpaceHosts;
1482
1501
  /** Serializes account-registry writes for the active node instance. */
1483
1502
  private accountRegistryTail;
1484
- /** Bounds best-effort registry work on the sign-in critical path. */
1503
+ /**
1504
+ * Hang guard for the background registry sync. It is off the sign-in path,
1505
+ * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
1506
+ */
1485
1507
  private accountRegistryDeadlineMs;
1486
1508
  private pendingAccountRegistrySync?;
1487
1509
  /**
@@ -1691,6 +1713,15 @@ declare class TinyCloudNode {
1691
1713
  * @param options - Optional per-call SIWE overrides for this sign-in only
1692
1714
  */
1693
1715
  signIn(options?: SignInOptions): Promise<void>;
1716
+ /**
1717
+ * Wait for the account-registry sync that {@link signIn} starts in the
1718
+ * background (account index, this app's manifest record, space list).
1719
+ * Resolves once it has finished, failed or hit its hang guard; it never
1720
+ * rejects, and failures are logged as warnings. `signIn()` does not wait for
1721
+ * it, so call this only when you need the registry written, or before a first
1722
+ * write against a SQLite-backed node older than 1.20.2 (TC-899).
1723
+ */
1724
+ waitForAccountRegistrySync(): Promise<void>;
1694
1725
  private ownedSpaceId;
1695
1726
  private bootstrapAccountIfNeeded;
1696
1727
  private resolveBootstrapDecision;
@@ -1811,23 +1842,35 @@ declare class TinyCloudNode {
1811
1842
  * registered on the node.
1812
1843
  *
1813
1844
  * Calling this resolves `name` to the owner's owned-space URI
1814
- * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It first consults the
1815
- * account-space spaces registry (`account/spaces/{space_id}`, the canonical
1816
- * KV source of truth, fronted by a best-effort SQLite index): if the space is
1817
- * already registered/hosted it returns the URI WITHOUT submitting a host
1818
- * delegation, avoiding a redundant host-SIWE signature prompt for owners who
1819
- * already use the space. Only when the space is absent — or the registry
1820
- * check fails for any reason (e.g. a cold SQLite index reporting
1821
- * `no such table: spaces`) — does it fall through to {@link hostOwnedSpace}.
1822
- *
1823
- * The registry check is purely an optimization: any failure falls back to
1824
- * hosting, and the host SIWE is idempotent server-side, so re-hosting an
1825
- * existing space remains a safe no-op. Must be called after {@link signIn}.
1845
+ * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It returns WITHOUT
1846
+ * submitting a host delegation when the space is known to be hosted: the
1847
+ * sign-in activation listed it as `activated` (any space the session's recap
1848
+ * covers), this session already hosted it, or the account-space spaces
1849
+ * registry (`account/spaces/{space_id}`, the canonical KV source of truth,
1850
+ * fronted by a best-effort SQLite index) has its record. Otherwise it falls
1851
+ * through to {@link hostOwnedSpace}, which asks for a signature; a host
1852
+ * already in flight for the same space (e.g. from the background registry
1853
+ * sync) is shared rather than asked for twice.
1854
+ *
1855
+ * Hosting is still needed when the space really isn't hosted, or when the
1856
+ * session's recap doesn't cover it and the registry has no record; the host
1857
+ * SIWE is idempotent server-side. Must be called after {@link signIn}.
1826
1858
  *
1827
1859
  * @param name - The owned space name (e.g. `"secrets"`).
1828
1860
  * @returns The hosted owned-space URI.
1829
1861
  */
1830
1862
  ensureOwnedSpaceHosted(name: string): Promise<string>;
1863
+ /**
1864
+ * Submit the host delegation for an owned space, sharing one in-flight
1865
+ * request (and so one approval) between concurrent callers in this session,
1866
+ * such as the background registry sync and {@link ensureOwnedSpaceHosted}.
1867
+ * Each caller's `signal` releases only its own wait; the request itself is
1868
+ * cancelled once no caller is left waiting, or when the session is replaced
1869
+ * ({@link cancelOwnedSpaceHosts}), so a late approval is discarded.
1870
+ */
1871
+ private hostOwnedSpaceOnce;
1872
+ /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
1873
+ private cancelOwnedSpaceHosts;
1831
1874
  /** Resolve and authenticate the owner encoded by an owned-space URI. */
1832
1875
  credentialSpaceOwnerDid(spaceId: string): string;
1833
1876
  /**
@@ -337,6 +337,11 @@ declare class NodeUserAuthorization implements IUserAuthorization {
337
337
  */
338
338
  private _nodeInfoIdentity?;
339
339
  private _lastActivationSkippedSpaceIds;
340
+ /**
341
+ * Spaces the sign-in activation reported hosted. Only the first POST of a
342
+ * delegation lists them: the node answers a repeat with empty lists.
343
+ */
344
+ private _lastActivationActivatedSpaceIds;
340
345
  constructor(config: NodeUserAuthorizationConfig);
341
346
  /**
342
347
  * Return the manifest currently driving sign-in behavior, or
@@ -414,6 +419,7 @@ declare class NodeUserAuthorization implements IUserAuthorization {
414
419
  */
415
420
  nodeIdForHost(host: string): string | undefined;
416
421
  get lastActivationSkippedSpaceIds(): string[];
422
+ get lastActivationActivatedSpaceIds(): string[];
417
423
  /**
418
424
  * Compute the `abilities` map the WASM `prepareSession` call should
419
425
  * see at sign-in time.
@@ -1479,9 +1485,25 @@ declare class TinyCloudNode {
1479
1485
  * hosting is confirmed against the session that was active at the time.
1480
1486
  */
1481
1487
  private readonly confirmedHostedSpaceIds;
1488
+ /**
1489
+ * Owned space ids known to be hosted for the current session: the sign-in
1490
+ * activation listed them as `activated`, or this session hosted them. Unlike
1491
+ * {@link confirmedHostedSpaceIds}, nothing here is inferred from a space being
1492
+ * merely absent from `skipped`. Read by {@link ensureOwnedSpaceHosted}.
1493
+ */
1494
+ private readonly hostedOwnedSpaceIds;
1495
+ /**
1496
+ * In-flight host delegations, so concurrent callers share one approval per
1497
+ * space. `waiters` counts callers still waiting; the request is cancelled
1498
+ * once every caller with a signal has aborted and none waits without one.
1499
+ */
1500
+ private readonly ownedSpaceHosts;
1482
1501
  /** Serializes account-registry writes for the active node instance. */
1483
1502
  private accountRegistryTail;
1484
- /** Bounds best-effort registry work on the sign-in critical path. */
1503
+ /**
1504
+ * Hang guard for the background registry sync. It is off the sign-in path,
1505
+ * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
1506
+ */
1485
1507
  private accountRegistryDeadlineMs;
1486
1508
  private pendingAccountRegistrySync?;
1487
1509
  /**
@@ -1691,6 +1713,15 @@ declare class TinyCloudNode {
1691
1713
  * @param options - Optional per-call SIWE overrides for this sign-in only
1692
1714
  */
1693
1715
  signIn(options?: SignInOptions): Promise<void>;
1716
+ /**
1717
+ * Wait for the account-registry sync that {@link signIn} starts in the
1718
+ * background (account index, this app's manifest record, space list).
1719
+ * Resolves once it has finished, failed or hit its hang guard; it never
1720
+ * rejects, and failures are logged as warnings. `signIn()` does not wait for
1721
+ * it, so call this only when you need the registry written, or before a first
1722
+ * write against a SQLite-backed node older than 1.20.2 (TC-899).
1723
+ */
1724
+ waitForAccountRegistrySync(): Promise<void>;
1694
1725
  private ownedSpaceId;
1695
1726
  private bootstrapAccountIfNeeded;
1696
1727
  private resolveBootstrapDecision;
@@ -1811,23 +1842,35 @@ declare class TinyCloudNode {
1811
1842
  * registered on the node.
1812
1843
  *
1813
1844
  * Calling this resolves `name` to the owner's owned-space URI
1814
- * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It first consults the
1815
- * account-space spaces registry (`account/spaces/{space_id}`, the canonical
1816
- * KV source of truth, fronted by a best-effort SQLite index): if the space is
1817
- * already registered/hosted it returns the URI WITHOUT submitting a host
1818
- * delegation, avoiding a redundant host-SIWE signature prompt for owners who
1819
- * already use the space. Only when the space is absent — or the registry
1820
- * check fails for any reason (e.g. a cold SQLite index reporting
1821
- * `no such table: spaces`) — does it fall through to {@link hostOwnedSpace}.
1822
- *
1823
- * The registry check is purely an optimization: any failure falls back to
1824
- * hosting, and the host SIWE is idempotent server-side, so re-hosting an
1825
- * existing space remains a safe no-op. Must be called after {@link signIn}.
1845
+ * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It returns WITHOUT
1846
+ * submitting a host delegation when the space is known to be hosted: the
1847
+ * sign-in activation listed it as `activated` (any space the session's recap
1848
+ * covers), this session already hosted it, or the account-space spaces
1849
+ * registry (`account/spaces/{space_id}`, the canonical KV source of truth,
1850
+ * fronted by a best-effort SQLite index) has its record. Otherwise it falls
1851
+ * through to {@link hostOwnedSpace}, which asks for a signature; a host
1852
+ * already in flight for the same space (e.g. from the background registry
1853
+ * sync) is shared rather than asked for twice.
1854
+ *
1855
+ * Hosting is still needed when the space really isn't hosted, or when the
1856
+ * session's recap doesn't cover it and the registry has no record; the host
1857
+ * SIWE is idempotent server-side. Must be called after {@link signIn}.
1826
1858
  *
1827
1859
  * @param name - The owned space name (e.g. `"secrets"`).
1828
1860
  * @returns The hosted owned-space URI.
1829
1861
  */
1830
1862
  ensureOwnedSpaceHosted(name: string): Promise<string>;
1863
+ /**
1864
+ * Submit the host delegation for an owned space, sharing one in-flight
1865
+ * request (and so one approval) between concurrent callers in this session,
1866
+ * such as the background registry sync and {@link ensureOwnedSpaceHosted}.
1867
+ * Each caller's `signal` releases only its own wait; the request itself is
1868
+ * cancelled once no caller is left waiting, or when the session is replaced
1869
+ * ({@link cancelOwnedSpaceHosts}), so a late approval is discarded.
1870
+ */
1871
+ private hostOwnedSpaceOnce;
1872
+ /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
1873
+ private cancelOwnedSpaceHosts;
1831
1874
  /** Resolve and authenticate the owner encoded by an owned-space URI. */
1832
1875
  credentialSpaceOwnerDid(spaceId: string): string;
1833
1876
  /**
package/dist/core.cjs CHANGED
@@ -462,6 +462,11 @@ var NodeUserAuthorization = class {
462
462
  this._nodeFeatures = [];
463
463
  this._nodeFeaturesKnown = false;
464
464
  this._lastActivationSkippedSpaceIds = [];
465
+ /**
466
+ * Spaces the sign-in activation reported hosted. Only the first POST of a
467
+ * delegation lists them: the node answers a repeat with empty lists.
468
+ */
469
+ this._lastActivationActivatedSpaceIds = [];
465
470
  this.wasm = config.wasmBindings;
466
471
  this.signer = config.signer;
467
472
  this.signStrategy = config.signStrategy ?? defaultSignStrategy;
@@ -591,6 +596,7 @@ var NodeUserAuthorization = class {
591
596
  this._nodeInfoIdentity = void 0;
592
597
  this.tinycloudHosts = hosts && hosts.length > 0 ? [...hosts] : void 0;
593
598
  this._lastActivationSkippedSpaceIds = [];
599
+ this._lastActivationActivatedSpaceIds = [];
594
600
  if (!session) {
595
601
  this._session = void 0;
596
602
  this._tinyCloudSession = void 0;
@@ -685,6 +691,9 @@ var NodeUserAuthorization = class {
685
691
  get lastActivationSkippedSpaceIds() {
686
692
  return [...this._lastActivationSkippedSpaceIds];
687
693
  }
694
+ get lastActivationActivatedSpaceIds() {
695
+ return [...this._lastActivationActivatedSpaceIds];
696
+ }
688
697
  /**
689
698
  * Compute the `abilities` map the WASM `prepareSession` call should
690
699
  * see at sign-in time.
@@ -1180,8 +1189,10 @@ var NodeUserAuthorization = class {
1180
1189
  recordActivationSkippedSpaces(result, primarySpaceId) {
1181
1190
  if (result.success) {
1182
1191
  this._lastActivationSkippedSpaceIds = result.skipped ?? [];
1192
+ this._lastActivationActivatedSpaceIds = result.activated ?? [];
1183
1193
  return;
1184
1194
  }
1195
+ this._lastActivationActivatedSpaceIds = [];
1185
1196
  this._lastActivationSkippedSpaceIds = result.status === 404 ? [primarySpaceId] : [];
1186
1197
  }
1187
1198
  async createBootstrapSession(options) {
@@ -3433,10 +3444,26 @@ var _TinyCloudNode = class _TinyCloudNode {
3433
3444
  * hosting is confirmed against the session that was active at the time.
3434
3445
  */
3435
3446
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
3447
+ /**
3448
+ * Owned space ids known to be hosted for the current session: the sign-in
3449
+ * activation listed them as `activated`, or this session hosted them. Unlike
3450
+ * {@link confirmedHostedSpaceIds}, nothing here is inferred from a space being
3451
+ * merely absent from `skipped`. Read by {@link ensureOwnedSpaceHosted}.
3452
+ */
3453
+ this.hostedOwnedSpaceIds = /* @__PURE__ */ new Set();
3454
+ /**
3455
+ * In-flight host delegations, so concurrent callers share one approval per
3456
+ * space. `waiters` counts callers still waiting; the request is cancelled
3457
+ * once every caller with a signal has aborted and none waits without one.
3458
+ */
3459
+ this.ownedSpaceHosts = /* @__PURE__ */ new Map();
3436
3460
  /** Serializes account-registry writes for the active node instance. */
3437
3461
  this.accountRegistryTail = Promise.resolve();
3438
- /** Bounds best-effort registry work on the sign-in critical path. */
3439
- this.accountRegistryDeadlineMs = 4e3;
3462
+ /**
3463
+ * Hang guard for the background registry sync. It is off the sign-in path,
3464
+ * so it must outlast a slow but healthy sync (9–18 s on prod 1.20.2, TC-998).
3465
+ */
3466
+ this.accountRegistryDeadlineMs = 3e4;
3440
3467
  /**
3441
3468
  * True when the last signIn() detected an interactive signer and skipped
3442
3469
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -4031,6 +4058,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4031
4058
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
4032
4059
  );
4033
4060
  }
4061
+ this.pendingAccountRegistrySync?.controller.abort();
4034
4062
  await this.accountRegistryTail;
4035
4063
  await this.wasmBindings.ensureInitialized?.();
4036
4064
  const address = (0, import_sdk_core10.canonicalizeAddress)(await this.signer.getAddress());
@@ -4051,8 +4079,13 @@ var _TinyCloudNode = class _TinyCloudNode {
4051
4079
  this._serviceContext = void 0;
4052
4080
  this.runtimePermissionGrants = [];
4053
4081
  this.confirmedHostedSpaceIds.clear();
4082
+ this.hostedOwnedSpaceIds.clear();
4054
4083
  await this.tc.signIn(options);
4084
+ this.cancelOwnedSpaceHosts();
4055
4085
  this.syncResolvedHostFromAuth();
4086
+ for (const spaceId of this.auth.lastActivationActivatedSpaceIds ?? []) {
4087
+ this.hostedOwnedSpaceIds.add(spaceId);
4088
+ }
4056
4089
  this.replicationDelegateSession = void 0;
4057
4090
  this.replicationScopeError = void 0;
4058
4091
  if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
@@ -4077,10 +4110,20 @@ var _TinyCloudNode = class _TinyCloudNode {
4077
4110
  }
4078
4111
  if (!bootstrapped) {
4079
4112
  this.scheduleAccountRegistrySync();
4080
- await this.accountRegistryTail;
4081
4113
  }
4082
4114
  this.notificationHandler.success("Successfully signed in");
4083
4115
  }
4116
+ /**
4117
+ * Wait for the account-registry sync that {@link signIn} starts in the
4118
+ * background (account index, this app's manifest record, space list).
4119
+ * Resolves once it has finished, failed or hit its hang guard; it never
4120
+ * rejects, and failures are logged as warnings. `signIn()` does not wait for
4121
+ * it, so call this only when you need the registry written, or before a first
4122
+ * write against a SQLite-backed node older than 1.20.2 (TC-899).
4123
+ */
4124
+ async waitForAccountRegistrySync() {
4125
+ await this.accountRegistryTail;
4126
+ }
4084
4127
  ownedSpaceId(name) {
4085
4128
  if (!this._address) {
4086
4129
  throw new Error("Cannot resolve owned space before sign-in");
@@ -4686,17 +4729,17 @@ var _TinyCloudNode = class _TinyCloudNode {
4686
4729
  clearTimeout(deadline);
4687
4730
  }
4688
4731
  });
4689
- this.pendingAccountRegistrySync = { session, promise };
4690
- const clearPendingSync = () => {
4732
+ this.pendingAccountRegistrySync = { session, promise, controller };
4733
+ const settle = () => {
4734
+ clearTimeout(deadline);
4691
4735
  if (this.pendingAccountRegistrySync?.promise === promise) {
4692
4736
  this.pendingAccountRegistrySync = void 0;
4693
4737
  }
4694
4738
  };
4695
- void promise.then(clearPendingSync, clearPendingSync);
4696
- void promise.then(
4697
- () => clearTimeout(deadline),
4698
- () => clearTimeout(deadline)
4699
- );
4739
+ void promise.then(settle, (error) => {
4740
+ settle();
4741
+ console.warn("TinyCloud account registry sync failed", error);
4742
+ });
4700
4743
  }
4701
4744
  enqueueAccountRegistryOperation(session, task) {
4702
4745
  const operation = this.accountRegistryTail.then(async () => {
@@ -4734,10 +4777,15 @@ var _TinyCloudNode = class _TinyCloudNode {
4734
4777
  }
4735
4778
  async withAccountRegistryRetry(task, signal) {
4736
4779
  const delays = [250, 1e3, 3e3];
4780
+ const stopped = (detail) => {
4781
+ if (signal.reason instanceof DOMException && signal.reason.name === "TimeoutError") {
4782
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", detail);
4783
+ }
4784
+ };
4737
4785
  let lastError;
4738
4786
  for (let attempt = 0; attempt < delays.length; attempt += 1) {
4739
4787
  if (signal.aborted) {
4740
- console.warn("TinyCloud account registry sync stopped: deadline exceeded", signal.reason);
4788
+ stopped(signal.reason);
4741
4789
  return;
4742
4790
  }
4743
4791
  try {
@@ -4745,7 +4793,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4745
4793
  return;
4746
4794
  } catch (error) {
4747
4795
  if (signal.aborted) {
4748
- console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
4796
+ stopped(error);
4749
4797
  return;
4750
4798
  }
4751
4799
  const verdict = (0, import_sdk_core10.authorizationVerdictOf)(error);
@@ -4838,7 +4886,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4838
4886
  if (!session) {
4839
4887
  throw new Error("Owned space hosting requires an active session");
4840
4888
  }
4841
- if (this.confirmedHostedSpaceIds.has(spaceId)) {
4889
+ if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
4842
4890
  return;
4843
4891
  }
4844
4892
  const host = this.hosts[0] ?? this.config.host;
@@ -4856,8 +4904,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4856
4904
  { cause: activation }
4857
4905
  );
4858
4906
  }
4859
- const authorization = this.auth;
4860
- const created = signal ? await authorization.hostOwnedSpaceResult(spaceId, void 0, signal) : await authorization.hostOwnedSpaceResult(spaceId);
4907
+ const created = await this.hostOwnedSpaceOnce(spaceId, signal);
4861
4908
  if (!created.success) {
4862
4909
  throw Object.assign(
4863
4910
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
@@ -4874,7 +4921,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4874
4921
  { cause: retry }
4875
4922
  );
4876
4923
  }
4877
- this.confirmedHostedSpaceIds.add(spaceId);
4924
+ if (this.auth.tinyCloudSession === session) {
4925
+ this.confirmedHostedSpaceIds.add(spaceId);
4926
+ this.hostedOwnedSpaceIds.add(spaceId);
4927
+ }
4878
4928
  }
4879
4929
  /**
4880
4930
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -4906,7 +4956,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4906
4956
  if (!host) {
4907
4957
  throw new Error("Owned space hosting requires a TinyCloud host");
4908
4958
  }
4909
- const hosted = await this.auth.hostOwnedSpaceResult(spaceId);
4959
+ const hosted = await this.hostOwnedSpaceOnce(spaceId);
4910
4960
  if (!hosted.success) {
4911
4961
  throw Object.assign(
4912
4962
  new Error(`Failed to host owned space ${spaceId}: ${describeHostFailure2(hosted)}`),
@@ -4925,6 +4975,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4925
4975
  { cause: activation }
4926
4976
  );
4927
4977
  }
4978
+ if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
4928
4979
  try {
4929
4980
  await this.enqueueAccountRegistryOperation(session, async () => {
4930
4981
  await this.account.spaces.register({
@@ -4952,18 +5003,19 @@ var _TinyCloudNode = class _TinyCloudNode {
4952
5003
  * registered on the node.
4953
5004
  *
4954
5005
  * Calling this resolves `name` to the owner's owned-space URI
4955
- * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It first consults the
4956
- * account-space spaces registry (`account/spaces/{space_id}`, the canonical
4957
- * KV source of truth, fronted by a best-effort SQLite index): if the space is
4958
- * already registered/hosted it returns the URI WITHOUT submitting a host
4959
- * delegation, avoiding a redundant host-SIWE signature prompt for owners who
4960
- * already use the space. Only when the space is absent — or the registry
4961
- * check fails for any reason (e.g. a cold SQLite index reporting
4962
- * `no such table: spaces`) — does it fall through to {@link hostOwnedSpace}.
4963
- *
4964
- * The registry check is purely an optimization: any failure falls back to
4965
- * hosting, and the host SIWE is idempotent server-side, so re-hosting an
4966
- * existing space remains a safe no-op. Must be called after {@link signIn}.
5006
+ * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It returns WITHOUT
5007
+ * submitting a host delegation when the space is known to be hosted: the
5008
+ * sign-in activation listed it as `activated` (any space the session's recap
5009
+ * covers), this session already hosted it, or the account-space spaces
5010
+ * registry (`account/spaces/{space_id}`, the canonical KV source of truth,
5011
+ * fronted by a best-effort SQLite index) has its record. Otherwise it falls
5012
+ * through to {@link hostOwnedSpace}, which asks for a signature; a host
5013
+ * already in flight for the same space (e.g. from the background registry
5014
+ * sync) is shared rather than asked for twice.
5015
+ *
5016
+ * Hosting is still needed when the space really isn't hosted, or when the
5017
+ * session's recap doesn't cover it and the registry has no record; the host
5018
+ * SIWE is idempotent server-side. Must be called after {@link signIn}.
4967
5019
  *
4968
5020
  * @param name - The owned space name (e.g. `"secrets"`).
4969
5021
  * @returns The hosted owned-space URI.
@@ -4972,13 +5024,57 @@ var _TinyCloudNode = class _TinyCloudNode {
4972
5024
  if (!this.auth || !this.auth.tinyCloudSession) {
4973
5025
  throw new Error("Not signed in. Call signIn() first.");
4974
5026
  }
4975
- await this.accountRegistryTail;
4976
5027
  const spaceId = this.ownedSpaceId(name);
4977
- if (await this.isOwnedSpaceRegistered(spaceId)) {
5028
+ if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
4978
5029
  return spaceId;
4979
5030
  }
4980
5031
  return this.hostOwnedSpace(name);
4981
5032
  }
5033
+ /**
5034
+ * Submit the host delegation for an owned space, sharing one in-flight
5035
+ * request (and so one approval) between concurrent callers in this session,
5036
+ * such as the background registry sync and {@link ensureOwnedSpaceHosted}.
5037
+ * Each caller's `signal` releases only its own wait; the request itself is
5038
+ * cancelled once no caller is left waiting, or when the session is replaced
5039
+ * ({@link cancelOwnedSpaceHosts}), so a late approval is discarded.
5040
+ */
5041
+ hostOwnedSpaceOnce(spaceId, signal) {
5042
+ if (signal?.aborted) return Promise.reject(signal.reason);
5043
+ let entry = this.ownedSpaceHosts.get(spaceId);
5044
+ if (!entry) {
5045
+ const controller = new AbortController();
5046
+ const promise = this.auth.hostOwnedSpaceResult(spaceId, void 0, controller.signal);
5047
+ const created = { promise, controller, waiters: 0 };
5048
+ this.ownedSpaceHosts.set(spaceId, created);
5049
+ const forget = () => {
5050
+ if (this.ownedSpaceHosts.get(spaceId) === created) this.ownedSpaceHosts.delete(spaceId);
5051
+ };
5052
+ promise.then(forget, forget);
5053
+ entry = created;
5054
+ }
5055
+ if (!signal) {
5056
+ entry.waiters = Number.POSITIVE_INFINITY;
5057
+ return entry.promise;
5058
+ }
5059
+ entry.waiters += 1;
5060
+ const shared = entry;
5061
+ return new Promise((resolve, reject2) => {
5062
+ const onAbort = () => {
5063
+ reject2(signal.reason);
5064
+ shared.waiters -= 1;
5065
+ if (shared.waiters !== 0) return;
5066
+ shared.controller.abort(signal.reason);
5067
+ if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
5068
+ };
5069
+ signal.addEventListener("abort", onAbort, { once: true });
5070
+ shared.promise.then(resolve, reject2).finally(() => signal.removeEventListener("abort", onAbort));
5071
+ });
5072
+ }
5073
+ /** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
5074
+ cancelOwnedSpaceHosts() {
5075
+ for (const { controller } of this.ownedSpaceHosts.values()) controller.abort();
5076
+ this.ownedSpaceHosts.clear();
5077
+ }
4982
5078
  /** Resolve and authenticate the owner encoded by an owned-space URI. */
4983
5079
  credentialSpaceOwnerDid(spaceId) {
4984
5080
  const ownerDid = this.ownerDidFromSpaceId(spaceId);
@@ -5221,6 +5317,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5221
5317
  sessionExpiry: stagedSessionExpiry,
5222
5318
  recap: stagedRecap
5223
5319
  });
5320
+ this.pendingAccountRegistrySync?.controller.abort();
5321
+ await this.accountRegistryTail;
5224
5322
  const oldGraph = this._serviceGraph;
5225
5323
  const oldCore = this.tc;
5226
5324
  this.sessionManager = stagedManager;
@@ -5254,6 +5352,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5254
5352
  this._recapOperationsCache = stagedPrimary.cache;
5255
5353
  this.runtimePermissionGrants = stagedPrimary.grants;
5256
5354
  this.confirmedHostedSpaceIds.clear();
5355
+ this.hostedOwnedSpaceIds.clear();
5356
+ this.cancelOwnedSpaceHosts();
5257
5357
  this._restoredNodeFeatures = restoredNodeFeatures;
5258
5358
  if (this.auth) {
5259
5359
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost], restoredNodeFeatures);