@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/index.js CHANGED
@@ -4,6 +4,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __typeError = (msg) => {
8
+ throw TypeError(msg);
9
+ };
7
10
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
11
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
12
  }) : x)(function(x) {
@@ -37,6 +40,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
40
  mod
38
41
  ));
39
42
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
43
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
44
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
45
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
46
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
47
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
40
48
 
41
49
  // ../../node_modules/bn.js/lib/bn.js
42
50
  var require_bn = __commonJS({
@@ -14355,8 +14363,8 @@ function searchPath(object, path) {
14355
14363
  }
14356
14364
  return currentChild;
14357
14365
  }
14358
- function uuidV4(randomBytes2) {
14359
- const bytes = arrayify(randomBytes2);
14366
+ function uuidV4(randomBytes3) {
14367
+ const bytes = arrayify(randomBytes3);
14360
14368
  bytes[6] = bytes[6] & 15 | 64;
14361
14369
  bytes[8] = bytes[8] & 63 | 128;
14362
14370
  const value = hexlify(bytes);
@@ -17027,6 +17035,477 @@ var require_utils2 = __commonJS({
17027
17035
  }
17028
17036
  });
17029
17037
 
17038
+ // src/replication/config.ts
17039
+ import { kvPrefixCovers } from "@tinycloud/sdk-services";
17040
+ function assertValidReplicationConfig(replication) {
17041
+ if (replication === void 0 || replication.enabled !== true) return;
17042
+ if (replication.storage === void 0 || replication.storage === null) {
17043
+ throw new TypeError("replication.enabled requires replication.storage");
17044
+ }
17045
+ if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
17046
+ throw new TypeError("replication.prefixes must be a non-empty array");
17047
+ }
17048
+ for (const prefix of replication.prefixes) {
17049
+ if (typeof prefix !== "string" || prefix === "") {
17050
+ throw new TypeError("replication.prefixes must not contain an empty prefix");
17051
+ }
17052
+ }
17053
+ for (const [index, prefix] of replication.prefixes.entries()) {
17054
+ for (const other of replication.prefixes.slice(index + 1)) {
17055
+ if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
17056
+ throw new TypeError(
17057
+ `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
17058
+ );
17059
+ }
17060
+ }
17061
+ if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
17062
+ throw new TypeError(
17063
+ `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
17064
+ );
17065
+ }
17066
+ }
17067
+ }
17068
+ var init_config = __esm({
17069
+ "src/replication/config.ts"() {
17070
+ "use strict";
17071
+ }
17072
+ });
17073
+
17074
+ // src/replication/authority-sign-in.ts
17075
+ import {
17076
+ grantPathsForSelector,
17077
+ KV,
17078
+ isCapabilitySubset
17079
+ } from "@tinycloud/sdk-core";
17080
+ import { requiresSecretsOptIn } from "@tinycloud/sdk-services";
17081
+ function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
17082
+ const requiredPaths = grantPathsForSelector(prefix);
17083
+ return isCapabilitySubset(
17084
+ requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),
17085
+ [...entries]
17086
+ ).subset;
17087
+ }
17088
+ function augmentSignInEntriesWithReplication(input) {
17089
+ const out = [];
17090
+ const replication = input.replication;
17091
+ if (replication === void 0) return out;
17092
+ for (const prefix of replication.prefixes) {
17093
+ if (requiresSecretsOptIn(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17094
+ continue;
17095
+ }
17096
+ if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {
17097
+ continue;
17098
+ }
17099
+ for (const path of grantPathsForSelector(prefix)) {
17100
+ out.push({
17101
+ service: KV_SERVICE,
17102
+ space: input.primarySpaceId,
17103
+ path,
17104
+ actions: [KV_GET, KV_SYNC]
17105
+ });
17106
+ }
17107
+ }
17108
+ return out;
17109
+ }
17110
+ var KV_GET, KV_SYNC, KV_SERVICE;
17111
+ var init_authority_sign_in = __esm({
17112
+ "src/replication/authority-sign-in.ts"() {
17113
+ "use strict";
17114
+ KV_GET = KV.GET;
17115
+ KV_SYNC = KV.SYNC;
17116
+ KV_SERVICE = "tinycloud.kv";
17117
+ }
17118
+ });
17119
+
17120
+ // src/replication/runtime.ts
17121
+ var runtime_exports = {};
17122
+ __export(runtime_exports, {
17123
+ ReplicationRuntime: () => ReplicationRuntime
17124
+ });
17125
+ import { pkhDid as pkhDid3 } from "@tinycloud/sdk-core";
17126
+ function resolvedOptions(options) {
17127
+ return {
17128
+ ...options,
17129
+ syncIntervalMs: options.syncIntervalMs ?? 6e4,
17130
+ maxStalenessMs: options.maxStalenessMs ?? 12e4,
17131
+ staleSyncTimeoutMs: options.staleSyncTimeoutMs ?? 5e3,
17132
+ allowSecrets: options.allowSecrets === true,
17133
+ verify: options.verify === true
17134
+ };
17135
+ }
17136
+ var scheduler, ReplicationRuntime;
17137
+ var init_runtime = __esm({
17138
+ "src/replication/runtime.ts"() {
17139
+ "use strict";
17140
+ scheduler = {
17141
+ now: () => Date.now(),
17142
+ setTimeout(fn, ms) {
17143
+ const timer = setTimeout(fn, ms);
17144
+ timer.unref?.();
17145
+ return () => clearTimeout(timer);
17146
+ }
17147
+ };
17148
+ ReplicationRuntime = class {
17149
+ constructor(options, clock = scheduler) {
17150
+ this.options = options;
17151
+ this.clock = clock;
17152
+ this.pending = /* @__PURE__ */ new Map();
17153
+ this.attached = /* @__PURE__ */ new Map();
17154
+ this.previous = Promise.resolve();
17155
+ this.binding = Promise.resolve();
17156
+ this.generation = 0;
17157
+ this.bindingActive = false;
17158
+ this.pendingPurgeSet = false;
17159
+ this.closed = false;
17160
+ this.control = {
17161
+ status: async () => {
17162
+ await this.binding;
17163
+ return this.current ? this.current.status() : [];
17164
+ },
17165
+ sync: async (input) => {
17166
+ await this.binding;
17167
+ if (this.purgedGeneration === this.generation) return;
17168
+ if (this.current) await this.current.sync(input);
17169
+ },
17170
+ purge: (input) => {
17171
+ if (this.bindingActive) {
17172
+ this.pendingPurge = input;
17173
+ this.pendingPurgeSet = true;
17174
+ const existing = this.current ?? this.lastController;
17175
+ if (existing) {
17176
+ this.pendingPurgeResult = existing.purge(input);
17177
+ this.purgedGeneration = this.generation;
17178
+ }
17179
+ return this.binding.then(() => this.pendingPurgeResult ?? { purged: [], failed: [] });
17180
+ }
17181
+ const controller = this.current ?? this.lastController;
17182
+ if (!controller) return Promise.resolve({ purged: [], failed: [] });
17183
+ this.purgedGeneration = this.generation;
17184
+ return controller.purge(input);
17185
+ },
17186
+ clearPending: async () => {
17187
+ await this.binding;
17188
+ if (this.current) return this.current.clearPending();
17189
+ const pending = this.lastPending;
17190
+ if (!pending) return 0;
17191
+ return pending.update((state) => {
17192
+ const before = state.records.length;
17193
+ state.records = state.records.filter(
17194
+ (record) => record.state === "committed" || record.state === "in_flight" && this.clock.now() - Date.parse(record.at) <= 10 * 6e4
17195
+ );
17196
+ return before - state.records.length;
17197
+ });
17198
+ },
17199
+ close: async () => {
17200
+ this.closed = true;
17201
+ this.unbind();
17202
+ await this.binding.catch(() => void 0);
17203
+ await this.previous;
17204
+ }
17205
+ };
17206
+ }
17207
+ loadServices() {
17208
+ return this.services ?? (this.services = import("@tinycloud/sdk-services/kv/replication"));
17209
+ }
17210
+ detachAll() {
17211
+ for (const kv of this.attached.keys()) kv.setReadThrough(null);
17212
+ this.attached.clear();
17213
+ }
17214
+ bind(input) {
17215
+ if (this.closed || !this.options.enabled) return Promise.resolve();
17216
+ this.bindingActive = true;
17217
+ const generation = ++this.generation;
17218
+ this.purgedGeneration = void 0;
17219
+ const { context, session } = input;
17220
+ const emit = (event) => {
17221
+ try {
17222
+ this.options.onEvent?.(event);
17223
+ } catch {
17224
+ }
17225
+ const { type, ...data } = event;
17226
+ context.emit(type, data);
17227
+ };
17228
+ const previous = this.current;
17229
+ this.current = void 0;
17230
+ for (const kv of this.attached.keys()) kv.setReadThrough(null);
17231
+ this.primarySpace = session.spaceId;
17232
+ const close = previous ? previous.close().catch(() => void 0) : Promise.resolve();
17233
+ this.previous = this.previous.then(() => close);
17234
+ const previousClose = this.previous;
17235
+ this.binding = this.loadServices().then((services) => {
17236
+ if (generation !== this.generation || this.closed) return;
17237
+ let identity;
17238
+ try {
17239
+ identity = services.canonicalReplicationIdentity({
17240
+ host: context.hosts[0],
17241
+ space: session.spaceId,
17242
+ principal: input.principal ?? pkhDid3(input.address, input.chainId)
17243
+ });
17244
+ } catch {
17245
+ for (const prefix of this.options.prefixes) {
17246
+ emit({ type: "replication.state", at: new Date(this.clock.now()).toISOString(), space: session.spaceId, replica: prefix, state: "unavailable", code: "CONFIG_INVALID" });
17247
+ }
17248
+ return;
17249
+ }
17250
+ const key2 = services.replicationIdentityKey(identity);
17251
+ let pending = this.pending.get(key2);
17252
+ if (!pending) {
17253
+ pending = this.options.storage.pendingWrites?.(identity) ?? services.createMemoryPendingStore(identity);
17254
+ this.pending.set(key2, pending);
17255
+ }
17256
+ this.lastPending = pending;
17257
+ const controller = services.createKVReplication({
17258
+ options: resolvedOptions(this.options),
17259
+ mode: this.options.mode ?? "background",
17260
+ storage: this.options.storage,
17261
+ identity,
17262
+ session: { id: session.delegationCid, did: session.verificationMethod, space: session.spaceId },
17263
+ authority: input.authority,
17264
+ pending,
17265
+ scheduler: this.clock,
17266
+ emit,
17267
+ fetch: context.fetch,
17268
+ previous: previousClose
17269
+ });
17270
+ this.current = controller;
17271
+ this.lastController = controller;
17272
+ this.lastIdentity = identity;
17273
+ for (const [kv, space] of this.attached) {
17274
+ kv.setReadThrough(space === this.primarySpace ? controller : null);
17275
+ }
17276
+ for (const scoped of input.primaryKV) this.attach(scoped.space, scoped.kv);
17277
+ if (this.pendingPurgeSet) {
17278
+ this.pendingPurgeResult = controller.purge(this.pendingPurge);
17279
+ this.purgedGeneration = generation;
17280
+ this.pendingPurge = void 0;
17281
+ this.pendingPurgeSet = false;
17282
+ }
17283
+ }).finally(() => {
17284
+ if (generation === this.generation) this.bindingActive = false;
17285
+ });
17286
+ for (const kv of this.attached.keys()) kv.setReadThroughReady?.(this.binding);
17287
+ return this.binding;
17288
+ }
17289
+ attach(space, kv) {
17290
+ this.attached.set(kv, space);
17291
+ kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
17292
+ return this.binding.then(() => {
17293
+ kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
17294
+ });
17295
+ }
17296
+ unbind() {
17297
+ this.generation++;
17298
+ this.bindingActive = false;
17299
+ this.pendingPurgeSet = false;
17300
+ const controller = this.current;
17301
+ this.current = void 0;
17302
+ this.primarySpace = void 0;
17303
+ this.detachAll();
17304
+ const binding = this.binding;
17305
+ const close = binding.then(() => controller?.close()).catch(() => void 0);
17306
+ this.previous = this.previous.then(() => close);
17307
+ return this.previous;
17308
+ }
17309
+ get boundIdentity() {
17310
+ return this.lastIdentity;
17311
+ }
17312
+ };
17313
+ }
17314
+ });
17315
+
17316
+ // src/replication/authority.ts
17317
+ var authority_exports = {};
17318
+ __export(authority_exports, {
17319
+ AUTHORITY_EXPIRY_MARGIN_MS: () => AUTHORITY_EXPIRY_MARGIN_MS,
17320
+ assertValidReplicationConfig: () => assertValidReplicationConfig,
17321
+ augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
17322
+ createReplicationAuthority: () => createReplicationAuthority,
17323
+ hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
17324
+ replicationScopeFromSignedSession: () => replicationScopeFromSignedSession,
17325
+ ucanAttCovers: () => ucanAttCovers,
17326
+ ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
17327
+ });
17328
+ import {
17329
+ grantPathsForSelector as grantPathsForSelector2,
17330
+ actionContains,
17331
+ KV as KV4,
17332
+ canonicalizeRecapCaveats as canonicalizeRecapCaveats3,
17333
+ isCapabilitySubset as isCapabilitySubset3,
17334
+ parseSpaceUri as parseSpaceUri2
17335
+ } from "@tinycloud/sdk-core";
17336
+ import { parseUcanGrant as replicaParseUcanGrant } from "@tinycloud/replica";
17337
+ function replicationEntries(spaceId, prefix) {
17338
+ return grantPathsForSelector2(prefix).map((path) => ({
17339
+ service: KV_SERVICE2,
17340
+ space: spaceId,
17341
+ path,
17342
+ actions: [KV_GET2, KV_SYNC2]
17343
+ }));
17344
+ }
17345
+ function caveatBranchesUnconstrained(value) {
17346
+ if (value === void 0 || value === null) return true;
17347
+ if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
17348
+ return true;
17349
+ }
17350
+ if (!Array.isArray(value)) return false;
17351
+ try {
17352
+ return canonicalizeRecapCaveats3(value) === canonicalizeRecapCaveats3(void 0);
17353
+ } catch {
17354
+ return false;
17355
+ }
17356
+ }
17357
+ function kvPathInResource(resource, space) {
17358
+ for (const base2 of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
17359
+ if (resource.startsWith(base2)) return resource.slice(base2.length);
17360
+ }
17361
+ return void 0;
17362
+ }
17363
+ function ucanAttCovers(att, space, prefix, ability, options) {
17364
+ const requiredPaths = prefix.endsWith("/") ? [prefix] : [prefix, `${prefix}/`];
17365
+ return requiredPaths.every((requestedPath) => {
17366
+ for (const [resource, abilities] of Object.entries(att)) {
17367
+ const path = kvPathInResource(resource, space);
17368
+ if (path === void 0) continue;
17369
+ for (const [granted, caveats] of Object.entries(abilities)) {
17370
+ if (!actionContains(granted, ability)) continue;
17371
+ if (!isCapabilitySubset3(
17372
+ [{ service: KV_SERVICE2, space, path: requestedPath, actions: [ability] }],
17373
+ [{ service: KV_SERVICE2, space, path, actions: [granted] }]
17374
+ ).subset) continue;
17375
+ if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
17376
+ }
17377
+ }
17378
+ return false;
17379
+ });
17380
+ }
17381
+ function ucanAttUnconstrainedFor(att, space, prefix) {
17382
+ return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
17383
+ }
17384
+ function replicationScopeFromSignedSession(input) {
17385
+ let grant;
17386
+ try {
17387
+ grant = parseUcanGrant(input.delegationHeader.Authorization);
17388
+ } catch {
17389
+ return void 0;
17390
+ }
17391
+ const bareDid = (value) => value.split("#", 1)[0];
17392
+ if (grant.cid !== input.delegationCid || bareDid(grant.audience) !== bareDid(input.verificationMethod)) return void 0;
17393
+ for (const resource of Object.keys(grant.att)) {
17394
+ const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv\//)?.[1];
17395
+ if (space !== input.persistedSpace) continue;
17396
+ const owner = parseSpaceUri2(space);
17397
+ if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };
17398
+ }
17399
+ return void 0;
17400
+ }
17401
+ function createReplicationAuthority(host) {
17402
+ const planOrRefusal = (entries) => {
17403
+ const plan = host.planDelegation(entries);
17404
+ if ("refused" in plan) return plan;
17405
+ return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
17406
+ };
17407
+ const sessionAudience = (value) => value.split("#", 1)[0];
17408
+ const signedSessionGrant = (session) => {
17409
+ const grant = parseUcanGrant(session.delegationHeader.Authorization);
17410
+ if (grant.cid !== session.delegationCid || sessionAudience(grant.audience) !== sessionAudience(session.verificationMethod)) return void 0;
17411
+ return grant;
17412
+ };
17413
+ const authority = {
17414
+ get sessionOnly() {
17415
+ const session = host.replicationSession();
17416
+ return session !== void 0 && session.siwe === void 0;
17417
+ },
17418
+ sessionGrant(prefix) {
17419
+ const session = host.replicationSession();
17420
+ if (session === void 0) return { refused: "SESSION_EXPIRING" };
17421
+ const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
17422
+ if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
17423
+ return { refused: "SESSION_EXPIRING" };
17424
+ }
17425
+ let grant;
17426
+ try {
17427
+ const verified = signedSessionGrant(session);
17428
+ if (!verified) return { refused: "NOT_COVERED" };
17429
+ grant = verified;
17430
+ } catch {
17431
+ return { refused: "NOT_COVERED" };
17432
+ }
17433
+ const tokenExpiry = grant.expiresAt === null ? void 0 : grant.expiresAt * 1e3;
17434
+ if (tokenExpiry === void 0 || tokenExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
17435
+ return { refused: "SESSION_EXPIRING" };
17436
+ }
17437
+ const covered = ucanAttCovers(grant.att, session.spaceId, prefix, KV_GET2, { ignoreCaveats: true }) && ucanAttCovers(grant.att, session.spaceId, prefix, KV_SYNC2, { ignoreCaveats: true });
17438
+ if (!covered) return { refused: "NOT_COVERED" };
17439
+ if (!ucanAttUnconstrainedFor(grant.att, session.spaceId, prefix)) {
17440
+ return { refused: "CAVEATED_AUTHORITY" };
17441
+ }
17442
+ return {
17443
+ ucan: grant.jwt,
17444
+ device: { did: session.verificationMethod, jwk: session.jwk }
17445
+ };
17446
+ },
17447
+ plan(prefix) {
17448
+ const session = host.replicationSession();
17449
+ if (session === void 0) return { refused: "SESSION_EXPIRING" };
17450
+ const sessionGrant = this.sessionGrant(prefix);
17451
+ if (!("refused" in sessionGrant)) {
17452
+ const signed2 = signedSessionGrant(session);
17453
+ if (!signed2 || signed2.expiresAt === null) return { refused: "NOT_COVERED" };
17454
+ return {
17455
+ path: "session",
17456
+ parentCid: session.delegationCid,
17457
+ expiresAt: signed2.expiresAt * 1e3
17458
+ };
17459
+ }
17460
+ if (authority.sessionOnly) return sessionGrant;
17461
+ return planOrRefusal(replicationEntries(session.spaceId, prefix));
17462
+ },
17463
+ async mint(deviceDid, prefix, signal) {
17464
+ if (authority.sessionOnly) throw new Error("compact delegate sessions cannot mint device grants");
17465
+ const session = host.replicationSession();
17466
+ if (session === void 0) throw new Error("replication mint without a session");
17467
+ const entries = replicationEntries(session.spaceId, prefix);
17468
+ const plan = planOrRefusal(entries);
17469
+ if ("refused" in plan) {
17470
+ throw Object.assign(
17471
+ new Error(`replication authority refused the mint: ${plan.refused}`),
17472
+ { refused: plan.refused }
17473
+ );
17474
+ }
17475
+ signal.throwIfAborted();
17476
+ const minted = await new Promise((resolve, reject2) => {
17477
+ const onAbort = () => reject2(signal.reason ?? new Error("replication mint aborted"));
17478
+ signal.addEventListener("abort", onAbort, { once: true });
17479
+ host.mintDelegation(deviceDid, entries).then(
17480
+ (result) => {
17481
+ signal.removeEventListener("abort", onAbort);
17482
+ resolve(result);
17483
+ },
17484
+ (error) => {
17485
+ signal.removeEventListener("abort", onAbort);
17486
+ reject2(error instanceof Error ? error : new Error(String(error)));
17487
+ }
17488
+ );
17489
+ });
17490
+ return { ucan: minted.ucan, parentCid: plan.parentCid, expiresAt: minted.expiresAt };
17491
+ }
17492
+ };
17493
+ return authority;
17494
+ }
17495
+ var parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
17496
+ var init_authority = __esm({
17497
+ "src/replication/authority.ts"() {
17498
+ "use strict";
17499
+ init_config();
17500
+ init_authority_sign_in();
17501
+ parseUcanGrant = replicaParseUcanGrant;
17502
+ KV_GET2 = KV4.GET;
17503
+ KV_SYNC2 = KV4.SYNC;
17504
+ KV_SERVICE2 = "tinycloud.kv";
17505
+ AUTHORITY_EXPIRY_MARGIN_MS = 6e4;
17506
+ }
17507
+ });
17508
+
17030
17509
  // src/NodeWasmBindings.ts
17031
17510
  import {
17032
17511
  invoke,
@@ -17688,6 +18167,21 @@ function deserializeDelegation(data) {
17688
18167
  };
17689
18168
  }
17690
18169
 
18170
+ // src/TinyCloudNode.ts
18171
+ init_config();
18172
+
18173
+ // src/replication/module-registry.ts
18174
+ var nodeLoaders;
18175
+ function registerReplicationLoaders(loaders) {
18176
+ nodeLoaders = loaders;
18177
+ }
18178
+ function getNodeReplicationLoaders() {
18179
+ if (nodeLoaders === void 0) {
18180
+ throw new Error("Replication requires registering replication controller loaders.");
18181
+ }
18182
+ return nodeLoaders;
18183
+ }
18184
+
17691
18185
  // src/TinyCloudNode.ts
17692
18186
  import {
17693
18187
  TinyCloud,
@@ -17726,12 +18220,12 @@ import {
17726
18220
  canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
17727
18221
  recapCaveatsEqual,
17728
18222
  expandPermissionEntries as expandPermissionEntriesCore,
17729
- isCapabilitySubset,
18223
+ isCapabilitySubset as isCapabilitySubset2,
17730
18224
  isExplicitOnlyAction,
17731
18225
  parseRecapCapabilities,
17732
18226
  SERVICE_LONG_TO_SHORT,
17733
18227
  SERVICE_SHORT_TO_LONG,
17734
- KV as KV2,
18228
+ KV as KV3,
17735
18229
  SQL as SQL2,
17736
18230
  DUCKDB as DUCKDB2,
17737
18231
  ENCRYPTION as ENCRYPTION2,
@@ -17741,6 +18235,7 @@ import {
17741
18235
  verifyDidKeyEd25519Signature,
17742
18236
  canonicalizeAddress as canonicalizeAddress2,
17743
18237
  pkhDid as pkhDid2,
18238
+ principalDid,
17744
18239
  resolveTinyCloudHosts as resolveTinyCloudHosts2,
17745
18240
  publishLocationRecord,
17746
18241
  principalDidEquals as principalDidEquals3,
@@ -17760,6 +18255,7 @@ import {
17760
18255
  } from "@tinycloud/sdk-services";
17761
18256
 
17762
18257
  // src/authorization/NodeUserAuthorization.ts
18258
+ init_authority_sign_in();
17763
18259
  import { createHash } from "crypto";
17764
18260
  import { SiweMessage } from "siwe";
17765
18261
  import {
@@ -17787,7 +18283,7 @@ import {
17787
18283
  parseCanonicalRecapResource,
17788
18284
  ACCOUNT_REGISTRY_SPACE,
17789
18285
  ACCOUNT_MANIFEST_PERMISSIONS,
17790
- KV,
18286
+ KV as KV2,
17791
18287
  SQL,
17792
18288
  DUCKDB,
17793
18289
  CAPABILITIES,
@@ -17941,7 +18437,7 @@ var NodeUserAuthorization = class {
17941
18437
  this.spacePrefix = config.spacePrefix ?? "default";
17942
18438
  this.defaultActions = config.defaultActions ?? {
17943
18439
  kv: {
17944
- "": [KV.PUT, KV.GET, KV.DEL, KV.LIST, KV.METADATA]
18440
+ "": [KV2.PUT, KV2.GET, KV2.DEL, KV2.LIST, KV2.METADATA]
17945
18441
  },
17946
18442
  sql: {
17947
18443
  "": [SQL.READ, SQL.WRITE, SQL.SCHEMA, SQL.ADMIN, SQL.EXPORT]
@@ -17979,6 +18475,7 @@ var NodeUserAuthorization = class {
17979
18475
  this.nonce = config.nonce;
17980
18476
  this.siweConfig = config.siweConfig;
17981
18477
  this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
18478
+ this.replication = config.replication?.enabled === true ? config.replication : void 0;
17982
18479
  this._manifest = config.manifest;
17983
18480
  this._capabilityRequest = config.capabilityRequest;
17984
18481
  this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
@@ -18187,6 +18684,143 @@ var NodeUserAuthorization = class {
18187
18684
  }
18188
18685
  return makePkhSpaceId(address, chainId, space);
18189
18686
  }
18687
+ /**
18688
+ * The plain-session primary abilities: `defaultActions` (the shared table)
18689
+ * unless the primary space IS the account space, where account-registry
18690
+ * permissions merge into a private clone so the shared table is never
18691
+ * mutated. Shared between `resolveSignInCapabilities` and
18692
+ * `signInPermissionInputs` so sign-in and `replicationSignInEntries`
18693
+ * can't disagree (§4.6).
18694
+ */
18695
+ primaryActionsForSignIn() {
18696
+ const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
18697
+ if (this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE) {
18698
+ const accountAbilities = resourceCapabilitiesToAbilitiesMap(
18699
+ ACCOUNT_MANIFEST_PERMISSIONS
18700
+ );
18701
+ for (const [service, paths] of Object.entries(accountAbilities)) {
18702
+ const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
18703
+ for (const [path, actions] of Object.entries(paths)) {
18704
+ const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
18705
+ for (const action of actions) {
18706
+ if (!existingActions.includes(action)) existingActions.push(action);
18707
+ }
18708
+ }
18709
+ }
18710
+ }
18711
+ return primaryActions;
18712
+ }
18713
+ /**
18714
+ * The abilities a `spacePrefix: "secrets"` plain session actually requests
18715
+ * for its primary space: the vault-secrets subtree only — the
18716
+ * `defaultActions` root `get` is REPLACED, not merged (sign-in installs
18717
+ * this map under the primary space id). Shared between
18718
+ * `resolveSignInCapabilities` and `signInPermissionInputs` so the
18719
+ * replication augmentation measures coverage against the effective
18720
+ * pre-replication request, never the uninstalled `primaryActions` (§4.1).
18721
+ */
18722
+ secretsPrimaryAbilities() {
18723
+ return {
18724
+ kv: {
18725
+ "vault/secrets/": [
18726
+ KV2.GET,
18727
+ KV2.PUT,
18728
+ KV2.DEL,
18729
+ KV2.LIST,
18730
+ KV2.METADATA
18731
+ ]
18732
+ }
18733
+ };
18734
+ }
18735
+ /**
18736
+ * The effective sign-in request as `PermissionEntry`s plus the resolved
18737
+ * primary space id — the inputs to the replication augmentation (§4.1).
18738
+ * `resolveSignInCapabilities` merges the augmented entries into its
18739
+ * abilities; `replicationSignInEntries()` returns them verbatim, so the
18740
+ * entries the SIWE recap carries and the restore check sees are the same.
18741
+ */
18742
+ signInPermissionInputs(address, chainId) {
18743
+ const request = this.getCapabilityRequest();
18744
+ if (request === void 0) {
18745
+ const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
18746
+ const effectivePrimary = this.spacePrefix === "secrets" ? this.secretsPrimaryAbilities() : this.primaryActionsForSignIn();
18747
+ const kvPaths = effectivePrimary["kv"] ?? {};
18748
+ return {
18749
+ primarySpaceId: primarySpaceId2,
18750
+ entries: Object.entries(kvPaths).map(([path, actions]) => ({
18751
+ service: "tinycloud.kv",
18752
+ space: primarySpaceId2,
18753
+ path,
18754
+ actions: [...actions]
18755
+ }))
18756
+ };
18757
+ }
18758
+ const spaceResources = request.resources.filter(
18759
+ (entry) => entry.service !== ENCRYPTION_PERMISSION_SERVICE
18760
+ );
18761
+ const primarySpaceName = spaceResources.find((entry) => entry.space !== "account")?.space ?? DEFAULT_MANIFEST_SPACE;
18762
+ const primarySpaceId = this.resolveSpaceName(
18763
+ primarySpaceName,
18764
+ address,
18765
+ chainId
18766
+ );
18767
+ return {
18768
+ primarySpaceId,
18769
+ entries: spaceResources.map((entry) => ({
18770
+ service: entry.service,
18771
+ space: this.resolveSpaceName(entry.space, address, chainId),
18772
+ path: entry.path,
18773
+ actions: [...entry.actions]
18774
+ }))
18775
+ };
18776
+ }
18777
+ /**
18778
+ * Merge replication augmentation entries into a space's abilities map
18779
+ * without mutating shared tables (`defaultActions` may back the base map):
18780
+ * the primary entry in `spaceAbilities` is replaced with a copy whose `kv`
18781
+ * paths gain `get`/`sync`. Returns the merged map for `abilities`.
18782
+ */
18783
+ mergeReplicationEntries(spaceAbilities, primarySpaceId, abilities, entries) {
18784
+ var _a;
18785
+ if (entries.length === 0) {
18786
+ return abilities;
18787
+ }
18788
+ const base2 = spaceAbilities[primarySpaceId] ?? abilities;
18789
+ const kv = {};
18790
+ for (const [path, actions] of Object.entries(base2["kv"] ?? {})) {
18791
+ kv[path] = [...actions];
18792
+ }
18793
+ for (const entry of entries) {
18794
+ const list = kv[_a = entry.path] ?? (kv[_a] = []);
18795
+ for (const action of entry.actions) {
18796
+ if (!list.includes(action)) list.push(action);
18797
+ }
18798
+ }
18799
+ const merged = { ...base2, kv };
18800
+ spaceAbilities[primarySpaceId] = merged;
18801
+ return merged;
18802
+ }
18803
+ /**
18804
+ * The replication entries this authorization's sign-in request carries
18805
+ * for the signed-in or restored address (TC-858 §4.1/§4.6): the output of
18806
+ * the same augmentation `resolveSignInCapabilities` runs — one
18807
+ * `{tinycloud.kv, primarySpace, prefix, [get, sync]}` per configured
18808
+ * prefix that already has an unrestricted covering `get` and passes the
18809
+ * secrets opt-in gate. Empty when replication is off or no address is
18810
+ * known. Pure.
18811
+ */
18812
+ replicationSignInEntries() {
18813
+ if (this.replication === void 0) return [];
18814
+ const address = this._address ?? this._tinyCloudSession?.address;
18815
+ const chainId = this._chainId ?? this._tinyCloudSession?.chainId;
18816
+ if (address === void 0 || chainId === void 0) return [];
18817
+ const { entries, primarySpaceId } = this.signInPermissionInputs(address, chainId);
18818
+ return augmentSignInEntriesWithReplication({
18819
+ entries,
18820
+ primarySpaceId,
18821
+ replication: this.replication
18822
+ });
18823
+ }
18190
18824
  defaultEncryptionNetworkId(address, chainId) {
18191
18825
  return `urn:tinycloud:encryption:${pkhDid(address, chainId)}:default`;
18192
18826
  }
@@ -18196,39 +18830,25 @@ var NodeUserAuthorization = class {
18196
18830
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
18197
18831
  const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
18198
18832
  const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
18199
- const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
18833
+ const primaryActions = this.primaryActionsForSignIn();
18200
18834
  const spaceAbilities2 = {
18201
18835
  [primarySpaceId2]: primaryActions,
18202
- [secretsSpaceId]: {
18203
- kv: {
18204
- "vault/secrets/": [
18205
- KV.GET,
18206
- KV.PUT,
18207
- KV.DEL,
18208
- KV.LIST,
18209
- KV.METADATA
18210
- ]
18211
- }
18212
- }
18836
+ [secretsSpaceId]: this.secretsPrimaryAbilities()
18213
18837
  };
18214
18838
  if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
18215
18839
  spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
18216
- } else if (this.includeAccountRegistryPermissions) {
18217
- const accountAbilities = resourceCapabilitiesToAbilitiesMap(
18218
- ACCOUNT_MANIFEST_PERMISSIONS
18219
- );
18220
- for (const [service, paths] of Object.entries(accountAbilities)) {
18221
- const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
18222
- for (const [path, actions] of Object.entries(paths)) {
18223
- const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
18224
- for (const action of actions) {
18225
- if (!existingActions.includes(action)) existingActions.push(action);
18226
- }
18227
- }
18228
- }
18229
18840
  }
18841
+ const mergedPrimary = this.mergeReplicationEntries(
18842
+ spaceAbilities2,
18843
+ primarySpaceId2,
18844
+ primaryActions,
18845
+ augmentSignInEntriesWithReplication({
18846
+ ...this.signInPermissionInputs(address, chainId),
18847
+ replication: this.replication
18848
+ })
18849
+ );
18230
18850
  return {
18231
- abilities: primaryActions,
18851
+ abilities: mergedPrimary,
18232
18852
  spaceId: primarySpaceId2,
18233
18853
  spaceAbilities: spaceAbilities2,
18234
18854
  rawAbilities: {
@@ -18264,8 +18884,17 @@ var NodeUserAuthorization = class {
18264
18884
  for (const [space, abilities] of Object.entries(bySpace)) {
18265
18885
  spaceAbilities[this.resolveSpaceName(space, address, chainId)] = abilities;
18266
18886
  }
18887
+ const mergedPrimaryAbilities = this.mergeReplicationEntries(
18888
+ spaceAbilities,
18889
+ primarySpaceId,
18890
+ spaceAbilities[primarySpaceId] ?? resourceCapabilitiesToAbilitiesMap([]),
18891
+ augmentSignInEntriesWithReplication({
18892
+ ...this.signInPermissionInputs(address, chainId),
18893
+ replication: this.replication
18894
+ })
18895
+ );
18267
18896
  return {
18268
- abilities: spaceAbilities[primarySpaceId] ?? resourceCapabilitiesToAbilitiesMap([]),
18897
+ abilities: mergedPrimaryAbilities,
18269
18898
  spaceId: primarySpaceId,
18270
18899
  spaceAbilities,
18271
18900
  rawAbilities: Object.keys(rawAbilities).length > 0 ? rawAbilities : void 0
@@ -18323,14 +18952,14 @@ var NodeUserAuthorization = class {
18323
18952
  * Create the space on the TinyCloud server (host delegation).
18324
18953
  * This registers the user as the owner of the space.
18325
18954
  */
18326
- async hostSpace(targetSpaceId, purpose) {
18955
+ async hostSpace(targetSpaceId, purpose, signal) {
18327
18956
  if (!this._tinyCloudSession || !this._address || !this._chainId) {
18328
18957
  throw new Error("Must be signed in to host space");
18329
18958
  }
18330
18959
  await this.ensureTinyCloudHosts();
18331
18960
  const host = this.primaryTinyCloudHost;
18332
18961
  const spaceId = targetSpaceId ?? this._tinyCloudSession.spaceId;
18333
- const peerId = await fetchPeerId(host, spaceId);
18962
+ const peerId = await fetchPeerId(host, spaceId, signal);
18334
18963
  const siwe = this.wasm.generateHostSIWEMessage({
18335
18964
  address: this._address,
18336
18965
  chainId: this._chainId,
@@ -18339,9 +18968,28 @@ var NodeUserAuthorization = class {
18339
18968
  spaceId,
18340
18969
  peerId
18341
18970
  });
18342
- const signature2 = await this.signMessage(siwe, purpose);
18971
+ if (signal?.aborted) throw signal.reason;
18972
+ const signing = this.signMessage(siwe, purpose);
18973
+ let signature2;
18974
+ if (!signal) {
18975
+ signature2 = await signing;
18976
+ } else {
18977
+ let rejectOnAbort;
18978
+ const aborted = new Promise((_resolve, reject2) => {
18979
+ rejectOnAbort = reject2;
18980
+ });
18981
+ const onAbort = () => rejectOnAbort(signal.reason);
18982
+ signal.addEventListener("abort", onAbort, { once: true });
18983
+ if (signal.aborted) onAbort();
18984
+ try {
18985
+ signature2 = await Promise.race([signing, aborted]);
18986
+ } finally {
18987
+ signal.removeEventListener("abort", onAbort);
18988
+ }
18989
+ }
18990
+ if (signal?.aborted) throw signal.reason;
18343
18991
  const headers = this.wasm.siweToDelegationHeaders({ siwe, signature: signature2 });
18344
- return submitHostDelegation(host, headers);
18992
+ return submitHostDelegation(host, headers, signal);
18345
18993
  }
18346
18994
  /**
18347
18995
  * Create a specific space on the server via host delegation.
@@ -18362,8 +19010,8 @@ var NodeUserAuthorization = class {
18362
19010
  * keeps its HTTP status (e.g. a typed 401/403) instead of collapsing to
18363
19011
  * `false`.
18364
19012
  */
18365
- async hostOwnedSpaceResult(spaceId, purpose) {
18366
- return this.hostSpace(spaceId, purpose);
19013
+ async hostOwnedSpaceResult(spaceId, purpose, signal) {
19014
+ return this.hostSpace(spaceId, purpose, signal);
18367
19015
  }
18368
19016
  /**
18369
19017
  * Ensure the user's space exists on the TinyCloud server.
@@ -19742,11 +20390,11 @@ function isSecretPayload(value) {
19742
20390
  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";
19743
20391
  }
19744
20392
  var ROOT_DELEGATION_ACTIONS = [
19745
- KV2.PUT,
19746
- KV2.GET,
19747
- KV2.DEL,
19748
- KV2.LIST,
19749
- KV2.METADATA,
20393
+ KV3.PUT,
20394
+ KV3.GET,
20395
+ KV3.DEL,
20396
+ KV3.LIST,
20397
+ KV3.METADATA,
19750
20398
  SQL2.READ,
19751
20399
  SQL2.WRITE,
19752
20400
  SQL2.ADMIN,
@@ -20197,6 +20845,8 @@ var _TinyCloudNode = class _TinyCloudNode {
20197
20845
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
20198
20846
  /** Serializes account-registry writes for the active node instance. */
20199
20847
  this.accountRegistryTail = Promise.resolve();
20848
+ /** Bounds best-effort registry work on the sign-in critical path. */
20849
+ this.accountRegistryDeadlineMs = 4e3;
20200
20850
  /**
20201
20851
  * True when the last signIn() detected an interactive signer and skipped
20202
20852
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -20275,11 +20925,46 @@ var _TinyCloudNode = class _TinyCloudNode {
20275
20925
  });
20276
20926
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
20277
20927
  };
20928
+ assertValidReplicationConfig(config.replication);
20278
20929
  this.explicitHost = config.host;
20279
20930
  this.config = {
20280
20931
  ...config,
20281
20932
  host: config.host ?? DEFAULT_HOST
20282
20933
  };
20934
+ if (this.config.replication?.enabled) {
20935
+ const runtime = getNodeReplicationLoaders().runtime().then(({ ReplicationRuntime: ReplicationRuntime2 }) => {
20936
+ const instance = new ReplicationRuntime2(this.config.replication);
20937
+ this.replicationRuntimeInstance = instance;
20938
+ return instance;
20939
+ });
20940
+ this.replicationRuntime = runtime;
20941
+ this.replicationControl = {
20942
+ status: async () => {
20943
+ const status = await runtime.then((ready) => ready.control.status());
20944
+ const scopeError = this.replicationScopeError;
20945
+ if (!scopeError) return status;
20946
+ return this.config.replication.prefixes.map((prefix) => ({
20947
+ prefix,
20948
+ state: "unavailable",
20949
+ lastError: {
20950
+ at: (/* @__PURE__ */ new Date()).toISOString(),
20951
+ code: "SESSION_SCOPE_MISMATCH",
20952
+ message: scopeError
20953
+ },
20954
+ pending: { inFlight: 0, committed: 0, ambiguous: 0 },
20955
+ pinned: [],
20956
+ lagMs: null
20957
+ }));
20958
+ },
20959
+ sync: async (input) => (await runtime).control.sync(input),
20960
+ purge: (input) => {
20961
+ const instance = this.replicationRuntimeInstance;
20962
+ return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
20963
+ },
20964
+ clearPending: async () => (await runtime).control.clearPending(),
20965
+ close: async () => (await runtime).control.close()
20966
+ };
20967
+ }
20283
20968
  if (config.wasmBindings) {
20284
20969
  this.wasmBindings = config.wasmBindings;
20285
20970
  } else if (_TinyCloudNode.nodeDefaults) {
@@ -20400,7 +21085,14 @@ var _TinyCloudNode = class _TinyCloudNode {
20400
21085
  siweConfig: config.siweConfig,
20401
21086
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
20402
21087
  capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
20403
- includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
21088
+ includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions,
21089
+ // Sign-in needs only the entry inputs (prefixes + secrets opt-in);
21090
+ // storage and scheduling never leave TinyCloudNode.
21091
+ replication: config.replication?.enabled === true ? {
21092
+ enabled: true,
21093
+ prefixes: config.replication.prefixes,
21094
+ allowSecrets: config.replication.allowSecrets === true
21095
+ } : void 0
20404
21096
  });
20405
21097
  this.tc = new TinyCloud(this.auth, {
20406
21098
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -20651,6 +21343,9 @@ var _TinyCloudNode = class _TinyCloudNode {
20651
21343
  this.confirmedHostedSpaceIds.clear();
20652
21344
  await this.tc.signIn(options);
20653
21345
  this.syncResolvedHostFromAuth();
21346
+ this.replicationDelegateSession = void 0;
21347
+ this.replicationScopeError = void 0;
21348
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
20654
21349
  const oldGraph = this._serviceGraph;
20655
21350
  this._serviceGraph = this.createServiceGraphLifetime();
20656
21351
  oldGraph.retire();
@@ -20660,7 +21355,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20660
21355
  this.sessionKeyId = signedInSession.sessionKey;
20661
21356
  this.sessionKeyJwk = signedInSession.jwk;
20662
21357
  }
20663
- this.initializeServices();
21358
+ await this.initializeServices();
20664
21359
  const primarySession = this.currentTinyCloudSession();
20665
21360
  if (primarySession) {
20666
21361
  this.registerPrimarySessionGrant(primarySession);
@@ -20672,6 +21367,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20672
21367
  }
20673
21368
  if (!bootstrapped) {
20674
21369
  this.scheduleAccountRegistrySync();
21370
+ await this.accountRegistryTail;
20675
21371
  }
20676
21372
  this.notificationHandler.success("Successfully signed in");
20677
21373
  }
@@ -21124,7 +21820,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21124
21820
  provenance: "primary"
21125
21821
  });
21126
21822
  }
21127
- async writeManifestRegistryRecords() {
21823
+ async writeManifestRegistryRecords(account, signal) {
21128
21824
  const request = this.capabilityRequest;
21129
21825
  if (!request || request.registryRecords.length === 0) {
21130
21826
  return;
@@ -21133,8 +21829,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21133
21829
  throw new Error("Manifest registry write requires wallet mode");
21134
21830
  }
21135
21831
  const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
21136
- await this.ensureOwnedSpaceHostedById(accountSpaceId);
21137
- const result = await this.account.applications.register(request.manifests);
21832
+ await this.ensureOwnedSpaceHostedById(accountSpaceId, signal);
21833
+ if (signal.aborted) throw signal.reason;
21834
+ const result = await account.applications.register(request.manifests);
21138
21835
  if (!result.ok) {
21139
21836
  throw Object.assign(
21140
21837
  new Error(`Failed to write manifest registry records: ${result.error.message}`),
@@ -21142,34 +21839,113 @@ var _TinyCloudNode = class _TinyCloudNode {
21142
21839
  );
21143
21840
  }
21144
21841
  }
21842
+ createAccountRegistryServices(context, signal) {
21843
+ const session = context.session;
21844
+ if (!session) throw new Error("Account registry requires an active service session");
21845
+ const contexts = [context];
21846
+ const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
21847
+ const createScopedContext = (spaceId) => {
21848
+ const scoped = this._serviceGraph.track(new ServiceContext2({
21849
+ invoke: context.invoke,
21850
+ invokeAny: context.invokeAny,
21851
+ fetch: context.fetch,
21852
+ hosts: context.hosts,
21853
+ telemetry: this.config.telemetry
21854
+ }));
21855
+ scoped.setOperationAbortSignal(signal);
21856
+ scoped.setSession({ ...session, spaceId });
21857
+ contexts.push(scoped);
21858
+ return scoped;
21859
+ };
21860
+ const sql = new SQLService2({});
21861
+ const sqlContext = createScopedContext(accountSpaceId);
21862
+ sql.initialize(sqlContext);
21863
+ const spaces = new SpaceService({
21864
+ hosts: context.hosts,
21865
+ session,
21866
+ invoke: context.invoke,
21867
+ fetch: (url, init2) => context.fetch(url, {
21868
+ ...init2,
21869
+ signal: init2?.signal ? AbortSignal.any([init2.signal, signal]) : signal
21870
+ }),
21871
+ userDid: this.did,
21872
+ createKVService: (spaceId) => {
21873
+ const kv = new KVService2({});
21874
+ kv.initialize(createScopedContext(spaceId));
21875
+ return kv;
21876
+ }
21877
+ });
21878
+ return {
21879
+ contexts,
21880
+ account: new AccountService({
21881
+ getDid: () => this.did,
21882
+ getHost: () => context.hosts[0] ?? this.config.host,
21883
+ getPrimarySpaceId: () => session.spaceId,
21884
+ getAccountSpaceId: () => accountSpaceId,
21885
+ getSpaces: () => spaces,
21886
+ getAccountDb: () => sql.db("account"),
21887
+ ensureAccountSpaceHosted: () => this.ensureOwnedSpaceHostedById(accountSpaceId, signal)
21888
+ })
21889
+ };
21890
+ }
21145
21891
  scheduleAccountRegistrySync() {
21146
21892
  const session = this.currentTinyCloudSession();
21147
21893
  if (!session || this.pendingAccountRegistrySync?.session === session) {
21148
21894
  return;
21149
21895
  }
21896
+ const controller = new AbortController();
21897
+ const deadline = setTimeout(
21898
+ () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
21899
+ this.accountRegistryDeadlineMs
21900
+ );
21150
21901
  const promise = this.enqueueAccountRegistryOperation(session, async () => {
21151
- await this.withAccountRegistryRetry(async () => {
21152
- if (this.currentTinyCloudSession() !== session) {
21153
- return;
21154
- }
21155
- await this.account.index.ensure();
21156
- if (this.currentTinyCloudSession() !== session) {
21157
- return;
21158
- }
21159
- await this.writeManifestRegistryRecords();
21160
- if (this.currentTinyCloudSession() !== session) {
21161
- return;
21162
- }
21163
- if (this.currentSessionCanListSpaces()) {
21164
- const spaces = await this.account.spaces.syncAccessible();
21165
- if (!spaces.ok) {
21166
- throw Object.assign(
21167
- new Error(`Failed to sync account spaces: ${spaces.error.message}`),
21168
- { cause: spaces.error }
21169
- );
21902
+ const sharedContext = this._serviceContext;
21903
+ if (!sharedContext) return;
21904
+ const context = this._serviceGraph.track(new ServiceContext2({
21905
+ invoke: sharedContext.invoke,
21906
+ invokeAny: sharedContext.invokeAny,
21907
+ fetch: sharedContext.fetch,
21908
+ hosts: sharedContext.hosts,
21909
+ telemetry: this.config.telemetry
21910
+ }));
21911
+ context.setOperationAbortSignal(controller.signal);
21912
+ const current = this.currentTinyCloudSession();
21913
+ if (!current) return;
21914
+ context.setSession({
21915
+ delegationHeader: current.delegationHeader,
21916
+ delegationCid: current.delegationCid,
21917
+ spaceId: current.spaceId,
21918
+ verificationMethod: current.verificationMethod,
21919
+ jwk: current.jwk
21920
+ });
21921
+ const registry = this.createAccountRegistryServices(context, controller.signal);
21922
+ try {
21923
+ await this.withAccountRegistryRetry(async () => {
21924
+ if (controller.signal.aborted) throw controller.signal.reason;
21925
+ if (this.currentTinyCloudSession() !== session) return;
21926
+ await registry.account.index.ensure();
21927
+ if (controller.signal.aborted) throw controller.signal.reason;
21928
+ if (this.currentTinyCloudSession() !== session) return;
21929
+ await this.writeManifestRegistryRecords(registry.account, controller.signal);
21930
+ if (controller.signal.aborted) throw controller.signal.reason;
21931
+ if (this.currentTinyCloudSession() !== session) return;
21932
+ if (this.currentSessionCanListSpaces()) {
21933
+ const result = await registry.account.spaces.syncAccessible();
21934
+ if (controller.signal.aborted) throw controller.signal.reason;
21935
+ if (!result.ok) {
21936
+ throw Object.assign(
21937
+ new Error(`Failed to sync account spaces: ${result.error.message}`),
21938
+ { cause: result.error }
21939
+ );
21940
+ }
21170
21941
  }
21942
+ }, controller.signal);
21943
+ } finally {
21944
+ for (const operationContext of registry.contexts) {
21945
+ operationContext.retire();
21171
21946
  }
21172
- });
21947
+ clearTimeout(deadline);
21948
+ }
21173
21949
  });
21174
21950
  this.pendingAccountRegistrySync = { session, promise };
21175
21951
  const clearPendingSync = () => {
@@ -21178,6 +21954,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21178
21954
  }
21179
21955
  };
21180
21956
  void promise.then(clearPendingSync, clearPendingSync);
21957
+ void promise.then(
21958
+ () => clearTimeout(deadline),
21959
+ () => clearTimeout(deadline)
21960
+ );
21181
21961
  }
21182
21962
  enqueueAccountRegistryOperation(session, task) {
21183
21963
  const operation = this.accountRegistryTail.then(async () => {
@@ -21213,14 +21993,22 @@ var _TinyCloudNode = class _TinyCloudNode {
21213
21993
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
21214
21994
  );
21215
21995
  }
21216
- async withAccountRegistryRetry(task) {
21996
+ async withAccountRegistryRetry(task, signal) {
21217
21997
  const delays = [250, 1e3, 3e3];
21218
21998
  let lastError;
21219
21999
  for (let attempt = 0; attempt < delays.length; attempt += 1) {
22000
+ if (signal.aborted) {
22001
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", signal.reason);
22002
+ return;
22003
+ }
21220
22004
  try {
21221
22005
  await task();
21222
22006
  return;
21223
22007
  } catch (error) {
22008
+ if (signal.aborted) {
22009
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
22010
+ return;
22011
+ }
21224
22012
  const verdict = authorizationVerdictOf(error);
21225
22013
  const message = error instanceof Error ? error.message : String(error);
21226
22014
  const isAuthorizationVerdict = verdict === void 0 ? /Unauthorized Action|\b(?:401|403)\b/.test(message) : verdict !== "other";
@@ -21232,12 +22020,28 @@ var _TinyCloudNode = class _TinyCloudNode {
21232
22020
  return;
21233
22021
  }
21234
22022
  if (isStorageFullError(error) || isStorageFullError(error?.cause) || message.includes(STORAGE_FULL_MESSAGE) || message.includes(STORAGE_WRITE_TOO_LARGE_MESSAGE)) {
21235
- console.warn("TinyCloud account registry sync stopped: storage is full", error);
22023
+ console.warn(
22024
+ "TinyCloud account registry sync stopped: storage is full",
22025
+ error
22026
+ );
21236
22027
  return;
21237
22028
  }
21238
22029
  lastError = error;
21239
22030
  if (attempt < delays.length - 1) {
21240
- await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
22031
+ await new Promise((resolve, reject2) => {
22032
+ const timer = setTimeout(() => {
22033
+ signal.removeEventListener("abort", onAbort);
22034
+ resolve();
22035
+ }, delays[attempt]);
22036
+ const onAbort = () => {
22037
+ clearTimeout(timer);
22038
+ signal.removeEventListener("abort", onAbort);
22039
+ reject2(signal.reason);
22040
+ };
22041
+ signal.addEventListener("abort", onAbort, { once: true });
22042
+ }).catch((error2) => {
22043
+ if (!signal.aborted) throw error2;
22044
+ });
21241
22045
  }
21242
22046
  }
21243
22047
  }
@@ -21287,7 +22091,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21287
22091
  * self-revealing: the next write to the space fails immediately and loudly
21288
22092
  * with `404 Space not found`.
21289
22093
  */
21290
- async ensureOwnedSpaceHostedById(spaceId) {
22094
+ async ensureOwnedSpaceHostedById(spaceId, signal) {
21291
22095
  if (!this.auth) {
21292
22096
  throw new Error("Owned space hosting requires wallet mode");
21293
22097
  }
@@ -21302,7 +22106,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21302
22106
  if (!host) {
21303
22107
  throw new Error("Owned space hosting requires a TinyCloud host");
21304
22108
  }
21305
- const activation = await activateSessionWithHost2(host, session.delegationHeader);
22109
+ const activation = await activateSessionWithHost2(host, session.delegationHeader, signal);
21306
22110
  if (activation.success && !activation.skipped?.includes(spaceId)) {
21307
22111
  this.confirmedHostedSpaceIds.add(spaceId);
21308
22112
  return;
@@ -21313,15 +22117,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21313
22117
  { cause: activation }
21314
22118
  );
21315
22119
  }
21316
- const created = await this.auth.hostOwnedSpaceResult(spaceId);
22120
+ const authorization = this.auth;
22121
+ const created = signal ? await authorization.hostOwnedSpaceResult(spaceId, void 0, signal) : await authorization.hostOwnedSpaceResult(spaceId);
21317
22122
  if (!created.success) {
21318
22123
  throw Object.assign(
21319
22124
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
21320
22125
  { cause: created }
21321
22126
  );
21322
22127
  }
21323
- await new Promise((resolve) => setTimeout(resolve, 100));
21324
- const retry = await activateSessionWithHost2(host, session.delegationHeader);
22128
+ if (signal?.aborted) throw signal.reason;
22129
+ const retry = await activateSessionWithHost2(host, session.delegationHeader, signal);
21325
22130
  if (!retry.success || retry.skipped?.includes(spaceId)) {
21326
22131
  throw Object.assign(
21327
22132
  new Error(
@@ -21599,11 +22404,39 @@ var _TinyCloudNode = class _TinyCloudNode {
21599
22404
  const stagedHost = resolvedHost ?? this.config.host;
21600
22405
  const stagedAddress = restoredAddress;
21601
22406
  const stagedChainId = sessionData.chainId ?? 1;
21602
- const stagedNodeDid = stagedAddress ? pkhDid2(stagedAddress, stagedChainId) : canonicalVerificationMethod;
21603
- const serviceSession = {
21604
- delegationHeader: sessionData.delegationHeader,
21605
- delegationCid: sessionData.delegationCid,
21606
- spaceId: sessionData.spaceId,
22407
+ let replicationPrincipal;
22408
+ let compactReplicationSession;
22409
+ let replicationScopeError;
22410
+ if (this.config.replication?.enabled) {
22411
+ if (stagedAddress !== void 0) {
22412
+ replicationPrincipal = pkhDid2(stagedAddress, stagedChainId);
22413
+ } else {
22414
+ const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
22415
+ const signedScope = replicationScopeFromSignedSession2({
22416
+ delegationHeader: sessionData.delegationHeader,
22417
+ delegationCid: sessionData.delegationCid,
22418
+ verificationMethod: restoredVerificationMethod,
22419
+ persistedSpace: sessionData.spaceId
22420
+ });
22421
+ if (!signedScope) {
22422
+ replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
22423
+ } else {
22424
+ replicationPrincipal = signedScope.principal;
22425
+ compactReplicationSession = {
22426
+ delegationHeader: sessionData.delegationHeader,
22427
+ delegationCid: sessionData.delegationCid,
22428
+ spaceId: signedScope.space,
22429
+ verificationMethod: restoredVerificationMethod,
22430
+ jwk: stagedJwk
22431
+ };
22432
+ }
22433
+ }
22434
+ }
22435
+ const stagedNodeDid = stagedAddress ? pkhDid2(stagedAddress, stagedChainId) : canonicalVerificationMethod;
22436
+ const serviceSession = {
22437
+ delegationHeader: sessionData.delegationHeader,
22438
+ delegationCid: sessionData.delegationCid,
22439
+ spaceId: sessionData.spaceId,
21607
22440
  verificationMethod: restoredVerificationMethod,
21608
22441
  jwk: stagedJwk
21609
22442
  };
@@ -21677,7 +22510,18 @@ var _TinyCloudNode = class _TinyCloudNode {
21677
22510
  } else {
21678
22511
  this._restoredTcSession = stagedTcSession;
21679
22512
  }
22513
+ this.replicationDelegateSession = compactReplicationSession;
22514
+ this.replicationScopeError = replicationScopeError;
22515
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
21680
22516
  oldGraph.retire();
22517
+ if (replicationScopeError) {
22518
+ stagedGraph.serviceContext.emit("replication.state", {
22519
+ state: "unavailable",
22520
+ code: "SESSION_SCOPE_MISMATCH",
22521
+ detail: replicationScopeError
22522
+ });
22523
+ }
22524
+ await this.bindReplicationRuntime(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
21681
22525
  oldCore?.retireServices?.();
21682
22526
  }
21683
22527
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
@@ -21831,6 +22675,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21831
22675
  }));
21832
22676
  context.setSession(config.session);
21833
22677
  service.initialize(context);
22678
+ this.attachReplicationKV(config.session.spaceId, service);
21834
22679
  return service;
21835
22680
  }
21836
22681
  });
@@ -21871,6 +22716,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21871
22716
  }));
21872
22717
  context.setSession({ ...input.serviceSession, spaceId });
21873
22718
  scopedKv.initialize(context);
22719
+ this.attachReplicationKV(spaceId, scopedKv);
21874
22720
  return scopedKv;
21875
22721
  },
21876
22722
  createVaultService: (spaceId) => {
@@ -21885,6 +22731,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21885
22731
  }));
21886
22732
  context.setSession({ ...input.serviceSession, spaceId });
21887
22733
  scopedKv.initialize(context);
22734
+ this.attachReplicationKV(spaceId, scopedKv);
21888
22735
  const scopedVault = this.createVaultService(spaceId, scopedKv, encryption, {
21889
22736
  host: input.host,
21890
22737
  did: input.nodeDid,
@@ -22165,7 +23012,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22165
23012
  siweConfig: this.config.siweConfig,
22166
23013
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
22167
23014
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
22168
- includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
23015
+ includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions,
23016
+ replication: this.config.replication?.enabled === true ? {
23017
+ enabled: true,
23018
+ prefixes: this.config.replication.prefixes,
23019
+ allowSecrets: this.config.replication.allowSecrets === true
23020
+ } : void 0
22169
23021
  });
22170
23022
  this.tc = new TinyCloud(this.auth, {
22171
23023
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -22218,7 +23070,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22218
23070
  siweConfig: this.config.siweConfig,
22219
23071
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
22220
23072
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
22221
- includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
23073
+ includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions,
23074
+ replication: this.config.replication?.enabled === true ? {
23075
+ enabled: true,
23076
+ prefixes: this.config.replication.prefixes,
23077
+ allowSecrets: this.config.replication.allowSecrets === true
23078
+ } : void 0
22222
23079
  });
22223
23080
  this.tc = new TinyCloud(this.auth, {
22224
23081
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -22230,7 +23087,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22230
23087
  * Initialize the service context and KV service after sign-in.
22231
23088
  * @internal
22232
23089
  */
22233
- initializeServices() {
23090
+ async initializeServices() {
22234
23091
  const session = this.currentTinyCloudSession();
22235
23092
  if (!session) {
22236
23093
  return;
@@ -22273,12 +23130,18 @@ var _TinyCloudNode = class _TinyCloudNode {
22273
23130
  jwk: session.jwk
22274
23131
  };
22275
23132
  this._serviceContext.setSession(serviceSession);
23133
+ await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
22276
23134
  this.tc.serviceContext.setSession(serviceSession);
22277
23135
  this._vault = this.createVaultService(session.spaceId, this._kv);
22278
23136
  this._vault.initialize(this._serviceContext);
22279
23137
  this._serviceContext.registerService("vault", this._vault);
22280
23138
  this.initializeV2Services(serviceSession);
22281
23139
  }
23140
+ attachReplicationKV(spaceId, kvService) {
23141
+ if (!this.replicationRuntime) return;
23142
+ const attached = this.replicationRuntime.then((runtime) => runtime.attach(spaceId, kvService));
23143
+ kvService.setReadThroughReady(attached);
23144
+ }
22282
23145
  createSpaceScopedKVService(spaceId) {
22283
23146
  const kvService = new KVService2({});
22284
23147
  if (this._serviceContext) {
@@ -22294,6 +23157,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22294
23157
  spaceScopedContext.setSession({ ...session, spaceId });
22295
23158
  }
22296
23159
  kvService.initialize(spaceScopedContext);
23160
+ this.attachReplicationKV(spaceId, kvService);
22297
23161
  }
22298
23162
  return kvService;
22299
23163
  }
@@ -22756,6 +23620,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22756
23620
  }));
22757
23621
  context.setSession(config.session);
22758
23622
  service.initialize(context);
23623
+ this.attachReplicationKV(config.session.spaceId, service);
22759
23624
  return service;
22760
23625
  },
22761
23626
  onRootDelegationNeeded: this.signer ? async (params) => {
@@ -23080,6 +23945,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23080
23945
  }));
23081
23946
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
23082
23947
  kv.initialize(spaceScopedContext);
23948
+ this.attachReplicationKV(spaceId, kv);
23083
23949
  return kv;
23084
23950
  }
23085
23951
  /**
@@ -23801,7 +24667,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23801
24667
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
23802
24668
  }
23803
24669
  await this.auth.hostPublicSpace(publicSpaceId);
23804
- const kvActions = [KV2.PUT, KV2.GET, KV2.DEL, KV2.LIST, KV2.METADATA];
24670
+ const kvActions = [KV3.PUT, KV3.GET, KV3.DEL, KV3.LIST, KV3.METADATA];
23805
24671
  const abilities = { kv: { "": kvActions } };
23806
24672
  const now = /* @__PURE__ */ new Date();
23807
24673
  const expiryMs = EXPIRY3.EPHEMERAL_MS;
@@ -24042,31 +24908,31 @@ var _TinyCloudNode = class _TinyCloudNode {
24042
24908
  );
24043
24909
  return { delegation: delegation2, prompted: true };
24044
24910
  }
24045
- const granted = this.projectSignedRecapCapabilities(session.siwe);
24046
- const { subset, missing } = this.signedCapabilitySubset(expandedEntries, granted);
24047
- if (!subset) {
24048
- const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
24049
- const runtimeGrant = this.findGrantForOperations(
24050
- runtimeOperations,
24051
- { excludePrimary: true }
24052
- );
24053
- if (runtimeGrant) {
24054
- const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
24055
- if (runtimeGrant.expiresAt.getTime() <= Date.now() + marginMs) {
24056
- throw new SessionExpiredError(runtimeGrant.expiresAt);
24057
- }
24058
- const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
24059
- const delegation2 = await this.createDelegationViaRuntimeGrant(
24060
- did,
24061
- expandedEntries,
24062
- runtimeExpiration,
24063
- runtimeGrant,
24064
- runtimeOperations,
24065
- onPrepared
24066
- );
24067
- return { delegation: delegation2, prompted: false };
24911
+ const plan = this.planDerivation(
24912
+ expandedEntries,
24913
+ session,
24914
+ effectiveExpiration,
24915
+ Date.now()
24916
+ );
24917
+ if ("refused" in plan) {
24918
+ if (plan.refused === "SESSION_EXPIRING") {
24919
+ throw new SessionExpiredError(plan.expiredAt ?? sessionExpiry ?? /* @__PURE__ */ new Date(0));
24068
24920
  }
24069
- throw new PermissionNotInManifestError(missing, granted);
24921
+ if (plan.refused === "CAVEATED_AUTHORITY") {
24922
+ throw new CaveatedDelegationUnsupportedError(plan.caveated ?? []);
24923
+ }
24924
+ throw new PermissionNotInManifestError(plan.missing ?? [], plan.granted ?? []);
24925
+ }
24926
+ if (plan.path === "runtime") {
24927
+ const delegation2 = await this.createDelegationViaRuntimeGrant(
24928
+ did,
24929
+ expandedEntries,
24930
+ plan.effectiveExpiration,
24931
+ plan.grant,
24932
+ plan.operations,
24933
+ onPrepared
24934
+ );
24935
+ return { delegation: delegation2, prompted: false };
24070
24936
  }
24071
24937
  const delegation = await this.createDelegationViaWasmPath(
24072
24938
  did,
@@ -24077,6 +24943,149 @@ var _TinyCloudNode = class _TinyCloudNode {
24077
24943
  );
24078
24944
  return { delegation, prompted: false };
24079
24945
  }
24946
+ /**
24947
+ * What `delegateTo` would select for `entries`, without signing or any
24948
+ * network call (TC-858 plan v3 §4.3): the session path when the signed
24949
+ * recap covers every entry, the runtime-grant path otherwise, or a
24950
+ * refusal. A refusal of `CAVEATED_AUTHORITY` means coverage exists only
24951
+ * through caveated branches, which the action-only WASM child-delegation
24952
+ * boundary cannot preserve — replication never derives authority through
24953
+ * them. `expiredAt`/`missing`/`granted`/`caveated` carry the payload
24954
+ * `delegateTo` turns back into its typed errors.
24955
+ * @internal
24956
+ */
24957
+ planDerivation(expandedEntries, session, effectiveExpiration, nowMs) {
24958
+ const caveatedRequest = expandedEntries.filter(
24959
+ (entry) => !recapCaveatsEqual(entry.caveats, void 0)
24960
+ );
24961
+ if (caveatedRequest.length > 0) {
24962
+ return { refused: "CAVEATED_AUTHORITY", caveated: caveatedRequest };
24963
+ }
24964
+ const granted = this.projectSignedRecapCapabilities(session.siwe);
24965
+ const { subset, missing } = this.signedCapabilitySubset(expandedEntries, granted);
24966
+ if (subset) {
24967
+ return {
24968
+ path: "session",
24969
+ parentCid: session.delegationCid,
24970
+ expiresAt: effectiveExpiration.getTime(),
24971
+ effectiveExpiration
24972
+ };
24973
+ }
24974
+ const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
24975
+ const runtimeGrant = this.findGrantForOperations(
24976
+ runtimeOperations,
24977
+ { excludePrimary: true }
24978
+ );
24979
+ if (runtimeGrant) {
24980
+ const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
24981
+ if (runtimeGrant.expiresAt.getTime() <= nowMs + marginMs) {
24982
+ return { refused: "SESSION_EXPIRING", expiredAt: runtimeGrant.expiresAt };
24983
+ }
24984
+ const caveated = this.caveatedRuntimeGrantEntries(
24985
+ expandedEntries,
24986
+ runtimeOperations,
24987
+ runtimeGrant
24988
+ );
24989
+ if (caveated.length > 0) {
24990
+ return { refused: "CAVEATED_AUTHORITY", caveated };
24991
+ }
24992
+ const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
24993
+ return {
24994
+ path: "runtime",
24995
+ parentCid: runtimeGrant.session.delegationCid,
24996
+ expiresAt: runtimeExpiration.getTime(),
24997
+ effectiveExpiration: runtimeExpiration,
24998
+ grant: runtimeGrant,
24999
+ operations: runtimeOperations
25000
+ };
25001
+ }
25002
+ const relaxedGranted = granted.map((entry) => ({ ...entry, caveats: void 0 }));
25003
+ const relaxedRequested = expandedEntries.map((entry) => ({ ...entry, caveats: void 0 }));
25004
+ const relaxed = this.signedCapabilitySubset(relaxedRequested, relaxedGranted);
25005
+ if (relaxed.subset) {
25006
+ return {
25007
+ refused: "CAVEATED_AUTHORITY",
25008
+ caveated: missing.map((entry) => ({ ...entry, caveats: [{ via: "session-recap" }] }))
25009
+ };
25010
+ }
25011
+ return { refused: "NOT_COVERED", missing, granted };
25012
+ }
25013
+ /**
25014
+ * The plan `delegateTo(device, entries)` would follow, as a pure lookup:
25015
+ * no WASM sign and no `POST /delegate`. Backs
25016
+ * `ReplicationAuthority.plan` (and `mint` inside it) so a predicted
25017
+ * parent and expiry can never disagree with what a mint would use (§4.3,
25018
+ * §4.4). Session expiry inside the 60 s margin refuses with
25019
+ * `SESSION_EXPIRING`; the same check delegateTo performs.
25020
+ * @internal
25021
+ */
25022
+ planDelegation(entries, options) {
25023
+ if (!Array.isArray(entries) || entries.length === 0) {
25024
+ throw new Error("planDelegation requires a non-empty permissions array");
25025
+ }
25026
+ const session = this.currentTinyCloudSession();
25027
+ if (!session) {
25028
+ return { refused: "SESSION_EXPIRING", expiredAt: /* @__PURE__ */ new Date(0) };
25029
+ }
25030
+ const nowMs = Date.now();
25031
+ const sessionExpiry = extractSiweExpiration(session.siwe);
25032
+ if (sessionExpiry !== void 0 && sessionExpiry.getTime() <= nowMs + _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS) {
25033
+ return { refused: "SESSION_EXPIRING", expiredAt: sessionExpiry };
25034
+ }
25035
+ const expandedEntries = this.expandPermissionEntries(entries);
25036
+ const expiryMs = resolveExpiryMs(options?.expiry);
25037
+ const expirationTime = new Date(nowMs + expiryMs);
25038
+ const effectiveExpiration = sessionExpiry !== void 0 && sessionExpiry < expirationTime ? sessionExpiry : expirationTime;
25039
+ return this.planDerivation(expandedEntries, session, effectiveExpiration, nowMs);
25040
+ }
25041
+ /**
25042
+ * The replication entries this node's sign-in request carries for the
25043
+ * current replication config and the signed-in or restored address: the
25044
+ * output of the same augmentation step `resolveSignInCapabilities` runs,
25045
+ * with the unrestricted-get and secrets gates applied (§4.1, §4.6).
25046
+ * Empty when replication is off or no address is known. Pure.
25047
+ */
25048
+ replicationSignInEntries() {
25049
+ if (this.config.replication?.enabled !== true) return [];
25050
+ const auth = this.auth;
25051
+ if (auth === void 0 || typeof auth.replicationSignInEntries !== "function") {
25052
+ return [];
25053
+ }
25054
+ return auth.replicationSignInEntries();
25055
+ }
25056
+ async replicationAuthority() {
25057
+ const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
25058
+ return createReplicationAuthority2({
25059
+ replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
25060
+ siweExpiration: (siwe) => extractSiweExpiration(siwe),
25061
+ planDelegation: (entries, options) => this.planDelegation(entries, options),
25062
+ mintDelegation: async (deviceDid, entries) => {
25063
+ const result = await this.delegateTo(principalDid(deviceDid), entries);
25064
+ return {
25065
+ ucan: result.delegation.delegationHeader.Authorization,
25066
+ expiresAt: result.delegation.expiry.getTime()
25067
+ };
25068
+ }
25069
+ });
25070
+ }
25071
+ async bindReplicationRuntime(context, session, kv, principal) {
25072
+ if (!this.replicationRuntime) return;
25073
+ const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : pkhDid2(this._address, this._chainId));
25074
+ if (identityPrincipal === void 0) return;
25075
+ const runtime = await this.replicationRuntime;
25076
+ const authority = await this.replicationAuthority();
25077
+ await runtime.bind({
25078
+ context,
25079
+ session,
25080
+ principal: identityPrincipal,
25081
+ authority,
25082
+ primaryKV: [{ space: session.spaceId, kv }]
25083
+ });
25084
+ }
25085
+ /** Stable control facade for this node's optional replication runtime. */
25086
+ get replication() {
25087
+ return this.replicationControl;
25088
+ }
24080
25089
  /**
24081
25090
  * Materialize one manifest-declared delegation using the current session key.
24082
25091
  * Delivery is intentionally out of band; callers decide how to transmit the
@@ -24299,6 +25308,18 @@ var _TinyCloudNode = class _TinyCloudNode {
24299
25308
  * it only accepts action maps, so reject the selected constrained branch.
24300
25309
  */
24301
25310
  assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant) {
25311
+ const caveated = this.caveatedRuntimeGrantEntries(entries, requestedOperations, grant);
25312
+ if (caveated.length > 0) {
25313
+ throw new CaveatedDelegationUnsupportedError(caveated);
25314
+ }
25315
+ }
25316
+ /**
25317
+ * The entries of `grant` that cover a requested operation while carrying
25318
+ * caveats — the branches {@link assertRuntimeGrantCaveatsPreservable}
25319
+ * rejects and {@link planDerivation} classifies as `CAVEATED_AUTHORITY`.
25320
+ * Shared so the pure plan and the mint path can't disagree.
25321
+ */
25322
+ caveatedRuntimeGrantEntries(entries, requestedOperations, grant) {
24302
25323
  let operationIndex = 0;
24303
25324
  const caveated = [];
24304
25325
  for (const entry of entries) {
@@ -24317,9 +25338,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24317
25338
  }
24318
25339
  }
24319
25340
  }
24320
- if (caveated.length > 0) {
24321
- throw new CaveatedDelegationUnsupportedError(caveated);
24322
- }
25341
+ return caveated;
24323
25342
  }
24324
25343
  /** Reject caveated parent branches before action-only child signing. */
24325
25344
  assertPortableDelegationCaveatsPreservable(delegation) {
@@ -24424,7 +25443,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24424
25443
  const missing = requested.filter((entry) => {
24425
25444
  const raw = this.isEncryptionPermissionEntry(entry) || this.isRawRevocationPermissionEntry(entry);
24426
25445
  const comparable = raw && this.isEncryptionPermissionEntry(entry) ? { ...entry, space: "encryption" } : entry;
24427
- return !isCapabilitySubset([comparable], raw ? rawGranted : spaceGranted).subset;
25446
+ return !isCapabilitySubset2([comparable], raw ? rawGranted : spaceGranted).subset;
24428
25447
  });
24429
25448
  return { subset: missing.length === 0, missing };
24430
25449
  }
@@ -24896,7 +25915,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24896
25915
  session.chainId
24897
25916
  );
24898
25917
  const publicAbilities = {
24899
- kv: { "": [KV2.GET, KV2.PUT, KV2.METADATA] }
25918
+ kv: { "": [KV3.GET, KV3.PUT, KV3.METADATA] }
24900
25919
  };
24901
25920
  const publicPrepared = this.wasmBindings.prepareSession({
24902
25921
  abilities: publicAbilities,
@@ -24924,7 +25943,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24924
25943
  delegationHeader: publicSession.delegationHeader,
24925
25944
  spaceId: publicSpaceId,
24926
25945
  path: "",
24927
- actions: [KV2.GET, KV2.PUT, KV2.METADATA],
25946
+ actions: [KV3.GET, KV3.PUT, KV3.METADATA],
24928
25947
  disableSubDelegation: params.disableSubDelegation ?? false,
24929
25948
  expiry: expirationTime,
24930
25949
  delegateDID: params.delegateDID,
@@ -25189,6 +26208,12 @@ TinyCloudNode.registerNodeDefaults({
25189
26208
  createSigner: (privateKey, chainId) => new PrivateKeySigner(privateKey, chainId)
25190
26209
  });
25191
26210
 
26211
+ // src/replication/node-loader.ts
26212
+ registerReplicationLoaders({
26213
+ runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
26214
+ authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
26215
+ });
26216
+
25192
26217
  // src/index.ts
25193
26218
  import { TinyCloud as TinyCloud2 } from "@tinycloud/sdk-core";
25194
26219
  import {
@@ -25206,8 +26231,8 @@ import {
25206
26231
  isEvmAddress,
25207
26232
  makePkhSpaceId as makePkhSpaceId2,
25208
26233
  parsePkhDid,
25209
- pkhDid as pkhDid3,
25210
- principalDid,
26234
+ pkhDid as pkhDid4,
26235
+ principalDid as principalDid3,
25211
26236
  principalDidEquals as principalDidEquals4,
25212
26237
  parseCanonicalNetworkId,
25213
26238
  TinyCloudDebugLogger,
@@ -25401,7 +26426,7 @@ import {
25401
26426
  resolveManifest as resolveManifest2,
25402
26427
  validateManifest,
25403
26428
  loadManifest,
25404
- isCapabilitySubset as isCapabilitySubset2,
26429
+ isCapabilitySubset as isCapabilitySubset4,
25405
26430
  expandActionShortNames,
25406
26431
  expandPermissionEntries as expandPermissionEntries2,
25407
26432
  expandPermissionEntry,
@@ -25410,7 +26435,7 @@ import {
25410
26435
  } from "@tinycloud/sdk-core";
25411
26436
  import {
25412
26437
  DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
25413
- KVService as KVService3,
26438
+ KVService as KVService4,
25414
26439
  PrefixedKVService
25415
26440
  } from "@tinycloud/sdk-core";
25416
26441
  import { SQLService as SQLService3, SQLAction, DatabaseHandle } from "@tinycloud/sdk-core";
@@ -25494,7 +26519,7 @@ import {
25494
26519
  SpaceService as SpaceService2,
25495
26520
  SpaceErrorCodes,
25496
26521
  createSpaceService,
25497
- parseSpaceUri,
26522
+ parseSpaceUri as parseSpaceUri3,
25498
26523
  buildSpaceUri,
25499
26524
  makePublicSpaceId as makePublicSpaceId2,
25500
26525
  Space
@@ -25505,7 +26530,849 @@ import {
25505
26530
  UnsupportedFeatureError as UnsupportedFeatureError2,
25506
26531
  checkNodeInfo as checkNodeInfo2
25507
26532
  } from "@tinycloud/sdk-core";
25508
- import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
26533
+ import { ServiceContext as ServiceContext4 } from "@tinycloud/sdk-core";
26534
+
26535
+ // src/replication/sqlite.ts
26536
+ init_authority();
26537
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
26538
+ import { chmod, mkdir, open as openFile, readFile, realpath, rename, rm, stat } from "fs/promises";
26539
+ import { basename, dirname as dirname2, join as join2 } from "path";
26540
+ import { KVService as KVService3, ServiceContext as ServiceContext3, principalDid as principalDid2 } from "@tinycloud/sdk-core";
26541
+ import {
26542
+ canonicalReplicationIdentity,
26543
+ replicationIdentityKey,
26544
+ requiresSecretsOptIn as requiresSecretsOptIn2
26545
+ } from "@tinycloud/sdk-services";
26546
+ var ReplicaStorageError = class extends Error {
26547
+ constructor(code, message, options) {
26548
+ super(message);
26549
+ this.name = "ReplicaStorageError";
26550
+ this.code = code;
26551
+ this.cause = options?.cause;
26552
+ }
26553
+ };
26554
+ function storageError(code, action, error) {
26555
+ if (error instanceof ReplicaStorageError) return error;
26556
+ const detail = error instanceof Error ? error.message : String(error);
26557
+ return new ReplicaStorageError(code, `${action} failed: ${detail}`, { cause: error });
26558
+ }
26559
+ function isNotFound(error) {
26560
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
26561
+ }
26562
+ async function pathExists(path) {
26563
+ try {
26564
+ await stat(path);
26565
+ return true;
26566
+ } catch (error) {
26567
+ if (isNotFound(error)) return false;
26568
+ throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
26569
+ }
26570
+ }
26571
+ function principalOf(did) {
26572
+ return principalDid2(did);
26573
+ }
26574
+ var BASE32 = "abcdefghijklmnopqrstuvwxyz234567";
26575
+ function sha256Base32(material) {
26576
+ const digest = createHash2("sha256").update(material, "utf8").digest();
26577
+ let out = "";
26578
+ let buffer = 0;
26579
+ let bits = 0;
26580
+ for (const byte of digest) {
26581
+ buffer = buffer << 8 | byte;
26582
+ bits += 8;
26583
+ while (bits >= 5) {
26584
+ out += BASE32[buffer >>> bits - 5 & 31];
26585
+ bits -= 5;
26586
+ }
26587
+ }
26588
+ if (bits > 0) out += BASE32[buffer << 5 - bits & 31];
26589
+ return out.slice(0, 26);
26590
+ }
26591
+ function partitionDirOf(root, identity) {
26592
+ return join2(root, sha256Base32(replicationIdentityKey(identity)));
26593
+ }
26594
+ function replicaHashOf(prefix, deviceDid) {
26595
+ return sha256Base32(`${prefix}
26596
+ ${principalOf(deviceDid)}`);
26597
+ }
26598
+ async function writeJsonAtomic(path, value, dirSync = fsyncDir) {
26599
+ const tmp = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
26600
+ let handle;
26601
+ try {
26602
+ handle = await openFile(tmp, "w", 384);
26603
+ await handle.writeFile(JSON.stringify(value), "utf8");
26604
+ await handle.sync();
26605
+ await handle.close();
26606
+ await rename(tmp, path);
26607
+ await dirSync(dirname2(path));
26608
+ await chmod(path, 384).catch(() => void 0);
26609
+ } catch (error) {
26610
+ try {
26611
+ await handle?.close();
26612
+ } catch {
26613
+ }
26614
+ await rm(tmp, { force: true }).catch(() => void 0);
26615
+ throw storageError("STORAGE_ERROR", `Writing ${path}`, error);
26616
+ }
26617
+ }
26618
+ var DIR_SYNC_UNSUPPORTED = {
26619
+ ENOTSUP: true,
26620
+ ENOSYS: true,
26621
+ EINVAL: true,
26622
+ EISDIR: true,
26623
+ EPERM: true
26624
+ };
26625
+ async function fsyncDir(dir) {
26626
+ let handle;
26627
+ try {
26628
+ handle = await openFile(dir, "r");
26629
+ await handle.sync();
26630
+ } catch (error) {
26631
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
26632
+ if (typeof code === "string" && DIR_SYNC_UNSUPPORTED[code] === true) return;
26633
+ throw storageError("STORAGE_ERROR", `Syncing ${dir}`, error);
26634
+ } finally {
26635
+ await handle?.close().catch(() => void 0);
26636
+ }
26637
+ }
26638
+ async function readJsonFile(path) {
26639
+ try {
26640
+ return JSON.parse(await readFile(path, "utf8"));
26641
+ } catch (error) {
26642
+ if (isNotFound(error)) return void 0;
26643
+ throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
26644
+ }
26645
+ }
26646
+ function identityRecordOf(value) {
26647
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
26648
+ return void 0;
26649
+ }
26650
+ if (!("host" in value) || !("space" in value) || !("principal" in value) || typeof value.host !== "string" || typeof value.space !== "string" || typeof value.principal !== "string") {
26651
+ return void 0;
26652
+ }
26653
+ return { host: value.host, space: value.space, principal: value.principal };
26654
+ }
26655
+ function deviceRecordOf(value) {
26656
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
26657
+ return void 0;
26658
+ }
26659
+ if (!("did" in value) || !("jwk" in value) || typeof value.did !== "string" || value.jwk === null || typeof value.jwk !== "object") {
26660
+ return void 0;
26661
+ }
26662
+ return { did: value.did, jwk: value.jwk };
26663
+ }
26664
+ function sameIdentity(a, b) {
26665
+ try {
26666
+ const left = canonicalReplicationIdentity(a);
26667
+ const right = canonicalReplicationIdentity(b);
26668
+ return left.host === right.host && left.space === right.space && left.principal === right.principal;
26669
+ } catch {
26670
+ return false;
26671
+ }
26672
+ }
26673
+ async function ensurePartitionIdentity(dir, identity, dirSync) {
26674
+ try {
26675
+ await mkdir(dir, { recursive: true, mode: 448 });
26676
+ await chmod(dir, 448);
26677
+ } catch (error) {
26678
+ throw storageError("STORAGE_ERROR", `Creating ${dir}`, error);
26679
+ }
26680
+ const file = join2(dir, "identity.json");
26681
+ const existing = await readJsonFile(file);
26682
+ if (existing === void 0) {
26683
+ await writeJsonAtomic(file, { v: 1, ...canonicalReplicationIdentity(identity) }, dirSync);
26684
+ return;
26685
+ }
26686
+ const record = identityRecordOf(existing);
26687
+ if (record === void 0 || !sameIdentity(record, identity)) {
26688
+ throw new ReplicaStorageError(
26689
+ "STORAGE_ERROR",
26690
+ `The partition at ${dir} belongs to a different identity.`
26691
+ );
26692
+ }
26693
+ }
26694
+ var PENDING_WRITE_LOCKS = /* @__PURE__ */ new Map();
26695
+ function serializePendingWrite(file, section) {
26696
+ const tail = PENDING_WRITE_LOCKS.get(file) ?? Promise.resolve();
26697
+ const entered = tail.then(section);
26698
+ const next = entered.then(
26699
+ () => void 0,
26700
+ () => void 0
26701
+ );
26702
+ PENDING_WRITE_LOCKS.set(file, next);
26703
+ void next.then(() => {
26704
+ if (PENDING_WRITE_LOCKS.get(file) === next) PENDING_WRITE_LOCKS.delete(file);
26705
+ });
26706
+ return entered;
26707
+ }
26708
+ function parsePendingState(text, file, identity) {
26709
+ let state;
26710
+ try {
26711
+ state = JSON.parse(text);
26712
+ } catch (error) {
26713
+ throw storageError("STORAGE_ERROR", `Parsing ${file}`, error);
26714
+ }
26715
+ if (state === null || typeof state !== "object" || !("v" in state) || state.v !== 2 || !("identity" in state) || !("committedEpoch" in state) || typeof state.committedEpoch !== "number" || !("seq" in state) || typeof state.seq !== "number" || !("records" in state) || !Array.isArray(state.records)) {
26716
+ throw new ReplicaStorageError("STORAGE_ERROR", `Pending-write state at ${file} is corrupt.`);
26717
+ }
26718
+ const storedIdentity = identityRecordOf(state.identity);
26719
+ if (storedIdentity === void 0 || !sameIdentity(storedIdentity, identity)) {
26720
+ throw new ReplicaStorageError("STORAGE_ERROR", `Pending-write state at ${file} belongs to a different identity.`);
26721
+ }
26722
+ return state;
26723
+ }
26724
+ var _dir, _guard, _dirSync, _file, _FilePendingWriteStore_instances, canonicalFile_fn;
26725
+ var FilePendingWriteStore = class {
26726
+ constructor(identity, dir, guard, dirSync) {
26727
+ __privateAdd(this, _FilePendingWriteStore_instances);
26728
+ this.durable = true;
26729
+ __privateAdd(this, _dir);
26730
+ __privateAdd(this, _guard);
26731
+ __privateAdd(this, _dirSync);
26732
+ __privateAdd(this, _file);
26733
+ this.identity = identity;
26734
+ __privateSet(this, _dir, dir);
26735
+ __privateSet(this, _guard, guard);
26736
+ __privateSet(this, _dirSync, dirSync);
26737
+ }
26738
+ async read() {
26739
+ const file = await __privateMethod(this, _FilePendingWriteStore_instances, canonicalFile_fn).call(this);
26740
+ let text;
26741
+ try {
26742
+ text = await readFile(file, "utf8");
26743
+ } catch (error) {
26744
+ if (isNotFound(error)) {
26745
+ return { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
26746
+ }
26747
+ throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
26748
+ }
26749
+ return parsePendingState(text, file, this.identity);
26750
+ }
26751
+ async readExisting() {
26752
+ let file;
26753
+ try {
26754
+ file = join2(await realpath(__privateGet(this, _dir)), "pending.json");
26755
+ } catch (error) {
26756
+ if (isNotFound(error)) return void 0;
26757
+ throw storageError("STORAGE_ERROR", `Resolving pending-write state in ${__privateGet(this, _dir)}`, error);
26758
+ }
26759
+ try {
26760
+ const text = await readFile(file, "utf8");
26761
+ return parsePendingState(text, file, this.identity);
26762
+ } catch (error) {
26763
+ if (isNotFound(error)) return void 0;
26764
+ if (error instanceof ReplicaStorageError) throw error;
26765
+ throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
26766
+ }
26767
+ }
26768
+ async update(mutate) {
26769
+ const file = await __privateMethod(this, _FilePendingWriteStore_instances, canonicalFile_fn).call(this);
26770
+ return serializePendingWrite(file, async () => {
26771
+ let currentFile;
26772
+ try {
26773
+ currentFile = join2(await realpath(dirname2(file)), basename(file));
26774
+ } catch (error) {
26775
+ throw storageError("STORAGE_ERROR", "Resolving the open pending-write partition", error);
26776
+ }
26777
+ if (currentFile !== file) {
26778
+ throw new ReplicaStorageError(
26779
+ "STORAGE_ERROR",
26780
+ `The pending-write partition changed while open: ${dirname2(file)}.`
26781
+ );
26782
+ }
26783
+ return __privateGet(this, _guard).call(this, async () => {
26784
+ const state = await this.read();
26785
+ const result = mutate(state);
26786
+ await writeJsonAtomic(file, state, __privateGet(this, _dirSync));
26787
+ return result;
26788
+ });
26789
+ });
26790
+ }
26791
+ };
26792
+ _dir = new WeakMap();
26793
+ _guard = new WeakMap();
26794
+ _dirSync = new WeakMap();
26795
+ _file = new WeakMap();
26796
+ _FilePendingWriteStore_instances = new WeakSet();
26797
+ canonicalFile_fn = async function() {
26798
+ __privateGet(this, _file) ?? __privateSet(this, _file, (async () => {
26799
+ await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
26800
+ const file = join2(__privateGet(this, _dir), "pending.json");
26801
+ try {
26802
+ return join2(await realpath(dirname2(file)), basename(file));
26803
+ } catch (error) {
26804
+ throw storageError("STORAGE_ERROR", `Resolving ${file}`, error);
26805
+ }
26806
+ })());
26807
+ return __privateGet(this, _file);
26808
+ };
26809
+ async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
26810
+ return guard(async () => {
26811
+ try {
26812
+ await mkdir(idDir, { recursive: true, mode: 448 });
26813
+ await chmod(idDir, 448);
26814
+ } catch (error) {
26815
+ throw storageError("STORAGE_ERROR", `Creating ${idDir}`, error);
26816
+ }
26817
+ const file = join2(idDir, "device.jwk");
26818
+ const existing = await readJsonFile(file);
26819
+ if (existing !== void 0) {
26820
+ const device2 = deviceRecordOf(existing);
26821
+ if (device2 === void 0) {
26822
+ throw new ReplicaStorageError(
26823
+ "STORAGE_ERROR",
26824
+ `The replica device key at ${file} is unreadable; remove the partition or restore it.`
26825
+ );
26826
+ }
26827
+ return device2;
26828
+ }
26829
+ const device = await createDevice();
26830
+ await writeJsonAtomic(file, { did: device.did, jwk: device.jwk }, dirSync);
26831
+ return device;
26832
+ });
26833
+ }
26834
+ async function createDeviceKey() {
26835
+ const wasm = await import("@tinycloud/node-sdk-wasm");
26836
+ const manager = new wasm.TCWSessionManager();
26837
+ const keyId = manager.createSessionKey("replica-device") ?? "replica-device";
26838
+ const jwkText = manager.jwk(keyId);
26839
+ if (jwkText === null || jwkText === void 0) {
26840
+ throw new ReplicaStorageError(
26841
+ "STORAGE_ERROR",
26842
+ "The session manager returned no JWK for the replica device key."
26843
+ );
26844
+ }
26845
+ return { did: manager.getDID(keyId), jwk: JSON.parse(jwkText) };
26846
+ }
26847
+ var SYNC_LEASE_TTL_MS = 12e4;
26848
+ var FENCE_FILE = "fence.json";
26849
+ async function readFence(replicaDir) {
26850
+ const fence = await readJsonFile(join2(replicaDir, FENCE_FILE));
26851
+ return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
26852
+ }
26853
+ function newestGrantInfo(replica, record, pending, spec) {
26854
+ if (record === null) return null;
26855
+ const parsed = replica.parseUcanGrant(record.bytes);
26856
+ return {
26857
+ cid: record.cid,
26858
+ parentCid: parsed.prf[0] ?? null,
26859
+ expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
26860
+ state: pending !== null && record === pending ? "pending" : "active",
26861
+ unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
26862
+ };
26863
+ }
26864
+ var _device, _store, _replicaDir, _replica, _guard2, _dirSync2, _generation, _ino, _transportOverride, _closed, _inFlight, _transportFactory, _SqliteReplicaHandle_instances, assertOpen_fn, assertLive_fn, transports_fn;
26865
+ var SqliteReplicaHandle = class {
26866
+ constructor(input) {
26867
+ __privateAdd(this, _SqliteReplicaHandle_instances);
26868
+ __privateAdd(this, _device);
26869
+ __privateAdd(this, _store);
26870
+ __privateAdd(this, _replicaDir);
26871
+ __privateAdd(this, _replica);
26872
+ __privateAdd(this, _guard2);
26873
+ __privateAdd(this, _dirSync2);
26874
+ /** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
26875
+ __privateAdd(this, _generation);
26876
+ /** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
26877
+ __privateAdd(this, _ino);
26878
+ __privateAdd(this, _transportOverride);
26879
+ __privateAdd(this, _closed, false);
26880
+ __privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
26881
+ __privateAdd(this, _transportFactory);
26882
+ this.spec = input.spec;
26883
+ __privateSet(this, _device, input.device);
26884
+ this.deviceDid = input.device.did;
26885
+ __privateSet(this, _store, input.store);
26886
+ __privateSet(this, _replicaDir, input.replicaDir);
26887
+ __privateSet(this, _replica, input.replica);
26888
+ __privateSet(this, _guard2, input.guard);
26889
+ __privateSet(this, _transportOverride, input.transportFor);
26890
+ __privateSet(this, _dirSync2, input.dirSync);
26891
+ __privateSet(this, _generation, input.generation);
26892
+ __privateSet(this, _ino, input.ino);
26893
+ }
26894
+ async get(key2) {
26895
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
26896
+ const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).get(key2);
26897
+ const meta = {
26898
+ asOf: result.meta.asOf,
26899
+ coverage: result.meta.coverage,
26900
+ authority: result.meta.authority,
26901
+ syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
26902
+ };
26903
+ switch (result.status) {
26904
+ case "present":
26905
+ return {
26906
+ status: "present",
26907
+ key: result.key,
26908
+ value: result.value,
26909
+ etag: result.etag,
26910
+ metadata: result.metadata,
26911
+ meta
26912
+ };
26913
+ case "content_missing":
26914
+ return {
26915
+ status: "content_missing",
26916
+ key: result.key,
26917
+ etag: result.etag,
26918
+ metadata: result.metadata,
26919
+ meta
26920
+ };
26921
+ default:
26922
+ return { status: result.status, key: result.key, meta };
26923
+ }
26924
+ }
26925
+ async list(options) {
26926
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
26927
+ const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).list({
26928
+ prefix: options.prefix,
26929
+ ...options.after === void 0 ? {} : { after: options.after },
26930
+ ...options.limit === void 0 ? {} : { limit: options.limit }
26931
+ });
26932
+ return {
26933
+ keys: result.entries.map((entry) => entry.key),
26934
+ meta: {
26935
+ asOf: result.meta.asOf,
26936
+ coverage: result.meta.coverage,
26937
+ authority: result.meta.authority,
26938
+ syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
26939
+ }
26940
+ };
26941
+ }
26942
+ /**
26943
+ * The newest installed grant — the pending one when a not-yet-promoted
26944
+ * install exists — with `parentCid` = signed `prf[0]` and `unconstrained`
26945
+ * computed from the signed `att` (replica's own grantCovers ignores
26946
+ * caveats, so the adapter computes it, §2.1). A constrained installed
26947
+ * grant is still reported (unconstrained: false) and never reused.
26948
+ */
26949
+ async grant() {
26950
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
26951
+ const state = await __privateGet(this, _store).open();
26952
+ if (state === null) return null;
26953
+ return newestGrantInfo(
26954
+ __privateGet(this, _replica),
26955
+ state.pendingGrant ?? state.grant,
26956
+ state.pendingGrant,
26957
+ this.spec
26958
+ );
26959
+ }
26960
+ async installGrant(ucan) {
26961
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
26962
+ const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
26963
+ const state = await __privateGet(this, _store).open();
26964
+ if (state === null) {
26965
+ throw new (__privateGet(this, _replica)).ReplicaError(
26966
+ __privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
26967
+ "The replica has not been created."
26968
+ );
26969
+ }
26970
+ __privateGet(this, _replica).assertGrantInstallable(parsed, {
26971
+ deviceDid: principalOf(__privateGet(this, _device).did),
26972
+ space: this.spec.space,
26973
+ prefix: this.spec.prefix,
26974
+ now: Date.now()
26975
+ });
26976
+ if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
26977
+ throw new (__privateGet(this, _replica)).ReplicaError(
26978
+ __privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
26979
+ "The grant's get or sync coverage carries caveats; re-mint instead of installing it."
26980
+ );
26981
+ }
26982
+ await __privateGet(this, _store).installGrant(parsed);
26983
+ return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec);
26984
+ }
26985
+ async sync(options) {
26986
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
26987
+ const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
26988
+ const abort = new AbortController();
26989
+ options.signal.throwIfAborted();
26990
+ const onAbort = () => abort.abort(options.signal.reason);
26991
+ options.signal.addEventListener("abort", onAbort, { once: true });
26992
+ const engine = new (__privateGet(this, _replica)).Replica({
26993
+ store: __privateGet(this, _store),
26994
+ transportFor
26995
+ });
26996
+ const running = engine.sync({ signal: abort.signal });
26997
+ const registration = { promise: running, abort: () => abort.abort() };
26998
+ __privateGet(this, _inFlight).add(registration);
26999
+ try {
27000
+ const report = await running;
27001
+ await __privateGet(this, _guard2).call(this, () => writeJsonAtomic(
27002
+ join2(__privateGet(this, _replicaDir), FENCE_FILE),
27003
+ { v: 1, syncedThroughEpoch: options.syncStartEpoch },
27004
+ __privateGet(this, _dirSync2)
27005
+ ));
27006
+ return {
27007
+ status: "synced",
27008
+ pages: report.pages,
27009
+ changes: report.changes,
27010
+ deleted: report.deleted,
27011
+ fetched: report.fetched,
27012
+ contentMissing: report.contentMissing,
27013
+ coverage: report.coverage,
27014
+ syncedThroughEpoch: options.syncStartEpoch
27015
+ };
27016
+ } catch (error) {
27017
+ if (__privateGet(this, _replica).isReplicaError(error, __privateGet(this, _replica).ReplicaErrorCode.BUSY)) {
27018
+ return { status: "busy" };
27019
+ }
27020
+ throw error;
27021
+ } finally {
27022
+ __privateGet(this, _inFlight).delete(registration);
27023
+ options.signal.removeEventListener("abort", onAbort);
27024
+ }
27025
+ }
27026
+ async status() {
27027
+ await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
27028
+ const engine = new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) });
27029
+ const status = await engine.status();
27030
+ const syncedThroughEpoch = await readFence(__privateGet(this, _replicaDir));
27031
+ const state = await __privateGet(this, _store).open();
27032
+ const grant = state === null ? null : newestGrantInfo(
27033
+ __privateGet(this, _replica),
27034
+ state.pendingGrant ?? state.grant,
27035
+ state.pendingGrant,
27036
+ this.spec
27037
+ );
27038
+ return {
27039
+ coverage: status.coverage,
27040
+ lastSyncAt: status.lastSyncAt,
27041
+ syncedThroughEpoch,
27042
+ authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27043
+ grant,
27044
+ counts: status.counts,
27045
+ bytes: status.bytes,
27046
+ lastError: status.lastError
27047
+ };
27048
+ }
27049
+ async close() {
27050
+ if (__privateGet(this, _closed)) return;
27051
+ __privateSet(this, _closed, true);
27052
+ for (const sync of __privateGet(this, _inFlight)) sync.abort();
27053
+ await Promise.allSettled([...__privateGet(this, _inFlight)].map((sync) => sync.promise));
27054
+ await __privateGet(this, _store).close();
27055
+ }
27056
+ };
27057
+ _device = new WeakMap();
27058
+ _store = new WeakMap();
27059
+ _replicaDir = new WeakMap();
27060
+ _replica = new WeakMap();
27061
+ _guard2 = new WeakMap();
27062
+ _dirSync2 = new WeakMap();
27063
+ _generation = new WeakMap();
27064
+ _ino = new WeakMap();
27065
+ _transportOverride = new WeakMap();
27066
+ _closed = new WeakMap();
27067
+ _inFlight = new WeakMap();
27068
+ _transportFactory = new WeakMap();
27069
+ _SqliteReplicaHandle_instances = new WeakSet();
27070
+ assertOpen_fn = function() {
27071
+ if (__privateGet(this, _closed)) {
27072
+ throw new (__privateGet(this, _replica)).ReplicaError(
27073
+ __privateGet(this, _replica).ReplicaErrorCode.CLOSED,
27074
+ "The replica handle is closed."
27075
+ );
27076
+ }
27077
+ };
27078
+ assertLive_fn = async function() {
27079
+ __privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
27080
+ try {
27081
+ const stats = await stat(join2(__privateGet(this, _replicaDir), "replica.db"));
27082
+ if (__privateGet(this, _ino) !== void 0 && stats.ino !== __privateGet(this, _ino)) {
27083
+ throw new (__privateGet(this, _replica)).ReplicaError(
27084
+ __privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
27085
+ "The replica was recreated; open it again to continue."
27086
+ );
27087
+ }
27088
+ } catch (error) {
27089
+ if (isNotFound(error)) {
27090
+ throw new (__privateGet(this, _replica)).ReplicaError(
27091
+ __privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
27092
+ "The replica was purged; open it again to continue."
27093
+ );
27094
+ }
27095
+ throw error;
27096
+ }
27097
+ const state = await __privateGet(this, _store).open();
27098
+ if (state !== null && state.generation !== __privateGet(this, _generation)) {
27099
+ throw new (__privateGet(this, _replica)).ReplicaError(
27100
+ __privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
27101
+ "The replica was purged or reopened; open it again to continue."
27102
+ );
27103
+ }
27104
+ };
27105
+ transports_fn = async function() {
27106
+ if (__privateGet(this, _transportOverride) !== void 0) {
27107
+ const override = __privateGet(this, _transportOverride);
27108
+ const spec = this.spec;
27109
+ const device = __privateGet(this, _device);
27110
+ return (grant) => override(grant, spec, device);
27111
+ }
27112
+ __privateGet(this, _transportFactory) ?? __privateSet(this, _transportFactory, (async () => {
27113
+ const wasm = await import("@tinycloud/node-sdk-wasm");
27114
+ const kvSyncTransport = __privateGet(this, _replica).kvSyncTransport;
27115
+ const spec = this.spec;
27116
+ const device = __privateGet(this, _device);
27117
+ const fetch2 = spec.fetch ?? globalThis.fetch.bind(globalThis);
27118
+ return (grant) => {
27119
+ const context = new ServiceContext3({
27120
+ invoke: wasm.invoke,
27121
+ invokeAny: wasm.invokeAny,
27122
+ fetch: fetch2,
27123
+ hosts: [spec.identity.host]
27124
+ });
27125
+ const kv = new KVService3({});
27126
+ kv.initialize(context);
27127
+ context.registerService("kv", kv);
27128
+ context.setSession({
27129
+ delegationHeader: { Authorization: new TextDecoder().decode(grant.bytes) },
27130
+ delegationCid: grant.cid,
27131
+ spaceId: spec.space,
27132
+ verificationMethod: device.did,
27133
+ jwk: device.jwk
27134
+ });
27135
+ return kvSyncTransport(kv);
27136
+ };
27137
+ })());
27138
+ return __privateGet(this, _transportFactory);
27139
+ };
27140
+ function createSqliteReplicaStorage(options) {
27141
+ const root = options.dir;
27142
+ const guard = options.guard ?? ((section) => section());
27143
+ const createDevice = options.createDevice ?? createDeviceKey;
27144
+ async function openHandle(spec) {
27145
+ const [replica, sqlite] = await Promise.all([
27146
+ // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
27147
+ import("@tinycloud/replica"),
27148
+ // @ts-ignore — same as above.
27149
+ import("@tinycloud/replica/sqlite")
27150
+ ]);
27151
+ if (requiresSecretsOptIn2(spec.space, spec.prefix) && spec.allowSecrets !== true) {
27152
+ throw new replica.ReplicaError(
27153
+ replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
27154
+ `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
27155
+ );
27156
+ }
27157
+ const idDir = partitionDirOf(root, spec.identity);
27158
+ await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
27159
+ const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, guard, createDevice, options.dirSync);
27160
+ const device = { ...createdDevice, did: principalOf(createdDevice.did) };
27161
+ const replicaHash = replicaHashOf(spec.prefix, device.did);
27162
+ const replicaDir = join2(idDir, "replicas", replicaHash);
27163
+ const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
27164
+ create: true,
27165
+ guard
27166
+ });
27167
+ let generation;
27168
+ try {
27169
+ try {
27170
+ await store.init({
27171
+ name: spec.prefix.replace(/\/+$/, "") || "replica",
27172
+ replicaId: replicaHash,
27173
+ host: spec.identity.host,
27174
+ space: spec.space,
27175
+ prefix: spec.prefix,
27176
+ deviceDid: device.did,
27177
+ allowSecrets: spec.allowSecrets,
27178
+ localReadPolicy: "whileGrantValid",
27179
+ retentionGrantCid: null
27180
+ });
27181
+ } catch (error) {
27182
+ if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
27183
+ throw error;
27184
+ }
27185
+ }
27186
+ const state = await store.open();
27187
+ if (state === null) {
27188
+ throw new replica.ReplicaError(
27189
+ replica.ReplicaErrorCode.NOT_FOUND,
27190
+ "The replica row vanished during init."
27191
+ );
27192
+ }
27193
+ const config = state.config;
27194
+ if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
27195
+ throw new replica.ReplicaError(
27196
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
27197
+ `This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
27198
+ );
27199
+ }
27200
+ if (principalOf(config.deviceDid) !== principalOf(device.did)) {
27201
+ throw new replica.ReplicaError(
27202
+ replica.ReplicaErrorCode.CONFIG_MISMATCH,
27203
+ "The stored replica belongs to a different device key; purge the partition to rekey."
27204
+ );
27205
+ }
27206
+ generation = state.generation;
27207
+ } catch (error) {
27208
+ await store.close().catch(() => void 0);
27209
+ throw error;
27210
+ }
27211
+ let ino;
27212
+ try {
27213
+ ino = (await stat(join2(replicaDir, "replica.db"))).ino;
27214
+ } catch (error) {
27215
+ if (!isNotFound(error)) throw error;
27216
+ }
27217
+ return new SqliteReplicaHandle({
27218
+ spec,
27219
+ device,
27220
+ store,
27221
+ replicaDir,
27222
+ replica,
27223
+ guard,
27224
+ transportFor: options.transportFor,
27225
+ dirSync: options.dirSync,
27226
+ generation,
27227
+ ino
27228
+ });
27229
+ }
27230
+ async function inspectStatus(spec, { signal }) {
27231
+ const checkAbort = () => {
27232
+ if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
27233
+ };
27234
+ checkAbort();
27235
+ const idDir = partitionDirOf(root, spec.identity);
27236
+ if (!await pathExists(idDir)) return void 0;
27237
+ checkAbort();
27238
+ const identityFile = join2(idDir, "identity.json");
27239
+ const storedIdentity = identityRecordOf(await readJsonFile(identityFile));
27240
+ if (storedIdentity === void 0 || !sameIdentity(storedIdentity, spec.identity)) {
27241
+ throw new ReplicaStorageError("STORAGE_ERROR", `The partition at ${idDir} belongs to a different identity or has no identity record.`);
27242
+ }
27243
+ checkAbort();
27244
+ let device = spec.device;
27245
+ if (device === void 0) {
27246
+ const deviceFile = join2(idDir, "device.jwk");
27247
+ const storedDevice = await readJsonFile(deviceFile);
27248
+ if (storedDevice === void 0) return void 0;
27249
+ device = deviceRecordOf(storedDevice);
27250
+ if (device === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `The replica device key at ${deviceFile} is unreadable.`);
27251
+ }
27252
+ checkAbort();
27253
+ const replicaDir = join2(idDir, "replicas", replicaHashOf(spec.prefix, device.did));
27254
+ if (!await pathExists(replicaDir)) return void 0;
27255
+ checkAbort();
27256
+ const [replica, sqlite] = await Promise.all([
27257
+ // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
27258
+ import("@tinycloud/replica"),
27259
+ // @ts-ignore — same as above.
27260
+ import("@tinycloud/replica/sqlite")
27261
+ ]);
27262
+ checkAbort();
27263
+ const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
27264
+ checkAbort();
27265
+ if (inspected === null) return void 0;
27266
+ const { state, status } = inspected;
27267
+ if (state.config.host !== spec.identity.host || state.config.space !== spec.space || state.config.prefix !== spec.prefix || principalOf(state.config.deviceDid) !== principalOf(device.did)) {
27268
+ throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
27269
+ }
27270
+ const syncedThroughEpoch = await readFence(replicaDir);
27271
+ checkAbort();
27272
+ return {
27273
+ coverage: status.coverage,
27274
+ lastSyncAt: status.lastSyncAt,
27275
+ syncedThroughEpoch,
27276
+ authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
27277
+ grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
27278
+ counts: status.counts,
27279
+ bytes: status.bytes,
27280
+ lastError: status.lastError
27281
+ };
27282
+ }
27283
+ async function purgePartition(target) {
27284
+ const [replica, sqlite] = await Promise.all([
27285
+ // @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
27286
+ import("@tinycloud/replica"),
27287
+ // @ts-ignore — same as above.
27288
+ import("@tinycloud/replica/sqlite")
27289
+ ]);
27290
+ const idDir = partitionDirOf(root, target.identity);
27291
+ if (!await pathExists(idDir)) return;
27292
+ const deviceDids = [];
27293
+ const deviceFile = join2(idDir, "device.jwk");
27294
+ const stored = await readJsonFile(deviceFile);
27295
+ if (stored !== void 0) {
27296
+ const device = deviceRecordOf(stored);
27297
+ if (device === void 0) {
27298
+ throw new ReplicaStorageError(
27299
+ "STORAGE_ERROR",
27300
+ `The replica device key at ${deviceFile} is unreadable.`
27301
+ );
27302
+ }
27303
+ deviceDids.push(device.did);
27304
+ }
27305
+ if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
27306
+ const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
27307
+ let firstError;
27308
+ for (const deviceDid of devices) {
27309
+ const replicaDir = join2(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
27310
+ if (!await pathExists(replicaDir)) continue;
27311
+ try {
27312
+ let store;
27313
+ try {
27314
+ store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard });
27315
+ } catch (error) {
27316
+ if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
27317
+ await guard(() => rm(replicaDir, { recursive: true, force: true }));
27318
+ continue;
27319
+ }
27320
+ throw error;
27321
+ }
27322
+ try {
27323
+ const state = await store.open();
27324
+ if (state === null) {
27325
+ await store.close().catch(() => void 0);
27326
+ await guard(() => rm(replicaDir, { recursive: true, force: true }));
27327
+ continue;
27328
+ }
27329
+ const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
27330
+ if (lease === null) {
27331
+ throw new replica.ReplicaError(
27332
+ replica.ReplicaErrorCode.BUSY,
27333
+ "Another process holds this replica's sync lease."
27334
+ );
27335
+ }
27336
+ try {
27337
+ if (state.revoked === null) {
27338
+ await store.reset(lease, "purged");
27339
+ }
27340
+ await store.destroy(lease);
27341
+ } finally {
27342
+ await store.releaseLease(lease).catch(() => void 0);
27343
+ }
27344
+ } finally {
27345
+ await store.close().catch(() => void 0);
27346
+ }
27347
+ } catch (error) {
27348
+ firstError ?? (firstError = error);
27349
+ }
27350
+ }
27351
+ if (firstError !== void 0) throw firstError;
27352
+ }
27353
+ return {
27354
+ kind: "sqlite",
27355
+ open: openHandle,
27356
+ inspectStatus,
27357
+ purge: purgePartition,
27358
+ // Always the durable partitioned file store (review): the runtime
27359
+ // must never fall back to volatile in-memory pending state — a
27360
+ // restart that lost ambiguous write records would serve stale data.
27361
+ // Without a guard the file store still durably serializes each update;
27362
+ // the guard only adds cross-process read-modify-write exclusion.
27363
+ pendingWrites(identity) {
27364
+ return new FilePendingWriteStore(
27365
+ identity,
27366
+ partitionDirOf(root, identity),
27367
+ guard,
27368
+ options.dirSync
27369
+ );
27370
+ }
27371
+ };
27372
+ }
27373
+ function sqliteReplicaStorage(options) {
27374
+ return createSqliteReplicaStorage(options);
27375
+ }
25509
27376
  export {
25510
27377
  ACCEPTED_MARKER_VERSIONS,
25511
27378
  ACCOUNT_REGISTRY_PATH,
@@ -25543,7 +27410,7 @@ export {
25543
27410
  HooksService3 as HooksService,
25544
27411
  IdentityParseError,
25545
27412
  InvalidRestoredSessionError,
25546
- KVService3 as KVService,
27413
+ KVService4 as KVService,
25547
27414
  MAX_SECRET_SCOPE_LENGTH,
25548
27415
  ManifestValidationError,
25549
27416
  MemorySessionStorage,
@@ -25564,7 +27431,7 @@ export {
25564
27431
  STORAGE_FULL_MESSAGE2 as STORAGE_FULL_MESSAGE,
25565
27432
  STORAGE_WRITE_TOO_LARGE_MESSAGE2 as STORAGE_WRITE_TOO_LARGE_MESSAGE,
25566
27433
  SecretsService2 as SecretsService,
25567
- ServiceContext3 as ServiceContext,
27434
+ ServiceContext4 as ServiceContext,
25568
27435
  SessionExpiredError2 as SessionExpiredError,
25569
27436
  SharingService2 as SharingService,
25570
27437
  SilentNotificationHandler2 as SilentNotificationHandler,
@@ -25637,7 +27504,7 @@ export {
25637
27504
  hexDecode,
25638
27505
  hexEncode,
25639
27506
  installTinyCloudDebugGlobals,
25640
- isCapabilitySubset2 as isCapabilitySubset,
27507
+ isCapabilitySubset4 as isCapabilitySubset,
25641
27508
  isEvmAddress,
25642
27509
  isNetworkId,
25643
27510
  isStorageFullError2 as isStorageFullError,
@@ -25651,9 +27518,9 @@ export {
25651
27518
  parseNetworkId3 as parseNetworkId,
25652
27519
  parsePkhDid,
25653
27520
  parseSecretCatalogKey,
25654
- parseSpaceUri,
25655
- pkhDid3 as pkhDid,
25656
- principalDid,
27521
+ parseSpaceUri3 as parseSpaceUri,
27522
+ pkhDid4 as pkhDid,
27523
+ principalDid3 as principalDid,
25657
27524
  principalDidEquals4 as principalDidEquals,
25658
27525
  resolveManifest2 as resolveManifest,
25659
27526
  resolveSecretListPrefix2 as resolveSecretListPrefix,
@@ -25661,6 +27528,7 @@ export {
25661
27528
  resourceCapabilitiesToSpaceAbilitiesMap2 as resourceCapabilitiesToSpaceAbilitiesMap,
25662
27529
  serializeDelegation,
25663
27530
  signedCompactUcanExpiry,
27531
+ sqliteReplicaStorage,
25664
27532
  tinyCloudDebugLogger,
25665
27533
  validateEnvelope,
25666
27534
  validateManifest,