@tinycloud/node-sdk 2.11.0-beta.9 → 2.11.1-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.js CHANGED
@@ -220,6 +220,8 @@ var FileSessionStorage = class {
220
220
  };
221
221
 
222
222
  // src/authorization/NodeUserAuthorization.ts
223
+ import { createHash } from "crypto";
224
+ import { SiweMessage } from "siwe";
223
225
  import {
224
226
  fetchPeerId,
225
227
  submitHostDelegation,
@@ -238,6 +240,13 @@ import {
238
240
  canonicalizeAddress,
239
241
  makePkhSpaceId,
240
242
  pkhDid,
243
+ extractImmutableSiweFields,
244
+ diffImmutableSiweFields,
245
+ extractRecapAttenuations,
246
+ unauthorizedRecapCapabilities,
247
+ parseCanonicalRecapResource,
248
+ ACCOUNT_REGISTRY_SPACE,
249
+ ACCOUNT_MANIFEST_PERMISSIONS,
241
250
  KV,
242
251
  SQL,
243
252
  DUCKDB,
@@ -253,6 +262,27 @@ var defaultSignStrategy = { type: "auto-sign" };
253
262
  // src/authorization/NodeUserAuthorization.ts
254
263
  var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
255
264
  var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
265
+ function canonicalizeOpenKeyManifestJson(value) {
266
+ const json = JSON.stringify(value);
267
+ if (json === void 0) {
268
+ throw new TypeError("manifest is not JSON-serializable");
269
+ }
270
+ return canonicalizeJsonValue(JSON.parse(json));
271
+ }
272
+ function canonicalizeJsonValue(value) {
273
+ if (value === null || typeof value !== "object") {
274
+ return JSON.stringify(value);
275
+ }
276
+ if (Array.isArray(value)) {
277
+ return `[${value.map(canonicalizeJsonValue).join(",")}]`;
278
+ }
279
+ return `{${Object.keys(value).sort().map(
280
+ (key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(value[key])}`
281
+ ).join(",")}}`;
282
+ }
283
+ function canonicalOpenKeyManifestSha256Hex(value) {
284
+ return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
285
+ }
256
286
  function didPrincipalMatches(actual, expected) {
257
287
  try {
258
288
  return principalDidEquals(actual, expected);
@@ -270,6 +300,16 @@ function addRawAbility(rawAbilities, resource, action) {
270
300
  actions.push(action);
271
301
  }
272
302
  }
303
+ function cloneAbilitiesMap(actions) {
304
+ return Object.fromEntries(
305
+ Object.entries(actions).map(([service, paths]) => [
306
+ service,
307
+ Object.fromEntries(
308
+ Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
309
+ )
310
+ ])
311
+ );
312
+ }
273
313
  var NodeUserAuthorization = class {
274
314
  constructor(config) {
275
315
  this.activeSessionKeyId = "default";
@@ -541,23 +581,41 @@ var NodeUserAuthorization = class {
541
581
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
542
582
  const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
543
583
  const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
544
- return {
545
- abilities: this.defaultActions,
546
- spaceId: primarySpaceId2,
547
- spaceAbilities: {
548
- [primarySpaceId2]: this.defaultActions,
549
- [secretsSpaceId]: {
550
- kv: {
551
- "vault/secrets/": [
552
- KV.GET,
553
- KV.PUT,
554
- KV.DEL,
555
- KV.LIST,
556
- KV.METADATA
557
- ]
584
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
585
+ const spaceAbilities2 = {
586
+ [primarySpaceId2]: primaryActions,
587
+ [secretsSpaceId]: {
588
+ kv: {
589
+ "vault/secrets/": [
590
+ KV.GET,
591
+ KV.PUT,
592
+ KV.DEL,
593
+ KV.LIST,
594
+ KV.METADATA
595
+ ]
596
+ }
597
+ }
598
+ };
599
+ if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
600
+ spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
601
+ } else if (this.includeAccountRegistryPermissions) {
602
+ const accountAbilities = resourceCapabilitiesToAbilitiesMap(
603
+ ACCOUNT_MANIFEST_PERMISSIONS
604
+ );
605
+ for (const [service, paths] of Object.entries(accountAbilities)) {
606
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
607
+ for (const [path, actions] of Object.entries(paths)) {
608
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
609
+ for (const action of actions) {
610
+ if (!existingActions.includes(action)) existingActions.push(action);
558
611
  }
559
612
  }
560
- },
613
+ }
614
+ }
615
+ return {
616
+ abilities: primaryActions,
617
+ spaceId: primarySpaceId2,
618
+ spaceAbilities: spaceAbilities2,
561
619
  rawAbilities: {
562
620
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
563
621
  }
@@ -1138,6 +1196,398 @@ var NodeUserAuthorization = class {
1138
1196
  await this.ensureSpaceExists();
1139
1197
  return clientSession;
1140
1198
  }
1199
+ /**
1200
+ * End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
1201
+ *
1202
+ * This is the production entry point that wires
1203
+ * {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
1204
+ * {@link signInWithOpenKeyResult} into one call. Consumers pass in a
1205
+ * caller-provided `authorizeFn` (typically a thin wrapper around
1206
+ * `openkey.authorizeTinyCloud()`) so this method has no direct
1207
+ * dependency on the OpenKey SDK — it works with any transport that
1208
+ * conforms to the versioned protocol.
1209
+ *
1210
+ * The `authorizeFn` MUST:
1211
+ * 1. Send the prepared SIWE bytes to OpenKey.
1212
+ * 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
1213
+ * `signedMessage` — the bytes the signature actually verifies
1214
+ * against — plus `selectedActionKeys` and `permissions`).
1215
+ *
1216
+ * NodeUserAuthorization then completes the session with `signedMessage`
1217
+ * (never the prepared bytes), verifying the signature, the immutable
1218
+ * SIWE fields, and every subset invariant enforced by
1219
+ * {@link signInWithOpenKeyResult}. This preserves the exact-byte
1220
+ * guarantee: if the user did NOT narrow, the returned `signedMessage`
1221
+ * equals the prepared SIWE byte-for-byte; if the user narrowed, only
1222
+ * the ReCap block differs and every immutable header field survives.
1223
+ *
1224
+ * Legacy exact-byte {@link signIn} callers are unaffected — this method
1225
+ * is opt-in.
1226
+ *
1227
+ * @param authorizeFn Callback that invokes the versioned OpenKey
1228
+ * `authorizeTinyCloud()` protocol and returns the result. Receives
1229
+ * the prepared SIWE bytes plus a `host` hint the widget should bind
1230
+ * into its /authorize-sign-prepare context.
1231
+ * @param options Optional overrides for the OpenKey request.
1232
+ */
1233
+ async signInWithOpenKey(authorizeFn, options) {
1234
+ if (options?.host !== void 0) {
1235
+ const overrideHost = options.host.trim();
1236
+ if (!overrideHost) {
1237
+ throw new Error("OpenKey authorization host must not be empty");
1238
+ }
1239
+ this.tinycloudHosts = [overrideHost];
1240
+ }
1241
+ const signingAddress = canonicalizeAddress(await this.signer.getAddress());
1242
+ const signingChainId = await this.signer.getChainId();
1243
+ await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
1244
+ const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
1245
+ const presentation = this.buildPresentationEnvelope(options?.reason);
1246
+ const hostHint = this.primaryTinyCloudHost;
1247
+ const result = await authorizeFn({
1248
+ protocolVersion: 1,
1249
+ siwe: prepared.siwe,
1250
+ jwk,
1251
+ host: hostHint,
1252
+ keyId: options?.openkeyKeyId,
1253
+ presentation
1254
+ });
1255
+ return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
1256
+ }
1257
+ /**
1258
+ * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
1259
+ * envelope is display-only — the OpenKey widget renders it under an
1260
+ * honest trust label, and the server /authorize-sign-prepare route
1261
+ * decides whether to upgrade the label to `origin-bound` by fetching
1262
+ * the well-known manifest and comparing its digest to
1263
+ * `manifestDigest`.
1264
+ *
1265
+ * Returns `undefined` when no manifest is set — the widget renders as
1266
+ * "no manifest supplied" (unsigned).
1267
+ *
1268
+ * The digest is a deterministic SHA-256 over canonical JSON of the
1269
+ * first manifest payload; OpenKey parses the fetched well-known manifest
1270
+ * and applies the same canonicalization before comparing digests.
1271
+ */
1272
+ buildPresentationEnvelope(reason) {
1273
+ const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
1274
+ if (manifests.length === 0 && !reason) {
1275
+ return void 0;
1276
+ }
1277
+ const primary = manifests[0];
1278
+ let displayName;
1279
+ let manifestId;
1280
+ let manifestDigest;
1281
+ const envelopeManifests = [];
1282
+ for (const m of manifests) {
1283
+ envelopeManifests.push({
1284
+ name: m.name,
1285
+ appId: m.app_id,
1286
+ // Deep-copy to prevent later mutations from affecting the digest.
1287
+ payload: JSON.parse(JSON.stringify(m))
1288
+ });
1289
+ }
1290
+ if (primary) {
1291
+ displayName = primary.name;
1292
+ manifestId = primary.app_id;
1293
+ manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
1294
+ }
1295
+ return {
1296
+ protocolVersion: 1,
1297
+ displayName,
1298
+ reason: reason && reason.length > 0 ? reason : void 0,
1299
+ manifestId,
1300
+ manifestDigest,
1301
+ manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
1302
+ };
1303
+ }
1304
+ /**
1305
+ * Complete sign-in with a versioned OpenKey authorization result.
1306
+ *
1307
+ * Where `signInWithPreparedSession` completes with a signature over the
1308
+ * ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
1309
+ * OpenKey widget actually presented and signed (`signedMessage`). It
1310
+ * exists because the versioned authorizeTinyCloud protocol lets the user
1311
+ * narrow capabilities; the returned bytes may differ from the original
1312
+ * request and are the only ones the signature actually verifies against.
1313
+ *
1314
+ * The `result` parameter is validated at the trust boundary via
1315
+ * `validateAuthorizationResultV1` (in sdk-core). Callers should validate
1316
+ * BEFORE calling this method so unknown/missing fields are rejected
1317
+ * before any session state is created.
1318
+ *
1319
+ * Validation performed here (defence-in-depth: even a "valid" v1 payload
1320
+ * must clear these checks before any session state is created):
1321
+ * 1. `result.address` matches the local signer's address.
1322
+ * 2. `result.signature` recovers to `result.address` when verified against
1323
+ * `result.signedMessage`.
1324
+ * 3. Every immutable SIWE header field (domain, address, URI, version,
1325
+ * chainId, nonce, issuedAt) in the original prepared SIWE is preserved
1326
+ * byte-for-byte in `result.signedMessage`. `statement` is NOT in this
1327
+ * list because the WASM renders it from the ReCap contents; it MUST
1328
+ * change when the ReCap is narrowed.
1329
+ * 4. Every capability in `result.signedMessage`'s ReCap block was present
1330
+ * in the original prepared SIWE — narrowing OK, broadening rejected.
1331
+ * 5. Every `result.selectedActionKeys` entry corresponds to a
1332
+ * (resource, action) pair in the returned SIWE.
1333
+ */
1334
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
1335
+ if (result.protocolVersion !== 1) {
1336
+ throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
1337
+ }
1338
+ if (!result.signedMessage) {
1339
+ throw new Error("OpenKey result must include signedMessage");
1340
+ }
1341
+ if (!prepared.siwe) {
1342
+ throw new Error(
1343
+ "signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
1344
+ );
1345
+ }
1346
+ const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
1347
+ if (canonicalizeAddress(result.address) !== expectedAddress) {
1348
+ throw new Error(
1349
+ `OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
1350
+ );
1351
+ }
1352
+ let parsedSignedSiwe;
1353
+ try {
1354
+ parsedSignedSiwe = new SiweMessage(result.signedMessage);
1355
+ } catch (err) {
1356
+ throw new Error(
1357
+ `OpenKey signedMessage is not a parseable SIWE: ${err.message}`
1358
+ );
1359
+ }
1360
+ const verifyResult = await parsedSignedSiwe.verify(
1361
+ { signature: result.signature },
1362
+ { suppressExceptions: true }
1363
+ );
1364
+ if (!verifyResult.success) {
1365
+ const errData = verifyResult.error;
1366
+ const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
1367
+ const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
1368
+ throw new Error(
1369
+ `OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
1370
+ );
1371
+ }
1372
+ if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
1373
+ throw new Error(
1374
+ `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
1375
+ );
1376
+ }
1377
+ const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
1378
+ const originalFields = extractImmutableSiweFields(prepared.siwe);
1379
+ const signedFields = extractImmutableSiweFields(result.signedMessage);
1380
+ const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
1381
+ originalHasRecap
1382
+ });
1383
+ if (changedFields.length > 0) {
1384
+ throw new Error(
1385
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
1386
+ );
1387
+ }
1388
+ let originalCaps;
1389
+ let signedCaps;
1390
+ try {
1391
+ originalCaps = extractRecapAttenuations(prepared.siwe);
1392
+ } catch (err) {
1393
+ throw new Error(
1394
+ `Failed to decode prepared SIWE recap: ${err.message}`
1395
+ );
1396
+ }
1397
+ try {
1398
+ signedCaps = extractRecapAttenuations(result.signedMessage);
1399
+ } catch (err) {
1400
+ throw new Error(
1401
+ `Failed to decode OpenKey signedMessage recap: ${err.message}`
1402
+ );
1403
+ }
1404
+ const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
1405
+ if (unauthorized.length > 0) {
1406
+ const previewLimit = 5;
1407
+ const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
1408
+ const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
1409
+ throw new Error(
1410
+ `OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
1411
+ );
1412
+ }
1413
+ if (!result.signedMessage.includes(prepared.spaceId)) {
1414
+ throw new Error(
1415
+ `OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
1416
+ );
1417
+ }
1418
+ const grantedPairs = /* @__PURE__ */ new Set();
1419
+ const grantedFourPartIndex = /* @__PURE__ */ new Map();
1420
+ for (const [resource, actions] of Object.entries(signedCaps)) {
1421
+ const { space, path } = parseCanonicalRecapResource(resource);
1422
+ for (const action of Object.keys(actions)) {
1423
+ grantedPairs.add(`${resource}\0${action}`);
1424
+ const slashIdx = action.indexOf("/");
1425
+ const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
1426
+ if (service) {
1427
+ grantedFourPartIndex.set(
1428
+ `${service}\0${space}\0${path}\0${action}`,
1429
+ `${resource}\0${action}`
1430
+ );
1431
+ }
1432
+ }
1433
+ }
1434
+ const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
1435
+ const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
1436
+ for (const pair of grantedPairs) {
1437
+ const idx = pair.indexOf("\0");
1438
+ const action = idx >= 0 ? pair.slice(idx + 1) : pair;
1439
+ if (!isStructurallyRequired(action)) {
1440
+ nonRequiredGrantedPairs.add(pair);
1441
+ }
1442
+ }
1443
+ const allSignedGrantedPairs = new Set(grantedPairs);
1444
+ const seenRawKeys = /* @__PURE__ */ new Set();
1445
+ for (const rawKey of result.selectedActionKeys) {
1446
+ if (seenRawKeys.has(rawKey)) {
1447
+ throw new Error(
1448
+ `OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
1449
+ );
1450
+ }
1451
+ seenRawKeys.add(rawKey);
1452
+ }
1453
+ const selectedResourceActionPairs = /* @__PURE__ */ new Set();
1454
+ for (const rawKey of result.selectedActionKeys) {
1455
+ const parts = rawKey.split("\0");
1456
+ if (parts.length !== 4) {
1457
+ throw new Error(
1458
+ `OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
1459
+ );
1460
+ }
1461
+ const canonical = grantedFourPartIndex.get(rawKey);
1462
+ if (!canonical) {
1463
+ throw new Error(
1464
+ `OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
1465
+ );
1466
+ }
1467
+ selectedResourceActionPairs.add(canonical);
1468
+ }
1469
+ if (nonRequiredGrantedPairs.size > 0) {
1470
+ const missingPairs = [];
1471
+ for (const pair of nonRequiredGrantedPairs) {
1472
+ if (!selectedResourceActionPairs.has(pair)) {
1473
+ missingPairs.push(pair);
1474
+ }
1475
+ }
1476
+ if (missingPairs.length > 0) {
1477
+ const previewLimit = 5;
1478
+ const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1479
+ const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
1480
+ throw new Error(
1481
+ `OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
1482
+ );
1483
+ }
1484
+ }
1485
+ if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
1486
+ throw new Error(
1487
+ "OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
1488
+ );
1489
+ }
1490
+ const permissionCanonicalKeys = /* @__PURE__ */ new Set();
1491
+ const permissionPairsCovered = /* @__PURE__ */ new Set();
1492
+ for (const perm of result.permissions) {
1493
+ const { service, space, path, actions } = perm;
1494
+ if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
1495
+ throw new Error(
1496
+ `OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
1497
+ );
1498
+ }
1499
+ if (!Array.isArray(actions) || actions.length === 0) {
1500
+ throw new Error(
1501
+ `OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
1502
+ );
1503
+ }
1504
+ const canonicalKey = `${service}\0${space}\0${path}`;
1505
+ if (permissionCanonicalKeys.has(canonicalKey)) {
1506
+ throw new Error(
1507
+ `OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
1508
+ );
1509
+ }
1510
+ permissionCanonicalKeys.add(canonicalKey);
1511
+ let expectedResource;
1512
+ if (space.startsWith("tinycloud:")) {
1513
+ const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
1514
+ expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
1515
+ } else {
1516
+ if (path) {
1517
+ throw new Error(
1518
+ `OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
1519
+ );
1520
+ }
1521
+ expectedResource = space;
1522
+ }
1523
+ let matchedResource;
1524
+ if (signedCaps[expectedResource] !== void 0) {
1525
+ matchedResource = expectedResource;
1526
+ } else {
1527
+ throw new Error(
1528
+ `OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
1529
+ );
1530
+ }
1531
+ const grantedActionsForResource = new Set(
1532
+ Object.keys(signedCaps[matchedResource])
1533
+ );
1534
+ for (const action of actions) {
1535
+ if (typeof action !== "string" || !action) {
1536
+ throw new Error(
1537
+ `OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
1538
+ );
1539
+ }
1540
+ if (!grantedActionsForResource.has(action)) {
1541
+ throw new Error(
1542
+ `OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
1543
+ );
1544
+ }
1545
+ permissionPairsCovered.add(`${matchedResource}\0${action}`);
1546
+ }
1547
+ }
1548
+ if (allSignedGrantedPairs.size > 0) {
1549
+ const missingFromPerms = [];
1550
+ for (const pair of allSignedGrantedPairs) {
1551
+ if (!permissionPairsCovered.has(pair)) {
1552
+ missingFromPerms.push(pair);
1553
+ }
1554
+ }
1555
+ if (missingFromPerms.length > 0) {
1556
+ const previewLimit = 5;
1557
+ const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1558
+ const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
1559
+ throw new Error(
1560
+ `OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
1561
+ );
1562
+ }
1563
+ const extrasInPerms = [];
1564
+ for (const pair of permissionPairsCovered) {
1565
+ if (!allSignedGrantedPairs.has(pair)) {
1566
+ extrasInPerms.push(pair);
1567
+ }
1568
+ }
1569
+ if (extrasInPerms.length > 0) {
1570
+ const previewLimit = 5;
1571
+ const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
1572
+ const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
1573
+ throw new Error(
1574
+ `OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
1575
+ );
1576
+ }
1577
+ }
1578
+ void nonRequiredGrantedPairs;
1579
+ return this.signInWithPreparedSession(
1580
+ {
1581
+ siwe: result.signedMessage,
1582
+ jwk: prepared.jwk,
1583
+ spaceId: prepared.spaceId,
1584
+ verificationMethod: prepared.verificationMethod
1585
+ },
1586
+ result.signature,
1587
+ keyId,
1588
+ jwk
1589
+ );
1590
+ }
1141
1591
  /**
1142
1592
  * Clear persisted session data.
1143
1593
  */
@@ -1229,12 +1679,14 @@ import {
1229
1679
  SharingService,
1230
1680
  UnsupportedFeatureError,
1231
1681
  makePublicSpaceId,
1232
- ACCOUNT_REGISTRY_SPACE,
1682
+ ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
1683
+ BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST,
1233
1684
  BOOTSTRAP_SESSION_REQUESTS,
1234
1685
  SECRET_RECORDS_SCHEMA,
1235
1686
  SECRETS_SPACE as SECRETS_SPACE2,
1236
1687
  TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
1237
1688
  bootstrapSteps,
1689
+ ErrorCodes as ErrorCodes2,
1238
1690
  ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
1239
1691
  CaveatedDelegationUnsupportedError,
1240
1692
  PermissionNotInManifestError,
@@ -1261,7 +1713,9 @@ import {
1261
1713
  parseNetworkId as parseNetworkId2,
1262
1714
  resolveSecretPath as resolveSecretPath2,
1263
1715
  validateOwnerSharePolicyRegistrationBytes,
1264
- validateShareDeliveryAuthorizationBytes
1716
+ validateShareDeliveryAuthorizationBytes,
1717
+ verifyEip191MessageSignature,
1718
+ signCompactUcanRootAuthorization
1265
1719
  } from "@tinycloud/sdk-core";
1266
1720
  import {
1267
1721
  parsePermissionHint
@@ -1452,7 +1906,7 @@ function createWasmKeyProvider(sessionManager) {
1452
1906
  // src/delegateToHelpers.ts
1453
1907
  import {
1454
1908
  parseExpiry,
1455
- SiweMessage,
1909
+ SiweMessage as SiweMessage2,
1456
1910
  EXPIRY as EXPIRY2
1457
1911
  } from "@tinycloud/sdk-core";
1458
1912
  function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
@@ -1501,7 +1955,7 @@ function resolveExpiryMs(expiry) {
1501
1955
  return parseExpiry(expiry);
1502
1956
  }
1503
1957
  function extractSiweExpiration(siwe) {
1504
- const parsed = new SiweMessage(siwe);
1958
+ const parsed = new SiweMessage2(siwe);
1505
1959
  if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
1506
1960
  return void 0;
1507
1961
  }
@@ -1703,6 +2157,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
1703
2157
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
1704
2158
  var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
1705
2159
  var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
2160
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
2161
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
2162
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
1706
2163
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
1707
2164
  function isSecretPayload(value) {
1708
2165
  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";
@@ -1726,6 +2183,18 @@ var ROOT_DELEGATION_ACTIONS = [
1726
2183
  DUCKDB2.ALL
1727
2184
  ];
1728
2185
  var DEFAULT_SESSION_EXPIRATION_MS = EXPIRY3.SESSION_MS;
2186
+ function unifiedRootAttenuation(capabilities) {
2187
+ const attenuation = {};
2188
+ for (const capability of capabilities) {
2189
+ if (attenuation[capability.resource] !== void 0) throw new Error("unified owner root contains a duplicate resource");
2190
+ attenuation[capability.resource] = capability.kind === "encryption" ? { [capability.action]: [{}] } : Object.fromEntries(capability.actions.map((action) => [action, [{
2191
+ type: "xyz.tinycloud.resource/selector",
2192
+ kind: capability.selector,
2193
+ value: capability.resource
2194
+ }]]));
2195
+ }
2196
+ return attenuation;
2197
+ }
1729
2198
  function decodeAuthorizationBytes(authorization) {
1730
2199
  const encoded = authorization.replace(/^Bearer /i, "");
1731
2200
  const match = /^([A-Za-z0-9_-]+)(={1,2})?$/.exec(encoded);
@@ -1745,6 +2214,9 @@ function decodeAuthorizationBytes(authorization) {
1745
2214
  function isOpenKeyAutoSignStrategy(strategy) {
1746
2215
  return strategy?.openKeyAutoSign === true;
1747
2216
  }
2217
+ function openKeyApproval(strategy) {
2218
+ return strategy?.openKeyRequestApproval;
2219
+ }
1748
2220
  function isInteractiveSigner(config) {
1749
2221
  if (config.privateKey) {
1750
2222
  return false;
@@ -2102,6 +2574,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2102
2574
  * hosting is confirmed against the session that was active at the time.
2103
2575
  */
2104
2576
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
2577
+ /** Serializes account-registry writes for the active node instance. */
2578
+ this.accountRegistryTail = Promise.resolve();
2105
2579
  /**
2106
2580
  * True when the last signIn() detected an interactive signer and skipped
2107
2581
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -2110,9 +2584,12 @@ var _TinyCloudNode = class _TinyCloudNode {
2110
2584
  this._bootstrapSkipped = false;
2111
2585
  /**
2112
2586
  * Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
2113
- * true when bootstrap did not complete (interactive signer, auto-sign
2114
- * denied, or a bootstrap step failed); `reason` carries the cause so apps
2115
- * can surface a "finish account setup" call-to-action. `warnings` is
2587
+ * true when bootstrap did not complete in this invocation (interactive signer,
2588
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
2589
+ * step failed). `already-provisioned` means the bootstrap was not run here;
2590
+ * it does not mean a prior provisioning attempt failed to complete.
2591
+ * `reason` carries the cause so apps can surface a "finish account setup"
2592
+ * call-to-action. `warnings` is
2116
2593
  * present when bootstrap completed, but one or more steps recovered from
2117
2594
  * an ambiguous failure (e.g. a KV batch write reconciled via per-space
2118
2595
  * fallback) — clean vs. recovered runs are otherwise byte-identical.
@@ -2295,7 +2772,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2295
2772
  nonce: config.nonce,
2296
2773
  siweConfig: config.siweConfig,
2297
2774
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
2298
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2775
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2299
2776
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
2300
2777
  });
2301
2778
  this.tc = new TinyCloud(this.auth, {
@@ -2385,6 +2862,15 @@ var _TinyCloudNode = class _TinyCloudNode {
2385
2862
  get sessionDid() {
2386
2863
  return this.sessionManager.getDID(this.sessionKeyId);
2387
2864
  }
2865
+ /** Bare did:key principal used as the credential subject and holder binding. */
2866
+ get credentialHolderDid() {
2867
+ return this.sessionDid.split("#", 1)[0];
2868
+ }
2869
+ /** Canonical verification method for the active credential holder key. */
2870
+ get credentialHolderKid() {
2871
+ const holder = this.credentialHolderDid;
2872
+ return `${holder}#${holder.slice("did:key:".length)}`;
2873
+ }
2388
2874
  /**
2389
2875
  * Return the current session's signed ReCap capabilities after the session
2390
2876
  * has been authenticated or restored. This is intentionally distinct from
@@ -2435,7 +2921,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2435
2921
  if (!this._address) {
2436
2922
  return void 0;
2437
2923
  }
2438
- return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE);
2924
+ return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
2439
2925
  }
2440
2926
  /**
2441
2927
  * Account-level application and delegation helpers.
@@ -2486,6 +2972,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2486
2972
  "Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
2487
2973
  );
2488
2974
  }
2975
+ await this.accountRegistryTail;
2489
2976
  await this.wasmBindings.ensureInitialized?.();
2490
2977
  this._address = canonicalizeAddress2(await this.signer.getAddress());
2491
2978
  this._chainId = await this.signer.getChainId();
@@ -2538,6 +3025,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2538
3025
  this._bootstrapSkipped = false;
2539
3026
  this._bootstrapStatus = { skipped: false };
2540
3027
  if (this.config.autoBootstrapAccount === false) {
3028
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
2541
3029
  return false;
2542
3030
  }
2543
3031
  if (!this.auth || !this._address) {
@@ -2552,11 +3040,14 @@ var _TinyCloudNode = class _TinyCloudNode {
2552
3040
  return false;
2553
3041
  }
2554
3042
  const steps = bootstrapSteps(this._address, this._chainId);
2555
- if (!await this.isFreshBootstrapAccount(steps)) {
2556
- return false;
2557
- }
2558
3043
  try {
2559
- const warnings = await this.runAccountBootstrap(steps);
3044
+ const decision = await this.resolveBootstrapDecision(steps);
3045
+ if (decision.action === "skip") {
3046
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
3047
+ return false;
3048
+ }
3049
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
3050
+ await this.writeBootstrapCompletionMarker(steps);
2560
3051
  if (warnings.length > 0) {
2561
3052
  this._bootstrapStatus = { skipped: false, warnings };
2562
3053
  this.notificationHandler.warning(
@@ -2575,32 +3066,92 @@ var _TinyCloudNode = class _TinyCloudNode {
2575
3066
  }
2576
3067
  return true;
2577
3068
  }
2578
- async isFreshBootstrapAccount(steps) {
3069
+ async resolveBootstrapDecision(steps) {
2579
3070
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
2580
3071
  for (const step of steps) {
2581
3072
  if (step.kind === "session") {
2582
3073
  enshrinedSpaceIds.add(step.spaceId);
2583
3074
  }
2584
3075
  }
2585
- const skipped = this.auth.lastActivationSkippedSpaceIds;
3076
+ const auth = this.auth;
3077
+ if (!auth) {
3078
+ throw new Error("Account bootstrap requires an active wallet session");
3079
+ }
3080
+ const skipped = auth.lastActivationSkippedSpaceIds;
2586
3081
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
2587
- return true;
3082
+ return { action: "run", mode: "fresh" };
2588
3083
  }
2589
3084
  try {
2590
- const indexed = await this.account.index.spaces.list();
2591
- if (indexed.ok && indexed.data.length === 0) {
2592
- return true;
3085
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
3086
+ const markerPermission = {
3087
+ service: "tinycloud.kv",
3088
+ space: accountSpaceId,
3089
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
3090
+ actions: ["get"]
3091
+ };
3092
+ if (!this.hasRuntimePermissions([markerPermission])) {
3093
+ return { action: "run", mode: "repair" };
2593
3094
  }
2594
- } catch {
3095
+ const marker = await this.readBootstrapCompletionMarker();
3096
+ if (marker.ok) {
3097
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
3098
+ return { action: "skip" };
3099
+ }
3100
+ return { action: "run", mode: "repair" };
3101
+ }
3102
+ if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
3103
+ return { action: "run", mode: "fresh" };
3104
+ }
3105
+ } catch (err) {
3106
+ const reason = err instanceof Error ? err.message : String(err);
3107
+ this.notificationHandler.warning(
3108
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
3109
+ );
3110
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
3111
+ return { action: "run", mode: "repair" };
2595
3112
  }
2596
- try {
2597
- const spaces = await this.account.spaces.list();
2598
- return spaces.ok && spaces.data.length === 0;
2599
- } catch {
2600
- return false;
3113
+ return { action: "run", mode: "repair" };
3114
+ }
3115
+ async readBootstrapCompletionMarker() {
3116
+ return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
3117
+ BOOTSTRAP_COMPLETION_MARKER_KEY
3118
+ );
3119
+ }
3120
+ async writeBootstrapCompletionMarker(steps) {
3121
+ if (!this._address) {
3122
+ throw new Error("Bootstrap completion marker requires an active wallet session");
2601
3123
  }
3124
+ const canonicalStepIds = new Set(
3125
+ bootstrapSteps(this._address, this._chainId).map((step) => step.id)
3126
+ );
3127
+ const providedStepIds = new Set(steps.map((step) => step.id));
3128
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
3129
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
3130
+ }
3131
+ const marker = {
3132
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
3133
+ stepIds: [...canonicalStepIds],
3134
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3135
+ };
3136
+ const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
3137
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
3138
+ marker
3139
+ );
3140
+ if (!written.ok) {
3141
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
3142
+ }
3143
+ }
3144
+ isAcceptedBootstrapCompletionMarker(value) {
3145
+ if (typeof value !== "object" || value === null) return false;
3146
+ const entries = Object.entries(value);
3147
+ const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
3148
+ const version = markerValue("v");
3149
+ return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
3150
+ }
3151
+ markerReadIsUnhostedSpace(meta) {
3152
+ return meta?.status === 404;
2602
3153
  }
2603
- async runAccountBootstrap(steps) {
3154
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
2604
3155
  if (!this.auth || !this._address) {
2605
3156
  throw new Error("Account bootstrap requires an active wallet session");
2606
3157
  }
@@ -2701,9 +3252,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2701
3252
  if (step.kind === "seed-applications") {
2702
3253
  const registered = await this.account.applications.register(
2703
3254
  step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
2704
- // Bootstrap only runs on an account with no registry records, so the
2705
- // manifest-hash pre-read is definitionally a miss. Skip it; the write
2706
- // it guards is an INSERT OR REPLACE and is safe to repeat.
3255
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
3256
+ // repair, so skip the manifest-hash pre-read.
2707
3257
  { assumeUnregistered: true }
2708
3258
  );
2709
3259
  if (!registered.ok) {
@@ -2711,7 +3261,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2711
3261
  }
2712
3262
  }
2713
3263
  if (step.kind === "encryption-network-create") {
2714
- await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
3264
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
2715
3265
  }
2716
3266
  if (step.kind === "secret-records-schema") {
2717
3267
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -2894,7 +3444,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2894
3444
  if (!this.auth || !this.signer) {
2895
3445
  throw new Error("Manifest registry write requires wallet mode");
2896
3446
  }
2897
- const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE);
3447
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
2898
3448
  await this.ensureOwnedSpaceHostedById(accountSpaceId);
2899
3449
  const result = await this.account.applications.register(request.manifests);
2900
3450
  if (!result.ok) {
@@ -2904,16 +3454,48 @@ var _TinyCloudNode = class _TinyCloudNode {
2904
3454
  }
2905
3455
  }
2906
3456
  scheduleAccountRegistrySync() {
2907
- void this.withAccountRegistryRetry(async () => {
2908
- void this.account.index.ensure();
2909
- await this.writeManifestRegistryRecords();
2910
- if (this.currentSessionCanListSpaces()) {
2911
- const spaces = await this.account.spaces.syncAccessible();
2912
- if (!spaces.ok) {
2913
- throw new Error(`Failed to sync account spaces: ${spaces.error.message}`);
3457
+ const session = this.currentTinyCloudSession();
3458
+ if (!session || this.pendingAccountRegistrySync?.session === session) {
3459
+ return;
3460
+ }
3461
+ const promise = this.enqueueAccountRegistryOperation(session, async () => {
3462
+ await this.withAccountRegistryRetry(async () => {
3463
+ if (this.currentTinyCloudSession() !== session) {
3464
+ return;
3465
+ }
3466
+ await this.account.index.ensure();
3467
+ if (this.currentTinyCloudSession() !== session) {
3468
+ return;
3469
+ }
3470
+ await this.writeManifestRegistryRecords();
3471
+ if (this.currentTinyCloudSession() !== session) {
3472
+ return;
2914
3473
  }
3474
+ if (this.currentSessionCanListSpaces()) {
3475
+ const spaces = await this.account.spaces.syncAccessible();
3476
+ if (!spaces.ok) {
3477
+ throw new Error(`Failed to sync account spaces: ${spaces.error.message}`);
3478
+ }
3479
+ }
3480
+ });
3481
+ });
3482
+ this.pendingAccountRegistrySync = { session, promise };
3483
+ const clearPendingSync = () => {
3484
+ if (this.pendingAccountRegistrySync?.promise === promise) {
3485
+ this.pendingAccountRegistrySync = void 0;
2915
3486
  }
3487
+ };
3488
+ void promise.then(clearPendingSync, clearPendingSync);
3489
+ }
3490
+ enqueueAccountRegistryOperation(session, task) {
3491
+ const operation = this.accountRegistryTail.then(async () => {
3492
+ if (this.currentTinyCloudSession() === session) {
3493
+ await task();
3494
+ }
3495
+ });
3496
+ this.accountRegistryTail = operation.catch(() => {
2916
3497
  });
3498
+ return operation;
2917
3499
  }
2918
3500
  /**
2919
3501
  * Whether the current primary session may invoke `tinycloud.space/list`.
@@ -3066,7 +3648,8 @@ var _TinyCloudNode = class _TinyCloudNode {
3066
3648
  * @returns The hosted space URI.
3067
3649
  */
3068
3650
  async hostOwnedSpace(name) {
3069
- if (!this.auth || !this.auth.tinyCloudSession) {
3651
+ const session = this.auth?.tinyCloudSession;
3652
+ if (!this.auth || !session) {
3070
3653
  throw new Error("Not signed in. Call signIn() first.");
3071
3654
  }
3072
3655
  const spaceId = this.ownedSpaceId(name);
@@ -3082,22 +3665,26 @@ var _TinyCloudNode = class _TinyCloudNode {
3082
3665
  }
3083
3666
  const activation = await activateSessionWithHost2(
3084
3667
  host,
3085
- this.auth.tinyCloudSession.delegationHeader
3668
+ session.delegationHeader
3086
3669
  );
3087
3670
  if (!activation.success || activation.skipped?.includes(spaceId)) {
3088
3671
  throw new Error(
3089
3672
  `Failed to activate session for owned space ${spaceId}: ${activation.error ?? "space was skipped"}`
3090
3673
  );
3091
3674
  }
3092
- void this.account.spaces.register({
3093
- spaceId,
3094
- name,
3095
- ownerDid: this.did,
3096
- type: "owned",
3097
- permissions: ["*"],
3098
- status: "active"
3099
- }).catch(() => {
3100
- });
3675
+ try {
3676
+ await this.enqueueAccountRegistryOperation(session, async () => {
3677
+ await this.account.spaces.register({
3678
+ spaceId,
3679
+ name,
3680
+ ownerDid: this.did,
3681
+ type: "owned",
3682
+ permissions: ["*"],
3683
+ status: "active"
3684
+ });
3685
+ });
3686
+ } catch {
3687
+ }
3101
3688
  return spaceId;
3102
3689
  }
3103
3690
  /**
@@ -3132,23 +3719,20 @@ var _TinyCloudNode = class _TinyCloudNode {
3132
3719
  if (!this.auth || !this.auth.tinyCloudSession) {
3133
3720
  throw new Error("Not signed in. Call signIn() first.");
3134
3721
  }
3722
+ await this.accountRegistryTail;
3135
3723
  const spaceId = this.ownedSpaceId(name);
3136
3724
  if (await this.isOwnedSpaceRegistered(spaceId)) {
3137
3725
  return spaceId;
3138
3726
  }
3139
- const hosted = await this.hostOwnedSpace(name);
3140
- try {
3141
- await this.account.spaces.register({
3142
- spaceId,
3143
- name,
3144
- ownerDid: this.did,
3145
- type: "owned",
3146
- permissions: ["*"],
3147
- status: "active"
3148
- });
3149
- } catch {
3727
+ return this.hostOwnedSpace(name);
3728
+ }
3729
+ /** Resolve and authenticate the owner encoded by an owned-space URI. */
3730
+ credentialSpaceOwnerDid(spaceId) {
3731
+ const ownerDid = this.ownerDidFromSpaceId(spaceId);
3732
+ if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
3733
+ throw new Error("Credential space owner does not match the active TinyCloud owner");
3150
3734
  }
3151
- return hosted;
3735
+ return ownerDid;
3152
3736
  }
3153
3737
  /**
3154
3738
  * Check whether an owned space is already registered/hosted by consulting the
@@ -3693,6 +4277,106 @@ var _TinyCloudNode = class _TinyCloudNode {
3693
4277
  if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
3694
4278
  return signBytesWithJwk(bytes, session.jwk);
3695
4279
  }
4280
+ /** Author a proofless Policy/v3 owner root with the authenticated holder key. */
4281
+ async createUnifiedOwnerRoot(input) {
4282
+ const ownerDid = this.credentialHolderDid;
4283
+ if (input.ownerDid !== ownerDid) throw new Error("unified owner root signer does not match owner DID");
4284
+ const facts = {
4285
+ role: input.role,
4286
+ mode: input.role === "policy-authority" ? "policy-source" : "conditional-mint",
4287
+ ownerDid,
4288
+ policyId: input.policyId,
4289
+ policyDigestHex: input.policyDigestHex,
4290
+ policyCid: input.policyCid,
4291
+ contentSourceDigestHex: input.contentSourceDigestHex,
4292
+ capabilityCeilingHashHex: input.capabilityCeilingHashHex,
4293
+ nativeProjectionHashHex: input.nativeProjectionHashHex,
4294
+ nodeAudience: input.nodeAudience,
4295
+ ...input.role === "policy-enforcement" ? { enforcerDid: input.audienceDid } : {}
4296
+ };
4297
+ const root = await signCompactUcanRootAuthorization({
4298
+ issuerDid: ownerDid,
4299
+ audienceDid: input.audienceDid,
4300
+ attenuation: unifiedRootAttenuation(input.capabilities),
4301
+ facts: [facts],
4302
+ notBefore: Math.floor(input.notBefore.getTime() / 1e3),
4303
+ expiresAt: Math.floor(input.expiresAt.getTime() / 1e3),
4304
+ nonce: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
4305
+ sign: (bytes) => this.signSessionBytes(bytes)
4306
+ });
4307
+ const kv = input.capabilities.find((capability) => capability.kind === "kv");
4308
+ const resource = kv?.resource ?? "";
4309
+ const marker = resource.indexOf("/kv/");
4310
+ return {
4311
+ cid: root.cid,
4312
+ delegationHeader: { Authorization: root.authorization },
4313
+ delegateDID: input.audienceDid,
4314
+ spaceId: marker === -1 ? "" : resource.slice("tinycloud://".length, marker),
4315
+ path: marker === -1 ? resource : resource.slice(marker + 4),
4316
+ actions: kv?.actions ?? [],
4317
+ expiry: input.expiresAt
4318
+ };
4319
+ }
4320
+ /** Apply only an already-enabled signing policy to the exact credential request. */
4321
+ async autoSignCredentialBytes(bytes) {
4322
+ const strategy = this.config.signStrategy;
4323
+ if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
4324
+ if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
4325
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4326
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4327
+ const request = {
4328
+ address: this.address ?? "",
4329
+ chainId: this.session?.chainId ?? this._chainId,
4330
+ message: new TextDecoder().decode(bytes),
4331
+ type: "message",
4332
+ purpose: "message"
4333
+ };
4334
+ const decision = await strategy.handler(request);
4335
+ if (!decision.approved) {
4336
+ if (decision.needsApproval) return void 0;
4337
+ throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
4338
+ }
4339
+ if (typeof decision.signature !== "string" || !await verifyEip191MessageSignature(
4340
+ request.message,
4341
+ decision.signature,
4342
+ request.address
4343
+ )) {
4344
+ throw new Error("OpenKey credential signature evidence is invalid");
4345
+ }
4346
+ return this.signSessionBytes(bytes);
4347
+ }
4348
+ /** Invoke the configured interactive approval strategy before session signing. */
4349
+ async approveCredentialBytes(bytes) {
4350
+ const strategy = this.config.signStrategy;
4351
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4352
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4353
+ const request = {
4354
+ address: this.address ?? "",
4355
+ chainId: this.session?.chainId ?? this._chainId,
4356
+ message: new TextDecoder().decode(bytes),
4357
+ type: "message",
4358
+ purpose: "message"
4359
+ };
4360
+ const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
4361
+ if (approval) {
4362
+ const decision = await approval(request);
4363
+ if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
4364
+ if (decision.signature !== void 0 && !await verifyEip191MessageSignature(
4365
+ request.message,
4366
+ decision.signature,
4367
+ request.address
4368
+ )) {
4369
+ throw new Error("OpenKey credential signature evidence is invalid");
4370
+ }
4371
+ return this.signSessionBytes(bytes);
4372
+ }
4373
+ if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
4374
+ if (this.config.signer) {
4375
+ await this.config.signer.signMessage(request.message);
4376
+ return this.signSessionBytes(bytes);
4377
+ }
4378
+ throw new Error("Interactive OpenKey approval is not configured");
4379
+ }
3696
4380
  /** Bind a session-signed invocation to the canonical service audience. */
3697
4381
  async bindInvocationAudience(authorization, audience) {
3698
4382
  const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
@@ -3760,7 +4444,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3760
4444
  nonce: this.config.nonce,
3761
4445
  siweConfig: this.config.siweConfig,
3762
4446
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3763
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4447
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
3764
4448
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3765
4449
  });
3766
4450
  this.tc = new TinyCloud(this.auth, {
@@ -3813,7 +4497,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3813
4497
  nonce: this.config.nonce,
3814
4498
  siweConfig: this.config.siweConfig,
3815
4499
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3816
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4500
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
3817
4501
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3818
4502
  });
3819
4503
  this.tc = new TinyCloud(this.auth, {
@@ -6562,35 +7246,44 @@ var _TinyCloudNode = class _TinyCloudNode {
6562
7246
  if (parentDelegation.disableSubDelegation) {
6563
7247
  throw new Error("Parent delegation does not allow sub-delegation");
6564
7248
  }
6565
- if (!params.path.startsWith(parentDelegation.path)) {
7249
+ const parentResources = parentDelegation.resources ?? [{
7250
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7251
+ space: parentDelegation.spaceId,
7252
+ path: parentDelegation.path,
7253
+ actions: parentDelegation.actions
7254
+ }];
7255
+ const requestedResources = params.resources ?? [{
7256
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7257
+ space: parentDelegation.spaceId,
7258
+ path: params.path,
7259
+ actions: params.actions
7260
+ }];
7261
+ const resourceIsContained = (parent, child) => {
7262
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
7263
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
7264
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
7265
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
7266
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
7267
+ };
7268
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
6566
7269
  throw new Error(
6567
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
7270
+ "Sub-delegation resources exceed the received delegation"
6568
7271
  );
6569
7272
  }
6570
- const parentActions = new Set(parentDelegation.actions);
6571
- for (const action of params.actions) {
6572
- if (!parentActions.has(action)) {
6573
- throw new Error(
6574
- `Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
6575
- );
6576
- }
6577
- }
6578
7273
  const now = /* @__PURE__ */ new Date();
6579
7274
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
6580
7275
  const requestedExpiry = new Date(now.getTime() + expiryMs);
6581
7276
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
6582
7277
  const abilities = {};
6583
- const kvActions = params.actions.filter((a) => a.startsWith("tinycloud.kv/"));
6584
- const sqlActions = params.actions.filter((a) => a.startsWith("tinycloud.sql/"));
6585
- const duckdbActions = params.actions.filter((a) => a.startsWith("tinycloud.duckdb/"));
6586
- if (kvActions.length > 0) {
6587
- abilities.kv = { [params.path]: kvActions };
6588
- }
6589
- if (sqlActions.length > 0) {
6590
- abilities.sql = { [params.path]: sqlActions };
6591
- }
6592
- if (duckdbActions.length > 0) {
6593
- abilities.duckdb = { [params.path]: duckdbActions };
7278
+ const rawAbilities = {};
7279
+ for (const resource of requestedResources) {
7280
+ const service = resource.service.replace(/^tinycloud\./, "");
7281
+ if (service === "encryption") {
7282
+ rawAbilities[resource.path] = [...resource.actions];
7283
+ continue;
7284
+ }
7285
+ abilities[service] ?? (abilities[service] = {});
7286
+ abilities[service][resource.path] = [...resource.actions];
6594
7287
  }
6595
7288
  const targetHost = parentDelegation.host ?? this.config.host;
6596
7289
  const prepared = this.wasmBindings.prepareSession({
@@ -6602,7 +7295,8 @@ var _TinyCloudNode = class _TinyCloudNode {
6602
7295
  expirationTime: actualExpiry.toISOString(),
6603
7296
  spaceId: parentDelegation.spaceId,
6604
7297
  delegateUri: params.delegateDID,
6605
- parents: [parentDelegation.cid]
7298
+ parents: [parentDelegation.cid],
7299
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
6606
7300
  });
6607
7301
  const signature = await this.signer.signMessage(prepared.siwe);
6608
7302
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -6620,14 +7314,20 @@ var _TinyCloudNode = class _TinyCloudNode {
6620
7314
  cid: subDelegationSession.delegationCid,
6621
7315
  delegationHeader: subDelegationSession.delegationHeader,
6622
7316
  spaceId: parentDelegation.spaceId,
6623
- path: params.path,
6624
- actions: params.actions,
7317
+ path: requestedResources[0].path,
7318
+ actions: requestedResources[0].actions,
6625
7319
  disableSubDelegation: params.disableSubDelegation ?? false,
6626
7320
  expiry: actualExpiry,
6627
7321
  delegateDID: params.delegateDID,
6628
7322
  ownerAddress: parentDelegation.ownerAddress,
6629
7323
  chainId: parentDelegation.chainId,
6630
- host: targetHost
7324
+ host: targetHost,
7325
+ resources: requestedResources.map((resource) => ({
7326
+ service: resource.service.replace(/^tinycloud\./, ""),
7327
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
7328
+ path: resource.path,
7329
+ actions: [...resource.actions]
7330
+ }))
6631
7331
  };
6632
7332
  }
6633
7333
  };
@@ -6647,7 +7347,7 @@ var TinyCloudNode = _TinyCloudNode;
6647
7347
  // src/core.ts
6648
7348
  import {
6649
7349
  ACCOUNT_REGISTRY_PATH,
6650
- ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
7350
+ ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
6651
7351
  DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
6652
7352
  DEFAULT_MANIFEST_VERSION,
6653
7353
  VAULT_PERMISSION_SERVICE,
@@ -6670,8 +7370,341 @@ import {
6670
7370
  // src/delegation.ts
6671
7371
  import {
6672
7372
  canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
7373
+ decodeBase64Url,
6673
7374
  principalDidEquals as principalDidEquals3
6674
7375
  } from "@tinycloud/sdk-core";
7376
+ var SERVICE_BY_SHORT_NAME = {
7377
+ kv: "tinycloud.kv",
7378
+ sql: "tinycloud.sql",
7379
+ duckdb: "tinycloud.duckdb",
7380
+ hooks: "tinycloud.hooks"
7381
+ };
7382
+ function authorizationWithoutBearer(authorization) {
7383
+ return authorization.replace(/^Bearer /i, "");
7384
+ }
7385
+ function normalizedHost(host) {
7386
+ if (typeof host !== "string" || host.length === 0) {
7387
+ throw new Error("Validated runtime delegation requires a non-empty host.");
7388
+ }
7389
+ return host.replace(/\/+$/, "");
7390
+ }
7391
+ function didPrincipalsMatch(left, right) {
7392
+ try {
7393
+ return principalDidEquals3(left, right);
7394
+ } catch {
7395
+ return left === right;
7396
+ }
7397
+ }
7398
+ function compactUcanPayload(authorization) {
7399
+ const compact = authorizationWithoutBearer(authorization);
7400
+ const parts = compact.split(".");
7401
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
7402
+ throw new Error(
7403
+ "Validated runtime delegation requires a compact UCAN authorization so its signed authority can be derived."
7404
+ );
7405
+ }
7406
+ try {
7407
+ const payload = JSON.parse(
7408
+ new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64Url(parts[1]))
7409
+ );
7410
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
7411
+ throw new Error("payload is not an object");
7412
+ }
7413
+ return payload;
7414
+ } catch {
7415
+ throw new Error(
7416
+ "Validated runtime delegation authorization has an invalid compact UCAN payload."
7417
+ );
7418
+ }
7419
+ }
7420
+ function canonicalPermissionEntries(permissions) {
7421
+ return permissions.map((permission) => ({
7422
+ ...permission,
7423
+ actions: [...permission.actions].sort()
7424
+ })).sort(
7425
+ (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")) || canonicalizeRecapCaveats2(left.caveats).localeCompare(canonicalizeRecapCaveats2(right.caveats))
7426
+ );
7427
+ }
7428
+ function signedCaveats(value) {
7429
+ if (!Array.isArray(value) || value.some(
7430
+ (caveat) => caveat === null || typeof caveat !== "object" || Array.isArray(caveat)
7431
+ )) {
7432
+ throw new Error("Validated runtime delegation has malformed signed ReCap caveats.");
7433
+ }
7434
+ const cloned = JSON.parse(JSON.stringify(value));
7435
+ return canonicalizeRecapCaveats2(cloned) === canonicalizeRecapCaveats2(void 0) ? void 0 : cloned;
7436
+ }
7437
+ function groupedSignedEntries(service, space, path, abilities) {
7438
+ const groups = /* @__PURE__ */ new Map();
7439
+ for (const [action, rawCaveats] of Object.entries(abilities)) {
7440
+ const caveats = signedCaveats(rawCaveats);
7441
+ const identity = canonicalizeRecapCaveats2(caveats);
7442
+ const group = groups.get(identity) ?? { actions: [], caveats };
7443
+ group.actions.push(action);
7444
+ groups.set(identity, group);
7445
+ }
7446
+ return [...groups.values()].map((group) => ({
7447
+ service,
7448
+ ...space === void 0 ? {} : { space },
7449
+ path,
7450
+ actions: group.actions.sort(),
7451
+ ...group.caveats === void 0 ? {} : { caveats: group.caveats }
7452
+ }));
7453
+ }
7454
+ function signedAuthorityFromCompactUcan(authorization) {
7455
+ const payload = compactUcanPayload(authorization);
7456
+ if (typeof payload.aud !== "string" || payload.aud.length === 0) {
7457
+ throw new Error("Validated runtime delegation is missing a signed audience.");
7458
+ }
7459
+ if (typeof payload.iss !== "string" || payload.iss.length === 0) {
7460
+ throw new Error("Validated runtime delegation is missing a signed issuer.");
7461
+ }
7462
+ if (!Array.isArray(payload.prf) || payload.prf.some(
7463
+ (proof) => typeof proof !== "string" || proof.length === 0
7464
+ )) {
7465
+ throw new Error("Validated runtime delegation has malformed signed proofs.");
7466
+ }
7467
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
7468
+ throw new Error("Validated runtime delegation is missing a signed expiry.");
7469
+ }
7470
+ const expiry = new Date(payload.exp * 1e3);
7471
+ if (Number.isNaN(expiry.getTime())) {
7472
+ throw new Error("Validated runtime delegation has an invalid signed expiry.");
7473
+ }
7474
+ if (payload.att === null || typeof payload.att !== "object" || Array.isArray(payload.att)) {
7475
+ throw new Error("Validated runtime delegation is missing a signed attenuation.");
7476
+ }
7477
+ 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;
7478
+ const remainingRedelegationDepth = fact?.remainingRedelegationDepth;
7479
+ if (remainingRedelegationDepth !== void 0 && (!Number.isInteger(remainingRedelegationDepth) || remainingRedelegationDepth < 0 || remainingRedelegationDepth > 8)) {
7480
+ throw new Error("Validated runtime delegation has invalid signed redelegation depth.");
7481
+ }
7482
+ const permissions = [];
7483
+ for (const [resource, abilities] of Object.entries(payload.att)) {
7484
+ if (abilities === null || typeof abilities !== "object" || Array.isArray(abilities)) {
7485
+ throw new Error("Validated runtime delegation has an invalid signed attenuation.");
7486
+ }
7487
+ const abilityEntries = Object.entries(abilities);
7488
+ if (abilityEntries.length === 0) {
7489
+ throw new Error("Validated runtime delegation has an empty signed capability.");
7490
+ }
7491
+ if (resource.startsWith("urn:tinycloud:encryption:")) {
7492
+ permissions.push(...groupedSignedEntries(
7493
+ "tinycloud.encryption",
7494
+ void 0,
7495
+ resource,
7496
+ abilities
7497
+ ));
7498
+ continue;
7499
+ }
7500
+ const nativeMatch = /^(tinycloud:[^/]+)\/(kv|sql|duckdb|hooks)\/(.*)$/.exec(resource);
7501
+ const policyMatch = /^tinycloud:\/\/([^/]+)\/(kv|sql|duckdb|hooks)\/(.*)$/.exec(resource);
7502
+ const match = nativeMatch ?? policyMatch;
7503
+ if (!match) {
7504
+ throw new Error(
7505
+ `Validated runtime delegation has an unsupported signed resource '${resource}'.`
7506
+ );
7507
+ }
7508
+ const [, space, shortService, path] = match;
7509
+ const service = SERVICE_BY_SHORT_NAME[shortService];
7510
+ if (service === void 0) {
7511
+ throw new Error(
7512
+ `Validated runtime delegation has an unsupported signed service '${shortService}'.`
7513
+ );
7514
+ }
7515
+ permissions.push(...groupedSignedEntries(
7516
+ service,
7517
+ space,
7518
+ path,
7519
+ abilities
7520
+ ));
7521
+ }
7522
+ if (permissions.length === 0) {
7523
+ throw new Error("Validated runtime delegation has no signed capabilities.");
7524
+ }
7525
+ const canonicalPermissions = canonicalPermissionEntries(permissions);
7526
+ const canonicalResources = canonicalPermissions.map((permission) => ({
7527
+ service: permission.service === "tinycloud.encryption" ? "encryption" : permission.service.slice("tinycloud.".length),
7528
+ space: permission.service === "tinycloud.encryption" ? "encryption" : permission.space,
7529
+ path: permission.path,
7530
+ actions: [...permission.actions],
7531
+ ...permission.caveats === void 0 ? {} : { caveats: permission.caveats }
7532
+ }));
7533
+ return {
7534
+ issuer: payload.iss,
7535
+ audience: payload.aud,
7536
+ expiry,
7537
+ proofs: [...payload.prf],
7538
+ ...remainingRedelegationDepth === void 0 ? {} : { remainingRedelegationDepth },
7539
+ permissions: canonicalPermissions,
7540
+ resources: canonicalResources
7541
+ };
7542
+ }
7543
+ function canonicalResourcesFromPortableDelegation(delegation) {
7544
+ if (delegation.resources !== void 0 && delegation.resources.length > 0) {
7545
+ return delegation.resources.map((resource) => ({ ...resource, actions: [...resource.actions].sort() })).sort(
7546
+ (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")) || canonicalizeRecapCaveats2(left.caveats).localeCompare(canonicalizeRecapCaveats2(right.caveats))
7547
+ );
7548
+ }
7549
+ const byService = /* @__PURE__ */ new Map();
7550
+ for (const action of delegation.actions) {
7551
+ const [service] = action.split("/", 1);
7552
+ const shortService = service.replace(/^tinycloud\./, "");
7553
+ const values = byService.get(shortService) ?? [];
7554
+ values.push(action);
7555
+ byService.set(shortService, values);
7556
+ }
7557
+ return [...byService.entries()].map(([service, actions]) => ({
7558
+ service,
7559
+ space: service === "encryption" ? "encryption" : delegation.spaceId,
7560
+ path: delegation.path,
7561
+ actions: [...new Set(actions)].sort(),
7562
+ ...delegation.caveats === void 0 ? {} : { caveats: delegation.caveats }
7563
+ })).sort(
7564
+ (left, right) => left.service.localeCompare(right.service) || left.space.localeCompare(right.space) || left.path.localeCompare(right.path)
7565
+ );
7566
+ }
7567
+ function resourcesMatch(actual, expected) {
7568
+ const comparable = (resource) => ({
7569
+ ...resource,
7570
+ caveats: canonicalizeRecapCaveats2(resource.caveats)
7571
+ });
7572
+ return JSON.stringify(actual.map(comparable)) === JSON.stringify(expected.map(comparable));
7573
+ }
7574
+ async function activateValidatedRuntimeDelegation(node, delegation, options) {
7575
+ const host = normalizedHost(options.host);
7576
+ if (delegation.host !== void 0 && normalizedHost(delegation.host) !== host) {
7577
+ throw new Error(
7578
+ `Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`
7579
+ );
7580
+ }
7581
+ if (!(delegation.expiry instanceof Date) || Number.isNaN(delegation.expiry.getTime())) {
7582
+ throw new Error("Runtime delegation has an invalid expiry.");
7583
+ }
7584
+ if (delegation.expiry.getTime() <= Date.now()) {
7585
+ throw new Error("Runtime delegation is expired.");
7586
+ }
7587
+ const authorization = delegation.delegationHeader?.Authorization;
7588
+ if (typeof authorization !== "string" || authorization.length === 0) {
7589
+ throw new Error("Runtime delegation is missing authorization bytes.");
7590
+ }
7591
+ const cid = node.computeDelegationCid(authorization);
7592
+ if (cid !== delegation.cid) {
7593
+ throw new Error("Runtime delegation CID does not match authorization bytes.");
7594
+ }
7595
+ const signed = signedAuthorityFromCompactUcan(authorization);
7596
+ if (signed.expiry.getTime() <= Date.now()) {
7597
+ throw new Error("Runtime delegation is expired.");
7598
+ }
7599
+ if (delegation.expiry.getTime() !== signed.expiry.getTime()) {
7600
+ throw new Error("Runtime delegation expiry does not match signed authority.");
7601
+ }
7602
+ if (!didPrincipalsMatch(signed.audience, node.sessionDid)) {
7603
+ throw new Error(
7604
+ `Runtime delegation targets ${signed.audience} but this session is ${node.sessionDid}.`
7605
+ );
7606
+ }
7607
+ if (!didPrincipalsMatch(delegation.delegateDID, signed.audience)) {
7608
+ throw new Error("Runtime delegation audience does not match signed authority.");
7609
+ }
7610
+ const declaredResources = canonicalResourcesFromPortableDelegation(delegation);
7611
+ if (!resourcesMatch(declaredResources, signed.resources)) {
7612
+ throw new Error("Runtime delegation resources do not match signed authority.");
7613
+ }
7614
+ const previouslyInstalled = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
7615
+ if (previouslyInstalled !== void 0 && previouslyInstalled.delegationHeader.Authorization !== authorization) {
7616
+ throw new Error(
7617
+ "A different authorization is already installed for this runtime delegation CID."
7618
+ );
7619
+ }
7620
+ const primary = signed.resources[0];
7621
+ const signedSpace = signed.permissions.find(
7622
+ (permission) => permission.service !== "tinycloud.encryption"
7623
+ )?.space;
7624
+ const installedCandidate = {
7625
+ cid,
7626
+ delegationHeader: { Authorization: authorization },
7627
+ ownerAddress: delegation.ownerAddress,
7628
+ chainId: delegation.chainId,
7629
+ delegatorDID: signed.issuer,
7630
+ ...signed.proofs.length === 1 ? { parentCid: signed.proofs[0] } : {},
7631
+ ...signed.remainingRedelegationDepth === void 0 ? {} : {
7632
+ allowSubDelegation: signed.remainingRedelegationDepth > 0,
7633
+ disableSubDelegation: signed.remainingRedelegationDepth === 0
7634
+ },
7635
+ authHeader: authorization,
7636
+ spaceId: signedSpace ?? delegation.spaceId,
7637
+ path: primary.path,
7638
+ actions: [...primary.actions],
7639
+ ...primary.caveats === void 0 ? {} : { caveats: primary.caveats },
7640
+ resources: signed.resources.map((resource) => ({
7641
+ ...resource,
7642
+ actions: [...resource.actions]
7643
+ })),
7644
+ expiry: new Date(signed.expiry),
7645
+ delegateDID: signed.audience,
7646
+ host
7647
+ };
7648
+ if (node.isSessionOnly === true && typeof node.did === "string" && principalDidEquals3(node.did, node.sessionDid) && typeof node.useDelegation === "function") {
7649
+ await node.useDelegation(installedCandidate);
7650
+ } else {
7651
+ await node.useRuntimeDelegation(installedCandidate);
7652
+ }
7653
+ const installed = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
7654
+ if (!installed) {
7655
+ throw new Error("Runtime delegation activation did not install the validated authority.");
7656
+ }
7657
+ if (installed.delegationHeader.Authorization !== authorization || !didPrincipalsMatch(installed.delegateDID, signed.audience) || installed.expiry.getTime() !== signed.expiry.getTime() || !resourcesMatch(
7658
+ canonicalResourcesFromPortableDelegation(installed),
7659
+ signed.resources
7660
+ ) || normalizedHost(installed.host ?? "") !== host) {
7661
+ throw new Error(
7662
+ "Runtime delegation activation installed authority that differs from the validated authority."
7663
+ );
7664
+ }
7665
+ return {
7666
+ cid,
7667
+ delegation: installed,
7668
+ effectivePermissions: signed.permissions,
7669
+ expiry: new Date(signed.expiry),
7670
+ audience: signed.audience,
7671
+ host: normalizedHost(installed.host ?? host)
7672
+ };
7673
+ }
7674
+ async function activateCompactRuntimeDelegation(node, input) {
7675
+ const authorization = input.authorization;
7676
+ const cid = node.computeDelegationCid(authorization);
7677
+ if (cid !== input.cid) {
7678
+ throw new Error("Compact runtime delegation CID does not match authorization bytes.");
7679
+ }
7680
+ const signed = signedAuthorityFromCompactUcan(authorization);
7681
+ const primary = signed.resources[0];
7682
+ const signedSpace = signed.permissions.find(
7683
+ (permission) => permission.service !== "tinycloud.encryption"
7684
+ )?.space;
7685
+ const delegation = {
7686
+ cid,
7687
+ delegationHeader: { Authorization: authorization },
7688
+ ownerAddress: input.ownerAddress,
7689
+ chainId: input.chainId,
7690
+ delegatorDID: signed.issuer,
7691
+ authHeader: authorization,
7692
+ spaceId: signedSpace ?? primary.space,
7693
+ path: primary.path,
7694
+ actions: [...primary.actions],
7695
+ ...primary.caveats === void 0 ? {} : { caveats: primary.caveats },
7696
+ resources: signed.resources.map((resource) => ({
7697
+ ...resource,
7698
+ actions: [...resource.actions]
7699
+ })),
7700
+ expiry: new Date(signed.expiry),
7701
+ delegateDID: signed.audience,
7702
+ host: normalizedHost(input.host)
7703
+ };
7704
+ return activateValidatedRuntimeDelegation(node, delegation, {
7705
+ host: input.host
7706
+ });
7707
+ }
6675
7708
  function serializeDelegation(delegation) {
6676
7709
  return JSON.stringify({
6677
7710
  ...delegation,
@@ -6739,7 +7772,7 @@ import {
6739
7772
  import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
6740
7773
  export {
6741
7774
  ACCOUNT_REGISTRY_PATH,
6742
- ACCOUNT_REGISTRY_SPACE2 as ACCOUNT_REGISTRY_SPACE,
7775
+ ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
6743
7776
  AccountService,
6744
7777
  AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
6745
7778
  CapabilityKeyRegistry2 as CapabilityKeyRegistry,
@@ -6786,6 +7819,7 @@ export {
6786
7819
  VaultPublicSpaceKVActions,
6787
7820
  VersionCheckError,
6788
7821
  WasmKeyProvider,
7822
+ activateCompactRuntimeDelegation,
6789
7823
  addressStorageKey,
6790
7824
  buildSpaceUri,
6791
7825
  canonicalizeAddress3 as canonicalizeAddress,