@tinycloud/node-sdk 2.11.0-beta.9 → 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/index.cjs CHANGED
@@ -17025,10 +17025,13 @@ var require_utils2 = __commonJS({
17025
17025
  // src/index.ts
17026
17026
  var index_exports = {};
17027
17027
  __export(index_exports, {
17028
+ ACCEPTED_MARKER_VERSIONS: () => ACCEPTED_MARKER_VERSIONS,
17028
17029
  ACCOUNT_REGISTRY_PATH: () => import_sdk_core12.ACCOUNT_REGISTRY_PATH,
17029
17030
  ACCOUNT_REGISTRY_SPACE: () => import_sdk_core12.ACCOUNT_REGISTRY_SPACE,
17030
17031
  AccountService: () => import_sdk_core3.AccountService,
17031
17032
  AutoApproveSpaceCreationHandler: () => import_sdk_core11.AutoApproveSpaceCreationHandler,
17033
+ BOOTSTRAP_COMPLETION_MARKER_KEY: () => BOOTSTRAP_COMPLETION_MARKER_KEY,
17034
+ BOOTSTRAP_COMPLETION_MARKER_VERSION: () => BOOTSTRAP_COMPLETION_MARKER_VERSION,
17032
17035
  CapabilityKeyRegistry: () => import_sdk_core20.CapabilityKeyRegistry,
17033
17036
  CapabilityKeyRegistryErrorCodes: () => import_sdk_core20.CapabilityKeyRegistryErrorCodes,
17034
17037
  CaveatedDelegationUnsupportedError: () => import_sdk_core12.CaveatedDelegationUnsupportedError,
@@ -17165,7 +17168,8 @@ __export(index_exports, {
17165
17168
  tinyCloudDebugLogger: () => import_sdk_core11.tinyCloudDebugLogger,
17166
17169
  validateEnvelope: () => import_sdk_core17.validateEnvelope,
17167
17170
  validateManifest: () => import_sdk_core12.validateManifest,
17168
- verifyDecryptResponse: () => import_sdk_core17.verifyDecryptResponse
17171
+ verifyDecryptResponse: () => import_sdk_core17.verifyDecryptResponse,
17172
+ wireOpenKeyAuthorize: () => wireOpenKeyAuthorize
17169
17173
  });
17170
17174
  module.exports = __toCommonJS(index_exports);
17171
17175
 
@@ -17278,6 +17282,8 @@ var import_sdk_core7 = require("@tinycloud/sdk-core");
17278
17282
  var import_sdk_services = require("@tinycloud/sdk-services");
17279
17283
 
17280
17284
  // src/authorization/NodeUserAuthorization.ts
17285
+ var import_node_crypto = require("crypto");
17286
+ var import_siwe = require("siwe");
17281
17287
  var import_sdk_core2 = require("@tinycloud/sdk-core");
17282
17288
 
17283
17289
  // src/authorization/strategies.ts
@@ -17358,6 +17364,27 @@ var MemorySessionStorage = class {
17358
17364
  // src/authorization/NodeUserAuthorization.ts
17359
17365
  var DECRYPT_ACTION = import_sdk_core2.ENCRYPTION.DECRYPT;
17360
17366
  var NETWORK_CREATE_ACTION = import_sdk_core2.ENCRYPTION.NETWORK_CREATE;
17367
+ function canonicalizeOpenKeyManifestJson(value) {
17368
+ const json = JSON.stringify(value);
17369
+ if (json === void 0) {
17370
+ throw new TypeError("manifest is not JSON-serializable");
17371
+ }
17372
+ return canonicalizeJsonValue(JSON.parse(json));
17373
+ }
17374
+ function canonicalizeJsonValue(value) {
17375
+ if (value === null || typeof value !== "object") {
17376
+ return JSON.stringify(value);
17377
+ }
17378
+ if (Array.isArray(value)) {
17379
+ return `[${value.map(canonicalizeJsonValue).join(",")}]`;
17380
+ }
17381
+ return `{${Object.keys(value).sort().map(
17382
+ (key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
17383
+ ).join(",")}}`;
17384
+ }
17385
+ function canonicalOpenKeyManifestSha256Hex(value) {
17386
+ return (0, import_node_crypto.createHash)("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
17387
+ }
17361
17388
  function didPrincipalMatches(actual, expected) {
17362
17389
  try {
17363
17390
  return (0, import_sdk_core2.principalDidEquals)(actual, expected);
@@ -17375,6 +17402,16 @@ function addRawAbility(rawAbilities, resource, action) {
17375
17402
  actions.push(action);
17376
17403
  }
17377
17404
  }
17405
+ function cloneAbilitiesMap(actions) {
17406
+ return Object.fromEntries(
17407
+ Object.entries(actions).map(([service, paths]) => [
17408
+ service,
17409
+ Object.fromEntries(
17410
+ Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
17411
+ )
17412
+ ])
17413
+ );
17414
+ }
17378
17415
  var NodeUserAuthorization = class {
17379
17416
  constructor(config) {
17380
17417
  this.activeSessionKeyId = "default";
@@ -17646,23 +17683,41 @@ var NodeUserAuthorization = class {
17646
17683
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
17647
17684
  const primarySpaceId2 = (0, import_sdk_core2.makePkhSpaceId)(address, chainId, this.spacePrefix);
17648
17685
  const secretsSpaceId = (0, import_sdk_core2.makePkhSpaceId)(address, chainId, "secrets");
17649
- return {
17650
- abilities: this.defaultActions,
17651
- spaceId: primarySpaceId2,
17652
- spaceAbilities: {
17653
- [primarySpaceId2]: this.defaultActions,
17654
- [secretsSpaceId]: {
17655
- kv: {
17656
- "vault/secrets/": [
17657
- import_sdk_core2.KV.GET,
17658
- import_sdk_core2.KV.PUT,
17659
- import_sdk_core2.KV.DEL,
17660
- import_sdk_core2.KV.LIST,
17661
- import_sdk_core2.KV.METADATA
17662
- ]
17686
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === import_sdk_core2.ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
17687
+ const spaceAbilities2 = {
17688
+ [primarySpaceId2]: primaryActions,
17689
+ [secretsSpaceId]: {
17690
+ kv: {
17691
+ "vault/secrets/": [
17692
+ import_sdk_core2.KV.GET,
17693
+ import_sdk_core2.KV.PUT,
17694
+ import_sdk_core2.KV.DEL,
17695
+ import_sdk_core2.KV.LIST,
17696
+ import_sdk_core2.KV.METADATA
17697
+ ]
17698
+ }
17699
+ }
17700
+ };
17701
+ if (this.includeAccountRegistryPermissions && this.spacePrefix !== import_sdk_core2.ACCOUNT_REGISTRY_SPACE) {
17702
+ spaceAbilities2[(0, import_sdk_core2.makePkhSpaceId)(address, chainId, import_sdk_core2.ACCOUNT_REGISTRY_SPACE)] = (0, import_sdk_core2.resourceCapabilitiesToAbilitiesMap)(import_sdk_core2.ACCOUNT_MANIFEST_PERMISSIONS);
17703
+ } else if (this.includeAccountRegistryPermissions) {
17704
+ const accountAbilities = (0, import_sdk_core2.resourceCapabilitiesToAbilitiesMap)(
17705
+ import_sdk_core2.ACCOUNT_MANIFEST_PERMISSIONS
17706
+ );
17707
+ for (const [service, paths] of Object.entries(accountAbilities)) {
17708
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
17709
+ for (const [path, actions] of Object.entries(paths)) {
17710
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
17711
+ for (const action of actions) {
17712
+ if (!existingActions.includes(action)) existingActions.push(action);
17663
17713
  }
17664
17714
  }
17665
- },
17715
+ }
17716
+ }
17717
+ return {
17718
+ abilities: primaryActions,
17719
+ spaceId: primarySpaceId2,
17720
+ spaceAbilities: spaceAbilities2,
17666
17721
  rawAbilities: {
17667
17722
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
17668
17723
  }
@@ -18243,6 +18298,398 @@ var NodeUserAuthorization = class {
18243
18298
  await this.ensureSpaceExists();
18244
18299
  return clientSession;
18245
18300
  }
18301
+ /**
18302
+ * End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
18303
+ *
18304
+ * This is the production entry point that wires
18305
+ * {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
18306
+ * {@link signInWithOpenKeyResult} into one call. Consumers pass in a
18307
+ * caller-provided `authorizeFn` (typically a thin wrapper around
18308
+ * `openkey.authorizeTinyCloud()`) so this method has no direct
18309
+ * dependency on the OpenKey SDK — it works with any transport that
18310
+ * conforms to the versioned protocol.
18311
+ *
18312
+ * The `authorizeFn` MUST:
18313
+ * 1. Send the prepared SIWE bytes to OpenKey.
18314
+ * 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
18315
+ * `signedMessage` — the bytes the signature actually verifies
18316
+ * against — plus `selectedActionKeys` and `permissions`).
18317
+ *
18318
+ * NodeUserAuthorization then completes the session with `signedMessage`
18319
+ * (never the prepared bytes), verifying the signature, the immutable
18320
+ * SIWE fields, and every subset invariant enforced by
18321
+ * {@link signInWithOpenKeyResult}. This preserves the exact-byte
18322
+ * guarantee: if the user did NOT narrow, the returned `signedMessage`
18323
+ * equals the prepared SIWE byte-for-byte; if the user narrowed, only
18324
+ * the ReCap block differs and every immutable header field survives.
18325
+ *
18326
+ * Legacy exact-byte {@link signIn} callers are unaffected — this method
18327
+ * is opt-in.
18328
+ *
18329
+ * @param authorizeFn Callback that invokes the versioned OpenKey
18330
+ * `authorizeTinyCloud()` protocol and returns the result. Receives
18331
+ * the prepared SIWE bytes plus a `host` hint the widget should bind
18332
+ * into its /authorize-sign-prepare context.
18333
+ * @param options Optional overrides for the OpenKey request.
18334
+ */
18335
+ async signInWithOpenKey(authorizeFn, options) {
18336
+ if (options?.host !== void 0) {
18337
+ const overrideHost = options.host.trim();
18338
+ if (!overrideHost) {
18339
+ throw new Error("OpenKey authorization host must not be empty");
18340
+ }
18341
+ this.tinycloudHosts = [overrideHost];
18342
+ }
18343
+ const signingAddress = (0, import_sdk_core2.canonicalizeAddress)(await this.signer.getAddress());
18344
+ const signingChainId = await this.signer.getChainId();
18345
+ await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
18346
+ const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
18347
+ const presentation = this.buildPresentationEnvelope(options?.reason);
18348
+ const hostHint = this.primaryTinyCloudHost;
18349
+ const result = await authorizeFn({
18350
+ protocolVersion: 1,
18351
+ siwe: prepared.siwe,
18352
+ jwk,
18353
+ host: hostHint,
18354
+ keyId: options?.openkeyKeyId,
18355
+ presentation
18356
+ });
18357
+ return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
18358
+ }
18359
+ /**
18360
+ * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
18361
+ * envelope is display-only — the OpenKey widget renders it under an
18362
+ * honest trust label, and the server /authorize-sign-prepare route
18363
+ * decides whether to upgrade the label to `origin-bound` by fetching
18364
+ * the well-known manifest and comparing its digest to
18365
+ * `manifestDigest`.
18366
+ *
18367
+ * Returns `undefined` when no manifest is set — the widget renders as
18368
+ * "no manifest supplied" (unsigned).
18369
+ *
18370
+ * The digest is a deterministic SHA-256 over canonical JSON of the
18371
+ * first manifest payload; OpenKey parses the fetched well-known manifest
18372
+ * and applies the same canonicalization before comparing digests.
18373
+ */
18374
+ buildPresentationEnvelope(reason) {
18375
+ const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
18376
+ if (manifests.length === 0 && !reason) {
18377
+ return void 0;
18378
+ }
18379
+ const primary = manifests[0];
18380
+ let displayName;
18381
+ let manifestId;
18382
+ let manifestDigest;
18383
+ const envelopeManifests = [];
18384
+ for (const m of manifests) {
18385
+ envelopeManifests.push({
18386
+ name: m.name,
18387
+ appId: m.app_id,
18388
+ // Deep-copy to prevent later mutations from affecting the digest.
18389
+ payload: JSON.parse(JSON.stringify(m))
18390
+ });
18391
+ }
18392
+ if (primary) {
18393
+ displayName = primary.name;
18394
+ manifestId = primary.app_id;
18395
+ manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
18396
+ }
18397
+ return {
18398
+ protocolVersion: 1,
18399
+ displayName,
18400
+ reason: reason && reason.length > 0 ? reason : void 0,
18401
+ manifestId,
18402
+ manifestDigest,
18403
+ manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
18404
+ };
18405
+ }
18406
+ /**
18407
+ * Complete sign-in with a versioned OpenKey authorization result.
18408
+ *
18409
+ * Where `signInWithPreparedSession` completes with a signature over the
18410
+ * ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
18411
+ * OpenKey widget actually presented and signed (`signedMessage`). It
18412
+ * exists because the versioned authorizeTinyCloud protocol lets the user
18413
+ * narrow capabilities; the returned bytes may differ from the original
18414
+ * request and are the only ones the signature actually verifies against.
18415
+ *
18416
+ * The `result` parameter is validated at the trust boundary via
18417
+ * `validateAuthorizationResultV1` (in sdk-core). Callers should validate
18418
+ * BEFORE calling this method so unknown/missing fields are rejected
18419
+ * before any session state is created.
18420
+ *
18421
+ * Validation performed here (defence-in-depth: even a "valid" v1 payload
18422
+ * must clear these checks before any session state is created):
18423
+ * 1. `result.address` matches the local signer's address.
18424
+ * 2. `result.signature` recovers to `result.address` when verified against
18425
+ * `result.signedMessage`.
18426
+ * 3. Every immutable SIWE header field (domain, address, URI, version,
18427
+ * chainId, nonce, issuedAt) in the original prepared SIWE is preserved
18428
+ * byte-for-byte in `result.signedMessage`. `statement` is NOT in this
18429
+ * list because the WASM renders it from the ReCap contents; it MUST
18430
+ * change when the ReCap is narrowed.
18431
+ * 4. Every capability in `result.signedMessage`'s ReCap block was present
18432
+ * in the original prepared SIWE — narrowing OK, broadening rejected.
18433
+ * 5. Every `result.selectedActionKeys` entry corresponds to a
18434
+ * (resource, action) pair in the returned SIWE.
18435
+ */
18436
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
18437
+ if (result.protocolVersion !== 1) {
18438
+ throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
18439
+ }
18440
+ if (!result.signedMessage) {
18441
+ throw new Error("OpenKey result must include signedMessage");
18442
+ }
18443
+ if (!prepared.siwe) {
18444
+ throw new Error(
18445
+ "signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
18446
+ );
18447
+ }
18448
+ const expectedAddress = (0, import_sdk_core2.canonicalizeAddress)(await this.signer.getAddress());
18449
+ if ((0, import_sdk_core2.canonicalizeAddress)(result.address) !== expectedAddress) {
18450
+ throw new Error(
18451
+ `OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
18452
+ );
18453
+ }
18454
+ let parsedSignedSiwe;
18455
+ try {
18456
+ parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
18457
+ } catch (err) {
18458
+ throw new Error(
18459
+ `OpenKey signedMessage is not a parseable SIWE: ${err.message}`
18460
+ );
18461
+ }
18462
+ const verifyResult = await parsedSignedSiwe.verify(
18463
+ { signature: result.signature },
18464
+ { suppressExceptions: true }
18465
+ );
18466
+ if (!verifyResult.success) {
18467
+ const errData = verifyResult.error;
18468
+ const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
18469
+ const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
18470
+ throw new Error(
18471
+ `OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
18472
+ );
18473
+ }
18474
+ if ((0, import_sdk_core2.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
18475
+ throw new Error(
18476
+ `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
18477
+ );
18478
+ }
18479
+ const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
18480
+ const originalFields = (0, import_sdk_core2.extractImmutableSiweFields)(prepared.siwe);
18481
+ const signedFields = (0, import_sdk_core2.extractImmutableSiweFields)(result.signedMessage);
18482
+ const changedFields = (0, import_sdk_core2.diffImmutableSiweFields)(originalFields, signedFields, {
18483
+ originalHasRecap
18484
+ });
18485
+ if (changedFields.length > 0) {
18486
+ throw new Error(
18487
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
18488
+ );
18489
+ }
18490
+ let originalCaps;
18491
+ let signedCaps;
18492
+ try {
18493
+ originalCaps = (0, import_sdk_core2.extractRecapAttenuations)(prepared.siwe);
18494
+ } catch (err) {
18495
+ throw new Error(
18496
+ `Failed to decode prepared SIWE recap: ${err.message}`
18497
+ );
18498
+ }
18499
+ try {
18500
+ signedCaps = (0, import_sdk_core2.extractRecapAttenuations)(result.signedMessage);
18501
+ } catch (err) {
18502
+ throw new Error(
18503
+ `Failed to decode OpenKey signedMessage recap: ${err.message}`
18504
+ );
18505
+ }
18506
+ const unauthorized = (0, import_sdk_core2.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
18507
+ if (unauthorized.length > 0) {
18508
+ const previewLimit = 5;
18509
+ const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
18510
+ const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
18511
+ throw new Error(
18512
+ `OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
18513
+ );
18514
+ }
18515
+ if (!result.signedMessage.includes(prepared.spaceId)) {
18516
+ throw new Error(
18517
+ `OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
18518
+ );
18519
+ }
18520
+ const grantedPairs = /* @__PURE__ */ new Set();
18521
+ const grantedFourPartIndex = /* @__PURE__ */ new Map();
18522
+ for (const [resource, actions] of Object.entries(signedCaps)) {
18523
+ const { space, path } = (0, import_sdk_core2.parseCanonicalRecapResource)(resource);
18524
+ for (const action of Object.keys(actions)) {
18525
+ grantedPairs.add(`${resource}\0${action}`);
18526
+ const slashIdx = action.indexOf("/");
18527
+ const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
18528
+ if (service) {
18529
+ grantedFourPartIndex.set(
18530
+ `${service}\0${space}\0${path}\0${action}`,
18531
+ `${resource}\0${action}`
18532
+ );
18533
+ }
18534
+ }
18535
+ }
18536
+ const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
18537
+ const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
18538
+ for (const pair of grantedPairs) {
18539
+ const idx = pair.indexOf("\0");
18540
+ const action = idx >= 0 ? pair.slice(idx + 1) : pair;
18541
+ if (!isStructurallyRequired(action)) {
18542
+ nonRequiredGrantedPairs.add(pair);
18543
+ }
18544
+ }
18545
+ const allSignedGrantedPairs = new Set(grantedPairs);
18546
+ const seenRawKeys = /* @__PURE__ */ new Set();
18547
+ for (const rawKey of result.selectedActionKeys) {
18548
+ if (seenRawKeys.has(rawKey)) {
18549
+ throw new Error(
18550
+ `OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
18551
+ );
18552
+ }
18553
+ seenRawKeys.add(rawKey);
18554
+ }
18555
+ const selectedResourceActionPairs = /* @__PURE__ */ new Set();
18556
+ for (const rawKey of result.selectedActionKeys) {
18557
+ const parts = rawKey.split("\0");
18558
+ if (parts.length !== 4) {
18559
+ throw new Error(
18560
+ `OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
18561
+ );
18562
+ }
18563
+ const canonical = grantedFourPartIndex.get(rawKey);
18564
+ if (!canonical) {
18565
+ throw new Error(
18566
+ `OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
18567
+ );
18568
+ }
18569
+ selectedResourceActionPairs.add(canonical);
18570
+ }
18571
+ if (nonRequiredGrantedPairs.size > 0) {
18572
+ const missingPairs = [];
18573
+ for (const pair of nonRequiredGrantedPairs) {
18574
+ if (!selectedResourceActionPairs.has(pair)) {
18575
+ missingPairs.push(pair);
18576
+ }
18577
+ }
18578
+ if (missingPairs.length > 0) {
18579
+ const previewLimit = 5;
18580
+ const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18581
+ const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
18582
+ throw new Error(
18583
+ `OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
18584
+ );
18585
+ }
18586
+ }
18587
+ if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
18588
+ throw new Error(
18589
+ "OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
18590
+ );
18591
+ }
18592
+ const permissionCanonicalKeys = /* @__PURE__ */ new Set();
18593
+ const permissionPairsCovered = /* @__PURE__ */ new Set();
18594
+ for (const perm of result.permissions) {
18595
+ const { service, space, path, actions } = perm;
18596
+ if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
18597
+ throw new Error(
18598
+ `OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
18599
+ );
18600
+ }
18601
+ if (!Array.isArray(actions) || actions.length === 0) {
18602
+ throw new Error(
18603
+ `OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
18604
+ );
18605
+ }
18606
+ const canonicalKey = `${service}\0${space}\0${path}`;
18607
+ if (permissionCanonicalKeys.has(canonicalKey)) {
18608
+ throw new Error(
18609
+ `OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
18610
+ );
18611
+ }
18612
+ permissionCanonicalKeys.add(canonicalKey);
18613
+ let expectedResource;
18614
+ if (space.startsWith("tinycloud:")) {
18615
+ const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
18616
+ expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
18617
+ } else {
18618
+ if (path) {
18619
+ throw new Error(
18620
+ `OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
18621
+ );
18622
+ }
18623
+ expectedResource = space;
18624
+ }
18625
+ let matchedResource;
18626
+ if (signedCaps[expectedResource] !== void 0) {
18627
+ matchedResource = expectedResource;
18628
+ } else {
18629
+ throw new Error(
18630
+ `OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
18631
+ );
18632
+ }
18633
+ const grantedActionsForResource = new Set(
18634
+ Object.keys(signedCaps[matchedResource])
18635
+ );
18636
+ for (const action of actions) {
18637
+ if (typeof action !== "string" || !action) {
18638
+ throw new Error(
18639
+ `OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
18640
+ );
18641
+ }
18642
+ if (!grantedActionsForResource.has(action)) {
18643
+ throw new Error(
18644
+ `OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
18645
+ );
18646
+ }
18647
+ permissionPairsCovered.add(`${matchedResource}\0${action}`);
18648
+ }
18649
+ }
18650
+ if (allSignedGrantedPairs.size > 0) {
18651
+ const missingFromPerms = [];
18652
+ for (const pair of allSignedGrantedPairs) {
18653
+ if (!permissionPairsCovered.has(pair)) {
18654
+ missingFromPerms.push(pair);
18655
+ }
18656
+ }
18657
+ if (missingFromPerms.length > 0) {
18658
+ const previewLimit = 5;
18659
+ const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18660
+ const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
18661
+ throw new Error(
18662
+ `OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
18663
+ );
18664
+ }
18665
+ const extrasInPerms = [];
18666
+ for (const pair of permissionPairsCovered) {
18667
+ if (!allSignedGrantedPairs.has(pair)) {
18668
+ extrasInPerms.push(pair);
18669
+ }
18670
+ }
18671
+ if (extrasInPerms.length > 0) {
18672
+ const previewLimit = 5;
18673
+ const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18674
+ const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
18675
+ throw new Error(
18676
+ `OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
18677
+ );
18678
+ }
18679
+ }
18680
+ void nonRequiredGrantedPairs;
18681
+ return this.signInWithPreparedSession(
18682
+ {
18683
+ siwe: result.signedMessage,
18684
+ jwk: prepared.jwk,
18685
+ spaceId: prepared.spaceId,
18686
+ verificationMethod: prepared.verificationMethod
18687
+ },
18688
+ result.signature,
18689
+ keyId,
18690
+ jwk
18691
+ );
18692
+ }
18246
18693
  /**
18247
18694
  * Clear persisted session data.
18248
18695
  */
@@ -18733,6 +19180,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
18733
19180
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
18734
19181
  var NETWORK_CREATE_ACTION2 = import_sdk_core7.ENCRYPTION.NETWORK_CREATE;
18735
19182
  var DECRYPT_ACTION2 = import_sdk_core7.ENCRYPTION.DECRYPT;
19183
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
19184
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
19185
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
18736
19186
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
18737
19187
  function isSecretPayload(value) {
18738
19188
  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";
@@ -18775,6 +19225,9 @@ function decodeAuthorizationBytes(authorization) {
18775
19225
  function isOpenKeyAutoSignStrategy(strategy) {
18776
19226
  return strategy?.openKeyAutoSign === true;
18777
19227
  }
19228
+ function openKeyApproval(strategy) {
19229
+ return strategy?.openKeyRequestApproval;
19230
+ }
18778
19231
  function isInteractiveSigner(config) {
18779
19232
  if (config.privateKey) {
18780
19233
  return false;
@@ -19140,9 +19593,12 @@ var _TinyCloudNode = class _TinyCloudNode {
19140
19593
  this._bootstrapSkipped = false;
19141
19594
  /**
19142
19595
  * Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
19143
- * true when bootstrap did not complete (interactive signer, auto-sign
19144
- * denied, or a bootstrap step failed); `reason` carries the cause so apps
19145
- * can surface a "finish account setup" call-to-action. `warnings` is
19596
+ * true when bootstrap did not complete in this invocation (interactive signer,
19597
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
19598
+ * step failed). `already-provisioned` means the bootstrap was not run here;
19599
+ * it does not mean a prior provisioning attempt failed to complete.
19600
+ * `reason` carries the cause so apps can surface a "finish account setup"
19601
+ * call-to-action. `warnings` is
19146
19602
  * present when bootstrap completed, but one or more steps recovered from
19147
19603
  * an ambiguous failure (e.g. a KV batch write reconciled via per-space
19148
19604
  * fallback) — clean vs. recovered runs are otherwise byte-identical.
@@ -19325,7 +19781,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19325
19781
  nonce: config.nonce,
19326
19782
  siweConfig: config.siweConfig,
19327
19783
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
19328
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19784
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19329
19785
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
19330
19786
  });
19331
19787
  this.tc = new import_sdk_core7.TinyCloud(this.auth, {
@@ -19415,6 +19871,15 @@ var _TinyCloudNode = class _TinyCloudNode {
19415
19871
  get sessionDid() {
19416
19872
  return this.sessionManager.getDID(this.sessionKeyId);
19417
19873
  }
19874
+ /** Bare did:key principal used as the credential subject and holder binding. */
19875
+ get credentialHolderDid() {
19876
+ return this.sessionDid.split("#", 1)[0];
19877
+ }
19878
+ /** Canonical verification method for the active credential holder key. */
19879
+ get credentialHolderKid() {
19880
+ const holder = this.credentialHolderDid;
19881
+ return `${holder}#${holder.slice("did:key:".length)}`;
19882
+ }
19418
19883
  /**
19419
19884
  * Return the current session's signed ReCap capabilities after the session
19420
19885
  * has been authenticated or restored. This is intentionally distinct from
@@ -19568,6 +20033,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19568
20033
  this._bootstrapSkipped = false;
19569
20034
  this._bootstrapStatus = { skipped: false };
19570
20035
  if (this.config.autoBootstrapAccount === false) {
20036
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
19571
20037
  return false;
19572
20038
  }
19573
20039
  if (!this.auth || !this._address) {
@@ -19582,11 +20048,14 @@ var _TinyCloudNode = class _TinyCloudNode {
19582
20048
  return false;
19583
20049
  }
19584
20050
  const steps = (0, import_sdk_core7.bootstrapSteps)(this._address, this._chainId);
19585
- if (!await this.isFreshBootstrapAccount(steps)) {
19586
- return false;
19587
- }
19588
20051
  try {
19589
- const warnings = await this.runAccountBootstrap(steps);
20052
+ const decision = await this.resolveBootstrapDecision(steps);
20053
+ if (decision.action === "skip") {
20054
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
20055
+ return false;
20056
+ }
20057
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
20058
+ await this.writeBootstrapCompletionMarker(steps);
19590
20059
  if (warnings.length > 0) {
19591
20060
  this._bootstrapStatus = { skipped: false, warnings };
19592
20061
  this.notificationHandler.warning(
@@ -19605,32 +20074,92 @@ var _TinyCloudNode = class _TinyCloudNode {
19605
20074
  }
19606
20075
  return true;
19607
20076
  }
19608
- async isFreshBootstrapAccount(steps) {
20077
+ async resolveBootstrapDecision(steps) {
19609
20078
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
19610
20079
  for (const step of steps) {
19611
20080
  if (step.kind === "session") {
19612
20081
  enshrinedSpaceIds.add(step.spaceId);
19613
20082
  }
19614
20083
  }
19615
- const skipped = this.auth.lastActivationSkippedSpaceIds;
20084
+ const auth = this.auth;
20085
+ if (!auth) {
20086
+ throw new Error("Account bootstrap requires an active wallet session");
20087
+ }
20088
+ const skipped = auth.lastActivationSkippedSpaceIds;
19616
20089
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
19617
- return true;
20090
+ return { action: "run", mode: "fresh" };
19618
20091
  }
19619
20092
  try {
19620
- const indexed = await this.account.index.spaces.list();
19621
- if (indexed.ok && indexed.data.length === 0) {
19622
- return true;
20093
+ const accountSpaceId = this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE);
20094
+ const markerPermission = {
20095
+ service: "tinycloud.kv",
20096
+ space: accountSpaceId,
20097
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
20098
+ actions: ["get"]
20099
+ };
20100
+ if (!this.hasRuntimePermissions([markerPermission])) {
20101
+ return { action: "run", mode: "repair" };
19623
20102
  }
19624
- } catch {
20103
+ const marker = await this.readBootstrapCompletionMarker();
20104
+ if (marker.ok) {
20105
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
20106
+ return { action: "skip" };
20107
+ }
20108
+ return { action: "run", mode: "repair" };
20109
+ }
20110
+ if (marker.error.code === import_sdk_core7.ErrorCodes.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
20111
+ return { action: "run", mode: "fresh" };
20112
+ }
20113
+ } catch (err) {
20114
+ const reason = err instanceof Error ? err.message : String(err);
20115
+ this.notificationHandler.warning(
20116
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
20117
+ );
20118
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
20119
+ return { action: "run", mode: "repair" };
19625
20120
  }
19626
- try {
19627
- const spaces = await this.account.spaces.list();
19628
- return spaces.ok && spaces.data.length === 0;
19629
- } catch {
19630
- return false;
20121
+ return { action: "run", mode: "repair" };
20122
+ }
20123
+ async readBootstrapCompletionMarker() {
20124
+ return this.kvForSpace(this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE)).get(
20125
+ BOOTSTRAP_COMPLETION_MARKER_KEY
20126
+ );
20127
+ }
20128
+ async writeBootstrapCompletionMarker(steps) {
20129
+ if (!this._address) {
20130
+ throw new Error("Bootstrap completion marker requires an active wallet session");
20131
+ }
20132
+ const canonicalStepIds = new Set(
20133
+ (0, import_sdk_core7.bootstrapSteps)(this._address, this._chainId).map((step) => step.id)
20134
+ );
20135
+ const providedStepIds = new Set(steps.map((step) => step.id));
20136
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
20137
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
20138
+ }
20139
+ const marker = {
20140
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
20141
+ stepIds: [...canonicalStepIds],
20142
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
20143
+ };
20144
+ const written = await this.kvForSpace(this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE)).put(
20145
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
20146
+ marker
20147
+ );
20148
+ if (!written.ok) {
20149
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
19631
20150
  }
19632
20151
  }
19633
- async runAccountBootstrap(steps) {
20152
+ isAcceptedBootstrapCompletionMarker(value) {
20153
+ if (typeof value !== "object" || value === null) return false;
20154
+ const entries = Object.entries(value);
20155
+ const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
20156
+ const version23 = markerValue("v");
20157
+ return typeof version23 === "number" && Number.isInteger(version23) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version23);
20158
+ }
20159
+ markerReadIsUnhostedSpace(meta) {
20160
+ return meta?.status === 404;
20161
+ }
20162
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
19634
20163
  if (!this.auth || !this._address) {
19635
20164
  throw new Error("Account bootstrap requires an active wallet session");
19636
20165
  }
@@ -19731,9 +20260,8 @@ var _TinyCloudNode = class _TinyCloudNode {
19731
20260
  if (step.kind === "seed-applications") {
19732
20261
  const registered = await this.account.applications.register(
19733
20262
  step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
19734
- // Bootstrap only runs on an account with no registry records, so the
19735
- // manifest-hash pre-read is definitionally a miss. Skip it; the write
19736
- // it guards is an INSERT OR REPLACE and is safe to repeat.
20263
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
20264
+ // repair, so skip the manifest-hash pre-read.
19737
20265
  { assumeUnregistered: true }
19738
20266
  );
19739
20267
  if (!registered.ok) {
@@ -19741,7 +20269,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19741
20269
  }
19742
20270
  }
19743
20271
  if (step.kind === "encryption-network-create") {
19744
- await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
20272
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
19745
20273
  }
19746
20274
  if (step.kind === "secret-records-schema") {
19747
20275
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -20180,6 +20708,14 @@ var _TinyCloudNode = class _TinyCloudNode {
20180
20708
  }
20181
20709
  return hosted;
20182
20710
  }
20711
+ /** Resolve and authenticate the owner encoded by an owned-space URI. */
20712
+ credentialSpaceOwnerDid(spaceId) {
20713
+ const ownerDid = this.ownerDidFromSpaceId(spaceId);
20714
+ if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
20715
+ throw new Error("Credential space owner does not match the active TinyCloud owner");
20716
+ }
20717
+ return ownerDid;
20718
+ }
20183
20719
  /**
20184
20720
  * Check whether an owned space is already registered/hosted by consulting the
20185
20721
  * account spaces registry.
@@ -20723,6 +21259,66 @@ var _TinyCloudNode = class _TinyCloudNode {
20723
21259
  if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
20724
21260
  return signBytesWithJwk(bytes, session.jwk);
20725
21261
  }
21262
+ /** Apply only an already-enabled signing policy to the exact credential request. */
21263
+ async autoSignCredentialBytes(bytes) {
21264
+ const strategy = this.config.signStrategy;
21265
+ if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
21266
+ if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
21267
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21268
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21269
+ const request = {
21270
+ address: this.address ?? "",
21271
+ chainId: this.session?.chainId ?? this._chainId,
21272
+ message: new TextDecoder().decode(bytes),
21273
+ type: "message",
21274
+ purpose: "message"
21275
+ };
21276
+ const decision = await strategy.handler(request);
21277
+ if (!decision.approved) {
21278
+ if (decision.needsApproval) return void 0;
21279
+ throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
21280
+ }
21281
+ if (typeof decision.signature !== "string" || !await (0, import_sdk_core7.verifyEip191MessageSignature)(
21282
+ request.message,
21283
+ decision.signature,
21284
+ request.address
21285
+ )) {
21286
+ throw new Error("OpenKey credential signature evidence is invalid");
21287
+ }
21288
+ return this.signSessionBytes(bytes);
21289
+ }
21290
+ /** Invoke the configured interactive approval strategy before session signing. */
21291
+ async approveCredentialBytes(bytes) {
21292
+ const strategy = this.config.signStrategy;
21293
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21294
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21295
+ const request = {
21296
+ address: this.address ?? "",
21297
+ chainId: this.session?.chainId ?? this._chainId,
21298
+ message: new TextDecoder().decode(bytes),
21299
+ type: "message",
21300
+ purpose: "message"
21301
+ };
21302
+ const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
21303
+ if (approval) {
21304
+ const decision = await approval(request);
21305
+ if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
21306
+ if (decision.signature !== void 0 && !await (0, import_sdk_core7.verifyEip191MessageSignature)(
21307
+ request.message,
21308
+ decision.signature,
21309
+ request.address
21310
+ )) {
21311
+ throw new Error("OpenKey credential signature evidence is invalid");
21312
+ }
21313
+ return this.signSessionBytes(bytes);
21314
+ }
21315
+ if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
21316
+ if (this.config.signer) {
21317
+ await this.config.signer.signMessage(request.message);
21318
+ return this.signSessionBytes(bytes);
21319
+ }
21320
+ throw new Error("Interactive OpenKey approval is not configured");
21321
+ }
20726
21322
  /** Bind a session-signed invocation to the canonical service audience. */
20727
21323
  async bindInvocationAudience(authorization, audience) {
20728
21324
  const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
@@ -20790,7 +21386,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20790
21386
  nonce: this.config.nonce,
20791
21387
  siweConfig: this.config.siweConfig,
20792
21388
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20793
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21389
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20794
21390
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20795
21391
  });
20796
21392
  this.tc = new import_sdk_core7.TinyCloud(this.auth, {
@@ -20843,7 +21439,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20843
21439
  nonce: this.config.nonce,
20844
21440
  siweConfig: this.config.siweConfig,
20845
21441
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20846
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21442
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20847
21443
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20848
21444
  });
20849
21445
  this.tc = new import_sdk_core7.TinyCloud(this.auth, {
@@ -23592,35 +24188,44 @@ var _TinyCloudNode = class _TinyCloudNode {
23592
24188
  if (parentDelegation.disableSubDelegation) {
23593
24189
  throw new Error("Parent delegation does not allow sub-delegation");
23594
24190
  }
23595
- if (!params.path.startsWith(parentDelegation.path)) {
24191
+ const parentResources = parentDelegation.resources ?? [{
24192
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24193
+ space: parentDelegation.spaceId,
24194
+ path: parentDelegation.path,
24195
+ actions: parentDelegation.actions
24196
+ }];
24197
+ const requestedResources = params.resources ?? [{
24198
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24199
+ space: parentDelegation.spaceId,
24200
+ path: params.path,
24201
+ actions: params.actions
24202
+ }];
24203
+ const resourceIsContained = (parent, child) => {
24204
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
24205
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
24206
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
24207
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
24208
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
24209
+ };
24210
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
23596
24211
  throw new Error(
23597
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
24212
+ "Sub-delegation resources exceed the received delegation"
23598
24213
  );
23599
24214
  }
23600
- const parentActions = new Set(parentDelegation.actions);
23601
- for (const action of params.actions) {
23602
- if (!parentActions.has(action)) {
23603
- throw new Error(
23604
- `Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
23605
- );
23606
- }
23607
- }
23608
24215
  const now = /* @__PURE__ */ new Date();
23609
24216
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
23610
24217
  const requestedExpiry = new Date(now.getTime() + expiryMs);
23611
24218
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
23612
24219
  const abilities = {};
23613
- const kvActions = params.actions.filter((a) => a.startsWith("tinycloud.kv/"));
23614
- const sqlActions = params.actions.filter((a) => a.startsWith("tinycloud.sql/"));
23615
- const duckdbActions = params.actions.filter((a) => a.startsWith("tinycloud.duckdb/"));
23616
- if (kvActions.length > 0) {
23617
- abilities.kv = { [params.path]: kvActions };
23618
- }
23619
- if (sqlActions.length > 0) {
23620
- abilities.sql = { [params.path]: sqlActions };
23621
- }
23622
- if (duckdbActions.length > 0) {
23623
- abilities.duckdb = { [params.path]: duckdbActions };
24220
+ const rawAbilities = {};
24221
+ for (const resource of requestedResources) {
24222
+ const service = resource.service.replace(/^tinycloud\./, "");
24223
+ if (service === "encryption") {
24224
+ rawAbilities[resource.path] = [...resource.actions];
24225
+ continue;
24226
+ }
24227
+ abilities[service] ?? (abilities[service] = {});
24228
+ abilities[service][resource.path] = [...resource.actions];
23624
24229
  }
23625
24230
  const targetHost = parentDelegation.host ?? this.config.host;
23626
24231
  const prepared = this.wasmBindings.prepareSession({
@@ -23632,7 +24237,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23632
24237
  expirationTime: actualExpiry.toISOString(),
23633
24238
  spaceId: parentDelegation.spaceId,
23634
24239
  delegateUri: params.delegateDID,
23635
- parents: [parentDelegation.cid]
24240
+ parents: [parentDelegation.cid],
24241
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
23636
24242
  });
23637
24243
  const signature2 = await this.signer.signMessage(prepared.siwe);
23638
24244
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -23650,14 +24256,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23650
24256
  cid: subDelegationSession.delegationCid,
23651
24257
  delegationHeader: subDelegationSession.delegationHeader,
23652
24258
  spaceId: parentDelegation.spaceId,
23653
- path: params.path,
23654
- actions: params.actions,
24259
+ path: requestedResources[0].path,
24260
+ actions: requestedResources[0].actions,
23655
24261
  disableSubDelegation: params.disableSubDelegation ?? false,
23656
24262
  expiry: actualExpiry,
23657
24263
  delegateDID: params.delegateDID,
23658
24264
  ownerAddress: parentDelegation.ownerAddress,
23659
24265
  chainId: parentDelegation.chainId,
23660
- host: targetHost
24266
+ host: targetHost,
24267
+ resources: requestedResources.map((resource) => ({
24268
+ service: resource.service.replace(/^tinycloud\./, ""),
24269
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
24270
+ path: resource.path,
24271
+ actions: [...resource.actions]
24272
+ }))
23661
24273
  };
23662
24274
  }
23663
24275
  };
@@ -23804,6 +24416,50 @@ var FileSessionStorage = class {
23804
24416
  }
23805
24417
  };
23806
24418
 
24419
+ // src/authorization/openKeyBridge.ts
24420
+ function wireOpenKeyAuthorize(openkey) {
24421
+ return async (input) => {
24422
+ const result = await openkey.authorizeTinyCloud({
24423
+ protocolVersion: 1,
24424
+ siwe: input.siwe,
24425
+ keyId: input.keyId,
24426
+ jwk: input.jwk,
24427
+ host: input.host,
24428
+ // Forward the presentation envelope verbatim — the OpenKey SDK
24429
+ // validates and size-bounds it before use.
24430
+ presentation: input.presentation
24431
+ });
24432
+ if (result.protocolVersion !== 1) {
24433
+ throw new Error(
24434
+ `wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
24435
+ );
24436
+ }
24437
+ if (typeof result.signedMessage !== "string" || !result.signedMessage) {
24438
+ throw new Error(
24439
+ "wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
24440
+ );
24441
+ }
24442
+ if (!Array.isArray(result.selectedActionKeys)) {
24443
+ throw new Error(
24444
+ "wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
24445
+ );
24446
+ }
24447
+ if (!Array.isArray(result.permissions)) {
24448
+ throw new Error(
24449
+ "wireOpenKeyAuthorize: OpenKey returned no permissions"
24450
+ );
24451
+ }
24452
+ return {
24453
+ protocolVersion: 1,
24454
+ address: result.address,
24455
+ signature: result.signature,
24456
+ signedMessage: result.signedMessage,
24457
+ selectedActionKeys: result.selectedActionKeys,
24458
+ permissions: result.permissions
24459
+ };
24460
+ };
24461
+ }
24462
+
23807
24463
  // src/index.ts
23808
24464
  var import_sdk_core12 = require("@tinycloud/sdk-core");
23809
24465
 
@@ -23892,6 +24548,14 @@ function signedAuthorityFromCompactUcan(authorization) {
23892
24548
  if (typeof payload.aud !== "string" || payload.aud.length === 0) {
23893
24549
  throw new Error("Validated runtime delegation is missing a signed audience.");
23894
24550
  }
24551
+ if (typeof payload.iss !== "string" || payload.iss.length === 0) {
24552
+ throw new Error("Validated runtime delegation is missing a signed issuer.");
24553
+ }
24554
+ if (!Array.isArray(payload.prf) || payload.prf.some(
24555
+ (proof) => typeof proof !== "string" || proof.length === 0
24556
+ )) {
24557
+ throw new Error("Validated runtime delegation has malformed signed proofs.");
24558
+ }
23895
24559
  if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
23896
24560
  throw new Error("Validated runtime delegation is missing a signed expiry.");
23897
24561
  }
@@ -23902,6 +24566,11 @@ function signedAuthorityFromCompactUcan(authorization) {
23902
24566
  if (payload.att === null || typeof payload.att !== "object" || Array.isArray(payload.att)) {
23903
24567
  throw new Error("Validated runtime delegation is missing a signed attenuation.");
23904
24568
  }
24569
+ 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;
24570
+ const remainingRedelegationDepth = fact?.remainingRedelegationDepth;
24571
+ if (remainingRedelegationDepth !== void 0 && (!Number.isInteger(remainingRedelegationDepth) || remainingRedelegationDepth < 0 || remainingRedelegationDepth > 8)) {
24572
+ throw new Error("Validated runtime delegation has invalid signed redelegation depth.");
24573
+ }
23905
24574
  const permissions = [];
23906
24575
  for (const [resource, abilities] of Object.entries(payload.att)) {
23907
24576
  if (abilities === null || typeof abilities !== "object" || Array.isArray(abilities)) {
@@ -23952,8 +24621,11 @@ function signedAuthorityFromCompactUcan(authorization) {
23952
24621
  ...permission.caveats === void 0 ? {} : { caveats: permission.caveats }
23953
24622
  }));
23954
24623
  return {
24624
+ issuer: payload.iss,
23955
24625
  audience: payload.aud,
23956
24626
  expiry,
24627
+ proofs: [...payload.prf],
24628
+ ...remainingRedelegationDepth === void 0 ? {} : { remainingRedelegationDepth },
23957
24629
  permissions: canonicalPermissions,
23958
24630
  resources: canonicalResources
23959
24631
  };
@@ -24044,6 +24716,13 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
24044
24716
  delegationHeader: { Authorization: authorization },
24045
24717
  ownerAddress: delegation.ownerAddress,
24046
24718
  chainId: delegation.chainId,
24719
+ delegatorDID: signed2.issuer,
24720
+ ...signed2.proofs.length === 1 ? { parentCid: signed2.proofs[0] } : {},
24721
+ ...signed2.remainingRedelegationDepth === void 0 ? {} : {
24722
+ allowSubDelegation: signed2.remainingRedelegationDepth > 0,
24723
+ disableSubDelegation: signed2.remainingRedelegationDepth === 0
24724
+ },
24725
+ authHeader: authorization,
24047
24726
  spaceId: signedSpace ?? delegation.spaceId,
24048
24727
  path: primary.path,
24049
24728
  actions: [...primary.actions],
@@ -24137,10 +24816,13 @@ var import_sdk_core22 = require("@tinycloud/sdk-core");
24137
24816
  var import_sdk_core23 = require("@tinycloud/sdk-core");
24138
24817
  // Annotate the CommonJS export names for ESM import in node:
24139
24818
  0 && (module.exports = {
24819
+ ACCEPTED_MARKER_VERSIONS,
24140
24820
  ACCOUNT_REGISTRY_PATH,
24141
24821
  ACCOUNT_REGISTRY_SPACE,
24142
24822
  AccountService,
24143
24823
  AutoApproveSpaceCreationHandler,
24824
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
24825
+ BOOTSTRAP_COMPLETION_MARKER_VERSION,
24144
24826
  CapabilityKeyRegistry,
24145
24827
  CapabilityKeyRegistryErrorCodes,
24146
24828
  CaveatedDelegationUnsupportedError,
@@ -24277,7 +24959,8 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
24277
24959
  tinyCloudDebugLogger,
24278
24960
  validateEnvelope,
24279
24961
  validateManifest,
24280
- verifyDecryptResponse
24962
+ verifyDecryptResponse,
24963
+ wireOpenKeyAuthorize
24281
24964
  });
24282
24965
  /*! Bundled license information:
24283
24966