@tinycloud/node-sdk 2.11.0-beta.8 → 2.11.0

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.cjs CHANGED
@@ -322,6 +322,8 @@ var FileSessionStorage = class {
322
322
  };
323
323
 
324
324
  // src/authorization/NodeUserAuthorization.ts
325
+ var import_node_crypto = require("crypto");
326
+ var import_siwe = require("siwe");
325
327
  var import_sdk_core3 = require("@tinycloud/sdk-core");
326
328
 
327
329
  // src/authorization/strategies.ts
@@ -331,6 +333,27 @@ var defaultSignStrategy = { type: "auto-sign" };
331
333
  // src/authorization/NodeUserAuthorization.ts
332
334
  var DECRYPT_ACTION = import_sdk_core3.ENCRYPTION.DECRYPT;
333
335
  var NETWORK_CREATE_ACTION = import_sdk_core3.ENCRYPTION.NETWORK_CREATE;
336
+ function canonicalizeOpenKeyManifestJson(value) {
337
+ const json = JSON.stringify(value);
338
+ if (json === void 0) {
339
+ throw new TypeError("manifest is not JSON-serializable");
340
+ }
341
+ return canonicalizeJsonValue(JSON.parse(json));
342
+ }
343
+ function canonicalizeJsonValue(value) {
344
+ if (value === null || typeof value !== "object") {
345
+ return JSON.stringify(value);
346
+ }
347
+ if (Array.isArray(value)) {
348
+ return `[${value.map(canonicalizeJsonValue).join(",")}]`;
349
+ }
350
+ return `{${Object.keys(value).sort().map(
351
+ (key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(value[key])}`
352
+ ).join(",")}}`;
353
+ }
354
+ function canonicalOpenKeyManifestSha256Hex(value) {
355
+ return (0, import_node_crypto.createHash)("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
356
+ }
334
357
  function didPrincipalMatches(actual, expected) {
335
358
  try {
336
359
  return (0, import_sdk_core3.principalDidEquals)(actual, expected);
@@ -348,6 +371,16 @@ function addRawAbility(rawAbilities, resource, action) {
348
371
  actions.push(action);
349
372
  }
350
373
  }
374
+ function cloneAbilitiesMap(actions) {
375
+ return Object.fromEntries(
376
+ Object.entries(actions).map(([service, paths]) => [
377
+ service,
378
+ Object.fromEntries(
379
+ Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
380
+ )
381
+ ])
382
+ );
383
+ }
351
384
  var NodeUserAuthorization = class {
352
385
  constructor(config) {
353
386
  this.activeSessionKeyId = "default";
@@ -619,23 +652,41 @@ var NodeUserAuthorization = class {
619
652
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
620
653
  const primarySpaceId2 = (0, import_sdk_core3.makePkhSpaceId)(address, chainId, this.spacePrefix);
621
654
  const secretsSpaceId = (0, import_sdk_core3.makePkhSpaceId)(address, chainId, "secrets");
622
- return {
623
- abilities: this.defaultActions,
624
- spaceId: primarySpaceId2,
625
- spaceAbilities: {
626
- [primarySpaceId2]: this.defaultActions,
627
- [secretsSpaceId]: {
628
- kv: {
629
- "vault/secrets/": [
630
- import_sdk_core3.KV.GET,
631
- import_sdk_core3.KV.PUT,
632
- import_sdk_core3.KV.DEL,
633
- import_sdk_core3.KV.LIST,
634
- import_sdk_core3.KV.METADATA
635
- ]
655
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === import_sdk_core3.ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
656
+ const spaceAbilities2 = {
657
+ [primarySpaceId2]: primaryActions,
658
+ [secretsSpaceId]: {
659
+ kv: {
660
+ "vault/secrets/": [
661
+ import_sdk_core3.KV.GET,
662
+ import_sdk_core3.KV.PUT,
663
+ import_sdk_core3.KV.DEL,
664
+ import_sdk_core3.KV.LIST,
665
+ import_sdk_core3.KV.METADATA
666
+ ]
667
+ }
668
+ }
669
+ };
670
+ if (this.includeAccountRegistryPermissions && this.spacePrefix !== import_sdk_core3.ACCOUNT_REGISTRY_SPACE) {
671
+ spaceAbilities2[(0, import_sdk_core3.makePkhSpaceId)(address, chainId, import_sdk_core3.ACCOUNT_REGISTRY_SPACE)] = (0, import_sdk_core3.resourceCapabilitiesToAbilitiesMap)(import_sdk_core3.ACCOUNT_MANIFEST_PERMISSIONS);
672
+ } else if (this.includeAccountRegistryPermissions) {
673
+ const accountAbilities = (0, import_sdk_core3.resourceCapabilitiesToAbilitiesMap)(
674
+ import_sdk_core3.ACCOUNT_MANIFEST_PERMISSIONS
675
+ );
676
+ for (const [service, paths] of Object.entries(accountAbilities)) {
677
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
678
+ for (const [path, actions] of Object.entries(paths)) {
679
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
680
+ for (const action of actions) {
681
+ if (!existingActions.includes(action)) existingActions.push(action);
636
682
  }
637
683
  }
638
- },
684
+ }
685
+ }
686
+ return {
687
+ abilities: primaryActions,
688
+ spaceId: primarySpaceId2,
689
+ spaceAbilities: spaceAbilities2,
639
690
  rawAbilities: {
640
691
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
641
692
  }
@@ -1216,6 +1267,398 @@ var NodeUserAuthorization = class {
1216
1267
  await this.ensureSpaceExists();
1217
1268
  return clientSession;
1218
1269
  }
1270
+ /**
1271
+ * End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
1272
+ *
1273
+ * This is the production entry point that wires
1274
+ * {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
1275
+ * {@link signInWithOpenKeyResult} into one call. Consumers pass in a
1276
+ * caller-provided `authorizeFn` (typically a thin wrapper around
1277
+ * `openkey.authorizeTinyCloud()`) so this method has no direct
1278
+ * dependency on the OpenKey SDK — it works with any transport that
1279
+ * conforms to the versioned protocol.
1280
+ *
1281
+ * The `authorizeFn` MUST:
1282
+ * 1. Send the prepared SIWE bytes to OpenKey.
1283
+ * 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
1284
+ * `signedMessage` — the bytes the signature actually verifies
1285
+ * against — plus `selectedActionKeys` and `permissions`).
1286
+ *
1287
+ * NodeUserAuthorization then completes the session with `signedMessage`
1288
+ * (never the prepared bytes), verifying the signature, the immutable
1289
+ * SIWE fields, and every subset invariant enforced by
1290
+ * {@link signInWithOpenKeyResult}. This preserves the exact-byte
1291
+ * guarantee: if the user did NOT narrow, the returned `signedMessage`
1292
+ * equals the prepared SIWE byte-for-byte; if the user narrowed, only
1293
+ * the ReCap block differs and every immutable header field survives.
1294
+ *
1295
+ * Legacy exact-byte {@link signIn} callers are unaffected — this method
1296
+ * is opt-in.
1297
+ *
1298
+ * @param authorizeFn Callback that invokes the versioned OpenKey
1299
+ * `authorizeTinyCloud()` protocol and returns the result. Receives
1300
+ * the prepared SIWE bytes plus a `host` hint the widget should bind
1301
+ * into its /authorize-sign-prepare context.
1302
+ * @param options Optional overrides for the OpenKey request.
1303
+ */
1304
+ async signInWithOpenKey(authorizeFn, options) {
1305
+ if (options?.host !== void 0) {
1306
+ const overrideHost = options.host.trim();
1307
+ if (!overrideHost) {
1308
+ throw new Error("OpenKey authorization host must not be empty");
1309
+ }
1310
+ this.tinycloudHosts = [overrideHost];
1311
+ }
1312
+ const signingAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
1313
+ const signingChainId = await this.signer.getChainId();
1314
+ await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
1315
+ const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
1316
+ const presentation = this.buildPresentationEnvelope(options?.reason);
1317
+ const hostHint = this.primaryTinyCloudHost;
1318
+ const result = await authorizeFn({
1319
+ protocolVersion: 1,
1320
+ siwe: prepared.siwe,
1321
+ jwk,
1322
+ host: hostHint,
1323
+ keyId: options?.openkeyKeyId,
1324
+ presentation
1325
+ });
1326
+ return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
1327
+ }
1328
+ /**
1329
+ * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
1330
+ * envelope is display-only — the OpenKey widget renders it under an
1331
+ * honest trust label, and the server /authorize-sign-prepare route
1332
+ * decides whether to upgrade the label to `origin-bound` by fetching
1333
+ * the well-known manifest and comparing its digest to
1334
+ * `manifestDigest`.
1335
+ *
1336
+ * Returns `undefined` when no manifest is set — the widget renders as
1337
+ * "no manifest supplied" (unsigned).
1338
+ *
1339
+ * The digest is a deterministic SHA-256 over canonical JSON of the
1340
+ * first manifest payload; OpenKey parses the fetched well-known manifest
1341
+ * and applies the same canonicalization before comparing digests.
1342
+ */
1343
+ buildPresentationEnvelope(reason) {
1344
+ const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
1345
+ if (manifests.length === 0 && !reason) {
1346
+ return void 0;
1347
+ }
1348
+ const primary = manifests[0];
1349
+ let displayName;
1350
+ let manifestId;
1351
+ let manifestDigest;
1352
+ const envelopeManifests = [];
1353
+ for (const m of manifests) {
1354
+ envelopeManifests.push({
1355
+ name: m.name,
1356
+ appId: m.app_id,
1357
+ // Deep-copy to prevent later mutations from affecting the digest.
1358
+ payload: JSON.parse(JSON.stringify(m))
1359
+ });
1360
+ }
1361
+ if (primary) {
1362
+ displayName = primary.name;
1363
+ manifestId = primary.app_id;
1364
+ manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
1365
+ }
1366
+ return {
1367
+ protocolVersion: 1,
1368
+ displayName,
1369
+ reason: reason && reason.length > 0 ? reason : void 0,
1370
+ manifestId,
1371
+ manifestDigest,
1372
+ manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
1373
+ };
1374
+ }
1375
+ /**
1376
+ * Complete sign-in with a versioned OpenKey authorization result.
1377
+ *
1378
+ * Where `signInWithPreparedSession` completes with a signature over the
1379
+ * ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
1380
+ * OpenKey widget actually presented and signed (`signedMessage`). It
1381
+ * exists because the versioned authorizeTinyCloud protocol lets the user
1382
+ * narrow capabilities; the returned bytes may differ from the original
1383
+ * request and are the only ones the signature actually verifies against.
1384
+ *
1385
+ * The `result` parameter is validated at the trust boundary via
1386
+ * `validateAuthorizationResultV1` (in sdk-core). Callers should validate
1387
+ * BEFORE calling this method so unknown/missing fields are rejected
1388
+ * before any session state is created.
1389
+ *
1390
+ * Validation performed here (defence-in-depth: even a "valid" v1 payload
1391
+ * must clear these checks before any session state is created):
1392
+ * 1. `result.address` matches the local signer's address.
1393
+ * 2. `result.signature` recovers to `result.address` when verified against
1394
+ * `result.signedMessage`.
1395
+ * 3. Every immutable SIWE header field (domain, address, URI, version,
1396
+ * chainId, nonce, issuedAt) in the original prepared SIWE is preserved
1397
+ * byte-for-byte in `result.signedMessage`. `statement` is NOT in this
1398
+ * list because the WASM renders it from the ReCap contents; it MUST
1399
+ * change when the ReCap is narrowed.
1400
+ * 4. Every capability in `result.signedMessage`'s ReCap block was present
1401
+ * in the original prepared SIWE — narrowing OK, broadening rejected.
1402
+ * 5. Every `result.selectedActionKeys` entry corresponds to a
1403
+ * (resource, action) pair in the returned SIWE.
1404
+ */
1405
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
1406
+ if (result.protocolVersion !== 1) {
1407
+ throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
1408
+ }
1409
+ if (!result.signedMessage) {
1410
+ throw new Error("OpenKey result must include signedMessage");
1411
+ }
1412
+ if (!prepared.siwe) {
1413
+ throw new Error(
1414
+ "signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
1415
+ );
1416
+ }
1417
+ const expectedAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
1418
+ if ((0, import_sdk_core3.canonicalizeAddress)(result.address) !== expectedAddress) {
1419
+ throw new Error(
1420
+ `OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
1421
+ );
1422
+ }
1423
+ let parsedSignedSiwe;
1424
+ try {
1425
+ parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
1426
+ } catch (err) {
1427
+ throw new Error(
1428
+ `OpenKey signedMessage is not a parseable SIWE: ${err.message}`
1429
+ );
1430
+ }
1431
+ const verifyResult = await parsedSignedSiwe.verify(
1432
+ { signature: result.signature },
1433
+ { suppressExceptions: true }
1434
+ );
1435
+ if (!verifyResult.success) {
1436
+ const errData = verifyResult.error;
1437
+ const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
1438
+ const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
1439
+ throw new Error(
1440
+ `OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
1441
+ );
1442
+ }
1443
+ if ((0, import_sdk_core3.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
1444
+ throw new Error(
1445
+ `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
1446
+ );
1447
+ }
1448
+ const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
1449
+ const originalFields = (0, import_sdk_core3.extractImmutableSiweFields)(prepared.siwe);
1450
+ const signedFields = (0, import_sdk_core3.extractImmutableSiweFields)(result.signedMessage);
1451
+ const changedFields = (0, import_sdk_core3.diffImmutableSiweFields)(originalFields, signedFields, {
1452
+ originalHasRecap
1453
+ });
1454
+ if (changedFields.length > 0) {
1455
+ throw new Error(
1456
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
1457
+ );
1458
+ }
1459
+ let originalCaps;
1460
+ let signedCaps;
1461
+ try {
1462
+ originalCaps = (0, import_sdk_core3.extractRecapAttenuations)(prepared.siwe);
1463
+ } catch (err) {
1464
+ throw new Error(
1465
+ `Failed to decode prepared SIWE recap: ${err.message}`
1466
+ );
1467
+ }
1468
+ try {
1469
+ signedCaps = (0, import_sdk_core3.extractRecapAttenuations)(result.signedMessage);
1470
+ } catch (err) {
1471
+ throw new Error(
1472
+ `Failed to decode OpenKey signedMessage recap: ${err.message}`
1473
+ );
1474
+ }
1475
+ const unauthorized = (0, import_sdk_core3.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
1476
+ if (unauthorized.length > 0) {
1477
+ const previewLimit = 5;
1478
+ const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
1479
+ const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
1480
+ throw new Error(
1481
+ `OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
1482
+ );
1483
+ }
1484
+ if (!result.signedMessage.includes(prepared.spaceId)) {
1485
+ throw new Error(
1486
+ `OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
1487
+ );
1488
+ }
1489
+ const grantedPairs = /* @__PURE__ */ new Set();
1490
+ const grantedFourPartIndex = /* @__PURE__ */ new Map();
1491
+ for (const [resource, actions] of Object.entries(signedCaps)) {
1492
+ const { space, path } = (0, import_sdk_core3.parseCanonicalRecapResource)(resource);
1493
+ for (const action of Object.keys(actions)) {
1494
+ grantedPairs.add(`${resource}\0${action}`);
1495
+ const slashIdx = action.indexOf("/");
1496
+ const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
1497
+ if (service) {
1498
+ grantedFourPartIndex.set(
1499
+ `${service}\0${space}\0${path}\0${action}`,
1500
+ `${resource}\0${action}`
1501
+ );
1502
+ }
1503
+ }
1504
+ }
1505
+ const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
1506
+ const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
1507
+ for (const pair of grantedPairs) {
1508
+ const idx = pair.indexOf("\0");
1509
+ const action = idx >= 0 ? pair.slice(idx + 1) : pair;
1510
+ if (!isStructurallyRequired(action)) {
1511
+ nonRequiredGrantedPairs.add(pair);
1512
+ }
1513
+ }
1514
+ const allSignedGrantedPairs = new Set(grantedPairs);
1515
+ const seenRawKeys = /* @__PURE__ */ new Set();
1516
+ for (const rawKey of result.selectedActionKeys) {
1517
+ if (seenRawKeys.has(rawKey)) {
1518
+ throw new Error(
1519
+ `OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
1520
+ );
1521
+ }
1522
+ seenRawKeys.add(rawKey);
1523
+ }
1524
+ const selectedResourceActionPairs = /* @__PURE__ */ new Set();
1525
+ for (const rawKey of result.selectedActionKeys) {
1526
+ const parts = rawKey.split("\0");
1527
+ if (parts.length !== 4) {
1528
+ throw new Error(
1529
+ `OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
1530
+ );
1531
+ }
1532
+ const canonical = grantedFourPartIndex.get(rawKey);
1533
+ if (!canonical) {
1534
+ throw new Error(
1535
+ `OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
1536
+ );
1537
+ }
1538
+ selectedResourceActionPairs.add(canonical);
1539
+ }
1540
+ if (nonRequiredGrantedPairs.size > 0) {
1541
+ const missingPairs = [];
1542
+ for (const pair of nonRequiredGrantedPairs) {
1543
+ if (!selectedResourceActionPairs.has(pair)) {
1544
+ missingPairs.push(pair);
1545
+ }
1546
+ }
1547
+ if (missingPairs.length > 0) {
1548
+ const previewLimit = 5;
1549
+ const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1550
+ const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
1551
+ throw new Error(
1552
+ `OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
1553
+ );
1554
+ }
1555
+ }
1556
+ if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
1557
+ throw new Error(
1558
+ "OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
1559
+ );
1560
+ }
1561
+ const permissionCanonicalKeys = /* @__PURE__ */ new Set();
1562
+ const permissionPairsCovered = /* @__PURE__ */ new Set();
1563
+ for (const perm of result.permissions) {
1564
+ const { service, space, path, actions } = perm;
1565
+ if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
1566
+ throw new Error(
1567
+ `OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
1568
+ );
1569
+ }
1570
+ if (!Array.isArray(actions) || actions.length === 0) {
1571
+ throw new Error(
1572
+ `OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
1573
+ );
1574
+ }
1575
+ const canonicalKey = `${service}\0${space}\0${path}`;
1576
+ if (permissionCanonicalKeys.has(canonicalKey)) {
1577
+ throw new Error(
1578
+ `OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
1579
+ );
1580
+ }
1581
+ permissionCanonicalKeys.add(canonicalKey);
1582
+ let expectedResource;
1583
+ if (space.startsWith("tinycloud:")) {
1584
+ const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
1585
+ expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
1586
+ } else {
1587
+ if (path) {
1588
+ throw new Error(
1589
+ `OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
1590
+ );
1591
+ }
1592
+ expectedResource = space;
1593
+ }
1594
+ let matchedResource;
1595
+ if (signedCaps[expectedResource] !== void 0) {
1596
+ matchedResource = expectedResource;
1597
+ } else {
1598
+ throw new Error(
1599
+ `OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
1600
+ );
1601
+ }
1602
+ const grantedActionsForResource = new Set(
1603
+ Object.keys(signedCaps[matchedResource])
1604
+ );
1605
+ for (const action of actions) {
1606
+ if (typeof action !== "string" || !action) {
1607
+ throw new Error(
1608
+ `OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
1609
+ );
1610
+ }
1611
+ if (!grantedActionsForResource.has(action)) {
1612
+ throw new Error(
1613
+ `OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
1614
+ );
1615
+ }
1616
+ permissionPairsCovered.add(`${matchedResource}\0${action}`);
1617
+ }
1618
+ }
1619
+ if (allSignedGrantedPairs.size > 0) {
1620
+ const missingFromPerms = [];
1621
+ for (const pair of allSignedGrantedPairs) {
1622
+ if (!permissionPairsCovered.has(pair)) {
1623
+ missingFromPerms.push(pair);
1624
+ }
1625
+ }
1626
+ if (missingFromPerms.length > 0) {
1627
+ const previewLimit = 5;
1628
+ const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1629
+ const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
1630
+ throw new Error(
1631
+ `OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
1632
+ );
1633
+ }
1634
+ const extrasInPerms = [];
1635
+ for (const pair of permissionPairsCovered) {
1636
+ if (!allSignedGrantedPairs.has(pair)) {
1637
+ extrasInPerms.push(pair);
1638
+ }
1639
+ }
1640
+ if (extrasInPerms.length > 0) {
1641
+ const previewLimit = 5;
1642
+ const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1643
+ const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
1644
+ throw new Error(
1645
+ `OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
1646
+ );
1647
+ }
1648
+ }
1649
+ void nonRequiredGrantedPairs;
1650
+ return this.signInWithPreparedSession(
1651
+ {
1652
+ siwe: result.signedMessage,
1653
+ jwk: prepared.jwk,
1654
+ spaceId: prepared.spaceId,
1655
+ verificationMethod: prepared.verificationMethod
1656
+ },
1657
+ result.signature,
1658
+ keyId,
1659
+ jwk
1660
+ );
1661
+ }
1219
1662
  /**
1220
1663
  * Clear persisted session data.
1221
1664
  */
@@ -1710,6 +2153,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
1710
2153
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
1711
2154
  var NETWORK_CREATE_ACTION2 = import_sdk_core8.ENCRYPTION.NETWORK_CREATE;
1712
2155
  var DECRYPT_ACTION2 = import_sdk_core8.ENCRYPTION.DECRYPT;
2156
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
2157
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
2158
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
1713
2159
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
1714
2160
  function isSecretPayload(value) {
1715
2161
  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";
@@ -1752,6 +2198,9 @@ function decodeAuthorizationBytes(authorization) {
1752
2198
  function isOpenKeyAutoSignStrategy(strategy) {
1753
2199
  return strategy?.openKeyAutoSign === true;
1754
2200
  }
2201
+ function openKeyApproval(strategy) {
2202
+ return strategy?.openKeyRequestApproval;
2203
+ }
1755
2204
  function isInteractiveSigner(config) {
1756
2205
  if (config.privateKey) {
1757
2206
  return false;
@@ -2117,9 +2566,12 @@ var _TinyCloudNode = class _TinyCloudNode {
2117
2566
  this._bootstrapSkipped = false;
2118
2567
  /**
2119
2568
  * Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
2120
- * true when bootstrap did not complete (interactive signer, auto-sign
2121
- * denied, or a bootstrap step failed); `reason` carries the cause so apps
2122
- * can surface a "finish account setup" call-to-action. `warnings` is
2569
+ * true when bootstrap did not complete in this invocation (interactive signer,
2570
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
2571
+ * step failed). `already-provisioned` means the bootstrap was not run here;
2572
+ * it does not mean a prior provisioning attempt failed to complete.
2573
+ * `reason` carries the cause so apps can surface a "finish account setup"
2574
+ * call-to-action. `warnings` is
2123
2575
  * present when bootstrap completed, but one or more steps recovered from
2124
2576
  * an ambiguous failure (e.g. a KV batch write reconciled via per-space
2125
2577
  * fallback) — clean vs. recovered runs are otherwise byte-identical.
@@ -2302,7 +2754,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2302
2754
  nonce: config.nonce,
2303
2755
  siweConfig: config.siweConfig,
2304
2756
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
2305
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2757
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2306
2758
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
2307
2759
  });
2308
2760
  this.tc = new import_sdk_core8.TinyCloud(this.auth, {
@@ -2392,6 +2844,15 @@ var _TinyCloudNode = class _TinyCloudNode {
2392
2844
  get sessionDid() {
2393
2845
  return this.sessionManager.getDID(this.sessionKeyId);
2394
2846
  }
2847
+ /** Bare did:key principal used as the credential subject and holder binding. */
2848
+ get credentialHolderDid() {
2849
+ return this.sessionDid.split("#", 1)[0];
2850
+ }
2851
+ /** Canonical verification method for the active credential holder key. */
2852
+ get credentialHolderKid() {
2853
+ const holder = this.credentialHolderDid;
2854
+ return `${holder}#${holder.slice("did:key:".length)}`;
2855
+ }
2395
2856
  /**
2396
2857
  * Return the current session's signed ReCap capabilities after the session
2397
2858
  * has been authenticated or restored. This is intentionally distinct from
@@ -2545,6 +3006,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2545
3006
  this._bootstrapSkipped = false;
2546
3007
  this._bootstrapStatus = { skipped: false };
2547
3008
  if (this.config.autoBootstrapAccount === false) {
3009
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
2548
3010
  return false;
2549
3011
  }
2550
3012
  if (!this.auth || !this._address) {
@@ -2559,11 +3021,14 @@ var _TinyCloudNode = class _TinyCloudNode {
2559
3021
  return false;
2560
3022
  }
2561
3023
  const steps = (0, import_sdk_core8.bootstrapSteps)(this._address, this._chainId);
2562
- if (!await this.isFreshBootstrapAccount(steps)) {
2563
- return false;
2564
- }
2565
3024
  try {
2566
- const warnings = await this.runAccountBootstrap(steps);
3025
+ const decision = await this.resolveBootstrapDecision(steps);
3026
+ if (decision.action === "skip") {
3027
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
3028
+ return false;
3029
+ }
3030
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
3031
+ await this.writeBootstrapCompletionMarker(steps);
2567
3032
  if (warnings.length > 0) {
2568
3033
  this._bootstrapStatus = { skipped: false, warnings };
2569
3034
  this.notificationHandler.warning(
@@ -2582,32 +3047,92 @@ var _TinyCloudNode = class _TinyCloudNode {
2582
3047
  }
2583
3048
  return true;
2584
3049
  }
2585
- async isFreshBootstrapAccount(steps) {
3050
+ async resolveBootstrapDecision(steps) {
2586
3051
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
2587
3052
  for (const step of steps) {
2588
3053
  if (step.kind === "session") {
2589
3054
  enshrinedSpaceIds.add(step.spaceId);
2590
3055
  }
2591
3056
  }
2592
- const skipped = this.auth.lastActivationSkippedSpaceIds;
3057
+ const auth = this.auth;
3058
+ if (!auth) {
3059
+ throw new Error("Account bootstrap requires an active wallet session");
3060
+ }
3061
+ const skipped = auth.lastActivationSkippedSpaceIds;
2593
3062
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
2594
- return true;
3063
+ return { action: "run", mode: "fresh" };
2595
3064
  }
2596
3065
  try {
2597
- const indexed = await this.account.index.spaces.list();
2598
- if (indexed.ok && indexed.data.length === 0) {
2599
- return true;
3066
+ const accountSpaceId = this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE);
3067
+ const markerPermission = {
3068
+ service: "tinycloud.kv",
3069
+ space: accountSpaceId,
3070
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
3071
+ actions: ["get"]
3072
+ };
3073
+ if (!this.hasRuntimePermissions([markerPermission])) {
3074
+ return { action: "run", mode: "repair" };
2600
3075
  }
2601
- } catch {
3076
+ const marker = await this.readBootstrapCompletionMarker();
3077
+ if (marker.ok) {
3078
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
3079
+ return { action: "skip" };
3080
+ }
3081
+ return { action: "run", mode: "repair" };
3082
+ }
3083
+ if (marker.error.code === import_sdk_core8.ErrorCodes.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
3084
+ return { action: "run", mode: "fresh" };
3085
+ }
3086
+ } catch (err) {
3087
+ const reason = err instanceof Error ? err.message : String(err);
3088
+ this.notificationHandler.warning(
3089
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
3090
+ );
3091
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
3092
+ return { action: "run", mode: "repair" };
2602
3093
  }
2603
- try {
2604
- const spaces = await this.account.spaces.list();
2605
- return spaces.ok && spaces.data.length === 0;
2606
- } catch {
2607
- return false;
3094
+ return { action: "run", mode: "repair" };
3095
+ }
3096
+ async readBootstrapCompletionMarker() {
3097
+ return this.kvForSpace(this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE)).get(
3098
+ BOOTSTRAP_COMPLETION_MARKER_KEY
3099
+ );
3100
+ }
3101
+ async writeBootstrapCompletionMarker(steps) {
3102
+ if (!this._address) {
3103
+ throw new Error("Bootstrap completion marker requires an active wallet session");
2608
3104
  }
3105
+ const canonicalStepIds = new Set(
3106
+ (0, import_sdk_core8.bootstrapSteps)(this._address, this._chainId).map((step) => step.id)
3107
+ );
3108
+ const providedStepIds = new Set(steps.map((step) => step.id));
3109
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
3110
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
3111
+ }
3112
+ const marker = {
3113
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
3114
+ stepIds: [...canonicalStepIds],
3115
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3116
+ };
3117
+ const written = await this.kvForSpace(this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE)).put(
3118
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
3119
+ marker
3120
+ );
3121
+ if (!written.ok) {
3122
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
3123
+ }
3124
+ }
3125
+ isAcceptedBootstrapCompletionMarker(value) {
3126
+ if (typeof value !== "object" || value === null) return false;
3127
+ const entries = Object.entries(value);
3128
+ const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
3129
+ const version = markerValue("v");
3130
+ return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
2609
3131
  }
2610
- async runAccountBootstrap(steps) {
3132
+ markerReadIsUnhostedSpace(meta) {
3133
+ return meta?.status === 404;
3134
+ }
3135
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
2611
3136
  if (!this.auth || !this._address) {
2612
3137
  throw new Error("Account bootstrap requires an active wallet session");
2613
3138
  }
@@ -2708,9 +3233,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2708
3233
  if (step.kind === "seed-applications") {
2709
3234
  const registered = await this.account.applications.register(
2710
3235
  step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
2711
- // Bootstrap only runs on an account with no registry records, so the
2712
- // manifest-hash pre-read is definitionally a miss. Skip it; the write
2713
- // it guards is an INSERT OR REPLACE and is safe to repeat.
3236
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
3237
+ // repair, so skip the manifest-hash pre-read.
2714
3238
  { assumeUnregistered: true }
2715
3239
  );
2716
3240
  if (!registered.ok) {
@@ -2718,7 +3242,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2718
3242
  }
2719
3243
  }
2720
3244
  if (step.kind === "encryption-network-create") {
2721
- await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
3245
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
2722
3246
  }
2723
3247
  if (step.kind === "secret-records-schema") {
2724
3248
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -3157,6 +3681,14 @@ var _TinyCloudNode = class _TinyCloudNode {
3157
3681
  }
3158
3682
  return hosted;
3159
3683
  }
3684
+ /** Resolve and authenticate the owner encoded by an owned-space URI. */
3685
+ credentialSpaceOwnerDid(spaceId) {
3686
+ const ownerDid = this.ownerDidFromSpaceId(spaceId);
3687
+ if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
3688
+ throw new Error("Credential space owner does not match the active TinyCloud owner");
3689
+ }
3690
+ return ownerDid;
3691
+ }
3160
3692
  /**
3161
3693
  * Check whether an owned space is already registered/hosted by consulting the
3162
3694
  * account spaces registry.
@@ -3700,6 +4232,66 @@ var _TinyCloudNode = class _TinyCloudNode {
3700
4232
  if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
3701
4233
  return signBytesWithJwk(bytes, session.jwk);
3702
4234
  }
4235
+ /** Apply only an already-enabled signing policy to the exact credential request. */
4236
+ async autoSignCredentialBytes(bytes) {
4237
+ const strategy = this.config.signStrategy;
4238
+ if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
4239
+ if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
4240
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4241
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4242
+ const request = {
4243
+ address: this.address ?? "",
4244
+ chainId: this.session?.chainId ?? this._chainId,
4245
+ message: new TextDecoder().decode(bytes),
4246
+ type: "message",
4247
+ purpose: "message"
4248
+ };
4249
+ const decision = await strategy.handler(request);
4250
+ if (!decision.approved) {
4251
+ if (decision.needsApproval) return void 0;
4252
+ throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
4253
+ }
4254
+ if (typeof decision.signature !== "string" || !await (0, import_sdk_core8.verifyEip191MessageSignature)(
4255
+ request.message,
4256
+ decision.signature,
4257
+ request.address
4258
+ )) {
4259
+ throw new Error("OpenKey credential signature evidence is invalid");
4260
+ }
4261
+ return this.signSessionBytes(bytes);
4262
+ }
4263
+ /** Invoke the configured interactive approval strategy before session signing. */
4264
+ async approveCredentialBytes(bytes) {
4265
+ const strategy = this.config.signStrategy;
4266
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4267
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4268
+ const request = {
4269
+ address: this.address ?? "",
4270
+ chainId: this.session?.chainId ?? this._chainId,
4271
+ message: new TextDecoder().decode(bytes),
4272
+ type: "message",
4273
+ purpose: "message"
4274
+ };
4275
+ const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
4276
+ if (approval) {
4277
+ const decision = await approval(request);
4278
+ if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
4279
+ if (decision.signature !== void 0 && !await (0, import_sdk_core8.verifyEip191MessageSignature)(
4280
+ request.message,
4281
+ decision.signature,
4282
+ request.address
4283
+ )) {
4284
+ throw new Error("OpenKey credential signature evidence is invalid");
4285
+ }
4286
+ return this.signSessionBytes(bytes);
4287
+ }
4288
+ if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
4289
+ if (this.config.signer) {
4290
+ await this.config.signer.signMessage(request.message);
4291
+ return this.signSessionBytes(bytes);
4292
+ }
4293
+ throw new Error("Interactive OpenKey approval is not configured");
4294
+ }
3703
4295
  /** Bind a session-signed invocation to the canonical service audience. */
3704
4296
  async bindInvocationAudience(authorization, audience) {
3705
4297
  const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
@@ -3767,7 +4359,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3767
4359
  nonce: this.config.nonce,
3768
4360
  siweConfig: this.config.siweConfig,
3769
4361
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3770
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4362
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
3771
4363
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3772
4364
  });
3773
4365
  this.tc = new import_sdk_core8.TinyCloud(this.auth, {
@@ -3820,7 +4412,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3820
4412
  nonce: this.config.nonce,
3821
4413
  siweConfig: this.config.siweConfig,
3822
4414
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3823
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4415
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
3824
4416
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3825
4417
  });
3826
4418
  this.tc = new import_sdk_core8.TinyCloud(this.auth, {
@@ -6569,35 +7161,44 @@ var _TinyCloudNode = class _TinyCloudNode {
6569
7161
  if (parentDelegation.disableSubDelegation) {
6570
7162
  throw new Error("Parent delegation does not allow sub-delegation");
6571
7163
  }
6572
- if (!params.path.startsWith(parentDelegation.path)) {
7164
+ const parentResources = parentDelegation.resources ?? [{
7165
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7166
+ space: parentDelegation.spaceId,
7167
+ path: parentDelegation.path,
7168
+ actions: parentDelegation.actions
7169
+ }];
7170
+ const requestedResources = params.resources ?? [{
7171
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7172
+ space: parentDelegation.spaceId,
7173
+ path: params.path,
7174
+ actions: params.actions
7175
+ }];
7176
+ const resourceIsContained = (parent, child) => {
7177
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
7178
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
7179
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
7180
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
7181
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
7182
+ };
7183
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
6573
7184
  throw new Error(
6574
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
7185
+ "Sub-delegation resources exceed the received delegation"
6575
7186
  );
6576
7187
  }
6577
- const parentActions = new Set(parentDelegation.actions);
6578
- for (const action of params.actions) {
6579
- if (!parentActions.has(action)) {
6580
- throw new Error(
6581
- `Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
6582
- );
6583
- }
6584
- }
6585
7188
  const now = /* @__PURE__ */ new Date();
6586
7189
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
6587
7190
  const requestedExpiry = new Date(now.getTime() + expiryMs);
6588
7191
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
6589
7192
  const abilities = {};
6590
- const kvActions = params.actions.filter((a) => a.startsWith("tinycloud.kv/"));
6591
- const sqlActions = params.actions.filter((a) => a.startsWith("tinycloud.sql/"));
6592
- const duckdbActions = params.actions.filter((a) => a.startsWith("tinycloud.duckdb/"));
6593
- if (kvActions.length > 0) {
6594
- abilities.kv = { [params.path]: kvActions };
6595
- }
6596
- if (sqlActions.length > 0) {
6597
- abilities.sql = { [params.path]: sqlActions };
6598
- }
6599
- if (duckdbActions.length > 0) {
6600
- abilities.duckdb = { [params.path]: duckdbActions };
7193
+ const rawAbilities = {};
7194
+ for (const resource of requestedResources) {
7195
+ const service = resource.service.replace(/^tinycloud\./, "");
7196
+ if (service === "encryption") {
7197
+ rawAbilities[resource.path] = [...resource.actions];
7198
+ continue;
7199
+ }
7200
+ abilities[service] ?? (abilities[service] = {});
7201
+ abilities[service][resource.path] = [...resource.actions];
6601
7202
  }
6602
7203
  const targetHost = parentDelegation.host ?? this.config.host;
6603
7204
  const prepared = this.wasmBindings.prepareSession({
@@ -6609,7 +7210,8 @@ var _TinyCloudNode = class _TinyCloudNode {
6609
7210
  expirationTime: actualExpiry.toISOString(),
6610
7211
  spaceId: parentDelegation.spaceId,
6611
7212
  delegateUri: params.delegateDID,
6612
- parents: [parentDelegation.cid]
7213
+ parents: [parentDelegation.cid],
7214
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
6613
7215
  });
6614
7216
  const signature = await this.signer.signMessage(prepared.siwe);
6615
7217
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -6627,14 +7229,20 @@ var _TinyCloudNode = class _TinyCloudNode {
6627
7229
  cid: subDelegationSession.delegationCid,
6628
7230
  delegationHeader: subDelegationSession.delegationHeader,
6629
7231
  spaceId: parentDelegation.spaceId,
6630
- path: params.path,
6631
- actions: params.actions,
7232
+ path: requestedResources[0].path,
7233
+ actions: requestedResources[0].actions,
6632
7234
  disableSubDelegation: params.disableSubDelegation ?? false,
6633
7235
  expiry: actualExpiry,
6634
7236
  delegateDID: params.delegateDID,
6635
7237
  ownerAddress: parentDelegation.ownerAddress,
6636
7238
  chainId: parentDelegation.chainId,
6637
- host: targetHost
7239
+ host: targetHost,
7240
+ resources: requestedResources.map((resource) => ({
7241
+ service: resource.service.replace(/^tinycloud\./, ""),
7242
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
7243
+ path: resource.path,
7244
+ actions: [...resource.actions]
7245
+ }))
6638
7246
  };
6639
7247
  }
6640
7248
  };