@tinycloud/node-sdk 3.2.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");
@@ -21730,6 +21773,13 @@ var _TinyCloudNode = class _TinyCloudNode {
21730
21773
  * A marker written before a canonical space was added triggers one bounded repair.
21731
21774
  */
21732
21775
  isAcceptedBootstrapCompletionMarker(value, canonicalStepIds) {
21776
+ if (typeof value === "string") {
21777
+ try {
21778
+ value = JSON.parse(value);
21779
+ } catch {
21780
+ return false;
21781
+ }
21782
+ }
21733
21783
  if (typeof value !== "object" || value === null) return false;
21734
21784
  const entries = Object.entries(value);
21735
21785
  const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
@@ -22189,17 +22239,17 @@ var _TinyCloudNode = class _TinyCloudNode {
22189
22239
  clearTimeout(deadline);
22190
22240
  }
22191
22241
  });
22192
- this.pendingAccountRegistrySync = { session, promise };
22193
- const clearPendingSync = () => {
22242
+ this.pendingAccountRegistrySync = { session, promise, controller };
22243
+ const settle = () => {
22244
+ clearTimeout(deadline);
22194
22245
  if (this.pendingAccountRegistrySync?.promise === promise) {
22195
22246
  this.pendingAccountRegistrySync = void 0;
22196
22247
  }
22197
22248
  };
22198
- void promise.then(clearPendingSync, clearPendingSync);
22199
- void promise.then(
22200
- () => clearTimeout(deadline),
22201
- () => clearTimeout(deadline)
22202
- );
22249
+ void promise.then(settle, (error) => {
22250
+ settle();
22251
+ console.warn("TinyCloud account registry sync failed", error);
22252
+ });
22203
22253
  }
22204
22254
  enqueueAccountRegistryOperation(session, task) {
22205
22255
  const operation = this.accountRegistryTail.then(async () => {
@@ -22237,10 +22287,15 @@ var _TinyCloudNode = class _TinyCloudNode {
22237
22287
  }
22238
22288
  async withAccountRegistryRetry(task, signal) {
22239
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
+ };
22240
22295
  let lastError;
22241
22296
  for (let attempt = 0; attempt < delays.length; attempt += 1) {
22242
22297
  if (signal.aborted) {
22243
- console.warn("TinyCloud account registry sync stopped: deadline exceeded", signal.reason);
22298
+ stopped(signal.reason);
22244
22299
  return;
22245
22300
  }
22246
22301
  try {
@@ -22248,7 +22303,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22248
22303
  return;
22249
22304
  } catch (error) {
22250
22305
  if (signal.aborted) {
22251
- console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
22306
+ stopped(error);
22252
22307
  return;
22253
22308
  }
22254
22309
  const verdict = (0, import_sdk_core9.authorizationVerdictOf)(error);
@@ -22341,7 +22396,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22341
22396
  if (!session) {
22342
22397
  throw new Error("Owned space hosting requires an active session");
22343
22398
  }
22344
- if (this.confirmedHostedSpaceIds.has(spaceId)) {
22399
+ if (this.confirmedHostedSpaceIds.has(spaceId) || this.hostedOwnedSpaceIds.has(spaceId)) {
22345
22400
  return;
22346
22401
  }
22347
22402
  const host = this.hosts[0] ?? this.config.host;
@@ -22359,8 +22414,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22359
22414
  { cause: activation }
22360
22415
  );
22361
22416
  }
22362
- const authorization = this.auth;
22363
- const created = signal ? await authorization.hostOwnedSpaceResult(spaceId, void 0, signal) : await authorization.hostOwnedSpaceResult(spaceId);
22417
+ const created = await this.hostOwnedSpaceOnce(spaceId, signal);
22364
22418
  if (!created.success) {
22365
22419
  throw Object.assign(
22366
22420
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
@@ -22377,7 +22431,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22377
22431
  { cause: retry }
22378
22432
  );
22379
22433
  }
22380
- this.confirmedHostedSpaceIds.add(spaceId);
22434
+ if (this.auth.tinyCloudSession === session) {
22435
+ this.confirmedHostedSpaceIds.add(spaceId);
22436
+ this.hostedOwnedSpaceIds.add(spaceId);
22437
+ }
22381
22438
  }
22382
22439
  /**
22383
22440
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -22409,7 +22466,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22409
22466
  if (!host) {
22410
22467
  throw new Error("Owned space hosting requires a TinyCloud host");
22411
22468
  }
22412
- const hosted = await this.auth.hostOwnedSpaceResult(spaceId);
22469
+ const hosted = await this.hostOwnedSpaceOnce(spaceId);
22413
22470
  if (!hosted.success) {
22414
22471
  throw Object.assign(
22415
22472
  new Error(`Failed to host owned space ${spaceId}: ${describeHostFailure2(hosted)}`),
@@ -22428,6 +22485,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22428
22485
  { cause: activation }
22429
22486
  );
22430
22487
  }
22488
+ if (this.auth?.tinyCloudSession === session) this.hostedOwnedSpaceIds.add(spaceId);
22431
22489
  try {
22432
22490
  await this.enqueueAccountRegistryOperation(session, async () => {
22433
22491
  await this.account.spaces.register({
@@ -22455,18 +22513,19 @@ var _TinyCloudNode = class _TinyCloudNode {
22455
22513
  * registered on the node.
22456
22514
  *
22457
22515
  * Calling this resolves `name` to the owner's owned-space URI
22458
- * (`tinycloud:pkh:eip155:<chain>:<addr>:<name>`). It first consults the
22459
- * account-space spaces registry (`account/spaces/{space_id}`, the canonical
22460
- * KV source of truth, fronted by a best-effort SQLite index): if the space is
22461
- * already registered/hosted it returns the URI WITHOUT submitting a host
22462
- * delegation, avoiding a redundant host-SIWE signature prompt for owners who
22463
- * already use the space. Only when the space is absent — or the registry
22464
- * check fails for any reason (e.g. a cold SQLite index reporting
22465
- * `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.
22466
22525
  *
22467
- * The registry check is purely an optimization: any failure falls back to
22468
- * hosting, and the host SIWE is idempotent server-side, so re-hosting an
22469
- * 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}.
22470
22529
  *
22471
22530
  * @param name - The owned space name (e.g. `"secrets"`).
22472
22531
  * @returns The hosted owned-space URI.
@@ -22475,13 +22534,57 @@ var _TinyCloudNode = class _TinyCloudNode {
22475
22534
  if (!this.auth || !this.auth.tinyCloudSession) {
22476
22535
  throw new Error("Not signed in. Call signIn() first.");
22477
22536
  }
22478
- await this.accountRegistryTail;
22479
22537
  const spaceId = this.ownedSpaceId(name);
22480
- if (await this.isOwnedSpaceRegistered(spaceId)) {
22538
+ if (this.hostedOwnedSpaceIds.has(spaceId) || await this.isOwnedSpaceRegistered(spaceId)) {
22481
22539
  return spaceId;
22482
22540
  }
22483
22541
  return this.hostOwnedSpace(name);
22484
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
+ }
22485
22588
  /** Resolve and authenticate the owner encoded by an owned-space URI. */
22486
22589
  credentialSpaceOwnerDid(spaceId) {
22487
22590
  const ownerDid = this.ownerDidFromSpaceId(spaceId);
@@ -22724,6 +22827,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22724
22827
  sessionExpiry: stagedSessionExpiry,
22725
22828
  recap: stagedRecap
22726
22829
  });
22830
+ this.pendingAccountRegistrySync?.controller.abort();
22831
+ await this.accountRegistryTail;
22727
22832
  const oldGraph = this._serviceGraph;
22728
22833
  const oldCore = this.tc;
22729
22834
  this.sessionManager = stagedManager;
@@ -22757,6 +22862,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22757
22862
  this._recapOperationsCache = stagedPrimary.cache;
22758
22863
  this.runtimePermissionGrants = stagedPrimary.grants;
22759
22864
  this.confirmedHostedSpaceIds.clear();
22865
+ this.hostedOwnedSpaceIds.clear();
22866
+ this.cancelOwnedSpaceHosts();
22760
22867
  this._restoredNodeFeatures = restoredNodeFeatures;
22761
22868
  if (this.auth) {
22762
22869
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost], restoredNodeFeatures);