@tinycloud/node-sdk 2.11.0-beta.1 → 2.11.0-beta.3

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
@@ -17578,6 +17578,15 @@ var NodeUserAuthorization = class {
17578
17578
  get nodeFeatures() {
17579
17579
  return this._nodeFeatures;
17580
17580
  }
17581
+ /**
17582
+ * The node DID reported by `GET /info` for `host`, if the sign-in (or
17583
+ * restore) that populated it targeted that same host. Returns `undefined`
17584
+ * for any other host, and for nodes whose `/info` omits `nodeId`, so callers
17585
+ * fall back to fetching it themselves rather than using a foreign node's DID.
17586
+ */
17587
+ nodeIdForHost(host) {
17588
+ return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
17589
+ }
17581
17590
  get lastActivationSkippedSpaceIds() {
17582
17591
  return [...this._lastActivationSkippedSpaceIds];
17583
17592
  }
@@ -18047,11 +18056,13 @@ var NodeUserAuthorization = class {
18047
18056
  this._tinyCloudSession = tinyCloudSession;
18048
18057
  this._address = address;
18049
18058
  this._chainId = chainId;
18059
+ const infoHost = this.primaryTinyCloudHost;
18050
18060
  const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
18051
- this.primaryTinyCloudHost,
18061
+ infoHost,
18052
18062
  this.wasm.protocolVersion()
18053
18063
  );
18054
18064
  this._nodeFeatures = nodeInfo.features;
18065
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18055
18066
  for (const ext of this.extensions) {
18056
18067
  if (ext.afterSignIn) {
18057
18068
  await ext.afterSignIn(clientSession);
@@ -18217,11 +18228,13 @@ var NodeUserAuthorization = class {
18217
18228
  this._tinyCloudSession = tinyCloudSession;
18218
18229
  this._address = address;
18219
18230
  this._chainId = chainId;
18231
+ const infoHost = this.primaryTinyCloudHost;
18220
18232
  const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
18221
- this.primaryTinyCloudHost,
18233
+ infoHost,
18222
18234
  this.wasm.protocolVersion()
18223
18235
  );
18224
18236
  this._nodeFeatures = nodeInfo.features;
18237
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18225
18238
  for (const ext of this.extensions) {
18226
18239
  if (ext.afterSignIn) {
18227
18240
  await ext.afterSignIn(clientSession);
@@ -19111,6 +19124,12 @@ var _TinyCloudNode = class _TinyCloudNode {
19111
19124
  this._baseSecrets = /* @__PURE__ */ new Map();
19112
19125
  this._secrets = /* @__PURE__ */ new Map();
19113
19126
  this.runtimePermissionGrants = [];
19127
+ /**
19128
+ * Owned space ids this sign-in has already confirmed are hosted. Consulted by
19129
+ * {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
19130
+ * hosting is confirmed against the session that was active at the time.
19131
+ */
19132
+ this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
19114
19133
  /**
19115
19134
  * True when the last signIn() detected an interactive signer and skipped
19116
19135
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -19507,6 +19526,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19507
19526
  this._spaceService = void 0;
19508
19527
  this._serviceContext = void 0;
19509
19528
  this.runtimePermissionGrants = [];
19529
+ this.confirmedHostedSpaceIds.clear();
19510
19530
  await this.tc.signIn(options);
19511
19531
  this.syncResolvedHostFromAuth();
19512
19532
  const oldGraph = this._serviceGraph;
@@ -19689,14 +19709,18 @@ var _TinyCloudNode = class _TinyCloudNode {
19689
19709
  }
19690
19710
  if (step.kind === "seed-applications") {
19691
19711
  const registered = await this.account.applications.register(
19692
- step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
19712
+ step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
19713
+ // Bootstrap only runs on an account with no registry records, so the
19714
+ // manifest-hash pre-read is definitionally a miss. Skip it; the write
19715
+ // it guards is an INSERT OR REPLACE and is safe to repeat.
19716
+ { assumeUnregistered: true }
19693
19717
  );
19694
19718
  if (!registered.ok) {
19695
19719
  throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
19696
19720
  }
19697
19721
  }
19698
19722
  if (step.kind === "encryption-network-create") {
19699
- await this.ensureEncryptionNetwork(step.networkId);
19723
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
19700
19724
  }
19701
19725
  if (step.kind === "secret-records-schema") {
19702
19726
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -19975,6 +19999,22 @@ var _TinyCloudNode = class _TinyCloudNode {
19975
19999
  await this.ensureEncryptionNetwork(networkId);
19976
20000
  }
19977
20001
  }
20002
+ /**
20003
+ * Ensure one of this user's owned spaces is hosted, at most once per space
20004
+ * per sign-in.
20005
+ *
20006
+ * Account bootstrap funnels six calls through here (via
20007
+ * `AccountService.ensureAccountSpaceHosted`), and each one re-submits
20008
+ * `activateSessionWithHost` with the *primary* session. That session's recap
20009
+ * covers only `default` and `secrets`, so the account space appears in
20010
+ * neither `activated` nor `skipped` and the guard below returns immediately
20011
+ * — the repeat calls cannot change the outcome, they only cost round trips.
20012
+ *
20013
+ * The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
20014
+ * `signIn`) and only records confirmed successes. A wrong skip is
20015
+ * self-revealing: the next write to the space fails immediately and loudly
20016
+ * with `404 Space not found`.
20017
+ */
19978
20018
  async ensureOwnedSpaceHostedById(spaceId) {
19979
20019
  if (!this.auth) {
19980
20020
  throw new Error("Owned space hosting requires wallet mode");
@@ -19983,12 +20023,16 @@ var _TinyCloudNode = class _TinyCloudNode {
19983
20023
  if (!session) {
19984
20024
  throw new Error("Owned space hosting requires an active session");
19985
20025
  }
20026
+ if (this.confirmedHostedSpaceIds.has(spaceId)) {
20027
+ return;
20028
+ }
19986
20029
  const host = this.hosts[0] ?? this.config.host;
19987
20030
  if (!host) {
19988
20031
  throw new Error("Owned space hosting requires a TinyCloud host");
19989
20032
  }
19990
20033
  const activation = await (0, import_sdk_core7.activateSessionWithHost)(host, session.delegationHeader);
19991
20034
  if (activation.success && !activation.skipped?.includes(spaceId)) {
20035
+ this.confirmedHostedSpaceIds.add(spaceId);
19992
20036
  return;
19993
20037
  }
19994
20038
  if (!activation.success && activation.status !== 404) {
@@ -20007,6 +20051,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20007
20051
  `Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
20008
20052
  );
20009
20053
  }
20054
+ this.confirmedHostedSpaceIds.add(spaceId);
20010
20055
  }
20011
20056
  /**
20012
20057
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -20329,6 +20374,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20329
20374
  this.tc = stagedGraph.core;
20330
20375
  this._recapOperationsCache = stagedPrimary.cache;
20331
20376
  this.runtimePermissionGrants = stagedPrimary.grants;
20377
+ this.confirmedHostedSpaceIds.clear();
20332
20378
  if (this.auth) {
20333
20379
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
20334
20380
  this._restoredTcSession = void 0;
@@ -20911,7 +20957,17 @@ var _TinyCloudNode = class _TinyCloudNode {
20911
20957
  verifyNodeSignature: (nodeId, message, signature2) => (0, import_sdk_core7.verifyDidKeyEd25519Signature)(nodeId, message, signature2)
20912
20958
  };
20913
20959
  }
20960
+ /**
20961
+ * The node DID for {@link config.host}.
20962
+ *
20963
+ * Sign-in already performs `GET /info` for the protocol-version check, and
20964
+ * that response carries `nodeId`, so reuse it rather than fetching `/info` a
20965
+ * second time. Only a value recorded for this exact host qualifies; older
20966
+ * nodes that omit `nodeId` fall through to the fetch.
20967
+ */
20914
20968
  async fetchNodeId() {
20969
+ const cached = this.auth?.nodeIdForHost(this.config.host);
20970
+ if (cached) return cached;
20915
20971
  const response = await fetch(`${this.config.host}/info`);
20916
20972
  if (!response.ok) {
20917
20973
  throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
@@ -21683,6 +21739,13 @@ var _TinyCloudNode = class _TinyCloudNode {
21683
21739
  body
21684
21740
  )
21685
21741
  });
21742
+ if (response.status === 409) {
21743
+ const existing = await this.getEncryptionNetwork(networkId);
21744
+ if (existing) return existing;
21745
+ throw new Error(
21746
+ `Encryption network ${networkId} already exists but could not be read back`
21747
+ );
21748
+ }
21686
21749
  if (!response.ok) {
21687
21750
  throw new Error(
21688
21751
  `Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
@@ -21691,11 +21754,19 @@ var _TinyCloudNode = class _TinyCloudNode {
21691
21754
  const created = await response.json();
21692
21755
  return created.descriptor;
21693
21756
  }
21694
- async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME) {
21757
+ /**
21758
+ * Ensure an encryption network exists, creating it if necessary.
21759
+ *
21760
+ * @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
21761
+ * @param options - See {@link EnsureEncryptionNetworkOptions}.
21762
+ */
21763
+ async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
21695
21764
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
21696
- const existing = await this.getEncryptionNetwork(networkId);
21697
- if (existing) {
21698
- return existing;
21765
+ if (!options.assumeMissing) {
21766
+ const existing = await this.getEncryptionNetwork(networkId);
21767
+ if (existing) {
21768
+ return existing;
21769
+ }
21699
21770
  }
21700
21771
  const parsed = (0, import_sdk_core7.parseNetworkId)(networkId);
21701
21772
  if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {