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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.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,8 @@ import {
1261
1713
  parseNetworkId as parseNetworkId2,
1262
1714
  resolveSecretPath as resolveSecretPath2,
1263
1715
  validateOwnerSharePolicyRegistrationBytes,
1264
- validateShareDeliveryAuthorizationBytes
1716
+ validateShareDeliveryAuthorizationBytes,
1717
+ verifyEip191MessageSignature
1265
1718
  } from "@tinycloud/sdk-core";
1266
1719
  import {
1267
1720
  parsePermissionHint
@@ -1452,7 +1905,7 @@ function createWasmKeyProvider(sessionManager) {
1452
1905
  // src/delegateToHelpers.ts
1453
1906
  import {
1454
1907
  parseExpiry,
1455
- SiweMessage,
1908
+ SiweMessage as SiweMessage2,
1456
1909
  EXPIRY as EXPIRY2
1457
1910
  } from "@tinycloud/sdk-core";
1458
1911
  function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
@@ -1501,7 +1954,7 @@ function resolveExpiryMs(expiry) {
1501
1954
  return parseExpiry(expiry);
1502
1955
  }
1503
1956
  function extractSiweExpiration(siwe) {
1504
- const parsed = new SiweMessage(siwe);
1957
+ const parsed = new SiweMessage2(siwe);
1505
1958
  if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
1506
1959
  return void 0;
1507
1960
  }
@@ -1703,6 +2156,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
1703
2156
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
1704
2157
  var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
1705
2158
  var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
2159
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
2160
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
2161
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
1706
2162
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
1707
2163
  function isSecretPayload(value) {
1708
2164
  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";
@@ -1745,6 +2201,9 @@ function decodeAuthorizationBytes(authorization) {
1745
2201
  function isOpenKeyAutoSignStrategy(strategy) {
1746
2202
  return strategy?.openKeyAutoSign === true;
1747
2203
  }
2204
+ function openKeyApproval(strategy) {
2205
+ return strategy?.openKeyRequestApproval;
2206
+ }
1748
2207
  function isInteractiveSigner(config) {
1749
2208
  if (config.privateKey) {
1750
2209
  return false;
@@ -2110,9 +2569,12 @@ var _TinyCloudNode = class _TinyCloudNode {
2110
2569
  this._bootstrapSkipped = false;
2111
2570
  /**
2112
2571
  * 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
2572
+ * true when bootstrap did not complete in this invocation (interactive signer,
2573
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
2574
+ * step failed). `already-provisioned` means the bootstrap was not run here;
2575
+ * it does not mean a prior provisioning attempt failed to complete.
2576
+ * `reason` carries the cause so apps can surface a "finish account setup"
2577
+ * call-to-action. `warnings` is
2116
2578
  * present when bootstrap completed, but one or more steps recovered from
2117
2579
  * an ambiguous failure (e.g. a KV batch write reconciled via per-space
2118
2580
  * fallback) — clean vs. recovered runs are otherwise byte-identical.
@@ -2295,7 +2757,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2295
2757
  nonce: config.nonce,
2296
2758
  siweConfig: config.siweConfig,
2297
2759
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
2298
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2760
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
2299
2761
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
2300
2762
  });
2301
2763
  this.tc = new TinyCloud(this.auth, {
@@ -2385,6 +2847,15 @@ var _TinyCloudNode = class _TinyCloudNode {
2385
2847
  get sessionDid() {
2386
2848
  return this.sessionManager.getDID(this.sessionKeyId);
2387
2849
  }
2850
+ /** Bare did:key principal used as the credential subject and holder binding. */
2851
+ get credentialHolderDid() {
2852
+ return this.sessionDid.split("#", 1)[0];
2853
+ }
2854
+ /** Canonical verification method for the active credential holder key. */
2855
+ get credentialHolderKid() {
2856
+ const holder = this.credentialHolderDid;
2857
+ return `${holder}#${holder.slice("did:key:".length)}`;
2858
+ }
2388
2859
  /**
2389
2860
  * Return the current session's signed ReCap capabilities after the session
2390
2861
  * has been authenticated or restored. This is intentionally distinct from
@@ -2435,7 +2906,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2435
2906
  if (!this._address) {
2436
2907
  return void 0;
2437
2908
  }
2438
- return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE);
2909
+ return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
2439
2910
  }
2440
2911
  /**
2441
2912
  * Account-level application and delegation helpers.
@@ -2538,6 +3009,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2538
3009
  this._bootstrapSkipped = false;
2539
3010
  this._bootstrapStatus = { skipped: false };
2540
3011
  if (this.config.autoBootstrapAccount === false) {
3012
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
2541
3013
  return false;
2542
3014
  }
2543
3015
  if (!this.auth || !this._address) {
@@ -2552,11 +3024,14 @@ var _TinyCloudNode = class _TinyCloudNode {
2552
3024
  return false;
2553
3025
  }
2554
3026
  const steps = bootstrapSteps(this._address, this._chainId);
2555
- if (!await this.isFreshBootstrapAccount(steps)) {
2556
- return false;
2557
- }
2558
3027
  try {
2559
- const warnings = await this.runAccountBootstrap(steps);
3028
+ const decision = await this.resolveBootstrapDecision(steps);
3029
+ if (decision.action === "skip") {
3030
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
3031
+ return false;
3032
+ }
3033
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
3034
+ await this.writeBootstrapCompletionMarker(steps);
2560
3035
  if (warnings.length > 0) {
2561
3036
  this._bootstrapStatus = { skipped: false, warnings };
2562
3037
  this.notificationHandler.warning(
@@ -2575,32 +3050,92 @@ var _TinyCloudNode = class _TinyCloudNode {
2575
3050
  }
2576
3051
  return true;
2577
3052
  }
2578
- async isFreshBootstrapAccount(steps) {
3053
+ async resolveBootstrapDecision(steps) {
2579
3054
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
2580
3055
  for (const step of steps) {
2581
3056
  if (step.kind === "session") {
2582
3057
  enshrinedSpaceIds.add(step.spaceId);
2583
3058
  }
2584
3059
  }
2585
- const skipped = this.auth.lastActivationSkippedSpaceIds;
3060
+ const auth = this.auth;
3061
+ if (!auth) {
3062
+ throw new Error("Account bootstrap requires an active wallet session");
3063
+ }
3064
+ const skipped = auth.lastActivationSkippedSpaceIds;
2586
3065
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
2587
- return true;
3066
+ return { action: "run", mode: "fresh" };
2588
3067
  }
2589
3068
  try {
2590
- const indexed = await this.account.index.spaces.list();
2591
- if (indexed.ok && indexed.data.length === 0) {
2592
- return true;
3069
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
3070
+ const markerPermission = {
3071
+ service: "tinycloud.kv",
3072
+ space: accountSpaceId,
3073
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
3074
+ actions: ["get"]
3075
+ };
3076
+ if (!this.hasRuntimePermissions([markerPermission])) {
3077
+ return { action: "run", mode: "repair" };
2593
3078
  }
2594
- } catch {
3079
+ const marker = await this.readBootstrapCompletionMarker();
3080
+ if (marker.ok) {
3081
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
3082
+ return { action: "skip" };
3083
+ }
3084
+ return { action: "run", mode: "repair" };
3085
+ }
3086
+ if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
3087
+ return { action: "run", mode: "fresh" };
3088
+ }
3089
+ } catch (err) {
3090
+ const reason = err instanceof Error ? err.message : String(err);
3091
+ this.notificationHandler.warning(
3092
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
3093
+ );
3094
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
3095
+ return { action: "run", mode: "repair" };
2595
3096
  }
2596
- try {
2597
- const spaces = await this.account.spaces.list();
2598
- return spaces.ok && spaces.data.length === 0;
2599
- } catch {
2600
- return false;
3097
+ return { action: "run", mode: "repair" };
3098
+ }
3099
+ async readBootstrapCompletionMarker() {
3100
+ return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
3101
+ BOOTSTRAP_COMPLETION_MARKER_KEY
3102
+ );
3103
+ }
3104
+ async writeBootstrapCompletionMarker(steps) {
3105
+ if (!this._address) {
3106
+ throw new Error("Bootstrap completion marker requires an active wallet session");
2601
3107
  }
3108
+ const canonicalStepIds = new Set(
3109
+ bootstrapSteps(this._address, this._chainId).map((step) => step.id)
3110
+ );
3111
+ const providedStepIds = new Set(steps.map((step) => step.id));
3112
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
3113
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
3114
+ }
3115
+ const marker = {
3116
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
3117
+ stepIds: [...canonicalStepIds],
3118
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3119
+ };
3120
+ const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
3121
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
3122
+ marker
3123
+ );
3124
+ if (!written.ok) {
3125
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
3126
+ }
3127
+ }
3128
+ isAcceptedBootstrapCompletionMarker(value) {
3129
+ if (typeof value !== "object" || value === null) return false;
3130
+ const entries = Object.entries(value);
3131
+ const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
3132
+ const version = markerValue("v");
3133
+ return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
2602
3134
  }
2603
- async runAccountBootstrap(steps) {
3135
+ markerReadIsUnhostedSpace(meta) {
3136
+ return meta?.status === 404;
3137
+ }
3138
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
2604
3139
  if (!this.auth || !this._address) {
2605
3140
  throw new Error("Account bootstrap requires an active wallet session");
2606
3141
  }
@@ -2701,9 +3236,8 @@ var _TinyCloudNode = class _TinyCloudNode {
2701
3236
  if (step.kind === "seed-applications") {
2702
3237
  const registered = await this.account.applications.register(
2703
3238
  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.
3239
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
3240
+ // repair, so skip the manifest-hash pre-read.
2707
3241
  { assumeUnregistered: true }
2708
3242
  );
2709
3243
  if (!registered.ok) {
@@ -2711,7 +3245,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2711
3245
  }
2712
3246
  }
2713
3247
  if (step.kind === "encryption-network-create") {
2714
- await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
3248
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
2715
3249
  }
2716
3250
  if (step.kind === "secret-records-schema") {
2717
3251
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -2894,7 +3428,7 @@ var _TinyCloudNode = class _TinyCloudNode {
2894
3428
  if (!this.auth || !this.signer) {
2895
3429
  throw new Error("Manifest registry write requires wallet mode");
2896
3430
  }
2897
- const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE);
3431
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
2898
3432
  await this.ensureOwnedSpaceHostedById(accountSpaceId);
2899
3433
  const result = await this.account.applications.register(request.manifests);
2900
3434
  if (!result.ok) {
@@ -3150,6 +3684,14 @@ var _TinyCloudNode = class _TinyCloudNode {
3150
3684
  }
3151
3685
  return hosted;
3152
3686
  }
3687
+ /** Resolve and authenticate the owner encoded by an owned-space URI. */
3688
+ credentialSpaceOwnerDid(spaceId) {
3689
+ const ownerDid = this.ownerDidFromSpaceId(spaceId);
3690
+ if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
3691
+ throw new Error("Credential space owner does not match the active TinyCloud owner");
3692
+ }
3693
+ return ownerDid;
3694
+ }
3153
3695
  /**
3154
3696
  * Check whether an owned space is already registered/hosted by consulting the
3155
3697
  * account spaces registry.
@@ -3693,6 +4235,66 @@ var _TinyCloudNode = class _TinyCloudNode {
3693
4235
  if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
3694
4236
  return signBytesWithJwk(bytes, session.jwk);
3695
4237
  }
4238
+ /** Apply only an already-enabled signing policy to the exact credential request. */
4239
+ async autoSignCredentialBytes(bytes) {
4240
+ const strategy = this.config.signStrategy;
4241
+ if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
4242
+ if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
4243
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4244
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4245
+ const request = {
4246
+ address: this.address ?? "",
4247
+ chainId: this.session?.chainId ?? this._chainId,
4248
+ message: new TextDecoder().decode(bytes),
4249
+ type: "message",
4250
+ purpose: "message"
4251
+ };
4252
+ const decision = await strategy.handler(request);
4253
+ if (!decision.approved) {
4254
+ if (decision.needsApproval) return void 0;
4255
+ throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
4256
+ }
4257
+ if (typeof decision.signature !== "string" || !await verifyEip191MessageSignature(
4258
+ request.message,
4259
+ decision.signature,
4260
+ request.address
4261
+ )) {
4262
+ throw new Error("OpenKey credential signature evidence is invalid");
4263
+ }
4264
+ return this.signSessionBytes(bytes);
4265
+ }
4266
+ /** Invoke the configured interactive approval strategy before session signing. */
4267
+ async approveCredentialBytes(bytes) {
4268
+ const strategy = this.config.signStrategy;
4269
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
4270
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
4271
+ const request = {
4272
+ address: this.address ?? "",
4273
+ chainId: this.session?.chainId ?? this._chainId,
4274
+ message: new TextDecoder().decode(bytes),
4275
+ type: "message",
4276
+ purpose: "message"
4277
+ };
4278
+ const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
4279
+ if (approval) {
4280
+ const decision = await approval(request);
4281
+ if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
4282
+ if (decision.signature !== void 0 && !await verifyEip191MessageSignature(
4283
+ request.message,
4284
+ decision.signature,
4285
+ request.address
4286
+ )) {
4287
+ throw new Error("OpenKey credential signature evidence is invalid");
4288
+ }
4289
+ return this.signSessionBytes(bytes);
4290
+ }
4291
+ if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
4292
+ if (this.config.signer) {
4293
+ await this.config.signer.signMessage(request.message);
4294
+ return this.signSessionBytes(bytes);
4295
+ }
4296
+ throw new Error("Interactive OpenKey approval is not configured");
4297
+ }
3696
4298
  /** Bind a session-signed invocation to the canonical service audience. */
3697
4299
  async bindInvocationAudience(authorization, audience) {
3698
4300
  const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
@@ -3760,7 +4362,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3760
4362
  nonce: this.config.nonce,
3761
4363
  siweConfig: this.config.siweConfig,
3762
4364
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3763
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4365
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
3764
4366
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3765
4367
  });
3766
4368
  this.tc = new TinyCloud(this.auth, {
@@ -3813,7 +4415,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3813
4415
  nonce: this.config.nonce,
3814
4416
  siweConfig: this.config.siweConfig,
3815
4417
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
3816
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
4418
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
3817
4419
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
3818
4420
  });
3819
4421
  this.tc = new TinyCloud(this.auth, {
@@ -6562,35 +7164,44 @@ var _TinyCloudNode = class _TinyCloudNode {
6562
7164
  if (parentDelegation.disableSubDelegation) {
6563
7165
  throw new Error("Parent delegation does not allow sub-delegation");
6564
7166
  }
6565
- if (!params.path.startsWith(parentDelegation.path)) {
7167
+ const parentResources = parentDelegation.resources ?? [{
7168
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7169
+ space: parentDelegation.spaceId,
7170
+ path: parentDelegation.path,
7171
+ actions: parentDelegation.actions
7172
+ }];
7173
+ const requestedResources = params.resources ?? [{
7174
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
7175
+ space: parentDelegation.spaceId,
7176
+ path: params.path,
7177
+ actions: params.actions
7178
+ }];
7179
+ const resourceIsContained = (parent, child) => {
7180
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
7181
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
7182
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
7183
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
7184
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
7185
+ };
7186
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
6566
7187
  throw new Error(
6567
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
7188
+ "Sub-delegation resources exceed the received delegation"
6568
7189
  );
6569
7190
  }
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
7191
  const now = /* @__PURE__ */ new Date();
6579
7192
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
6580
7193
  const requestedExpiry = new Date(now.getTime() + expiryMs);
6581
7194
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
6582
7195
  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 };
7196
+ const rawAbilities = {};
7197
+ for (const resource of requestedResources) {
7198
+ const service = resource.service.replace(/^tinycloud\./, "");
7199
+ if (service === "encryption") {
7200
+ rawAbilities[resource.path] = [...resource.actions];
7201
+ continue;
7202
+ }
7203
+ abilities[service] ?? (abilities[service] = {});
7204
+ abilities[service][resource.path] = [...resource.actions];
6594
7205
  }
6595
7206
  const targetHost = parentDelegation.host ?? this.config.host;
6596
7207
  const prepared = this.wasmBindings.prepareSession({
@@ -6602,7 +7213,8 @@ var _TinyCloudNode = class _TinyCloudNode {
6602
7213
  expirationTime: actualExpiry.toISOString(),
6603
7214
  spaceId: parentDelegation.spaceId,
6604
7215
  delegateUri: params.delegateDID,
6605
- parents: [parentDelegation.cid]
7216
+ parents: [parentDelegation.cid],
7217
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
6606
7218
  });
6607
7219
  const signature = await this.signer.signMessage(prepared.siwe);
6608
7220
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -6620,14 +7232,20 @@ var _TinyCloudNode = class _TinyCloudNode {
6620
7232
  cid: subDelegationSession.delegationCid,
6621
7233
  delegationHeader: subDelegationSession.delegationHeader,
6622
7234
  spaceId: parentDelegation.spaceId,
6623
- path: params.path,
6624
- actions: params.actions,
7235
+ path: requestedResources[0].path,
7236
+ actions: requestedResources[0].actions,
6625
7237
  disableSubDelegation: params.disableSubDelegation ?? false,
6626
7238
  expiry: actualExpiry,
6627
7239
  delegateDID: params.delegateDID,
6628
7240
  ownerAddress: parentDelegation.ownerAddress,
6629
7241
  chainId: parentDelegation.chainId,
6630
- host: targetHost
7242
+ host: targetHost,
7243
+ resources: requestedResources.map((resource) => ({
7244
+ service: resource.service.replace(/^tinycloud\./, ""),
7245
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
7246
+ path: resource.path,
7247
+ actions: [...resource.actions]
7248
+ }))
6631
7249
  };
6632
7250
  }
6633
7251
  };
@@ -6647,7 +7265,7 @@ var TinyCloudNode = _TinyCloudNode;
6647
7265
  // src/core.ts
6648
7266
  import {
6649
7267
  ACCOUNT_REGISTRY_PATH,
6650
- ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
7268
+ ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
6651
7269
  DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
6652
7270
  DEFAULT_MANIFEST_VERSION,
6653
7271
  VAULT_PERMISSION_SERVICE,
@@ -6739,7 +7357,7 @@ import {
6739
7357
  import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
6740
7358
  export {
6741
7359
  ACCOUNT_REGISTRY_PATH,
6742
- ACCOUNT_REGISTRY_SPACE2 as ACCOUNT_REGISTRY_SPACE,
7360
+ ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
6743
7361
  AccountService,
6744
7362
  AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
6745
7363
  CapabilityKeyRegistry2 as CapabilityKeyRegistry,