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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,
@@ -17179,7 +17658,9 @@ var RUNTIME_DELEGATION_REJECTIONS = [
17179
17658
  "audience_mismatch",
17180
17659
  "resources_mismatch",
17181
17660
  "unsupported",
17182
- "activation_rejected"
17661
+ "activation_rejected",
17662
+ "activation_unavailable",
17663
+ "activation_unauthenticated"
17183
17664
  ];
17184
17665
  var RuntimeDelegationRejectedError = class extends Error {
17185
17666
  constructor(reason, message) {
@@ -17188,6 +17669,18 @@ var RuntimeDelegationRejectedError = class extends Error {
17188
17669
  this.reason = reason;
17189
17670
  }
17190
17671
  };
17672
+ function activationRejectionReason(error) {
17673
+ if (typeof error !== "object" || error === null) return "activation_rejected";
17674
+ if ("code" in error && error.code === "ACTIVATION_TRANSPORT_FAILED") {
17675
+ return "activation_unavailable";
17676
+ }
17677
+ const cause = "cause" in error ? error.cause : void 0;
17678
+ const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
17679
+ if (typeof status !== "number") return "activation_rejected";
17680
+ if (status === 408 || status === 429 || status >= 500) return "activation_unavailable";
17681
+ if (status === 401) return "activation_unauthenticated";
17682
+ return "activation_rejected";
17683
+ }
17191
17684
  function reject(reason, message) {
17192
17685
  throw new RuntimeDelegationRejectedError(reason, message);
17193
17686
  }
@@ -17572,7 +18065,10 @@ async function activateValidatedRuntimeDelegation(node, delegation, options) {
17572
18065
  await node.useRuntimeDelegation(installedCandidate);
17573
18066
  }
17574
18067
  } catch (error) {
17575
- reject("activation_rejected", error instanceof Error ? error.message : String(error));
18068
+ reject(
18069
+ activationRejectionReason(error),
18070
+ error instanceof Error ? error.message : String(error)
18071
+ );
17576
18072
  }
17577
18073
  const installed = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
17578
18074
  if (!installed) {
@@ -17671,6 +18167,21 @@ function deserializeDelegation(data) {
17671
18167
  };
17672
18168
  }
17673
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
+
17674
18185
  // src/TinyCloudNode.ts
17675
18186
  import {
17676
18187
  TinyCloud,
@@ -17709,12 +18220,12 @@ import {
17709
18220
  canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
17710
18221
  recapCaveatsEqual,
17711
18222
  expandPermissionEntries as expandPermissionEntriesCore,
17712
- isCapabilitySubset,
18223
+ isCapabilitySubset as isCapabilitySubset2,
17713
18224
  isExplicitOnlyAction,
17714
18225
  parseRecapCapabilities,
17715
18226
  SERVICE_LONG_TO_SHORT,
17716
18227
  SERVICE_SHORT_TO_LONG,
17717
- KV as KV2,
18228
+ KV as KV3,
17718
18229
  SQL as SQL2,
17719
18230
  DUCKDB as DUCKDB2,
17720
18231
  ENCRYPTION as ENCRYPTION2,
@@ -17724,6 +18235,7 @@ import {
17724
18235
  verifyDidKeyEd25519Signature,
17725
18236
  canonicalizeAddress as canonicalizeAddress2,
17726
18237
  pkhDid as pkhDid2,
18238
+ principalDid,
17727
18239
  resolveTinyCloudHosts as resolveTinyCloudHosts2,
17728
18240
  publishLocationRecord,
17729
18241
  principalDidEquals as principalDidEquals3,
@@ -17743,6 +18255,7 @@ import {
17743
18255
  } from "@tinycloud/sdk-services";
17744
18256
 
17745
18257
  // src/authorization/NodeUserAuthorization.ts
18258
+ init_authority_sign_in();
17746
18259
  import { createHash } from "crypto";
17747
18260
  import { SiweMessage } from "siwe";
17748
18261
  import {
@@ -17770,7 +18283,7 @@ import {
17770
18283
  parseCanonicalRecapResource,
17771
18284
  ACCOUNT_REGISTRY_SPACE,
17772
18285
  ACCOUNT_MANIFEST_PERMISSIONS,
17773
- KV,
18286
+ KV as KV2,
17774
18287
  SQL,
17775
18288
  DUCKDB,
17776
18289
  CAPABILITIES,
@@ -17924,7 +18437,7 @@ var NodeUserAuthorization = class {
17924
18437
  this.spacePrefix = config.spacePrefix ?? "default";
17925
18438
  this.defaultActions = config.defaultActions ?? {
17926
18439
  kv: {
17927
- "": [KV.PUT, KV.GET, KV.DEL, KV.LIST, KV.METADATA]
18440
+ "": [KV2.PUT, KV2.GET, KV2.DEL, KV2.LIST, KV2.METADATA]
17928
18441
  },
17929
18442
  sql: {
17930
18443
  "": [SQL.READ, SQL.WRITE, SQL.SCHEMA, SQL.ADMIN, SQL.EXPORT]
@@ -17962,6 +18475,7 @@ var NodeUserAuthorization = class {
17962
18475
  this.nonce = config.nonce;
17963
18476
  this.siweConfig = config.siweConfig;
17964
18477
  this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
18478
+ this.replication = config.replication?.enabled === true ? config.replication : void 0;
17965
18479
  this._manifest = config.manifest;
17966
18480
  this._capabilityRequest = config.capabilityRequest;
17967
18481
  this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
@@ -18170,6 +18684,143 @@ var NodeUserAuthorization = class {
18170
18684
  }
18171
18685
  return makePkhSpaceId(address, chainId, space);
18172
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
+ }
18173
18824
  defaultEncryptionNetworkId(address, chainId) {
18174
18825
  return `urn:tinycloud:encryption:${pkhDid(address, chainId)}:default`;
18175
18826
  }
@@ -18179,39 +18830,25 @@ var NodeUserAuthorization = class {
18179
18830
  const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
18180
18831
  const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
18181
18832
  const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
18182
- const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
18833
+ const primaryActions = this.primaryActionsForSignIn();
18183
18834
  const spaceAbilities2 = {
18184
18835
  [primarySpaceId2]: primaryActions,
18185
- [secretsSpaceId]: {
18186
- kv: {
18187
- "vault/secrets/": [
18188
- KV.GET,
18189
- KV.PUT,
18190
- KV.DEL,
18191
- KV.LIST,
18192
- KV.METADATA
18193
- ]
18194
- }
18195
- }
18836
+ [secretsSpaceId]: this.secretsPrimaryAbilities()
18196
18837
  };
18197
18838
  if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
18198
18839
  spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
18199
- } else if (this.includeAccountRegistryPermissions) {
18200
- const accountAbilities = resourceCapabilitiesToAbilitiesMap(
18201
- ACCOUNT_MANIFEST_PERMISSIONS
18202
- );
18203
- for (const [service, paths] of Object.entries(accountAbilities)) {
18204
- const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
18205
- for (const [path, actions] of Object.entries(paths)) {
18206
- const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
18207
- for (const action of actions) {
18208
- if (!existingActions.includes(action)) existingActions.push(action);
18209
- }
18210
- }
18211
- }
18212
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
+ );
18213
18850
  return {
18214
- abilities: primaryActions,
18851
+ abilities: mergedPrimary,
18215
18852
  spaceId: primarySpaceId2,
18216
18853
  spaceAbilities: spaceAbilities2,
18217
18854
  rawAbilities: {
@@ -18247,8 +18884,17 @@ var NodeUserAuthorization = class {
18247
18884
  for (const [space, abilities] of Object.entries(bySpace)) {
18248
18885
  spaceAbilities[this.resolveSpaceName(space, address, chainId)] = abilities;
18249
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
+ );
18250
18896
  return {
18251
- abilities: spaceAbilities[primarySpaceId] ?? resourceCapabilitiesToAbilitiesMap([]),
18897
+ abilities: mergedPrimaryAbilities,
18252
18898
  spaceId: primarySpaceId,
18253
18899
  spaceAbilities,
18254
18900
  rawAbilities: Object.keys(rawAbilities).length > 0 ? rawAbilities : void 0
@@ -18306,14 +18952,14 @@ var NodeUserAuthorization = class {
18306
18952
  * Create the space on the TinyCloud server (host delegation).
18307
18953
  * This registers the user as the owner of the space.
18308
18954
  */
18309
- async hostSpace(targetSpaceId, purpose) {
18955
+ async hostSpace(targetSpaceId, purpose, signal) {
18310
18956
  if (!this._tinyCloudSession || !this._address || !this._chainId) {
18311
18957
  throw new Error("Must be signed in to host space");
18312
18958
  }
18313
18959
  await this.ensureTinyCloudHosts();
18314
18960
  const host = this.primaryTinyCloudHost;
18315
18961
  const spaceId = targetSpaceId ?? this._tinyCloudSession.spaceId;
18316
- const peerId = await fetchPeerId(host, spaceId);
18962
+ const peerId = await fetchPeerId(host, spaceId, signal);
18317
18963
  const siwe = this.wasm.generateHostSIWEMessage({
18318
18964
  address: this._address,
18319
18965
  chainId: this._chainId,
@@ -18322,9 +18968,28 @@ var NodeUserAuthorization = class {
18322
18968
  spaceId,
18323
18969
  peerId
18324
18970
  });
18325
- 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;
18326
18991
  const headers = this.wasm.siweToDelegationHeaders({ siwe, signature: signature2 });
18327
- return submitHostDelegation(host, headers);
18992
+ return submitHostDelegation(host, headers, signal);
18328
18993
  }
18329
18994
  /**
18330
18995
  * Create a specific space on the server via host delegation.
@@ -18345,8 +19010,8 @@ var NodeUserAuthorization = class {
18345
19010
  * keeps its HTTP status (e.g. a typed 401/403) instead of collapsing to
18346
19011
  * `false`.
18347
19012
  */
18348
- async hostOwnedSpaceResult(spaceId, purpose) {
18349
- return this.hostSpace(spaceId, purpose);
19013
+ async hostOwnedSpaceResult(spaceId, purpose, signal) {
19014
+ return this.hostSpace(spaceId, purpose, signal);
18350
19015
  }
18351
19016
  /**
18352
19017
  * Ensure the user's space exists on the TinyCloud server.
@@ -19725,11 +20390,11 @@ function isSecretPayload(value) {
19725
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";
19726
20391
  }
19727
20392
  var ROOT_DELEGATION_ACTIONS = [
19728
- KV2.PUT,
19729
- KV2.GET,
19730
- KV2.DEL,
19731
- KV2.LIST,
19732
- KV2.METADATA,
20393
+ KV3.PUT,
20394
+ KV3.GET,
20395
+ KV3.DEL,
20396
+ KV3.LIST,
20397
+ KV3.METADATA,
19733
20398
  SQL2.READ,
19734
20399
  SQL2.WRITE,
19735
20400
  SQL2.ADMIN,
@@ -20180,6 +20845,8 @@ var _TinyCloudNode = class _TinyCloudNode {
20180
20845
  this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
20181
20846
  /** Serializes account-registry writes for the active node instance. */
20182
20847
  this.accountRegistryTail = Promise.resolve();
20848
+ /** Bounds best-effort registry work on the sign-in critical path. */
20849
+ this.accountRegistryDeadlineMs = 4e3;
20183
20850
  /**
20184
20851
  * True when the last signIn() detected an interactive signer and skipped
20185
20852
  * client-side bootstrap. Apps can read this to know whether bootstrap was
@@ -20258,11 +20925,46 @@ var _TinyCloudNode = class _TinyCloudNode {
20258
20925
  });
20259
20926
  return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
20260
20927
  };
20928
+ assertValidReplicationConfig(config.replication);
20261
20929
  this.explicitHost = config.host;
20262
20930
  this.config = {
20263
20931
  ...config,
20264
20932
  host: config.host ?? DEFAULT_HOST
20265
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
+ }
20266
20968
  if (config.wasmBindings) {
20267
20969
  this.wasmBindings = config.wasmBindings;
20268
20970
  } else if (_TinyCloudNode.nodeDefaults) {
@@ -20383,7 +21085,14 @@ var _TinyCloudNode = class _TinyCloudNode {
20383
21085
  siweConfig: config.siweConfig,
20384
21086
  manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
20385
21087
  capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
20386
- 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
20387
21096
  });
20388
21097
  this.tc = new TinyCloud(this.auth, {
20389
21098
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -20634,6 +21343,9 @@ var _TinyCloudNode = class _TinyCloudNode {
20634
21343
  this.confirmedHostedSpaceIds.clear();
20635
21344
  await this.tc.signIn(options);
20636
21345
  this.syncResolvedHostFromAuth();
21346
+ this.replicationDelegateSession = void 0;
21347
+ this.replicationScopeError = void 0;
21348
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
20637
21349
  const oldGraph = this._serviceGraph;
20638
21350
  this._serviceGraph = this.createServiceGraphLifetime();
20639
21351
  oldGraph.retire();
@@ -20643,7 +21355,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20643
21355
  this.sessionKeyId = signedInSession.sessionKey;
20644
21356
  this.sessionKeyJwk = signedInSession.jwk;
20645
21357
  }
20646
- this.initializeServices();
21358
+ await this.initializeServices();
20647
21359
  const primarySession = this.currentTinyCloudSession();
20648
21360
  if (primarySession) {
20649
21361
  this.registerPrimarySessionGrant(primarySession);
@@ -20655,6 +21367,7 @@ var _TinyCloudNode = class _TinyCloudNode {
20655
21367
  }
20656
21368
  if (!bootstrapped) {
20657
21369
  this.scheduleAccountRegistrySync();
21370
+ await this.accountRegistryTail;
20658
21371
  }
20659
21372
  this.notificationHandler.success("Successfully signed in");
20660
21373
  }
@@ -21107,7 +21820,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21107
21820
  provenance: "primary"
21108
21821
  });
21109
21822
  }
21110
- async writeManifestRegistryRecords() {
21823
+ async writeManifestRegistryRecords(account, signal) {
21111
21824
  const request = this.capabilityRequest;
21112
21825
  if (!request || request.registryRecords.length === 0) {
21113
21826
  return;
@@ -21116,8 +21829,9 @@ var _TinyCloudNode = class _TinyCloudNode {
21116
21829
  throw new Error("Manifest registry write requires wallet mode");
21117
21830
  }
21118
21831
  const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
21119
- await this.ensureOwnedSpaceHostedById(accountSpaceId);
21120
- 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);
21121
21835
  if (!result.ok) {
21122
21836
  throw Object.assign(
21123
21837
  new Error(`Failed to write manifest registry records: ${result.error.message}`),
@@ -21125,34 +21839,113 @@ var _TinyCloudNode = class _TinyCloudNode {
21125
21839
  );
21126
21840
  }
21127
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
+ }
21128
21891
  scheduleAccountRegistrySync() {
21129
21892
  const session = this.currentTinyCloudSession();
21130
21893
  if (!session || this.pendingAccountRegistrySync?.session === session) {
21131
21894
  return;
21132
21895
  }
21896
+ const controller = new AbortController();
21897
+ const deadline = setTimeout(
21898
+ () => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
21899
+ this.accountRegistryDeadlineMs
21900
+ );
21133
21901
  const promise = this.enqueueAccountRegistryOperation(session, async () => {
21134
- await this.withAccountRegistryRetry(async () => {
21135
- if (this.currentTinyCloudSession() !== session) {
21136
- return;
21137
- }
21138
- await this.account.index.ensure();
21139
- if (this.currentTinyCloudSession() !== session) {
21140
- return;
21141
- }
21142
- await this.writeManifestRegistryRecords();
21143
- if (this.currentTinyCloudSession() !== session) {
21144
- return;
21145
- }
21146
- if (this.currentSessionCanListSpaces()) {
21147
- const spaces = await this.account.spaces.syncAccessible();
21148
- if (!spaces.ok) {
21149
- throw Object.assign(
21150
- new Error(`Failed to sync account spaces: ${spaces.error.message}`),
21151
- { cause: spaces.error }
21152
- );
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
+ }
21153
21941
  }
21942
+ }, controller.signal);
21943
+ } finally {
21944
+ for (const operationContext of registry.contexts) {
21945
+ operationContext.retire();
21154
21946
  }
21155
- });
21947
+ clearTimeout(deadline);
21948
+ }
21156
21949
  });
21157
21950
  this.pendingAccountRegistrySync = { session, promise };
21158
21951
  const clearPendingSync = () => {
@@ -21161,6 +21954,10 @@ var _TinyCloudNode = class _TinyCloudNode {
21161
21954
  }
21162
21955
  };
21163
21956
  void promise.then(clearPendingSync, clearPendingSync);
21957
+ void promise.then(
21958
+ () => clearTimeout(deadline),
21959
+ () => clearTimeout(deadline)
21960
+ );
21164
21961
  }
21165
21962
  enqueueAccountRegistryOperation(session, task) {
21166
21963
  const operation = this.accountRegistryTail.then(async () => {
@@ -21196,14 +21993,22 @@ var _TinyCloudNode = class _TinyCloudNode {
21196
21993
  (operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
21197
21994
  );
21198
21995
  }
21199
- async withAccountRegistryRetry(task) {
21996
+ async withAccountRegistryRetry(task, signal) {
21200
21997
  const delays = [250, 1e3, 3e3];
21201
21998
  let lastError;
21202
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
+ }
21203
22004
  try {
21204
22005
  await task();
21205
22006
  return;
21206
22007
  } catch (error) {
22008
+ if (signal.aborted) {
22009
+ console.warn("TinyCloud account registry sync stopped: deadline exceeded", error);
22010
+ return;
22011
+ }
21207
22012
  const verdict = authorizationVerdictOf(error);
21208
22013
  const message = error instanceof Error ? error.message : String(error);
21209
22014
  const isAuthorizationVerdict = verdict === void 0 ? /Unauthorized Action|\b(?:401|403)\b/.test(message) : verdict !== "other";
@@ -21215,12 +22020,28 @@ var _TinyCloudNode = class _TinyCloudNode {
21215
22020
  return;
21216
22021
  }
21217
22022
  if (isStorageFullError(error) || isStorageFullError(error?.cause) || message.includes(STORAGE_FULL_MESSAGE) || message.includes(STORAGE_WRITE_TOO_LARGE_MESSAGE)) {
21218
- 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
+ );
21219
22027
  return;
21220
22028
  }
21221
22029
  lastError = error;
21222
22030
  if (attempt < delays.length - 1) {
21223
- 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
+ });
21224
22045
  }
21225
22046
  }
21226
22047
  }
@@ -21270,7 +22091,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21270
22091
  * self-revealing: the next write to the space fails immediately and loudly
21271
22092
  * with `404 Space not found`.
21272
22093
  */
21273
- async ensureOwnedSpaceHostedById(spaceId) {
22094
+ async ensureOwnedSpaceHostedById(spaceId, signal) {
21274
22095
  if (!this.auth) {
21275
22096
  throw new Error("Owned space hosting requires wallet mode");
21276
22097
  }
@@ -21285,7 +22106,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21285
22106
  if (!host) {
21286
22107
  throw new Error("Owned space hosting requires a TinyCloud host");
21287
22108
  }
21288
- const activation = await activateSessionWithHost2(host, session.delegationHeader);
22109
+ const activation = await activateSessionWithHost2(host, session.delegationHeader, signal);
21289
22110
  if (activation.success && !activation.skipped?.includes(spaceId)) {
21290
22111
  this.confirmedHostedSpaceIds.add(spaceId);
21291
22112
  return;
@@ -21296,15 +22117,16 @@ var _TinyCloudNode = class _TinyCloudNode {
21296
22117
  { cause: activation }
21297
22118
  );
21298
22119
  }
21299
- 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);
21300
22122
  if (!created.success) {
21301
22123
  throw Object.assign(
21302
22124
  new Error(`Failed to create owned space ${spaceId}: ${describeHostFailure2(created)}`),
21303
22125
  { cause: created }
21304
22126
  );
21305
22127
  }
21306
- await new Promise((resolve) => setTimeout(resolve, 100));
21307
- const retry = await activateSessionWithHost2(host, session.delegationHeader);
22128
+ if (signal?.aborted) throw signal.reason;
22129
+ const retry = await activateSessionWithHost2(host, session.delegationHeader, signal);
21308
22130
  if (!retry.success || retry.skipped?.includes(spaceId)) {
21309
22131
  throw Object.assign(
21310
22132
  new Error(
@@ -21582,8 +22404,36 @@ var _TinyCloudNode = class _TinyCloudNode {
21582
22404
  const stagedHost = resolvedHost ?? this.config.host;
21583
22405
  const stagedAddress = restoredAddress;
21584
22406
  const stagedChainId = sessionData.chainId ?? 1;
21585
- const stagedNodeDid = stagedAddress ? pkhDid2(stagedAddress, stagedChainId) : canonicalVerificationMethod;
21586
- const serviceSession = {
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 = {
21587
22437
  delegationHeader: sessionData.delegationHeader,
21588
22438
  delegationCid: sessionData.delegationCid,
21589
22439
  spaceId: sessionData.spaceId,
@@ -21660,7 +22510,18 @@ var _TinyCloudNode = class _TinyCloudNode {
21660
22510
  } else {
21661
22511
  this._restoredTcSession = stagedTcSession;
21662
22512
  }
22513
+ this.replicationDelegateSession = compactReplicationSession;
22514
+ this.replicationScopeError = replicationScopeError;
22515
+ if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
21663
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);
21664
22525
  oldCore?.retireServices?.();
21665
22526
  }
21666
22527
  stagePrimarySessionState(session, verificationMethod, host, sessionExpiry, recap) {
@@ -21814,6 +22675,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21814
22675
  }));
21815
22676
  context.setSession(config.session);
21816
22677
  service.initialize(context);
22678
+ this.attachReplicationKV(config.session.spaceId, service);
21817
22679
  return service;
21818
22680
  }
21819
22681
  });
@@ -21854,6 +22716,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21854
22716
  }));
21855
22717
  context.setSession({ ...input.serviceSession, spaceId });
21856
22718
  scopedKv.initialize(context);
22719
+ this.attachReplicationKV(spaceId, scopedKv);
21857
22720
  return scopedKv;
21858
22721
  },
21859
22722
  createVaultService: (spaceId) => {
@@ -21868,6 +22731,7 @@ var _TinyCloudNode = class _TinyCloudNode {
21868
22731
  }));
21869
22732
  context.setSession({ ...input.serviceSession, spaceId });
21870
22733
  scopedKv.initialize(context);
22734
+ this.attachReplicationKV(spaceId, scopedKv);
21871
22735
  const scopedVault = this.createVaultService(spaceId, scopedKv, encryption, {
21872
22736
  host: input.host,
21873
22737
  did: input.nodeDid,
@@ -22148,7 +23012,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22148
23012
  siweConfig: this.config.siweConfig,
22149
23013
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
22150
23014
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
22151
- 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
22152
23021
  });
22153
23022
  this.tc = new TinyCloud(this.auth, {
22154
23023
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -22201,7 +23070,12 @@ var _TinyCloudNode = class _TinyCloudNode {
22201
23070
  siweConfig: this.config.siweConfig,
22202
23071
  manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
22203
23072
  capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
22204
- 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
22205
23079
  });
22206
23080
  this.tc = new TinyCloud(this.auth, {
22207
23081
  invokeAny: this.invokeAnyWithRuntimePermissions,
@@ -22213,7 +23087,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22213
23087
  * Initialize the service context and KV service after sign-in.
22214
23088
  * @internal
22215
23089
  */
22216
- initializeServices() {
23090
+ async initializeServices() {
22217
23091
  const session = this.currentTinyCloudSession();
22218
23092
  if (!session) {
22219
23093
  return;
@@ -22256,12 +23130,18 @@ var _TinyCloudNode = class _TinyCloudNode {
22256
23130
  jwk: session.jwk
22257
23131
  };
22258
23132
  this._serviceContext.setSession(serviceSession);
23133
+ await this.bindReplicationRuntime(this._serviceContext, serviceSession, this._kv);
22259
23134
  this.tc.serviceContext.setSession(serviceSession);
22260
23135
  this._vault = this.createVaultService(session.spaceId, this._kv);
22261
23136
  this._vault.initialize(this._serviceContext);
22262
23137
  this._serviceContext.registerService("vault", this._vault);
22263
23138
  this.initializeV2Services(serviceSession);
22264
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
+ }
22265
23145
  createSpaceScopedKVService(spaceId) {
22266
23146
  const kvService = new KVService2({});
22267
23147
  if (this._serviceContext) {
@@ -22277,6 +23157,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22277
23157
  spaceScopedContext.setSession({ ...session, spaceId });
22278
23158
  }
22279
23159
  kvService.initialize(spaceScopedContext);
23160
+ this.attachReplicationKV(spaceId, kvService);
22280
23161
  }
22281
23162
  return kvService;
22282
23163
  }
@@ -22739,6 +23620,7 @@ var _TinyCloudNode = class _TinyCloudNode {
22739
23620
  }));
22740
23621
  context.setSession(config.session);
22741
23622
  service.initialize(context);
23623
+ this.attachReplicationKV(config.session.spaceId, service);
22742
23624
  return service;
22743
23625
  },
22744
23626
  onRootDelegationNeeded: this.signer ? async (params) => {
@@ -23063,6 +23945,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23063
23945
  }));
23064
23946
  spaceScopedContext.setSession({ ...this._serviceContext.session, spaceId });
23065
23947
  kv.initialize(spaceScopedContext);
23948
+ this.attachReplicationKV(spaceId, kv);
23066
23949
  return kv;
23067
23950
  }
23068
23951
  /**
@@ -23485,10 +24368,18 @@ var _TinyCloudNode = class _TinyCloudNode {
23485
24368
  );
23486
24369
  }
23487
24370
  const targetHost = delegation.host ?? this.config.host;
23488
- const activateResult = await activateSessionWithHost2(
23489
- targetHost,
23490
- delegation.delegationHeader
23491
- );
24371
+ let activateResult;
24372
+ try {
24373
+ activateResult = await activateSessionWithHost2(
24374
+ targetHost,
24375
+ delegation.delegationHeader
24376
+ );
24377
+ } catch (cause) {
24378
+ throw Object.assign(
24379
+ new Error("The TinyCloud host could not be reached to activate the delegation."),
24380
+ { code: "ACTIVATION_TRANSPORT_FAILED", cause }
24381
+ );
24382
+ }
23492
24383
  if (!activateResult.success) {
23493
24384
  if (this.operationsFromDelegation(delegation).some(
23494
24385
  (operation) => operation.resource?.startsWith("urn:cid:") === true && operation.service === "delegation" && operation.action === "tinycloud.delegation/revoke"
@@ -23776,7 +24667,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23776
24667
  throw new Error("Public space not enabled. Set enablePublicSpace: true in config.");
23777
24668
  }
23778
24669
  await this.auth.hostPublicSpace(publicSpaceId);
23779
- 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];
23780
24671
  const abilities = { kv: { "": kvActions } };
23781
24672
  const now = /* @__PURE__ */ new Date();
23782
24673
  const expiryMs = EXPIRY3.EPHEMERAL_MS;
@@ -24017,31 +24908,31 @@ var _TinyCloudNode = class _TinyCloudNode {
24017
24908
  );
24018
24909
  return { delegation: delegation2, prompted: true };
24019
24910
  }
24020
- const granted = this.projectSignedRecapCapabilities(session.siwe);
24021
- const { subset, missing } = this.signedCapabilitySubset(expandedEntries, granted);
24022
- if (!subset) {
24023
- const runtimeOperations = this.permissionEntriesToOperations(expandedEntries, session);
24024
- const runtimeGrant = this.findGrantForOperations(
24025
- runtimeOperations,
24026
- { excludePrimary: true }
24027
- );
24028
- if (runtimeGrant) {
24029
- const marginMs = _TinyCloudNode.SESSION_EXPIRY_SAFETY_MARGIN_MS;
24030
- if (runtimeGrant.expiresAt.getTime() <= Date.now() + marginMs) {
24031
- throw new SessionExpiredError(runtimeGrant.expiresAt);
24032
- }
24033
- const runtimeExpiration = runtimeGrant.expiresAt < effectiveExpiration ? runtimeGrant.expiresAt : effectiveExpiration;
24034
- const delegation2 = await this.createDelegationViaRuntimeGrant(
24035
- did,
24036
- expandedEntries,
24037
- runtimeExpiration,
24038
- runtimeGrant,
24039
- runtimeOperations,
24040
- onPrepared
24041
- );
24042
- 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));
24920
+ }
24921
+ if (plan.refused === "CAVEATED_AUTHORITY") {
24922
+ throw new CaveatedDelegationUnsupportedError(plan.caveated ?? []);
24043
24923
  }
24044
- throw new PermissionNotInManifestError(missing, granted);
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 };
24045
24936
  }
24046
24937
  const delegation = await this.createDelegationViaWasmPath(
24047
24938
  did,
@@ -24052,6 +24943,149 @@ var _TinyCloudNode = class _TinyCloudNode {
24052
24943
  );
24053
24944
  return { delegation, prompted: false };
24054
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
+ }
24055
25089
  /**
24056
25090
  * Materialize one manifest-declared delegation using the current session key.
24057
25091
  * Delivery is intentionally out of band; callers decide how to transmit the
@@ -24274,6 +25308,18 @@ var _TinyCloudNode = class _TinyCloudNode {
24274
25308
  * it only accepts action maps, so reject the selected constrained branch.
24275
25309
  */
24276
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) {
24277
25323
  let operationIndex = 0;
24278
25324
  const caveated = [];
24279
25325
  for (const entry of entries) {
@@ -24292,9 +25338,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24292
25338
  }
24293
25339
  }
24294
25340
  }
24295
- if (caveated.length > 0) {
24296
- throw new CaveatedDelegationUnsupportedError(caveated);
24297
- }
25341
+ return caveated;
24298
25342
  }
24299
25343
  /** Reject caveated parent branches before action-only child signing. */
24300
25344
  assertPortableDelegationCaveatsPreservable(delegation) {
@@ -24399,7 +25443,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24399
25443
  const missing = requested.filter((entry) => {
24400
25444
  const raw = this.isEncryptionPermissionEntry(entry) || this.isRawRevocationPermissionEntry(entry);
24401
25445
  const comparable = raw && this.isEncryptionPermissionEntry(entry) ? { ...entry, space: "encryption" } : entry;
24402
- return !isCapabilitySubset([comparable], raw ? rawGranted : spaceGranted).subset;
25446
+ return !isCapabilitySubset2([comparable], raw ? rawGranted : spaceGranted).subset;
24403
25447
  });
24404
25448
  return { subset: missing.length === 0, missing };
24405
25449
  }
@@ -24871,7 +25915,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24871
25915
  session.chainId
24872
25916
  );
24873
25917
  const publicAbilities = {
24874
- kv: { "": [KV2.GET, KV2.PUT, KV2.METADATA] }
25918
+ kv: { "": [KV3.GET, KV3.PUT, KV3.METADATA] }
24875
25919
  };
24876
25920
  const publicPrepared = this.wasmBindings.prepareSession({
24877
25921
  abilities: publicAbilities,
@@ -24899,7 +25943,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24899
25943
  delegationHeader: publicSession.delegationHeader,
24900
25944
  spaceId: publicSpaceId,
24901
25945
  path: "",
24902
- actions: [KV2.GET, KV2.PUT, KV2.METADATA],
25946
+ actions: [KV3.GET, KV3.PUT, KV3.METADATA],
24903
25947
  disableSubDelegation: params.disableSubDelegation ?? false,
24904
25948
  expiry: expirationTime,
24905
25949
  delegateDID: params.delegateDID,
@@ -25164,6 +26208,12 @@ TinyCloudNode.registerNodeDefaults({
25164
26208
  createSigner: (privateKey, chainId) => new PrivateKeySigner(privateKey, chainId)
25165
26209
  });
25166
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
+
25167
26217
  // src/index.ts
25168
26218
  import { TinyCloud as TinyCloud2 } from "@tinycloud/sdk-core";
25169
26219
  import {
@@ -25181,8 +26231,8 @@ import {
25181
26231
  isEvmAddress,
25182
26232
  makePkhSpaceId as makePkhSpaceId2,
25183
26233
  parsePkhDid,
25184
- pkhDid as pkhDid3,
25185
- principalDid,
26234
+ pkhDid as pkhDid4,
26235
+ principalDid as principalDid3,
25186
26236
  principalDidEquals as principalDidEquals4,
25187
26237
  parseCanonicalNetworkId,
25188
26238
  TinyCloudDebugLogger,
@@ -25376,7 +26426,7 @@ import {
25376
26426
  resolveManifest as resolveManifest2,
25377
26427
  validateManifest,
25378
26428
  loadManifest,
25379
- isCapabilitySubset as isCapabilitySubset2,
26429
+ isCapabilitySubset as isCapabilitySubset4,
25380
26430
  expandActionShortNames,
25381
26431
  expandPermissionEntries as expandPermissionEntries2,
25382
26432
  expandPermissionEntry,
@@ -25385,7 +26435,7 @@ import {
25385
26435
  } from "@tinycloud/sdk-core";
25386
26436
  import {
25387
26437
  DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
25388
- KVService as KVService3,
26438
+ KVService as KVService4,
25389
26439
  PrefixedKVService
25390
26440
  } from "@tinycloud/sdk-core";
25391
26441
  import { SQLService as SQLService3, SQLAction, DatabaseHandle } from "@tinycloud/sdk-core";
@@ -25402,6 +26452,10 @@ import {
25402
26452
  SecretsService as SecretsService2,
25403
26453
  SECRET_NAME_RE,
25404
26454
  canonicalizeSecretScope,
26455
+ appSecretScope,
26456
+ SECRET_SCOPE_RE,
26457
+ MAX_SECRET_SCOPE_LENGTH,
26458
+ TC832_RESERVED_SCOPES,
25405
26459
  parseSecretCatalogKey,
25406
26460
  resolveSecretListPrefix as resolveSecretListPrefix2,
25407
26461
  resolveSecretPath as resolveSecretPath3
@@ -25465,7 +26519,7 @@ import {
25465
26519
  SpaceService as SpaceService2,
25466
26520
  SpaceErrorCodes,
25467
26521
  createSpaceService,
25468
- parseSpaceUri,
26522
+ parseSpaceUri as parseSpaceUri3,
25469
26523
  buildSpaceUri,
25470
26524
  makePublicSpaceId as makePublicSpaceId2,
25471
26525
  Space
@@ -25476,7 +26530,849 @@ import {
25476
26530
  UnsupportedFeatureError as UnsupportedFeatureError2,
25477
26531
  checkNodeInfo as checkNodeInfo2
25478
26532
  } from "@tinycloud/sdk-core";
25479
- 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
+ }
25480
27376
  export {
25481
27377
  ACCEPTED_MARKER_VERSIONS,
25482
27378
  ACCOUNT_REGISTRY_PATH,
@@ -25514,7 +27410,8 @@ export {
25514
27410
  HooksService3 as HooksService,
25515
27411
  IdentityParseError,
25516
27412
  InvalidRestoredSessionError,
25517
- KVService3 as KVService,
27413
+ KVService4 as KVService,
27414
+ MAX_SECRET_SCOPE_LENGTH,
25518
27415
  ManifestValidationError,
25519
27416
  MemorySessionStorage,
25520
27417
  NETWORK_NAME_PATTERN,
@@ -25528,18 +27425,20 @@ export {
25528
27425
  RUNTIME_DELEGATION_REJECTIONS,
25529
27426
  RuntimeDelegationRejectedError,
25530
27427
  SECRET_NAME_RE,
27428
+ SECRET_SCOPE_RE,
25531
27429
  SQLAction,
25532
27430
  SQLService3 as SQLService,
25533
27431
  STORAGE_FULL_MESSAGE2 as STORAGE_FULL_MESSAGE,
25534
27432
  STORAGE_WRITE_TOO_LARGE_MESSAGE2 as STORAGE_WRITE_TOO_LARGE_MESSAGE,
25535
27433
  SecretsService2 as SecretsService,
25536
- ServiceContext3 as ServiceContext,
27434
+ ServiceContext4 as ServiceContext,
25537
27435
  SessionExpiredError2 as SessionExpiredError,
25538
27436
  SharingService2 as SharingService,
25539
27437
  SilentNotificationHandler2 as SilentNotificationHandler,
25540
27438
  Space,
25541
27439
  SpaceErrorCodes,
25542
27440
  SpaceService2 as SpaceService,
27441
+ TC832_RESERVED_SCOPES,
25543
27442
  TinyCloud2 as TinyCloud,
25544
27443
  TinyCloudDebugLogger,
25545
27444
  TinyCloudNode,
@@ -25553,6 +27452,7 @@ export {
25553
27452
  activateCompactRuntimeDelegation,
25554
27453
  activateValidatedRuntimeDelegation,
25555
27454
  addressStorageKey,
27455
+ appSecretScope,
25556
27456
  buildCanonicalDecryptRequest,
25557
27457
  buildDecryptAttenuation,
25558
27458
  buildDecryptFacts,
@@ -25604,7 +27504,7 @@ export {
25604
27504
  hexDecode,
25605
27505
  hexEncode,
25606
27506
  installTinyCloudDebugGlobals,
25607
- isCapabilitySubset2 as isCapabilitySubset,
27507
+ isCapabilitySubset4 as isCapabilitySubset,
25608
27508
  isEvmAddress,
25609
27509
  isNetworkId,
25610
27510
  isStorageFullError2 as isStorageFullError,
@@ -25618,9 +27518,9 @@ export {
25618
27518
  parseNetworkId3 as parseNetworkId,
25619
27519
  parsePkhDid,
25620
27520
  parseSecretCatalogKey,
25621
- parseSpaceUri,
25622
- pkhDid3 as pkhDid,
25623
- principalDid,
27521
+ parseSpaceUri3 as parseSpaceUri,
27522
+ pkhDid4 as pkhDid,
27523
+ principalDid3 as principalDid,
25624
27524
  principalDidEquals4 as principalDidEquals,
25625
27525
  resolveManifest2 as resolveManifest,
25626
27526
  resolveSecretListPrefix2 as resolveSecretListPrefix,
@@ -25628,6 +27528,7 @@ export {
25628
27528
  resourceCapabilitiesToSpaceAbilitiesMap2 as resourceCapabilitiesToSpaceAbilitiesMap,
25629
27529
  serializeDelegation,
25630
27530
  signedCompactUcanExpiry,
27531
+ sqliteReplicaStorage,
25631
27532
  tinyCloudDebugLogger,
25632
27533
  validateEnvelope,
25633
27534
  validateManifest,