@tinycloud/node-sdk 2.11.0-beta.9 → 3.0.0-beta.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
@@ -78,6 +78,7 @@ __export(core_exports, {
78
78
  VaultPublicSpaceKVActions: () => import_sdk_core16.VaultPublicSpaceKVActions,
79
79
  VersionCheckError: () => import_sdk_core20.VersionCheckError,
80
80
  WasmKeyProvider: () => WasmKeyProvider,
81
+ activateCompactRuntimeDelegation: () => activateCompactRuntimeDelegation,
81
82
  addressStorageKey: () => import_sdk_core11.addressStorageKey,
82
83
  buildSpaceUri: () => import_sdk_core19.buildSpaceUri,
83
84
  canonicalizeAddress: () => import_sdk_core11.canonicalizeAddress,
@@ -322,6 +323,8 @@ var FileSessionStorage = class {
322
323
  };
323
324
 
324
325
  // src/authorization/NodeUserAuthorization.ts
326
+ var import_node_crypto = require("crypto");
327
+ var import_siwe = require("siwe");
325
328
  var import_sdk_core3 = require("@tinycloud/sdk-core");
326
329
 
327
330
  // src/authorization/strategies.ts
@@ -331,6 +334,27 @@ var defaultSignStrategy = { type: "auto-sign" };
331
334
  // src/authorization/NodeUserAuthorization.ts
332
335
  var DECRYPT_ACTION = import_sdk_core3.ENCRYPTION.DECRYPT;
333
336
  var NETWORK_CREATE_ACTION = import_sdk_core3.ENCRYPTION.NETWORK_CREATE;
337
+ function canonicalizeOpenKeyManifestJson(value) {
338
+ const json = JSON.stringify(value);
339
+ if (json === void 0) {
340
+ throw new TypeError("manifest is not JSON-serializable");
341
+ }
342
+ return canonicalizeJsonValue(JSON.parse(json));
343
+ }
344
+ function canonicalizeJsonValue(value) {
345
+ if (value === null || typeof value !== "object") {
346
+ return JSON.stringify(value);
347
+ }
348
+ if (Array.isArray(value)) {
349
+ return `[${value.map(canonicalizeJsonValue).join(",")}]`;
350
+ }
351
+ return `{${Object.keys(value).sort().map(
352
+ (key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(value[key])}`
353
+ ).join(",")}}`;
354
+ }
355
+ function canonicalOpenKeyManifestSha256Hex(value) {
356
+ return (0, import_node_crypto.createHash)("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
357
+ }
334
358
  function didPrincipalMatches(actual, expected) {
335
359
  try {
336
360
  return (0, import_sdk_core3.principalDidEquals)(actual, expected);
@@ -348,6 +372,16 @@ function addRawAbility(rawAbilities, resource, action) {
348
372
  actions.push(action);
349
373
  }
350
374
  }
375
+ function cloneAbilitiesMap(actions) {
376
+ return Object.fromEntries(
377
+ Object.entries(actions).map(([service, paths]) => [
378
+ service,
379
+ Object.fromEntries(
380
+ Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
381
+ )
382
+ ])
383
+ );
384
+ }
351
385
  var NodeUserAuthorization = class {
352
386
  constructor(config) {
353
387
  this.activeSessionKeyId = "default";
@@ -619,23 +653,41 @@ var NodeUserAuthorization = class {
619
653
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
620
654
  const primarySpaceId2 = (0, import_sdk_core3.makePkhSpaceId)(address, chainId, this.spacePrefix);
621
655
  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
- ]
656
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === import_sdk_core3.ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
657
+ const spaceAbilities2 = {
658
+ [primarySpaceId2]: primaryActions,
659
+ [secretsSpaceId]: {
660
+ kv: {
661
+ "vault/secrets/": [
662
+ import_sdk_core3.KV.GET,
663
+ import_sdk_core3.KV.PUT,
664
+ import_sdk_core3.KV.DEL,
665
+ import_sdk_core3.KV.LIST,
666
+ import_sdk_core3.KV.METADATA
667
+ ]
668
+ }
669
+ }
670
+ };
671
+ if (this.includeAccountRegistryPermissions && this.spacePrefix !== import_sdk_core3.ACCOUNT_REGISTRY_SPACE) {
672
+ 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);
673
+ } else if (this.includeAccountRegistryPermissions) {
674
+ const accountAbilities = (0, import_sdk_core3.resourceCapabilitiesToAbilitiesMap)(
675
+ import_sdk_core3.ACCOUNT_MANIFEST_PERMISSIONS
676
+ );
677
+ for (const [service, paths] of Object.entries(accountAbilities)) {
678
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
679
+ for (const [path, actions] of Object.entries(paths)) {
680
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
681
+ for (const action of actions) {
682
+ if (!existingActions.includes(action)) existingActions.push(action);
636
683
  }
637
684
  }
638
- },
685
+ }
686
+ }
687
+ return {
688
+ abilities: primaryActions,
689
+ spaceId: primarySpaceId2,
690
+ spaceAbilities: spaceAbilities2,
639
691
  rawAbilities: {
640
692
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
641
693
  }
@@ -1216,6 +1268,398 @@ var NodeUserAuthorization = class {
1216
1268
  await this.ensureSpaceExists();
1217
1269
  return clientSession;
1218
1270
  }
1271
+ /**
1272
+ * End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
1273
+ *
1274
+ * This is the production entry point that wires
1275
+ * {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
1276
+ * {@link signInWithOpenKeyResult} into one call. Consumers pass in a
1277
+ * caller-provided `authorizeFn` (typically a thin wrapper around
1278
+ * `openkey.authorizeTinyCloud()`) so this method has no direct
1279
+ * dependency on the OpenKey SDK — it works with any transport that
1280
+ * conforms to the versioned protocol.
1281
+ *
1282
+ * The `authorizeFn` MUST:
1283
+ * 1. Send the prepared SIWE bytes to OpenKey.
1284
+ * 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
1285
+ * `signedMessage` — the bytes the signature actually verifies
1286
+ * against — plus `selectedActionKeys` and `permissions`).
1287
+ *
1288
+ * NodeUserAuthorization then completes the session with `signedMessage`
1289
+ * (never the prepared bytes), verifying the signature, the immutable
1290
+ * SIWE fields, and every subset invariant enforced by
1291
+ * {@link signInWithOpenKeyResult}. This preserves the exact-byte
1292
+ * guarantee: if the user did NOT narrow, the returned `signedMessage`
1293
+ * equals the prepared SIWE byte-for-byte; if the user narrowed, only
1294
+ * the ReCap block differs and every immutable header field survives.
1295
+ *
1296
+ * Legacy exact-byte {@link signIn} callers are unaffected — this method
1297
+ * is opt-in.
1298
+ *
1299
+ * @param authorizeFn Callback that invokes the versioned OpenKey
1300
+ * `authorizeTinyCloud()` protocol and returns the result. Receives
1301
+ * the prepared SIWE bytes plus a `host` hint the widget should bind
1302
+ * into its /authorize-sign-prepare context.
1303
+ * @param options Optional overrides for the OpenKey request.
1304
+ */
1305
+ async signInWithOpenKey(authorizeFn, options) {
1306
+ if (options?.host !== void 0) {
1307
+ const overrideHost = options.host.trim();
1308
+ if (!overrideHost) {
1309
+ throw new Error("OpenKey authorization host must not be empty");
1310
+ }
1311
+ this.tinycloudHosts = [overrideHost];
1312
+ }
1313
+ const signingAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
1314
+ const signingChainId = await this.signer.getChainId();
1315
+ await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
1316
+ const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
1317
+ const presentation = this.buildPresentationEnvelope(options?.reason);
1318
+ const hostHint = this.primaryTinyCloudHost;
1319
+ const result = await authorizeFn({
1320
+ protocolVersion: 1,
1321
+ siwe: prepared.siwe,
1322
+ jwk,
1323
+ host: hostHint,
1324
+ keyId: options?.openkeyKeyId,
1325
+ presentation
1326
+ });
1327
+ return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
1328
+ }
1329
+ /**
1330
+ * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
1331
+ * envelope is display-only — the OpenKey widget renders it under an
1332
+ * honest trust label, and the server /authorize-sign-prepare route
1333
+ * decides whether to upgrade the label to `origin-bound` by fetching
1334
+ * the well-known manifest and comparing its digest to
1335
+ * `manifestDigest`.
1336
+ *
1337
+ * Returns `undefined` when no manifest is set — the widget renders as
1338
+ * "no manifest supplied" (unsigned).
1339
+ *
1340
+ * The digest is a deterministic SHA-256 over canonical JSON of the
1341
+ * first manifest payload; OpenKey parses the fetched well-known manifest
1342
+ * and applies the same canonicalization before comparing digests.
1343
+ */
1344
+ buildPresentationEnvelope(reason) {
1345
+ const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
1346
+ if (manifests.length === 0 && !reason) {
1347
+ return void 0;
1348
+ }
1349
+ const primary = manifests[0];
1350
+ let displayName;
1351
+ let manifestId;
1352
+ let manifestDigest;
1353
+ const envelopeManifests = [];
1354
+ for (const m of manifests) {
1355
+ envelopeManifests.push({
1356
+ name: m.name,
1357
+ appId: m.app_id,
1358
+ // Deep-copy to prevent later mutations from affecting the digest.
1359
+ payload: JSON.parse(JSON.stringify(m))
1360
+ });
1361
+ }
1362
+ if (primary) {
1363
+ displayName = primary.name;
1364
+ manifestId = primary.app_id;
1365
+ manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
1366
+ }
1367
+ return {
1368
+ protocolVersion: 1,
1369
+ displayName,
1370
+ reason: reason && reason.length > 0 ? reason : void 0,
1371
+ manifestId,
1372
+ manifestDigest,
1373
+ manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
1374
+ };
1375
+ }
1376
+ /**
1377
+ * Complete sign-in with a versioned OpenKey authorization result.
1378
+ *
1379
+ * Where `signInWithPreparedSession` completes with a signature over the
1380
+ * ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
1381
+ * OpenKey widget actually presented and signed (`signedMessage`). It
1382
+ * exists because the versioned authorizeTinyCloud protocol lets the user
1383
+ * narrow capabilities; the returned bytes may differ from the original
1384
+ * request and are the only ones the signature actually verifies against.
1385
+ *
1386
+ * The `result` parameter is validated at the trust boundary via
1387
+ * `validateAuthorizationResultV1` (in sdk-core). Callers should validate
1388
+ * BEFORE calling this method so unknown/missing fields are rejected
1389
+ * before any session state is created.
1390
+ *
1391
+ * Validation performed here (defence-in-depth: even a "valid" v1 payload
1392
+ * must clear these checks before any session state is created):
1393
+ * 1. `result.address` matches the local signer's address.
1394
+ * 2. `result.signature` recovers to `result.address` when verified against
1395
+ * `result.signedMessage`.
1396
+ * 3. Every immutable SIWE header field (domain, address, URI, version,
1397
+ * chainId, nonce, issuedAt) in the original prepared SIWE is preserved
1398
+ * byte-for-byte in `result.signedMessage`. `statement` is NOT in this
1399
+ * list because the WASM renders it from the ReCap contents; it MUST
1400
+ * change when the ReCap is narrowed.
1401
+ * 4. Every capability in `result.signedMessage`'s ReCap block was present
1402
+ * in the original prepared SIWE — narrowing OK, broadening rejected.
1403
+ * 5. Every `result.selectedActionKeys` entry corresponds to a
1404
+ * (resource, action) pair in the returned SIWE.
1405
+ */
1406
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
1407
+ if (result.protocolVersion !== 1) {
1408
+ throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
1409
+ }
1410
+ if (!result.signedMessage) {
1411
+ throw new Error("OpenKey result must include signedMessage");
1412
+ }
1413
+ if (!prepared.siwe) {
1414
+ throw new Error(
1415
+ "signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
1416
+ );
1417
+ }
1418
+ const expectedAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
1419
+ if ((0, import_sdk_core3.canonicalizeAddress)(result.address) !== expectedAddress) {
1420
+ throw new Error(
1421
+ `OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
1422
+ );
1423
+ }
1424
+ let parsedSignedSiwe;
1425
+ try {
1426
+ parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
1427
+ } catch (err) {
1428
+ throw new Error(
1429
+ `OpenKey signedMessage is not a parseable SIWE: ${err.message}`
1430
+ );
1431
+ }
1432
+ const verifyResult = await parsedSignedSiwe.verify(
1433
+ { signature: result.signature },
1434
+ { suppressExceptions: true }
1435
+ );
1436
+ if (!verifyResult.success) {
1437
+ const errData = verifyResult.error;
1438
+ const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
1439
+ const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
1440
+ throw new Error(
1441
+ `OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
1442
+ );
1443
+ }
1444
+ if ((0, import_sdk_core3.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
1445
+ throw new Error(
1446
+ `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
1447
+ );
1448
+ }
1449
+ const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
1450
+ const originalFields = (0, import_sdk_core3.extractImmutableSiweFields)(prepared.siwe);
1451
+ const signedFields = (0, import_sdk_core3.extractImmutableSiweFields)(result.signedMessage);
1452
+ const changedFields = (0, import_sdk_core3.diffImmutableSiweFields)(originalFields, signedFields, {
1453
+ originalHasRecap
1454
+ });
1455
+ if (changedFields.length > 0) {
1456
+ throw new Error(
1457
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
1458
+ );
1459
+ }
1460
+ let originalCaps;
1461
+ let signedCaps;
1462
+ try {
1463
+ originalCaps = (0, import_sdk_core3.extractRecapAttenuations)(prepared.siwe);
1464
+ } catch (err) {
1465
+ throw new Error(
1466
+ `Failed to decode prepared SIWE recap: ${err.message}`
1467
+ );
1468
+ }
1469
+ try {
1470
+ signedCaps = (0, import_sdk_core3.extractRecapAttenuations)(result.signedMessage);
1471
+ } catch (err) {
1472
+ throw new Error(
1473
+ `Failed to decode OpenKey signedMessage recap: ${err.message}`
1474
+ );
1475
+ }
1476
+ const unauthorized = (0, import_sdk_core3.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
1477
+ if (unauthorized.length > 0) {
1478
+ const previewLimit = 5;
1479
+ const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
1480
+ const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
1481
+ throw new Error(
1482
+ `OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
1483
+ );
1484
+ }
1485
+ if (!result.signedMessage.includes(prepared.spaceId)) {
1486
+ throw new Error(
1487
+ `OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
1488
+ );
1489
+ }
1490
+ const grantedPairs = /* @__PURE__ */ new Set();
1491
+ const grantedFourPartIndex = /* @__PURE__ */ new Map();
1492
+ for (const [resource, actions] of Object.entries(signedCaps)) {
1493
+ const { space, path } = (0, import_sdk_core3.parseCanonicalRecapResource)(resource);
1494
+ for (const action of Object.keys(actions)) {
1495
+ grantedPairs.add(`${resource}\0${action}`);
1496
+ const slashIdx = action.indexOf("/");
1497
+ const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
1498
+ if (service) {
1499
+ grantedFourPartIndex.set(
1500
+ `${service}\0${space}\0${path}\0${action}`,
1501
+ `${resource}\0${action}`
1502
+ );
1503
+ }
1504
+ }
1505
+ }
1506
+ const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
1507
+ const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
1508
+ for (const pair of grantedPairs) {
1509
+ const idx = pair.indexOf("\0");
1510
+ const action = idx >= 0 ? pair.slice(idx + 1) : pair;
1511
+ if (!isStructurallyRequired(action)) {
1512
+ nonRequiredGrantedPairs.add(pair);
1513
+ }
1514
+ }
1515
+ const allSignedGrantedPairs = new Set(grantedPairs);
1516
+ const seenRawKeys = /* @__PURE__ */ new Set();
1517
+ for (const rawKey of result.selectedActionKeys) {
1518
+ if (seenRawKeys.has(rawKey)) {
1519
+ throw new Error(
1520
+ `OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
1521
+ );
1522
+ }
1523
+ seenRawKeys.add(rawKey);
1524
+ }
1525
+ const selectedResourceActionPairs = /* @__PURE__ */ new Set();
1526
+ for (const rawKey of result.selectedActionKeys) {
1527
+ const parts = rawKey.split("\0");
1528
+ if (parts.length !== 4) {
1529
+ throw new Error(
1530
+ `OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
1531
+ );
1532
+ }
1533
+ const canonical = grantedFourPartIndex.get(rawKey);
1534
+ if (!canonical) {
1535
+ throw new Error(
1536
+ `OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
1537
+ );
1538
+ }
1539
+ selectedResourceActionPairs.add(canonical);
1540
+ }
1541
+ if (nonRequiredGrantedPairs.size > 0) {
1542
+ const missingPairs = [];
1543
+ for (const pair of nonRequiredGrantedPairs) {
1544
+ if (!selectedResourceActionPairs.has(pair)) {
1545
+ missingPairs.push(pair);
1546
+ }
1547
+ }
1548
+ if (missingPairs.length > 0) {
1549
+ const previewLimit = 5;
1550
+ const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1551
+ const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
1552
+ throw new Error(
1553
+ `OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
1554
+ );
1555
+ }
1556
+ }
1557
+ if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
1558
+ throw new Error(
1559
+ "OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
1560
+ );
1561
+ }
1562
+ const permissionCanonicalKeys = /* @__PURE__ */ new Set();
1563
+ const permissionPairsCovered = /* @__PURE__ */ new Set();
1564
+ for (const perm of result.permissions) {
1565
+ const { service, space, path, actions } = perm;
1566
+ if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
1567
+ throw new Error(
1568
+ `OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
1569
+ );
1570
+ }
1571
+ if (!Array.isArray(actions) || actions.length === 0) {
1572
+ throw new Error(
1573
+ `OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
1574
+ );
1575
+ }
1576
+ const canonicalKey = `${service}\0${space}\0${path}`;
1577
+ if (permissionCanonicalKeys.has(canonicalKey)) {
1578
+ throw new Error(
1579
+ `OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
1580
+ );
1581
+ }
1582
+ permissionCanonicalKeys.add(canonicalKey);
1583
+ let expectedResource;
1584
+ if (space.startsWith("tinycloud:")) {
1585
+ const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
1586
+ expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
1587
+ } else {
1588
+ if (path) {
1589
+ throw new Error(
1590
+ `OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
1591
+ );
1592
+ }
1593
+ expectedResource = space;
1594
+ }
1595
+ let matchedResource;
1596
+ if (signedCaps[expectedResource] !== void 0) {
1597
+ matchedResource = expectedResource;
1598
+ } else {
1599
+ throw new Error(
1600
+ `OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
1601
+ );
1602
+ }
1603
+ const grantedActionsForResource = new Set(
1604
+ Object.keys(signedCaps[matchedResource])
1605
+ );
1606
+ for (const action of actions) {
1607
+ if (typeof action !== "string" || !action) {
1608
+ throw new Error(
1609
+ `OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
1610
+ );
1611
+ }
1612
+ if (!grantedActionsForResource.has(action)) {
1613
+ throw new Error(
1614
+ `OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
1615
+ );
1616
+ }
1617
+ permissionPairsCovered.add(`${matchedResource}\0${action}`);
1618
+ }
1619
+ }
1620
+ if (allSignedGrantedPairs.size > 0) {
1621
+ const missingFromPerms = [];
1622
+ for (const pair of allSignedGrantedPairs) {
1623
+ if (!permissionPairsCovered.has(pair)) {
1624
+ missingFromPerms.push(pair);
1625
+ }
1626
+ }
1627
+ if (missingFromPerms.length > 0) {
1628
+ const previewLimit = 5;
1629
+ const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1630
+ const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
1631
+ throw new Error(
1632
+ `OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
1633
+ );
1634
+ }
1635
+ const extrasInPerms = [];
1636
+ for (const pair of permissionPairsCovered) {
1637
+ if (!allSignedGrantedPairs.has(pair)) {
1638
+ extrasInPerms.push(pair);
1639
+ }
1640
+ }
1641
+ if (extrasInPerms.length > 0) {
1642
+ const previewLimit = 5;
1643
+ const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1644
+ const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
1645
+ throw new Error(
1646
+ `OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
1647
+ );
1648
+ }
1649
+ }
1650
+ void nonRequiredGrantedPairs;
1651
+ return this.signInWithPreparedSession(
1652
+ {
1653
+ siwe: result.signedMessage,
1654
+ jwk: prepared.jwk,
1655
+ spaceId: prepared.spaceId,
1656
+ verificationMethod: prepared.verificationMethod
1657
+ },
1658
+ result.signature,
1659
+ keyId,
1660
+ jwk
1661
+ );
1662
+ }
1219
1663
  /**
1220
1664
  * Clear persisted session data.
1221
1665
  */
@@ -1710,6 +2154,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
1710
2154
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
1711
2155
  var NETWORK_CREATE_ACTION2 = import_sdk_core8.ENCRYPTION.NETWORK_CREATE;
1712
2156
  var DECRYPT_ACTION2 = import_sdk_core8.ENCRYPTION.DECRYPT;
2157
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
2158
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
2159
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
1713
2160
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
1714
2161
  function isSecretPayload(value) {
1715
2162
  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";
@@ -1733,6 +2180,18 @@ var ROOT_DELEGATION_ACTIONS = [
1733
2180
  import_sdk_core8.DUCKDB.ALL
1734
2181
  ];
1735
2182
  var DEFAULT_SESSION_EXPIRATION_MS = import_sdk_core8.EXPIRY.SESSION_MS;
2183
+ function unifiedRootAttenuation(capabilities) {
2184
+ const attenuation = {};
2185
+ for (const capability of capabilities) {
2186
+ if (attenuation[capability.resource] !== void 0) throw new Error("unified owner root contains a duplicate resource");
2187
+ attenuation[capability.resource] = capability.kind === "encryption" ? { [capability.action]: [{}] } : Object.fromEntries(capability.actions.map((action) => [action, [{
2188
+ type: "xyz.tinycloud.resource/selector",
2189
+ kind: capability.selector,
2190
+ value: capability.resource
2191
+ }]]));
2192
+ }
2193
+ return attenuation;
2194
+ }
1736
2195
  function decodeAuthorizationBytes(authorization) {
1737
2196
  const encoded = authorization.replace(/^Bearer /i, "");
1738
2197
  const match = /^([A-Za-z0-9_-]+)(={1,2})?$/.exec(encoded);
@@ -1752,6 +2211,9 @@ function decodeAuthorizationBytes(authorization) {
1752
2211
  function isOpenKeyAutoSignStrategy(strategy) {
1753
2212
  return strategy?.openKeyAutoSign === true;
1754
2213
  }
2214
+ function openKeyApproval(strategy) {
2215
+ return strategy?.openKeyRequestApproval;
2216
+ }
1755
2217
  function isInteractiveSigner(config) {
1756
2218
  if (config.privateKey) {
1757
2219
  return false;
@@ -2109,6 +2571,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2109
2571
  * hosting is confirmed against the session that was active at the time.
2110
2572
  */
2111
2573
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
2574
+ /** Serializes account-registry writes for the active node instance. */
2575
+ this.accountRegistryTail = Promise.resolve();
2112
2576
  /**
2113
2577
  * True when the last signIn() detected an interactive signer and skipped
2114
2578
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -2117,9 +2581,12 @@ var _TinyCloudNode = class _TinyCloudNode {
2117
2581
  this._bootstrapSkipped = false;
2118
2582
  /**
2119
2583
  * 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
2584
+ * true when bootstrap did not complete in this invocation (interactive signer,
2585
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
2586
+ * step failed). `already-provisioned` means the bootstrap was not run here;
2587
+ * it does not mean a prior provisioning attempt failed to complete.
2588
+ * `reason` carries the cause so apps can surface a "finish account setup"
2589
+ * call-to-action. `warnings` is
2123
2590
  * present when bootstrap completed, but one or more steps recovered from
2124
2591
  * an ambiguous failure (e.g. a KV batch write reconciled via per-space
2125
2592
  * fallback) — clean vs. recovered runs are otherwise byte-identical.
@@ -2302,7 +2769,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2302
2769
  nonce: config.nonce,
2303
2770
  siweConfig: config.siweConfig,
2304
2771
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
2305
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2772
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2306
2773
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
2307
2774
  });
2308
2775
  this.tc = new import_sdk_core8.TinyCloud(this.auth, {
@@ -2392,6 +2859,15 @@ var _TinyCloudNode = class _TinyCloudNode {
2392
2859
  get sessionDid() {
2393
2860
  return this.sessionManager.getDID(this.sessionKeyId);
2394
2861
  }
2862
+ /** Bare did:key principal used as the credential subject and holder binding. */
2863
+ get credentialHolderDid() {
2864
+ return this.sessionDid.split("#", 1)[0];
2865
+ }
2866
+ /** Canonical verification method for the active credential holder key. */
2867
+ get credentialHolderKid() {
2868
+ const holder = this.credentialHolderDid;
2869
+ return `${holder}#${holder.slice("did:key:".length)}`;
2870
+ }
2395
2871
  /**
2396
2872
  * Return the current session's signed ReCap capabilities after the session
2397
2873
  * has been authenticated or restored. This is intentionally distinct from
@@ -2493,6 +2969,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2493
2969
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
2494
2970
  );
2495
2971
  }
2972
+ await this.accountRegistryTail;
2496
2973
  await this.wasmBindings.ensureInitialized?.();
2497
2974
  this._address = (0, import_sdk_core8.canonicalizeAddress)(await this.signer.getAddress());
2498
2975
  this._chainId = await this.signer.getChainId();
@@ -2545,6 +3022,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2545
3022
  this._bootstrapSkipped = false;
2546
3023
  this._bootstrapStatus = { skipped: false };
2547
3024
  if (this.config.autoBootstrapAccount === false) {
3025
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
2548
3026
  return false;
2549
3027
  }
2550
3028
  if (!this.auth || !this._address) {
@@ -2559,11 +3037,14 @@ var _TinyCloudNode = class _TinyCloudNode {
2559
3037
  return false;
2560
3038
  }
2561
3039
  const steps = (0, import_sdk_core8.bootstrapSteps)(this._address, this._chainId);
2562
- if (!await this.isFreshBootstrapAccount(steps)) {
2563
- return false;
2564
- }
2565
3040
  try {
2566
- const warnings = await this.runAccountBootstrap(steps);
3041
+ const decision = await this.resolveBootstrapDecision(steps);
3042
+ if (decision.action === "skip") {
3043
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
3044
+ return false;
3045
+ }
3046
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
3047
+ await this.writeBootstrapCompletionMarker(steps);
2567
3048
  if (warnings.length > 0) {
2568
3049
  this._bootstrapStatus = { skipped: false, warnings };
2569
3050
  this.notificationHandler.warning(
@@ -2582,32 +3063,92 @@ var _TinyCloudNode = class _TinyCloudNode {
2582
3063
  }
2583
3064
  return true;
2584
3065
  }
2585
- async isFreshBootstrapAccount(steps) {
3066
+ async resolveBootstrapDecision(steps) {
2586
3067
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
2587
3068
  for (const step of steps) {
2588
3069
  if (step.kind === "session") {
2589
3070
  enshrinedSpaceIds.add(step.spaceId);
2590
3071
  }
2591
3072
  }
2592
- const skipped = this.auth.lastActivationSkippedSpaceIds;
3073
+ const auth = this.auth;
3074
+ if (!auth) {
3075
+ throw new Error("Account bootstrap requires an active wallet session");
3076
+ }
3077
+ const skipped = auth.lastActivationSkippedSpaceIds;
2593
3078
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
2594
- return true;
3079
+ return { action: "run", mode: "fresh" };
2595
3080
  }
2596
3081
  try {
2597
- const indexed = await this.account.index.spaces.list();
2598
- if (indexed.ok && indexed.data.length === 0) {
2599
- return true;
3082
+ const accountSpaceId = this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE);
3083
+ const markerPermission = {
3084
+ service: "tinycloud.kv",
3085
+ space: accountSpaceId,
3086
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
3087
+ actions: ["get"]
3088
+ };
3089
+ if (!this.hasRuntimePermissions([markerPermission])) {
3090
+ return { action: "run", mode: "repair" };
2600
3091
  }
2601
- } catch {
3092
+ const marker = await this.readBootstrapCompletionMarker();
3093
+ if (marker.ok) {
3094
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
3095
+ return { action: "skip" };
3096
+ }
3097
+ return { action: "run", mode: "repair" };
3098
+ }
3099
+ if (marker.error.code === import_sdk_core8.ErrorCodes.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
3100
+ return { action: "run", mode: "fresh" };
3101
+ }
3102
+ } catch (err) {
3103
+ const reason = err instanceof Error ? err.message : String(err);
3104
+ this.notificationHandler.warning(
3105
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
3106
+ );
3107
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
3108
+ return { action: "run", mode: "repair" };
2602
3109
  }
2603
- try {
2604
- const spaces = await this.account.spaces.list();
2605
- return spaces.ok && spaces.data.length === 0;
2606
- } catch {
2607
- return false;
3110
+ return { action: "run", mode: "repair" };
3111
+ }
3112
+ async readBootstrapCompletionMarker() {
3113
+ return this.kvForSpace(this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE)).get(
3114
+ BOOTSTRAP_COMPLETION_MARKER_KEY
3115
+ );
3116
+ }
3117
+ async writeBootstrapCompletionMarker(steps) {
3118
+ if (!this._address) {
3119
+ throw new Error("Bootstrap completion marker requires an active wallet session");
3120
+ }
3121
+ const canonicalStepIds = new Set(
3122
+ (0, import_sdk_core8.bootstrapSteps)(this._address, this._chainId).map((step) => step.id)
3123
+ );
3124
+ const providedStepIds = new Set(steps.map((step) => step.id));
3125
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
3126
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
3127
+ }
3128
+ const marker = {
3129
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
3130
+ stepIds: [...canonicalStepIds],
3131
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3132
+ };
3133
+ const written = await this.kvForSpace(this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE)).put(
3134
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
3135
+ marker
3136
+ );
3137
+ if (!written.ok) {
3138
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
2608
3139
  }
2609
3140
  }
2610
- async runAccountBootstrap(steps) {
3141
+ isAcceptedBootstrapCompletionMarker(value) {
3142
+ if (typeof value !== "object" || value === null) return false;
3143
+ const entries = Object.entries(value);
3144
+ const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
3145
+ const version = markerValue("v");
3146
+ return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
3147
+ }
3148
+ markerReadIsUnhostedSpace(meta) {
3149
+ return meta?.status === 404;
3150
+ }
3151
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
2611
3152
  if (!this.auth || !this._address) {
2612
3153
  throw new Error("Account bootstrap requires an active wallet session");
2613
3154
  }
@@ -2708,9 +3249,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2708
3249
  if (step.kind === "seed-applications") {
2709
3250
  const registered = await this.account.applications.register(
2710
3251
  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.
3252
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
3253
+ // repair, so skip the manifest-hash pre-read.
2714
3254
  { assumeUnregistered: true }
2715
3255
  );
2716
3256
  if (!registered.ok) {
@@ -2718,7 +3258,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2718
3258
  }
2719
3259
  }
2720
3260
  if (step.kind === "encryption-network-create") {
2721
- await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
3261
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
2722
3262
  }
2723
3263
  if (step.kind === "secret-records-schema") {
2724
3264
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -2911,16 +3451,48 @@ var _TinyCloudNode = class _TinyCloudNode {
2911
3451
  }
2912
3452
  }
2913
3453
  scheduleAccountRegistrySync() {
2914
- void this.withAccountRegistryRetry(async () => {
2915
- void this.account.index.ensure();
2916
- await this.writeManifestRegistryRecords();
2917
- if (this.currentSessionCanListSpaces()) {
2918
- const spaces = await this.account.spaces.syncAccessible();
2919
- if (!spaces.ok) {
2920
- throw new Error(`Failed to sync account spaces: ${spaces.error.message}`);
3454
+ const session = this.currentTinyCloudSession();
3455
+ if (!session || this.pendingAccountRegistrySync?.session === session) {
3456
+ return;
3457
+ }
3458
+ const promise = this.enqueueAccountRegistryOperation(session, async () => {
3459
+ await this.withAccountRegistryRetry(async () => {
3460
+ if (this.currentTinyCloudSession() !== session) {
3461
+ return;
2921
3462
  }
3463
+ await this.account.index.ensure();
3464
+ if (this.currentTinyCloudSession() !== session) {
3465
+ return;
3466
+ }
3467
+ await this.writeManifestRegistryRecords();
3468
+ if (this.currentTinyCloudSession() !== session) {
3469
+ return;
3470
+ }
3471
+ if (this.currentSessionCanListSpaces()) {
3472
+ const spaces = await this.account.spaces.syncAccessible();
3473
+ if (!spaces.ok) {
3474
+ throw new Error(`Failed to sync account spaces: ${spaces.error.message}`);
3475
+ }
3476
+ }
3477
+ });
3478
+ });
3479
+ this.pendingAccountRegistrySync = { session, promise };
3480
+ const clearPendingSync = () => {
3481
+ if (this.pendingAccountRegistrySync?.promise === promise) {
3482
+ this.pendingAccountRegistrySync = void 0;
2922
3483
  }
3484
+ };
3485
+ void promise.then(clearPendingSync, clearPendingSync);
3486
+ }
3487
+ enqueueAccountRegistryOperation(session, task) {
3488
+ const operation = this.accountRegistryTail.then(async () => {
3489
+ if (this.currentTinyCloudSession() === session) {
3490
+ await task();
3491
+ }
3492
+ });
3493
+ this.accountRegistryTail = operation.catch(() => {
2923
3494
  });
3495
+ return operation;
2924
3496
  }
2925
3497
  /**
2926
3498
  * Whether the current primary session may invoke `tinycloud.space/list`.
@@ -3073,7 +3645,8 @@ var _TinyCloudNode = class _TinyCloudNode {
3073
3645
  * @returns The hosted space URI.
3074
3646
  */
3075
3647
  async hostOwnedSpace(name) {
3076
- if (!this.auth || !this.auth.tinyCloudSession) {
3648
+ const session = this.auth?.tinyCloudSession;
3649
+ if (!this.auth || !session) {
3077
3650
  throw new Error("Not signed in. Call signIn() first.");
3078
3651
  }
3079
3652
  const spaceId = this.ownedSpaceId(name);
@@ -3089,22 +3662,26 @@ var _TinyCloudNode = class _TinyCloudNode {
3089
3662
  }
3090
3663
  const activation = await (0, import_sdk_core8.activateSessionWithHost)(
3091
3664
  host,
3092
- this.auth.tinyCloudSession.delegationHeader
3665
+ session.delegationHeader
3093
3666
  );
3094
3667
  if (!activation.success || activation.skipped?.includes(spaceId)) {
3095
3668
  throw new Error(
3096
3669
  `Failed to activate session for owned space ${spaceId}: ${activation.error ?? "space was skipped"}`
3097
3670
  );
3098
3671
  }
3099
- void this.account.spaces.register({
3100
- spaceId,
3101
- name,
3102
- ownerDid: this.did,
3103
- type: "owned",
3104
- permissions: ["*"],
3105
- status: "active"
3106
- }).catch(() => {
3107
- });
3672
+ try {
3673
+ await this.enqueueAccountRegistryOperation(session, async () => {
3674
+ await this.account.spaces.register({
3675
+ spaceId,
3676
+ name,
3677
+ ownerDid: this.did,
3678
+ type: "owned",
3679
+ permissions: ["*"],
3680
+ status: "active"
3681
+ });
3682
+ });
3683
+ } catch {
3684
+ }
3108
3685
  return spaceId;
3109
3686
  }
3110
3687
  /**
@@ -3139,23 +3716,20 @@ var _TinyCloudNode = class _TinyCloudNode {
3139
3716
  if (!this.auth || !this.auth.tinyCloudSession) {
3140
3717
  throw new Error("Not signed in. Call signIn() first.");
3141
3718
  }
3719
+ await this.accountRegistryTail;
3142
3720
  const spaceId = this.ownedSpaceId(name);
3143
3721
  if (await this.isOwnedSpaceRegistered(spaceId)) {
3144
3722
  return spaceId;
3145
3723
  }
3146
- const hosted = await this.hostOwnedSpace(name);
3147
- try {
3148
- await this.account.spaces.register({
3149
- spaceId,
3150
- name,
3151
- ownerDid: this.did,
3152
- type: "owned",
3153
- permissions: ["*"],
3154
- status: "active"
3155
- });
3156
- } catch {
3724
+ return this.hostOwnedSpace(name);
3725
+ }
3726
+ /** Resolve and authenticate the owner encoded by an owned-space URI. */
3727
+ credentialSpaceOwnerDid(spaceId) {
3728
+ const ownerDid = this.ownerDidFromSpaceId(spaceId);
3729
+ if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
3730
+ throw new Error("Credential space owner does not match the active TinyCloud owner");
3157
3731
  }
3158
- return hosted;
3732
+ return ownerDid;
3159
3733
  }
3160
3734
  /**
3161
3735
  * Check whether an owned space is already registered/hosted by consulting the
@@ -3700,6 +4274,106 @@ var _TinyCloudNode = class _TinyCloudNode {
3700
4274
  if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
3701
4275
  return signBytesWithJwk(bytes, session.jwk);
3702
4276
  }
4277
+ /** Author a proofless Policy/v3 owner root with the authenticated holder key. */
4278
+ async createUnifiedOwnerRoot(input) {
4279
+ const ownerDid = this.credentialHolderDid;
4280
+ if (input.ownerDid !== ownerDid) throw new Error("unified owner root signer does not match owner DID");
4281
+ const facts = {
4282
+ role: input.role,
4283
+ mode: input.role === "policy-authority" ? "policy-source" : "conditional-mint",
4284
+ ownerDid,
4285
+ policyId: input.policyId,
4286
+ policyDigestHex: input.policyDigestHex,
4287
+ policyCid: input.policyCid,
4288
+ contentSourceDigestHex: input.contentSourceDigestHex,
4289
+ capabilityCeilingHashHex: input.capabilityCeilingHashHex,
4290
+ nativeProjectionHashHex: input.nativeProjectionHashHex,
4291
+ nodeAudience: input.nodeAudience,
4292
+ ...input.role === "policy-enforcement" ? { enforcerDid: input.audienceDid } : {}
4293
+ };
4294
+ const root = await (0, import_sdk_core8.signCompactUcanRootAuthorization)({
4295
+ issuerDid: ownerDid,
4296
+ audienceDid: input.audienceDid,
4297
+ attenuation: unifiedRootAttenuation(input.capabilities),
4298
+ facts: [facts],
4299
+ notBefore: Math.floor(input.notBefore.getTime() / 1e3),
4300
+ expiresAt: Math.floor(input.expiresAt.getTime() / 1e3),
4301
+ nonce: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
4302
+ sign: (bytes) => this.signSessionBytes(bytes)
4303
+ });
4304
+ const kv = input.capabilities.find((capability) => capability.kind === "kv");
4305
+ const resource = kv?.resource ?? "";
4306
+ const marker = resource.indexOf("/kv/");
4307
+ return {
4308
+ cid: root.cid,
4309
+ delegationHeader: { Authorization: root.authorization },
4310
+ delegateDID: input.audienceDid,
4311
+ spaceId: marker === -1 ? "" : resource.slice("tinycloud://".length, marker),
4312
+ path: marker === -1 ? resource : resource.slice(marker + 4),
4313
+ actions: kv?.actions ?? [],
4314
+ expiry: input.expiresAt
4315
+ };
4316
+ }
4317
+ /** Apply only an already-enabled signing policy to the exact credential request. */
4318
+ async autoSignCredentialBytes(bytes) {
4319
+ const strategy = this.config.signStrategy;
4320
+ if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
4321
+ if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
4322
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4323
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4324
+ const request = {
4325
+ address: this.address ?? "",
4326
+ chainId: this.session?.chainId ?? this._chainId,
4327
+ message: new TextDecoder().decode(bytes),
4328
+ type: "message",
4329
+ purpose: "message"
4330
+ };
4331
+ const decision = await strategy.handler(request);
4332
+ if (!decision.approved) {
4333
+ if (decision.needsApproval) return void 0;
4334
+ throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
4335
+ }
4336
+ if (typeof decision.signature !== "string" || !await (0, import_sdk_core8.verifyEip191MessageSignature)(
4337
+ request.message,
4338
+ decision.signature,
4339
+ request.address
4340
+ )) {
4341
+ throw new Error("OpenKey credential signature evidence is invalid");
4342
+ }
4343
+ return this.signSessionBytes(bytes);
4344
+ }
4345
+ /** Invoke the configured interactive approval strategy before session signing. */
4346
+ async approveCredentialBytes(bytes) {
4347
+ const strategy = this.config.signStrategy;
4348
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4349
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4350
+ const request = {
4351
+ address: this.address ?? "",
4352
+ chainId: this.session?.chainId ?? this._chainId,
4353
+ message: new TextDecoder().decode(bytes),
4354
+ type: "message",
4355
+ purpose: "message"
4356
+ };
4357
+ const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
4358
+ if (approval) {
4359
+ const decision = await approval(request);
4360
+ if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
4361
+ if (decision.signature !== void 0 && !await (0, import_sdk_core8.verifyEip191MessageSignature)(
4362
+ request.message,
4363
+ decision.signature,
4364
+ request.address
4365
+ )) {
4366
+ throw new Error("OpenKey credential signature evidence is invalid");
4367
+ }
4368
+ return this.signSessionBytes(bytes);
4369
+ }
4370
+ if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
4371
+ if (this.config.signer) {
4372
+ await this.config.signer.signMessage(request.message);
4373
+ return this.signSessionBytes(bytes);
4374
+ }
4375
+ throw new Error("Interactive OpenKey approval is not configured");
4376
+ }
3703
4377
  /** Bind a session-signed invocation to the canonical service audience. */
3704
4378
  async bindInvocationAudience(authorization, audience) {
3705
4379
  const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
@@ -3767,7 +4441,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3767
4441
  nonce: this.config.nonce,
3768
4442
  siweConfig: this.config.siweConfig,
3769
4443
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3770
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4444
+ 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
4445
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3772
4446
  });
3773
4447
  this.tc = new import_sdk_core8.TinyCloud(this.auth, {
@@ -3820,7 +4494,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3820
4494
  nonce: this.config.nonce,
3821
4495
  siweConfig: this.config.siweConfig,
3822
4496
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3823
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4497
+ 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
4498
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3825
4499
  });
3826
4500
  this.tc = new import_sdk_core8.TinyCloud(this.auth, {
@@ -6569,35 +7243,44 @@ var _TinyCloudNode = class _TinyCloudNode {
6569
7243
  if (parentDelegation.disableSubDelegation) {
6570
7244
  throw new Error("Parent delegation does not allow sub-delegation");
6571
7245
  }
6572
- if (!params.path.startsWith(parentDelegation.path)) {
7246
+ const parentResources = parentDelegation.resources ?? [{
7247
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7248
+ space: parentDelegation.spaceId,
7249
+ path: parentDelegation.path,
7250
+ actions: parentDelegation.actions
7251
+ }];
7252
+ const requestedResources = params.resources ?? [{
7253
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7254
+ space: parentDelegation.spaceId,
7255
+ path: params.path,
7256
+ actions: params.actions
7257
+ }];
7258
+ const resourceIsContained = (parent, child) => {
7259
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
7260
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
7261
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
7262
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
7263
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
7264
+ };
7265
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
6573
7266
  throw new Error(
6574
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
7267
+ "Sub-delegation resources exceed the received delegation"
6575
7268
  );
6576
7269
  }
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
7270
  const now = /* @__PURE__ */ new Date();
6586
7271
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
6587
7272
  const requestedExpiry = new Date(now.getTime() + expiryMs);
6588
7273
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
6589
7274
  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 };
7275
+ const rawAbilities = {};
7276
+ for (const resource of requestedResources) {
7277
+ const service = resource.service.replace(/^tinycloud\./, "");
7278
+ if (service === "encryption") {
7279
+ rawAbilities[resource.path] = [...resource.actions];
7280
+ continue;
7281
+ }
7282
+ abilities[service] ?? (abilities[service] = {});
7283
+ abilities[service][resource.path] = [...resource.actions];
6601
7284
  }
6602
7285
  const targetHost = parentDelegation.host ?? this.config.host;
6603
7286
  const prepared = this.wasmBindings.prepareSession({
@@ -6609,7 +7292,8 @@ var _TinyCloudNode = class _TinyCloudNode {
6609
7292
  expirationTime: actualExpiry.toISOString(),
6610
7293
  spaceId: parentDelegation.spaceId,
6611
7294
  delegateUri: params.delegateDID,
6612
- parents: [parentDelegation.cid]
7295
+ parents: [parentDelegation.cid],
7296
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
6613
7297
  });
6614
7298
  const signature = await this.signer.signMessage(prepared.siwe);
6615
7299
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -6627,14 +7311,20 @@ var _TinyCloudNode = class _TinyCloudNode {
6627
7311
  cid: subDelegationSession.delegationCid,
6628
7312
  delegationHeader: subDelegationSession.delegationHeader,
6629
7313
  spaceId: parentDelegation.spaceId,
6630
- path: params.path,
6631
- actions: params.actions,
7314
+ path: requestedResources[0].path,
7315
+ actions: requestedResources[0].actions,
6632
7316
  disableSubDelegation: params.disableSubDelegation ?? false,
6633
7317
  expiry: actualExpiry,
6634
7318
  delegateDID: params.delegateDID,
6635
7319
  ownerAddress: parentDelegation.ownerAddress,
6636
7320
  chainId: parentDelegation.chainId,
6637
- host: targetHost
7321
+ host: targetHost,
7322
+ resources: requestedResources.map((resource) => ({
7323
+ service: resource.service.replace(/^tinycloud\./, ""),
7324
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
7325
+ path: resource.path,
7326
+ actions: [...resource.actions]
7327
+ }))
6638
7328
  };
6639
7329
  }
6640
7330
  };
@@ -6656,6 +7346,338 @@ var import_sdk_core12 = require("@tinycloud/sdk-core");
6656
7346
 
6657
7347
  // src/delegation.ts
6658
7348
  var import_sdk_core9 = require("@tinycloud/sdk-core");
7349
+ var SERVICE_BY_SHORT_NAME = {
7350
+ kv: "tinycloud.kv",
7351
+ sql: "tinycloud.sql",
7352
+ duckdb: "tinycloud.duckdb",
7353
+ hooks: "tinycloud.hooks"
7354
+ };
7355
+ function authorizationWithoutBearer(authorization) {
7356
+ return authorization.replace(/^Bearer /i, "");
7357
+ }
7358
+ function normalizedHost(host) {
7359
+ if (typeof host !== "string" || host.length === 0) {
7360
+ throw new Error("Validated runtime delegation requires a non-empty host.");
7361
+ }
7362
+ return host.replace(/\/+$/, "");
7363
+ }
7364
+ function didPrincipalsMatch(left, right) {
7365
+ try {
7366
+ return (0, import_sdk_core9.principalDidEquals)(left, right);
7367
+ } catch {
7368
+ return left === right;
7369
+ }
7370
+ }
7371
+ function compactUcanPayload(authorization) {
7372
+ const compact = authorizationWithoutBearer(authorization);
7373
+ const parts = compact.split(".");
7374
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
7375
+ throw new Error(
7376
+ "Validated runtime delegation requires a compact UCAN authorization so its signed authority can be derived."
7377
+ );
7378
+ }
7379
+ try {
7380
+ const payload = JSON.parse(
7381
+ new TextDecoder("utf-8", { fatal: true }).decode((0, import_sdk_core9.decodeBase64Url)(parts[1]))
7382
+ );
7383
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
7384
+ throw new Error("payload is not an object");
7385
+ }
7386
+ return payload;
7387
+ } catch {
7388
+ throw new Error(
7389
+ "Validated runtime delegation authorization has an invalid compact UCAN payload."
7390
+ );
7391
+ }
7392
+ }
7393
+ function canonicalPermissionEntries(permissions) {
7394
+ return permissions.map((permission) => ({
7395
+ ...permission,
7396
+ actions: [...permission.actions].sort()
7397
+ })).sort(
7398
+ (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")) || (0, import_sdk_core9.canonicalizeRecapCaveats)(left.caveats).localeCompare((0, import_sdk_core9.canonicalizeRecapCaveats)(right.caveats))
7399
+ );
7400
+ }
7401
+ function signedCaveats(value) {
7402
+ if (!Array.isArray(value) || value.some(
7403
+ (caveat) => caveat === null || typeof caveat !== "object" || Array.isArray(caveat)
7404
+ )) {
7405
+ throw new Error("Validated runtime delegation has malformed signed ReCap caveats.");
7406
+ }
7407
+ const cloned = JSON.parse(JSON.stringify(value));
7408
+ return (0, import_sdk_core9.canonicalizeRecapCaveats)(cloned) === (0, import_sdk_core9.canonicalizeRecapCaveats)(void 0) ? void 0 : cloned;
7409
+ }
7410
+ function groupedSignedEntries(service, space, path, abilities) {
7411
+ const groups = /* @__PURE__ */ new Map();
7412
+ for (const [action, rawCaveats] of Object.entries(abilities)) {
7413
+ const caveats = signedCaveats(rawCaveats);
7414
+ const identity = (0, import_sdk_core9.canonicalizeRecapCaveats)(caveats);
7415
+ const group = groups.get(identity) ?? { actions: [], caveats };
7416
+ group.actions.push(action);
7417
+ groups.set(identity, group);
7418
+ }
7419
+ return [...groups.values()].map((group) => ({
7420
+ service,
7421
+ ...space === void 0 ? {} : { space },
7422
+ path,
7423
+ actions: group.actions.sort(),
7424
+ ...group.caveats === void 0 ? {} : { caveats: group.caveats }
7425
+ }));
7426
+ }
7427
+ function signedAuthorityFromCompactUcan(authorization) {
7428
+ const payload = compactUcanPayload(authorization);
7429
+ if (typeof payload.aud !== "string" || payload.aud.length === 0) {
7430
+ throw new Error("Validated runtime delegation is missing a signed audience.");
7431
+ }
7432
+ if (typeof payload.iss !== "string" || payload.iss.length === 0) {
7433
+ throw new Error("Validated runtime delegation is missing a signed issuer.");
7434
+ }
7435
+ if (!Array.isArray(payload.prf) || payload.prf.some(
7436
+ (proof) => typeof proof !== "string" || proof.length === 0
7437
+ )) {
7438
+ throw new Error("Validated runtime delegation has malformed signed proofs.");
7439
+ }
7440
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
7441
+ throw new Error("Validated runtime delegation is missing a signed expiry.");
7442
+ }
7443
+ const expiry = new Date(payload.exp * 1e3);
7444
+ if (Number.isNaN(expiry.getTime())) {
7445
+ throw new Error("Validated runtime delegation has an invalid signed expiry.");
7446
+ }
7447
+ if (payload.att === null || typeof payload.att !== "object" || Array.isArray(payload.att)) {
7448
+ throw new Error("Validated runtime delegation is missing a signed attenuation.");
7449
+ }
7450
+ const fact = Array.isArray(payload.fct) && payload.fct.length > 0 && payload.fct[0] !== null && typeof payload.fct[0] === "object" && !Array.isArray(payload.fct[0]) ? payload.fct[0] : void 0;
7451
+ const remainingRedelegationDepth = fact?.remainingRedelegationDepth;
7452
+ if (remainingRedelegationDepth !== void 0 && (!Number.isInteger(remainingRedelegationDepth) || remainingRedelegationDepth < 0 || remainingRedelegationDepth > 8)) {
7453
+ throw new Error("Validated runtime delegation has invalid signed redelegation depth.");
7454
+ }
7455
+ const permissions = [];
7456
+ for (const [resource, abilities] of Object.entries(payload.att)) {
7457
+ if (abilities === null || typeof abilities !== "object" || Array.isArray(abilities)) {
7458
+ throw new Error("Validated runtime delegation has an invalid signed attenuation.");
7459
+ }
7460
+ const abilityEntries = Object.entries(abilities);
7461
+ if (abilityEntries.length === 0) {
7462
+ throw new Error("Validated runtime delegation has an empty signed capability.");
7463
+ }
7464
+ if (resource.startsWith("urn:tinycloud:encryption:")) {
7465
+ permissions.push(...groupedSignedEntries(
7466
+ "tinycloud.encryption",
7467
+ void 0,
7468
+ resource,
7469
+ abilities
7470
+ ));
7471
+ continue;
7472
+ }
7473
+ const nativeMatch = /^(tinycloud:[^/]+)\/(kv|sql|duckdb|hooks)\/(.*)$/.exec(resource);
7474
+ const policyMatch = /^tinycloud:\/\/([^/]+)\/(kv|sql|duckdb|hooks)\/(.*)$/.exec(resource);
7475
+ const match = nativeMatch ?? policyMatch;
7476
+ if (!match) {
7477
+ throw new Error(
7478
+ `Validated runtime delegation has an unsupported signed resource '${resource}'.`
7479
+ );
7480
+ }
7481
+ const [, space, shortService, path] = match;
7482
+ const service = SERVICE_BY_SHORT_NAME[shortService];
7483
+ if (service === void 0) {
7484
+ throw new Error(
7485
+ `Validated runtime delegation has an unsupported signed service '${shortService}'.`
7486
+ );
7487
+ }
7488
+ permissions.push(...groupedSignedEntries(
7489
+ service,
7490
+ space,
7491
+ path,
7492
+ abilities
7493
+ ));
7494
+ }
7495
+ if (permissions.length === 0) {
7496
+ throw new Error("Validated runtime delegation has no signed capabilities.");
7497
+ }
7498
+ const canonicalPermissions = canonicalPermissionEntries(permissions);
7499
+ const canonicalResources = canonicalPermissions.map((permission) => ({
7500
+ service: permission.service === "tinycloud.encryption" ? "encryption" : permission.service.slice("tinycloud.".length),
7501
+ space: permission.service === "tinycloud.encryption" ? "encryption" : permission.space,
7502
+ path: permission.path,
7503
+ actions: [...permission.actions],
7504
+ ...permission.caveats === void 0 ? {} : { caveats: permission.caveats }
7505
+ }));
7506
+ return {
7507
+ issuer: payload.iss,
7508
+ audience: payload.aud,
7509
+ expiry,
7510
+ proofs: [...payload.prf],
7511
+ ...remainingRedelegationDepth === void 0 ? {} : { remainingRedelegationDepth },
7512
+ permissions: canonicalPermissions,
7513
+ resources: canonicalResources
7514
+ };
7515
+ }
7516
+ function canonicalResourcesFromPortableDelegation(delegation) {
7517
+ if (delegation.resources !== void 0 && delegation.resources.length > 0) {
7518
+ return delegation.resources.map((resource) => ({ ...resource, actions: [...resource.actions].sort() })).sort(
7519
+ (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")) || (0, import_sdk_core9.canonicalizeRecapCaveats)(left.caveats).localeCompare((0, import_sdk_core9.canonicalizeRecapCaveats)(right.caveats))
7520
+ );
7521
+ }
7522
+ const byService = /* @__PURE__ */ new Map();
7523
+ for (const action of delegation.actions) {
7524
+ const [service] = action.split("/", 1);
7525
+ const shortService = service.replace(/^tinycloud\./, "");
7526
+ const values = byService.get(shortService) ?? [];
7527
+ values.push(action);
7528
+ byService.set(shortService, values);
7529
+ }
7530
+ return [...byService.entries()].map(([service, actions]) => ({
7531
+ service,
7532
+ space: service === "encryption" ? "encryption" : delegation.spaceId,
7533
+ path: delegation.path,
7534
+ actions: [...new Set(actions)].sort(),
7535
+ ...delegation.caveats === void 0 ? {} : { caveats: delegation.caveats }
7536
+ })).sort(
7537
+ (left, right) => left.service.localeCompare(right.service) || left.space.localeCompare(right.space) || left.path.localeCompare(right.path)
7538
+ );
7539
+ }
7540
+ function resourcesMatch(actual, expected) {
7541
+ const comparable = (resource) => ({
7542
+ ...resource,
7543
+ caveats: (0, import_sdk_core9.canonicalizeRecapCaveats)(resource.caveats)
7544
+ });
7545
+ return JSON.stringify(actual.map(comparable)) === JSON.stringify(expected.map(comparable));
7546
+ }
7547
+ async function activateValidatedRuntimeDelegation(node, delegation, options) {
7548
+ const host = normalizedHost(options.host);
7549
+ if (delegation.host !== void 0 && normalizedHost(delegation.host) !== host) {
7550
+ throw new Error(
7551
+ `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`
7552
+ );
7553
+ }
7554
+ if (!(delegation.expiry instanceof Date) || Number.isNaN(delegation.expiry.getTime())) {
7555
+ throw new Error("Runtime delegation has an invalid expiry.");
7556
+ }
7557
+ if (delegation.expiry.getTime() <= Date.now()) {
7558
+ throw new Error("Runtime delegation is expired.");
7559
+ }
7560
+ const authorization = delegation.delegationHeader?.Authorization;
7561
+ if (typeof authorization !== "string" || authorization.length === 0) {
7562
+ throw new Error("Runtime delegation is missing authorization bytes.");
7563
+ }
7564
+ const cid = node.computeDelegationCid(authorization);
7565
+ if (cid !== delegation.cid) {
7566
+ throw new Error("Runtime delegation CID does not match authorization bytes.");
7567
+ }
7568
+ const signed = signedAuthorityFromCompactUcan(authorization);
7569
+ if (signed.expiry.getTime() <= Date.now()) {
7570
+ throw new Error("Runtime delegation is expired.");
7571
+ }
7572
+ if (delegation.expiry.getTime() !== signed.expiry.getTime()) {
7573
+ throw new Error("Runtime delegation expiry does not match signed authority.");
7574
+ }
7575
+ if (!didPrincipalsMatch(signed.audience, node.sessionDid)) {
7576
+ throw new Error(
7577
+ `Runtime delegation targets ${signed.audience} but this session is ${node.sessionDid}.`
7578
+ );
7579
+ }
7580
+ if (!didPrincipalsMatch(delegation.delegateDID, signed.audience)) {
7581
+ throw new Error("Runtime delegation audience does not match signed authority.");
7582
+ }
7583
+ const declaredResources = canonicalResourcesFromPortableDelegation(delegation);
7584
+ if (!resourcesMatch(declaredResources, signed.resources)) {
7585
+ throw new Error("Runtime delegation resources do not match signed authority.");
7586
+ }
7587
+ const previouslyInstalled = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
7588
+ if (previouslyInstalled !== void 0 && previouslyInstalled.delegationHeader.Authorization !== authorization) {
7589
+ throw new Error(
7590
+ "A different authorization is already installed for this runtime delegation CID."
7591
+ );
7592
+ }
7593
+ const primary = signed.resources[0];
7594
+ const signedSpace = signed.permissions.find(
7595
+ (permission) => permission.service !== "tinycloud.encryption"
7596
+ )?.space;
7597
+ const installedCandidate = {
7598
+ cid,
7599
+ delegationHeader: { Authorization: authorization },
7600
+ ownerAddress: delegation.ownerAddress,
7601
+ chainId: delegation.chainId,
7602
+ delegatorDID: signed.issuer,
7603
+ ...signed.proofs.length === 1 ? { parentCid: signed.proofs[0] } : {},
7604
+ ...signed.remainingRedelegationDepth === void 0 ? {} : {
7605
+ allowSubDelegation: signed.remainingRedelegationDepth > 0,
7606
+ disableSubDelegation: signed.remainingRedelegationDepth === 0
7607
+ },
7608
+ authHeader: authorization,
7609
+ spaceId: signedSpace ?? delegation.spaceId,
7610
+ path: primary.path,
7611
+ actions: [...primary.actions],
7612
+ ...primary.caveats === void 0 ? {} : { caveats: primary.caveats },
7613
+ resources: signed.resources.map((resource) => ({
7614
+ ...resource,
7615
+ actions: [...resource.actions]
7616
+ })),
7617
+ expiry: new Date(signed.expiry),
7618
+ delegateDID: signed.audience,
7619
+ host
7620
+ };
7621
+ if (node.isSessionOnly === true && typeof node.did === "string" && (0, import_sdk_core9.principalDidEquals)(node.did, node.sessionDid) && typeof node.useDelegation === "function") {
7622
+ await node.useDelegation(installedCandidate);
7623
+ } else {
7624
+ await node.useRuntimeDelegation(installedCandidate);
7625
+ }
7626
+ const installed = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
7627
+ if (!installed) {
7628
+ throw new Error("Runtime delegation activation did not install the validated authority.");
7629
+ }
7630
+ if (installed.delegationHeader.Authorization !== authorization || !didPrincipalsMatch(installed.delegateDID, signed.audience) || installed.expiry.getTime() !== signed.expiry.getTime() || !resourcesMatch(
7631
+ canonicalResourcesFromPortableDelegation(installed),
7632
+ signed.resources
7633
+ ) || normalizedHost(installed.host ?? "") !== host) {
7634
+ throw new Error(
7635
+ "Runtime delegation activation installed authority that differs from the validated authority."
7636
+ );
7637
+ }
7638
+ return {
7639
+ cid,
7640
+ delegation: installed,
7641
+ effectivePermissions: signed.permissions,
7642
+ expiry: new Date(signed.expiry),
7643
+ audience: signed.audience,
7644
+ host: normalizedHost(installed.host ?? host)
7645
+ };
7646
+ }
7647
+ async function activateCompactRuntimeDelegation(node, input) {
7648
+ const authorization = input.authorization;
7649
+ const cid = node.computeDelegationCid(authorization);
7650
+ if (cid !== input.cid) {
7651
+ throw new Error("Compact runtime delegation CID does not match authorization bytes.");
7652
+ }
7653
+ const signed = signedAuthorityFromCompactUcan(authorization);
7654
+ const primary = signed.resources[0];
7655
+ const signedSpace = signed.permissions.find(
7656
+ (permission) => permission.service !== "tinycloud.encryption"
7657
+ )?.space;
7658
+ const delegation = {
7659
+ cid,
7660
+ delegationHeader: { Authorization: authorization },
7661
+ ownerAddress: input.ownerAddress,
7662
+ chainId: input.chainId,
7663
+ delegatorDID: signed.issuer,
7664
+ authHeader: authorization,
7665
+ spaceId: signedSpace ?? primary.space,
7666
+ path: primary.path,
7667
+ actions: [...primary.actions],
7668
+ ...primary.caveats === void 0 ? {} : { caveats: primary.caveats },
7669
+ resources: signed.resources.map((resource) => ({
7670
+ ...resource,
7671
+ actions: [...resource.actions]
7672
+ })),
7673
+ expiry: new Date(signed.expiry),
7674
+ delegateDID: signed.audience,
7675
+ host: normalizedHost(input.host)
7676
+ };
7677
+ return activateValidatedRuntimeDelegation(node, delegation, {
7678
+ host: input.host
7679
+ });
7680
+ }
6659
7681
  function serializeDelegation(delegation) {
6660
7682
  return JSON.stringify({
6661
7683
  ...delegation,
@@ -6731,6 +7753,7 @@ var import_sdk_core21 = require("@tinycloud/sdk-core");
6731
7753
  VaultPublicSpaceKVActions,
6732
7754
  VersionCheckError,
6733
7755
  WasmKeyProvider,
7756
+ activateCompactRuntimeDelegation,
6734
7757
  addressStorageKey,
6735
7758
  buildSpaceUri,
6736
7759
  canonicalizeAddress,