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