@tinycloud/node-sdk 2.11.0-beta.0 → 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);
@@ -18913,19 +18926,87 @@ function persistedExpiry(value) {
18913
18926
  function sameInstant(left, right) {
18914
18927
  return left.getTime() === right.getTime();
18915
18928
  }
18916
- function sharingActionsToAbilities(path, actions) {
18917
- var _a;
18929
+ function ownerDelegationPermissions(params) {
18930
+ const hasPermissions = Object.prototype.hasOwnProperty.call(params, "permissions");
18931
+ const hasLegacy = Object.prototype.hasOwnProperty.call(params, "path") || Object.prototype.hasOwnProperty.call(params, "actions");
18932
+ if (hasPermissions === hasLegacy) {
18933
+ throw new Error("Owner delegation requires exactly one authority shape: path/actions or permissions");
18934
+ }
18935
+ const permissions = [];
18936
+ if (hasPermissions) {
18937
+ if (!Array.isArray(params.permissions)) {
18938
+ throw new Error("Owner delegation permissions are invalid");
18939
+ }
18940
+ permissions.push(...params.permissions.map((permission) => ({
18941
+ service: permission.service,
18942
+ path: permission.path,
18943
+ actions: [...permission.actions]
18944
+ })));
18945
+ } else {
18946
+ if (typeof params.path !== "string" || !Array.isArray(params.actions)) {
18947
+ throw new Error("Owner delegation requires bounded capabilities");
18948
+ }
18949
+ const byService = /* @__PURE__ */ new Map();
18950
+ for (const action of params.actions) {
18951
+ if (typeof action !== "string") {
18952
+ throw new Error("Owner delegation capabilities are unsupported");
18953
+ }
18954
+ const slash = action.indexOf("/");
18955
+ const service = slash === -1 ? "" : action.slice(0, slash);
18956
+ if (import_sdk_core7.SERVICE_LONG_TO_SHORT[service] === void 0) {
18957
+ throw new Error("Owner delegation capabilities are unsupported");
18958
+ }
18959
+ const actions = byService.get(service) ?? [];
18960
+ actions.push(action);
18961
+ byService.set(service, actions);
18962
+ }
18963
+ for (const [service, actions] of byService) {
18964
+ permissions.push({ service, path: params.path, actions });
18965
+ }
18966
+ }
18967
+ if (permissions.length === 0 || permissions.length > 16) {
18968
+ throw new Error("Owner delegation requires bounded capabilities");
18969
+ }
18970
+ const resources = /* @__PURE__ */ new Set();
18971
+ for (const permission of permissions) {
18972
+ if (typeof permission.service !== "string" || import_sdk_core7.SERVICE_LONG_TO_SHORT[permission.service] === void 0 || typeof permission.path !== "string" || permission.path.length === 0 || permission.path.trim() !== permission.path || /[\u0000-\u001f\u007f\\*]/.test(permission.path) || !Array.isArray(permission.actions) || permission.actions.length === 0 || permission.actions.length > 32) {
18973
+ throw new Error("Owner delegation requires bounded capabilities");
18974
+ }
18975
+ const resourceKey = `${permission.service}\0${permission.path}`;
18976
+ if (resources.has(resourceKey)) {
18977
+ throw new Error("Owner delegation permissions contain duplicate resources");
18978
+ }
18979
+ resources.add(resourceKey);
18980
+ const actions = /* @__PURE__ */ new Set();
18981
+ for (const action of permission.actions) {
18982
+ if (typeof action !== "string" || !action.startsWith(`${permission.service}/`) || action.length === permission.service.length + 1 || actions.has(action)) {
18983
+ throw new Error("Owner delegation capabilities are unsupported");
18984
+ }
18985
+ actions.add(action);
18986
+ }
18987
+ if (permission.service === import_sdk_core7.ENCRYPTION_PERMISSION_SERVICE) {
18988
+ try {
18989
+ (0, import_sdk_core7.parseNetworkId)(permission.path);
18990
+ } catch {
18991
+ throw new Error("Owner delegation encryption permission requires a canonical network URN");
18992
+ }
18993
+ }
18994
+ }
18995
+ return permissions;
18996
+ }
18997
+ function ownerPermissionsToAbilities(permissions) {
18918
18998
  const abilities = {};
18919
- for (const action of actions) {
18920
- const slash = action.indexOf("/");
18921
- if (slash === -1) return void 0;
18922
- const shortService = import_sdk_core7.SERVICE_LONG_TO_SHORT[action.slice(0, slash)];
18923
- if (shortService === void 0) return void 0;
18999
+ const rawAbilities = {};
19000
+ for (const permission of permissions) {
19001
+ if (permission.service === import_sdk_core7.ENCRYPTION_PERMISSION_SERVICE) {
19002
+ rawAbilities[permission.path] = [...permission.actions];
19003
+ continue;
19004
+ }
19005
+ const shortService = import_sdk_core7.SERVICE_LONG_TO_SHORT[permission.service];
18924
19006
  abilities[shortService] ?? (abilities[shortService] = {});
18925
- (_a = abilities[shortService])[path] ?? (_a[path] = []);
18926
- abilities[shortService][path].push(action);
19007
+ abilities[shortService][permission.path] = [...permission.actions];
18927
19008
  }
18928
- return Object.keys(abilities).length > 0 ? abilities : void 0;
19009
+ return { abilities, rawAbilities };
18929
19010
  }
18930
19011
  function base64UrlEncode(bytes) {
18931
19012
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -19043,6 +19124,12 @@ var _TinyCloudNode = class _TinyCloudNode {
19043
19124
  this._baseSecrets = /* @__PURE__ */ new Map();
19044
19125
  this._secrets = /* @__PURE__ */ new Map();
19045
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();
19046
19133
  /**
19047
19134
  * True when the last signIn() detected an interactive signer and skipped
19048
19135
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -19439,6 +19526,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19439
19526
  this._spaceService = void 0;
19440
19527
  this._serviceContext = void 0;
19441
19528
  this.runtimePermissionGrants = [];
19529
+ this.confirmedHostedSpaceIds.clear();
19442
19530
  await this.tc.signIn(options);
19443
19531
  this.syncResolvedHostFromAuth();
19444
19532
  const oldGraph = this._serviceGraph;
@@ -19621,14 +19709,18 @@ var _TinyCloudNode = class _TinyCloudNode {
19621
19709
  }
19622
19710
  if (step.kind === "seed-applications") {
19623
19711
  const registered = await this.account.applications.register(
19624
- 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 }
19625
19717
  );
19626
19718
  if (!registered.ok) {
19627
19719
  throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
19628
19720
  }
19629
19721
  }
19630
19722
  if (step.kind === "encryption-network-create") {
19631
- await this.ensureEncryptionNetwork(step.networkId);
19723
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
19632
19724
  }
19633
19725
  if (step.kind === "secret-records-schema") {
19634
19726
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -19907,6 +19999,22 @@ var _TinyCloudNode = class _TinyCloudNode {
19907
19999
  await this.ensureEncryptionNetwork(networkId);
19908
20000
  }
19909
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
+ */
19910
20018
  async ensureOwnedSpaceHostedById(spaceId) {
19911
20019
  if (!this.auth) {
19912
20020
  throw new Error("Owned space hosting requires wallet mode");
@@ -19915,12 +20023,16 @@ var _TinyCloudNode = class _TinyCloudNode {
19915
20023
  if (!session) {
19916
20024
  throw new Error("Owned space hosting requires an active session");
19917
20025
  }
20026
+ if (this.confirmedHostedSpaceIds.has(spaceId)) {
20027
+ return;
20028
+ }
19918
20029
  const host = this.hosts[0] ?? this.config.host;
19919
20030
  if (!host) {
19920
20031
  throw new Error("Owned space hosting requires a TinyCloud host");
19921
20032
  }
19922
20033
  const activation = await (0, import_sdk_core7.activateSessionWithHost)(host, session.delegationHeader);
19923
20034
  if (activation.success && !activation.skipped?.includes(spaceId)) {
20035
+ this.confirmedHostedSpaceIds.add(spaceId);
19924
20036
  return;
19925
20037
  }
19926
20038
  if (!activation.success && activation.status !== 404) {
@@ -19939,6 +20051,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19939
20051
  `Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
19940
20052
  );
19941
20053
  }
20054
+ this.confirmedHostedSpaceIds.add(spaceId);
19942
20055
  }
19943
20056
  /**
19944
20057
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -20261,6 +20374,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20261
20374
  this.tc = stagedGraph.core;
20262
20375
  this._recapOperationsCache = stagedPrimary.cache;
20263
20376
  this.runtimePermissionGrants = stagedPrimary.grants;
20377
+ this.confirmedHostedSpaceIds.clear();
20264
20378
  if (this.auth) {
20265
20379
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
20266
20380
  this._restoredTcSession = void 0;
@@ -20843,7 +20957,17 @@ var _TinyCloudNode = class _TinyCloudNode {
20843
20957
  verifyNodeSignature: (nodeId, message, signature2) => (0, import_sdk_core7.verifyDidKeyEd25519Signature)(nodeId, message, signature2)
20844
20958
  };
20845
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
+ */
20846
20968
  async fetchNodeId() {
20969
+ const cached = this.auth?.nodeIdForHost(this.config.host);
20970
+ if (cached) return cached;
20847
20971
  const response = await fetch(`${this.config.host}/info`);
20848
20972
  if (!response.ok) {
20849
20973
  throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
@@ -21299,21 +21423,29 @@ var _TinyCloudNode = class _TinyCloudNode {
21299
21423
  async createOwnerDelegation(params, assertActive) {
21300
21424
  const assertOwnerGraphActive = assertActive ?? this._serviceGraph.assertActive.bind(this._serviceGraph);
21301
21425
  assertOwnerGraphActive();
21302
- if (!params.delegateDid.startsWith("did:key:") || params.actions.length === 0 || params.path.length === 0) {
21426
+ if (!params.delegateDid.startsWith("did:key:")) {
21303
21427
  throw new Error("Owner delegation requires an external did:key audience and bounded capabilities");
21304
21428
  }
21429
+ const permissions = ownerDelegationPermissions(params);
21305
21430
  const now = /* @__PURE__ */ new Date();
21306
- if (params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core7.EXPIRY.MAX_MS) {
21431
+ if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core7.EXPIRY.MAX_MS) {
21307
21432
  throw new Error("Owner delegation expiry must be explicit, future, and within EXPIRY.MAX_MS");
21308
21433
  }
21309
21434
  if (!this.signer) throw new Error("Owner wallet signer is required");
21310
21435
  const session = this.currentTinyCloudSession();
21311
21436
  if (!session) throw new Error("Owner session is required");
21312
- const abilities = sharingActionsToAbilities(params.path, [...params.actions]);
21313
- if (!abilities) throw new Error("Owner delegation capabilities are unsupported");
21437
+ const ownerDid = (0, import_sdk_core7.pkhDid)(session.address, session.chainId);
21438
+ for (const permission of permissions) {
21439
+ if (permission.service === import_sdk_core7.ENCRYPTION_PERMISSION_SERVICE && !(0, import_sdk_core7.principalDidEquals)((0, import_sdk_core7.parseNetworkId)(permission.path).ownerDid, ownerDid)) {
21440
+ throw new Error("Owner delegation cannot grant a foreign encryption network");
21441
+ }
21442
+ }
21443
+ const { abilities, rawAbilities } = ownerPermissionsToAbilities(permissions);
21444
+ const primary = permissions[0];
21314
21445
  const host = this.config.host;
21315
21446
  const prepared = this.wasmBindings.prepareSession({
21316
21447
  abilities,
21448
+ ...Object.keys(rawAbilities).length === 0 ? {} : { rawAbilities },
21317
21449
  address: this.wasmBindings.ensureEip55(session.address),
21318
21450
  chainId: session.chainId,
21319
21451
  domain: this.siweDomain,
@@ -21333,10 +21465,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21333
21465
  const delegation = {
21334
21466
  cid: delegationSession.delegationCid,
21335
21467
  delegateDID: params.delegateDid,
21336
- delegatorDID: (0, import_sdk_core7.pkhDid)(session.address, session.chainId),
21468
+ delegatorDID: ownerDid,
21337
21469
  spaceId: params.spaceId,
21338
- path: params.path,
21339
- actions: [...params.actions],
21470
+ path: primary.path,
21471
+ actions: [...primary.actions],
21340
21472
  expiry: params.expiresAt,
21341
21473
  isRevoked: false,
21342
21474
  allowSubDelegation: true,
@@ -21347,6 +21479,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21347
21479
  delegation,
21348
21480
  signedDagCbor: decodeAuthorizationBytes(delegationSession.delegationHeader.Authorization),
21349
21481
  delegationCid: delegationSession.delegationCid,
21482
+ permissions,
21350
21483
  nodeReceipt: {
21351
21484
  commitEventCid: activation.commitEventCid,
21352
21485
  activated: activation.activated ?? [],
@@ -21606,6 +21739,13 @@ var _TinyCloudNode = class _TinyCloudNode {
21606
21739
  body
21607
21740
  )
21608
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
+ }
21609
21749
  if (!response.ok) {
21610
21750
  throw new Error(
21611
21751
  `Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
@@ -21614,11 +21754,19 @@ var _TinyCloudNode = class _TinyCloudNode {
21614
21754
  const created = await response.json();
21615
21755
  return created.descriptor;
21616
21756
  }
21617
- 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 = {}) {
21618
21764
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
21619
- const existing = await this.getEncryptionNetwork(networkId);
21620
- if (existing) {
21621
- return existing;
21765
+ if (!options.assumeMissing) {
21766
+ const existing = await this.getEncryptionNetwork(networkId);
21767
+ if (existing) {
21768
+ return existing;
21769
+ }
21622
21770
  }
21623
21771
  const parsed = (0, import_sdk_core7.parseNetworkId)(networkId);
21624
21772
  if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {