@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/index.js CHANGED
@@ -17099,6 +17099,9 @@ function assertValidReplicationConfig(replication) {
17099
17099
  if (replication?.syncScope !== void 0 && replication.syncScope !== "prefixes") {
17100
17100
  throw new TypeError('replication.syncScope must be "prefixes"');
17101
17101
  }
17102
+ if (replication?.contentFetch !== void 0 && replication.contentFetch !== "eager" && replication.contentFetch !== "lazy") {
17103
+ throw new TypeError('replication.contentFetch must be "eager" or "lazy"');
17104
+ }
17102
17105
  if (replication === void 0 || !replication.enabled) return;
17103
17106
  if (replication.storage === void 0 || replication.storage === null) {
17104
17107
  throw new TypeError("replication.enabled requires replication.storage");
@@ -17151,6 +17154,7 @@ function resolvedOptions(options) {
17151
17154
  maxStalenessMs: options.maxStalenessMs ?? 12e4,
17152
17155
  staleSyncTimeoutMs: options.staleSyncTimeoutMs ?? 5e3,
17153
17156
  allowSecrets: options.allowSecrets === true,
17157
+ contentFetch: options.contentFetch ?? "eager",
17154
17158
  verify: options.verify === true
17155
17159
  };
17156
17160
  }
@@ -17421,6 +17425,7 @@ __export(authority_exports, {
17421
17425
  assertValidReplicationConfig: () => assertValidReplicationConfig,
17422
17426
  augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17423
17427
  createReplicationAuthority: () => createReplicationAuthority,
17428
+ eraseReplicaLocalCopy: () => eraseReplicaLocalCopy,
17424
17429
  establishReplicaSessionStamp: () => establishReplicaSessionStamp,
17425
17430
  hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17426
17431
  mintNodeReplicationGrant: () => mintNodeReplicationGrant,
@@ -17461,6 +17466,38 @@ function reportReplicaDenial(result, reason, onEvent, context) {
17461
17466
  } catch {
17462
17467
  }
17463
17468
  }
17469
+ async function eraseReplicaLocalCopy(lifecycle, trigger, denyStatus, handlers) {
17470
+ if (lifecycle?.kind !== "indexeddb" || lifecycle.eraseLocalCopy === void 0) return void 0;
17471
+ let outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
17472
+ for (let attempt = 0; attempt <= ERASE_RETRY_DELAYS_MS.length; attempt++) {
17473
+ if (attempt > 0) await new Promise((resolve2) => setTimeout(resolve2, ERASE_RETRY_DELAYS_MS[attempt - 1]));
17474
+ try {
17475
+ outcome = await lifecycle.eraseLocalCopy({ signal: AbortSignal.timeout(ERASE_ATTEMPT_MS) });
17476
+ } catch {
17477
+ outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
17478
+ }
17479
+ if (outcome.status !== "failed") break;
17480
+ }
17481
+ const keysKept = denyStatus === "marker" || denyStatus === "unguarded";
17482
+ const purge = outcome.status === "erased" ? { ok: true } : { ok: false, retryNow: outcome.status === "failed" };
17483
+ if (outcome.status === "erased" || outcome.status === "refused" && !keysKept) return { outcome, purge };
17484
+ const logoutPending = outcome.status === "failed" && outcome.marker;
17485
+ const event = {
17486
+ type: "replica.purge.failed",
17487
+ trigger,
17488
+ reason: logoutPending ? "logout_pending" : "gate_unavailable",
17489
+ code: outcome.code,
17490
+ retryNow: outcome.status === "failed"
17491
+ };
17492
+ for (const handler of handlers) {
17493
+ try {
17494
+ handler(event);
17495
+ } catch (error) {
17496
+ console.error('Error in "replica.purge.failed" handler:', error);
17497
+ }
17498
+ }
17499
+ return logoutPending ? { outcome, purge, logoutPending } : { outcome, purge };
17500
+ }
17464
17501
  function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
17465
17502
  if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
17466
17503
  try {
@@ -17719,13 +17756,15 @@ function createReplicationAuthority(host) {
17719
17756
  };
17720
17757
  return authority;
17721
17758
  }
17722
- var parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17759
+ var parseUcanGrant, ERASE_RETRY_DELAYS_MS, ERASE_ATTEMPT_MS, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17723
17760
  var init_authority = __esm({
17724
17761
  "src/replication/authority.ts"() {
17725
17762
  "use strict";
17726
17763
  init_config();
17727
17764
  init_authority_sign_in();
17728
17765
  parseUcanGrant = replicaParseUcanGrant;
17766
+ ERASE_RETRY_DELAYS_MS = [150, 450];
17767
+ ERASE_ATTEMPT_MS = 3e3;
17729
17768
  KV_GET2 = KV4.GET;
17730
17769
  KV_SYNC2 = KV4.SYNC;
17731
17770
  KV_SERVICE2 = "tinycloud.kv";
@@ -18748,9 +18787,9 @@ import {
18748
18787
  tinyCloudDebugLogger
18749
18788
  } from "@tinycloud/sdk-services";
18750
18789
  import { RequestLog, observeReplicaReads, readOnlyRequestLog } from "@tinycloud/sdk-services/telemetry/requestLog";
18790
+ import { parseEncryptionResource as parseEncryptionResource2 } from "@tinycloud/sdk-services/encryption/resource";
18751
18791
 
18752
18792
  // src/authorization/NodeUserAuthorization.ts
18753
- init_authority_sign_in();
18754
18793
  import { createHash } from "crypto";
18755
18794
  import { SiweMessage } from "siwe";
18756
18795
  import {
@@ -18759,10 +18798,10 @@ import {
18759
18798
  activateSessionWithHost,
18760
18799
  checkNodeInfo,
18761
18800
  AutoApproveSpaceCreationHandler,
18801
+ ManifestValidationError,
18762
18802
  DEFAULT_MANIFEST_SPACE,
18763
18803
  ENCRYPTION_PERMISSION_SERVICE,
18764
18804
  composeManifestRequest,
18765
- parseNetworkId,
18766
18805
  principalDidEquals as principalDidEquals2,
18767
18806
  resourceCapabilitiesToAbilitiesMap,
18768
18807
  resourceCapabilitiesToSpaceAbilitiesMap,
@@ -18790,6 +18829,35 @@ import {
18790
18829
  HOOKS,
18791
18830
  ENCRYPTION
18792
18831
  } from "@tinycloud/sdk-core";
18832
+ import { parseEncryptionResource } from "@tinycloud/sdk-services/encryption/resource";
18833
+ import { parseNetworkId } from "@tinycloud/sdk-services";
18834
+
18835
+ // src/authorization/appKeyHostBinding.ts
18836
+ var DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
18837
+ var IPV4 = /^\d+\.\d+\.\d+\.\d+$/;
18838
+ var APP_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
18839
+ function appKeyHostBinding(domain, appId) {
18840
+ if (!APP_ID.test(appId)) return false;
18841
+ if (domain.endsWith(":")) return false;
18842
+ let url;
18843
+ try {
18844
+ url = new URL(`https://${domain}`);
18845
+ } catch {
18846
+ return false;
18847
+ }
18848
+ if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "") return false;
18849
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
18850
+ const host = url.hostname;
18851
+ if (host.startsWith("[") || host.endsWith(".") || IPV4.test(host)) return false;
18852
+ const labels = host.split(".");
18853
+ if (labels.length < 2 || labels[labels.length - 1] === "localhost") return false;
18854
+ if (!labels.every((label) => DOMAIN_LABEL.test(label))) return false;
18855
+ const reversed = labels.reverse().join(".");
18856
+ return appId === reversed || appId.startsWith(`${reversed}.`);
18857
+ }
18858
+
18859
+ // src/authorization/NodeUserAuthorization.ts
18860
+ init_authority_sign_in();
18793
18861
 
18794
18862
  // src/authorization/caveatRefusal.ts
18795
18863
  import {
@@ -18947,6 +19015,7 @@ function cloneAbilitiesMap(actions) {
18947
19015
  var NodeUserAuthorization = class {
18948
19016
  constructor(config) {
18949
19017
  this.activeSessionKeyId = "default";
19018
+ this.preparedSessionCounter = 0;
18950
19019
  /** Incremented by resetSession; a sign-in started earlier publishes nothing. */
18951
19020
  this.sessionGeneration = 0;
18952
19021
  /**
@@ -19303,6 +19372,23 @@ var NodeUserAuthorization = class {
19303
19372
  );
19304
19373
  return this._capabilityRequest;
19305
19374
  }
19375
+ async refreshNamedKeyFeatureForSignIn(address, chainId) {
19376
+ const request = this.getCapabilityRequest();
19377
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
19378
+ const hasNamedKeys = request?.manifests.some((manifest) => manifest.keys?.length && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id));
19379
+ if (hasNamedKeys) {
19380
+ await this.resolveTinyCloudHostsForSignIn(address, chainId);
19381
+ await this.refreshNodeInfo();
19382
+ }
19383
+ }
19384
+ /** Manifest key paths that this sign-in cannot grant. */
19385
+ get unavailableKeyPaths() {
19386
+ const request = this.getCapabilityRequest();
19387
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
19388
+ return (request?.manifests ?? []).flatMap(
19389
+ (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)
19390
+ );
19391
+ }
19306
19392
  resolveSpaceName(space, address, chainId) {
19307
19393
  if (space.startsWith("tinycloud:")) {
19308
19394
  return space;
@@ -19494,15 +19580,31 @@ var NodeUserAuthorization = class {
19494
19580
  }
19495
19581
  const rawAbilities = {};
19496
19582
  const currentDid = pkhDid(address, chainId);
19583
+ if (this._nodeFeatures.includes("encryption-named-keys-v1")) {
19584
+ const siweDomain = this.siweConfig?.domain ?? this.domain;
19585
+ for (const manifest of request.manifests) {
19586
+ if (!appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id)) continue;
19587
+ for (const declaration of manifest.keys ?? []) {
19588
+ const path = "path" in declaration ? declaration.path : declaration.prefix;
19589
+ const resource = `urn:tinycloud:encryption:${currentDid}:default/${path}`;
19590
+ for (const ability of declaration.abilities) {
19591
+ addRawAbility(rawAbilities, resource, `tinycloud.encryption/${ability}`);
19592
+ }
19593
+ }
19594
+ }
19595
+ }
19497
19596
  const spaceResources = request.resources.filter((entry) => {
19498
19597
  if (entry.service !== ENCRYPTION_PERMISSION_SERVICE) {
19499
19598
  return true;
19500
19599
  }
19600
+ const parsed = entry.path.includes("/") ? parseEncryptionResource(entry.path) : { ...parseNetworkId(entry.path), kind: "network" };
19601
+ if (parsed.kind !== "network") {
19602
+ throw new ManifestValidationError("named keys go in manifest.keys");
19603
+ }
19501
19604
  for (const action of entry.actions) {
19502
19605
  addRawAbility(rawAbilities, entry.path, action);
19503
19606
  }
19504
- if (entry.actions.includes(DECRYPT_ACTION)) {
19505
- const parsed = parseNetworkId(entry.path);
19607
+ if (entry.actions.includes(DECRYPT_ACTION) && parsed.kind === "network") {
19506
19608
  if (didPrincipalMatches(parsed.ownerDid, currentDid)) {
19507
19609
  addRawAbility(rawAbilities, entry.path, NETWORK_CREATE_ACTION);
19508
19610
  }
@@ -19898,6 +20000,8 @@ var NodeUserAuthorization = class {
19898
20000
  this._chainId = chainId;
19899
20001
  await this.resolveTinyCloudHostsForSignIn(address, chainId);
19900
20002
  this.assertSignInCurrent(generation);
20003
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
20004
+ this.assertSignInCurrent(generation);
19901
20005
  const keyId = `session-${Date.now()}`;
19902
20006
  this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
19903
20007
  this.activeSessionKeyId = keyId;
@@ -20044,9 +20148,9 @@ var NodeUserAuthorization = class {
20044
20148
  refuseCaveated(this.getCapabilityRequest()?.manifests);
20045
20149
  const address = canonicalizeAddress(await this.signer.getAddress());
20046
20150
  const chainId = await this.signer.getChainId();
20047
- const keyId = `session-${Date.now()}`;
20048
- this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
20049
- this.activeSessionKeyId = keyId;
20151
+ await this.refreshNamedKeyFeatureForSignIn(address, chainId);
20152
+ const keyId = `openkey-session-${Date.now()}-${++this.preparedSessionCounter}`;
20153
+ this.sessionManager.createSessionKey(keyId);
20050
20154
  const jwkString = this.sessionManager.jwk(keyId);
20051
20155
  if (!jwkString) {
20052
20156
  throw new Error("Failed to create session key");
@@ -20127,6 +20231,7 @@ var NodeUserAuthorization = class {
20127
20231
  await this.sessionStorage.save(address, persistedData);
20128
20232
  this._session = clientSession;
20129
20233
  this._tinyCloudSession = tinyCloudSession;
20234
+ this.activeSessionKeyId = keyId;
20130
20235
  this._address = address;
20131
20236
  this._chainId = chainId;
20132
20237
  await this.refreshNodeInfo();
@@ -20173,6 +20278,11 @@ var NodeUserAuthorization = class {
20173
20278
  * @param options Optional overrides for the OpenKey request.
20174
20279
  */
20175
20280
  async signInWithOpenKey(authorizeFn, options) {
20281
+ const pending = await this.prepareOpenKeySignIn(options);
20282
+ return this.completeOpenKeySignIn(pending, authorizeFn(pending.request), options);
20283
+ }
20284
+ /** Finish address, host and SIWE preparation before an approval click. */
20285
+ async prepareOpenKeySignIn(options) {
20176
20286
  refuseCaveated(this.getCapabilityRequest()?.manifests);
20177
20287
  if (options?.host !== void 0) {
20178
20288
  const overrideHost = options.host.trim();
@@ -20184,7 +20294,7 @@ var NodeUserAuthorization = class {
20184
20294
  const signingAddress = canonicalizeAddress(await this.signer.getAddress());
20185
20295
  const signingChainId = await this.signer.getChainId();
20186
20296
  await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
20187
- const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
20297
+ const { prepared, keyId, jwk, address, chainId } = await this.prepareSessionForSigning();
20188
20298
  const presentation = this.buildPresentationEnvelope(options?.reason);
20189
20299
  const hostHint = this.primaryTinyCloudHost;
20190
20300
  const originalRequest = {
@@ -20196,12 +20306,32 @@ var NodeUserAuthorization = class {
20196
20306
  keyId: options?.openkeyKeyId,
20197
20307
  presentation
20198
20308
  };
20309
+ return {
20310
+ request: originalRequest,
20311
+ prepared,
20312
+ keyId,
20313
+ jwk,
20314
+ address,
20315
+ chainId,
20316
+ generation: this.sessionGeneration,
20317
+ createdAt: Date.now()
20318
+ };
20319
+ }
20320
+ /** Complete a prepared approval. The caller starts authorization before awaiting this method. */
20321
+ async completeOpenKeySignIn(pending, authorization, options) {
20322
+ if (Date.now() - pending.createdAt > 5 * 6e4) {
20323
+ throw new Error("Prepared OpenKey sign-in expired; prepare again");
20324
+ }
20325
+ if (pending.generation !== this.sessionGeneration) {
20326
+ throw new Error("Prepared OpenKey sign-in expired after sign-out");
20327
+ }
20328
+ const { request: originalRequest, prepared, keyId, jwk } = pending;
20199
20329
  let result;
20200
20330
  let signed2;
20201
20331
  let signature2;
20202
20332
  for (let attempt = 0; ; ) {
20203
20333
  try {
20204
- result ?? (result = await authorizeFn(originalRequest));
20334
+ result ?? (result = await authorization);
20205
20335
  signed2 = await this.verifyOpenKeyResult(result, prepared);
20206
20336
  signature2 = result.signature;
20207
20337
  break;
@@ -21112,6 +21242,7 @@ var ROOT_DELEGATION_ACTIONS = [
21112
21242
  DUCKDB2.ALL
21113
21243
  ];
21114
21244
  var DEFAULT_SESSION_EXPIRATION_MS = EXPIRY3.SESSION_MS;
21245
+ var OWNER_REVOCATION_WINDOW_MS = 10 * 6e4;
21115
21246
  function unifiedRootAttenuation(capabilities) {
21116
21247
  const attenuation = {};
21117
21248
  for (const capability of capabilities) {
@@ -21576,6 +21707,7 @@ function replicaRestoreRefusal(error) {
21576
21707
  case "purge_pending":
21577
21708
  case "logout_pending":
21578
21709
  case "gate_unavailable":
21710
+ case "wallet_provider_required":
21579
21711
  return error.gate;
21580
21712
  }
21581
21713
  }
@@ -21608,8 +21740,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21608
21740
  constructor(config = {}) {
21609
21741
  this.signer = null;
21610
21742
  this.auth = null;
21743
+ this.consumedOpenKeyPendings = /* @__PURE__ */ new WeakSet();
21611
21744
  this.tc = null;
21612
21745
  this._chainId = 1;
21746
+ this.replicaPurgeFailedHandlers = /* @__PURE__ */ new Set();
21613
21747
  /** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
21614
21748
  this.establishReplicaStamp = async (session) => {
21615
21749
  const replication = this.config.replication;
@@ -21641,6 +21775,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21641
21775
  return void 0;
21642
21776
  }
21643
21777
  };
21778
+ this._keysUnavailable = [];
21644
21779
  this._baseVaults = /* @__PURE__ */ new Map();
21645
21780
  this._baseSecrets = /* @__PURE__ */ new Map();
21646
21781
  this._secrets = /* @__PURE__ */ new Map();
@@ -21816,6 +21951,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21816
21951
  return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
21817
21952
  },
21818
21953
  clearPending: async () => (await runtime).control.clearPending(),
21954
+ purgeLocalCopy: async () => (await this.eraseReplicaLocalCopy("purge_local_copy"))?.purge ?? { ok: false, retryNow: false },
21819
21955
  close: async () => (await runtime).control.close()
21820
21956
  };
21821
21957
  }
@@ -21870,18 +22006,14 @@ var _TinyCloudNode = class _TinyCloudNode {
21870
22006
  get freshEncryptionNetworkCreated() {
21871
22007
  return this._freshEncryptionNetworkCreated;
21872
22008
  }
21873
- /**
21874
- * Subscribe to `storage.full`. It fires on the first write any service had
21875
- * rejected because the owner's TinyCloud storage is full
21876
- * (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`), so an app can show
21877
- * one read-only banner. Reads keep working. For the signed-in account's own
21878
- * storage it fires once per episode: the episode ends only when a write to
21879
- * that storage succeeds, or on sign-out. A `storage.status()` read does not
21880
- * end it. A rejection on another owner's delegated space fires once per
21881
- * owner, until that owner's `storage.status()` reports space again. Returns
21882
- * the unsubscribe function.
21883
- */
21884
22009
  on(event, handler) {
22010
+ if (event === "replica.purge.failed") {
22011
+ const listener = handler;
22012
+ this.replicaPurgeFailedHandlers.add(listener);
22013
+ return () => {
22014
+ this.replicaPurgeFailedHandlers.delete(listener);
22015
+ };
22016
+ }
21885
22017
  if (event !== "storage.full") {
21886
22018
  throw new Error(`Unknown TinyCloud event: ${String(event)}`);
21887
22019
  }
@@ -22362,6 +22494,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22362
22494
  const leavingAuth = this.auth;
22363
22495
  const leavingContext = this._serviceContext;
22364
22496
  let denied;
22497
+ let erased;
22365
22498
  try {
22366
22499
  this.teardownSession();
22367
22500
  } finally {
@@ -22372,6 +22505,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22372
22505
  try {
22373
22506
  await this.accountRegistryTail;
22374
22507
  await this.quiesceReplicaRuntime();
22508
+ if (denied) erased = await this.eraseReplicaLocalCopy("sign_out", denied.status);
22375
22509
  } finally {
22376
22510
  if (leavingAddress) {
22377
22511
  await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
@@ -22379,8 +22513,37 @@ var _TinyCloudNode = class _TinyCloudNode {
22379
22513
  }
22380
22514
  }
22381
22515
  }
22382
- if (denied?.status === "unguarded") {
22383
- throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
22516
+ if (denied?.status === "unguarded" && erased?.outcome.status !== "erased") {
22517
+ throw Object.assign(
22518
+ new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`),
22519
+ {
22520
+ code: "REPLICA_PURGE_FAILED",
22521
+ reason: "gate_unavailable",
22522
+ retryNow: erased?.outcome.status === "failed"
22523
+ }
22524
+ );
22525
+ }
22526
+ }
22527
+ /**
22528
+ * Erases the browser local copy after a deny (retry, event and next-load marker live in the lazy
22529
+ * replication authority). Never rejects; `undefined` when replication is off or the storage keeps
22530
+ * no browser database.
22531
+ */
22532
+ async eraseReplicaLocalCopy(trigger, denyStatus) {
22533
+ const replication = this.config.replication;
22534
+ if (!replication?.enabled) return void 0;
22535
+ try {
22536
+ const { eraseReplicaLocalCopy: eraseReplicaLocalCopy2 } = await getNodeReplicationLoaders().authority();
22537
+ const erased = await eraseReplicaLocalCopy2(
22538
+ replication.storage.lifecycle,
22539
+ trigger,
22540
+ denyStatus,
22541
+ this.replicaPurgeFailedHandlers
22542
+ );
22543
+ if (erased?.logoutPending) this.replicaRestoreGate = "logout_pending";
22544
+ return erased;
22545
+ } catch {
22546
+ return void 0;
22384
22547
  }
22385
22548
  }
22386
22549
  /**
@@ -22440,6 +22603,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22440
22603
  this._hooks = void 0;
22441
22604
  this._vault = void 0;
22442
22605
  this._encryption = void 0;
22606
+ this._namedKeyClient = void 0;
22607
+ this._keysUnavailable = [];
22443
22608
  this._baseVaults.clear();
22444
22609
  this._baseSecrets.clear();
22445
22610
  this._secrets.clear();
@@ -22516,6 +22681,37 @@ var _TinyCloudNode = class _TinyCloudNode {
22516
22681
  }
22517
22682
  await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
22518
22683
  }
22684
+ /** Prepare the OpenKey request before the approval click. */
22685
+ async prepareOpenKeySignIn() {
22686
+ if (!this.signer || !this.tc || !this.auth) {
22687
+ throw new Error("Cannot prepare OpenKey sign-in in session-only mode. Provide a signer in config.");
22688
+ }
22689
+ await this.signOutInFlight;
22690
+ await this.accountRegistryTail;
22691
+ await this.wasmBindings.ensureInitialized?.();
22692
+ return this.auth.prepareOpenKeySignIn();
22693
+ }
22694
+ /** Start OpenKey authorization synchronously, then complete normal session setup. */
22695
+ signInWithPreparedOpenKey(pending, authorizeFn) {
22696
+ if (!this.signer || !this.tc || !this.auth || this.signOutInFlight) {
22697
+ throw new Error("Prepared OpenKey sign-in is unavailable");
22698
+ }
22699
+ if (this.consumedOpenKeyPendings.has(pending)) {
22700
+ throw new Error("Prepared OpenKey sign-in was already used");
22701
+ }
22702
+ if (Date.now() - pending.createdAt > 5 * 6e4) {
22703
+ throw new Error("Prepared OpenKey sign-in expired; prepare again");
22704
+ }
22705
+ this.consumedOpenKeyPendings.add(pending);
22706
+ const authorization = authorizeFn(pending.request);
22707
+ void authorization.catch(() => void 0);
22708
+ return this.signInWith(() => {
22709
+ if (this._address !== pending.address || this._chainId !== pending.chainId) {
22710
+ throw new Error("OpenKey account changed after sign-in preparation");
22711
+ }
22712
+ return this.auth.completeOpenKeySignIn(pending, authorization);
22713
+ });
22714
+ }
22519
22715
  async signInWith(performSignIn) {
22520
22716
  refuseCaveated(this.auth?.capabilityRequest?.manifests);
22521
22717
  const epoch = this.sessionEpoch;
@@ -22544,6 +22740,10 @@ var _TinyCloudNode = class _TinyCloudNode {
22544
22740
  const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? pkhDid2(this.address, this._chainId) : void 0);
22545
22741
  if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
22546
22742
  const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
22743
+ if (denial) {
22744
+ await this.quiesceReplicaRuntime();
22745
+ await this.eraseReplicaLocalCopy("account_switch", denial.status);
22746
+ }
22547
22747
  this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
22548
22748
  }
22549
22749
  if (address !== this._address || chainId !== this._chainId) {
@@ -22558,6 +22758,8 @@ var _TinyCloudNode = class _TinyCloudNode {
22558
22758
  this._hooks = void 0;
22559
22759
  this._vault = void 0;
22560
22760
  this._encryption = void 0;
22761
+ this._namedKeyClient = void 0;
22762
+ this._keysUnavailable = [];
22561
22763
  this._baseVaults.clear();
22562
22764
  this._baseSecrets.clear();
22563
22765
  this._secrets.clear();
@@ -22610,6 +22812,26 @@ var _TinyCloudNode = class _TinyCloudNode {
22610
22812
  this.assertSessionEpoch(epoch, "Sign-in");
22611
22813
  await this.ensureRequestedEncryptionNetworks();
22612
22814
  this.assertSessionEpoch(epoch, "Sign-in");
22815
+ this._keysUnavailable = this.auth?.unavailableKeyPaths ?? [];
22816
+ if (this._keysUnavailable.length > 0) {
22817
+ this.notificationHandler.warning(`Named keys unavailable: ${this._keysUnavailable.join(", ")}`);
22818
+ }
22819
+ if (this.nodeFeatures.includes("encryption-named-keys-v1")) {
22820
+ for (const manifest of this.auth?.capabilityRequest?.manifests ?? []) {
22821
+ for (const declaration of manifest.keys ?? []) {
22822
+ if ("path" in declaration && declaration.abilities.includes("key.create") && !this._keysUnavailable.includes(declaration.path)) {
22823
+ try {
22824
+ await this.keys.ensureKey(declaration.path);
22825
+ } catch (error) {
22826
+ this._keysUnavailable.push(declaration.path);
22827
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z_]+$/.test(error.code) ? error.code : "UNKNOWN";
22828
+ this.notificationHandler.warning(`Named key unavailable: ${declaration.path} (${code})`);
22829
+ }
22830
+ this.assertSessionEpoch(epoch, "Sign-in");
22831
+ }
22832
+ }
22833
+ }
22834
+ }
22613
22835
  if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
22614
22836
  await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(SECRETS_SPACE2));
22615
22837
  }
@@ -23397,7 +23619,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23397
23619
  const networkIds = /* @__PURE__ */ new Set();
23398
23620
  for (const resource of request.resources) {
23399
23621
  if (resource.service === ENCRYPTION_PERMISSION_SERVICE2 && resource.path.startsWith("urn:tinycloud:encryption:") && resource.actions.includes(DECRYPT_ACTION2)) {
23400
- networkIds.add(resource.path);
23622
+ const parsed = resource.path.includes("/") ? parseEncryptionResource2(resource.path) : { ...parseNetworkId2(resource.path), kind: "network" };
23623
+ if (parsed.kind === "network") networkIds.add(resource.path);
23401
23624
  }
23402
23625
  }
23403
23626
  return [...networkIds];
@@ -23947,6 +24170,8 @@ var _TinyCloudNode = class _TinyCloudNode {
23947
24170
  this._hooks = stagedGraph.hooks;
23948
24171
  this._vault = stagedGraph.vault;
23949
24172
  this._encryption = stagedGraph.encryption;
24173
+ this._namedKeyClient = void 0;
24174
+ this._keysUnavailable = [];
23950
24175
  this._capabilityRegistry = stagedGraph.capabilityRegistry;
23951
24176
  this._keyProvider = stagedGraph.keyProvider;
23952
24177
  this._sharingService = stagedGraph.sharingService;
@@ -25300,7 +25525,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25300
25525
  * Sign a wallet (CACAO) revocation of the delegation `cid` for the node's
25301
25526
  * `POST /revoke`, and return its Authorization header value. The node
25302
25527
  * accepts it when the owner delegated `cid` or owns a space its abilities
25303
- * target, whichever key issued it. Valid for 60 seconds. Requires the
25528
+ * target, whichever key issued it. Valid for 10 minutes. Requires the
25304
25529
  * owner wallet signer.
25305
25530
  */
25306
25531
  async signOwnerRevocation(cid) {
@@ -25316,7 +25541,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25316
25541
  chainId: session.chainId,
25317
25542
  nonce: generateNonce(),
25318
25543
  issuedAt: issuedAt.toISOString(),
25319
- expirationTime: new Date(issuedAt.getTime() + 6e4).toISOString()
25544
+ expirationTime: new Date(issuedAt.getTime() + OWNER_REVOCATION_WINDOW_MS).toISOString()
25320
25545
  }).prepareMessage();
25321
25546
  const signature2 = await this.signer.signMessage(siwe);
25322
25547
  return this.wasmBindings.siweToDelegationHeaders({ siwe, signature: signature2 }).Authorization;
@@ -25543,6 +25768,40 @@ var _TinyCloudNode = class _TinyCloudNode {
25543
25768
  get encryption() {
25544
25769
  return this.getEncryptionService();
25545
25770
  }
25771
+ /** Named keys bound to the current session. */
25772
+ get keys() {
25773
+ return new Proxy({}, {
25774
+ get: (_target, property) => {
25775
+ if (property === "refreshNodeInfo") {
25776
+ return () => {
25777
+ void this._namedKeyClient?.then((client) => client.refreshNodeInfo());
25778
+ };
25779
+ }
25780
+ if (typeof property !== "string" || ![
25781
+ "getPublicKey",
25782
+ "describeKey",
25783
+ "createKey",
25784
+ "blockKey",
25785
+ "unblockKey",
25786
+ "ensureKey",
25787
+ "encrypt",
25788
+ "decrypt",
25789
+ "decryptMany"
25790
+ ].includes(property)) return void 0;
25791
+ return (...args) => {
25792
+ this._namedKeyClient ?? (this._namedKeyClient = import("./encryption-keys.js").then(({ createNamedKeyClient }) => createNamedKeyClient(this)));
25793
+ return this._namedKeyClient.then((client) => {
25794
+ const method = client[property];
25795
+ return method.apply(client, args);
25796
+ });
25797
+ };
25798
+ }
25799
+ });
25800
+ }
25801
+ /** Manifest key paths unavailable during the latest `signIn()` attempt. */
25802
+ get keysUnavailable() {
25803
+ return this._keysUnavailable;
25804
+ }
25546
25805
  async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
25547
25806
  const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
25548
25807
  return this.fetchEncryptionNetworkAt(
@@ -27982,7 +28241,7 @@ var FileSessionStorage = class {
27982
28241
  };
27983
28242
 
27984
28243
  // src/authorization/openKeyBridge.ts
27985
- function wireOpenKeyAuthorize(openkey) {
28244
+ function wireOpenKeyAuthorize(openkey, opts) {
27986
28245
  return async (input) => {
27987
28246
  const result = await openkey.authorizeTinyCloud({
27988
28247
  protocolVersion: 1,
@@ -27993,7 +28252,7 @@ function wireOpenKeyAuthorize(openkey) {
27993
28252
  // Forward the presentation envelope verbatim — the OpenKey SDK
27994
28253
  // validates and size-bounds it before use.
27995
28254
  presentation: input.presentation
27996
- });
28255
+ }, opts);
27997
28256
  if (result.protocolVersion !== 1) {
27998
28257
  throw new Error(
27999
28258
  `wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
@@ -28035,7 +28294,7 @@ import {
28035
28294
  CaveatedDelegationUnsupportedError as CaveatedDelegationUnsupportedError3,
28036
28295
  PermissionNotInManifestError as PermissionNotInManifestError2,
28037
28296
  SessionExpiredError as SessionExpiredError2,
28038
- ManifestValidationError,
28297
+ ManifestValidationError as ManifestValidationError2,
28039
28298
  composeManifestRequest as composeManifestRequest2,
28040
28299
  resolveManifest as resolveManifest2,
28041
28300
  validateManifest,
@@ -29180,7 +29439,8 @@ var SqliteReplicaHandle = class {
29180
29439
  options.signal.addEventListener("abort", onAbort, { once: true });
29181
29440
  const engine = new (__privateGet(this, _replica)).Replica({
29182
29441
  store: __privateGet(this, _store),
29183
- transportFor
29442
+ transportFor,
29443
+ ...this.spec.contentFetch === void 0 ? {} : { contentFetch: this.spec.contentFetch }
29184
29444
  });
29185
29445
  const running = engine.sync({ signal: abort.signal });
29186
29446
  const registration = { promise: running, abort: () => abort.abort() };
@@ -30699,7 +30959,7 @@ export {
30699
30959
  InvalidRestoredSessionError,
30700
30960
  KVService4 as KVService,
30701
30961
  MAX_SECRET_SCOPE_LENGTH,
30702
- ManifestValidationError,
30962
+ ManifestValidationError2 as ManifestValidationError,
30703
30963
  MemorySessionStorage,
30704
30964
  NETWORK_NAME_PATTERN,
30705
30965
  NetworkIdError,