@tinycloud/node-sdk 2.6.0-beta.0 → 2.6.0-beta.2

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.
@@ -910,6 +910,12 @@ declare class TinyCloudNode {
910
910
  private _delegationManager?;
911
911
  private _spaceService?;
912
912
  private runtimePermissionGrants;
913
+ /**
914
+ * Memoized `recapOperationsFromSession` result, keyed by the exact SIWE it
915
+ * was parsed from. The primary session is stable for the life of a sign-in,
916
+ * so this avoids re-parsing the recap on every registration.
917
+ */
918
+ private _recapOperationsCache?;
913
919
  /**
914
920
  * TinyCloudSession captured by {@link restoreSession} when there's no
915
921
  * auth-layer signer available (session-only mode used by OpenKey-backed
@@ -1052,6 +1058,42 @@ declare class TinyCloudNode {
1052
1058
  private isFreshBootstrapAccount;
1053
1059
  private runAccountBootstrap;
1054
1060
  private registerBootstrapRuntimeGrant;
1061
+ /**
1062
+ * Map the base session's OWN recap into runtime permission operations.
1063
+ *
1064
+ * Uses the RAW `parseRecapFromSiwe` binding — NOT `parseRecapCapabilities`,
1065
+ * whose `normalizeSpace` collapses `tinycloud:pkh:...:<owner>:<space>` to a
1066
+ * bare short name and would conflate two owners' identically-named spaces.
1067
+ * We must keep the full owner-scoped URI so a synthetic primary grant can
1068
+ * never cover an operation on a different owner's space.
1069
+ *
1070
+ * Mirrors {@link operationsFromDelegation}'s op shape: encryption network
1071
+ * entries (`urn:tinycloud:encryption:` paths) become `resource` ops; every
1072
+ * other entry becomes a `spaceId` op carrying the raw recap `space` URI.
1073
+ * One op per action.
1074
+ *
1075
+ * Returns `[]` for session-only / restored-without-siwe modes and for any
1076
+ * unparseable SIWE — the primary grant is simply not registered in that case.
1077
+ */
1078
+ private recapOperationsFromSession;
1079
+ /**
1080
+ * Register the base (primary) session's own recap as a synthetic runtime
1081
+ * grant tagged `provenance: "primary"` so it always out-ranks other covering
1082
+ * grants in {@link findGrantForOperations}. This closes the selection-design
1083
+ * hazard where a broad — possibly broken — bootstrap/delegated grant could
1084
+ * hijack an operation the primary session itself already authorized (TC-111).
1085
+ *
1086
+ * Two safety exclusions:
1087
+ * - Ops whose space is in `lastActivationSkippedSpaceIds` are dropped: the
1088
+ * node refused to activate those spaces this sign-in even though the recap
1089
+ * claims them. Including them would let the synthetic primary out-rank a
1090
+ * working grant and 401 (the "skipped-activation inverted hijack").
1091
+ * - Encryption `resource` ops are kept as-is (space-independent).
1092
+ *
1093
+ * No-ops when nothing remains after exclusion. Callers (`signIn`,
1094
+ * `restoreSession`) clear `runtimePermissionGrants` first, so no dupes.
1095
+ */
1096
+ private registerPrimarySessionGrant;
1055
1097
  private writeManifestRegistryRecords;
1056
1098
  private scheduleAccountRegistrySync;
1057
1099
  private withAccountRegistryRetry;
@@ -910,6 +910,12 @@ declare class TinyCloudNode {
910
910
  private _delegationManager?;
911
911
  private _spaceService?;
912
912
  private runtimePermissionGrants;
913
+ /**
914
+ * Memoized `recapOperationsFromSession` result, keyed by the exact SIWE it
915
+ * was parsed from. The primary session is stable for the life of a sign-in,
916
+ * so this avoids re-parsing the recap on every registration.
917
+ */
918
+ private _recapOperationsCache?;
913
919
  /**
914
920
  * TinyCloudSession captured by {@link restoreSession} when there's no
915
921
  * auth-layer signer available (session-only mode used by OpenKey-backed
@@ -1052,6 +1058,42 @@ declare class TinyCloudNode {
1052
1058
  private isFreshBootstrapAccount;
1053
1059
  private runAccountBootstrap;
1054
1060
  private registerBootstrapRuntimeGrant;
1061
+ /**
1062
+ * Map the base session's OWN recap into runtime permission operations.
1063
+ *
1064
+ * Uses the RAW `parseRecapFromSiwe` binding — NOT `parseRecapCapabilities`,
1065
+ * whose `normalizeSpace` collapses `tinycloud:pkh:...:<owner>:<space>` to a
1066
+ * bare short name and would conflate two owners' identically-named spaces.
1067
+ * We must keep the full owner-scoped URI so a synthetic primary grant can
1068
+ * never cover an operation on a different owner's space.
1069
+ *
1070
+ * Mirrors {@link operationsFromDelegation}'s op shape: encryption network
1071
+ * entries (`urn:tinycloud:encryption:` paths) become `resource` ops; every
1072
+ * other entry becomes a `spaceId` op carrying the raw recap `space` URI.
1073
+ * One op per action.
1074
+ *
1075
+ * Returns `[]` for session-only / restored-without-siwe modes and for any
1076
+ * unparseable SIWE — the primary grant is simply not registered in that case.
1077
+ */
1078
+ private recapOperationsFromSession;
1079
+ /**
1080
+ * Register the base (primary) session's own recap as a synthetic runtime
1081
+ * grant tagged `provenance: "primary"` so it always out-ranks other covering
1082
+ * grants in {@link findGrantForOperations}. This closes the selection-design
1083
+ * hazard where a broad — possibly broken — bootstrap/delegated grant could
1084
+ * hijack an operation the primary session itself already authorized (TC-111).
1085
+ *
1086
+ * Two safety exclusions:
1087
+ * - Ops whose space is in `lastActivationSkippedSpaceIds` are dropped: the
1088
+ * node refused to activate those spaces this sign-in even though the recap
1089
+ * claims them. Including them would let the synthetic primary out-rank a
1090
+ * working grant and 401 (the "skipped-activation inverted hijack").
1091
+ * - Encryption `resource` ops are kept as-is (space-independent).
1092
+ *
1093
+ * No-ops when nothing remains after exclusion. Callers (`signIn`,
1094
+ * `restoreSession`) clear `runtimePermissionGrants` first, so no dupes.
1095
+ */
1096
+ private registerPrimarySessionGrant;
1055
1097
  private writeManifestRegistryRecords;
1056
1098
  private scheduleAccountRegistrySync;
1057
1099
  private withAccountRegistryRetry;
package/dist/core.cjs CHANGED
@@ -2147,6 +2147,10 @@ var _TinyCloudNode = class _TinyCloudNode {
2147
2147
  await this.tc.signIn(options);
2148
2148
  this.syncResolvedHostFromAuth();
2149
2149
  this.initializeServices();
2150
+ const primarySession = this.currentTinyCloudSession();
2151
+ if (primarySession) {
2152
+ this.registerPrimarySessionGrant(primarySession);
2153
+ }
2150
2154
  const bootstrapped = await this.bootstrapAccountIfNeeded();
2151
2155
  await this.ensureRequestedEncryptionNetworks();
2152
2156
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
@@ -2390,7 +2394,107 @@ var _TinyCloudNode = class _TinyCloudNode {
2390
2394
  host: this.config.host
2391
2395
  },
2392
2396
  operations,
2393
- expiresAt
2397
+ expiresAt,
2398
+ provenance: "bootstrap"
2399
+ });
2400
+ }
2401
+ /**
2402
+ * Map the base session's OWN recap into runtime permission operations.
2403
+ *
2404
+ * Uses the RAW `parseRecapFromSiwe` binding — NOT `parseRecapCapabilities`,
2405
+ * whose `normalizeSpace` collapses `tinycloud:pkh:...:<owner>:<space>` to a
2406
+ * bare short name and would conflate two owners' identically-named spaces.
2407
+ * We must keep the full owner-scoped URI so a synthetic primary grant can
2408
+ * never cover an operation on a different owner's space.
2409
+ *
2410
+ * Mirrors {@link operationsFromDelegation}'s op shape: encryption network
2411
+ * entries (`urn:tinycloud:encryption:` paths) become `resource` ops; every
2412
+ * other entry becomes a `spaceId` op carrying the raw recap `space` URI.
2413
+ * One op per action.
2414
+ *
2415
+ * Returns `[]` for session-only / restored-without-siwe modes and for any
2416
+ * unparseable SIWE — the primary grant is simply not registered in that case.
2417
+ */
2418
+ recapOperationsFromSession(session) {
2419
+ const siwe = session.siwe;
2420
+ if (!siwe) {
2421
+ return [];
2422
+ }
2423
+ if (this._recapOperationsCache?.siwe === siwe) {
2424
+ return this._recapOperationsCache.operations;
2425
+ }
2426
+ let operations = [];
2427
+ try {
2428
+ const entries = this.wasmBindings.parseRecapFromSiwe(siwe);
2429
+ if (Array.isArray(entries)) {
2430
+ operations = entries.flatMap((entry) => {
2431
+ const service = this.invocationServiceName(entry.service);
2432
+ return entry.actions.map((action) => ({
2433
+ ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId: entry.space },
2434
+ service,
2435
+ path: entry.path,
2436
+ action
2437
+ }));
2438
+ });
2439
+ }
2440
+ } catch {
2441
+ operations = [];
2442
+ }
2443
+ this._recapOperationsCache = { siwe, operations };
2444
+ return operations;
2445
+ }
2446
+ /**
2447
+ * Register the base (primary) session's own recap as a synthetic runtime
2448
+ * grant tagged `provenance: "primary"` so it always out-ranks other covering
2449
+ * grants in {@link findGrantForOperations}. This closes the selection-design
2450
+ * hazard where a broad — possibly broken — bootstrap/delegated grant could
2451
+ * hijack an operation the primary session itself already authorized (TC-111).
2452
+ *
2453
+ * Two safety exclusions:
2454
+ * - Ops whose space is in `lastActivationSkippedSpaceIds` are dropped: the
2455
+ * node refused to activate those spaces this sign-in even though the recap
2456
+ * claims them. Including them would let the synthetic primary out-rank a
2457
+ * working grant and 401 (the "skipped-activation inverted hijack").
2458
+ * - Encryption `resource` ops are kept as-is (space-independent).
2459
+ *
2460
+ * No-ops when nothing remains after exclusion. Callers (`signIn`,
2461
+ * `restoreSession`) clear `runtimePermissionGrants` first, so no dupes.
2462
+ */
2463
+ registerPrimarySessionGrant(session) {
2464
+ const skipped = this.auth ? this.auth.lastActivationSkippedSpaceIds ?? [] : [];
2465
+ const operations = this.recapOperationsFromSession(session).filter(
2466
+ (operation) => operation.spaceId === void 0 || !skipped.some((spaceId) => this.spaceIdsEqual(spaceId, operation.spaceId))
2467
+ );
2468
+ if (operations.length === 0) {
2469
+ return;
2470
+ }
2471
+ const expiresAt = extractSiweExpiration(session.siwe) ?? this.getSessionExpiry();
2472
+ const actions = [...new Set(operations.map((operation) => operation.action))];
2473
+ this.runtimePermissionGrants.push({
2474
+ session: {
2475
+ delegationHeader: session.delegationHeader,
2476
+ delegationCid: session.delegationCid,
2477
+ spaceId: session.spaceId,
2478
+ verificationMethod: session.verificationMethod,
2479
+ jwk: session.jwk
2480
+ },
2481
+ delegation: {
2482
+ cid: session.delegationCid,
2483
+ delegationHeader: session.delegationHeader,
2484
+ delegateDID: session.verificationMethod,
2485
+ delegatorDID: this.did,
2486
+ spaceId: session.spaceId,
2487
+ path: "",
2488
+ actions,
2489
+ expiry: expiresAt,
2490
+ allowSubDelegation: true,
2491
+ ownerAddress: session.address,
2492
+ chainId: session.chainId,
2493
+ host: this.config.host
2494
+ },
2495
+ operations,
2496
+ expiresAt,
2497
+ provenance: "primary"
2394
2498
  });
2395
2499
  }
2396
2500
  async writeManifestRegistryRecords() {
@@ -2726,6 +2830,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2726
2830
  } else {
2727
2831
  this._restoredTcSession = tcSession;
2728
2832
  }
2833
+ this.registerPrimarySessionGrant(tcSession);
2729
2834
  }
2730
2835
  }
2731
2836
  /**
@@ -3764,7 +3869,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3764
3869
  getRuntimePermissionDelegations(permissions) {
3765
3870
  this.pruneExpiredRuntimePermissionGrants();
3766
3871
  if (permissions === void 0) {
3767
- return this.runtimePermissionGrants.map((grant) => grant.delegation);
3872
+ return this.runtimePermissionGrants.filter((grant) => grant.provenance !== "primary").map((grant) => grant.delegation);
3768
3873
  }
3769
3874
  const session = this.currentTinyCloudSession();
3770
3875
  if (!session || !Array.isArray(permissions) || permissions.length === 0) {
@@ -3916,7 +4021,8 @@ var _TinyCloudNode = class _TinyCloudNode {
3916
4021
  },
3917
4022
  delegation,
3918
4023
  operations: this.permissionOperations(delegatedEntries, spaceId),
3919
- expiresAt
4024
+ expiresAt,
4025
+ provenance: "runtime"
3920
4026
  });
3921
4027
  delegations.push(delegation);
3922
4028
  }
@@ -4577,7 +4683,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4577
4683
  return grants;
4578
4684
  }
4579
4685
  for (const operation of operations) {
4580
- const grant = this.findGrantForOperation(operation);
4686
+ const grant = this.findGrantForOperation(operation, { excludePrimary: true });
4581
4687
  if (!grant) {
4582
4688
  return [];
4583
4689
  }
@@ -4617,7 +4723,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4617
4723
  },
4618
4724
  delegation,
4619
4725
  operations,
4620
- expiresAt: delegation.expiry
4726
+ expiresAt: delegation.expiry,
4727
+ provenance: "delegated"
4621
4728
  };
4622
4729
  }
4623
4730
  installRuntimeGrantFromServiceSession(delegation, session, expiresAt) {
@@ -4632,7 +4739,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4632
4739
  session,
4633
4740
  delegation,
4634
4741
  operations,
4635
- expiresAt
4742
+ expiresAt,
4743
+ provenance: "delegated"
4636
4744
  });
4637
4745
  }
4638
4746
  delegatedResourcesForEntries(entries, spaceId) {
@@ -4732,21 +4840,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4732
4840
  });
4733
4841
  return grant?.session ?? fallback;
4734
4842
  }
4735
- findGrantForOperations(operations) {
4843
+ findGrantForOperations(operations, options) {
4736
4844
  if (operations.length === 0) {
4737
4845
  return void 0;
4738
4846
  }
4739
4847
  this.pruneExpiredRuntimePermissionGrants();
4740
- return this.runtimePermissionGrants.find((grant) => {
4848
+ const covering = this.runtimePermissionGrants.filter((grant) => {
4849
+ if (options?.excludePrimary && grant.provenance === "primary") {
4850
+ return false;
4851
+ }
4741
4852
  return operations.every(
4742
4853
  (operation) => grant.operations.some(
4743
4854
  (granted) => this.operationCovers(granted, operation)
4744
4855
  )
4745
4856
  );
4746
4857
  });
4858
+ if (covering.length === 0) {
4859
+ return void 0;
4860
+ }
4861
+ return covering.find((grant) => grant.provenance === "primary") ?? covering[0];
4747
4862
  }
4748
- findGrantForOperation(operation) {
4749
- return this.findGrantForOperations([operation]);
4863
+ findGrantForOperation(operation, options) {
4864
+ return this.findGrantForOperations([operation], options);
4750
4865
  }
4751
4866
  pruneExpiredRuntimePermissionGrants() {
4752
4867
  const now = Date.now();