@tinycloud/node-sdk 3.2.6-beta.6 → 3.3.0-beta.11
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/{WasmKeyProvider-DEdSyQ0I.d.cts → TinyCloudNode-CcNh20QX.d.cts} +181 -286
- package/dist/{WasmKeyProvider-DEdSyQ0I.d.ts → TinyCloudNode-CcNh20QX.d.ts} +181 -286
- package/dist/WasmKeyProvider-5Ee7fcAw.d.cts +217 -0
- package/dist/WasmKeyProvider-5Ee7fcAw.d.ts +217 -0
- package/dist/authority-Cwotwza3.d.cts +118 -0
- package/dist/authority-D_u9a7Wl.d.ts +118 -0
- package/dist/core.cjs +401 -168
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +37 -4
- package/dist/core.d.ts +37 -4
- package/dist/core.js +408 -176
- package/dist/core.js.map +1 -1
- package/dist/gated-network-BEF2XSTC.d.cts +16 -0
- package/dist/gated-network-BEF2XSTC.d.ts +16 -0
- package/dist/index.cjs +2735 -545
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -89
- package/dist/index.d.ts +35 -89
- package/dist/index.js +2730 -545
- package/dist/index.js.map +1 -1
- package/dist/replication/authority.cjs +153 -15
- package/dist/replication/authority.cjs.map +1 -1
- package/dist/replication/authority.d.cts +4 -1
- package/dist/replication/authority.d.ts +4 -1
- package/dist/replication/authority.js +147 -14
- package/dist/replication/authority.js.map +1 -1
- package/dist/replication/gated-network.cjs +90 -0
- package/dist/replication/gated-network.cjs.map +1 -0
- package/dist/replication/gated-network.d.cts +2 -0
- package/dist/replication/gated-network.d.ts +2 -0
- package/dist/replication/gated-network.js +65 -0
- package/dist/replication/gated-network.js.map +1 -0
- package/dist/replication/runtime.cjs +84 -6
- package/dist/replication/runtime.cjs.map +1 -1
- package/dist/replication/runtime.d.cts +2 -1
- package/dist/replication/runtime.d.ts +2 -1
- package/dist/replication/runtime.js +84 -6
- package/dist/replication/runtime.js.map +1 -1
- package/dist/{runtime-B5P5SRpC.d.ts → runtime-D1o9Eybt.d.cts} +14 -3
- package/dist/{runtime-B5P5SRpC.d.cts → runtime-v62FzQHz.d.ts} +14 -3
- package/package.json +14 -4
- package/dist/authority-DDGtccBK.d.cts +0 -96
- package/dist/authority-DDGtccBK.d.ts +0 -96
package/dist/index.cjs
CHANGED
|
@@ -4408,11 +4408,11 @@ var init_lib4 = __esm({
|
|
|
4408
4408
|
init_version4();
|
|
4409
4409
|
__awaiter = function(thisArg, _arguments, P, generator) {
|
|
4410
4410
|
function adopt(value) {
|
|
4411
|
-
return value instanceof P ? value : new P(function(
|
|
4412
|
-
|
|
4411
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
4412
|
+
resolve2(value);
|
|
4413
4413
|
});
|
|
4414
4414
|
}
|
|
4415
|
-
return new (P || (P = Promise))(function(
|
|
4415
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
4416
4416
|
function fulfilled(value) {
|
|
4417
4417
|
try {
|
|
4418
4418
|
step(generator.next(value));
|
|
@@ -4428,7 +4428,7 @@ var init_lib4 = __esm({
|
|
|
4428
4428
|
}
|
|
4429
4429
|
}
|
|
4430
4430
|
function step(result) {
|
|
4431
|
-
result.done ?
|
|
4431
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
4432
4432
|
}
|
|
4433
4433
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
4434
4434
|
});
|
|
@@ -8044,11 +8044,11 @@ var init_typed_data = __esm({
|
|
|
8044
8044
|
init_id();
|
|
8045
8045
|
__awaiter2 = function(thisArg, _arguments, P, generator) {
|
|
8046
8046
|
function adopt(value) {
|
|
8047
|
-
return value instanceof P ? value : new P(function(
|
|
8048
|
-
|
|
8047
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
8048
|
+
resolve2(value);
|
|
8049
8049
|
});
|
|
8050
8050
|
}
|
|
8051
|
-
return new (P || (P = Promise))(function(
|
|
8051
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
8052
8052
|
function fulfilled(value) {
|
|
8053
8053
|
try {
|
|
8054
8054
|
step(generator.next(value));
|
|
@@ -8064,7 +8064,7 @@ var init_typed_data = __esm({
|
|
|
8064
8064
|
}
|
|
8065
8065
|
}
|
|
8066
8066
|
function step(result) {
|
|
8067
|
-
result.done ?
|
|
8067
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
8068
8068
|
}
|
|
8069
8069
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8070
8070
|
});
|
|
@@ -14906,7 +14906,7 @@ var require_scrypt = __commonJS({
|
|
|
14906
14906
|
}
|
|
14907
14907
|
const lib = {
|
|
14908
14908
|
scrypt: function(password, salt, N2, r2, p, dkLen, progressCallback) {
|
|
14909
|
-
return new Promise(function(
|
|
14909
|
+
return new Promise(function(resolve2, reject2) {
|
|
14910
14910
|
let lastProgress = 0;
|
|
14911
14911
|
if (progressCallback) {
|
|
14912
14912
|
progressCallback(0);
|
|
@@ -14918,7 +14918,7 @@ var require_scrypt = __commonJS({
|
|
|
14918
14918
|
if (progressCallback && lastProgress !== 1) {
|
|
14919
14919
|
progressCallback(1);
|
|
14920
14920
|
}
|
|
14921
|
-
|
|
14921
|
+
resolve2(new Uint8Array(key2));
|
|
14922
14922
|
} else if (progressCallback && progress !== lastProgress) {
|
|
14923
14923
|
lastProgress = progress;
|
|
14924
14924
|
return progressCallback(progress);
|
|
@@ -15298,11 +15298,11 @@ var init_keystore = __esm({
|
|
|
15298
15298
|
init_version15();
|
|
15299
15299
|
__awaiter3 = function(thisArg, _arguments, P, generator) {
|
|
15300
15300
|
function adopt(value) {
|
|
15301
|
-
return value instanceof P ? value : new P(function(
|
|
15302
|
-
|
|
15301
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
15302
|
+
resolve2(value);
|
|
15303
15303
|
});
|
|
15304
15304
|
}
|
|
15305
|
-
return new (P || (P = Promise))(function(
|
|
15305
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
15306
15306
|
function fulfilled(value) {
|
|
15307
15307
|
try {
|
|
15308
15308
|
step(generator.next(value));
|
|
@@ -15318,7 +15318,7 @@ var init_keystore = __esm({
|
|
|
15318
15318
|
}
|
|
15319
15319
|
}
|
|
15320
15320
|
function step(result) {
|
|
15321
|
-
result.done ?
|
|
15321
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
15322
15322
|
}
|
|
15323
15323
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15324
15324
|
});
|
|
@@ -15611,11 +15611,11 @@ var init_lib25 = __esm({
|
|
|
15611
15611
|
init_version19();
|
|
15612
15612
|
__awaiter4 = function(thisArg, _arguments, P, generator) {
|
|
15613
15613
|
function adopt(value) {
|
|
15614
|
-
return value instanceof P ? value : new P(function(
|
|
15615
|
-
|
|
15614
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
15615
|
+
resolve2(value);
|
|
15616
15616
|
});
|
|
15617
15617
|
}
|
|
15618
|
-
return new (P || (P = Promise))(function(
|
|
15618
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
15619
15619
|
function fulfilled(value) {
|
|
15620
15620
|
try {
|
|
15621
15621
|
step(generator.next(value));
|
|
@@ -15631,7 +15631,7 @@ var init_lib25 = __esm({
|
|
|
15631
15631
|
}
|
|
15632
15632
|
}
|
|
15633
15633
|
function step(result) {
|
|
15634
|
-
result.done ?
|
|
15634
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
15635
15635
|
}
|
|
15636
15636
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15637
15637
|
});
|
|
@@ -15693,11 +15693,11 @@ var init_lib26 = __esm({
|
|
|
15693
15693
|
init_version20();
|
|
15694
15694
|
__awaiter5 = function(thisArg, _arguments, P, generator) {
|
|
15695
15695
|
function adopt(value) {
|
|
15696
|
-
return value instanceof P ? value : new P(function(
|
|
15697
|
-
|
|
15696
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
15697
|
+
resolve2(value);
|
|
15698
15698
|
});
|
|
15699
15699
|
}
|
|
15700
|
-
return new (P || (P = Promise))(function(
|
|
15700
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
15701
15701
|
function fulfilled(value) {
|
|
15702
15702
|
try {
|
|
15703
15703
|
step(generator.next(value));
|
|
@@ -15713,7 +15713,7 @@ var init_lib26 = __esm({
|
|
|
15713
15713
|
}
|
|
15714
15714
|
}
|
|
15715
15715
|
function step(result) {
|
|
15716
|
-
result.done ?
|
|
15716
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
15717
15717
|
}
|
|
15718
15718
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15719
15719
|
});
|
|
@@ -16015,11 +16015,11 @@ var init_lib27 = __esm({
|
|
|
16015
16015
|
init_version21();
|
|
16016
16016
|
__awaiter6 = function(thisArg, _arguments, P, generator) {
|
|
16017
16017
|
function adopt(value) {
|
|
16018
|
-
return value instanceof P ? value : new P(function(
|
|
16019
|
-
|
|
16018
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
16019
|
+
resolve2(value);
|
|
16020
16020
|
});
|
|
16021
16021
|
}
|
|
16022
|
-
return new (P || (P = Promise))(function(
|
|
16022
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
16023
16023
|
function fulfilled(value) {
|
|
16024
16024
|
try {
|
|
16025
16025
|
step(generator.next(value));
|
|
@@ -16035,7 +16035,7 @@ var init_lib27 = __esm({
|
|
|
16035
16035
|
}
|
|
16036
16036
|
}
|
|
16037
16037
|
function step(result) {
|
|
16038
|
-
result.done ?
|
|
16038
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
16039
16039
|
}
|
|
16040
16040
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16041
16041
|
});
|
|
@@ -16253,11 +16253,11 @@ var init_geturl = __esm({
|
|
|
16253
16253
|
init_lib2();
|
|
16254
16254
|
__awaiter7 = function(thisArg, _arguments, P, generator) {
|
|
16255
16255
|
function adopt(value) {
|
|
16256
|
-
return value instanceof P ? value : new P(function(
|
|
16257
|
-
|
|
16256
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
16257
|
+
resolve2(value);
|
|
16258
16258
|
});
|
|
16259
16259
|
}
|
|
16260
|
-
return new (P || (P = Promise))(function(
|
|
16260
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
16261
16261
|
function fulfilled(value) {
|
|
16262
16262
|
try {
|
|
16263
16263
|
step(generator.next(value));
|
|
@@ -16273,7 +16273,7 @@ var init_geturl = __esm({
|
|
|
16273
16273
|
}
|
|
16274
16274
|
}
|
|
16275
16275
|
function step(result) {
|
|
16276
|
-
result.done ?
|
|
16276
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
16277
16277
|
}
|
|
16278
16278
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16279
16279
|
});
|
|
@@ -16289,8 +16289,8 @@ __export(lib_exports21, {
|
|
|
16289
16289
|
poll: () => poll
|
|
16290
16290
|
});
|
|
16291
16291
|
function staller(duration) {
|
|
16292
|
-
return new Promise((
|
|
16293
|
-
setTimeout(
|
|
16292
|
+
return new Promise((resolve2) => {
|
|
16293
|
+
setTimeout(resolve2, duration);
|
|
16294
16294
|
});
|
|
16295
16295
|
}
|
|
16296
16296
|
function bodyify(value, type) {
|
|
@@ -16410,7 +16410,7 @@ function _fetchData(connection, body, processFunc) {
|
|
|
16410
16410
|
options.headers = flatHeaders;
|
|
16411
16411
|
const runningTimeout = (function() {
|
|
16412
16412
|
let timer = null;
|
|
16413
|
-
const promise = new Promise(function(
|
|
16413
|
+
const promise = new Promise(function(resolve2, reject2) {
|
|
16414
16414
|
if (timeout) {
|
|
16415
16415
|
timer = setTimeout(() => {
|
|
16416
16416
|
if (timer == null) {
|
|
@@ -16580,7 +16580,7 @@ function poll(func, options) {
|
|
|
16580
16580
|
if (options.interval == null) {
|
|
16581
16581
|
options.interval = 250;
|
|
16582
16582
|
}
|
|
16583
|
-
return new Promise(function(
|
|
16583
|
+
return new Promise(function(resolve2, reject2) {
|
|
16584
16584
|
let timer = null;
|
|
16585
16585
|
let done = false;
|
|
16586
16586
|
const cancel = () => {
|
|
@@ -16606,7 +16606,7 @@ function poll(func, options) {
|
|
|
16606
16606
|
return func().then(function(result) {
|
|
16607
16607
|
if (result !== void 0) {
|
|
16608
16608
|
if (cancel()) {
|
|
16609
|
-
|
|
16609
|
+
resolve2(result);
|
|
16610
16610
|
}
|
|
16611
16611
|
} else if (options.oncePoll) {
|
|
16612
16612
|
options.oncePoll.once("poll", check);
|
|
@@ -16652,11 +16652,11 @@ var init_lib28 = __esm({
|
|
|
16652
16652
|
init_geturl();
|
|
16653
16653
|
__awaiter8 = function(thisArg, _arguments, P, generator) {
|
|
16654
16654
|
function adopt(value) {
|
|
16655
|
-
return value instanceof P ? value : new P(function(
|
|
16656
|
-
|
|
16655
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
16656
|
+
resolve2(value);
|
|
16657
16657
|
});
|
|
16658
16658
|
}
|
|
16659
|
-
return new (P || (P = Promise))(function(
|
|
16659
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
16660
16660
|
function fulfilled(value) {
|
|
16661
16661
|
try {
|
|
16662
16662
|
step(generator.next(value));
|
|
@@ -16672,7 +16672,7 @@ var init_lib28 = __esm({
|
|
|
16672
16672
|
}
|
|
16673
16673
|
}
|
|
16674
16674
|
function step(result) {
|
|
16675
|
-
result.done ?
|
|
16675
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
16676
16676
|
}
|
|
16677
16677
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16678
16678
|
});
|
|
@@ -17030,45 +17030,6 @@ var require_utils2 = __commonJS({
|
|
|
17030
17030
|
}
|
|
17031
17031
|
});
|
|
17032
17032
|
|
|
17033
|
-
// src/replication/config.ts
|
|
17034
|
-
function assertValidReplicationConfig(replication) {
|
|
17035
|
-
if (replication === void 0 || replication.enabled !== true) return;
|
|
17036
|
-
if (replication.storage === void 0 || replication.storage === null) {
|
|
17037
|
-
throw new TypeError("replication.enabled requires replication.storage");
|
|
17038
|
-
}
|
|
17039
|
-
if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
|
|
17040
|
-
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
17041
|
-
}
|
|
17042
|
-
for (const prefix of replication.prefixes) {
|
|
17043
|
-
try {
|
|
17044
|
-
(0, import_prefix.assertFolderPrefix)(prefix);
|
|
17045
|
-
} catch (error) {
|
|
17046
|
-
throw new TypeError(error instanceof Error ? error.message : String(error));
|
|
17047
|
-
}
|
|
17048
|
-
}
|
|
17049
|
-
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
17050
|
-
for (const other of replication.prefixes.slice(index + 1)) {
|
|
17051
|
-
if ((0, import_prefix.kvPrefixCovers)(prefix, other) || (0, import_prefix.kvPrefixCovers)(other, prefix)) {
|
|
17052
|
-
throw new TypeError(
|
|
17053
|
-
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
17054
|
-
);
|
|
17055
|
-
}
|
|
17056
|
-
}
|
|
17057
|
-
if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
|
|
17058
|
-
throw new TypeError(
|
|
17059
|
-
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
17060
|
-
);
|
|
17061
|
-
}
|
|
17062
|
-
}
|
|
17063
|
-
}
|
|
17064
|
-
var import_prefix;
|
|
17065
|
-
var init_config = __esm({
|
|
17066
|
-
"src/replication/config.ts"() {
|
|
17067
|
-
"use strict";
|
|
17068
|
-
import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
|
|
17069
|
-
}
|
|
17070
|
-
});
|
|
17071
|
-
|
|
17072
17033
|
// src/replication/authority-sign-in.ts
|
|
17073
17034
|
function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
|
|
17074
17035
|
return (0, import_sdk_core2.isCapabilitySubset)(
|
|
@@ -17082,7 +17043,7 @@ function augmentSignInEntriesWithReplication(input) {
|
|
|
17082
17043
|
if (replication === void 0) return out;
|
|
17083
17044
|
const eligiblePaths = [];
|
|
17084
17045
|
for (const prefix of replication.prefixes) {
|
|
17085
|
-
(0,
|
|
17046
|
+
(0, import_prefix.assertFolderPrefix)(prefix);
|
|
17086
17047
|
const path = prefix === "/" ? "" : prefix;
|
|
17087
17048
|
if ((0, import_sdk_services.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
|
|
17088
17049
|
continue;
|
|
@@ -17110,19 +17071,63 @@ function augmentSignInEntriesWithReplication(input) {
|
|
|
17110
17071
|
}
|
|
17111
17072
|
return out;
|
|
17112
17073
|
}
|
|
17113
|
-
var import_sdk_core2, import_sdk_services,
|
|
17074
|
+
var import_sdk_core2, import_sdk_services, import_prefix, KV_GET, KV_SYNC, KV_SERVICE;
|
|
17114
17075
|
var init_authority_sign_in = __esm({
|
|
17115
17076
|
"src/replication/authority-sign-in.ts"() {
|
|
17116
17077
|
"use strict";
|
|
17117
17078
|
import_sdk_core2 = require("@tinycloud/sdk-core");
|
|
17118
17079
|
import_sdk_services = require("@tinycloud/sdk-services");
|
|
17119
|
-
|
|
17080
|
+
import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
|
|
17120
17081
|
KV_GET = import_sdk_core2.KV.GET;
|
|
17121
17082
|
KV_SYNC = import_sdk_core2.KV.SYNC;
|
|
17122
17083
|
KV_SERVICE = "tinycloud.kv";
|
|
17123
17084
|
}
|
|
17124
17085
|
});
|
|
17125
17086
|
|
|
17087
|
+
// src/replication/config.ts
|
|
17088
|
+
function assertValidReplicationConfig(replication) {
|
|
17089
|
+
if (replication === void 0 || !replication.enabled) return;
|
|
17090
|
+
if (replication.storage === void 0 || replication.storage === null) {
|
|
17091
|
+
throw new TypeError("replication.enabled requires replication.storage");
|
|
17092
|
+
}
|
|
17093
|
+
if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
|
|
17094
|
+
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
17095
|
+
}
|
|
17096
|
+
const grantTtlMs = replication.grantTtlMs;
|
|
17097
|
+
if (grantTtlMs !== void 0 && (!Number.isFinite(grantTtlMs) || grantTtlMs <= 0 || grantTtlMs > import_grantPolicyConstants.MAX_GRANT_TTL_MS)) {
|
|
17098
|
+
throw new TypeError(`replication.grantTtlMs must be a positive number of milliseconds no greater than ${import_grantPolicyConstants.MAX_GRANT_TTL_MS}`);
|
|
17099
|
+
}
|
|
17100
|
+
for (const prefix of replication.prefixes) {
|
|
17101
|
+
try {
|
|
17102
|
+
(0, import_prefix2.assertFolderPrefix)(prefix);
|
|
17103
|
+
} catch (error) {
|
|
17104
|
+
throw new TypeError(error instanceof Error ? error.message : String(error));
|
|
17105
|
+
}
|
|
17106
|
+
}
|
|
17107
|
+
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
17108
|
+
for (const other of replication.prefixes.slice(index + 1)) {
|
|
17109
|
+
if ((0, import_prefix2.kvPrefixCovers)(prefix, other) || (0, import_prefix2.kvPrefixCovers)(other, prefix)) {
|
|
17110
|
+
throw new TypeError(
|
|
17111
|
+
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
17112
|
+
);
|
|
17113
|
+
}
|
|
17114
|
+
}
|
|
17115
|
+
if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
|
|
17116
|
+
throw new TypeError(
|
|
17117
|
+
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
17118
|
+
);
|
|
17119
|
+
}
|
|
17120
|
+
}
|
|
17121
|
+
}
|
|
17122
|
+
var import_grantPolicyConstants, import_prefix2;
|
|
17123
|
+
var init_config = __esm({
|
|
17124
|
+
"src/replication/config.ts"() {
|
|
17125
|
+
"use strict";
|
|
17126
|
+
import_grantPolicyConstants = require("@tinycloud/sdk-services/kv/replication/grantPolicyConstants");
|
|
17127
|
+
import_prefix2 = require("@tinycloud/sdk-services/kv/replication/prefix");
|
|
17128
|
+
}
|
|
17129
|
+
});
|
|
17130
|
+
|
|
17126
17131
|
// src/replication/runtime.ts
|
|
17127
17132
|
var runtime_exports = {};
|
|
17128
17133
|
__export(runtime_exports, {
|
|
@@ -17166,10 +17171,23 @@ var init_runtime = __esm({
|
|
|
17166
17171
|
this.control = {
|
|
17167
17172
|
status: async () => {
|
|
17168
17173
|
await this.binding;
|
|
17174
|
+
if (this.gatedReason) return this.options.prefixes.map((prefix) => ({
|
|
17175
|
+
prefix,
|
|
17176
|
+
state: "gated",
|
|
17177
|
+
gate: this.gatedReason,
|
|
17178
|
+
errorCode: "REPLICA_GATED",
|
|
17179
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
17180
|
+
pinned: [],
|
|
17181
|
+
lagMs: null
|
|
17182
|
+
}));
|
|
17169
17183
|
return this.current ? this.current.status() : [];
|
|
17170
17184
|
},
|
|
17171
17185
|
sync: async (input) => {
|
|
17172
17186
|
await this.binding;
|
|
17187
|
+
if (this.gatedReason) throw Object.assign(new Error("Replica restore is gated."), {
|
|
17188
|
+
code: "REPLICA_GATED",
|
|
17189
|
+
gate: this.gatedReason
|
|
17190
|
+
});
|
|
17173
17191
|
if (this.purgedGeneration === this.generation) return;
|
|
17174
17192
|
if (this.current) await this.current.sync(input);
|
|
17175
17193
|
},
|
|
@@ -17217,11 +17235,59 @@ var init_runtime = __esm({
|
|
|
17217
17235
|
for (const kv of this.attached.keys()) kv.setReadThrough(null);
|
|
17218
17236
|
this.attached.clear();
|
|
17219
17237
|
}
|
|
17238
|
+
/** Observe a refused restore without opening replica state; loader failure leaves raw network KV usable. */
|
|
17239
|
+
bindGatedNetworkOnly(input, loadGatedNetwork) {
|
|
17240
|
+
if (this.closed || !this.options.enabled) return Promise.resolve();
|
|
17241
|
+
const retired = this.unbind();
|
|
17242
|
+
const generation = this.generation;
|
|
17243
|
+
this.purgedGeneration = void 0;
|
|
17244
|
+
this.pendingPurge = void 0;
|
|
17245
|
+
this.pendingPurgeResult = void 0;
|
|
17246
|
+
this.pending.clear();
|
|
17247
|
+
this.lastController = void 0;
|
|
17248
|
+
this.lastPending = void 0;
|
|
17249
|
+
this.lastIdentity = void 0;
|
|
17250
|
+
this.gatedReason = input.gate;
|
|
17251
|
+
this.primarySpace = input.session.spaceId;
|
|
17252
|
+
this.binding = retired.then(async () => {
|
|
17253
|
+
let create;
|
|
17254
|
+
try {
|
|
17255
|
+
({ createGatedNetworkReadThrough: create } = await loadGatedNetwork());
|
|
17256
|
+
} catch {
|
|
17257
|
+
return;
|
|
17258
|
+
}
|
|
17259
|
+
if (generation !== this.generation || this.closed || typeof create !== "function") return;
|
|
17260
|
+
const networkOnly = create({
|
|
17261
|
+
space: input.session.spaceId,
|
|
17262
|
+
prefixes: this.options.prefixes,
|
|
17263
|
+
now: () => this.clock.now(),
|
|
17264
|
+
emit: (event) => {
|
|
17265
|
+
if (generation !== this.generation || this.closed || this.gated !== networkOnly) return;
|
|
17266
|
+
try {
|
|
17267
|
+
this.options.onEvent?.(event);
|
|
17268
|
+
} catch {
|
|
17269
|
+
}
|
|
17270
|
+
const { type, ...data } = event;
|
|
17271
|
+
try {
|
|
17272
|
+
input.context.emit(type, data);
|
|
17273
|
+
} catch {
|
|
17274
|
+
}
|
|
17275
|
+
}
|
|
17276
|
+
});
|
|
17277
|
+
this.gated = networkOnly;
|
|
17278
|
+
for (const scoped of input.primaryKV) this.attach(scoped.space, scoped.kv);
|
|
17279
|
+
});
|
|
17280
|
+
return this.binding;
|
|
17281
|
+
}
|
|
17220
17282
|
bind(input) {
|
|
17221
17283
|
if (this.closed || !this.options.enabled) return Promise.resolve();
|
|
17284
|
+
this.gated = void 0;
|
|
17285
|
+
this.gatedReason = void 0;
|
|
17222
17286
|
this.bindingActive = true;
|
|
17223
17287
|
const generation = ++this.generation;
|
|
17224
17288
|
this.purgedGeneration = void 0;
|
|
17289
|
+
this.unsubscribeConsentExpired?.();
|
|
17290
|
+
this.unsubscribeConsentExpired = void 0;
|
|
17225
17291
|
const { context, session } = input;
|
|
17226
17292
|
const emit = (event) => {
|
|
17227
17293
|
try {
|
|
@@ -17254,11 +17320,17 @@ var init_runtime = __esm({
|
|
|
17254
17320
|
return;
|
|
17255
17321
|
}
|
|
17256
17322
|
const key2 = services.replicationIdentityKey(identity);
|
|
17257
|
-
let
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
this.
|
|
17323
|
+
let cached = this.pending.get(key2);
|
|
17324
|
+
const stamp = input.stamp;
|
|
17325
|
+
if (!cached || cached.stamp?.incarnation !== stamp?.incarnation || cached.stamp?.lifecycleEpoch !== stamp?.lifecycleEpoch) {
|
|
17326
|
+
const store = this.options.storage.pendingWrites?.(identity, { stamp });
|
|
17327
|
+
if (!store && this.options.storage.lifecycle) {
|
|
17328
|
+
throw Object.assign(new Error("Gated replication requires a stamped pending store"), { code: "REPLICA_GATED", gate: "gate_unavailable" });
|
|
17329
|
+
}
|
|
17330
|
+
cached = { stamp, store: store ?? services.createMemoryPendingStore(identity) };
|
|
17331
|
+
this.pending.set(key2, cached);
|
|
17261
17332
|
}
|
|
17333
|
+
const pending = cached.store;
|
|
17262
17334
|
this.lastPending = pending;
|
|
17263
17335
|
const controller = services.createKVReplication({
|
|
17264
17336
|
options: resolvedOptions(this.options),
|
|
@@ -17266,6 +17338,7 @@ var init_runtime = __esm({
|
|
|
17266
17338
|
storage: this.options.storage,
|
|
17267
17339
|
identity,
|
|
17268
17340
|
session: { id: session.delegationCid, did: session.verificationMethod, space: session.spaceId },
|
|
17341
|
+
stamp: input.stamp,
|
|
17269
17342
|
authority: input.authority,
|
|
17270
17343
|
pending,
|
|
17271
17344
|
scheduler: this.clock,
|
|
@@ -17275,6 +17348,9 @@ var init_runtime = __esm({
|
|
|
17275
17348
|
previous: previousClose
|
|
17276
17349
|
});
|
|
17277
17350
|
this.current = controller;
|
|
17351
|
+
this.unsubscribeConsentExpired = this.options.storage.lifecycle?.subscribeConsentExpired?.(
|
|
17352
|
+
(stamp2) => controller.observeConsentExpired(stamp2)
|
|
17353
|
+
);
|
|
17278
17354
|
this.lastController = controller;
|
|
17279
17355
|
this.lastIdentity = identity;
|
|
17280
17356
|
for (const [kv, space] of this.attached) {
|
|
@@ -17295,17 +17371,24 @@ var init_runtime = __esm({
|
|
|
17295
17371
|
}
|
|
17296
17372
|
attach(space, kv) {
|
|
17297
17373
|
this.attached.set(kv, space);
|
|
17298
|
-
kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
|
|
17374
|
+
kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
|
|
17375
|
+
const generation = this.generation;
|
|
17299
17376
|
return this.binding.then(() => {
|
|
17300
|
-
|
|
17377
|
+
if (generation === this.generation && !this.closed && this.attached.get(kv) === space) {
|
|
17378
|
+
kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
|
|
17379
|
+
}
|
|
17301
17380
|
});
|
|
17302
17381
|
}
|
|
17303
17382
|
unbind() {
|
|
17304
17383
|
this.generation++;
|
|
17305
17384
|
this.bindingActive = false;
|
|
17306
17385
|
this.pendingPurgeSet = false;
|
|
17386
|
+
this.unsubscribeConsentExpired?.();
|
|
17387
|
+
this.unsubscribeConsentExpired = void 0;
|
|
17307
17388
|
const controller = this.current;
|
|
17308
17389
|
this.current = void 0;
|
|
17390
|
+
this.gated = void 0;
|
|
17391
|
+
this.gatedReason = void 0;
|
|
17309
17392
|
this.primarySpace = void 0;
|
|
17310
17393
|
this.detachAll();
|
|
17311
17394
|
const binding = this.binding;
|
|
@@ -17324,14 +17407,94 @@ var init_runtime = __esm({
|
|
|
17324
17407
|
var authority_exports = {};
|
|
17325
17408
|
__export(authority_exports, {
|
|
17326
17409
|
AUTHORITY_EXPIRY_MARGIN_MS: () => AUTHORITY_EXPIRY_MARGIN_MS,
|
|
17410
|
+
assertRenewalParent: () => assertRenewalParent,
|
|
17327
17411
|
assertValidReplicationConfig: () => assertValidReplicationConfig,
|
|
17328
17412
|
augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
|
|
17329
17413
|
createReplicationAuthority: () => createReplicationAuthority,
|
|
17414
|
+
establishReplicaSessionStamp: () => establishReplicaSessionStamp,
|
|
17330
17415
|
hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
|
|
17416
|
+
mintNodeReplicationGrant: () => mintNodeReplicationGrant,
|
|
17331
17417
|
replicationScopeFromSignedSession: () => replicationScopeFromSignedSession,
|
|
17418
|
+
reportReplicaDenial: () => reportReplicaDenial,
|
|
17419
|
+
restoreReplicaSessionStamp: () => restoreReplicaSessionStamp,
|
|
17332
17420
|
ucanAttCovers: () => ucanAttCovers,
|
|
17333
|
-
ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
|
|
17421
|
+
ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor,
|
|
17422
|
+
verifiedOwnerConsentAnchor: () => verifiedOwnerConsentAnchor
|
|
17334
17423
|
});
|
|
17424
|
+
function reportReplicaDenial(result, reason, onEvent, context) {
|
|
17425
|
+
if (result.status !== "denied" && result.status !== "deleted" && result.status !== "marker") return;
|
|
17426
|
+
const event = {
|
|
17427
|
+
type: "replication.lifecycle",
|
|
17428
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17429
|
+
reason,
|
|
17430
|
+
action: result.status === "denied" ? "denied" : result.status === "deleted" ? "database_deleted" : "marker_set",
|
|
17431
|
+
...result.status === "denied" ? { partitions: result.intents } : {},
|
|
17432
|
+
...result.status === "marker" ? { code: result.code } : {}
|
|
17433
|
+
};
|
|
17434
|
+
try {
|
|
17435
|
+
onEvent?.(event);
|
|
17436
|
+
} catch {
|
|
17437
|
+
}
|
|
17438
|
+
const { type, ...data } = event;
|
|
17439
|
+
try {
|
|
17440
|
+
context?.emit(type, data);
|
|
17441
|
+
} catch {
|
|
17442
|
+
}
|
|
17443
|
+
}
|
|
17444
|
+
function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
|
|
17445
|
+
if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
|
|
17446
|
+
try {
|
|
17447
|
+
const verified = wasmBindings.validatePersistedSession?.({
|
|
17448
|
+
delegationHeader: session.delegationHeader,
|
|
17449
|
+
delegationCid: session.delegationCid,
|
|
17450
|
+
spaceId: session.spaceId,
|
|
17451
|
+
jwk: session.jwk,
|
|
17452
|
+
address: session.address,
|
|
17453
|
+
chainId: session.chainId,
|
|
17454
|
+
siwe: session.siwe,
|
|
17455
|
+
signature: session.signature
|
|
17456
|
+
});
|
|
17457
|
+
if (!verified || !Number.isSafeInteger(Date.parse(verified.expiresAt ?? ""))) return null;
|
|
17458
|
+
const signedIssuedAt = new import_siwe3.SiweMessage(session.siwe).issuedAt;
|
|
17459
|
+
if (typeof signedIssuedAt !== "string") return null;
|
|
17460
|
+
const issuedAt = Date.parse(signedIssuedAt);
|
|
17461
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 0) return null;
|
|
17462
|
+
const granted = exactVerifiedRecap2(verified).filter((entry) => entry.space === session.spaceId && entry.service === "kv").map((entry) => ({ ...entry, service: "tinycloud.kv" }));
|
|
17463
|
+
const coversGetAndSync = granted.some((entry) => (0, import_sdk_core11.isCapabilitySubset)([{
|
|
17464
|
+
service: "tinycloud.kv",
|
|
17465
|
+
space: session.spaceId,
|
|
17466
|
+
path: entry.path,
|
|
17467
|
+
actions: [import_sdk_core11.KV.GET, import_sdk_core11.KV.SYNC]
|
|
17468
|
+
}], granted).subset);
|
|
17469
|
+
return coversGetAndSync ? { at: issuedAt, source: "siwe" } : null;
|
|
17470
|
+
} catch {
|
|
17471
|
+
return null;
|
|
17472
|
+
}
|
|
17473
|
+
}
|
|
17474
|
+
function establishReplicaSessionStamp(storage, principal, anchor) {
|
|
17475
|
+
if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
|
|
17476
|
+
return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
|
|
17477
|
+
}
|
|
17478
|
+
if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
|
|
17479
|
+
return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
|
|
17480
|
+
}
|
|
17481
|
+
throw Object.assign(new Error("Replica sign-in requires its platform lifecycle gate."), {
|
|
17482
|
+
code: "REPLICA_GATED",
|
|
17483
|
+
gate: "gate_unavailable"
|
|
17484
|
+
});
|
|
17485
|
+
}
|
|
17486
|
+
function restoreReplicaSessionStamp(storage, principal, persisted) {
|
|
17487
|
+
if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
|
|
17488
|
+
return storage.lifecycle.restoreSessionStamp(principal, persisted);
|
|
17489
|
+
}
|
|
17490
|
+
if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
|
|
17491
|
+
return storage.lifecycle.restoreSessionStamp(principal, persisted);
|
|
17492
|
+
}
|
|
17493
|
+
throw Object.assign(new Error("Replica restore requires its platform lifecycle gate."), {
|
|
17494
|
+
code: "REPLICA_GATED",
|
|
17495
|
+
gate: "gate_unavailable"
|
|
17496
|
+
});
|
|
17497
|
+
}
|
|
17335
17498
|
function replicationEntries(spaceId, prefix) {
|
|
17336
17499
|
return [{
|
|
17337
17500
|
service: KV_SERVICE2,
|
|
@@ -17379,6 +17542,20 @@ function ucanAttCovers(att, space, prefix, ability, options) {
|
|
|
17379
17542
|
function ucanAttUnconstrainedFor(att, space, prefix) {
|
|
17380
17543
|
return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
|
|
17381
17544
|
}
|
|
17545
|
+
async function mintNodeReplicationGrant(deviceDid, entries, options, renewalSession, node) {
|
|
17546
|
+
if (options?.renewalOnly && !renewalSession) throw new Error("replication renewal lost its owner session");
|
|
17547
|
+
const delegateOptions = options?.renewalOnly ? {
|
|
17548
|
+
expiry: options.expiryMs,
|
|
17549
|
+
onPrepared: async (prepared) => {
|
|
17550
|
+
assertRenewalParent(prepared.delegationHeader.Authorization, renewalSession);
|
|
17551
|
+
}
|
|
17552
|
+
} : options?.expiryMs === void 0 ? void 0 : { expiry: options.expiryMs };
|
|
17553
|
+
const result = await node.delegateTo((0, import_sdk_core11.principalDid)(deviceDid), entries, delegateOptions);
|
|
17554
|
+
return {
|
|
17555
|
+
ucan: result.delegation.delegationHeader.Authorization,
|
|
17556
|
+
expiresAt: result.delegation.expiry.getTime()
|
|
17557
|
+
};
|
|
17558
|
+
}
|
|
17382
17559
|
function replicationScopeFromSignedSession(input) {
|
|
17383
17560
|
let grant;
|
|
17384
17561
|
try {
|
|
@@ -17396,9 +17573,28 @@ function replicationScopeFromSignedSession(input) {
|
|
|
17396
17573
|
}
|
|
17397
17574
|
return void 0;
|
|
17398
17575
|
}
|
|
17576
|
+
function assertRenewalParent(authorization, session) {
|
|
17577
|
+
const grant = parseUcanGrant(authorization);
|
|
17578
|
+
if (grant.issuer !== session.verificationMethod.split("#", 1)[0] || grant.prf.length !== 1 || grant.prf[0] !== session.delegationCid) {
|
|
17579
|
+
throw Object.assign(new Error("replication renewal requires the bound owner session parent"), { refused: "NOT_COVERED" });
|
|
17580
|
+
}
|
|
17581
|
+
}
|
|
17399
17582
|
function createReplicationAuthority(host) {
|
|
17400
|
-
const
|
|
17401
|
-
|
|
17583
|
+
const boundSession = host.replicationSession();
|
|
17584
|
+
let verifiedAnchor = null;
|
|
17585
|
+
try {
|
|
17586
|
+
const candidate = boundSession && host.verifiedConsentAnchor?.(boundSession);
|
|
17587
|
+
if (candidate && Number.isFinite(candidate.at) && candidate.source !== "first_install") {
|
|
17588
|
+
verifiedAnchor = { at: candidate.at, source: candidate.source };
|
|
17589
|
+
}
|
|
17590
|
+
} catch {
|
|
17591
|
+
}
|
|
17592
|
+
const activeSession = () => {
|
|
17593
|
+
const session = host.replicationSession();
|
|
17594
|
+
return session !== void 0 && session === boundSession ? session : void 0;
|
|
17595
|
+
};
|
|
17596
|
+
const planOrRefusal = (entries, expiryMs) => {
|
|
17597
|
+
const plan = host.planDelegation(entries, expiryMs === void 0 ? void 0 : { expiry: expiryMs });
|
|
17402
17598
|
if ("refused" in plan) return plan;
|
|
17403
17599
|
return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
|
|
17404
17600
|
};
|
|
@@ -17410,12 +17606,20 @@ function createReplicationAuthority(host) {
|
|
|
17410
17606
|
};
|
|
17411
17607
|
const authority = {
|
|
17412
17608
|
get sessionOnly() {
|
|
17413
|
-
const session =
|
|
17414
|
-
return session
|
|
17609
|
+
const session = activeSession();
|
|
17610
|
+
return session === void 0 || session.siwe === void 0;
|
|
17611
|
+
},
|
|
17612
|
+
consentAnchor() {
|
|
17613
|
+
return activeSession() ? verifiedAnchor : null;
|
|
17614
|
+
},
|
|
17615
|
+
get canRenew() {
|
|
17616
|
+
const session = activeSession();
|
|
17617
|
+
return verifiedAnchor?.source === "siwe" && session !== void 0 && session.siwe !== void 0;
|
|
17415
17618
|
},
|
|
17416
17619
|
sessionGrant(prefix) {
|
|
17417
|
-
const session =
|
|
17620
|
+
const session = activeSession();
|
|
17418
17621
|
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
17622
|
+
if (verifiedAnchor === null && session.siwe !== void 0) return { refused: "NOT_COVERED" };
|
|
17419
17623
|
const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
|
|
17420
17624
|
if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
|
|
17421
17625
|
return { refused: "SESSION_EXPIRING" };
|
|
@@ -17443,8 +17647,9 @@ function createReplicationAuthority(host) {
|
|
|
17443
17647
|
};
|
|
17444
17648
|
},
|
|
17445
17649
|
plan(prefix) {
|
|
17446
|
-
const session =
|
|
17650
|
+
const session = activeSession();
|
|
17447
17651
|
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
17652
|
+
if (verifiedAnchor === null) return { refused: "NOT_COVERED" };
|
|
17448
17653
|
const sessionGrant = this.sessionGrant(prefix);
|
|
17449
17654
|
if (!("refused" in sessionGrant)) {
|
|
17450
17655
|
const signed2 = signedSessionGrant(session);
|
|
@@ -17458,26 +17663,30 @@ function createReplicationAuthority(host) {
|
|
|
17458
17663
|
if (authority.sessionOnly) return sessionGrant;
|
|
17459
17664
|
return planOrRefusal(replicationEntries(session.spaceId, prefix));
|
|
17460
17665
|
},
|
|
17461
|
-
async mint(deviceDid, prefix, signal) {
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
if (session === void 0) throw new Error("
|
|
17666
|
+
async mint(deviceDid, prefix, signal, options) {
|
|
17667
|
+
const session = activeSession();
|
|
17668
|
+
if (session === void 0) throw new Error("replication mint without the bound session");
|
|
17669
|
+
if (session.siwe === void 0) throw new Error("compact delegate sessions cannot mint device grants");
|
|
17670
|
+
if (verifiedAnchor?.source !== "siwe") throw new Error("replication mint requires verified owner consent");
|
|
17465
17671
|
const entries = replicationEntries(session.spaceId, prefix);
|
|
17466
|
-
const plan = planOrRefusal(entries);
|
|
17672
|
+
const plan = planOrRefusal(entries, options?.expiryMs);
|
|
17467
17673
|
if ("refused" in plan) {
|
|
17468
17674
|
throw Object.assign(
|
|
17469
17675
|
new Error(`replication authority refused the mint: ${plan.refused}`),
|
|
17470
17676
|
{ refused: plan.refused }
|
|
17471
17677
|
);
|
|
17472
17678
|
}
|
|
17679
|
+
if (options?.renewalOnly && plan.path !== "session") {
|
|
17680
|
+
throw Object.assign(new Error("replication renewal requires a session parent"), { refused: "NOT_COVERED" });
|
|
17681
|
+
}
|
|
17473
17682
|
signal.throwIfAborted();
|
|
17474
|
-
const minted = await new Promise((
|
|
17683
|
+
const minted = await new Promise((resolve2, reject2) => {
|
|
17475
17684
|
const onAbort = () => reject2(signal.reason ?? new Error("replication mint aborted"));
|
|
17476
17685
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
17477
|
-
host.mintDelegation(deviceDid, entries).then(
|
|
17686
|
+
host.mintDelegation(deviceDid, entries, options).then(
|
|
17478
17687
|
(result) => {
|
|
17479
17688
|
signal.removeEventListener("abort", onAbort);
|
|
17480
|
-
|
|
17689
|
+
resolve2(result);
|
|
17481
17690
|
},
|
|
17482
17691
|
(error) => {
|
|
17483
17692
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -17490,10 +17699,11 @@ function createReplicationAuthority(host) {
|
|
|
17490
17699
|
};
|
|
17491
17700
|
return authority;
|
|
17492
17701
|
}
|
|
17493
|
-
var import_sdk_core11, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
|
|
17702
|
+
var import_siwe3, import_sdk_core11, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
|
|
17494
17703
|
var init_authority = __esm({
|
|
17495
17704
|
"src/replication/authority.ts"() {
|
|
17496
17705
|
"use strict";
|
|
17706
|
+
import_siwe3 = require("siwe");
|
|
17497
17707
|
import_sdk_core11 = require("@tinycloud/sdk-core");
|
|
17498
17708
|
import_replica = require("@tinycloud/replica");
|
|
17499
17709
|
init_config();
|
|
@@ -17506,6 +17716,78 @@ var init_authority = __esm({
|
|
|
17506
17716
|
}
|
|
17507
17717
|
});
|
|
17508
17718
|
|
|
17719
|
+
// src/replication/gated-network.ts
|
|
17720
|
+
var gated_network_exports = {};
|
|
17721
|
+
__export(gated_network_exports, {
|
|
17722
|
+
createGatedNetworkReadThrough: () => createGatedNetworkReadThrough
|
|
17723
|
+
});
|
|
17724
|
+
function createGatedNetworkReadThrough(input) {
|
|
17725
|
+
const report = (op, space, path, replica, reason, outcome, latencyMs) => input.emit({
|
|
17726
|
+
type: "replication.read",
|
|
17727
|
+
at: new Date(input.now()).toISOString(),
|
|
17728
|
+
op,
|
|
17729
|
+
space,
|
|
17730
|
+
key: path,
|
|
17731
|
+
replica,
|
|
17732
|
+
source: reason === "cursor_restart" ? "none" : "network",
|
|
17733
|
+
reason,
|
|
17734
|
+
outcome,
|
|
17735
|
+
latencyMs,
|
|
17736
|
+
stalenessMs: null,
|
|
17737
|
+
coverage: null,
|
|
17738
|
+
authority: null,
|
|
17739
|
+
...reason === "replica_gated" ? { code: "REPLICA_GATED" } : {}
|
|
17740
|
+
});
|
|
17741
|
+
const readNetwork = async (r2, op, path, prefix, reason) => {
|
|
17742
|
+
const started = input.now();
|
|
17743
|
+
let outcome = "error";
|
|
17744
|
+
try {
|
|
17745
|
+
const result = await r2.network();
|
|
17746
|
+
outcome = result.ok ? "found" : op === "get" && reason === "not_covered" && "error" in result && result.error.code === "KV_NOT_FOUND" ? "not_found" : "error";
|
|
17747
|
+
return result;
|
|
17748
|
+
} finally {
|
|
17749
|
+
report(op, r2.space, path, prefix, reason, outcome, input.now() - started);
|
|
17750
|
+
}
|
|
17751
|
+
};
|
|
17752
|
+
const covered = (path) => input.prefixes.find((prefix) => (0, import_prefix3.kvPrefixCovers)(prefix, path));
|
|
17753
|
+
return {
|
|
17754
|
+
get: (r2) => {
|
|
17755
|
+
if (r2.space !== input.space) return r2.network();
|
|
17756
|
+
if (r2.options?.source === "network") return readNetwork(r2, "get", r2.path, null, "NETWORK_REQUESTED");
|
|
17757
|
+
const prefix = covered(r2.path);
|
|
17758
|
+
return readNetwork(r2, "get", r2.path, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
|
|
17759
|
+
},
|
|
17760
|
+
list: (r2) => {
|
|
17761
|
+
if (r2.space !== input.space) return r2.network();
|
|
17762
|
+
if (r2.options?.source === "network") return readNetwork(r2, "list", r2.listPath, null, "NETWORK_REQUESTED");
|
|
17763
|
+
if (r2.options?.cursor?.startsWith("tcr1.")) {
|
|
17764
|
+
report("list", r2.space, r2.listPath, null, "cursor_restart", "error", 0);
|
|
17765
|
+
return Promise.resolve({ ok: false, error: {
|
|
17766
|
+
code: "INVALID_INPUT",
|
|
17767
|
+
message: "Local list cursor cannot be served; restart without the cursor",
|
|
17768
|
+
service: "kv",
|
|
17769
|
+
meta: { replication: "cursor_restart" }
|
|
17770
|
+
} });
|
|
17771
|
+
}
|
|
17772
|
+
if (r2.options?.cursor) return readNetwork(r2, "list", r2.listPath, null, "network_cursor");
|
|
17773
|
+
if (r2.options?.raw) return readNetwork(r2, "list", r2.listPath, null, "unsupported_option");
|
|
17774
|
+
const prefix = covered(r2.listPath);
|
|
17775
|
+
return readNetwork(r2, "list", r2.listPath, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
|
|
17776
|
+
},
|
|
17777
|
+
write: (r2) => r2.network(),
|
|
17778
|
+
observeNetworkRequested: (r2) => {
|
|
17779
|
+
if (r2.space === input.space) report(r2.op, r2.space, r2.path, null, r2.reason, r2.outcome, r2.latencyMs);
|
|
17780
|
+
}
|
|
17781
|
+
};
|
|
17782
|
+
}
|
|
17783
|
+
var import_prefix3;
|
|
17784
|
+
var init_gated_network = __esm({
|
|
17785
|
+
"src/replication/gated-network.ts"() {
|
|
17786
|
+
"use strict";
|
|
17787
|
+
import_prefix3 = require("@tinycloud/sdk-services/kv/replication/prefix");
|
|
17788
|
+
}
|
|
17789
|
+
});
|
|
17790
|
+
|
|
17509
17791
|
// src/index.ts
|
|
17510
17792
|
var src_exports = {};
|
|
17511
17793
|
__export(src_exports, {
|
|
@@ -17557,6 +17839,8 @@ __export(src_exports, {
|
|
|
17557
17839
|
PrefixedKVService: () => import_sdk_core17.PrefixedKVService,
|
|
17558
17840
|
PrivateKeySigner: () => PrivateKeySigner,
|
|
17559
17841
|
ProtocolMismatchError: () => import_sdk_core26.ProtocolMismatchError,
|
|
17842
|
+
REPLICA_LOGOUT_PENDING_FILE: () => REPLICA_LOGOUT_PENDING_FILE,
|
|
17843
|
+
RESTORED_GATE_MESSAGE: () => RESTORED_GATE_MESSAGE,
|
|
17560
17844
|
RUNTIME_DELEGATION_REJECTIONS: () => RUNTIME_DELEGATION_REJECTIONS,
|
|
17561
17845
|
RuntimeDelegationRejectedError: () => RuntimeDelegationRejectedError,
|
|
17562
17846
|
SECRET_NAME_RE: () => import_sdk_core20.SECRET_NAME_RE,
|
|
@@ -17589,6 +17873,7 @@ __export(src_exports, {
|
|
|
17589
17873
|
activateValidatedRuntimeDelegation: () => activateValidatedRuntimeDelegation,
|
|
17590
17874
|
addressStorageKey: () => import_sdk_core15.addressStorageKey,
|
|
17591
17875
|
appSecretScope: () => import_sdk_core20.appSecretScope,
|
|
17876
|
+
assertProfileLogoutClear: () => assertProfileLogoutClear,
|
|
17592
17877
|
buildCanonicalDecryptRequest: () => import_sdk_core21.buildCanonicalDecryptRequest,
|
|
17593
17878
|
buildDecryptAttenuation: () => import_sdk_core21.buildDecryptAttenuation,
|
|
17594
17879
|
buildDecryptFacts: () => import_sdk_core21.buildDecryptFacts,
|
|
@@ -17609,6 +17894,7 @@ __export(src_exports, {
|
|
|
17609
17894
|
composeManifestRequest: () => import_sdk_core16.composeManifestRequest,
|
|
17610
17895
|
createCapabilityKeyRegistry: () => import_sdk_core24.createCapabilityKeyRegistry,
|
|
17611
17896
|
createOpenKeyCallbackSigningStrategy: () => import_sdk_core3.createOpenKeyCallbackSigningStrategy,
|
|
17897
|
+
createProfileReplicaPurge: () => createProfileReplicaPurge,
|
|
17612
17898
|
createSharingService: () => import_sdk_core23.createSharingService,
|
|
17613
17899
|
createSpaceService: () => import_sdk_core25.createSpaceService,
|
|
17614
17900
|
createVaultCrypto: () => import_sdk_core20.createVaultCrypto,
|
|
@@ -17645,6 +17931,7 @@ __export(src_exports, {
|
|
|
17645
17931
|
isNetworkId: () => import_sdk_core21.isNetworkId,
|
|
17646
17932
|
isStorageFullError: () => import_sdk_core15.isStorageFullError,
|
|
17647
17933
|
loadManifest: () => import_sdk_core16.loadManifest,
|
|
17934
|
+
logoutPrincipalDigest: () => logoutPrincipalDigest,
|
|
17648
17935
|
makePkhSpaceId: () => import_sdk_core15.makePkhSpaceId,
|
|
17649
17936
|
makePublicSpaceId: () => import_sdk_core25.makePublicSpaceId,
|
|
17650
17937
|
networkDiscoveryKey: () => import_sdk_core21.networkDiscoveryKey,
|
|
@@ -18330,7 +18617,6 @@ function deserializeDelegation(data) {
|
|
|
18330
18617
|
|
|
18331
18618
|
// src/TinyCloudNode.ts
|
|
18332
18619
|
var import_siwe2 = require("siwe");
|
|
18333
|
-
init_config();
|
|
18334
18620
|
|
|
18335
18621
|
// src/replication/module-registry.ts
|
|
18336
18622
|
var nodeLoaders;
|
|
@@ -18559,6 +18845,7 @@ var NodeUserAuthorization = class {
|
|
|
18559
18845
|
this.siweConfig = config.siweConfig;
|
|
18560
18846
|
this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
|
|
18561
18847
|
this.replication = config.replication?.enabled === true ? config.replication : void 0;
|
|
18848
|
+
this.establishReplicaStamp = config.establishReplicaStamp;
|
|
18562
18849
|
this._manifest = config.manifest;
|
|
18563
18850
|
this._capabilityRequest = config.capabilityRequest;
|
|
18564
18851
|
this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
|
|
@@ -19189,7 +19476,7 @@ var NodeUserAuthorization = class {
|
|
|
19189
19476
|
);
|
|
19190
19477
|
throw error;
|
|
19191
19478
|
}
|
|
19192
|
-
await new Promise((
|
|
19479
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
19193
19480
|
const retryResult = await (0, import_sdk_core4.activateSessionWithHost)(
|
|
19194
19481
|
host,
|
|
19195
19482
|
this._tinyCloudSession.delegationHeader
|
|
@@ -19229,7 +19516,7 @@ var NodeUserAuthorization = class {
|
|
|
19229
19516
|
);
|
|
19230
19517
|
throw error;
|
|
19231
19518
|
}
|
|
19232
|
-
await new Promise((
|
|
19519
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
19233
19520
|
const retryResult = await (0, import_sdk_core4.activateSessionWithHost)(
|
|
19234
19521
|
host,
|
|
19235
19522
|
this._tinyCloudSession.delegationHeader
|
|
@@ -19328,6 +19615,27 @@ var NodeUserAuthorization = class {
|
|
|
19328
19615
|
signature: signature2
|
|
19329
19616
|
};
|
|
19330
19617
|
}
|
|
19618
|
+
persistedData(session, expiresAt, createdAt, replicaStamp) {
|
|
19619
|
+
return {
|
|
19620
|
+
address: session.address,
|
|
19621
|
+
chainId: session.chainId,
|
|
19622
|
+
sessionKey: JSON.stringify(session.jwk),
|
|
19623
|
+
siwe: session.siwe,
|
|
19624
|
+
signature: session.signature,
|
|
19625
|
+
tinycloudSession: {
|
|
19626
|
+
delegationHeader: session.delegationHeader,
|
|
19627
|
+
delegationCid: session.delegationCid,
|
|
19628
|
+
spaceId: session.spaceId,
|
|
19629
|
+
spaces: session.spaces,
|
|
19630
|
+
verificationMethod: session.verificationMethod
|
|
19631
|
+
},
|
|
19632
|
+
expiresAt,
|
|
19633
|
+
createdAt,
|
|
19634
|
+
version: "1.0",
|
|
19635
|
+
tinycloudHosts: this.tinycloudHosts,
|
|
19636
|
+
...replicaStamp ? { replicaStamp } : {}
|
|
19637
|
+
};
|
|
19638
|
+
}
|
|
19331
19639
|
/**
|
|
19332
19640
|
* Sign in and create a new session.
|
|
19333
19641
|
*
|
|
@@ -19407,24 +19715,13 @@ var NodeUserAuthorization = class {
|
|
|
19407
19715
|
siwe: prepared.siwe,
|
|
19408
19716
|
signature: signature2
|
|
19409
19717
|
};
|
|
19410
|
-
const
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
delegationHeader: session.delegationHeader,
|
|
19418
|
-
delegationCid: session.delegationCid,
|
|
19419
|
-
spaceId,
|
|
19420
|
-
spaces: spacesMetadata,
|
|
19421
|
-
verificationMethod: this.sessionManager.getDID(keyId)
|
|
19422
|
-
},
|
|
19423
|
-
expiresAt: expirationTime.toISOString(),
|
|
19424
|
-
createdAt: now.toISOString(),
|
|
19425
|
-
version: "1.0",
|
|
19426
|
-
tinycloudHosts: this.tinycloudHosts
|
|
19427
|
-
};
|
|
19718
|
+
const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
|
|
19719
|
+
const persistedData = this.persistedData(
|
|
19720
|
+
tinyCloudSession,
|
|
19721
|
+
expirationTime.toISOString(),
|
|
19722
|
+
now.toISOString(),
|
|
19723
|
+
replicaStamp
|
|
19724
|
+
);
|
|
19428
19725
|
await this.enqueuePersistence(async () => {
|
|
19429
19726
|
await this.sessionStorage.save(address, persistedData);
|
|
19430
19727
|
if (generation !== this.sessionGeneration) {
|
|
@@ -19578,28 +19875,12 @@ var NodeUserAuthorization = class {
|
|
|
19578
19875
|
siwe: prepared.siwe,
|
|
19579
19876
|
signature: signature2
|
|
19580
19877
|
};
|
|
19581
|
-
const expirationMatch = prepared.siwe
|
|
19582
|
-
const issuedAtMatch = prepared.siwe
|
|
19878
|
+
const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
|
|
19879
|
+
const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
|
|
19583
19880
|
const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
|
|
19584
19881
|
const createdAt = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
19585
|
-
const
|
|
19586
|
-
|
|
19587
|
-
chainId,
|
|
19588
|
-
sessionKey: JSON.stringify(jwk),
|
|
19589
|
-
siwe: prepared.siwe,
|
|
19590
|
-
signature: signature2,
|
|
19591
|
-
tinycloudSession: {
|
|
19592
|
-
delegationHeader: session.delegationHeader,
|
|
19593
|
-
delegationCid: session.delegationCid,
|
|
19594
|
-
spaceId: prepared.spaceId,
|
|
19595
|
-
spaces: spacesMetadata,
|
|
19596
|
-
verificationMethod: this.sessionManager.getDID(keyId)
|
|
19597
|
-
},
|
|
19598
|
-
expiresAt,
|
|
19599
|
-
createdAt,
|
|
19600
|
-
version: "1.0",
|
|
19601
|
-
tinycloudHosts: this.tinycloudHosts
|
|
19602
|
-
};
|
|
19882
|
+
const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
|
|
19883
|
+
const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
|
|
19603
19884
|
await this.sessionStorage.save(address, persistedData);
|
|
19604
19885
|
this._session = clientSession;
|
|
19605
19886
|
this._tinyCloudSession = tinyCloudSession;
|
|
@@ -19662,15 +19943,36 @@ var NodeUserAuthorization = class {
|
|
|
19662
19943
|
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
19663
19944
|
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
19664
19945
|
const hostHint = this.primaryTinyCloudHost;
|
|
19665
|
-
const
|
|
19946
|
+
const originalRequest = {
|
|
19666
19947
|
protocolVersion: 1,
|
|
19667
19948
|
siwe: prepared.siwe,
|
|
19668
19949
|
jwk,
|
|
19950
|
+
verificationMethod: prepared.verificationMethod,
|
|
19669
19951
|
host: hostHint,
|
|
19670
19952
|
keyId: options?.openkeyKeyId,
|
|
19671
19953
|
presentation
|
|
19672
|
-
}
|
|
19673
|
-
|
|
19954
|
+
};
|
|
19955
|
+
let result;
|
|
19956
|
+
let signed2;
|
|
19957
|
+
let signature2;
|
|
19958
|
+
for (let attempt = 0; ; ) {
|
|
19959
|
+
try {
|
|
19960
|
+
result ?? (result = await authorizeFn(originalRequest));
|
|
19961
|
+
signed2 = await this.verifyOpenKeyResult(result, prepared);
|
|
19962
|
+
signature2 = result.signature;
|
|
19963
|
+
break;
|
|
19964
|
+
} catch (error) {
|
|
19965
|
+
if (!options?.retryResult) throw error;
|
|
19966
|
+
const next = await options.retryResult(
|
|
19967
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
19968
|
+
originalRequest,
|
|
19969
|
+
++attempt
|
|
19970
|
+
);
|
|
19971
|
+
if (next === null) throw error;
|
|
19972
|
+
result = next;
|
|
19973
|
+
}
|
|
19974
|
+
}
|
|
19975
|
+
return this.signInWithPreparedSession(signed2, signature2, keyId, jwk);
|
|
19674
19976
|
}
|
|
19675
19977
|
/**
|
|
19676
19978
|
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
@@ -19750,6 +20052,11 @@ var NodeUserAuthorization = class {
|
|
|
19750
20052
|
* (resource, action) pair in the returned SIWE.
|
|
19751
20053
|
*/
|
|
19752
20054
|
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
20055
|
+
const signed2 = await this.verifyOpenKeyResult(result, prepared);
|
|
20056
|
+
return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
|
|
20057
|
+
}
|
|
20058
|
+
/** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
|
|
20059
|
+
async verifyOpenKeyResult(result, prepared) {
|
|
19753
20060
|
if (result.protocolVersion !== 1) {
|
|
19754
20061
|
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
19755
20062
|
}
|
|
@@ -19994,17 +20301,12 @@ var NodeUserAuthorization = class {
|
|
|
19994
20301
|
}
|
|
19995
20302
|
}
|
|
19996
20303
|
void nonRequiredGrantedPairs;
|
|
19997
|
-
return
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
},
|
|
20004
|
-
result.signature,
|
|
20005
|
-
keyId,
|
|
20006
|
-
jwk
|
|
20007
|
-
);
|
|
20304
|
+
return {
|
|
20305
|
+
siwe: result.signedMessage,
|
|
20306
|
+
jwk: prepared.jwk,
|
|
20307
|
+
spaceId: prepared.spaceId,
|
|
20308
|
+
verificationMethod: prepared.verificationMethod
|
|
20309
|
+
};
|
|
20008
20310
|
}
|
|
20009
20311
|
/**
|
|
20010
20312
|
* Clear persisted session data.
|
|
@@ -20056,7 +20358,7 @@ var NodeUserAuthorization = class {
|
|
|
20056
20358
|
* Request signature via event emitter with timeout.
|
|
20057
20359
|
*/
|
|
20058
20360
|
requestSignatureViaEmitter(request, emitter, timeout) {
|
|
20059
|
-
return new Promise((
|
|
20361
|
+
return new Promise((resolve2, reject2) => {
|
|
20060
20362
|
const timeoutId = setTimeout(() => {
|
|
20061
20363
|
reject2(new Error("Sign request timed out"));
|
|
20062
20364
|
}, timeout);
|
|
@@ -20068,7 +20370,7 @@ var NodeUserAuthorization = class {
|
|
|
20068
20370
|
);
|
|
20069
20371
|
} else {
|
|
20070
20372
|
const signature2 = response.signature ?? await this.signer.signMessage(request.message);
|
|
20071
|
-
|
|
20373
|
+
resolve2(signature2);
|
|
20072
20374
|
}
|
|
20073
20375
|
};
|
|
20074
20376
|
emitter.emit("sign-request", request, respond);
|
|
@@ -20873,6 +21175,7 @@ function ownerPermissionsToAbilities(permissions) {
|
|
|
20873
21175
|
}
|
|
20874
21176
|
return { abilities, rawAbilities };
|
|
20875
21177
|
}
|
|
21178
|
+
var TERMINAL_DELEGATION_REQUIRES_SESSION_KEY = "disableSubDelegation requires a session-key delegation";
|
|
20876
21179
|
function base64UrlEncode(bytes) {
|
|
20877
21180
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
20878
21181
|
let output = "";
|
|
@@ -20984,6 +21287,20 @@ function accountIndexWarningDiagnostic(error) {
|
|
|
20984
21287
|
}
|
|
20985
21288
|
return diagnostic;
|
|
20986
21289
|
}
|
|
21290
|
+
function replicaRestoreRefusal(error) {
|
|
21291
|
+
if (error !== null && typeof error === "object" && "code" in error && error.code === "REPLICA_GATED" && "gate" in error) {
|
|
21292
|
+
switch (error.gate) {
|
|
21293
|
+
case "not_established":
|
|
21294
|
+
case "stamp_stale":
|
|
21295
|
+
case "principal_mismatch":
|
|
21296
|
+
case "purge_pending":
|
|
21297
|
+
case "logout_pending":
|
|
21298
|
+
case "gate_unavailable":
|
|
21299
|
+
return error.gate;
|
|
21300
|
+
}
|
|
21301
|
+
}
|
|
21302
|
+
return "gate_unavailable";
|
|
21303
|
+
}
|
|
20987
21304
|
var _TinyCloudNode = class _TinyCloudNode {
|
|
20988
21305
|
/**
|
|
20989
21306
|
* Create a new TinyCloudNode instance.
|
|
@@ -21013,6 +21330,37 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21013
21330
|
this.auth = null;
|
|
21014
21331
|
this.tc = null;
|
|
21015
21332
|
this._chainId = 1;
|
|
21333
|
+
/** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
|
|
21334
|
+
this.establishReplicaStamp = async (session) => {
|
|
21335
|
+
const replication = this.config.replication;
|
|
21336
|
+
if (!replication?.enabled) return void 0;
|
|
21337
|
+
const epoch = this.sessionEpoch;
|
|
21338
|
+
const attempt = this.latestSessionAttempt;
|
|
21339
|
+
try {
|
|
21340
|
+
const { verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2, establishReplicaSessionStamp: establishReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
|
|
21341
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
|
|
21342
|
+
const anchor = verifiedOwnerConsentAnchor2(session, this.wasmBindings, exactVerifiedRecap);
|
|
21343
|
+
if (anchor === null) {
|
|
21344
|
+
throw Object.assign(new Error("Replica sign-in requires a verified finite owner SIWE and exact-space KV get+sync."), {
|
|
21345
|
+
code: "REPLICA_GATED",
|
|
21346
|
+
gate: "not_established"
|
|
21347
|
+
});
|
|
21348
|
+
}
|
|
21349
|
+
const stamp = await establishReplicaSessionStamp2(
|
|
21350
|
+
replication.storage,
|
|
21351
|
+
(0, import_sdk_core9.pkhDid)(session.address, session.chainId),
|
|
21352
|
+
anchor
|
|
21353
|
+
);
|
|
21354
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
|
|
21355
|
+
this.replicaSessionStamp = stamp;
|
|
21356
|
+
return stamp;
|
|
21357
|
+
} catch (error) {
|
|
21358
|
+
if (epoch === this.sessionEpoch && attempt === this.latestSessionAttempt) {
|
|
21359
|
+
this.replicaRestoreGate = replicaRestoreRefusal(error);
|
|
21360
|
+
}
|
|
21361
|
+
return void 0;
|
|
21362
|
+
}
|
|
21363
|
+
};
|
|
21016
21364
|
this._baseVaults = /* @__PURE__ */ new Map();
|
|
21017
21365
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
21018
21366
|
this._secrets = /* @__PURE__ */ new Map();
|
|
@@ -21137,7 +21485,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21137
21485
|
});
|
|
21138
21486
|
return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
|
|
21139
21487
|
};
|
|
21140
|
-
|
|
21488
|
+
if (config.replication?.enabled) getNodeReplicationLoaders().validateConfig(config.replication);
|
|
21141
21489
|
this.explicitHost = config.host;
|
|
21142
21490
|
this.config = {
|
|
21143
21491
|
...config,
|
|
@@ -21152,6 +21500,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21152
21500
|
this.replicationRuntime = runtime;
|
|
21153
21501
|
this.replicationControl = {
|
|
21154
21502
|
status: async () => {
|
|
21503
|
+
const gate = this.replicaRestoreGate;
|
|
21504
|
+
if (gate) return this.config.replication.prefixes.map((prefix) => ({
|
|
21505
|
+
prefix,
|
|
21506
|
+
state: "gated",
|
|
21507
|
+
gate,
|
|
21508
|
+
errorCode: "REPLICA_GATED",
|
|
21509
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
21510
|
+
pinned: [],
|
|
21511
|
+
lagMs: null
|
|
21512
|
+
}));
|
|
21155
21513
|
const status = await runtime.then((ready) => ready.control.status());
|
|
21156
21514
|
const scopeError = this.replicationScopeError;
|
|
21157
21515
|
if (!scopeError) return status;
|
|
@@ -21168,7 +21526,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21168
21526
|
lagMs: null
|
|
21169
21527
|
}));
|
|
21170
21528
|
},
|
|
21171
|
-
sync: async (input) =>
|
|
21529
|
+
sync: async (input) => {
|
|
21530
|
+
if (this.replicaRestoreGate) {
|
|
21531
|
+
throw Object.assign(new Error("Replica restore is gated."), {
|
|
21532
|
+
code: "REPLICA_GATED",
|
|
21533
|
+
gate: this.replicaRestoreGate
|
|
21534
|
+
});
|
|
21535
|
+
}
|
|
21536
|
+
return (await runtime).control.sync(input);
|
|
21537
|
+
},
|
|
21172
21538
|
purge: (input) => {
|
|
21173
21539
|
const instance = this.replicationRuntimeInstance;
|
|
21174
21540
|
return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
|
|
@@ -21384,6 +21750,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21384
21750
|
signer: this.signer,
|
|
21385
21751
|
signStrategy: config.signStrategy ?? { type: "auto-sign" },
|
|
21386
21752
|
wasmBindings: this.wasmBindings,
|
|
21753
|
+
...config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
21387
21754
|
sessionManager: this.sessionManager,
|
|
21388
21755
|
sessionStorage: config.sessionStorage ?? new MemorySessionStorage(),
|
|
21389
21756
|
domain: this.siweDomain,
|
|
@@ -21581,11 +21948,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21581
21948
|
return this.signer === null;
|
|
21582
21949
|
}
|
|
21583
21950
|
/**
|
|
21584
|
-
*
|
|
21585
|
-
* Available after signIn().
|
|
21951
|
+
* Primary space of the signed-in or restored network session, including compact sessions.
|
|
21586
21952
|
*/
|
|
21587
21953
|
get spaceId() {
|
|
21588
|
-
return this.auth?.tinyCloudSession?.spaceId;
|
|
21954
|
+
return this.auth?.tinyCloudSession?.spaceId ?? this._activeServiceSession?.spaceId;
|
|
21589
21955
|
}
|
|
21590
21956
|
/**
|
|
21591
21957
|
* Get the account space ID for this wallet identity.
|
|
@@ -21633,6 +21999,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21633
21999
|
get restorableSession() {
|
|
21634
22000
|
return this.currentTinyCloudSession();
|
|
21635
22001
|
}
|
|
22002
|
+
/** The active, verified replica stamp. Undefined when the replica is refused or the session has ended. */
|
|
22003
|
+
get replicaStamp() {
|
|
22004
|
+
const stamp = this.replicaSessionStamp;
|
|
22005
|
+
if (!stamp || this.replicaRestoreGate !== void 0 || !this.currentTinyCloudSession()) return void 0;
|
|
22006
|
+
return { incarnation: stamp.incarnation, lifecycleEpoch: stamp.lifecycleEpoch };
|
|
22007
|
+
}
|
|
21636
22008
|
/**
|
|
21637
22009
|
* Sign out and end the session on this instance.
|
|
21638
22010
|
*
|
|
@@ -21653,18 +22025,60 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21653
22025
|
}
|
|
21654
22026
|
async endSession() {
|
|
21655
22027
|
const leavingAddress = this.address ?? this._restoredTcSession?.address;
|
|
22028
|
+
const leavingChainId = this._chainId;
|
|
22029
|
+
const leavingPrincipal = this.replicaSessionPrincipal ?? (leavingAddress ? (0, import_sdk_core9.pkhDid)(leavingAddress, leavingChainId) : void 0);
|
|
21656
22030
|
const leavingAuth = this.auth;
|
|
22031
|
+
const leavingContext = this._serviceContext;
|
|
22032
|
+
let denied;
|
|
21657
22033
|
try {
|
|
21658
22034
|
this.teardownSession();
|
|
21659
22035
|
} finally {
|
|
22036
|
+
const denial = leavingPrincipal ? this.denyReplicaGate({ reason: "sign_out", principal: leavingPrincipal }, leavingContext) : Promise.resolve(void 0);
|
|
21660
22037
|
try {
|
|
21661
|
-
await
|
|
21662
|
-
if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
|
|
22038
|
+
denied = await denial;
|
|
21663
22039
|
} finally {
|
|
21664
|
-
|
|
21665
|
-
await
|
|
22040
|
+
try {
|
|
22041
|
+
await this.accountRegistryTail;
|
|
22042
|
+
await this.quiesceReplicaRuntime();
|
|
22043
|
+
} finally {
|
|
22044
|
+
if (leavingAddress) {
|
|
22045
|
+
await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
|
|
22046
|
+
}
|
|
22047
|
+
}
|
|
22048
|
+
}
|
|
22049
|
+
}
|
|
22050
|
+
if (denied?.status === "unguarded") {
|
|
22051
|
+
throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
|
|
22052
|
+
}
|
|
22053
|
+
}
|
|
22054
|
+
/**
|
|
22055
|
+
* Step 1 of replica sign-out. Never rejects: failures come back as `unguarded` so the session
|
|
22056
|
+
* is still cleared. Returns `undefined` when replication is off.
|
|
22057
|
+
*/
|
|
22058
|
+
async denyReplicaGate(input, context = this._serviceContext) {
|
|
22059
|
+
const replication = this.config.replication;
|
|
22060
|
+
if (!replication?.enabled) return void 0;
|
|
22061
|
+
try {
|
|
22062
|
+
const lifecycle = replication.storage.lifecycle;
|
|
22063
|
+
if (lifecycle?.kind !== (replication.storage.kind === "indexeddb" ? "indexeddb" : "file")) {
|
|
22064
|
+
return { status: "unguarded", code: "gate_unavailable" };
|
|
22065
|
+
}
|
|
22066
|
+
const result = await lifecycle.deny(input);
|
|
22067
|
+
if (result.status === "denied" || result.status === "deleted" || result.status === "marker") {
|
|
22068
|
+
try {
|
|
22069
|
+
(await getNodeReplicationLoaders().authority()).reportReplicaDenial(
|
|
22070
|
+
result,
|
|
22071
|
+
input.reason,
|
|
22072
|
+
replication.onEvent,
|
|
22073
|
+
context
|
|
22074
|
+
);
|
|
22075
|
+
} catch {
|
|
21666
22076
|
}
|
|
21667
22077
|
}
|
|
22078
|
+
return result;
|
|
22079
|
+
} catch (error) {
|
|
22080
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable";
|
|
22081
|
+
return { status: "unguarded", code };
|
|
21668
22082
|
}
|
|
21669
22083
|
}
|
|
21670
22084
|
/**
|
|
@@ -21684,15 +22098,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21684
22098
|
this._activeServiceSession = void 0;
|
|
21685
22099
|
oldGraph.retire();
|
|
21686
22100
|
oldCore?.retireServices();
|
|
21687
|
-
oldAuth?.resetSession(
|
|
21688
|
-
this.sessionManager,
|
|
21689
|
-
this.sessionKeyId,
|
|
21690
|
-
this.explicitHost ? [this.explicitHost] : void 0
|
|
21691
|
-
);
|
|
21692
22101
|
this.config.host = this.explicitHost ?? DEFAULT_HOST;
|
|
21693
22102
|
this._chainId = 1;
|
|
21694
22103
|
this._restoredNodeFeatures = void 0;
|
|
21695
|
-
this._activeServiceSession = void 0;
|
|
21696
22104
|
this._serviceContext = void 0;
|
|
21697
22105
|
this._kv = void 0;
|
|
21698
22106
|
this._sql = void 0;
|
|
@@ -21717,6 +22125,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21717
22125
|
this._bootstrapStatus = { skipped: false };
|
|
21718
22126
|
this.replicationDelegateSession = void 0;
|
|
21719
22127
|
this.replicationScopeError = void 0;
|
|
22128
|
+
this.replicaSessionPrincipal = void 0;
|
|
22129
|
+
this.replicaSessionStamp = void 0;
|
|
22130
|
+
this.replicaRestoreGate = void 0;
|
|
21720
22131
|
const sessionKey = createDefaultSessionKey(this.wasmBindings);
|
|
21721
22132
|
this.sessionManager = sessionKey.manager;
|
|
21722
22133
|
this.sessionKeyId = sessionKey.keyId;
|
|
@@ -21726,6 +22137,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21726
22137
|
sessionKey.keyId,
|
|
21727
22138
|
this.explicitHost ? [this.explicitHost] : void 0
|
|
21728
22139
|
);
|
|
22140
|
+
this.replicaSessionPrincipal = void 0;
|
|
22141
|
+
this.replicaSessionStamp = void 0;
|
|
22142
|
+
this.replicaRestoreGate = void 0;
|
|
21729
22143
|
this._serviceGraph = this.createServiceGraphLifetime();
|
|
21730
22144
|
this.auth = oldAuth;
|
|
21731
22145
|
this.tc = this.auth ? new import_sdk_core9.TinyCloud(this.auth, {
|
|
@@ -21755,11 +22169,25 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21755
22169
|
"Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
|
|
21756
22170
|
);
|
|
21757
22171
|
}
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
22172
|
+
await this.signInWith(() => this.tc.signIn(options));
|
|
22173
|
+
}
|
|
22174
|
+
/**
|
|
22175
|
+
* Fresh OpenKey sign-in. The callback receives the SDK's original SIWE and
|
|
22176
|
+
* session JWK before approval; the signed result is verified against those
|
|
22177
|
+
* bytes before the network session or replica stamp is committed.
|
|
22178
|
+
*/
|
|
22179
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
22180
|
+
if (!this.signer || !this.tc || !this.auth) {
|
|
22181
|
+
throw new Error("Cannot signInWithOpenKey() in session-only mode. Provide a signer in config.");
|
|
22182
|
+
}
|
|
22183
|
+
await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
|
|
22184
|
+
}
|
|
22185
|
+
async signInWith(performSignIn) {
|
|
22186
|
+
const epoch = this.sessionEpoch;
|
|
22187
|
+
const attempt = ++this.latestSessionAttempt;
|
|
22188
|
+
await this.signOutInFlight;
|
|
21761
22189
|
try {
|
|
21762
|
-
await this.establishSignIn(epoch,
|
|
22190
|
+
await this.establishSignIn(epoch, performSignIn);
|
|
21763
22191
|
} catch (error) {
|
|
21764
22192
|
if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
|
|
21765
22193
|
await this.endSession();
|
|
@@ -21767,7 +22195,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21767
22195
|
throw error;
|
|
21768
22196
|
}
|
|
21769
22197
|
}
|
|
21770
|
-
async establishSignIn(epoch,
|
|
22198
|
+
async establishSignIn(epoch, performSignIn) {
|
|
21771
22199
|
if (!this.signer || !this.tc) {
|
|
21772
22200
|
throw new Error("Not signed in. Call signIn() first.");
|
|
21773
22201
|
}
|
|
@@ -21777,6 +22205,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21777
22205
|
const address = (0, import_sdk_core9.canonicalizeAddress)(await this.signer.getAddress());
|
|
21778
22206
|
const chainId = await this.signer.getChainId();
|
|
21779
22207
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
22208
|
+
const nextPrincipal = (0, import_sdk_core9.pkhDid)(address, chainId);
|
|
22209
|
+
const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core9.pkhDid)(this.address, this._chainId) : void 0);
|
|
22210
|
+
if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
|
|
22211
|
+
const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
|
|
22212
|
+
this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
|
|
22213
|
+
}
|
|
21780
22214
|
if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
|
|
21781
22215
|
this._address = address;
|
|
21782
22216
|
this._chainId = chainId;
|
|
@@ -21794,7 +22228,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21794
22228
|
this.runtimePermissionGrants = [];
|
|
21795
22229
|
this.confirmedHostedSpaceIds.clear();
|
|
21796
22230
|
this.hostedOwnedSpaceIds.clear();
|
|
21797
|
-
|
|
22231
|
+
const signInAttempt = this.latestSessionAttempt;
|
|
22232
|
+
const previousReplicaGate = this.replicaRestoreGate;
|
|
22233
|
+
const previousReplicaStamp = this.replicaSessionStamp;
|
|
22234
|
+
this.replicaRestoreGate = void 0;
|
|
22235
|
+
this.replicaSessionStamp = void 0;
|
|
22236
|
+
try {
|
|
22237
|
+
await performSignIn();
|
|
22238
|
+
} catch (error) {
|
|
22239
|
+
if (epoch === this.sessionEpoch && signInAttempt === this.latestSessionAttempt) {
|
|
22240
|
+
this.replicaRestoreGate = previousReplicaGate;
|
|
22241
|
+
this.replicaSessionStamp = previousReplicaStamp;
|
|
22242
|
+
}
|
|
22243
|
+
throw error;
|
|
22244
|
+
}
|
|
21798
22245
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
21799
22246
|
this.cancelOwnedSpaceHosts();
|
|
21800
22247
|
this.syncResolvedHostFromAuth();
|
|
@@ -21802,8 +22249,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21802
22249
|
this.hostedOwnedSpaceIds.add(spaceId);
|
|
21803
22250
|
}
|
|
21804
22251
|
this.replicationDelegateSession = void 0;
|
|
22252
|
+
this.replicaSessionPrincipal = (0, import_sdk_core9.pkhDid)(address, chainId);
|
|
21805
22253
|
this.replicationScopeError = void 0;
|
|
21806
|
-
|
|
22254
|
+
await this.quiesceReplicaRuntime();
|
|
21807
22255
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
21808
22256
|
const oldGraph = this._serviceGraph;
|
|
21809
22257
|
this._serviceGraph = this.createServiceGraphLifetime();
|
|
@@ -22055,7 +22503,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22055
22503
|
);
|
|
22056
22504
|
}
|
|
22057
22505
|
}
|
|
22058
|
-
await new Promise((
|
|
22506
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
22059
22507
|
for (const step of steps) {
|
|
22060
22508
|
if (step.kind !== "activate") continue;
|
|
22061
22509
|
graph.assertActive();
|
|
@@ -22390,6 +22838,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22390
22838
|
if (this.ownStorageFull) {
|
|
22391
22839
|
return;
|
|
22392
22840
|
}
|
|
22841
|
+
if (!this.currentSessionCoversAccountRegistry()) {
|
|
22842
|
+
return;
|
|
22843
|
+
}
|
|
22393
22844
|
const controller = new AbortController();
|
|
22394
22845
|
const deadline = setTimeout(
|
|
22395
22846
|
() => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
|
|
@@ -22504,6 +22955,27 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22504
22955
|
(operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
|
|
22505
22956
|
);
|
|
22506
22957
|
}
|
|
22958
|
+
/**
|
|
22959
|
+
* Whether the current primary session's recap covers the account index read
|
|
22960
|
+
* (`tinycloud.sql/read` on the owned account space's `account` database),
|
|
22961
|
+
* the first call of every registry sync. As in
|
|
22962
|
+
* {@link currentSessionCanListSpaces}, a session with no parseable recap
|
|
22963
|
+
* keeps today's behavior.
|
|
22964
|
+
*/
|
|
22965
|
+
currentSessionCoversAccountRegistry() {
|
|
22966
|
+
const session = this.currentTinyCloudSession();
|
|
22967
|
+
const operations = session ? this.recapOperationsFromSession(session) : [];
|
|
22968
|
+
if (operations.length === 0) {
|
|
22969
|
+
return true;
|
|
22970
|
+
}
|
|
22971
|
+
const indexRead = {
|
|
22972
|
+
spaceId: this.ownedSpaceId(import_sdk_core9.ACCOUNT_REGISTRY_SPACE),
|
|
22973
|
+
service: "sql",
|
|
22974
|
+
path: "account",
|
|
22975
|
+
action: "tinycloud.sql/read"
|
|
22976
|
+
};
|
|
22977
|
+
return operations.some((granted) => this.operationCovers(granted, indexRead));
|
|
22978
|
+
}
|
|
22507
22979
|
async withAccountRegistryRetry(task, signal) {
|
|
22508
22980
|
const delays = [250, 1e3, 3e3];
|
|
22509
22981
|
const stopped = (detail) => {
|
|
@@ -22544,10 +23016,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22544
23016
|
}
|
|
22545
23017
|
lastError = error;
|
|
22546
23018
|
if (attempt < delays.length - 1) {
|
|
22547
|
-
await new Promise((
|
|
23019
|
+
await new Promise((resolve2, reject2) => {
|
|
22548
23020
|
const timer = setTimeout(() => {
|
|
22549
23021
|
signal.removeEventListener("abort", onAbort);
|
|
22550
|
-
|
|
23022
|
+
resolve2();
|
|
22551
23023
|
}, delays[attempt]);
|
|
22552
23024
|
const onAbort = () => {
|
|
22553
23025
|
clearTimeout(timer);
|
|
@@ -22787,7 +23259,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22787
23259
|
}
|
|
22788
23260
|
entry.waiters += 1;
|
|
22789
23261
|
const shared = entry;
|
|
22790
|
-
return new Promise((
|
|
23262
|
+
return new Promise((resolve2, reject2) => {
|
|
22791
23263
|
const onAbort = () => {
|
|
22792
23264
|
reject2(signal.reason);
|
|
22793
23265
|
shared.waiters -= 1;
|
|
@@ -22796,7 +23268,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22796
23268
|
if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
|
|
22797
23269
|
};
|
|
22798
23270
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
22799
|
-
shared.promise.then(
|
|
23271
|
+
shared.promise.then(resolve2, reject2).finally(() => signal.removeEventListener("abort", onAbort));
|
|
22800
23272
|
});
|
|
22801
23273
|
}
|
|
22802
23274
|
/** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
|
|
@@ -22970,11 +23442,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22970
23442
|
);
|
|
22971
23443
|
const stagedHost = resolvedHost ?? this.config.host;
|
|
22972
23444
|
let restoredNodeFeatures;
|
|
23445
|
+
let replicaProtocolMismatch = false;
|
|
22973
23446
|
if (this.config.replication?.enabled) {
|
|
22974
23447
|
try {
|
|
22975
23448
|
restoredNodeFeatures = (await (0, import_sdk_core9.checkNodeInfo)(stagedHost, this.wasmBindings.protocolVersion())).features;
|
|
22976
23449
|
} catch (error) {
|
|
22977
|
-
|
|
23450
|
+
replicaProtocolMismatch = error instanceof import_sdk_core9.ProtocolMismatchError;
|
|
22978
23451
|
}
|
|
22979
23452
|
}
|
|
22980
23453
|
const stagedAddress = restoredAddress;
|
|
@@ -22982,28 +23455,55 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22982
23455
|
let replicationPrincipal;
|
|
22983
23456
|
let compactReplicationSession;
|
|
22984
23457
|
let replicationScopeError;
|
|
23458
|
+
let replicationScopeUnavailable = false;
|
|
22985
23459
|
if (this.config.replication?.enabled) {
|
|
22986
23460
|
if (stagedAddress !== void 0) {
|
|
22987
23461
|
replicationPrincipal = (0, import_sdk_core9.pkhDid)(stagedAddress, stagedChainId);
|
|
22988
23462
|
} else {
|
|
22989
|
-
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
delegationCid: sessionData.delegationCid,
|
|
22993
|
-
verificationMethod: restoredVerificationMethod,
|
|
22994
|
-
persistedSpace: sessionData.spaceId
|
|
22995
|
-
});
|
|
22996
|
-
if (!signedScope) {
|
|
22997
|
-
replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
|
|
22998
|
-
} else {
|
|
22999
|
-
replicationPrincipal = signedScope.principal;
|
|
23000
|
-
compactReplicationSession = {
|
|
23463
|
+
try {
|
|
23464
|
+
const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
|
|
23465
|
+
const signedScope = replicationScopeFromSignedSession2({
|
|
23001
23466
|
delegationHeader: sessionData.delegationHeader,
|
|
23002
23467
|
delegationCid: sessionData.delegationCid,
|
|
23003
|
-
spaceId: signedScope.space,
|
|
23004
23468
|
verificationMethod: restoredVerificationMethod,
|
|
23005
|
-
|
|
23006
|
-
};
|
|
23469
|
+
persistedSpace: sessionData.spaceId
|
|
23470
|
+
});
|
|
23471
|
+
if (!signedScope) {
|
|
23472
|
+
replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
|
|
23473
|
+
} else {
|
|
23474
|
+
replicationPrincipal = signedScope.principal;
|
|
23475
|
+
compactReplicationSession = {
|
|
23476
|
+
delegationHeader: sessionData.delegationHeader,
|
|
23477
|
+
delegationCid: sessionData.delegationCid,
|
|
23478
|
+
spaceId: signedScope.space,
|
|
23479
|
+
verificationMethod: restoredVerificationMethod,
|
|
23480
|
+
jwk: stagedJwk
|
|
23481
|
+
};
|
|
23482
|
+
}
|
|
23483
|
+
} catch {
|
|
23484
|
+
replicationScopeUnavailable = true;
|
|
23485
|
+
replicationScopeError = "Replica scope could not be verified for this network session.";
|
|
23486
|
+
}
|
|
23487
|
+
}
|
|
23488
|
+
}
|
|
23489
|
+
let replicaRestoreGate;
|
|
23490
|
+
let restoredReplicaStamp;
|
|
23491
|
+
const replication = this.config.replication;
|
|
23492
|
+
if (replication?.enabled) {
|
|
23493
|
+
if (replicaProtocolMismatch) {
|
|
23494
|
+
replicaRestoreGate = "gate_unavailable";
|
|
23495
|
+
} else if (replicationPrincipal === void 0) {
|
|
23496
|
+
replicaRestoreGate = replicationScopeUnavailable ? "gate_unavailable" : "principal_mismatch";
|
|
23497
|
+
} else {
|
|
23498
|
+
try {
|
|
23499
|
+
const { restoreReplicaSessionStamp: restoreReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
|
|
23500
|
+
restoredReplicaStamp = await restoreReplicaSessionStamp2(
|
|
23501
|
+
replication.storage,
|
|
23502
|
+
replicationPrincipal,
|
|
23503
|
+
sessionData.replicaStamp ?? void 0
|
|
23504
|
+
);
|
|
23505
|
+
} catch (error) {
|
|
23506
|
+
replicaRestoreGate = replicaRestoreRefusal(error);
|
|
23007
23507
|
}
|
|
23008
23508
|
}
|
|
23009
23509
|
}
|
|
@@ -23096,7 +23596,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23096
23596
|
}
|
|
23097
23597
|
this.replicationDelegateSession = compactReplicationSession;
|
|
23098
23598
|
this.replicationScopeError = replicationScopeError;
|
|
23099
|
-
|
|
23599
|
+
this.replicaRestoreGate = replicaRestoreGate;
|
|
23600
|
+
this.replicaSessionPrincipal = replicationPrincipal;
|
|
23601
|
+
this.replicaSessionStamp = restoredReplicaStamp;
|
|
23602
|
+
await this.quiesceReplicaRuntime();
|
|
23100
23603
|
if (epoch !== this.sessionEpoch) {
|
|
23101
23604
|
oldGraph.retire();
|
|
23102
23605
|
oldCore?.retireServices?.();
|
|
@@ -23111,7 +23614,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23111
23614
|
detail: replicationScopeError
|
|
23112
23615
|
});
|
|
23113
23616
|
}
|
|
23114
|
-
await this.
|
|
23617
|
+
await this.bindReplicaOrGate(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
|
|
23115
23618
|
oldCore?.retireServices?.();
|
|
23116
23619
|
if (epoch !== this.sessionEpoch) {
|
|
23117
23620
|
if (attempt === this.latestSessionAttempt) await this.endSession();
|
|
@@ -23244,6 +23747,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23244
23747
|
session: input.serviceSession,
|
|
23245
23748
|
invoke: graph.invoke,
|
|
23246
23749
|
invokeAny: graph.invokeAny,
|
|
23750
|
+
signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
|
|
23247
23751
|
fetch: graph.fetch
|
|
23248
23752
|
});
|
|
23249
23753
|
const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
|
|
@@ -23587,6 +24091,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23587
24091
|
signer: this.signer,
|
|
23588
24092
|
signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
|
|
23589
24093
|
wasmBindings: this.wasmBindings,
|
|
24094
|
+
...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
23590
24095
|
sessionManager: this.sessionManager,
|
|
23591
24096
|
sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
|
|
23592
24097
|
domain: this.siweDomain,
|
|
@@ -23645,6 +24150,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23645
24150
|
signer: this.signer,
|
|
23646
24151
|
signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
|
|
23647
24152
|
wasmBindings: this.wasmBindings,
|
|
24153
|
+
...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
23648
24154
|
sessionManager: this.sessionManager,
|
|
23649
24155
|
sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
|
|
23650
24156
|
domain: this.siweDomain,
|
|
@@ -23725,7 +24231,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23725
24231
|
jwk: session.jwk
|
|
23726
24232
|
};
|
|
23727
24233
|
this._serviceContext.setSession(serviceSession);
|
|
23728
|
-
await this.
|
|
24234
|
+
await this.bindReplicaOrGate(this._serviceContext, serviceSession, this._kv);
|
|
23729
24235
|
this.tc.serviceContext.setSession(serviceSession);
|
|
23730
24236
|
this._vault = this.createVaultService(session.spaceId, this._kv);
|
|
23731
24237
|
this._vault.initialize(this._serviceContext);
|
|
@@ -24119,6 +24625,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24119
24625
|
session: serviceSession,
|
|
24120
24626
|
invoke: graph.invoke,
|
|
24121
24627
|
invokeAny: graph.invokeAny,
|
|
24628
|
+
signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
|
|
24122
24629
|
fetch: graph.fetch
|
|
24123
24630
|
});
|
|
24124
24631
|
this._spaceService = new import_sdk_core9.SpaceService({
|
|
@@ -24254,11 +24761,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24254
24761
|
* Both SharingService (single-entry) and
|
|
24255
24762
|
* {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
|
|
24256
24763
|
* the same code path so there's exactly one place that touches the
|
|
24257
|
-
* WASM boundary.
|
|
24764
|
+
* WASM boundary. `terminal` adds the node's terminal marker, which the
|
|
24765
|
+
* WASM builder cannot emit.
|
|
24258
24766
|
*
|
|
24259
24767
|
* @internal
|
|
24260
24768
|
*/
|
|
24261
|
-
createDelegationWrapper(params) {
|
|
24769
|
+
createDelegationWrapper(params, terminal = false) {
|
|
24262
24770
|
const wasmSession = {
|
|
24263
24771
|
delegationHeader: params.session.delegationHeader,
|
|
24264
24772
|
delegationCid: params.session.delegationCid,
|
|
@@ -24278,8 +24786,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24278
24786
|
params.notBeforeSecs
|
|
24279
24787
|
);
|
|
24280
24788
|
return {
|
|
24281
|
-
delegation: result.delegation,
|
|
24282
|
-
cid: result.cid,
|
|
24789
|
+
...terminal ? (0, import_sdk_core9.markDelegationTerminal)(result.delegation, params.session.jwk) : { delegation: result.delegation, cid: result.cid },
|
|
24283
24790
|
// Rust serde `rename_all = "camelCase"` emits `delegateDid`
|
|
24284
24791
|
// (lowercase d); the TypeScript interface uses `delegateDID`
|
|
24285
24792
|
// (historical, matches Delegation.delegateDID). Normalize here.
|
|
@@ -24421,7 +24928,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24421
24928
|
};
|
|
24422
24929
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core9.canonicalizeEncryptionJson)(body)))));
|
|
24423
24930
|
const request = { ...body, requestBodyDigest };
|
|
24424
|
-
const authorization = authorizationHeader(this.
|
|
24931
|
+
const authorization = authorizationHeader(this.wasmBindings.invoke(serviceSession, "kv", input.resourcePath, "tinycloud.kv/get"));
|
|
24425
24932
|
const response = await fetch(`${this.config.host}/policy/v3/deliveries/authorize`, {
|
|
24426
24933
|
method: "POST",
|
|
24427
24934
|
headers: { accept: "application/json", "content-type": "application/json", authorization },
|
|
@@ -24736,6 +25243,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24736
25243
|
}
|
|
24737
25244
|
};
|
|
24738
25245
|
}
|
|
25246
|
+
if (result.status === "aad_mismatch") return { status: "read_failed" };
|
|
24739
25247
|
return { status: result.status };
|
|
24740
25248
|
}
|
|
24741
25249
|
if (!isSecretPayload(result.entry.value)) {
|
|
@@ -25411,7 +25919,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25411
25919
|
return this.delegationManager.create(params);
|
|
25412
25920
|
}
|
|
25413
25921
|
/**
|
|
25414
|
-
* Revoke a delegation
|
|
25922
|
+
* Revoke a delegation with the owner wallet when available. Session-only
|
|
25923
|
+
* clients continue to use their delegated revocation authority.
|
|
25415
25924
|
*
|
|
25416
25925
|
* @param cid - The CID of the delegation to revoke
|
|
25417
25926
|
* @returns Result indicating success or failure
|
|
@@ -25527,6 +26036,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25527
26036
|
effectiveExpiration = sessionExpiry;
|
|
25528
26037
|
}
|
|
25529
26038
|
if (options?.forceWalletSign) {
|
|
26039
|
+
if (options.disableSubDelegation) {
|
|
26040
|
+
throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
|
|
26041
|
+
}
|
|
25530
26042
|
if (expandedEntries.length > 1) {
|
|
25531
26043
|
throw new Error(
|
|
25532
26044
|
"delegateTo with forceWalletSign=true supports at most one PermissionEntry. Multi-entry requests must go through the session-key UCAN path (drop forceWalletSign) or the legacy createDelegation method."
|
|
@@ -25562,7 +26074,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25562
26074
|
plan.effectiveExpiration,
|
|
25563
26075
|
plan.grant,
|
|
25564
26076
|
plan.operations,
|
|
25565
|
-
onPrepared
|
|
26077
|
+
onPrepared,
|
|
26078
|
+
options?.disableSubDelegation === true
|
|
25566
26079
|
);
|
|
25567
26080
|
return { delegation: delegation2, prompted: false };
|
|
25568
26081
|
}
|
|
@@ -25571,7 +26084,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25571
26084
|
expandedEntries,
|
|
25572
26085
|
effectiveExpiration,
|
|
25573
26086
|
session,
|
|
25574
|
-
onPrepared
|
|
26087
|
+
onPrepared,
|
|
26088
|
+
options?.disableSubDelegation === true
|
|
25575
26089
|
);
|
|
25576
26090
|
return { delegation, prompted: false };
|
|
25577
26091
|
}
|
|
@@ -25686,34 +26200,67 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25686
26200
|
return auth.replicationSignInEntries();
|
|
25687
26201
|
}
|
|
25688
26202
|
async replicationAuthority() {
|
|
25689
|
-
const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
|
|
26203
|
+
const { createReplicationAuthority: createReplicationAuthority2, mintNodeReplicationGrant: mintNodeReplicationGrant2, verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2 } = await getNodeReplicationLoaders().authority();
|
|
25690
26204
|
return createReplicationAuthority2({
|
|
25691
26205
|
replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
|
|
26206
|
+
verifiedConsentAnchor: (session) => {
|
|
26207
|
+
const current = this.currentTinyCloudSession();
|
|
26208
|
+
return session === current && current !== void 0 ? verifiedOwnerConsentAnchor2(current, this.wasmBindings, exactVerifiedRecap) : null;
|
|
26209
|
+
},
|
|
25692
26210
|
siweExpiration: (siwe) => extractSiweExpiration(siwe),
|
|
25693
26211
|
planDelegation: (entries, options) => this.planDelegation(entries, options),
|
|
25694
|
-
mintDelegation:
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
25699
|
-
|
|
25700
|
-
|
|
26212
|
+
mintDelegation: (deviceDid, entries, options) => mintNodeReplicationGrant2(
|
|
26213
|
+
deviceDid,
|
|
26214
|
+
entries,
|
|
26215
|
+
options,
|
|
26216
|
+
options?.renewalOnly ? this.currentTinyCloudSession() : void 0,
|
|
26217
|
+
this
|
|
26218
|
+
)
|
|
25701
26219
|
});
|
|
25702
26220
|
}
|
|
25703
|
-
async
|
|
26221
|
+
async quiesceReplicaRuntime() {
|
|
25704
26222
|
if (!this.replicationRuntime) return;
|
|
25705
|
-
|
|
25706
|
-
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
25715
|
-
|
|
25716
|
-
|
|
26223
|
+
try {
|
|
26224
|
+
await (await this.replicationRuntime).unbind();
|
|
26225
|
+
} catch {
|
|
26226
|
+
this.replicaRestoreGate = "gate_unavailable";
|
|
26227
|
+
}
|
|
26228
|
+
}
|
|
26229
|
+
async bindReplicaOrGate(context, session, kv, principal) {
|
|
26230
|
+
if (!this.replicationRuntime) return;
|
|
26231
|
+
const gate = this.replicaRestoreGate;
|
|
26232
|
+
const epoch = this.sessionEpoch;
|
|
26233
|
+
const attempt = this.latestSessionAttempt;
|
|
26234
|
+
try {
|
|
26235
|
+
const runtime = await this.replicationRuntime;
|
|
26236
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context || gate !== this.replicaRestoreGate) return;
|
|
26237
|
+
if (gate) {
|
|
26238
|
+
await runtime.bindGatedNetworkOnly({
|
|
26239
|
+
context,
|
|
26240
|
+
session,
|
|
26241
|
+
gate,
|
|
26242
|
+
primaryKV: [{ space: session.spaceId, kv }]
|
|
26243
|
+
}, getNodeReplicationLoaders().gatedNetwork);
|
|
26244
|
+
return;
|
|
26245
|
+
}
|
|
26246
|
+
const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : (0, import_sdk_core9.pkhDid)(this._address, this._chainId));
|
|
26247
|
+
if (identityPrincipal === void 0) return;
|
|
26248
|
+
const authority = await this.replicationAuthority();
|
|
26249
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
|
|
26250
|
+
await runtime.bind({
|
|
26251
|
+
context,
|
|
26252
|
+
session,
|
|
26253
|
+
principal: identityPrincipal,
|
|
26254
|
+
stamp: this.replicaSessionStamp,
|
|
26255
|
+
authority,
|
|
26256
|
+
nodeFeatures: this.replicationNodeFeatures,
|
|
26257
|
+
primaryKV: [{ space: session.spaceId, kv }]
|
|
26258
|
+
});
|
|
26259
|
+
} catch {
|
|
26260
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
|
|
26261
|
+
this.replicaRestoreGate = "gate_unavailable";
|
|
26262
|
+
await this.quiesceReplicaRuntime();
|
|
26263
|
+
}
|
|
25717
26264
|
}
|
|
25718
26265
|
/** Stable control facade for this node's optional replication runtime. */
|
|
25719
26266
|
get replication() {
|
|
@@ -25769,7 +26316,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25769
26316
|
*
|
|
25770
26317
|
* @internal
|
|
25771
26318
|
*/
|
|
25772
|
-
async createDelegationViaWasmPath(did, entries, expirationTime, session, onPrepared) {
|
|
26319
|
+
async createDelegationViaWasmPath(did, entries, expirationTime, session, onPrepared, disableSubDelegation = false) {
|
|
25773
26320
|
if (entries.length === 0) {
|
|
25774
26321
|
throw new Error(
|
|
25775
26322
|
"createDelegationViaWasmPath requires a non-empty entries array"
|
|
@@ -25829,7 +26376,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25829
26376
|
spaceId,
|
|
25830
26377
|
abilities,
|
|
25831
26378
|
expirationSecs
|
|
25832
|
-
});
|
|
26379
|
+
}, disableSubDelegation);
|
|
25833
26380
|
const primary = result.resources[0];
|
|
25834
26381
|
const delegationHeader = { Authorization: result.delegation };
|
|
25835
26382
|
const delegation = {
|
|
@@ -25839,7 +26386,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25839
26386
|
path: primary.path,
|
|
25840
26387
|
actions: primary.actions,
|
|
25841
26388
|
resources: result.resources,
|
|
25842
|
-
disableSubDelegation
|
|
26389
|
+
disableSubDelegation,
|
|
25843
26390
|
expiry: result.expiry,
|
|
25844
26391
|
delegateDID: did,
|
|
25845
26392
|
ownerAddress: session.address,
|
|
@@ -25861,7 +26408,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25861
26408
|
}
|
|
25862
26409
|
return delegation;
|
|
25863
26410
|
}
|
|
25864
|
-
async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations, onPrepared) {
|
|
26411
|
+
async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations, onPrepared, disableSubDelegation = false) {
|
|
25865
26412
|
this.assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant);
|
|
25866
26413
|
this.assertDelegationCaveatsPreservable(entries);
|
|
25867
26414
|
const result = this.createDelegationWrapper({
|
|
@@ -25870,7 +26417,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25870
26417
|
spaceId: grant.session.spaceId,
|
|
25871
26418
|
abilities: this.permissionsToAbilities(entries),
|
|
25872
26419
|
expirationSecs: Math.floor(expirationTime.getTime() / 1e3)
|
|
25873
|
-
});
|
|
26420
|
+
}, disableSubDelegation);
|
|
25874
26421
|
const primary = result.resources[0];
|
|
25875
26422
|
const delegationHeader = { Authorization: result.delegation };
|
|
25876
26423
|
const targetHost = grant.delegation.host ?? this.config.host;
|
|
@@ -25881,7 +26428,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25881
26428
|
path: primary.path,
|
|
25882
26429
|
actions: primary.actions,
|
|
25883
26430
|
resources: result.resources,
|
|
25884
|
-
disableSubDelegation
|
|
26431
|
+
disableSubDelegation,
|
|
25885
26432
|
expiry: result.expiry,
|
|
25886
26433
|
delegateDID: did,
|
|
25887
26434
|
ownerAddress: grant.delegation.ownerAddress,
|
|
@@ -26465,11 +27012,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26465
27012
|
params.spaceIdOverride
|
|
26466
27013
|
);
|
|
26467
27014
|
try {
|
|
26468
|
-
const result = await this.delegateTo(
|
|
26469
|
-
|
|
26470
|
-
|
|
26471
|
-
|
|
26472
|
-
);
|
|
27015
|
+
const result = await this.delegateTo(resolvedDelegateDID, entries, {
|
|
27016
|
+
...params.expiryMs !== void 0 ? { expiry: params.expiryMs } : {},
|
|
27017
|
+
disableSubDelegation: params.disableSubDelegation
|
|
27018
|
+
});
|
|
26473
27019
|
return result.delegation;
|
|
26474
27020
|
} catch (err2) {
|
|
26475
27021
|
if (err2 instanceof import_sdk_core9.PermissionNotInManifestError) {
|
|
@@ -26490,6 +27036,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26490
27036
|
* @internal
|
|
26491
27037
|
*/
|
|
26492
27038
|
async createDelegationWalletPath(params) {
|
|
27039
|
+
if (params.disableSubDelegation) {
|
|
27040
|
+
throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
|
|
27041
|
+
}
|
|
26493
27042
|
if (!this.signer) {
|
|
26494
27043
|
throw new Error("Cannot createDelegation() in session-only mode. Requires wallet mode.");
|
|
26495
27044
|
}
|
|
@@ -26545,7 +27094,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26545
27094
|
spaceId: params.spaceIdOverride ?? session.spaceId,
|
|
26546
27095
|
path: params.path,
|
|
26547
27096
|
actions: params.actions,
|
|
26548
|
-
disableSubDelegation:
|
|
27097
|
+
disableSubDelegation: false,
|
|
26549
27098
|
expiry: expirationTime,
|
|
26550
27099
|
delegateDID: params.delegateDID,
|
|
26551
27100
|
ownerAddress: session.address,
|
|
@@ -26588,7 +27137,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26588
27137
|
spaceId: publicSpaceId,
|
|
26589
27138
|
path: "",
|
|
26590
27139
|
actions: [import_sdk_core9.KV.GET, import_sdk_core9.KV.PUT, import_sdk_core9.KV.METADATA],
|
|
26591
|
-
disableSubDelegation:
|
|
27140
|
+
disableSubDelegation: false,
|
|
26592
27141
|
expiry: expirationTime,
|
|
26593
27142
|
delegateDID: params.delegateDID,
|
|
26594
27143
|
ownerAddress: session.address,
|
|
@@ -26755,6 +27304,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26755
27304
|
if (parentDelegation.disableSubDelegation) {
|
|
26756
27305
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
26757
27306
|
}
|
|
27307
|
+
if (params.disableSubDelegation) {
|
|
27308
|
+
throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
|
|
27309
|
+
}
|
|
26758
27310
|
const parentResources = parentDelegation.resources ?? [{
|
|
26759
27311
|
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
26760
27312
|
space: parentDelegation.spaceId,
|
|
@@ -26827,7 +27379,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26827
27379
|
spaceId: parentDelegation.spaceId,
|
|
26828
27380
|
path: requestedResources[0].path,
|
|
26829
27381
|
actions: requestedResources[0].actions,
|
|
26830
|
-
disableSubDelegation:
|
|
27382
|
+
disableSubDelegation: false,
|
|
26831
27383
|
expiry: actualExpiry,
|
|
26832
27384
|
delegateDID: params.delegateDID,
|
|
26833
27385
|
ownerAddress: parentDelegation.ownerAddress,
|
|
@@ -26862,9 +27414,16 @@ TinyCloudNode.registerNodeDefaults({
|
|
|
26862
27414
|
});
|
|
26863
27415
|
|
|
26864
27416
|
// src/replication/node-loader.ts
|
|
27417
|
+
init_config();
|
|
26865
27418
|
registerReplicationLoaders({
|
|
27419
|
+
validateConfig: assertValidReplicationConfig,
|
|
26866
27420
|
runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
|
|
26867
|
-
authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
|
|
27421
|
+
authority: () => Promise.resolve().then(() => (init_authority(), authority_exports)),
|
|
27422
|
+
gatedNetwork: () => Promise.resolve().then(() => (init_gated_network(), gated_network_exports)),
|
|
27423
|
+
// Lazy by contract: replica loads only when replication is enabled. Literal specifier typed by
|
|
27424
|
+
// ProfileGateModule; TS2307 under moduleResolution "node" is suppressed (see replica-contract.ts).
|
|
27425
|
+
// @ts-ignore
|
|
27426
|
+
gate: () => import("@tinycloud/replica/profile-gate")
|
|
26868
27427
|
});
|
|
26869
27428
|
|
|
26870
27429
|
// src/index.ts
|
|
@@ -27050,12 +27609,515 @@ var import_sdk_core26 = require("@tinycloud/sdk-core");
|
|
|
27050
27609
|
var import_sdk_core27 = require("@tinycloud/sdk-core");
|
|
27051
27610
|
|
|
27052
27611
|
// src/replication/sqlite.ts
|
|
27612
|
+
var import_node_async_hooks = require("async_hooks");
|
|
27053
27613
|
var import_node_crypto2 = require("crypto");
|
|
27614
|
+
var import_node_fs = require("fs");
|
|
27054
27615
|
var import_promises = require("fs/promises");
|
|
27055
27616
|
var import_node_path = require("path");
|
|
27056
27617
|
var import_sdk_core13 = require("@tinycloud/sdk-core");
|
|
27057
27618
|
var import_sdk_services3 = require("@tinycloud/sdk-services");
|
|
27619
|
+
var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
|
|
27058
27620
|
init_authority();
|
|
27621
|
+
var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
|
|
27622
|
+
function assertProfileLogoutClear(profileDir) {
|
|
27623
|
+
assertLogoutClear((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
27624
|
+
}
|
|
27625
|
+
function assertLogoutClear(path) {
|
|
27626
|
+
if (path === void 0) return;
|
|
27627
|
+
try {
|
|
27628
|
+
(0, import_node_fs.lstatSync)(path);
|
|
27629
|
+
} catch (error) {
|
|
27630
|
+
if (isNotFound(error)) return;
|
|
27631
|
+
}
|
|
27632
|
+
throw gated("gate_unavailable");
|
|
27633
|
+
}
|
|
27634
|
+
function safeInventoryPath(path) {
|
|
27635
|
+
return path.length > 0 && path.split("/").every((part) => part.length > 0 && part !== "." && part !== ".." && !part.includes("\\") && !part.includes("\0")) && (path === "replication" || path.startsWith("replication/") || path === "replicas" || path.startsWith("replicas/"));
|
|
27636
|
+
}
|
|
27637
|
+
function logoutPrincipalDigest(principal) {
|
|
27638
|
+
return (0, import_node_crypto2.createHash)("sha256").update("tinycloud-replica-logout\0").update(principal, "utf8").digest("hex");
|
|
27639
|
+
}
|
|
27640
|
+
function compareInventoryString(left, right) {
|
|
27641
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
27642
|
+
}
|
|
27643
|
+
function approvalEntries(report) {
|
|
27644
|
+
return [
|
|
27645
|
+
...report.unknownEntries,
|
|
27646
|
+
...report.knownEntries,
|
|
27647
|
+
...report.knownRoots.map((root) => ({ ...root, kind: "directory" })),
|
|
27648
|
+
...report.missingDatabases.map((path) => ({ path, kind: "missing", dev: "", ino: "" }))
|
|
27649
|
+
].map(({ path, kind, dev, ino }) => ({ path, kind, dev, ino })).sort((a, b) => compareInventoryString(a.path, b.path) || compareInventoryString(a.kind, b.kind) || compareInventoryString(a.dev, b.dev) || compareInventoryString(a.ino, b.ino));
|
|
27650
|
+
}
|
|
27651
|
+
function inventoryApprovalToken(marker, principal, entries) {
|
|
27652
|
+
const markerId = marker.v === 2 ? [marker.v, marker.nonce, marker.principalDigest] : [marker.v, marker.incarnation, marker.lifecycleEpoch, marker.principalDigest];
|
|
27653
|
+
return (0, import_node_crypto2.createHash)("sha256").update("tc-logout-approve-v1\0").update(JSON.stringify([
|
|
27654
|
+
markerId,
|
|
27655
|
+
marker.approvalSalt,
|
|
27656
|
+
principal,
|
|
27657
|
+
marker.v === 2 ? null : marker.lifecycleEpoch,
|
|
27658
|
+
entries
|
|
27659
|
+
])).digest().subarray(0, 16).toString("hex");
|
|
27660
|
+
}
|
|
27661
|
+
function changedInventoryRefusal(marker, principal, report, entries = approvalEntries(report)) {
|
|
27662
|
+
const token = inventoryApprovalToken(marker, principal, entries);
|
|
27663
|
+
const error = localChangesRefusal(report.unknownPaths);
|
|
27664
|
+
error.message += ` Current token-bound inventory (path/kind/dev/ino): ${entries.length === 0 ? "(empty)" : entries.map((entry) => JSON.stringify(entry)).join(", ")}. To approve this exact changed inventory in a separate command, use --discard-local-changes --approve-inventory ${token}.`;
|
|
27665
|
+
return Object.assign(error, { inventoryApprovalToken: token, inventoryApprovalEntries: entries });
|
|
27666
|
+
}
|
|
27667
|
+
function storedInventoryChanged(marker, report) {
|
|
27668
|
+
try {
|
|
27669
|
+
assertApprovedInventory(report, {
|
|
27670
|
+
unknownEntries: marker.approvedUnknown,
|
|
27671
|
+
knownRoots: marker.knownRoots,
|
|
27672
|
+
knownEntries: marker.knownEntries,
|
|
27673
|
+
missingDatabases: marker.missingDatabases
|
|
27674
|
+
});
|
|
27675
|
+
return false;
|
|
27676
|
+
} catch (error) {
|
|
27677
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
|
|
27678
|
+
return true;
|
|
27679
|
+
}
|
|
27680
|
+
throw error;
|
|
27681
|
+
}
|
|
27682
|
+
}
|
|
27683
|
+
function decimalIdentity(value) {
|
|
27684
|
+
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
27685
|
+
}
|
|
27686
|
+
function logoutMarkerOf(value) {
|
|
27687
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw gated("gate_unavailable");
|
|
27688
|
+
const row = value;
|
|
27689
|
+
const entry = (item) => item !== null && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 4 && typeof item.path === "string" && safeInventoryPath(item.path) && ["file", "directory", "symlink"].includes(item.kind) && decimalIdentity(item.dev) && decimalIdentity(item.ino);
|
|
27690
|
+
const root = (item) => item !== null && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 3 && typeof item.path === "string" && safeInventoryPath(item.path) && decimalIdentity(item.dev) && decimalIdentity(item.ino);
|
|
27691
|
+
if (typeof row.principalDigest !== "string" || !/^[0-9a-f]{64}$/.test(row.principalDigest) || typeof row.approvalSalt !== "string" || !/^[0-9a-f]{32}$/.test(row.approvalSalt) || !Array.isArray(row.approvedUnknown) || !row.approvedUnknown.every(entry) || !Array.isArray(row.knownRoots) || !row.knownRoots.every(root) || !Array.isArray(row.knownEntries) || !row.knownEntries.every(entry) || !Array.isArray(row.missingDatabases) || !row.missingDatabases.every(
|
|
27692
|
+
(path) => typeof path === "string" && safeInventoryPath(path) && path.endsWith("/replica.db")
|
|
27693
|
+
) || row.discardLocalChanges !== void 0 && row.discardLocalChanges !== true || row.remotePending !== void 0 && row.remotePending !== true || row.remoteAbandoned !== void 0 && row.remoteAbandoned !== true || row.remotePending === true && row.remoteAbandoned === true) throw gated("gate_unavailable");
|
|
27694
|
+
const optional = Number(row.discardLocalChanges === true) + Number(row.remotePending === true) + Number(row.remoteAbandoned === true);
|
|
27695
|
+
if (row.v === 2 && Object.keys(row).length === 9 + optional && row.scope === "orphan_profile" && typeof row.nonce === "string" && /^[0-9a-f]{32}$/.test(row.nonce)) return row;
|
|
27696
|
+
if (row.v === 3 && Object.keys(row).length === 10 + optional && row.scope === "inactive_gate" && typeof row.incarnation === "string" && row.incarnation.length > 0 && Number.isSafeInteger(row.lifecycleEpoch) && row.lifecycleEpoch >= 0) {
|
|
27697
|
+
return row;
|
|
27698
|
+
}
|
|
27699
|
+
const bound = Number(row.deniedEpoch !== void 0);
|
|
27700
|
+
if (Object.keys(row).length !== 10 + optional + bound || row.v !== 1 || typeof row.incarnation !== "string" || row.incarnation.length === 0 || !Number.isSafeInteger(row.lifecycleEpoch) || row.lifecycleEpoch < 0 || typeof row.principalHash !== "string" || !/^[0-9a-f]{64}$/.test(row.principalHash) || bound === 1 && (!Number.isSafeInteger(row.deniedEpoch) || row.deniedEpoch <= row.lifecycleEpoch)) {
|
|
27701
|
+
throw gated("gate_unavailable");
|
|
27702
|
+
}
|
|
27703
|
+
return row;
|
|
27704
|
+
}
|
|
27705
|
+
async function readLogoutMarker(path) {
|
|
27706
|
+
const value = await readJsonFile(path);
|
|
27707
|
+
return value === void 0 ? null : logoutMarkerOf(value);
|
|
27708
|
+
}
|
|
27709
|
+
async function orphanRoot(profileDir, name) {
|
|
27710
|
+
let profile;
|
|
27711
|
+
try {
|
|
27712
|
+
profile = await (0, import_promises.lstat)(profileDir);
|
|
27713
|
+
} catch {
|
|
27714
|
+
throw gated("gate_unavailable");
|
|
27715
|
+
}
|
|
27716
|
+
let pinned = false;
|
|
27717
|
+
if (process.platform === "linux" && /^\/proc\/self\/fd\/[1-9]\d*$/.test(profileDir) && profile.isSymbolicLink()) {
|
|
27718
|
+
try {
|
|
27719
|
+
pinned = (await (0, import_promises.stat)(profileDir)).isDirectory();
|
|
27720
|
+
} catch {
|
|
27721
|
+
throw gated("gate_unavailable");
|
|
27722
|
+
}
|
|
27723
|
+
}
|
|
27724
|
+
if (!profile.isDirectory() && !pinned) throw gated("gate_unavailable");
|
|
27725
|
+
const root = (0, import_node_path.join)(profileDir, name);
|
|
27726
|
+
let entry;
|
|
27727
|
+
try {
|
|
27728
|
+
entry = await (0, import_promises.lstat)(root);
|
|
27729
|
+
} catch (error) {
|
|
27730
|
+
if (isNotFound(error)) return null;
|
|
27731
|
+
throw error;
|
|
27732
|
+
}
|
|
27733
|
+
if (!entry.isDirectory() || await (0, import_promises.realpath)(root) !== (0, import_node_path.join)(await (0, import_promises.realpath)(profileDir), name)) {
|
|
27734
|
+
throw gated("gate_unavailable");
|
|
27735
|
+
}
|
|
27736
|
+
return root;
|
|
27737
|
+
}
|
|
27738
|
+
async function hasOrphanPending(profileDir) {
|
|
27739
|
+
const root = await orphanRoot(profileDir, "replication");
|
|
27740
|
+
if (root === null) return false;
|
|
27741
|
+
let entries;
|
|
27742
|
+
try {
|
|
27743
|
+
entries = await (0, import_promises.readdir)(root, { withFileTypes: true });
|
|
27744
|
+
} catch {
|
|
27745
|
+
return true;
|
|
27746
|
+
}
|
|
27747
|
+
for (const entry of entries) {
|
|
27748
|
+
if (!entry.isDirectory()) return true;
|
|
27749
|
+
const dir = (0, import_node_path.join)(root, entry.name);
|
|
27750
|
+
try {
|
|
27751
|
+
if (!(await (0, import_promises.lstat)(dir)).isDirectory()) return true;
|
|
27752
|
+
} catch {
|
|
27753
|
+
return true;
|
|
27754
|
+
}
|
|
27755
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) return true;
|
|
27756
|
+
let children;
|
|
27757
|
+
try {
|
|
27758
|
+
children = await (0, import_promises.readdir)(dir, { withFileTypes: true });
|
|
27759
|
+
} catch {
|
|
27760
|
+
return true;
|
|
27761
|
+
}
|
|
27762
|
+
if (children.length !== 2 || children.some((child) => !child.isFile() || child.name !== "identity.json" && child.name !== "pending.json")) return true;
|
|
27763
|
+
const pendingFile = (0, import_node_path.join)(dir, "pending.json");
|
|
27764
|
+
let text;
|
|
27765
|
+
try {
|
|
27766
|
+
const pending = await (0, import_promises.lstat)(pendingFile);
|
|
27767
|
+
if (!pending.isFile()) return true;
|
|
27768
|
+
text = await (0, import_promises.readFile)(pendingFile, "utf8");
|
|
27769
|
+
} catch (error) {
|
|
27770
|
+
if (isNotFound(error)) return true;
|
|
27771
|
+
return true;
|
|
27772
|
+
}
|
|
27773
|
+
try {
|
|
27774
|
+
const identityFile = (0, import_node_path.join)(dir, "identity.json");
|
|
27775
|
+
if (!(await (0, import_promises.lstat)(identityFile)).isFile()) return true;
|
|
27776
|
+
const identity = identityRecordOf(await readJsonFile(identityFile));
|
|
27777
|
+
if (identity === void 0 || parsePendingState(text, pendingFile, identity).records.length !== 0) {
|
|
27778
|
+
return true;
|
|
27779
|
+
}
|
|
27780
|
+
} catch {
|
|
27781
|
+
return true;
|
|
27782
|
+
}
|
|
27783
|
+
}
|
|
27784
|
+
return false;
|
|
27785
|
+
}
|
|
27786
|
+
function inventoryIdentity(path, dev, ino) {
|
|
27787
|
+
return { path, dev: dev.toString(), ino: ino.toString() };
|
|
27788
|
+
}
|
|
27789
|
+
async function inventoryUnknown(profileDir, relative, entries) {
|
|
27790
|
+
const path = (0, import_node_path.join)(profileDir, relative);
|
|
27791
|
+
const info = await (0, import_promises.lstat)(path, { bigint: true });
|
|
27792
|
+
const kind = info.isDirectory() ? "directory" : info.isSymbolicLink() ? "symlink" : "file";
|
|
27793
|
+
entries.push({ ...inventoryIdentity(relative, info.dev, info.ino), kind });
|
|
27794
|
+
if (kind === "directory") {
|
|
27795
|
+
for (const child of await (0, import_promises.readdir)(path)) await inventoryUnknown(profileDir, `${relative}/${child}`, entries);
|
|
27796
|
+
}
|
|
27797
|
+
}
|
|
27798
|
+
async function inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly = false, diagnosticFiles = []) {
|
|
27799
|
+
const report = {
|
|
27800
|
+
knownLegacyReplicas: 0,
|
|
27801
|
+
unknownLegacyPaths: [],
|
|
27802
|
+
unknownPaths: [],
|
|
27803
|
+
unknownEntries: [],
|
|
27804
|
+
knownRoots: [],
|
|
27805
|
+
knownEntries: [],
|
|
27806
|
+
missingDatabases: []
|
|
27807
|
+
};
|
|
27808
|
+
const legacy = await orphanRoot(profileDir, "replicas");
|
|
27809
|
+
const active = legacyOnly ? null : await orphanRoot(profileDir, "replication");
|
|
27810
|
+
for (const [name, dir] of [["replicas", legacy], ["replication", active]]) {
|
|
27811
|
+
if (dir === null) continue;
|
|
27812
|
+
const info = await (0, import_promises.lstat)(dir, { bigint: true });
|
|
27813
|
+
report.knownRoots.push(inventoryIdentity(name, info.dev, info.ino));
|
|
27814
|
+
}
|
|
27815
|
+
if (legacy !== null) {
|
|
27816
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
27817
|
+
for (const entry of await (0, import_promises.readdir)(legacy, { withFileTypes: true })) {
|
|
27818
|
+
const relative = `replicas/${entry.name}`;
|
|
27819
|
+
const child = (0, import_node_path.join)(legacy, entry.name);
|
|
27820
|
+
if (!entry.isDirectory()) {
|
|
27821
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27822
|
+
continue;
|
|
27823
|
+
}
|
|
27824
|
+
try {
|
|
27825
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(child);
|
|
27826
|
+
report.knownRoots.push({ path: relative, ...layout.rootIdentity });
|
|
27827
|
+
report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
|
|
27828
|
+
...item,
|
|
27829
|
+
path: `${relative}/${item.path}`
|
|
27830
|
+
})));
|
|
27831
|
+
report.knownEntries.push(...layout.knownEntries.map((item) => ({
|
|
27832
|
+
...item,
|
|
27833
|
+
path: `${relative}/${item.path}`
|
|
27834
|
+
})));
|
|
27835
|
+
if (layout.unknownPaths.length === 0) report.knownLegacyReplicas++;
|
|
27836
|
+
if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
27837
|
+
report.missingDatabases.push(`${relative}/replica.db`);
|
|
27838
|
+
}
|
|
27839
|
+
} catch {
|
|
27840
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27841
|
+
}
|
|
27842
|
+
}
|
|
27843
|
+
}
|
|
27844
|
+
if (active !== null) {
|
|
27845
|
+
const cleanOrphan = !gateReadable && (approvedMarker?.v === 2 || !await hasOrphanPending(profileDir));
|
|
27846
|
+
for (const entry of await (0, import_promises.readdir)(active, { withFileTypes: true })) {
|
|
27847
|
+
const relative = `replication/${entry.name}`;
|
|
27848
|
+
const dir = (0, import_node_path.join)(active, entry.name);
|
|
27849
|
+
if (approvedMarker?.approvedUnknown.some((item) => item.path === relative)) {
|
|
27850
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27851
|
+
continue;
|
|
27852
|
+
}
|
|
27853
|
+
if (gateReadable && diagnosticFiles.includes(entry.name)) {
|
|
27854
|
+
const info2 = await (0, import_promises.lstat)(dir, { bigint: true });
|
|
27855
|
+
if (info2.isFile()) {
|
|
27856
|
+
report.knownEntries.push({ ...inventoryIdentity(relative, info2.dev, info2.ino), kind: "file" });
|
|
27857
|
+
continue;
|
|
27858
|
+
}
|
|
27859
|
+
}
|
|
27860
|
+
if (!gateReadable && !cleanOrphan || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
27861
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27862
|
+
continue;
|
|
27863
|
+
}
|
|
27864
|
+
const info = await (0, import_promises.lstat)(dir, { bigint: true });
|
|
27865
|
+
report.knownRoots.push(inventoryIdentity(relative, info.dev, info.ino));
|
|
27866
|
+
const identityPath = (0, import_node_path.join)(dir, "identity.json");
|
|
27867
|
+
let identity;
|
|
27868
|
+
try {
|
|
27869
|
+
if ((await (0, import_promises.lstat)(identityPath)).isFile()) identity = identityRecordOf(await readJsonFile(identityPath));
|
|
27870
|
+
} catch {
|
|
27871
|
+
}
|
|
27872
|
+
if (identity === void 0) {
|
|
27873
|
+
const approvedRoot = approvedMarker?.knownRoots.find((item) => item.path === relative);
|
|
27874
|
+
if (approvedRoot?.dev === info.dev.toString() && approvedRoot.ino === info.ino.toString()) {
|
|
27875
|
+
const children = await (0, import_promises.readdir)(dir);
|
|
27876
|
+
if (children.length === 0) continue;
|
|
27877
|
+
if (children.length === 1 && children[0] === "replicas") {
|
|
27878
|
+
const replicasPath = `${relative}/replicas`;
|
|
27879
|
+
const replicasDir = (0, import_node_path.join)(dir, "replicas");
|
|
27880
|
+
const replicasInfo = await (0, import_promises.lstat)(replicasDir, { bigint: true });
|
|
27881
|
+
const approvedReplicas = approvedMarker.knownRoots.find((item) => item.path === replicasPath);
|
|
27882
|
+
if (replicasInfo.isDirectory() && !replicasInfo.isSymbolicLink() && approvedReplicas?.dev === replicasInfo.dev.toString() && approvedReplicas.ino === replicasInfo.ino.toString() && (await (0, import_promises.readdir)(replicasDir)).length === 0) {
|
|
27883
|
+
report.knownRoots.push(inventoryIdentity(replicasPath, replicasInfo.dev, replicasInfo.ino));
|
|
27884
|
+
continue;
|
|
27885
|
+
}
|
|
27886
|
+
}
|
|
27887
|
+
}
|
|
27888
|
+
report.knownRoots.pop();
|
|
27889
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27890
|
+
continue;
|
|
27891
|
+
}
|
|
27892
|
+
for (const child of await (0, import_promises.readdir)(dir, { withFileTypes: true })) {
|
|
27893
|
+
const childRelative = `${relative}/${child.name}`;
|
|
27894
|
+
const childPath = (0, import_node_path.join)(dir, child.name);
|
|
27895
|
+
if ((child.name === "identity.json" || child.name === "device.jwk") && child.isFile()) {
|
|
27896
|
+
const childInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
|
|
27897
|
+
report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
|
|
27898
|
+
continue;
|
|
27899
|
+
}
|
|
27900
|
+
if (child.name === "pending.json" && child.isFile()) {
|
|
27901
|
+
try {
|
|
27902
|
+
if (parsePendingState(await (0, import_promises.readFile)(childPath, "utf8"), childPath, identity).records.length === 0) {
|
|
27903
|
+
const childInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
|
|
27904
|
+
report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
|
|
27905
|
+
continue;
|
|
27906
|
+
}
|
|
27907
|
+
} catch {
|
|
27908
|
+
}
|
|
27909
|
+
}
|
|
27910
|
+
if (child.name === "replicas" && child.isDirectory()) {
|
|
27911
|
+
const replicasInfo = await (0, import_promises.lstat)(childPath, { bigint: true });
|
|
27912
|
+
report.knownRoots.push(inventoryIdentity(childRelative, replicasInfo.dev, replicasInfo.ino));
|
|
27913
|
+
for (const replicaChild of await (0, import_promises.readdir)(childPath, { withFileTypes: true })) {
|
|
27914
|
+
const replicaRelative = `${childRelative}/${replicaChild.name}`;
|
|
27915
|
+
if (!replicaChild.isDirectory() || !/^[a-z2-7]{26}$/.test(replicaChild.name)) {
|
|
27916
|
+
await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
|
|
27917
|
+
continue;
|
|
27918
|
+
}
|
|
27919
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
27920
|
+
try {
|
|
27921
|
+
const layout = await sqlite.inspectSqliteReplicaLayout((0, import_node_path.join)(childPath, replicaChild.name), {
|
|
27922
|
+
allowAdapterFence: true
|
|
27923
|
+
});
|
|
27924
|
+
report.knownRoots.push({ path: replicaRelative, ...layout.rootIdentity });
|
|
27925
|
+
report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
|
|
27926
|
+
...item,
|
|
27927
|
+
path: `${replicaRelative}/${item.path}`
|
|
27928
|
+
})));
|
|
27929
|
+
report.knownEntries.push(...layout.knownEntries.map((item) => ({
|
|
27930
|
+
...item,
|
|
27931
|
+
path: `${replicaRelative}/${item.path}`
|
|
27932
|
+
})));
|
|
27933
|
+
if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
27934
|
+
report.missingDatabases.push(`${replicaRelative}/replica.db`);
|
|
27935
|
+
}
|
|
27936
|
+
} catch {
|
|
27937
|
+
await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
|
|
27938
|
+
}
|
|
27939
|
+
}
|
|
27940
|
+
continue;
|
|
27941
|
+
}
|
|
27942
|
+
await inventoryUnknown(profileDir, childRelative, report.unknownEntries);
|
|
27943
|
+
}
|
|
27944
|
+
}
|
|
27945
|
+
}
|
|
27946
|
+
report.unknownEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
27947
|
+
report.knownEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
27948
|
+
report.knownRoots.sort((a, b) => a.path.localeCompare(b.path));
|
|
27949
|
+
report.missingDatabases.sort();
|
|
27950
|
+
report.unknownPaths = [...report.unknownEntries.map((entry) => entry.path), ...report.missingDatabases].sort();
|
|
27951
|
+
report.unknownLegacyPaths = [...report.unknownPaths];
|
|
27952
|
+
return report;
|
|
27953
|
+
}
|
|
27954
|
+
function assertApprovedInventory(current, approval, allowRemoved = false) {
|
|
27955
|
+
const approved = new Map(approval.unknownEntries.map((item) => [item.path, item]));
|
|
27956
|
+
const known = new Map(approval.knownRoots.map((item) => [item.path, item]));
|
|
27957
|
+
const knownFiles = new Map(approval.knownEntries.map((item) => [item.path, item]));
|
|
27958
|
+
for (const entry of current.unknownEntries) {
|
|
27959
|
+
const expected = approved.get(entry.path);
|
|
27960
|
+
if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
|
|
27961
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
27962
|
+
}
|
|
27963
|
+
}
|
|
27964
|
+
for (const root of current.knownRoots) {
|
|
27965
|
+
const expected = known.get(root.path);
|
|
27966
|
+
if (expected?.dev !== root.dev || expected.ino !== root.ino) throw localChangesRefusal([root.path]);
|
|
27967
|
+
}
|
|
27968
|
+
for (const entry of current.knownEntries) {
|
|
27969
|
+
if (allowRemoved && entry.kind === "file" && /^(?:replication\/[a-z2-7]{26}\/replicas\/[a-z2-7]{26}|replicas\/[^/]+)\/replica\.db-(?:wal|shm|journal)$/.test(entry.path)) {
|
|
27970
|
+
const database = entry.path.replace(/-(?:wal|shm|journal)$/, "");
|
|
27971
|
+
const original = knownFiles.get(database);
|
|
27972
|
+
const present = current.knownEntries.find((item) => item.path === database);
|
|
27973
|
+
if (!approved.has(entry.path) && original?.kind === "file" && present?.dev === original.dev && present.ino === original.ino) continue;
|
|
27974
|
+
}
|
|
27975
|
+
const expected = knownFiles.get(entry.path);
|
|
27976
|
+
if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
|
|
27977
|
+
throw localChangesRefusal([entry.path]);
|
|
27978
|
+
}
|
|
27979
|
+
}
|
|
27980
|
+
for (const path of current.missingDatabases) {
|
|
27981
|
+
if (!approval.missingDatabases.includes(path) && !(allowRemoved && knownFiles.get(path)?.kind === "file")) throw localChangesRefusal([path]);
|
|
27982
|
+
}
|
|
27983
|
+
if (!allowRemoved && (current.unknownEntries.length !== approved.size || current.knownRoots.length !== known.size || current.knownEntries.length !== knownFiles.size || current.missingDatabases.length !== approval.missingDatabases.length)) {
|
|
27984
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
27985
|
+
}
|
|
27986
|
+
}
|
|
27987
|
+
async function assertApprovedParents(profileDir, marker, path) {
|
|
27988
|
+
if (process.platform !== "linux") {
|
|
27989
|
+
const absolute = (0, import_node_path.resolve)(profileDir);
|
|
27990
|
+
let ancestor = (0, import_node_path.parse)(absolute).root;
|
|
27991
|
+
for (const part of absolute.slice(ancestor.length).split(import_node_path.sep).filter(Boolean)) {
|
|
27992
|
+
ancestor = (0, import_node_path.join)(ancestor, part);
|
|
27993
|
+
const details = await (0, import_promises.lstat)(ancestor, { bigint: true });
|
|
27994
|
+
if (!details.isDirectory() || details.isSymbolicLink()) throw localChangesRefusal([path]);
|
|
27995
|
+
}
|
|
27996
|
+
}
|
|
27997
|
+
let relativeParent = "";
|
|
27998
|
+
for (const part of path.split("/").slice(0, -1)) {
|
|
27999
|
+
relativeParent = relativeParent ? `${relativeParent}/${part}` : part;
|
|
28000
|
+
let details;
|
|
28001
|
+
try {
|
|
28002
|
+
details = await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, relativeParent), { bigint: true });
|
|
28003
|
+
} catch (error) {
|
|
28004
|
+
if (isNotFound(error)) return false;
|
|
28005
|
+
throw error;
|
|
28006
|
+
}
|
|
28007
|
+
const expected = marker.knownRoots.find((root) => root.path === relativeParent) ?? marker.approvedUnknown.find((item) => item.path === relativeParent && item.kind === "directory");
|
|
28008
|
+
if (!details.isDirectory() || details.isSymbolicLink() || expected?.dev !== details.dev.toString() || expected.ino !== details.ino.toString()) {
|
|
28009
|
+
throw localChangesRefusal([relativeParent]);
|
|
28010
|
+
}
|
|
28011
|
+
}
|
|
28012
|
+
return true;
|
|
28013
|
+
}
|
|
28014
|
+
async function eraseApprovedUnknown(profileDir, marker, prefix, diagnosticFiles) {
|
|
28015
|
+
const entries = marker.approvedUnknown.filter((entry) => entry.path.startsWith(`${prefix}/`));
|
|
28016
|
+
for (const entry of entries.sort((a, b) => b.path.length - a.path.length)) {
|
|
28017
|
+
assertApprovedInventory(await inspectProfileReplicaLayout(profileDir, marker.v !== 2, marker, false, diagnosticFiles), {
|
|
28018
|
+
unknownEntries: marker.approvedUnknown,
|
|
28019
|
+
knownRoots: marker.knownRoots,
|
|
28020
|
+
knownEntries: marker.knownEntries,
|
|
28021
|
+
missingDatabases: marker.missingDatabases
|
|
28022
|
+
}, true);
|
|
28023
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28024
|
+
const file = (0, import_node_path.join)(profileDir, entry.path);
|
|
28025
|
+
let current;
|
|
28026
|
+
try {
|
|
28027
|
+
current = await (0, import_promises.lstat)(file, { bigint: true });
|
|
28028
|
+
} catch (error) {
|
|
28029
|
+
if (isNotFound(error)) continue;
|
|
28030
|
+
throw error;
|
|
28031
|
+
}
|
|
28032
|
+
const kind = current.isDirectory() ? "directory" : current.isSymbolicLink() ? "symlink" : "file";
|
|
28033
|
+
if (kind !== entry.kind || current.dev.toString() !== entry.dev || current.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
|
|
28034
|
+
if (process.platform !== "linux" && kind === "symlink") throw localChangesRefusal([entry.path]);
|
|
28035
|
+
if (kind === "directory") await (0, import_promises.rmdir)(file);
|
|
28036
|
+
else await (0, import_promises.rm)(file);
|
|
28037
|
+
}
|
|
28038
|
+
}
|
|
28039
|
+
async function eraseKnownFlagResidue(profileDir, marker, diagnosticFiles) {
|
|
28040
|
+
const diagnostics = new Set(diagnosticFiles.map((name) => `replication/${name}`));
|
|
28041
|
+
for (const entry of marker.knownEntries.filter((item) => diagnostics.has(item.path) || /^replication\/[a-z2-7]{26}\/(?:identity\.json|pending\.json|device\.jwk)$/.test(item.path)).sort((a, b) => Number(a.path.endsWith("/identity.json")) - Number(b.path.endsWith("/identity.json")))) {
|
|
28042
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28043
|
+
const file = (0, import_node_path.join)(profileDir, entry.path);
|
|
28044
|
+
let details;
|
|
28045
|
+
try {
|
|
28046
|
+
details = await (0, import_promises.lstat)(file, { bigint: true });
|
|
28047
|
+
} catch (error) {
|
|
28048
|
+
if (isNotFound(error)) continue;
|
|
28049
|
+
throw error;
|
|
28050
|
+
}
|
|
28051
|
+
if (!details.isFile() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
|
|
28052
|
+
await (0, import_promises.rm)(file);
|
|
28053
|
+
}
|
|
28054
|
+
const roots = marker.knownRoots.filter((item) => item.path === "replication" || item.path.startsWith("replication/"));
|
|
28055
|
+
roots.sort((a, b) => b.path.split("/").length - a.path.split("/").length);
|
|
28056
|
+
for (const entry of roots) {
|
|
28057
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28058
|
+
const path = (0, import_node_path.join)(profileDir, entry.path);
|
|
28059
|
+
let details;
|
|
28060
|
+
try {
|
|
28061
|
+
details = await (0, import_promises.lstat)(path, { bigint: true });
|
|
28062
|
+
} catch (error) {
|
|
28063
|
+
if (isNotFound(error)) continue;
|
|
28064
|
+
throw error;
|
|
28065
|
+
}
|
|
28066
|
+
if (!details.isDirectory() || details.isSymbolicLink() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) {
|
|
28067
|
+
throw localChangesRefusal([entry.path]);
|
|
28068
|
+
}
|
|
28069
|
+
await (0, import_promises.rmdir)(path);
|
|
28070
|
+
}
|
|
28071
|
+
}
|
|
28072
|
+
function localChangesRefusal(unknownPaths = []) {
|
|
28073
|
+
const error = gated("gate_unavailable");
|
|
28074
|
+
error.message = "Unknown or unsynced local replica data may be lost; sync first, or retry logout with --discard-local-changes." + (unknownPaths.length === 0 ? "" : ` Unknown: ${unknownPaths.map((path) => JSON.stringify(path)).join(", ")}.`);
|
|
28075
|
+
return error;
|
|
28076
|
+
}
|
|
28077
|
+
function gateOwnedByAnother(state, principalHash) {
|
|
28078
|
+
return state.principalHash !== null ? state.principalHash !== principalHash : state.eraseDeferred && state.deferredPrincipalHash !== principalHash;
|
|
28079
|
+
}
|
|
28080
|
+
function isGateUnavailable(error) {
|
|
28081
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable";
|
|
28082
|
+
}
|
|
28083
|
+
function restoredGate() {
|
|
28084
|
+
const error = gated("stamp_stale");
|
|
28085
|
+
error.message = RESTORED_GATE_MESSAGE;
|
|
28086
|
+
return Object.assign(error, { reportCode: "gate_restored" });
|
|
28087
|
+
}
|
|
28088
|
+
var RESTORED_GATE_MESSAGE = "The replica gate does not match this logout's recorded sign-out; an older gate may have been restored. Local replicas stay disabled for this profile: switch the default profile first if needed (`tc profile switch <other>`), then run `tc profile delete <name>` and sign in again.";
|
|
28089
|
+
function interruptedSignOut() {
|
|
28090
|
+
const error = gated("gate_unavailable");
|
|
28091
|
+
error.message = "A previous sign-out was interrupted; re-running `tc auth logout` completes it.";
|
|
28092
|
+
return error;
|
|
28093
|
+
}
|
|
28094
|
+
function assertSupportedProfileLayout(report) {
|
|
28095
|
+
if (process.platform === "linux") return;
|
|
28096
|
+
const link = report.unknownEntries.find((entry) => entry.kind === "symlink");
|
|
28097
|
+
if (link === void 0) return;
|
|
28098
|
+
const error = gated("gate_unavailable");
|
|
28099
|
+
error.message = `Replica symlink ${JSON.stringify(link.path)} cannot be removed safely on this platform.`;
|
|
28100
|
+
throw error;
|
|
28101
|
+
}
|
|
28102
|
+
async function fileGate() {
|
|
28103
|
+
return import("@tinycloud/replica/profile-gate");
|
|
28104
|
+
}
|
|
28105
|
+
function gated(reason) {
|
|
28106
|
+
return Object.assign(new Error(`Replica gate refused: ${reason}`), { code: "REPLICA_GATED", gate: reason });
|
|
28107
|
+
}
|
|
28108
|
+
async function requiredGuard(guard, section) {
|
|
28109
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
28110
|
+
let entered = false;
|
|
28111
|
+
try {
|
|
28112
|
+
return await guard(async () => {
|
|
28113
|
+
entered = true;
|
|
28114
|
+
return section();
|
|
28115
|
+
});
|
|
28116
|
+
} catch (error) {
|
|
28117
|
+
if (!entered) throw gated("gate_unavailable");
|
|
28118
|
+
throw error;
|
|
28119
|
+
}
|
|
28120
|
+
}
|
|
27059
28121
|
var ReplicaStorageError = class extends Error {
|
|
27060
28122
|
constructor(code, message, options) {
|
|
27061
28123
|
super(message);
|
|
@@ -27081,6 +28143,19 @@ async function pathExists(path) {
|
|
|
27081
28143
|
throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
|
|
27082
28144
|
}
|
|
27083
28145
|
}
|
|
28146
|
+
async function realDirectoryExists(path) {
|
|
28147
|
+
let stats;
|
|
28148
|
+
try {
|
|
28149
|
+
stats = await (0, import_promises.lstat)(path);
|
|
28150
|
+
} catch (error) {
|
|
28151
|
+
if (isNotFound(error)) return false;
|
|
28152
|
+
throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
|
|
28153
|
+
}
|
|
28154
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
28155
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Refusing the replaced replica path ${path}.`);
|
|
28156
|
+
}
|
|
28157
|
+
return true;
|
|
28158
|
+
}
|
|
27084
28159
|
function principalOf(did) {
|
|
27085
28160
|
return (0, import_sdk_core13.principalDid)(did);
|
|
27086
28161
|
}
|
|
@@ -27234,90 +28309,117 @@ function parsePendingState(text, file, identity) {
|
|
|
27234
28309
|
}
|
|
27235
28310
|
return state;
|
|
27236
28311
|
}
|
|
27237
|
-
var _dir, _guard, _dirSync,
|
|
28312
|
+
var _dir, _profileDir, _guard, _stamp, _dirSync, _logoutPendingFile, _generation, _canonicalDir, _FilePendingWriteStore_instances, withGate_fn, readFile_fn;
|
|
27238
28313
|
var FilePendingWriteStore = class {
|
|
27239
|
-
constructor(identity, dir, guard, dirSync) {
|
|
28314
|
+
constructor(identity, dir, profileDir, guard, stamp, dirSync, logoutPendingFile) {
|
|
27240
28315
|
__privateAdd(this, _FilePendingWriteStore_instances);
|
|
27241
28316
|
this.durable = true;
|
|
27242
28317
|
__privateAdd(this, _dir);
|
|
28318
|
+
__privateAdd(this, _profileDir);
|
|
27243
28319
|
__privateAdd(this, _guard);
|
|
28320
|
+
__privateAdd(this, _stamp);
|
|
27244
28321
|
__privateAdd(this, _dirSync);
|
|
27245
|
-
__privateAdd(this,
|
|
28322
|
+
__privateAdd(this, _logoutPendingFile);
|
|
28323
|
+
__privateAdd(this, _generation);
|
|
28324
|
+
__privateAdd(this, _canonicalDir);
|
|
27246
28325
|
this.identity = identity;
|
|
27247
28326
|
__privateSet(this, _dir, dir);
|
|
28327
|
+
__privateSet(this, _profileDir, profileDir);
|
|
27248
28328
|
__privateSet(this, _guard, guard);
|
|
28329
|
+
__privateSet(this, _stamp, stamp);
|
|
27249
28330
|
__privateSet(this, _dirSync, dirSync);
|
|
28331
|
+
__privateSet(this, _logoutPendingFile, logoutPendingFile);
|
|
27250
28332
|
}
|
|
27251
28333
|
async read() {
|
|
27252
|
-
|
|
27253
|
-
let text;
|
|
27254
|
-
try {
|
|
27255
|
-
text = await (0, import_promises.readFile)(file, "utf8");
|
|
27256
|
-
} catch (error) {
|
|
27257
|
-
if (isNotFound(error)) {
|
|
27258
|
-
return { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
|
|
27259
|
-
}
|
|
27260
|
-
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
27261
|
-
}
|
|
27262
|
-
return parsePendingState(text, file, this.identity);
|
|
28334
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] });
|
|
27263
28335
|
}
|
|
27264
28336
|
async readExisting() {
|
|
27265
|
-
|
|
27266
|
-
try {
|
|
27267
|
-
file = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
|
|
27268
|
-
} catch (error) {
|
|
27269
|
-
if (isNotFound(error)) return void 0;
|
|
27270
|
-
throw storageError("STORAGE_ERROR", `Resolving pending-write state in ${__privateGet(this, _dir)}`, error);
|
|
27271
|
-
}
|
|
27272
|
-
try {
|
|
27273
|
-
const text = await (0, import_promises.readFile)(file, "utf8");
|
|
27274
|
-
return parsePendingState(text, file, this.identity);
|
|
27275
|
-
} catch (error) {
|
|
27276
|
-
if (isNotFound(error)) return void 0;
|
|
27277
|
-
if (error instanceof ReplicaStorageError) throw error;
|
|
27278
|
-
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
27279
|
-
}
|
|
28337
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, (file) => __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file));
|
|
27280
28338
|
}
|
|
27281
28339
|
async update(mutate) {
|
|
27282
|
-
|
|
27283
|
-
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
}
|
|
27288
|
-
|
|
27289
|
-
|
|
27290
|
-
|
|
27291
|
-
|
|
27292
|
-
"STORAGE_ERROR",
|
|
27293
|
-
`The pending-write partition changed while open: ${(0, import_node_path.dirname)(file)}.`
|
|
27294
|
-
);
|
|
27295
|
-
}
|
|
27296
|
-
return __privateGet(this, _guard).call(this, async () => {
|
|
27297
|
-
const state = await this.read();
|
|
27298
|
-
const result = mutate(state);
|
|
27299
|
-
await writeJsonAtomic(file, state, __privateGet(this, _dirSync));
|
|
27300
|
-
return result;
|
|
27301
|
-
});
|
|
28340
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => {
|
|
28341
|
+
await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
|
|
28342
|
+
const currentFile = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
|
|
28343
|
+
if (__privateGet(this, _canonicalDir) !== void 0 && (0, import_node_path.dirname)(currentFile) !== __privateGet(this, _canonicalDir)) {
|
|
28344
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
|
|
28345
|
+
}
|
|
28346
|
+
const state = await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, currentFile) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
|
|
28347
|
+
const result = await mutate(state);
|
|
28348
|
+
await writeJsonAtomic(currentFile, state, __privateGet(this, _dirSync));
|
|
28349
|
+
return result;
|
|
27302
28350
|
});
|
|
27303
28351
|
}
|
|
27304
28352
|
};
|
|
27305
28353
|
_dir = new WeakMap();
|
|
28354
|
+
_profileDir = new WeakMap();
|
|
27306
28355
|
_guard = new WeakMap();
|
|
28356
|
+
_stamp = new WeakMap();
|
|
27307
28357
|
_dirSync = new WeakMap();
|
|
27308
|
-
|
|
28358
|
+
_logoutPendingFile = new WeakMap();
|
|
28359
|
+
_generation = new WeakMap();
|
|
28360
|
+
_canonicalDir = new WeakMap();
|
|
27309
28361
|
_FilePendingWriteStore_instances = new WeakSet();
|
|
27310
|
-
|
|
27311
|
-
__privateGet(this,
|
|
27312
|
-
|
|
27313
|
-
|
|
28362
|
+
withGate_fn = async function(section) {
|
|
28363
|
+
if (__privateGet(this, _stamp) === void 0) throw gated("not_established");
|
|
28364
|
+
if (__privateGet(this, _guard) === void 0) throw gated("gate_unavailable");
|
|
28365
|
+
const gate = await fileGate();
|
|
28366
|
+
const snapshot = await gate.readGateSnapshot(__privateGet(this, _profileDir));
|
|
28367
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28368
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28369
|
+
gate.principalHashOf(snapshot.salt, this.identity.principal),
|
|
28370
|
+
gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(this.identity))
|
|
28371
|
+
]);
|
|
28372
|
+
const access = {
|
|
28373
|
+
stamp: __privateGet(this, _stamp),
|
|
28374
|
+
incarnation: snapshot.incarnation,
|
|
28375
|
+
principalHash,
|
|
28376
|
+
identityKeyHash
|
|
28377
|
+
};
|
|
28378
|
+
let canonicalRoot;
|
|
28379
|
+
try {
|
|
28380
|
+
canonicalRoot = await (0, import_promises.realpath)((0, import_node_path.dirname)(__privateGet(this, _dir)));
|
|
28381
|
+
} catch (error) {
|
|
28382
|
+
if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${(0, import_node_path.dirname)(__privateGet(this, _dir))}`, error);
|
|
28383
|
+
canonicalRoot = (0, import_node_path.join)(await (0, import_promises.realpath)((0, import_node_path.dirname)((0, import_node_path.dirname)(__privateGet(this, _dir)))), (0, import_node_path.basename)((0, import_node_path.dirname)(__privateGet(this, _dir))));
|
|
28384
|
+
}
|
|
28385
|
+
const queueKey = (0, import_node_path.join)(canonicalRoot, (0, import_node_path.basename)(__privateGet(this, _dir)), "pending.json");
|
|
28386
|
+
return serializePendingWrite(queueKey, () => requiredGuard(__privateGet(this, _guard), async () => {
|
|
28387
|
+
assertLogoutClear(__privateGet(this, _logoutPendingFile));
|
|
28388
|
+
const before = await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
|
|
28389
|
+
const generation = before.pendingGenerations[identityKeyHash] ?? 0;
|
|
28390
|
+
if (__privateGet(this, _generation) !== void 0 && __privateGet(this, _generation) !== generation) {
|
|
28391
|
+
throw new ReplicaStorageError("RESET_REQUIRED", "The pending identity was erased; reopen the store.");
|
|
28392
|
+
}
|
|
28393
|
+
let file = (0, import_node_path.join)(__privateGet(this, _dir), "pending.json");
|
|
27314
28394
|
try {
|
|
27315
|
-
|
|
28395
|
+
file = (0, import_node_path.join)(await (0, import_promises.realpath)(__privateGet(this, _dir)), "pending.json");
|
|
28396
|
+
const recorded = identityRecordOf(await readJsonFile((0, import_node_path.join)((0, import_node_path.dirname)(file), "identity.json")));
|
|
28397
|
+
if (recorded === void 0 || !sameIdentity(recorded, this.identity)) {
|
|
28398
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending partition belongs to a different identity.");
|
|
28399
|
+
}
|
|
27316
28400
|
} catch (error) {
|
|
27317
|
-
|
|
28401
|
+
if (error instanceof ReplicaStorageError) throw error;
|
|
28402
|
+
if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${__privateGet(this, _dir)}`, error);
|
|
27318
28403
|
}
|
|
27319
|
-
|
|
27320
|
-
|
|
28404
|
+
if (__privateGet(this, _canonicalDir) !== void 0 && (0, import_node_path.dirname)(file) !== __privateGet(this, _canonicalDir)) {
|
|
28405
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
|
|
28406
|
+
}
|
|
28407
|
+
const result = await section(file);
|
|
28408
|
+
await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
|
|
28409
|
+
assertLogoutClear(__privateGet(this, _logoutPendingFile));
|
|
28410
|
+
__privateSet(this, _generation, generation);
|
|
28411
|
+
if (await pathExists(__privateGet(this, _dir))) __privateSet(this, _canonicalDir, await (0, import_promises.realpath)(__privateGet(this, _dir)));
|
|
28412
|
+
return result;
|
|
28413
|
+
}));
|
|
28414
|
+
};
|
|
28415
|
+
readFile_fn = async function(file) {
|
|
28416
|
+
try {
|
|
28417
|
+
return parsePendingState(await (0, import_promises.readFile)(file, "utf8"), file, this.identity);
|
|
28418
|
+
} catch (error) {
|
|
28419
|
+
if (isNotFound(error)) return void 0;
|
|
28420
|
+
if (error instanceof ReplicaStorageError) throw error;
|
|
28421
|
+
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
28422
|
+
}
|
|
27321
28423
|
};
|
|
27322
28424
|
async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
|
|
27323
28425
|
return guard(async () => {
|
|
@@ -27363,7 +28465,7 @@ async function readFence(replicaDir) {
|
|
|
27363
28465
|
const fence = await readJsonFile((0, import_node_path.join)(replicaDir, FENCE_FILE));
|
|
27364
28466
|
return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
|
|
27365
28467
|
}
|
|
27366
|
-
function newestGrantInfo(replica, record, pending, spec) {
|
|
28468
|
+
function newestGrantInfo(replica, record, pending, spec, install) {
|
|
27367
28469
|
if (record === null) return null;
|
|
27368
28470
|
const parsed = replica.parseUcanGrant(record.bytes);
|
|
27369
28471
|
return {
|
|
@@ -27371,10 +28473,11 @@ function newestGrantInfo(replica, record, pending, spec) {
|
|
|
27371
28473
|
parentCid: parsed.prf[0] ?? null,
|
|
27372
28474
|
expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
|
|
27373
28475
|
state: pending !== null && record === pending ? "pending" : "active",
|
|
27374
|
-
unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
|
|
28476
|
+
unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix),
|
|
28477
|
+
...install === null ? {} : install
|
|
27375
28478
|
};
|
|
27376
28479
|
}
|
|
27377
|
-
var _device, _store, _replicaDir, _replica, _guard2, _dirSync2,
|
|
28480
|
+
var _device, _store, _replicaDir, _replica, _guard2, _read, _policy, _recordGrantInstall, _dirSync2, _generation2, _ino, _transportOverride, _closed, _inFlight, _transportFactory, _SqliteReplicaHandle_instances, assertOpen_fn, assertLive_fn, transports_fn, assertEffective_fn, assertSyncEffective_fn;
|
|
27378
28481
|
var SqliteReplicaHandle = class {
|
|
27379
28482
|
constructor(input) {
|
|
27380
28483
|
__privateAdd(this, _SqliteReplicaHandle_instances);
|
|
@@ -27383,9 +28486,12 @@ var SqliteReplicaHandle = class {
|
|
|
27383
28486
|
__privateAdd(this, _replicaDir);
|
|
27384
28487
|
__privateAdd(this, _replica);
|
|
27385
28488
|
__privateAdd(this, _guard2);
|
|
28489
|
+
__privateAdd(this, _read);
|
|
28490
|
+
__privateAdd(this, _policy);
|
|
28491
|
+
__privateAdd(this, _recordGrantInstall);
|
|
27386
28492
|
__privateAdd(this, _dirSync2);
|
|
27387
28493
|
/** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
|
|
27388
|
-
__privateAdd(this,
|
|
28494
|
+
__privateAdd(this, _generation2);
|
|
27389
28495
|
/** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
|
|
27390
28496
|
__privateAdd(this, _ino);
|
|
27391
28497
|
__privateAdd(this, _transportOverride);
|
|
@@ -27393,64 +28499,76 @@ var SqliteReplicaHandle = class {
|
|
|
27393
28499
|
__privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
|
|
27394
28500
|
__privateAdd(this, _transportFactory);
|
|
27395
28501
|
this.spec = input.spec;
|
|
28502
|
+
this.replicaId = input.replicaId;
|
|
27396
28503
|
__privateSet(this, _device, input.device);
|
|
27397
28504
|
this.deviceDid = input.device.did;
|
|
27398
28505
|
__privateSet(this, _store, input.store);
|
|
27399
28506
|
__privateSet(this, _replicaDir, input.replicaDir);
|
|
27400
28507
|
__privateSet(this, _replica, input.replica);
|
|
27401
28508
|
__privateSet(this, _guard2, input.guard);
|
|
28509
|
+
__privateSet(this, _read, input.read);
|
|
28510
|
+
__privateSet(this, _policy, input.policy);
|
|
28511
|
+
__privateSet(this, _recordGrantInstall, input.recordGrantInstall);
|
|
27402
28512
|
__privateSet(this, _transportOverride, input.transportFor);
|
|
27403
28513
|
__privateSet(this, _dirSync2, input.dirSync);
|
|
27404
|
-
__privateSet(this,
|
|
28514
|
+
__privateSet(this, _generation2, input.generation);
|
|
27405
28515
|
__privateSet(this, _ino, input.ino);
|
|
27406
28516
|
}
|
|
27407
28517
|
async get(key2) {
|
|
27408
|
-
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
27412
|
-
|
|
27413
|
-
|
|
27414
|
-
syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
|
|
27415
|
-
};
|
|
27416
|
-
switch (result.status) {
|
|
27417
|
-
case "present":
|
|
27418
|
-
return {
|
|
27419
|
-
status: "present",
|
|
27420
|
-
key: result.key,
|
|
27421
|
-
value: result.value,
|
|
27422
|
-
etag: result.etag,
|
|
27423
|
-
metadata: result.metadata,
|
|
27424
|
-
meta
|
|
27425
|
-
};
|
|
27426
|
-
case "content_missing":
|
|
27427
|
-
return {
|
|
27428
|
-
status: "content_missing",
|
|
27429
|
-
key: result.key,
|
|
27430
|
-
etag: result.etag,
|
|
27431
|
-
metadata: result.metadata,
|
|
27432
|
-
meta
|
|
27433
|
-
};
|
|
27434
|
-
default:
|
|
27435
|
-
return { status: result.status, key: result.key, meta };
|
|
27436
|
-
}
|
|
27437
|
-
}
|
|
27438
|
-
async list(options) {
|
|
27439
|
-
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
27440
|
-
const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).list({
|
|
27441
|
-
prefix: options.prefix,
|
|
27442
|
-
...options.after === void 0 ? {} : { after: options.after },
|
|
27443
|
-
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
27444
|
-
});
|
|
27445
|
-
return {
|
|
27446
|
-
keys: result.entries.map((entry) => entry.key),
|
|
27447
|
-
meta: {
|
|
28518
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28519
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28520
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28521
|
+
const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).get(key2);
|
|
28522
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28523
|
+
const meta = {
|
|
27448
28524
|
asOf: result.meta.asOf,
|
|
27449
28525
|
coverage: result.meta.coverage,
|
|
27450
28526
|
authority: result.meta.authority,
|
|
27451
|
-
syncedThroughEpoch
|
|
28527
|
+
syncedThroughEpoch
|
|
28528
|
+
};
|
|
28529
|
+
switch (result.status) {
|
|
28530
|
+
case "present":
|
|
28531
|
+
return {
|
|
28532
|
+
status: "present",
|
|
28533
|
+
key: result.key,
|
|
28534
|
+
value: result.value,
|
|
28535
|
+
etag: result.etag,
|
|
28536
|
+
metadata: result.metadata,
|
|
28537
|
+
meta
|
|
28538
|
+
};
|
|
28539
|
+
case "content_missing":
|
|
28540
|
+
return {
|
|
28541
|
+
status: "content_missing",
|
|
28542
|
+
key: result.key,
|
|
28543
|
+
etag: result.etag,
|
|
28544
|
+
metadata: result.metadata,
|
|
28545
|
+
meta
|
|
28546
|
+
};
|
|
28547
|
+
default:
|
|
28548
|
+
return { status: result.status, key: result.key, meta };
|
|
27452
28549
|
}
|
|
27453
|
-
};
|
|
28550
|
+
}, true);
|
|
28551
|
+
}
|
|
28552
|
+
async list(options) {
|
|
28553
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28554
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28555
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28556
|
+
const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).list({
|
|
28557
|
+
prefix: options.prefix,
|
|
28558
|
+
...options.after === void 0 ? {} : { after: options.after },
|
|
28559
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
28560
|
+
});
|
|
28561
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28562
|
+
return {
|
|
28563
|
+
keys: result.entries.map((entry) => entry.key),
|
|
28564
|
+
meta: {
|
|
28565
|
+
asOf: result.meta.asOf,
|
|
28566
|
+
coverage: result.meta.coverage,
|
|
28567
|
+
authority: result.meta.authority,
|
|
28568
|
+
syncedThroughEpoch
|
|
28569
|
+
}
|
|
28570
|
+
};
|
|
28571
|
+
}, true);
|
|
27454
28572
|
}
|
|
27455
28573
|
/**
|
|
27456
28574
|
* The newest installed grant — the pending one when a not-yet-promoted
|
|
@@ -27460,43 +28578,59 @@ var SqliteReplicaHandle = class {
|
|
|
27460
28578
|
* grant is still reported (unconstrained: false) and never reused.
|
|
27461
28579
|
*/
|
|
27462
28580
|
async grant() {
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
|
|
27470
|
-
|
|
27471
|
-
|
|
28581
|
+
return __privateGet(this, _read).call(this, async (snapshot) => {
|
|
28582
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28583
|
+
const state = await snapshot.open();
|
|
28584
|
+
if (state === null) return null;
|
|
28585
|
+
const selected = state.pendingGrant ?? state.grant;
|
|
28586
|
+
const install = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
|
|
28587
|
+
return newestGrantInfo(
|
|
28588
|
+
__privateGet(this, _replica),
|
|
28589
|
+
state.pendingGrant ?? state.grant,
|
|
28590
|
+
state.pendingGrant,
|
|
28591
|
+
this.spec,
|
|
28592
|
+
install
|
|
28593
|
+
);
|
|
28594
|
+
});
|
|
27472
28595
|
}
|
|
27473
28596
|
async installGrant(ucan) {
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
27478
|
-
|
|
27479
|
-
__privateGet(this, _replica).
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
|
|
27483
|
-
|
|
27484
|
-
|
|
27485
|
-
|
|
27486
|
-
|
|
27487
|
-
|
|
28597
|
+
return __privateGet(this, _guard2).call(this, async () => {
|
|
28598
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
28599
|
+
const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
|
|
28600
|
+
const state = await __privateGet(this, _store).open();
|
|
28601
|
+
if (state === null) {
|
|
28602
|
+
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
28603
|
+
__privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
|
|
28604
|
+
"The replica has not been created."
|
|
28605
|
+
);
|
|
28606
|
+
}
|
|
28607
|
+
__privateGet(this, _replica).assertGrantInstallable(parsed, {
|
|
28608
|
+
deviceDid: principalOf(__privateGet(this, _device).did),
|
|
28609
|
+
space: this.spec.space,
|
|
28610
|
+
prefix: this.spec.prefix,
|
|
28611
|
+
now: Date.now()
|
|
28612
|
+
});
|
|
28613
|
+
if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
|
|
28614
|
+
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
28615
|
+
__privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
28616
|
+
"The grant's get or sync coverage carries caveats; re-mint instead of installing it."
|
|
28617
|
+
);
|
|
28618
|
+
}
|
|
28619
|
+
const now = Date.now();
|
|
28620
|
+
const policy = await __privateGet(this, _policy).call(this, now, true);
|
|
28621
|
+
if (policy === null || now >= policy.consentDeadline) {
|
|
28622
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The replica's consent window has expired.");
|
|
28623
|
+
}
|
|
28624
|
+
const installedAt = await __privateGet(this, _recordGrantInstall).call(this, parsed.cid, now);
|
|
28625
|
+
const installed = await __privateGet(this, _store).installGatedGrant(parsed, { ...policy, at: installedAt });
|
|
28626
|
+
return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec, installed);
|
|
27488
28627
|
});
|
|
27489
|
-
if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
|
|
27490
|
-
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
27491
|
-
__privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
27492
|
-
"The grant's get or sync coverage carries caveats; re-mint instead of installing it."
|
|
27493
|
-
);
|
|
27494
|
-
}
|
|
27495
|
-
await __privateGet(this, _store).installGrant(parsed);
|
|
27496
|
-
return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec);
|
|
27497
28628
|
}
|
|
27498
28629
|
async sync(options) {
|
|
27499
|
-
await
|
|
28630
|
+
await __privateGet(this, _guard2).call(this, async () => {
|
|
28631
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
28632
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertSyncEffective_fn).call(this);
|
|
28633
|
+
});
|
|
27500
28634
|
const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
|
|
27501
28635
|
const abort = new AbortController();
|
|
27502
28636
|
options.signal.throwIfAborted();
|
|
@@ -27537,27 +28671,33 @@ var SqliteReplicaHandle = class {
|
|
|
27537
28671
|
}
|
|
27538
28672
|
}
|
|
27539
28673
|
async status() {
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
|
|
27547
|
-
state
|
|
27548
|
-
|
|
27549
|
-
|
|
27550
|
-
|
|
27551
|
-
|
|
27552
|
-
|
|
27553
|
-
|
|
27554
|
-
|
|
27555
|
-
|
|
27556
|
-
|
|
27557
|
-
|
|
27558
|
-
|
|
27559
|
-
|
|
27560
|
-
|
|
28674
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28675
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28676
|
+
const engine = new (__privateGet(this, _replica)).Replica({ store: snapshot });
|
|
28677
|
+
const status = await engine.status();
|
|
28678
|
+
const state = await snapshot.open();
|
|
28679
|
+
const selected = state?.pendingGrant ?? state?.grant ?? null;
|
|
28680
|
+
const selectedInstall = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
|
|
28681
|
+
const activeRecord = state?.grant ?? null;
|
|
28682
|
+
const activeInstall = activeRecord === null ? null : await snapshot.getGrantInstall(activeRecord.cid);
|
|
28683
|
+
const grant = state === null ? null : newestGrantInfo(
|
|
28684
|
+
__privateGet(this, _replica),
|
|
28685
|
+
state.pendingGrant ?? state.grant,
|
|
28686
|
+
state.pendingGrant,
|
|
28687
|
+
this.spec,
|
|
28688
|
+
selectedInstall
|
|
28689
|
+
);
|
|
28690
|
+
return {
|
|
28691
|
+
coverage: status.coverage,
|
|
28692
|
+
lastSyncAt: status.lastSyncAt,
|
|
28693
|
+
syncedThroughEpoch,
|
|
28694
|
+
authority: activeRecord !== null && (activeInstall === null || Date.now() >= activeInstall.effectiveExpiresAt || !Number.isSafeInteger(activeInstall.effectiveExpiresAt)) ? { state: "expired", expiresAt: activeInstall === null ? null : new Date(activeInstall.effectiveExpiresAt).toISOString() } : { state: status.authority.state, expiresAt: status.authority.expiresAt },
|
|
28695
|
+
grant,
|
|
28696
|
+
counts: status.counts,
|
|
28697
|
+
bytes: status.bytes,
|
|
28698
|
+
lastError: status.lastError
|
|
28699
|
+
};
|
|
28700
|
+
}, true);
|
|
27561
28701
|
}
|
|
27562
28702
|
async close() {
|
|
27563
28703
|
if (__privateGet(this, _closed)) return;
|
|
@@ -27572,8 +28712,11 @@ _store = new WeakMap();
|
|
|
27572
28712
|
_replicaDir = new WeakMap();
|
|
27573
28713
|
_replica = new WeakMap();
|
|
27574
28714
|
_guard2 = new WeakMap();
|
|
28715
|
+
_read = new WeakMap();
|
|
28716
|
+
_policy = new WeakMap();
|
|
28717
|
+
_recordGrantInstall = new WeakMap();
|
|
27575
28718
|
_dirSync2 = new WeakMap();
|
|
27576
|
-
|
|
28719
|
+
_generation2 = new WeakMap();
|
|
27577
28720
|
_ino = new WeakMap();
|
|
27578
28721
|
_transportOverride = new WeakMap();
|
|
27579
28722
|
_closed = new WeakMap();
|
|
@@ -27608,7 +28751,7 @@ assertLive_fn = async function() {
|
|
|
27608
28751
|
throw error;
|
|
27609
28752
|
}
|
|
27610
28753
|
const state = await __privateGet(this, _store).open();
|
|
27611
|
-
if (state !== null && state.generation !== __privateGet(this,
|
|
28754
|
+
if (state !== null && state.generation !== __privateGet(this, _generation2)) {
|
|
27612
28755
|
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
27613
28756
|
__privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
|
|
27614
28757
|
"The replica was purged or reopened; open it again to continue."
|
|
@@ -27650,97 +28793,1099 @@ transports_fn = async function() {
|
|
|
27650
28793
|
})());
|
|
27651
28794
|
return __privateGet(this, _transportFactory);
|
|
27652
28795
|
};
|
|
28796
|
+
assertEffective_fn = async function(snapshot) {
|
|
28797
|
+
const state = await snapshot.open();
|
|
28798
|
+
if (state?.grant === void 0 || state.grant === null) return;
|
|
28799
|
+
const installed = await snapshot.getGrantInstall(state.grant.cid);
|
|
28800
|
+
if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || Date.now() >= installed.effectiveExpiresAt || Date.now() >= installed.consentDeadline) {
|
|
28801
|
+
throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
|
|
28802
|
+
}
|
|
28803
|
+
};
|
|
28804
|
+
assertSyncEffective_fn = async function() {
|
|
28805
|
+
const state = await __privateGet(this, _store).open();
|
|
28806
|
+
const selected = state?.pendingGrant ?? state?.grant;
|
|
28807
|
+
if (selected === void 0 || selected === null) return;
|
|
28808
|
+
const installed = await __privateGet(this, _store).getGrantInstall(selected.cid);
|
|
28809
|
+
const now = Date.now();
|
|
28810
|
+
if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || now >= installed.effectiveExpiresAt || now >= installed.consentDeadline) {
|
|
28811
|
+
throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
|
|
28812
|
+
}
|
|
28813
|
+
};
|
|
27653
28814
|
function createSqliteReplicaStorage(options) {
|
|
27654
28815
|
const root = options.dir;
|
|
27655
|
-
const
|
|
28816
|
+
const profileDir = (0, import_node_path.dirname)(options.gateFile ?? (0, import_node_path.join)((0, import_node_path.dirname)(root), import_contractConstants.REPLICA_GATE_FILE));
|
|
28817
|
+
const diagnosticFiles = [...options.diagnosticFiles ?? []];
|
|
28818
|
+
for (const name of diagnosticFiles) {
|
|
28819
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || /^[a-z2-7]{26}$/.test(name)) {
|
|
28820
|
+
throw new TypeError(`Invalid replication diagnostics file name ${JSON.stringify(name)}.`);
|
|
28821
|
+
}
|
|
28822
|
+
}
|
|
28823
|
+
const inspectLayout = (gateReadable, approvedMarker, legacyOnly = false) => inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly, diagnosticFiles);
|
|
28824
|
+
const guard = options.guard;
|
|
27656
28825
|
const createDevice = options.createDevice ?? createDeviceKey;
|
|
27657
|
-
|
|
27658
|
-
|
|
27659
|
-
|
|
27660
|
-
|
|
27661
|
-
|
|
27662
|
-
|
|
28826
|
+
const heldLock = new import_node_async_hooks.AsyncLocalStorage();
|
|
28827
|
+
let visibleStamp;
|
|
28828
|
+
let visiblePrincipalHash;
|
|
28829
|
+
let checkedGate;
|
|
28830
|
+
async function profileSection(section) {
|
|
28831
|
+
if (heldLock.getStore() === true) return section();
|
|
28832
|
+
return requiredGuard(guard, () => heldLock.run(true, section));
|
|
28833
|
+
}
|
|
28834
|
+
async function guarded(principal, stamp, identity, partitionId, section) {
|
|
28835
|
+
if (stamp === void 0) throw gated("not_established");
|
|
28836
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
28837
|
+
const gate = await fileGate();
|
|
28838
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
28839
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28840
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28841
|
+
gate.principalHashOf(snapshot.salt, principal),
|
|
28842
|
+
identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(identity))
|
|
27663
28843
|
]);
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
|
|
27667
|
-
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
|
|
27671
|
-
|
|
27672
|
-
|
|
27673
|
-
|
|
27674
|
-
|
|
27675
|
-
|
|
27676
|
-
const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
|
|
27677
|
-
create: true,
|
|
27678
|
-
guard
|
|
28844
|
+
return profileSection(async () => {
|
|
28845
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28846
|
+
await gate.assertGateAccess(profileDir, {
|
|
28847
|
+
stamp,
|
|
28848
|
+
incarnation: snapshot.incarnation,
|
|
28849
|
+
principalHash,
|
|
28850
|
+
identityKeyHash,
|
|
28851
|
+
partitionId
|
|
28852
|
+
}, snapshot.salt);
|
|
28853
|
+
const result = await section();
|
|
28854
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28855
|
+
return result;
|
|
27679
28856
|
});
|
|
27680
|
-
|
|
28857
|
+
}
|
|
28858
|
+
async function readGated(spec, partitionId, store, replicaDir, generation, section, withFence = false) {
|
|
28859
|
+
if (spec.stamp === void 0) throw gated("not_established");
|
|
28860
|
+
const gate = await fileGate();
|
|
28861
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
28862
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28863
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28864
|
+
gate.principalHashOf(snapshot.salt, spec.identity.principal),
|
|
28865
|
+
gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity))
|
|
28866
|
+
]);
|
|
28867
|
+
const access = {
|
|
28868
|
+
stamp: spec.stamp,
|
|
28869
|
+
incarnation: snapshot.incarnation,
|
|
28870
|
+
principalHash,
|
|
28871
|
+
identityKeyHash,
|
|
28872
|
+
partitionId
|
|
28873
|
+
};
|
|
28874
|
+
const path = options.gateFile ?? (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
|
|
28875
|
+
let checked;
|
|
28876
|
+
const checkGate = async () => {
|
|
28877
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28878
|
+
let before;
|
|
28879
|
+
try {
|
|
28880
|
+
before = await (0, import_promises.stat)(path, { bigint: true });
|
|
28881
|
+
} catch {
|
|
28882
|
+
throw gated("gate_unavailable");
|
|
28883
|
+
}
|
|
28884
|
+
if (checked?.ino === before.ino && checked.size === before.size && checked.mtimeNs === before.mtimeNs) return;
|
|
28885
|
+
const checkedState = await gate.assertGateAccess(profileDir, access, snapshot.salt);
|
|
28886
|
+
if (spec.grantPolicy?.consentAnchor === null && checkedState.firstInstall[identityKeyHash] === void 0) {
|
|
28887
|
+
const installed = await store.open();
|
|
28888
|
+
if (installed !== null && (installed.grant !== null || installed.pendingGrant !== null)) throw gated("gate_unavailable");
|
|
28889
|
+
}
|
|
28890
|
+
let after;
|
|
28891
|
+
try {
|
|
28892
|
+
after = await (0, import_promises.stat)(path, { bigint: true });
|
|
28893
|
+
} catch {
|
|
28894
|
+
throw gated("gate_unavailable");
|
|
28895
|
+
}
|
|
28896
|
+
if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
|
|
28897
|
+
throw gated("gate_unavailable");
|
|
28898
|
+
}
|
|
28899
|
+
checked = after;
|
|
28900
|
+
};
|
|
28901
|
+
if (withFence) await checkGate();
|
|
28902
|
+
const fence = withFence ? await readFence(replicaDir) : 0;
|
|
28903
|
+
return store.readTransaction(generation, checkGate, (sqliteSnapshot) => section(sqliteSnapshot, fence));
|
|
28904
|
+
}
|
|
28905
|
+
async function identityDirs() {
|
|
28906
|
+
let entries;
|
|
28907
|
+
try {
|
|
28908
|
+
entries = await (0, import_promises.readdir)(root, { withFileTypes: true });
|
|
28909
|
+
} catch (error) {
|
|
28910
|
+
if (isNotFound(error)) return [];
|
|
28911
|
+
throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
|
|
28912
|
+
}
|
|
27681
28913
|
try {
|
|
28914
|
+
const marker = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
28915
|
+
if (marker !== null) {
|
|
28916
|
+
const inventory = await inspectLayout(true);
|
|
28917
|
+
assertApprovedInventory(inventory, {
|
|
28918
|
+
unknownEntries: marker.approvedUnknown,
|
|
28919
|
+
knownRoots: marker.knownRoots,
|
|
28920
|
+
knownEntries: marker.knownEntries,
|
|
28921
|
+
missingDatabases: marker.missingDatabases
|
|
28922
|
+
}, true);
|
|
28923
|
+
}
|
|
28924
|
+
for (const entry of entries) {
|
|
28925
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
28926
|
+
if (marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && (entry.isDirectory() ? item.kind === "directory" : entry.isSymbolicLink() ? item.kind === "symlink" : item.kind === "file"))) continue;
|
|
28927
|
+
if (diagnosticFiles.includes(entry.name)) {
|
|
28928
|
+
const info = await (0, import_promises.lstat)((0, import_node_path.join)(root, entry.name)).catch((error) => {
|
|
28929
|
+
if (isNotFound(error)) return null;
|
|
28930
|
+
throw error;
|
|
28931
|
+
});
|
|
28932
|
+
if (info === null || info.isFile()) continue;
|
|
28933
|
+
}
|
|
28934
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Unknown identity partition at ${root}.`);
|
|
28935
|
+
}
|
|
28936
|
+
}
|
|
28937
|
+
return entries.filter((entry) => entry.isDirectory() && /^[a-z2-7]{26}$/.test(entry.name) && !marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && item.kind === "directory")).map((entry) => (0, import_node_path.join)(root, entry.name));
|
|
28938
|
+
} catch (error) {
|
|
28939
|
+
throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
|
|
28940
|
+
}
|
|
28941
|
+
}
|
|
28942
|
+
async function collectIntents(principalHash, salt) {
|
|
28943
|
+
const gate = await fileGate();
|
|
28944
|
+
const intents = [];
|
|
28945
|
+
for (const idDir of await identityDirs()) {
|
|
28946
|
+
const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
|
|
28947
|
+
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
|
|
28948
|
+
if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
|
|
28949
|
+
const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
|
|
28950
|
+
const recordedAt = Date.now();
|
|
28951
|
+
if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
|
|
28952
|
+
intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
|
|
28953
|
+
}
|
|
28954
|
+
const replicasDir = (0, import_node_path.join)(idDir, "replicas");
|
|
28955
|
+
let entries;
|
|
27682
28956
|
try {
|
|
27683
|
-
await
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
28957
|
+
entries = await (0, import_promises.readdir)(replicasDir, { withFileTypes: true });
|
|
28958
|
+
} catch (error) {
|
|
28959
|
+
if (isNotFound(error)) continue;
|
|
28960
|
+
throw storageError("STORAGE_ERROR", `Listing ${replicasDir}`, error);
|
|
28961
|
+
}
|
|
28962
|
+
for (const entry of entries) {
|
|
28963
|
+
if (!entry.isDirectory()) continue;
|
|
28964
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
28965
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Unrecognized replica partition at ${replicasDir}.`);
|
|
28966
|
+
}
|
|
28967
|
+
const partitionId = `${identityKeyHash}:${entry.name}`;
|
|
28968
|
+
intents.push({ id: partitionId, kind: "partition", partitionId, principalHash, recordedAt });
|
|
28969
|
+
}
|
|
28970
|
+
}
|
|
28971
|
+
return intents;
|
|
28972
|
+
}
|
|
28973
|
+
async function retainGrantInstalls(gate, gateState, identityKeyHash, store) {
|
|
28974
|
+
for (const { cid, installedAt } of await store.grantInstallTimes()) {
|
|
28975
|
+
const key2 = await gate.grantInstallHashOf(gateState.salt, identityKeyHash, cid);
|
|
28976
|
+
await gate.recordGrantInstall(profileDir, key2, installedAt);
|
|
28977
|
+
}
|
|
28978
|
+
}
|
|
28979
|
+
async function removeCorruptOrphanGate(gate) {
|
|
28980
|
+
const path = (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
|
|
28981
|
+
const before = await (0, import_promises.lstat)(path, { bigint: true });
|
|
28982
|
+
if (!before.isFile() || before.isSymbolicLink()) throw gated("gate_unavailable");
|
|
28983
|
+
let corrupt = false;
|
|
28984
|
+
try {
|
|
28985
|
+
await gate.readGateSnapshot(profileDir);
|
|
28986
|
+
} catch (error) {
|
|
28987
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
|
|
28988
|
+
corrupt = true;
|
|
28989
|
+
} else throw error;
|
|
28990
|
+
}
|
|
28991
|
+
if (!corrupt) throw gated("stamp_stale");
|
|
28992
|
+
const current = await (0, import_promises.lstat)(path, { bigint: true });
|
|
28993
|
+
if (current.dev !== before.dev || current.ino !== before.ino || current.mtimeNs !== before.mtimeNs || current.size !== before.size) throw gated("stamp_stale");
|
|
28994
|
+
await (0, import_promises.rm)(path);
|
|
28995
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
28996
|
+
if (await gate.readGateSnapshot(profileDir) !== void 0) throw gated("stamp_stale");
|
|
28997
|
+
}
|
|
28998
|
+
async function readLogoutState(gate) {
|
|
28999
|
+
try {
|
|
29000
|
+
return { read: await gate.readLogoutGate(profileDir), corrupt: false };
|
|
29001
|
+
} catch (error) {
|
|
29002
|
+
if (isGateUnavailable(error)) return { read: void 0, corrupt: true };
|
|
29003
|
+
throw error;
|
|
29004
|
+
}
|
|
29005
|
+
}
|
|
29006
|
+
async function validateV1(gate, path, marker, read, mode) {
|
|
29007
|
+
if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
|
|
29008
|
+
const { state, floorLagging } = read;
|
|
29009
|
+
if (marker.deniedEpoch !== void 0) {
|
|
29010
|
+
if (floorLagging || !v1Denied(marker, state, marker.deniedEpoch)) throw restoredGate();
|
|
29011
|
+
return { marker, state, active: false };
|
|
29012
|
+
}
|
|
29013
|
+
if (!floorLagging && state.principalHash === null && state.lifecycleEpoch > marker.lifecycleEpoch && (marker.remotePending ? state.eraseDeferred && state.deferredPrincipalHash === marker.principalHash : !state.eraseDeferred && state.deferredPrincipalHash === null)) {
|
|
29014
|
+
return { marker: await bindDeniedEpoch(path, marker, state.lifecycleEpoch), state, active: false };
|
|
29015
|
+
}
|
|
29016
|
+
if (state.principalHash !== marker.principalHash || state.lifecycleEpoch !== marker.lifecycleEpoch) {
|
|
29017
|
+
throw gated("stamp_stale");
|
|
29018
|
+
}
|
|
29019
|
+
if (!floorLagging && !mode.denyActive) {
|
|
29020
|
+
if (!mode.active) throw gated("stamp_stale");
|
|
29021
|
+
return { marker, state, active: true };
|
|
29022
|
+
}
|
|
29023
|
+
if (mode.acceptLagging) return { marker, state, active: true };
|
|
29024
|
+
if (mode.deny === void 0) throw interruptedSignOut();
|
|
29025
|
+
const denied = await gate.denyGate(
|
|
29026
|
+
profileDir,
|
|
29027
|
+
{ reason: "sign_out", principal: "" },
|
|
29028
|
+
(principalHash) => collectIntents(principalHash, state.salt),
|
|
29029
|
+
{ salt: state.salt, principalHash: marker.principalHash },
|
|
29030
|
+
{ deferErase: mode.deny === "deferred" }
|
|
29031
|
+
);
|
|
29032
|
+
if (denied.status !== "denied") throw gated("gate_unavailable");
|
|
29033
|
+
const after = await gate.readLogoutGate(profileDir);
|
|
29034
|
+
if (after === void 0 || after.floorLagging || !v1Denied(marker, after.state, denied.lifecycleEpoch)) {
|
|
29035
|
+
throw gated("gate_unavailable");
|
|
29036
|
+
}
|
|
29037
|
+
return { marker: await bindDeniedEpoch(path, marker, denied.lifecycleEpoch), state: after.state, active: false };
|
|
29038
|
+
}
|
|
29039
|
+
function v1Denied(marker, state, epoch) {
|
|
29040
|
+
return state.incarnation === marker.incarnation && state.principalHash === null && state.lifecycleEpoch === epoch && (state.deferredPrincipalHash === marker.principalHash || !marker.remotePending && !state.eraseDeferred && state.deferredPrincipalHash === null);
|
|
29041
|
+
}
|
|
29042
|
+
async function bindDeniedEpoch(path, marker, deniedEpoch) {
|
|
29043
|
+
if (marker.deniedEpoch !== void 0) throw gated("stamp_stale");
|
|
29044
|
+
const bound = { ...marker, deniedEpoch };
|
|
29045
|
+
await writeJsonAtomic(path, bound, options.dirSync);
|
|
29046
|
+
return bound;
|
|
29047
|
+
}
|
|
29048
|
+
async function validatedRemoteMarker(path, marker, principal, mode) {
|
|
29049
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29050
|
+
const gate = await fileGate();
|
|
29051
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29052
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29053
|
+
if (marker.v === 2) {
|
|
29054
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29055
|
+
return { marker, state: void 0 };
|
|
29056
|
+
}
|
|
29057
|
+
if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
|
|
29058
|
+
if (marker.v === 3) {
|
|
29059
|
+
if (read.floorLagging || read.state.lifecycleEpoch !== marker.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
|
|
29060
|
+
return { marker, state: read.state };
|
|
29061
|
+
}
|
|
29062
|
+
if (marker.principalHash !== await gate.principalHashOf(read.state.salt, principal)) {
|
|
29063
|
+
throw gated("principal_mismatch");
|
|
29064
|
+
}
|
|
29065
|
+
const checked = await validateV1(gate, path, marker, read, mode);
|
|
29066
|
+
return { marker: checked.marker, state: checked.state };
|
|
29067
|
+
}
|
|
29068
|
+
async function currentLayout() {
|
|
29069
|
+
const { read, corrupt } = await readLogoutState(await fileGate());
|
|
29070
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29071
|
+
const report = await inspectLayout(read !== void 0);
|
|
29072
|
+
assertSupportedProfileLayout(report);
|
|
29073
|
+
return report;
|
|
29074
|
+
}
|
|
29075
|
+
async function commitPlainAbandon(path, marker, report, discardLocalChanges) {
|
|
29076
|
+
const { remotePending: _, discardLocalChanges: _oldDiscard, ...previous } = marker;
|
|
29077
|
+
const abandoned = {
|
|
29078
|
+
...previous,
|
|
29079
|
+
remoteAbandoned: true,
|
|
29080
|
+
approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
|
|
29081
|
+
knownRoots: report.knownRoots,
|
|
29082
|
+
knownEntries: report.knownEntries,
|
|
29083
|
+
missingDatabases: report.missingDatabases,
|
|
29084
|
+
...discardLocalChanges ? { discardLocalChanges: true } : {}
|
|
29085
|
+
};
|
|
29086
|
+
await writeJsonAtomic(path, abandoned, options.dirSync);
|
|
29087
|
+
return abandoned;
|
|
29088
|
+
}
|
|
29089
|
+
function deny(input, denyOptions) {
|
|
29090
|
+
const deferErase = denyOptions?.deferErase === true;
|
|
29091
|
+
if (deferErase && input.reason !== "sign_out") {
|
|
29092
|
+
throw new TypeError("Deferred erase is only available for sign-out.");
|
|
29093
|
+
}
|
|
29094
|
+
visibleStamp = void 0;
|
|
29095
|
+
visiblePrincipalHash = void 0;
|
|
29096
|
+
checkedGate = void 0;
|
|
29097
|
+
return (async () => {
|
|
29098
|
+
try {
|
|
29099
|
+
const gate = await fileGate();
|
|
29100
|
+
return await profileSection(async () => {
|
|
29101
|
+
const snapshot = (await gate.readLogoutGate(profileDir))?.state;
|
|
29102
|
+
const precomputed = snapshot === void 0 ? void 0 : {
|
|
29103
|
+
salt: snapshot.salt,
|
|
29104
|
+
principalHash: await gate.principalHashOf(snapshot.salt, input.principal)
|
|
29105
|
+
};
|
|
29106
|
+
const result = await gate.denyGate(
|
|
29107
|
+
profileDir,
|
|
29108
|
+
input,
|
|
29109
|
+
async (principalHash) => {
|
|
29110
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29111
|
+
return collectIntents(principalHash, snapshot.salt);
|
|
29112
|
+
},
|
|
29113
|
+
precomputed,
|
|
29114
|
+
{ deferErase }
|
|
29115
|
+
);
|
|
29116
|
+
if (result.status !== "denied") return result;
|
|
29117
|
+
if (input.reason === "sign_out" && snapshot !== void 0 && precomputed !== void 0) {
|
|
29118
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29119
|
+
const marker = await readLogoutMarker(path);
|
|
29120
|
+
if (marker?.v === 1 && marker.deniedEpoch === void 0 && marker.incarnation === snapshot.incarnation && marker.principalHash === precomputed.principalHash && marker.remotePending === true === deferErase) {
|
|
29121
|
+
await bindDeniedEpoch(path, marker, result.lifecycleEpoch);
|
|
29122
|
+
}
|
|
29123
|
+
}
|
|
29124
|
+
if (!deferErase) return result;
|
|
29125
|
+
const resumeToken = await gate.deferredGateToken(profileDir);
|
|
29126
|
+
if (resumeToken === null || resumeToken.deniedEpoch !== result.lifecycleEpoch) {
|
|
29127
|
+
return { status: "unguarded", code: "gate_unavailable" };
|
|
29128
|
+
}
|
|
29129
|
+
return { ...result, resumeToken };
|
|
27693
29130
|
});
|
|
27694
29131
|
} catch (error) {
|
|
27695
|
-
|
|
27696
|
-
|
|
29132
|
+
return { status: "unguarded", code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable" };
|
|
29133
|
+
}
|
|
29134
|
+
})();
|
|
29135
|
+
}
|
|
29136
|
+
const lifecycle = {
|
|
29137
|
+
kind: "file",
|
|
29138
|
+
async recover({ signal }) {
|
|
29139
|
+
try {
|
|
29140
|
+
return await lifecycle.erasePending({ signal });
|
|
29141
|
+
} catch (error) {
|
|
29142
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR";
|
|
29143
|
+
let remaining = 0;
|
|
29144
|
+
try {
|
|
29145
|
+
remaining = (await (await fileGate()).readGateSnapshot(profileDir))?.intents.length ?? 0;
|
|
29146
|
+
} catch {
|
|
27697
29147
|
}
|
|
29148
|
+
return { erased: [], failed: [{ id: "gate", code }], remaining };
|
|
27698
29149
|
}
|
|
27699
|
-
|
|
27700
|
-
|
|
27701
|
-
|
|
27702
|
-
|
|
27703
|
-
|
|
27704
|
-
|
|
29150
|
+
},
|
|
29151
|
+
async establishSessionStamp(principal, { consentAnchor }) {
|
|
29152
|
+
if (consentAnchor?.source === "first_install" || !Number.isSafeInteger(consentAnchor?.at) || consentAnchor.at < 0) {
|
|
29153
|
+
throw gated("not_established");
|
|
29154
|
+
}
|
|
29155
|
+
const gate = await fileGate();
|
|
29156
|
+
const stamp = await profileSection(async () => {
|
|
29157
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29158
|
+
return gate.establishSessionStamp(profileDir, principal, { siweIssuedAt: consentAnchor.at });
|
|
29159
|
+
});
|
|
29160
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29161
|
+
if (state === void 0) throw gated("gate_unavailable");
|
|
29162
|
+
visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
|
|
29163
|
+
checkedGate = void 0;
|
|
29164
|
+
visibleStamp = stamp;
|
|
29165
|
+
return stamp;
|
|
29166
|
+
},
|
|
29167
|
+
async restoreSessionStamp(principal, persisted) {
|
|
29168
|
+
const gate = await fileGate();
|
|
29169
|
+
const stamp = await profileSection(async () => {
|
|
29170
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29171
|
+
return gate.restoreSessionStamp(profileDir, principal, persisted);
|
|
29172
|
+
});
|
|
29173
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29174
|
+
if (state === void 0) throw gated("gate_unavailable");
|
|
29175
|
+
visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
|
|
29176
|
+
checkedGate = void 0;
|
|
29177
|
+
visibleStamp = stamp;
|
|
29178
|
+
return stamp;
|
|
29179
|
+
},
|
|
29180
|
+
async activationAllowed(next) {
|
|
29181
|
+
if (guard === void 0) return false;
|
|
29182
|
+
try {
|
|
29183
|
+
const gate = await fileGate();
|
|
29184
|
+
return await profileSection(async () => {
|
|
29185
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29186
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29187
|
+
return state !== void 0 && !state.eraseDeferred && state.intents.length === 0 && state.principalHash !== null && state.principalHash === await gate.principalHashOf(state.salt, next);
|
|
29188
|
+
});
|
|
29189
|
+
} catch {
|
|
29190
|
+
return false;
|
|
27705
29191
|
}
|
|
27706
|
-
|
|
27707
|
-
|
|
27708
|
-
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
|
|
29192
|
+
},
|
|
29193
|
+
checkDelivery(stamp) {
|
|
29194
|
+
if (guard === void 0 || visiblePrincipalHash === void 0) return { status: "gated", gate: "gate_unavailable" };
|
|
29195
|
+
try {
|
|
29196
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29197
|
+
} catch {
|
|
29198
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
27712
29199
|
}
|
|
27713
|
-
if (
|
|
27714
|
-
|
|
27715
|
-
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
27716
|
-
"The stored replica belongs to a different device key; purge the partition to rekey."
|
|
27717
|
-
);
|
|
29200
|
+
if (!stamp || visibleStamp?.incarnation !== stamp.incarnation || visibleStamp.lifecycleEpoch !== stamp.lifecycleEpoch) {
|
|
29201
|
+
return { status: "gated", gate: "stamp_stale" };
|
|
27718
29202
|
}
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
29203
|
+
try {
|
|
29204
|
+
const path = options.gateFile ?? (0, import_node_path.join)(profileDir, import_contractConstants.REPLICA_GATE_FILE);
|
|
29205
|
+
const stats = (0, import_node_fs.statSync)(path, { bigint: true });
|
|
29206
|
+
if (checkedGate === void 0 || checkedGate.ino !== stats.ino || checkedGate.mtimeNs !== stats.mtimeNs || checkedGate.size !== stats.size) {
|
|
29207
|
+
const raw = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
29208
|
+
if (typeof raw.incarnation !== "string" || !Number.isSafeInteger(raw.lifecycleEpoch) || raw.principalHash !== null && typeof raw.principalHash !== "string" || !Array.isArray(raw.intents)) {
|
|
29209
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
29210
|
+
}
|
|
29211
|
+
checkedGate = {
|
|
29212
|
+
ino: stats.ino,
|
|
29213
|
+
mtimeNs: stats.mtimeNs,
|
|
29214
|
+
size: stats.size,
|
|
29215
|
+
incarnation: raw.incarnation,
|
|
29216
|
+
epoch: raw.lifecycleEpoch,
|
|
29217
|
+
principalHash: raw.principalHash
|
|
29218
|
+
};
|
|
29219
|
+
}
|
|
29220
|
+
} catch {
|
|
29221
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
29222
|
+
}
|
|
29223
|
+
if (checkedGate.incarnation !== stamp.incarnation || checkedGate.epoch !== stamp.lifecycleEpoch) {
|
|
29224
|
+
return { status: "gated", gate: "stamp_stale" };
|
|
29225
|
+
}
|
|
29226
|
+
return checkedGate.principalHash === visiblePrincipalHash ? { status: "allow" } : { status: "gated", gate: "principal_mismatch" };
|
|
29227
|
+
},
|
|
29228
|
+
consentDeadline() {
|
|
29229
|
+
return null;
|
|
29230
|
+
},
|
|
29231
|
+
deliverable(stamp) {
|
|
29232
|
+
return lifecycle.checkDelivery(stamp).status === "allow";
|
|
29233
|
+
},
|
|
29234
|
+
deny,
|
|
29235
|
+
deferredErase() {
|
|
29236
|
+
return profileSection(async () => (await fileGate()).deferredGateToken(profileDir));
|
|
29237
|
+
},
|
|
29238
|
+
async resumeDeferredErase(token, { signal }) {
|
|
29239
|
+
signal.throwIfAborted();
|
|
29240
|
+
await profileSection(async () => (await fileGate()).resumeDeferredGate(profileDir, token));
|
|
29241
|
+
return lifecycle.erasePending({ signal });
|
|
29242
|
+
},
|
|
29243
|
+
async preflightLogout({ principal, discardLocalChanges = false }) {
|
|
29244
|
+
return profileSection(async () => {
|
|
29245
|
+
const marker = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29246
|
+
const gate = await fileGate();
|
|
29247
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29248
|
+
const state = read?.state;
|
|
29249
|
+
let otherAccount = false;
|
|
29250
|
+
if (state !== void 0) {
|
|
29251
|
+
const hash3 = await gate.principalHashOf(state.salt, principal);
|
|
29252
|
+
otherAccount = gateOwnedByAnother(state, hash3);
|
|
29253
|
+
if (marker?.v === 1 && marker.principalHash !== hash3) throw gated("principal_mismatch");
|
|
29254
|
+
}
|
|
29255
|
+
const report = await inspectLayout(state !== void 0, void 0, otherAccount);
|
|
29256
|
+
assertSupportedProfileLayout(report);
|
|
29257
|
+
if (!discardLocalChanges && (report.unknownPaths.length > 0 || corrupt && !otherAccount || !otherAccount && (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
|
|
29258
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29259
|
+
}
|
|
29260
|
+
return report;
|
|
29261
|
+
});
|
|
29262
|
+
},
|
|
29263
|
+
async prepareLogoutPending({ principal, approval, discardLocalChanges = false, remotePending }) {
|
|
29264
|
+
const gate = await fileGate();
|
|
29265
|
+
return profileSection(async () => {
|
|
29266
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29267
|
+
const previous = await readLogoutMarker(path);
|
|
29268
|
+
const { read, corrupt: corruptGate } = await readLogoutState(gate);
|
|
29269
|
+
let state = read?.state;
|
|
29270
|
+
const principalHash = state === void 0 ? void 0 : await gate.principalHashOf(state.salt, principal);
|
|
29271
|
+
const otherAccount = state !== void 0 && gateOwnedByAnother(state, principalHash);
|
|
29272
|
+
let report = await inspectLayout(state !== void 0, void 0, otherAccount);
|
|
29273
|
+
assertSupportedProfileLayout(report);
|
|
29274
|
+
const assertApproved = (current) => {
|
|
29275
|
+
assertApprovedInventory(current, approval);
|
|
29276
|
+
if (approval.knownLegacyReplicas !== current.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(current.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(current.unknownLegacyPaths)) {
|
|
29277
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
29278
|
+
}
|
|
29279
|
+
};
|
|
29280
|
+
assertApproved(report);
|
|
29281
|
+
if (otherAccount) {
|
|
29282
|
+
if (previous !== null) throw gated("principal_mismatch");
|
|
29283
|
+
if (!discardLocalChanges && report.unknownPaths.length > 0) throw localChangesRefusal(report.unknownPaths);
|
|
29284
|
+
return null;
|
|
29285
|
+
}
|
|
29286
|
+
if (!discardLocalChanges && (report.unknownPaths.length > 0 || corruptGate || (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
|
|
29287
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29288
|
+
}
|
|
29289
|
+
if (previous !== null) {
|
|
29290
|
+
if (previous.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29291
|
+
if (corruptGate) throw gated("gate_unavailable");
|
|
29292
|
+
let current = previous;
|
|
29293
|
+
if (previous.v === 2) {
|
|
29294
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29295
|
+
} else if (read === void 0 || previous.incarnation !== read.state.incarnation) {
|
|
29296
|
+
throw gated("stamp_stale");
|
|
29297
|
+
} else if (previous.v === 3) {
|
|
29298
|
+
if (read.floorLagging || read.state.lifecycleEpoch !== previous.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
|
|
29299
|
+
} else {
|
|
29300
|
+
if (previous.principalHash !== principalHash) throw gated("principal_mismatch");
|
|
29301
|
+
current = (await validateV1(gate, path, previous, read, { active: true, acceptLagging: true })).marker;
|
|
29302
|
+
}
|
|
29303
|
+
assertApprovedInventory(report, {
|
|
29304
|
+
unknownEntries: current.approvedUnknown,
|
|
29305
|
+
knownRoots: current.knownRoots,
|
|
29306
|
+
knownEntries: current.knownEntries,
|
|
29307
|
+
missingDatabases: current.missingDatabases
|
|
29308
|
+
});
|
|
29309
|
+
if (current.remotePending !== remotePending || current.remoteAbandoned) {
|
|
29310
|
+
throw gated("gate_unavailable");
|
|
29311
|
+
}
|
|
29312
|
+
return current;
|
|
29313
|
+
}
|
|
29314
|
+
if (state?.eraseDeferred) throw gated("stamp_stale");
|
|
29315
|
+
if (read?.floorLagging) {
|
|
29316
|
+
if (state.principalHash === null) {
|
|
29317
|
+
state = await gate.advanceInactiveGate(profileDir);
|
|
29318
|
+
} else {
|
|
29319
|
+
const salt = state.salt;
|
|
29320
|
+
const denied = await gate.denyGate(
|
|
29321
|
+
profileDir,
|
|
29322
|
+
{ reason: "sign_out", principal },
|
|
29323
|
+
(hash3) => collectIntents(hash3, salt),
|
|
29324
|
+
{ salt, principalHash }
|
|
29325
|
+
);
|
|
29326
|
+
if (denied.status !== "denied") throw gated("gate_unavailable");
|
|
29327
|
+
state = await gate.readGateSnapshot(profileDir);
|
|
29328
|
+
}
|
|
29329
|
+
report = await inspectLayout(true);
|
|
29330
|
+
assertSupportedProfileLayout(report);
|
|
29331
|
+
assertApproved(report);
|
|
29332
|
+
}
|
|
29333
|
+
if (state === void 0 && report.knownRoots.length === 0 && !corruptGate) return null;
|
|
29334
|
+
if (state !== void 0 && state.principalHash === null && !state.eraseDeferred && report.knownRoots.length === 0 && state.intents.length === 0) return null;
|
|
29335
|
+
const remoteState = remotePending ? { remotePending: true } : {};
|
|
29336
|
+
const approvalFields = {
|
|
29337
|
+
principalDigest: logoutPrincipalDigest(principal),
|
|
29338
|
+
approvalSalt: (0, import_node_crypto2.randomBytes)(16).toString("hex"),
|
|
29339
|
+
approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
|
|
29340
|
+
knownRoots: report.knownRoots,
|
|
29341
|
+
knownEntries: report.knownEntries,
|
|
29342
|
+
missingDatabases: report.missingDatabases,
|
|
29343
|
+
...discardLocalChanges ? { discardLocalChanges: true } : {},
|
|
29344
|
+
...remoteState
|
|
29345
|
+
};
|
|
29346
|
+
let marker;
|
|
29347
|
+
if (state === void 0) {
|
|
29348
|
+
marker = {
|
|
29349
|
+
v: 2,
|
|
29350
|
+
scope: "orphan_profile",
|
|
29351
|
+
nonce: (0, import_node_crypto2.randomBytes)(16).toString("hex"),
|
|
29352
|
+
...approvalFields
|
|
29353
|
+
};
|
|
29354
|
+
} else if (state.principalHash === null) {
|
|
29355
|
+
marker = {
|
|
29356
|
+
v: 3,
|
|
29357
|
+
scope: "inactive_gate",
|
|
29358
|
+
incarnation: state.incarnation,
|
|
29359
|
+
lifecycleEpoch: state.lifecycleEpoch,
|
|
29360
|
+
...approvalFields
|
|
29361
|
+
};
|
|
29362
|
+
} else {
|
|
29363
|
+
marker = {
|
|
29364
|
+
v: 1,
|
|
29365
|
+
incarnation: state.incarnation,
|
|
29366
|
+
lifecycleEpoch: state.lifecycleEpoch,
|
|
29367
|
+
principalHash,
|
|
29368
|
+
...approvalFields
|
|
29369
|
+
};
|
|
29370
|
+
}
|
|
29371
|
+
await writeJsonAtomic(path, marker, options.dirSync);
|
|
29372
|
+
visibleStamp = void 0;
|
|
29373
|
+
visiblePrincipalHash = void 0;
|
|
29374
|
+
checkedGate = void 0;
|
|
29375
|
+
if (corruptGate) await removeCorruptOrphanGate(gate);
|
|
29376
|
+
return marker;
|
|
29377
|
+
});
|
|
29378
|
+
},
|
|
29379
|
+
async completeRemoteLogout({ principal }) {
|
|
29380
|
+
return profileSection(async () => {
|
|
29381
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29382
|
+
const marker = await readLogoutMarker(path);
|
|
29383
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29384
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: false });
|
|
29385
|
+
const { remotePending: _, ...complete } = checked.marker;
|
|
29386
|
+
await writeJsonAtomic(path, complete, options.dirSync);
|
|
29387
|
+
return complete;
|
|
29388
|
+
});
|
|
29389
|
+
},
|
|
29390
|
+
async remotePendingLogout({ principal }) {
|
|
29391
|
+
return profileSection(async () => {
|
|
29392
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29393
|
+
const marker = await readLogoutMarker(path);
|
|
29394
|
+
if (marker === null || marker.remotePending !== true) return false;
|
|
29395
|
+
await validatedRemoteMarker(path, marker, principal, { active: false });
|
|
29396
|
+
return true;
|
|
29397
|
+
});
|
|
29398
|
+
},
|
|
29399
|
+
async abandonRemoteLogout({ principal, approval, discardLocalChanges = false }) {
|
|
29400
|
+
return profileSection(async () => {
|
|
29401
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29402
|
+
const marker = await readLogoutMarker(path);
|
|
29403
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29404
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29405
|
+
const report = await currentLayout();
|
|
29406
|
+
if (storedInventoryChanged(marker, report)) throw changedInventoryRefusal(marker, principal, report);
|
|
29407
|
+
assertApprovedInventory(report, approval);
|
|
29408
|
+
if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths) || !discardLocalChanges && report.unknownPaths.length > 0) {
|
|
29409
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29410
|
+
}
|
|
29411
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
|
|
29412
|
+
return commitPlainAbandon(path, checked.marker, report, discardLocalChanges);
|
|
29413
|
+
});
|
|
29414
|
+
},
|
|
29415
|
+
async approveChangedPlainLogout({ principal, approval, discardLocalChanges, inventoryApprovalToken: token }) {
|
|
29416
|
+
return profileSection(async () => {
|
|
29417
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29418
|
+
const marker = await readLogoutMarker(path);
|
|
29419
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29420
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29421
|
+
const report = await currentLayout();
|
|
29422
|
+
if (!storedInventoryChanged(marker, report)) throw gated("gate_unavailable");
|
|
29423
|
+
const entries = approvalEntries(report);
|
|
29424
|
+
if (discardLocalChanges !== true || typeof token !== "string" || !/^[0-9a-f]{32}$/.test(token) || token !== inventoryApprovalToken(marker, principal, entries)) {
|
|
29425
|
+
throw changedInventoryRefusal(marker, principal, report, entries);
|
|
29426
|
+
}
|
|
29427
|
+
assertApprovedInventory(report, approval);
|
|
29428
|
+
if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths)) {
|
|
29429
|
+
throw changedInventoryRefusal(marker, principal, report, entries);
|
|
29430
|
+
}
|
|
29431
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
|
|
29432
|
+
return commitPlainAbandon(path, checked.marker, report, true);
|
|
29433
|
+
});
|
|
29434
|
+
},
|
|
29435
|
+
async reapproveLogoutPending({ principal, approval }) {
|
|
29436
|
+
return profileSection(async () => {
|
|
29437
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29438
|
+
const marker = await readLogoutMarker(path);
|
|
29439
|
+
if (marker === null || marker.remotePending) throw gated("gate_unavailable");
|
|
29440
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29441
|
+
const report = await currentLayout();
|
|
29442
|
+
assertApprovedInventory(report, approval);
|
|
29443
|
+
if (JSON.stringify(report.unknownPaths) !== JSON.stringify(approval.unknownPaths)) {
|
|
29444
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29445
|
+
}
|
|
29446
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true });
|
|
29447
|
+
const amended = {
|
|
29448
|
+
...checked.marker,
|
|
29449
|
+
approvedUnknown: report.unknownEntries,
|
|
29450
|
+
knownRoots: report.knownRoots,
|
|
29451
|
+
knownEntries: report.knownEntries,
|
|
29452
|
+
missingDatabases: report.missingDatabases,
|
|
29453
|
+
...report.unknownPaths.length > 0 ? { discardLocalChanges: true } : {}
|
|
29454
|
+
};
|
|
29455
|
+
await writeJsonAtomic(path, amended, options.dirSync);
|
|
29456
|
+
return amended;
|
|
29457
|
+
});
|
|
29458
|
+
},
|
|
29459
|
+
async recoverLogoutPending({ signal, clearCredentials, eraseLegacyReplicas }) {
|
|
29460
|
+
const path = (0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29461
|
+
const failure = async (error) => ({
|
|
29462
|
+
erased: [],
|
|
29463
|
+
failed: [{ id: "logout", code: typeof error === "object" && error !== null && "reportCode" in error && typeof error.reportCode === "string" ? error.reportCode : typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code === "REPLICA_GATED" && "gate" in error && typeof error.gate === "string" ? error.gate : error.code : "STORAGE_ERROR" }],
|
|
29464
|
+
remaining: Math.max(1, (await lifecycle.pendingPurge().catch(() => [])).length)
|
|
29465
|
+
});
|
|
29466
|
+
try {
|
|
29467
|
+
const prepared = await profileSection(async () => {
|
|
29468
|
+
const marker2 = await readLogoutMarker(path);
|
|
29469
|
+
if (marker2 === null) {
|
|
29470
|
+
const state2 = await (await fileGate()).readGateSnapshot(profileDir);
|
|
29471
|
+
return { report: { erased: [], failed: [], remaining: state2?.intents.length ?? 0 } };
|
|
29472
|
+
}
|
|
29473
|
+
signal.throwIfAborted();
|
|
29474
|
+
if (marker2.remotePending) throw gated("gate_unavailable");
|
|
29475
|
+
const gate = await fileGate();
|
|
29476
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29477
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29478
|
+
let current = marker2;
|
|
29479
|
+
let state = read?.state;
|
|
29480
|
+
if (marker2.v === 2) {
|
|
29481
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29482
|
+
} else if (read === void 0 || read.state.incarnation !== marker2.incarnation) {
|
|
29483
|
+
throw gated("stamp_stale");
|
|
29484
|
+
} else if (marker2.v === 3) {
|
|
29485
|
+
if (read.floorLagging || read.state.principalHash !== null || read.state.eraseDeferred || read.state.lifecycleEpoch !== marker2.lifecycleEpoch) throw gated("stamp_stale");
|
|
29486
|
+
} else {
|
|
29487
|
+
const checked = await validateV1(
|
|
29488
|
+
gate,
|
|
29489
|
+
path,
|
|
29490
|
+
marker2,
|
|
29491
|
+
read,
|
|
29492
|
+
{ active: true, deny: "ordinary", denyActive: true }
|
|
29493
|
+
);
|
|
29494
|
+
current = checked.marker;
|
|
29495
|
+
state = checked.state;
|
|
29496
|
+
}
|
|
29497
|
+
await clearCredentials();
|
|
29498
|
+
for (const file of ["session.json", "key.json"]) {
|
|
29499
|
+
try {
|
|
29500
|
+
await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, file));
|
|
29501
|
+
throw gated("gate_unavailable");
|
|
29502
|
+
} catch (error) {
|
|
29503
|
+
if (!isNotFound(error)) throw error;
|
|
29504
|
+
}
|
|
29505
|
+
}
|
|
29506
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29507
|
+
const report = await inspectLayout(state !== void 0, marker2);
|
|
29508
|
+
assertApprovedInventory(report, {
|
|
29509
|
+
unknownEntries: marker2.approvedUnknown,
|
|
29510
|
+
knownRoots: marker2.knownRoots,
|
|
29511
|
+
knownEntries: marker2.knownEntries,
|
|
29512
|
+
missingDatabases: marker2.missingDatabases
|
|
29513
|
+
}, true);
|
|
29514
|
+
if (!marker2.discardLocalChanges && report.unknownEntries.length > 0) {
|
|
29515
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29516
|
+
}
|
|
29517
|
+
if (current.v === 1 && state?.eraseDeferred) {
|
|
29518
|
+
await gate.resumeDeferredGate(profileDir, {
|
|
29519
|
+
deniedEpoch: current.deniedEpoch,
|
|
29520
|
+
principalHash: current.principalHash
|
|
29521
|
+
});
|
|
29522
|
+
}
|
|
29523
|
+
await eraseLegacyReplicas({
|
|
29524
|
+
discardLocalChanges: marker2.discardLocalChanges === true,
|
|
29525
|
+
approvedUnknown: marker2.approvedUnknown,
|
|
29526
|
+
knownRoots: marker2.knownRoots,
|
|
29527
|
+
knownEntries: marker2.knownEntries,
|
|
29528
|
+
missingDatabases: marker2.missingDatabases
|
|
29529
|
+
});
|
|
29530
|
+
try {
|
|
29531
|
+
await (0, import_promises.lstat)((0, import_node_path.join)(profileDir, "replicas"));
|
|
29532
|
+
throw gated("gate_unavailable");
|
|
29533
|
+
} catch (error) {
|
|
29534
|
+
if (!isNotFound(error)) throw error;
|
|
29535
|
+
}
|
|
29536
|
+
return { marker: current };
|
|
29537
|
+
});
|
|
29538
|
+
if ("report" in prepared) return prepared.report;
|
|
29539
|
+
const marker = prepared.marker;
|
|
29540
|
+
let erased = { erased: [], failed: [], remaining: 0 };
|
|
29541
|
+
if (marker.v !== 2) {
|
|
29542
|
+
erased = await eraseGateIntents({ signal }, marker);
|
|
29543
|
+
if (erased.failed.length > 0 || erased.remaining > 0) return erased;
|
|
29544
|
+
}
|
|
29545
|
+
await profileSection(async () => {
|
|
29546
|
+
const pending = await readLogoutMarker(path);
|
|
29547
|
+
if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
|
|
29548
|
+
throw gated("stamp_stale");
|
|
29549
|
+
}
|
|
29550
|
+
const gate = await fileGate();
|
|
29551
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29552
|
+
if (marker.v === 2 && state !== void 0) throw gated("stamp_stale");
|
|
29553
|
+
const report = await inspectLayout(state !== void 0, marker);
|
|
29554
|
+
assertApprovedInventory(report, {
|
|
29555
|
+
unknownEntries: marker.approvedUnknown,
|
|
29556
|
+
knownRoots: marker.knownRoots,
|
|
29557
|
+
knownEntries: marker.knownEntries,
|
|
29558
|
+
missingDatabases: marker.missingDatabases
|
|
29559
|
+
}, true);
|
|
29560
|
+
await eraseApprovedUnknown(profileDir, marker, "replication", diagnosticFiles);
|
|
29561
|
+
await eraseKnownFlagResidue(profileDir, marker, diagnosticFiles);
|
|
29562
|
+
const finalReport = await inspectLayout(state !== void 0, marker);
|
|
29563
|
+
assertApprovedInventory(finalReport, {
|
|
29564
|
+
unknownEntries: marker.approvedUnknown,
|
|
29565
|
+
knownRoots: marker.knownRoots,
|
|
29566
|
+
knownEntries: marker.knownEntries,
|
|
29567
|
+
missingDatabases: marker.missingDatabases
|
|
29568
|
+
}, true);
|
|
29569
|
+
if (finalReport.unknownPaths.length > 0) throw localChangesRefusal(finalReport.unknownPaths);
|
|
29570
|
+
if (finalReport.knownRoots.length > 0) throw gated("gate_unavailable");
|
|
29571
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29572
|
+
await (0, import_promises.rm)(path);
|
|
29573
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29574
|
+
});
|
|
29575
|
+
return erased;
|
|
29576
|
+
} catch (error) {
|
|
29577
|
+
return failure(error);
|
|
29578
|
+
}
|
|
29579
|
+
},
|
|
29580
|
+
async pendingPurge() {
|
|
29581
|
+
const gate = await fileGate();
|
|
29582
|
+
return (await gate.readGateSnapshot(profileDir))?.intents ?? [];
|
|
29583
|
+
},
|
|
29584
|
+
erasePending({ signal }) {
|
|
29585
|
+
return eraseGateIntents({ signal });
|
|
27723
29586
|
}
|
|
27724
|
-
|
|
29587
|
+
};
|
|
29588
|
+
async function eraseGateIntents({ signal }, marker) {
|
|
29589
|
+
const gate = await fileGate();
|
|
29590
|
+
const erased = [];
|
|
29591
|
+
const failed = [];
|
|
29592
|
+
const intents = await lifecycle.pendingPurge();
|
|
27725
29593
|
try {
|
|
27726
|
-
|
|
27727
|
-
|
|
27728
|
-
|
|
27729
|
-
|
|
27730
|
-
|
|
27731
|
-
|
|
27732
|
-
|
|
27733
|
-
|
|
27734
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
29594
|
+
if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
|
|
29595
|
+
else if (marker.remotePending || await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE)) === null) {
|
|
29596
|
+
throw gated("gate_unavailable");
|
|
29597
|
+
}
|
|
29598
|
+
} catch {
|
|
29599
|
+
return { erased, failed: [{ id: "logout", code: "REPLICA_GATED" }], remaining: Math.max(1, intents.length) };
|
|
29600
|
+
}
|
|
29601
|
+
const held = await profileSection(() => gate.readGateSnapshot(profileDir));
|
|
29602
|
+
if (held?.eraseDeferred) return { erased, failed, remaining: held.intents.length, deferred: true };
|
|
29603
|
+
for (const intent of intents) {
|
|
29604
|
+
signal.throwIfAborted();
|
|
29605
|
+
try {
|
|
29606
|
+
await profileSection(async () => {
|
|
29607
|
+
if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
|
|
29608
|
+
else {
|
|
29609
|
+
const pending = await readLogoutMarker((0, import_node_path.join)(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29610
|
+
if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
|
|
29611
|
+
throw gated("gate_unavailable");
|
|
29612
|
+
}
|
|
29613
|
+
assertApprovedInventory(await inspectLayout(true), {
|
|
29614
|
+
unknownEntries: marker.approvedUnknown,
|
|
29615
|
+
knownRoots: marker.knownRoots,
|
|
29616
|
+
knownEntries: marker.knownEntries,
|
|
29617
|
+
missingDatabases: marker.missingDatabases
|
|
29618
|
+
}, true);
|
|
29619
|
+
}
|
|
29620
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29621
|
+
if (state?.eraseDeferred) return;
|
|
29622
|
+
if (!state?.intents.some((queued) => queued.id === intent.id)) return;
|
|
29623
|
+
let idDir;
|
|
29624
|
+
for (const dir of await identityDirs()) {
|
|
29625
|
+
const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
|
|
29626
|
+
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
|
|
29627
|
+
const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services3.replicationIdentityKey)(identity));
|
|
29628
|
+
if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
|
|
29629
|
+
idDir = dir;
|
|
29630
|
+
break;
|
|
29631
|
+
}
|
|
29632
|
+
}
|
|
29633
|
+
if (intent.kind === "pending") {
|
|
29634
|
+
if (intent.id !== `pending:${intent.identityKeyHash}`) throw gated("gate_unavailable");
|
|
29635
|
+
if (idDir !== void 0) {
|
|
29636
|
+
for (const name of ["pending.json", "device.jwk"]) {
|
|
29637
|
+
const file = (0, import_node_path.join)(idDir, name);
|
|
29638
|
+
let info;
|
|
29639
|
+
try {
|
|
29640
|
+
info = await (0, import_promises.lstat)(file, { bigint: true });
|
|
29641
|
+
} catch (error) {
|
|
29642
|
+
if (isNotFound(error)) continue;
|
|
29643
|
+
throw error;
|
|
29644
|
+
}
|
|
29645
|
+
if (!info.isFile()) throw gated("gate_unavailable");
|
|
29646
|
+
if (marker !== void 0) {
|
|
29647
|
+
const relative = `replication/${(0, import_node_path.basename)(idDir)}/${name}`;
|
|
29648
|
+
const expected = [...marker.knownEntries, ...marker.approvedUnknown].find(
|
|
29649
|
+
(item) => item.path === relative
|
|
29650
|
+
);
|
|
29651
|
+
if (expected?.kind !== "file" || expected.dev !== info.dev.toString() || expected.ino !== info.ino.toString()) throw localChangesRefusal([relative]);
|
|
29652
|
+
}
|
|
29653
|
+
await (0, import_promises.rm)(file);
|
|
29654
|
+
}
|
|
29655
|
+
await fsyncDir(idDir);
|
|
29656
|
+
}
|
|
29657
|
+
} else {
|
|
29658
|
+
if (!/^[0-9a-f]{64}:[a-z2-7]{26}$/.test(intent.partitionId) || intent.id !== intent.partitionId) {
|
|
29659
|
+
throw gated("gate_unavailable");
|
|
29660
|
+
}
|
|
29661
|
+
if (idDir !== void 0) {
|
|
29662
|
+
const replicaDir = (0, import_node_path.join)(idDir, "replicas", intent.partitionId.slice(65));
|
|
29663
|
+
let dirInfo;
|
|
29664
|
+
if (await realDirectoryExists((0, import_node_path.join)(idDir, "replicas"))) {
|
|
29665
|
+
try {
|
|
29666
|
+
dirInfo = await (0, import_promises.lstat)(replicaDir, { bigint: true });
|
|
29667
|
+
} catch (error) {
|
|
29668
|
+
if (!isNotFound(error)) throw error;
|
|
29669
|
+
}
|
|
29670
|
+
}
|
|
29671
|
+
if (dirInfo !== void 0) {
|
|
29672
|
+
const relative = `replication/${(0, import_node_path.basename)(idDir)}/replicas/${(0, import_node_path.basename)(replicaDir)}`;
|
|
29673
|
+
const expectedRoot = marker?.knownRoots.find((item) => item.path === relative);
|
|
29674
|
+
if (!dirInfo.isDirectory() || marker !== void 0 && (expectedRoot?.dev !== dirInfo.dev.toString() || expectedRoot.ino !== dirInfo.ino.toString())) throw localChangesRefusal([relative]);
|
|
29675
|
+
const approvedUnknown = marker?.approvedUnknown.filter(
|
|
29676
|
+
(item) => item.path.startsWith(`${relative}/`)
|
|
29677
|
+
).map(
|
|
29678
|
+
(item) => ({ ...item, path: item.path.slice(relative.length + 1) })
|
|
29679
|
+
);
|
|
29680
|
+
const expectedKnown = marker?.knownEntries.filter(
|
|
29681
|
+
(item) => item.path.startsWith(`${relative}/`)
|
|
29682
|
+
).map(
|
|
29683
|
+
(item) => ({ ...item, path: item.path.slice(relative.length + 1) })
|
|
29684
|
+
);
|
|
29685
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
29686
|
+
if (!await pathExists((0, import_node_path.join)(replicaDir, "replica.db"))) {
|
|
29687
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
|
|
29688
|
+
if (!layout.unknownPaths.includes("replica.db") || layout.unknownEntries.some((item) => item.path === "replica.db") || layout.unknownPaths.some((path) => path !== "replica.db" && !approvedUnknown?.some((item) => item.path === path))) {
|
|
29689
|
+
throw localChangesRefusal([relative]);
|
|
29690
|
+
}
|
|
29691
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29692
|
+
approvedUnknown,
|
|
29693
|
+
expectedKnown: layout.knownEntries,
|
|
29694
|
+
expectedRoot,
|
|
29695
|
+
allowAdapterFence: true,
|
|
29696
|
+
allowMissingDatabase: true
|
|
29697
|
+
});
|
|
29698
|
+
} else {
|
|
29699
|
+
let store;
|
|
29700
|
+
try {
|
|
29701
|
+
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
|
|
29702
|
+
const row = await store.open();
|
|
29703
|
+
if (row !== null) {
|
|
29704
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
|
|
29705
|
+
if (lease === null) throw gated("gate_unavailable");
|
|
29706
|
+
try {
|
|
29707
|
+
if (row.revoked === null) await store.reset(lease, "purged");
|
|
29708
|
+
let deletionKnown = expectedKnown;
|
|
29709
|
+
if (marker !== void 0) {
|
|
29710
|
+
const current = await sqlite.inspectSqliteReplicaLayout(replicaDir, {
|
|
29711
|
+
allowAdapterFence: true
|
|
29712
|
+
});
|
|
29713
|
+
if (current.rootIdentity.dev !== expectedRoot?.dev || current.rootIdentity.ino !== expectedRoot.ino) throw localChangesRefusal([relative]);
|
|
29714
|
+
const approved = new Map(expectedKnown?.map((entry) => [entry.path, entry]));
|
|
29715
|
+
for (const entry of current.knownEntries) {
|
|
29716
|
+
const prior = approved.get(entry.path);
|
|
29717
|
+
if (prior?.kind === entry.kind && prior.dev === entry.dev && prior.ino === entry.ino) continue;
|
|
29718
|
+
if (entry.kind !== "file" || !["replica.db-wal", "replica.db-shm", "replica.db-journal"].includes(entry.path)) {
|
|
29719
|
+
throw localChangesRefusal([`${relative}/${entry.path}`]);
|
|
29720
|
+
}
|
|
29721
|
+
}
|
|
29722
|
+
deletionKnown = current.knownEntries;
|
|
29723
|
+
}
|
|
29724
|
+
await store.destroy(lease, {
|
|
29725
|
+
approvedUnknown,
|
|
29726
|
+
expectedKnown: deletionKnown,
|
|
29727
|
+
expectedRoot,
|
|
29728
|
+
allowAdapterFence: true
|
|
29729
|
+
});
|
|
29730
|
+
} finally {
|
|
29731
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
29732
|
+
}
|
|
29733
|
+
} else {
|
|
29734
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29735
|
+
approvedUnknown,
|
|
29736
|
+
expectedKnown,
|
|
29737
|
+
expectedRoot,
|
|
29738
|
+
allowAdapterFence: true
|
|
29739
|
+
});
|
|
29740
|
+
}
|
|
29741
|
+
} finally {
|
|
29742
|
+
await store?.close().catch(() => void 0);
|
|
29743
|
+
}
|
|
29744
|
+
}
|
|
29745
|
+
await fsyncDir((0, import_node_path.join)(idDir, "replicas"));
|
|
29746
|
+
}
|
|
29747
|
+
}
|
|
29748
|
+
}
|
|
29749
|
+
await gate.clearGateIntent(profileDir, intent.id);
|
|
29750
|
+
erased.push(intent.id);
|
|
29751
|
+
});
|
|
29752
|
+
} catch (error) {
|
|
29753
|
+
failed.push({ id: intent.id, code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR" });
|
|
29754
|
+
}
|
|
29755
|
+
}
|
|
29756
|
+
return { erased, failed, remaining: (await lifecycle.pendingPurge()).length };
|
|
29757
|
+
}
|
|
29758
|
+
async function openHandle(spec) {
|
|
29759
|
+
const [replica, sqlite] = await Promise.all([
|
|
29760
|
+
// @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
|
|
29761
|
+
import("@tinycloud/replica"),
|
|
29762
|
+
// @ts-ignore — same as above.
|
|
29763
|
+
import("@tinycloud/replica/sqlite")
|
|
29764
|
+
]);
|
|
29765
|
+
if (spec.stamp === void 0) throw gated("not_established");
|
|
29766
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
29767
|
+
if (spec.grantPolicy === void 0) {
|
|
29768
|
+
throw new ReplicaStorageError("REPLICA_INVALID_ARGUMENT", "A replica grant policy is required.");
|
|
29769
|
+
}
|
|
29770
|
+
const gate = await fileGate();
|
|
29771
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
29772
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29773
|
+
const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services3.replicationIdentityKey)(spec.identity));
|
|
29774
|
+
const resolvePolicy = async (at, recordFirst) => {
|
|
29775
|
+
const policy = spec.grantPolicy;
|
|
29776
|
+
if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
|
|
29777
|
+
throw new TypeError("A replica grant requires a finite TTL not exceeding seven days.");
|
|
29778
|
+
}
|
|
29779
|
+
const verified = policy.consentAnchor;
|
|
29780
|
+
if (verified !== null && (!Number.isSafeInteger(verified.at) || verified.at < 0 || verified.source === "first_install")) {
|
|
29781
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The verified consent anchor is unavailable.");
|
|
29782
|
+
}
|
|
29783
|
+
const first = recordFirst ? await gate.recordFirstInstall(profileDir, identityKeyHash, at) : await gate.readFirstInstall(profileDir, identityKeyHash);
|
|
29784
|
+
const anchor = verified === null ? first === void 0 ? null : { at: first, source: "first_install" } : { at: Math.min(verified.at, at), source: verified.source };
|
|
29785
|
+
if (anchor === null) return null;
|
|
29786
|
+
const deadline = anchor.at + import_contractConstants.CONSENT_WINDOW_MS;
|
|
29787
|
+
if (!Number.isSafeInteger(anchor.at) || anchor.at < 0 || !Number.isSafeInteger(deadline)) {
|
|
29788
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The durable consent anchor is invalid.");
|
|
29789
|
+
}
|
|
29790
|
+
return { at, ttlMs: policy.grantTtlMs, consentDeadline: deadline, consentAnchor: anchor };
|
|
29791
|
+
};
|
|
29792
|
+
let partitionId;
|
|
29793
|
+
const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
|
|
29794
|
+
return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
|
|
29795
|
+
if ((0, import_sdk_services3.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
|
|
29796
|
+
throw new replica.ReplicaError(
|
|
29797
|
+
replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
29798
|
+
`Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
|
|
29799
|
+
);
|
|
29800
|
+
}
|
|
29801
|
+
const idDir = partitionDirOf(root, spec.identity);
|
|
29802
|
+
await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
|
|
29803
|
+
const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, profileSection, createDevice, options.dirSync);
|
|
29804
|
+
const device = { ...createdDevice, did: principalOf(createdDevice.did) };
|
|
29805
|
+
const replicaHash = replicaHashOf(spec.prefix, device.did);
|
|
29806
|
+
partitionId = `${identityKeyHash}:${replicaHash}`;
|
|
29807
|
+
const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHash);
|
|
29808
|
+
const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
|
|
29809
|
+
create: true,
|
|
29810
|
+
guard: storeGuard
|
|
29811
|
+
});
|
|
29812
|
+
let generation;
|
|
29813
|
+
try {
|
|
29814
|
+
try {
|
|
29815
|
+
await store.init({
|
|
29816
|
+
name: spec.prefix.replace(/\/+$/, "") || "replica",
|
|
29817
|
+
replicaId: replicaHash,
|
|
29818
|
+
host: spec.identity.host,
|
|
29819
|
+
space: spec.space,
|
|
29820
|
+
prefix: spec.prefix,
|
|
29821
|
+
deviceDid: device.did,
|
|
29822
|
+
allowSecrets: spec.allowSecrets,
|
|
29823
|
+
localReadPolicy: "whileGrantValid",
|
|
29824
|
+
retentionGrantCid: null
|
|
29825
|
+
});
|
|
29826
|
+
} catch (error) {
|
|
29827
|
+
if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
|
|
29828
|
+
throw error;
|
|
29829
|
+
}
|
|
29830
|
+
}
|
|
29831
|
+
const state = await store.open();
|
|
29832
|
+
if (state === null) {
|
|
29833
|
+
throw new replica.ReplicaError(
|
|
29834
|
+
replica.ReplicaErrorCode.NOT_FOUND,
|
|
29835
|
+
"The replica row vanished during init."
|
|
29836
|
+
);
|
|
29837
|
+
}
|
|
29838
|
+
const config = state.config;
|
|
29839
|
+
if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
|
|
29840
|
+
throw new replica.ReplicaError(
|
|
29841
|
+
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
29842
|
+
`This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
|
|
29843
|
+
);
|
|
29844
|
+
}
|
|
29845
|
+
if (principalOf(config.deviceDid) !== principalOf(device.did)) {
|
|
29846
|
+
throw new replica.ReplicaError(
|
|
29847
|
+
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
29848
|
+
"The stored replica belongs to a different device key; purge the partition to rekey."
|
|
29849
|
+
);
|
|
29850
|
+
}
|
|
29851
|
+
await retainGrantInstalls(gate, snapshot, identityKeyHash, store);
|
|
29852
|
+
if (state.grant !== null || state.pendingGrant !== null) {
|
|
29853
|
+
const policy = await resolvePolicy(Date.now(), false);
|
|
29854
|
+
if (policy === null) throw gated("gate_unavailable");
|
|
29855
|
+
if (state.grant !== null) await store.recomputeGrantInstall(state.grant, policy);
|
|
29856
|
+
if (state.pendingGrant !== null) await store.recomputeGrantInstall(state.pendingGrant, policy);
|
|
29857
|
+
}
|
|
29858
|
+
generation = state.generation;
|
|
29859
|
+
} catch (error) {
|
|
29860
|
+
await store.close().catch(() => void 0);
|
|
29861
|
+
throw error;
|
|
29862
|
+
}
|
|
29863
|
+
let ino;
|
|
29864
|
+
try {
|
|
29865
|
+
ino = (await (0, import_promises.stat)((0, import_node_path.join)(replicaDir, "replica.db"))).ino;
|
|
29866
|
+
} catch (error) {
|
|
29867
|
+
if (!isNotFound(error)) throw error;
|
|
29868
|
+
}
|
|
29869
|
+
return new SqliteReplicaHandle({
|
|
29870
|
+
spec,
|
|
29871
|
+
replicaId: replicaHash,
|
|
29872
|
+
device,
|
|
29873
|
+
store,
|
|
29874
|
+
replicaDir,
|
|
29875
|
+
replica,
|
|
29876
|
+
guard: storeGuard,
|
|
29877
|
+
read: (section, withFence) => readGated(spec, partitionId, store, replicaDir, generation, section, withFence),
|
|
29878
|
+
policy: resolvePolicy,
|
|
29879
|
+
recordGrantInstall: async (cid, at) => gate.recordGrantInstall(profileDir, await gate.grantInstallHashOf(snapshot.salt, identityKeyHash, cid), at),
|
|
29880
|
+
transportFor: options.transportFor,
|
|
29881
|
+
dirSync: options.dirSync,
|
|
29882
|
+
generation,
|
|
29883
|
+
ino
|
|
29884
|
+
});
|
|
27741
29885
|
});
|
|
27742
29886
|
}
|
|
27743
29887
|
async function inspectStatus(spec, { signal }) {
|
|
29888
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
27744
29889
|
const checkAbort = () => {
|
|
27745
29890
|
if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
|
|
27746
29891
|
};
|
|
@@ -27773,6 +29918,8 @@ function createSqliteReplicaStorage(options) {
|
|
|
27773
29918
|
import("@tinycloud/replica/sqlite")
|
|
27774
29919
|
]);
|
|
27775
29920
|
checkAbort();
|
|
29921
|
+
const syncedThroughEpoch = await readFence(replicaDir);
|
|
29922
|
+
checkAbort();
|
|
27776
29923
|
const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
|
|
27777
29924
|
checkAbort();
|
|
27778
29925
|
if (inspected === null) return void 0;
|
|
@@ -27780,14 +29927,17 @@ function createSqliteReplicaStorage(options) {
|
|
|
27780
29927
|
if (state.config.host !== spec.identity.host || state.config.space !== spec.space || state.config.prefix !== spec.prefix || principalOf(state.config.deviceDid) !== principalOf(device.did)) {
|
|
27781
29928
|
throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
|
|
27782
29929
|
}
|
|
27783
|
-
const syncedThroughEpoch = await readFence(replicaDir);
|
|
27784
29930
|
checkAbort();
|
|
29931
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29932
|
+
const gateState = await (await fileGate()).readGateSnapshot(profileDir);
|
|
29933
|
+
const purgePending = gateState !== void 0 && (gateState.eraseDeferred || gateState.intents.length > 0);
|
|
27785
29934
|
return {
|
|
29935
|
+
purgePending,
|
|
27786
29936
|
coverage: status.coverage,
|
|
27787
29937
|
lastSyncAt: status.lastSyncAt,
|
|
27788
29938
|
syncedThroughEpoch,
|
|
27789
|
-
authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
|
|
27790
|
-
grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
|
|
29939
|
+
authority: state.grant !== null && (inspected.activeGrantInstall === null || Date.now() >= inspected.activeGrantInstall.effectiveExpiresAt) ? { state: "expired", expiresAt: inspected.activeGrantInstall === null ? null : new Date(inspected.activeGrantInstall.effectiveExpiresAt).toISOString() } : { state: status.authority.state, expiresAt: status.authority.expiresAt },
|
|
29940
|
+
grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec, inspected.grantInstall),
|
|
27791
29941
|
counts: status.counts,
|
|
27792
29942
|
bytes: status.bytes,
|
|
27793
29943
|
lastError: status.lastError
|
|
@@ -27800,89 +29950,124 @@ function createSqliteReplicaStorage(options) {
|
|
|
27800
29950
|
// @ts-ignore — same as above.
|
|
27801
29951
|
import("@tinycloud/replica/sqlite")
|
|
27802
29952
|
]);
|
|
27803
|
-
|
|
27804
|
-
|
|
27805
|
-
|
|
27806
|
-
|
|
27807
|
-
|
|
27808
|
-
|
|
27809
|
-
const
|
|
27810
|
-
if (
|
|
27811
|
-
|
|
27812
|
-
|
|
27813
|
-
|
|
27814
|
-
|
|
27815
|
-
|
|
27816
|
-
|
|
27817
|
-
}
|
|
27818
|
-
if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
|
|
27819
|
-
const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
|
|
27820
|
-
let firstError;
|
|
27821
|
-
for (const deviceDid of devices) {
|
|
27822
|
-
const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
|
|
27823
|
-
if (!await pathExists(replicaDir)) continue;
|
|
27824
|
-
try {
|
|
27825
|
-
let store;
|
|
27826
|
-
try {
|
|
27827
|
-
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard });
|
|
27828
|
-
} catch (error) {
|
|
27829
|
-
if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
|
|
27830
|
-
await guard(() => (0, import_promises.rm)(replicaDir, { recursive: true, force: true }));
|
|
27831
|
-
continue;
|
|
27832
|
-
}
|
|
27833
|
-
throw error;
|
|
29953
|
+
return profileSection(async () => {
|
|
29954
|
+
const idDir = partitionDirOf(root, target.identity);
|
|
29955
|
+
if (!await realDirectoryExists(idDir)) return;
|
|
29956
|
+
const hasReplicas = await realDirectoryExists((0, import_node_path.join)(idDir, "replicas"));
|
|
29957
|
+
const deviceDids = [];
|
|
29958
|
+
const deviceFile = (0, import_node_path.join)(idDir, "device.jwk");
|
|
29959
|
+
const stored = await readJsonFile(deviceFile);
|
|
29960
|
+
if (stored !== void 0) {
|
|
29961
|
+
const device = deviceRecordOf(stored);
|
|
29962
|
+
if (device === void 0) {
|
|
29963
|
+
throw new ReplicaStorageError(
|
|
29964
|
+
"STORAGE_ERROR",
|
|
29965
|
+
`The replica device key at ${deviceFile} is unreadable.`
|
|
29966
|
+
);
|
|
27834
29967
|
}
|
|
29968
|
+
deviceDids.push(device.did);
|
|
29969
|
+
}
|
|
29970
|
+
if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
|
|
29971
|
+
const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
|
|
29972
|
+
const removeLeftover = async (replicaDir) => {
|
|
29973
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
|
|
29974
|
+
const missingDatabase = !await pathExists((0, import_node_path.join)(replicaDir, "replica.db"));
|
|
29975
|
+
const unexpected = layout.unknownPaths.filter((path) => path !== "replica.db");
|
|
29976
|
+
if (unexpected.length !== 0 || layout.unknownPaths.includes("replica.db") !== missingDatabase || layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
29977
|
+
throw new ReplicaStorageError(
|
|
29978
|
+
"STORAGE_ERROR",
|
|
29979
|
+
`Refusing to remove unexpected replica path ${(0, import_node_path.join)(replicaDir, unexpected[0] ?? "replica.db")}.`
|
|
29980
|
+
);
|
|
29981
|
+
}
|
|
29982
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29983
|
+
expectedRoot: layout.rootIdentity,
|
|
29984
|
+
expectedKnown: layout.knownEntries,
|
|
29985
|
+
allowAdapterFence: true,
|
|
29986
|
+
allowMissingDatabase: missingDatabase
|
|
29987
|
+
});
|
|
29988
|
+
};
|
|
29989
|
+
let firstError;
|
|
29990
|
+
for (const deviceDid of devices) {
|
|
29991
|
+
const replicaDir = (0, import_node_path.join)(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
|
|
29992
|
+
if (!hasReplicas || !await realDirectoryExists(replicaDir)) continue;
|
|
27835
29993
|
try {
|
|
27836
|
-
|
|
27837
|
-
|
|
27838
|
-
await
|
|
27839
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
throw
|
|
27845
|
-
replica.ReplicaErrorCode.BUSY,
|
|
27846
|
-
"Another process holds this replica's sync lease."
|
|
27847
|
-
);
|
|
29994
|
+
let store;
|
|
29995
|
+
try {
|
|
29996
|
+
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
|
|
29997
|
+
} catch (error) {
|
|
29998
|
+
if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
|
|
29999
|
+
await removeLeftover(replicaDir);
|
|
30000
|
+
continue;
|
|
30001
|
+
}
|
|
30002
|
+
throw error;
|
|
27848
30003
|
}
|
|
27849
30004
|
try {
|
|
27850
|
-
|
|
27851
|
-
|
|
30005
|
+
const state = await store.open();
|
|
30006
|
+
if (state === null) {
|
|
30007
|
+
await store.close().catch(() => void 0);
|
|
30008
|
+
await removeLeftover(replicaDir);
|
|
30009
|
+
continue;
|
|
30010
|
+
}
|
|
30011
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
|
|
30012
|
+
if (lease === null) {
|
|
30013
|
+
throw new replica.ReplicaError(
|
|
30014
|
+
replica.ReplicaErrorCode.BUSY,
|
|
30015
|
+
"Another process holds this replica's sync lease."
|
|
30016
|
+
);
|
|
30017
|
+
}
|
|
30018
|
+
try {
|
|
30019
|
+
const gate = await fileGate();
|
|
30020
|
+
const currentGate = await gate.readGateSnapshot(profileDir);
|
|
30021
|
+
if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
|
|
30022
|
+
const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services3.replicationIdentityKey)(target.identity));
|
|
30023
|
+
await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
|
|
30024
|
+
}
|
|
30025
|
+
if (state.revoked === null) {
|
|
30026
|
+
await store.reset(lease, "purged");
|
|
30027
|
+
}
|
|
30028
|
+
await store.destroy(lease, { allowAdapterFence: true });
|
|
30029
|
+
} finally {
|
|
30030
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
27852
30031
|
}
|
|
27853
|
-
await store.destroy(lease);
|
|
27854
30032
|
} finally {
|
|
27855
|
-
await store.
|
|
30033
|
+
await store.close().catch(() => void 0);
|
|
27856
30034
|
}
|
|
27857
|
-
}
|
|
27858
|
-
|
|
30035
|
+
} catch (error) {
|
|
30036
|
+
firstError ?? (firstError = error);
|
|
27859
30037
|
}
|
|
27860
|
-
} catch (error) {
|
|
27861
|
-
firstError ?? (firstError = error);
|
|
27862
30038
|
}
|
|
27863
|
-
|
|
27864
|
-
|
|
30039
|
+
if (firstError !== void 0) throw firstError;
|
|
30040
|
+
});
|
|
27865
30041
|
}
|
|
27866
30042
|
return {
|
|
27867
30043
|
kind: "sqlite",
|
|
30044
|
+
lifecycle,
|
|
27868
30045
|
open: openHandle,
|
|
27869
30046
|
inspectStatus,
|
|
27870
30047
|
purge: purgePartition,
|
|
27871
|
-
//
|
|
27872
|
-
|
|
27873
|
-
// restart that lost ambiguous write records would serve stale data.
|
|
27874
|
-
// Without a guard the file store still durably serializes each update;
|
|
27875
|
-
// the guard only adds cross-process read-modify-write exclusion.
|
|
27876
|
-
pendingWrites(identity) {
|
|
30048
|
+
// File-backed pending writes are always bound to a stamped gate.
|
|
30049
|
+
pendingWrites(identity, opts) {
|
|
27877
30050
|
return new FilePendingWriteStore(
|
|
27878
30051
|
identity,
|
|
27879
30052
|
partitionDirOf(root, identity),
|
|
30053
|
+
profileDir,
|
|
27880
30054
|
guard,
|
|
27881
|
-
|
|
30055
|
+
opts?.stamp,
|
|
30056
|
+
options.dirSync,
|
|
30057
|
+
options.logoutPendingFile
|
|
27882
30058
|
);
|
|
27883
30059
|
}
|
|
27884
30060
|
};
|
|
27885
30061
|
}
|
|
30062
|
+
function createProfileReplicaPurge(options) {
|
|
30063
|
+
return createSqliteReplicaStorage({
|
|
30064
|
+
dir: (0, import_node_path.join)(options.profileDir, "replication"),
|
|
30065
|
+
guard: options.guard,
|
|
30066
|
+
dirSync: options.dirSync,
|
|
30067
|
+
logoutPendingFile: (0, import_node_path.join)(options.profileDir, REPLICA_LOGOUT_PENDING_FILE),
|
|
30068
|
+
diagnosticFiles: options.diagnosticFiles
|
|
30069
|
+
}).lifecycle;
|
|
30070
|
+
}
|
|
27886
30071
|
function sqliteReplicaStorage(options) {
|
|
27887
30072
|
return createSqliteReplicaStorage(options);
|
|
27888
30073
|
}
|
|
@@ -27936,6 +30121,8 @@ function sqliteReplicaStorage(options) {
|
|
|
27936
30121
|
PrefixedKVService,
|
|
27937
30122
|
PrivateKeySigner,
|
|
27938
30123
|
ProtocolMismatchError,
|
|
30124
|
+
REPLICA_LOGOUT_PENDING_FILE,
|
|
30125
|
+
RESTORED_GATE_MESSAGE,
|
|
27939
30126
|
RUNTIME_DELEGATION_REJECTIONS,
|
|
27940
30127
|
RuntimeDelegationRejectedError,
|
|
27941
30128
|
SECRET_NAME_RE,
|
|
@@ -27968,6 +30155,7 @@ function sqliteReplicaStorage(options) {
|
|
|
27968
30155
|
activateValidatedRuntimeDelegation,
|
|
27969
30156
|
addressStorageKey,
|
|
27970
30157
|
appSecretScope,
|
|
30158
|
+
assertProfileLogoutClear,
|
|
27971
30159
|
buildCanonicalDecryptRequest,
|
|
27972
30160
|
buildDecryptAttenuation,
|
|
27973
30161
|
buildDecryptFacts,
|
|
@@ -27988,6 +30176,7 @@ function sqliteReplicaStorage(options) {
|
|
|
27988
30176
|
composeManifestRequest,
|
|
27989
30177
|
createCapabilityKeyRegistry,
|
|
27990
30178
|
createOpenKeyCallbackSigningStrategy,
|
|
30179
|
+
createProfileReplicaPurge,
|
|
27991
30180
|
createSharingService,
|
|
27992
30181
|
createSpaceService,
|
|
27993
30182
|
createVaultCrypto,
|
|
@@ -28024,6 +30213,7 @@ function sqliteReplicaStorage(options) {
|
|
|
28024
30213
|
isNetworkId,
|
|
28025
30214
|
isStorageFullError,
|
|
28026
30215
|
loadManifest,
|
|
30216
|
+
logoutPrincipalDigest,
|
|
28027
30217
|
makePkhSpaceId,
|
|
28028
30218
|
makePublicSpaceId,
|
|
28029
30219
|
networkDiscoveryKey,
|