@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.js CHANGED
@@ -17179,12 +17179,14 @@ import {
17179
17179
  SharingService,
17180
17180
  UnsupportedFeatureError,
17181
17181
  makePublicSpaceId,
17182
- ACCOUNT_REGISTRY_SPACE,
17182
+ ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
17183
+ BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST,
17183
17184
  BOOTSTRAP_SESSION_REQUESTS,
17184
17185
  SECRET_RECORDS_SCHEMA,
17185
17186
  SECRETS_SPACE as SECRETS_SPACE2,
17186
17187
  TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
17187
17188
  bootstrapSteps,
17189
+ ErrorCodes as ErrorCodes2,
17188
17190
  ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
17189
17191
  CaveatedDelegationUnsupportedError,
17190
17192
  PermissionNotInManifestError,
@@ -17218,6 +17220,8 @@ import {
17218
17220
  } from "@tinycloud/sdk-services";
17219
17221
 
17220
17222
  // src/authorization/NodeUserAuthorization.ts
17223
+ import { createHash } from "crypto";
17224
+ import { SiweMessage } from "siwe";
17221
17225
  import {
17222
17226
  fetchPeerId,
17223
17227
  submitHostDelegation,
@@ -17236,6 +17240,13 @@ import {
17236
17240
  canonicalizeAddress,
17237
17241
  makePkhSpaceId,
17238
17242
  pkhDid,
17243
+ extractImmutableSiweFields,
17244
+ diffImmutableSiweFields,
17245
+ extractRecapAttenuations,
17246
+ unauthorizedRecapCapabilities,
17247
+ parseCanonicalRecapResource,
17248
+ ACCOUNT_REGISTRY_SPACE,
17249
+ ACCOUNT_MANIFEST_PERMISSIONS,
17239
17250
  KV,
17240
17251
  SQL,
17241
17252
  DUCKDB,
@@ -17322,6 +17333,27 @@ var MemorySessionStorage = class {
17322
17333
  // src/authorization/NodeUserAuthorization.ts
17323
17334
  var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
17324
17335
  var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
17336
+ function canonicalizeOpenKeyManifestJson(value) {
17337
+ const json = JSON.stringify(value);
17338
+ if (json === void 0) {
17339
+ throw new TypeError("manifest is not JSON-serializable");
17340
+ }
17341
+ return canonicalizeJsonValue(JSON.parse(json));
17342
+ }
17343
+ function canonicalizeJsonValue(value) {
17344
+ if (value === null || typeof value !== "object") {
17345
+ return JSON.stringify(value);
17346
+ }
17347
+ if (Array.isArray(value)) {
17348
+ return `[${value.map(canonicalizeJsonValue).join(",")}]`;
17349
+ }
17350
+ return `{${Object.keys(value).sort().map(
17351
+ (key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
17352
+ ).join(",")}}`;
17353
+ }
17354
+ function canonicalOpenKeyManifestSha256Hex(value) {
17355
+ return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
17356
+ }
17325
17357
  function didPrincipalMatches(actual, expected) {
17326
17358
  try {
17327
17359
  return principalDidEquals(actual, expected);
@@ -17339,6 +17371,16 @@ function addRawAbility(rawAbilities, resource, action) {
17339
17371
  actions.push(action);
17340
17372
  }
17341
17373
  }
17374
+ function cloneAbilitiesMap(actions) {
17375
+ return Object.fromEntries(
17376
+ Object.entries(actions).map(([service, paths]) => [
17377
+ service,
17378
+ Object.fromEntries(
17379
+ Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
17380
+ )
17381
+ ])
17382
+ );
17383
+ }
17342
17384
  var NodeUserAuthorization = class {
17343
17385
  constructor(config) {
17344
17386
  this.activeSessionKeyId = "default";
@@ -17542,6 +17584,15 @@ var NodeUserAuthorization = class {
17542
17584
  get nodeFeatures() {
17543
17585
  return this._nodeFeatures;
17544
17586
  }
17587
+ /**
17588
+ * The node DID reported by `GET /info` for `host`, if the sign-in (or
17589
+ * restore) that populated it targeted that same host. Returns `undefined`
17590
+ * for any other host, and for nodes whose `/info` omits `nodeId`, so callers
17591
+ * fall back to fetching it themselves rather than using a foreign node's DID.
17592
+ */
17593
+ nodeIdForHost(host) {
17594
+ return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
17595
+ }
17545
17596
  get lastActivationSkippedSpaceIds() {
17546
17597
  return [...this._lastActivationSkippedSpaceIds];
17547
17598
  }
@@ -17601,23 +17652,41 @@ var NodeUserAuthorization = class {
17601
17652
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
17602
17653
  const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
17603
17654
  const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
17604
- return {
17605
- abilities: this.defaultActions,
17606
- spaceId: primarySpaceId2,
17607
- spaceAbilities: {
17608
- [primarySpaceId2]: this.defaultActions,
17609
- [secretsSpaceId]: {
17610
- kv: {
17611
- "vault/secrets/": [
17612
- KV.GET,
17613
- KV.PUT,
17614
- KV.DEL,
17615
- KV.LIST,
17616
- KV.METADATA
17617
- ]
17655
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
17656
+ const spaceAbilities2 = {
17657
+ [primarySpaceId2]: primaryActions,
17658
+ [secretsSpaceId]: {
17659
+ kv: {
17660
+ "vault/secrets/": [
17661
+ KV.GET,
17662
+ KV.PUT,
17663
+ KV.DEL,
17664
+ KV.LIST,
17665
+ KV.METADATA
17666
+ ]
17667
+ }
17668
+ }
17669
+ };
17670
+ if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
17671
+ spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
17672
+ } else if (this.includeAccountRegistryPermissions) {
17673
+ const accountAbilities = resourceCapabilitiesToAbilitiesMap(
17674
+ ACCOUNT_MANIFEST_PERMISSIONS
17675
+ );
17676
+ for (const [service, paths] of Object.entries(accountAbilities)) {
17677
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
17678
+ for (const [path, actions] of Object.entries(paths)) {
17679
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
17680
+ for (const action of actions) {
17681
+ if (!existingActions.includes(action)) existingActions.push(action);
17618
17682
  }
17619
17683
  }
17620
- },
17684
+ }
17685
+ }
17686
+ return {
17687
+ abilities: primaryActions,
17688
+ spaceId: primarySpaceId2,
17689
+ spaceAbilities: spaceAbilities2,
17621
17690
  rawAbilities: {
17622
17691
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
17623
17692
  }
@@ -18011,11 +18080,13 @@ var NodeUserAuthorization = class {
18011
18080
  this._tinyCloudSession = tinyCloudSession;
18012
18081
  this._address = address;
18013
18082
  this._chainId = chainId;
18083
+ const infoHost = this.primaryTinyCloudHost;
18014
18084
  const nodeInfo = await checkNodeInfo(
18015
- this.primaryTinyCloudHost,
18085
+ infoHost,
18016
18086
  this.wasm.protocolVersion()
18017
18087
  );
18018
18088
  this._nodeFeatures = nodeInfo.features;
18089
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18019
18090
  for (const ext of this.extensions) {
18020
18091
  if (ext.afterSignIn) {
18021
18092
  await ext.afterSignIn(clientSession);
@@ -18181,11 +18252,13 @@ var NodeUserAuthorization = class {
18181
18252
  this._tinyCloudSession = tinyCloudSession;
18182
18253
  this._address = address;
18183
18254
  this._chainId = chainId;
18255
+ const infoHost = this.primaryTinyCloudHost;
18184
18256
  const nodeInfo = await checkNodeInfo(
18185
- this.primaryTinyCloudHost,
18257
+ infoHost,
18186
18258
  this.wasm.protocolVersion()
18187
18259
  );
18188
18260
  this._nodeFeatures = nodeInfo.features;
18261
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18189
18262
  for (const ext of this.extensions) {
18190
18263
  if (ext.afterSignIn) {
18191
18264
  await ext.afterSignIn(clientSession);
@@ -18194,6 +18267,398 @@ var NodeUserAuthorization = class {
18194
18267
  await this.ensureSpaceExists();
18195
18268
  return clientSession;
18196
18269
  }
18270
+ /**
18271
+ * End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
18272
+ *
18273
+ * This is the production entry point that wires
18274
+ * {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
18275
+ * {@link signInWithOpenKeyResult} into one call. Consumers pass in a
18276
+ * caller-provided `authorizeFn` (typically a thin wrapper around
18277
+ * `openkey.authorizeTinyCloud()`) so this method has no direct
18278
+ * dependency on the OpenKey SDK — it works with any transport that
18279
+ * conforms to the versioned protocol.
18280
+ *
18281
+ * The `authorizeFn` MUST:
18282
+ * 1. Send the prepared SIWE bytes to OpenKey.
18283
+ * 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
18284
+ * `signedMessage` — the bytes the signature actually verifies
18285
+ * against — plus `selectedActionKeys` and `permissions`).
18286
+ *
18287
+ * NodeUserAuthorization then completes the session with `signedMessage`
18288
+ * (never the prepared bytes), verifying the signature, the immutable
18289
+ * SIWE fields, and every subset invariant enforced by
18290
+ * {@link signInWithOpenKeyResult}. This preserves the exact-byte
18291
+ * guarantee: if the user did NOT narrow, the returned `signedMessage`
18292
+ * equals the prepared SIWE byte-for-byte; if the user narrowed, only
18293
+ * the ReCap block differs and every immutable header field survives.
18294
+ *
18295
+ * Legacy exact-byte {@link signIn} callers are unaffected — this method
18296
+ * is opt-in.
18297
+ *
18298
+ * @param authorizeFn Callback that invokes the versioned OpenKey
18299
+ * `authorizeTinyCloud()` protocol and returns the result. Receives
18300
+ * the prepared SIWE bytes plus a `host` hint the widget should bind
18301
+ * into its /authorize-sign-prepare context.
18302
+ * @param options Optional overrides for the OpenKey request.
18303
+ */
18304
+ async signInWithOpenKey(authorizeFn, options) {
18305
+ if (options?.host !== void 0) {
18306
+ const overrideHost = options.host.trim();
18307
+ if (!overrideHost) {
18308
+ throw new Error("OpenKey authorization host must not be empty");
18309
+ }
18310
+ this.tinycloudHosts = [overrideHost];
18311
+ }
18312
+ const signingAddress = canonicalizeAddress(await this.signer.getAddress());
18313
+ const signingChainId = await this.signer.getChainId();
18314
+ await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
18315
+ const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
18316
+ const presentation = this.buildPresentationEnvelope(options?.reason);
18317
+ const hostHint = this.primaryTinyCloudHost;
18318
+ const result = await authorizeFn({
18319
+ protocolVersion: 1,
18320
+ siwe: prepared.siwe,
18321
+ jwk,
18322
+ host: hostHint,
18323
+ keyId: options?.openkeyKeyId,
18324
+ presentation
18325
+ });
18326
+ return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
18327
+ }
18328
+ /**
18329
+ * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
18330
+ * envelope is display-only — the OpenKey widget renders it under an
18331
+ * honest trust label, and the server /authorize-sign-prepare route
18332
+ * decides whether to upgrade the label to `origin-bound` by fetching
18333
+ * the well-known manifest and comparing its digest to
18334
+ * `manifestDigest`.
18335
+ *
18336
+ * Returns `undefined` when no manifest is set — the widget renders as
18337
+ * "no manifest supplied" (unsigned).
18338
+ *
18339
+ * The digest is a deterministic SHA-256 over canonical JSON of the
18340
+ * first manifest payload; OpenKey parses the fetched well-known manifest
18341
+ * and applies the same canonicalization before comparing digests.
18342
+ */
18343
+ buildPresentationEnvelope(reason) {
18344
+ const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
18345
+ if (manifests.length === 0 && !reason) {
18346
+ return void 0;
18347
+ }
18348
+ const primary = manifests[0];
18349
+ let displayName;
18350
+ let manifestId;
18351
+ let manifestDigest;
18352
+ const envelopeManifests = [];
18353
+ for (const m of manifests) {
18354
+ envelopeManifests.push({
18355
+ name: m.name,
18356
+ appId: m.app_id,
18357
+ // Deep-copy to prevent later mutations from affecting the digest.
18358
+ payload: JSON.parse(JSON.stringify(m))
18359
+ });
18360
+ }
18361
+ if (primary) {
18362
+ displayName = primary.name;
18363
+ manifestId = primary.app_id;
18364
+ manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
18365
+ }
18366
+ return {
18367
+ protocolVersion: 1,
18368
+ displayName,
18369
+ reason: reason && reason.length > 0 ? reason : void 0,
18370
+ manifestId,
18371
+ manifestDigest,
18372
+ manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
18373
+ };
18374
+ }
18375
+ /**
18376
+ * Complete sign-in with a versioned OpenKey authorization result.
18377
+ *
18378
+ * Where `signInWithPreparedSession` completes with a signature over the
18379
+ * ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
18380
+ * OpenKey widget actually presented and signed (`signedMessage`). It
18381
+ * exists because the versioned authorizeTinyCloud protocol lets the user
18382
+ * narrow capabilities; the returned bytes may differ from the original
18383
+ * request and are the only ones the signature actually verifies against.
18384
+ *
18385
+ * The `result` parameter is validated at the trust boundary via
18386
+ * `validateAuthorizationResultV1` (in sdk-core). Callers should validate
18387
+ * BEFORE calling this method so unknown/missing fields are rejected
18388
+ * before any session state is created.
18389
+ *
18390
+ * Validation performed here (defence-in-depth: even a "valid" v1 payload
18391
+ * must clear these checks before any session state is created):
18392
+ * 1. `result.address` matches the local signer's address.
18393
+ * 2. `result.signature` recovers to `result.address` when verified against
18394
+ * `result.signedMessage`.
18395
+ * 3. Every immutable SIWE header field (domain, address, URI, version,
18396
+ * chainId, nonce, issuedAt) in the original prepared SIWE is preserved
18397
+ * byte-for-byte in `result.signedMessage`. `statement` is NOT in this
18398
+ * list because the WASM renders it from the ReCap contents; it MUST
18399
+ * change when the ReCap is narrowed.
18400
+ * 4. Every capability in `result.signedMessage`'s ReCap block was present
18401
+ * in the original prepared SIWE — narrowing OK, broadening rejected.
18402
+ * 5. Every `result.selectedActionKeys` entry corresponds to a
18403
+ * (resource, action) pair in the returned SIWE.
18404
+ */
18405
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
18406
+ if (result.protocolVersion !== 1) {
18407
+ throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
18408
+ }
18409
+ if (!result.signedMessage) {
18410
+ throw new Error("OpenKey result must include signedMessage");
18411
+ }
18412
+ if (!prepared.siwe) {
18413
+ throw new Error(
18414
+ "signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
18415
+ );
18416
+ }
18417
+ const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
18418
+ if (canonicalizeAddress(result.address) !== expectedAddress) {
18419
+ throw new Error(
18420
+ `OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
18421
+ );
18422
+ }
18423
+ let parsedSignedSiwe;
18424
+ try {
18425
+ parsedSignedSiwe = new SiweMessage(result.signedMessage);
18426
+ } catch (err) {
18427
+ throw new Error(
18428
+ `OpenKey signedMessage is not a parseable SIWE: ${err.message}`
18429
+ );
18430
+ }
18431
+ const verifyResult = await parsedSignedSiwe.verify(
18432
+ { signature: result.signature },
18433
+ { suppressExceptions: true }
18434
+ );
18435
+ if (!verifyResult.success) {
18436
+ const errData = verifyResult.error;
18437
+ const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
18438
+ const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
18439
+ throw new Error(
18440
+ `OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
18441
+ );
18442
+ }
18443
+ if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
18444
+ throw new Error(
18445
+ `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
18446
+ );
18447
+ }
18448
+ const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
18449
+ const originalFields = extractImmutableSiweFields(prepared.siwe);
18450
+ const signedFields = extractImmutableSiweFields(result.signedMessage);
18451
+ const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
18452
+ originalHasRecap
18453
+ });
18454
+ if (changedFields.length > 0) {
18455
+ throw new Error(
18456
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
18457
+ );
18458
+ }
18459
+ let originalCaps;
18460
+ let signedCaps;
18461
+ try {
18462
+ originalCaps = extractRecapAttenuations(prepared.siwe);
18463
+ } catch (err) {
18464
+ throw new Error(
18465
+ `Failed to decode prepared SIWE recap: ${err.message}`
18466
+ );
18467
+ }
18468
+ try {
18469
+ signedCaps = extractRecapAttenuations(result.signedMessage);
18470
+ } catch (err) {
18471
+ throw new Error(
18472
+ `Failed to decode OpenKey signedMessage recap: ${err.message}`
18473
+ );
18474
+ }
18475
+ const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
18476
+ if (unauthorized.length > 0) {
18477
+ const previewLimit = 5;
18478
+ const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
18479
+ const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
18480
+ throw new Error(
18481
+ `OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
18482
+ );
18483
+ }
18484
+ if (!result.signedMessage.includes(prepared.spaceId)) {
18485
+ throw new Error(
18486
+ `OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
18487
+ );
18488
+ }
18489
+ const grantedPairs = /* @__PURE__ */ new Set();
18490
+ const grantedFourPartIndex = /* @__PURE__ */ new Map();
18491
+ for (const [resource, actions] of Object.entries(signedCaps)) {
18492
+ const { space, path } = parseCanonicalRecapResource(resource);
18493
+ for (const action of Object.keys(actions)) {
18494
+ grantedPairs.add(`${resource}\0${action}`);
18495
+ const slashIdx = action.indexOf("/");
18496
+ const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
18497
+ if (service) {
18498
+ grantedFourPartIndex.set(
18499
+ `${service}\0${space}\0${path}\0${action}`,
18500
+ `${resource}\0${action}`
18501
+ );
18502
+ }
18503
+ }
18504
+ }
18505
+ const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
18506
+ const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
18507
+ for (const pair of grantedPairs) {
18508
+ const idx = pair.indexOf("\0");
18509
+ const action = idx >= 0 ? pair.slice(idx + 1) : pair;
18510
+ if (!isStructurallyRequired(action)) {
18511
+ nonRequiredGrantedPairs.add(pair);
18512
+ }
18513
+ }
18514
+ const allSignedGrantedPairs = new Set(grantedPairs);
18515
+ const seenRawKeys = /* @__PURE__ */ new Set();
18516
+ for (const rawKey of result.selectedActionKeys) {
18517
+ if (seenRawKeys.has(rawKey)) {
18518
+ throw new Error(
18519
+ `OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
18520
+ );
18521
+ }
18522
+ seenRawKeys.add(rawKey);
18523
+ }
18524
+ const selectedResourceActionPairs = /* @__PURE__ */ new Set();
18525
+ for (const rawKey of result.selectedActionKeys) {
18526
+ const parts = rawKey.split("\0");
18527
+ if (parts.length !== 4) {
18528
+ throw new Error(
18529
+ `OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
18530
+ );
18531
+ }
18532
+ const canonical = grantedFourPartIndex.get(rawKey);
18533
+ if (!canonical) {
18534
+ throw new Error(
18535
+ `OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
18536
+ );
18537
+ }
18538
+ selectedResourceActionPairs.add(canonical);
18539
+ }
18540
+ if (nonRequiredGrantedPairs.size > 0) {
18541
+ const missingPairs = [];
18542
+ for (const pair of nonRequiredGrantedPairs) {
18543
+ if (!selectedResourceActionPairs.has(pair)) {
18544
+ missingPairs.push(pair);
18545
+ }
18546
+ }
18547
+ if (missingPairs.length > 0) {
18548
+ const previewLimit = 5;
18549
+ const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18550
+ const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
18551
+ throw new Error(
18552
+ `OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
18553
+ );
18554
+ }
18555
+ }
18556
+ if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
18557
+ throw new Error(
18558
+ "OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
18559
+ );
18560
+ }
18561
+ const permissionCanonicalKeys = /* @__PURE__ */ new Set();
18562
+ const permissionPairsCovered = /* @__PURE__ */ new Set();
18563
+ for (const perm of result.permissions) {
18564
+ const { service, space, path, actions } = perm;
18565
+ if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
18566
+ throw new Error(
18567
+ `OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
18568
+ );
18569
+ }
18570
+ if (!Array.isArray(actions) || actions.length === 0) {
18571
+ throw new Error(
18572
+ `OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
18573
+ );
18574
+ }
18575
+ const canonicalKey = `${service}\0${space}\0${path}`;
18576
+ if (permissionCanonicalKeys.has(canonicalKey)) {
18577
+ throw new Error(
18578
+ `OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
18579
+ );
18580
+ }
18581
+ permissionCanonicalKeys.add(canonicalKey);
18582
+ let expectedResource;
18583
+ if (space.startsWith("tinycloud:")) {
18584
+ const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
18585
+ expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
18586
+ } else {
18587
+ if (path) {
18588
+ throw new Error(
18589
+ `OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
18590
+ );
18591
+ }
18592
+ expectedResource = space;
18593
+ }
18594
+ let matchedResource;
18595
+ if (signedCaps[expectedResource] !== void 0) {
18596
+ matchedResource = expectedResource;
18597
+ } else {
18598
+ throw new Error(
18599
+ `OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
18600
+ );
18601
+ }
18602
+ const grantedActionsForResource = new Set(
18603
+ Object.keys(signedCaps[matchedResource])
18604
+ );
18605
+ for (const action of actions) {
18606
+ if (typeof action !== "string" || !action) {
18607
+ throw new Error(
18608
+ `OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
18609
+ );
18610
+ }
18611
+ if (!grantedActionsForResource.has(action)) {
18612
+ throw new Error(
18613
+ `OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
18614
+ );
18615
+ }
18616
+ permissionPairsCovered.add(`${matchedResource}\0${action}`);
18617
+ }
18618
+ }
18619
+ if (allSignedGrantedPairs.size > 0) {
18620
+ const missingFromPerms = [];
18621
+ for (const pair of allSignedGrantedPairs) {
18622
+ if (!permissionPairsCovered.has(pair)) {
18623
+ missingFromPerms.push(pair);
18624
+ }
18625
+ }
18626
+ if (missingFromPerms.length > 0) {
18627
+ const previewLimit = 5;
18628
+ const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18629
+ const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
18630
+ throw new Error(
18631
+ `OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
18632
+ );
18633
+ }
18634
+ const extrasInPerms = [];
18635
+ for (const pair of permissionPairsCovered) {
18636
+ if (!allSignedGrantedPairs.has(pair)) {
18637
+ extrasInPerms.push(pair);
18638
+ }
18639
+ }
18640
+ if (extrasInPerms.length > 0) {
18641
+ const previewLimit = 5;
18642
+ const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18643
+ const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
18644
+ throw new Error(
18645
+ `OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
18646
+ );
18647
+ }
18648
+ }
18649
+ void nonRequiredGrantedPairs;
18650
+ return this.signInWithPreparedSession(
18651
+ {
18652
+ siwe: result.signedMessage,
18653
+ jwk: prepared.jwk,
18654
+ spaceId: prepared.spaceId,
18655
+ verificationMethod: prepared.verificationMethod
18656
+ },
18657
+ result.signature,
18658
+ keyId,
18659
+ jwk
18660
+ );
18661
+ }
18197
18662
  /**
18198
18663
  * Clear persisted session data.
18199
18664
  */
@@ -18449,7 +18914,7 @@ function createWasmKeyProvider(sessionManager) {
18449
18914
  // src/delegateToHelpers.ts
18450
18915
  import {
18451
18916
  parseExpiry,
18452
- SiweMessage,
18917
+ SiweMessage as SiweMessage2,
18453
18918
  EXPIRY as EXPIRY2
18454
18919
  } from "@tinycloud/sdk-core";
18455
18920
  function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
@@ -18498,7 +18963,7 @@ function resolveExpiryMs(expiry) {
18498
18963
  return parseExpiry(expiry);
18499
18964
  }
18500
18965
  function extractSiweExpiration(siwe) {
18501
- const parsed = new SiweMessage(siwe);
18966
+ const parsed = new SiweMessage2(siwe);
18502
18967
  if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
18503
18968
  return void 0;
18504
18969
  }
@@ -18700,6 +19165,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
18700
19165
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
18701
19166
  var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
18702
19167
  var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
19168
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
19169
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
19170
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
18703
19171
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
18704
19172
  function isSecretPayload(value) {
18705
19173
  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";
@@ -19010,7 +19478,9 @@ function base64UrlDecode(value) {
19010
19478
  return new Uint8Array(bytes);
19011
19479
  }
19012
19480
  async function signJwtInputWithJwk(signingInput, jwk) {
19013
- const bytes = new TextEncoder().encode(signingInput);
19481
+ return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
19482
+ }
19483
+ async function signBytesWithJwk(bytes, jwk) {
19014
19484
  try {
19015
19485
  const subtle = globalThis.crypto?.subtle;
19016
19486
  if (!subtle) {
@@ -19091,6 +19561,12 @@ var _TinyCloudNode = class _TinyCloudNode {
19091
19561
  this._baseSecrets = /* @__PURE__ */ new Map();
19092
19562
  this._secrets = /* @__PURE__ */ new Map();
19093
19563
  this.runtimePermissionGrants = [];
19564
+ /**
19565
+ * Owned space ids this sign-in has already confirmed are hosted. Consulted by
19566
+ * {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
19567
+ * hosting is confirmed against the session that was active at the time.
19568
+ */
19569
+ this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
19094
19570
  /**
19095
19571
  * True when the last signIn() detected an interactive signer and skipped
19096
19572
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -19099,9 +19575,15 @@ var _TinyCloudNode = class _TinyCloudNode {
19099
19575
  this._bootstrapSkipped = false;
19100
19576
  /**
19101
19577
  * Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
19102
- * true when bootstrap did not complete (interactive signer, auto-sign
19103
- * denied, or a bootstrap step failed); `reason` carries the cause so apps
19104
- * can surface a "finish account setup" call-to-action.
19578
+ * true when bootstrap did not complete in this invocation (interactive signer,
19579
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
19580
+ * step failed). `already-provisioned` means the bootstrap was not run here;
19581
+ * it does not mean a prior provisioning attempt failed to complete.
19582
+ * `reason` carries the cause so apps can surface a "finish account setup"
19583
+ * call-to-action. `warnings` is
19584
+ * present when bootstrap completed, but one or more steps recovered from
19585
+ * an ambiguous failure (e.g. a KV batch write reconciled via per-space
19586
+ * fallback) — clean vs. recovered runs are otherwise byte-identical.
19105
19587
  */
19106
19588
  this._bootstrapStatus = {
19107
19589
  skipped: false
@@ -19281,7 +19763,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19281
19763
  nonce: config.nonce,
19282
19764
  siweConfig: config.siweConfig,
19283
19765
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
19284
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19766
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19285
19767
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
19286
19768
  });
19287
19769
  this.tc = new TinyCloud(this.auth, {
@@ -19421,7 +19903,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19421
19903
  if (!this._address) {
19422
19904
  return void 0;
19423
19905
  }
19424
- return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE);
19906
+ return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
19425
19907
  }
19426
19908
  /**
19427
19909
  * Account-level application and delegation helpers.
@@ -19487,6 +19969,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19487
19969
  this._spaceService = void 0;
19488
19970
  this._serviceContext = void 0;
19489
19971
  this.runtimePermissionGrants = [];
19972
+ this.confirmedHostedSpaceIds.clear();
19490
19973
  await this.tc.signIn(options);
19491
19974
  this.syncResolvedHostFromAuth();
19492
19975
  const oldGraph = this._serviceGraph;
@@ -19523,6 +20006,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19523
20006
  this._bootstrapSkipped = false;
19524
20007
  this._bootstrapStatus = { skipped: false };
19525
20008
  if (this.config.autoBootstrapAccount === false) {
20009
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
19526
20010
  return false;
19527
20011
  }
19528
20012
  if (!this.auth || !this._address) {
@@ -19537,11 +20021,20 @@ var _TinyCloudNode = class _TinyCloudNode {
19537
20021
  return false;
19538
20022
  }
19539
20023
  const steps = bootstrapSteps(this._address, this._chainId);
19540
- if (!await this.isFreshBootstrapAccount(steps)) {
19541
- return false;
19542
- }
19543
20024
  try {
19544
- await this.runAccountBootstrap(steps);
20025
+ const decision = await this.resolveBootstrapDecision(steps);
20026
+ if (decision.action === "skip") {
20027
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
20028
+ return false;
20029
+ }
20030
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
20031
+ await this.writeBootstrapCompletionMarker(steps);
20032
+ if (warnings.length > 0) {
20033
+ this._bootstrapStatus = { skipped: false, warnings };
20034
+ this.notificationHandler.warning(
20035
+ `Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
20036
+ );
20037
+ }
19545
20038
  } catch (err) {
19546
20039
  const reason = err instanceof Error ? err.message : String(err);
19547
20040
  this._bootstrapSkipped = true;
@@ -19554,32 +20047,92 @@ var _TinyCloudNode = class _TinyCloudNode {
19554
20047
  }
19555
20048
  return true;
19556
20049
  }
19557
- async isFreshBootstrapAccount(steps) {
20050
+ async resolveBootstrapDecision(steps) {
19558
20051
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
19559
20052
  for (const step of steps) {
19560
20053
  if (step.kind === "session") {
19561
20054
  enshrinedSpaceIds.add(step.spaceId);
19562
20055
  }
19563
20056
  }
19564
- const skipped = this.auth.lastActivationSkippedSpaceIds;
20057
+ const auth = this.auth;
20058
+ if (!auth) {
20059
+ throw new Error("Account bootstrap requires an active wallet session");
20060
+ }
20061
+ const skipped = auth.lastActivationSkippedSpaceIds;
19565
20062
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
19566
- return true;
20063
+ return { action: "run", mode: "fresh" };
19567
20064
  }
19568
20065
  try {
19569
- const indexed = await this.account.index.spaces.list();
19570
- if (indexed.ok && indexed.data.length === 0) {
19571
- return true;
20066
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
20067
+ const markerPermission = {
20068
+ service: "tinycloud.kv",
20069
+ space: accountSpaceId,
20070
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
20071
+ actions: ["get"]
20072
+ };
20073
+ if (!this.hasRuntimePermissions([markerPermission])) {
20074
+ return { action: "run", mode: "repair" };
19572
20075
  }
19573
- } catch {
20076
+ const marker = await this.readBootstrapCompletionMarker();
20077
+ if (marker.ok) {
20078
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
20079
+ return { action: "skip" };
20080
+ }
20081
+ return { action: "run", mode: "repair" };
20082
+ }
20083
+ if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
20084
+ return { action: "run", mode: "fresh" };
20085
+ }
20086
+ } catch (err) {
20087
+ const reason = err instanceof Error ? err.message : String(err);
20088
+ this.notificationHandler.warning(
20089
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
20090
+ );
20091
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
20092
+ return { action: "run", mode: "repair" };
19574
20093
  }
19575
- try {
19576
- const spaces = await this.account.spaces.list();
19577
- return spaces.ok && spaces.data.length === 0;
19578
- } catch {
19579
- return false;
20094
+ return { action: "run", mode: "repair" };
20095
+ }
20096
+ async readBootstrapCompletionMarker() {
20097
+ return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
20098
+ BOOTSTRAP_COMPLETION_MARKER_KEY
20099
+ );
20100
+ }
20101
+ async writeBootstrapCompletionMarker(steps) {
20102
+ if (!this._address) {
20103
+ throw new Error("Bootstrap completion marker requires an active wallet session");
19580
20104
  }
20105
+ const canonicalStepIds = new Set(
20106
+ bootstrapSteps(this._address, this._chainId).map((step) => step.id)
20107
+ );
20108
+ const providedStepIds = new Set(steps.map((step) => step.id));
20109
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
20110
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
20111
+ }
20112
+ const marker = {
20113
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
20114
+ stepIds: [...canonicalStepIds],
20115
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
20116
+ };
20117
+ const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
20118
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
20119
+ marker
20120
+ );
20121
+ if (!written.ok) {
20122
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
20123
+ }
20124
+ }
20125
+ isAcceptedBootstrapCompletionMarker(value) {
20126
+ if (typeof value !== "object" || value === null) return false;
20127
+ const entries = Object.entries(value);
20128
+ const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
20129
+ const version23 = markerValue("v");
20130
+ return typeof version23 === "number" && Number.isInteger(version23) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version23);
20131
+ }
20132
+ markerReadIsUnhostedSpace(meta) {
20133
+ return meta?.status === 404;
19581
20134
  }
19582
- async runAccountBootstrap(steps) {
20135
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
19583
20136
  if (!this.auth || !this._address) {
19584
20137
  throw new Error("Account bootstrap requires an active wallet session");
19585
20138
  }
@@ -19590,6 +20143,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19590
20143
  const auth = this.auth;
19591
20144
  const sessions = /* @__PURE__ */ new Map();
19592
20145
  const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
20146
+ const warnings = [];
19593
20147
  const primarySession = auth.tinyCloudSession;
19594
20148
  const defaultSpaceId = this.ownedSpaceId("default");
19595
20149
  const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
@@ -19651,32 +20205,44 @@ var _TinyCloudNode = class _TinyCloudNode {
19651
20205
  }
19652
20206
  }
19653
20207
  if (step.kind === "seed-spaces") {
19654
- for (const space of step.spaces) {
19655
- const registered = await this.account.spaces.register({
20208
+ const registered = await this.account.spaces.registerBatch(
20209
+ step.spaces.map((space) => ({
19656
20210
  spaceId: space.spaceId,
19657
20211
  name: space.name,
19658
20212
  ownerDid: this.did,
19659
20213
  type: "owned",
19660
20214
  permissions: ["*"],
19661
20215
  status: "active"
20216
+ }))
20217
+ );
20218
+ if (!registered.ok) {
20219
+ throw new Error(
20220
+ `Failed to seed account spaces: ${registered.error.message}`
20221
+ );
20222
+ }
20223
+ const batchError = registered.data.recoveredFromBatchError;
20224
+ if (batchError) {
20225
+ warnings.push({
20226
+ stepId: step.id,
20227
+ kind: "batch-write-reconciled",
20228
+ code: batchError.code,
20229
+ message: batchError.message
19662
20230
  });
19663
- if (!registered.ok) {
19664
- throw new Error(
19665
- `Failed to seed account space ${space.spaceId}: ${registered.error.message}`
19666
- );
19667
- }
19668
20231
  }
19669
20232
  }
19670
20233
  if (step.kind === "seed-applications") {
19671
20234
  const registered = await this.account.applications.register(
19672
- step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
20235
+ step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
20236
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
20237
+ // repair, so skip the manifest-hash pre-read.
20238
+ { assumeUnregistered: true }
19673
20239
  );
19674
20240
  if (!registered.ok) {
19675
20241
  throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
19676
20242
  }
19677
20243
  }
19678
20244
  if (step.kind === "encryption-network-create") {
19679
- await this.ensureEncryptionNetwork(step.networkId);
20245
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
19680
20246
  }
19681
20247
  if (step.kind === "secret-records-schema") {
19682
20248
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -19696,6 +20262,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19696
20262
  }
19697
20263
  }
19698
20264
  }
20265
+ return warnings;
19699
20266
  }
19700
20267
  registerBootstrapRuntimeGrant(session, request, rawAbilities) {
19701
20268
  const operations = [];
@@ -19858,7 +20425,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19858
20425
  if (!this.auth || !this.signer) {
19859
20426
  throw new Error("Manifest registry write requires wallet mode");
19860
20427
  }
19861
- const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE);
20428
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
19862
20429
  await this.ensureOwnedSpaceHostedById(accountSpaceId);
19863
20430
  const result = await this.account.applications.register(request.manifests);
19864
20431
  if (!result.ok) {
@@ -19955,6 +20522,22 @@ var _TinyCloudNode = class _TinyCloudNode {
19955
20522
  await this.ensureEncryptionNetwork(networkId);
19956
20523
  }
19957
20524
  }
20525
+ /**
20526
+ * Ensure one of this user's owned spaces is hosted, at most once per space
20527
+ * per sign-in.
20528
+ *
20529
+ * Account bootstrap funnels six calls through here (via
20530
+ * `AccountService.ensureAccountSpaceHosted`), and each one re-submits
20531
+ * `activateSessionWithHost` with the *primary* session. That session's recap
20532
+ * covers only `default` and `secrets`, so the account space appears in
20533
+ * neither `activated` nor `skipped` and the guard below returns immediately
20534
+ * — the repeat calls cannot change the outcome, they only cost round trips.
20535
+ *
20536
+ * The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
20537
+ * `signIn`) and only records confirmed successes. A wrong skip is
20538
+ * self-revealing: the next write to the space fails immediately and loudly
20539
+ * with `404 Space not found`.
20540
+ */
19958
20541
  async ensureOwnedSpaceHostedById(spaceId) {
19959
20542
  if (!this.auth) {
19960
20543
  throw new Error("Owned space hosting requires wallet mode");
@@ -19963,12 +20546,16 @@ var _TinyCloudNode = class _TinyCloudNode {
19963
20546
  if (!session) {
19964
20547
  throw new Error("Owned space hosting requires an active session");
19965
20548
  }
20549
+ if (this.confirmedHostedSpaceIds.has(spaceId)) {
20550
+ return;
20551
+ }
19966
20552
  const host = this.hosts[0] ?? this.config.host;
19967
20553
  if (!host) {
19968
20554
  throw new Error("Owned space hosting requires a TinyCloud host");
19969
20555
  }
19970
20556
  const activation = await activateSessionWithHost2(host, session.delegationHeader);
19971
20557
  if (activation.success && !activation.skipped?.includes(spaceId)) {
20558
+ this.confirmedHostedSpaceIds.add(spaceId);
19972
20559
  return;
19973
20560
  }
19974
20561
  if (!activation.success && activation.status !== 404) {
@@ -19987,6 +20574,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19987
20574
  `Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
19988
20575
  );
19989
20576
  }
20577
+ this.confirmedHostedSpaceIds.add(spaceId);
19990
20578
  }
19991
20579
  /**
19992
20580
  * Host one of this user's owned spaces by name (e.g. `"applications"`).
@@ -20302,6 +20890,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20302
20890
  this._delegationManager = stagedGraph.delegationManager;
20303
20891
  this._spaceService = stagedGraph.spaceService;
20304
20892
  this._serviceGraph = stagedGraph.graph;
20893
+ this._activeServiceSession = serviceSession;
20305
20894
  this._baseSecrets = /* @__PURE__ */ new Map();
20306
20895
  this._secrets = /* @__PURE__ */ new Map();
20307
20896
  this._publicKV = void 0;
@@ -20309,6 +20898,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20309
20898
  this.tc = stagedGraph.core;
20310
20899
  this._recapOperationsCache = stagedPrimary.cache;
20311
20900
  this.runtimePermissionGrants = stagedPrimary.grants;
20901
+ this.confirmedHostedSpaceIds.clear();
20312
20902
  if (this.auth) {
20313
20903
  this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
20314
20904
  this._restoredTcSession = void 0;
@@ -20618,6 +21208,28 @@ var _TinyCloudNode = class _TinyCloudNode {
20618
21208
  currentTinyCloudSession() {
20619
21209
  return this.auth?.tinyCloudSession ?? this._restoredTcSession;
20620
21210
  }
21211
+ /**
21212
+ * Mint a multi-capability invocation through the established session key.
21213
+ * Callers receive only the signed headers; session key material remains
21214
+ * owned by the session manager and is never part of the caller contract.
21215
+ */
21216
+ invokeAny(entries, facts) {
21217
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21218
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21219
+ return this._serviceGraph.invokeAny(session, entries, facts);
21220
+ }
21221
+ /** Sign protocol bytes with the established in-memory session key. */
21222
+ async signSessionBytes(bytes) {
21223
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21224
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21225
+ return signBytesWithJwk(bytes, session.jwk);
21226
+ }
21227
+ /** Bind a session-signed invocation to the canonical service audience. */
21228
+ async bindInvocationAudience(authorization, audience) {
21229
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21230
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21231
+ return rewriteInvocationAudience(authorization, audience, session.jwk);
21232
+ }
20621
21233
  /**
20622
21234
  * Connect a wallet to upgrade from session-only mode to wallet mode.
20623
21235
  *
@@ -20679,7 +21291,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20679
21291
  nonce: this.config.nonce,
20680
21292
  siweConfig: this.config.siweConfig,
20681
21293
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20682
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21294
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20683
21295
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20684
21296
  });
20685
21297
  this.tc = new TinyCloud(this.auth, {
@@ -20732,7 +21344,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20732
21344
  nonce: this.config.nonce,
20733
21345
  siweConfig: this.config.siweConfig,
20734
21346
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20735
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21347
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20736
21348
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20737
21349
  });
20738
21350
  this.tc = new TinyCloud(this.auth, {
@@ -20799,6 +21411,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20799
21411
  if (this._serviceContext) {
20800
21412
  const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
20801
21413
  invoke: this._serviceContext.invoke,
21414
+ invokeAny: this._serviceContext.invokeAny,
20802
21415
  fetch: this._serviceContext.fetch,
20803
21416
  hosts: this._serviceContext.hosts,
20804
21417
  telemetry: this.config.telemetry
@@ -20891,7 +21504,17 @@ var _TinyCloudNode = class _TinyCloudNode {
20891
21504
  verifyNodeSignature: (nodeId, message, signature2) => verifyDidKeyEd25519Signature(nodeId, message, signature2)
20892
21505
  };
20893
21506
  }
21507
+ /**
21508
+ * The node DID for {@link config.host}.
21509
+ *
21510
+ * Sign-in already performs `GET /info` for the protocol-version check, and
21511
+ * that response carries `nodeId`, so reuse it rather than fetching `/info` a
21512
+ * second time. Only a value recorded for this exact host qualifies; older
21513
+ * nodes that omit `nodeId` fall through to the fetch.
21514
+ */
20894
21515
  async fetchNodeId() {
21516
+ const cached = this.auth?.nodeIdForHost(this.config.host);
21517
+ if (cached) return cached;
20895
21518
  const response = await fetch(`${this.config.host}/info`);
20896
21519
  if (!response.ok) {
20897
21520
  throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
@@ -21454,6 +22077,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21454
22077
  shareUrl: input.shareUrl,
21455
22078
  documentName: input.documentName,
21456
22079
  jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
22080
+ idempotencyKey: input.idempotencyKey,
21457
22081
  expiresAt: input.expiresAt
21458
22082
  };
21459
22083
  const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalizeEncryptionJson(body)))));
@@ -21663,6 +22287,13 @@ var _TinyCloudNode = class _TinyCloudNode {
21663
22287
  body
21664
22288
  )
21665
22289
  });
22290
+ if (response.status === 409) {
22291
+ const existing = await this.getEncryptionNetwork(networkId);
22292
+ if (existing) return existing;
22293
+ throw new Error(
22294
+ `Encryption network ${networkId} already exists but could not be read back`
22295
+ );
22296
+ }
21666
22297
  if (!response.ok) {
21667
22298
  throw new Error(
21668
22299
  `Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
@@ -21671,11 +22302,19 @@ var _TinyCloudNode = class _TinyCloudNode {
21671
22302
  const created = await response.json();
21672
22303
  return created.descriptor;
21673
22304
  }
21674
- async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME) {
22305
+ /**
22306
+ * Ensure an encryption network exists, creating it if necessary.
22307
+ *
22308
+ * @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
22309
+ * @param options - See {@link EnsureEncryptionNetworkOptions}.
22310
+ */
22311
+ async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
21675
22312
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
21676
- const existing = await this.getEncryptionNetwork(networkId);
21677
- if (existing) {
21678
- return existing;
22313
+ if (!options.assumeMissing) {
22314
+ const existing = await this.getEncryptionNetwork(networkId);
22315
+ if (existing) {
22316
+ return existing;
22317
+ }
21679
22318
  }
21680
22319
  const parsed = parseNetworkId2(networkId);
21681
22320
  if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
@@ -23454,35 +24093,44 @@ var _TinyCloudNode = class _TinyCloudNode {
23454
24093
  if (parentDelegation.disableSubDelegation) {
23455
24094
  throw new Error("Parent delegation does not allow sub-delegation");
23456
24095
  }
23457
- if (!params.path.startsWith(parentDelegation.path)) {
24096
+ const parentResources = parentDelegation.resources ?? [{
24097
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24098
+ space: parentDelegation.spaceId,
24099
+ path: parentDelegation.path,
24100
+ actions: parentDelegation.actions
24101
+ }];
24102
+ const requestedResources = params.resources ?? [{
24103
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24104
+ space: parentDelegation.spaceId,
24105
+ path: params.path,
24106
+ actions: params.actions
24107
+ }];
24108
+ const resourceIsContained = (parent, child) => {
24109
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
24110
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
24111
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
24112
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
24113
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
24114
+ };
24115
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
23458
24116
  throw new Error(
23459
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
24117
+ "Sub-delegation resources exceed the received delegation"
23460
24118
  );
23461
24119
  }
23462
- const parentActions = new Set(parentDelegation.actions);
23463
- for (const action of params.actions) {
23464
- if (!parentActions.has(action)) {
23465
- throw new Error(
23466
- `Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
23467
- );
23468
- }
23469
- }
23470
24120
  const now = /* @__PURE__ */ new Date();
23471
24121
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
23472
24122
  const requestedExpiry = new Date(now.getTime() + expiryMs);
23473
24123
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
23474
24124
  const abilities = {};
23475
- const kvActions = params.actions.filter((a) => a.startsWith("tinycloud.kv/"));
23476
- const sqlActions = params.actions.filter((a) => a.startsWith("tinycloud.sql/"));
23477
- const duckdbActions = params.actions.filter((a) => a.startsWith("tinycloud.duckdb/"));
23478
- if (kvActions.length > 0) {
23479
- abilities.kv = { [params.path]: kvActions };
23480
- }
23481
- if (sqlActions.length > 0) {
23482
- abilities.sql = { [params.path]: sqlActions };
23483
- }
23484
- if (duckdbActions.length > 0) {
23485
- abilities.duckdb = { [params.path]: duckdbActions };
24125
+ const rawAbilities = {};
24126
+ for (const resource of requestedResources) {
24127
+ const service = resource.service.replace(/^tinycloud\./, "");
24128
+ if (service === "encryption") {
24129
+ rawAbilities[resource.path] = [...resource.actions];
24130
+ continue;
24131
+ }
24132
+ abilities[service] ?? (abilities[service] = {});
24133
+ abilities[service][resource.path] = [...resource.actions];
23486
24134
  }
23487
24135
  const targetHost = parentDelegation.host ?? this.config.host;
23488
24136
  const prepared = this.wasmBindings.prepareSession({
@@ -23494,7 +24142,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23494
24142
  expirationTime: actualExpiry.toISOString(),
23495
24143
  spaceId: parentDelegation.spaceId,
23496
24144
  delegateUri: params.delegateDID,
23497
- parents: [parentDelegation.cid]
24145
+ parents: [parentDelegation.cid],
24146
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
23498
24147
  });
23499
24148
  const signature2 = await this.signer.signMessage(prepared.siwe);
23500
24149
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -23512,14 +24161,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23512
24161
  cid: subDelegationSession.delegationCid,
23513
24162
  delegationHeader: subDelegationSession.delegationHeader,
23514
24163
  spaceId: parentDelegation.spaceId,
23515
- path: params.path,
23516
- actions: params.actions,
24164
+ path: requestedResources[0].path,
24165
+ actions: requestedResources[0].actions,
23517
24166
  disableSubDelegation: params.disableSubDelegation ?? false,
23518
24167
  expiry: actualExpiry,
23519
24168
  delegateDID: params.delegateDID,
23520
24169
  ownerAddress: parentDelegation.ownerAddress,
23521
24170
  chainId: parentDelegation.chainId,
23522
- host: targetHost
24171
+ host: targetHost,
24172
+ resources: requestedResources.map((resource) => ({
24173
+ service: resource.service.replace(/^tinycloud\./, ""),
24174
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
24175
+ path: resource.path,
24176
+ actions: [...resource.actions]
24177
+ }))
23523
24178
  };
23524
24179
  }
23525
24180
  };
@@ -23692,10 +24347,54 @@ var FileSessionStorage = class {
23692
24347
  }
23693
24348
  };
23694
24349
 
24350
+ // src/authorization/openKeyBridge.ts
24351
+ function wireOpenKeyAuthorize(openkey) {
24352
+ return async (input) => {
24353
+ const result = await openkey.authorizeTinyCloud({
24354
+ protocolVersion: 1,
24355
+ siwe: input.siwe,
24356
+ keyId: input.keyId,
24357
+ jwk: input.jwk,
24358
+ host: input.host,
24359
+ // Forward the presentation envelope verbatim — the OpenKey SDK
24360
+ // validates and size-bounds it before use.
24361
+ presentation: input.presentation
24362
+ });
24363
+ if (result.protocolVersion !== 1) {
24364
+ throw new Error(
24365
+ `wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
24366
+ );
24367
+ }
24368
+ if (typeof result.signedMessage !== "string" || !result.signedMessage) {
24369
+ throw new Error(
24370
+ "wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
24371
+ );
24372
+ }
24373
+ if (!Array.isArray(result.selectedActionKeys)) {
24374
+ throw new Error(
24375
+ "wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
24376
+ );
24377
+ }
24378
+ if (!Array.isArray(result.permissions)) {
24379
+ throw new Error(
24380
+ "wireOpenKeyAuthorize: OpenKey returned no permissions"
24381
+ );
24382
+ }
24383
+ return {
24384
+ protocolVersion: 1,
24385
+ address: result.address,
24386
+ signature: result.signature,
24387
+ signedMessage: result.signedMessage,
24388
+ selectedActionKeys: result.selectedActionKeys,
24389
+ permissions: result.permissions
24390
+ };
24391
+ };
24392
+ }
24393
+
23695
24394
  // src/index.ts
23696
24395
  import {
23697
24396
  ACCOUNT_REGISTRY_PATH,
23698
- ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
24397
+ ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
23699
24398
  DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
23700
24399
  DEFAULT_MANIFEST_VERSION,
23701
24400
  VAULT_PERMISSION_SERVICE,
@@ -24129,10 +24828,13 @@ import {
24129
24828
  } from "@tinycloud/sdk-core";
24130
24829
  import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
24131
24830
  export {
24831
+ ACCEPTED_MARKER_VERSIONS,
24132
24832
  ACCOUNT_REGISTRY_PATH,
24133
- ACCOUNT_REGISTRY_SPACE2 as ACCOUNT_REGISTRY_SPACE,
24833
+ ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
24134
24834
  AccountService,
24135
24835
  AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
24836
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
24837
+ BOOTSTRAP_COMPLETION_MARKER_VERSION,
24136
24838
  CapabilityKeyRegistry2 as CapabilityKeyRegistry,
24137
24839
  CapabilityKeyRegistryErrorCodes,
24138
24840
  CaveatedDelegationUnsupportedError2 as CaveatedDelegationUnsupportedError,
@@ -24269,7 +24971,8 @@ export {
24269
24971
  tinyCloudDebugLogger,
24270
24972
  validateEnvelope,
24271
24973
  validateManifest,
24272
- verifyDecryptResponse
24974
+ verifyDecryptResponse,
24975
+ wireOpenKeyAuthorize
24273
24976
  };
24274
24977
  /*! Bundled license information:
24275
24978