@tinycloud/node-sdk 3.2.0-beta.1 → 3.2.0-beta.3

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/core.js CHANGED
@@ -1,5 +1,19 @@
1
1
  // src/core.ts
2
2
  import { TinyCloud as TinyCloud2 } from "@tinycloud/sdk-core";
3
+
4
+ // src/replication/module-registry.ts
5
+ var nodeLoaders;
6
+ function registerReplicationLoaders(loaders) {
7
+ nodeLoaders = loaders;
8
+ }
9
+ function getNodeReplicationLoaders() {
10
+ if (nodeLoaders === void 0) {
11
+ throw new Error("Replication requires registering replication controller loaders.");
12
+ }
13
+ return nodeLoaders;
14
+ }
15
+
16
+ // src/core.ts
3
17
  import {
4
18
  SilentNotificationHandler as SilentNotificationHandler2,
5
19
  AutoApproveSpaceCreationHandler as AutoApproveSpaceCreationHandler2,
@@ -16,7 +30,7 @@ import {
16
30
  makePkhSpaceId as makePkhSpaceId2,
17
31
  parsePkhDid,
18
32
  pkhDid as pkhDid3,
19
- principalDid,
33
+ principalDid as principalDid2,
20
34
  principalDidEquals as principalDidEquals4,
21
35
  parseCanonicalNetworkId,
22
36
  TinyCloudDebugLogger,
@@ -247,7 +261,7 @@ import {
247
261
  parseCanonicalRecapResource,
248
262
  ACCOUNT_REGISTRY_SPACE,
249
263
  ACCOUNT_MANIFEST_PERMISSIONS,
250
- KV,
264
+ KV as KV2,
251
265
  SQL,
252
266
  DUCKDB,
253
267
  CAPABILITIES,
@@ -255,6 +269,46 @@ import {
255
269
  ENCRYPTION
256
270
  } from "@tinycloud/sdk-core";
257
271
 
272
+ // src/replication/authority-sign-in.ts
273
+ import {
274
+ grantPathsForSelector,
275
+ KV,
276
+ isCapabilitySubset
277
+ } from "@tinycloud/sdk-core";
278
+ import { requiresSecretsOptIn } from "@tinycloud/sdk-services";
279
+ var KV_GET = KV.GET;
280
+ var KV_SYNC = KV.SYNC;
281
+ var KV_SERVICE = "tinycloud.kv";
282
+ function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
283
+ const requiredPaths = grantPathsForSelector(prefix);
284
+ return isCapabilitySubset(
285
+ requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),
286
+ [...entries]
287
+ ).subset;
288
+ }
289
+ function augmentSignInEntriesWithReplication(input) {
290
+ const out = [];
291
+ const replication = input.replication;
292
+ if (replication === void 0) return out;
293
+ for (const prefix of replication.prefixes) {
294
+ if (requiresSecretsOptIn(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
295
+ continue;
296
+ }
297
+ if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {
298
+ continue;
299
+ }
300
+ for (const path of grantPathsForSelector(prefix)) {
301
+ out.push({
302
+ service: KV_SERVICE,
303
+ space: input.primarySpaceId,
304
+ path,
305
+ actions: [KV_GET, KV_SYNC]
306
+ });
307
+ }
308
+ }
309
+ return out;
310
+ }
311
+
258
312
  // src/authorization/strategies.ts
259
313
  import { createOpenKeyCallbackSigningStrategy } from "@tinycloud/sdk-core";
260
314
  var defaultSignStrategy = { type: "auto-sign" };
@@ -330,7 +384,7 @@ var NodeUserAuthorization = class {
330
384
  this.spacePrefix = config.spacePrefix ?? "default";
331
385
  this.defaultActions = config.defaultActions ?? {
332
386
  kv: {
333
- "": [KV.PUT, KV.GET, KV.DEL, KV.LIST, KV.METADATA]
387
+ "": [KV2.PUT, KV2.GET, KV2.DEL, KV2.LIST, KV2.METADATA]
334
388
  },
335
389
  sql: {
336
390
  "": [SQL.READ, SQL.WRITE, SQL.SCHEMA, SQL.ADMIN, SQL.EXPORT]
@@ -368,6 +422,7 @@ var NodeUserAuthorization = class {
368
422
  this.nonce = config.nonce;
369
423
  this.siweConfig = config.siweConfig;
370
424
  this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
425
+ this.replication = config.replication?.enabled === true ? config.replication : void 0;
371
426
  this._manifest = config.manifest;
372
427
  this._capabilityRequest = config.capabilityRequest;
373
428
  this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
@@ -576,6 +631,143 @@ var NodeUserAuthorization = class {
576
631
  }
577
632
  return makePkhSpaceId(address, chainId, space);
578
633
  }
634
+ /**
635
+ * The plain-session primary abilities: `defaultActions` (the shared table)
636
+ * unless the primary space IS the account space, where account-registry
637
+ * permissions merge into a private clone so the shared table is never
638
+ * mutated. Shared between `resolveSignInCapabilities` and
639
+ * `signInPermissionInputs` so sign-in and `replicationSignInEntries`
640
+ * can't disagree (§4.6).
641
+ */
642
+ primaryActionsForSignIn() {
643
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
644
+ if (this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE) {
645
+ const accountAbilities = resourceCapabilitiesToAbilitiesMap(
646
+ ACCOUNT_MANIFEST_PERMISSIONS
647
+ );
648
+ for (const [service, paths] of Object.entries(accountAbilities)) {
649
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
650
+ for (const [path, actions] of Object.entries(paths)) {
651
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
652
+ for (const action of actions) {
653
+ if (!existingActions.includes(action)) existingActions.push(action);
654
+ }
655
+ }
656
+ }
657
+ }
658
+ return primaryActions;
659
+ }
660
+ /**
661
+ * The abilities a `spacePrefix: "secrets"` plain session actually requests
662
+ * for its primary space: the vault-secrets subtree only — the
663
+ * `defaultActions` root `get` is REPLACED, not merged (sign-in installs
664
+ * this map under the primary space id). Shared between
665
+ * `resolveSignInCapabilities` and `signInPermissionInputs` so the
666
+ * replication augmentation measures coverage against the effective
667
+ * pre-replication request, never the uninstalled `primaryActions` (§4.1).
668
+ */
669
+ secretsPrimaryAbilities() {
670
+ return {
671
+ kv: {
672
+ "vault/secrets/": [
673
+ KV2.GET,
674
+ KV2.PUT,
675
+ KV2.DEL,
676
+ KV2.LIST,
677
+ KV2.METADATA
678
+ ]
679
+ }
680
+ };
681
+ }
682
+ /**
683
+ * The effective sign-in request as `PermissionEntry`s plus the resolved
684
+ * primary space id — the inputs to the replication augmentation (§4.1).
685
+ * `resolveSignInCapabilities` merges the augmented entries into its
686
+ * abilities; `replicationSignInEntries()` returns them verbatim, so the
687
+ * entries the SIWE recap carries and the restore check sees are the same.
688
+ */
689
+ signInPermissionInputs(address, chainId) {
690
+ const request = this.getCapabilityRequest();
691
+ if (request === void 0) {
692
+ const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
693
+ const effectivePrimary = this.spacePrefix === "secrets" ? this.secretsPrimaryAbilities() : this.primaryActionsForSignIn();
694
+ const kvPaths = effectivePrimary["kv"] ?? {};
695
+ return {
696
+ primarySpaceId: primarySpaceId2,
697
+ entries: Object.entries(kvPaths).map(([path, actions]) => ({
698
+ service: "tinycloud.kv",
699
+ space: primarySpaceId2,
700
+ path,
701
+ actions: [...actions]
702
+ }))
703
+ };
704
+ }
705
+ const spaceResources = request.resources.filter(
706
+ (entry) => entry.service !== ENCRYPTION_PERMISSION_SERVICE
707
+ );
708
+ const primarySpaceName = spaceResources.find((entry) => entry.space !== "account")?.space ?? DEFAULT_MANIFEST_SPACE;
709
+ const primarySpaceId = this.resolveSpaceName(
710
+ primarySpaceName,
711
+ address,
712
+ chainId
713
+ );
714
+ return {
715
+ primarySpaceId,
716
+ entries: spaceResources.map((entry) => ({
717
+ service: entry.service,
718
+ space: this.resolveSpaceName(entry.space, address, chainId),
719
+ path: entry.path,
720
+ actions: [...entry.actions]
721
+ }))
722
+ };
723
+ }
724
+ /**
725
+ * Merge replication augmentation entries into a space's abilities map
726
+ * without mutating shared tables (`defaultActions` may back the base map):
727
+ * the primary entry in `spaceAbilities` is replaced with a copy whose `kv`
728
+ * paths gain `get`/`sync`. Returns the merged map for `abilities`.
729
+ */
730
+ mergeReplicationEntries(spaceAbilities, primarySpaceId, abilities, entries) {
731
+ var _a;
732
+ if (entries.length === 0) {
733
+ return abilities;
734
+ }
735
+ const base = spaceAbilities[primarySpaceId] ?? abilities;
736
+ const kv = {};
737
+ for (const [path, actions] of Object.entries(base["kv"] ?? {})) {
738
+ kv[path] = [...actions];
739
+ }
740
+ for (const entry of entries) {
741
+ const list = kv[_a = entry.path] ?? (kv[_a] = []);
742
+ for (const action of entry.actions) {
743
+ if (!list.includes(action)) list.push(action);
744
+ }
745
+ }
746
+ const merged = { ...base, kv };
747
+ spaceAbilities[primarySpaceId] = merged;
748
+ return merged;
749
+ }
750
+ /**
751
+ * The replication entries this authorization's sign-in request carries
752
+ * for the signed-in or restored address (TC-858 §4.1/§4.6): the output of
753
+ * the same augmentation `resolveSignInCapabilities` runs — one
754
+ * `{tinycloud.kv, primarySpace, prefix, [get, sync]}` per configured
755
+ * prefix that already has an unrestricted covering `get` and passes the
756
+ * secrets opt-in gate. Empty when replication is off or no address is
757
+ * known. Pure.
758
+ */
759
+ replicationSignInEntries() {
760
+ if (this.replication === void 0) return [];
761
+ const address = this._address ?? this._tinyCloudSession?.address;
762
+ const chainId = this._chainId ?? this._tinyCloudSession?.chainId;
763
+ if (address === void 0 || chainId === void 0) return [];
764
+ const { entries, primarySpaceId } = this.signInPermissionInputs(address, chainId);
765
+ return augmentSignInEntriesWithReplication({
766
+ entries,
767
+ primarySpaceId,
768
+ replication: this.replication
769
+ });
770
+ }
579
771
  defaultEncryptionNetworkId(address, chainId) {
580
772
  return `urn:tinycloud:encryption:${pkhDid(address, chainId)}:default`;
581
773
  }
@@ -585,39 +777,25 @@ var NodeUserAuthorization = class {
585
777
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
586
778
  const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
587
779
  const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
588
- const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
780
+ const primaryActions = this.primaryActionsForSignIn();
589
781
  const spaceAbilities2 = {
590
782
  [primarySpaceId2]: primaryActions,
591
- [secretsSpaceId]: {
592
- kv: {
593
- "vault/secrets/": [
594
- KV.GET,
595
- KV.PUT,
596
- KV.DEL,
597
- KV.LIST,
598
- KV.METADATA
599
- ]
600
- }
601
- }
783
+ [secretsSpaceId]: this.secretsPrimaryAbilities()
602
784
  };
603
785
  if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
604
786
  spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
605
- } else if (this.includeAccountRegistryPermissions) {
606
- const accountAbilities = resourceCapabilitiesToAbilitiesMap(
607
- ACCOUNT_MANIFEST_PERMISSIONS
608
- );
609
- for (const [service, paths] of Object.entries(accountAbilities)) {
610
- const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
611
- for (const [path, actions] of Object.entries(paths)) {
612
- const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
613
- for (const action of actions) {
614
- if (!existingActions.includes(action)) existingActions.push(action);
615
- }
616
- }
617
- }
618
787
  }
788
+ const mergedPrimary = this.mergeReplicationEntries(
789
+ spaceAbilities2,
790
+ primarySpaceId2,
791
+ primaryActions,
792
+ augmentSignInEntriesWithReplication({
793
+ ...this.signInPermissionInputs(address, chainId),
794
+ replication: this.replication
795
+ })
796
+ );
619
797
  return {
620
- abilities: primaryActions,
798
+ abilities: mergedPrimary,
621
799
  spaceId: primarySpaceId2,
622
800
  spaceAbilities: spaceAbilities2,
623
801
  rawAbilities: {
@@ -653,8 +831,17 @@ var NodeUserAuthorization = class {
653
831
  for (const [space, abilities] of Object.entries(bySpace)) {
654
832
  spaceAbilities[this.resolveSpaceName(space, address, chainId)] = abilities;
655
833
  }
834
+ const mergedPrimaryAbilities = this.mergeReplicationEntries(
835
+ spaceAbilities,
836
+ primarySpaceId,
837
+ spaceAbilities[primarySpaceId] ?? resourceCapabilitiesToAbilitiesMap([]),
838
+ augmentSignInEntriesWithReplication({
839
+ ...this.signInPermissionInputs(address, chainId),
840
+ replication: this.replication
841
+ })
842
+ );
656
843
  return {
657
- abilities: spaceAbilities[primarySpaceId] ?? resourceCapabilitiesToAbilitiesMap([]),
844
+ abilities: mergedPrimaryAbilities,
658
845
  spaceId: primarySpaceId,
659
846
  spaceAbilities,
660
847
  rawAbilities: Object.keys(rawAbilities).length > 0 ? rawAbilities : void 0
@@ -712,14 +899,14 @@ var NodeUserAuthorization = class {
712
899
  * Create the space on the TinyCloud server (host delegation).
713
900
  * This registers the user as the owner of the space.
714
901
  */
715
- async hostSpace(targetSpaceId, purpose) {
902
+ async hostSpace(targetSpaceId, purpose, signal) {
716
903
  if (!this._tinyCloudSession || !this._address || !this._chainId) {
717
904
  throw new Error("Must be signed in to host space");
718
905
  }
719
906
  await this.ensureTinyCloudHosts();
720
907
  const host = this.primaryTinyCloudHost;
721
908
  const spaceId = targetSpaceId ?? this._tinyCloudSession.spaceId;
722
- const peerId = await fetchPeerId(host, spaceId);
909
+ const peerId = await fetchPeerId(host, spaceId, signal);
723
910
  const siwe = this.wasm.generateHostSIWEMessage({
724
911
  address: this._address,
725
912
  chainId: this._chainId,
@@ -728,9 +915,28 @@ var NodeUserAuthorization = class {
728
915
  spaceId,
729
916
  peerId
730
917
  });
731
- const signature = await this.signMessage(siwe, purpose);
918
+ if (signal?.aborted) throw signal.reason;
919
+ const signing = this.signMessage(siwe, purpose);
920
+ let signature;
921
+ if (!signal) {
922
+ signature = await signing;
923
+ } else {
924
+ let rejectOnAbort;
925
+ const aborted = new Promise((_resolve, reject2) => {
926
+ rejectOnAbort = reject2;
927
+ });
928
+ const onAbort = () => rejectOnAbort(signal.reason);
929
+ signal.addEventListener("abort", onAbort, { once: true });
930
+ if (signal.aborted) onAbort();
931
+ try {
932
+ signature = await Promise.race([signing, aborted]);
933
+ } finally {
934
+ signal.removeEventListener("abort", onAbort);
935
+ }
936
+ }
937
+ if (signal?.aborted) throw signal.reason;
732
938
  const headers = this.wasm.siweToDelegationHeaders({ siwe, signature });
733
- return submitHostDelegation(host, headers);
939
+ return submitHostDelegation(host, headers, signal);
734
940
  }
735
941
  /**
736
942
  * Create a specific space on the server via host delegation.
@@ -751,8 +957,8 @@ var NodeUserAuthorization = class {
751
957
  * keeps its HTTP status (e.g. a typed 401/403) instead of collapsing to
752
958
  * `false`.
753
959
  */
754
- async hostOwnedSpaceResult(spaceId, purpose) {
755
- return this.hostSpace(spaceId, purpose);
960
+ async hostOwnedSpaceResult(spaceId, purpose, signal) {
961
+ return this.hostSpace(spaceId, purpose, signal);
756
962
  }
757
963
  /**
758
964
  * Ensure the user's space exists on the TinyCloud server.
@@ -1691,6 +1897,18 @@ var RuntimeDelegationRejectedError = class extends Error {
1691
1897
  this.reason = reason;
1692
1898
  }
1693
1899
  };
1900
+ function activationRejectionReason(error) {
1901
+ if (typeof error !== "object" || error === null) return "activation_rejected";
1902
+ if ("code" in error && error.code === "ACTIVATION_TRANSPORT_FAILED") {
1903
+ return "activation_unavailable";
1904
+ }
1905
+ const cause = "cause" in error ? error.cause : void 0;
1906
+ const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
1907
+ if (typeof status !== "number") return "activation_rejected";
1908
+ if (status === 408 || status === 429 || status >= 500) return "activation_unavailable";
1909
+ if (status === 401) return "activation_unauthenticated";
1910
+ return "activation_rejected";
1911
+ }
1694
1912
  function reject(reason, message) {
1695
1913
  throw new RuntimeDelegationRejectedError(reason, message);
1696
1914
  }
@@ -2075,7 +2293,10 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
2075
2293
  await node.useRuntimeDelegation(installedCandidate);
2076
2294
  }
2077
2295
  } catch (error) {
2078
- reject("activation_rejected", error instanceof Error ? error.message : String(error));
2296
+ reject(
2297
+ activationRejectionReason(error),
2298
+ error instanceof Error ? error.message : String(error)
2299
+ );
2079
2300
  }
2080
2301
  const installed = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
2081
2302
  if (!installed) {
@@ -2148,6 +2369,37 @@ function deserializeDelegation(data) {
2148
2369
  };
2149
2370
  }
2150
2371
 
2372
+ // src/replication/config.ts
2373
+ import { kvPrefixCovers } from "@tinycloud/sdk-services";
2374
+ function assertValidReplicationConfig(replication) {
2375
+ if (replication === void 0 || replication.enabled !== true) return;
2376
+ if (replication.storage === void 0 || replication.storage === null) {
2377
+ throw new TypeError("replication.enabled requires replication.storage");
2378
+ }
2379
+ if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
2380
+ throw new TypeError("replication.prefixes must be a non-empty array");
2381
+ }
2382
+ for (const prefix of replication.prefixes) {
2383
+ if (typeof prefix !== "string" || prefix === "") {
2384
+ throw new TypeError("replication.prefixes must not contain an empty prefix");
2385
+ }
2386
+ }
2387
+ for (const [index, prefix] of replication.prefixes.entries()) {
2388
+ for (const other of replication.prefixes.slice(index + 1)) {
2389
+ if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
2390
+ throw new TypeError(
2391
+ `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
2392
+ );
2393
+ }
2394
+ }
2395
+ if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
2396
+ throw new TypeError(
2397
+ `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
2398
+ );
2399
+ }
2400
+ }
2401
+ }
2402
+
2151
2403
  // src/TinyCloudNode.ts
2152
2404
  import {
2153
2405
  TinyCloud,
@@ -2186,12 +2438,12 @@ import {
2186
2438
  canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
2187
2439
  recapCaveatsEqual,
2188
2440
  expandPermissionEntries as expandPermissionEntriesCore,
2189
- isCapabilitySubset,
2441
+ isCapabilitySubset as isCapabilitySubset2,
2190
2442
  isExplicitOnlyAction,
2191
2443
  parseRecapCapabilities,
2192
2444
  SERVICE_LONG_TO_SHORT,
2193
2445
  SERVICE_SHORT_TO_LONG,
2194
- KV as KV2,
2446
+ KV as KV3,
2195
2447
  SQL as SQL2,
2196
2448
  DUCKDB as DUCKDB2,
2197
2449
  ENCRYPTION as ENCRYPTION2,
@@ -2201,6 +2453,7 @@ import {
2201
2453
  verifyDidKeyEd25519Signature,
2202
2454
  canonicalizeAddress as canonicalizeAddress2,
2203
2455
  pkhDid as pkhDid2,
2456
+ principalDid,
2204
2457
  resolveTinyCloudHosts as resolveTinyCloudHosts2,
2205
2458
  publishLocationRecord,
2206
2459
  principalDidEquals as principalDidEquals3,
@@ -2672,11 +2925,11 @@ function isSecretPayload(value) {
2672
2925
  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";
2673
2926
  }
2674
2927
  var ROOT_DELEGATION_ACTIONS = [
2675
- KV2.PUT,
2676
- KV2.GET,
2677
- KV2.DEL,
2678
- KV2.LIST,
2679
- KV2.METADATA,
2928
+ KV3.PUT,
2929
+ KV3.GET,
2930
+ KV3.DEL,
2931
+ KV3.LIST,
2932
+ KV3.METADATA,
2680
2933
  SQL2.READ,
2681
2934
  SQL2.WRITE,
2682
2935
  SQL2.ADMIN,
@@ -3127,6 +3380,8 @@ var _TinyCloudNode = class _TinyCloudNode {
3127
3380
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
3128
3381
  /** Serializes account-registry writes for the active node instance. */
3129
3382
  this.accountRegistryTail = Promise.resolve();
3383
+ /** Bounds best-effort registry work on the sign-in critical path. */
3384
+ this.accountRegistryDeadlineMs = 4e3;
3130
3385
  /**
3131
3386
  * True when the last signIn() detected an interactive signer and skipped
3132
3387
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -3205,11 +3460,46 @@ var _TinyCloudNode = class _TinyCloudNode {
3205
3460
  });
3206
3461
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
3207
3462
  };
3463
+ assertValidReplicationConfig(config.replication);
3208
3464
  this.explicitHost = config.host;
3209
3465
  this.config = {
3210
3466
  ...config,
3211
3467
  host: config.host ?? DEFAULT_HOST
3212
3468
  };
3469
+ if (this.config.replication?.enabled) {
3470
+ const runtime = getNodeReplicationLoaders().runtime().then(({ ReplicationRuntime }) => {
3471
+ const instance = new ReplicationRuntime(this.config.replication);
3472
+ this.replicationRuntimeInstance = instance;
3473
+ return instance;
3474
+ });
3475
+ this.replicationRuntime = runtime;
3476
+ this.replicationControl = {
3477
+ status: async () => {
3478
+ const status = await runtime.then((ready) => ready.control.status());
3479
+ const scopeError = this.replicationScopeError;
3480
+ if (!scopeError) return status;
3481
+ return this.config.replication.prefixes.map((prefix) => ({
3482
+ prefix,
3483
+ state: "unavailable",
3484
+ lastError: {
3485
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3486
+ code: "SESSION_SCOPE_MISMATCH",
3487
+ message: scopeError
3488
+ },
3489
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
3490
+ pinned: [],
3491
+ lagMs: null
3492
+ }));
3493
+ },
3494
+ sync: async (input) => (await runtime).control.sync(input),
3495
+ purge: (input) => {
3496
+ const instance = this.replicationRuntimeInstance;
3497
+ return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
3498
+ },
3499
+ clearPending: async () => (await runtime).control.clearPending(),
3500
+ close: async () => (await runtime).control.close()
3501
+ };
3502
+ }
3213
3503
  if (config.wasmBindings) {
3214
3504
  this.wasmBindings = config.wasmBindings;
3215
3505
  } else if (_TinyCloudNode.nodeDefaults) {
@@ -3330,7 +3620,14 @@ var _TinyCloudNode = class _TinyCloudNode {
3330
3620
  siweConfig: config.siweConfig,
3331
3621
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
3332
3622
  capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
3333
- includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
3623
+ includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions,
3624
+ // Sign-in needs only the entry inputs (prefixes + secrets opt-in);
3625
+ // storage and scheduling never leave TinyCloudNode.
3626
+ replication: config.replication?.enabled === true ? {
3627
+ enabled: true,
3628
+ prefixes: config.replication.prefixes,
3629
+ allowSecrets: config.replication.allowSecrets === true
3630
+ } : void 0
3334
3631
  });
3335
3632
  this.tc = new TinyCloud(this.auth, {
3336
3633
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -3581,6 +3878,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3581
3878
  this.confirmedHostedSpaceIds.clear();
3582
3879
  await this.tc.signIn(options);
3583
3880
  this.syncResolvedHostFromAuth();
3881
+ this.replicationDelegateSession = void 0;
3882
+ this.replicationScopeError = void 0;
3883
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
3584
3884
  const oldGraph = this._serviceGraph;
3585
3885
  this._serviceGraph = this.createServiceGraphLifetime();
3586
3886
  oldGraph.retire();
@@ -3590,7 +3890,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3590
3890
  this.sessionKeyId = signedInSession.sessionKey;
3591
3891
  this.sessionKeyJwk = signedInSession.jwk;
3592
3892
  }
3593
- this.initializeServices();
3893
+ await this.initializeServices();
3594
3894
  const primarySession = this.currentTinyCloudSession();
3595
3895
  if (primarySession) {
3596
3896
  this.registerPrimarySessionGrant(primarySession);
@@ -3602,6 +3902,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3602
3902
  }
3603
3903
  if (!bootstrapped) {
3604
3904
  this.scheduleAccountRegistrySync();
3905
+ await this.accountRegistryTail;
3605
3906
  }
3606
3907
  this.notificationHandler.success("Successfully signed in");
3607
3908
  }
@@ -4054,7 +4355,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4054
4355
  provenance: "primary"
4055
4356
  });
4056
4357
  }
4057
- async writeManifestRegistryRecords() {
4358
+ async writeManifestRegistryRecords(account, signal) {
4058
4359
  const request = this.capabilityRequest;
4059
4360
  if (!request || request.registryRecords.length === 0) {
4060
4361
  return;
@@ -4063,8 +4364,9 @@ var _TinyCloudNode = class _TinyCloudNode {
4063
4364
  throw new Error("Manifest registry write requires wallet mode");
4064
4365
  }
4065
4366
  const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
4066
- await this.ensureOwnedSpaceHostedById(accountSpaceId);
4067
- const result = await this.account.applications.register(request.manifests);
4367
+ await this.ensureOwnedSpaceHostedById(accountSpaceId, signal);
4368
+ if (signal.aborted) throw signal.reason;
4369
+ const result = await account.applications.register(request.manifests);
4068
4370
  if (!result.ok) {
4069
4371
  throw Object.assign(
4070
4372
  new Error(`Failed to write manifest registry records: ${result.error.message}`),
@@ -4072,34 +4374,113 @@ var _TinyCloudNode = class _TinyCloudNode {
4072
4374
  );
4073
4375
  }
4074
4376
  }
4377
+ createAccountRegistryServices(context, signal) {
4378
+ const session = context.session;
4379
+ if (!session) throw new Error("Account registry requires an active service session");
4380
+ const contexts = [context];
4381
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
4382
+ const createScopedContext = (spaceId) => {
4383
+ const scoped = this._serviceGraph.track(new ServiceContext2({
4384
+ invoke: context.invoke,
4385
+ invokeAny: context.invokeAny,
4386
+ fetch: context.fetch,
4387
+ hosts: context.hosts,
4388
+ telemetry: this.config.telemetry
4389
+ }));
4390
+ scoped.setOperationAbortSignal(signal);
4391
+ scoped.setSession({ ...session, spaceId });
4392
+ contexts.push(scoped);
4393
+ return scoped;
4394
+ };
4395
+ const sql = new SQLService2({});
4396
+ const sqlContext = createScopedContext(accountSpaceId);
4397
+ sql.initialize(sqlContext);
4398
+ const spaces = new SpaceService({
4399
+ hosts: context.hosts,
4400
+ session,
4401
+ invoke: context.invoke,
4402
+ fetch: (url, init) => context.fetch(url, {
4403
+ ...init,
4404
+ signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal
4405
+ }),
4406
+ userDid: this.did,
4407
+ createKVService: (spaceId) => {
4408
+ const kv = new KVService2({});
4409
+ kv.initialize(createScopedContext(spaceId));
4410
+ return kv;
4411
+ }
4412
+ });
4413
+ return {
4414
+ contexts,
4415
+ account: new AccountService({
4416
+ getDid: () => this.did,
4417
+ getHost: () => context.hosts[0] ?? this.config.host,
4418
+ getPrimarySpaceId: () => session.spaceId,
4419
+ getAccountSpaceId: () => accountSpaceId,
4420
+ getSpaces: () => spaces,
4421
+ getAccountDb: () => sql.db("account"),
4422
+ ensureAccountSpaceHosted: () => this.ensureOwnedSpaceHostedById(accountSpaceId, signal)
4423
+ })
4424
+ };
4425
+ }
4075
4426
  scheduleAccountRegistrySync() {
4076
4427
  const session = this.currentTinyCloudSession();
4077
4428
  if (!session || this.pendingAccountRegistrySync?.session === session) {
4078
4429
  return;
4079
4430
  }
4431
+ const controller = new AbortController();
4432
+ const deadline = setTimeout(
4433
+ () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
4434
+ this.accountRegistryDeadlineMs
4435
+ );
4080
4436
  const promise = this.enqueueAccountRegistryOperation(session, async () => {
4081
- await this.withAccountRegistryRetry(async () => {
4082
- if (this.currentTinyCloudSession() !== session) {
4083
- return;
4084
- }
4085
- await this.account.index.ensure();
4086
- if (this.currentTinyCloudSession() !== session) {
4087
- return;
4088
- }
4089
- await this.writeManifestRegistryRecords();
4090
- if (this.currentTinyCloudSession() !== session) {
4091
- return;
4092
- }
4093
- if (this.currentSessionCanListSpaces()) {
4094
- const spaces = await this.account.spaces.syncAccessible();
4095
- if (!spaces.ok) {
4096
- throw Object.assign(
4097
- new Error(`Failed to sync account spaces: ${spaces.error.message}`),
4098
- { cause: spaces.error }
4099
- );
4437
+ const sharedContext = this._serviceContext;
4438
+ if (!sharedContext) return;
4439
+ const context = this._serviceGraph.track(new ServiceContext2({
4440
+ invoke: sharedContext.invoke,
4441
+ invokeAny: sharedContext.invokeAny,
4442
+ fetch: sharedContext.fetch,
4443
+ hosts: sharedContext.hosts,
4444
+ telemetry: this.config.telemetry
4445
+ }));
4446
+ context.setOperationAbortSignal(controller.signal);
4447
+ const current = this.currentTinyCloudSession();
4448
+ if (!current) return;
4449
+ context.setSession({
4450
+ delegationHeader: current.delegationHeader,
4451
+ delegationCid: current.delegationCid,
4452
+ spaceId: current.spaceId,
4453
+ verificationMethod: current.verificationMethod,
4454
+ jwk: current.jwk
4455
+ });
4456
+ const registry = this.createAccountRegistryServices(context, controller.signal);
4457
+ try {
4458
+ await this.withAccountRegistryRetry(async () => {
4459
+ if (controller.signal.aborted) throw controller.signal.reason;
4460
+ if (this.currentTinyCloudSession() !== session) return;
4461
+ await registry.account.index.ensure();
4462
+ if (controller.signal.aborted) throw controller.signal.reason;
4463
+ if (this.currentTinyCloudSession() !== session) return;
4464
+ await this.writeManifestRegistryRecords(registry.account, controller.signal);
4465
+ if (controller.signal.aborted) throw controller.signal.reason;
4466
+ if (this.currentTinyCloudSession() !== session) return;
4467
+ if (this.currentSessionCanListSpaces()) {
4468
+ const result = await registry.account.spaces.syncAccessible();
4469
+ if (controller.signal.aborted) throw controller.signal.reason;
4470
+ if (!result.ok) {
4471
+ throw Object.assign(
4472
+ new Error(`Failed to sync account spaces: ${result.error.message}`),
4473
+ { cause: result.error }
4474
+ );
4475
+ }
4100
4476
  }
4477
+ }, controller.signal);
4478
+ } finally {
4479
+ for (const operationContext of registry.contexts) {
4480
+ operationContext.retire();
4101
4481
  }
4102
- });
4482
+ clearTimeout(deadline);
4483
+ }
4103
4484
  });
4104
4485
  this.pendingAccountRegistrySync = { session, promise };
4105
4486
  const clearPendingSync = () => {
@@ -4108,6 +4489,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4108
4489
  }
4109
4490
  };
4110
4491
  void promise.then(clearPendingSync, clearPendingSync);
4492
+ void promise.then(
4493
+ () => clearTimeout(deadline),
4494
+ () => clearTimeout(deadline)
4495
+ );
4111
4496
  }
4112
4497
  enqueueAccountRegistryOperation(session, task) {
4113
4498
  const operation = this.accountRegistryTail.then(async () => {
@@ -4143,14 +4528,22 @@ var _TinyCloudNode = class _TinyCloudNode {
4143
4528
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
4144
4529
  );
4145
4530
  }
4146
- async withAccountRegistryRetry(task) {
4531
+ async withAccountRegistryRetry(task, signal) {
4147
4532
  const delays = [250, 1e3, 3e3];
4148
4533
  let lastError;
4149
4534
  for (let attempt = 0; attempt < delays.length; attempt += 1) {
4535
+ if (signal.aborted) {
4536
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", signal.reason);
4537
+ return;
4538
+ }
4150
4539
  try {
4151
4540
  await task();
4152
4541
  return;
4153
4542
  } catch (error) {
4543
+ if (signal.aborted) {
4544
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
4545
+ return;
4546
+ }
4154
4547
  const verdict = authorizationVerdictOf(error);
4155
4548
  const message = error instanceof Error ? error.message : String(error);
4156
4549
  const isAuthorizationVerdict = verdict === void 0 ? /Unauthorized Action|\b(?:401|403)\b/.test(message) : verdict !== "other";
@@ -4162,12 +4555,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4162
4555
  return;
4163
4556
  }
4164
4557
  if (isStorageFullError(error) || isStorageFullError(error?.cause) || message.includes(STORAGE_FULL_MESSAGE) || message.includes(STORAGE_WRITE_TOO_LARGE_MESSAGE)) {
4165
- console.warn("TinyCloud account registry sync stopped: storage is full", error);
4558
+ console.warn(
4559
+ "TinyCloud account registry sync stopped: storage is full",
4560
+ error
4561
+ );
4166
4562
  return;
4167
4563
  }
4168
4564
  lastError = error;
4169
4565
  if (attempt < delays.length - 1) {
4170
- await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
4566
+ await new Promise((resolve, reject2) => {
4567
+ const timer = setTimeout(() => {
4568
+ signal.removeEventListener("abort", onAbort);
4569
+ resolve();
4570
+ }, delays[attempt]);
4571
+ const onAbort = () => {
4572
+ clearTimeout(timer);
4573
+ signal.removeEventListener("abort", onAbort);
4574
+ reject2(signal.reason);
4575
+ };
4576
+ signal.addEventListener("abort", onAbort, { once: true });
4577
+ }).catch((error2) => {
4578
+ if (!signal.aborted) throw error2;
4579
+ });
4171
4580
  }
4172
4581
  }
4173
4582
  }
@@ -4217,7 +4626,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4217
4626
  * self-revealing: the next write to the space fails immediately and loudly
4218
4627
  * with `404 Space not found`.
4219
4628
  */
4220
- async ensureOwnedSpaceHostedById(spaceId) {
4629
+ async ensureOwnedSpaceHostedById(spaceId, signal) {
4221
4630
  if (!this.auth) {
4222
4631
  throw new Error("Owned space hosting requires wallet mode");
4223
4632
  }
@@ -4232,7 +4641,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4232
4641
  if (!host) {
4233
4642
  throw new Error("Owned space hosting requires a TinyCloud host");
4234
4643
  }
4235
- const activation = await activateSessionWithHost2(host, session.delegationHeader);
4644
+ const activation = await activateSessionWithHost2(host, session.delegationHeader, signal);
4236
4645
  if (activation.success && !activation.skipped?.includes(spaceId)) {
4237
4646
  this.confirmedHostedSpaceIds.add(spaceId);
4238
4647
  return;
@@ -4243,15 +4652,16 @@ var _TinyCloudNode = class _TinyCloudNode {
4243
4652
  { cause: activation }
4244
4653
  );
4245
4654
  }
4246
- const created = await this.auth.hostOwnedSpaceResult(spaceId);
4655
+ const authorization = this.auth;
4656
+ const created = signal ? await authorization.hostOwnedSpaceResult(spaceId, void 0, signal) : await authorization.hostOwnedSpaceResult(spaceId);
4247
4657
  if (!created.success) {
4248
4658
  throw Object.assign(
4249
4659
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
4250
4660
  { cause: created }
4251
4661
  );
4252
4662
  }
4253
- await new Promise((resolve) => setTimeout(resolve, 100));
4254
- const retry = await activateSessionWithHost2(host, session.delegationHeader);
4663
+ if (signal?.aborted) throw signal.reason;
4664
+ const retry = await activateSessionWithHost2(host, session.delegationHeader, signal);
4255
4665
  if (!retry.success || retry.skipped?.includes(spaceId)) {
4256
4666
  throw Object.assign(
4257
4667
  new Error(
@@ -4529,6 +4939,34 @@ var _TinyCloudNode = class _TinyCloudNode {
4529
4939
  const stagedHost = resolvedHost ?? this.config.host;
4530
4940
  const stagedAddress = restoredAddress;
4531
4941
  const stagedChainId = sessionData.chainId ?? 1;
4942
+ let replicationPrincipal;
4943
+ let compactReplicationSession;
4944
+ let replicationScopeError;
4945
+ if (this.config.replication?.enabled) {
4946
+ if (stagedAddress !== void 0) {
4947
+ replicationPrincipal = pkhDid2(stagedAddress, stagedChainId);
4948
+ } else {
4949
+ const { replicationScopeFromSignedSession } = await getNodeReplicationLoaders().authority();
4950
+ const signedScope = replicationScopeFromSignedSession({
4951
+ delegationHeader: sessionData.delegationHeader,
4952
+ delegationCid: sessionData.delegationCid,
4953
+ verificationMethod: restoredVerificationMethod,
4954
+ persistedSpace: sessionData.spaceId
4955
+ });
4956
+ if (!signedScope) {
4957
+ replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
4958
+ } else {
4959
+ replicationPrincipal = signedScope.principal;
4960
+ compactReplicationSession = {
4961
+ delegationHeader: sessionData.delegationHeader,
4962
+ delegationCid: sessionData.delegationCid,
4963
+ spaceId: signedScope.space,
4964
+ verificationMethod: restoredVerificationMethod,
4965
+ jwk: stagedJwk
4966
+ };
4967
+ }
4968
+ }
4969
+ }
4532
4970
  const stagedNodeDid = stagedAddress ? pkhDid2(stagedAddress, stagedChainId) : canonicalVerificationMethod;
4533
4971
  const serviceSession = {
4534
4972
  delegationHeader: sessionData.delegationHeader,
@@ -4607,7 +5045,18 @@ var _TinyCloudNode = class _TinyCloudNode {
4607
5045
  } else {
4608
5046
  this._restoredTcSession = stagedTcSession;
4609
5047
  }
5048
+ this.replicationDelegateSession = compactReplicationSession;
5049
+ this.replicationScopeError = replicationScopeError;
5050
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4610
5051
  oldGraph.retire();
5052
+ if (replicationScopeError) {
5053
+ stagedGraph.serviceContext.emit("replication.state", {
5054
+ state: "unavailable",
5055
+ code: "SESSION_SCOPE_MISMATCH",
5056
+ detail: replicationScopeError
5057
+ });
5058
+ }
5059
+ await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
4611
5060
  oldCore?.retireServices?.();
4612
5061
  }
4613
5062
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
@@ -4761,6 +5210,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4761
5210
  }));
4762
5211
  context.setSession(config.session);
4763
5212
  service.initialize(context);
5213
+ this.attachReplicationKV(config.session.spaceId, service);
4764
5214
  return service;
4765
5215
  }
4766
5216
  });
@@ -4801,6 +5251,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4801
5251
  }));
4802
5252
  context.setSession({ ...input.serviceSession, spaceId });
4803
5253
  scopedKv.initialize(context);
5254
+ this.attachReplicationKV(spaceId, scopedKv);
4804
5255
  return scopedKv;
4805
5256
  },
4806
5257
  createVaultService: (spaceId) => {
@@ -4815,6 +5266,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4815
5266
  }));
4816
5267
  context.setSession({ ...input.serviceSession, spaceId });
4817
5268
  scopedKv.initialize(context);
5269
+ this.attachReplicationKV(spaceId, scopedKv);
4818
5270
  const scopedVault = this.createVaultService(spaceId, scopedKv, encryption, {
4819
5271
  host: input.host,
4820
5272
  did: input.nodeDid,
@@ -5095,7 +5547,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5095
5547
  siweConfig: this.config.siweConfig,
5096
5548
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
5097
5549
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
5098
- includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
5550
+ includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions,
5551
+ replication: this.config.replication?.enabled === true ? {
5552
+ enabled: true,
5553
+ prefixes: this.config.replication.prefixes,
5554
+ allowSecrets: this.config.replication.allowSecrets === true
5555
+ } : void 0
5099
5556
  });
5100
5557
  this.tc = new TinyCloud(this.auth, {
5101
5558
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -5148,7 +5605,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5148
5605
  siweConfig: this.config.siweConfig,
5149
5606
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
5150
5607
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
5151
- includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
5608
+ includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions,
5609
+ replication: this.config.replication?.enabled === true ? {
5610
+ enabled: true,
5611
+ prefixes: this.config.replication.prefixes,
5612
+ allowSecrets: this.config.replication.allowSecrets === true
5613
+ } : void 0
5152
5614
  });
5153
5615
  this.tc = new TinyCloud(this.auth, {
5154
5616
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -5160,7 +5622,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5160
5622
  * Initialize the service context and KV service after sign-in.
5161
5623
  * @internal
5162
5624
  */
5163
- initializeServices() {
5625
+ async initializeServices() {
5164
5626
  const session = this.currentTinyCloudSession();
5165
5627
  if (!session) {
5166
5628
  return;
@@ -5203,12 +5665,18 @@ var _TinyCloudNode = class _TinyCloudNode {
5203
5665
  jwk: session.jwk
5204
5666
  };
5205
5667
  this._serviceContext.setSession(serviceSession);
5668
+ await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
5206
5669
  this.tc.serviceContext.setSession(serviceSession);
5207
5670
  this._vault = this.createVaultService(session.spaceId, this._kv);
5208
5671
  this._vault.initialize(this._serviceContext);
5209
5672
  this._serviceContext.registerService("vault", this._vault);
5210
5673
  this.initializeV2Services(serviceSession);
5211
5674
  }
5675
+ attachReplicationKV(spaceId, kvService) {
5676
+ if (!this.replicationRuntime) return;
5677
+ const attached = this.replicationRuntime.then((runtime) => runtime.attach(spaceId, kvService));
5678
+ kvService.setReadThroughReady(attached);
5679
+ }
5212
5680
  createSpaceScopedKVService(spaceId) {
5213
5681
  const kvService = new KVService2({});
5214
5682
  if (this._serviceContext) {
@@ -5224,6 +5692,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5224
5692
  spaceScopedContext.setSession({ ...session, spaceId });
5225
5693
  }
5226
5694
  kvService.initialize(spaceScopedContext);
5695
+ this.attachReplicationKV(spaceId, kvService);
5227
5696
  }
5228
5697
  return kvService;
5229
5698
  }
@@ -5686,6 +6155,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5686
6155
  }));
5687
6156
  context.setSession(config.session);
5688
6157
  service.initialize(context);
6158
+ this.attachReplicationKV(config.session.spaceId, service);
5689
6159
  return service;
5690
6160
  },
5691
6161
  onRootDelegationNeeded: this.signer ? async (params) => {
@@ -6010,6 +6480,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6010
6480
  }));
6011
6481
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
6012
6482
  kv.initialize(spaceScopedContext);
6483
+ this.attachReplicationKV(spaceId, kv);
6013
6484
  return kv;
6014
6485
  }
6015
6486
  /**
@@ -6432,10 +6903,18 @@ var _TinyCloudNode = class _TinyCloudNode {
6432
6903
  );
6433
6904
  }
6434
6905
  const targetHost = delegation.host ?? this.config.host;
6435
- const activateResult = await activateSessionWithHost2(
6436
- targetHost,
6437
- delegation.delegationHeader
6438
- );
6906
+ let activateResult;
6907
+ try {
6908
+ activateResult = await activateSessionWithHost2(
6909
+ targetHost,
6910
+ delegation.delegationHeader
6911
+ );
6912
+ } catch (cause) {
6913
+ throw Object.assign(
6914
+ new Error("The TinyCloud host could not be reached to activate the delegation."),
6915
+ { code: "ACTIVATION_TRANSPORT_FAILED", cause }
6916
+ );
6917
+ }
6439
6918
  if (!activateResult.success) {
6440
6919
  if (this.operationsFromDelegation(delegation).some(
6441
6920
  (operation) => operation.resource?.startsWith("urn:cid:") === true && operation.service === "delegation" && operation.action === "tinycloud.delegation/revoke"
@@ -6723,7 +7202,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6723
7202
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
6724
7203
  }
6725
7204
  await this.auth.hostPublicSpace(publicSpaceId);
6726
- const kvActions = [KV2.PUT, KV2.GET, KV2.DEL, KV2.LIST, KV2.METADATA];
7205
+ const kvActions = [KV3.PUT, KV3.GET, KV3.DEL, KV3.LIST, KV3.METADATA];
6727
7206
  const abilities = { kv: { "": kvActions } };
6728
7207
  const now = /* @__PURE__ */ new Date();
6729
7208
  const expiryMs = EXPIRY3.EPHEMERAL_MS;
@@ -6964,31 +7443,31 @@ var _TinyCloudNode = class _TinyCloudNode {
6964
7443
  );
6965
7444
  return { delegation: delegation2, prompted: true };
6966
7445
  }
6967
- const granted = this.projectSignedRecapCapabilities(session.siwe);
6968
- const { subset, missing } = this.signedCapabilitySubset(expandedEntries, granted);
6969
- if (!subset) {
6970
- const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
6971
- const runtimeGrant = this.findGrantForOperations(
6972
- runtimeOperations,
6973
- { excludePrimary: true }
6974
- );
6975
- if (runtimeGrant) {
6976
- const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
6977
- if (runtimeGrant.expiresAt.getTime() <= Date.now() + marginMs) {
6978
- throw new SessionExpiredError(runtimeGrant.expiresAt);
6979
- }
6980
- const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
6981
- const delegation2 = await this.createDelegationViaRuntimeGrant(
6982
- did,
6983
- expandedEntries,
6984
- runtimeExpiration,
6985
- runtimeGrant,
6986
- runtimeOperations,
6987
- onPrepared
6988
- );
6989
- return { delegation: delegation2, prompted: false };
7446
+ const plan = this.planDerivation(
7447
+ expandedEntries,
7448
+ session,
7449
+ effectiveExpiration,
7450
+ Date.now()
7451
+ );
7452
+ if ("refused" in plan) {
7453
+ if (plan.refused === "SESSION_EXPIRING") {
7454
+ throw new SessionExpiredError(plan.expiredAt ?? sessionExpiry ?? /* @__PURE__ */ new Date(0));
7455
+ }
7456
+ if (plan.refused === "CAVEATED_AUTHORITY") {
7457
+ throw new CaveatedDelegationUnsupportedError(plan.caveated ?? []);
6990
7458
  }
6991
- throw new PermissionNotInManifestError(missing, granted);
7459
+ throw new PermissionNotInManifestError(plan.missing ?? [], plan.granted ?? []);
7460
+ }
7461
+ if (plan.path === "runtime") {
7462
+ const delegation2 = await this.createDelegationViaRuntimeGrant(
7463
+ did,
7464
+ expandedEntries,
7465
+ plan.effectiveExpiration,
7466
+ plan.grant,
7467
+ plan.operations,
7468
+ onPrepared
7469
+ );
7470
+ return { delegation: delegation2, prompted: false };
6992
7471
  }
6993
7472
  const delegation = await this.createDelegationViaWasmPath(
6994
7473
  did,
@@ -6999,6 +7478,149 @@ var _TinyCloudNode = class _TinyCloudNode {
6999
7478
  );
7000
7479
  return { delegation, prompted: false };
7001
7480
  }
7481
+ /**
7482
+ * What `delegateTo` would select for `entries`, without signing or any
7483
+ * network call (TC-858 plan v3 §4.3): the session path when the signed
7484
+ * recap covers every entry, the runtime-grant path otherwise, or a
7485
+ * refusal. A refusal of `CAVEATED_AUTHORITY` means coverage exists only
7486
+ * through caveated branches, which the action-only WASM child-delegation
7487
+ * boundary cannot preserve — replication never derives authority through
7488
+ * them. `expiredAt`/`missing`/`granted`/`caveated` carry the payload
7489
+ * `delegateTo` turns back into its typed errors.
7490
+ * @internal
7491
+ */
7492
+ planDerivation(expandedEntries, session, effectiveExpiration, nowMs) {
7493
+ const caveatedRequest = expandedEntries.filter(
7494
+ (entry) => !recapCaveatsEqual(entry.caveats, void 0)
7495
+ );
7496
+ if (caveatedRequest.length > 0) {
7497
+ return { refused: "CAVEATED_AUTHORITY", caveated: caveatedRequest };
7498
+ }
7499
+ const granted = this.projectSignedRecapCapabilities(session.siwe);
7500
+ const { subset, missing } = this.signedCapabilitySubset(expandedEntries, granted);
7501
+ if (subset) {
7502
+ return {
7503
+ path: "session",
7504
+ parentCid: session.delegationCid,
7505
+ expiresAt: effectiveExpiration.getTime(),
7506
+ effectiveExpiration
7507
+ };
7508
+ }
7509
+ const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
7510
+ const runtimeGrant = this.findGrantForOperations(
7511
+ runtimeOperations,
7512
+ { excludePrimary: true }
7513
+ );
7514
+ if (runtimeGrant) {
7515
+ const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
7516
+ if (runtimeGrant.expiresAt.getTime() <= nowMs + marginMs) {
7517
+ return { refused: "SESSION_EXPIRING", expiredAt: runtimeGrant.expiresAt };
7518
+ }
7519
+ const caveated = this.caveatedRuntimeGrantEntries(
7520
+ expandedEntries,
7521
+ runtimeOperations,
7522
+ runtimeGrant
7523
+ );
7524
+ if (caveated.length > 0) {
7525
+ return { refused: "CAVEATED_AUTHORITY", caveated };
7526
+ }
7527
+ const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
7528
+ return {
7529
+ path: "runtime",
7530
+ parentCid: runtimeGrant.session.delegationCid,
7531
+ expiresAt: runtimeExpiration.getTime(),
7532
+ effectiveExpiration: runtimeExpiration,
7533
+ grant: runtimeGrant,
7534
+ operations: runtimeOperations
7535
+ };
7536
+ }
7537
+ const relaxedGranted = granted.map((entry) => ({ ...entry, caveats: void 0 }));
7538
+ const relaxedRequested = expandedEntries.map((entry) => ({ ...entry, caveats: void 0 }));
7539
+ const relaxed = this.signedCapabilitySubset(relaxedRequested, relaxedGranted);
7540
+ if (relaxed.subset) {
7541
+ return {
7542
+ refused: "CAVEATED_AUTHORITY",
7543
+ caveated: missing.map((entry) => ({ ...entry, caveats: [{ via: "session-recap" }] }))
7544
+ };
7545
+ }
7546
+ return { refused: "NOT_COVERED", missing, granted };
7547
+ }
7548
+ /**
7549
+ * The plan `delegateTo(device, entries)` would follow, as a pure lookup:
7550
+ * no WASM sign and no `POST /delegate`. Backs
7551
+ * `ReplicationAuthority.plan` (and `mint` inside it) so a predicted
7552
+ * parent and expiry can never disagree with what a mint would use (§4.3,
7553
+ * §4.4). Session expiry inside the 60 s margin refuses with
7554
+ * `SESSION_EXPIRING`; the same check delegateTo performs.
7555
+ * @internal
7556
+ */
7557
+ planDelegation(entries, options) {
7558
+ if (!Array.isArray(entries) || entries.length === 0) {
7559
+ throw new Error("planDelegation requires a non-empty permissions array");
7560
+ }
7561
+ const session = this.currentTinyCloudSession();
7562
+ if (!session) {
7563
+ return { refused: "SESSION_EXPIRING", expiredAt: /* @__PURE__ */ new Date(0) };
7564
+ }
7565
+ const nowMs = Date.now();
7566
+ const sessionExpiry = extractSiweExpiration(session.siwe);
7567
+ if (sessionExpiry !== void 0 && sessionExpiry.getTime() <= nowMs + _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS) {
7568
+ return { refused: "SESSION_EXPIRING", expiredAt: sessionExpiry };
7569
+ }
7570
+ const expandedEntries = this.expandPermissionEntries(entries);
7571
+ const expiryMs = resolveExpiryMs(options?.expiry);
7572
+ const expirationTime = new Date(nowMs + expiryMs);
7573
+ const effectiveExpiration = sessionExpiry !== void 0 && sessionExpiry < expirationTime ? sessionExpiry : expirationTime;
7574
+ return this.planDerivation(expandedEntries, session, effectiveExpiration, nowMs);
7575
+ }
7576
+ /**
7577
+ * The replication entries this node's sign-in request carries for the
7578
+ * current replication config and the signed-in or restored address: the
7579
+ * output of the same augmentation step `resolveSignInCapabilities` runs,
7580
+ * with the unrestricted-get and secrets gates applied (§4.1, §4.6).
7581
+ * Empty when replication is off or no address is known. Pure.
7582
+ */
7583
+ replicationSignInEntries() {
7584
+ if (this.config.replication?.enabled !== true) return [];
7585
+ const auth = this.auth;
7586
+ if (auth === void 0 || typeof auth.replicationSignInEntries !== "function") {
7587
+ return [];
7588
+ }
7589
+ return auth.replicationSignInEntries();
7590
+ }
7591
+ async replicationAuthority() {
7592
+ const { createReplicationAuthority } = await getNodeReplicationLoaders().authority();
7593
+ return createReplicationAuthority({
7594
+ replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
7595
+ siweExpiration: (siwe) => extractSiweExpiration(siwe),
7596
+ planDelegation: (entries, options) => this.planDelegation(entries, options),
7597
+ mintDelegation: async (deviceDid, entries) => {
7598
+ const result = await this.delegateTo(principalDid(deviceDid), entries);
7599
+ return {
7600
+ ucan: result.delegation.delegationHeader.Authorization,
7601
+ expiresAt: result.delegation.expiry.getTime()
7602
+ };
7603
+ }
7604
+ });
7605
+ }
7606
+ async bindReplicationRuntime(context, session, kv, principal) {
7607
+ if (!this.replicationRuntime) return;
7608
+ const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : pkhDid2(this._address, this._chainId));
7609
+ if (identityPrincipal === void 0) return;
7610
+ const runtime = await this.replicationRuntime;
7611
+ const authority = await this.replicationAuthority();
7612
+ await runtime.bind({
7613
+ context,
7614
+ session,
7615
+ principal: identityPrincipal,
7616
+ authority,
7617
+ primaryKV: [{ space: session.spaceId, kv }]
7618
+ });
7619
+ }
7620
+ /** Stable control facade for this node's optional replication runtime. */
7621
+ get replication() {
7622
+ return this.replicationControl;
7623
+ }
7002
7624
  /**
7003
7625
  * Materialize one manifest-declared delegation using the current session key.
7004
7626
  * Delivery is intentionally out of band; callers decide how to transmit the
@@ -7221,6 +7843,18 @@ var _TinyCloudNode = class _TinyCloudNode {
7221
7843
  * it only accepts action maps, so reject the selected constrained branch.
7222
7844
  */
7223
7845
  assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant) {
7846
+ const caveated = this.caveatedRuntimeGrantEntries(entries, requestedOperations, grant);
7847
+ if (caveated.length > 0) {
7848
+ throw new CaveatedDelegationUnsupportedError(caveated);
7849
+ }
7850
+ }
7851
+ /**
7852
+ * The entries of `grant` that cover a requested operation while carrying
7853
+ * caveats — the branches {@link assertRuntimeGrantCaveatsPreservable}
7854
+ * rejects and {@link planDerivation} classifies as `CAVEATED_AUTHORITY`.
7855
+ * Shared so the pure plan and the mint path can't disagree.
7856
+ */
7857
+ caveatedRuntimeGrantEntries(entries, requestedOperations, grant) {
7224
7858
  let operationIndex = 0;
7225
7859
  const caveated = [];
7226
7860
  for (const entry of entries) {
@@ -7239,9 +7873,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7239
7873
  }
7240
7874
  }
7241
7875
  }
7242
- if (caveated.length > 0) {
7243
- throw new CaveatedDelegationUnsupportedError(caveated);
7244
- }
7876
+ return caveated;
7245
7877
  }
7246
7878
  /** Reject caveated parent branches before action-only child signing. */
7247
7879
  assertPortableDelegationCaveatsPreservable(delegation) {
@@ -7346,7 +7978,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7346
7978
  const missing = requested.filter((entry) => {
7347
7979
  const raw = this.isEncryptionPermissionEntry(entry) || this.isRawRevocationPermissionEntry(entry);
7348
7980
  const comparable = raw && this.isEncryptionPermissionEntry(entry) ? { ...entry, space: "encryption" } : entry;
7349
- return !isCapabilitySubset([comparable], raw ? rawGranted : spaceGranted).subset;
7981
+ return !isCapabilitySubset2([comparable], raw ? rawGranted : spaceGranted).subset;
7350
7982
  });
7351
7983
  return { subset: missing.length === 0, missing };
7352
7984
  }
@@ -7818,7 +8450,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7818
8450
  session.chainId
7819
8451
  );
7820
8452
  const publicAbilities = {
7821
- kv: { "": [KV2.GET, KV2.PUT, KV2.METADATA] }
8453
+ kv: { "": [KV3.GET, KV3.PUT, KV3.METADATA] }
7822
8454
  };
7823
8455
  const publicPrepared = this.wasmBindings.prepareSession({
7824
8456
  abilities: publicAbilities,
@@ -7846,7 +8478,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7846
8478
  delegationHeader: publicSession.delegationHeader,
7847
8479
  spaceId: publicSpaceId,
7848
8480
  path: "",
7849
- actions: [KV2.GET, KV2.PUT, KV2.METADATA],
8481
+ actions: [KV3.GET, KV3.PUT, KV3.METADATA],
7850
8482
  disableSubDelegation: params.disableSubDelegation ?? false,
7851
8483
  expiry: expirationTime,
7852
8484
  delegateDID: params.delegateDID,
@@ -8120,7 +8752,7 @@ import {
8120
8752
  resolveManifest as resolveManifest2,
8121
8753
  validateManifest,
8122
8754
  loadManifest,
8123
- isCapabilitySubset as isCapabilitySubset2,
8755
+ isCapabilitySubset as isCapabilitySubset3,
8124
8756
  expandActionShortNames,
8125
8757
  expandPermissionEntries as expandPermissionEntries2,
8126
8758
  expandPermissionEntry,
@@ -8165,7 +8797,7 @@ import {
8165
8797
  SpaceService as SpaceService2,
8166
8798
  SpaceErrorCodes,
8167
8799
  createSpaceService,
8168
- parseSpaceUri,
8800
+ parseSpaceUri as parseSpaceUri2,
8169
8801
  buildSpaceUri,
8170
8802
  makePublicSpaceId as makePublicSpaceId2,
8171
8803
  Space
@@ -8256,7 +8888,7 @@ export {
8256
8888
  expandPermissionEntry,
8257
8889
  getTinyCloudDebugLogs,
8258
8890
  installTinyCloudDebugGlobals,
8259
- isCapabilitySubset2 as isCapabilitySubset,
8891
+ isCapabilitySubset3 as isCapabilitySubset,
8260
8892
  isEvmAddress,
8261
8893
  loadManifest,
8262
8894
  makePkhSpaceId2 as makePkhSpaceId,
@@ -8265,10 +8897,11 @@ export {
8265
8897
  parseExpiry2 as parseExpiry,
8266
8898
  parsePkhDid,
8267
8899
  parseSecretCatalogKey,
8268
- parseSpaceUri,
8900
+ parseSpaceUri2 as parseSpaceUri,
8269
8901
  pkhDid3 as pkhDid,
8270
- principalDid,
8902
+ principalDid2 as principalDid,
8271
8903
  principalDidEquals4 as principalDidEquals,
8904
+ registerReplicationLoaders,
8272
8905
  resolveManifest2 as resolveManifest,
8273
8906
  resolveSecretListPrefix2 as resolveSecretListPrefix,
8274
8907
  resolveSecretPath3 as resolveSecretPath,