@tinycloud/node-sdk 2.2.0-beta.6 → 2.2.0-beta.8

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
@@ -959,6 +959,7 @@ import {
959
959
  DuckDbService as DuckDbService2,
960
960
  HooksService as HooksService2,
961
961
  DataVaultService,
962
+ SecretsService,
962
963
  createVaultCrypto,
963
964
  ServiceContext as ServiceContext2,
964
965
  SilentNotificationHandler,
@@ -1217,6 +1218,151 @@ function extractSiweExpiration(siwe) {
1217
1218
  return d;
1218
1219
  }
1219
1220
 
1221
+ // src/NodeSecretsService.ts
1222
+ import {
1223
+ ErrorCodes,
1224
+ resolveManifest
1225
+ } from "@tinycloud/sdk-core";
1226
+ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
1227
+ var SECRET_PREFIX = "secrets/";
1228
+ var SECRETS_SPACE = "secrets";
1229
+ function ok() {
1230
+ return { ok: true, data: void 0 };
1231
+ }
1232
+ function secretsError(code, message, cause) {
1233
+ return {
1234
+ ok: false,
1235
+ error: {
1236
+ code,
1237
+ service: "secrets",
1238
+ message,
1239
+ ...cause ? { cause } : {}
1240
+ }
1241
+ };
1242
+ }
1243
+ function actionUrn(action) {
1244
+ return `tinycloud.kv/${action}`;
1245
+ }
1246
+ function secretResourcePath(base, name) {
1247
+ return `${base}/${SECRET_PREFIX}${name}`;
1248
+ }
1249
+ function secretPermissionEntries(name, action) {
1250
+ return [
1251
+ {
1252
+ service: "tinycloud.kv",
1253
+ space: SECRETS_SPACE,
1254
+ path: secretResourcePath("keys", name),
1255
+ actions: [action],
1256
+ skipPrefix: true
1257
+ },
1258
+ {
1259
+ service: "tinycloud.kv",
1260
+ space: SECRETS_SPACE,
1261
+ path: secretResourcePath("vault", name),
1262
+ actions: [action],
1263
+ skipPrefix: true
1264
+ }
1265
+ ];
1266
+ }
1267
+ function isSecretsSpace(space) {
1268
+ return space === SECRETS_SPACE || space.endsWith(`:${SECRETS_SPACE}`);
1269
+ }
1270
+ var NodeSecretsService = class {
1271
+ constructor(config) {
1272
+ this.config = config;
1273
+ this.shouldRestoreUnlock = false;
1274
+ }
1275
+ get vault() {
1276
+ return this.service.vault;
1277
+ }
1278
+ get isUnlocked() {
1279
+ return this.service.isUnlocked;
1280
+ }
1281
+ async unlock(signer) {
1282
+ const effectiveSigner = signer ?? this.config.getUnlockSigner?.();
1283
+ if (effectiveSigner !== void 0) {
1284
+ this.unlockSigner = effectiveSigner;
1285
+ }
1286
+ const result = await this.service.unlock(effectiveSigner);
1287
+ if (result.ok) {
1288
+ this.shouldRestoreUnlock = true;
1289
+ }
1290
+ return result;
1291
+ }
1292
+ lock() {
1293
+ this.shouldRestoreUnlock = false;
1294
+ this.service.lock();
1295
+ }
1296
+ get(name) {
1297
+ return this.service.get(name);
1298
+ }
1299
+ async put(name, value) {
1300
+ const permission = await this.ensureMutationPermission(name, "put");
1301
+ if (!permission.ok) return permission;
1302
+ return this.service.put(name, value);
1303
+ }
1304
+ async delete(name) {
1305
+ const permission = await this.ensureMutationPermission(name, "del");
1306
+ if (!permission.ok) return permission;
1307
+ return this.service.delete(name);
1308
+ }
1309
+ list() {
1310
+ return this.service.list();
1311
+ }
1312
+ get service() {
1313
+ return this.config.getService();
1314
+ }
1315
+ async ensureMutationPermission(name, action) {
1316
+ if (!SECRET_NAME_RE.test(name)) {
1317
+ return secretsError(
1318
+ ErrorCodes.INVALID_INPUT,
1319
+ `Invalid secret name ${JSON.stringify(name)}. Secret names must match ${SECRET_NAME_RE.source}.`
1320
+ );
1321
+ }
1322
+ if (this.hasMutationPermission(name, action)) {
1323
+ return ok();
1324
+ }
1325
+ if (!this.config.canEscalate()) {
1326
+ return secretsError(
1327
+ ErrorCodes.PERMISSION_DENIED,
1328
+ `Cannot autosign ${actionUrn(action)} for ${name}; TinyCloudNode needs wallet mode with a signer or privateKey.`
1329
+ );
1330
+ }
1331
+ try {
1332
+ await this.config.grantPermissions(secretPermissionEntries(name, action));
1333
+ return this.restoreUnlockAfterEscalation();
1334
+ } catch (error) {
1335
+ return secretsError(
1336
+ ErrorCodes.PERMISSION_DENIED,
1337
+ error instanceof Error ? error.message : `Autosign escalation for ${actionUrn(action)} on ${name} failed.`,
1338
+ error instanceof Error ? error : void 0
1339
+ );
1340
+ }
1341
+ }
1342
+ async restoreUnlockAfterEscalation() {
1343
+ if (!this.shouldRestoreUnlock) {
1344
+ return ok();
1345
+ }
1346
+ return this.service.unlock(this.unlockSigner);
1347
+ }
1348
+ hasMutationPermission(name, action) {
1349
+ const manifest = this.config.getManifest();
1350
+ if (manifest === void 0) {
1351
+ return false;
1352
+ }
1353
+ const manifests = Array.isArray(manifest) ? manifest : [manifest];
1354
+ const requiredAction = actionUrn(action);
1355
+ return manifests.some((entry) => {
1356
+ const resolved = resolveManifest(entry);
1357
+ return ["keys", "vault"].every(
1358
+ (base) => resolved.resources.some(
1359
+ (resource) => resource.service === "tinycloud.kv" && isSecretsSpace(resource.space) && resource.path === secretResourcePath(base, name) && resource.actions.includes(requiredAction)
1360
+ )
1361
+ );
1362
+ });
1363
+ }
1364
+ };
1365
+
1220
1366
  // src/TinyCloudNode.ts
1221
1367
  var DEFAULT_HOST = "https://node.tinycloud.xyz";
1222
1368
  var _TinyCloudNode = class _TinyCloudNode {
@@ -1248,6 +1394,30 @@ var _TinyCloudNode = class _TinyCloudNode {
1248
1394
  this.auth = null;
1249
1395
  this.tc = null;
1250
1396
  this._chainId = 1;
1397
+ this.runtimePermissionGrants = [];
1398
+ this.invokeWithRuntimePermissions = (session, service, path, action, facts) => {
1399
+ return this.wasmBindings.invoke(
1400
+ this.selectInvocationSession(session, service, path, action),
1401
+ service,
1402
+ path,
1403
+ action,
1404
+ facts
1405
+ );
1406
+ };
1407
+ this.invokeAnyWithRuntimePermissions = (session, entries, facts) => {
1408
+ if (!this.wasmBindings.invokeAny) {
1409
+ throw new Error("WASM binding does not support invokeAny");
1410
+ }
1411
+ const grant = this.findGrantForOperations(
1412
+ entries.map((entry) => ({
1413
+ spaceId: entry.spaceId,
1414
+ service: this.invocationServiceName(entry.service),
1415
+ path: entry.path,
1416
+ action: entry.action
1417
+ }))
1418
+ );
1419
+ return this.wasmBindings.invokeAny(grant?.session ?? session, entries, facts);
1420
+ };
1251
1421
  this.explicitHost = config.host;
1252
1422
  this.config = {
1253
1423
  ...config,
@@ -1283,7 +1453,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1283
1453
  this._sharingService = new SharingService({
1284
1454
  hosts: [this.config.host],
1285
1455
  // session: undefined - not needed for receive()
1286
- invoke: this.wasmBindings.invoke,
1456
+ invoke: this.invokeWithRuntimePermissions,
1287
1457
  fetch: globalThis.fetch.bind(globalThis),
1288
1458
  keyProvider: this._keyProvider,
1289
1459
  registry: this._capabilityRegistry,
@@ -1351,7 +1521,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1351
1521
  includeAccountRegistryPermissions: config.includeAccountRegistryPermissions
1352
1522
  });
1353
1523
  this.tc = new TinyCloud(this.auth, {
1354
- invokeAny: this.wasmBindings.invokeAny
1524
+ invokeAny: this.invokeAnyWithRuntimePermissions
1355
1525
  });
1356
1526
  }
1357
1527
  syncResolvedHostFromAuth() {
@@ -1471,7 +1641,12 @@ var _TinyCloudNode = class _TinyCloudNode {
1471
1641
  this._sql = void 0;
1472
1642
  this._duckdb = void 0;
1473
1643
  this._hooks = void 0;
1644
+ this._vault = void 0;
1645
+ this._baseSecrets = void 0;
1646
+ this._secrets = void 0;
1647
+ this._spaceService = void 0;
1474
1648
  this._serviceContext = void 0;
1649
+ this.runtimePermissionGrants = [];
1475
1650
  await this.tc.signIn(options);
1476
1651
  this.syncResolvedHostFromAuth();
1477
1652
  this.initializeServices();
@@ -1556,7 +1731,12 @@ var _TinyCloudNode = class _TinyCloudNode {
1556
1731
  this._sql = void 0;
1557
1732
  this._duckdb = void 0;
1558
1733
  this._hooks = void 0;
1734
+ this._vault = void 0;
1735
+ this._baseSecrets = void 0;
1736
+ this._secrets = void 0;
1737
+ this._spaceService = void 0;
1559
1738
  this._serviceContext = void 0;
1739
+ this.runtimePermissionGrants = [];
1560
1740
  if (sessionData.address) {
1561
1741
  this._address = sessionData.address;
1562
1742
  }
@@ -1564,8 +1744,8 @@ var _TinyCloudNode = class _TinyCloudNode {
1564
1744
  this._chainId = sessionData.chainId;
1565
1745
  }
1566
1746
  this._serviceContext = new ServiceContext2({
1567
- invoke: this.wasmBindings.invoke,
1568
- invokeAny: this.wasmBindings.invokeAny,
1747
+ invoke: this.invokeWithRuntimePermissions,
1748
+ invokeAny: this.invokeAnyWithRuntimePermissions,
1569
1749
  fetch: globalThis.fetch.bind(globalThis),
1570
1750
  hosts: [this.config.host]
1571
1751
  });
@@ -1589,41 +1769,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1589
1769
  jwk: sessionData.jwk
1590
1770
  };
1591
1771
  this._serviceContext.setSession(serviceSession);
1592
- const wasm = this.wasmBindings;
1593
- const vaultCrypto = createVaultCrypto({
1594
- vault_encrypt: wasm.vault_encrypt,
1595
- vault_decrypt: wasm.vault_decrypt,
1596
- vault_derive_key: wasm.vault_derive_key,
1597
- vault_x25519_from_seed: wasm.vault_x25519_from_seed,
1598
- vault_x25519_dh: wasm.vault_x25519_dh,
1599
- vault_random_bytes: wasm.vault_random_bytes,
1600
- vault_sha256: wasm.vault_sha256
1601
- });
1602
- const self = this;
1603
- this._vault = new DataVaultService({
1604
- spaceId: sessionData.spaceId,
1605
- crypto: vaultCrypto,
1606
- tc: {
1607
- kv: this._kv,
1608
- ensurePublicSpace: async () => {
1609
- try {
1610
- await self.ensurePublicSpace();
1611
- return { ok: true, data: void 0 };
1612
- } catch (error) {
1613
- return { ok: false, error: { code: "STORAGE_ERROR", message: error instanceof Error ? error.message : String(error), service: "vault" } };
1614
- }
1615
- },
1616
- get publicKV() {
1617
- return self._publicKV ?? self.tc.publicKV;
1618
- },
1619
- readPublicSpace: (host, spaceId, key) => TinyCloud.readPublicSpace(host, spaceId, key),
1620
- makePublicSpaceId: TinyCloud.makePublicSpaceId,
1621
- did: this.did,
1622
- address: sessionData.address ?? this._address ?? "",
1623
- chainId: sessionData.chainId ?? this._chainId,
1624
- hosts: [this.config.host]
1625
- }
1626
- });
1772
+ this._vault = this.createVaultService(sessionData.spaceId, this._kv);
1627
1773
  this._vault.initialize(this._serviceContext);
1628
1774
  this._serviceContext.registerService("vault", this._vault);
1629
1775
  this.initializeV2Services(serviceSession);
@@ -1685,7 +1831,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1685
1831
  includeAccountRegistryPermissions: this.config.includeAccountRegistryPermissions
1686
1832
  });
1687
1833
  this.tc = new TinyCloud(this.auth, {
1688
- invokeAny: this.wasmBindings.invokeAny
1834
+ invokeAny: this.invokeAnyWithRuntimePermissions
1689
1835
  });
1690
1836
  this.config.prefix = prefix;
1691
1837
  }
@@ -1729,7 +1875,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1729
1875
  includeAccountRegistryPermissions: this.config.includeAccountRegistryPermissions
1730
1876
  });
1731
1877
  this.tc = new TinyCloud(this.auth, {
1732
- invokeAny: this.wasmBindings.invokeAny
1878
+ invokeAny: this.invokeAnyWithRuntimePermissions
1733
1879
  });
1734
1880
  this.config.prefix = prefix;
1735
1881
  }
@@ -1742,10 +1888,10 @@ var _TinyCloudNode = class _TinyCloudNode {
1742
1888
  if (!session) {
1743
1889
  return;
1744
1890
  }
1745
- this.tc.initializeServices(this.wasmBindings.invoke, [this.config.host]);
1891
+ this.tc.initializeServices(this.invokeWithRuntimePermissions, [this.config.host]);
1746
1892
  this._serviceContext = new ServiceContext2({
1747
- invoke: this.wasmBindings.invoke,
1748
- invokeAny: this.wasmBindings.invokeAny,
1893
+ invoke: this.invokeWithRuntimePermissions,
1894
+ invokeAny: this.invokeAnyWithRuntimePermissions,
1749
1895
  fetch: globalThis.fetch.bind(globalThis),
1750
1896
  hosts: [this.config.host]
1751
1897
  });
@@ -1775,6 +1921,28 @@ var _TinyCloudNode = class _TinyCloudNode {
1775
1921
  };
1776
1922
  this._serviceContext.setSession(serviceSession);
1777
1923
  this.tc.serviceContext.setSession(serviceSession);
1924
+ this._vault = this.createVaultService(session.spaceId, this._kv);
1925
+ this._vault.initialize(this._serviceContext);
1926
+ this._serviceContext.registerService("vault", this._vault);
1927
+ this.initializeV2Services(serviceSession);
1928
+ }
1929
+ createSpaceScopedKVService(spaceId) {
1930
+ const kvService = new KVService2({});
1931
+ if (this._serviceContext) {
1932
+ const spaceScopedContext = new ServiceContext2({
1933
+ invoke: this._serviceContext.invoke,
1934
+ fetch: this._serviceContext.fetch,
1935
+ hosts: this._serviceContext.hosts
1936
+ });
1937
+ const session = this._serviceContext.session;
1938
+ if (session) {
1939
+ spaceScopedContext.setSession({ ...session, spaceId });
1940
+ }
1941
+ kvService.initialize(spaceScopedContext);
1942
+ }
1943
+ return kvService;
1944
+ }
1945
+ createVaultService(spaceId, kv) {
1778
1946
  const wasm = this.wasmBindings;
1779
1947
  const vaultCrypto = createVaultCrypto({
1780
1948
  vault_encrypt: wasm.vault_encrypt,
@@ -1786,11 +1954,11 @@ var _TinyCloudNode = class _TinyCloudNode {
1786
1954
  vault_sha256: wasm.vault_sha256
1787
1955
  });
1788
1956
  const self = this;
1789
- this._vault = new DataVaultService({
1790
- spaceId: session.spaceId,
1957
+ return new DataVaultService({
1958
+ spaceId,
1791
1959
  crypto: vaultCrypto,
1792
1960
  tc: {
1793
- kv: this._kv,
1961
+ kv,
1794
1962
  ensurePublicSpace: async () => {
1795
1963
  try {
1796
1964
  await self.ensurePublicSpace();
@@ -1802,17 +1970,14 @@ var _TinyCloudNode = class _TinyCloudNode {
1802
1970
  get publicKV() {
1803
1971
  return self._publicKV ?? self.tc.publicKV;
1804
1972
  },
1805
- readPublicSpace: (host, spaceId, key) => TinyCloud.readPublicSpace(host, spaceId, key),
1973
+ readPublicSpace: (host, targetSpaceId, key) => TinyCloud.readPublicSpace(host, targetSpaceId, key),
1806
1974
  makePublicSpaceId: TinyCloud.makePublicSpaceId,
1807
1975
  did: this.did,
1808
- address: this._address,
1976
+ address: this._address ?? "",
1809
1977
  chainId: this._chainId,
1810
1978
  hosts: [this.config.host]
1811
1979
  }
1812
1980
  });
1813
- this._vault.initialize(this._serviceContext);
1814
- this._serviceContext.registerService("vault", this._vault);
1815
- this.initializeV2Services(serviceSession);
1816
1981
  }
1817
1982
  /**
1818
1983
  * Initialize the v2 delegation system services.
@@ -1896,7 +2061,7 @@ var _TinyCloudNode = class _TinyCloudNode {
1896
2061
  this._delegationManager = new DelegationManager({
1897
2062
  hosts: [this.config.host],
1898
2063
  session: serviceSession,
1899
- invoke: this.wasmBindings.invoke,
2064
+ invoke: this.invokeWithRuntimePermissions,
1900
2065
  fetch: globalThis.fetch.bind(globalThis)
1901
2066
  });
1902
2067
  this._spaceService = new SpaceService({
@@ -1907,20 +2072,15 @@ var _TinyCloudNode = class _TinyCloudNode {
1907
2072
  capabilityRegistry: this._capabilityRegistry,
1908
2073
  userDid: this.did,
1909
2074
  createKVService: (spaceId) => {
1910
- const kvService = new KVService2({});
2075
+ return this.createSpaceScopedKVService(spaceId);
2076
+ },
2077
+ createVaultService: (spaceId) => {
2078
+ const kvService = this.createSpaceScopedKVService(spaceId);
2079
+ const vaultService = this.createVaultService(spaceId, kvService);
1911
2080
  if (this._serviceContext) {
1912
- const spaceScopedContext = new ServiceContext2({
1913
- invoke: this._serviceContext.invoke,
1914
- fetch: this._serviceContext.fetch,
1915
- hosts: this._serviceContext.hosts
1916
- });
1917
- const session = this._serviceContext.session;
1918
- if (session) {
1919
- spaceScopedContext.setSession({ ...session, spaceId });
1920
- }
1921
- kvService.initialize(spaceScopedContext);
2081
+ vaultService.initialize(this._serviceContext);
1922
2082
  }
1923
- return kvService;
2083
+ return vaultService;
1924
2084
  },
1925
2085
  // Enable space.delegations.create() via SIWE-based delegation
1926
2086
  createDelegation: async (params) => {
@@ -2157,6 +2317,33 @@ var _TinyCloudNode = class _TinyCloudNode {
2157
2317
  }
2158
2318
  return this._vault;
2159
2319
  }
2320
+ /**
2321
+ * App-facing secrets API backed by the `secrets` space vault.
2322
+ */
2323
+ get secrets() {
2324
+ if (!this._spaceService) {
2325
+ throw new Error("Not signed in. Call signIn() first.");
2326
+ }
2327
+ if (!this._secrets) {
2328
+ this._secrets = new NodeSecretsService({
2329
+ getService: () => this.getBaseSecrets(),
2330
+ getManifest: () => this.manifest,
2331
+ grantPermissions: (additional) => this.grantRuntimePermissions(additional),
2332
+ canEscalate: () => this.signer !== void 0 && this.tc !== void 0,
2333
+ getUnlockSigner: () => this.signer ?? void 0
2334
+ });
2335
+ }
2336
+ return this._secrets;
2337
+ }
2338
+ getBaseSecrets() {
2339
+ if (!this._spaceService) {
2340
+ throw new Error("Not signed in. Call signIn() first.");
2341
+ }
2342
+ if (!this._baseSecrets) {
2343
+ this._baseSecrets = new SecretsService(() => this.space("secrets").vault);
2344
+ }
2345
+ return this._baseSecrets;
2346
+ }
2160
2347
  /**
2161
2348
  * Hooks write stream subscription API.
2162
2349
  */
@@ -2227,6 +2414,171 @@ var _TinyCloudNode = class _TinyCloudNode {
2227
2414
  }
2228
2415
  };
2229
2416
  }
2417
+ /**
2418
+ * Check whether the current session or an approved runtime delegation covers
2419
+ * every requested permission.
2420
+ */
2421
+ hasRuntimePermissions(permissions) {
2422
+ const session = this.auth?.tinyCloudSession;
2423
+ if (!session || !Array.isArray(permissions) || permissions.length === 0) {
2424
+ return false;
2425
+ }
2426
+ const expanded = this.expandPermissionEntries(permissions);
2427
+ if (this.sessionCoversPermissionEntries(session, expanded)) {
2428
+ return true;
2429
+ }
2430
+ return this.findRuntimeGrantsForPermissionEntries(expanded, session).length > 0;
2431
+ }
2432
+ /**
2433
+ * Return installed runtime permission delegations. When `permissions` is
2434
+ * provided, only delegations currently covering those permissions are
2435
+ * returned. Base-session manifest permissions are not represented here.
2436
+ */
2437
+ getRuntimePermissionDelegations(permissions) {
2438
+ this.pruneExpiredRuntimePermissionGrants();
2439
+ if (permissions === void 0) {
2440
+ return this.runtimePermissionGrants.map((grant) => grant.delegation);
2441
+ }
2442
+ const session = this.auth?.tinyCloudSession;
2443
+ if (!session || !Array.isArray(permissions) || permissions.length === 0) {
2444
+ return [];
2445
+ }
2446
+ const expanded = this.expandPermissionEntries(permissions);
2447
+ return this.findRuntimeGrantsForPermissionEntries(expanded, session).map(
2448
+ (grant) => grant.delegation
2449
+ );
2450
+ }
2451
+ /**
2452
+ * Install a portable runtime permission delegation into this SDK instance so
2453
+ * matching service calls and downstream `delegateTo()` calls can use it.
2454
+ */
2455
+ async useRuntimeDelegation(delegation) {
2456
+ const session = this.auth?.tinyCloudSession;
2457
+ if (!session) {
2458
+ throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
2459
+ }
2460
+ if (delegation.expiry.getTime() <= Date.now()) {
2461
+ throw new SessionExpiredError(delegation.expiry);
2462
+ }
2463
+ const expectedDids = /* @__PURE__ */ new Set([session.verificationMethod, this.sessionDid]);
2464
+ if (!expectedDids.has(delegation.delegateDID)) {
2465
+ throw new Error(
2466
+ `Runtime delegation targets ${delegation.delegateDID} but this session key is ${session.verificationMethod}.`
2467
+ );
2468
+ }
2469
+ const targetHost = delegation.host ?? this.config.host;
2470
+ const activateResult = await activateSessionWithHost2(
2471
+ targetHost,
2472
+ delegation.delegationHeader
2473
+ );
2474
+ if (!activateResult.success) {
2475
+ throw new Error(
2476
+ `Failed to activate runtime permission delegation: ${activateResult.error}`
2477
+ );
2478
+ }
2479
+ this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
2480
+ (grant) => grant.delegation.cid !== delegation.cid
2481
+ );
2482
+ this.runtimePermissionGrants.push(
2483
+ this.runtimeGrantFromDelegation(delegation, session)
2484
+ );
2485
+ }
2486
+ /**
2487
+ * Store additional permissions as narrow delegations to the current session
2488
+ * key. Future service invocations automatically use a stored delegation when
2489
+ * its `(space, service, path, action)` covers the request.
2490
+ */
2491
+ async grantRuntimePermissions(permissions, options) {
2492
+ if (!Array.isArray(permissions) || permissions.length === 0) {
2493
+ throw new Error("grantRuntimePermissions requires a non-empty permissions array");
2494
+ }
2495
+ const session = this.auth?.tinyCloudSession;
2496
+ if (!session) {
2497
+ throw new SessionExpiredError(/* @__PURE__ */ new Date(0));
2498
+ }
2499
+ const sessionExpiry = extractSiweExpiration(session.siwe);
2500
+ if (sessionExpiry !== void 0) {
2501
+ const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
2502
+ if (sessionExpiry.getTime() <= Date.now() + marginMs) {
2503
+ throw new SessionExpiredError(sessionExpiry);
2504
+ }
2505
+ }
2506
+ const expanded = this.expandPermissionEntries(permissions);
2507
+ if (this.sessionCoversPermissionEntries(session, expanded)) {
2508
+ return [];
2509
+ }
2510
+ const existingGrants = this.findRuntimeGrantsForPermissionEntries(expanded, session);
2511
+ if (existingGrants.length > 0) {
2512
+ return existingGrants.map((grant) => grant.delegation);
2513
+ }
2514
+ if (!this.signer) {
2515
+ throw new Error(
2516
+ "grantRuntimePermissions requires wallet mode with a signer or privateKey."
2517
+ );
2518
+ }
2519
+ const bySpace = /* @__PURE__ */ new Map();
2520
+ for (const entry of expanded) {
2521
+ const spaceId = this.resolvePermissionSpace(entry.space, session);
2522
+ const current = bySpace.get(spaceId) ?? [];
2523
+ current.push(entry);
2524
+ bySpace.set(spaceId, current);
2525
+ }
2526
+ const now = /* @__PURE__ */ new Date();
2527
+ const requestedExpiryMs = resolveExpiryMs(options?.expiry);
2528
+ let expiresAt = new Date(now.getTime() + requestedExpiryMs);
2529
+ if (sessionExpiry !== void 0 && sessionExpiry < expiresAt) {
2530
+ expiresAt = sessionExpiry;
2531
+ }
2532
+ const delegations = [];
2533
+ for (const [spaceId, entries] of bySpace) {
2534
+ const abilities = this.permissionsToAbilities(entries);
2535
+ const prepared = this.wasmBindings.prepareSession({
2536
+ abilities,
2537
+ address: this.wasmBindings.ensureEip55(session.address),
2538
+ chainId: session.chainId,
2539
+ domain: this.siweDomain,
2540
+ issuedAt: now.toISOString(),
2541
+ expirationTime: expiresAt.toISOString(),
2542
+ spaceId,
2543
+ jwk: session.jwk
2544
+ });
2545
+ const signature = await this.signer.signMessage(prepared.siwe);
2546
+ const delegatedSession = this.wasmBindings.completeSessionSetup({
2547
+ ...prepared,
2548
+ signature
2549
+ });
2550
+ const activateResult = await activateSessionWithHost2(
2551
+ this.config.host,
2552
+ delegatedSession.delegationHeader
2553
+ );
2554
+ if (!activateResult.success) {
2555
+ throw new Error(
2556
+ `Failed to activate runtime permission delegation: ${activateResult.error}`
2557
+ );
2558
+ }
2559
+ const delegation = this.runtimeDelegationFromSession(
2560
+ delegatedSession,
2561
+ entries,
2562
+ spaceId,
2563
+ session,
2564
+ expiresAt
2565
+ );
2566
+ this.runtimePermissionGrants.push({
2567
+ session: {
2568
+ delegationHeader: delegatedSession.delegationHeader,
2569
+ delegationCid: delegatedSession.delegationCid,
2570
+ spaceId,
2571
+ verificationMethod: session.verificationMethod,
2572
+ jwk: session.jwk
2573
+ },
2574
+ delegation,
2575
+ operations: this.permissionOperations(entries, spaceId),
2576
+ expiresAt
2577
+ });
2578
+ delegations.push(delegation);
2579
+ }
2580
+ return delegations;
2581
+ }
2230
2582
  /**
2231
2583
  * Get the DelegationManager for delegation CRUD operations.
2232
2584
  *
@@ -2295,6 +2647,12 @@ var _TinyCloudNode = class _TinyCloudNode {
2295
2647
  get spaceService() {
2296
2648
  return this.spaces;
2297
2649
  }
2650
+ /**
2651
+ * Get a Space object by short name or full URI.
2652
+ */
2653
+ space(nameOrUri) {
2654
+ return this.spaces.get(nameOrUri);
2655
+ }
2298
2656
  /**
2299
2657
  * Get the SharingService for creating and receiving v2 sharing links.
2300
2658
  *
@@ -2409,7 +2767,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2409
2767
  if (this._serviceContext) {
2410
2768
  const publicKV = new KVService2({ prefix: "" });
2411
2769
  const publicContext = new ServiceContext2({
2412
- invoke: this.wasmBindings.invoke,
2770
+ invoke: this.invokeWithRuntimePermissions,
2413
2771
  fetch: this._serviceContext.fetch,
2414
2772
  hosts: this._serviceContext.hosts
2415
2773
  });
@@ -2497,8 +2855,9 @@ var _TinyCloudNode = class _TinyCloudNode {
2497
2855
  * Issue a delegation using the capability-chain flow.
2498
2856
  *
2499
2857
  * When every requested permission is a subset of the current
2500
- * session's recap, the delegation is signed by the session key via
2501
- * WASM no wallet prompt. When at least one is NOT derivable, a
2858
+ * session's recap, or of one installed runtime permission delegation,
2859
+ * the delegation is signed by the session key via WASM no wallet
2860
+ * prompt. When at least one is NOT derivable, a
2502
2861
  * {@link PermissionNotInManifestError} is raised (carrying the
2503
2862
  * missing entries) so the caller can trigger an escalation flow
2504
2863
  * (e.g. `TinyCloudWeb.requestPermissions`). Passing
@@ -2578,6 +2937,23 @@ var _TinyCloudNode = class _TinyCloudNode {
2578
2937
  );
2579
2938
  const { subset, missing } = isCapabilitySubset(expandedEntries, granted);
2580
2939
  if (!subset) {
2940
+ const runtimeGrant = this.findGrantForOperations(
2941
+ this.permissionEntriesToOperations(expandedEntries, session)
2942
+ );
2943
+ if (runtimeGrant) {
2944
+ const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
2945
+ if (runtimeGrant.expiresAt.getTime() <= Date.now() + marginMs) {
2946
+ throw new SessionExpiredError(runtimeGrant.expiresAt);
2947
+ }
2948
+ const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
2949
+ const delegation2 = await this.createDelegationViaRuntimeGrant(
2950
+ did,
2951
+ expandedEntries,
2952
+ runtimeExpiration,
2953
+ runtimeGrant
2954
+ );
2955
+ return { delegation: delegation2, prompted: false };
2956
+ }
2581
2957
  throw new PermissionNotInManifestError(missing, granted);
2582
2958
  }
2583
2959
  const delegation = await this.createDelegationViaWasmPath(
@@ -2722,6 +3098,41 @@ var _TinyCloudNode = class _TinyCloudNode {
2722
3098
  host: this.config.host
2723
3099
  };
2724
3100
  }
3101
+ async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant) {
3102
+ const result = this.createDelegationWrapper({
3103
+ session: grant.session,
3104
+ delegateDID: did,
3105
+ spaceId: grant.session.spaceId,
3106
+ abilities: this.permissionsToAbilities(entries),
3107
+ expirationSecs: Math.floor(expirationTime.getTime() / 1e3)
3108
+ });
3109
+ const primary = result.resources[0];
3110
+ const delegationHeader = { Authorization: result.delegation };
3111
+ const targetHost = grant.delegation.host ?? this.config.host;
3112
+ const activateResult = await activateSessionWithHost2(
3113
+ targetHost,
3114
+ delegationHeader
3115
+ );
3116
+ if (!activateResult.success) {
3117
+ throw new Error(
3118
+ `Failed to activate delegation with host: ${activateResult.error}`
3119
+ );
3120
+ }
3121
+ return {
3122
+ cid: result.cid,
3123
+ delegationHeader,
3124
+ spaceId: grant.session.spaceId,
3125
+ path: primary.path,
3126
+ actions: primary.actions,
3127
+ resources: result.resources,
3128
+ disableSubDelegation: false,
3129
+ expiry: result.expiry,
3130
+ delegateDID: did,
3131
+ ownerAddress: grant.delegation.ownerAddress,
3132
+ chainId: grant.delegation.chainId,
3133
+ host: targetHost
3134
+ };
3135
+ }
2725
3136
  resolvePermissionSpace(space, session) {
2726
3137
  if (space === void 0) {
2727
3138
  return this.wasmBindings.makeSpaceId(
@@ -2738,6 +3149,223 @@ var _TinyCloudNode = class _TinyCloudNode {
2738
3149
  }
2739
3150
  return this.wasmBindings.makeSpaceId(session.address, session.chainId, space);
2740
3151
  }
3152
+ expandPermissionEntries(permissions) {
3153
+ return permissions.map((entry) => ({
3154
+ ...entry,
3155
+ actions: expandActionShortNames(entry.service, entry.actions)
3156
+ }));
3157
+ }
3158
+ shortServiceName(service) {
3159
+ const short = SERVICE_LONG_TO_SHORT[service];
3160
+ if (short === void 0) {
3161
+ throw new Error(
3162
+ `unknown service '${service}' \u2014 no short-form mapping`
3163
+ );
3164
+ }
3165
+ return short;
3166
+ }
3167
+ permissionsToAbilities(entries) {
3168
+ const abilities = {};
3169
+ for (const entry of entries) {
3170
+ const service = this.shortServiceName(entry.service);
3171
+ abilities[service] ?? (abilities[service] = {});
3172
+ const existing = abilities[service][entry.path] ?? [];
3173
+ const seen = new Set(existing);
3174
+ for (const action of entry.actions) {
3175
+ if (!seen.has(action)) {
3176
+ existing.push(action);
3177
+ seen.add(action);
3178
+ }
3179
+ }
3180
+ abilities[service][entry.path] = existing;
3181
+ }
3182
+ return abilities;
3183
+ }
3184
+ permissionOperations(entries, spaceId) {
3185
+ return entries.flatMap((entry) => {
3186
+ const service = this.shortServiceName(entry.service);
3187
+ return entry.actions.map((action) => ({
3188
+ spaceId,
3189
+ service,
3190
+ path: entry.path,
3191
+ action
3192
+ }));
3193
+ });
3194
+ }
3195
+ sessionCoversPermissionEntries(session, entries) {
3196
+ try {
3197
+ const granted = parseRecapCapabilities(
3198
+ (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
3199
+ session.siwe
3200
+ );
3201
+ return isCapabilitySubset(entries, granted).subset;
3202
+ } catch {
3203
+ return false;
3204
+ }
3205
+ }
3206
+ permissionEntriesToOperations(entries, session) {
3207
+ return entries.flatMap((entry) => {
3208
+ const spaceId = this.resolvePermissionSpace(entry.space, session);
3209
+ const service = this.shortServiceName(entry.service);
3210
+ return entry.actions.map((action) => ({
3211
+ spaceId,
3212
+ service,
3213
+ path: entry.path,
3214
+ action
3215
+ }));
3216
+ });
3217
+ }
3218
+ findRuntimeGrantsForPermissionEntries(entries, session) {
3219
+ const grants = [];
3220
+ const operations = this.permissionEntriesToOperations(entries, session);
3221
+ if (operations.length === 0) {
3222
+ return grants;
3223
+ }
3224
+ for (const operation of operations) {
3225
+ const grant = this.findGrantForOperation(operation);
3226
+ if (!grant) {
3227
+ return [];
3228
+ }
3229
+ if (!grants.includes(grant)) {
3230
+ grants.push(grant);
3231
+ }
3232
+ }
3233
+ return grants;
3234
+ }
3235
+ runtimeDelegationFromSession(delegatedSession, entries, spaceId, session, expiresAt) {
3236
+ const resources = this.delegatedResourcesForEntries(entries, spaceId);
3237
+ const primary = resources[0];
3238
+ return {
3239
+ cid: delegatedSession.delegationCid,
3240
+ delegationHeader: delegatedSession.delegationHeader,
3241
+ spaceId,
3242
+ path: primary.path,
3243
+ actions: primary.actions,
3244
+ resources,
3245
+ disableSubDelegation: false,
3246
+ expiry: expiresAt,
3247
+ delegateDID: session.verificationMethod,
3248
+ ownerAddress: session.address,
3249
+ chainId: session.chainId,
3250
+ host: this.config.host
3251
+ };
3252
+ }
3253
+ runtimeGrantFromDelegation(delegation, session) {
3254
+ const operations = this.operationsFromDelegation(delegation);
3255
+ return {
3256
+ session: {
3257
+ delegationHeader: delegation.delegationHeader,
3258
+ delegationCid: delegation.cid,
3259
+ spaceId: delegation.spaceId,
3260
+ verificationMethod: session.verificationMethod,
3261
+ jwk: session.jwk
3262
+ },
3263
+ delegation,
3264
+ operations,
3265
+ expiresAt: delegation.expiry
3266
+ };
3267
+ }
3268
+ delegatedResourcesForEntries(entries, spaceId) {
3269
+ return entries.map((entry) => ({
3270
+ service: this.shortServiceName(entry.service),
3271
+ space: spaceId,
3272
+ path: entry.path,
3273
+ actions: [...entry.actions]
3274
+ }));
3275
+ }
3276
+ operationsFromDelegation(delegation) {
3277
+ const resources = delegation.resources !== void 0 && delegation.resources.length > 0 ? delegation.resources : this.flatDelegationResources(delegation);
3278
+ return resources.flatMap(
3279
+ (resource) => resource.actions.map((action) => ({
3280
+ spaceId: resource.space,
3281
+ service: this.invocationServiceName(resource.service),
3282
+ path: resource.path,
3283
+ action
3284
+ }))
3285
+ );
3286
+ }
3287
+ flatDelegationResources(delegation) {
3288
+ const byService = /* @__PURE__ */ new Map();
3289
+ for (const action of delegation.actions) {
3290
+ const service = this.shortServiceName(action.split("/")[0]);
3291
+ const actions = byService.get(service) ?? [];
3292
+ actions.push(action);
3293
+ byService.set(service, actions);
3294
+ }
3295
+ return [...byService.entries()].map(([service, actions]) => ({
3296
+ service,
3297
+ space: delegation.spaceId,
3298
+ path: delegation.path,
3299
+ actions
3300
+ }));
3301
+ }
3302
+ selectInvocationSession(fallback, service, path, action) {
3303
+ const grant = this.findGrantForOperation({
3304
+ spaceId: fallback.spaceId,
3305
+ service: this.invocationServiceName(service),
3306
+ path,
3307
+ action
3308
+ });
3309
+ return grant?.session ?? fallback;
3310
+ }
3311
+ findGrantForOperations(operations) {
3312
+ if (operations.length === 0) {
3313
+ return void 0;
3314
+ }
3315
+ this.pruneExpiredRuntimePermissionGrants();
3316
+ return this.runtimePermissionGrants.find((grant) => {
3317
+ return operations.every(
3318
+ (operation) => grant.operations.some(
3319
+ (granted) => this.operationCovers(granted, operation)
3320
+ )
3321
+ );
3322
+ });
3323
+ }
3324
+ findGrantForOperation(operation) {
3325
+ return this.findGrantForOperations([operation]);
3326
+ }
3327
+ pruneExpiredRuntimePermissionGrants() {
3328
+ const now = Date.now();
3329
+ this.runtimePermissionGrants = this.runtimePermissionGrants.filter(
3330
+ (grant) => grant.expiresAt.getTime() > now
3331
+ );
3332
+ }
3333
+ operationCovers(granted, requested) {
3334
+ return granted.spaceId === requested.spaceId && granted.service === requested.service && this.actionContains(granted.action, requested.action) && this.pathContains(granted.path, requested.path);
3335
+ }
3336
+ actionContains(grantedAction, requestedAction) {
3337
+ if (grantedAction === requestedAction) {
3338
+ return true;
3339
+ }
3340
+ if (grantedAction.endsWith("/*")) {
3341
+ const prefix = grantedAction.slice(0, -2);
3342
+ return requestedAction.startsWith(`${prefix}/`);
3343
+ }
3344
+ return false;
3345
+ }
3346
+ invocationServiceName(service) {
3347
+ return service.startsWith("tinycloud.") ? this.shortServiceName(service) : service;
3348
+ }
3349
+ pathContains(grantedPath, requestedPath) {
3350
+ if (grantedPath === "" || grantedPath === "/") {
3351
+ return true;
3352
+ }
3353
+ if (grantedPath.endsWith("/**")) {
3354
+ return requestedPath.startsWith(grantedPath.slice(0, -3));
3355
+ }
3356
+ if (grantedPath.endsWith("/*")) {
3357
+ const prefix = grantedPath.slice(0, -2);
3358
+ if (!requestedPath.startsWith(prefix)) {
3359
+ return false;
3360
+ }
3361
+ const remainder = requestedPath.slice(prefix.length);
3362
+ return !remainder.includes("/") || remainder === "/";
3363
+ }
3364
+ if (grantedPath.endsWith("/")) {
3365
+ return requestedPath.startsWith(grantedPath);
3366
+ }
3367
+ return grantedPath === requestedPath;
3368
+ }
2741
3369
  /**
2742
3370
  * Issue a delegation via the legacy wallet-signed SIWE path for a single
2743
3371
  * {@link PermissionEntry}. Shares the implementation with the public
@@ -3129,7 +3757,7 @@ import {
3129
3757
  SessionExpiredError as SessionExpiredError2,
3130
3758
  ManifestValidationError,
3131
3759
  composeManifestRequest as composeManifestRequest2,
3132
- resolveManifest,
3760
+ resolveManifest as resolveManifest2,
3133
3761
  validateManifest,
3134
3762
  loadManifest,
3135
3763
  isCapabilitySubset as isCapabilitySubset2,
@@ -3158,7 +3786,7 @@ function deserializeDelegation(data) {
3158
3786
  import { KVService as KVService3, PrefixedKVService } from "@tinycloud/sdk-core";
3159
3787
  import { SQLService as SQLService3, SQLAction, DatabaseHandle } from "@tinycloud/sdk-core";
3160
3788
  import { DuckDbService as DuckDbService3, DuckDbDatabaseHandle, DuckDbAction } from "@tinycloud/sdk-core";
3161
- import { DataVaultService as DataVaultService2, VaultHeaders, VaultPublicSpaceKVActions, createVaultCrypto as createVaultCrypto2 } from "@tinycloud/sdk-core";
3789
+ import { DataVaultService as DataVaultService2, VaultHeaders, VaultPublicSpaceKVActions, createVaultCrypto as createVaultCrypto2, SecretsService as SecretsService2 } from "@tinycloud/sdk-core";
3162
3790
  import {
3163
3791
  DelegationManager as DelegationManager2,
3164
3792
  SharingService as SharingService2,
@@ -3212,6 +3840,7 @@ export {
3212
3840
  ProtocolMismatchError,
3213
3841
  SQLAction,
3214
3842
  SQLService3 as SQLService,
3843
+ SecretsService2 as SecretsService,
3215
3844
  ServiceContext3 as ServiceContext,
3216
3845
  SessionExpiredError2 as SessionExpiredError,
3217
3846
  SharingService2 as SharingService,
@@ -3243,7 +3872,7 @@ export {
3243
3872
  makePublicSpaceId2 as makePublicSpaceId,
3244
3873
  parseExpiry2 as parseExpiry,
3245
3874
  parseSpaceUri,
3246
- resolveManifest,
3875
+ resolveManifest2 as resolveManifest,
3247
3876
  resourceCapabilitiesToSpaceAbilitiesMap2 as resourceCapabilitiesToSpaceAbilitiesMap,
3248
3877
  serializeDelegation,
3249
3878
  validateManifest