@tinycloud/node-sdk 2.7.0-beta.3 → 2.7.0-beta.4

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/core.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  parsePkhDid,
18
18
  pkhDid as pkhDid3,
19
19
  principalDid,
20
- principalDidEquals as principalDidEquals3,
20
+ principalDidEquals as principalDidEquals4,
21
21
  parseCanonicalNetworkId,
22
22
  TinyCloudDebugLogger,
23
23
  tinyCloudDebugLogger,
@@ -391,6 +391,30 @@ var NodeUserAuthorization = class {
391
391
  this.tinycloudHosts = [...hosts];
392
392
  }
393
393
  }
394
+ /** @internal Atomically adopt the state staged by TinyCloudNode.restoreSession. */
395
+ installRestoredSession(sessionManager, session, hosts) {
396
+ this.sessionManager = sessionManager;
397
+ this.tinycloudHosts = hosts && hosts.length > 0 ? [...hosts] : void 0;
398
+ this._lastActivationSkippedSpaceIds = [];
399
+ if (!session) {
400
+ this._session = void 0;
401
+ this._tinyCloudSession = void 0;
402
+ this._address = void 0;
403
+ this._chainId = void 0;
404
+ return;
405
+ }
406
+ this._tinyCloudSession = { ...session };
407
+ this._address = session.address;
408
+ this._chainId = session.chainId;
409
+ this._session = {
410
+ address: session.address,
411
+ walletAddress: session.address,
412
+ chainId: session.chainId,
413
+ sessionKey: session.sessionKey,
414
+ siwe: session.siwe,
415
+ signature: session.signature
416
+ };
417
+ }
394
418
  /**
395
419
  * Ensure `tinycloudHosts` are resolved before a host-needing call.
396
420
  *
@@ -1171,6 +1195,7 @@ import {
1171
1195
  HooksService as HooksService2,
1172
1196
  DataVaultService,
1173
1197
  EncryptionService,
1198
+ DecryptTransportResponseError,
1174
1199
  SecretsService,
1175
1200
  createVaultCrypto,
1176
1201
  ServiceContext as ServiceContext2,
@@ -1188,12 +1213,16 @@ import {
1188
1213
  TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
1189
1214
  bootstrapSteps,
1190
1215
  ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
1216
+ CaveatedDelegationUnsupportedError,
1191
1217
  PermissionNotInManifestError,
1192
1218
  SessionExpiredError,
1219
+ canonicalizeRecapCaveats,
1220
+ recapCaveatsEqual,
1193
1221
  expandPermissionEntries as expandPermissionEntriesCore,
1194
1222
  isCapabilitySubset,
1195
1223
  parseRecapCapabilities,
1196
1224
  SERVICE_LONG_TO_SHORT,
1225
+ SERVICE_SHORT_TO_LONG,
1197
1226
  KV as KV2,
1198
1227
  SQL as SQL2,
1199
1228
  DUCKDB as DUCKDB2,
@@ -1206,8 +1235,12 @@ import {
1206
1235
  pkhDid as pkhDid2,
1207
1236
  resolveTinyCloudHosts as resolveTinyCloudHosts2,
1208
1237
  principalDidEquals as principalDidEquals2,
1209
- parseNetworkId as parseNetworkId2
1238
+ parseNetworkId as parseNetworkId2,
1239
+ resolveSecretPath as resolveSecretPath2
1210
1240
  } from "@tinycloud/sdk-core";
1241
+ import {
1242
+ parsePermissionHint
1243
+ } from "@tinycloud/sdk-services";
1211
1244
 
1212
1245
  // src/account/AccountService.ts
1213
1246
  import { AccountService } from "@tinycloud/sdk-core";
@@ -1646,6 +1679,9 @@ var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
1646
1679
  var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
1647
1680
  var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
1648
1681
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
1682
+ function isSecretPayload(value) {
1683
+ return typeof value === "object" && value !== null && "value" in value && typeof value.value === "string" && "createdAt" in value && typeof value.createdAt === "string" && "updatedAt" in value && typeof value.updatedAt === "string";
1684
+ }
1649
1685
  var ROOT_DELEGATION_ACTIONS = [
1650
1686
  KV2.PUT,
1651
1687
  KV2.GET,
@@ -1700,6 +1736,141 @@ function didPrincipalMatches2(actual, expected) {
1700
1736
  return actual === expected;
1701
1737
  }
1702
1738
  }
1739
+ function clonePersistedSessionJwk(jwk) {
1740
+ if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) {
1741
+ throw new Error("Persisted session has an invalid private Ed25519 session key.");
1742
+ }
1743
+ try {
1744
+ const serialized = JSON.stringify(jwk);
1745
+ const cloned = serialized === void 0 ? void 0 : JSON.parse(serialized);
1746
+ if (cloned === null || typeof cloned !== "object" || Array.isArray(cloned)) {
1747
+ throw new Error("invalid JWK object");
1748
+ }
1749
+ return cloned;
1750
+ } catch {
1751
+ throw new Error("Persisted session has an invalid private Ed25519 session key.");
1752
+ }
1753
+ }
1754
+ function cloneRecapCaveats(caveats) {
1755
+ if (!caveats) return [];
1756
+ const cloneJson = (value) => {
1757
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
1758
+ return value;
1759
+ }
1760
+ if (typeof value === "number") {
1761
+ if (Number.isFinite(value)) return value;
1762
+ throw new Error("ReCap caveats must contain only JSON values.");
1763
+ }
1764
+ if (Array.isArray(value)) return value.map(cloneJson);
1765
+ if (typeof value === "object") {
1766
+ const prototype = Object.getPrototypeOf(value);
1767
+ if (prototype !== Object.prototype && prototype !== null) {
1768
+ throw new Error("ReCap caveats must contain only JSON values.");
1769
+ }
1770
+ const copy = /* @__PURE__ */ Object.create(null);
1771
+ for (const [key, nested] of Object.entries(value)) {
1772
+ copy[key] = cloneJson(nested);
1773
+ }
1774
+ return copy;
1775
+ }
1776
+ throw new Error("ReCap caveats must contain only JSON values.");
1777
+ };
1778
+ try {
1779
+ return JSON.parse(JSON.stringify(caveats.map(cloneJson)));
1780
+ } catch {
1781
+ throw new Error("Verified persisted session ReCap contains invalid caveats.");
1782
+ }
1783
+ }
1784
+ var ServiceGraphLifetime = class {
1785
+ constructor(invokeFn, invokeAnyFn, fetchFn) {
1786
+ this.invokeFn = invokeFn;
1787
+ this.invokeAnyFn = invokeAnyFn;
1788
+ this.fetchFn = fetchFn;
1789
+ this.abortController = new AbortController();
1790
+ this.contexts = /* @__PURE__ */ new Set();
1791
+ this.retired = false;
1792
+ this.invoke = (session, service, path, action, facts) => {
1793
+ this.assertActive();
1794
+ return this.invokeFn(session, service, path, action, facts);
1795
+ };
1796
+ this.invokeAny = (session, entries, facts) => {
1797
+ this.assertActive();
1798
+ return this.invokeAnyFn(session, entries, facts);
1799
+ };
1800
+ this.fetch = (url, init) => {
1801
+ this.assertActive();
1802
+ return this.fetchFn(url, {
1803
+ ...init,
1804
+ signal: this.combineSignals(init?.signal)
1805
+ });
1806
+ };
1807
+ }
1808
+ track(context) {
1809
+ this.assertActive();
1810
+ this.contexts.add(context);
1811
+ return context;
1812
+ }
1813
+ retire() {
1814
+ if (this.retired) return;
1815
+ this.retired = true;
1816
+ this.abortController.abort();
1817
+ try {
1818
+ for (const context of this.contexts) {
1819
+ try {
1820
+ const retire = context.retire;
1821
+ if (retire) retire.call(context);
1822
+ else context.abort();
1823
+ } catch (error) {
1824
+ console.error("Error retiring service graph context:", error);
1825
+ }
1826
+ }
1827
+ } finally {
1828
+ this.contexts.clear();
1829
+ }
1830
+ }
1831
+ assertActive() {
1832
+ if (this.retired) {
1833
+ throw new Error("Service graph has been retired by session replacement.");
1834
+ }
1835
+ }
1836
+ combineSignals(signal) {
1837
+ if (!signal) return this.abortController.signal;
1838
+ const combined = new AbortController();
1839
+ const abort = () => combined.abort();
1840
+ if (signal.aborted || this.abortController.signal.aborted) {
1841
+ abort();
1842
+ } else {
1843
+ signal.addEventListener("abort", abort, { once: true });
1844
+ this.abortController.signal.addEventListener("abort", abort, { once: true });
1845
+ }
1846
+ return combined.signal;
1847
+ }
1848
+ };
1849
+ var UnsupportedSessionRestoreError = class extends Error {
1850
+ constructor(reason = "it cannot replace and enumerate every live session signer") {
1851
+ super(`Persisted session restore is unsupported by this WASM binding because ${reason}.`);
1852
+ this.code = "RESTORE_SESSION_KEY_REPLACEMENT_UNSUPPORTED";
1853
+ this.name = "UnsupportedSessionRestoreError";
1854
+ }
1855
+ };
1856
+ function canonicalRestoredVerificationMethod(canonicalVerificationMethod, persistedVerificationMethod) {
1857
+ if (typeof persistedVerificationMethod !== "string") return void 0;
1858
+ const principal = canonicalVerificationMethod.split("#", 1)[0];
1859
+ return persistedVerificationMethod === principal || persistedVerificationMethod === canonicalVerificationMethod ? canonicalVerificationMethod : void 0;
1860
+ }
1861
+ function persistedExpiry(value) {
1862
+ if (typeof value !== "string") {
1863
+ throw new Error("Persisted session without a SIWE expiration must include expiresAt.");
1864
+ }
1865
+ const expiry = new Date(value);
1866
+ if (!Number.isFinite(expiry.getTime()) || expiry.getTime() <= Date.now()) {
1867
+ throw new Error("Persisted session expiry is invalid or expired.");
1868
+ }
1869
+ return expiry;
1870
+ }
1871
+ function sameInstant(left, right) {
1872
+ return left.getTime() === right.getTime();
1873
+ }
1703
1874
  function sharingActionsToAbilities(path, actions) {
1704
1875
  var _a;
1705
1876
  const abilities = {};
@@ -1826,6 +1997,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1826
1997
  this.auth = null;
1827
1998
  this.tc = null;
1828
1999
  this._chainId = 1;
2000
+ this._baseVaults = /* @__PURE__ */ new Map();
1829
2001
  this._baseSecrets = /* @__PURE__ */ new Map();
1830
2002
  this._secrets = /* @__PURE__ */ new Map();
1831
2003
  this.runtimePermissionGrants = [];
@@ -1845,26 +2017,55 @@ var _TinyCloudNode = class _TinyCloudNode {
1845
2017
  skipped: false
1846
2018
  };
1847
2019
  this.invokeWithRuntimePermissions = (session, service, path, action, facts) => {
1848
- return this.wasmBindings.invoke(
1849
- this.selectInvocationSession(session, service, path, action),
1850
- service,
2020
+ const operation = {
2021
+ spaceId: session.spaceId,
2022
+ service: this.invocationServiceName(service),
1851
2023
  path,
1852
- action,
1853
- facts
2024
+ action
2025
+ };
2026
+ const grant = this.findGrantForOperation(operation);
2027
+ const grantedOperation = grant?.operations.find(
2028
+ (candidate) => this.operationCovers(candidate, operation)
1854
2029
  );
2030
+ const invocationSession = !grant || grant.provenance === "primary" ? session : grant.session;
2031
+ if ((grantedOperation?.caveats?.length ?? 0) > 0) {
2032
+ if (!this.wasmBindings.invokeAny) {
2033
+ throw new Error("WASM binding does not support caveat-preserving invocation");
2034
+ }
2035
+ return this.wasmBindings.invokeAny(invocationSession, [{
2036
+ spaceId: invocationSession.spaceId,
2037
+ service,
2038
+ path,
2039
+ action,
2040
+ caveats: cloneRecapCaveats(grantedOperation.caveats)
2041
+ }], facts);
2042
+ }
2043
+ return this.wasmBindings.invoke(invocationSession, service, path, action, facts);
1855
2044
  };
1856
2045
  this.invokeAnyWithRuntimePermissions = (session, entries, facts) => {
1857
2046
  if (!this.wasmBindings.invokeAny) {
1858
2047
  throw new Error("WASM binding does not support invokeAny");
1859
2048
  }
1860
- const grant = this.findGrantForOperations(
1861
- entries.flatMap((entry) => {
1862
- const operation = this.operationFromInvokeAnyEntry(entry);
1863
- return operation ? [operation] : [];
1864
- })
1865
- );
2049
+ const operations = entries.flatMap((entry) => {
2050
+ const operation = this.operationFromInvokeAnyEntry(entry);
2051
+ return operation ? [operation] : [];
2052
+ });
2053
+ const grant = this.findGrantForOperations(operations);
1866
2054
  const invocationSession = !grant || grant.provenance === "primary" ? session : grant.session;
1867
- return this.wasmBindings.invokeAny(invocationSession, entries, facts);
2055
+ const caveatPreservingEntries = grant ? entries.map((entry) => {
2056
+ const requested = this.operationFromInvokeAnyEntry(entry);
2057
+ const granted = requested && grant.operations.find(
2058
+ (candidate) => this.operationCovers(candidate, requested)
2059
+ );
2060
+ if (!granted?.caveats?.length) {
2061
+ return entry;
2062
+ }
2063
+ if (entry.caveats !== void 0 && !recapCaveatsEqual(entry.caveats, granted.caveats)) {
2064
+ throw new Error("Invocation caveats do not match signed ReCap authority.");
2065
+ }
2066
+ return { ...entry, caveats: cloneRecapCaveats(granted.caveats) };
2067
+ }) : entries;
2068
+ return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
1868
2069
  };
1869
2070
  this.explicitHost = config.host;
1870
2071
  this.config = {
@@ -1893,16 +2094,19 @@ var _TinyCloudNode = class _TinyCloudNode {
1893
2094
  throw new Error("Failed to get session key JWK");
1894
2095
  }
1895
2096
  this.sessionKeyJwk = JSON.parse(jwkStr);
2097
+ this._serviceGraph = this.createServiceGraphLifetime();
1896
2098
  this._capabilityRegistry = new CapabilityKeyRegistry();
1897
2099
  this._keyProvider = new WasmKeyProvider({
1898
2100
  sessionManager: this.sessionManager
1899
2101
  });
1900
2102
  this.notificationHandler = config.notificationHandler ?? new SilentNotificationHandler();
2103
+ const receiveOnlyGraph = this._serviceGraph;
1901
2104
  this._sharingService = new SharingService({
1902
2105
  hosts: [this.config.host],
1903
2106
  // session: undefined - not needed for receive()
1904
- invoke: this.invokeWithRuntimePermissions,
1905
- fetch: globalThis.fetch.bind(globalThis),
2107
+ invoke: receiveOnlyGraph.invoke,
2108
+ fetch: receiveOnlyGraph.fetch,
2109
+ assertActive: () => receiveOnlyGraph.assertActive(),
1906
2110
  keyProvider: this._keyProvider,
1907
2111
  registry: this._capabilityRegistry,
1908
2112
  createDelegationWasm: (params) => this.createDelegationWrapper(params),
@@ -1914,12 +2118,12 @@ var _TinyCloudNode = class _TinyCloudNode {
1914
2118
  createKVService: (config2) => {
1915
2119
  const prefix = config2.pathPrefix?.replace(/\/$/, "");
1916
2120
  const kvService = new KVService2({ prefix });
1917
- const kvContext = new ServiceContext2({
2121
+ const kvContext = receiveOnlyGraph.track(new ServiceContext2({
1918
2122
  invoke: config2.invoke,
1919
2123
  fetch: config2.fetch ?? globalThis.fetch.bind(globalThis),
1920
2124
  hosts: config2.hosts,
1921
2125
  telemetry: this.config.telemetry
1922
- });
2126
+ }));
1923
2127
  kvContext.setSession(config2.session);
1924
2128
  kvService.initialize(kvContext);
1925
2129
  return kvService;
@@ -1993,6 +2197,16 @@ var _TinyCloudNode = class _TinyCloudNode {
1993
2197
  shouldUseBootstrapSignInRequest(config) {
1994
2198
  return config.autoBootstrapAccount !== false && config.manifest === void 0 && config.capabilityRequest === void 0 && (config.prefix ?? "default") === "default" && isOpenKeyAutoSignStrategy(config.signStrategy);
1995
2199
  }
2200
+ createServiceGraphLifetime() {
2201
+ return new ServiceGraphLifetime(
2202
+ this.invokeWithRuntimePermissions,
2203
+ this.invokeAnyWithRuntimePermissions,
2204
+ // Resolve fetch at request time. This keeps the graph-owned lifetime
2205
+ // signal while preserving the SDK's existing injectable global fetch
2206
+ // behavior (including adapters that install it after construction).
2207
+ (url, init) => globalThis.fetch(url, init)
2208
+ );
2209
+ }
1996
2210
  syncResolvedHostFromAuth() {
1997
2211
  const host = this.auth?.hosts[0];
1998
2212
  if (host) {
@@ -2062,6 +2276,27 @@ var _TinyCloudNode = class _TinyCloudNode {
2062
2276
  get sessionDid() {
2063
2277
  return this.sessionManager.getDID(this.sessionKeyId);
2064
2278
  }
2279
+ /**
2280
+ * Return the current session's signed ReCap capabilities after the session
2281
+ * has been authenticated or restored. This is intentionally distinct from
2282
+ * installed runtime delegations: it reports base-session authority only.
2283
+ *
2284
+ * Invalid or unparseable signed ReCap material throws so callers fail closed
2285
+ * rather than treating a malformed session as unrestricted authority.
2286
+ */
2287
+ getVerifiedSessionCapabilities() {
2288
+ const session = this.currentTinyCloudSession();
2289
+ if (!session || !session.siwe) return [];
2290
+ return parseRecapCapabilities(
2291
+ (siwe) => this.parseRecapWithCaveats(siwe),
2292
+ session.siwe
2293
+ ).map((entry) => entry.service === "tinycloud.encryption" ? {
2294
+ service: entry.service,
2295
+ path: entry.path,
2296
+ actions: [...entry.actions],
2297
+ ...entry.caveats === void 0 ? {} : { caveats: entry.caveats }
2298
+ } : entry);
2299
+ }
2065
2300
  /**
2066
2301
  * Get the Ethereum address for this user.
2067
2302
  */
@@ -2151,6 +2386,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2151
2386
  this._hooks = void 0;
2152
2387
  this._vault = void 0;
2153
2388
  this._encryption = void 0;
2389
+ this._baseVaults.clear();
2154
2390
  this._baseSecrets.clear();
2155
2391
  this._secrets.clear();
2156
2392
  this._spaceService = void 0;
@@ -2158,6 +2394,10 @@ var _TinyCloudNode = class _TinyCloudNode {
2158
2394
  this.runtimePermissionGrants = [];
2159
2395
  await this.tc.signIn(options);
2160
2396
  this.syncResolvedHostFromAuth();
2397
+ const oldGraph = this._serviceGraph;
2398
+ this._serviceGraph = this.createServiceGraphLifetime();
2399
+ oldGraph.retire();
2400
+ this.tc.retireServices();
2161
2401
  const signedInSession = this.currentTinyCloudSession();
2162
2402
  if (signedInSession) {
2163
2403
  this.sessionKeyId = signedInSession.sessionKey;
@@ -2442,7 +2682,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2442
2682
  }
2443
2683
  let operations = [];
2444
2684
  try {
2445
- const entries = this.wasmBindings.parseRecapFromSiwe(siwe);
2685
+ const entries = this.parseRecapWithCaveats(siwe);
2446
2686
  if (Array.isArray(entries)) {
2447
2687
  operations = entries.flatMap((entry) => {
2448
2688
  const service = this.invocationServiceName(entry.service);
@@ -2450,7 +2690,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2450
2690
  ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId: entry.space },
2451
2691
  service,
2452
2692
  path: entry.path,
2453
- action
2693
+ action,
2694
+ caveats: cloneRecapCaveats(entry.caveats)
2454
2695
  }));
2455
2696
  });
2456
2697
  }
@@ -2803,86 +3044,441 @@ var _TinyCloudNode = class _TinyCloudNode {
2803
3044
  */
2804
3045
  async restoreSession(sessionData) {
2805
3046
  await this.wasmBindings.ensureInitialized?.();
2806
- this._kv = void 0;
2807
- this._sql = void 0;
2808
- this._duckdb = void 0;
2809
- this._hooks = void 0;
2810
- this._vault = void 0;
2811
- this._encryption = void 0;
2812
- this._baseSecrets.clear();
2813
- this._secrets.clear();
2814
- this._spaceService = void 0;
2815
- this._serviceContext = void 0;
2816
- this.runtimePermissionGrants = [];
2817
- const restoredAddress = sessionData.address ? canonicalizeAddress2(sessionData.address) : void 0;
2818
- if (restoredAddress) {
2819
- this._address = restoredAddress;
3047
+ const restoredJwk = clonePersistedSessionJwk(sessionData.jwk);
3048
+ if (sessionData.chainId !== void 0 && (!Number.isSafeInteger(sessionData.chainId) || sessionData.chainId <= 0)) {
3049
+ throw new Error("Persisted session chain ID must be a positive safe integer.");
3050
+ }
3051
+ const stagedManager = this.wasmBindings.createSessionManager();
3052
+ const stagedReplace = stagedManager.replaceSessionKey;
3053
+ const liveKeys = this.sessionManager.listSessionKeys;
3054
+ const stagedKeys = stagedManager.listSessionKeys;
3055
+ if (typeof stagedReplace !== "function" || typeof liveKeys !== "function" || typeof stagedKeys !== "function") {
3056
+ throw new UnsupportedSessionRestoreError();
3057
+ }
3058
+ let stagedKeyId;
3059
+ let stagedJwk;
3060
+ let canonicalVerificationMethod;
3061
+ try {
3062
+ const keyIds = liveKeys.call(this.sessionManager);
3063
+ if (!Array.isArray(keyIds) || !keyIds.every((keyId) => typeof keyId === "string")) {
3064
+ throw new UnsupportedSessionRestoreError("it cannot reliably enumerate every live session signer");
3065
+ }
3066
+ for (const keyId of keyIds) {
3067
+ if (keyId === this.sessionKeyId) continue;
3068
+ const jwk = this.sessionManager.jwk(keyId);
3069
+ if (!jwk) throw new Error("missing live session key");
3070
+ stagedReplace.call(stagedManager, clonePersistedSessionJwk(JSON.parse(jwk)), keyId);
3071
+ }
3072
+ stagedKeyId = stagedReplace.call(stagedManager, restoredJwk, this.sessionKeyId);
3073
+ const stagedJwkJson = stagedManager.jwk(stagedKeyId);
3074
+ if (!stagedJwkJson) throw new Error("missing restored session key");
3075
+ stagedJwk = clonePersistedSessionJwk(JSON.parse(stagedJwkJson));
3076
+ canonicalVerificationMethod = stagedManager.getDID(stagedKeyId);
3077
+ } catch (error) {
3078
+ if (error instanceof UnsupportedSessionRestoreError) throw error;
3079
+ throw new Error("Persisted session has an invalid private Ed25519 session key.");
2820
3080
  }
2821
- if (sessionData.chainId) {
2822
- this._chainId = sessionData.chainId;
3081
+ const restoredVerificationMethod = canonicalRestoredVerificationMethod(
3082
+ canonicalVerificationMethod,
3083
+ sessionData.verificationMethod
3084
+ );
3085
+ if (!restoredVerificationMethod) {
3086
+ throw new Error(
3087
+ "Persisted session verification method does not match its private Ed25519 session key."
3088
+ );
3089
+ }
3090
+ const proofValues = [
3091
+ sessionData.siwe,
3092
+ sessionData.signature,
3093
+ sessionData.address,
3094
+ sessionData.chainId
3095
+ ];
3096
+ const hasPersistedProof = proofValues.every((value) => value !== void 0);
3097
+ if (!hasPersistedProof && (proofValues.some((value) => value !== void 0) || sessionData.expiresAt !== void 0)) {
3098
+ throw new Error("Persisted session authority metadata is incomplete.");
3099
+ }
3100
+ const restoredAddress = hasPersistedProof ? canonicalizeAddress2(sessionData.address) : void 0;
3101
+ let stagedSessionExpiry = /* @__PURE__ */ new Date(0);
3102
+ let stagedRecap = [];
3103
+ if (hasPersistedProof) {
3104
+ if (typeof this.wasmBindings.validatePersistedSession !== "function") {
3105
+ throw new UnsupportedSessionRestoreError("it cannot verify persisted SIWE authority");
3106
+ }
3107
+ const verified = this.wasmBindings.validatePersistedSession({
3108
+ delegationHeader: sessionData.delegationHeader,
3109
+ delegationCid: sessionData.delegationCid,
3110
+ spaceId: sessionData.spaceId,
3111
+ jwk: stagedJwk,
3112
+ address: restoredAddress,
3113
+ chainId: sessionData.chainId,
3114
+ siwe: sessionData.siwe,
3115
+ signature: sessionData.signature
3116
+ });
3117
+ const exactRecap = verified.verifiedRecap;
3118
+ if (!Array.isArray(exactRecap) || !exactRecap.every(
3119
+ (entry) => entry !== null && typeof entry === "object" && typeof entry.service === "string" && typeof entry.space === "string" && typeof entry.path === "string" && Array.isArray(entry.actions) && entry.actions.every((action) => typeof action === "string") && Array.isArray(entry.caveats) && entry.caveats.every(
3120
+ (caveat) => caveat !== null && typeof caveat === "object" && !Array.isArray(caveat)
3121
+ )
3122
+ )) {
3123
+ throw new UnsupportedSessionRestoreError(
3124
+ "it cannot reconstruct caveat-preserving persisted ReCap authority"
3125
+ );
3126
+ }
3127
+ stagedRecap = exactRecap.map((entry) => ({
3128
+ service: entry.service,
3129
+ space: entry.space,
3130
+ path: entry.path,
3131
+ actions: [...entry.actions],
3132
+ caveats: cloneRecapCaveats(entry.caveats)
3133
+ }));
3134
+ const signedExpiry = verified.expiresAt === void 0 ? void 0 : persistedExpiry(verified.expiresAt);
3135
+ const persistedPolicyExpiry = sessionData.expiresAt === void 0 ? void 0 : persistedExpiry(sessionData.expiresAt);
3136
+ if (signedExpiry && persistedPolicyExpiry && !sameInstant(signedExpiry, persistedPolicyExpiry)) {
3137
+ throw new Error("Persisted session expiry does not match its signed SIWE authority.");
3138
+ }
3139
+ stagedSessionExpiry = signedExpiry ?? persistedPolicyExpiry ?? persistedExpiry(void 0);
2823
3140
  }
2824
3141
  const resolvedHost = await this.resolveRestoredHost(
2825
3142
  sessionData.tinycloudHosts,
2826
3143
  restoredAddress,
2827
3144
  sessionData.chainId
2828
3145
  );
2829
- if (resolvedHost) {
2830
- this.config.host = resolvedHost;
2831
- }
2832
- this._serviceContext = new ServiceContext2({
2833
- invoke: this.invokeWithRuntimePermissions,
2834
- invokeAny: this.invokeAnyWithRuntimePermissions,
2835
- fetch: globalThis.fetch.bind(globalThis),
2836
- hosts: [this.config.host],
2837
- telemetry: this.config.telemetry
2838
- });
2839
- this._kv = new KVService2({});
2840
- this._kv.initialize(this._serviceContext);
2841
- this._serviceContext.registerService("kv", this._kv);
2842
- this._sql = new SQLService2({});
2843
- this._sql.initialize(this._serviceContext);
2844
- this._serviceContext.registerService("sql", this._sql);
2845
- this._duckdb = new DuckDbService2({});
2846
- this._duckdb.initialize(this._serviceContext);
2847
- this._serviceContext.registerService("duckdb", this._duckdb);
2848
- this._hooks = new HooksService2({});
2849
- this._hooks.initialize(this._serviceContext);
2850
- this._serviceContext.registerService("hooks", this._hooks);
3146
+ const stagedHost = resolvedHost ?? this.config.host;
3147
+ const stagedAddress = restoredAddress;
3148
+ const stagedChainId = sessionData.chainId ?? 1;
3149
+ const stagedNodeDid = stagedAddress ? pkhDid2(stagedAddress, stagedChainId) : canonicalVerificationMethod;
2851
3150
  const serviceSession = {
2852
3151
  delegationHeader: sessionData.delegationHeader,
2853
3152
  delegationCid: sessionData.delegationCid,
2854
3153
  spaceId: sessionData.spaceId,
2855
- verificationMethod: sessionData.verificationMethod,
2856
- jwk: sessionData.jwk
3154
+ verificationMethod: restoredVerificationMethod,
3155
+ jwk: stagedJwk
2857
3156
  };
2858
- this._serviceContext.setSession(serviceSession);
2859
- this._vault = this.createVaultService(sessionData.spaceId, this._kv);
2860
- this._vault.initialize(this._serviceContext);
2861
- this._serviceContext.registerService("vault", this._vault);
2862
- this.initializeV2Services(serviceSession);
2863
- if (sessionData.siwe && restoredAddress && sessionData.chainId) {
2864
- const tcSession = {
2865
- address: restoredAddress,
2866
- chainId: sessionData.chainId,
2867
- sessionKey: JSON.stringify(sessionData.jwk),
2868
- spaceId: sessionData.spaceId,
2869
- delegationCid: sessionData.delegationCid,
2870
- delegationHeader: sessionData.delegationHeader,
2871
- verificationMethod: sessionData.verificationMethod,
2872
- jwk: sessionData.jwk,
2873
- siwe: sessionData.siwe,
2874
- signature: sessionData.signature ?? ""
2875
- };
2876
- if (this.auth) {
2877
- this.auth.setRestoredTinyCloudSession(
2878
- tcSession,
2879
- this.config.host ? [this.config.host] : void 0
2880
- );
2881
- } else {
2882
- this._restoredTcSession = tcSession;
3157
+ const stagedTcSession = hasPersistedProof ? {
3158
+ address: restoredAddress,
3159
+ chainId: sessionData.chainId,
3160
+ sessionKey: JSON.stringify(stagedJwk),
3161
+ spaceId: sessionData.spaceId,
3162
+ spaces: sessionData.spaces,
3163
+ delegationCid: sessionData.delegationCid,
3164
+ delegationHeader: sessionData.delegationHeader,
3165
+ verificationMethod: restoredVerificationMethod,
3166
+ jwk: stagedJwk,
3167
+ siwe: sessionData.siwe,
3168
+ signature: sessionData.signature
3169
+ } : void 0;
3170
+ const stagedPrimary = this.stagePrimarySessionState(
3171
+ stagedTcSession,
3172
+ stagedNodeDid,
3173
+ stagedHost,
3174
+ stagedSessionExpiry,
3175
+ stagedRecap
3176
+ );
3177
+ const stagedGraph = this.stageRestoredServiceGraph({
3178
+ host: stagedHost,
3179
+ manager: stagedManager,
3180
+ serviceSession,
3181
+ verificationMethod: canonicalVerificationMethod,
3182
+ nodeDid: stagedNodeDid,
3183
+ address: stagedAddress,
3184
+ chainId: stagedChainId,
3185
+ tinyCloudSession: stagedTcSession,
3186
+ sessionExpiry: stagedSessionExpiry,
3187
+ recap: stagedRecap
3188
+ });
3189
+ const oldGraph = this._serviceGraph;
3190
+ const oldCore = this.tc;
3191
+ this.sessionManager = stagedManager;
3192
+ this.sessionKeyId = stagedKeyId;
3193
+ this.sessionKeyJwk = stagedJwk;
3194
+ if (resolvedHost) this.config.host = resolvedHost;
3195
+ this._address = stagedAddress;
3196
+ this._chainId = stagedChainId;
3197
+ this._serviceContext = stagedGraph.serviceContext;
3198
+ this._kv = stagedGraph.kv;
3199
+ this._sql = stagedGraph.sql;
3200
+ this._duckdb = stagedGraph.duckdb;
3201
+ this._hooks = stagedGraph.hooks;
3202
+ this._vault = stagedGraph.vault;
3203
+ this._encryption = stagedGraph.encryption;
3204
+ this._capabilityRegistry = stagedGraph.capabilityRegistry;
3205
+ this._keyProvider = stagedGraph.keyProvider;
3206
+ this._sharingService = stagedGraph.sharingService;
3207
+ this._delegationManager = stagedGraph.delegationManager;
3208
+ this._spaceService = stagedGraph.spaceService;
3209
+ this._serviceGraph = stagedGraph.graph;
3210
+ this._baseSecrets = /* @__PURE__ */ new Map();
3211
+ this._secrets = /* @__PURE__ */ new Map();
3212
+ this._publicKV = void 0;
3213
+ this._account = void 0;
3214
+ this.tc = stagedGraph.core;
3215
+ this._recapOperationsCache = stagedPrimary.cache;
3216
+ this.runtimePermissionGrants = stagedPrimary.grants;
3217
+ if (this.auth) {
3218
+ this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
3219
+ this._restoredTcSession = void 0;
3220
+ } else {
3221
+ this._restoredTcSession = stagedTcSession;
3222
+ }
3223
+ oldGraph.retire();
3224
+ oldCore?.retireServices?.();
3225
+ }
3226
+ stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
3227
+ if (!session?.siwe) return { cache: void 0, grants: [] };
3228
+ let operations = [];
3229
+ operations = recap.flatMap((entry) => {
3230
+ const service = this.invocationServiceName(entry.service);
3231
+ return entry.actions.map((action) => ({
3232
+ ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId: entry.space },
3233
+ service,
3234
+ path: entry.path,
3235
+ action,
3236
+ caveats: cloneRecapCaveats(entry.caveats)
3237
+ }));
3238
+ });
3239
+ const cache = { siwe: session.siwe, operations };
3240
+ if (operations.length === 0) return { cache, grants: [] };
3241
+ const expiresAt = sessionExpiry;
3242
+ return {
3243
+ cache,
3244
+ grants: [{
3245
+ session: {
3246
+ delegationHeader: session.delegationHeader,
3247
+ delegationCid: session.delegationCid,
3248
+ spaceId: session.spaceId,
3249
+ verificationMethod: session.verificationMethod,
3250
+ jwk: session.jwk
3251
+ },
3252
+ delegation: {
3253
+ cid: session.delegationCid,
3254
+ delegationHeader: session.delegationHeader,
3255
+ delegateDID: session.verificationMethod,
3256
+ delegatorDID: verificationMethod,
3257
+ spaceId: session.spaceId,
3258
+ path: "",
3259
+ actions: [...new Set(operations.map((operation) => operation.action))],
3260
+ expiry: expiresAt,
3261
+ allowSubDelegation: true,
3262
+ ownerAddress: session.address,
3263
+ chainId: session.chainId,
3264
+ host
3265
+ },
3266
+ operations,
3267
+ expiresAt,
3268
+ provenance: "primary"
3269
+ }]
3270
+ };
3271
+ }
3272
+ stageRestoredServiceGraph(input) {
3273
+ const graph = this.createServiceGraphLifetime();
3274
+ const core = this.auth ? new TinyCloud(this.auth, {
3275
+ invokeAny: graph.invokeAny,
3276
+ telemetry: this.config.telemetry
3277
+ }) : null;
3278
+ if (core) {
3279
+ core.initializeServices(graph.invoke, [input.host], graph.fetch);
3280
+ const coreContext = graph.track(core.serviceContext);
3281
+ coreContext.setSession(input.serviceSession);
3282
+ }
3283
+ const serviceContext = graph.track(new ServiceContext2({
3284
+ invoke: graph.invoke,
3285
+ invokeAny: graph.invokeAny,
3286
+ fetch: graph.fetch,
3287
+ hosts: [input.host],
3288
+ telemetry: this.config.telemetry
3289
+ }));
3290
+ const kv = new KVService2({});
3291
+ kv.initialize(serviceContext);
3292
+ serviceContext.registerService("kv", kv);
3293
+ const sql = new SQLService2({});
3294
+ sql.initialize(serviceContext);
3295
+ serviceContext.registerService("sql", sql);
3296
+ const duckdb = new DuckDbService2({});
3297
+ duckdb.initialize(serviceContext);
3298
+ serviceContext.registerService("duckdb", duckdb);
3299
+ const hooks = new HooksService2({});
3300
+ hooks.initialize(serviceContext);
3301
+ serviceContext.registerService("hooks", hooks);
3302
+ serviceContext.setSession(input.serviceSession);
3303
+ const encryption = this.createEncryptionService({
3304
+ graph,
3305
+ host: input.host,
3306
+ session: input.serviceSession,
3307
+ did: input.nodeDid,
3308
+ address: input.address,
3309
+ chainId: input.chainId
3310
+ });
3311
+ encryption.initialize(serviceContext);
3312
+ serviceContext.registerService("encryption", encryption);
3313
+ const vault = this.createVaultService(input.serviceSession.spaceId, kv, encryption, {
3314
+ host: input.host,
3315
+ did: input.nodeDid,
3316
+ address: input.address,
3317
+ chainId: input.chainId,
3318
+ serviceGraph: graph
3319
+ });
3320
+ vault.initialize(serviceContext);
3321
+ serviceContext.registerService("vault", vault);
3322
+ const capabilityRegistry = new CapabilityKeyRegistry();
3323
+ if (input.tinyCloudSession) {
3324
+ const session = input.tinyCloudSession;
3325
+ const delegations = input.recap.map((entry) => ({
3326
+ cid: session.delegationCid,
3327
+ delegateDID: session.verificationMethod,
3328
+ spaceId: entry.space,
3329
+ path: entry.path,
3330
+ actions: [...entry.actions],
3331
+ caveats: cloneRecapCaveats(entry.caveats),
3332
+ expiry: input.sessionExpiry,
3333
+ isRevoked: false,
3334
+ allowSubDelegation: true
3335
+ }));
3336
+ if (delegations.length > 0) {
3337
+ capabilityRegistry.registerKey({
3338
+ id: session.sessionKey,
3339
+ did: session.verificationMethod,
3340
+ type: "session",
3341
+ jwk: session.jwk,
3342
+ priority: 0
3343
+ }, delegations);
2883
3344
  }
2884
- this.registerPrimarySessionGrant(tcSession);
2885
3345
  }
3346
+ const delegationManager = new DelegationManager({
3347
+ hosts: [input.host],
3348
+ session: input.serviceSession,
3349
+ invoke: graph.invoke,
3350
+ invokeAny: graph.invokeAny,
3351
+ fetch: graph.fetch
3352
+ });
3353
+ const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
3354
+ const sharingService = new SharingService({
3355
+ hosts: [input.host],
3356
+ invoke: graph.invoke,
3357
+ fetch: graph.fetch,
3358
+ assertActive: () => graph.assertActive(),
3359
+ keyProvider,
3360
+ registry: capabilityRegistry,
3361
+ createDelegationWasm: (params) => this.createDelegationWrapper(params),
3362
+ computeCid: (data, codec) => {
3363
+ if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
3364
+ return this.wasmBindings.computeCid(data, codec);
3365
+ },
3366
+ createKVService: (config) => {
3367
+ const service = new KVService2({ prefix: config.pathPrefix?.replace(/\/$/, "") });
3368
+ const context = graph.track(new ServiceContext2({
3369
+ invoke: config.invoke,
3370
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
3371
+ hosts: config.hosts,
3372
+ telemetry: this.config.telemetry
3373
+ }));
3374
+ context.setSession(config.session);
3375
+ service.initialize(context);
3376
+ return service;
3377
+ }
3378
+ });
3379
+ sharingService.updateConfig({
3380
+ session: input.serviceSession,
3381
+ delegationManager,
3382
+ sessionExpiry: input.sessionExpiry,
3383
+ createDelegationWasm: (params) => {
3384
+ graph.assertActive();
3385
+ return this.createDelegationWrapper(params);
3386
+ },
3387
+ onRootDelegationNeeded: this.signer ? async (params) => {
3388
+ graph.assertActive();
3389
+ const delegation = await this.createRootDelegationForSharing(
3390
+ params,
3391
+ () => graph.assertActive()
3392
+ );
3393
+ graph.assertActive();
3394
+ return delegation;
3395
+ } : void 0
3396
+ });
3397
+ const spaceService = new SpaceService({
3398
+ hosts: [input.host],
3399
+ session: input.serviceSession,
3400
+ invoke: graph.invoke,
3401
+ fetch: graph.fetch,
3402
+ capabilityRegistry,
3403
+ userDid: input.nodeDid,
3404
+ createKVService: (spaceId) => {
3405
+ graph.assertActive();
3406
+ const scopedKv = new KVService2({});
3407
+ const context = graph.track(new ServiceContext2({
3408
+ invoke: graph.invoke,
3409
+ invokeAny: graph.invokeAny,
3410
+ fetch: graph.fetch,
3411
+ hosts: [input.host],
3412
+ telemetry: this.config.telemetry
3413
+ }));
3414
+ context.setSession({ ...input.serviceSession, spaceId });
3415
+ scopedKv.initialize(context);
3416
+ return scopedKv;
3417
+ },
3418
+ createVaultService: (spaceId) => {
3419
+ graph.assertActive();
3420
+ const scopedKv = new KVService2({});
3421
+ const context = graph.track(new ServiceContext2({
3422
+ invoke: graph.invoke,
3423
+ invokeAny: graph.invokeAny,
3424
+ fetch: graph.fetch,
3425
+ hosts: [input.host],
3426
+ telemetry: this.config.telemetry
3427
+ }));
3428
+ context.setSession({ ...input.serviceSession, spaceId });
3429
+ scopedKv.initialize(context);
3430
+ const scopedVault = this.createVaultService(spaceId, scopedKv, encryption, {
3431
+ host: input.host,
3432
+ did: input.nodeDid,
3433
+ address: input.address,
3434
+ chainId: input.chainId,
3435
+ serviceGraph: graph
3436
+ });
3437
+ scopedVault.initialize(context);
3438
+ return scopedVault;
3439
+ },
3440
+ createSecretsService: (spaceId) => {
3441
+ graph.assertActive();
3442
+ return this.secretsForSpace(spaceId);
3443
+ },
3444
+ createDelegation: async (params) => {
3445
+ graph.assertActive();
3446
+ try {
3447
+ const portableDelegation = await this.createDelegation({
3448
+ delegateDID: params.delegateDID,
3449
+ path: params.path,
3450
+ actions: params.actions,
3451
+ disableSubDelegation: params.disableSubDelegation,
3452
+ expiryMs: params.expiry ? params.expiry.getTime() - Date.now() : void 0
3453
+ });
3454
+ return { ok: true, data: {
3455
+ cid: portableDelegation.cid,
3456
+ delegateDID: portableDelegation.delegateDID,
3457
+ delegatorDID: this.did,
3458
+ spaceId: portableDelegation.spaceId,
3459
+ path: portableDelegation.path,
3460
+ actions: portableDelegation.actions,
3461
+ expiry: portableDelegation.expiry,
3462
+ isRevoked: false,
3463
+ allowSubDelegation: !portableDelegation.disableSubDelegation,
3464
+ createdAt: /* @__PURE__ */ new Date(),
3465
+ authHeader: portableDelegation.delegationHeader.Authorization
3466
+ } };
3467
+ } catch (error) {
3468
+ return { ok: false, error: {
3469
+ code: "CREATION_FAILED",
3470
+ message: error instanceof Error ? error.message : String(error),
3471
+ service: "delegation"
3472
+ } };
3473
+ }
3474
+ },
3475
+ onSpaceRegistered: async (space) => {
3476
+ graph.assertActive();
3477
+ await this.account.spaces.register(space);
3478
+ }
3479
+ });
3480
+ spaceService.updateConfig({ sharingService });
3481
+ return { core, graph, serviceContext, kv, sql, duckdb, hooks, vault, encryption, capabilityRegistry, keyProvider, sharingService, delegationManager, spaceService };
2886
3482
  }
2887
3483
  /**
2888
3484
  * Resolve the host a restored session should target.
@@ -3044,14 +3640,19 @@ var _TinyCloudNode = class _TinyCloudNode {
3044
3640
  if (!session) {
3045
3641
  return;
3046
3642
  }
3047
- this.tc.initializeServices(this.invokeWithRuntimePermissions, [this.config.host]);
3048
- this._serviceContext = new ServiceContext2({
3049
- invoke: this.invokeWithRuntimePermissions,
3050
- invokeAny: this.invokeAnyWithRuntimePermissions,
3051
- fetch: globalThis.fetch.bind(globalThis),
3643
+ this.tc.initializeServices(
3644
+ this._serviceGraph.invoke,
3645
+ [this.config.host],
3646
+ this._serviceGraph.fetch
3647
+ );
3648
+ this._serviceGraph.track(this.tc.serviceContext);
3649
+ this._serviceContext = this._serviceGraph.track(new ServiceContext2({
3650
+ invoke: this._serviceGraph.invoke,
3651
+ invokeAny: this._serviceGraph.invokeAny,
3652
+ fetch: this._serviceGraph.fetch,
3052
3653
  hosts: [this.config.host],
3053
3654
  telemetry: this.config.telemetry
3054
- });
3655
+ }));
3055
3656
  this._kv = new KVService2({});
3056
3657
  this._kv.initialize(this._serviceContext);
3057
3658
  this._serviceContext.registerService("kv", this._kv);
@@ -3086,12 +3687,12 @@ var _TinyCloudNode = class _TinyCloudNode {
3086
3687
  createSpaceScopedKVService(spaceId) {
3087
3688
  const kvService = new KVService2({});
3088
3689
  if (this._serviceContext) {
3089
- const spaceScopedContext = new ServiceContext2({
3690
+ const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
3090
3691
  invoke: this._serviceContext.invoke,
3091
3692
  fetch: this._serviceContext.fetch,
3092
3693
  hosts: this._serviceContext.hosts,
3093
3694
  telemetry: this.config.telemetry
3094
- });
3695
+ }));
3095
3696
  const session = this._serviceContext.session;
3096
3697
  if (session) {
3097
3698
  spaceScopedContext.setSession({ ...session, spaceId });
@@ -3124,6 +3725,18 @@ var _TinyCloudNode = class _TinyCloudNode {
3124
3725
  }
3125
3726
  return session;
3126
3727
  }
3728
+ /**
3729
+ * Runtime-permission helpers can be used from an authenticated Node session
3730
+ * before its public ServiceContext is initialized. They still need the
3731
+ * current signed session, never an optional or stale authority object.
3732
+ */
3733
+ requireEncryptionSession() {
3734
+ const session = this._serviceContext?.session ?? this.auth?.tinyCloudSession;
3735
+ if (!session) {
3736
+ throw new Error("Not signed in. Call signIn() first.");
3737
+ }
3738
+ return session;
3739
+ }
3127
3740
  createEncryptionCrypto() {
3128
3741
  const wasm = this.wasmBindings;
3129
3742
  const columnEncrypt = (key, plaintext) => {
@@ -3179,14 +3792,15 @@ var _TinyCloudNode = class _TinyCloudNode {
3179
3792
  }
3180
3793
  return info.nodeId;
3181
3794
  }
3182
- async signRawNetworkAuthorization(input) {
3795
+ async signRawNetworkAuthorization(input, binding) {
3796
+ binding?.graph.assertActive();
3183
3797
  if (!this.wasmBindings.invokeAny) {
3184
3798
  throw new Error("WASM binding does not support raw-resource invokeAny");
3185
3799
  }
3186
3800
  if (!this.wasmBindings.computeCid) {
3187
3801
  throw new Error("WASM binding does not support invocation CID computation");
3188
3802
  }
3189
- const session = this.requireServiceSession();
3803
+ const session = binding?.session ?? this.requireEncryptionSession();
3190
3804
  const headers = this.invokeAnyWithRuntimePermissions(
3191
3805
  session,
3192
3806
  [
@@ -3205,6 +3819,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3205
3819
  input.targetNode,
3206
3820
  session.jwk
3207
3821
  );
3822
+ binding?.graph.assertActive();
3208
3823
  return {
3209
3824
  authorization: audienceBound,
3210
3825
  invocationCid: this.wasmBindings.computeCid(
@@ -3213,25 +3828,55 @@ var _TinyCloudNode = class _TinyCloudNode {
3213
3828
  )
3214
3829
  };
3215
3830
  }
3216
- createEncryptionService() {
3831
+ async fetchEncryptionNetworkAt(host, networkId, fetchFn) {
3832
+ const response = await fetchFn(
3833
+ `${host}/encryption/networks/${encodeURIComponent(networkId)}`
3834
+ );
3835
+ if (response.status === 404) return null;
3836
+ if (!response.ok) {
3837
+ throw new Error(
3838
+ `Failed to fetch encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
3839
+ );
3840
+ }
3841
+ const body = await response.json();
3842
+ return "descriptor" in body && body.descriptor ? body.descriptor : body;
3843
+ }
3844
+ createEncryptionService(binding) {
3845
+ const graph = binding?.graph ?? this._serviceGraph;
3846
+ const host = binding?.host ?? this.config.host;
3847
+ const session = binding?.session ?? this.requireEncryptionSession();
3848
+ const did = binding?.did ?? this.did;
3849
+ const address = binding?.address ?? this._address;
3850
+ const chainId = binding?.chainId ?? this._chainId;
3217
3851
  const crypto = this.createEncryptionCrypto();
3218
3852
  const transport = {
3219
- postDecrypt: async ({ networkId, authorization, canonicalBody }) => {
3220
- const response = await fetch(
3221
- `${this.config.host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
3853
+ postDecrypt: async ({ networkId, authorization, canonicalBody, signal }) => {
3854
+ graph.assertActive();
3855
+ const response = await graph.fetch(
3856
+ `${host}/encryption/networks/${encodeURIComponent(networkId)}/decrypt`,
3222
3857
  {
3223
3858
  method: "POST",
3224
3859
  headers: {
3225
3860
  Authorization: authorization,
3226
3861
  "Content-Type": "application/json"
3227
3862
  },
3228
- body: canonicalBody
3863
+ body: canonicalBody,
3864
+ signal
3229
3865
  }
3230
3866
  );
3867
+ graph.assertActive();
3231
3868
  if (!response.ok) {
3232
- throw new Error(
3233
- `decrypt failed ${response.status}: ${await response.text()}`
3234
- );
3869
+ let permissionHint;
3870
+ if (response.status === 401 || response.status === 403) {
3871
+ try {
3872
+ const body = await response.json();
3873
+ const record = typeof body === "object" && body !== null ? body : void 0;
3874
+ const nested = typeof record?.error === "object" && record.error !== null ? record.error : void 0;
3875
+ permissionHint = parsePermissionHint(record?.permissionHint) ?? parsePermissionHint(nested?.permissionHint);
3876
+ } catch {
3877
+ }
3878
+ }
3879
+ throw new DecryptTransportResponseError(response.status, permissionHint);
3235
3880
  }
3236
3881
  return await response.json();
3237
3882
  }
@@ -3240,12 +3885,14 @@ var _TinyCloudNode = class _TinyCloudNode {
3240
3885
  crypto,
3241
3886
  signer: {
3242
3887
  signDecryptInvocation: async (input) => {
3888
+ graph.assertActive();
3243
3889
  const signed = await this.signRawNetworkAuthorization({
3244
3890
  targetNode: input.targetNode,
3245
3891
  networkId: input.networkId,
3246
3892
  action: DECRYPT_ACTION2,
3247
3893
  facts: input.facts
3248
- });
3894
+ }, { graph, session });
3895
+ graph.assertActive();
3249
3896
  return {
3250
3897
  ...signed,
3251
3898
  canonicalBody: canonicalizeEncryptionJson(
@@ -3256,25 +3903,35 @@ var _TinyCloudNode = class _TinyCloudNode {
3256
3903
  },
3257
3904
  transport,
3258
3905
  node: {
3259
- fetchByNetworkId: (networkId) => this.getEncryptionNetwork(networkId)
3906
+ fetchByNetworkId: async (networkId) => {
3907
+ graph.assertActive();
3908
+ const descriptor = await this.fetchEncryptionNetworkAt(host, networkId, graph.fetch);
3909
+ graph.assertActive();
3910
+ return descriptor;
3911
+ }
3260
3912
  },
3261
3913
  wellKnown: {
3262
3914
  fetchWellKnown: async (principal, discoveryKey) => {
3263
- if (!this._address || !didPrincipalMatches2(principal, this.did)) {
3915
+ graph.assertActive();
3916
+ if (!address || !didPrincipalMatches2(principal, did)) {
3264
3917
  return null;
3265
3918
  }
3266
- if (!this.config.host) {
3267
- return null;
3268
- }
3269
- const publicSpaceId = makePublicSpaceId(this._address, this._chainId);
3270
- const result = await TinyCloud.readPublicSpace(this.config.host, publicSpaceId, discoveryKey);
3271
- if (!result.ok) {
3919
+ const publicSpaceId = makePublicSpaceId(address, chainId);
3920
+ const encodedKey = discoveryKey.split("/").map(encodeURIComponent).join("/");
3921
+ const response = await graph.fetch(
3922
+ `${host}/public/${encodeURIComponent(publicSpaceId)}/kv/${encodedKey}`,
3923
+ { method: "GET" }
3924
+ );
3925
+ graph.assertActive();
3926
+ if (!response.ok) {
3272
3927
  return null;
3273
3928
  }
3274
- const body = result.data;
3929
+ const body = await response.json();
3930
+ graph.assertActive();
3275
3931
  return "descriptor" in body && body.descriptor ? body.descriptor : body;
3276
3932
  }
3277
- }
3933
+ },
3934
+ assertActive: () => graph.assertActive()
3278
3935
  });
3279
3936
  }
3280
3937
  getEncryptionService() {
@@ -3288,8 +3945,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3288
3945
  }
3289
3946
  return this._encryption;
3290
3947
  }
3291
- createVaultService(spaceId, kv) {
3948
+ createVaultService(spaceId, kv, encryptionService = this.getEncryptionService(), nodeContext) {
3292
3949
  const wasm = this.wasmBindings;
3950
+ const serviceGraph = nodeContext?.serviceGraph ?? this._serviceGraph;
3293
3951
  const vaultCrypto = createVaultCrypto({
3294
3952
  vault_encrypt: wasm.vault_encrypt,
3295
3953
  vault_decrypt: wasm.vault_decrypt,
@@ -3300,12 +3958,17 @@ var _TinyCloudNode = class _TinyCloudNode {
3300
3958
  vault_sha256: wasm.vault_sha256
3301
3959
  });
3302
3960
  const self = this;
3961
+ const did = nodeContext?.did ?? this.did;
3962
+ const address = nodeContext?.address ?? this._address;
3963
+ const chainId = nodeContext?.chainId ?? this._chainId;
3964
+ const host = nodeContext?.host ?? this.config.host;
3965
+ const ownerDid = this.ownerDidFromSpaceId(spaceId) ?? did;
3303
3966
  return new DataVaultService({
3304
3967
  spaceId,
3305
3968
  crypto: vaultCrypto,
3306
3969
  encryption: {
3307
- networkId: this.getEncryptionNetworkIdForSpace(spaceId),
3308
- service: this.getEncryptionService(),
3970
+ networkId: `urn:tinycloud:encryption:${ownerDid}:${DEFAULT_ENCRYPTION_NETWORK_NAME}`,
3971
+ service: encryptionService,
3309
3972
  decryptCapabilityProof: () => ({
3310
3973
  proofs: [this.requireServiceSession().delegationCid]
3311
3974
  })
@@ -3314,6 +3977,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3314
3977
  kv,
3315
3978
  ensurePublicSpace: async () => {
3316
3979
  try {
3980
+ serviceGraph.assertActive();
3317
3981
  await self.ensurePublicSpace();
3318
3982
  return { ok: true, data: void 0 };
3319
3983
  } catch (error) {
@@ -3321,14 +3985,15 @@ var _TinyCloudNode = class _TinyCloudNode {
3321
3985
  }
3322
3986
  },
3323
3987
  get publicKV() {
3988
+ serviceGraph.assertActive();
3324
3989
  return self._publicKV ?? self.tc.publicKV;
3325
3990
  },
3326
- readPublicSpace: (host, targetSpaceId, key) => TinyCloud.readPublicSpace(host, targetSpaceId, key),
3991
+ readPublicSpace: (host2, targetSpaceId, key) => TinyCloud.readPublicSpace(host2, targetSpaceId, key),
3327
3992
  makePublicSpaceId: TinyCloud.makePublicSpaceId,
3328
- did: this.did,
3329
- address: this._address ?? "",
3330
- chainId: this._chainId,
3331
- hosts: [this.config.host]
3993
+ did,
3994
+ address: address ?? "",
3995
+ chainId,
3996
+ hosts: [host]
3332
3997
  }
3333
3998
  });
3334
3999
  }
@@ -3337,6 +4002,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3337
4002
  * @internal
3338
4003
  */
3339
4004
  initializeV2Services(serviceSession) {
4005
+ const graph = this._serviceGraph;
3340
4006
  this._capabilityRegistry = new CapabilityKeyRegistry();
3341
4007
  const tcSession = this.auth?.tinyCloudSession;
3342
4008
  if (tcSession && this._address) {
@@ -3381,21 +4047,23 @@ var _TinyCloudNode = class _TinyCloudNode {
3381
4047
  hosts: [this.config.host],
3382
4048
  accountSpaceId: this.accountSpaceId,
3383
4049
  session: serviceSession,
3384
- invoke: this.invokeWithRuntimePermissions,
3385
- invokeAny: this.invokeAnyWithRuntimePermissions,
3386
- fetch: globalThis.fetch.bind(globalThis)
4050
+ invoke: graph.invoke,
4051
+ invokeAny: graph.invokeAny,
4052
+ fetch: graph.fetch
3387
4053
  });
3388
4054
  this._spaceService = new SpaceService({
3389
4055
  hosts: [this.config.host],
3390
4056
  session: serviceSession,
3391
- invoke: this.wasmBindings.invoke,
3392
- fetch: globalThis.fetch.bind(globalThis),
4057
+ invoke: graph.invoke,
4058
+ fetch: graph.fetch,
3393
4059
  capabilityRegistry: this._capabilityRegistry,
3394
4060
  userDid: this.did,
3395
4061
  createKVService: (spaceId) => {
4062
+ graph.assertActive();
3396
4063
  return this.createSpaceScopedKVService(spaceId);
3397
4064
  },
3398
4065
  createVaultService: (spaceId) => {
4066
+ graph.assertActive();
3399
4067
  const kvService = this.createSpaceScopedKVService(spaceId);
3400
4068
  const vaultService = this.createVaultService(spaceId, kvService);
3401
4069
  if (this._serviceContext) {
@@ -3404,11 +4072,13 @@ var _TinyCloudNode = class _TinyCloudNode {
3404
4072
  return vaultService;
3405
4073
  },
3406
4074
  createSecretsService: (spaceId) => {
4075
+ graph.assertActive();
3407
4076
  return this.secretsForSpace(spaceId);
3408
4077
  },
3409
4078
  // Enable space.delegations.create() via SIWE-based delegation
3410
4079
  createDelegation: async (params) => {
3411
4080
  try {
4081
+ graph.assertActive();
3412
4082
  const portableDelegation = await this.createDelegation({
3413
4083
  delegateDID: params.delegateDID,
3414
4084
  path: params.path,
@@ -3442,18 +4112,50 @@ var _TinyCloudNode = class _TinyCloudNode {
3442
4112
  }
3443
4113
  },
3444
4114
  onSpaceRegistered: async (space) => {
4115
+ graph.assertActive();
3445
4116
  await this.account.spaces.register(space);
3446
4117
  }
3447
4118
  });
3448
- this._sharingService.updateConfig({
4119
+ this._sharingService = new SharingService({
4120
+ hosts: [this.config.host],
3449
4121
  session: serviceSession,
4122
+ invoke: graph.invoke,
4123
+ fetch: graph.fetch,
4124
+ assertActive: () => graph.assertActive(),
4125
+ keyProvider: this._keyProvider,
4126
+ registry: this._capabilityRegistry,
3450
4127
  delegationManager: this._delegationManager,
3451
4128
  sessionExpiry: this.getSessionExpiry(),
3452
- // WASM-based delegation creation (preferred - no server roundtrip)
3453
- createDelegationWasm: (params) => this.createDelegationWrapper(params),
3454
- // Root delegation for long-lived share links (bypasses session expiry)
3455
- // In node-sdk we have direct signer access, so no popup needed
3456
- onRootDelegationNeeded: this.signer ? async (params) => this.createRootDelegationForSharing(params) : void 0
4129
+ createDelegationWasm: (params) => {
4130
+ graph.assertActive();
4131
+ return this.createDelegationWrapper(params);
4132
+ },
4133
+ computeCid: (data, codec) => {
4134
+ if (!this.wasmBindings.computeCid) throw new Error("computeCid is unavailable");
4135
+ return this.wasmBindings.computeCid(data, codec);
4136
+ },
4137
+ createKVService: (config) => {
4138
+ graph.assertActive();
4139
+ const service = new KVService2({ prefix: config.pathPrefix?.replace(/\/$/, "") });
4140
+ const context = graph.track(new ServiceContext2({
4141
+ invoke: config.invoke,
4142
+ fetch: config.fetch ?? graph.fetch,
4143
+ hosts: config.hosts,
4144
+ telemetry: this.config.telemetry
4145
+ }));
4146
+ context.setSession(config.session);
4147
+ service.initialize(context);
4148
+ return service;
4149
+ },
4150
+ onRootDelegationNeeded: this.signer ? async (params) => {
4151
+ graph.assertActive();
4152
+ const delegation = await this.createRootDelegationForSharing(
4153
+ params,
4154
+ () => graph.assertActive()
4155
+ );
4156
+ graph.assertActive();
4157
+ return delegation;
4158
+ } : void 0
3457
4159
  });
3458
4160
  this._spaceService.updateConfig({
3459
4161
  sharingService: this._sharingService
@@ -3520,7 +4222,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3520
4222
  * with expiry longer than the current session.
3521
4223
  * @internal
3522
4224
  */
3523
- async createOwnerDelegation(params) {
4225
+ async createOwnerDelegation(params, assertActive) {
4226
+ const assertOwnerGraphActive = assertActive ?? this._serviceGraph.assertActive.bind(this._serviceGraph);
4227
+ assertOwnerGraphActive();
3524
4228
  if (!params.delegateDid.startsWith("did:key:") || params.actions.length === 0 || params.path.length === 0) {
3525
4229
  throw new Error("Owner delegation requires an external did:key audience and bounded capabilities");
3526
4230
  }
@@ -3545,8 +4249,10 @@ var _TinyCloudNode = class _TinyCloudNode {
3545
4249
  delegateUri: params.delegateDid
3546
4250
  });
3547
4251
  const signature = await this.signer.signMessage(prepared.siwe);
4252
+ assertOwnerGraphActive();
3548
4253
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature });
3549
4254
  const activation = await activateSessionWithHost2(host, delegationSession.delegationHeader);
4255
+ assertOwnerGraphActive();
3550
4256
  if (!activation.success) {
3551
4257
  throw new Error(`Owner delegation import failed: ${activation.status} ${activation.error ?? ""}`.trim());
3552
4258
  }
@@ -3574,7 +4280,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3574
4280
  }
3575
4281
  };
3576
4282
  }
3577
- async createRootDelegationForSharing(params) {
4283
+ async createRootDelegationForSharing(params, assertActive) {
3578
4284
  try {
3579
4285
  return (await this.createOwnerDelegation({
3580
4286
  delegateDid: params.shareKeyDID,
@@ -3582,8 +4288,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3582
4288
  path: params.path,
3583
4289
  actions: params.actions,
3584
4290
  expiresAt: params.requestedExpiry
3585
- })).delegation;
4291
+ }, assertActive)).delegation;
3586
4292
  } catch {
4293
+ assertActive?.();
3587
4294
  return void 0;
3588
4295
  }
3589
4296
  }
@@ -3655,13 +4362,13 @@ var _TinyCloudNode = class _TinyCloudNode {
3655
4362
  throw new Error("Not signed in. Call signIn() first.");
3656
4363
  }
3657
4364
  const sql = new SQLService2({});
3658
- const spaceScopedContext = new ServiceContext2({
4365
+ const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
3659
4366
  invoke: this._serviceContext.invoke,
3660
4367
  invokeAny: this._serviceContext.invokeAny,
3661
4368
  fetch: this._serviceContext.fetch,
3662
4369
  hosts: this._serviceContext.hosts,
3663
4370
  telemetry: this.config.telemetry
3664
- });
4371
+ }));
3665
4372
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
3666
4373
  sql.initialize(spaceScopedContext);
3667
4374
  return sql;
@@ -3685,12 +4392,12 @@ var _TinyCloudNode = class _TinyCloudNode {
3685
4392
  throw new Error("Not signed in. Call signIn() first.");
3686
4393
  }
3687
4394
  const kv = new KVService2({});
3688
- const spaceScopedContext = new ServiceContext2({
4395
+ const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
3689
4396
  invoke: this._serviceContext.invoke,
3690
4397
  invokeAny: this._serviceContext.invokeAny,
3691
4398
  fetch: this._serviceContext.fetch,
3692
4399
  hosts: this._serviceContext.hosts
3693
- });
4400
+ }));
3694
4401
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
3695
4402
  kv.initialize(spaceScopedContext);
3696
4403
  return kv;
@@ -3726,19 +4433,11 @@ var _TinyCloudNode = class _TinyCloudNode {
3726
4433
  }
3727
4434
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
3728
4435
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
3729
- const response = await fetch(
3730
- `${this.config.host}/encryption/networks/${encodeURIComponent(networkId)}`
4436
+ return this.fetchEncryptionNetworkAt(
4437
+ this.config.host,
4438
+ networkId,
4439
+ globalThis.fetch.bind(globalThis)
3731
4440
  );
3732
- if (response.status === 404) {
3733
- return null;
3734
- }
3735
- if (!response.ok) {
3736
- throw new Error(
3737
- `Failed to fetch encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
3738
- );
3739
- }
3740
- const body = await response.json();
3741
- return "descriptor" in body && body.descriptor ? body.descriptor : body;
3742
4441
  }
3743
4442
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
3744
4443
  const targetNode = await this.fetchNodeId();
@@ -3832,6 +4531,38 @@ var _TinyCloudNode = class _TinyCloudNode {
3832
4531
  }
3833
4532
  return secrets;
3834
4533
  }
4534
+ /**
4535
+ * Read a secret from an explicit target space without requesting or
4536
+ * auto-signing authority. Active runtime delegations are selected by the
4537
+ * normal KV and encryption invocation paths.
4538
+ */
4539
+ async readSecret(input) {
4540
+ const resolved = resolveSecretPath2(input.name, { scope: input.scope });
4541
+ const targetSpace = input.space.startsWith("tinycloud:") ? input.space : this.ownedSpaceId(input.space);
4542
+ const result = await this.getBaseVault(targetSpace).readNetworkEncrypted(
4543
+ resolved.vaultKey
4544
+ );
4545
+ if (result.status !== "ok") {
4546
+ if (result.status === "permission_required") {
4547
+ const hint = parsePermissionHint(result.hint);
4548
+ if (hint === void 0) return { status: "read_failed" };
4549
+ return {
4550
+ status: "permission_required",
4551
+ hint: {
4552
+ service: hint.service,
4553
+ ...hint.space === void 0 ? {} : { space: hint.space },
4554
+ path: hint.path,
4555
+ actions: [...hint.actions]
4556
+ }
4557
+ };
4558
+ }
4559
+ return { status: result.status };
4560
+ }
4561
+ if (!isSecretPayload(result.entry.value)) {
4562
+ return { status: "invalid_payload" };
4563
+ }
4564
+ return { status: "ok", value: result.entry.value.value };
4565
+ }
3835
4566
  getBaseSecrets(spaceId) {
3836
4567
  if (!this._spaceService) {
3837
4568
  throw new Error("Not signed in. Call signIn() first.");
@@ -3839,16 +4570,29 @@ var _TinyCloudNode = class _TinyCloudNode {
3839
4570
  const resolvedSpace = spaceId.startsWith("tinycloud:") ? spaceId : this.ownedSpaceId(spaceId);
3840
4571
  let secrets = this._baseSecrets.get(resolvedSpace);
3841
4572
  if (!secrets) {
3842
- const kvService = this.createSpaceScopedKVService(resolvedSpace);
3843
- const vaultService = this.createVaultService(resolvedSpace, kvService);
3844
- if (this._serviceContext) {
3845
- vaultService.initialize(this._serviceContext);
3846
- }
3847
- secrets = new SecretsService(() => vaultService);
4573
+ secrets = new SecretsService(() => this.getBaseVault(resolvedSpace));
3848
4574
  this._baseSecrets.set(resolvedSpace, secrets);
3849
4575
  }
3850
4576
  return secrets;
3851
4577
  }
4578
+ getBaseVault(spaceId) {
4579
+ if (!this._spaceService) {
4580
+ throw new Error("Not signed in. Call signIn() first.");
4581
+ }
4582
+ const resolvedSpace = spaceId.startsWith("tinycloud:") ? spaceId : this.ownedSpaceId(spaceId);
4583
+ let vault = this._baseVaults.get(resolvedSpace);
4584
+ if (!vault) {
4585
+ vault = this.createVaultService(
4586
+ resolvedSpace,
4587
+ this.createSpaceScopedKVService(resolvedSpace)
4588
+ );
4589
+ if (this._serviceContext) {
4590
+ vault.initialize(this._serviceContext);
4591
+ }
4592
+ this._baseVaults.set(resolvedSpace, vault);
4593
+ }
4594
+ return vault;
4595
+ }
3852
4596
  /**
3853
4597
  * Hooks write stream subscription API.
3854
4598
  */
@@ -3953,6 +4697,63 @@ var _TinyCloudNode = class _TinyCloudNode {
3953
4697
  (grant) => grant.delegation
3954
4698
  );
3955
4699
  }
4700
+ /**
4701
+ * Return the effective capabilities of installed runtime grants.
4702
+ *
4703
+ * The result is projected from the SDK's activated, expiry-pruned grant
4704
+ * operations rather than from PortableDelegation transport metadata. Base
4705
+ * session authority is intentionally omitted; callers that need the full
4706
+ * live authority must combine this with {@link getVerifiedSessionCapabilities}.
4707
+ */
4708
+ getEffectiveRuntimePermissionEntries() {
4709
+ const session = this.currentTinyCloudSession();
4710
+ if (!session) return [];
4711
+ this.pruneExpiredRuntimePermissionGrants();
4712
+ const grouped = /* @__PURE__ */ new Map();
4713
+ for (const grant of this.runtimePermissionGrants) {
4714
+ if (grant.provenance === "primary") continue;
4715
+ for (const operation of grant.operations) {
4716
+ const service = SERVICE_SHORT_TO_LONG[operation.service];
4717
+ if (service === void 0 || typeof operation.path !== "string" || typeof operation.action !== "string") {
4718
+ continue;
4719
+ }
4720
+ let space;
4721
+ if (service !== "tinycloud.encryption") {
4722
+ if (typeof operation.spaceId !== "string") continue;
4723
+ try {
4724
+ space = this.resolvePermissionSpace(operation.spaceId, session);
4725
+ } catch {
4726
+ continue;
4727
+ }
4728
+ }
4729
+ const caveats = operation.caveats === void 0 || operation.caveats.length === 0 ? void 0 : cloneRecapCaveats(operation.caveats);
4730
+ const identity = JSON.stringify({
4731
+ service,
4732
+ ...space === void 0 ? {} : { space },
4733
+ path: operation.path,
4734
+ caveats: canonicalizeRecapCaveats(caveats)
4735
+ });
4736
+ const existing = grouped.get(identity);
4737
+ if (existing === void 0) {
4738
+ grouped.set(identity, {
4739
+ service,
4740
+ ...space === void 0 ? {} : { space },
4741
+ path: operation.path,
4742
+ actions: [operation.action],
4743
+ ...caveats === void 0 ? {} : { caveats }
4744
+ });
4745
+ continue;
4746
+ }
4747
+ if (!existing.actions.includes(operation.action)) {
4748
+ existing.actions.push(operation.action);
4749
+ existing.actions.sort();
4750
+ }
4751
+ }
4752
+ }
4753
+ return [...grouped.values()].sort(
4754
+ (left, right) => left.service.localeCompare(right.service) || (left.space ?? "").localeCompare(right.space ?? "") || left.path.localeCompare(right.path) || left.actions.join("\0").localeCompare(right.actions.join("\0"))
4755
+ );
4756
+ }
3956
4757
  /**
3957
4758
  * Install a portable runtime permission delegation into this SDK instance so
3958
4759
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -4021,6 +4822,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4021
4822
  "grantRuntimePermissions requires wallet mode with a signer or privateKey."
4022
4823
  );
4023
4824
  }
4825
+ this.assertDelegationCaveatsPreservable(expanded);
4024
4826
  const rawEntries = expanded.filter(
4025
4827
  (entry) => this.isEncryptionPermissionEntry(entry)
4026
4828
  );
@@ -4282,12 +5084,12 @@ var _TinyCloudNode = class _TinyCloudNode {
4282
5084
  }
4283
5085
  if (this._serviceContext) {
4284
5086
  const publicKV = new KVService2({ prefix: "" });
4285
- const publicContext = new ServiceContext2({
4286
- invoke: this.invokeWithRuntimePermissions,
4287
- fetch: this._serviceContext.fetch,
5087
+ const publicContext = this._serviceGraph.track(new ServiceContext2({
5088
+ invoke: this._serviceGraph.invoke,
5089
+ fetch: this._serviceGraph.fetch,
4288
5090
  hosts: this._serviceContext.hosts,
4289
5091
  telemetry: this.config.telemetry
4290
- });
5092
+ }));
4291
5093
  publicContext.setSession({
4292
5094
  delegationHeader: delegationSession.delegationHeader,
4293
5095
  delegationCid: delegationSession.delegationCid,
@@ -4354,13 +5156,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4354
5156
  async getDelegationStatus(cid) {
4355
5157
  return this.delegationManager.status(cid);
4356
5158
  }
4357
- /** Compute the canonical CID of a compact delegation authorization. */
5159
+ /** Compute the canonical CID of a compact UCAN or DAG-CBOR delegation authorization. */
4358
5160
  computeDelegationCid(authorization) {
4359
5161
  if (!authorization || !this.wasmBindings.computeCid) {
4360
5162
  throw new Error("Delegation CID computation is unavailable");
4361
5163
  }
5164
+ const compact = authorization.replace(/^Bearer /i, "");
4362
5165
  return this.wasmBindings.computeCid(
4363
- new TextEncoder().encode(authorization),
5166
+ compact.includes(".") ? new TextEncoder().encode(compact) : decodeAuthorizationBytes(authorization),
4364
5167
  0x55n
4365
5168
  );
4366
5169
  }
@@ -4452,6 +5255,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4452
5255
  "delegateTo with forceWalletSign=true supports at most one PermissionEntry. Multi-entry requests must go through the session-key UCAN path (drop forceWalletSign) or the legacy createDelegation method."
4453
5256
  );
4454
5257
  }
5258
+ this.assertDelegationCaveatsPreservable(expandedEntries);
4455
5259
  const delegation2 = await this.createDelegationLegacyWalletPath(
4456
5260
  did,
4457
5261
  expandedEntries[0],
@@ -4460,13 +5264,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4460
5264
  return { delegation: delegation2, prompted: true };
4461
5265
  }
4462
5266
  const granted = parseRecapCapabilities(
4463
- (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
5267
+ (siwe) => this.parseRecapWithCaveats(siwe),
4464
5268
  session.siwe
4465
5269
  );
4466
5270
  const { subset, missing } = isCapabilitySubset(expandedEntries, granted);
4467
5271
  if (!subset) {
5272
+ const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
4468
5273
  const runtimeGrant = this.findGrantForOperations(
4469
- this.permissionEntriesToOperations(expandedEntries, session),
5274
+ runtimeOperations,
4470
5275
  { excludePrimary: true }
4471
5276
  );
4472
5277
  if (runtimeGrant) {
@@ -4479,7 +5284,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4479
5284
  did,
4480
5285
  expandedEntries,
4481
5286
  runtimeExpiration,
4482
- runtimeGrant
5287
+ runtimeGrant,
5288
+ runtimeOperations
4483
5289
  );
4484
5290
  return { delegation: delegation2, prompted: false };
4485
5291
  }
@@ -4549,6 +5355,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4549
5355
  "createDelegationViaWasmPath requires a non-empty entries array"
4550
5356
  );
4551
5357
  }
5358
+ this.assertDelegationCaveatsPreservable(entries);
4552
5359
  const resolvedSpaces = /* @__PURE__ */ new Set();
4553
5360
  for (const entry of entries) {
4554
5361
  if (this.isEncryptionPermissionEntry(entry)) {
@@ -4629,7 +5436,9 @@ var _TinyCloudNode = class _TinyCloudNode {
4629
5436
  host: this.config.host
4630
5437
  };
4631
5438
  }
4632
- async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant) {
5439
+ async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations) {
5440
+ this.assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant);
5441
+ this.assertDelegationCaveatsPreservable(entries);
4633
5442
  const result = this.createDelegationWrapper({
4634
5443
  session: grant.session,
4635
5444
  delegateDID: did,
@@ -4683,6 +5492,74 @@ var _TinyCloudNode = class _TinyCloudNode {
4683
5492
  expandPermissionEntries(permissions) {
4684
5493
  return expandPermissionEntriesCore(permissions);
4685
5494
  }
5495
+ /**
5496
+ * The current WASM child-delegation APIs accept action-only maps. Refuse any
5497
+ * constrained branch rather than signing a broader action-only child UCAN.
5498
+ */
5499
+ assertDelegationCaveatsPreservable(entries) {
5500
+ const caveated = entries.filter(
5501
+ (entry) => !recapCaveatsEqual(entry.caveats, void 0)
5502
+ );
5503
+ if (caveated.length > 0) {
5504
+ throw new CaveatedDelegationUnsupportedError(caveated);
5505
+ }
5506
+ }
5507
+ /**
5508
+ * Runtime grant selection intentionally allows an uncaveated operation to
5509
+ * select a caveated grant so invocation can restore the signed caveat before
5510
+ * crossing the caveat-aware WASM boundary. Child delegation cannot do that:
5511
+ * it only accepts action maps, so reject the selected constrained branch.
5512
+ */
5513
+ assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant) {
5514
+ let operationIndex = 0;
5515
+ const caveated = [];
5516
+ for (const entry of entries) {
5517
+ for (const action of entry.actions) {
5518
+ const requested = requestedOperations[operationIndex++];
5519
+ if (!requested) continue;
5520
+ const constrained = grant.operations.find(
5521
+ (granted) => this.operationCovers(granted, requested) && !recapCaveatsEqual(granted.caveats, void 0)
5522
+ );
5523
+ if (constrained) {
5524
+ caveated.push({
5525
+ ...entry,
5526
+ actions: [action],
5527
+ caveats: cloneRecapCaveats(constrained.caveats)
5528
+ });
5529
+ }
5530
+ }
5531
+ }
5532
+ if (caveated.length > 0) {
5533
+ throw new CaveatedDelegationUnsupportedError(caveated);
5534
+ }
5535
+ }
5536
+ /** Reject caveated parent branches before action-only child signing. */
5537
+ assertPortableDelegationCaveatsPreservable(delegation) {
5538
+ const entries = [];
5539
+ const add = (service, space, path, actions, caveats) => {
5540
+ if (recapCaveatsEqual(caveats, void 0)) return;
5541
+ entries.push({
5542
+ service: service.startsWith("tinycloud.") ? service : `tinycloud.${service}`,
5543
+ space,
5544
+ path,
5545
+ actions: [...actions],
5546
+ caveats: cloneRecapCaveats(caveats)
5547
+ });
5548
+ };
5549
+ add(
5550
+ delegation.actions[0]?.split("/", 1)[0] ?? "tinycloud.unknown",
5551
+ delegation.spaceId,
5552
+ delegation.path,
5553
+ delegation.actions,
5554
+ delegation.caveats
5555
+ );
5556
+ for (const resource of delegation.resources ?? []) {
5557
+ add(resource.service, resource.space, resource.path, resource.actions, resource.caveats);
5558
+ }
5559
+ if (entries.length > 0) {
5560
+ throw new CaveatedDelegationUnsupportedError(entries);
5561
+ }
5562
+ }
4686
5563
  shortServiceName(service) {
4687
5564
  const short = SERVICE_LONG_TO_SHORT[service];
4688
5565
  if (short === void 0) {
@@ -4737,14 +5614,15 @@ var _TinyCloudNode = class _TinyCloudNode {
4737
5614
  ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId },
4738
5615
  service,
4739
5616
  path: entry.path,
4740
- action
5617
+ action,
5618
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
4741
5619
  }));
4742
5620
  });
4743
5621
  }
4744
5622
  sessionCoversPermissionEntries(session, entries) {
4745
5623
  try {
4746
5624
  const granted = parseRecapCapabilities(
4747
- (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
5625
+ (siwe) => this.parseRecapWithCaveats(siwe),
4748
5626
  session.siwe
4749
5627
  );
4750
5628
  return isCapabilitySubset(entries, granted).subset;
@@ -4760,7 +5638,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4760
5638
  ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId },
4761
5639
  service,
4762
5640
  path: entry.path,
4763
- action
5641
+ action,
5642
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
4764
5643
  }));
4765
5644
  });
4766
5645
  }
@@ -4836,7 +5715,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4836
5715
  service: this.shortServiceName(entry.service),
4837
5716
  space: this.isEncryptionPermissionEntry(entry) ? "encryption" : spaceId,
4838
5717
  path: entry.path,
4839
- actions: [...entry.actions]
5718
+ actions: [...entry.actions],
5719
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
4840
5720
  }));
4841
5721
  }
4842
5722
  operationsFromDelegation(delegation) {
@@ -4847,7 +5727,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4847
5727
  ...this.isEncryptionNetworkOperation(service, resource.path) ? { resource: resource.path } : { spaceId: resource.space },
4848
5728
  service,
4849
5729
  path: resource.path,
4850
- action
5730
+ action,
5731
+ caveats: cloneRecapCaveats(resource.caveats)
4851
5732
  }));
4852
5733
  });
4853
5734
  }
@@ -4863,7 +5744,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4863
5744
  service,
4864
5745
  space: delegation.spaceId,
4865
5746
  path: delegation.path,
4866
- actions
5747
+ actions,
5748
+ ...delegation.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(delegation.caveats) }
4867
5749
  }));
4868
5750
  }
4869
5751
  /**
@@ -4964,6 +5846,9 @@ var _TinyCloudNode = class _TinyCloudNode {
4964
5846
  if (granted.service !== requested.service || !this.actionContains(granted.action, requested.action)) {
4965
5847
  return false;
4966
5848
  }
5849
+ if (requested.caveats !== void 0 && !recapCaveatsEqual(granted.caveats, requested.caveats)) {
5850
+ return false;
5851
+ }
4967
5852
  if (granted.resource !== void 0 || requested.resource !== void 0) {
4968
5853
  return granted.resource !== void 0 && requested.resource !== void 0 && granted.resource === requested.resource && this.pathContains(granted.path, requested.path);
4969
5854
  }
@@ -4998,6 +5883,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4998
5883
  invocationServiceName(service) {
4999
5884
  return service.startsWith("tinycloud.") ? this.shortServiceName(service) : service;
5000
5885
  }
5886
+ /** Prefer the v2 caveat-preserving parser while retaining old custom WASM. */
5887
+ parseRecapWithCaveats(siwe) {
5888
+ return this.wasmBindings.parseVerifiedRecapFromSiwe?.(siwe) ?? this.wasmBindings.parseRecapFromSiwe(siwe);
5889
+ }
5001
5890
  isEncryptionNetworkOperation(service, path) {
5002
5891
  return service === "encryption" && path.startsWith("urn:tinycloud:encryption:");
5003
5892
  }
@@ -5008,7 +5897,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5008
5897
  resource: entry.resource,
5009
5898
  service,
5010
5899
  path: entry.path,
5011
- action: entry.action
5900
+ action: entry.action,
5901
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
5012
5902
  };
5013
5903
  }
5014
5904
  if (this.isEncryptionNetworkOperation(service, entry.path)) {
@@ -5016,7 +5906,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5016
5906
  resource: entry.path,
5017
5907
  service,
5018
5908
  path: entry.path,
5019
- action: entry.action
5909
+ action: entry.action,
5910
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
5020
5911
  };
5021
5912
  }
5022
5913
  if (typeof entry.spaceId === "string") {
@@ -5024,7 +5915,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5024
5915
  spaceId: entry.spaceId,
5025
5916
  service,
5026
5917
  path: entry.path,
5027
- action: entry.action
5918
+ action: entry.action,
5919
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
5028
5920
  };
5029
5921
  }
5030
5922
  return void 0;
@@ -5364,6 +6256,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5364
6256
  * @returns A portable delegation for the sub-delegate
5365
6257
  */
5366
6258
  async createSubDelegation(parentDelegation, params) {
6259
+ this.assertPortableDelegationCaveatsPreservable(parentDelegation);
5367
6260
  if (!this.signer) {
5368
6261
  throw new Error("Cannot createSubDelegation() in session-only mode. Requires wallet mode.");
5369
6262
  }
@@ -5462,6 +6355,7 @@ import {
5462
6355
  DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
5463
6356
  DEFAULT_MANIFEST_VERSION,
5464
6357
  VAULT_PERMISSION_SERVICE,
6358
+ CaveatedDelegationUnsupportedError as CaveatedDelegationUnsupportedError2,
5465
6359
  PermissionNotInManifestError as PermissionNotInManifestError2,
5466
6360
  SessionExpiredError as SessionExpiredError2,
5467
6361
  ManifestValidationError,
@@ -5478,6 +6372,10 @@ import {
5478
6372
  } from "@tinycloud/sdk-core";
5479
6373
 
5480
6374
  // src/delegation.ts
6375
+ import {
6376
+ canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
6377
+ principalDidEquals as principalDidEquals3
6378
+ } from "@tinycloud/sdk-core";
5481
6379
  function serializeDelegation(delegation) {
5482
6380
  return JSON.stringify({
5483
6381
  ...delegation,
@@ -5514,7 +6412,7 @@ import {
5514
6412
  SECRET_NAME_RE,
5515
6413
  canonicalizeSecretScope,
5516
6414
  resolveSecretListPrefix as resolveSecretListPrefix2,
5517
- resolveSecretPath as resolveSecretPath2
6415
+ resolveSecretPath as resolveSecretPath3
5518
6416
  } from "@tinycloud/sdk-core";
5519
6417
  import {
5520
6418
  DelegationManager as DelegationManager2,
@@ -5550,6 +6448,7 @@ export {
5550
6448
  AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
5551
6449
  CapabilityKeyRegistry2 as CapabilityKeyRegistry,
5552
6450
  CapabilityKeyRegistryErrorCodes,
6451
+ CaveatedDelegationUnsupportedError2 as CaveatedDelegationUnsupportedError,
5553
6452
  DEFAULT_MANIFEST_SPACE2 as DEFAULT_MANIFEST_SPACE,
5554
6453
  DEFAULT_MANIFEST_VERSION,
5555
6454
  DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
@@ -5585,6 +6484,7 @@ export {
5585
6484
  TinyCloudDebugLogger,
5586
6485
  TinyCloudNode,
5587
6486
  UnsupportedFeatureError2 as UnsupportedFeatureError,
6487
+ UnsupportedSessionRestoreError,
5588
6488
  VAULT_PERMISSION_SERVICE,
5589
6489
  VaultHeaders,
5590
6490
  VaultPublicSpaceKVActions,
@@ -5629,10 +6529,10 @@ export {
5629
6529
  parseSpaceUri,
5630
6530
  pkhDid3 as pkhDid,
5631
6531
  principalDid,
5632
- principalDidEquals3 as principalDidEquals,
6532
+ principalDidEquals4 as principalDidEquals,
5633
6533
  resolveManifest2 as resolveManifest,
5634
6534
  resolveSecretListPrefix2 as resolveSecretListPrefix,
5635
- resolveSecretPath2 as resolveSecretPath,
6535
+ resolveSecretPath3 as resolveSecretPath,
5636
6536
  resourceCapabilitiesToSpaceAbilitiesMap2 as resourceCapabilitiesToSpaceAbilitiesMap,
5637
6537
  serializeDelegation,
5638
6538
  tinyCloudDebugLogger,