@tinycloud/node-sdk 2.7.0-beta.2 → 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
  */
@@ -2104,6 +2339,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2104
2339
  getPrimarySpaceId: () => this.spaceId,
2105
2340
  getAccountSpaceId: () => this.accountSpaceId,
2106
2341
  getSpaces: () => this.spaces,
2342
+ getDelegationManager: () => this.delegationManager,
2107
2343
  getAccountDb: () => this.accountSpaceId ? this.sqlForSpace(this.accountSpaceId).db("account") : void 0,
2108
2344
  ensureAccountSpaceHosted: async () => {
2109
2345
  if (this.accountSpaceId && this.auth) {
@@ -2150,6 +2386,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2150
2386
  this._hooks = void 0;
2151
2387
  this._vault = void 0;
2152
2388
  this._encryption = void 0;
2389
+ this._baseVaults.clear();
2153
2390
  this._baseSecrets.clear();
2154
2391
  this._secrets.clear();
2155
2392
  this._spaceService = void 0;
@@ -2157,6 +2394,10 @@ var _TinyCloudNode = class _TinyCloudNode {
2157
2394
  this.runtimePermissionGrants = [];
2158
2395
  await this.tc.signIn(options);
2159
2396
  this.syncResolvedHostFromAuth();
2397
+ const oldGraph = this._serviceGraph;
2398
+ this._serviceGraph = this.createServiceGraphLifetime();
2399
+ oldGraph.retire();
2400
+ this.tc.retireServices();
2160
2401
  const signedInSession = this.currentTinyCloudSession();
2161
2402
  if (signedInSession) {
2162
2403
  this.sessionKeyId = signedInSession.sessionKey;
@@ -2441,7 +2682,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2441
2682
  }
2442
2683
  let operations = [];
2443
2684
  try {
2444
- const entries = this.wasmBindings.parseRecapFromSiwe(siwe);
2685
+ const entries = this.parseRecapWithCaveats(siwe);
2445
2686
  if (Array.isArray(entries)) {
2446
2687
  operations = entries.flatMap((entry) => {
2447
2688
  const service = this.invocationServiceName(entry.service);
@@ -2449,7 +2690,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2449
2690
  ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId: entry.space },
2450
2691
  service,
2451
2692
  path: entry.path,
2452
- action
2693
+ action,
2694
+ caveats: cloneRecapCaveats(entry.caveats)
2453
2695
  }));
2454
2696
  });
2455
2697
  }
@@ -2802,86 +3044,441 @@ var _TinyCloudNode = class _TinyCloudNode {
2802
3044
  */
2803
3045
  async restoreSession(sessionData) {
2804
3046
  await this.wasmBindings.ensureInitialized?.();
2805
- this._kv = void 0;
2806
- this._sql = void 0;
2807
- this._duckdb = void 0;
2808
- this._hooks = void 0;
2809
- this._vault = void 0;
2810
- this._encryption = void 0;
2811
- this._baseSecrets.clear();
2812
- this._secrets.clear();
2813
- this._spaceService = void 0;
2814
- this._serviceContext = void 0;
2815
- this.runtimePermissionGrants = [];
2816
- const restoredAddress = sessionData.address ? canonicalizeAddress2(sessionData.address) : void 0;
2817
- if (restoredAddress) {
2818
- 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.");
2819
3080
  }
2820
- if (sessionData.chainId) {
2821
- 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);
2822
3140
  }
2823
3141
  const resolvedHost = await this.resolveRestoredHost(
2824
3142
  sessionData.tinycloudHosts,
2825
3143
  restoredAddress,
2826
3144
  sessionData.chainId
2827
3145
  );
2828
- if (resolvedHost) {
2829
- this.config.host = resolvedHost;
2830
- }
2831
- this._serviceContext = new ServiceContext2({
2832
- invoke: this.invokeWithRuntimePermissions,
2833
- invokeAny: this.invokeAnyWithRuntimePermissions,
2834
- fetch: globalThis.fetch.bind(globalThis),
2835
- hosts: [this.config.host],
2836
- telemetry: this.config.telemetry
2837
- });
2838
- this._kv = new KVService2({});
2839
- this._kv.initialize(this._serviceContext);
2840
- this._serviceContext.registerService("kv", this._kv);
2841
- this._sql = new SQLService2({});
2842
- this._sql.initialize(this._serviceContext);
2843
- this._serviceContext.registerService("sql", this._sql);
2844
- this._duckdb = new DuckDbService2({});
2845
- this._duckdb.initialize(this._serviceContext);
2846
- this._serviceContext.registerService("duckdb", this._duckdb);
2847
- this._hooks = new HooksService2({});
2848
- this._hooks.initialize(this._serviceContext);
2849
- 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;
2850
3150
  const serviceSession = {
2851
3151
  delegationHeader: sessionData.delegationHeader,
2852
3152
  delegationCid: sessionData.delegationCid,
2853
3153
  spaceId: sessionData.spaceId,
2854
- verificationMethod: sessionData.verificationMethod,
2855
- jwk: sessionData.jwk
3154
+ verificationMethod: restoredVerificationMethod,
3155
+ jwk: stagedJwk
2856
3156
  };
2857
- this._serviceContext.setSession(serviceSession);
2858
- this._vault = this.createVaultService(sessionData.spaceId, this._kv);
2859
- this._vault.initialize(this._serviceContext);
2860
- this._serviceContext.registerService("vault", this._vault);
2861
- this.initializeV2Services(serviceSession);
2862
- if (sessionData.siwe && restoredAddress && sessionData.chainId) {
2863
- const tcSession = {
2864
- address: restoredAddress,
2865
- chainId: sessionData.chainId,
2866
- sessionKey: JSON.stringify(sessionData.jwk),
2867
- spaceId: sessionData.spaceId,
2868
- delegationCid: sessionData.delegationCid,
2869
- delegationHeader: sessionData.delegationHeader,
2870
- verificationMethod: sessionData.verificationMethod,
2871
- jwk: sessionData.jwk,
2872
- siwe: sessionData.siwe,
2873
- signature: sessionData.signature ?? ""
2874
- };
2875
- if (this.auth) {
2876
- this.auth.setRestoredTinyCloudSession(
2877
- tcSession,
2878
- this.config.host ? [this.config.host] : void 0
2879
- );
2880
- } else {
2881
- 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);
2882
3344
  }
2883
- this.registerPrimarySessionGrant(tcSession);
2884
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 };
2885
3482
  }
2886
3483
  /**
2887
3484
  * Resolve the host a restored session should target.
@@ -3043,14 +3640,19 @@ var _TinyCloudNode = class _TinyCloudNode {
3043
3640
  if (!session) {
3044
3641
  return;
3045
3642
  }
3046
- this.tc.initializeServices(this.invokeWithRuntimePermissions, [this.config.host]);
3047
- this._serviceContext = new ServiceContext2({
3048
- invoke: this.invokeWithRuntimePermissions,
3049
- invokeAny: this.invokeAnyWithRuntimePermissions,
3050
- 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,
3051
3653
  hosts: [this.config.host],
3052
3654
  telemetry: this.config.telemetry
3053
- });
3655
+ }));
3054
3656
  this._kv = new KVService2({});
3055
3657
  this._kv.initialize(this._serviceContext);
3056
3658
  this._serviceContext.registerService("kv", this._kv);
@@ -3085,12 +3687,12 @@ var _TinyCloudNode = class _TinyCloudNode {
3085
3687
  createSpaceScopedKVService(spaceId) {
3086
3688
  const kvService = new KVService2({});
3087
3689
  if (this._serviceContext) {
3088
- const spaceScopedContext = new ServiceContext2({
3690
+ const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
3089
3691
  invoke: this._serviceContext.invoke,
3090
3692
  fetch: this._serviceContext.fetch,
3091
3693
  hosts: this._serviceContext.hosts,
3092
3694
  telemetry: this.config.telemetry
3093
- });
3695
+ }));
3094
3696
  const session = this._serviceContext.session;
3095
3697
  if (session) {
3096
3698
  spaceScopedContext.setSession({ ...session, spaceId });
@@ -3123,6 +3725,18 @@ var _TinyCloudNode = class _TinyCloudNode {
3123
3725
  }
3124
3726
  return session;
3125
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
+ }
3126
3740
  createEncryptionCrypto() {
3127
3741
  const wasm = this.wasmBindings;
3128
3742
  const columnEncrypt = (key, plaintext) => {
@@ -3178,14 +3792,15 @@ var _TinyCloudNode = class _TinyCloudNode {
3178
3792
  }
3179
3793
  return info.nodeId;
3180
3794
  }
3181
- async signRawNetworkAuthorization(input) {
3795
+ async signRawNetworkAuthorization(input, binding) {
3796
+ binding?.graph.assertActive();
3182
3797
  if (!this.wasmBindings.invokeAny) {
3183
3798
  throw new Error("WASM binding does not support raw-resource invokeAny");
3184
3799
  }
3185
3800
  if (!this.wasmBindings.computeCid) {
3186
3801
  throw new Error("WASM binding does not support invocation CID computation");
3187
3802
  }
3188
- const session = this.requireServiceSession();
3803
+ const session = binding?.session ?? this.requireEncryptionSession();
3189
3804
  const headers = this.invokeAnyWithRuntimePermissions(
3190
3805
  session,
3191
3806
  [
@@ -3204,6 +3819,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3204
3819
  input.targetNode,
3205
3820
  session.jwk
3206
3821
  );
3822
+ binding?.graph.assertActive();
3207
3823
  return {
3208
3824
  authorization: audienceBound,
3209
3825
  invocationCid: this.wasmBindings.computeCid(
@@ -3212,25 +3828,55 @@ var _TinyCloudNode = class _TinyCloudNode {
3212
3828
  )
3213
3829
  };
3214
3830
  }
3215
- 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;
3216
3851
  const crypto = this.createEncryptionCrypto();
3217
3852
  const transport = {
3218
- postDecrypt: async ({ networkId, authorization, canonicalBody }) => {
3219
- const response = await fetch(
3220
- `${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`,
3221
3857
  {
3222
3858
  method: "POST",
3223
3859
  headers: {
3224
3860
  Authorization: authorization,
3225
3861
  "Content-Type": "application/json"
3226
3862
  },
3227
- body: canonicalBody
3863
+ body: canonicalBody,
3864
+ signal
3228
3865
  }
3229
3866
  );
3867
+ graph.assertActive();
3230
3868
  if (!response.ok) {
3231
- throw new Error(
3232
- `decrypt failed ${response.status}: ${await response.text()}`
3233
- );
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);
3234
3880
  }
3235
3881
  return await response.json();
3236
3882
  }
@@ -3239,12 +3885,14 @@ var _TinyCloudNode = class _TinyCloudNode {
3239
3885
  crypto,
3240
3886
  signer: {
3241
3887
  signDecryptInvocation: async (input) => {
3888
+ graph.assertActive();
3242
3889
  const signed = await this.signRawNetworkAuthorization({
3243
3890
  targetNode: input.targetNode,
3244
3891
  networkId: input.networkId,
3245
3892
  action: DECRYPT_ACTION2,
3246
3893
  facts: input.facts
3247
- });
3894
+ }, { graph, session });
3895
+ graph.assertActive();
3248
3896
  return {
3249
3897
  ...signed,
3250
3898
  canonicalBody: canonicalizeEncryptionJson(
@@ -3255,25 +3903,35 @@ var _TinyCloudNode = class _TinyCloudNode {
3255
3903
  },
3256
3904
  transport,
3257
3905
  node: {
3258
- 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
+ }
3259
3912
  },
3260
3913
  wellKnown: {
3261
3914
  fetchWellKnown: async (principal, discoveryKey) => {
3262
- if (!this._address || !didPrincipalMatches2(principal, this.did)) {
3915
+ graph.assertActive();
3916
+ if (!address || !didPrincipalMatches2(principal, did)) {
3263
3917
  return null;
3264
3918
  }
3265
- if (!this.config.host) {
3266
- return null;
3267
- }
3268
- const publicSpaceId = makePublicSpaceId(this._address, this._chainId);
3269
- const result = await TinyCloud.readPublicSpace(this.config.host, publicSpaceId, discoveryKey);
3270
- 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) {
3271
3927
  return null;
3272
3928
  }
3273
- const body = result.data;
3929
+ const body = await response.json();
3930
+ graph.assertActive();
3274
3931
  return "descriptor" in body && body.descriptor ? body.descriptor : body;
3275
3932
  }
3276
- }
3933
+ },
3934
+ assertActive: () => graph.assertActive()
3277
3935
  });
3278
3936
  }
3279
3937
  getEncryptionService() {
@@ -3287,8 +3945,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3287
3945
  }
3288
3946
  return this._encryption;
3289
3947
  }
3290
- createVaultService(spaceId, kv) {
3948
+ createVaultService(spaceId, kv, encryptionService = this.getEncryptionService(), nodeContext) {
3291
3949
  const wasm = this.wasmBindings;
3950
+ const serviceGraph = nodeContext?.serviceGraph ?? this._serviceGraph;
3292
3951
  const vaultCrypto = createVaultCrypto({
3293
3952
  vault_encrypt: wasm.vault_encrypt,
3294
3953
  vault_decrypt: wasm.vault_decrypt,
@@ -3299,12 +3958,17 @@ var _TinyCloudNode = class _TinyCloudNode {
3299
3958
  vault_sha256: wasm.vault_sha256
3300
3959
  });
3301
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;
3302
3966
  return new DataVaultService({
3303
3967
  spaceId,
3304
3968
  crypto: vaultCrypto,
3305
3969
  encryption: {
3306
- networkId: this.getEncryptionNetworkIdForSpace(spaceId),
3307
- service: this.getEncryptionService(),
3970
+ networkId: `urn:tinycloud:encryption:${ownerDid}:${DEFAULT_ENCRYPTION_NETWORK_NAME}`,
3971
+ service: encryptionService,
3308
3972
  decryptCapabilityProof: () => ({
3309
3973
  proofs: [this.requireServiceSession().delegationCid]
3310
3974
  })
@@ -3313,6 +3977,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3313
3977
  kv,
3314
3978
  ensurePublicSpace: async () => {
3315
3979
  try {
3980
+ serviceGraph.assertActive();
3316
3981
  await self.ensurePublicSpace();
3317
3982
  return { ok: true, data: void 0 };
3318
3983
  } catch (error) {
@@ -3320,14 +3985,15 @@ var _TinyCloudNode = class _TinyCloudNode {
3320
3985
  }
3321
3986
  },
3322
3987
  get publicKV() {
3988
+ serviceGraph.assertActive();
3323
3989
  return self._publicKV ?? self.tc.publicKV;
3324
3990
  },
3325
- readPublicSpace: (host, targetSpaceId, key) => TinyCloud.readPublicSpace(host, targetSpaceId, key),
3991
+ readPublicSpace: (host2, targetSpaceId, key) => TinyCloud.readPublicSpace(host2, targetSpaceId, key),
3326
3992
  makePublicSpaceId: TinyCloud.makePublicSpaceId,
3327
- did: this.did,
3328
- address: this._address ?? "",
3329
- chainId: this._chainId,
3330
- hosts: [this.config.host]
3993
+ did,
3994
+ address: address ?? "",
3995
+ chainId,
3996
+ hosts: [host]
3331
3997
  }
3332
3998
  });
3333
3999
  }
@@ -3336,6 +4002,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3336
4002
  * @internal
3337
4003
  */
3338
4004
  initializeV2Services(serviceSession) {
4005
+ const graph = this._serviceGraph;
3339
4006
  this._capabilityRegistry = new CapabilityKeyRegistry();
3340
4007
  const tcSession = this.auth?.tinyCloudSession;
3341
4008
  if (tcSession && this._address) {
@@ -3378,22 +4045,25 @@ var _TinyCloudNode = class _TinyCloudNode {
3378
4045
  }
3379
4046
  this._delegationManager = new DelegationManager({
3380
4047
  hosts: [this.config.host],
4048
+ accountSpaceId: this.accountSpaceId,
3381
4049
  session: serviceSession,
3382
- invoke: this.invokeWithRuntimePermissions,
3383
- invokeAny: this.invokeAnyWithRuntimePermissions,
3384
- fetch: globalThis.fetch.bind(globalThis)
4050
+ invoke: graph.invoke,
4051
+ invokeAny: graph.invokeAny,
4052
+ fetch: graph.fetch
3385
4053
  });
3386
4054
  this._spaceService = new SpaceService({
3387
4055
  hosts: [this.config.host],
3388
4056
  session: serviceSession,
3389
- invoke: this.wasmBindings.invoke,
3390
- fetch: globalThis.fetch.bind(globalThis),
4057
+ invoke: graph.invoke,
4058
+ fetch: graph.fetch,
3391
4059
  capabilityRegistry: this._capabilityRegistry,
3392
4060
  userDid: this.did,
3393
4061
  createKVService: (spaceId) => {
4062
+ graph.assertActive();
3394
4063
  return this.createSpaceScopedKVService(spaceId);
3395
4064
  },
3396
4065
  createVaultService: (spaceId) => {
4066
+ graph.assertActive();
3397
4067
  const kvService = this.createSpaceScopedKVService(spaceId);
3398
4068
  const vaultService = this.createVaultService(spaceId, kvService);
3399
4069
  if (this._serviceContext) {
@@ -3402,11 +4072,13 @@ var _TinyCloudNode = class _TinyCloudNode {
3402
4072
  return vaultService;
3403
4073
  },
3404
4074
  createSecretsService: (spaceId) => {
4075
+ graph.assertActive();
3405
4076
  return this.secretsForSpace(spaceId);
3406
4077
  },
3407
4078
  // Enable space.delegations.create() via SIWE-based delegation
3408
4079
  createDelegation: async (params) => {
3409
4080
  try {
4081
+ graph.assertActive();
3410
4082
  const portableDelegation = await this.createDelegation({
3411
4083
  delegateDID: params.delegateDID,
3412
4084
  path: params.path,
@@ -3440,18 +4112,50 @@ var _TinyCloudNode = class _TinyCloudNode {
3440
4112
  }
3441
4113
  },
3442
4114
  onSpaceRegistered: async (space) => {
4115
+ graph.assertActive();
3443
4116
  await this.account.spaces.register(space);
3444
4117
  }
3445
4118
  });
3446
- this._sharingService.updateConfig({
4119
+ this._sharingService = new SharingService({
4120
+ hosts: [this.config.host],
3447
4121
  session: serviceSession,
4122
+ invoke: graph.invoke,
4123
+ fetch: graph.fetch,
4124
+ assertActive: () => graph.assertActive(),
4125
+ keyProvider: this._keyProvider,
4126
+ registry: this._capabilityRegistry,
3448
4127
  delegationManager: this._delegationManager,
3449
4128
  sessionExpiry: this.getSessionExpiry(),
3450
- // WASM-based delegation creation (preferred - no server roundtrip)
3451
- createDelegationWasm: (params) => this.createDelegationWrapper(params),
3452
- // Root delegation for long-lived share links (bypasses session expiry)
3453
- // In node-sdk we have direct signer access, so no popup needed
3454
- 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
3455
4159
  });
3456
4160
  this._spaceService.updateConfig({
3457
4161
  sharingService: this._sharingService
@@ -3518,7 +4222,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3518
4222
  * with expiry longer than the current session.
3519
4223
  * @internal
3520
4224
  */
3521
- async createOwnerDelegation(params) {
4225
+ async createOwnerDelegation(params, assertActive) {
4226
+ const assertOwnerGraphActive = assertActive ?? this._serviceGraph.assertActive.bind(this._serviceGraph);
4227
+ assertOwnerGraphActive();
3522
4228
  if (!params.delegateDid.startsWith("did:key:") || params.actions.length === 0 || params.path.length === 0) {
3523
4229
  throw new Error("Owner delegation requires an external did:key audience and bounded capabilities");
3524
4230
  }
@@ -3543,8 +4249,10 @@ var _TinyCloudNode = class _TinyCloudNode {
3543
4249
  delegateUri: params.delegateDid
3544
4250
  });
3545
4251
  const signature = await this.signer.signMessage(prepared.siwe);
4252
+ assertOwnerGraphActive();
3546
4253
  const delegationSession = this.wasmBindings.completeSessionSetup({ ...prepared, signature });
3547
4254
  const activation = await activateSessionWithHost2(host, delegationSession.delegationHeader);
4255
+ assertOwnerGraphActive();
3548
4256
  if (!activation.success) {
3549
4257
  throw new Error(`Owner delegation import failed: ${activation.status} ${activation.error ?? ""}`.trim());
3550
4258
  }
@@ -3572,7 +4280,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3572
4280
  }
3573
4281
  };
3574
4282
  }
3575
- async createRootDelegationForSharing(params) {
4283
+ async createRootDelegationForSharing(params, assertActive) {
3576
4284
  try {
3577
4285
  return (await this.createOwnerDelegation({
3578
4286
  delegateDid: params.shareKeyDID,
@@ -3580,8 +4288,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3580
4288
  path: params.path,
3581
4289
  actions: params.actions,
3582
4290
  expiresAt: params.requestedExpiry
3583
- })).delegation;
4291
+ }, assertActive)).delegation;
3584
4292
  } catch {
4293
+ assertActive?.();
3585
4294
  return void 0;
3586
4295
  }
3587
4296
  }
@@ -3653,13 +4362,13 @@ var _TinyCloudNode = class _TinyCloudNode {
3653
4362
  throw new Error("Not signed in. Call signIn() first.");
3654
4363
  }
3655
4364
  const sql = new SQLService2({});
3656
- const spaceScopedContext = new ServiceContext2({
4365
+ const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
3657
4366
  invoke: this._serviceContext.invoke,
3658
4367
  invokeAny: this._serviceContext.invokeAny,
3659
4368
  fetch: this._serviceContext.fetch,
3660
4369
  hosts: this._serviceContext.hosts,
3661
4370
  telemetry: this.config.telemetry
3662
- });
4371
+ }));
3663
4372
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
3664
4373
  sql.initialize(spaceScopedContext);
3665
4374
  return sql;
@@ -3683,12 +4392,12 @@ var _TinyCloudNode = class _TinyCloudNode {
3683
4392
  throw new Error("Not signed in. Call signIn() first.");
3684
4393
  }
3685
4394
  const kv = new KVService2({});
3686
- const spaceScopedContext = new ServiceContext2({
4395
+ const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
3687
4396
  invoke: this._serviceContext.invoke,
3688
4397
  invokeAny: this._serviceContext.invokeAny,
3689
4398
  fetch: this._serviceContext.fetch,
3690
4399
  hosts: this._serviceContext.hosts
3691
- });
4400
+ }));
3692
4401
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
3693
4402
  kv.initialize(spaceScopedContext);
3694
4403
  return kv;
@@ -3724,19 +4433,11 @@ var _TinyCloudNode = class _TinyCloudNode {
3724
4433
  }
3725
4434
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
3726
4435
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
3727
- const response = await fetch(
3728
- `${this.config.host}/encryption/networks/${encodeURIComponent(networkId)}`
4436
+ return this.fetchEncryptionNetworkAt(
4437
+ this.config.host,
4438
+ networkId,
4439
+ globalThis.fetch.bind(globalThis)
3729
4440
  );
3730
- if (response.status === 404) {
3731
- return null;
3732
- }
3733
- if (!response.ok) {
3734
- throw new Error(
3735
- `Failed to fetch encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
3736
- );
3737
- }
3738
- const body = await response.json();
3739
- return "descriptor" in body && body.descriptor ? body.descriptor : body;
3740
4441
  }
3741
4442
  async createEncryptionNetwork(name = DEFAULT_ENCRYPTION_NETWORK_NAME) {
3742
4443
  const targetNode = await this.fetchNodeId();
@@ -3830,6 +4531,38 @@ var _TinyCloudNode = class _TinyCloudNode {
3830
4531
  }
3831
4532
  return secrets;
3832
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
+ }
3833
4566
  getBaseSecrets(spaceId) {
3834
4567
  if (!this._spaceService) {
3835
4568
  throw new Error("Not signed in. Call signIn() first.");
@@ -3837,16 +4570,29 @@ var _TinyCloudNode = class _TinyCloudNode {
3837
4570
  const resolvedSpace = spaceId.startsWith("tinycloud:") ? spaceId : this.ownedSpaceId(spaceId);
3838
4571
  let secrets = this._baseSecrets.get(resolvedSpace);
3839
4572
  if (!secrets) {
3840
- const kvService = this.createSpaceScopedKVService(resolvedSpace);
3841
- const vaultService = this.createVaultService(resolvedSpace, kvService);
3842
- if (this._serviceContext) {
3843
- vaultService.initialize(this._serviceContext);
3844
- }
3845
- secrets = new SecretsService(() => vaultService);
4573
+ secrets = new SecretsService(() => this.getBaseVault(resolvedSpace));
3846
4574
  this._baseSecrets.set(resolvedSpace, secrets);
3847
4575
  }
3848
4576
  return secrets;
3849
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
+ }
3850
4596
  /**
3851
4597
  * Hooks write stream subscription API.
3852
4598
  */
@@ -3951,6 +4697,63 @@ var _TinyCloudNode = class _TinyCloudNode {
3951
4697
  (grant) => grant.delegation
3952
4698
  );
3953
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
+ }
3954
4757
  /**
3955
4758
  * Install a portable runtime permission delegation into this SDK instance so
3956
4759
  * matching service calls and downstream `delegateTo()` calls can use it.
@@ -4019,6 +4822,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4019
4822
  "grantRuntimePermissions requires wallet mode with a signer or privateKey."
4020
4823
  );
4021
4824
  }
4825
+ this.assertDelegationCaveatsPreservable(expanded);
4022
4826
  const rawEntries = expanded.filter(
4023
4827
  (entry) => this.isEncryptionPermissionEntry(entry)
4024
4828
  );
@@ -4280,12 +5084,12 @@ var _TinyCloudNode = class _TinyCloudNode {
4280
5084
  }
4281
5085
  if (this._serviceContext) {
4282
5086
  const publicKV = new KVService2({ prefix: "" });
4283
- const publicContext = new ServiceContext2({
4284
- invoke: this.invokeWithRuntimePermissions,
4285
- fetch: this._serviceContext.fetch,
5087
+ const publicContext = this._serviceGraph.track(new ServiceContext2({
5088
+ invoke: this._serviceGraph.invoke,
5089
+ fetch: this._serviceGraph.fetch,
4286
5090
  hosts: this._serviceContext.hosts,
4287
5091
  telemetry: this.config.telemetry
4288
- });
5092
+ }));
4289
5093
  publicContext.setSession({
4290
5094
  delegationHeader: delegationSession.delegationHeader,
4291
5095
  delegationCid: delegationSession.delegationCid,
@@ -4352,13 +5156,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4352
5156
  async getDelegationStatus(cid) {
4353
5157
  return this.delegationManager.status(cid);
4354
5158
  }
4355
- /** Compute the canonical CID of a compact delegation authorization. */
5159
+ /** Compute the canonical CID of a compact UCAN or DAG-CBOR delegation authorization. */
4356
5160
  computeDelegationCid(authorization) {
4357
5161
  if (!authorization || !this.wasmBindings.computeCid) {
4358
5162
  throw new Error("Delegation CID computation is unavailable");
4359
5163
  }
5164
+ const compact = authorization.replace(/^Bearer /i, "");
4360
5165
  return this.wasmBindings.computeCid(
4361
- new TextEncoder().encode(authorization),
5166
+ compact.includes(".") ? new TextEncoder().encode(compact) : decodeAuthorizationBytes(authorization),
4362
5167
  0x55n
4363
5168
  );
4364
5169
  }
@@ -4450,6 +5255,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4450
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."
4451
5256
  );
4452
5257
  }
5258
+ this.assertDelegationCaveatsPreservable(expandedEntries);
4453
5259
  const delegation2 = await this.createDelegationLegacyWalletPath(
4454
5260
  did,
4455
5261
  expandedEntries[0],
@@ -4458,13 +5264,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4458
5264
  return { delegation: delegation2, prompted: true };
4459
5265
  }
4460
5266
  const granted = parseRecapCapabilities(
4461
- (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
5267
+ (siwe) => this.parseRecapWithCaveats(siwe),
4462
5268
  session.siwe
4463
5269
  );
4464
5270
  const { subset, missing } = isCapabilitySubset(expandedEntries, granted);
4465
5271
  if (!subset) {
5272
+ const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
4466
5273
  const runtimeGrant = this.findGrantForOperations(
4467
- this.permissionEntriesToOperations(expandedEntries, session),
5274
+ runtimeOperations,
4468
5275
  { excludePrimary: true }
4469
5276
  );
4470
5277
  if (runtimeGrant) {
@@ -4477,7 +5284,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4477
5284
  did,
4478
5285
  expandedEntries,
4479
5286
  runtimeExpiration,
4480
- runtimeGrant
5287
+ runtimeGrant,
5288
+ runtimeOperations
4481
5289
  );
4482
5290
  return { delegation: delegation2, prompted: false };
4483
5291
  }
@@ -4547,6 +5355,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4547
5355
  "createDelegationViaWasmPath requires a non-empty entries array"
4548
5356
  );
4549
5357
  }
5358
+ this.assertDelegationCaveatsPreservable(entries);
4550
5359
  const resolvedSpaces = /* @__PURE__ */ new Set();
4551
5360
  for (const entry of entries) {
4552
5361
  if (this.isEncryptionPermissionEntry(entry)) {
@@ -4627,7 +5436,9 @@ var _TinyCloudNode = class _TinyCloudNode {
4627
5436
  host: this.config.host
4628
5437
  };
4629
5438
  }
4630
- async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant) {
5439
+ async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations) {
5440
+ this.assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant);
5441
+ this.assertDelegationCaveatsPreservable(entries);
4631
5442
  const result = this.createDelegationWrapper({
4632
5443
  session: grant.session,
4633
5444
  delegateDID: did,
@@ -4681,6 +5492,74 @@ var _TinyCloudNode = class _TinyCloudNode {
4681
5492
  expandPermissionEntries(permissions) {
4682
5493
  return expandPermissionEntriesCore(permissions);
4683
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
+ }
4684
5563
  shortServiceName(service) {
4685
5564
  const short = SERVICE_LONG_TO_SHORT[service];
4686
5565
  if (short === void 0) {
@@ -4735,14 +5614,15 @@ var _TinyCloudNode = class _TinyCloudNode {
4735
5614
  ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId },
4736
5615
  service,
4737
5616
  path: entry.path,
4738
- action
5617
+ action,
5618
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
4739
5619
  }));
4740
5620
  });
4741
5621
  }
4742
5622
  sessionCoversPermissionEntries(session, entries) {
4743
5623
  try {
4744
5624
  const granted = parseRecapCapabilities(
4745
- (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
5625
+ (siwe) => this.parseRecapWithCaveats(siwe),
4746
5626
  session.siwe
4747
5627
  );
4748
5628
  return isCapabilitySubset(entries, granted).subset;
@@ -4758,7 +5638,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4758
5638
  ...this.isEncryptionNetworkOperation(service, entry.path) ? { resource: entry.path } : { spaceId },
4759
5639
  service,
4760
5640
  path: entry.path,
4761
- action
5641
+ action,
5642
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
4762
5643
  }));
4763
5644
  });
4764
5645
  }
@@ -4834,7 +5715,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4834
5715
  service: this.shortServiceName(entry.service),
4835
5716
  space: this.isEncryptionPermissionEntry(entry) ? "encryption" : spaceId,
4836
5717
  path: entry.path,
4837
- actions: [...entry.actions]
5718
+ actions: [...entry.actions],
5719
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
4838
5720
  }));
4839
5721
  }
4840
5722
  operationsFromDelegation(delegation) {
@@ -4845,7 +5727,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4845
5727
  ...this.isEncryptionNetworkOperation(service, resource.path) ? { resource: resource.path } : { spaceId: resource.space },
4846
5728
  service,
4847
5729
  path: resource.path,
4848
- action
5730
+ action,
5731
+ caveats: cloneRecapCaveats(resource.caveats)
4849
5732
  }));
4850
5733
  });
4851
5734
  }
@@ -4861,7 +5744,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4861
5744
  service,
4862
5745
  space: delegation.spaceId,
4863
5746
  path: delegation.path,
4864
- actions
5747
+ actions,
5748
+ ...delegation.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(delegation.caveats) }
4865
5749
  }));
4866
5750
  }
4867
5751
  /**
@@ -4962,6 +5846,9 @@ var _TinyCloudNode = class _TinyCloudNode {
4962
5846
  if (granted.service !== requested.service || !this.actionContains(granted.action, requested.action)) {
4963
5847
  return false;
4964
5848
  }
5849
+ if (requested.caveats !== void 0 && !recapCaveatsEqual(granted.caveats, requested.caveats)) {
5850
+ return false;
5851
+ }
4965
5852
  if (granted.resource !== void 0 || requested.resource !== void 0) {
4966
5853
  return granted.resource !== void 0 && requested.resource !== void 0 && granted.resource === requested.resource && this.pathContains(granted.path, requested.path);
4967
5854
  }
@@ -4996,6 +5883,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4996
5883
  invocationServiceName(service) {
4997
5884
  return service.startsWith("tinycloud.") ? this.shortServiceName(service) : service;
4998
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
+ }
4999
5890
  isEncryptionNetworkOperation(service, path) {
5000
5891
  return service === "encryption" && path.startsWith("urn:tinycloud:encryption:");
5001
5892
  }
@@ -5006,7 +5897,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5006
5897
  resource: entry.resource,
5007
5898
  service,
5008
5899
  path: entry.path,
5009
- action: entry.action
5900
+ action: entry.action,
5901
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
5010
5902
  };
5011
5903
  }
5012
5904
  if (this.isEncryptionNetworkOperation(service, entry.path)) {
@@ -5014,7 +5906,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5014
5906
  resource: entry.path,
5015
5907
  service,
5016
5908
  path: entry.path,
5017
- action: entry.action
5909
+ action: entry.action,
5910
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
5018
5911
  };
5019
5912
  }
5020
5913
  if (typeof entry.spaceId === "string") {
@@ -5022,7 +5915,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5022
5915
  spaceId: entry.spaceId,
5023
5916
  service,
5024
5917
  path: entry.path,
5025
- action: entry.action
5918
+ action: entry.action,
5919
+ ...entry.caveats === void 0 ? {} : { caveats: cloneRecapCaveats(entry.caveats) }
5026
5920
  };
5027
5921
  }
5028
5922
  return void 0;
@@ -5362,6 +6256,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5362
6256
  * @returns A portable delegation for the sub-delegate
5363
6257
  */
5364
6258
  async createSubDelegation(parentDelegation, params) {
6259
+ this.assertPortableDelegationCaveatsPreservable(parentDelegation);
5365
6260
  if (!this.signer) {
5366
6261
  throw new Error("Cannot createSubDelegation() in session-only mode. Requires wallet mode.");
5367
6262
  }
@@ -5460,6 +6355,7 @@ import {
5460
6355
  DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
5461
6356
  DEFAULT_MANIFEST_VERSION,
5462
6357
  VAULT_PERMISSION_SERVICE,
6358
+ CaveatedDelegationUnsupportedError as CaveatedDelegationUnsupportedError2,
5463
6359
  PermissionNotInManifestError as PermissionNotInManifestError2,
5464
6360
  SessionExpiredError as SessionExpiredError2,
5465
6361
  ManifestValidationError,
@@ -5476,6 +6372,10 @@ import {
5476
6372
  } from "@tinycloud/sdk-core";
5477
6373
 
5478
6374
  // src/delegation.ts
6375
+ import {
6376
+ canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
6377
+ principalDidEquals as principalDidEquals3
6378
+ } from "@tinycloud/sdk-core";
5479
6379
  function serializeDelegation(delegation) {
5480
6380
  return JSON.stringify({
5481
6381
  ...delegation,
@@ -5512,7 +6412,7 @@ import {
5512
6412
  SECRET_NAME_RE,
5513
6413
  canonicalizeSecretScope,
5514
6414
  resolveSecretListPrefix as resolveSecretListPrefix2,
5515
- resolveSecretPath as resolveSecretPath2
6415
+ resolveSecretPath as resolveSecretPath3
5516
6416
  } from "@tinycloud/sdk-core";
5517
6417
  import {
5518
6418
  DelegationManager as DelegationManager2,
@@ -5548,6 +6448,7 @@ export {
5548
6448
  AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
5549
6449
  CapabilityKeyRegistry2 as CapabilityKeyRegistry,
5550
6450
  CapabilityKeyRegistryErrorCodes,
6451
+ CaveatedDelegationUnsupportedError2 as CaveatedDelegationUnsupportedError,
5551
6452
  DEFAULT_MANIFEST_SPACE2 as DEFAULT_MANIFEST_SPACE,
5552
6453
  DEFAULT_MANIFEST_VERSION,
5553
6454
  DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
@@ -5583,6 +6484,7 @@ export {
5583
6484
  TinyCloudDebugLogger,
5584
6485
  TinyCloudNode,
5585
6486
  UnsupportedFeatureError2 as UnsupportedFeatureError,
6487
+ UnsupportedSessionRestoreError,
5586
6488
  VAULT_PERMISSION_SERVICE,
5587
6489
  VaultHeaders,
5588
6490
  VaultPublicSpaceKVActions,
@@ -5627,10 +6529,10 @@ export {
5627
6529
  parseSpaceUri,
5628
6530
  pkhDid3 as pkhDid,
5629
6531
  principalDid,
5630
- principalDidEquals3 as principalDidEquals,
6532
+ principalDidEquals4 as principalDidEquals,
5631
6533
  resolveManifest2 as resolveManifest,
5632
6534
  resolveSecretListPrefix2 as resolveSecretListPrefix,
5633
- resolveSecretPath2 as resolveSecretPath,
6535
+ resolveSecretPath3 as resolveSecretPath,
5634
6536
  resourceCapabilitiesToSpaceAbilitiesMap2 as resourceCapabilitiesToSpaceAbilitiesMap,
5635
6537
  serializeDelegation,
5636
6538
  tinyCloudDebugLogger,