@tinycloud/node-sdk 2.11.0-beta.1 → 2.11.0-beta.11

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";
@@ -17578,6 +17615,15 @@ var NodeUserAuthorization = class {
17578
17615
  get nodeFeatures() {
17579
17616
  return this._nodeFeatures;
17580
17617
  }
17618
+ /**
17619
+ * The node DID reported by `GET /info` for `host`, if the sign-in (or
17620
+ * restore) that populated it targeted that same host. Returns `undefined`
17621
+ * for any other host, and for nodes whose `/info` omits `nodeId`, so callers
17622
+ * fall back to fetching it themselves rather than using a foreign node's DID.
17623
+ */
17624
+ nodeIdForHost(host) {
17625
+ return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
17626
+ }
17581
17627
  get lastActivationSkippedSpaceIds() {
17582
17628
  return [...this._lastActivationSkippedSpaceIds];
17583
17629
  }
@@ -17637,23 +17683,41 @@ var NodeUserAuthorization = class {
17637
17683
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
17638
17684
  const primarySpaceId2 = (0, import_sdk_core2.makePkhSpaceId)(address, chainId, this.spacePrefix);
17639
17685
  const secretsSpaceId = (0, import_sdk_core2.makePkhSpaceId)(address, chainId, "secrets");
17640
- return {
17641
- abilities: this.defaultActions,
17642
- spaceId: primarySpaceId2,
17643
- spaceAbilities: {
17644
- [primarySpaceId2]: this.defaultActions,
17645
- [secretsSpaceId]: {
17646
- kv: {
17647
- "vault/secrets/": [
17648
- import_sdk_core2.KV.GET,
17649
- import_sdk_core2.KV.PUT,
17650
- import_sdk_core2.KV.DEL,
17651
- import_sdk_core2.KV.LIST,
17652
- import_sdk_core2.KV.METADATA
17653
- ]
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);
17654
17713
  }
17655
17714
  }
17656
- },
17715
+ }
17716
+ }
17717
+ return {
17718
+ abilities: primaryActions,
17719
+ spaceId: primarySpaceId2,
17720
+ spaceAbilities: spaceAbilities2,
17657
17721
  rawAbilities: {
17658
17722
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
17659
17723
  }
@@ -18047,11 +18111,13 @@ var NodeUserAuthorization = class {
18047
18111
  this._tinyCloudSession = tinyCloudSession;
18048
18112
  this._address = address;
18049
18113
  this._chainId = chainId;
18114
+ const infoHost = this.primaryTinyCloudHost;
18050
18115
  const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
18051
- this.primaryTinyCloudHost,
18116
+ infoHost,
18052
18117
  this.wasm.protocolVersion()
18053
18118
  );
18054
18119
  this._nodeFeatures = nodeInfo.features;
18120
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18055
18121
  for (const ext of this.extensions) {
18056
18122
  if (ext.afterSignIn) {
18057
18123
  await ext.afterSignIn(clientSession);
@@ -18217,11 +18283,13 @@ var NodeUserAuthorization = class {
18217
18283
  this._tinyCloudSession = tinyCloudSession;
18218
18284
  this._address = address;
18219
18285
  this._chainId = chainId;
18286
+ const infoHost = this.primaryTinyCloudHost;
18220
18287
  const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
18221
- this.primaryTinyCloudHost,
18288
+ infoHost,
18222
18289
  this.wasm.protocolVersion()
18223
18290
  );
18224
18291
  this._nodeFeatures = nodeInfo.features;
18292
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18225
18293
  for (const ext of this.extensions) {
18226
18294
  if (ext.afterSignIn) {
18227
18295
  await ext.afterSignIn(clientSession);
@@ -18230,6 +18298,398 @@ var NodeUserAuthorization = class {
18230
18298
  await this.ensureSpaceExists();
18231
18299
  return clientSession;
18232
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
+ }
18233
18693
  /**
18234
18694
  * Clear persisted session data.
18235
18695
  */
@@ -18720,6 +19180,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
18720
19180
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
18721
19181
  var NETWORK_CREATE_ACTION2 = import_sdk_core7.ENCRYPTION.NETWORK_CREATE;
18722
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];
18723
19186
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
18724
19187
  function isSecretPayload(value) {
18725
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";
@@ -19030,7 +19493,9 @@ function base64UrlDecode(value) {
19030
19493
  return new Uint8Array(bytes);
19031
19494
  }
19032
19495
  async function signJwtInputWithJwk(signingInput, jwk) {
19033
- const bytes = new TextEncoder().encode(signingInput);
19496
+ return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
19497
+ }
19498
+ async function signBytesWithJwk(bytes, jwk) {
19034
19499
  try {
19035
19500
  const subtle = globalThis.crypto?.subtle;
19036
19501
  if (!subtle) {
@@ -19111,6 +19576,12 @@ var _TinyCloudNode = class _TinyCloudNode {
19111
19576
  this._baseSecrets = /* @__PURE__ */ new Map();
19112
19577
  this._secrets = /* @__PURE__ */ new Map();
19113
19578
  this.runtimePermissionGrants = [];
19579
+ /**
19580
+ * Owned space ids this sign-in has already confirmed are hosted. Consulted by
19581
+ * {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
19582
+ * hosting is confirmed against the session that was active at the time.
19583
+ */
19584
+ this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
19114
19585
  /**
19115
19586
  * True when the last signIn() detected an interactive signer and skipped
19116
19587
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -19119,9 +19590,15 @@ var _TinyCloudNode = class _TinyCloudNode {
19119
19590
  this._bootstrapSkipped = false;
19120
19591
  /**
19121
19592
  * Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
19122
- * true when bootstrap did not complete (interactive signer, auto-sign
19123
- * denied, or a bootstrap step failed); `reason` carries the cause so apps
19124
- * can surface a "finish account setup" call-to-action.
19593
+ * true when bootstrap did not complete in this invocation (interactive signer,
19594
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
19595
+ * step failed). `already-provisioned` means the bootstrap was not run here;
19596
+ * it does not mean a prior provisioning attempt failed to complete.
19597
+ * `reason` carries the cause so apps can surface a "finish account setup"
19598
+ * call-to-action. `warnings` is
19599
+ * present when bootstrap completed, but one or more steps recovered from
19600
+ * an ambiguous failure (e.g. a KV batch write reconciled via per-space
19601
+ * fallback) — clean vs. recovered runs are otherwise byte-identical.
19125
19602
  */
19126
19603
  this._bootstrapStatus = {
19127
19604
  skipped: false
@@ -19301,7 +19778,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19301
19778
  nonce: config.nonce,
19302
19779
  siweConfig: config.siweConfig,
19303
19780
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
19304
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19781
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19305
19782
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
19306
19783
  });
19307
19784
  this.tc = new import_sdk_core7.TinyCloud(this.auth, {
@@ -19507,6 +19984,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19507
19984
  this._spaceService = void 0;
19508
19985
  this._serviceContext = void 0;
19509
19986
  this.runtimePermissionGrants = [];
19987
+ this.confirmedHostedSpaceIds.clear();
19510
19988
  await this.tc.signIn(options);
19511
19989
  this.syncResolvedHostFromAuth();
19512
19990
  const oldGraph = this._serviceGraph;
@@ -19543,6 +20021,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19543
20021
  this._bootstrapSkipped = false;
19544
20022
  this._bootstrapStatus = { skipped: false };
19545
20023
  if (this.config.autoBootstrapAccount === false) {
20024
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
19546
20025
  return false;
19547
20026
  }
19548
20027
  if (!this.auth || !this._address) {
@@ -19557,11 +20036,20 @@ var _TinyCloudNode = class _TinyCloudNode {
19557
20036
  return false;
19558
20037
  }
19559
20038
  const steps = (0, import_sdk_core7.bootstrapSteps)(this._address, this._chainId);
19560
- if (!await this.isFreshBootstrapAccount(steps)) {
19561
- return false;
19562
- }
19563
20039
  try {
19564
- await this.runAccountBootstrap(steps);
20040
+ const decision = await this.resolveBootstrapDecision(steps);
20041
+ if (decision.action === "skip") {
20042
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
20043
+ return false;
20044
+ }
20045
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
20046
+ await this.writeBootstrapCompletionMarker(steps);
20047
+ if (warnings.length > 0) {
20048
+ this._bootstrapStatus = { skipped: false, warnings };
20049
+ this.notificationHandler.warning(
20050
+ `Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
20051
+ );
20052
+ }
19565
20053
  } catch (err) {
19566
20054
  const reason = err instanceof Error ? err.message : String(err);
19567
20055
  this._bootstrapSkipped = true;
@@ -19574,32 +20062,92 @@ var _TinyCloudNode = class _TinyCloudNode {
19574
20062
  }
19575
20063
  return true;
19576
20064
  }
19577
- async isFreshBootstrapAccount(steps) {
20065
+ async resolveBootstrapDecision(steps) {
19578
20066
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
19579
20067
  for (const step of steps) {
19580
20068
  if (step.kind === "session") {
19581
20069
  enshrinedSpaceIds.add(step.spaceId);
19582
20070
  }
19583
20071
  }
19584
- const skipped = this.auth.lastActivationSkippedSpaceIds;
20072
+ const auth = this.auth;
20073
+ if (!auth) {
20074
+ throw new Error("Account bootstrap requires an active wallet session");
20075
+ }
20076
+ const skipped = auth.lastActivationSkippedSpaceIds;
19585
20077
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
19586
- return true;
20078
+ return { action: "run", mode: "fresh" };
19587
20079
  }
19588
20080
  try {
19589
- const indexed = await this.account.index.spaces.list();
19590
- if (indexed.ok && indexed.data.length === 0) {
19591
- return true;
20081
+ const accountSpaceId = this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE);
20082
+ const markerPermission = {
20083
+ service: "tinycloud.kv",
20084
+ space: accountSpaceId,
20085
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
20086
+ actions: ["get"]
20087
+ };
20088
+ if (!this.hasRuntimePermissions([markerPermission])) {
20089
+ return { action: "run", mode: "repair" };
19592
20090
  }
19593
- } catch {
20091
+ const marker = await this.readBootstrapCompletionMarker();
20092
+ if (marker.ok) {
20093
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
20094
+ return { action: "skip" };
20095
+ }
20096
+ return { action: "run", mode: "repair" };
20097
+ }
20098
+ if (marker.error.code === import_sdk_core7.ErrorCodes.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
20099
+ return { action: "run", mode: "fresh" };
20100
+ }
20101
+ } catch (err) {
20102
+ const reason = err instanceof Error ? err.message : String(err);
20103
+ this.notificationHandler.warning(
20104
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
20105
+ );
20106
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
20107
+ return { action: "run", mode: "repair" };
19594
20108
  }
19595
- try {
19596
- const spaces = await this.account.spaces.list();
19597
- return spaces.ok && spaces.data.length === 0;
19598
- } catch {
19599
- return false;
20109
+ return { action: "run", mode: "repair" };
20110
+ }
20111
+ async readBootstrapCompletionMarker() {
20112
+ return this.kvForSpace(this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE)).get(
20113
+ BOOTSTRAP_COMPLETION_MARKER_KEY
20114
+ );
20115
+ }
20116
+ async writeBootstrapCompletionMarker(steps) {
20117
+ if (!this._address) {
20118
+ throw new Error("Bootstrap completion marker requires an active wallet session");
19600
20119
  }
20120
+ const canonicalStepIds = new Set(
20121
+ (0, import_sdk_core7.bootstrapSteps)(this._address, this._chainId).map((step) => step.id)
20122
+ );
20123
+ const providedStepIds = new Set(steps.map((step) => step.id));
20124
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
20125
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
20126
+ }
20127
+ const marker = {
20128
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
20129
+ stepIds: [...canonicalStepIds],
20130
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
20131
+ };
20132
+ const written = await this.kvForSpace(this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE)).put(
20133
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
20134
+ marker
20135
+ );
20136
+ if (!written.ok) {
20137
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
20138
+ }
20139
+ }
20140
+ isAcceptedBootstrapCompletionMarker(value) {
20141
+ if (typeof value !== "object" || value === null) return false;
20142
+ const entries = Object.entries(value);
20143
+ const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
20144
+ const version23 = markerValue("v");
20145
+ return typeof version23 === "number" && Number.isInteger(version23) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version23);
19601
20146
  }
19602
- async runAccountBootstrap(steps) {
20147
+ markerReadIsUnhostedSpace(meta) {
20148
+ return meta?.status === 404;
20149
+ }
20150
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
19603
20151
  if (!this.auth || !this._address) {
19604
20152
  throw new Error("Account bootstrap requires an active wallet session");
19605
20153
  }
@@ -19610,6 +20158,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19610
20158
  const auth = this.auth;
19611
20159
  const sessions = /* @__PURE__ */ new Map();
19612
20160
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
20161
+ const warnings = [];
19613
20162
  const primarySession = auth.tinyCloudSession;
19614
20163
  const defaultSpaceId = this.ownedSpaceId("default");
19615
20164
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default;
@@ -19671,32 +20220,44 @@ var _TinyCloudNode = class _TinyCloudNode {
19671
20220
  }
19672
20221
  }
19673
20222
  if (step.kind === "seed-spaces") {
19674
- for (const space of step.spaces) {
19675
- const registered = await this.account.spaces.register({
20223
+ const registered = await this.account.spaces.registerBatch(
20224
+ step.spaces.map((space) => ({
19676
20225
  spaceId: space.spaceId,
19677
20226
  name: space.name,
19678
20227
  ownerDid: this.did,
19679
20228
  type: "owned",
19680
20229
  permissions: ["*"],
19681
20230
  status: "active"
20231
+ }))
20232
+ );
20233
+ if (!registered.ok) {
20234
+ throw new Error(
20235
+ `Failed to seed account spaces: ${registered.error.message}`
20236
+ );
20237
+ }
20238
+ const batchError = registered.data.recoveredFromBatchError;
20239
+ if (batchError) {
20240
+ warnings.push({
20241
+ stepId: step.id,
20242
+ kind: "batch-write-reconciled",
20243
+ code: batchError.code,
20244
+ message: batchError.message
19682
20245
  });
19683
- if (!registered.ok) {
19684
- throw new Error(
19685
- `Failed to seed account space ${space.spaceId}: ${registered.error.message}`
19686
- );
19687
- }
19688
20246
  }
19689
20247
  }
19690
20248
  if (step.kind === "seed-applications") {
19691
20249
  const registered = await this.account.applications.register(
19692
- step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
20250
+ step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
20251
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
20252
+ // repair, so skip the manifest-hash pre-read.
20253
+ { assumeUnregistered: true }
19693
20254
  );
19694
20255
  if (!registered.ok) {
19695
20256
  throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
19696
20257
  }
19697
20258
  }
19698
20259
  if (step.kind === "encryption-network-create") {
19699
- await this.ensureEncryptionNetwork(step.networkId);
20260
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
19700
20261
  }
19701
20262
  if (step.kind === "secret-records-schema") {
19702
20263
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -19716,6 +20277,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19716
20277
  }
19717
20278
  }
19718
20279
  }
20280
+ return warnings;
19719
20281
  }
19720
20282
  registerBootstrapRuntimeGrant(session, request, rawAbilities) {
19721
20283
  const operations = [];
@@ -19975,6 +20537,22 @@ var _TinyCloudNode = class _TinyCloudNode {
19975
20537
  await this.ensureEncryptionNetwork(networkId);
19976
20538
  }
19977
20539
  }
20540
+ /**
20541
+ * Ensure one of this user's owned spaces is hosted, at most once per space
20542
+ * per sign-in.
20543
+ *
20544
+ * Account bootstrap funnels six calls through here (via
20545
+ * `AccountService.ensureAccountSpaceHosted`), and each one re-submits
20546
+ * `activateSessionWithHost` with the *primary* session. That session's recap
20547
+ * covers only `default` and `secrets`, so the account space appears in
20548
+ * neither `activated` nor `skipped` and the guard below returns immediately
20549
+ * — the repeat calls cannot change the outcome, they only cost round trips.
20550
+ *
20551
+ * The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
20552
+ * `signIn`) and only records confirmed successes. A wrong skip is
20553
+ * self-revealing: the next write to the space fails immediately and loudly
20554
+ * with `404 Space not found`.
20555
+ */
19978
20556
  async ensureOwnedSpaceHostedById(spaceId) {
19979
20557
  if (!this.auth) {
19980
20558
  throw new Error("Owned space hosting requires wallet mode");
@@ -19983,12 +20561,16 @@ var _TinyCloudNode = class _TinyCloudNode {
19983
20561
  if (!session) {
19984
20562
  throw new Error("Owned space hosting requires an active session");
19985
20563
  }
20564
+ if (this.confirmedHostedSpaceIds.has(spaceId)) {
20565
+ return;
20566
+ }
19986
20567
  const host = this.hosts[0] ?? this.config.host;
19987
20568
  if (!host) {
19988
20569
  throw new Error("Owned space hosting requires a TinyCloud host");
19989
20570
  }
19990
20571
  const activation = await (0, import_sdk_core7.activateSessionWithHost)(host, session.delegationHeader);
19991
20572
  if (activation.success && !activation.skipped?.includes(spaceId)) {
20573
+ this.confirmedHostedSpaceIds.add(spaceId);
19992
20574
  return;
19993
20575
  }
19994
20576
  if (!activation.success && activation.status !== 404) {
@@ -20007,6 +20589,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20007
20589
  `Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
20008
20590
  );
20009
20591
  }
20592
+ this.confirmedHostedSpaceIds.add(spaceId);
20010
20593
  }
20011
20594
  /**
20012
20595
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -20322,6 +20905,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20322
20905
  this._delegationManager = stagedGraph.delegationManager;
20323
20906
  this._spaceService = stagedGraph.spaceService;
20324
20907
  this._serviceGraph = stagedGraph.graph;
20908
+ this._activeServiceSession = serviceSession;
20325
20909
  this._baseSecrets = /* @__PURE__ */ new Map();
20326
20910
  this._secrets = /* @__PURE__ */ new Map();
20327
20911
  this._publicKV = void 0;
@@ -20329,6 +20913,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20329
20913
  this.tc = stagedGraph.core;
20330
20914
  this._recapOperationsCache = stagedPrimary.cache;
20331
20915
  this.runtimePermissionGrants = stagedPrimary.grants;
20916
+ this.confirmedHostedSpaceIds.clear();
20332
20917
  if (this.auth) {
20333
20918
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
20334
20919
  this._restoredTcSession = void 0;
@@ -20638,6 +21223,28 @@ var _TinyCloudNode = class _TinyCloudNode {
20638
21223
  currentTinyCloudSession() {
20639
21224
  return this.auth?.tinyCloudSession ?? this._restoredTcSession;
20640
21225
  }
21226
+ /**
21227
+ * Mint a multi-capability invocation through the established session key.
21228
+ * Callers receive only the signed headers; session key material remains
21229
+ * owned by the session manager and is never part of the caller contract.
21230
+ */
21231
+ invokeAny(entries, facts) {
21232
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21233
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21234
+ return this._serviceGraph.invokeAny(session, entries, facts);
21235
+ }
21236
+ /** Sign protocol bytes with the established in-memory session key. */
21237
+ async signSessionBytes(bytes) {
21238
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21239
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21240
+ return signBytesWithJwk(bytes, session.jwk);
21241
+ }
21242
+ /** Bind a session-signed invocation to the canonical service audience. */
21243
+ async bindInvocationAudience(authorization, audience) {
21244
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21245
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21246
+ return rewriteInvocationAudience(authorization, audience, session.jwk);
21247
+ }
20641
21248
  /**
20642
21249
  * Connect a wallet to upgrade from session-only mode to wallet mode.
20643
21250
  *
@@ -20699,7 +21306,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20699
21306
  nonce: this.config.nonce,
20700
21307
  siweConfig: this.config.siweConfig,
20701
21308
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20702
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21309
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20703
21310
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20704
21311
  });
20705
21312
  this.tc = new import_sdk_core7.TinyCloud(this.auth, {
@@ -20752,7 +21359,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20752
21359
  nonce: this.config.nonce,
20753
21360
  siweConfig: this.config.siweConfig,
20754
21361
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20755
- capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21362
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20756
21363
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20757
21364
  });
20758
21365
  this.tc = new import_sdk_core7.TinyCloud(this.auth, {
@@ -20819,6 +21426,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20819
21426
  if (this._serviceContext) {
20820
21427
  const spaceScopedContext = this._serviceGraph.track(new import_sdk_core7.ServiceContext({
20821
21428
  invoke: this._serviceContext.invoke,
21429
+ invokeAny: this._serviceContext.invokeAny,
20822
21430
  fetch: this._serviceContext.fetch,
20823
21431
  hosts: this._serviceContext.hosts,
20824
21432
  telemetry: this.config.telemetry
@@ -20911,7 +21519,17 @@ var _TinyCloudNode = class _TinyCloudNode {
20911
21519
  verifyNodeSignature: (nodeId, message, signature2) => (0, import_sdk_core7.verifyDidKeyEd25519Signature)(nodeId, message, signature2)
20912
21520
  };
20913
21521
  }
21522
+ /**
21523
+ * The node DID for {@link config.host}.
21524
+ *
21525
+ * Sign-in already performs `GET /info` for the protocol-version check, and
21526
+ * that response carries `nodeId`, so reuse it rather than fetching `/info` a
21527
+ * second time. Only a value recorded for this exact host qualifies; older
21528
+ * nodes that omit `nodeId` fall through to the fetch.
21529
+ */
20914
21530
  async fetchNodeId() {
21531
+ const cached = this.auth?.nodeIdForHost(this.config.host);
21532
+ if (cached) return cached;
20915
21533
  const response = await fetch(`${this.config.host}/info`);
20916
21534
  if (!response.ok) {
20917
21535
  throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
@@ -21474,6 +22092,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21474
22092
  shareUrl: input.shareUrl,
21475
22093
  documentName: input.documentName,
21476
22094
  jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
22095
+ idempotencyKey: input.idempotencyKey,
21477
22096
  expiresAt: input.expiresAt
21478
22097
  };
21479
22098
  const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core7.canonicalizeEncryptionJson)(body)))));
@@ -21683,6 +22302,13 @@ var _TinyCloudNode = class _TinyCloudNode {
21683
22302
  body
21684
22303
  )
21685
22304
  });
22305
+ if (response.status === 409) {
22306
+ const existing = await this.getEncryptionNetwork(networkId);
22307
+ if (existing) return existing;
22308
+ throw new Error(
22309
+ `Encryption network ${networkId} already exists but could not be read back`
22310
+ );
22311
+ }
21686
22312
  if (!response.ok) {
21687
22313
  throw new Error(
21688
22314
  `Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
@@ -21691,11 +22317,19 @@ var _TinyCloudNode = class _TinyCloudNode {
21691
22317
  const created = await response.json();
21692
22318
  return created.descriptor;
21693
22319
  }
21694
- async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME) {
22320
+ /**
22321
+ * Ensure an encryption network exists, creating it if necessary.
22322
+ *
22323
+ * @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
22324
+ * @param options - See {@link EnsureEncryptionNetworkOptions}.
22325
+ */
22326
+ async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
21695
22327
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
21696
- const existing = await this.getEncryptionNetwork(networkId);
21697
- if (existing) {
21698
- return existing;
22328
+ if (!options.assumeMissing) {
22329
+ const existing = await this.getEncryptionNetwork(networkId);
22330
+ if (existing) {
22331
+ return existing;
22332
+ }
21699
22333
  }
21700
22334
  const parsed = (0, import_sdk_core7.parseNetworkId)(networkId);
21701
22335
  if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
@@ -23474,35 +24108,44 @@ var _TinyCloudNode = class _TinyCloudNode {
23474
24108
  if (parentDelegation.disableSubDelegation) {
23475
24109
  throw new Error("Parent delegation does not allow sub-delegation");
23476
24110
  }
23477
- if (!params.path.startsWith(parentDelegation.path)) {
24111
+ const parentResources = parentDelegation.resources ?? [{
24112
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24113
+ space: parentDelegation.spaceId,
24114
+ path: parentDelegation.path,
24115
+ actions: parentDelegation.actions
24116
+ }];
24117
+ const requestedResources = params.resources ?? [{
24118
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24119
+ space: parentDelegation.spaceId,
24120
+ path: params.path,
24121
+ actions: params.actions
24122
+ }];
24123
+ const resourceIsContained = (parent, child) => {
24124
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
24125
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
24126
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
24127
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
24128
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
24129
+ };
24130
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
23478
24131
  throw new Error(
23479
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
24132
+ "Sub-delegation resources exceed the received delegation"
23480
24133
  );
23481
24134
  }
23482
- const parentActions = new Set(parentDelegation.actions);
23483
- for (const action of params.actions) {
23484
- if (!parentActions.has(action)) {
23485
- throw new Error(
23486
- `Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
23487
- );
23488
- }
23489
- }
23490
24135
  const now = /* @__PURE__ */ new Date();
23491
24136
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
23492
24137
  const requestedExpiry = new Date(now.getTime() + expiryMs);
23493
24138
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
23494
24139
  const abilities = {};
23495
- const kvActions = params.actions.filter((a) => a.startsWith("tinycloud.kv/"));
23496
- const sqlActions = params.actions.filter((a) => a.startsWith("tinycloud.sql/"));
23497
- const duckdbActions = params.actions.filter((a) => a.startsWith("tinycloud.duckdb/"));
23498
- if (kvActions.length > 0) {
23499
- abilities.kv = { [params.path]: kvActions };
23500
- }
23501
- if (sqlActions.length > 0) {
23502
- abilities.sql = { [params.path]: sqlActions };
23503
- }
23504
- if (duckdbActions.length > 0) {
23505
- abilities.duckdb = { [params.path]: duckdbActions };
24140
+ const rawAbilities = {};
24141
+ for (const resource of requestedResources) {
24142
+ const service = resource.service.replace(/^tinycloud\./, "");
24143
+ if (service === "encryption") {
24144
+ rawAbilities[resource.path] = [...resource.actions];
24145
+ continue;
24146
+ }
24147
+ abilities[service] ?? (abilities[service] = {});
24148
+ abilities[service][resource.path] = [...resource.actions];
23506
24149
  }
23507
24150
  const targetHost = parentDelegation.host ?? this.config.host;
23508
24151
  const prepared = this.wasmBindings.prepareSession({
@@ -23514,7 +24157,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23514
24157
  expirationTime: actualExpiry.toISOString(),
23515
24158
  spaceId: parentDelegation.spaceId,
23516
24159
  delegateUri: params.delegateDID,
23517
- parents: [parentDelegation.cid]
24160
+ parents: [parentDelegation.cid],
24161
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
23518
24162
  });
23519
24163
  const signature2 = await this.signer.signMessage(prepared.siwe);
23520
24164
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -23532,14 +24176,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23532
24176
  cid: subDelegationSession.delegationCid,
23533
24177
  delegationHeader: subDelegationSession.delegationHeader,
23534
24178
  spaceId: parentDelegation.spaceId,
23535
- path: params.path,
23536
- actions: params.actions,
24179
+ path: requestedResources[0].path,
24180
+ actions: requestedResources[0].actions,
23537
24181
  disableSubDelegation: params.disableSubDelegation ?? false,
23538
24182
  expiry: actualExpiry,
23539
24183
  delegateDID: params.delegateDID,
23540
24184
  ownerAddress: parentDelegation.ownerAddress,
23541
24185
  chainId: parentDelegation.chainId,
23542
- host: targetHost
24186
+ host: targetHost,
24187
+ resources: requestedResources.map((resource) => ({
24188
+ service: resource.service.replace(/^tinycloud\./, ""),
24189
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
24190
+ path: resource.path,
24191
+ actions: [...resource.actions]
24192
+ }))
23543
24193
  };
23544
24194
  }
23545
24195
  };
@@ -23686,6 +24336,50 @@ var FileSessionStorage = class {
23686
24336
  }
23687
24337
  };
23688
24338
 
24339
+ // src/authorization/openKeyBridge.ts
24340
+ function wireOpenKeyAuthorize(openkey) {
24341
+ return async (input) => {
24342
+ const result = await openkey.authorizeTinyCloud({
24343
+ protocolVersion: 1,
24344
+ siwe: input.siwe,
24345
+ keyId: input.keyId,
24346
+ jwk: input.jwk,
24347
+ host: input.host,
24348
+ // Forward the presentation envelope verbatim — the OpenKey SDK
24349
+ // validates and size-bounds it before use.
24350
+ presentation: input.presentation
24351
+ });
24352
+ if (result.protocolVersion !== 1) {
24353
+ throw new Error(
24354
+ `wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
24355
+ );
24356
+ }
24357
+ if (typeof result.signedMessage !== "string" || !result.signedMessage) {
24358
+ throw new Error(
24359
+ "wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
24360
+ );
24361
+ }
24362
+ if (!Array.isArray(result.selectedActionKeys)) {
24363
+ throw new Error(
24364
+ "wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
24365
+ );
24366
+ }
24367
+ if (!Array.isArray(result.permissions)) {
24368
+ throw new Error(
24369
+ "wireOpenKeyAuthorize: OpenKey returned no permissions"
24370
+ );
24371
+ }
24372
+ return {
24373
+ protocolVersion: 1,
24374
+ address: result.address,
24375
+ signature: result.signature,
24376
+ signedMessage: result.signedMessage,
24377
+ selectedActionKeys: result.selectedActionKeys,
24378
+ permissions: result.permissions
24379
+ };
24380
+ };
24381
+ }
24382
+
23689
24383
  // src/index.ts
23690
24384
  var import_sdk_core12 = require("@tinycloud/sdk-core");
23691
24385
 
@@ -24019,10 +24713,13 @@ var import_sdk_core22 = require("@tinycloud/sdk-core");
24019
24713
  var import_sdk_core23 = require("@tinycloud/sdk-core");
24020
24714
  // Annotate the CommonJS export names for ESM import in node:
24021
24715
  0 && (module.exports = {
24716
+ ACCEPTED_MARKER_VERSIONS,
24022
24717
  ACCOUNT_REGISTRY_PATH,
24023
24718
  ACCOUNT_REGISTRY_SPACE,
24024
24719
  AccountService,
24025
24720
  AutoApproveSpaceCreationHandler,
24721
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
24722
+ BOOTSTRAP_COMPLETION_MARKER_VERSION,
24026
24723
  CapabilityKeyRegistry,
24027
24724
  CapabilityKeyRegistryErrorCodes,
24028
24725
  CaveatedDelegationUnsupportedError,
@@ -24159,7 +24856,8 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
24159
24856
  tinyCloudDebugLogger,
24160
24857
  validateEnvelope,
24161
24858
  validateManifest,
24162
- verifyDecryptResponse
24859
+ verifyDecryptResponse,
24860
+ wireOpenKeyAuthorize
24163
24861
  });
24164
24862
  /*! Bundled license information:
24165
24863