@tinycloud/node-sdk 3.2.6-beta.6 → 3.3.0-beta.7
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-BchWB_zv.d.cts} +162 -283
- package/dist/{WasmKeyProvider-DEdSyQ0I.d.ts → TinyCloudNode-BchWB_zv.d.ts} +162 -283
- package/dist/WasmKeyProvider-5Ee7fcAw.d.cts +217 -0
- package/dist/WasmKeyProvider-5Ee7fcAw.d.ts +217 -0
- package/dist/authority-4HXYteKF.d.ts +118 -0
- package/dist/authority-lrA-698X.d.cts +118 -0
- package/dist/core.cjs +368 -147
- 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 +374 -155
- 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 +2702 -524
- 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 +2702 -530
- 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.js
CHANGED
|
@@ -4413,11 +4413,11 @@ var init_lib4 = __esm({
|
|
|
4413
4413
|
init_version4();
|
|
4414
4414
|
__awaiter = function(thisArg, _arguments, P, generator) {
|
|
4415
4415
|
function adopt(value) {
|
|
4416
|
-
return value instanceof P ? value : new P(function(
|
|
4417
|
-
|
|
4416
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
4417
|
+
resolve2(value);
|
|
4418
4418
|
});
|
|
4419
4419
|
}
|
|
4420
|
-
return new (P || (P = Promise))(function(
|
|
4420
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
4421
4421
|
function fulfilled(value) {
|
|
4422
4422
|
try {
|
|
4423
4423
|
step(generator.next(value));
|
|
@@ -4433,7 +4433,7 @@ var init_lib4 = __esm({
|
|
|
4433
4433
|
}
|
|
4434
4434
|
}
|
|
4435
4435
|
function step(result) {
|
|
4436
|
-
result.done ?
|
|
4436
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
4437
4437
|
}
|
|
4438
4438
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
4439
4439
|
});
|
|
@@ -8049,11 +8049,11 @@ var init_typed_data = __esm({
|
|
|
8049
8049
|
init_id();
|
|
8050
8050
|
__awaiter2 = function(thisArg, _arguments, P, generator) {
|
|
8051
8051
|
function adopt(value) {
|
|
8052
|
-
return value instanceof P ? value : new P(function(
|
|
8053
|
-
|
|
8052
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
8053
|
+
resolve2(value);
|
|
8054
8054
|
});
|
|
8055
8055
|
}
|
|
8056
|
-
return new (P || (P = Promise))(function(
|
|
8056
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
8057
8057
|
function fulfilled(value) {
|
|
8058
8058
|
try {
|
|
8059
8059
|
step(generator.next(value));
|
|
@@ -8069,7 +8069,7 @@ var init_typed_data = __esm({
|
|
|
8069
8069
|
}
|
|
8070
8070
|
}
|
|
8071
8071
|
function step(result) {
|
|
8072
|
-
result.done ?
|
|
8072
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
8073
8073
|
}
|
|
8074
8074
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8075
8075
|
});
|
|
@@ -14911,7 +14911,7 @@ var require_scrypt = __commonJS({
|
|
|
14911
14911
|
}
|
|
14912
14912
|
const lib = {
|
|
14913
14913
|
scrypt: function(password, salt, N2, r2, p, dkLen, progressCallback) {
|
|
14914
|
-
return new Promise(function(
|
|
14914
|
+
return new Promise(function(resolve2, reject2) {
|
|
14915
14915
|
let lastProgress = 0;
|
|
14916
14916
|
if (progressCallback) {
|
|
14917
14917
|
progressCallback(0);
|
|
@@ -14923,7 +14923,7 @@ var require_scrypt = __commonJS({
|
|
|
14923
14923
|
if (progressCallback && lastProgress !== 1) {
|
|
14924
14924
|
progressCallback(1);
|
|
14925
14925
|
}
|
|
14926
|
-
|
|
14926
|
+
resolve2(new Uint8Array(key2));
|
|
14927
14927
|
} else if (progressCallback && progress !== lastProgress) {
|
|
14928
14928
|
lastProgress = progress;
|
|
14929
14929
|
return progressCallback(progress);
|
|
@@ -15303,11 +15303,11 @@ var init_keystore = __esm({
|
|
|
15303
15303
|
init_version15();
|
|
15304
15304
|
__awaiter3 = function(thisArg, _arguments, P, generator) {
|
|
15305
15305
|
function adopt(value) {
|
|
15306
|
-
return value instanceof P ? value : new P(function(
|
|
15307
|
-
|
|
15306
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
15307
|
+
resolve2(value);
|
|
15308
15308
|
});
|
|
15309
15309
|
}
|
|
15310
|
-
return new (P || (P = Promise))(function(
|
|
15310
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
15311
15311
|
function fulfilled(value) {
|
|
15312
15312
|
try {
|
|
15313
15313
|
step(generator.next(value));
|
|
@@ -15323,7 +15323,7 @@ var init_keystore = __esm({
|
|
|
15323
15323
|
}
|
|
15324
15324
|
}
|
|
15325
15325
|
function step(result) {
|
|
15326
|
-
result.done ?
|
|
15326
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
15327
15327
|
}
|
|
15328
15328
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15329
15329
|
});
|
|
@@ -15616,11 +15616,11 @@ var init_lib25 = __esm({
|
|
|
15616
15616
|
init_version19();
|
|
15617
15617
|
__awaiter4 = function(thisArg, _arguments, P, generator) {
|
|
15618
15618
|
function adopt(value) {
|
|
15619
|
-
return value instanceof P ? value : new P(function(
|
|
15620
|
-
|
|
15619
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
15620
|
+
resolve2(value);
|
|
15621
15621
|
});
|
|
15622
15622
|
}
|
|
15623
|
-
return new (P || (P = Promise))(function(
|
|
15623
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
15624
15624
|
function fulfilled(value) {
|
|
15625
15625
|
try {
|
|
15626
15626
|
step(generator.next(value));
|
|
@@ -15636,7 +15636,7 @@ var init_lib25 = __esm({
|
|
|
15636
15636
|
}
|
|
15637
15637
|
}
|
|
15638
15638
|
function step(result) {
|
|
15639
|
-
result.done ?
|
|
15639
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
15640
15640
|
}
|
|
15641
15641
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15642
15642
|
});
|
|
@@ -15698,11 +15698,11 @@ var init_lib26 = __esm({
|
|
|
15698
15698
|
init_version20();
|
|
15699
15699
|
__awaiter5 = function(thisArg, _arguments, P, generator) {
|
|
15700
15700
|
function adopt(value) {
|
|
15701
|
-
return value instanceof P ? value : new P(function(
|
|
15702
|
-
|
|
15701
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
15702
|
+
resolve2(value);
|
|
15703
15703
|
});
|
|
15704
15704
|
}
|
|
15705
|
-
return new (P || (P = Promise))(function(
|
|
15705
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
15706
15706
|
function fulfilled(value) {
|
|
15707
15707
|
try {
|
|
15708
15708
|
step(generator.next(value));
|
|
@@ -15718,7 +15718,7 @@ var init_lib26 = __esm({
|
|
|
15718
15718
|
}
|
|
15719
15719
|
}
|
|
15720
15720
|
function step(result) {
|
|
15721
|
-
result.done ?
|
|
15721
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
15722
15722
|
}
|
|
15723
15723
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15724
15724
|
});
|
|
@@ -16020,11 +16020,11 @@ var init_lib27 = __esm({
|
|
|
16020
16020
|
init_version21();
|
|
16021
16021
|
__awaiter6 = function(thisArg, _arguments, P, generator) {
|
|
16022
16022
|
function adopt(value) {
|
|
16023
|
-
return value instanceof P ? value : new P(function(
|
|
16024
|
-
|
|
16023
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
16024
|
+
resolve2(value);
|
|
16025
16025
|
});
|
|
16026
16026
|
}
|
|
16027
|
-
return new (P || (P = Promise))(function(
|
|
16027
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
16028
16028
|
function fulfilled(value) {
|
|
16029
16029
|
try {
|
|
16030
16030
|
step(generator.next(value));
|
|
@@ -16040,7 +16040,7 @@ var init_lib27 = __esm({
|
|
|
16040
16040
|
}
|
|
16041
16041
|
}
|
|
16042
16042
|
function step(result) {
|
|
16043
|
-
result.done ?
|
|
16043
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
16044
16044
|
}
|
|
16045
16045
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16046
16046
|
});
|
|
@@ -16258,11 +16258,11 @@ var init_geturl = __esm({
|
|
|
16258
16258
|
init_lib2();
|
|
16259
16259
|
__awaiter7 = function(thisArg, _arguments, P, generator) {
|
|
16260
16260
|
function adopt(value) {
|
|
16261
|
-
return value instanceof P ? value : new P(function(
|
|
16262
|
-
|
|
16261
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
16262
|
+
resolve2(value);
|
|
16263
16263
|
});
|
|
16264
16264
|
}
|
|
16265
|
-
return new (P || (P = Promise))(function(
|
|
16265
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
16266
16266
|
function fulfilled(value) {
|
|
16267
16267
|
try {
|
|
16268
16268
|
step(generator.next(value));
|
|
@@ -16278,7 +16278,7 @@ var init_geturl = __esm({
|
|
|
16278
16278
|
}
|
|
16279
16279
|
}
|
|
16280
16280
|
function step(result) {
|
|
16281
|
-
result.done ?
|
|
16281
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
16282
16282
|
}
|
|
16283
16283
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16284
16284
|
});
|
|
@@ -16294,8 +16294,8 @@ __export(lib_exports21, {
|
|
|
16294
16294
|
poll: () => poll
|
|
16295
16295
|
});
|
|
16296
16296
|
function staller(duration) {
|
|
16297
|
-
return new Promise((
|
|
16298
|
-
setTimeout(
|
|
16297
|
+
return new Promise((resolve2) => {
|
|
16298
|
+
setTimeout(resolve2, duration);
|
|
16299
16299
|
});
|
|
16300
16300
|
}
|
|
16301
16301
|
function bodyify(value, type) {
|
|
@@ -16415,7 +16415,7 @@ function _fetchData(connection, body, processFunc) {
|
|
|
16415
16415
|
options.headers = flatHeaders;
|
|
16416
16416
|
const runningTimeout = (function() {
|
|
16417
16417
|
let timer = null;
|
|
16418
|
-
const promise = new Promise(function(
|
|
16418
|
+
const promise = new Promise(function(resolve2, reject2) {
|
|
16419
16419
|
if (timeout) {
|
|
16420
16420
|
timer = setTimeout(() => {
|
|
16421
16421
|
if (timer == null) {
|
|
@@ -16585,7 +16585,7 @@ function poll(func, options) {
|
|
|
16585
16585
|
if (options.interval == null) {
|
|
16586
16586
|
options.interval = 250;
|
|
16587
16587
|
}
|
|
16588
|
-
return new Promise(function(
|
|
16588
|
+
return new Promise(function(resolve2, reject2) {
|
|
16589
16589
|
let timer = null;
|
|
16590
16590
|
let done = false;
|
|
16591
16591
|
const cancel = () => {
|
|
@@ -16611,7 +16611,7 @@ function poll(func, options) {
|
|
|
16611
16611
|
return func().then(function(result) {
|
|
16612
16612
|
if (result !== void 0) {
|
|
16613
16613
|
if (cancel()) {
|
|
16614
|
-
|
|
16614
|
+
resolve2(result);
|
|
16615
16615
|
}
|
|
16616
16616
|
} else if (options.oncePoll) {
|
|
16617
16617
|
options.oncePoll.once("poll", check);
|
|
@@ -16657,11 +16657,11 @@ var init_lib28 = __esm({
|
|
|
16657
16657
|
init_geturl();
|
|
16658
16658
|
__awaiter8 = function(thisArg, _arguments, P, generator) {
|
|
16659
16659
|
function adopt(value) {
|
|
16660
|
-
return value instanceof P ? value : new P(function(
|
|
16661
|
-
|
|
16660
|
+
return value instanceof P ? value : new P(function(resolve2) {
|
|
16661
|
+
resolve2(value);
|
|
16662
16662
|
});
|
|
16663
16663
|
}
|
|
16664
|
-
return new (P || (P = Promise))(function(
|
|
16664
|
+
return new (P || (P = Promise))(function(resolve2, reject2) {
|
|
16665
16665
|
function fulfilled(value) {
|
|
16666
16666
|
try {
|
|
16667
16667
|
step(generator.next(value));
|
|
@@ -16677,7 +16677,7 @@ var init_lib28 = __esm({
|
|
|
16677
16677
|
}
|
|
16678
16678
|
}
|
|
16679
16679
|
function step(result) {
|
|
16680
|
-
result.done ?
|
|
16680
|
+
result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
16681
16681
|
}
|
|
16682
16682
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16683
16683
|
});
|
|
@@ -17035,44 +17035,6 @@ var require_utils2 = __commonJS({
|
|
|
17035
17035
|
}
|
|
17036
17036
|
});
|
|
17037
17037
|
|
|
17038
|
-
// src/replication/config.ts
|
|
17039
|
-
import { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from "@tinycloud/sdk-services/kv/replication/prefix";
|
|
17040
|
-
function assertValidReplicationConfig(replication) {
|
|
17041
|
-
if (replication === void 0 || replication.enabled !== true) return;
|
|
17042
|
-
if (replication.storage === void 0 || replication.storage === null) {
|
|
17043
|
-
throw new TypeError("replication.enabled requires replication.storage");
|
|
17044
|
-
}
|
|
17045
|
-
if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
|
|
17046
|
-
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
17047
|
-
}
|
|
17048
|
-
for (const prefix of replication.prefixes) {
|
|
17049
|
-
try {
|
|
17050
|
-
assertPureFolderPrefix(prefix);
|
|
17051
|
-
} catch (error) {
|
|
17052
|
-
throw new TypeError(error instanceof Error ? error.message : String(error));
|
|
17053
|
-
}
|
|
17054
|
-
}
|
|
17055
|
-
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
17056
|
-
for (const other of replication.prefixes.slice(index + 1)) {
|
|
17057
|
-
if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
|
|
17058
|
-
throw new TypeError(
|
|
17059
|
-
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
17060
|
-
);
|
|
17061
|
-
}
|
|
17062
|
-
}
|
|
17063
|
-
if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
|
|
17064
|
-
throw new TypeError(
|
|
17065
|
-
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
17066
|
-
);
|
|
17067
|
-
}
|
|
17068
|
-
}
|
|
17069
|
-
}
|
|
17070
|
-
var init_config = __esm({
|
|
17071
|
-
"src/replication/config.ts"() {
|
|
17072
|
-
"use strict";
|
|
17073
|
-
}
|
|
17074
|
-
});
|
|
17075
|
-
|
|
17076
17038
|
// src/replication/authority-sign-in.ts
|
|
17077
17039
|
import {
|
|
17078
17040
|
KV,
|
|
@@ -17130,6 +17092,49 @@ var init_authority_sign_in = __esm({
|
|
|
17130
17092
|
}
|
|
17131
17093
|
});
|
|
17132
17094
|
|
|
17095
|
+
// src/replication/config.ts
|
|
17096
|
+
import { MAX_GRANT_TTL_MS } from "@tinycloud/sdk-services/kv/replication/grantPolicyConstants";
|
|
17097
|
+
import { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from "@tinycloud/sdk-services/kv/replication/prefix";
|
|
17098
|
+
function assertValidReplicationConfig(replication) {
|
|
17099
|
+
if (replication === void 0 || !replication.enabled) return;
|
|
17100
|
+
if (replication.storage === void 0 || replication.storage === null) {
|
|
17101
|
+
throw new TypeError("replication.enabled requires replication.storage");
|
|
17102
|
+
}
|
|
17103
|
+
if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
|
|
17104
|
+
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
17105
|
+
}
|
|
17106
|
+
const grantTtlMs = replication.grantTtlMs;
|
|
17107
|
+
if (grantTtlMs !== void 0 && (!Number.isFinite(grantTtlMs) || grantTtlMs <= 0 || grantTtlMs > MAX_GRANT_TTL_MS)) {
|
|
17108
|
+
throw new TypeError(`replication.grantTtlMs must be a positive number of milliseconds no greater than ${MAX_GRANT_TTL_MS}`);
|
|
17109
|
+
}
|
|
17110
|
+
for (const prefix of replication.prefixes) {
|
|
17111
|
+
try {
|
|
17112
|
+
assertPureFolderPrefix(prefix);
|
|
17113
|
+
} catch (error) {
|
|
17114
|
+
throw new TypeError(error instanceof Error ? error.message : String(error));
|
|
17115
|
+
}
|
|
17116
|
+
}
|
|
17117
|
+
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
17118
|
+
for (const other of replication.prefixes.slice(index + 1)) {
|
|
17119
|
+
if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
|
|
17120
|
+
throw new TypeError(
|
|
17121
|
+
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
17122
|
+
);
|
|
17123
|
+
}
|
|
17124
|
+
}
|
|
17125
|
+
if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
|
|
17126
|
+
throw new TypeError(
|
|
17127
|
+
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
17128
|
+
);
|
|
17129
|
+
}
|
|
17130
|
+
}
|
|
17131
|
+
}
|
|
17132
|
+
var init_config = __esm({
|
|
17133
|
+
"src/replication/config.ts"() {
|
|
17134
|
+
"use strict";
|
|
17135
|
+
}
|
|
17136
|
+
});
|
|
17137
|
+
|
|
17133
17138
|
// src/replication/runtime.ts
|
|
17134
17139
|
var runtime_exports = {};
|
|
17135
17140
|
__export(runtime_exports, {
|
|
@@ -17173,10 +17178,23 @@ var init_runtime = __esm({
|
|
|
17173
17178
|
this.control = {
|
|
17174
17179
|
status: async () => {
|
|
17175
17180
|
await this.binding;
|
|
17181
|
+
if (this.gatedReason) return this.options.prefixes.map((prefix) => ({
|
|
17182
|
+
prefix,
|
|
17183
|
+
state: "gated",
|
|
17184
|
+
gate: this.gatedReason,
|
|
17185
|
+
errorCode: "REPLICA_GATED",
|
|
17186
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
17187
|
+
pinned: [],
|
|
17188
|
+
lagMs: null
|
|
17189
|
+
}));
|
|
17176
17190
|
return this.current ? this.current.status() : [];
|
|
17177
17191
|
},
|
|
17178
17192
|
sync: async (input) => {
|
|
17179
17193
|
await this.binding;
|
|
17194
|
+
if (this.gatedReason) throw Object.assign(new Error("Replica restore is gated."), {
|
|
17195
|
+
code: "REPLICA_GATED",
|
|
17196
|
+
gate: this.gatedReason
|
|
17197
|
+
});
|
|
17180
17198
|
if (this.purgedGeneration === this.generation) return;
|
|
17181
17199
|
if (this.current) await this.current.sync(input);
|
|
17182
17200
|
},
|
|
@@ -17224,11 +17242,59 @@ var init_runtime = __esm({
|
|
|
17224
17242
|
for (const kv of this.attached.keys()) kv.setReadThrough(null);
|
|
17225
17243
|
this.attached.clear();
|
|
17226
17244
|
}
|
|
17245
|
+
/** Observe a refused restore without opening replica state; loader failure leaves raw network KV usable. */
|
|
17246
|
+
bindGatedNetworkOnly(input, loadGatedNetwork) {
|
|
17247
|
+
if (this.closed || !this.options.enabled) return Promise.resolve();
|
|
17248
|
+
const retired = this.unbind();
|
|
17249
|
+
const generation = this.generation;
|
|
17250
|
+
this.purgedGeneration = void 0;
|
|
17251
|
+
this.pendingPurge = void 0;
|
|
17252
|
+
this.pendingPurgeResult = void 0;
|
|
17253
|
+
this.pending.clear();
|
|
17254
|
+
this.lastController = void 0;
|
|
17255
|
+
this.lastPending = void 0;
|
|
17256
|
+
this.lastIdentity = void 0;
|
|
17257
|
+
this.gatedReason = input.gate;
|
|
17258
|
+
this.primarySpace = input.session.spaceId;
|
|
17259
|
+
this.binding = retired.then(async () => {
|
|
17260
|
+
let create;
|
|
17261
|
+
try {
|
|
17262
|
+
({ createGatedNetworkReadThrough: create } = await loadGatedNetwork());
|
|
17263
|
+
} catch {
|
|
17264
|
+
return;
|
|
17265
|
+
}
|
|
17266
|
+
if (generation !== this.generation || this.closed || typeof create !== "function") return;
|
|
17267
|
+
const networkOnly = create({
|
|
17268
|
+
space: input.session.spaceId,
|
|
17269
|
+
prefixes: this.options.prefixes,
|
|
17270
|
+
now: () => this.clock.now(),
|
|
17271
|
+
emit: (event) => {
|
|
17272
|
+
if (generation !== this.generation || this.closed || this.gated !== networkOnly) return;
|
|
17273
|
+
try {
|
|
17274
|
+
this.options.onEvent?.(event);
|
|
17275
|
+
} catch {
|
|
17276
|
+
}
|
|
17277
|
+
const { type, ...data } = event;
|
|
17278
|
+
try {
|
|
17279
|
+
input.context.emit(type, data);
|
|
17280
|
+
} catch {
|
|
17281
|
+
}
|
|
17282
|
+
}
|
|
17283
|
+
});
|
|
17284
|
+
this.gated = networkOnly;
|
|
17285
|
+
for (const scoped of input.primaryKV) this.attach(scoped.space, scoped.kv);
|
|
17286
|
+
});
|
|
17287
|
+
return this.binding;
|
|
17288
|
+
}
|
|
17227
17289
|
bind(input) {
|
|
17228
17290
|
if (this.closed || !this.options.enabled) return Promise.resolve();
|
|
17291
|
+
this.gated = void 0;
|
|
17292
|
+
this.gatedReason = void 0;
|
|
17229
17293
|
this.bindingActive = true;
|
|
17230
17294
|
const generation = ++this.generation;
|
|
17231
17295
|
this.purgedGeneration = void 0;
|
|
17296
|
+
this.unsubscribeConsentExpired?.();
|
|
17297
|
+
this.unsubscribeConsentExpired = void 0;
|
|
17232
17298
|
const { context, session } = input;
|
|
17233
17299
|
const emit = (event) => {
|
|
17234
17300
|
try {
|
|
@@ -17261,11 +17327,17 @@ var init_runtime = __esm({
|
|
|
17261
17327
|
return;
|
|
17262
17328
|
}
|
|
17263
17329
|
const key2 = services.replicationIdentityKey(identity);
|
|
17264
|
-
let
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
this.
|
|
17330
|
+
let cached = this.pending.get(key2);
|
|
17331
|
+
const stamp = input.stamp;
|
|
17332
|
+
if (!cached || cached.stamp?.incarnation !== stamp?.incarnation || cached.stamp?.lifecycleEpoch !== stamp?.lifecycleEpoch) {
|
|
17333
|
+
const store = this.options.storage.pendingWrites?.(identity, { stamp });
|
|
17334
|
+
if (!store && this.options.storage.lifecycle) {
|
|
17335
|
+
throw Object.assign(new Error("Gated replication requires a stamped pending store"), { code: "REPLICA_GATED", gate: "gate_unavailable" });
|
|
17336
|
+
}
|
|
17337
|
+
cached = { stamp, store: store ?? services.createMemoryPendingStore(identity) };
|
|
17338
|
+
this.pending.set(key2, cached);
|
|
17268
17339
|
}
|
|
17340
|
+
const pending = cached.store;
|
|
17269
17341
|
this.lastPending = pending;
|
|
17270
17342
|
const controller = services.createKVReplication({
|
|
17271
17343
|
options: resolvedOptions(this.options),
|
|
@@ -17273,6 +17345,7 @@ var init_runtime = __esm({
|
|
|
17273
17345
|
storage: this.options.storage,
|
|
17274
17346
|
identity,
|
|
17275
17347
|
session: { id: session.delegationCid, did: session.verificationMethod, space: session.spaceId },
|
|
17348
|
+
stamp: input.stamp,
|
|
17276
17349
|
authority: input.authority,
|
|
17277
17350
|
pending,
|
|
17278
17351
|
scheduler: this.clock,
|
|
@@ -17282,6 +17355,9 @@ var init_runtime = __esm({
|
|
|
17282
17355
|
previous: previousClose
|
|
17283
17356
|
});
|
|
17284
17357
|
this.current = controller;
|
|
17358
|
+
this.unsubscribeConsentExpired = this.options.storage.lifecycle?.subscribeConsentExpired?.(
|
|
17359
|
+
(stamp2) => controller.observeConsentExpired(stamp2)
|
|
17360
|
+
);
|
|
17285
17361
|
this.lastController = controller;
|
|
17286
17362
|
this.lastIdentity = identity;
|
|
17287
17363
|
for (const [kv, space] of this.attached) {
|
|
@@ -17302,17 +17378,24 @@ var init_runtime = __esm({
|
|
|
17302
17378
|
}
|
|
17303
17379
|
attach(space, kv) {
|
|
17304
17380
|
this.attached.set(kv, space);
|
|
17305
|
-
kv.setReadThrough(space === this.primarySpace ? this.current ?? null : null);
|
|
17381
|
+
kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
|
|
17382
|
+
const generation = this.generation;
|
|
17306
17383
|
return this.binding.then(() => {
|
|
17307
|
-
|
|
17384
|
+
if (generation === this.generation && !this.closed && this.attached.get(kv) === space) {
|
|
17385
|
+
kv.setReadThrough(space === this.primarySpace ? this.current ?? this.gated ?? null : null);
|
|
17386
|
+
}
|
|
17308
17387
|
});
|
|
17309
17388
|
}
|
|
17310
17389
|
unbind() {
|
|
17311
17390
|
this.generation++;
|
|
17312
17391
|
this.bindingActive = false;
|
|
17313
17392
|
this.pendingPurgeSet = false;
|
|
17393
|
+
this.unsubscribeConsentExpired?.();
|
|
17394
|
+
this.unsubscribeConsentExpired = void 0;
|
|
17314
17395
|
const controller = this.current;
|
|
17315
17396
|
this.current = void 0;
|
|
17397
|
+
this.gated = void 0;
|
|
17398
|
+
this.gatedReason = void 0;
|
|
17316
17399
|
this.primarySpace = void 0;
|
|
17317
17400
|
this.detachAll();
|
|
17318
17401
|
const binding = this.binding;
|
|
@@ -17331,22 +17414,104 @@ var init_runtime = __esm({
|
|
|
17331
17414
|
var authority_exports = {};
|
|
17332
17415
|
__export(authority_exports, {
|
|
17333
17416
|
AUTHORITY_EXPIRY_MARGIN_MS: () => AUTHORITY_EXPIRY_MARGIN_MS,
|
|
17417
|
+
assertRenewalParent: () => assertRenewalParent,
|
|
17334
17418
|
assertValidReplicationConfig: () => assertValidReplicationConfig,
|
|
17335
17419
|
augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
|
|
17336
17420
|
createReplicationAuthority: () => createReplicationAuthority,
|
|
17421
|
+
establishReplicaSessionStamp: () => establishReplicaSessionStamp,
|
|
17337
17422
|
hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
|
|
17423
|
+
mintNodeReplicationGrant: () => mintNodeReplicationGrant,
|
|
17338
17424
|
replicationScopeFromSignedSession: () => replicationScopeFromSignedSession,
|
|
17425
|
+
reportReplicaDenial: () => reportReplicaDenial,
|
|
17426
|
+
restoreReplicaSessionStamp: () => restoreReplicaSessionStamp,
|
|
17339
17427
|
ucanAttCovers: () => ucanAttCovers,
|
|
17340
|
-
ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
|
|
17428
|
+
ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor,
|
|
17429
|
+
verifiedOwnerConsentAnchor: () => verifiedOwnerConsentAnchor
|
|
17341
17430
|
});
|
|
17431
|
+
import { SiweMessage as SiweMessage4 } from "siwe";
|
|
17342
17432
|
import {
|
|
17343
17433
|
actionContains,
|
|
17344
17434
|
KV as KV4,
|
|
17345
17435
|
canonicalizeRecapCaveats as canonicalizeRecapCaveats3,
|
|
17346
17436
|
isCapabilitySubset as isCapabilitySubset3,
|
|
17437
|
+
principalDid,
|
|
17347
17438
|
parseSpaceUri as parseSpaceUri2
|
|
17348
17439
|
} from "@tinycloud/sdk-core";
|
|
17349
17440
|
import { parseUcanGrant as replicaParseUcanGrant } from "@tinycloud/replica";
|
|
17441
|
+
function reportReplicaDenial(result, reason, onEvent, context) {
|
|
17442
|
+
if (result.status !== "denied" && result.status !== "deleted" && result.status !== "marker") return;
|
|
17443
|
+
const event = {
|
|
17444
|
+
type: "replication.lifecycle",
|
|
17445
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17446
|
+
reason,
|
|
17447
|
+
action: result.status === "denied" ? "denied" : result.status === "deleted" ? "database_deleted" : "marker_set",
|
|
17448
|
+
...result.status === "denied" ? { partitions: result.intents } : {},
|
|
17449
|
+
...result.status === "marker" ? { code: result.code } : {}
|
|
17450
|
+
};
|
|
17451
|
+
try {
|
|
17452
|
+
onEvent?.(event);
|
|
17453
|
+
} catch {
|
|
17454
|
+
}
|
|
17455
|
+
const { type, ...data } = event;
|
|
17456
|
+
try {
|
|
17457
|
+
context?.emit(type, data);
|
|
17458
|
+
} catch {
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
17461
|
+
function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
|
|
17462
|
+
if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
|
|
17463
|
+
try {
|
|
17464
|
+
const verified = wasmBindings.validatePersistedSession?.({
|
|
17465
|
+
delegationHeader: session.delegationHeader,
|
|
17466
|
+
delegationCid: session.delegationCid,
|
|
17467
|
+
spaceId: session.spaceId,
|
|
17468
|
+
jwk: session.jwk,
|
|
17469
|
+
address: session.address,
|
|
17470
|
+
chainId: session.chainId,
|
|
17471
|
+
siwe: session.siwe,
|
|
17472
|
+
signature: session.signature
|
|
17473
|
+
});
|
|
17474
|
+
if (!verified || !Number.isSafeInteger(Date.parse(verified.expiresAt ?? ""))) return null;
|
|
17475
|
+
const signedIssuedAt = new SiweMessage4(session.siwe).issuedAt;
|
|
17476
|
+
if (typeof signedIssuedAt !== "string") return null;
|
|
17477
|
+
const issuedAt = Date.parse(signedIssuedAt);
|
|
17478
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 0) return null;
|
|
17479
|
+
const granted = exactVerifiedRecap2(verified).filter((entry) => entry.space === session.spaceId && entry.service === "kv").map((entry) => ({ ...entry, service: "tinycloud.kv" }));
|
|
17480
|
+
const coversGetAndSync = granted.some((entry) => isCapabilitySubset3([{
|
|
17481
|
+
service: "tinycloud.kv",
|
|
17482
|
+
space: session.spaceId,
|
|
17483
|
+
path: entry.path,
|
|
17484
|
+
actions: [KV4.GET, KV4.SYNC]
|
|
17485
|
+
}], granted).subset);
|
|
17486
|
+
return coversGetAndSync ? { at: issuedAt, source: "siwe" } : null;
|
|
17487
|
+
} catch {
|
|
17488
|
+
return null;
|
|
17489
|
+
}
|
|
17490
|
+
}
|
|
17491
|
+
function establishReplicaSessionStamp(storage, principal, anchor) {
|
|
17492
|
+
if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
|
|
17493
|
+
return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
|
|
17494
|
+
}
|
|
17495
|
+
if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
|
|
17496
|
+
return storage.lifecycle.establishSessionStamp(principal, { consentAnchor: anchor });
|
|
17497
|
+
}
|
|
17498
|
+
throw Object.assign(new Error("Replica sign-in requires its platform lifecycle gate."), {
|
|
17499
|
+
code: "REPLICA_GATED",
|
|
17500
|
+
gate: "gate_unavailable"
|
|
17501
|
+
});
|
|
17502
|
+
}
|
|
17503
|
+
function restoreReplicaSessionStamp(storage, principal, persisted) {
|
|
17504
|
+
if (storage.kind === "indexeddb" && storage.lifecycle?.kind === "indexeddb") {
|
|
17505
|
+
return storage.lifecycle.restoreSessionStamp(principal, persisted);
|
|
17506
|
+
}
|
|
17507
|
+
if (storage.kind === "sqlite" && storage.lifecycle?.kind === "file") {
|
|
17508
|
+
return storage.lifecycle.restoreSessionStamp(principal, persisted);
|
|
17509
|
+
}
|
|
17510
|
+
throw Object.assign(new Error("Replica restore requires its platform lifecycle gate."), {
|
|
17511
|
+
code: "REPLICA_GATED",
|
|
17512
|
+
gate: "gate_unavailable"
|
|
17513
|
+
});
|
|
17514
|
+
}
|
|
17350
17515
|
function replicationEntries(spaceId, prefix) {
|
|
17351
17516
|
return [{
|
|
17352
17517
|
service: KV_SERVICE2,
|
|
@@ -17394,6 +17559,20 @@ function ucanAttCovers(att, space, prefix, ability, options) {
|
|
|
17394
17559
|
function ucanAttUnconstrainedFor(att, space, prefix) {
|
|
17395
17560
|
return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
|
|
17396
17561
|
}
|
|
17562
|
+
async function mintNodeReplicationGrant(deviceDid, entries, options, renewalSession, node) {
|
|
17563
|
+
if (options?.renewalOnly && !renewalSession) throw new Error("replication renewal lost its owner session");
|
|
17564
|
+
const delegateOptions = options?.renewalOnly ? {
|
|
17565
|
+
expiry: options.expiryMs,
|
|
17566
|
+
onPrepared: async (prepared) => {
|
|
17567
|
+
assertRenewalParent(prepared.delegationHeader.Authorization, renewalSession);
|
|
17568
|
+
}
|
|
17569
|
+
} : options?.expiryMs === void 0 ? void 0 : { expiry: options.expiryMs };
|
|
17570
|
+
const result = await node.delegateTo(principalDid(deviceDid), entries, delegateOptions);
|
|
17571
|
+
return {
|
|
17572
|
+
ucan: result.delegation.delegationHeader.Authorization,
|
|
17573
|
+
expiresAt: result.delegation.expiry.getTime()
|
|
17574
|
+
};
|
|
17575
|
+
}
|
|
17397
17576
|
function replicationScopeFromSignedSession(input) {
|
|
17398
17577
|
let grant;
|
|
17399
17578
|
try {
|
|
@@ -17411,9 +17590,28 @@ function replicationScopeFromSignedSession(input) {
|
|
|
17411
17590
|
}
|
|
17412
17591
|
return void 0;
|
|
17413
17592
|
}
|
|
17593
|
+
function assertRenewalParent(authorization, session) {
|
|
17594
|
+
const grant = parseUcanGrant(authorization);
|
|
17595
|
+
if (grant.issuer !== session.verificationMethod.split("#", 1)[0] || grant.prf.length !== 1 || grant.prf[0] !== session.delegationCid) {
|
|
17596
|
+
throw Object.assign(new Error("replication renewal requires the bound owner session parent"), { refused: "NOT_COVERED" });
|
|
17597
|
+
}
|
|
17598
|
+
}
|
|
17414
17599
|
function createReplicationAuthority(host) {
|
|
17415
|
-
const
|
|
17416
|
-
|
|
17600
|
+
const boundSession = host.replicationSession();
|
|
17601
|
+
let verifiedAnchor = null;
|
|
17602
|
+
try {
|
|
17603
|
+
const candidate = boundSession && host.verifiedConsentAnchor?.(boundSession);
|
|
17604
|
+
if (candidate && Number.isFinite(candidate.at) && candidate.source !== "first_install") {
|
|
17605
|
+
verifiedAnchor = { at: candidate.at, source: candidate.source };
|
|
17606
|
+
}
|
|
17607
|
+
} catch {
|
|
17608
|
+
}
|
|
17609
|
+
const activeSession = () => {
|
|
17610
|
+
const session = host.replicationSession();
|
|
17611
|
+
return session !== void 0 && session === boundSession ? session : void 0;
|
|
17612
|
+
};
|
|
17613
|
+
const planOrRefusal = (entries, expiryMs) => {
|
|
17614
|
+
const plan = host.planDelegation(entries, expiryMs === void 0 ? void 0 : { expiry: expiryMs });
|
|
17417
17615
|
if ("refused" in plan) return plan;
|
|
17418
17616
|
return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
|
|
17419
17617
|
};
|
|
@@ -17425,12 +17623,20 @@ function createReplicationAuthority(host) {
|
|
|
17425
17623
|
};
|
|
17426
17624
|
const authority = {
|
|
17427
17625
|
get sessionOnly() {
|
|
17428
|
-
const session =
|
|
17429
|
-
return session
|
|
17626
|
+
const session = activeSession();
|
|
17627
|
+
return session === void 0 || session.siwe === void 0;
|
|
17628
|
+
},
|
|
17629
|
+
consentAnchor() {
|
|
17630
|
+
return activeSession() ? verifiedAnchor : null;
|
|
17631
|
+
},
|
|
17632
|
+
get canRenew() {
|
|
17633
|
+
const session = activeSession();
|
|
17634
|
+
return verifiedAnchor?.source === "siwe" && session !== void 0 && session.siwe !== void 0;
|
|
17430
17635
|
},
|
|
17431
17636
|
sessionGrant(prefix) {
|
|
17432
|
-
const session =
|
|
17637
|
+
const session = activeSession();
|
|
17433
17638
|
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
17639
|
+
if (verifiedAnchor === null && session.siwe !== void 0) return { refused: "NOT_COVERED" };
|
|
17434
17640
|
const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
|
|
17435
17641
|
if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
|
|
17436
17642
|
return { refused: "SESSION_EXPIRING" };
|
|
@@ -17458,8 +17664,9 @@ function createReplicationAuthority(host) {
|
|
|
17458
17664
|
};
|
|
17459
17665
|
},
|
|
17460
17666
|
plan(prefix) {
|
|
17461
|
-
const session =
|
|
17667
|
+
const session = activeSession();
|
|
17462
17668
|
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
17669
|
+
if (verifiedAnchor === null) return { refused: "NOT_COVERED" };
|
|
17463
17670
|
const sessionGrant = this.sessionGrant(prefix);
|
|
17464
17671
|
if (!("refused" in sessionGrant)) {
|
|
17465
17672
|
const signed2 = signedSessionGrant(session);
|
|
@@ -17473,26 +17680,30 @@ function createReplicationAuthority(host) {
|
|
|
17473
17680
|
if (authority.sessionOnly) return sessionGrant;
|
|
17474
17681
|
return planOrRefusal(replicationEntries(session.spaceId, prefix));
|
|
17475
17682
|
},
|
|
17476
|
-
async mint(deviceDid, prefix, signal) {
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
if (session === void 0) throw new Error("
|
|
17683
|
+
async mint(deviceDid, prefix, signal, options) {
|
|
17684
|
+
const session = activeSession();
|
|
17685
|
+
if (session === void 0) throw new Error("replication mint without the bound session");
|
|
17686
|
+
if (session.siwe === void 0) throw new Error("compact delegate sessions cannot mint device grants");
|
|
17687
|
+
if (verifiedAnchor?.source !== "siwe") throw new Error("replication mint requires verified owner consent");
|
|
17480
17688
|
const entries = replicationEntries(session.spaceId, prefix);
|
|
17481
|
-
const plan = planOrRefusal(entries);
|
|
17689
|
+
const plan = planOrRefusal(entries, options?.expiryMs);
|
|
17482
17690
|
if ("refused" in plan) {
|
|
17483
17691
|
throw Object.assign(
|
|
17484
17692
|
new Error(`replication authority refused the mint: ${plan.refused}`),
|
|
17485
17693
|
{ refused: plan.refused }
|
|
17486
17694
|
);
|
|
17487
17695
|
}
|
|
17696
|
+
if (options?.renewalOnly && plan.path !== "session") {
|
|
17697
|
+
throw Object.assign(new Error("replication renewal requires a session parent"), { refused: "NOT_COVERED" });
|
|
17698
|
+
}
|
|
17488
17699
|
signal.throwIfAborted();
|
|
17489
|
-
const minted = await new Promise((
|
|
17700
|
+
const minted = await new Promise((resolve2, reject2) => {
|
|
17490
17701
|
const onAbort = () => reject2(signal.reason ?? new Error("replication mint aborted"));
|
|
17491
17702
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
17492
|
-
host.mintDelegation(deviceDid, entries).then(
|
|
17703
|
+
host.mintDelegation(deviceDid, entries, options).then(
|
|
17493
17704
|
(result) => {
|
|
17494
17705
|
signal.removeEventListener("abort", onAbort);
|
|
17495
|
-
|
|
17706
|
+
resolve2(result);
|
|
17496
17707
|
},
|
|
17497
17708
|
(error) => {
|
|
17498
17709
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -17519,6 +17730,77 @@ var init_authority = __esm({
|
|
|
17519
17730
|
}
|
|
17520
17731
|
});
|
|
17521
17732
|
|
|
17733
|
+
// src/replication/gated-network.ts
|
|
17734
|
+
var gated_network_exports = {};
|
|
17735
|
+
__export(gated_network_exports, {
|
|
17736
|
+
createGatedNetworkReadThrough: () => createGatedNetworkReadThrough
|
|
17737
|
+
});
|
|
17738
|
+
import { kvPrefixCovers as kvPrefixCovers2 } from "@tinycloud/sdk-services/kv/replication/prefix";
|
|
17739
|
+
function createGatedNetworkReadThrough(input) {
|
|
17740
|
+
const report = (op, space, path, replica, reason, outcome, latencyMs) => input.emit({
|
|
17741
|
+
type: "replication.read",
|
|
17742
|
+
at: new Date(input.now()).toISOString(),
|
|
17743
|
+
op,
|
|
17744
|
+
space,
|
|
17745
|
+
key: path,
|
|
17746
|
+
replica,
|
|
17747
|
+
source: reason === "cursor_restart" ? "none" : "network",
|
|
17748
|
+
reason,
|
|
17749
|
+
outcome,
|
|
17750
|
+
latencyMs,
|
|
17751
|
+
stalenessMs: null,
|
|
17752
|
+
coverage: null,
|
|
17753
|
+
authority: null,
|
|
17754
|
+
...reason === "replica_gated" ? { code: "REPLICA_GATED" } : {}
|
|
17755
|
+
});
|
|
17756
|
+
const readNetwork = async (r2, op, path, prefix, reason) => {
|
|
17757
|
+
const started = input.now();
|
|
17758
|
+
let outcome = "error";
|
|
17759
|
+
try {
|
|
17760
|
+
const result = await r2.network();
|
|
17761
|
+
outcome = result.ok ? "found" : op === "get" && reason === "not_covered" && "error" in result && result.error.code === "KV_NOT_FOUND" ? "not_found" : "error";
|
|
17762
|
+
return result;
|
|
17763
|
+
} finally {
|
|
17764
|
+
report(op, r2.space, path, prefix, reason, outcome, input.now() - started);
|
|
17765
|
+
}
|
|
17766
|
+
};
|
|
17767
|
+
const covered = (path) => input.prefixes.find((prefix) => kvPrefixCovers2(prefix, path));
|
|
17768
|
+
return {
|
|
17769
|
+
get: (r2) => {
|
|
17770
|
+
if (r2.space !== input.space) return r2.network();
|
|
17771
|
+
if (r2.options?.source === "network") return readNetwork(r2, "get", r2.path, null, "NETWORK_REQUESTED");
|
|
17772
|
+
const prefix = covered(r2.path);
|
|
17773
|
+
return readNetwork(r2, "get", r2.path, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
|
|
17774
|
+
},
|
|
17775
|
+
list: (r2) => {
|
|
17776
|
+
if (r2.space !== input.space) return r2.network();
|
|
17777
|
+
if (r2.options?.source === "network") return readNetwork(r2, "list", r2.listPath, null, "NETWORK_REQUESTED");
|
|
17778
|
+
if (r2.options?.cursor?.startsWith("tcr1.")) {
|
|
17779
|
+
report("list", r2.space, r2.listPath, null, "cursor_restart", "error", 0);
|
|
17780
|
+
return Promise.resolve({ ok: false, error: {
|
|
17781
|
+
code: "INVALID_INPUT",
|
|
17782
|
+
message: "Local list cursor cannot be served; restart without the cursor",
|
|
17783
|
+
service: "kv",
|
|
17784
|
+
meta: { replication: "cursor_restart" }
|
|
17785
|
+
} });
|
|
17786
|
+
}
|
|
17787
|
+
if (r2.options?.cursor) return readNetwork(r2, "list", r2.listPath, null, "network_cursor");
|
|
17788
|
+
if (r2.options?.raw) return readNetwork(r2, "list", r2.listPath, null, "unsupported_option");
|
|
17789
|
+
const prefix = covered(r2.listPath);
|
|
17790
|
+
return readNetwork(r2, "list", r2.listPath, prefix ?? null, prefix === void 0 ? "not_covered" : "replica_gated");
|
|
17791
|
+
},
|
|
17792
|
+
write: (r2) => r2.network(),
|
|
17793
|
+
observeNetworkRequested: (r2) => {
|
|
17794
|
+
if (r2.space === input.space) report(r2.op, r2.space, r2.path, null, r2.reason, r2.outcome, r2.latencyMs);
|
|
17795
|
+
}
|
|
17796
|
+
};
|
|
17797
|
+
}
|
|
17798
|
+
var init_gated_network = __esm({
|
|
17799
|
+
"src/replication/gated-network.ts"() {
|
|
17800
|
+
"use strict";
|
|
17801
|
+
}
|
|
17802
|
+
});
|
|
17803
|
+
|
|
17522
17804
|
// src/NodeWasmBindings.ts
|
|
17523
17805
|
import {
|
|
17524
17806
|
invoke,
|
|
@@ -18205,7 +18487,6 @@ function deserializeDelegation(data) {
|
|
|
18205
18487
|
}
|
|
18206
18488
|
|
|
18207
18489
|
// src/TinyCloudNode.ts
|
|
18208
|
-
init_config();
|
|
18209
18490
|
import { generateNonce, SiweMessage as SiweMessage3 } from "siwe";
|
|
18210
18491
|
|
|
18211
18492
|
// src/replication/module-registry.ts
|
|
@@ -18224,7 +18505,7 @@ function getNodeReplicationLoaders() {
|
|
|
18224
18505
|
import {
|
|
18225
18506
|
TinyCloud,
|
|
18226
18507
|
checkNodeInfo as checkNodeInfo2,
|
|
18227
|
-
|
|
18508
|
+
ProtocolMismatchError,
|
|
18228
18509
|
activateSessionWithHost as activateSessionWithHost2,
|
|
18229
18510
|
authorizationVerdictOf,
|
|
18230
18511
|
httpResponseError,
|
|
@@ -18275,7 +18556,6 @@ import {
|
|
|
18275
18556
|
verifyDidKeyEd25519Signature,
|
|
18276
18557
|
canonicalizeAddress as canonicalizeAddress2,
|
|
18277
18558
|
pkhDid as pkhDid2,
|
|
18278
|
-
principalDid,
|
|
18279
18559
|
resolveTinyCloudHosts as resolveTinyCloudHosts2,
|
|
18280
18560
|
publishLocationRecord,
|
|
18281
18561
|
principalDidEquals as principalDidEquals3,
|
|
@@ -18541,6 +18821,7 @@ var NodeUserAuthorization = class {
|
|
|
18541
18821
|
this.siweConfig = config.siweConfig;
|
|
18542
18822
|
this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
|
|
18543
18823
|
this.replication = config.replication?.enabled === true ? config.replication : void 0;
|
|
18824
|
+
this.establishReplicaStamp = config.establishReplicaStamp;
|
|
18544
18825
|
this._manifest = config.manifest;
|
|
18545
18826
|
this._capabilityRequest = config.capabilityRequest;
|
|
18546
18827
|
this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
|
|
@@ -19171,7 +19452,7 @@ var NodeUserAuthorization = class {
|
|
|
19171
19452
|
);
|
|
19172
19453
|
throw error;
|
|
19173
19454
|
}
|
|
19174
|
-
await new Promise((
|
|
19455
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
19175
19456
|
const retryResult = await activateSessionWithHost(
|
|
19176
19457
|
host,
|
|
19177
19458
|
this._tinyCloudSession.delegationHeader
|
|
@@ -19211,7 +19492,7 @@ var NodeUserAuthorization = class {
|
|
|
19211
19492
|
);
|
|
19212
19493
|
throw error;
|
|
19213
19494
|
}
|
|
19214
|
-
await new Promise((
|
|
19495
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
19215
19496
|
const retryResult = await activateSessionWithHost(
|
|
19216
19497
|
host,
|
|
19217
19498
|
this._tinyCloudSession.delegationHeader
|
|
@@ -19310,6 +19591,27 @@ var NodeUserAuthorization = class {
|
|
|
19310
19591
|
signature: signature2
|
|
19311
19592
|
};
|
|
19312
19593
|
}
|
|
19594
|
+
persistedData(session, expiresAt, createdAt, replicaStamp) {
|
|
19595
|
+
return {
|
|
19596
|
+
address: session.address,
|
|
19597
|
+
chainId: session.chainId,
|
|
19598
|
+
sessionKey: JSON.stringify(session.jwk),
|
|
19599
|
+
siwe: session.siwe,
|
|
19600
|
+
signature: session.signature,
|
|
19601
|
+
tinycloudSession: {
|
|
19602
|
+
delegationHeader: session.delegationHeader,
|
|
19603
|
+
delegationCid: session.delegationCid,
|
|
19604
|
+
spaceId: session.spaceId,
|
|
19605
|
+
spaces: session.spaces,
|
|
19606
|
+
verificationMethod: session.verificationMethod
|
|
19607
|
+
},
|
|
19608
|
+
expiresAt,
|
|
19609
|
+
createdAt,
|
|
19610
|
+
version: "1.0",
|
|
19611
|
+
tinycloudHosts: this.tinycloudHosts,
|
|
19612
|
+
...replicaStamp ? { replicaStamp } : {}
|
|
19613
|
+
};
|
|
19614
|
+
}
|
|
19313
19615
|
/**
|
|
19314
19616
|
* Sign in and create a new session.
|
|
19315
19617
|
*
|
|
@@ -19389,24 +19691,13 @@ var NodeUserAuthorization = class {
|
|
|
19389
19691
|
siwe: prepared.siwe,
|
|
19390
19692
|
signature: signature2
|
|
19391
19693
|
};
|
|
19392
|
-
const
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
|
|
19397
|
-
|
|
19398
|
-
|
|
19399
|
-
delegationHeader: session.delegationHeader,
|
|
19400
|
-
delegationCid: session.delegationCid,
|
|
19401
|
-
spaceId,
|
|
19402
|
-
spaces: spacesMetadata,
|
|
19403
|
-
verificationMethod: this.sessionManager.getDID(keyId)
|
|
19404
|
-
},
|
|
19405
|
-
expiresAt: expirationTime.toISOString(),
|
|
19406
|
-
createdAt: now.toISOString(),
|
|
19407
|
-
version: "1.0",
|
|
19408
|
-
tinycloudHosts: this.tinycloudHosts
|
|
19409
|
-
};
|
|
19694
|
+
const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
|
|
19695
|
+
const persistedData = this.persistedData(
|
|
19696
|
+
tinyCloudSession,
|
|
19697
|
+
expirationTime.toISOString(),
|
|
19698
|
+
now.toISOString(),
|
|
19699
|
+
replicaStamp
|
|
19700
|
+
);
|
|
19410
19701
|
await this.enqueuePersistence(async () => {
|
|
19411
19702
|
await this.sessionStorage.save(address, persistedData);
|
|
19412
19703
|
if (generation !== this.sessionGeneration) {
|
|
@@ -19560,28 +19851,12 @@ var NodeUserAuthorization = class {
|
|
|
19560
19851
|
siwe: prepared.siwe,
|
|
19561
19852
|
signature: signature2
|
|
19562
19853
|
};
|
|
19563
|
-
const expirationMatch = prepared.siwe
|
|
19564
|
-
const issuedAtMatch = prepared.siwe
|
|
19854
|
+
const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
|
|
19855
|
+
const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
|
|
19565
19856
|
const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
|
|
19566
19857
|
const createdAt = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
19567
|
-
const
|
|
19568
|
-
|
|
19569
|
-
chainId,
|
|
19570
|
-
sessionKey: JSON.stringify(jwk),
|
|
19571
|
-
siwe: prepared.siwe,
|
|
19572
|
-
signature: signature2,
|
|
19573
|
-
tinycloudSession: {
|
|
19574
|
-
delegationHeader: session.delegationHeader,
|
|
19575
|
-
delegationCid: session.delegationCid,
|
|
19576
|
-
spaceId: prepared.spaceId,
|
|
19577
|
-
spaces: spacesMetadata,
|
|
19578
|
-
verificationMethod: this.sessionManager.getDID(keyId)
|
|
19579
|
-
},
|
|
19580
|
-
expiresAt,
|
|
19581
|
-
createdAt,
|
|
19582
|
-
version: "1.0",
|
|
19583
|
-
tinycloudHosts: this.tinycloudHosts
|
|
19584
|
-
};
|
|
19858
|
+
const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
|
|
19859
|
+
const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
|
|
19585
19860
|
await this.sessionStorage.save(address, persistedData);
|
|
19586
19861
|
this._session = clientSession;
|
|
19587
19862
|
this._tinyCloudSession = tinyCloudSession;
|
|
@@ -19644,15 +19919,36 @@ var NodeUserAuthorization = class {
|
|
|
19644
19919
|
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
19645
19920
|
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
19646
19921
|
const hostHint = this.primaryTinyCloudHost;
|
|
19647
|
-
const
|
|
19922
|
+
const originalRequest = {
|
|
19648
19923
|
protocolVersion: 1,
|
|
19649
19924
|
siwe: prepared.siwe,
|
|
19650
19925
|
jwk,
|
|
19926
|
+
verificationMethod: prepared.verificationMethod,
|
|
19651
19927
|
host: hostHint,
|
|
19652
19928
|
keyId: options?.openkeyKeyId,
|
|
19653
19929
|
presentation
|
|
19654
|
-
}
|
|
19655
|
-
|
|
19930
|
+
};
|
|
19931
|
+
let result;
|
|
19932
|
+
let signed2;
|
|
19933
|
+
let signature2;
|
|
19934
|
+
for (let attempt = 0; ; ) {
|
|
19935
|
+
try {
|
|
19936
|
+
result ?? (result = await authorizeFn(originalRequest));
|
|
19937
|
+
signed2 = await this.verifyOpenKeyResult(result, prepared);
|
|
19938
|
+
signature2 = result.signature;
|
|
19939
|
+
break;
|
|
19940
|
+
} catch (error) {
|
|
19941
|
+
if (!options?.retryResult) throw error;
|
|
19942
|
+
const next = await options.retryResult(
|
|
19943
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
19944
|
+
originalRequest,
|
|
19945
|
+
++attempt
|
|
19946
|
+
);
|
|
19947
|
+
if (next === null) throw error;
|
|
19948
|
+
result = next;
|
|
19949
|
+
}
|
|
19950
|
+
}
|
|
19951
|
+
return this.signInWithPreparedSession(signed2, signature2, keyId, jwk);
|
|
19656
19952
|
}
|
|
19657
19953
|
/**
|
|
19658
19954
|
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
@@ -19732,6 +20028,11 @@ var NodeUserAuthorization = class {
|
|
|
19732
20028
|
* (resource, action) pair in the returned SIWE.
|
|
19733
20029
|
*/
|
|
19734
20030
|
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
20031
|
+
const signed2 = await this.verifyOpenKeyResult(result, prepared);
|
|
20032
|
+
return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
|
|
20033
|
+
}
|
|
20034
|
+
/** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
|
|
20035
|
+
async verifyOpenKeyResult(result, prepared) {
|
|
19735
20036
|
if (result.protocolVersion !== 1) {
|
|
19736
20037
|
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
19737
20038
|
}
|
|
@@ -19976,17 +20277,12 @@ var NodeUserAuthorization = class {
|
|
|
19976
20277
|
}
|
|
19977
20278
|
}
|
|
19978
20279
|
void nonRequiredGrantedPairs;
|
|
19979
|
-
return
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
},
|
|
19986
|
-
result.signature,
|
|
19987
|
-
keyId,
|
|
19988
|
-
jwk
|
|
19989
|
-
);
|
|
20280
|
+
return {
|
|
20281
|
+
siwe: result.signedMessage,
|
|
20282
|
+
jwk: prepared.jwk,
|
|
20283
|
+
spaceId: prepared.spaceId,
|
|
20284
|
+
verificationMethod: prepared.verificationMethod
|
|
20285
|
+
};
|
|
19990
20286
|
}
|
|
19991
20287
|
/**
|
|
19992
20288
|
* Clear persisted session data.
|
|
@@ -20038,7 +20334,7 @@ var NodeUserAuthorization = class {
|
|
|
20038
20334
|
* Request signature via event emitter with timeout.
|
|
20039
20335
|
*/
|
|
20040
20336
|
requestSignatureViaEmitter(request, emitter, timeout) {
|
|
20041
|
-
return new Promise((
|
|
20337
|
+
return new Promise((resolve2, reject2) => {
|
|
20042
20338
|
const timeoutId = setTimeout(() => {
|
|
20043
20339
|
reject2(new Error("Sign request timed out"));
|
|
20044
20340
|
}, timeout);
|
|
@@ -20050,7 +20346,7 @@ var NodeUserAuthorization = class {
|
|
|
20050
20346
|
);
|
|
20051
20347
|
} else {
|
|
20052
20348
|
const signature2 = response.signature ?? await this.signer.signMessage(request.message);
|
|
20053
|
-
|
|
20349
|
+
resolve2(signature2);
|
|
20054
20350
|
}
|
|
20055
20351
|
};
|
|
20056
20352
|
emitter.emit("sign-request", request, respond);
|
|
@@ -20982,6 +21278,20 @@ function accountIndexWarningDiagnostic(error) {
|
|
|
20982
21278
|
}
|
|
20983
21279
|
return diagnostic;
|
|
20984
21280
|
}
|
|
21281
|
+
function replicaRestoreRefusal(error) {
|
|
21282
|
+
if (error !== null && typeof error === "object" && "code" in error && error.code === "REPLICA_GATED" && "gate" in error) {
|
|
21283
|
+
switch (error.gate) {
|
|
21284
|
+
case "not_established":
|
|
21285
|
+
case "stamp_stale":
|
|
21286
|
+
case "principal_mismatch":
|
|
21287
|
+
case "purge_pending":
|
|
21288
|
+
case "logout_pending":
|
|
21289
|
+
case "gate_unavailable":
|
|
21290
|
+
return error.gate;
|
|
21291
|
+
}
|
|
21292
|
+
}
|
|
21293
|
+
return "gate_unavailable";
|
|
21294
|
+
}
|
|
20985
21295
|
var _TinyCloudNode = class _TinyCloudNode {
|
|
20986
21296
|
/**
|
|
20987
21297
|
* Create a new TinyCloudNode instance.
|
|
@@ -21011,6 +21321,37 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21011
21321
|
this.auth = null;
|
|
21012
21322
|
this.tc = null;
|
|
21013
21323
|
this._chainId = 1;
|
|
21324
|
+
/** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
|
|
21325
|
+
this.establishReplicaStamp = async (session) => {
|
|
21326
|
+
const replication = this.config.replication;
|
|
21327
|
+
if (!replication?.enabled) return void 0;
|
|
21328
|
+
const epoch = this.sessionEpoch;
|
|
21329
|
+
const attempt = this.latestSessionAttempt;
|
|
21330
|
+
try {
|
|
21331
|
+
const { verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2, establishReplicaSessionStamp: establishReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
|
|
21332
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
|
|
21333
|
+
const anchor = verifiedOwnerConsentAnchor2(session, this.wasmBindings, exactVerifiedRecap);
|
|
21334
|
+
if (anchor === null) {
|
|
21335
|
+
throw Object.assign(new Error("Replica sign-in requires a verified finite owner SIWE and exact-space KV get+sync."), {
|
|
21336
|
+
code: "REPLICA_GATED",
|
|
21337
|
+
gate: "not_established"
|
|
21338
|
+
});
|
|
21339
|
+
}
|
|
21340
|
+
const stamp = await establishReplicaSessionStamp2(
|
|
21341
|
+
replication.storage,
|
|
21342
|
+
pkhDid2(session.address, session.chainId),
|
|
21343
|
+
anchor
|
|
21344
|
+
);
|
|
21345
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
|
|
21346
|
+
this.replicaSessionStamp = stamp;
|
|
21347
|
+
return stamp;
|
|
21348
|
+
} catch (error) {
|
|
21349
|
+
if (epoch === this.sessionEpoch && attempt === this.latestSessionAttempt) {
|
|
21350
|
+
this.replicaRestoreGate = replicaRestoreRefusal(error);
|
|
21351
|
+
}
|
|
21352
|
+
return void 0;
|
|
21353
|
+
}
|
|
21354
|
+
};
|
|
21014
21355
|
this._baseVaults = /* @__PURE__ */ new Map();
|
|
21015
21356
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
21016
21357
|
this._secrets = /* @__PURE__ */ new Map();
|
|
@@ -21135,7 +21476,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21135
21476
|
});
|
|
21136
21477
|
return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
|
|
21137
21478
|
};
|
|
21138
|
-
|
|
21479
|
+
if (config.replication?.enabled) getNodeReplicationLoaders().validateConfig(config.replication);
|
|
21139
21480
|
this.explicitHost = config.host;
|
|
21140
21481
|
this.config = {
|
|
21141
21482
|
...config,
|
|
@@ -21150,6 +21491,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21150
21491
|
this.replicationRuntime = runtime;
|
|
21151
21492
|
this.replicationControl = {
|
|
21152
21493
|
status: async () => {
|
|
21494
|
+
const gate = this.replicaRestoreGate;
|
|
21495
|
+
if (gate) return this.config.replication.prefixes.map((prefix) => ({
|
|
21496
|
+
prefix,
|
|
21497
|
+
state: "gated",
|
|
21498
|
+
gate,
|
|
21499
|
+
errorCode: "REPLICA_GATED",
|
|
21500
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
21501
|
+
pinned: [],
|
|
21502
|
+
lagMs: null
|
|
21503
|
+
}));
|
|
21153
21504
|
const status = await runtime.then((ready) => ready.control.status());
|
|
21154
21505
|
const scopeError = this.replicationScopeError;
|
|
21155
21506
|
if (!scopeError) return status;
|
|
@@ -21166,7 +21517,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21166
21517
|
lagMs: null
|
|
21167
21518
|
}));
|
|
21168
21519
|
},
|
|
21169
|
-
sync: async (input) =>
|
|
21520
|
+
sync: async (input) => {
|
|
21521
|
+
if (this.replicaRestoreGate) {
|
|
21522
|
+
throw Object.assign(new Error("Replica restore is gated."), {
|
|
21523
|
+
code: "REPLICA_GATED",
|
|
21524
|
+
gate: this.replicaRestoreGate
|
|
21525
|
+
});
|
|
21526
|
+
}
|
|
21527
|
+
return (await runtime).control.sync(input);
|
|
21528
|
+
},
|
|
21170
21529
|
purge: (input) => {
|
|
21171
21530
|
const instance = this.replicationRuntimeInstance;
|
|
21172
21531
|
return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
|
|
@@ -21382,6 +21741,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21382
21741
|
signer: this.signer,
|
|
21383
21742
|
signStrategy: config.signStrategy ?? { type: "auto-sign" },
|
|
21384
21743
|
wasmBindings: this.wasmBindings,
|
|
21744
|
+
...config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
21385
21745
|
sessionManager: this.sessionManager,
|
|
21386
21746
|
sessionStorage: config.sessionStorage ?? new MemorySessionStorage(),
|
|
21387
21747
|
domain: this.siweDomain,
|
|
@@ -21579,11 +21939,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21579
21939
|
return this.signer === null;
|
|
21580
21940
|
}
|
|
21581
21941
|
/**
|
|
21582
|
-
*
|
|
21583
|
-
* Available after signIn().
|
|
21942
|
+
* Primary space of the signed-in or restored network session, including compact sessions.
|
|
21584
21943
|
*/
|
|
21585
21944
|
get spaceId() {
|
|
21586
|
-
return this.auth?.tinyCloudSession?.spaceId;
|
|
21945
|
+
return this.auth?.tinyCloudSession?.spaceId ?? this._activeServiceSession?.spaceId;
|
|
21587
21946
|
}
|
|
21588
21947
|
/**
|
|
21589
21948
|
* Get the account space ID for this wallet identity.
|
|
@@ -21631,6 +21990,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21631
21990
|
get restorableSession() {
|
|
21632
21991
|
return this.currentTinyCloudSession();
|
|
21633
21992
|
}
|
|
21993
|
+
/** The active, verified replica stamp. Undefined when the replica is refused or the session has ended. */
|
|
21994
|
+
get replicaStamp() {
|
|
21995
|
+
const stamp = this.replicaSessionStamp;
|
|
21996
|
+
if (!stamp || this.replicaRestoreGate !== void 0 || !this.currentTinyCloudSession()) return void 0;
|
|
21997
|
+
return { incarnation: stamp.incarnation, lifecycleEpoch: stamp.lifecycleEpoch };
|
|
21998
|
+
}
|
|
21634
21999
|
/**
|
|
21635
22000
|
* Sign out and end the session on this instance.
|
|
21636
22001
|
*
|
|
@@ -21651,18 +22016,60 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21651
22016
|
}
|
|
21652
22017
|
async endSession() {
|
|
21653
22018
|
const leavingAddress = this.address ?? this._restoredTcSession?.address;
|
|
22019
|
+
const leavingChainId = this._chainId;
|
|
22020
|
+
const leavingPrincipal = this.replicaSessionPrincipal ?? (leavingAddress ? pkhDid2(leavingAddress, leavingChainId) : void 0);
|
|
21654
22021
|
const leavingAuth = this.auth;
|
|
22022
|
+
const leavingContext = this._serviceContext;
|
|
22023
|
+
let denied;
|
|
21655
22024
|
try {
|
|
21656
22025
|
this.teardownSession();
|
|
21657
22026
|
} finally {
|
|
22027
|
+
const denial = leavingPrincipal ? this.denyReplicaGate({ reason: "sign_out", principal: leavingPrincipal }, leavingContext) : Promise.resolve(void 0);
|
|
21658
22028
|
try {
|
|
21659
|
-
await
|
|
21660
|
-
if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
|
|
22029
|
+
denied = await denial;
|
|
21661
22030
|
} finally {
|
|
21662
|
-
|
|
21663
|
-
await
|
|
22031
|
+
try {
|
|
22032
|
+
await this.accountRegistryTail;
|
|
22033
|
+
await this.quiesceReplicaRuntime();
|
|
22034
|
+
} finally {
|
|
22035
|
+
if (leavingAddress) {
|
|
22036
|
+
await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
|
|
22037
|
+
}
|
|
22038
|
+
}
|
|
22039
|
+
}
|
|
22040
|
+
}
|
|
22041
|
+
if (denied?.status === "unguarded") {
|
|
22042
|
+
throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
|
|
22043
|
+
}
|
|
22044
|
+
}
|
|
22045
|
+
/**
|
|
22046
|
+
* Step 1 of replica sign-out. Never rejects: failures come back as `unguarded` so the session
|
|
22047
|
+
* is still cleared. Returns `undefined` when replication is off.
|
|
22048
|
+
*/
|
|
22049
|
+
async denyReplicaGate(input, context = this._serviceContext) {
|
|
22050
|
+
const replication = this.config.replication;
|
|
22051
|
+
if (!replication?.enabled) return void 0;
|
|
22052
|
+
try {
|
|
22053
|
+
const lifecycle = replication.storage.lifecycle;
|
|
22054
|
+
if (lifecycle?.kind !== (replication.storage.kind === "indexeddb" ? "indexeddb" : "file")) {
|
|
22055
|
+
return { status: "unguarded", code: "gate_unavailable" };
|
|
22056
|
+
}
|
|
22057
|
+
const result = await lifecycle.deny(input);
|
|
22058
|
+
if (result.status === "denied" || result.status === "deleted" || result.status === "marker") {
|
|
22059
|
+
try {
|
|
22060
|
+
(await getNodeReplicationLoaders().authority()).reportReplicaDenial(
|
|
22061
|
+
result,
|
|
22062
|
+
input.reason,
|
|
22063
|
+
replication.onEvent,
|
|
22064
|
+
context
|
|
22065
|
+
);
|
|
22066
|
+
} catch {
|
|
21664
22067
|
}
|
|
21665
22068
|
}
|
|
22069
|
+
return result;
|
|
22070
|
+
} catch (error) {
|
|
22071
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable";
|
|
22072
|
+
return { status: "unguarded", code };
|
|
21666
22073
|
}
|
|
21667
22074
|
}
|
|
21668
22075
|
/**
|
|
@@ -21682,15 +22089,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21682
22089
|
this._activeServiceSession = void 0;
|
|
21683
22090
|
oldGraph.retire();
|
|
21684
22091
|
oldCore?.retireServices();
|
|
21685
|
-
oldAuth?.resetSession(
|
|
21686
|
-
this.sessionManager,
|
|
21687
|
-
this.sessionKeyId,
|
|
21688
|
-
this.explicitHost ? [this.explicitHost] : void 0
|
|
21689
|
-
);
|
|
21690
22092
|
this.config.host = this.explicitHost ?? DEFAULT_HOST;
|
|
21691
22093
|
this._chainId = 1;
|
|
21692
22094
|
this._restoredNodeFeatures = void 0;
|
|
21693
|
-
this._activeServiceSession = void 0;
|
|
21694
22095
|
this._serviceContext = void 0;
|
|
21695
22096
|
this._kv = void 0;
|
|
21696
22097
|
this._sql = void 0;
|
|
@@ -21715,6 +22116,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21715
22116
|
this._bootstrapStatus = { skipped: false };
|
|
21716
22117
|
this.replicationDelegateSession = void 0;
|
|
21717
22118
|
this.replicationScopeError = void 0;
|
|
22119
|
+
this.replicaSessionPrincipal = void 0;
|
|
22120
|
+
this.replicaSessionStamp = void 0;
|
|
22121
|
+
this.replicaRestoreGate = void 0;
|
|
21718
22122
|
const sessionKey = createDefaultSessionKey(this.wasmBindings);
|
|
21719
22123
|
this.sessionManager = sessionKey.manager;
|
|
21720
22124
|
this.sessionKeyId = sessionKey.keyId;
|
|
@@ -21724,6 +22128,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21724
22128
|
sessionKey.keyId,
|
|
21725
22129
|
this.explicitHost ? [this.explicitHost] : void 0
|
|
21726
22130
|
);
|
|
22131
|
+
this.replicaSessionPrincipal = void 0;
|
|
22132
|
+
this.replicaSessionStamp = void 0;
|
|
22133
|
+
this.replicaRestoreGate = void 0;
|
|
21727
22134
|
this._serviceGraph = this.createServiceGraphLifetime();
|
|
21728
22135
|
this.auth = oldAuth;
|
|
21729
22136
|
this.tc = this.auth ? new TinyCloud(this.auth, {
|
|
@@ -21753,11 +22160,25 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21753
22160
|
"Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
|
|
21754
22161
|
);
|
|
21755
22162
|
}
|
|
22163
|
+
await this.signInWith(() => this.tc.signIn(options));
|
|
22164
|
+
}
|
|
22165
|
+
/**
|
|
22166
|
+
* Fresh OpenKey sign-in. The callback receives the SDK's original SIWE and
|
|
22167
|
+
* session JWK before approval; the signed result is verified against those
|
|
22168
|
+
* bytes before the network session or replica stamp is committed.
|
|
22169
|
+
*/
|
|
22170
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
22171
|
+
if (!this.signer || !this.tc || !this.auth) {
|
|
22172
|
+
throw new Error("Cannot signInWithOpenKey() in session-only mode. Provide a signer in config.");
|
|
22173
|
+
}
|
|
22174
|
+
await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
|
|
22175
|
+
}
|
|
22176
|
+
async signInWith(performSignIn) {
|
|
21756
22177
|
const epoch = this.sessionEpoch;
|
|
21757
22178
|
const attempt = ++this.latestSessionAttempt;
|
|
21758
22179
|
await this.signOutInFlight;
|
|
21759
22180
|
try {
|
|
21760
|
-
await this.establishSignIn(epoch,
|
|
22181
|
+
await this.establishSignIn(epoch, performSignIn);
|
|
21761
22182
|
} catch (error) {
|
|
21762
22183
|
if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
|
|
21763
22184
|
await this.endSession();
|
|
@@ -21765,7 +22186,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21765
22186
|
throw error;
|
|
21766
22187
|
}
|
|
21767
22188
|
}
|
|
21768
|
-
async establishSignIn(epoch,
|
|
22189
|
+
async establishSignIn(epoch, performSignIn) {
|
|
21769
22190
|
if (!this.signer || !this.tc) {
|
|
21770
22191
|
throw new Error("Not signed in. Call signIn() first.");
|
|
21771
22192
|
}
|
|
@@ -21775,6 +22196,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21775
22196
|
const address = canonicalizeAddress2(await this.signer.getAddress());
|
|
21776
22197
|
const chainId = await this.signer.getChainId();
|
|
21777
22198
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
22199
|
+
const nextPrincipal = pkhDid2(address, chainId);
|
|
22200
|
+
const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? pkhDid2(this.address, this._chainId) : void 0);
|
|
22201
|
+
if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
|
|
22202
|
+
const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
|
|
22203
|
+
this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
|
|
22204
|
+
}
|
|
21778
22205
|
if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
|
|
21779
22206
|
this._address = address;
|
|
21780
22207
|
this._chainId = chainId;
|
|
@@ -21792,7 +22219,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21792
22219
|
this.runtimePermissionGrants = [];
|
|
21793
22220
|
this.confirmedHostedSpaceIds.clear();
|
|
21794
22221
|
this.hostedOwnedSpaceIds.clear();
|
|
21795
|
-
|
|
22222
|
+
const signInAttempt = this.latestSessionAttempt;
|
|
22223
|
+
const previousReplicaGate = this.replicaRestoreGate;
|
|
22224
|
+
const previousReplicaStamp = this.replicaSessionStamp;
|
|
22225
|
+
this.replicaRestoreGate = void 0;
|
|
22226
|
+
this.replicaSessionStamp = void 0;
|
|
22227
|
+
try {
|
|
22228
|
+
await performSignIn();
|
|
22229
|
+
} catch (error) {
|
|
22230
|
+
if (epoch === this.sessionEpoch && signInAttempt === this.latestSessionAttempt) {
|
|
22231
|
+
this.replicaRestoreGate = previousReplicaGate;
|
|
22232
|
+
this.replicaSessionStamp = previousReplicaStamp;
|
|
22233
|
+
}
|
|
22234
|
+
throw error;
|
|
22235
|
+
}
|
|
21796
22236
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
21797
22237
|
this.cancelOwnedSpaceHosts();
|
|
21798
22238
|
this.syncResolvedHostFromAuth();
|
|
@@ -21800,8 +22240,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21800
22240
|
this.hostedOwnedSpaceIds.add(spaceId);
|
|
21801
22241
|
}
|
|
21802
22242
|
this.replicationDelegateSession = void 0;
|
|
22243
|
+
this.replicaSessionPrincipal = pkhDid2(address, chainId);
|
|
21803
22244
|
this.replicationScopeError = void 0;
|
|
21804
|
-
|
|
22245
|
+
await this.quiesceReplicaRuntime();
|
|
21805
22246
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
21806
22247
|
const oldGraph = this._serviceGraph;
|
|
21807
22248
|
this._serviceGraph = this.createServiceGraphLifetime();
|
|
@@ -22053,7 +22494,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22053
22494
|
);
|
|
22054
22495
|
}
|
|
22055
22496
|
}
|
|
22056
|
-
await new Promise((
|
|
22497
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
22057
22498
|
for (const step of steps) {
|
|
22058
22499
|
if (step.kind !== "activate") continue;
|
|
22059
22500
|
graph.assertActive();
|
|
@@ -22388,6 +22829,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22388
22829
|
if (this.ownStorageFull) {
|
|
22389
22830
|
return;
|
|
22390
22831
|
}
|
|
22832
|
+
if (!this.currentSessionCoversAccountRegistry()) {
|
|
22833
|
+
return;
|
|
22834
|
+
}
|
|
22391
22835
|
const controller = new AbortController();
|
|
22392
22836
|
const deadline = setTimeout(
|
|
22393
22837
|
() => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
|
|
@@ -22502,6 +22946,27 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22502
22946
|
(operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
|
|
22503
22947
|
);
|
|
22504
22948
|
}
|
|
22949
|
+
/**
|
|
22950
|
+
* Whether the current primary session's recap covers the account index read
|
|
22951
|
+
* (`tinycloud.sql/read` on the owned account space's `account` database),
|
|
22952
|
+
* the first call of every registry sync. As in
|
|
22953
|
+
* {@link currentSessionCanListSpaces}, a session with no parseable recap
|
|
22954
|
+
* keeps today's behavior.
|
|
22955
|
+
*/
|
|
22956
|
+
currentSessionCoversAccountRegistry() {
|
|
22957
|
+
const session = this.currentTinyCloudSession();
|
|
22958
|
+
const operations = session ? this.recapOperationsFromSession(session) : [];
|
|
22959
|
+
if (operations.length === 0) {
|
|
22960
|
+
return true;
|
|
22961
|
+
}
|
|
22962
|
+
const indexRead = {
|
|
22963
|
+
spaceId: this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2),
|
|
22964
|
+
service: "sql",
|
|
22965
|
+
path: "account",
|
|
22966
|
+
action: "tinycloud.sql/read"
|
|
22967
|
+
};
|
|
22968
|
+
return operations.some((granted) => this.operationCovers(granted, indexRead));
|
|
22969
|
+
}
|
|
22505
22970
|
async withAccountRegistryRetry(task, signal) {
|
|
22506
22971
|
const delays = [250, 1e3, 3e3];
|
|
22507
22972
|
const stopped = (detail) => {
|
|
@@ -22542,10 +23007,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22542
23007
|
}
|
|
22543
23008
|
lastError = error;
|
|
22544
23009
|
if (attempt < delays.length - 1) {
|
|
22545
|
-
await new Promise((
|
|
23010
|
+
await new Promise((resolve2, reject2) => {
|
|
22546
23011
|
const timer = setTimeout(() => {
|
|
22547
23012
|
signal.removeEventListener("abort", onAbort);
|
|
22548
|
-
|
|
23013
|
+
resolve2();
|
|
22549
23014
|
}, delays[attempt]);
|
|
22550
23015
|
const onAbort = () => {
|
|
22551
23016
|
clearTimeout(timer);
|
|
@@ -22785,7 +23250,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22785
23250
|
}
|
|
22786
23251
|
entry.waiters += 1;
|
|
22787
23252
|
const shared = entry;
|
|
22788
|
-
return new Promise((
|
|
23253
|
+
return new Promise((resolve2, reject2) => {
|
|
22789
23254
|
const onAbort = () => {
|
|
22790
23255
|
reject2(signal.reason);
|
|
22791
23256
|
shared.waiters -= 1;
|
|
@@ -22794,7 +23259,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22794
23259
|
if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
|
|
22795
23260
|
};
|
|
22796
23261
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
22797
|
-
shared.promise.then(
|
|
23262
|
+
shared.promise.then(resolve2, reject2).finally(() => signal.removeEventListener("abort", onAbort));
|
|
22798
23263
|
});
|
|
22799
23264
|
}
|
|
22800
23265
|
/** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
|
|
@@ -22968,11 +23433,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22968
23433
|
);
|
|
22969
23434
|
const stagedHost = resolvedHost ?? this.config.host;
|
|
22970
23435
|
let restoredNodeFeatures;
|
|
23436
|
+
let replicaProtocolMismatch = false;
|
|
22971
23437
|
if (this.config.replication?.enabled) {
|
|
22972
23438
|
try {
|
|
22973
23439
|
restoredNodeFeatures = (await checkNodeInfo2(stagedHost, this.wasmBindings.protocolVersion())).features;
|
|
22974
23440
|
} catch (error) {
|
|
22975
|
-
|
|
23441
|
+
replicaProtocolMismatch = error instanceof ProtocolMismatchError;
|
|
22976
23442
|
}
|
|
22977
23443
|
}
|
|
22978
23444
|
const stagedAddress = restoredAddress;
|
|
@@ -22980,28 +23446,55 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22980
23446
|
let replicationPrincipal;
|
|
22981
23447
|
let compactReplicationSession;
|
|
22982
23448
|
let replicationScopeError;
|
|
23449
|
+
let replicationScopeUnavailable = false;
|
|
22983
23450
|
if (this.config.replication?.enabled) {
|
|
22984
23451
|
if (stagedAddress !== void 0) {
|
|
22985
23452
|
replicationPrincipal = pkhDid2(stagedAddress, stagedChainId);
|
|
22986
23453
|
} else {
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
delegationCid: sessionData.delegationCid,
|
|
22991
|
-
verificationMethod: restoredVerificationMethod,
|
|
22992
|
-
persistedSpace: sessionData.spaceId
|
|
22993
|
-
});
|
|
22994
|
-
if (!signedScope) {
|
|
22995
|
-
replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
|
|
22996
|
-
} else {
|
|
22997
|
-
replicationPrincipal = signedScope.principal;
|
|
22998
|
-
compactReplicationSession = {
|
|
23454
|
+
try {
|
|
23455
|
+
const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
|
|
23456
|
+
const signedScope = replicationScopeFromSignedSession2({
|
|
22999
23457
|
delegationHeader: sessionData.delegationHeader,
|
|
23000
23458
|
delegationCid: sessionData.delegationCid,
|
|
23001
|
-
spaceId: signedScope.space,
|
|
23002
23459
|
verificationMethod: restoredVerificationMethod,
|
|
23003
|
-
|
|
23004
|
-
};
|
|
23460
|
+
persistedSpace: sessionData.spaceId
|
|
23461
|
+
});
|
|
23462
|
+
if (!signedScope) {
|
|
23463
|
+
replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
|
|
23464
|
+
} else {
|
|
23465
|
+
replicationPrincipal = signedScope.principal;
|
|
23466
|
+
compactReplicationSession = {
|
|
23467
|
+
delegationHeader: sessionData.delegationHeader,
|
|
23468
|
+
delegationCid: sessionData.delegationCid,
|
|
23469
|
+
spaceId: signedScope.space,
|
|
23470
|
+
verificationMethod: restoredVerificationMethod,
|
|
23471
|
+
jwk: stagedJwk
|
|
23472
|
+
};
|
|
23473
|
+
}
|
|
23474
|
+
} catch {
|
|
23475
|
+
replicationScopeUnavailable = true;
|
|
23476
|
+
replicationScopeError = "Replica scope could not be verified for this network session.";
|
|
23477
|
+
}
|
|
23478
|
+
}
|
|
23479
|
+
}
|
|
23480
|
+
let replicaRestoreGate;
|
|
23481
|
+
let restoredReplicaStamp;
|
|
23482
|
+
const replication = this.config.replication;
|
|
23483
|
+
if (replication?.enabled) {
|
|
23484
|
+
if (replicaProtocolMismatch) {
|
|
23485
|
+
replicaRestoreGate = "gate_unavailable";
|
|
23486
|
+
} else if (replicationPrincipal === void 0) {
|
|
23487
|
+
replicaRestoreGate = replicationScopeUnavailable ? "gate_unavailable" : "principal_mismatch";
|
|
23488
|
+
} else {
|
|
23489
|
+
try {
|
|
23490
|
+
const { restoreReplicaSessionStamp: restoreReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
|
|
23491
|
+
restoredReplicaStamp = await restoreReplicaSessionStamp2(
|
|
23492
|
+
replication.storage,
|
|
23493
|
+
replicationPrincipal,
|
|
23494
|
+
sessionData.replicaStamp ?? void 0
|
|
23495
|
+
);
|
|
23496
|
+
} catch (error) {
|
|
23497
|
+
replicaRestoreGate = replicaRestoreRefusal(error);
|
|
23005
23498
|
}
|
|
23006
23499
|
}
|
|
23007
23500
|
}
|
|
@@ -23094,7 +23587,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23094
23587
|
}
|
|
23095
23588
|
this.replicationDelegateSession = compactReplicationSession;
|
|
23096
23589
|
this.replicationScopeError = replicationScopeError;
|
|
23097
|
-
|
|
23590
|
+
this.replicaRestoreGate = replicaRestoreGate;
|
|
23591
|
+
this.replicaSessionPrincipal = replicationPrincipal;
|
|
23592
|
+
this.replicaSessionStamp = restoredReplicaStamp;
|
|
23593
|
+
await this.quiesceReplicaRuntime();
|
|
23098
23594
|
if (epoch !== this.sessionEpoch) {
|
|
23099
23595
|
oldGraph.retire();
|
|
23100
23596
|
oldCore?.retireServices?.();
|
|
@@ -23109,7 +23605,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23109
23605
|
detail: replicationScopeError
|
|
23110
23606
|
});
|
|
23111
23607
|
}
|
|
23112
|
-
await this.
|
|
23608
|
+
await this.bindReplicaOrGate(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
|
|
23113
23609
|
oldCore?.retireServices?.();
|
|
23114
23610
|
if (epoch !== this.sessionEpoch) {
|
|
23115
23611
|
if (attempt === this.latestSessionAttempt) await this.endSession();
|
|
@@ -23242,6 +23738,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23242
23738
|
session: input.serviceSession,
|
|
23243
23739
|
invoke: graph.invoke,
|
|
23244
23740
|
invokeAny: graph.invokeAny,
|
|
23741
|
+
signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
|
|
23245
23742
|
fetch: graph.fetch
|
|
23246
23743
|
});
|
|
23247
23744
|
const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
|
|
@@ -23585,6 +24082,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23585
24082
|
signer: this.signer,
|
|
23586
24083
|
signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
|
|
23587
24084
|
wasmBindings: this.wasmBindings,
|
|
24085
|
+
...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
23588
24086
|
sessionManager: this.sessionManager,
|
|
23589
24087
|
sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
|
|
23590
24088
|
domain: this.siweDomain,
|
|
@@ -23643,6 +24141,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23643
24141
|
signer: this.signer,
|
|
23644
24142
|
signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
|
|
23645
24143
|
wasmBindings: this.wasmBindings,
|
|
24144
|
+
...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
23646
24145
|
sessionManager: this.sessionManager,
|
|
23647
24146
|
sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
|
|
23648
24147
|
domain: this.siweDomain,
|
|
@@ -23723,7 +24222,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23723
24222
|
jwk: session.jwk
|
|
23724
24223
|
};
|
|
23725
24224
|
this._serviceContext.setSession(serviceSession);
|
|
23726
|
-
await this.
|
|
24225
|
+
await this.bindReplicaOrGate(this._serviceContext, serviceSession, this._kv);
|
|
23727
24226
|
this.tc.serviceContext.setSession(serviceSession);
|
|
23728
24227
|
this._vault = this.createVaultService(session.spaceId, this._kv);
|
|
23729
24228
|
this._vault.initialize(this._serviceContext);
|
|
@@ -24117,6 +24616,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24117
24616
|
session: serviceSession,
|
|
24118
24617
|
invoke: graph.invoke,
|
|
24119
24618
|
invokeAny: graph.invokeAny,
|
|
24619
|
+
signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
|
|
24120
24620
|
fetch: graph.fetch
|
|
24121
24621
|
});
|
|
24122
24622
|
this._spaceService = new SpaceService({
|
|
@@ -25409,7 +25909,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25409
25909
|
return this.delegationManager.create(params);
|
|
25410
25910
|
}
|
|
25411
25911
|
/**
|
|
25412
|
-
* Revoke a delegation
|
|
25912
|
+
* Revoke a delegation with the owner wallet when available. Session-only
|
|
25913
|
+
* clients continue to use their delegated revocation authority.
|
|
25413
25914
|
*
|
|
25414
25915
|
* @param cid - The CID of the delegation to revoke
|
|
25415
25916
|
* @returns Result indicating success or failure
|
|
@@ -25684,34 +26185,67 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25684
26185
|
return auth.replicationSignInEntries();
|
|
25685
26186
|
}
|
|
25686
26187
|
async replicationAuthority() {
|
|
25687
|
-
const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
|
|
26188
|
+
const { createReplicationAuthority: createReplicationAuthority2, mintNodeReplicationGrant: mintNodeReplicationGrant2, verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2 } = await getNodeReplicationLoaders().authority();
|
|
25688
26189
|
return createReplicationAuthority2({
|
|
25689
26190
|
replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
|
|
26191
|
+
verifiedConsentAnchor: (session) => {
|
|
26192
|
+
const current = this.currentTinyCloudSession();
|
|
26193
|
+
return session === current && current !== void 0 ? verifiedOwnerConsentAnchor2(current, this.wasmBindings, exactVerifiedRecap) : null;
|
|
26194
|
+
},
|
|
25690
26195
|
siweExpiration: (siwe) => extractSiweExpiration(siwe),
|
|
25691
26196
|
planDelegation: (entries, options) => this.planDelegation(entries, options),
|
|
25692
|
-
mintDelegation:
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
26197
|
+
mintDelegation: (deviceDid, entries, options) => mintNodeReplicationGrant2(
|
|
26198
|
+
deviceDid,
|
|
26199
|
+
entries,
|
|
26200
|
+
options,
|
|
26201
|
+
options?.renewalOnly ? this.currentTinyCloudSession() : void 0,
|
|
26202
|
+
this
|
|
26203
|
+
)
|
|
25699
26204
|
});
|
|
25700
26205
|
}
|
|
25701
|
-
async
|
|
26206
|
+
async quiesceReplicaRuntime() {
|
|
25702
26207
|
if (!this.replicationRuntime) return;
|
|
25703
|
-
|
|
25704
|
-
|
|
25705
|
-
|
|
25706
|
-
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
26208
|
+
try {
|
|
26209
|
+
await (await this.replicationRuntime).unbind();
|
|
26210
|
+
} catch {
|
|
26211
|
+
this.replicaRestoreGate = "gate_unavailable";
|
|
26212
|
+
}
|
|
26213
|
+
}
|
|
26214
|
+
async bindReplicaOrGate(context, session, kv, principal) {
|
|
26215
|
+
if (!this.replicationRuntime) return;
|
|
26216
|
+
const gate = this.replicaRestoreGate;
|
|
26217
|
+
const epoch = this.sessionEpoch;
|
|
26218
|
+
const attempt = this.latestSessionAttempt;
|
|
26219
|
+
try {
|
|
26220
|
+
const runtime = await this.replicationRuntime;
|
|
26221
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context || gate !== this.replicaRestoreGate) return;
|
|
26222
|
+
if (gate) {
|
|
26223
|
+
await runtime.bindGatedNetworkOnly({
|
|
26224
|
+
context,
|
|
26225
|
+
session,
|
|
26226
|
+
gate,
|
|
26227
|
+
primaryKV: [{ space: session.spaceId, kv }]
|
|
26228
|
+
}, getNodeReplicationLoaders().gatedNetwork);
|
|
26229
|
+
return;
|
|
26230
|
+
}
|
|
26231
|
+
const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : pkhDid2(this._address, this._chainId));
|
|
26232
|
+
if (identityPrincipal === void 0) return;
|
|
26233
|
+
const authority = await this.replicationAuthority();
|
|
26234
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
|
|
26235
|
+
await runtime.bind({
|
|
26236
|
+
context,
|
|
26237
|
+
session,
|
|
26238
|
+
principal: identityPrincipal,
|
|
26239
|
+
stamp: this.replicaSessionStamp,
|
|
26240
|
+
authority,
|
|
26241
|
+
nodeFeatures: this.replicationNodeFeatures,
|
|
26242
|
+
primaryKV: [{ space: session.spaceId, kv }]
|
|
26243
|
+
});
|
|
26244
|
+
} catch {
|
|
26245
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
|
|
26246
|
+
this.replicaRestoreGate = "gate_unavailable";
|
|
26247
|
+
await this.quiesceReplicaRuntime();
|
|
26248
|
+
}
|
|
25715
26249
|
}
|
|
25716
26250
|
/** Stable control facade for this node's optional replication runtime. */
|
|
25717
26251
|
get replication() {
|
|
@@ -26860,9 +27394,16 @@ TinyCloudNode.registerNodeDefaults({
|
|
|
26860
27394
|
});
|
|
26861
27395
|
|
|
26862
27396
|
// src/replication/node-loader.ts
|
|
27397
|
+
init_config();
|
|
26863
27398
|
registerReplicationLoaders({
|
|
27399
|
+
validateConfig: assertValidReplicationConfig,
|
|
26864
27400
|
runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
|
|
26865
|
-
authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
|
|
27401
|
+
authority: () => Promise.resolve().then(() => (init_authority(), authority_exports)),
|
|
27402
|
+
gatedNetwork: () => Promise.resolve().then(() => (init_gated_network(), gated_network_exports)),
|
|
27403
|
+
// Lazy by contract: replica loads only when replication is enabled. Literal specifier typed by
|
|
27404
|
+
// ProfileGateModule; TS2307 under moduleResolution "node" is suppressed (see replica-contract.ts).
|
|
27405
|
+
// @ts-ignore
|
|
27406
|
+
gate: () => import("@tinycloud/replica/profile-gate")
|
|
26866
27407
|
});
|
|
26867
27408
|
|
|
26868
27409
|
// src/index.ts
|
|
@@ -27178,8 +27719,8 @@ import {
|
|
|
27178
27719
|
Space
|
|
27179
27720
|
} from "@tinycloud/sdk-core";
|
|
27180
27721
|
import {
|
|
27181
|
-
ProtocolMismatchError,
|
|
27182
|
-
VersionCheckError
|
|
27722
|
+
ProtocolMismatchError as ProtocolMismatchError2,
|
|
27723
|
+
VersionCheckError,
|
|
27183
27724
|
UnsupportedFeatureError as UnsupportedFeatureError2,
|
|
27184
27725
|
checkNodeInfo as checkNodeInfo3
|
|
27185
27726
|
} from "@tinycloud/sdk-core";
|
|
@@ -27187,15 +27728,518 @@ import { ServiceContext as ServiceContext4 } from "@tinycloud/sdk-core";
|
|
|
27187
27728
|
|
|
27188
27729
|
// src/replication/sqlite.ts
|
|
27189
27730
|
init_authority();
|
|
27731
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
27190
27732
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
27191
|
-
import {
|
|
27192
|
-
import {
|
|
27733
|
+
import { lstatSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
27734
|
+
import { chmod, lstat, mkdir, open as openFile, readFile, readdir, realpath, rename, rm, rmdir, stat } from "fs/promises";
|
|
27735
|
+
import { basename, dirname as dirname2, join as join2, parse as parse2, resolve, sep } from "path";
|
|
27193
27736
|
import { KVService as KVService3, ServiceContext as ServiceContext3, principalDid as principalDid2 } from "@tinycloud/sdk-core";
|
|
27194
27737
|
import {
|
|
27195
27738
|
canonicalReplicationIdentity,
|
|
27196
27739
|
replicationIdentityKey,
|
|
27197
27740
|
requiresSecretsOptIn as requiresSecretsOptIn2
|
|
27198
27741
|
} from "@tinycloud/sdk-services";
|
|
27742
|
+
import { CONSENT_WINDOW_MS, MAX_GRANT_TTL_MS as MAX_GRANT_TTL_MS2, REPLICA_GATE_FILE } from "@tinycloud/sdk-services/kv/replication/contractConstants";
|
|
27743
|
+
var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
|
|
27744
|
+
function assertProfileLogoutClear(profileDir) {
|
|
27745
|
+
assertLogoutClear(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
27746
|
+
}
|
|
27747
|
+
function assertLogoutClear(path) {
|
|
27748
|
+
if (path === void 0) return;
|
|
27749
|
+
try {
|
|
27750
|
+
lstatSync(path);
|
|
27751
|
+
} catch (error) {
|
|
27752
|
+
if (isNotFound(error)) return;
|
|
27753
|
+
}
|
|
27754
|
+
throw gated("gate_unavailable");
|
|
27755
|
+
}
|
|
27756
|
+
function safeInventoryPath(path) {
|
|
27757
|
+
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/"));
|
|
27758
|
+
}
|
|
27759
|
+
function logoutPrincipalDigest(principal) {
|
|
27760
|
+
return createHash2("sha256").update("tinycloud-replica-logout\0").update(principal, "utf8").digest("hex");
|
|
27761
|
+
}
|
|
27762
|
+
function compareInventoryString(left, right) {
|
|
27763
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
27764
|
+
}
|
|
27765
|
+
function approvalEntries(report) {
|
|
27766
|
+
return [
|
|
27767
|
+
...report.unknownEntries,
|
|
27768
|
+
...report.knownEntries,
|
|
27769
|
+
...report.knownRoots.map((root) => ({ ...root, kind: "directory" })),
|
|
27770
|
+
...report.missingDatabases.map((path) => ({ path, kind: "missing", dev: "", ino: "" }))
|
|
27771
|
+
].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));
|
|
27772
|
+
}
|
|
27773
|
+
function inventoryApprovalToken(marker, principal, entries) {
|
|
27774
|
+
const markerId = marker.v === 2 ? [marker.v, marker.nonce, marker.principalDigest] : [marker.v, marker.incarnation, marker.lifecycleEpoch, marker.principalDigest];
|
|
27775
|
+
return createHash2("sha256").update("tc-logout-approve-v1\0").update(JSON.stringify([
|
|
27776
|
+
markerId,
|
|
27777
|
+
marker.approvalSalt,
|
|
27778
|
+
principal,
|
|
27779
|
+
marker.v === 2 ? null : marker.lifecycleEpoch,
|
|
27780
|
+
entries
|
|
27781
|
+
])).digest().subarray(0, 16).toString("hex");
|
|
27782
|
+
}
|
|
27783
|
+
function changedInventoryRefusal(marker, principal, report, entries = approvalEntries(report)) {
|
|
27784
|
+
const token = inventoryApprovalToken(marker, principal, entries);
|
|
27785
|
+
const error = localChangesRefusal(report.unknownPaths);
|
|
27786
|
+
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}.`;
|
|
27787
|
+
return Object.assign(error, { inventoryApprovalToken: token, inventoryApprovalEntries: entries });
|
|
27788
|
+
}
|
|
27789
|
+
function storedInventoryChanged(marker, report) {
|
|
27790
|
+
try {
|
|
27791
|
+
assertApprovedInventory(report, {
|
|
27792
|
+
unknownEntries: marker.approvedUnknown,
|
|
27793
|
+
knownRoots: marker.knownRoots,
|
|
27794
|
+
knownEntries: marker.knownEntries,
|
|
27795
|
+
missingDatabases: marker.missingDatabases
|
|
27796
|
+
});
|
|
27797
|
+
return false;
|
|
27798
|
+
} catch (error) {
|
|
27799
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
|
|
27800
|
+
return true;
|
|
27801
|
+
}
|
|
27802
|
+
throw error;
|
|
27803
|
+
}
|
|
27804
|
+
}
|
|
27805
|
+
function decimalIdentity(value) {
|
|
27806
|
+
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
27807
|
+
}
|
|
27808
|
+
function logoutMarkerOf(value) {
|
|
27809
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw gated("gate_unavailable");
|
|
27810
|
+
const row = value;
|
|
27811
|
+
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);
|
|
27812
|
+
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);
|
|
27813
|
+
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(
|
|
27814
|
+
(path) => typeof path === "string" && safeInventoryPath(path) && path.endsWith("/replica.db")
|
|
27815
|
+
) || 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");
|
|
27816
|
+
const optional = Number(row.discardLocalChanges === true) + Number(row.remotePending === true) + Number(row.remoteAbandoned === true);
|
|
27817
|
+
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;
|
|
27818
|
+
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) {
|
|
27819
|
+
return row;
|
|
27820
|
+
}
|
|
27821
|
+
const bound = Number(row.deniedEpoch !== void 0);
|
|
27822
|
+
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)) {
|
|
27823
|
+
throw gated("gate_unavailable");
|
|
27824
|
+
}
|
|
27825
|
+
return row;
|
|
27826
|
+
}
|
|
27827
|
+
async function readLogoutMarker(path) {
|
|
27828
|
+
const value = await readJsonFile(path);
|
|
27829
|
+
return value === void 0 ? null : logoutMarkerOf(value);
|
|
27830
|
+
}
|
|
27831
|
+
async function orphanRoot(profileDir, name) {
|
|
27832
|
+
let profile;
|
|
27833
|
+
try {
|
|
27834
|
+
profile = await lstat(profileDir);
|
|
27835
|
+
} catch {
|
|
27836
|
+
throw gated("gate_unavailable");
|
|
27837
|
+
}
|
|
27838
|
+
let pinned = false;
|
|
27839
|
+
if (process.platform === "linux" && /^\/proc\/self\/fd\/[1-9]\d*$/.test(profileDir) && profile.isSymbolicLink()) {
|
|
27840
|
+
try {
|
|
27841
|
+
pinned = (await stat(profileDir)).isDirectory();
|
|
27842
|
+
} catch {
|
|
27843
|
+
throw gated("gate_unavailable");
|
|
27844
|
+
}
|
|
27845
|
+
}
|
|
27846
|
+
if (!profile.isDirectory() && !pinned) throw gated("gate_unavailable");
|
|
27847
|
+
const root = join2(profileDir, name);
|
|
27848
|
+
let entry;
|
|
27849
|
+
try {
|
|
27850
|
+
entry = await lstat(root);
|
|
27851
|
+
} catch (error) {
|
|
27852
|
+
if (isNotFound(error)) return null;
|
|
27853
|
+
throw error;
|
|
27854
|
+
}
|
|
27855
|
+
if (!entry.isDirectory() || await realpath(root) !== join2(await realpath(profileDir), name)) {
|
|
27856
|
+
throw gated("gate_unavailable");
|
|
27857
|
+
}
|
|
27858
|
+
return root;
|
|
27859
|
+
}
|
|
27860
|
+
async function hasOrphanPending(profileDir) {
|
|
27861
|
+
const root = await orphanRoot(profileDir, "replication");
|
|
27862
|
+
if (root === null) return false;
|
|
27863
|
+
let entries;
|
|
27864
|
+
try {
|
|
27865
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
27866
|
+
} catch {
|
|
27867
|
+
return true;
|
|
27868
|
+
}
|
|
27869
|
+
for (const entry of entries) {
|
|
27870
|
+
if (!entry.isDirectory()) return true;
|
|
27871
|
+
const dir = join2(root, entry.name);
|
|
27872
|
+
try {
|
|
27873
|
+
if (!(await lstat(dir)).isDirectory()) return true;
|
|
27874
|
+
} catch {
|
|
27875
|
+
return true;
|
|
27876
|
+
}
|
|
27877
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) return true;
|
|
27878
|
+
let children;
|
|
27879
|
+
try {
|
|
27880
|
+
children = await readdir(dir, { withFileTypes: true });
|
|
27881
|
+
} catch {
|
|
27882
|
+
return true;
|
|
27883
|
+
}
|
|
27884
|
+
if (children.length !== 2 || children.some((child) => !child.isFile() || child.name !== "identity.json" && child.name !== "pending.json")) return true;
|
|
27885
|
+
const pendingFile = join2(dir, "pending.json");
|
|
27886
|
+
let text;
|
|
27887
|
+
try {
|
|
27888
|
+
const pending = await lstat(pendingFile);
|
|
27889
|
+
if (!pending.isFile()) return true;
|
|
27890
|
+
text = await readFile(pendingFile, "utf8");
|
|
27891
|
+
} catch (error) {
|
|
27892
|
+
if (isNotFound(error)) return true;
|
|
27893
|
+
return true;
|
|
27894
|
+
}
|
|
27895
|
+
try {
|
|
27896
|
+
const identityFile = join2(dir, "identity.json");
|
|
27897
|
+
if (!(await lstat(identityFile)).isFile()) return true;
|
|
27898
|
+
const identity = identityRecordOf(await readJsonFile(identityFile));
|
|
27899
|
+
if (identity === void 0 || parsePendingState(text, pendingFile, identity).records.length !== 0) {
|
|
27900
|
+
return true;
|
|
27901
|
+
}
|
|
27902
|
+
} catch {
|
|
27903
|
+
return true;
|
|
27904
|
+
}
|
|
27905
|
+
}
|
|
27906
|
+
return false;
|
|
27907
|
+
}
|
|
27908
|
+
function inventoryIdentity(path, dev, ino) {
|
|
27909
|
+
return { path, dev: dev.toString(), ino: ino.toString() };
|
|
27910
|
+
}
|
|
27911
|
+
async function inventoryUnknown(profileDir, relative, entries) {
|
|
27912
|
+
const path = join2(profileDir, relative);
|
|
27913
|
+
const info = await lstat(path, { bigint: true });
|
|
27914
|
+
const kind = info.isDirectory() ? "directory" : info.isSymbolicLink() ? "symlink" : "file";
|
|
27915
|
+
entries.push({ ...inventoryIdentity(relative, info.dev, info.ino), kind });
|
|
27916
|
+
if (kind === "directory") {
|
|
27917
|
+
for (const child of await readdir(path)) await inventoryUnknown(profileDir, `${relative}/${child}`, entries);
|
|
27918
|
+
}
|
|
27919
|
+
}
|
|
27920
|
+
async function inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly = false, diagnosticFiles = []) {
|
|
27921
|
+
const report = {
|
|
27922
|
+
knownLegacyReplicas: 0,
|
|
27923
|
+
unknownLegacyPaths: [],
|
|
27924
|
+
unknownPaths: [],
|
|
27925
|
+
unknownEntries: [],
|
|
27926
|
+
knownRoots: [],
|
|
27927
|
+
knownEntries: [],
|
|
27928
|
+
missingDatabases: []
|
|
27929
|
+
};
|
|
27930
|
+
const legacy = await orphanRoot(profileDir, "replicas");
|
|
27931
|
+
const active = legacyOnly ? null : await orphanRoot(profileDir, "replication");
|
|
27932
|
+
for (const [name, dir] of [["replicas", legacy], ["replication", active]]) {
|
|
27933
|
+
if (dir === null) continue;
|
|
27934
|
+
const info = await lstat(dir, { bigint: true });
|
|
27935
|
+
report.knownRoots.push(inventoryIdentity(name, info.dev, info.ino));
|
|
27936
|
+
}
|
|
27937
|
+
if (legacy !== null) {
|
|
27938
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
27939
|
+
for (const entry of await readdir(legacy, { withFileTypes: true })) {
|
|
27940
|
+
const relative = `replicas/${entry.name}`;
|
|
27941
|
+
const child = join2(legacy, entry.name);
|
|
27942
|
+
if (!entry.isDirectory()) {
|
|
27943
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27944
|
+
continue;
|
|
27945
|
+
}
|
|
27946
|
+
try {
|
|
27947
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(child);
|
|
27948
|
+
report.knownRoots.push({ path: relative, ...layout.rootIdentity });
|
|
27949
|
+
report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
|
|
27950
|
+
...item,
|
|
27951
|
+
path: `${relative}/${item.path}`
|
|
27952
|
+
})));
|
|
27953
|
+
report.knownEntries.push(...layout.knownEntries.map((item) => ({
|
|
27954
|
+
...item,
|
|
27955
|
+
path: `${relative}/${item.path}`
|
|
27956
|
+
})));
|
|
27957
|
+
if (layout.unknownPaths.length === 0) report.knownLegacyReplicas++;
|
|
27958
|
+
if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
27959
|
+
report.missingDatabases.push(`${relative}/replica.db`);
|
|
27960
|
+
}
|
|
27961
|
+
} catch {
|
|
27962
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27963
|
+
}
|
|
27964
|
+
}
|
|
27965
|
+
}
|
|
27966
|
+
if (active !== null) {
|
|
27967
|
+
const cleanOrphan = !gateReadable && (approvedMarker?.v === 2 || !await hasOrphanPending(profileDir));
|
|
27968
|
+
for (const entry of await readdir(active, { withFileTypes: true })) {
|
|
27969
|
+
const relative = `replication/${entry.name}`;
|
|
27970
|
+
const dir = join2(active, entry.name);
|
|
27971
|
+
if (approvedMarker?.approvedUnknown.some((item) => item.path === relative)) {
|
|
27972
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27973
|
+
continue;
|
|
27974
|
+
}
|
|
27975
|
+
if (gateReadable && diagnosticFiles.includes(entry.name)) {
|
|
27976
|
+
const info2 = await lstat(dir, { bigint: true });
|
|
27977
|
+
if (info2.isFile()) {
|
|
27978
|
+
report.knownEntries.push({ ...inventoryIdentity(relative, info2.dev, info2.ino), kind: "file" });
|
|
27979
|
+
continue;
|
|
27980
|
+
}
|
|
27981
|
+
}
|
|
27982
|
+
if (!gateReadable && !cleanOrphan || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
27983
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27984
|
+
continue;
|
|
27985
|
+
}
|
|
27986
|
+
const info = await lstat(dir, { bigint: true });
|
|
27987
|
+
report.knownRoots.push(inventoryIdentity(relative, info.dev, info.ino));
|
|
27988
|
+
const identityPath = join2(dir, "identity.json");
|
|
27989
|
+
let identity;
|
|
27990
|
+
try {
|
|
27991
|
+
if ((await lstat(identityPath)).isFile()) identity = identityRecordOf(await readJsonFile(identityPath));
|
|
27992
|
+
} catch {
|
|
27993
|
+
}
|
|
27994
|
+
if (identity === void 0) {
|
|
27995
|
+
const approvedRoot = approvedMarker?.knownRoots.find((item) => item.path === relative);
|
|
27996
|
+
if (approvedRoot?.dev === info.dev.toString() && approvedRoot.ino === info.ino.toString()) {
|
|
27997
|
+
const children = await readdir(dir);
|
|
27998
|
+
if (children.length === 0) continue;
|
|
27999
|
+
if (children.length === 1 && children[0] === "replicas") {
|
|
28000
|
+
const replicasPath = `${relative}/replicas`;
|
|
28001
|
+
const replicasDir = join2(dir, "replicas");
|
|
28002
|
+
const replicasInfo = await lstat(replicasDir, { bigint: true });
|
|
28003
|
+
const approvedReplicas = approvedMarker.knownRoots.find((item) => item.path === replicasPath);
|
|
28004
|
+
if (replicasInfo.isDirectory() && !replicasInfo.isSymbolicLink() && approvedReplicas?.dev === replicasInfo.dev.toString() && approvedReplicas.ino === replicasInfo.ino.toString() && (await readdir(replicasDir)).length === 0) {
|
|
28005
|
+
report.knownRoots.push(inventoryIdentity(replicasPath, replicasInfo.dev, replicasInfo.ino));
|
|
28006
|
+
continue;
|
|
28007
|
+
}
|
|
28008
|
+
}
|
|
28009
|
+
}
|
|
28010
|
+
report.knownRoots.pop();
|
|
28011
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
28012
|
+
continue;
|
|
28013
|
+
}
|
|
28014
|
+
for (const child of await readdir(dir, { withFileTypes: true })) {
|
|
28015
|
+
const childRelative = `${relative}/${child.name}`;
|
|
28016
|
+
const childPath = join2(dir, child.name);
|
|
28017
|
+
if ((child.name === "identity.json" || child.name === "device.jwk") && child.isFile()) {
|
|
28018
|
+
const childInfo = await lstat(childPath, { bigint: true });
|
|
28019
|
+
report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
|
|
28020
|
+
continue;
|
|
28021
|
+
}
|
|
28022
|
+
if (child.name === "pending.json" && child.isFile()) {
|
|
28023
|
+
try {
|
|
28024
|
+
if (parsePendingState(await readFile(childPath, "utf8"), childPath, identity).records.length === 0) {
|
|
28025
|
+
const childInfo = await lstat(childPath, { bigint: true });
|
|
28026
|
+
report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
|
|
28027
|
+
continue;
|
|
28028
|
+
}
|
|
28029
|
+
} catch {
|
|
28030
|
+
}
|
|
28031
|
+
}
|
|
28032
|
+
if (child.name === "replicas" && child.isDirectory()) {
|
|
28033
|
+
const replicasInfo = await lstat(childPath, { bigint: true });
|
|
28034
|
+
report.knownRoots.push(inventoryIdentity(childRelative, replicasInfo.dev, replicasInfo.ino));
|
|
28035
|
+
for (const replicaChild of await readdir(childPath, { withFileTypes: true })) {
|
|
28036
|
+
const replicaRelative = `${childRelative}/${replicaChild.name}`;
|
|
28037
|
+
if (!replicaChild.isDirectory() || !/^[a-z2-7]{26}$/.test(replicaChild.name)) {
|
|
28038
|
+
await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
|
|
28039
|
+
continue;
|
|
28040
|
+
}
|
|
28041
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
28042
|
+
try {
|
|
28043
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(join2(childPath, replicaChild.name), {
|
|
28044
|
+
allowAdapterFence: true
|
|
28045
|
+
});
|
|
28046
|
+
report.knownRoots.push({ path: replicaRelative, ...layout.rootIdentity });
|
|
28047
|
+
report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
|
|
28048
|
+
...item,
|
|
28049
|
+
path: `${replicaRelative}/${item.path}`
|
|
28050
|
+
})));
|
|
28051
|
+
report.knownEntries.push(...layout.knownEntries.map((item) => ({
|
|
28052
|
+
...item,
|
|
28053
|
+
path: `${replicaRelative}/${item.path}`
|
|
28054
|
+
})));
|
|
28055
|
+
if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
28056
|
+
report.missingDatabases.push(`${replicaRelative}/replica.db`);
|
|
28057
|
+
}
|
|
28058
|
+
} catch {
|
|
28059
|
+
await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
|
|
28060
|
+
}
|
|
28061
|
+
}
|
|
28062
|
+
continue;
|
|
28063
|
+
}
|
|
28064
|
+
await inventoryUnknown(profileDir, childRelative, report.unknownEntries);
|
|
28065
|
+
}
|
|
28066
|
+
}
|
|
28067
|
+
}
|
|
28068
|
+
report.unknownEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
28069
|
+
report.knownEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
28070
|
+
report.knownRoots.sort((a, b) => a.path.localeCompare(b.path));
|
|
28071
|
+
report.missingDatabases.sort();
|
|
28072
|
+
report.unknownPaths = [...report.unknownEntries.map((entry) => entry.path), ...report.missingDatabases].sort();
|
|
28073
|
+
report.unknownLegacyPaths = [...report.unknownPaths];
|
|
28074
|
+
return report;
|
|
28075
|
+
}
|
|
28076
|
+
function assertApprovedInventory(current, approval, allowRemoved = false) {
|
|
28077
|
+
const approved = new Map(approval.unknownEntries.map((item) => [item.path, item]));
|
|
28078
|
+
const known = new Map(approval.knownRoots.map((item) => [item.path, item]));
|
|
28079
|
+
const knownFiles = new Map(approval.knownEntries.map((item) => [item.path, item]));
|
|
28080
|
+
for (const entry of current.unknownEntries) {
|
|
28081
|
+
const expected = approved.get(entry.path);
|
|
28082
|
+
if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
|
|
28083
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
28084
|
+
}
|
|
28085
|
+
}
|
|
28086
|
+
for (const root of current.knownRoots) {
|
|
28087
|
+
const expected = known.get(root.path);
|
|
28088
|
+
if (expected?.dev !== root.dev || expected.ino !== root.ino) throw localChangesRefusal([root.path]);
|
|
28089
|
+
}
|
|
28090
|
+
for (const entry of current.knownEntries) {
|
|
28091
|
+
if (allowRemoved && entry.kind === "file" && /^(?:replication\/[a-z2-7]{26}\/replicas\/[a-z2-7]{26}|replicas\/[^/]+)\/replica\.db-(?:wal|shm|journal)$/.test(entry.path)) {
|
|
28092
|
+
const database = entry.path.replace(/-(?:wal|shm|journal)$/, "");
|
|
28093
|
+
const original = knownFiles.get(database);
|
|
28094
|
+
const present = current.knownEntries.find((item) => item.path === database);
|
|
28095
|
+
if (!approved.has(entry.path) && original?.kind === "file" && present?.dev === original.dev && present.ino === original.ino) continue;
|
|
28096
|
+
}
|
|
28097
|
+
const expected = knownFiles.get(entry.path);
|
|
28098
|
+
if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
|
|
28099
|
+
throw localChangesRefusal([entry.path]);
|
|
28100
|
+
}
|
|
28101
|
+
}
|
|
28102
|
+
for (const path of current.missingDatabases) {
|
|
28103
|
+
if (!approval.missingDatabases.includes(path) && !(allowRemoved && knownFiles.get(path)?.kind === "file")) throw localChangesRefusal([path]);
|
|
28104
|
+
}
|
|
28105
|
+
if (!allowRemoved && (current.unknownEntries.length !== approved.size || current.knownRoots.length !== known.size || current.knownEntries.length !== knownFiles.size || current.missingDatabases.length !== approval.missingDatabases.length)) {
|
|
28106
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
28107
|
+
}
|
|
28108
|
+
}
|
|
28109
|
+
async function assertApprovedParents(profileDir, marker, path) {
|
|
28110
|
+
if (process.platform !== "linux") {
|
|
28111
|
+
const absolute = resolve(profileDir);
|
|
28112
|
+
let ancestor = parse2(absolute).root;
|
|
28113
|
+
for (const part of absolute.slice(ancestor.length).split(sep).filter(Boolean)) {
|
|
28114
|
+
ancestor = join2(ancestor, part);
|
|
28115
|
+
const details = await lstat(ancestor, { bigint: true });
|
|
28116
|
+
if (!details.isDirectory() || details.isSymbolicLink()) throw localChangesRefusal([path]);
|
|
28117
|
+
}
|
|
28118
|
+
}
|
|
28119
|
+
let relativeParent = "";
|
|
28120
|
+
for (const part of path.split("/").slice(0, -1)) {
|
|
28121
|
+
relativeParent = relativeParent ? `${relativeParent}/${part}` : part;
|
|
28122
|
+
let details;
|
|
28123
|
+
try {
|
|
28124
|
+
details = await lstat(join2(profileDir, relativeParent), { bigint: true });
|
|
28125
|
+
} catch (error) {
|
|
28126
|
+
if (isNotFound(error)) return false;
|
|
28127
|
+
throw error;
|
|
28128
|
+
}
|
|
28129
|
+
const expected = marker.knownRoots.find((root) => root.path === relativeParent) ?? marker.approvedUnknown.find((item) => item.path === relativeParent && item.kind === "directory");
|
|
28130
|
+
if (!details.isDirectory() || details.isSymbolicLink() || expected?.dev !== details.dev.toString() || expected.ino !== details.ino.toString()) {
|
|
28131
|
+
throw localChangesRefusal([relativeParent]);
|
|
28132
|
+
}
|
|
28133
|
+
}
|
|
28134
|
+
return true;
|
|
28135
|
+
}
|
|
28136
|
+
async function eraseApprovedUnknown(profileDir, marker, prefix, diagnosticFiles) {
|
|
28137
|
+
const entries = marker.approvedUnknown.filter((entry) => entry.path.startsWith(`${prefix}/`));
|
|
28138
|
+
for (const entry of entries.sort((a, b) => b.path.length - a.path.length)) {
|
|
28139
|
+
assertApprovedInventory(await inspectProfileReplicaLayout(profileDir, marker.v !== 2, marker, false, diagnosticFiles), {
|
|
28140
|
+
unknownEntries: marker.approvedUnknown,
|
|
28141
|
+
knownRoots: marker.knownRoots,
|
|
28142
|
+
knownEntries: marker.knownEntries,
|
|
28143
|
+
missingDatabases: marker.missingDatabases
|
|
28144
|
+
}, true);
|
|
28145
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28146
|
+
const file = join2(profileDir, entry.path);
|
|
28147
|
+
let current;
|
|
28148
|
+
try {
|
|
28149
|
+
current = await lstat(file, { bigint: true });
|
|
28150
|
+
} catch (error) {
|
|
28151
|
+
if (isNotFound(error)) continue;
|
|
28152
|
+
throw error;
|
|
28153
|
+
}
|
|
28154
|
+
const kind = current.isDirectory() ? "directory" : current.isSymbolicLink() ? "symlink" : "file";
|
|
28155
|
+
if (kind !== entry.kind || current.dev.toString() !== entry.dev || current.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
|
|
28156
|
+
if (process.platform !== "linux" && kind === "symlink") throw localChangesRefusal([entry.path]);
|
|
28157
|
+
if (kind === "directory") await rmdir(file);
|
|
28158
|
+
else await rm(file);
|
|
28159
|
+
}
|
|
28160
|
+
}
|
|
28161
|
+
async function eraseKnownFlagResidue(profileDir, marker, diagnosticFiles) {
|
|
28162
|
+
const diagnostics = new Set(diagnosticFiles.map((name) => `replication/${name}`));
|
|
28163
|
+
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")))) {
|
|
28164
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28165
|
+
const file = join2(profileDir, entry.path);
|
|
28166
|
+
let details;
|
|
28167
|
+
try {
|
|
28168
|
+
details = await lstat(file, { bigint: true });
|
|
28169
|
+
} catch (error) {
|
|
28170
|
+
if (isNotFound(error)) continue;
|
|
28171
|
+
throw error;
|
|
28172
|
+
}
|
|
28173
|
+
if (!details.isFile() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
|
|
28174
|
+
await rm(file);
|
|
28175
|
+
}
|
|
28176
|
+
const roots = marker.knownRoots.filter((item) => item.path === "replication" || item.path.startsWith("replication/"));
|
|
28177
|
+
roots.sort((a, b) => b.path.split("/").length - a.path.split("/").length);
|
|
28178
|
+
for (const entry of roots) {
|
|
28179
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28180
|
+
const path = join2(profileDir, entry.path);
|
|
28181
|
+
let details;
|
|
28182
|
+
try {
|
|
28183
|
+
details = await lstat(path, { bigint: true });
|
|
28184
|
+
} catch (error) {
|
|
28185
|
+
if (isNotFound(error)) continue;
|
|
28186
|
+
throw error;
|
|
28187
|
+
}
|
|
28188
|
+
if (!details.isDirectory() || details.isSymbolicLink() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) {
|
|
28189
|
+
throw localChangesRefusal([entry.path]);
|
|
28190
|
+
}
|
|
28191
|
+
await rmdir(path);
|
|
28192
|
+
}
|
|
28193
|
+
}
|
|
28194
|
+
function localChangesRefusal(unknownPaths = []) {
|
|
28195
|
+
const error = gated("gate_unavailable");
|
|
28196
|
+
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(", ")}.`);
|
|
28197
|
+
return error;
|
|
28198
|
+
}
|
|
28199
|
+
function gateOwnedByAnother(state, principalHash) {
|
|
28200
|
+
return state.principalHash !== null ? state.principalHash !== principalHash : state.eraseDeferred && state.deferredPrincipalHash !== principalHash;
|
|
28201
|
+
}
|
|
28202
|
+
function isGateUnavailable(error) {
|
|
28203
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable";
|
|
28204
|
+
}
|
|
28205
|
+
function restoredGate() {
|
|
28206
|
+
const error = gated("stamp_stale");
|
|
28207
|
+
error.message = RESTORED_GATE_MESSAGE;
|
|
28208
|
+
return Object.assign(error, { reportCode: "gate_restored" });
|
|
28209
|
+
}
|
|
28210
|
+
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.";
|
|
28211
|
+
function interruptedSignOut() {
|
|
28212
|
+
const error = gated("gate_unavailable");
|
|
28213
|
+
error.message = "A previous sign-out was interrupted; re-running `tc auth logout` completes it.";
|
|
28214
|
+
return error;
|
|
28215
|
+
}
|
|
28216
|
+
function assertSupportedProfileLayout(report) {
|
|
28217
|
+
if (process.platform === "linux") return;
|
|
28218
|
+
const link = report.unknownEntries.find((entry) => entry.kind === "symlink");
|
|
28219
|
+
if (link === void 0) return;
|
|
28220
|
+
const error = gated("gate_unavailable");
|
|
28221
|
+
error.message = `Replica symlink ${JSON.stringify(link.path)} cannot be removed safely on this platform.`;
|
|
28222
|
+
throw error;
|
|
28223
|
+
}
|
|
28224
|
+
async function fileGate() {
|
|
28225
|
+
return import("@tinycloud/replica/profile-gate");
|
|
28226
|
+
}
|
|
28227
|
+
function gated(reason) {
|
|
28228
|
+
return Object.assign(new Error(`Replica gate refused: ${reason}`), { code: "REPLICA_GATED", gate: reason });
|
|
28229
|
+
}
|
|
28230
|
+
async function requiredGuard(guard, section) {
|
|
28231
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
28232
|
+
let entered = false;
|
|
28233
|
+
try {
|
|
28234
|
+
return await guard(async () => {
|
|
28235
|
+
entered = true;
|
|
28236
|
+
return section();
|
|
28237
|
+
});
|
|
28238
|
+
} catch (error) {
|
|
28239
|
+
if (!entered) throw gated("gate_unavailable");
|
|
28240
|
+
throw error;
|
|
28241
|
+
}
|
|
28242
|
+
}
|
|
27199
28243
|
var ReplicaStorageError = class extends Error {
|
|
27200
28244
|
constructor(code, message, options) {
|
|
27201
28245
|
super(message);
|
|
@@ -27221,6 +28265,19 @@ async function pathExists(path) {
|
|
|
27221
28265
|
throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
|
|
27222
28266
|
}
|
|
27223
28267
|
}
|
|
28268
|
+
async function realDirectoryExists(path) {
|
|
28269
|
+
let stats;
|
|
28270
|
+
try {
|
|
28271
|
+
stats = await lstat(path);
|
|
28272
|
+
} catch (error) {
|
|
28273
|
+
if (isNotFound(error)) return false;
|
|
28274
|
+
throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
|
|
28275
|
+
}
|
|
28276
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
28277
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Refusing the replaced replica path ${path}.`);
|
|
28278
|
+
}
|
|
28279
|
+
return true;
|
|
28280
|
+
}
|
|
27224
28281
|
function principalOf(did) {
|
|
27225
28282
|
return principalDid2(did);
|
|
27226
28283
|
}
|
|
@@ -27374,90 +28431,117 @@ function parsePendingState(text, file, identity) {
|
|
|
27374
28431
|
}
|
|
27375
28432
|
return state;
|
|
27376
28433
|
}
|
|
27377
|
-
var _dir, _guard, _dirSync,
|
|
28434
|
+
var _dir, _profileDir, _guard, _stamp, _dirSync, _logoutPendingFile, _generation, _canonicalDir, _FilePendingWriteStore_instances, withGate_fn, readFile_fn;
|
|
27378
28435
|
var FilePendingWriteStore = class {
|
|
27379
|
-
constructor(identity, dir, guard, dirSync) {
|
|
28436
|
+
constructor(identity, dir, profileDir, guard, stamp, dirSync, logoutPendingFile) {
|
|
27380
28437
|
__privateAdd(this, _FilePendingWriteStore_instances);
|
|
27381
28438
|
this.durable = true;
|
|
27382
28439
|
__privateAdd(this, _dir);
|
|
28440
|
+
__privateAdd(this, _profileDir);
|
|
27383
28441
|
__privateAdd(this, _guard);
|
|
28442
|
+
__privateAdd(this, _stamp);
|
|
27384
28443
|
__privateAdd(this, _dirSync);
|
|
27385
|
-
__privateAdd(this,
|
|
28444
|
+
__privateAdd(this, _logoutPendingFile);
|
|
28445
|
+
__privateAdd(this, _generation);
|
|
28446
|
+
__privateAdd(this, _canonicalDir);
|
|
27386
28447
|
this.identity = identity;
|
|
27387
28448
|
__privateSet(this, _dir, dir);
|
|
28449
|
+
__privateSet(this, _profileDir, profileDir);
|
|
27388
28450
|
__privateSet(this, _guard, guard);
|
|
28451
|
+
__privateSet(this, _stamp, stamp);
|
|
27389
28452
|
__privateSet(this, _dirSync, dirSync);
|
|
28453
|
+
__privateSet(this, _logoutPendingFile, logoutPendingFile);
|
|
27390
28454
|
}
|
|
27391
28455
|
async read() {
|
|
27392
|
-
|
|
27393
|
-
let text;
|
|
27394
|
-
try {
|
|
27395
|
-
text = await readFile(file, "utf8");
|
|
27396
|
-
} catch (error) {
|
|
27397
|
-
if (isNotFound(error)) {
|
|
27398
|
-
return { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
|
|
27399
|
-
}
|
|
27400
|
-
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
27401
|
-
}
|
|
27402
|
-
return parsePendingState(text, file, this.identity);
|
|
28456
|
+
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: [] });
|
|
27403
28457
|
}
|
|
27404
28458
|
async readExisting() {
|
|
27405
|
-
|
|
28459
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, (file) => __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file));
|
|
28460
|
+
}
|
|
28461
|
+
async update(mutate) {
|
|
28462
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => {
|
|
28463
|
+
await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
|
|
28464
|
+
const currentFile = join2(await realpath(__privateGet(this, _dir)), "pending.json");
|
|
28465
|
+
if (__privateGet(this, _canonicalDir) !== void 0 && dirname2(currentFile) !== __privateGet(this, _canonicalDir)) {
|
|
28466
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
|
|
28467
|
+
}
|
|
28468
|
+
const state = await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, currentFile) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
|
|
28469
|
+
const result = await mutate(state);
|
|
28470
|
+
await writeJsonAtomic(currentFile, state, __privateGet(this, _dirSync));
|
|
28471
|
+
return result;
|
|
28472
|
+
});
|
|
28473
|
+
}
|
|
28474
|
+
};
|
|
28475
|
+
_dir = new WeakMap();
|
|
28476
|
+
_profileDir = new WeakMap();
|
|
28477
|
+
_guard = new WeakMap();
|
|
28478
|
+
_stamp = new WeakMap();
|
|
28479
|
+
_dirSync = new WeakMap();
|
|
28480
|
+
_logoutPendingFile = new WeakMap();
|
|
28481
|
+
_generation = new WeakMap();
|
|
28482
|
+
_canonicalDir = new WeakMap();
|
|
28483
|
+
_FilePendingWriteStore_instances = new WeakSet();
|
|
28484
|
+
withGate_fn = async function(section) {
|
|
28485
|
+
if (__privateGet(this, _stamp) === void 0) throw gated("not_established");
|
|
28486
|
+
if (__privateGet(this, _guard) === void 0) throw gated("gate_unavailable");
|
|
28487
|
+
const gate = await fileGate();
|
|
28488
|
+
const snapshot = await gate.readGateSnapshot(__privateGet(this, _profileDir));
|
|
28489
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28490
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28491
|
+
gate.principalHashOf(snapshot.salt, this.identity.principal),
|
|
28492
|
+
gate.identityHashOf(snapshot.salt, replicationIdentityKey(this.identity))
|
|
28493
|
+
]);
|
|
28494
|
+
const access = {
|
|
28495
|
+
stamp: __privateGet(this, _stamp),
|
|
28496
|
+
incarnation: snapshot.incarnation,
|
|
28497
|
+
principalHash,
|
|
28498
|
+
identityKeyHash
|
|
28499
|
+
};
|
|
28500
|
+
let canonicalRoot;
|
|
28501
|
+
try {
|
|
28502
|
+
canonicalRoot = await realpath(dirname2(__privateGet(this, _dir)));
|
|
28503
|
+
} catch (error) {
|
|
28504
|
+
if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${dirname2(__privateGet(this, _dir))}`, error);
|
|
28505
|
+
canonicalRoot = join2(await realpath(dirname2(dirname2(__privateGet(this, _dir)))), basename(dirname2(__privateGet(this, _dir))));
|
|
28506
|
+
}
|
|
28507
|
+
const queueKey = join2(canonicalRoot, basename(__privateGet(this, _dir)), "pending.json");
|
|
28508
|
+
return serializePendingWrite(queueKey, () => requiredGuard(__privateGet(this, _guard), async () => {
|
|
28509
|
+
assertLogoutClear(__privateGet(this, _logoutPendingFile));
|
|
28510
|
+
const before = await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
|
|
28511
|
+
const generation = before.pendingGenerations[identityKeyHash] ?? 0;
|
|
28512
|
+
if (__privateGet(this, _generation) !== void 0 && __privateGet(this, _generation) !== generation) {
|
|
28513
|
+
throw new ReplicaStorageError("RESET_REQUIRED", "The pending identity was erased; reopen the store.");
|
|
28514
|
+
}
|
|
28515
|
+
let file = join2(__privateGet(this, _dir), "pending.json");
|
|
27406
28516
|
try {
|
|
27407
28517
|
file = join2(await realpath(__privateGet(this, _dir)), "pending.json");
|
|
28518
|
+
const recorded = identityRecordOf(await readJsonFile(join2(dirname2(file), "identity.json")));
|
|
28519
|
+
if (recorded === void 0 || !sameIdentity(recorded, this.identity)) {
|
|
28520
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending partition belongs to a different identity.");
|
|
28521
|
+
}
|
|
27408
28522
|
} catch (error) {
|
|
27409
|
-
if (
|
|
27410
|
-
throw storageError("STORAGE_ERROR", `Resolving
|
|
28523
|
+
if (error instanceof ReplicaStorageError) throw error;
|
|
28524
|
+
if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${__privateGet(this, _dir)}`, error);
|
|
27411
28525
|
}
|
|
27412
|
-
|
|
27413
|
-
|
|
27414
|
-
return parsePendingState(text, file, this.identity);
|
|
27415
|
-
} catch (error) {
|
|
27416
|
-
if (isNotFound(error)) return void 0;
|
|
27417
|
-
if (error instanceof ReplicaStorageError) throw error;
|
|
27418
|
-
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
28526
|
+
if (__privateGet(this, _canonicalDir) !== void 0 && dirname2(file) !== __privateGet(this, _canonicalDir)) {
|
|
28527
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
|
|
27419
28528
|
}
|
|
27420
|
-
|
|
27421
|
-
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
27427
|
-
} catch (error) {
|
|
27428
|
-
throw storageError("STORAGE_ERROR", "Resolving the open pending-write partition", error);
|
|
27429
|
-
}
|
|
27430
|
-
if (currentFile !== file) {
|
|
27431
|
-
throw new ReplicaStorageError(
|
|
27432
|
-
"STORAGE_ERROR",
|
|
27433
|
-
`The pending-write partition changed while open: ${dirname2(file)}.`
|
|
27434
|
-
);
|
|
27435
|
-
}
|
|
27436
|
-
return __privateGet(this, _guard).call(this, async () => {
|
|
27437
|
-
const state = await this.read();
|
|
27438
|
-
const result = mutate(state);
|
|
27439
|
-
await writeJsonAtomic(file, state, __privateGet(this, _dirSync));
|
|
27440
|
-
return result;
|
|
27441
|
-
});
|
|
27442
|
-
});
|
|
27443
|
-
}
|
|
28529
|
+
const result = await section(file);
|
|
28530
|
+
await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
|
|
28531
|
+
assertLogoutClear(__privateGet(this, _logoutPendingFile));
|
|
28532
|
+
__privateSet(this, _generation, generation);
|
|
28533
|
+
if (await pathExists(__privateGet(this, _dir))) __privateSet(this, _canonicalDir, await realpath(__privateGet(this, _dir)));
|
|
28534
|
+
return result;
|
|
28535
|
+
}));
|
|
27444
28536
|
};
|
|
27445
|
-
|
|
27446
|
-
|
|
27447
|
-
|
|
27448
|
-
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27453
|
-
const file = join2(__privateGet(this, _dir), "pending.json");
|
|
27454
|
-
try {
|
|
27455
|
-
return join2(await realpath(dirname2(file)), basename(file));
|
|
27456
|
-
} catch (error) {
|
|
27457
|
-
throw storageError("STORAGE_ERROR", `Resolving ${file}`, error);
|
|
27458
|
-
}
|
|
27459
|
-
})());
|
|
27460
|
-
return __privateGet(this, _file);
|
|
28537
|
+
readFile_fn = async function(file) {
|
|
28538
|
+
try {
|
|
28539
|
+
return parsePendingState(await readFile(file, "utf8"), file, this.identity);
|
|
28540
|
+
} catch (error) {
|
|
28541
|
+
if (isNotFound(error)) return void 0;
|
|
28542
|
+
if (error instanceof ReplicaStorageError) throw error;
|
|
28543
|
+
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
28544
|
+
}
|
|
27461
28545
|
};
|
|
27462
28546
|
async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
|
|
27463
28547
|
return guard(async () => {
|
|
@@ -27503,7 +28587,7 @@ async function readFence(replicaDir) {
|
|
|
27503
28587
|
const fence = await readJsonFile(join2(replicaDir, FENCE_FILE));
|
|
27504
28588
|
return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
|
|
27505
28589
|
}
|
|
27506
|
-
function newestGrantInfo(replica, record, pending, spec) {
|
|
28590
|
+
function newestGrantInfo(replica, record, pending, spec, install) {
|
|
27507
28591
|
if (record === null) return null;
|
|
27508
28592
|
const parsed = replica.parseUcanGrant(record.bytes);
|
|
27509
28593
|
return {
|
|
@@ -27511,10 +28595,11 @@ function newestGrantInfo(replica, record, pending, spec) {
|
|
|
27511
28595
|
parentCid: parsed.prf[0] ?? null,
|
|
27512
28596
|
expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
|
|
27513
28597
|
state: pending !== null && record === pending ? "pending" : "active",
|
|
27514
|
-
unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
|
|
28598
|
+
unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix),
|
|
28599
|
+
...install === null ? {} : install
|
|
27515
28600
|
};
|
|
27516
28601
|
}
|
|
27517
|
-
var _device, _store, _replicaDir, _replica, _guard2, _dirSync2,
|
|
28602
|
+
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;
|
|
27518
28603
|
var SqliteReplicaHandle = class {
|
|
27519
28604
|
constructor(input) {
|
|
27520
28605
|
__privateAdd(this, _SqliteReplicaHandle_instances);
|
|
@@ -27523,9 +28608,12 @@ var SqliteReplicaHandle = class {
|
|
|
27523
28608
|
__privateAdd(this, _replicaDir);
|
|
27524
28609
|
__privateAdd(this, _replica);
|
|
27525
28610
|
__privateAdd(this, _guard2);
|
|
28611
|
+
__privateAdd(this, _read);
|
|
28612
|
+
__privateAdd(this, _policy);
|
|
28613
|
+
__privateAdd(this, _recordGrantInstall);
|
|
27526
28614
|
__privateAdd(this, _dirSync2);
|
|
27527
28615
|
/** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
|
|
27528
|
-
__privateAdd(this,
|
|
28616
|
+
__privateAdd(this, _generation2);
|
|
27529
28617
|
/** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
|
|
27530
28618
|
__privateAdd(this, _ino);
|
|
27531
28619
|
__privateAdd(this, _transportOverride);
|
|
@@ -27533,64 +28621,76 @@ var SqliteReplicaHandle = class {
|
|
|
27533
28621
|
__privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
|
|
27534
28622
|
__privateAdd(this, _transportFactory);
|
|
27535
28623
|
this.spec = input.spec;
|
|
28624
|
+
this.replicaId = input.replicaId;
|
|
27536
28625
|
__privateSet(this, _device, input.device);
|
|
27537
28626
|
this.deviceDid = input.device.did;
|
|
27538
28627
|
__privateSet(this, _store, input.store);
|
|
27539
28628
|
__privateSet(this, _replicaDir, input.replicaDir);
|
|
27540
28629
|
__privateSet(this, _replica, input.replica);
|
|
27541
28630
|
__privateSet(this, _guard2, input.guard);
|
|
28631
|
+
__privateSet(this, _read, input.read);
|
|
28632
|
+
__privateSet(this, _policy, input.policy);
|
|
28633
|
+
__privateSet(this, _recordGrantInstall, input.recordGrantInstall);
|
|
27542
28634
|
__privateSet(this, _transportOverride, input.transportFor);
|
|
27543
28635
|
__privateSet(this, _dirSync2, input.dirSync);
|
|
27544
|
-
__privateSet(this,
|
|
28636
|
+
__privateSet(this, _generation2, input.generation);
|
|
27545
28637
|
__privateSet(this, _ino, input.ino);
|
|
27546
28638
|
}
|
|
27547
28639
|
async get(key2) {
|
|
27548
|
-
|
|
27549
|
-
|
|
27550
|
-
|
|
27551
|
-
|
|
27552
|
-
|
|
27553
|
-
|
|
27554
|
-
syncedThroughEpoch: await readFence(__privateGet(this, _replicaDir))
|
|
27555
|
-
};
|
|
27556
|
-
switch (result.status) {
|
|
27557
|
-
case "present":
|
|
27558
|
-
return {
|
|
27559
|
-
status: "present",
|
|
27560
|
-
key: result.key,
|
|
27561
|
-
value: result.value,
|
|
27562
|
-
etag: result.etag,
|
|
27563
|
-
metadata: result.metadata,
|
|
27564
|
-
meta
|
|
27565
|
-
};
|
|
27566
|
-
case "content_missing":
|
|
27567
|
-
return {
|
|
27568
|
-
status: "content_missing",
|
|
27569
|
-
key: result.key,
|
|
27570
|
-
etag: result.etag,
|
|
27571
|
-
metadata: result.metadata,
|
|
27572
|
-
meta
|
|
27573
|
-
};
|
|
27574
|
-
default:
|
|
27575
|
-
return { status: result.status, key: result.key, meta };
|
|
27576
|
-
}
|
|
27577
|
-
}
|
|
27578
|
-
async list(options) {
|
|
27579
|
-
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
27580
|
-
const result = await new (__privateGet(this, _replica)).Replica({ store: __privateGet(this, _store) }).list({
|
|
27581
|
-
prefix: options.prefix,
|
|
27582
|
-
...options.after === void 0 ? {} : { after: options.after },
|
|
27583
|
-
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
27584
|
-
});
|
|
27585
|
-
return {
|
|
27586
|
-
keys: result.entries.map((entry) => entry.key),
|
|
27587
|
-
meta: {
|
|
28640
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28641
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28642
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28643
|
+
const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).get(key2);
|
|
28644
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28645
|
+
const meta = {
|
|
27588
28646
|
asOf: result.meta.asOf,
|
|
27589
28647
|
coverage: result.meta.coverage,
|
|
27590
28648
|
authority: result.meta.authority,
|
|
27591
|
-
syncedThroughEpoch
|
|
28649
|
+
syncedThroughEpoch
|
|
28650
|
+
};
|
|
28651
|
+
switch (result.status) {
|
|
28652
|
+
case "present":
|
|
28653
|
+
return {
|
|
28654
|
+
status: "present",
|
|
28655
|
+
key: result.key,
|
|
28656
|
+
value: result.value,
|
|
28657
|
+
etag: result.etag,
|
|
28658
|
+
metadata: result.metadata,
|
|
28659
|
+
meta
|
|
28660
|
+
};
|
|
28661
|
+
case "content_missing":
|
|
28662
|
+
return {
|
|
28663
|
+
status: "content_missing",
|
|
28664
|
+
key: result.key,
|
|
28665
|
+
etag: result.etag,
|
|
28666
|
+
metadata: result.metadata,
|
|
28667
|
+
meta
|
|
28668
|
+
};
|
|
28669
|
+
default:
|
|
28670
|
+
return { status: result.status, key: result.key, meta };
|
|
27592
28671
|
}
|
|
27593
|
-
};
|
|
28672
|
+
}, true);
|
|
28673
|
+
}
|
|
28674
|
+
async list(options) {
|
|
28675
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28676
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28677
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28678
|
+
const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).list({
|
|
28679
|
+
prefix: options.prefix,
|
|
28680
|
+
...options.after === void 0 ? {} : { after: options.after },
|
|
28681
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
28682
|
+
});
|
|
28683
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28684
|
+
return {
|
|
28685
|
+
keys: result.entries.map((entry) => entry.key),
|
|
28686
|
+
meta: {
|
|
28687
|
+
asOf: result.meta.asOf,
|
|
28688
|
+
coverage: result.meta.coverage,
|
|
28689
|
+
authority: result.meta.authority,
|
|
28690
|
+
syncedThroughEpoch
|
|
28691
|
+
}
|
|
28692
|
+
};
|
|
28693
|
+
}, true);
|
|
27594
28694
|
}
|
|
27595
28695
|
/**
|
|
27596
28696
|
* The newest installed grant — the pending one when a not-yet-promoted
|
|
@@ -27600,43 +28700,59 @@ var SqliteReplicaHandle = class {
|
|
|
27600
28700
|
* grant is still reported (unconstrained: false) and never reused.
|
|
27601
28701
|
*/
|
|
27602
28702
|
async grant() {
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
|
|
27611
|
-
|
|
28703
|
+
return __privateGet(this, _read).call(this, async (snapshot) => {
|
|
28704
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28705
|
+
const state = await snapshot.open();
|
|
28706
|
+
if (state === null) return null;
|
|
28707
|
+
const selected = state.pendingGrant ?? state.grant;
|
|
28708
|
+
const install = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
|
|
28709
|
+
return newestGrantInfo(
|
|
28710
|
+
__privateGet(this, _replica),
|
|
28711
|
+
state.pendingGrant ?? state.grant,
|
|
28712
|
+
state.pendingGrant,
|
|
28713
|
+
this.spec,
|
|
28714
|
+
install
|
|
28715
|
+
);
|
|
28716
|
+
});
|
|
27612
28717
|
}
|
|
27613
28718
|
async installGrant(ucan) {
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
__privateGet(this, _replica).
|
|
27620
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
|
|
27624
|
-
|
|
27625
|
-
|
|
27626
|
-
|
|
27627
|
-
|
|
28719
|
+
return __privateGet(this, _guard2).call(this, async () => {
|
|
28720
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
28721
|
+
const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
|
|
28722
|
+
const state = await __privateGet(this, _store).open();
|
|
28723
|
+
if (state === null) {
|
|
28724
|
+
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
28725
|
+
__privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
|
|
28726
|
+
"The replica has not been created."
|
|
28727
|
+
);
|
|
28728
|
+
}
|
|
28729
|
+
__privateGet(this, _replica).assertGrantInstallable(parsed, {
|
|
28730
|
+
deviceDid: principalOf(__privateGet(this, _device).did),
|
|
28731
|
+
space: this.spec.space,
|
|
28732
|
+
prefix: this.spec.prefix,
|
|
28733
|
+
now: Date.now()
|
|
28734
|
+
});
|
|
28735
|
+
if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
|
|
28736
|
+
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
28737
|
+
__privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
28738
|
+
"The grant's get or sync coverage carries caveats; re-mint instead of installing it."
|
|
28739
|
+
);
|
|
28740
|
+
}
|
|
28741
|
+
const now = Date.now();
|
|
28742
|
+
const policy = await __privateGet(this, _policy).call(this, now, true);
|
|
28743
|
+
if (policy === null || now >= policy.consentDeadline) {
|
|
28744
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The replica's consent window has expired.");
|
|
28745
|
+
}
|
|
28746
|
+
const installedAt = await __privateGet(this, _recordGrantInstall).call(this, parsed.cid, now);
|
|
28747
|
+
const installed = await __privateGet(this, _store).installGatedGrant(parsed, { ...policy, at: installedAt });
|
|
28748
|
+
return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec, installed);
|
|
27628
28749
|
});
|
|
27629
|
-
if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
|
|
27630
|
-
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
27631
|
-
__privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
27632
|
-
"The grant's get or sync coverage carries caveats; re-mint instead of installing it."
|
|
27633
|
-
);
|
|
27634
|
-
}
|
|
27635
|
-
await __privateGet(this, _store).installGrant(parsed);
|
|
27636
|
-
return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec);
|
|
27637
28750
|
}
|
|
27638
28751
|
async sync(options) {
|
|
27639
|
-
await
|
|
28752
|
+
await __privateGet(this, _guard2).call(this, async () => {
|
|
28753
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
28754
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertSyncEffective_fn).call(this);
|
|
28755
|
+
});
|
|
27640
28756
|
const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
|
|
27641
28757
|
const abort = new AbortController();
|
|
27642
28758
|
options.signal.throwIfAborted();
|
|
@@ -27677,27 +28793,33 @@ var SqliteReplicaHandle = class {
|
|
|
27677
28793
|
}
|
|
27678
28794
|
}
|
|
27679
28795
|
async status() {
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
state
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27700
|
-
|
|
28796
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28797
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28798
|
+
const engine = new (__privateGet(this, _replica)).Replica({ store: snapshot });
|
|
28799
|
+
const status = await engine.status();
|
|
28800
|
+
const state = await snapshot.open();
|
|
28801
|
+
const selected = state?.pendingGrant ?? state?.grant ?? null;
|
|
28802
|
+
const selectedInstall = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
|
|
28803
|
+
const activeRecord = state?.grant ?? null;
|
|
28804
|
+
const activeInstall = activeRecord === null ? null : await snapshot.getGrantInstall(activeRecord.cid);
|
|
28805
|
+
const grant = state === null ? null : newestGrantInfo(
|
|
28806
|
+
__privateGet(this, _replica),
|
|
28807
|
+
state.pendingGrant ?? state.grant,
|
|
28808
|
+
state.pendingGrant,
|
|
28809
|
+
this.spec,
|
|
28810
|
+
selectedInstall
|
|
28811
|
+
);
|
|
28812
|
+
return {
|
|
28813
|
+
coverage: status.coverage,
|
|
28814
|
+
lastSyncAt: status.lastSyncAt,
|
|
28815
|
+
syncedThroughEpoch,
|
|
28816
|
+
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 },
|
|
28817
|
+
grant,
|
|
28818
|
+
counts: status.counts,
|
|
28819
|
+
bytes: status.bytes,
|
|
28820
|
+
lastError: status.lastError
|
|
28821
|
+
};
|
|
28822
|
+
}, true);
|
|
27701
28823
|
}
|
|
27702
28824
|
async close() {
|
|
27703
28825
|
if (__privateGet(this, _closed)) return;
|
|
@@ -27712,8 +28834,11 @@ _store = new WeakMap();
|
|
|
27712
28834
|
_replicaDir = new WeakMap();
|
|
27713
28835
|
_replica = new WeakMap();
|
|
27714
28836
|
_guard2 = new WeakMap();
|
|
28837
|
+
_read = new WeakMap();
|
|
28838
|
+
_policy = new WeakMap();
|
|
28839
|
+
_recordGrantInstall = new WeakMap();
|
|
27715
28840
|
_dirSync2 = new WeakMap();
|
|
27716
|
-
|
|
28841
|
+
_generation2 = new WeakMap();
|
|
27717
28842
|
_ino = new WeakMap();
|
|
27718
28843
|
_transportOverride = new WeakMap();
|
|
27719
28844
|
_closed = new WeakMap();
|
|
@@ -27748,7 +28873,7 @@ assertLive_fn = async function() {
|
|
|
27748
28873
|
throw error;
|
|
27749
28874
|
}
|
|
27750
28875
|
const state = await __privateGet(this, _store).open();
|
|
27751
|
-
if (state !== null && state.generation !== __privateGet(this,
|
|
28876
|
+
if (state !== null && state.generation !== __privateGet(this, _generation2)) {
|
|
27752
28877
|
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
27753
28878
|
__privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
|
|
27754
28879
|
"The replica was purged or reopened; open it again to continue."
|
|
@@ -27790,97 +28915,1099 @@ transports_fn = async function() {
|
|
|
27790
28915
|
})());
|
|
27791
28916
|
return __privateGet(this, _transportFactory);
|
|
27792
28917
|
};
|
|
28918
|
+
assertEffective_fn = async function(snapshot) {
|
|
28919
|
+
const state = await snapshot.open();
|
|
28920
|
+
if (state?.grant === void 0 || state.grant === null) return;
|
|
28921
|
+
const installed = await snapshot.getGrantInstall(state.grant.cid);
|
|
28922
|
+
if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || Date.now() >= installed.effectiveExpiresAt || Date.now() >= installed.consentDeadline) {
|
|
28923
|
+
throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
|
|
28924
|
+
}
|
|
28925
|
+
};
|
|
28926
|
+
assertSyncEffective_fn = async function() {
|
|
28927
|
+
const state = await __privateGet(this, _store).open();
|
|
28928
|
+
const selected = state?.pendingGrant ?? state?.grant;
|
|
28929
|
+
if (selected === void 0 || selected === null) return;
|
|
28930
|
+
const installed = await __privateGet(this, _store).getGrantInstall(selected.cid);
|
|
28931
|
+
const now = Date.now();
|
|
28932
|
+
if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || now >= installed.effectiveExpiresAt || now >= installed.consentDeadline) {
|
|
28933
|
+
throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
|
|
28934
|
+
}
|
|
28935
|
+
};
|
|
27793
28936
|
function createSqliteReplicaStorage(options) {
|
|
27794
28937
|
const root = options.dir;
|
|
27795
|
-
const
|
|
28938
|
+
const profileDir = dirname2(options.gateFile ?? join2(dirname2(root), REPLICA_GATE_FILE));
|
|
28939
|
+
const diagnosticFiles = [...options.diagnosticFiles ?? []];
|
|
28940
|
+
for (const name of diagnosticFiles) {
|
|
28941
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || /^[a-z2-7]{26}$/.test(name)) {
|
|
28942
|
+
throw new TypeError(`Invalid replication diagnostics file name ${JSON.stringify(name)}.`);
|
|
28943
|
+
}
|
|
28944
|
+
}
|
|
28945
|
+
const inspectLayout = (gateReadable, approvedMarker, legacyOnly = false) => inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly, diagnosticFiles);
|
|
28946
|
+
const guard = options.guard;
|
|
27796
28947
|
const createDevice = options.createDevice ?? createDeviceKey;
|
|
27797
|
-
|
|
27798
|
-
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
28948
|
+
const heldLock = new AsyncLocalStorage();
|
|
28949
|
+
let visibleStamp;
|
|
28950
|
+
let visiblePrincipalHash;
|
|
28951
|
+
let checkedGate;
|
|
28952
|
+
async function profileSection(section) {
|
|
28953
|
+
if (heldLock.getStore() === true) return section();
|
|
28954
|
+
return requiredGuard(guard, () => heldLock.run(true, section));
|
|
28955
|
+
}
|
|
28956
|
+
async function guarded(principal, stamp, identity, partitionId, section) {
|
|
28957
|
+
if (stamp === void 0) throw gated("not_established");
|
|
28958
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
28959
|
+
const gate = await fileGate();
|
|
28960
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
28961
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28962
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28963
|
+
gate.principalHashOf(snapshot.salt, principal),
|
|
28964
|
+
identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, replicationIdentityKey(identity))
|
|
27803
28965
|
]);
|
|
27804
|
-
|
|
27805
|
-
|
|
27806
|
-
|
|
27807
|
-
|
|
27808
|
-
|
|
27809
|
-
|
|
27810
|
-
|
|
27811
|
-
|
|
27812
|
-
|
|
27813
|
-
|
|
27814
|
-
|
|
27815
|
-
|
|
27816
|
-
const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
|
|
27817
|
-
create: true,
|
|
27818
|
-
guard
|
|
28966
|
+
return profileSection(async () => {
|
|
28967
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28968
|
+
await gate.assertGateAccess(profileDir, {
|
|
28969
|
+
stamp,
|
|
28970
|
+
incarnation: snapshot.incarnation,
|
|
28971
|
+
principalHash,
|
|
28972
|
+
identityKeyHash,
|
|
28973
|
+
partitionId
|
|
28974
|
+
}, snapshot.salt);
|
|
28975
|
+
const result = await section();
|
|
28976
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28977
|
+
return result;
|
|
27819
28978
|
});
|
|
27820
|
-
|
|
28979
|
+
}
|
|
28980
|
+
async function readGated(spec, partitionId, store, replicaDir, generation, section, withFence = false) {
|
|
28981
|
+
if (spec.stamp === void 0) throw gated("not_established");
|
|
28982
|
+
const gate = await fileGate();
|
|
28983
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
28984
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28985
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28986
|
+
gate.principalHashOf(snapshot.salt, spec.identity.principal),
|
|
28987
|
+
gate.identityHashOf(snapshot.salt, replicationIdentityKey(spec.identity))
|
|
28988
|
+
]);
|
|
28989
|
+
const access = {
|
|
28990
|
+
stamp: spec.stamp,
|
|
28991
|
+
incarnation: snapshot.incarnation,
|
|
28992
|
+
principalHash,
|
|
28993
|
+
identityKeyHash,
|
|
28994
|
+
partitionId
|
|
28995
|
+
};
|
|
28996
|
+
const path = options.gateFile ?? join2(profileDir, REPLICA_GATE_FILE);
|
|
28997
|
+
let checked;
|
|
28998
|
+
const checkGate = async () => {
|
|
28999
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29000
|
+
let before;
|
|
29001
|
+
try {
|
|
29002
|
+
before = await stat(path, { bigint: true });
|
|
29003
|
+
} catch {
|
|
29004
|
+
throw gated("gate_unavailable");
|
|
29005
|
+
}
|
|
29006
|
+
if (checked?.ino === before.ino && checked.size === before.size && checked.mtimeNs === before.mtimeNs) return;
|
|
29007
|
+
const checkedState = await gate.assertGateAccess(profileDir, access, snapshot.salt);
|
|
29008
|
+
if (spec.grantPolicy?.consentAnchor === null && checkedState.firstInstall[identityKeyHash] === void 0) {
|
|
29009
|
+
const installed = await store.open();
|
|
29010
|
+
if (installed !== null && (installed.grant !== null || installed.pendingGrant !== null)) throw gated("gate_unavailable");
|
|
29011
|
+
}
|
|
29012
|
+
let after;
|
|
29013
|
+
try {
|
|
29014
|
+
after = await stat(path, { bigint: true });
|
|
29015
|
+
} catch {
|
|
29016
|
+
throw gated("gate_unavailable");
|
|
29017
|
+
}
|
|
29018
|
+
if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
|
|
29019
|
+
throw gated("gate_unavailable");
|
|
29020
|
+
}
|
|
29021
|
+
checked = after;
|
|
29022
|
+
};
|
|
29023
|
+
if (withFence) await checkGate();
|
|
29024
|
+
const fence = withFence ? await readFence(replicaDir) : 0;
|
|
29025
|
+
return store.readTransaction(generation, checkGate, (sqliteSnapshot) => section(sqliteSnapshot, fence));
|
|
29026
|
+
}
|
|
29027
|
+
async function identityDirs() {
|
|
29028
|
+
let entries;
|
|
29029
|
+
try {
|
|
29030
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
29031
|
+
} catch (error) {
|
|
29032
|
+
if (isNotFound(error)) return [];
|
|
29033
|
+
throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
|
|
29034
|
+
}
|
|
29035
|
+
try {
|
|
29036
|
+
const marker = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29037
|
+
if (marker !== null) {
|
|
29038
|
+
const inventory = await inspectLayout(true);
|
|
29039
|
+
assertApprovedInventory(inventory, {
|
|
29040
|
+
unknownEntries: marker.approvedUnknown,
|
|
29041
|
+
knownRoots: marker.knownRoots,
|
|
29042
|
+
knownEntries: marker.knownEntries,
|
|
29043
|
+
missingDatabases: marker.missingDatabases
|
|
29044
|
+
}, true);
|
|
29045
|
+
}
|
|
29046
|
+
for (const entry of entries) {
|
|
29047
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
29048
|
+
if (marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && (entry.isDirectory() ? item.kind === "directory" : entry.isSymbolicLink() ? item.kind === "symlink" : item.kind === "file"))) continue;
|
|
29049
|
+
if (diagnosticFiles.includes(entry.name)) {
|
|
29050
|
+
const info = await lstat(join2(root, entry.name)).catch((error) => {
|
|
29051
|
+
if (isNotFound(error)) return null;
|
|
29052
|
+
throw error;
|
|
29053
|
+
});
|
|
29054
|
+
if (info === null || info.isFile()) continue;
|
|
29055
|
+
}
|
|
29056
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Unknown identity partition at ${root}.`);
|
|
29057
|
+
}
|
|
29058
|
+
}
|
|
29059
|
+
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) => join2(root, entry.name));
|
|
29060
|
+
} catch (error) {
|
|
29061
|
+
throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
|
|
29062
|
+
}
|
|
29063
|
+
}
|
|
29064
|
+
async function collectIntents(principalHash, salt) {
|
|
29065
|
+
const gate = await fileGate();
|
|
29066
|
+
const intents = [];
|
|
29067
|
+
for (const idDir of await identityDirs()) {
|
|
29068
|
+
const identity = identityRecordOf(await readJsonFile(join2(idDir, "identity.json")));
|
|
29069
|
+
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
|
|
29070
|
+
if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
|
|
29071
|
+
const identityKeyHash = await gate.identityHashOf(salt, replicationIdentityKey(identity));
|
|
29072
|
+
const recordedAt = Date.now();
|
|
29073
|
+
if (await pathExists(join2(idDir, "pending.json")) || await pathExists(join2(idDir, "device.jwk"))) {
|
|
29074
|
+
intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
|
|
29075
|
+
}
|
|
29076
|
+
const replicasDir = join2(idDir, "replicas");
|
|
29077
|
+
let entries;
|
|
29078
|
+
try {
|
|
29079
|
+
entries = await readdir(replicasDir, { withFileTypes: true });
|
|
29080
|
+
} catch (error) {
|
|
29081
|
+
if (isNotFound(error)) continue;
|
|
29082
|
+
throw storageError("STORAGE_ERROR", `Listing ${replicasDir}`, error);
|
|
29083
|
+
}
|
|
29084
|
+
for (const entry of entries) {
|
|
29085
|
+
if (!entry.isDirectory()) continue;
|
|
29086
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
29087
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Unrecognized replica partition at ${replicasDir}.`);
|
|
29088
|
+
}
|
|
29089
|
+
const partitionId = `${identityKeyHash}:${entry.name}`;
|
|
29090
|
+
intents.push({ id: partitionId, kind: "partition", partitionId, principalHash, recordedAt });
|
|
29091
|
+
}
|
|
29092
|
+
}
|
|
29093
|
+
return intents;
|
|
29094
|
+
}
|
|
29095
|
+
async function retainGrantInstalls(gate, gateState, identityKeyHash, store) {
|
|
29096
|
+
for (const { cid, installedAt } of await store.grantInstallTimes()) {
|
|
29097
|
+
const key2 = await gate.grantInstallHashOf(gateState.salt, identityKeyHash, cid);
|
|
29098
|
+
await gate.recordGrantInstall(profileDir, key2, installedAt);
|
|
29099
|
+
}
|
|
29100
|
+
}
|
|
29101
|
+
async function removeCorruptOrphanGate(gate) {
|
|
29102
|
+
const path = join2(profileDir, REPLICA_GATE_FILE);
|
|
29103
|
+
const before = await lstat(path, { bigint: true });
|
|
29104
|
+
if (!before.isFile() || before.isSymbolicLink()) throw gated("gate_unavailable");
|
|
29105
|
+
let corrupt = false;
|
|
29106
|
+
try {
|
|
29107
|
+
await gate.readGateSnapshot(profileDir);
|
|
29108
|
+
} catch (error) {
|
|
29109
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
|
|
29110
|
+
corrupt = true;
|
|
29111
|
+
} else throw error;
|
|
29112
|
+
}
|
|
29113
|
+
if (!corrupt) throw gated("stamp_stale");
|
|
29114
|
+
const current = await lstat(path, { bigint: true });
|
|
29115
|
+
if (current.dev !== before.dev || current.ino !== before.ino || current.mtimeNs !== before.mtimeNs || current.size !== before.size) throw gated("stamp_stale");
|
|
29116
|
+
await rm(path);
|
|
29117
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29118
|
+
if (await gate.readGateSnapshot(profileDir) !== void 0) throw gated("stamp_stale");
|
|
29119
|
+
}
|
|
29120
|
+
async function readLogoutState(gate) {
|
|
27821
29121
|
try {
|
|
29122
|
+
return { read: await gate.readLogoutGate(profileDir), corrupt: false };
|
|
29123
|
+
} catch (error) {
|
|
29124
|
+
if (isGateUnavailable(error)) return { read: void 0, corrupt: true };
|
|
29125
|
+
throw error;
|
|
29126
|
+
}
|
|
29127
|
+
}
|
|
29128
|
+
async function validateV1(gate, path, marker, read, mode) {
|
|
29129
|
+
if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
|
|
29130
|
+
const { state, floorLagging } = read;
|
|
29131
|
+
if (marker.deniedEpoch !== void 0) {
|
|
29132
|
+
if (floorLagging || !v1Denied(marker, state, marker.deniedEpoch)) throw restoredGate();
|
|
29133
|
+
return { marker, state, active: false };
|
|
29134
|
+
}
|
|
29135
|
+
if (!floorLagging && state.principalHash === null && state.lifecycleEpoch > marker.lifecycleEpoch && (marker.remotePending ? state.eraseDeferred && state.deferredPrincipalHash === marker.principalHash : !state.eraseDeferred && state.deferredPrincipalHash === null)) {
|
|
29136
|
+
return { marker: await bindDeniedEpoch(path, marker, state.lifecycleEpoch), state, active: false };
|
|
29137
|
+
}
|
|
29138
|
+
if (state.principalHash !== marker.principalHash || state.lifecycleEpoch !== marker.lifecycleEpoch) {
|
|
29139
|
+
throw gated("stamp_stale");
|
|
29140
|
+
}
|
|
29141
|
+
if (!floorLagging && !mode.denyActive) {
|
|
29142
|
+
if (!mode.active) throw gated("stamp_stale");
|
|
29143
|
+
return { marker, state, active: true };
|
|
29144
|
+
}
|
|
29145
|
+
if (mode.acceptLagging) return { marker, state, active: true };
|
|
29146
|
+
if (mode.deny === void 0) throw interruptedSignOut();
|
|
29147
|
+
const denied = await gate.denyGate(
|
|
29148
|
+
profileDir,
|
|
29149
|
+
{ reason: "sign_out", principal: "" },
|
|
29150
|
+
(principalHash) => collectIntents(principalHash, state.salt),
|
|
29151
|
+
{ salt: state.salt, principalHash: marker.principalHash },
|
|
29152
|
+
{ deferErase: mode.deny === "deferred" }
|
|
29153
|
+
);
|
|
29154
|
+
if (denied.status !== "denied") throw gated("gate_unavailable");
|
|
29155
|
+
const after = await gate.readLogoutGate(profileDir);
|
|
29156
|
+
if (after === void 0 || after.floorLagging || !v1Denied(marker, after.state, denied.lifecycleEpoch)) {
|
|
29157
|
+
throw gated("gate_unavailable");
|
|
29158
|
+
}
|
|
29159
|
+
return { marker: await bindDeniedEpoch(path, marker, denied.lifecycleEpoch), state: after.state, active: false };
|
|
29160
|
+
}
|
|
29161
|
+
function v1Denied(marker, state, epoch) {
|
|
29162
|
+
return state.incarnation === marker.incarnation && state.principalHash === null && state.lifecycleEpoch === epoch && (state.deferredPrincipalHash === marker.principalHash || !marker.remotePending && !state.eraseDeferred && state.deferredPrincipalHash === null);
|
|
29163
|
+
}
|
|
29164
|
+
async function bindDeniedEpoch(path, marker, deniedEpoch) {
|
|
29165
|
+
if (marker.deniedEpoch !== void 0) throw gated("stamp_stale");
|
|
29166
|
+
const bound = { ...marker, deniedEpoch };
|
|
29167
|
+
await writeJsonAtomic(path, bound, options.dirSync);
|
|
29168
|
+
return bound;
|
|
29169
|
+
}
|
|
29170
|
+
async function validatedRemoteMarker(path, marker, principal, mode) {
|
|
29171
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29172
|
+
const gate = await fileGate();
|
|
29173
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29174
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29175
|
+
if (marker.v === 2) {
|
|
29176
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29177
|
+
return { marker, state: void 0 };
|
|
29178
|
+
}
|
|
29179
|
+
if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
|
|
29180
|
+
if (marker.v === 3) {
|
|
29181
|
+
if (read.floorLagging || read.state.lifecycleEpoch !== marker.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
|
|
29182
|
+
return { marker, state: read.state };
|
|
29183
|
+
}
|
|
29184
|
+
if (marker.principalHash !== await gate.principalHashOf(read.state.salt, principal)) {
|
|
29185
|
+
throw gated("principal_mismatch");
|
|
29186
|
+
}
|
|
29187
|
+
const checked = await validateV1(gate, path, marker, read, mode);
|
|
29188
|
+
return { marker: checked.marker, state: checked.state };
|
|
29189
|
+
}
|
|
29190
|
+
async function currentLayout() {
|
|
29191
|
+
const { read, corrupt } = await readLogoutState(await fileGate());
|
|
29192
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29193
|
+
const report = await inspectLayout(read !== void 0);
|
|
29194
|
+
assertSupportedProfileLayout(report);
|
|
29195
|
+
return report;
|
|
29196
|
+
}
|
|
29197
|
+
async function commitPlainAbandon(path, marker, report, discardLocalChanges) {
|
|
29198
|
+
const { remotePending: _, discardLocalChanges: _oldDiscard, ...previous } = marker;
|
|
29199
|
+
const abandoned = {
|
|
29200
|
+
...previous,
|
|
29201
|
+
remoteAbandoned: true,
|
|
29202
|
+
approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
|
|
29203
|
+
knownRoots: report.knownRoots,
|
|
29204
|
+
knownEntries: report.knownEntries,
|
|
29205
|
+
missingDatabases: report.missingDatabases,
|
|
29206
|
+
...discardLocalChanges ? { discardLocalChanges: true } : {}
|
|
29207
|
+
};
|
|
29208
|
+
await writeJsonAtomic(path, abandoned, options.dirSync);
|
|
29209
|
+
return abandoned;
|
|
29210
|
+
}
|
|
29211
|
+
function deny(input, denyOptions) {
|
|
29212
|
+
const deferErase = denyOptions?.deferErase === true;
|
|
29213
|
+
if (deferErase && input.reason !== "sign_out") {
|
|
29214
|
+
throw new TypeError("Deferred erase is only available for sign-out.");
|
|
29215
|
+
}
|
|
29216
|
+
visibleStamp = void 0;
|
|
29217
|
+
visiblePrincipalHash = void 0;
|
|
29218
|
+
checkedGate = void 0;
|
|
29219
|
+
return (async () => {
|
|
27822
29220
|
try {
|
|
27823
|
-
await
|
|
27824
|
-
|
|
27825
|
-
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
|
|
29221
|
+
const gate = await fileGate();
|
|
29222
|
+
return await profileSection(async () => {
|
|
29223
|
+
const snapshot = (await gate.readLogoutGate(profileDir))?.state;
|
|
29224
|
+
const precomputed = snapshot === void 0 ? void 0 : {
|
|
29225
|
+
salt: snapshot.salt,
|
|
29226
|
+
principalHash: await gate.principalHashOf(snapshot.salt, input.principal)
|
|
29227
|
+
};
|
|
29228
|
+
const result = await gate.denyGate(
|
|
29229
|
+
profileDir,
|
|
29230
|
+
input,
|
|
29231
|
+
async (principalHash) => {
|
|
29232
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29233
|
+
return collectIntents(principalHash, snapshot.salt);
|
|
29234
|
+
},
|
|
29235
|
+
precomputed,
|
|
29236
|
+
{ deferErase }
|
|
29237
|
+
);
|
|
29238
|
+
if (result.status !== "denied") return result;
|
|
29239
|
+
if (input.reason === "sign_out" && snapshot !== void 0 && precomputed !== void 0) {
|
|
29240
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29241
|
+
const marker = await readLogoutMarker(path);
|
|
29242
|
+
if (marker?.v === 1 && marker.deniedEpoch === void 0 && marker.incarnation === snapshot.incarnation && marker.principalHash === precomputed.principalHash && marker.remotePending === true === deferErase) {
|
|
29243
|
+
await bindDeniedEpoch(path, marker, result.lifecycleEpoch);
|
|
29244
|
+
}
|
|
29245
|
+
}
|
|
29246
|
+
if (!deferErase) return result;
|
|
29247
|
+
const resumeToken = await gate.deferredGateToken(profileDir);
|
|
29248
|
+
if (resumeToken === null || resumeToken.deniedEpoch !== result.lifecycleEpoch) {
|
|
29249
|
+
return { status: "unguarded", code: "gate_unavailable" };
|
|
29250
|
+
}
|
|
29251
|
+
return { ...result, resumeToken };
|
|
27833
29252
|
});
|
|
27834
29253
|
} catch (error) {
|
|
27835
|
-
|
|
27836
|
-
|
|
29254
|
+
return { status: "unguarded", code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable" };
|
|
29255
|
+
}
|
|
29256
|
+
})();
|
|
29257
|
+
}
|
|
29258
|
+
const lifecycle = {
|
|
29259
|
+
kind: "file",
|
|
29260
|
+
async recover({ signal }) {
|
|
29261
|
+
try {
|
|
29262
|
+
return await lifecycle.erasePending({ signal });
|
|
29263
|
+
} catch (error) {
|
|
29264
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR";
|
|
29265
|
+
let remaining = 0;
|
|
29266
|
+
try {
|
|
29267
|
+
remaining = (await (await fileGate()).readGateSnapshot(profileDir))?.intents.length ?? 0;
|
|
29268
|
+
} catch {
|
|
27837
29269
|
}
|
|
29270
|
+
return { erased: [], failed: [{ id: "gate", code }], remaining };
|
|
27838
29271
|
}
|
|
27839
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
|
|
29272
|
+
},
|
|
29273
|
+
async establishSessionStamp(principal, { consentAnchor }) {
|
|
29274
|
+
if (consentAnchor?.source === "first_install" || !Number.isSafeInteger(consentAnchor?.at) || consentAnchor.at < 0) {
|
|
29275
|
+
throw gated("not_established");
|
|
29276
|
+
}
|
|
29277
|
+
const gate = await fileGate();
|
|
29278
|
+
const stamp = await profileSection(async () => {
|
|
29279
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29280
|
+
return gate.establishSessionStamp(profileDir, principal, { siweIssuedAt: consentAnchor.at });
|
|
29281
|
+
});
|
|
29282
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29283
|
+
if (state === void 0) throw gated("gate_unavailable");
|
|
29284
|
+
visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
|
|
29285
|
+
checkedGate = void 0;
|
|
29286
|
+
visibleStamp = stamp;
|
|
29287
|
+
return stamp;
|
|
29288
|
+
},
|
|
29289
|
+
async restoreSessionStamp(principal, persisted) {
|
|
29290
|
+
const gate = await fileGate();
|
|
29291
|
+
const stamp = await profileSection(async () => {
|
|
29292
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29293
|
+
return gate.restoreSessionStamp(profileDir, principal, persisted);
|
|
29294
|
+
});
|
|
29295
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29296
|
+
if (state === void 0) throw gated("gate_unavailable");
|
|
29297
|
+
visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
|
|
29298
|
+
checkedGate = void 0;
|
|
29299
|
+
visibleStamp = stamp;
|
|
29300
|
+
return stamp;
|
|
29301
|
+
},
|
|
29302
|
+
async activationAllowed(next) {
|
|
29303
|
+
if (guard === void 0) return false;
|
|
29304
|
+
try {
|
|
29305
|
+
const gate = await fileGate();
|
|
29306
|
+
return await profileSection(async () => {
|
|
29307
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29308
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29309
|
+
return state !== void 0 && !state.eraseDeferred && state.intents.length === 0 && state.principalHash !== null && state.principalHash === await gate.principalHashOf(state.salt, next);
|
|
29310
|
+
});
|
|
29311
|
+
} catch {
|
|
29312
|
+
return false;
|
|
27845
29313
|
}
|
|
27846
|
-
|
|
27847
|
-
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
29314
|
+
},
|
|
29315
|
+
checkDelivery(stamp) {
|
|
29316
|
+
if (guard === void 0 || visiblePrincipalHash === void 0) return { status: "gated", gate: "gate_unavailable" };
|
|
29317
|
+
try {
|
|
29318
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29319
|
+
} catch {
|
|
29320
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
27852
29321
|
}
|
|
27853
|
-
if (
|
|
27854
|
-
|
|
27855
|
-
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
27856
|
-
"The stored replica belongs to a different device key; purge the partition to rekey."
|
|
27857
|
-
);
|
|
29322
|
+
if (!stamp || visibleStamp?.incarnation !== stamp.incarnation || visibleStamp.lifecycleEpoch !== stamp.lifecycleEpoch) {
|
|
29323
|
+
return { status: "gated", gate: "stamp_stale" };
|
|
27858
29324
|
}
|
|
27859
|
-
|
|
27860
|
-
|
|
27861
|
-
|
|
27862
|
-
|
|
29325
|
+
try {
|
|
29326
|
+
const path = options.gateFile ?? join2(profileDir, REPLICA_GATE_FILE);
|
|
29327
|
+
const stats = statSync(path, { bigint: true });
|
|
29328
|
+
if (checkedGate === void 0 || checkedGate.ino !== stats.ino || checkedGate.mtimeNs !== stats.mtimeNs || checkedGate.size !== stats.size) {
|
|
29329
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
29330
|
+
if (typeof raw.incarnation !== "string" || !Number.isSafeInteger(raw.lifecycleEpoch) || raw.principalHash !== null && typeof raw.principalHash !== "string" || !Array.isArray(raw.intents)) {
|
|
29331
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
29332
|
+
}
|
|
29333
|
+
checkedGate = {
|
|
29334
|
+
ino: stats.ino,
|
|
29335
|
+
mtimeNs: stats.mtimeNs,
|
|
29336
|
+
size: stats.size,
|
|
29337
|
+
incarnation: raw.incarnation,
|
|
29338
|
+
epoch: raw.lifecycleEpoch,
|
|
29339
|
+
principalHash: raw.principalHash
|
|
29340
|
+
};
|
|
29341
|
+
}
|
|
29342
|
+
} catch {
|
|
29343
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
29344
|
+
}
|
|
29345
|
+
if (checkedGate.incarnation !== stamp.incarnation || checkedGate.epoch !== stamp.lifecycleEpoch) {
|
|
29346
|
+
return { status: "gated", gate: "stamp_stale" };
|
|
29347
|
+
}
|
|
29348
|
+
return checkedGate.principalHash === visiblePrincipalHash ? { status: "allow" } : { status: "gated", gate: "principal_mismatch" };
|
|
29349
|
+
},
|
|
29350
|
+
consentDeadline() {
|
|
29351
|
+
return null;
|
|
29352
|
+
},
|
|
29353
|
+
deliverable(stamp) {
|
|
29354
|
+
return lifecycle.checkDelivery(stamp).status === "allow";
|
|
29355
|
+
},
|
|
29356
|
+
deny,
|
|
29357
|
+
deferredErase() {
|
|
29358
|
+
return profileSection(async () => (await fileGate()).deferredGateToken(profileDir));
|
|
29359
|
+
},
|
|
29360
|
+
async resumeDeferredErase(token, { signal }) {
|
|
29361
|
+
signal.throwIfAborted();
|
|
29362
|
+
await profileSection(async () => (await fileGate()).resumeDeferredGate(profileDir, token));
|
|
29363
|
+
return lifecycle.erasePending({ signal });
|
|
29364
|
+
},
|
|
29365
|
+
async preflightLogout({ principal, discardLocalChanges = false }) {
|
|
29366
|
+
return profileSection(async () => {
|
|
29367
|
+
const marker = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29368
|
+
const gate = await fileGate();
|
|
29369
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29370
|
+
const state = read?.state;
|
|
29371
|
+
let otherAccount = false;
|
|
29372
|
+
if (state !== void 0) {
|
|
29373
|
+
const hash3 = await gate.principalHashOf(state.salt, principal);
|
|
29374
|
+
otherAccount = gateOwnedByAnother(state, hash3);
|
|
29375
|
+
if (marker?.v === 1 && marker.principalHash !== hash3) throw gated("principal_mismatch");
|
|
29376
|
+
}
|
|
29377
|
+
const report = await inspectLayout(state !== void 0, void 0, otherAccount);
|
|
29378
|
+
assertSupportedProfileLayout(report);
|
|
29379
|
+
if (!discardLocalChanges && (report.unknownPaths.length > 0 || corrupt && !otherAccount || !otherAccount && (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
|
|
29380
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29381
|
+
}
|
|
29382
|
+
return report;
|
|
29383
|
+
});
|
|
29384
|
+
},
|
|
29385
|
+
async prepareLogoutPending({ principal, approval, discardLocalChanges = false, remotePending }) {
|
|
29386
|
+
const gate = await fileGate();
|
|
29387
|
+
return profileSection(async () => {
|
|
29388
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29389
|
+
const previous = await readLogoutMarker(path);
|
|
29390
|
+
const { read, corrupt: corruptGate } = await readLogoutState(gate);
|
|
29391
|
+
let state = read?.state;
|
|
29392
|
+
const principalHash = state === void 0 ? void 0 : await gate.principalHashOf(state.salt, principal);
|
|
29393
|
+
const otherAccount = state !== void 0 && gateOwnedByAnother(state, principalHash);
|
|
29394
|
+
let report = await inspectLayout(state !== void 0, void 0, otherAccount);
|
|
29395
|
+
assertSupportedProfileLayout(report);
|
|
29396
|
+
const assertApproved = (current) => {
|
|
29397
|
+
assertApprovedInventory(current, approval);
|
|
29398
|
+
if (approval.knownLegacyReplicas !== current.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(current.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(current.unknownLegacyPaths)) {
|
|
29399
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
29400
|
+
}
|
|
29401
|
+
};
|
|
29402
|
+
assertApproved(report);
|
|
29403
|
+
if (otherAccount) {
|
|
29404
|
+
if (previous !== null) throw gated("principal_mismatch");
|
|
29405
|
+
if (!discardLocalChanges && report.unknownPaths.length > 0) throw localChangesRefusal(report.unknownPaths);
|
|
29406
|
+
return null;
|
|
29407
|
+
}
|
|
29408
|
+
if (!discardLocalChanges && (report.unknownPaths.length > 0 || corruptGate || (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
|
|
29409
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29410
|
+
}
|
|
29411
|
+
if (previous !== null) {
|
|
29412
|
+
if (previous.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29413
|
+
if (corruptGate) throw gated("gate_unavailable");
|
|
29414
|
+
let current = previous;
|
|
29415
|
+
if (previous.v === 2) {
|
|
29416
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29417
|
+
} else if (read === void 0 || previous.incarnation !== read.state.incarnation) {
|
|
29418
|
+
throw gated("stamp_stale");
|
|
29419
|
+
} else if (previous.v === 3) {
|
|
29420
|
+
if (read.floorLagging || read.state.lifecycleEpoch !== previous.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
|
|
29421
|
+
} else {
|
|
29422
|
+
if (previous.principalHash !== principalHash) throw gated("principal_mismatch");
|
|
29423
|
+
current = (await validateV1(gate, path, previous, read, { active: true, acceptLagging: true })).marker;
|
|
29424
|
+
}
|
|
29425
|
+
assertApprovedInventory(report, {
|
|
29426
|
+
unknownEntries: current.approvedUnknown,
|
|
29427
|
+
knownRoots: current.knownRoots,
|
|
29428
|
+
knownEntries: current.knownEntries,
|
|
29429
|
+
missingDatabases: current.missingDatabases
|
|
29430
|
+
});
|
|
29431
|
+
if (current.remotePending !== remotePending || current.remoteAbandoned) {
|
|
29432
|
+
throw gated("gate_unavailable");
|
|
29433
|
+
}
|
|
29434
|
+
return current;
|
|
29435
|
+
}
|
|
29436
|
+
if (state?.eraseDeferred) throw gated("stamp_stale");
|
|
29437
|
+
if (read?.floorLagging) {
|
|
29438
|
+
if (state.principalHash === null) {
|
|
29439
|
+
state = await gate.advanceInactiveGate(profileDir);
|
|
29440
|
+
} else {
|
|
29441
|
+
const salt = state.salt;
|
|
29442
|
+
const denied = await gate.denyGate(
|
|
29443
|
+
profileDir,
|
|
29444
|
+
{ reason: "sign_out", principal },
|
|
29445
|
+
(hash3) => collectIntents(hash3, salt),
|
|
29446
|
+
{ salt, principalHash }
|
|
29447
|
+
);
|
|
29448
|
+
if (denied.status !== "denied") throw gated("gate_unavailable");
|
|
29449
|
+
state = await gate.readGateSnapshot(profileDir);
|
|
29450
|
+
}
|
|
29451
|
+
report = await inspectLayout(true);
|
|
29452
|
+
assertSupportedProfileLayout(report);
|
|
29453
|
+
assertApproved(report);
|
|
29454
|
+
}
|
|
29455
|
+
if (state === void 0 && report.knownRoots.length === 0 && !corruptGate) return null;
|
|
29456
|
+
if (state !== void 0 && state.principalHash === null && !state.eraseDeferred && report.knownRoots.length === 0 && state.intents.length === 0) return null;
|
|
29457
|
+
const remoteState = remotePending ? { remotePending: true } : {};
|
|
29458
|
+
const approvalFields = {
|
|
29459
|
+
principalDigest: logoutPrincipalDigest(principal),
|
|
29460
|
+
approvalSalt: randomBytes2(16).toString("hex"),
|
|
29461
|
+
approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
|
|
29462
|
+
knownRoots: report.knownRoots,
|
|
29463
|
+
knownEntries: report.knownEntries,
|
|
29464
|
+
missingDatabases: report.missingDatabases,
|
|
29465
|
+
...discardLocalChanges ? { discardLocalChanges: true } : {},
|
|
29466
|
+
...remoteState
|
|
29467
|
+
};
|
|
29468
|
+
let marker;
|
|
29469
|
+
if (state === void 0) {
|
|
29470
|
+
marker = {
|
|
29471
|
+
v: 2,
|
|
29472
|
+
scope: "orphan_profile",
|
|
29473
|
+
nonce: randomBytes2(16).toString("hex"),
|
|
29474
|
+
...approvalFields
|
|
29475
|
+
};
|
|
29476
|
+
} else if (state.principalHash === null) {
|
|
29477
|
+
marker = {
|
|
29478
|
+
v: 3,
|
|
29479
|
+
scope: "inactive_gate",
|
|
29480
|
+
incarnation: state.incarnation,
|
|
29481
|
+
lifecycleEpoch: state.lifecycleEpoch,
|
|
29482
|
+
...approvalFields
|
|
29483
|
+
};
|
|
29484
|
+
} else {
|
|
29485
|
+
marker = {
|
|
29486
|
+
v: 1,
|
|
29487
|
+
incarnation: state.incarnation,
|
|
29488
|
+
lifecycleEpoch: state.lifecycleEpoch,
|
|
29489
|
+
principalHash,
|
|
29490
|
+
...approvalFields
|
|
29491
|
+
};
|
|
29492
|
+
}
|
|
29493
|
+
await writeJsonAtomic(path, marker, options.dirSync);
|
|
29494
|
+
visibleStamp = void 0;
|
|
29495
|
+
visiblePrincipalHash = void 0;
|
|
29496
|
+
checkedGate = void 0;
|
|
29497
|
+
if (corruptGate) await removeCorruptOrphanGate(gate);
|
|
29498
|
+
return marker;
|
|
29499
|
+
});
|
|
29500
|
+
},
|
|
29501
|
+
async completeRemoteLogout({ principal }) {
|
|
29502
|
+
return profileSection(async () => {
|
|
29503
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29504
|
+
const marker = await readLogoutMarker(path);
|
|
29505
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29506
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: false });
|
|
29507
|
+
const { remotePending: _, ...complete } = checked.marker;
|
|
29508
|
+
await writeJsonAtomic(path, complete, options.dirSync);
|
|
29509
|
+
return complete;
|
|
29510
|
+
});
|
|
29511
|
+
},
|
|
29512
|
+
async remotePendingLogout({ principal }) {
|
|
29513
|
+
return profileSection(async () => {
|
|
29514
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29515
|
+
const marker = await readLogoutMarker(path);
|
|
29516
|
+
if (marker === null || marker.remotePending !== true) return false;
|
|
29517
|
+
await validatedRemoteMarker(path, marker, principal, { active: false });
|
|
29518
|
+
return true;
|
|
29519
|
+
});
|
|
29520
|
+
},
|
|
29521
|
+
async abandonRemoteLogout({ principal, approval, discardLocalChanges = false }) {
|
|
29522
|
+
return profileSection(async () => {
|
|
29523
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29524
|
+
const marker = await readLogoutMarker(path);
|
|
29525
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29526
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29527
|
+
const report = await currentLayout();
|
|
29528
|
+
if (storedInventoryChanged(marker, report)) throw changedInventoryRefusal(marker, principal, report);
|
|
29529
|
+
assertApprovedInventory(report, approval);
|
|
29530
|
+
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) {
|
|
29531
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29532
|
+
}
|
|
29533
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
|
|
29534
|
+
return commitPlainAbandon(path, checked.marker, report, discardLocalChanges);
|
|
29535
|
+
});
|
|
29536
|
+
},
|
|
29537
|
+
async approveChangedPlainLogout({ principal, approval, discardLocalChanges, inventoryApprovalToken: token }) {
|
|
29538
|
+
return profileSection(async () => {
|
|
29539
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29540
|
+
const marker = await readLogoutMarker(path);
|
|
29541
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29542
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29543
|
+
const report = await currentLayout();
|
|
29544
|
+
if (!storedInventoryChanged(marker, report)) throw gated("gate_unavailable");
|
|
29545
|
+
const entries = approvalEntries(report);
|
|
29546
|
+
if (discardLocalChanges !== true || typeof token !== "string" || !/^[0-9a-f]{32}$/.test(token) || token !== inventoryApprovalToken(marker, principal, entries)) {
|
|
29547
|
+
throw changedInventoryRefusal(marker, principal, report, entries);
|
|
29548
|
+
}
|
|
29549
|
+
assertApprovedInventory(report, approval);
|
|
29550
|
+
if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths)) {
|
|
29551
|
+
throw changedInventoryRefusal(marker, principal, report, entries);
|
|
29552
|
+
}
|
|
29553
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
|
|
29554
|
+
return commitPlainAbandon(path, checked.marker, report, true);
|
|
29555
|
+
});
|
|
29556
|
+
},
|
|
29557
|
+
async reapproveLogoutPending({ principal, approval }) {
|
|
29558
|
+
return profileSection(async () => {
|
|
29559
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29560
|
+
const marker = await readLogoutMarker(path);
|
|
29561
|
+
if (marker === null || marker.remotePending) throw gated("gate_unavailable");
|
|
29562
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29563
|
+
const report = await currentLayout();
|
|
29564
|
+
assertApprovedInventory(report, approval);
|
|
29565
|
+
if (JSON.stringify(report.unknownPaths) !== JSON.stringify(approval.unknownPaths)) {
|
|
29566
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29567
|
+
}
|
|
29568
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true });
|
|
29569
|
+
const amended = {
|
|
29570
|
+
...checked.marker,
|
|
29571
|
+
approvedUnknown: report.unknownEntries,
|
|
29572
|
+
knownRoots: report.knownRoots,
|
|
29573
|
+
knownEntries: report.knownEntries,
|
|
29574
|
+
missingDatabases: report.missingDatabases,
|
|
29575
|
+
...report.unknownPaths.length > 0 ? { discardLocalChanges: true } : {}
|
|
29576
|
+
};
|
|
29577
|
+
await writeJsonAtomic(path, amended, options.dirSync);
|
|
29578
|
+
return amended;
|
|
29579
|
+
});
|
|
29580
|
+
},
|
|
29581
|
+
async recoverLogoutPending({ signal, clearCredentials, eraseLegacyReplicas }) {
|
|
29582
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29583
|
+
const failure = async (error) => ({
|
|
29584
|
+
erased: [],
|
|
29585
|
+
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" }],
|
|
29586
|
+
remaining: Math.max(1, (await lifecycle.pendingPurge().catch(() => [])).length)
|
|
29587
|
+
});
|
|
29588
|
+
try {
|
|
29589
|
+
const prepared = await profileSection(async () => {
|
|
29590
|
+
const marker2 = await readLogoutMarker(path);
|
|
29591
|
+
if (marker2 === null) {
|
|
29592
|
+
const state2 = await (await fileGate()).readGateSnapshot(profileDir);
|
|
29593
|
+
return { report: { erased: [], failed: [], remaining: state2?.intents.length ?? 0 } };
|
|
29594
|
+
}
|
|
29595
|
+
signal.throwIfAborted();
|
|
29596
|
+
if (marker2.remotePending) throw gated("gate_unavailable");
|
|
29597
|
+
const gate = await fileGate();
|
|
29598
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29599
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29600
|
+
let current = marker2;
|
|
29601
|
+
let state = read?.state;
|
|
29602
|
+
if (marker2.v === 2) {
|
|
29603
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29604
|
+
} else if (read === void 0 || read.state.incarnation !== marker2.incarnation) {
|
|
29605
|
+
throw gated("stamp_stale");
|
|
29606
|
+
} else if (marker2.v === 3) {
|
|
29607
|
+
if (read.floorLagging || read.state.principalHash !== null || read.state.eraseDeferred || read.state.lifecycleEpoch !== marker2.lifecycleEpoch) throw gated("stamp_stale");
|
|
29608
|
+
} else {
|
|
29609
|
+
const checked = await validateV1(
|
|
29610
|
+
gate,
|
|
29611
|
+
path,
|
|
29612
|
+
marker2,
|
|
29613
|
+
read,
|
|
29614
|
+
{ active: true, deny: "ordinary", denyActive: true }
|
|
29615
|
+
);
|
|
29616
|
+
current = checked.marker;
|
|
29617
|
+
state = checked.state;
|
|
29618
|
+
}
|
|
29619
|
+
await clearCredentials();
|
|
29620
|
+
for (const file of ["session.json", "key.json"]) {
|
|
29621
|
+
try {
|
|
29622
|
+
await lstat(join2(profileDir, file));
|
|
29623
|
+
throw gated("gate_unavailable");
|
|
29624
|
+
} catch (error) {
|
|
29625
|
+
if (!isNotFound(error)) throw error;
|
|
29626
|
+
}
|
|
29627
|
+
}
|
|
29628
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29629
|
+
const report = await inspectLayout(state !== void 0, marker2);
|
|
29630
|
+
assertApprovedInventory(report, {
|
|
29631
|
+
unknownEntries: marker2.approvedUnknown,
|
|
29632
|
+
knownRoots: marker2.knownRoots,
|
|
29633
|
+
knownEntries: marker2.knownEntries,
|
|
29634
|
+
missingDatabases: marker2.missingDatabases
|
|
29635
|
+
}, true);
|
|
29636
|
+
if (!marker2.discardLocalChanges && report.unknownEntries.length > 0) {
|
|
29637
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29638
|
+
}
|
|
29639
|
+
if (current.v === 1 && state?.eraseDeferred) {
|
|
29640
|
+
await gate.resumeDeferredGate(profileDir, {
|
|
29641
|
+
deniedEpoch: current.deniedEpoch,
|
|
29642
|
+
principalHash: current.principalHash
|
|
29643
|
+
});
|
|
29644
|
+
}
|
|
29645
|
+
await eraseLegacyReplicas({
|
|
29646
|
+
discardLocalChanges: marker2.discardLocalChanges === true,
|
|
29647
|
+
approvedUnknown: marker2.approvedUnknown,
|
|
29648
|
+
knownRoots: marker2.knownRoots,
|
|
29649
|
+
knownEntries: marker2.knownEntries,
|
|
29650
|
+
missingDatabases: marker2.missingDatabases
|
|
29651
|
+
});
|
|
29652
|
+
try {
|
|
29653
|
+
await lstat(join2(profileDir, "replicas"));
|
|
29654
|
+
throw gated("gate_unavailable");
|
|
29655
|
+
} catch (error) {
|
|
29656
|
+
if (!isNotFound(error)) throw error;
|
|
29657
|
+
}
|
|
29658
|
+
return { marker: current };
|
|
29659
|
+
});
|
|
29660
|
+
if ("report" in prepared) return prepared.report;
|
|
29661
|
+
const marker = prepared.marker;
|
|
29662
|
+
let erased = { erased: [], failed: [], remaining: 0 };
|
|
29663
|
+
if (marker.v !== 2) {
|
|
29664
|
+
erased = await eraseGateIntents({ signal }, marker);
|
|
29665
|
+
if (erased.failed.length > 0 || erased.remaining > 0) return erased;
|
|
29666
|
+
}
|
|
29667
|
+
await profileSection(async () => {
|
|
29668
|
+
const pending = await readLogoutMarker(path);
|
|
29669
|
+
if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
|
|
29670
|
+
throw gated("stamp_stale");
|
|
29671
|
+
}
|
|
29672
|
+
const gate = await fileGate();
|
|
29673
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29674
|
+
if (marker.v === 2 && state !== void 0) throw gated("stamp_stale");
|
|
29675
|
+
const report = await inspectLayout(state !== void 0, marker);
|
|
29676
|
+
assertApprovedInventory(report, {
|
|
29677
|
+
unknownEntries: marker.approvedUnknown,
|
|
29678
|
+
knownRoots: marker.knownRoots,
|
|
29679
|
+
knownEntries: marker.knownEntries,
|
|
29680
|
+
missingDatabases: marker.missingDatabases
|
|
29681
|
+
}, true);
|
|
29682
|
+
await eraseApprovedUnknown(profileDir, marker, "replication", diagnosticFiles);
|
|
29683
|
+
await eraseKnownFlagResidue(profileDir, marker, diagnosticFiles);
|
|
29684
|
+
const finalReport = await inspectLayout(state !== void 0, marker);
|
|
29685
|
+
assertApprovedInventory(finalReport, {
|
|
29686
|
+
unknownEntries: marker.approvedUnknown,
|
|
29687
|
+
knownRoots: marker.knownRoots,
|
|
29688
|
+
knownEntries: marker.knownEntries,
|
|
29689
|
+
missingDatabases: marker.missingDatabases
|
|
29690
|
+
}, true);
|
|
29691
|
+
if (finalReport.unknownPaths.length > 0) throw localChangesRefusal(finalReport.unknownPaths);
|
|
29692
|
+
if (finalReport.knownRoots.length > 0) throw gated("gate_unavailable");
|
|
29693
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29694
|
+
await rm(path);
|
|
29695
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29696
|
+
});
|
|
29697
|
+
return erased;
|
|
29698
|
+
} catch (error) {
|
|
29699
|
+
return failure(error);
|
|
29700
|
+
}
|
|
29701
|
+
},
|
|
29702
|
+
async pendingPurge() {
|
|
29703
|
+
const gate = await fileGate();
|
|
29704
|
+
return (await gate.readGateSnapshot(profileDir))?.intents ?? [];
|
|
29705
|
+
},
|
|
29706
|
+
erasePending({ signal }) {
|
|
29707
|
+
return eraseGateIntents({ signal });
|
|
27863
29708
|
}
|
|
27864
|
-
|
|
29709
|
+
};
|
|
29710
|
+
async function eraseGateIntents({ signal }, marker) {
|
|
29711
|
+
const gate = await fileGate();
|
|
29712
|
+
const erased = [];
|
|
29713
|
+
const failed = [];
|
|
29714
|
+
const intents = await lifecycle.pendingPurge();
|
|
27865
29715
|
try {
|
|
27866
|
-
|
|
27867
|
-
|
|
27868
|
-
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
|
|
27872
|
-
|
|
27873
|
-
|
|
27874
|
-
|
|
27875
|
-
|
|
27876
|
-
|
|
27877
|
-
|
|
27878
|
-
|
|
27879
|
-
|
|
27880
|
-
|
|
29716
|
+
if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
|
|
29717
|
+
else if (marker.remotePending || await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE)) === null) {
|
|
29718
|
+
throw gated("gate_unavailable");
|
|
29719
|
+
}
|
|
29720
|
+
} catch {
|
|
29721
|
+
return { erased, failed: [{ id: "logout", code: "REPLICA_GATED" }], remaining: Math.max(1, intents.length) };
|
|
29722
|
+
}
|
|
29723
|
+
const held = await profileSection(() => gate.readGateSnapshot(profileDir));
|
|
29724
|
+
if (held?.eraseDeferred) return { erased, failed, remaining: held.intents.length, deferred: true };
|
|
29725
|
+
for (const intent of intents) {
|
|
29726
|
+
signal.throwIfAborted();
|
|
29727
|
+
try {
|
|
29728
|
+
await profileSection(async () => {
|
|
29729
|
+
if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
|
|
29730
|
+
else {
|
|
29731
|
+
const pending = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29732
|
+
if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
|
|
29733
|
+
throw gated("gate_unavailable");
|
|
29734
|
+
}
|
|
29735
|
+
assertApprovedInventory(await inspectLayout(true), {
|
|
29736
|
+
unknownEntries: marker.approvedUnknown,
|
|
29737
|
+
knownRoots: marker.knownRoots,
|
|
29738
|
+
knownEntries: marker.knownEntries,
|
|
29739
|
+
missingDatabases: marker.missingDatabases
|
|
29740
|
+
}, true);
|
|
29741
|
+
}
|
|
29742
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29743
|
+
if (state?.eraseDeferred) return;
|
|
29744
|
+
if (!state?.intents.some((queued) => queued.id === intent.id)) return;
|
|
29745
|
+
let idDir;
|
|
29746
|
+
for (const dir of await identityDirs()) {
|
|
29747
|
+
const identity = identityRecordOf(await readJsonFile(join2(dir, "identity.json")));
|
|
29748
|
+
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
|
|
29749
|
+
const hash3 = await gate.identityHashOf(state.salt, replicationIdentityKey(identity));
|
|
29750
|
+
if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
|
|
29751
|
+
idDir = dir;
|
|
29752
|
+
break;
|
|
29753
|
+
}
|
|
29754
|
+
}
|
|
29755
|
+
if (intent.kind === "pending") {
|
|
29756
|
+
if (intent.id !== `pending:${intent.identityKeyHash}`) throw gated("gate_unavailable");
|
|
29757
|
+
if (idDir !== void 0) {
|
|
29758
|
+
for (const name of ["pending.json", "device.jwk"]) {
|
|
29759
|
+
const file = join2(idDir, name);
|
|
29760
|
+
let info;
|
|
29761
|
+
try {
|
|
29762
|
+
info = await lstat(file, { bigint: true });
|
|
29763
|
+
} catch (error) {
|
|
29764
|
+
if (isNotFound(error)) continue;
|
|
29765
|
+
throw error;
|
|
29766
|
+
}
|
|
29767
|
+
if (!info.isFile()) throw gated("gate_unavailable");
|
|
29768
|
+
if (marker !== void 0) {
|
|
29769
|
+
const relative = `replication/${basename(idDir)}/${name}`;
|
|
29770
|
+
const expected = [...marker.knownEntries, ...marker.approvedUnknown].find(
|
|
29771
|
+
(item) => item.path === relative
|
|
29772
|
+
);
|
|
29773
|
+
if (expected?.kind !== "file" || expected.dev !== info.dev.toString() || expected.ino !== info.ino.toString()) throw localChangesRefusal([relative]);
|
|
29774
|
+
}
|
|
29775
|
+
await rm(file);
|
|
29776
|
+
}
|
|
29777
|
+
await fsyncDir(idDir);
|
|
29778
|
+
}
|
|
29779
|
+
} else {
|
|
29780
|
+
if (!/^[0-9a-f]{64}:[a-z2-7]{26}$/.test(intent.partitionId) || intent.id !== intent.partitionId) {
|
|
29781
|
+
throw gated("gate_unavailable");
|
|
29782
|
+
}
|
|
29783
|
+
if (idDir !== void 0) {
|
|
29784
|
+
const replicaDir = join2(idDir, "replicas", intent.partitionId.slice(65));
|
|
29785
|
+
let dirInfo;
|
|
29786
|
+
if (await realDirectoryExists(join2(idDir, "replicas"))) {
|
|
29787
|
+
try {
|
|
29788
|
+
dirInfo = await lstat(replicaDir, { bigint: true });
|
|
29789
|
+
} catch (error) {
|
|
29790
|
+
if (!isNotFound(error)) throw error;
|
|
29791
|
+
}
|
|
29792
|
+
}
|
|
29793
|
+
if (dirInfo !== void 0) {
|
|
29794
|
+
const relative = `replication/${basename(idDir)}/replicas/${basename(replicaDir)}`;
|
|
29795
|
+
const expectedRoot = marker?.knownRoots.find((item) => item.path === relative);
|
|
29796
|
+
if (!dirInfo.isDirectory() || marker !== void 0 && (expectedRoot?.dev !== dirInfo.dev.toString() || expectedRoot.ino !== dirInfo.ino.toString())) throw localChangesRefusal([relative]);
|
|
29797
|
+
const approvedUnknown = marker?.approvedUnknown.filter(
|
|
29798
|
+
(item) => item.path.startsWith(`${relative}/`)
|
|
29799
|
+
).map(
|
|
29800
|
+
(item) => ({ ...item, path: item.path.slice(relative.length + 1) })
|
|
29801
|
+
);
|
|
29802
|
+
const expectedKnown = marker?.knownEntries.filter(
|
|
29803
|
+
(item) => item.path.startsWith(`${relative}/`)
|
|
29804
|
+
).map(
|
|
29805
|
+
(item) => ({ ...item, path: item.path.slice(relative.length + 1) })
|
|
29806
|
+
);
|
|
29807
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
29808
|
+
if (!await pathExists(join2(replicaDir, "replica.db"))) {
|
|
29809
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
|
|
29810
|
+
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))) {
|
|
29811
|
+
throw localChangesRefusal([relative]);
|
|
29812
|
+
}
|
|
29813
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29814
|
+
approvedUnknown,
|
|
29815
|
+
expectedKnown: layout.knownEntries,
|
|
29816
|
+
expectedRoot,
|
|
29817
|
+
allowAdapterFence: true,
|
|
29818
|
+
allowMissingDatabase: true
|
|
29819
|
+
});
|
|
29820
|
+
} else {
|
|
29821
|
+
let store;
|
|
29822
|
+
try {
|
|
29823
|
+
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
|
|
29824
|
+
const row = await store.open();
|
|
29825
|
+
if (row !== null) {
|
|
29826
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
|
|
29827
|
+
if (lease === null) throw gated("gate_unavailable");
|
|
29828
|
+
try {
|
|
29829
|
+
if (row.revoked === null) await store.reset(lease, "purged");
|
|
29830
|
+
let deletionKnown = expectedKnown;
|
|
29831
|
+
if (marker !== void 0) {
|
|
29832
|
+
const current = await sqlite.inspectSqliteReplicaLayout(replicaDir, {
|
|
29833
|
+
allowAdapterFence: true
|
|
29834
|
+
});
|
|
29835
|
+
if (current.rootIdentity.dev !== expectedRoot?.dev || current.rootIdentity.ino !== expectedRoot.ino) throw localChangesRefusal([relative]);
|
|
29836
|
+
const approved = new Map(expectedKnown?.map((entry) => [entry.path, entry]));
|
|
29837
|
+
for (const entry of current.knownEntries) {
|
|
29838
|
+
const prior = approved.get(entry.path);
|
|
29839
|
+
if (prior?.kind === entry.kind && prior.dev === entry.dev && prior.ino === entry.ino) continue;
|
|
29840
|
+
if (entry.kind !== "file" || !["replica.db-wal", "replica.db-shm", "replica.db-journal"].includes(entry.path)) {
|
|
29841
|
+
throw localChangesRefusal([`${relative}/${entry.path}`]);
|
|
29842
|
+
}
|
|
29843
|
+
}
|
|
29844
|
+
deletionKnown = current.knownEntries;
|
|
29845
|
+
}
|
|
29846
|
+
await store.destroy(lease, {
|
|
29847
|
+
approvedUnknown,
|
|
29848
|
+
expectedKnown: deletionKnown,
|
|
29849
|
+
expectedRoot,
|
|
29850
|
+
allowAdapterFence: true
|
|
29851
|
+
});
|
|
29852
|
+
} finally {
|
|
29853
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
29854
|
+
}
|
|
29855
|
+
} else {
|
|
29856
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29857
|
+
approvedUnknown,
|
|
29858
|
+
expectedKnown,
|
|
29859
|
+
expectedRoot,
|
|
29860
|
+
allowAdapterFence: true
|
|
29861
|
+
});
|
|
29862
|
+
}
|
|
29863
|
+
} finally {
|
|
29864
|
+
await store?.close().catch(() => void 0);
|
|
29865
|
+
}
|
|
29866
|
+
}
|
|
29867
|
+
await fsyncDir(join2(idDir, "replicas"));
|
|
29868
|
+
}
|
|
29869
|
+
}
|
|
29870
|
+
}
|
|
29871
|
+
await gate.clearGateIntent(profileDir, intent.id);
|
|
29872
|
+
erased.push(intent.id);
|
|
29873
|
+
});
|
|
29874
|
+
} catch (error) {
|
|
29875
|
+
failed.push({ id: intent.id, code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR" });
|
|
29876
|
+
}
|
|
29877
|
+
}
|
|
29878
|
+
return { erased, failed, remaining: (await lifecycle.pendingPurge()).length };
|
|
29879
|
+
}
|
|
29880
|
+
async function openHandle(spec) {
|
|
29881
|
+
const [replica, sqlite] = await Promise.all([
|
|
29882
|
+
// @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
|
|
29883
|
+
import("@tinycloud/replica"),
|
|
29884
|
+
// @ts-ignore — same as above.
|
|
29885
|
+
import("@tinycloud/replica/sqlite")
|
|
29886
|
+
]);
|
|
29887
|
+
if (spec.stamp === void 0) throw gated("not_established");
|
|
29888
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
29889
|
+
if (spec.grantPolicy === void 0) {
|
|
29890
|
+
throw new ReplicaStorageError("REPLICA_INVALID_ARGUMENT", "A replica grant policy is required.");
|
|
29891
|
+
}
|
|
29892
|
+
const gate = await fileGate();
|
|
29893
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
29894
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29895
|
+
const identityKeyHash = await gate.identityHashOf(snapshot.salt, replicationIdentityKey(spec.identity));
|
|
29896
|
+
const resolvePolicy = async (at, recordFirst) => {
|
|
29897
|
+
const policy = spec.grantPolicy;
|
|
29898
|
+
if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > MAX_GRANT_TTL_MS2) {
|
|
29899
|
+
throw new TypeError("A replica grant requires a finite TTL not exceeding seven days.");
|
|
29900
|
+
}
|
|
29901
|
+
const verified = policy.consentAnchor;
|
|
29902
|
+
if (verified !== null && (!Number.isSafeInteger(verified.at) || verified.at < 0 || verified.source === "first_install")) {
|
|
29903
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The verified consent anchor is unavailable.");
|
|
29904
|
+
}
|
|
29905
|
+
const first = recordFirst ? await gate.recordFirstInstall(profileDir, identityKeyHash, at) : await gate.readFirstInstall(profileDir, identityKeyHash);
|
|
29906
|
+
const anchor = verified === null ? first === void 0 ? null : { at: first, source: "first_install" } : { at: Math.min(verified.at, at), source: verified.source };
|
|
29907
|
+
if (anchor === null) return null;
|
|
29908
|
+
const deadline = anchor.at + CONSENT_WINDOW_MS;
|
|
29909
|
+
if (!Number.isSafeInteger(anchor.at) || anchor.at < 0 || !Number.isSafeInteger(deadline)) {
|
|
29910
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The durable consent anchor is invalid.");
|
|
29911
|
+
}
|
|
29912
|
+
return { at, ttlMs: policy.grantTtlMs, consentDeadline: deadline, consentAnchor: anchor };
|
|
29913
|
+
};
|
|
29914
|
+
let partitionId;
|
|
29915
|
+
const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
|
|
29916
|
+
return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
|
|
29917
|
+
if (requiresSecretsOptIn2(spec.space, spec.prefix) && spec.allowSecrets !== true) {
|
|
29918
|
+
throw new replica.ReplicaError(
|
|
29919
|
+
replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
29920
|
+
`Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
|
|
29921
|
+
);
|
|
29922
|
+
}
|
|
29923
|
+
const idDir = partitionDirOf(root, spec.identity);
|
|
29924
|
+
await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
|
|
29925
|
+
const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, profileSection, createDevice, options.dirSync);
|
|
29926
|
+
const device = { ...createdDevice, did: principalOf(createdDevice.did) };
|
|
29927
|
+
const replicaHash = replicaHashOf(spec.prefix, device.did);
|
|
29928
|
+
partitionId = `${identityKeyHash}:${replicaHash}`;
|
|
29929
|
+
const replicaDir = join2(idDir, "replicas", replicaHash);
|
|
29930
|
+
const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
|
|
29931
|
+
create: true,
|
|
29932
|
+
guard: storeGuard
|
|
29933
|
+
});
|
|
29934
|
+
let generation;
|
|
29935
|
+
try {
|
|
29936
|
+
try {
|
|
29937
|
+
await store.init({
|
|
29938
|
+
name: spec.prefix.replace(/\/+$/, "") || "replica",
|
|
29939
|
+
replicaId: replicaHash,
|
|
29940
|
+
host: spec.identity.host,
|
|
29941
|
+
space: spec.space,
|
|
29942
|
+
prefix: spec.prefix,
|
|
29943
|
+
deviceDid: device.did,
|
|
29944
|
+
allowSecrets: spec.allowSecrets,
|
|
29945
|
+
localReadPolicy: "whileGrantValid",
|
|
29946
|
+
retentionGrantCid: null
|
|
29947
|
+
});
|
|
29948
|
+
} catch (error) {
|
|
29949
|
+
if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
|
|
29950
|
+
throw error;
|
|
29951
|
+
}
|
|
29952
|
+
}
|
|
29953
|
+
const state = await store.open();
|
|
29954
|
+
if (state === null) {
|
|
29955
|
+
throw new replica.ReplicaError(
|
|
29956
|
+
replica.ReplicaErrorCode.NOT_FOUND,
|
|
29957
|
+
"The replica row vanished during init."
|
|
29958
|
+
);
|
|
29959
|
+
}
|
|
29960
|
+
const config = state.config;
|
|
29961
|
+
if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
|
|
29962
|
+
throw new replica.ReplicaError(
|
|
29963
|
+
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
29964
|
+
`This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
|
|
29965
|
+
);
|
|
29966
|
+
}
|
|
29967
|
+
if (principalOf(config.deviceDid) !== principalOf(device.did)) {
|
|
29968
|
+
throw new replica.ReplicaError(
|
|
29969
|
+
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
29970
|
+
"The stored replica belongs to a different device key; purge the partition to rekey."
|
|
29971
|
+
);
|
|
29972
|
+
}
|
|
29973
|
+
await retainGrantInstalls(gate, snapshot, identityKeyHash, store);
|
|
29974
|
+
if (state.grant !== null || state.pendingGrant !== null) {
|
|
29975
|
+
const policy = await resolvePolicy(Date.now(), false);
|
|
29976
|
+
if (policy === null) throw gated("gate_unavailable");
|
|
29977
|
+
if (state.grant !== null) await store.recomputeGrantInstall(state.grant, policy);
|
|
29978
|
+
if (state.pendingGrant !== null) await store.recomputeGrantInstall(state.pendingGrant, policy);
|
|
29979
|
+
}
|
|
29980
|
+
generation = state.generation;
|
|
29981
|
+
} catch (error) {
|
|
29982
|
+
await store.close().catch(() => void 0);
|
|
29983
|
+
throw error;
|
|
29984
|
+
}
|
|
29985
|
+
let ino;
|
|
29986
|
+
try {
|
|
29987
|
+
ino = (await stat(join2(replicaDir, "replica.db"))).ino;
|
|
29988
|
+
} catch (error) {
|
|
29989
|
+
if (!isNotFound(error)) throw error;
|
|
29990
|
+
}
|
|
29991
|
+
return new SqliteReplicaHandle({
|
|
29992
|
+
spec,
|
|
29993
|
+
replicaId: replicaHash,
|
|
29994
|
+
device,
|
|
29995
|
+
store,
|
|
29996
|
+
replicaDir,
|
|
29997
|
+
replica,
|
|
29998
|
+
guard: storeGuard,
|
|
29999
|
+
read: (section, withFence) => readGated(spec, partitionId, store, replicaDir, generation, section, withFence),
|
|
30000
|
+
policy: resolvePolicy,
|
|
30001
|
+
recordGrantInstall: async (cid, at) => gate.recordGrantInstall(profileDir, await gate.grantInstallHashOf(snapshot.salt, identityKeyHash, cid), at),
|
|
30002
|
+
transportFor: options.transportFor,
|
|
30003
|
+
dirSync: options.dirSync,
|
|
30004
|
+
generation,
|
|
30005
|
+
ino
|
|
30006
|
+
});
|
|
27881
30007
|
});
|
|
27882
30008
|
}
|
|
27883
30009
|
async function inspectStatus(spec, { signal }) {
|
|
30010
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
27884
30011
|
const checkAbort = () => {
|
|
27885
30012
|
if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
|
|
27886
30013
|
};
|
|
@@ -27913,6 +30040,8 @@ function createSqliteReplicaStorage(options) {
|
|
|
27913
30040
|
import("@tinycloud/replica/sqlite")
|
|
27914
30041
|
]);
|
|
27915
30042
|
checkAbort();
|
|
30043
|
+
const syncedThroughEpoch = await readFence(replicaDir);
|
|
30044
|
+
checkAbort();
|
|
27916
30045
|
const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
|
|
27917
30046
|
checkAbort();
|
|
27918
30047
|
if (inspected === null) return void 0;
|
|
@@ -27920,14 +30049,17 @@ function createSqliteReplicaStorage(options) {
|
|
|
27920
30049
|
if (state.config.host !== spec.identity.host || state.config.space !== spec.space || state.config.prefix !== spec.prefix || principalOf(state.config.deviceDid) !== principalOf(device.did)) {
|
|
27921
30050
|
throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
|
|
27922
30051
|
}
|
|
27923
|
-
const syncedThroughEpoch = await readFence(replicaDir);
|
|
27924
30052
|
checkAbort();
|
|
30053
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
30054
|
+
const gateState = await (await fileGate()).readGateSnapshot(profileDir);
|
|
30055
|
+
const purgePending = gateState !== void 0 && (gateState.eraseDeferred || gateState.intents.length > 0);
|
|
27925
30056
|
return {
|
|
30057
|
+
purgePending,
|
|
27926
30058
|
coverage: status.coverage,
|
|
27927
30059
|
lastSyncAt: status.lastSyncAt,
|
|
27928
30060
|
syncedThroughEpoch,
|
|
27929
|
-
authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
|
|
27930
|
-
grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
|
|
30061
|
+
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 },
|
|
30062
|
+
grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec, inspected.grantInstall),
|
|
27931
30063
|
counts: status.counts,
|
|
27932
30064
|
bytes: status.bytes,
|
|
27933
30065
|
lastError: status.lastError
|
|
@@ -27940,89 +30072,124 @@ function createSqliteReplicaStorage(options) {
|
|
|
27940
30072
|
// @ts-ignore — same as above.
|
|
27941
30073
|
import("@tinycloud/replica/sqlite")
|
|
27942
30074
|
]);
|
|
27943
|
-
|
|
27944
|
-
|
|
27945
|
-
|
|
27946
|
-
|
|
27947
|
-
|
|
27948
|
-
|
|
27949
|
-
const
|
|
27950
|
-
if (
|
|
27951
|
-
|
|
27952
|
-
|
|
27953
|
-
|
|
27954
|
-
|
|
27955
|
-
|
|
27956
|
-
|
|
27957
|
-
|
|
27958
|
-
|
|
27959
|
-
|
|
27960
|
-
|
|
27961
|
-
|
|
27962
|
-
const
|
|
27963
|
-
|
|
27964
|
-
|
|
27965
|
-
|
|
27966
|
-
|
|
27967
|
-
|
|
27968
|
-
|
|
27969
|
-
|
|
27970
|
-
|
|
27971
|
-
continue;
|
|
27972
|
-
}
|
|
27973
|
-
throw error;
|
|
30075
|
+
return profileSection(async () => {
|
|
30076
|
+
const idDir = partitionDirOf(root, target.identity);
|
|
30077
|
+
if (!await realDirectoryExists(idDir)) return;
|
|
30078
|
+
const hasReplicas = await realDirectoryExists(join2(idDir, "replicas"));
|
|
30079
|
+
const deviceDids = [];
|
|
30080
|
+
const deviceFile = join2(idDir, "device.jwk");
|
|
30081
|
+
const stored = await readJsonFile(deviceFile);
|
|
30082
|
+
if (stored !== void 0) {
|
|
30083
|
+
const device = deviceRecordOf(stored);
|
|
30084
|
+
if (device === void 0) {
|
|
30085
|
+
throw new ReplicaStorageError(
|
|
30086
|
+
"STORAGE_ERROR",
|
|
30087
|
+
`The replica device key at ${deviceFile} is unreadable.`
|
|
30088
|
+
);
|
|
30089
|
+
}
|
|
30090
|
+
deviceDids.push(device.did);
|
|
30091
|
+
}
|
|
30092
|
+
if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
|
|
30093
|
+
const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
|
|
30094
|
+
const removeLeftover = async (replicaDir) => {
|
|
30095
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
|
|
30096
|
+
const missingDatabase = !await pathExists(join2(replicaDir, "replica.db"));
|
|
30097
|
+
const unexpected = layout.unknownPaths.filter((path) => path !== "replica.db");
|
|
30098
|
+
if (unexpected.length !== 0 || layout.unknownPaths.includes("replica.db") !== missingDatabase || layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
30099
|
+
throw new ReplicaStorageError(
|
|
30100
|
+
"STORAGE_ERROR",
|
|
30101
|
+
`Refusing to remove unexpected replica path ${join2(replicaDir, unexpected[0] ?? "replica.db")}.`
|
|
30102
|
+
);
|
|
27974
30103
|
}
|
|
30104
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
30105
|
+
expectedRoot: layout.rootIdentity,
|
|
30106
|
+
expectedKnown: layout.knownEntries,
|
|
30107
|
+
allowAdapterFence: true,
|
|
30108
|
+
allowMissingDatabase: missingDatabase
|
|
30109
|
+
});
|
|
30110
|
+
};
|
|
30111
|
+
let firstError;
|
|
30112
|
+
for (const deviceDid of devices) {
|
|
30113
|
+
const replicaDir = join2(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
|
|
30114
|
+
if (!hasReplicas || !await realDirectoryExists(replicaDir)) continue;
|
|
27975
30115
|
try {
|
|
27976
|
-
|
|
27977
|
-
|
|
27978
|
-
await
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
throw
|
|
27985
|
-
replica.ReplicaErrorCode.BUSY,
|
|
27986
|
-
"Another process holds this replica's sync lease."
|
|
27987
|
-
);
|
|
30116
|
+
let store;
|
|
30117
|
+
try {
|
|
30118
|
+
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
|
|
30119
|
+
} catch (error) {
|
|
30120
|
+
if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
|
|
30121
|
+
await removeLeftover(replicaDir);
|
|
30122
|
+
continue;
|
|
30123
|
+
}
|
|
30124
|
+
throw error;
|
|
27988
30125
|
}
|
|
27989
30126
|
try {
|
|
27990
|
-
|
|
27991
|
-
|
|
30127
|
+
const state = await store.open();
|
|
30128
|
+
if (state === null) {
|
|
30129
|
+
await store.close().catch(() => void 0);
|
|
30130
|
+
await removeLeftover(replicaDir);
|
|
30131
|
+
continue;
|
|
30132
|
+
}
|
|
30133
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
|
|
30134
|
+
if (lease === null) {
|
|
30135
|
+
throw new replica.ReplicaError(
|
|
30136
|
+
replica.ReplicaErrorCode.BUSY,
|
|
30137
|
+
"Another process holds this replica's sync lease."
|
|
30138
|
+
);
|
|
30139
|
+
}
|
|
30140
|
+
try {
|
|
30141
|
+
const gate = await fileGate();
|
|
30142
|
+
const currentGate = await gate.readGateSnapshot(profileDir);
|
|
30143
|
+
if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
|
|
30144
|
+
const identityKeyHash = await gate.identityHashOf(currentGate.salt, replicationIdentityKey(target.identity));
|
|
30145
|
+
await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
|
|
30146
|
+
}
|
|
30147
|
+
if (state.revoked === null) {
|
|
30148
|
+
await store.reset(lease, "purged");
|
|
30149
|
+
}
|
|
30150
|
+
await store.destroy(lease, { allowAdapterFence: true });
|
|
30151
|
+
} finally {
|
|
30152
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
27992
30153
|
}
|
|
27993
|
-
await store.destroy(lease);
|
|
27994
30154
|
} finally {
|
|
27995
|
-
await store.
|
|
30155
|
+
await store.close().catch(() => void 0);
|
|
27996
30156
|
}
|
|
27997
|
-
}
|
|
27998
|
-
|
|
30157
|
+
} catch (error) {
|
|
30158
|
+
firstError ?? (firstError = error);
|
|
27999
30159
|
}
|
|
28000
|
-
} catch (error) {
|
|
28001
|
-
firstError ?? (firstError = error);
|
|
28002
30160
|
}
|
|
28003
|
-
|
|
28004
|
-
|
|
30161
|
+
if (firstError !== void 0) throw firstError;
|
|
30162
|
+
});
|
|
28005
30163
|
}
|
|
28006
30164
|
return {
|
|
28007
30165
|
kind: "sqlite",
|
|
30166
|
+
lifecycle,
|
|
28008
30167
|
open: openHandle,
|
|
28009
30168
|
inspectStatus,
|
|
28010
30169
|
purge: purgePartition,
|
|
28011
|
-
//
|
|
28012
|
-
|
|
28013
|
-
// restart that lost ambiguous write records would serve stale data.
|
|
28014
|
-
// Without a guard the file store still durably serializes each update;
|
|
28015
|
-
// the guard only adds cross-process read-modify-write exclusion.
|
|
28016
|
-
pendingWrites(identity) {
|
|
30170
|
+
// File-backed pending writes are always bound to a stamped gate.
|
|
30171
|
+
pendingWrites(identity, opts) {
|
|
28017
30172
|
return new FilePendingWriteStore(
|
|
28018
30173
|
identity,
|
|
28019
30174
|
partitionDirOf(root, identity),
|
|
30175
|
+
profileDir,
|
|
28020
30176
|
guard,
|
|
28021
|
-
|
|
30177
|
+
opts?.stamp,
|
|
30178
|
+
options.dirSync,
|
|
30179
|
+
options.logoutPendingFile
|
|
28022
30180
|
);
|
|
28023
30181
|
}
|
|
28024
30182
|
};
|
|
28025
30183
|
}
|
|
30184
|
+
function createProfileReplicaPurge(options) {
|
|
30185
|
+
return createSqliteReplicaStorage({
|
|
30186
|
+
dir: join2(options.profileDir, "replication"),
|
|
30187
|
+
guard: options.guard,
|
|
30188
|
+
dirSync: options.dirSync,
|
|
30189
|
+
logoutPendingFile: join2(options.profileDir, REPLICA_LOGOUT_PENDING_FILE),
|
|
30190
|
+
diagnosticFiles: options.diagnosticFiles
|
|
30191
|
+
}).lifecycle;
|
|
30192
|
+
}
|
|
28026
30193
|
function sqliteReplicaStorage(options) {
|
|
28027
30194
|
return createSqliteReplicaStorage(options);
|
|
28028
30195
|
}
|
|
@@ -28074,7 +30241,9 @@ export {
|
|
|
28074
30241
|
PermissionNotInManifestError2 as PermissionNotInManifestError,
|
|
28075
30242
|
PrefixedKVService,
|
|
28076
30243
|
PrivateKeySigner,
|
|
28077
|
-
ProtocolMismatchError,
|
|
30244
|
+
ProtocolMismatchError2 as ProtocolMismatchError,
|
|
30245
|
+
REPLICA_LOGOUT_PENDING_FILE,
|
|
30246
|
+
RESTORED_GATE_MESSAGE,
|
|
28078
30247
|
RUNTIME_DELEGATION_REJECTIONS,
|
|
28079
30248
|
RuntimeDelegationRejectedError,
|
|
28080
30249
|
SECRET_NAME_RE,
|
|
@@ -28101,12 +30270,13 @@ export {
|
|
|
28101
30270
|
VAULT_PERMISSION_SERVICE,
|
|
28102
30271
|
VaultHeaders,
|
|
28103
30272
|
VaultPublicSpaceKVActions,
|
|
28104
|
-
|
|
30273
|
+
VersionCheckError,
|
|
28105
30274
|
WasmKeyProvider,
|
|
28106
30275
|
activateCompactRuntimeDelegation,
|
|
28107
30276
|
activateValidatedRuntimeDelegation,
|
|
28108
30277
|
addressStorageKey,
|
|
28109
30278
|
appSecretScope,
|
|
30279
|
+
assertProfileLogoutClear,
|
|
28110
30280
|
buildCanonicalDecryptRequest,
|
|
28111
30281
|
buildDecryptAttenuation,
|
|
28112
30282
|
buildDecryptFacts,
|
|
@@ -28127,6 +30297,7 @@ export {
|
|
|
28127
30297
|
composeManifestRequest2 as composeManifestRequest,
|
|
28128
30298
|
createCapabilityKeyRegistry,
|
|
28129
30299
|
createOpenKeyCallbackSigningStrategy,
|
|
30300
|
+
createProfileReplicaPurge,
|
|
28130
30301
|
createSharingService,
|
|
28131
30302
|
createSpaceService,
|
|
28132
30303
|
createVaultCrypto2 as createVaultCrypto,
|
|
@@ -28163,6 +30334,7 @@ export {
|
|
|
28163
30334
|
isNetworkId,
|
|
28164
30335
|
isStorageFullError2 as isStorageFullError,
|
|
28165
30336
|
loadManifest,
|
|
30337
|
+
logoutPrincipalDigest,
|
|
28166
30338
|
makePkhSpaceId2 as makePkhSpaceId,
|
|
28167
30339
|
makePublicSpaceId2 as makePublicSpaceId,
|
|
28168
30340
|
networkDiscoveryKey,
|