@tinycloud/node-sdk 3.8.0-beta.1 → 3.8.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.
Files changed (39) hide show
  1. package/dist/{TinyCloudNode-Ckqu_fqH.d.ts → TinyCloudNode-BVDUdEcf.d.cts} +53 -5
  2. package/dist/{TinyCloudNode-DDfx8Fx5.d.cts → TinyCloudNode-jMK_2Uj8.d.ts} +53 -5
  3. package/dist/{authority-BwM_s3Ya.d.cts → authority-DJ-PE4Xo.d.cts} +20 -4
  4. package/dist/{authority-DcyJpajL.d.ts → authority-i22vHbbp.d.ts} +20 -4
  5. package/dist/core.cjs +300 -32
  6. package/dist/core.cjs.map +1 -1
  7. package/dist/core.d.cts +5 -4
  8. package/dist/core.d.ts +5 -4
  9. package/dist/core.js +292 -27
  10. package/dist/core.js.map +1 -1
  11. package/dist/credentials-core.d.cts +1 -1
  12. package/dist/credentials-core.d.ts +1 -1
  13. package/dist/credentials.d.cts +1 -1
  14. package/dist/credentials.d.ts +1 -1
  15. package/dist/{delegation-0LKZkmP2.d.cts → delegation-Ckc3UETl.d.cts} +1 -1
  16. package/dist/{delegation-0LKZkmP2.d.ts → delegation-Ckc3UETl.d.ts} +1 -1
  17. package/dist/encryption-keys.d.cts +2 -2
  18. package/dist/encryption-keys.d.ts +2 -2
  19. package/dist/index.cjs +304 -44
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +4 -3
  22. package/dist/index.d.ts +4 -3
  23. package/dist/index.js +291 -31
  24. package/dist/index.js.map +1 -1
  25. package/dist/replication/authority.cjs +39 -0
  26. package/dist/replication/authority.cjs.map +1 -1
  27. package/dist/replication/authority.d.cts +4 -3
  28. package/dist/replication/authority.d.ts +4 -3
  29. package/dist/replication/authority.js +38 -0
  30. package/dist/replication/authority.js.map +1 -1
  31. package/dist/replication/runtime.cjs +1 -0
  32. package/dist/replication/runtime.cjs.map +1 -1
  33. package/dist/replication/runtime.d.cts +1 -1
  34. package/dist/replication/runtime.d.ts +1 -1
  35. package/dist/replication/runtime.js +1 -0
  36. package/dist/replication/runtime.js.map +1 -1
  37. package/dist/{runtime-v62FzQHz.d.ts → runtime-CO4VXnPq.d.ts} +2 -1
  38. package/dist/{runtime-D1o9Eybt.d.cts → runtime-mRhSu-vJ.d.cts} +2 -1
  39. package/package.json +4 -4
package/dist/core.cjs CHANGED
@@ -58,6 +58,7 @@ __export(core_exports, {
58
58
  PermissionNotInManifestError: () => import_sdk_core14.PermissionNotInManifestError,
59
59
  PrefixedKVService: () => import_sdk_core15.PrefixedKVService,
60
60
  ProtocolMismatchError: () => import_sdk_core22.ProtocolMismatchError,
61
+ RuntimeDelegationRejectedError: () => RuntimeDelegationRejectedError,
61
62
  SECRET_NAME_RE: () => import_sdk_core18.SECRET_NAME_RE,
62
63
  SQLAction: () => import_sdk_core16.SQLAction,
63
64
  SQLService: () => import_sdk_core16.SQLService,
@@ -80,6 +81,7 @@ __export(core_exports, {
80
81
  VersionCheckError: () => import_sdk_core22.VersionCheckError,
81
82
  WasmKeyProvider: () => WasmKeyProvider,
82
83
  activateCompactRuntimeDelegation: () => activateCompactRuntimeDelegation,
84
+ activateValidatedRuntimeDelegation: () => activateValidatedRuntimeDelegation,
83
85
  addressStorageKey: () => import_sdk_core13.addressStorageKey,
84
86
  buildSpaceUri: () => import_sdk_core21.buildSpaceUri,
85
87
  canonicalizeAddress: () => import_sdk_core13.canonicalizeAddress,
@@ -130,7 +132,8 @@ __export(core_exports, {
130
132
  resourceCapabilitiesToSpaceAbilitiesMap: () => import_sdk_core14.resourceCapabilitiesToSpaceAbilitiesMap,
131
133
  serializeDelegation: () => serializeDelegation,
132
134
  tinyCloudDebugLogger: () => import_sdk_core13.tinyCloudDebugLogger,
133
- validateManifest: () => import_sdk_core14.validateManifest
135
+ validateManifest: () => import_sdk_core14.validateManifest,
136
+ wireOpenKeyAuthorize: () => wireOpenKeyAuthorize
134
137
  });
135
138
  module.exports = __toCommonJS(core_exports);
136
139
  var import_sdk_core12 = require("@tinycloud/sdk-core");
@@ -345,6 +348,32 @@ var FileSessionStorage = class {
345
348
  var import_node_crypto = require("crypto");
346
349
  var import_siwe = require("siwe");
347
350
  var import_sdk_core5 = require("@tinycloud/sdk-core");
351
+ var import_resource = require("@tinycloud/sdk-services/encryption/resource");
352
+ var import_sdk_services2 = require("@tinycloud/sdk-services");
353
+
354
+ // src/authorization/appKeyHostBinding.ts
355
+ var DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
356
+ var IPV4 = /^\d+\.\d+\.\d+\.\d+$/;
357
+ var APP_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
358
+ function appKeyHostBinding(domain, appId) {
359
+ if (!APP_ID.test(appId)) return false;
360
+ if (domain.endsWith(":")) return false;
361
+ let url;
362
+ try {
363
+ url = new URL(`https://${domain}`);
364
+ } catch {
365
+ return false;
366
+ }
367
+ if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "") return false;
368
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
369
+ const host = url.hostname;
370
+ if (host.startsWith("[") || host.endsWith(".") || IPV4.test(host)) return false;
371
+ const labels = host.split(".");
372
+ if (labels.length < 2 || labels[labels.length - 1] === "localhost") return false;
373
+ if (!labels.every((label) => DOMAIN_LABEL.test(label))) return false;
374
+ const reversed = labels.reverse().join(".");
375
+ return appId === reversed || appId.startsWith(`${reversed}.`);
376
+ }
348
377
 
349
378
  // src/replication/authority-sign-in.ts
350
379
  var import_sdk_core2 = require("@tinycloud/sdk-core");
@@ -476,6 +505,7 @@ function cloneAbilitiesMap(actions) {
476
505
  var NodeUserAuthorization = class {
477
506
  constructor(config) {
478
507
  this.activeSessionKeyId = "default";
508
+ this.preparedSessionCounter = 0;
479
509
  /** Incremented by resetSession; a sign-in started earlier publishes nothing. */
480
510
  this.sessionGeneration = 0;
481
511
  /**
@@ -832,6 +862,23 @@ var NodeUserAuthorization = class {
832
862
  );
833
863
  return this._capabilityRequest;
834
864
  }
865
+ async refreshNamedKeyFeatureForSignIn(address, chainId) {
866
+ const request = this.getCapabilityRequest();
867
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
868
+ const hasNamedKeys = request?.manifests.some((manifest) => manifest.keys?.length && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id));
869
+ if (hasNamedKeys) {
870
+ await this.resolveTinyCloudHostsForSignIn(address, chainId);
871
+ await this.refreshNodeInfo();
872
+ }
873
+ }
874
+ /** Manifest key paths that this sign-in cannot grant. */
875
+ get unavailableKeyPaths() {
876
+ const request = this.getCapabilityRequest();
877
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
878
+ return (request?.manifests ?? []).flatMap(
879
+ (manifest) => this._nodeFeatures.includes("encryption-named-keys-v1") && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id) ? [] : (manifest.keys ?? []).map((entry) => "path" in entry ? entry.path : entry.prefix)
880
+ );
881
+ }
835
882
  resolveSpaceName(space, address, chainId) {
836
883
  if (space.startsWith("tinycloud:")) {
837
884
  return space;
@@ -1023,15 +1070,31 @@ var NodeUserAuthorization = class {
1023
1070
  }
1024
1071
  const rawAbilities = {};
1025
1072
  const currentDid = (0, import_sdk_core5.pkhDid)(address, chainId);
1073
+ if (this._nodeFeatures.includes("encryption-named-keys-v1")) {
1074
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
1075
+ for (const manifest of request.manifests) {
1076
+ if (!appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id)) continue;
1077
+ for (const declaration of manifest.keys ?? []) {
1078
+ const path = "path" in declaration ? declaration.path : declaration.prefix;
1079
+ const resource = `urn:tinycloud:encryption:${currentDid}:default/${path}`;
1080
+ for (const ability of declaration.abilities) {
1081
+ addRawAbility(rawAbilities, resource, `tinycloud.encryption/${ability}`);
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1026
1086
  const spaceResources = request.resources.filter((entry) => {
1027
1087
  if (entry.service !== import_sdk_core5.ENCRYPTION_PERMISSION_SERVICE) {
1028
1088
  return true;
1029
1089
  }
1090
+ const parsed = entry.path.includes("/") ? (0, import_resource.parseEncryptionResource)(entry.path) : { ...(0, import_sdk_services2.parseNetworkId)(entry.path), kind: "network" };
1091
+ if (parsed.kind !== "network") {
1092
+ throw new import_sdk_core5.ManifestValidationError("named keys go in manifest.keys");
1093
+ }
1030
1094
  for (const action of entry.actions) {
1031
1095
  addRawAbility(rawAbilities, entry.path, action);
1032
1096
  }
1033
- if (entry.actions.includes(DECRYPT_ACTION)) {
1034
- const parsed = (0, import_sdk_core5.parseNetworkId)(entry.path);
1097
+ if (entry.actions.includes(DECRYPT_ACTION) && parsed.kind === "network") {
1035
1098
  if (didPrincipalMatches(parsed.ownerDid, currentDid)) {
1036
1099
  addRawAbility(rawAbilities, entry.path, NETWORK_CREATE_ACTION);
1037
1100
  }
@@ -1427,6 +1490,8 @@ var NodeUserAuthorization = class {
1427
1490
  this._chainId = chainId;
1428
1491
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
1429
1492
  this.assertSignInCurrent(generation);
1493
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
1494
+ this.assertSignInCurrent(generation);
1430
1495
  const keyId = `session-${Date.now()}`;
1431
1496
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
1432
1497
  this.activeSessionKeyId = keyId;
@@ -1573,9 +1638,9 @@ var NodeUserAuthorization = class {
1573
1638
  refuseCaveated(this.getCapabilityRequest()?.manifests);
1574
1639
  const address = (0, import_sdk_core5.canonicalizeAddress)(await this.signer.getAddress());
1575
1640
  const chainId = await this.signer.getChainId();
1576
- const keyId = `session-${Date.now()}`;
1577
- this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
1578
- this.activeSessionKeyId = keyId;
1641
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
1642
+ const keyId = `openkey-session-${Date.now()}-${++this.preparedSessionCounter}`;
1643
+ this.sessionManager.createSessionKey(keyId);
1579
1644
  const jwkString = this.sessionManager.jwk(keyId);
1580
1645
  if (!jwkString) {
1581
1646
  throw new Error("Failed to create session key");
@@ -1656,6 +1721,7 @@ var NodeUserAuthorization = class {
1656
1721
  await this.sessionStorage.save(address, persistedData);
1657
1722
  this._session = clientSession;
1658
1723
  this._tinyCloudSession = tinyCloudSession;
1724
+ this.activeSessionKeyId = keyId;
1659
1725
  this._address = address;
1660
1726
  this._chainId = chainId;
1661
1727
  await this.refreshNodeInfo();
@@ -1702,6 +1768,11 @@ var NodeUserAuthorization = class {
1702
1768
  * @param options Optional overrides for the OpenKey request.
1703
1769
  */
1704
1770
  async signInWithOpenKey(authorizeFn, options) {
1771
+ const pending = await this.prepareOpenKeySignIn(options);
1772
+ return this.completeOpenKeySignIn(pending, authorizeFn(pending.request), options);
1773
+ }
1774
+ /** Finish address, host and SIWE preparation before an approval click. */
1775
+ async prepareOpenKeySignIn(options) {
1705
1776
  refuseCaveated(this.getCapabilityRequest()?.manifests);
1706
1777
  if (options?.host !== void 0) {
1707
1778
  const overrideHost = options.host.trim();
@@ -1713,7 +1784,7 @@ var NodeUserAuthorization = class {
1713
1784
  const signingAddress = (0, import_sdk_core5.canonicalizeAddress)(await this.signer.getAddress());
1714
1785
  const signingChainId = await this.signer.getChainId();
1715
1786
  await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
1716
- const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
1787
+ const { prepared, keyId, jwk, address, chainId } = await this.prepareSessionForSigning();
1717
1788
  const presentation = this.buildPresentationEnvelope(options?.reason);
1718
1789
  const hostHint = this.primaryTinyCloudHost;
1719
1790
  const originalRequest = {
@@ -1725,12 +1796,32 @@ var NodeUserAuthorization = class {
1725
1796
  keyId: options?.openkeyKeyId,
1726
1797
  presentation
1727
1798
  };
1799
+ return {
1800
+ request: originalRequest,
1801
+ prepared,
1802
+ keyId,
1803
+ jwk,
1804
+ address,
1805
+ chainId,
1806
+ generation: this.sessionGeneration,
1807
+ createdAt: Date.now()
1808
+ };
1809
+ }
1810
+ /** Complete a prepared approval. The caller starts authorization before awaiting this method. */
1811
+ async completeOpenKeySignIn(pending, authorization, options) {
1812
+ if (Date.now() - pending.createdAt > 5 * 6e4) {
1813
+ throw new Error("Prepared OpenKey sign-in expired; prepare again");
1814
+ }
1815
+ if (pending.generation !== this.sessionGeneration) {
1816
+ throw new Error("Prepared OpenKey sign-in expired after sign-out");
1817
+ }
1818
+ const { request: originalRequest, prepared, keyId, jwk } = pending;
1728
1819
  let result;
1729
1820
  let signed;
1730
1821
  let signature;
1731
1822
  for (let attempt = 0; ; ) {
1732
1823
  try {
1733
- result ?? (result = await authorizeFn(originalRequest));
1824
+ result ?? (result = await authorization);
1734
1825
  signed = await this.verifyOpenKeyResult(result, prepared);
1735
1826
  signature = result.signature;
1736
1827
  break;
@@ -2151,8 +2242,52 @@ var NodeUserAuthorization = class {
2151
2242
  }
2152
2243
  };
2153
2244
 
2245
+ // src/authorization/openKeyBridge.ts
2246
+ function wireOpenKeyAuthorize(openkey, opts) {
2247
+ return async (input) => {
2248
+ const result = await openkey.authorizeTinyCloud({
2249
+ protocolVersion: 1,
2250
+ siwe: input.siwe,
2251
+ keyId: input.keyId,
2252
+ jwk: input.jwk,
2253
+ host: input.host,
2254
+ // Forward the presentation envelope verbatim — the OpenKey SDK
2255
+ // validates and size-bounds it before use.
2256
+ presentation: input.presentation
2257
+ }, opts);
2258
+ if (result.protocolVersion !== 1) {
2259
+ throw new Error(
2260
+ `wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
2261
+ );
2262
+ }
2263
+ if (typeof result.signedMessage !== "string" || !result.signedMessage) {
2264
+ throw new Error(
2265
+ "wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
2266
+ );
2267
+ }
2268
+ if (!Array.isArray(result.selectedActionKeys)) {
2269
+ throw new Error(
2270
+ "wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
2271
+ );
2272
+ }
2273
+ if (!Array.isArray(result.permissions)) {
2274
+ throw new Error(
2275
+ "wireOpenKeyAuthorize: OpenKey returned no permissions"
2276
+ );
2277
+ }
2278
+ return {
2279
+ protocolVersion: 1,
2280
+ address: result.address,
2281
+ signature: result.signature,
2282
+ signedMessage: result.signedMessage,
2283
+ selectedActionKeys: result.selectedActionKeys,
2284
+ permissions: result.permissions
2285
+ };
2286
+ };
2287
+ }
2288
+
2154
2289
  // src/recordingServiceContext.ts
2155
- var import_sdk_services2 = require("@tinycloud/sdk-services");
2290
+ var import_sdk_services3 = require("@tinycloud/sdk-services");
2156
2291
  var import_requestLog = require("@tinycloud/sdk-services/telemetry/requestLog");
2157
2292
 
2158
2293
  // src/recordingRegistry.ts
@@ -2197,9 +2332,9 @@ function replicationEvent(sample, op) {
2197
2332
  }
2198
2333
  function createServiceContext(config) {
2199
2334
  const { requestSink, ...base } = config;
2200
- return requestSink === void 0 ? new import_sdk_services2.ServiceContext(base) : new RecordingServiceContext(base, requestSink);
2335
+ return requestSink === void 0 ? new import_sdk_services3.ServiceContext(base) : new RecordingServiceContext(base, requestSink);
2201
2336
  }
2202
- var RecordingServiceContext = class extends import_sdk_services2.ServiceContext {
2337
+ var RecordingServiceContext = class extends import_sdk_services3.ServiceContext {
2203
2338
  constructor(config, sink) {
2204
2339
  const pending = {};
2205
2340
  super({
@@ -2793,8 +2928,9 @@ function deserializeDelegation(data) {
2793
2928
  // src/TinyCloudNode.ts
2794
2929
  var import_siwe2 = require("siwe");
2795
2930
  var import_sdk_core11 = require("@tinycloud/sdk-core");
2796
- var import_sdk_services3 = require("@tinycloud/sdk-services");
2931
+ var import_sdk_services4 = require("@tinycloud/sdk-services");
2797
2932
  var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
2933
+ var import_resource2 = require("@tinycloud/sdk-services/encryption/resource");
2798
2934
 
2799
2935
  // src/account/AccountService.ts
2800
2936
  var import_sdk_core7 = require("@tinycloud/sdk-core");
@@ -3271,6 +3407,7 @@ var ROOT_DELEGATION_ACTIONS = [
3271
3407
  import_sdk_core11.DUCKDB.ALL
3272
3408
  ];
3273
3409
  var DEFAULT_SESSION_EXPIRATION_MS = import_sdk_core11.EXPIRY.SESSION_MS;
3410
+ var OWNER_REVOCATION_WINDOW_MS = 10 * 6e4;
3274
3411
  function unifiedRootAttenuation(capabilities) {
3275
3412
  const attenuation = {};
3276
3413
  for (const capability of capabilities) {
@@ -3735,6 +3872,7 @@ function replicaRestoreRefusal(error) {
3735
3872
  case "purge_pending":
3736
3873
  case "logout_pending":
3737
3874
  case "gate_unavailable":
3875
+ case "wallet_provider_required":
3738
3876
  return error.gate;
3739
3877
  }
3740
3878
  }
@@ -3767,8 +3905,10 @@ var _TinyCloudNode = class _TinyCloudNode {
3767
3905
  constructor(config = {}) {
3768
3906
  this.signer = null;
3769
3907
  this.auth = null;
3908
+ this.consumedOpenKeyPendings = /* @__PURE__ */ new WeakSet();
3770
3909
  this.tc = null;
3771
3910
  this._chainId = 1;
3911
+ this.replicaPurgeFailedHandlers = /* @__PURE__ */ new Set();
3772
3912
  /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
3773
3913
  this.establishReplicaStamp = async (session) => {
3774
3914
  const replication = this.config.replication;
@@ -3800,6 +3940,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3800
3940
  return void 0;
3801
3941
  }
3802
3942
  };
3943
+ this._keysUnavailable = [];
3803
3944
  this._baseVaults = /* @__PURE__ */ new Map();
3804
3945
  this._baseSecrets = /* @__PURE__ */ new Map();
3805
3946
  this._secrets = /* @__PURE__ */ new Map();
@@ -3975,6 +4116,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3975
4116
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
3976
4117
  },
3977
4118
  clearPending: async () => (await runtime).control.clearPending(),
4119
+ purgeLocalCopy: async () => (await this.eraseReplicaLocalCopy("purge_local_copy"))?.purge ?? { ok: false, retryNow: false },
3978
4120
  close: async () => (await runtime).control.close()
3979
4121
  };
3980
4122
  }
@@ -4029,18 +4171,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4029
4171
  get freshEncryptionNetworkCreated() {
4030
4172
  return this._freshEncryptionNetworkCreated;
4031
4173
  }
4032
- /**
4033
- * Subscribe to `storage.full`. It fires on the first write any service had
4034
- * rejected because the owner's TinyCloud storage is full
4035
- * (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`), so an app can show
4036
- * one read-only banner. Reads keep working. For the signed-in account's own
4037
- * storage it fires once per episode: the episode ends only when a write to
4038
- * that storage succeeds, or on sign-out. A `storage.status()` read does not
4039
- * end it. A rejection on another owner's delegated space fires once per
4040
- * owner, until that owner's `storage.status()` reports space again. Returns
4041
- * the unsubscribe function.
4042
- */
4043
4174
  on(event, handler) {
4175
+ if (event === "replica.purge.failed") {
4176
+ const listener = handler;
4177
+ this.replicaPurgeFailedHandlers.add(listener);
4178
+ return () => {
4179
+ this.replicaPurgeFailedHandlers.delete(listener);
4180
+ };
4181
+ }
4044
4182
  if (event !== "storage.full") {
4045
4183
  throw new Error(`Unknown TinyCloud event: ${String(event)}`);
4046
4184
  }
@@ -4521,6 +4659,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4521
4659
  const leavingAuth = this.auth;
4522
4660
  const leavingContext = this._serviceContext;
4523
4661
  let denied;
4662
+ let erased;
4524
4663
  try {
4525
4664
  this.teardownSession();
4526
4665
  } finally {
@@ -4531,6 +4670,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4531
4670
  try {
4532
4671
  await this.accountRegistryTail;
4533
4672
  await this.quiesceReplicaRuntime();
4673
+ if (denied) erased = await this.eraseReplicaLocalCopy("sign_out", denied.status);
4534
4674
  } finally {
4535
4675
  if (leavingAddress) {
4536
4676
  await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
@@ -4538,8 +4678,37 @@ var _TinyCloudNode = class _TinyCloudNode {
4538
4678
  }
4539
4679
  }
4540
4680
  }
4541
- if (denied?.status === "unguarded") {
4542
- throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
4681
+ if (denied?.status === "unguarded" && erased?.outcome.status !== "erased") {
4682
+ throw Object.assign(
4683
+ new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`),
4684
+ {
4685
+ code: "REPLICA_PURGE_FAILED",
4686
+ reason: "gate_unavailable",
4687
+ retryNow: erased?.outcome.status === "failed"
4688
+ }
4689
+ );
4690
+ }
4691
+ }
4692
+ /**
4693
+ * Erases the browser local copy after a deny (retry, event and next-load marker live in the lazy
4694
+ * replication authority). Never rejects; `undefined` when replication is off or the storage keeps
4695
+ * no browser database.
4696
+ */
4697
+ async eraseReplicaLocalCopy(trigger, denyStatus) {
4698
+ const replication = this.config.replication;
4699
+ if (!replication?.enabled) return void 0;
4700
+ try {
4701
+ const { eraseReplicaLocalCopy } = await getNodeReplicationLoaders().authority();
4702
+ const erased = await eraseReplicaLocalCopy(
4703
+ replication.storage.lifecycle,
4704
+ trigger,
4705
+ denyStatus,
4706
+ this.replicaPurgeFailedHandlers
4707
+ );
4708
+ if (erased?.logoutPending) this.replicaRestoreGate = "logout_pending";
4709
+ return erased;
4710
+ } catch {
4711
+ return void 0;
4543
4712
  }
4544
4713
  }
4545
4714
  /**
@@ -4599,6 +4768,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4599
4768
  this._hooks = void 0;
4600
4769
  this._vault = void 0;
4601
4770
  this._encryption = void 0;
4771
+ this._namedKeyClient = void 0;
4772
+ this._keysUnavailable = [];
4602
4773
  this._baseVaults.clear();
4603
4774
  this._baseSecrets.clear();
4604
4775
  this._secrets.clear();
@@ -4675,6 +4846,37 @@ var _TinyCloudNode = class _TinyCloudNode {
4675
4846
  }
4676
4847
  await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
4677
4848
  }
4849
+ /** Prepare the OpenKey request before the approval click. */
4850
+ async prepareOpenKeySignIn() {
4851
+ if (!this.signer || !this.tc || !this.auth) {
4852
+ throw new Error("Cannot prepare OpenKey sign-in in session-only mode. Provide a signer in config.");
4853
+ }
4854
+ await this.signOutInFlight;
4855
+ await this.accountRegistryTail;
4856
+ await this.wasmBindings.ensureInitialized?.();
4857
+ return this.auth.prepareOpenKeySignIn();
4858
+ }
4859
+ /** Start OpenKey authorization synchronously, then complete normal session setup. */
4860
+ signInWithPreparedOpenKey(pending, authorizeFn) {
4861
+ if (!this.signer || !this.tc || !this.auth || this.signOutInFlight) {
4862
+ throw new Error("Prepared OpenKey sign-in is unavailable");
4863
+ }
4864
+ if (this.consumedOpenKeyPendings.has(pending)) {
4865
+ throw new Error("Prepared OpenKey sign-in was already used");
4866
+ }
4867
+ if (Date.now() - pending.createdAt > 5 * 6e4) {
4868
+ throw new Error("Prepared OpenKey sign-in expired; prepare again");
4869
+ }
4870
+ this.consumedOpenKeyPendings.add(pending);
4871
+ const authorization = authorizeFn(pending.request);
4872
+ void authorization.catch(() => void 0);
4873
+ return this.signInWith(() => {
4874
+ if (this._address !== pending.address || this._chainId !== pending.chainId) {
4875
+ throw new Error("OpenKey account changed after sign-in preparation");
4876
+ }
4877
+ return this.auth.completeOpenKeySignIn(pending, authorization);
4878
+ });
4879
+ }
4678
4880
  async signInWith(performSignIn) {
4679
4881
  refuseCaveated(this.auth?.capabilityRequest?.manifests);
4680
4882
  const epoch = this.sessionEpoch;
@@ -4703,6 +4905,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4703
4905
  const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core11.pkhDid)(this.address, this._chainId) : void 0);
4704
4906
  if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
4705
4907
  const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
4908
+ if (denial) {
4909
+ await this.quiesceReplicaRuntime();
4910
+ await this.eraseReplicaLocalCopy("account_switch", denial.status);
4911
+ }
4706
4912
  this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
4707
4913
  }
4708
4914
  if (address !== this._address || chainId !== this._chainId) {
@@ -4717,6 +4923,8 @@ var _TinyCloudNode = class _TinyCloudNode {
4717
4923
  this._hooks = void 0;
4718
4924
  this._vault = void 0;
4719
4925
  this._encryption = void 0;
4926
+ this._namedKeyClient = void 0;
4927
+ this._keysUnavailable = [];
4720
4928
  this._baseVaults.clear();
4721
4929
  this._baseSecrets.clear();
4722
4930
  this._secrets.clear();
@@ -4769,6 +4977,26 @@ var _TinyCloudNode = class _TinyCloudNode {
4769
4977
  this.assertSessionEpoch(epoch, "Sign-in");
4770
4978
  await this.ensureRequestedEncryptionNetworks();
4771
4979
  this.assertSessionEpoch(epoch, "Sign-in");
4980
+ this._keysUnavailable = this.auth?.unavailableKeyPaths ?? [];
4981
+ if (this._keysUnavailable.length > 0) {
4982
+ this.notificationHandler.warning(`Named keys unavailable: ${this._keysUnavailable.join(", ")}`);
4983
+ }
4984
+ if (this.nodeFeatures.includes("encryption-named-keys-v1")) {
4985
+ for (const manifest of this.auth?.capabilityRequest?.manifests ?? []) {
4986
+ for (const declaration of manifest.keys ?? []) {
4987
+ if ("path" in declaration && declaration.abilities.includes("key.create") && !this._keysUnavailable.includes(declaration.path)) {
4988
+ try {
4989
+ await this.keys.ensureKey(declaration.path);
4990
+ } catch (error) {
4991
+ this._keysUnavailable.push(declaration.path);
4992
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z_]+$/.test(error.code) ? error.code : "UNKNOWN";
4993
+ this.notificationHandler.warning(`Named key unavailable: ${declaration.path} (${code})`);
4994
+ }
4995
+ this.assertSessionEpoch(epoch, "Sign-in");
4996
+ }
4997
+ }
4998
+ }
4999
+ }
4772
5000
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
4773
5001
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core11.SECRETS_SPACE));
4774
5002
  }
@@ -5345,7 +5573,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5345
5573
  if (!this.currentSessionCoversAccountRegistry()) {
5346
5574
  if (!_TinyCloudNode.accountRegistrySkipLogged) {
5347
5575
  _TinyCloudNode.accountRegistrySkipLogged = true;
5348
- import_sdk_services3.tinyCloudDebugLogger.log("registry.skip");
5576
+ import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
5349
5577
  }
5350
5578
  return;
5351
5579
  }
@@ -5556,7 +5784,8 @@ var _TinyCloudNode = class _TinyCloudNode {
5556
5784
  const networkIds = /* @__PURE__ */ new Set();
5557
5785
  for (const resource of request.resources) {
5558
5786
  if (resource.service === import_sdk_core11.ENCRYPTION_PERMISSION_SERVICE && resource.path.startsWith("urn:tinycloud:encryption:") && resource.actions.includes(DECRYPT_ACTION2)) {
5559
- networkIds.add(resource.path);
5787
+ const parsed = resource.path.includes("/") ? (0, import_resource2.parseEncryptionResource)(resource.path) : { ...(0, import_sdk_core11.parseNetworkId)(resource.path), kind: "network" };
5788
+ if (parsed.kind === "network") networkIds.add(resource.path);
5560
5789
  }
5561
5790
  }
5562
5791
  return [...networkIds];
@@ -6106,6 +6335,8 @@ var _TinyCloudNode = class _TinyCloudNode {
6106
6335
  this._hooks = stagedGraph.hooks;
6107
6336
  this._vault = stagedGraph.vault;
6108
6337
  this._encryption = stagedGraph.encryption;
6338
+ this._namedKeyClient = void 0;
6339
+ this._keysUnavailable = [];
6109
6340
  this._capabilityRegistry = stagedGraph.capabilityRegistry;
6110
6341
  this._keyProvider = stagedGraph.keyProvider;
6111
6342
  this._sharingService = stagedGraph.sharingService;
@@ -7015,7 +7246,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7015
7246
  );
7016
7247
  graph.assertActive();
7017
7248
  if (!response.ok) {
7018
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services3.parsePermissionHint)({
7249
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services4.parsePermissionHint)({
7019
7250
  service: "tinycloud.encryption",
7020
7251
  path: networkId,
7021
7252
  actions: ["tinycloud.encryption/decrypt"]
@@ -7459,7 +7690,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7459
7690
  * Sign a wallet (CACAO) revocation of the delegation `cid` for the node's
7460
7691
  * `POST /revoke`, and return its Authorization header value. The node
7461
7692
  * accepts it when the owner delegated `cid` or owns a space its abilities
7462
- * target, whichever key issued it. Valid for 60 seconds. Requires the
7693
+ * target, whichever key issued it. Valid for 10 minutes. Requires the
7463
7694
  * owner wallet signer.
7464
7695
  */
7465
7696
  async signOwnerRevocation(cid) {
@@ -7475,7 +7706,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7475
7706
  chainId: session.chainId,
7476
7707
  nonce: (0, import_siwe2.generateNonce)(),
7477
7708
  issuedAt: issuedAt.toISOString(),
7478
- expirationTime: new Date(issuedAt.getTime() + 6e4).toISOString()
7709
+ expirationTime: new Date(issuedAt.getTime() + OWNER_REVOCATION_WINDOW_MS).toISOString()
7479
7710
  }).prepareMessage();
7480
7711
  const signature = await this.signer.signMessage(siwe);
7481
7712
  return this.wasmBindings.siweToDelegationHeaders({ siwe, signature }).Authorization;
@@ -7702,6 +7933,40 @@ var _TinyCloudNode = class _TinyCloudNode {
7702
7933
  get encryption() {
7703
7934
  return this.getEncryptionService();
7704
7935
  }
7936
+ /** Named keys bound to the current session. */
7937
+ get keys() {
7938
+ return new Proxy({}, {
7939
+ get: (_target, property) => {
7940
+ if (property === "refreshNodeInfo") {
7941
+ return () => {
7942
+ void this._namedKeyClient?.then((client) => client.refreshNodeInfo());
7943
+ };
7944
+ }
7945
+ if (typeof property !== "string" || ![
7946
+ "getPublicKey",
7947
+ "describeKey",
7948
+ "createKey",
7949
+ "blockKey",
7950
+ "unblockKey",
7951
+ "ensureKey",
7952
+ "encrypt",
7953
+ "decrypt",
7954
+ "decryptMany"
7955
+ ].includes(property)) return void 0;
7956
+ return (...args) => {
7957
+ this._namedKeyClient ?? (this._namedKeyClient = import("./encryption-keys.cjs").then(({ createNamedKeyClient }) => createNamedKeyClient(this)));
7958
+ return this._namedKeyClient.then((client) => {
7959
+ const method = client[property];
7960
+ return method.apply(client, args);
7961
+ });
7962
+ };
7963
+ }
7964
+ });
7965
+ }
7966
+ /** Manifest key paths unavailable during the latest `signIn()` attempt. */
7967
+ get keysUnavailable() {
7968
+ return this._keysUnavailable;
7969
+ }
7705
7970
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
7706
7971
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
7707
7972
  return this.fetchEncryptionNetworkAt(
@@ -7829,7 +8094,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7829
8094
  );
7830
8095
  if (result.status !== "ok") {
7831
8096
  if (result.status === "permission_required") {
7832
- const hint = (0, import_sdk_services3.parsePermissionHint)(result.hint);
8097
+ const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
7833
8098
  if (hint === void 0) return { status: "read_failed" };
7834
8099
  return {
7835
8100
  status: "permission_required",
@@ -10007,6 +10272,7 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
10007
10272
  PermissionNotInManifestError,
10008
10273
  PrefixedKVService,
10009
10274
  ProtocolMismatchError,
10275
+ RuntimeDelegationRejectedError,
10010
10276
  SECRET_NAME_RE,
10011
10277
  SQLAction,
10012
10278
  SQLService,
@@ -10029,6 +10295,7 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
10029
10295
  VersionCheckError,
10030
10296
  WasmKeyProvider,
10031
10297
  activateCompactRuntimeDelegation,
10298
+ activateValidatedRuntimeDelegation,
10032
10299
  addressStorageKey,
10033
10300
  buildSpaceUri,
10034
10301
  canonicalizeAddress,
@@ -10079,6 +10346,7 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
10079
10346
  resourceCapabilitiesToSpaceAbilitiesMap,
10080
10347
  serializeDelegation,
10081
10348
  tinyCloudDebugLogger,
10082
- validateManifest
10349
+ validateManifest,
10350
+ wireOpenKeyAuthorize
10083
10351
  });
10084
10352
  //# sourceMappingURL=core.cjs.map