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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,
@@ -17211,13 +17213,16 @@ import {
17211
17213
  parseNetworkId as parseNetworkId2,
17212
17214
  resolveSecretPath as resolveSecretPath2,
17213
17215
  validateOwnerSharePolicyRegistrationBytes,
17214
- validateShareDeliveryAuthorizationBytes
17216
+ validateShareDeliveryAuthorizationBytes,
17217
+ verifyEip191MessageSignature
17215
17218
  } from "@tinycloud/sdk-core";
17216
17219
  import {
17217
17220
  parsePermissionHint
17218
17221
  } from "@tinycloud/sdk-services";
17219
17222
 
17220
17223
  // src/authorization/NodeUserAuthorization.ts
17224
+ import { createHash } from "crypto";
17225
+ import { SiweMessage } from "siwe";
17221
17226
  import {
17222
17227
  fetchPeerId,
17223
17228
  submitHostDelegation,
@@ -17236,6 +17241,13 @@ import {
17236
17241
  canonicalizeAddress,
17237
17242
  makePkhSpaceId,
17238
17243
  pkhDid,
17244
+ extractImmutableSiweFields,
17245
+ diffImmutableSiweFields,
17246
+ extractRecapAttenuations,
17247
+ unauthorizedRecapCapabilities,
17248
+ parseCanonicalRecapResource,
17249
+ ACCOUNT_REGISTRY_SPACE,
17250
+ ACCOUNT_MANIFEST_PERMISSIONS,
17239
17251
  KV,
17240
17252
  SQL,
17241
17253
  DUCKDB,
@@ -17322,6 +17334,27 @@ var MemorySessionStorage = class {
17322
17334
  // src/authorization/NodeUserAuthorization.ts
17323
17335
  var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
17324
17336
  var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
17337
+ function canonicalizeOpenKeyManifestJson(value) {
17338
+ const json = JSON.stringify(value);
17339
+ if (json === void 0) {
17340
+ throw new TypeError("manifest is not JSON-serializable");
17341
+ }
17342
+ return canonicalizeJsonValue(JSON.parse(json));
17343
+ }
17344
+ function canonicalizeJsonValue(value) {
17345
+ if (value === null || typeof value !== "object") {
17346
+ return JSON.stringify(value);
17347
+ }
17348
+ if (Array.isArray(value)) {
17349
+ return `[${value.map(canonicalizeJsonValue).join(",")}]`;
17350
+ }
17351
+ return `{${Object.keys(value).sort().map(
17352
+ (key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
17353
+ ).join(",")}}`;
17354
+ }
17355
+ function canonicalOpenKeyManifestSha256Hex(value) {
17356
+ return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
17357
+ }
17325
17358
  function didPrincipalMatches(actual, expected) {
17326
17359
  try {
17327
17360
  return principalDidEquals(actual, expected);
@@ -17339,6 +17372,16 @@ function addRawAbility(rawAbilities, resource, action) {
17339
17372
  actions.push(action);
17340
17373
  }
17341
17374
  }
17375
+ function cloneAbilitiesMap(actions) {
17376
+ return Object.fromEntries(
17377
+ Object.entries(actions).map(([service, paths]) => [
17378
+ service,
17379
+ Object.fromEntries(
17380
+ Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
17381
+ )
17382
+ ])
17383
+ );
17384
+ }
17342
17385
  var NodeUserAuthorization = class {
17343
17386
  constructor(config) {
17344
17387
  this.activeSessionKeyId = "default";
@@ -17610,23 +17653,41 @@ var NodeUserAuthorization = class {
17610
17653
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
17611
17654
  const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
17612
17655
  const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
17613
- return {
17614
- abilities: this.defaultActions,
17615
- spaceId: primarySpaceId2,
17616
- spaceAbilities: {
17617
- [primarySpaceId2]: this.defaultActions,
17618
- [secretsSpaceId]: {
17619
- kv: {
17620
- "vault/secrets/": [
17621
- KV.GET,
17622
- KV.PUT,
17623
- KV.DEL,
17624
- KV.LIST,
17625
- KV.METADATA
17626
- ]
17656
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
17657
+ const spaceAbilities2 = {
17658
+ [primarySpaceId2]: primaryActions,
17659
+ [secretsSpaceId]: {
17660
+ kv: {
17661
+ "vault/secrets/": [
17662
+ KV.GET,
17663
+ KV.PUT,
17664
+ KV.DEL,
17665
+ KV.LIST,
17666
+ KV.METADATA
17667
+ ]
17668
+ }
17669
+ }
17670
+ };
17671
+ if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
17672
+ spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
17673
+ } else if (this.includeAccountRegistryPermissions) {
17674
+ const accountAbilities = resourceCapabilitiesToAbilitiesMap(
17675
+ ACCOUNT_MANIFEST_PERMISSIONS
17676
+ );
17677
+ for (const [service, paths] of Object.entries(accountAbilities)) {
17678
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
17679
+ for (const [path, actions] of Object.entries(paths)) {
17680
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
17681
+ for (const action of actions) {
17682
+ if (!existingActions.includes(action)) existingActions.push(action);
17627
17683
  }
17628
17684
  }
17629
- },
17685
+ }
17686
+ }
17687
+ return {
17688
+ abilities: primaryActions,
17689
+ spaceId: primarySpaceId2,
17690
+ spaceAbilities: spaceAbilities2,
17630
17691
  rawAbilities: {
17631
17692
  [defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
17632
17693
  }
@@ -18207,6 +18268,398 @@ var NodeUserAuthorization = class {
18207
18268
  await this.ensureSpaceExists();
18208
18269
  return clientSession;
18209
18270
  }
18271
+ /**
18272
+ * End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
18273
+ *
18274
+ * This is the production entry point that wires
18275
+ * {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
18276
+ * {@link signInWithOpenKeyResult} into one call. Consumers pass in a
18277
+ * caller-provided `authorizeFn` (typically a thin wrapper around
18278
+ * `openkey.authorizeTinyCloud()`) so this method has no direct
18279
+ * dependency on the OpenKey SDK — it works with any transport that
18280
+ * conforms to the versioned protocol.
18281
+ *
18282
+ * The `authorizeFn` MUST:
18283
+ * 1. Send the prepared SIWE bytes to OpenKey.
18284
+ * 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
18285
+ * `signedMessage` — the bytes the signature actually verifies
18286
+ * against — plus `selectedActionKeys` and `permissions`).
18287
+ *
18288
+ * NodeUserAuthorization then completes the session with `signedMessage`
18289
+ * (never the prepared bytes), verifying the signature, the immutable
18290
+ * SIWE fields, and every subset invariant enforced by
18291
+ * {@link signInWithOpenKeyResult}. This preserves the exact-byte
18292
+ * guarantee: if the user did NOT narrow, the returned `signedMessage`
18293
+ * equals the prepared SIWE byte-for-byte; if the user narrowed, only
18294
+ * the ReCap block differs and every immutable header field survives.
18295
+ *
18296
+ * Legacy exact-byte {@link signIn} callers are unaffected — this method
18297
+ * is opt-in.
18298
+ *
18299
+ * @param authorizeFn Callback that invokes the versioned OpenKey
18300
+ * `authorizeTinyCloud()` protocol and returns the result. Receives
18301
+ * the prepared SIWE bytes plus a `host` hint the widget should bind
18302
+ * into its /authorize-sign-prepare context.
18303
+ * @param options Optional overrides for the OpenKey request.
18304
+ */
18305
+ async signInWithOpenKey(authorizeFn, options) {
18306
+ if (options?.host !== void 0) {
18307
+ const overrideHost = options.host.trim();
18308
+ if (!overrideHost) {
18309
+ throw new Error("OpenKey authorization host must not be empty");
18310
+ }
18311
+ this.tinycloudHosts = [overrideHost];
18312
+ }
18313
+ const signingAddress = canonicalizeAddress(await this.signer.getAddress());
18314
+ const signingChainId = await this.signer.getChainId();
18315
+ await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
18316
+ const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
18317
+ const presentation = this.buildPresentationEnvelope(options?.reason);
18318
+ const hostHint = this.primaryTinyCloudHost;
18319
+ const result = await authorizeFn({
18320
+ protocolVersion: 1,
18321
+ siwe: prepared.siwe,
18322
+ jwk,
18323
+ host: hostHint,
18324
+ keyId: options?.openkeyKeyId,
18325
+ presentation
18326
+ });
18327
+ return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
18328
+ }
18329
+ /**
18330
+ * Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
18331
+ * envelope is display-only — the OpenKey widget renders it under an
18332
+ * honest trust label, and the server /authorize-sign-prepare route
18333
+ * decides whether to upgrade the label to `origin-bound` by fetching
18334
+ * the well-known manifest and comparing its digest to
18335
+ * `manifestDigest`.
18336
+ *
18337
+ * Returns `undefined` when no manifest is set — the widget renders as
18338
+ * "no manifest supplied" (unsigned).
18339
+ *
18340
+ * The digest is a deterministic SHA-256 over canonical JSON of the
18341
+ * first manifest payload; OpenKey parses the fetched well-known manifest
18342
+ * and applies the same canonicalization before comparing digests.
18343
+ */
18344
+ buildPresentationEnvelope(reason) {
18345
+ const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
18346
+ if (manifests.length === 0 && !reason) {
18347
+ return void 0;
18348
+ }
18349
+ const primary = manifests[0];
18350
+ let displayName;
18351
+ let manifestId;
18352
+ let manifestDigest;
18353
+ const envelopeManifests = [];
18354
+ for (const m of manifests) {
18355
+ envelopeManifests.push({
18356
+ name: m.name,
18357
+ appId: m.app_id,
18358
+ // Deep-copy to prevent later mutations from affecting the digest.
18359
+ payload: JSON.parse(JSON.stringify(m))
18360
+ });
18361
+ }
18362
+ if (primary) {
18363
+ displayName = primary.name;
18364
+ manifestId = primary.app_id;
18365
+ manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
18366
+ }
18367
+ return {
18368
+ protocolVersion: 1,
18369
+ displayName,
18370
+ reason: reason && reason.length > 0 ? reason : void 0,
18371
+ manifestId,
18372
+ manifestDigest,
18373
+ manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
18374
+ };
18375
+ }
18376
+ /**
18377
+ * Complete sign-in with a versioned OpenKey authorization result.
18378
+ *
18379
+ * Where `signInWithPreparedSession` completes with a signature over the
18380
+ * ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
18381
+ * OpenKey widget actually presented and signed (`signedMessage`). It
18382
+ * exists because the versioned authorizeTinyCloud protocol lets the user
18383
+ * narrow capabilities; the returned bytes may differ from the original
18384
+ * request and are the only ones the signature actually verifies against.
18385
+ *
18386
+ * The `result` parameter is validated at the trust boundary via
18387
+ * `validateAuthorizationResultV1` (in sdk-core). Callers should validate
18388
+ * BEFORE calling this method so unknown/missing fields are rejected
18389
+ * before any session state is created.
18390
+ *
18391
+ * Validation performed here (defence-in-depth: even a "valid" v1 payload
18392
+ * must clear these checks before any session state is created):
18393
+ * 1. `result.address` matches the local signer's address.
18394
+ * 2. `result.signature` recovers to `result.address` when verified against
18395
+ * `result.signedMessage`.
18396
+ * 3. Every immutable SIWE header field (domain, address, URI, version,
18397
+ * chainId, nonce, issuedAt) in the original prepared SIWE is preserved
18398
+ * byte-for-byte in `result.signedMessage`. `statement` is NOT in this
18399
+ * list because the WASM renders it from the ReCap contents; it MUST
18400
+ * change when the ReCap is narrowed.
18401
+ * 4. Every capability in `result.signedMessage`'s ReCap block was present
18402
+ * in the original prepared SIWE — narrowing OK, broadening rejected.
18403
+ * 5. Every `result.selectedActionKeys` entry corresponds to a
18404
+ * (resource, action) pair in the returned SIWE.
18405
+ */
18406
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
18407
+ if (result.protocolVersion !== 1) {
18408
+ throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
18409
+ }
18410
+ if (!result.signedMessage) {
18411
+ throw new Error("OpenKey result must include signedMessage");
18412
+ }
18413
+ if (!prepared.siwe) {
18414
+ throw new Error(
18415
+ "signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
18416
+ );
18417
+ }
18418
+ const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
18419
+ if (canonicalizeAddress(result.address) !== expectedAddress) {
18420
+ throw new Error(
18421
+ `OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
18422
+ );
18423
+ }
18424
+ let parsedSignedSiwe;
18425
+ try {
18426
+ parsedSignedSiwe = new SiweMessage(result.signedMessage);
18427
+ } catch (err) {
18428
+ throw new Error(
18429
+ `OpenKey signedMessage is not a parseable SIWE: ${err.message}`
18430
+ );
18431
+ }
18432
+ const verifyResult = await parsedSignedSiwe.verify(
18433
+ { signature: result.signature },
18434
+ { suppressExceptions: true }
18435
+ );
18436
+ if (!verifyResult.success) {
18437
+ const errData = verifyResult.error;
18438
+ const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
18439
+ const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
18440
+ throw new Error(
18441
+ `OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
18442
+ );
18443
+ }
18444
+ if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
18445
+ throw new Error(
18446
+ `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
18447
+ );
18448
+ }
18449
+ const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
18450
+ const originalFields = extractImmutableSiweFields(prepared.siwe);
18451
+ const signedFields = extractImmutableSiweFields(result.signedMessage);
18452
+ const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
18453
+ originalHasRecap
18454
+ });
18455
+ if (changedFields.length > 0) {
18456
+ throw new Error(
18457
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
18458
+ );
18459
+ }
18460
+ let originalCaps;
18461
+ let signedCaps;
18462
+ try {
18463
+ originalCaps = extractRecapAttenuations(prepared.siwe);
18464
+ } catch (err) {
18465
+ throw new Error(
18466
+ `Failed to decode prepared SIWE recap: ${err.message}`
18467
+ );
18468
+ }
18469
+ try {
18470
+ signedCaps = extractRecapAttenuations(result.signedMessage);
18471
+ } catch (err) {
18472
+ throw new Error(
18473
+ `Failed to decode OpenKey signedMessage recap: ${err.message}`
18474
+ );
18475
+ }
18476
+ const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
18477
+ if (unauthorized.length > 0) {
18478
+ const previewLimit = 5;
18479
+ const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
18480
+ const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
18481
+ throw new Error(
18482
+ `OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
18483
+ );
18484
+ }
18485
+ if (!result.signedMessage.includes(prepared.spaceId)) {
18486
+ throw new Error(
18487
+ `OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
18488
+ );
18489
+ }
18490
+ const grantedPairs = /* @__PURE__ */ new Set();
18491
+ const grantedFourPartIndex = /* @__PURE__ */ new Map();
18492
+ for (const [resource, actions] of Object.entries(signedCaps)) {
18493
+ const { space, path } = parseCanonicalRecapResource(resource);
18494
+ for (const action of Object.keys(actions)) {
18495
+ grantedPairs.add(`${resource}\0${action}`);
18496
+ const slashIdx = action.indexOf("/");
18497
+ const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
18498
+ if (service) {
18499
+ grantedFourPartIndex.set(
18500
+ `${service}\0${space}\0${path}\0${action}`,
18501
+ `${resource}\0${action}`
18502
+ );
18503
+ }
18504
+ }
18505
+ }
18506
+ const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
18507
+ const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
18508
+ for (const pair of grantedPairs) {
18509
+ const idx = pair.indexOf("\0");
18510
+ const action = idx >= 0 ? pair.slice(idx + 1) : pair;
18511
+ if (!isStructurallyRequired(action)) {
18512
+ nonRequiredGrantedPairs.add(pair);
18513
+ }
18514
+ }
18515
+ const allSignedGrantedPairs = new Set(grantedPairs);
18516
+ const seenRawKeys = /* @__PURE__ */ new Set();
18517
+ for (const rawKey of result.selectedActionKeys) {
18518
+ if (seenRawKeys.has(rawKey)) {
18519
+ throw new Error(
18520
+ `OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
18521
+ );
18522
+ }
18523
+ seenRawKeys.add(rawKey);
18524
+ }
18525
+ const selectedResourceActionPairs = /* @__PURE__ */ new Set();
18526
+ for (const rawKey of result.selectedActionKeys) {
18527
+ const parts = rawKey.split("\0");
18528
+ if (parts.length !== 4) {
18529
+ throw new Error(
18530
+ `OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
18531
+ );
18532
+ }
18533
+ const canonical = grantedFourPartIndex.get(rawKey);
18534
+ if (!canonical) {
18535
+ throw new Error(
18536
+ `OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
18537
+ );
18538
+ }
18539
+ selectedResourceActionPairs.add(canonical);
18540
+ }
18541
+ if (nonRequiredGrantedPairs.size > 0) {
18542
+ const missingPairs = [];
18543
+ for (const pair of nonRequiredGrantedPairs) {
18544
+ if (!selectedResourceActionPairs.has(pair)) {
18545
+ missingPairs.push(pair);
18546
+ }
18547
+ }
18548
+ if (missingPairs.length > 0) {
18549
+ const previewLimit = 5;
18550
+ const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18551
+ const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
18552
+ throw new Error(
18553
+ `OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
18554
+ );
18555
+ }
18556
+ }
18557
+ if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
18558
+ throw new Error(
18559
+ "OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
18560
+ );
18561
+ }
18562
+ const permissionCanonicalKeys = /* @__PURE__ */ new Set();
18563
+ const permissionPairsCovered = /* @__PURE__ */ new Set();
18564
+ for (const perm of result.permissions) {
18565
+ const { service, space, path, actions } = perm;
18566
+ if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
18567
+ throw new Error(
18568
+ `OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
18569
+ );
18570
+ }
18571
+ if (!Array.isArray(actions) || actions.length === 0) {
18572
+ throw new Error(
18573
+ `OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
18574
+ );
18575
+ }
18576
+ const canonicalKey = `${service}\0${space}\0${path}`;
18577
+ if (permissionCanonicalKeys.has(canonicalKey)) {
18578
+ throw new Error(
18579
+ `OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
18580
+ );
18581
+ }
18582
+ permissionCanonicalKeys.add(canonicalKey);
18583
+ let expectedResource;
18584
+ if (space.startsWith("tinycloud:")) {
18585
+ const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
18586
+ expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
18587
+ } else {
18588
+ if (path) {
18589
+ throw new Error(
18590
+ `OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
18591
+ );
18592
+ }
18593
+ expectedResource = space;
18594
+ }
18595
+ let matchedResource;
18596
+ if (signedCaps[expectedResource] !== void 0) {
18597
+ matchedResource = expectedResource;
18598
+ } else {
18599
+ throw new Error(
18600
+ `OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
18601
+ );
18602
+ }
18603
+ const grantedActionsForResource = new Set(
18604
+ Object.keys(signedCaps[matchedResource])
18605
+ );
18606
+ for (const action of actions) {
18607
+ if (typeof action !== "string" || !action) {
18608
+ throw new Error(
18609
+ `OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
18610
+ );
18611
+ }
18612
+ if (!grantedActionsForResource.has(action)) {
18613
+ throw new Error(
18614
+ `OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
18615
+ );
18616
+ }
18617
+ permissionPairsCovered.add(`${matchedResource}\0${action}`);
18618
+ }
18619
+ }
18620
+ if (allSignedGrantedPairs.size > 0) {
18621
+ const missingFromPerms = [];
18622
+ for (const pair of allSignedGrantedPairs) {
18623
+ if (!permissionPairsCovered.has(pair)) {
18624
+ missingFromPerms.push(pair);
18625
+ }
18626
+ }
18627
+ if (missingFromPerms.length > 0) {
18628
+ const previewLimit = 5;
18629
+ const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18630
+ const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
18631
+ throw new Error(
18632
+ `OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
18633
+ );
18634
+ }
18635
+ const extrasInPerms = [];
18636
+ for (const pair of permissionPairsCovered) {
18637
+ if (!allSignedGrantedPairs.has(pair)) {
18638
+ extrasInPerms.push(pair);
18639
+ }
18640
+ }
18641
+ if (extrasInPerms.length > 0) {
18642
+ const previewLimit = 5;
18643
+ const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
18644
+ const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
18645
+ throw new Error(
18646
+ `OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
18647
+ );
18648
+ }
18649
+ }
18650
+ void nonRequiredGrantedPairs;
18651
+ return this.signInWithPreparedSession(
18652
+ {
18653
+ siwe: result.signedMessage,
18654
+ jwk: prepared.jwk,
18655
+ spaceId: prepared.spaceId,
18656
+ verificationMethod: prepared.verificationMethod
18657
+ },
18658
+ result.signature,
18659
+ keyId,
18660
+ jwk
18661
+ );
18662
+ }
18210
18663
  /**
18211
18664
  * Clear persisted session data.
18212
18665
  */
@@ -18462,7 +18915,7 @@ function createWasmKeyProvider(sessionManager) {
18462
18915
  // src/delegateToHelpers.ts
18463
18916
  import {
18464
18917
  parseExpiry,
18465
- SiweMessage,
18918
+ SiweMessage as SiweMessage2,
18466
18919
  EXPIRY as EXPIRY2
18467
18920
  } from "@tinycloud/sdk-core";
18468
18921
  function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
@@ -18511,7 +18964,7 @@ function resolveExpiryMs(expiry) {
18511
18964
  return parseExpiry(expiry);
18512
18965
  }
18513
18966
  function extractSiweExpiration(siwe) {
18514
- const parsed = new SiweMessage(siwe);
18967
+ const parsed = new SiweMessage2(siwe);
18515
18968
  if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
18516
18969
  return void 0;
18517
18970
  }
@@ -18713,6 +19166,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
18713
19166
  var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
18714
19167
  var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
18715
19168
  var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
19169
+ var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
19170
+ var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
19171
+ var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
18716
19172
  var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
18717
19173
  function isSecretPayload(value) {
18718
19174
  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";
@@ -18755,6 +19211,9 @@ function decodeAuthorizationBytes(authorization) {
18755
19211
  function isOpenKeyAutoSignStrategy(strategy) {
18756
19212
  return strategy?.openKeyAutoSign === true;
18757
19213
  }
19214
+ function openKeyApproval(strategy) {
19215
+ return strategy?.openKeyRequestApproval;
19216
+ }
18758
19217
  function isInteractiveSigner(config) {
18759
19218
  if (config.privateKey) {
18760
19219
  return false;
@@ -19120,9 +19579,12 @@ var _TinyCloudNode = class _TinyCloudNode {
19120
19579
  this._bootstrapSkipped = false;
19121
19580
  /**
19122
19581
  * Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
19123
- * true when bootstrap did not complete (interactive signer, auto-sign
19124
- * denied, or a bootstrap step failed); `reason` carries the cause so apps
19125
- * can surface a "finish account setup" call-to-action. `warnings` is
19582
+ * true when bootstrap did not complete in this invocation (interactive signer,
19583
+ * auto-bootstrap disabled, already provisioned, or a decision/provisioning
19584
+ * step failed). `already-provisioned` means the bootstrap was not run here;
19585
+ * it does not mean a prior provisioning attempt failed to complete.
19586
+ * `reason` carries the cause so apps can surface a "finish account setup"
19587
+ * call-to-action. `warnings` is
19126
19588
  * present when bootstrap completed, but one or more steps recovered from
19127
19589
  * an ambiguous failure (e.g. a KV batch write reconciled via per-space
19128
19590
  * fallback) — clean vs. recovered runs are otherwise byte-identical.
@@ -19305,7 +19767,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19305
19767
  nonce: config.nonce,
19306
19768
  siweConfig: config.siweConfig,
19307
19769
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
19308
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19770
+ capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
19309
19771
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
19310
19772
  });
19311
19773
  this.tc = new TinyCloud(this.auth, {
@@ -19395,6 +19857,15 @@ var _TinyCloudNode = class _TinyCloudNode {
19395
19857
  get sessionDid() {
19396
19858
  return this.sessionManager.getDID(this.sessionKeyId);
19397
19859
  }
19860
+ /** Bare did:key principal used as the credential subject and holder binding. */
19861
+ get credentialHolderDid() {
19862
+ return this.sessionDid.split("#", 1)[0];
19863
+ }
19864
+ /** Canonical verification method for the active credential holder key. */
19865
+ get credentialHolderKid() {
19866
+ const holder = this.credentialHolderDid;
19867
+ return `${holder}#${holder.slice("did:key:".length)}`;
19868
+ }
19398
19869
  /**
19399
19870
  * Return the current session's signed ReCap capabilities after the session
19400
19871
  * has been authenticated or restored. This is intentionally distinct from
@@ -19445,7 +19916,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19445
19916
  if (!this._address) {
19446
19917
  return void 0;
19447
19918
  }
19448
- return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE);
19919
+ return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
19449
19920
  }
19450
19921
  /**
19451
19922
  * Account-level application and delegation helpers.
@@ -19548,6 +20019,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19548
20019
  this._bootstrapSkipped = false;
19549
20020
  this._bootstrapStatus = { skipped: false };
19550
20021
  if (this.config.autoBootstrapAccount === false) {
20022
+ this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
19551
20023
  return false;
19552
20024
  }
19553
20025
  if (!this.auth || !this._address) {
@@ -19562,11 +20034,14 @@ var _TinyCloudNode = class _TinyCloudNode {
19562
20034
  return false;
19563
20035
  }
19564
20036
  const steps = bootstrapSteps(this._address, this._chainId);
19565
- if (!await this.isFreshBootstrapAccount(steps)) {
19566
- return false;
19567
- }
19568
20037
  try {
19569
- const warnings = await this.runAccountBootstrap(steps);
20038
+ const decision = await this.resolveBootstrapDecision(steps);
20039
+ if (decision.action === "skip") {
20040
+ this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
20041
+ return false;
20042
+ }
20043
+ const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
20044
+ await this.writeBootstrapCompletionMarker(steps);
19570
20045
  if (warnings.length > 0) {
19571
20046
  this._bootstrapStatus = { skipped: false, warnings };
19572
20047
  this.notificationHandler.warning(
@@ -19585,32 +20060,92 @@ var _TinyCloudNode = class _TinyCloudNode {
19585
20060
  }
19586
20061
  return true;
19587
20062
  }
19588
- async isFreshBootstrapAccount(steps) {
20063
+ async resolveBootstrapDecision(steps) {
19589
20064
  const enshrinedSpaceIds = /* @__PURE__ */ new Set();
19590
20065
  for (const step of steps) {
19591
20066
  if (step.kind === "session") {
19592
20067
  enshrinedSpaceIds.add(step.spaceId);
19593
20068
  }
19594
20069
  }
19595
- const skipped = this.auth.lastActivationSkippedSpaceIds;
20070
+ const auth = this.auth;
20071
+ if (!auth) {
20072
+ throw new Error("Account bootstrap requires an active wallet session");
20073
+ }
20074
+ const skipped = auth.lastActivationSkippedSpaceIds;
19596
20075
  if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
19597
- return true;
20076
+ return { action: "run", mode: "fresh" };
19598
20077
  }
19599
20078
  try {
19600
- const indexed = await this.account.index.spaces.list();
19601
- if (indexed.ok && indexed.data.length === 0) {
19602
- return true;
20079
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
20080
+ const markerPermission = {
20081
+ service: "tinycloud.kv",
20082
+ space: accountSpaceId,
20083
+ path: BOOTSTRAP_COMPLETION_MARKER_KEY,
20084
+ actions: ["get"]
20085
+ };
20086
+ if (!this.hasRuntimePermissions([markerPermission])) {
20087
+ return { action: "run", mode: "repair" };
19603
20088
  }
19604
- } catch {
20089
+ const marker = await this.readBootstrapCompletionMarker();
20090
+ if (marker.ok) {
20091
+ if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
20092
+ return { action: "skip" };
20093
+ }
20094
+ return { action: "run", mode: "repair" };
20095
+ }
20096
+ if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
20097
+ return { action: "run", mode: "fresh" };
20098
+ }
20099
+ } catch (err) {
20100
+ const reason = err instanceof Error ? err.message : String(err);
20101
+ this.notificationHandler.warning(
20102
+ `Account bootstrap decision failed; attempting one bounded repair: ${reason}`
20103
+ );
20104
+ console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
20105
+ return { action: "run", mode: "repair" };
19605
20106
  }
19606
- try {
19607
- const spaces = await this.account.spaces.list();
19608
- return spaces.ok && spaces.data.length === 0;
19609
- } catch {
19610
- return false;
20107
+ return { action: "run", mode: "repair" };
20108
+ }
20109
+ async readBootstrapCompletionMarker() {
20110
+ return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
20111
+ BOOTSTRAP_COMPLETION_MARKER_KEY
20112
+ );
20113
+ }
20114
+ async writeBootstrapCompletionMarker(steps) {
20115
+ if (!this._address) {
20116
+ throw new Error("Bootstrap completion marker requires an active wallet session");
20117
+ }
20118
+ const canonicalStepIds = new Set(
20119
+ bootstrapSteps(this._address, this._chainId).map((step) => step.id)
20120
+ );
20121
+ const providedStepIds = new Set(steps.map((step) => step.id));
20122
+ if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
20123
+ throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
20124
+ }
20125
+ const marker = {
20126
+ v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
20127
+ stepIds: [...canonicalStepIds],
20128
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
20129
+ };
20130
+ const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
20131
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
20132
+ marker
20133
+ );
20134
+ if (!written.ok) {
20135
+ throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
19611
20136
  }
19612
20137
  }
19613
- async runAccountBootstrap(steps) {
20138
+ isAcceptedBootstrapCompletionMarker(value) {
20139
+ if (typeof value !== "object" || value === null) return false;
20140
+ const entries = Object.entries(value);
20141
+ const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
20142
+ const version23 = markerValue("v");
20143
+ return typeof version23 === "number" && Number.isInteger(version23) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version23);
20144
+ }
20145
+ markerReadIsUnhostedSpace(meta) {
20146
+ return meta?.status === 404;
20147
+ }
20148
+ async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
19614
20149
  if (!this.auth || !this._address) {
19615
20150
  throw new Error("Account bootstrap requires an active wallet session");
19616
20151
  }
@@ -19711,9 +20246,8 @@ var _TinyCloudNode = class _TinyCloudNode {
19711
20246
  if (step.kind === "seed-applications") {
19712
20247
  const registered = await this.account.applications.register(
19713
20248
  step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
19714
- // Bootstrap only runs on an account with no registry records, so the
19715
- // manifest-hash pre-read is definitionally a miss. Skip it; the write
19716
- // it guards is an INSERT OR REPLACE and is safe to repeat.
20249
+ // The write is an INSERT OR REPLACE and is safe to repeat during a
20250
+ // repair, so skip the manifest-hash pre-read.
19717
20251
  { assumeUnregistered: true }
19718
20252
  );
19719
20253
  if (!registered.ok) {
@@ -19721,7 +20255,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19721
20255
  }
19722
20256
  }
19723
20257
  if (step.kind === "encryption-network-create") {
19724
- await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
20258
+ await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
19725
20259
  }
19726
20260
  if (step.kind === "secret-records-schema") {
19727
20261
  const db = this.sqlForSpace(step.spaceId).db(step.database);
@@ -19904,7 +20438,7 @@ var _TinyCloudNode = class _TinyCloudNode {
19904
20438
  if (!this.auth || !this.signer) {
19905
20439
  throw new Error("Manifest registry write requires wallet mode");
19906
20440
  }
19907
- const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE);
20441
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
19908
20442
  await this.ensureOwnedSpaceHostedById(accountSpaceId);
19909
20443
  const result = await this.account.applications.register(request.manifests);
19910
20444
  if (!result.ok) {
@@ -20160,6 +20694,14 @@ var _TinyCloudNode = class _TinyCloudNode {
20160
20694
  }
20161
20695
  return hosted;
20162
20696
  }
20697
+ /** Resolve and authenticate the owner encoded by an owned-space URI. */
20698
+ credentialSpaceOwnerDid(spaceId) {
20699
+ const ownerDid = this.ownerDidFromSpaceId(spaceId);
20700
+ if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
20701
+ throw new Error("Credential space owner does not match the active TinyCloud owner");
20702
+ }
20703
+ return ownerDid;
20704
+ }
20163
20705
  /**
20164
20706
  * Check whether an owned space is already registered/hosted by consulting the
20165
20707
  * account spaces registry.
@@ -20703,6 +21245,66 @@ var _TinyCloudNode = class _TinyCloudNode {
20703
21245
  if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
20704
21246
  return signBytesWithJwk(bytes, session.jwk);
20705
21247
  }
21248
+ /** Apply only an already-enabled signing policy to the exact credential request. */
21249
+ async autoSignCredentialBytes(bytes) {
21250
+ const strategy = this.config.signStrategy;
21251
+ if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
21252
+ if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
21253
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21254
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21255
+ const request = {
21256
+ address: this.address ?? "",
21257
+ chainId: this.session?.chainId ?? this._chainId,
21258
+ message: new TextDecoder().decode(bytes),
21259
+ type: "message",
21260
+ purpose: "message"
21261
+ };
21262
+ const decision = await strategy.handler(request);
21263
+ if (!decision.approved) {
21264
+ if (decision.needsApproval) return void 0;
21265
+ throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
21266
+ }
21267
+ if (typeof decision.signature !== "string" || !await verifyEip191MessageSignature(
21268
+ request.message,
21269
+ decision.signature,
21270
+ request.address
21271
+ )) {
21272
+ throw new Error("OpenKey credential signature evidence is invalid");
21273
+ }
21274
+ return this.signSessionBytes(bytes);
21275
+ }
21276
+ /** Invoke the configured interactive approval strategy before session signing. */
21277
+ async approveCredentialBytes(bytes) {
21278
+ const strategy = this.config.signStrategy;
21279
+ const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
21280
+ if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
21281
+ const request = {
21282
+ address: this.address ?? "",
21283
+ chainId: this.session?.chainId ?? this._chainId,
21284
+ message: new TextDecoder().decode(bytes),
21285
+ type: "message",
21286
+ purpose: "message"
21287
+ };
21288
+ const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
21289
+ if (approval) {
21290
+ const decision = await approval(request);
21291
+ if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
21292
+ if (decision.signature !== void 0 && !await verifyEip191MessageSignature(
21293
+ request.message,
21294
+ decision.signature,
21295
+ request.address
21296
+ )) {
21297
+ throw new Error("OpenKey credential signature evidence is invalid");
21298
+ }
21299
+ return this.signSessionBytes(bytes);
21300
+ }
21301
+ if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
21302
+ if (this.config.signer) {
21303
+ await this.config.signer.signMessage(request.message);
21304
+ return this.signSessionBytes(bytes);
21305
+ }
21306
+ throw new Error("Interactive OpenKey approval is not configured");
21307
+ }
20706
21308
  /** Bind a session-signed invocation to the canonical service audience. */
20707
21309
  async bindInvocationAudience(authorization, audience) {
20708
21310
  const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
@@ -20770,7 +21372,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20770
21372
  nonce: this.config.nonce,
20771
21373
  siweConfig: this.config.siweConfig,
20772
21374
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20773
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21375
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20774
21376
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20775
21377
  });
20776
21378
  this.tc = new TinyCloud(this.auth, {
@@ -20823,7 +21425,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20823
21425
  nonce: this.config.nonce,
20824
21426
  siweConfig: this.config.siweConfig,
20825
21427
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
20826
- capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
21428
+ capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
20827
21429
  includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
20828
21430
  });
20829
21431
  this.tc = new TinyCloud(this.auth, {
@@ -23572,35 +24174,44 @@ var _TinyCloudNode = class _TinyCloudNode {
23572
24174
  if (parentDelegation.disableSubDelegation) {
23573
24175
  throw new Error("Parent delegation does not allow sub-delegation");
23574
24176
  }
23575
- if (!params.path.startsWith(parentDelegation.path)) {
24177
+ const parentResources = parentDelegation.resources ?? [{
24178
+ service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24179
+ space: parentDelegation.spaceId,
24180
+ path: parentDelegation.path,
24181
+ actions: parentDelegation.actions
24182
+ }];
24183
+ const requestedResources = params.resources ?? [{
24184
+ service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
24185
+ space: parentDelegation.spaceId,
24186
+ path: params.path,
24187
+ actions: params.actions
24188
+ }];
24189
+ const resourceIsContained = (parent, child) => {
24190
+ const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
24191
+ const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
24192
+ if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
24193
+ const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
24194
+ return pathContained && child.actions.every((action) => parent.actions.includes(action));
24195
+ };
24196
+ if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
23576
24197
  throw new Error(
23577
- `Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`
24198
+ "Sub-delegation resources exceed the received delegation"
23578
24199
  );
23579
24200
  }
23580
- const parentActions = new Set(parentDelegation.actions);
23581
- for (const action of params.actions) {
23582
- if (!parentActions.has(action)) {
23583
- throw new Error(
23584
- `Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
23585
- );
23586
- }
23587
- }
23588
24201
  const now = /* @__PURE__ */ new Date();
23589
24202
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
23590
24203
  const requestedExpiry = new Date(now.getTime() + expiryMs);
23591
24204
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
23592
24205
  const abilities = {};
23593
- const kvActions = params.actions.filter((a) => a.startsWith("tinycloud.kv/"));
23594
- const sqlActions = params.actions.filter((a) => a.startsWith("tinycloud.sql/"));
23595
- const duckdbActions = params.actions.filter((a) => a.startsWith("tinycloud.duckdb/"));
23596
- if (kvActions.length > 0) {
23597
- abilities.kv = { [params.path]: kvActions };
23598
- }
23599
- if (sqlActions.length > 0) {
23600
- abilities.sql = { [params.path]: sqlActions };
23601
- }
23602
- if (duckdbActions.length > 0) {
23603
- abilities.duckdb = { [params.path]: duckdbActions };
24206
+ const rawAbilities = {};
24207
+ for (const resource of requestedResources) {
24208
+ const service = resource.service.replace(/^tinycloud\./, "");
24209
+ if (service === "encryption") {
24210
+ rawAbilities[resource.path] = [...resource.actions];
24211
+ continue;
24212
+ }
24213
+ abilities[service] ?? (abilities[service] = {});
24214
+ abilities[service][resource.path] = [...resource.actions];
23604
24215
  }
23605
24216
  const targetHost = parentDelegation.host ?? this.config.host;
23606
24217
  const prepared = this.wasmBindings.prepareSession({
@@ -23612,7 +24223,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23612
24223
  expirationTime: actualExpiry.toISOString(),
23613
24224
  spaceId: parentDelegation.spaceId,
23614
24225
  delegateUri: params.delegateDID,
23615
- parents: [parentDelegation.cid]
24226
+ parents: [parentDelegation.cid],
24227
+ ...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
23616
24228
  });
23617
24229
  const signature2 = await this.signer.signMessage(prepared.siwe);
23618
24230
  const subDelegationSession = this.wasmBindings.completeSessionSetup({
@@ -23630,14 +24242,20 @@ var _TinyCloudNode = class _TinyCloudNode {
23630
24242
  cid: subDelegationSession.delegationCid,
23631
24243
  delegationHeader: subDelegationSession.delegationHeader,
23632
24244
  spaceId: parentDelegation.spaceId,
23633
- path: params.path,
23634
- actions: params.actions,
24245
+ path: requestedResources[0].path,
24246
+ actions: requestedResources[0].actions,
23635
24247
  disableSubDelegation: params.disableSubDelegation ?? false,
23636
24248
  expiry: actualExpiry,
23637
24249
  delegateDID: params.delegateDID,
23638
24250
  ownerAddress: parentDelegation.ownerAddress,
23639
24251
  chainId: parentDelegation.chainId,
23640
- host: targetHost
24252
+ host: targetHost,
24253
+ resources: requestedResources.map((resource) => ({
24254
+ service: resource.service.replace(/^tinycloud\./, ""),
24255
+ space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
24256
+ path: resource.path,
24257
+ actions: [...resource.actions]
24258
+ }))
23641
24259
  };
23642
24260
  }
23643
24261
  };
@@ -23810,10 +24428,54 @@ var FileSessionStorage = class {
23810
24428
  }
23811
24429
  };
23812
24430
 
24431
+ // src/authorization/openKeyBridge.ts
24432
+ function wireOpenKeyAuthorize(openkey) {
24433
+ return async (input) => {
24434
+ const result = await openkey.authorizeTinyCloud({
24435
+ protocolVersion: 1,
24436
+ siwe: input.siwe,
24437
+ keyId: input.keyId,
24438
+ jwk: input.jwk,
24439
+ host: input.host,
24440
+ // Forward the presentation envelope verbatim — the OpenKey SDK
24441
+ // validates and size-bounds it before use.
24442
+ presentation: input.presentation
24443
+ });
24444
+ if (result.protocolVersion !== 1) {
24445
+ throw new Error(
24446
+ `wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
24447
+ );
24448
+ }
24449
+ if (typeof result.signedMessage !== "string" || !result.signedMessage) {
24450
+ throw new Error(
24451
+ "wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
24452
+ );
24453
+ }
24454
+ if (!Array.isArray(result.selectedActionKeys)) {
24455
+ throw new Error(
24456
+ "wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
24457
+ );
24458
+ }
24459
+ if (!Array.isArray(result.permissions)) {
24460
+ throw new Error(
24461
+ "wireOpenKeyAuthorize: OpenKey returned no permissions"
24462
+ );
24463
+ }
24464
+ return {
24465
+ protocolVersion: 1,
24466
+ address: result.address,
24467
+ signature: result.signature,
24468
+ signedMessage: result.signedMessage,
24469
+ selectedActionKeys: result.selectedActionKeys,
24470
+ permissions: result.permissions
24471
+ };
24472
+ };
24473
+ }
24474
+
23813
24475
  // src/index.ts
23814
24476
  import {
23815
24477
  ACCOUNT_REGISTRY_PATH,
23816
- ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
24478
+ ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
23817
24479
  DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
23818
24480
  DEFAULT_MANIFEST_VERSION,
23819
24481
  VAULT_PERMISSION_SERVICE,
@@ -23921,6 +24583,14 @@ function signedAuthorityFromCompactUcan(authorization) {
23921
24583
  if (typeof payload.aud !== "string" || payload.aud.length === 0) {
23922
24584
  throw new Error("Validated runtime delegation is missing a signed audience.");
23923
24585
  }
24586
+ if (typeof payload.iss !== "string" || payload.iss.length === 0) {
24587
+ throw new Error("Validated runtime delegation is missing a signed issuer.");
24588
+ }
24589
+ if (!Array.isArray(payload.prf) || payload.prf.some(
24590
+ (proof) => typeof proof !== "string" || proof.length === 0
24591
+ )) {
24592
+ throw new Error("Validated runtime delegation has malformed signed proofs.");
24593
+ }
23924
24594
  if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
23925
24595
  throw new Error("Validated runtime delegation is missing a signed expiry.");
23926
24596
  }
@@ -23931,6 +24601,11 @@ function signedAuthorityFromCompactUcan(authorization) {
23931
24601
  if (payload.att === null || typeof payload.att !== "object" || Array.isArray(payload.att)) {
23932
24602
  throw new Error("Validated runtime delegation is missing a signed attenuation.");
23933
24603
  }
24604
+ const fact = Array.isArray(payload.fct) && payload.fct.length > 0 && payload.fct[0] !== null && typeof payload.fct[0] === "object" && !Array.isArray(payload.fct[0]) ? payload.fct[0] : void 0;
24605
+ const remainingRedelegationDepth = fact?.remainingRedelegationDepth;
24606
+ if (remainingRedelegationDepth !== void 0 && (!Number.isInteger(remainingRedelegationDepth) || remainingRedelegationDepth < 0 || remainingRedelegationDepth > 8)) {
24607
+ throw new Error("Validated runtime delegation has invalid signed redelegation depth.");
24608
+ }
23934
24609
  const permissions = [];
23935
24610
  for (const [resource, abilities] of Object.entries(payload.att)) {
23936
24611
  if (abilities === null || typeof abilities !== "object" || Array.isArray(abilities)) {
@@ -23981,8 +24656,11 @@ function signedAuthorityFromCompactUcan(authorization) {
23981
24656
  ...permission.caveats === void 0 ? {} : { caveats: permission.caveats }
23982
24657
  }));
23983
24658
  return {
24659
+ issuer: payload.iss,
23984
24660
  audience: payload.aud,
23985
24661
  expiry,
24662
+ proofs: [...payload.prf],
24663
+ ...remainingRedelegationDepth === void 0 ? {} : { remainingRedelegationDepth },
23986
24664
  permissions: canonicalPermissions,
23987
24665
  resources: canonicalResources
23988
24666
  };
@@ -24073,6 +24751,13 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
24073
24751
  delegationHeader: { Authorization: authorization },
24074
24752
  ownerAddress: delegation.ownerAddress,
24075
24753
  chainId: delegation.chainId,
24754
+ delegatorDID: signed2.issuer,
24755
+ ...signed2.proofs.length === 1 ? { parentCid: signed2.proofs[0] } : {},
24756
+ ...signed2.remainingRedelegationDepth === void 0 ? {} : {
24757
+ allowSubDelegation: signed2.remainingRedelegationDepth > 0,
24758
+ disableSubDelegation: signed2.remainingRedelegationDepth === 0
24759
+ },
24760
+ authHeader: authorization,
24076
24761
  spaceId: signedSpace ?? delegation.spaceId,
24077
24762
  path: primary.path,
24078
24763
  actions: [...primary.actions],
@@ -24247,10 +24932,13 @@ import {
24247
24932
  } from "@tinycloud/sdk-core";
24248
24933
  import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
24249
24934
  export {
24935
+ ACCEPTED_MARKER_VERSIONS,
24250
24936
  ACCOUNT_REGISTRY_PATH,
24251
- ACCOUNT_REGISTRY_SPACE2 as ACCOUNT_REGISTRY_SPACE,
24937
+ ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
24252
24938
  AccountService,
24253
24939
  AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
24940
+ BOOTSTRAP_COMPLETION_MARKER_KEY,
24941
+ BOOTSTRAP_COMPLETION_MARKER_VERSION,
24254
24942
  CapabilityKeyRegistry2 as CapabilityKeyRegistry,
24255
24943
  CapabilityKeyRegistryErrorCodes,
24256
24944
  CaveatedDelegationUnsupportedError2 as CaveatedDelegationUnsupportedError,
@@ -24387,7 +25075,8 @@ export {
24387
25075
  tinyCloudDebugLogger,
24388
25076
  validateEnvelope,
24389
25077
  validateManifest,
24390
- verifyDecryptResponse
25078
+ verifyDecryptResponse,
25079
+ wireOpenKeyAuthorize
24391
25080
  };
24392
25081
  /*! Bundled license information:
24393
25082