@tinycloud/node-sdk 3.2.0-beta.2 → 3.2.0-beta.4

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.
@@ -2163,6 +2369,37 @@ function deserializeDelegation(data) {
2163
2369
  };
2164
2370
  }
2165
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
+
2166
2403
  // src/TinyCloudNode.ts
2167
2404
  import {
2168
2405
  TinyCloud,
@@ -2201,12 +2438,12 @@ import {
2201
2438
  canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
2202
2439
  recapCaveatsEqual,
2203
2440
  expandPermissionEntries as expandPermissionEntriesCore,
2204
- isCapabilitySubset,
2441
+ isCapabilitySubset as isCapabilitySubset2,
2205
2442
  isExplicitOnlyAction,
2206
2443
  parseRecapCapabilities,
2207
2444
  SERVICE_LONG_TO_SHORT,
2208
2445
  SERVICE_SHORT_TO_LONG,
2209
- KV as KV2,
2446
+ KV as KV3,
2210
2447
  SQL as SQL2,
2211
2448
  DUCKDB as DUCKDB2,
2212
2449
  ENCRYPTION as ENCRYPTION2,
@@ -2216,6 +2453,7 @@ import {
2216
2453
  verifyDidKeyEd25519Signature,
2217
2454
  canonicalizeAddress as canonicalizeAddress2,
2218
2455
  pkhDid as pkhDid2,
2456
+ principalDid,
2219
2457
  resolveTinyCloudHosts as resolveTinyCloudHosts2,
2220
2458
  publishLocationRecord,
2221
2459
  principalDidEquals as principalDidEquals3,
@@ -2687,11 +2925,11 @@ function isSecretPayload(value) {
2687
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";
2688
2926
  }
2689
2927
  var ROOT_DELEGATION_ACTIONS = [
2690
- KV2.PUT,
2691
- KV2.GET,
2692
- KV2.DEL,
2693
- KV2.LIST,
2694
- KV2.METADATA,
2928
+ KV3.PUT,
2929
+ KV3.GET,
2930
+ KV3.DEL,
2931
+ KV3.LIST,
2932
+ KV3.METADATA,
2695
2933
  SQL2.READ,
2696
2934
  SQL2.WRITE,
2697
2935
  SQL2.ADMIN,
@@ -3142,6 +3380,8 @@ var _TinyCloudNode = class _TinyCloudNode {
3142
3380
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
3143
3381
  /** Serializes account-registry writes for the active node instance. */
3144
3382
  this.accountRegistryTail = Promise.resolve();
3383
+ /** Bounds best-effort registry work on the sign-in critical path. */
3384
+ this.accountRegistryDeadlineMs = 4e3;
3145
3385
  /**
3146
3386
  * True when the last signIn() detected an interactive signer and skipped
3147
3387
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -3220,11 +3460,46 @@ var _TinyCloudNode = class _TinyCloudNode {
3220
3460
  });
3221
3461
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
3222
3462
  };
3463
+ assertValidReplicationConfig(config.replication);
3223
3464
  this.explicitHost = config.host;
3224
3465
  this.config = {
3225
3466
  ...config,
3226
3467
  host: config.host ?? DEFAULT_HOST
3227
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
+ }
3228
3503
  if (config.wasmBindings) {
3229
3504
  this.wasmBindings = config.wasmBindings;
3230
3505
  } else if (_TinyCloudNode.nodeDefaults) {
@@ -3345,7 +3620,14 @@ var _TinyCloudNode = class _TinyCloudNode {
3345
3620
  siweConfig: config.siweConfig,
3346
3621
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
3347
3622
  capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
3348
- 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
3349
3631
  });
3350
3632
  this.tc = new TinyCloud(this.auth, {
3351
3633
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -3596,6 +3878,9 @@ var _TinyCloudNode = class _TinyCloudNode {
3596
3878
  this.confirmedHostedSpaceIds.clear();
3597
3879
  await this.tc.signIn(options);
3598
3880
  this.syncResolvedHostFromAuth();
3881
+ this.replicationDelegateSession = void 0;
3882
+ this.replicationScopeError = void 0;
3883
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
3599
3884
  const oldGraph = this._serviceGraph;
3600
3885
  this._serviceGraph = this.createServiceGraphLifetime();
3601
3886
  oldGraph.retire();
@@ -3605,7 +3890,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3605
3890
  this.sessionKeyId = signedInSession.sessionKey;
3606
3891
  this.sessionKeyJwk = signedInSession.jwk;
3607
3892
  }
3608
- this.initializeServices();
3893
+ await this.initializeServices();
3609
3894
  const primarySession = this.currentTinyCloudSession();
3610
3895
  if (primarySession) {
3611
3896
  this.registerPrimarySessionGrant(primarySession);
@@ -3617,6 +3902,7 @@ var _TinyCloudNode = class _TinyCloudNode {
3617
3902
  }
3618
3903
  if (!bootstrapped) {
3619
3904
  this.scheduleAccountRegistrySync();
3905
+ await this.accountRegistryTail;
3620
3906
  }
3621
3907
  this.notificationHandler.success("Successfully signed in");
3622
3908
  }
@@ -4069,7 +4355,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4069
4355
  provenance: "primary"
4070
4356
  });
4071
4357
  }
4072
- async writeManifestRegistryRecords() {
4358
+ async writeManifestRegistryRecords(account, signal) {
4073
4359
  const request = this.capabilityRequest;
4074
4360
  if (!request || request.registryRecords.length === 0) {
4075
4361
  return;
@@ -4078,8 +4364,9 @@ var _TinyCloudNode = class _TinyCloudNode {
4078
4364
  throw new Error("Manifest registry write requires wallet mode");
4079
4365
  }
4080
4366
  const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
4081
- await this.ensureOwnedSpaceHostedById(accountSpaceId);
4082
- 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);
4083
4370
  if (!result.ok) {
4084
4371
  throw Object.assign(
4085
4372
  new Error(`Failed to write manifest registry records: ${result.error.message}`),
@@ -4087,34 +4374,113 @@ var _TinyCloudNode = class _TinyCloudNode {
4087
4374
  );
4088
4375
  }
4089
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
+ }
4090
4426
  scheduleAccountRegistrySync() {
4091
4427
  const session = this.currentTinyCloudSession();
4092
4428
  if (!session || this.pendingAccountRegistrySync?.session === session) {
4093
4429
  return;
4094
4430
  }
4431
+ const controller = new AbortController();
4432
+ const deadline = setTimeout(
4433
+ () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
4434
+ this.accountRegistryDeadlineMs
4435
+ );
4095
4436
  const promise = this.enqueueAccountRegistryOperation(session, async () => {
4096
- await this.withAccountRegistryRetry(async () => {
4097
- if (this.currentTinyCloudSession() !== session) {
4098
- return;
4099
- }
4100
- await this.account.index.ensure();
4101
- if (this.currentTinyCloudSession() !== session) {
4102
- return;
4103
- }
4104
- await this.writeManifestRegistryRecords();
4105
- if (this.currentTinyCloudSession() !== session) {
4106
- return;
4107
- }
4108
- if (this.currentSessionCanListSpaces()) {
4109
- const spaces = await this.account.spaces.syncAccessible();
4110
- if (!spaces.ok) {
4111
- throw Object.assign(
4112
- new Error(`Failed to sync account spaces: ${spaces.error.message}`),
4113
- { cause: spaces.error }
4114
- );
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
+ }
4115
4476
  }
4477
+ }, controller.signal);
4478
+ } finally {
4479
+ for (const operationContext of registry.contexts) {
4480
+ operationContext.retire();
4116
4481
  }
4117
- });
4482
+ clearTimeout(deadline);
4483
+ }
4118
4484
  });
4119
4485
  this.pendingAccountRegistrySync = { session, promise };
4120
4486
  const clearPendingSync = () => {
@@ -4123,6 +4489,10 @@ var _TinyCloudNode = class _TinyCloudNode {
4123
4489
  }
4124
4490
  };
4125
4491
  void promise.then(clearPendingSync, clearPendingSync);
4492
+ void promise.then(
4493
+ () => clearTimeout(deadline),
4494
+ () => clearTimeout(deadline)
4495
+ );
4126
4496
  }
4127
4497
  enqueueAccountRegistryOperation(session, task) {
4128
4498
  const operation = this.accountRegistryTail.then(async () => {
@@ -4158,14 +4528,22 @@ var _TinyCloudNode = class _TinyCloudNode {
4158
4528
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
4159
4529
  );
4160
4530
  }
4161
- async withAccountRegistryRetry(task) {
4531
+ async withAccountRegistryRetry(task, signal) {
4162
4532
  const delays = [250, 1e3, 3e3];
4163
4533
  let lastError;
4164
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
+ }
4165
4539
  try {
4166
4540
  await task();
4167
4541
  return;
4168
4542
  } catch (error) {
4543
+ if (signal.aborted) {
4544
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
4545
+ return;
4546
+ }
4169
4547
  const verdict = authorizationVerdictOf(error);
4170
4548
  const message = error instanceof Error ? error.message : String(error);
4171
4549
  const isAuthorizationVerdict = verdict === void 0 ? /Unauthorized Action|\b(?:401|403)\b/.test(message) : verdict !== "other";
@@ -4177,12 +4555,28 @@ var _TinyCloudNode = class _TinyCloudNode {
4177
4555
  return;
4178
4556
  }
4179
4557
  if (isStorageFullError(error) || isStorageFullError(error?.cause) || message.includes(STORAGE_FULL_MESSAGE) || message.includes(STORAGE_WRITE_TOO_LARGE_MESSAGE)) {
4180
- 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
+ );
4181
4562
  return;
4182
4563
  }
4183
4564
  lastError = error;
4184
4565
  if (attempt < delays.length - 1) {
4185
- 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
+ });
4186
4580
  }
4187
4581
  }
4188
4582
  }
@@ -4232,7 +4626,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4232
4626
  * self-revealing: the next write to the space fails immediately and loudly
4233
4627
  * with `404 Space not found`.
4234
4628
  */
4235
- async ensureOwnedSpaceHostedById(spaceId) {
4629
+ async ensureOwnedSpaceHostedById(spaceId, signal) {
4236
4630
  if (!this.auth) {
4237
4631
  throw new Error("Owned space hosting requires wallet mode");
4238
4632
  }
@@ -4247,7 +4641,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4247
4641
  if (!host) {
4248
4642
  throw new Error("Owned space hosting requires a TinyCloud host");
4249
4643
  }
4250
- const activation = await activateSessionWithHost2(host, session.delegationHeader);
4644
+ const activation = await activateSessionWithHost2(host, session.delegationHeader, signal);
4251
4645
  if (activation.success && !activation.skipped?.includes(spaceId)) {
4252
4646
  this.confirmedHostedSpaceIds.add(spaceId);
4253
4647
  return;
@@ -4258,15 +4652,16 @@ var _TinyCloudNode = class _TinyCloudNode {
4258
4652
  { cause: activation }
4259
4653
  );
4260
4654
  }
4261
- 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);
4262
4657
  if (!created.success) {
4263
4658
  throw Object.assign(
4264
4659
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
4265
4660
  { cause: created }
4266
4661
  );
4267
4662
  }
4268
- await new Promise((resolve) => setTimeout(resolve, 100));
4269
- const retry = await activateSessionWithHost2(host, session.delegationHeader);
4663
+ if (signal?.aborted) throw signal.reason;
4664
+ const retry = await activateSessionWithHost2(host, session.delegationHeader, signal);
4270
4665
  if (!retry.success || retry.skipped?.includes(spaceId)) {
4271
4666
  throw Object.assign(
4272
4667
  new Error(
@@ -4544,6 +4939,34 @@ var _TinyCloudNode = class _TinyCloudNode {
4544
4939
  const stagedHost = resolvedHost ?? this.config.host;
4545
4940
  const stagedAddress = restoredAddress;
4546
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
+ }
4547
4970
  const stagedNodeDid = stagedAddress ? pkhDid2(stagedAddress, stagedChainId) : canonicalVerificationMethod;
4548
4971
  const serviceSession = {
4549
4972
  delegationHeader: sessionData.delegationHeader,
@@ -4622,7 +5045,18 @@ var _TinyCloudNode = class _TinyCloudNode {
4622
5045
  } else {
4623
5046
  this._restoredTcSession = stagedTcSession;
4624
5047
  }
5048
+ this.replicationDelegateSession = compactReplicationSession;
5049
+ this.replicationScopeError = replicationScopeError;
5050
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
4625
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);
4626
5060
  oldCore?.retireServices?.();
4627
5061
  }
4628
5062
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
@@ -4776,6 +5210,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4776
5210
  }));
4777
5211
  context.setSession(config.session);
4778
5212
  service.initialize(context);
5213
+ this.attachReplicationKV(config.session.spaceId, service);
4779
5214
  return service;
4780
5215
  }
4781
5216
  });
@@ -4816,6 +5251,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4816
5251
  }));
4817
5252
  context.setSession({ ...input.serviceSession, spaceId });
4818
5253
  scopedKv.initialize(context);
5254
+ this.attachReplicationKV(spaceId, scopedKv);
4819
5255
  return scopedKv;
4820
5256
  },
4821
5257
  createVaultService: (spaceId) => {
@@ -4830,6 +5266,7 @@ var _TinyCloudNode = class _TinyCloudNode {
4830
5266
  }));
4831
5267
  context.setSession({ ...input.serviceSession, spaceId });
4832
5268
  scopedKv.initialize(context);
5269
+ this.attachReplicationKV(spaceId, scopedKv);
4833
5270
  const scopedVault = this.createVaultService(spaceId, scopedKv, encryption, {
4834
5271
  host: input.host,
4835
5272
  did: input.nodeDid,
@@ -5110,7 +5547,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5110
5547
  siweConfig: this.config.siweConfig,
5111
5548
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
5112
5549
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
5113
- 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
5114
5556
  });
5115
5557
  this.tc = new TinyCloud(this.auth, {
5116
5558
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -5163,7 +5605,12 @@ var _TinyCloudNode = class _TinyCloudNode {
5163
5605
  siweConfig: this.config.siweConfig,
5164
5606
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
5165
5607
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
5166
- 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
5167
5614
  });
5168
5615
  this.tc = new TinyCloud(this.auth, {
5169
5616
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -5175,7 +5622,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5175
5622
  * Initialize the service context and KV service after sign-in.
5176
5623
  * @internal
5177
5624
  */
5178
- initializeServices() {
5625
+ async initializeServices() {
5179
5626
  const session = this.currentTinyCloudSession();
5180
5627
  if (!session) {
5181
5628
  return;
@@ -5218,12 +5665,18 @@ var _TinyCloudNode = class _TinyCloudNode {
5218
5665
  jwk: session.jwk
5219
5666
  };
5220
5667
  this._serviceContext.setSession(serviceSession);
5668
+ await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
5221
5669
  this.tc.serviceContext.setSession(serviceSession);
5222
5670
  this._vault = this.createVaultService(session.spaceId, this._kv);
5223
5671
  this._vault.initialize(this._serviceContext);
5224
5672
  this._serviceContext.registerService("vault", this._vault);
5225
5673
  this.initializeV2Services(serviceSession);
5226
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
+ }
5227
5680
  createSpaceScopedKVService(spaceId) {
5228
5681
  const kvService = new KVService2({});
5229
5682
  if (this._serviceContext) {
@@ -5239,6 +5692,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5239
5692
  spaceScopedContext.setSession({ ...session, spaceId });
5240
5693
  }
5241
5694
  kvService.initialize(spaceScopedContext);
5695
+ this.attachReplicationKV(spaceId, kvService);
5242
5696
  }
5243
5697
  return kvService;
5244
5698
  }
@@ -5701,6 +6155,7 @@ var _TinyCloudNode = class _TinyCloudNode {
5701
6155
  }));
5702
6156
  context.setSession(config.session);
5703
6157
  service.initialize(context);
6158
+ this.attachReplicationKV(config.session.spaceId, service);
5704
6159
  return service;
5705
6160
  },
5706
6161
  onRootDelegationNeeded: this.signer ? async (params) => {
@@ -6025,6 +6480,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6025
6480
  }));
6026
6481
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
6027
6482
  kv.initialize(spaceScopedContext);
6483
+ this.attachReplicationKV(spaceId, kv);
6028
6484
  return kv;
6029
6485
  }
6030
6486
  /**
@@ -6746,7 +7202,7 @@ var _TinyCloudNode = class _TinyCloudNode {
6746
7202
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
6747
7203
  }
6748
7204
  await this.auth.hostPublicSpace(publicSpaceId);
6749
- 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];
6750
7206
  const abilities = { kv: { "": kvActions } };
6751
7207
  const now = /* @__PURE__ */ new Date();
6752
7208
  const expiryMs = EXPIRY3.EPHEMERAL_MS;
@@ -6987,31 +7443,31 @@ var _TinyCloudNode = class _TinyCloudNode {
6987
7443
  );
6988
7444
  return { delegation: delegation2, prompted: true };
6989
7445
  }
6990
- const granted = this.projectSignedRecapCapabilities(session.siwe);
6991
- const { subset, missing } = this.signedCapabilitySubset(expandedEntries, granted);
6992
- if (!subset) {
6993
- const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
6994
- const runtimeGrant = this.findGrantForOperations(
6995
- runtimeOperations,
6996
- { excludePrimary: true }
6997
- );
6998
- if (runtimeGrant) {
6999
- const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
7000
- if (runtimeGrant.expiresAt.getTime() <= Date.now() + marginMs) {
7001
- throw new SessionExpiredError(runtimeGrant.expiresAt);
7002
- }
7003
- const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
7004
- const delegation2 = await this.createDelegationViaRuntimeGrant(
7005
- did,
7006
- expandedEntries,
7007
- runtimeExpiration,
7008
- runtimeGrant,
7009
- runtimeOperations,
7010
- onPrepared
7011
- );
7012
- 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));
7013
7455
  }
7014
- throw new PermissionNotInManifestError(missing, granted);
7456
+ if (plan.refused === "CAVEATED_AUTHORITY") {
7457
+ throw new CaveatedDelegationUnsupportedError(plan.caveated ?? []);
7458
+ }
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 };
7015
7471
  }
7016
7472
  const delegation = await this.createDelegationViaWasmPath(
7017
7473
  did,
@@ -7022,6 +7478,149 @@ var _TinyCloudNode = class _TinyCloudNode {
7022
7478
  );
7023
7479
  return { delegation, prompted: false };
7024
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
+ }
7025
7624
  /**
7026
7625
  * Materialize one manifest-declared delegation using the current session key.
7027
7626
  * Delivery is intentionally out of band; callers decide how to transmit the
@@ -7244,6 +7843,18 @@ var _TinyCloudNode = class _TinyCloudNode {
7244
7843
  * it only accepts action maps, so reject the selected constrained branch.
7245
7844
  */
7246
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) {
7247
7858
  let operationIndex = 0;
7248
7859
  const caveated = [];
7249
7860
  for (const entry of entries) {
@@ -7262,9 +7873,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7262
7873
  }
7263
7874
  }
7264
7875
  }
7265
- if (caveated.length > 0) {
7266
- throw new CaveatedDelegationUnsupportedError(caveated);
7267
- }
7876
+ return caveated;
7268
7877
  }
7269
7878
  /** Reject caveated parent branches before action-only child signing. */
7270
7879
  assertPortableDelegationCaveatsPreservable(delegation) {
@@ -7369,7 +7978,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7369
7978
  const missing = requested.filter((entry) => {
7370
7979
  const raw = this.isEncryptionPermissionEntry(entry) || this.isRawRevocationPermissionEntry(entry);
7371
7980
  const comparable = raw && this.isEncryptionPermissionEntry(entry) ? { ...entry, space: "encryption" } : entry;
7372
- return !isCapabilitySubset([comparable], raw ? rawGranted : spaceGranted).subset;
7981
+ return !isCapabilitySubset2([comparable], raw ? rawGranted : spaceGranted).subset;
7373
7982
  });
7374
7983
  return { subset: missing.length === 0, missing };
7375
7984
  }
@@ -7841,7 +8450,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7841
8450
  session.chainId
7842
8451
  );
7843
8452
  const publicAbilities = {
7844
- kv: { "": [KV2.GET, KV2.PUT, KV2.METADATA] }
8453
+ kv: { "": [KV3.GET, KV3.PUT, KV3.METADATA] }
7845
8454
  };
7846
8455
  const publicPrepared = this.wasmBindings.prepareSession({
7847
8456
  abilities: publicAbilities,
@@ -7869,7 +8478,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7869
8478
  delegationHeader: publicSession.delegationHeader,
7870
8479
  spaceId: publicSpaceId,
7871
8480
  path: "",
7872
- actions: [KV2.GET, KV2.PUT, KV2.METADATA],
8481
+ actions: [KV3.GET, KV3.PUT, KV3.METADATA],
7873
8482
  disableSubDelegation: params.disableSubDelegation ?? false,
7874
8483
  expiry: expirationTime,
7875
8484
  delegateDID: params.delegateDID,
@@ -8143,7 +8752,7 @@ import {
8143
8752
  resolveManifest as resolveManifest2,
8144
8753
  validateManifest,
8145
8754
  loadManifest,
8146
- isCapabilitySubset as isCapabilitySubset2,
8755
+ isCapabilitySubset as isCapabilitySubset3,
8147
8756
  expandActionShortNames,
8148
8757
  expandPermissionEntries as expandPermissionEntries2,
8149
8758
  expandPermissionEntry,
@@ -8188,7 +8797,7 @@ import {
8188
8797
  SpaceService as SpaceService2,
8189
8798
  SpaceErrorCodes,
8190
8799
  createSpaceService,
8191
- parseSpaceUri,
8800
+ parseSpaceUri as parseSpaceUri2,
8192
8801
  buildSpaceUri,
8193
8802
  makePublicSpaceId as makePublicSpaceId2,
8194
8803
  Space
@@ -8279,7 +8888,7 @@ export {
8279
8888
  expandPermissionEntry,
8280
8889
  getTinyCloudDebugLogs,
8281
8890
  installTinyCloudDebugGlobals,
8282
- isCapabilitySubset2 as isCapabilitySubset,
8891
+ isCapabilitySubset3 as isCapabilitySubset,
8283
8892
  isEvmAddress,
8284
8893
  loadManifest,
8285
8894
  makePkhSpaceId2 as makePkhSpaceId,
@@ -8288,10 +8897,11 @@ export {
8288
8897
  parseExpiry2 as parseExpiry,
8289
8898
  parsePkhDid,
8290
8899
  parseSecretCatalogKey,
8291
- parseSpaceUri,
8900
+ parseSpaceUri2 as parseSpaceUri,
8292
8901
  pkhDid3 as pkhDid,
8293
- principalDid,
8902
+ principalDid2 as principalDid,
8294
8903
  principalDidEquals4 as principalDidEquals,
8904
+ registerReplicationLoaders,
8295
8905
  resolveManifest2 as resolveManifest,
8296
8906
  resolveSecretListPrefix2 as resolveSecretListPrefix,
8297
8907
  resolveSecretPath3 as resolveSecretPath,