@tinycloud/node-sdk 2.6.0-beta.0 → 2.6.0-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
@@ -19164,6 +19164,10 @@ var _TinyCloudNode = class _TinyCloudNode {
19164
19164
  await this.tc.signIn(options);
19165
19165
  this.syncResolvedHostFromAuth();
19166
19166
  this.initializeServices();
19167
+ const primarySession = this.currentTinyCloudSession();
19168
+ if (primarySession) {
19169
+ this.registerPrimarySessionGrant(primarySession);
19170
+ }
19167
19171
  const bootstrapped = await this.bootstrapAccountIfNeeded();
19168
19172
  await this.ensureRequestedEncryptionNetworks();
19169
19173
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
@@ -19407,7 +19411,107 @@ var _TinyCloudNode = class _TinyCloudNode {
19407
19411
  host: this.config.host
19408
19412
  },
19409
19413
  operations,
19410
- expiresAt
19414
+ expiresAt,
19415
+ provenance: "bootstrap"
19416
+ });
19417
+ }
19418
+ /**
19419
+ * Map the base session's OWN recap into runtime permission operations.
19420
+ *
19421
+ * Uses the RAW `parseRecapFromSiwe` binding — NOT `parseRecapCapabilities`,
19422
+ * whose `normalizeSpace` collapses `tinycloud:pkh:...:<owner>:<space>` to a
19423
+ * bare short name and would conflate two owners' identically-named spaces.
19424
+ * We must keep the full owner-scoped URI so a synthetic primary grant can
19425
+ * never cover an operation on a different owner's space.
19426
+ *
19427
+ * Mirrors {@link operationsFromDelegation}'s op shape: encryption network
19428
+ * entries (`urn:tinycloud:encryption:` paths) become `resource` ops; every
19429
+ * other entry becomes a `spaceId` op carrying the raw recap `space` URI.
19430
+ * One op per action.
19431
+ *
19432
+ * Returns `[]` for session-only / restored-without-siwe modes and for any
19433
+ * unparseable SIWE — the primary grant is simply not registered in that case.
19434
+ */
19435
+ recapOperationsFromSession(session) {
19436
+ const siwe = session.siwe;
19437
+ if (!siwe) {
19438
+ return [];
19439
+ }
19440
+ if (this._recapOperationsCache?.siwe === siwe) {
19441
+ return this._recapOperationsCache.operations;
19442
+ }
19443
+ let operations = [];
19444
+ try {
19445
+ const entries = this.wasmBindings.parseRecapFromSiwe(siwe);
19446
+ if (Array.isArray(entries)) {
19447
+ operations = entries.flatMap((entry) => {
19448
+ const service = this.invocationServiceName(entry.service);
19449
+ return entry.actions.map((action) => ({
19450
+ ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId: entry.space },
19451
+ service,
19452
+ path: entry.path,
19453
+ action
19454
+ }));
19455
+ });
19456
+ }
19457
+ } catch {
19458
+ operations = [];
19459
+ }
19460
+ this._recapOperationsCache = { siwe, operations };
19461
+ return operations;
19462
+ }
19463
+ /**
19464
+ * Register the base (primary) session's own recap as a synthetic runtime
19465
+ * grant tagged `provenance: "primary"` so it always out-ranks other covering
19466
+ * grants in {@link findGrantForOperations}. This closes the selection-design
19467
+ * hazard where a broad — possibly broken — bootstrap/delegated grant could
19468
+ * hijack an operation the primary session itself already authorized (TC-111).
19469
+ *
19470
+ * Two safety exclusions:
19471
+ * - Ops whose space is in `lastActivationSkippedSpaceIds` are dropped: the
19472
+ * node refused to activate those spaces this sign-in even though the recap
19473
+ * claims them. Including them would let the synthetic primary out-rank a
19474
+ * working grant and 401 (the "skipped-activation inverted hijack").
19475
+ * - Encryption `resource` ops are kept as-is (space-independent).
19476
+ *
19477
+ * No-ops when nothing remains after exclusion. Callers (`signIn`,
19478
+ * `restoreSession`) clear `runtimePermissionGrants` first, so no dupes.
19479
+ */
19480
+ registerPrimarySessionGrant(session) {
19481
+ const skipped = this.auth ? this.auth.lastActivationSkippedSpaceIds ?? [] : [];
19482
+ const operations = this.recapOperationsFromSession(session).filter(
19483
+ (operation) => operation.spaceId === void 0 || !skipped.some((spaceId) => this.spaceIdsEqual(spaceId, operation.spaceId))
19484
+ );
19485
+ if (operations.length === 0) {
19486
+ return;
19487
+ }
19488
+ const expiresAt = extractSiweExpiration(session.siwe) ?? this.getSessionExpiry();
19489
+ const actions = [...new Set(operations.map((operation) => operation.action))];
19490
+ this.runtimePermissionGrants.push({
19491
+ session: {
19492
+ delegationHeader: session.delegationHeader,
19493
+ delegationCid: session.delegationCid,
19494
+ spaceId: session.spaceId,
19495
+ verificationMethod: session.verificationMethod,
19496
+ jwk: session.jwk
19497
+ },
19498
+ delegation: {
19499
+ cid: session.delegationCid,
19500
+ delegationHeader: session.delegationHeader,
19501
+ delegateDID: session.verificationMethod,
19502
+ delegatorDID: this.did,
19503
+ spaceId: session.spaceId,
19504
+ path: "",
19505
+ actions,
19506
+ expiry: expiresAt,
19507
+ allowSubDelegation: true,
19508
+ ownerAddress: session.address,
19509
+ chainId: session.chainId,
19510
+ host: this.config.host
19511
+ },
19512
+ operations,
19513
+ expiresAt,
19514
+ provenance: "primary"
19411
19515
  });
19412
19516
  }
19413
19517
  async writeManifestRegistryRecords() {
@@ -19743,6 +19847,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19743
19847
  } else {
19744
19848
  this._restoredTcSession = tcSession;
19745
19849
  }
19850
+ this.registerPrimarySessionGrant(tcSession);
19746
19851
  }
19747
19852
  }
19748
19853
  /**
@@ -20781,7 +20886,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20781
20886
  getRuntimePermissionDelegations(permissions) {
20782
20887
  this.pruneExpiredRuntimePermissionGrants();
20783
20888
  if (permissions === void 0) {
20784
- return this.runtimePermissionGrants.map((grant) => grant.delegation);
20889
+ return this.runtimePermissionGrants.filter((grant) => grant.provenance !== "primary").map((grant) => grant.delegation);
20785
20890
  }
20786
20891
  const session = this.currentTinyCloudSession();
20787
20892
  if (!session || !Array.isArray(permissions) || permissions.length === 0) {
@@ -20933,7 +21038,8 @@ var _TinyCloudNode = class _TinyCloudNode {
20933
21038
  },
20934
21039
  delegation,
20935
21040
  operations: this.permissionOperations(delegatedEntries, spaceId),
20936
- expiresAt
21041
+ expiresAt,
21042
+ provenance: "runtime"
20937
21043
  });
20938
21044
  delegations.push(delegation);
20939
21045
  }
@@ -21594,7 +21700,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21594
21700
  return grants;
21595
21701
  }
21596
21702
  for (const operation of operations) {
21597
- const grant = this.findGrantForOperation(operation);
21703
+ const grant = this.findGrantForOperation(operation, { excludePrimary: true });
21598
21704
  if (!grant) {
21599
21705
  return [];
21600
21706
  }
@@ -21634,7 +21740,8 @@ var _TinyCloudNode = class _TinyCloudNode {
21634
21740
  },
21635
21741
  delegation,
21636
21742
  operations,
21637
- expiresAt: delegation.expiry
21743
+ expiresAt: delegation.expiry,
21744
+ provenance: "delegated"
21638
21745
  };
21639
21746
  }
21640
21747
  installRuntimeGrantFromServiceSession(delegation, session, expiresAt) {
@@ -21649,7 +21756,8 @@ var _TinyCloudNode = class _TinyCloudNode {
21649
21756
  session,
21650
21757
  delegation,
21651
21758
  operations,
21652
- expiresAt
21759
+ expiresAt,
21760
+ provenance: "delegated"
21653
21761
  });
21654
21762
  }
21655
21763
  delegatedResourcesForEntries(entries, spaceId) {
@@ -21749,21 +21857,28 @@ var _TinyCloudNode = class _TinyCloudNode {
21749
21857
  });
21750
21858
  return grant?.session ?? fallback;
21751
21859
  }
21752
- findGrantForOperations(operations) {
21860
+ findGrantForOperations(operations, options) {
21753
21861
  if (operations.length === 0) {
21754
21862
  return void 0;
21755
21863
  }
21756
21864
  this.pruneExpiredRuntimePermissionGrants();
21757
- return this.runtimePermissionGrants.find((grant) => {
21865
+ const covering = this.runtimePermissionGrants.filter((grant) => {
21866
+ if (options?.excludePrimary && grant.provenance === "primary") {
21867
+ return false;
21868
+ }
21758
21869
  return operations.every(
21759
21870
  (operation) => grant.operations.some(
21760
21871
  (granted) => this.operationCovers(granted, operation)
21761
21872
  )
21762
21873
  );
21763
21874
  });
21875
+ if (covering.length === 0) {
21876
+ return void 0;
21877
+ }
21878
+ return covering.find((grant) => grant.provenance === "primary") ?? covering[0];
21764
21879
  }
21765
- findGrantForOperation(operation) {
21766
- return this.findGrantForOperations([operation]);
21880
+ findGrantForOperation(operation, options) {
21881
+ return this.findGrantForOperations([operation], options);
21767
21882
  }
21768
21883
  pruneExpiredRuntimePermissionGrants() {
21769
21884
  const now = Date.now();