@tinycloud/node-sdk 3.2.6-beta.6 → 3.3.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{WasmKeyProvider-DEdSyQ0I.d.cts → TinyCloudNode-CcNh20QX.d.cts} +181 -286
- package/dist/{WasmKeyProvider-DEdSyQ0I.d.ts → TinyCloudNode-CcNh20QX.d.ts} +181 -286
- package/dist/WasmKeyProvider-5Ee7fcAw.d.cts +217 -0
- package/dist/WasmKeyProvider-5Ee7fcAw.d.ts +217 -0
- package/dist/authority-Cwotwza3.d.cts +118 -0
- package/dist/authority-D_u9a7Wl.d.ts +118 -0
- package/dist/core.cjs +401 -168
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +37 -4
- package/dist/core.d.ts +37 -4
- package/dist/core.js +408 -176
- package/dist/core.js.map +1 -1
- package/dist/gated-network-BEF2XSTC.d.cts +16 -0
- package/dist/gated-network-BEF2XSTC.d.ts +16 -0
- package/dist/index.cjs +2735 -545
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -89
- package/dist/index.d.ts +35 -89
- package/dist/index.js +2730 -545
- package/dist/index.js.map +1 -1
- package/dist/replication/authority.cjs +153 -15
- package/dist/replication/authority.cjs.map +1 -1
- package/dist/replication/authority.d.cts +4 -1
- package/dist/replication/authority.d.ts +4 -1
- package/dist/replication/authority.js +147 -14
- package/dist/replication/authority.js.map +1 -1
- package/dist/replication/gated-network.cjs +90 -0
- package/dist/replication/gated-network.cjs.map +1 -0
- package/dist/replication/gated-network.d.cts +2 -0
- package/dist/replication/gated-network.d.ts +2 -0
- package/dist/replication/gated-network.js +65 -0
- package/dist/replication/gated-network.js.map +1 -0
- package/dist/replication/runtime.cjs +84 -6
- package/dist/replication/runtime.cjs.map +1 -1
- package/dist/replication/runtime.d.cts +2 -1
- package/dist/replication/runtime.d.ts +2 -1
- package/dist/replication/runtime.js +84 -6
- package/dist/replication/runtime.js.map +1 -1
- package/dist/{runtime-B5P5SRpC.d.ts → runtime-D1o9Eybt.d.cts} +14 -3
- package/dist/{runtime-B5P5SRpC.d.cts → runtime-v62FzQHz.d.ts} +14 -3
- package/package.json +14 -4
- package/dist/authority-DDGtccBK.d.cts +0 -96
- package/dist/authority-DDGtccBK.d.ts +0 -96
package/dist/index.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,
|
|
@@ -18286,6 +18566,7 @@ import {
|
|
|
18286
18566
|
validateShareDeliveryAuthorizationV3Bytes,
|
|
18287
18567
|
verifyEip191MessageSignature,
|
|
18288
18568
|
signCompactUcanRootAuthorization,
|
|
18569
|
+
markDelegationTerminal,
|
|
18289
18570
|
isStorageFullError,
|
|
18290
18571
|
STORAGE_FULL_MESSAGE,
|
|
18291
18572
|
STORAGE_WRITE_TOO_LARGE_MESSAGE,
|
|
@@ -18541,6 +18822,7 @@ var NodeUserAuthorization = class {
|
|
|
18541
18822
|
this.siweConfig = config.siweConfig;
|
|
18542
18823
|
this.includeAccountRegistryPermissions = config.includeAccountRegistryPermissions ?? true;
|
|
18543
18824
|
this.replication = config.replication?.enabled === true ? config.replication : void 0;
|
|
18825
|
+
this.establishReplicaStamp = config.establishReplicaStamp;
|
|
18544
18826
|
this._manifest = config.manifest;
|
|
18545
18827
|
this._capabilityRequest = config.capabilityRequest;
|
|
18546
18828
|
this.sessionManager = config.sessionManager ?? this.wasm.createSessionManager();
|
|
@@ -19171,7 +19453,7 @@ var NodeUserAuthorization = class {
|
|
|
19171
19453
|
);
|
|
19172
19454
|
throw error;
|
|
19173
19455
|
}
|
|
19174
|
-
await new Promise((
|
|
19456
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
19175
19457
|
const retryResult = await activateSessionWithHost(
|
|
19176
19458
|
host,
|
|
19177
19459
|
this._tinyCloudSession.delegationHeader
|
|
@@ -19211,7 +19493,7 @@ var NodeUserAuthorization = class {
|
|
|
19211
19493
|
);
|
|
19212
19494
|
throw error;
|
|
19213
19495
|
}
|
|
19214
|
-
await new Promise((
|
|
19496
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
19215
19497
|
const retryResult = await activateSessionWithHost(
|
|
19216
19498
|
host,
|
|
19217
19499
|
this._tinyCloudSession.delegationHeader
|
|
@@ -19310,6 +19592,27 @@ var NodeUserAuthorization = class {
|
|
|
19310
19592
|
signature: signature2
|
|
19311
19593
|
};
|
|
19312
19594
|
}
|
|
19595
|
+
persistedData(session, expiresAt, createdAt, replicaStamp) {
|
|
19596
|
+
return {
|
|
19597
|
+
address: session.address,
|
|
19598
|
+
chainId: session.chainId,
|
|
19599
|
+
sessionKey: JSON.stringify(session.jwk),
|
|
19600
|
+
siwe: session.siwe,
|
|
19601
|
+
signature: session.signature,
|
|
19602
|
+
tinycloudSession: {
|
|
19603
|
+
delegationHeader: session.delegationHeader,
|
|
19604
|
+
delegationCid: session.delegationCid,
|
|
19605
|
+
spaceId: session.spaceId,
|
|
19606
|
+
spaces: session.spaces,
|
|
19607
|
+
verificationMethod: session.verificationMethod
|
|
19608
|
+
},
|
|
19609
|
+
expiresAt,
|
|
19610
|
+
createdAt,
|
|
19611
|
+
version: "1.0",
|
|
19612
|
+
tinycloudHosts: this.tinycloudHosts,
|
|
19613
|
+
...replicaStamp ? { replicaStamp } : {}
|
|
19614
|
+
};
|
|
19615
|
+
}
|
|
19313
19616
|
/**
|
|
19314
19617
|
* Sign in and create a new session.
|
|
19315
19618
|
*
|
|
@@ -19389,24 +19692,13 @@ var NodeUserAuthorization = class {
|
|
|
19389
19692
|
siwe: prepared.siwe,
|
|
19390
19693
|
signature: signature2
|
|
19391
19694
|
};
|
|
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
|
-
};
|
|
19695
|
+
const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
|
|
19696
|
+
const persistedData = this.persistedData(
|
|
19697
|
+
tinyCloudSession,
|
|
19698
|
+
expirationTime.toISOString(),
|
|
19699
|
+
now.toISOString(),
|
|
19700
|
+
replicaStamp
|
|
19701
|
+
);
|
|
19410
19702
|
await this.enqueuePersistence(async () => {
|
|
19411
19703
|
await this.sessionStorage.save(address, persistedData);
|
|
19412
19704
|
if (generation !== this.sessionGeneration) {
|
|
@@ -19560,28 +19852,12 @@ var NodeUserAuthorization = class {
|
|
|
19560
19852
|
siwe: prepared.siwe,
|
|
19561
19853
|
signature: signature2
|
|
19562
19854
|
};
|
|
19563
|
-
const expirationMatch = prepared.siwe
|
|
19564
|
-
const issuedAtMatch = prepared.siwe
|
|
19855
|
+
const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
|
|
19856
|
+
const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
|
|
19565
19857
|
const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
|
|
19566
19858
|
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
|
-
};
|
|
19859
|
+
const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
|
|
19860
|
+
const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
|
|
19585
19861
|
await this.sessionStorage.save(address, persistedData);
|
|
19586
19862
|
this._session = clientSession;
|
|
19587
19863
|
this._tinyCloudSession = tinyCloudSession;
|
|
@@ -19644,15 +19920,36 @@ var NodeUserAuthorization = class {
|
|
|
19644
19920
|
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
19645
19921
|
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
19646
19922
|
const hostHint = this.primaryTinyCloudHost;
|
|
19647
|
-
const
|
|
19923
|
+
const originalRequest = {
|
|
19648
19924
|
protocolVersion: 1,
|
|
19649
19925
|
siwe: prepared.siwe,
|
|
19650
19926
|
jwk,
|
|
19927
|
+
verificationMethod: prepared.verificationMethod,
|
|
19651
19928
|
host: hostHint,
|
|
19652
19929
|
keyId: options?.openkeyKeyId,
|
|
19653
19930
|
presentation
|
|
19654
|
-
}
|
|
19655
|
-
|
|
19931
|
+
};
|
|
19932
|
+
let result;
|
|
19933
|
+
let signed2;
|
|
19934
|
+
let signature2;
|
|
19935
|
+
for (let attempt = 0; ; ) {
|
|
19936
|
+
try {
|
|
19937
|
+
result ?? (result = await authorizeFn(originalRequest));
|
|
19938
|
+
signed2 = await this.verifyOpenKeyResult(result, prepared);
|
|
19939
|
+
signature2 = result.signature;
|
|
19940
|
+
break;
|
|
19941
|
+
} catch (error) {
|
|
19942
|
+
if (!options?.retryResult) throw error;
|
|
19943
|
+
const next = await options.retryResult(
|
|
19944
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
19945
|
+
originalRequest,
|
|
19946
|
+
++attempt
|
|
19947
|
+
);
|
|
19948
|
+
if (next === null) throw error;
|
|
19949
|
+
result = next;
|
|
19950
|
+
}
|
|
19951
|
+
}
|
|
19952
|
+
return this.signInWithPreparedSession(signed2, signature2, keyId, jwk);
|
|
19656
19953
|
}
|
|
19657
19954
|
/**
|
|
19658
19955
|
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
@@ -19732,6 +20029,11 @@ var NodeUserAuthorization = class {
|
|
|
19732
20029
|
* (resource, action) pair in the returned SIWE.
|
|
19733
20030
|
*/
|
|
19734
20031
|
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
20032
|
+
const signed2 = await this.verifyOpenKeyResult(result, prepared);
|
|
20033
|
+
return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
|
|
20034
|
+
}
|
|
20035
|
+
/** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
|
|
20036
|
+
async verifyOpenKeyResult(result, prepared) {
|
|
19735
20037
|
if (result.protocolVersion !== 1) {
|
|
19736
20038
|
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
19737
20039
|
}
|
|
@@ -19976,17 +20278,12 @@ var NodeUserAuthorization = class {
|
|
|
19976
20278
|
}
|
|
19977
20279
|
}
|
|
19978
20280
|
void nonRequiredGrantedPairs;
|
|
19979
|
-
return
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
},
|
|
19986
|
-
result.signature,
|
|
19987
|
-
keyId,
|
|
19988
|
-
jwk
|
|
19989
|
-
);
|
|
20281
|
+
return {
|
|
20282
|
+
siwe: result.signedMessage,
|
|
20283
|
+
jwk: prepared.jwk,
|
|
20284
|
+
spaceId: prepared.spaceId,
|
|
20285
|
+
verificationMethod: prepared.verificationMethod
|
|
20286
|
+
};
|
|
19990
20287
|
}
|
|
19991
20288
|
/**
|
|
19992
20289
|
* Clear persisted session data.
|
|
@@ -20038,7 +20335,7 @@ var NodeUserAuthorization = class {
|
|
|
20038
20335
|
* Request signature via event emitter with timeout.
|
|
20039
20336
|
*/
|
|
20040
20337
|
requestSignatureViaEmitter(request, emitter, timeout) {
|
|
20041
|
-
return new Promise((
|
|
20338
|
+
return new Promise((resolve2, reject2) => {
|
|
20042
20339
|
const timeoutId = setTimeout(() => {
|
|
20043
20340
|
reject2(new Error("Sign request timed out"));
|
|
20044
20341
|
}, timeout);
|
|
@@ -20050,7 +20347,7 @@ var NodeUserAuthorization = class {
|
|
|
20050
20347
|
);
|
|
20051
20348
|
} else {
|
|
20052
20349
|
const signature2 = response.signature ?? await this.signer.signMessage(request.message);
|
|
20053
|
-
|
|
20350
|
+
resolve2(signature2);
|
|
20054
20351
|
}
|
|
20055
20352
|
};
|
|
20056
20353
|
emitter.emit("sign-request", request, respond);
|
|
@@ -20871,6 +21168,7 @@ function ownerPermissionsToAbilities(permissions) {
|
|
|
20871
21168
|
}
|
|
20872
21169
|
return { abilities, rawAbilities };
|
|
20873
21170
|
}
|
|
21171
|
+
var TERMINAL_DELEGATION_REQUIRES_SESSION_KEY = "disableSubDelegation requires a session-key delegation";
|
|
20874
21172
|
function base64UrlEncode(bytes) {
|
|
20875
21173
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
20876
21174
|
let output = "";
|
|
@@ -20982,6 +21280,20 @@ function accountIndexWarningDiagnostic(error) {
|
|
|
20982
21280
|
}
|
|
20983
21281
|
return diagnostic;
|
|
20984
21282
|
}
|
|
21283
|
+
function replicaRestoreRefusal(error) {
|
|
21284
|
+
if (error !== null && typeof error === "object" && "code" in error && error.code === "REPLICA_GATED" && "gate" in error) {
|
|
21285
|
+
switch (error.gate) {
|
|
21286
|
+
case "not_established":
|
|
21287
|
+
case "stamp_stale":
|
|
21288
|
+
case "principal_mismatch":
|
|
21289
|
+
case "purge_pending":
|
|
21290
|
+
case "logout_pending":
|
|
21291
|
+
case "gate_unavailable":
|
|
21292
|
+
return error.gate;
|
|
21293
|
+
}
|
|
21294
|
+
}
|
|
21295
|
+
return "gate_unavailable";
|
|
21296
|
+
}
|
|
20985
21297
|
var _TinyCloudNode = class _TinyCloudNode {
|
|
20986
21298
|
/**
|
|
20987
21299
|
* Create a new TinyCloudNode instance.
|
|
@@ -21011,6 +21323,37 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21011
21323
|
this.auth = null;
|
|
21012
21324
|
this.tc = null;
|
|
21013
21325
|
this._chainId = 1;
|
|
21326
|
+
/** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
|
|
21327
|
+
this.establishReplicaStamp = async (session) => {
|
|
21328
|
+
const replication = this.config.replication;
|
|
21329
|
+
if (!replication?.enabled) return void 0;
|
|
21330
|
+
const epoch = this.sessionEpoch;
|
|
21331
|
+
const attempt = this.latestSessionAttempt;
|
|
21332
|
+
try {
|
|
21333
|
+
const { verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2, establishReplicaSessionStamp: establishReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
|
|
21334
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
|
|
21335
|
+
const anchor = verifiedOwnerConsentAnchor2(session, this.wasmBindings, exactVerifiedRecap);
|
|
21336
|
+
if (anchor === null) {
|
|
21337
|
+
throw Object.assign(new Error("Replica sign-in requires a verified finite owner SIWE and exact-space KV get+sync."), {
|
|
21338
|
+
code: "REPLICA_GATED",
|
|
21339
|
+
gate: "not_established"
|
|
21340
|
+
});
|
|
21341
|
+
}
|
|
21342
|
+
const stamp = await establishReplicaSessionStamp2(
|
|
21343
|
+
replication.storage,
|
|
21344
|
+
pkhDid2(session.address, session.chainId),
|
|
21345
|
+
anchor
|
|
21346
|
+
);
|
|
21347
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt) return void 0;
|
|
21348
|
+
this.replicaSessionStamp = stamp;
|
|
21349
|
+
return stamp;
|
|
21350
|
+
} catch (error) {
|
|
21351
|
+
if (epoch === this.sessionEpoch && attempt === this.latestSessionAttempt) {
|
|
21352
|
+
this.replicaRestoreGate = replicaRestoreRefusal(error);
|
|
21353
|
+
}
|
|
21354
|
+
return void 0;
|
|
21355
|
+
}
|
|
21356
|
+
};
|
|
21014
21357
|
this._baseVaults = /* @__PURE__ */ new Map();
|
|
21015
21358
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
21016
21359
|
this._secrets = /* @__PURE__ */ new Map();
|
|
@@ -21135,7 +21478,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21135
21478
|
});
|
|
21136
21479
|
return this.wasmBindings.invokeAny(invocationSession, caveatPreservingEntries, facts);
|
|
21137
21480
|
};
|
|
21138
|
-
|
|
21481
|
+
if (config.replication?.enabled) getNodeReplicationLoaders().validateConfig(config.replication);
|
|
21139
21482
|
this.explicitHost = config.host;
|
|
21140
21483
|
this.config = {
|
|
21141
21484
|
...config,
|
|
@@ -21150,6 +21493,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21150
21493
|
this.replicationRuntime = runtime;
|
|
21151
21494
|
this.replicationControl = {
|
|
21152
21495
|
status: async () => {
|
|
21496
|
+
const gate = this.replicaRestoreGate;
|
|
21497
|
+
if (gate) return this.config.replication.prefixes.map((prefix) => ({
|
|
21498
|
+
prefix,
|
|
21499
|
+
state: "gated",
|
|
21500
|
+
gate,
|
|
21501
|
+
errorCode: "REPLICA_GATED",
|
|
21502
|
+
pending: { inFlight: 0, committed: 0, ambiguous: 0 },
|
|
21503
|
+
pinned: [],
|
|
21504
|
+
lagMs: null
|
|
21505
|
+
}));
|
|
21153
21506
|
const status = await runtime.then((ready) => ready.control.status());
|
|
21154
21507
|
const scopeError = this.replicationScopeError;
|
|
21155
21508
|
if (!scopeError) return status;
|
|
@@ -21166,7 +21519,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21166
21519
|
lagMs: null
|
|
21167
21520
|
}));
|
|
21168
21521
|
},
|
|
21169
|
-
sync: async (input) =>
|
|
21522
|
+
sync: async (input) => {
|
|
21523
|
+
if (this.replicaRestoreGate) {
|
|
21524
|
+
throw Object.assign(new Error("Replica restore is gated."), {
|
|
21525
|
+
code: "REPLICA_GATED",
|
|
21526
|
+
gate: this.replicaRestoreGate
|
|
21527
|
+
});
|
|
21528
|
+
}
|
|
21529
|
+
return (await runtime).control.sync(input);
|
|
21530
|
+
},
|
|
21170
21531
|
purge: (input) => {
|
|
21171
21532
|
const instance = this.replicationRuntimeInstance;
|
|
21172
21533
|
return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
|
|
@@ -21382,6 +21743,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21382
21743
|
signer: this.signer,
|
|
21383
21744
|
signStrategy: config.signStrategy ?? { type: "auto-sign" },
|
|
21384
21745
|
wasmBindings: this.wasmBindings,
|
|
21746
|
+
...config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
21385
21747
|
sessionManager: this.sessionManager,
|
|
21386
21748
|
sessionStorage: config.sessionStorage ?? new MemorySessionStorage(),
|
|
21387
21749
|
domain: this.siweDomain,
|
|
@@ -21579,11 +21941,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21579
21941
|
return this.signer === null;
|
|
21580
21942
|
}
|
|
21581
21943
|
/**
|
|
21582
|
-
*
|
|
21583
|
-
* Available after signIn().
|
|
21944
|
+
* Primary space of the signed-in or restored network session, including compact sessions.
|
|
21584
21945
|
*/
|
|
21585
21946
|
get spaceId() {
|
|
21586
|
-
return this.auth?.tinyCloudSession?.spaceId;
|
|
21947
|
+
return this.auth?.tinyCloudSession?.spaceId ?? this._activeServiceSession?.spaceId;
|
|
21587
21948
|
}
|
|
21588
21949
|
/**
|
|
21589
21950
|
* Get the account space ID for this wallet identity.
|
|
@@ -21631,6 +21992,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21631
21992
|
get restorableSession() {
|
|
21632
21993
|
return this.currentTinyCloudSession();
|
|
21633
21994
|
}
|
|
21995
|
+
/** The active, verified replica stamp. Undefined when the replica is refused or the session has ended. */
|
|
21996
|
+
get replicaStamp() {
|
|
21997
|
+
const stamp = this.replicaSessionStamp;
|
|
21998
|
+
if (!stamp || this.replicaRestoreGate !== void 0 || !this.currentTinyCloudSession()) return void 0;
|
|
21999
|
+
return { incarnation: stamp.incarnation, lifecycleEpoch: stamp.lifecycleEpoch };
|
|
22000
|
+
}
|
|
21634
22001
|
/**
|
|
21635
22002
|
* Sign out and end the session on this instance.
|
|
21636
22003
|
*
|
|
@@ -21651,18 +22018,60 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21651
22018
|
}
|
|
21652
22019
|
async endSession() {
|
|
21653
22020
|
const leavingAddress = this.address ?? this._restoredTcSession?.address;
|
|
22021
|
+
const leavingChainId = this._chainId;
|
|
22022
|
+
const leavingPrincipal = this.replicaSessionPrincipal ?? (leavingAddress ? pkhDid2(leavingAddress, leavingChainId) : void 0);
|
|
21654
22023
|
const leavingAuth = this.auth;
|
|
22024
|
+
const leavingContext = this._serviceContext;
|
|
22025
|
+
let denied;
|
|
21655
22026
|
try {
|
|
21656
22027
|
this.teardownSession();
|
|
21657
22028
|
} finally {
|
|
22029
|
+
const denial = leavingPrincipal ? this.denyReplicaGate({ reason: "sign_out", principal: leavingPrincipal }, leavingContext) : Promise.resolve(void 0);
|
|
21658
22030
|
try {
|
|
21659
|
-
await
|
|
21660
|
-
if (this.replicationRuntime) await (await this.replicationRuntime).unbind();
|
|
22031
|
+
denied = await denial;
|
|
21661
22032
|
} finally {
|
|
21662
|
-
|
|
21663
|
-
await
|
|
22033
|
+
try {
|
|
22034
|
+
await this.accountRegistryTail;
|
|
22035
|
+
await this.quiesceReplicaRuntime();
|
|
22036
|
+
} finally {
|
|
22037
|
+
if (leavingAddress) {
|
|
22038
|
+
await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
|
|
22039
|
+
}
|
|
22040
|
+
}
|
|
22041
|
+
}
|
|
22042
|
+
}
|
|
22043
|
+
if (denied?.status === "unguarded") {
|
|
22044
|
+
throw new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`);
|
|
22045
|
+
}
|
|
22046
|
+
}
|
|
22047
|
+
/**
|
|
22048
|
+
* Step 1 of replica sign-out. Never rejects: failures come back as `unguarded` so the session
|
|
22049
|
+
* is still cleared. Returns `undefined` when replication is off.
|
|
22050
|
+
*/
|
|
22051
|
+
async denyReplicaGate(input, context = this._serviceContext) {
|
|
22052
|
+
const replication = this.config.replication;
|
|
22053
|
+
if (!replication?.enabled) return void 0;
|
|
22054
|
+
try {
|
|
22055
|
+
const lifecycle = replication.storage.lifecycle;
|
|
22056
|
+
if (lifecycle?.kind !== (replication.storage.kind === "indexeddb" ? "indexeddb" : "file")) {
|
|
22057
|
+
return { status: "unguarded", code: "gate_unavailable" };
|
|
22058
|
+
}
|
|
22059
|
+
const result = await lifecycle.deny(input);
|
|
22060
|
+
if (result.status === "denied" || result.status === "deleted" || result.status === "marker") {
|
|
22061
|
+
try {
|
|
22062
|
+
(await getNodeReplicationLoaders().authority()).reportReplicaDenial(
|
|
22063
|
+
result,
|
|
22064
|
+
input.reason,
|
|
22065
|
+
replication.onEvent,
|
|
22066
|
+
context
|
|
22067
|
+
);
|
|
22068
|
+
} catch {
|
|
21664
22069
|
}
|
|
21665
22070
|
}
|
|
22071
|
+
return result;
|
|
22072
|
+
} catch (error) {
|
|
22073
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable";
|
|
22074
|
+
return { status: "unguarded", code };
|
|
21666
22075
|
}
|
|
21667
22076
|
}
|
|
21668
22077
|
/**
|
|
@@ -21682,15 +22091,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21682
22091
|
this._activeServiceSession = void 0;
|
|
21683
22092
|
oldGraph.retire();
|
|
21684
22093
|
oldCore?.retireServices();
|
|
21685
|
-
oldAuth?.resetSession(
|
|
21686
|
-
this.sessionManager,
|
|
21687
|
-
this.sessionKeyId,
|
|
21688
|
-
this.explicitHost ? [this.explicitHost] : void 0
|
|
21689
|
-
);
|
|
21690
22094
|
this.config.host = this.explicitHost ?? DEFAULT_HOST;
|
|
21691
22095
|
this._chainId = 1;
|
|
21692
22096
|
this._restoredNodeFeatures = void 0;
|
|
21693
|
-
this._activeServiceSession = void 0;
|
|
21694
22097
|
this._serviceContext = void 0;
|
|
21695
22098
|
this._kv = void 0;
|
|
21696
22099
|
this._sql = void 0;
|
|
@@ -21715,6 +22118,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21715
22118
|
this._bootstrapStatus = { skipped: false };
|
|
21716
22119
|
this.replicationDelegateSession = void 0;
|
|
21717
22120
|
this.replicationScopeError = void 0;
|
|
22121
|
+
this.replicaSessionPrincipal = void 0;
|
|
22122
|
+
this.replicaSessionStamp = void 0;
|
|
22123
|
+
this.replicaRestoreGate = void 0;
|
|
21718
22124
|
const sessionKey = createDefaultSessionKey(this.wasmBindings);
|
|
21719
22125
|
this.sessionManager = sessionKey.manager;
|
|
21720
22126
|
this.sessionKeyId = sessionKey.keyId;
|
|
@@ -21724,6 +22130,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21724
22130
|
sessionKey.keyId,
|
|
21725
22131
|
this.explicitHost ? [this.explicitHost] : void 0
|
|
21726
22132
|
);
|
|
22133
|
+
this.replicaSessionPrincipal = void 0;
|
|
22134
|
+
this.replicaSessionStamp = void 0;
|
|
22135
|
+
this.replicaRestoreGate = void 0;
|
|
21727
22136
|
this._serviceGraph = this.createServiceGraphLifetime();
|
|
21728
22137
|
this.auth = oldAuth;
|
|
21729
22138
|
this.tc = this.auth ? new TinyCloud(this.auth, {
|
|
@@ -21753,11 +22162,25 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21753
22162
|
"Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
|
|
21754
22163
|
);
|
|
21755
22164
|
}
|
|
22165
|
+
await this.signInWith(() => this.tc.signIn(options));
|
|
22166
|
+
}
|
|
22167
|
+
/**
|
|
22168
|
+
* Fresh OpenKey sign-in. The callback receives the SDK's original SIWE and
|
|
22169
|
+
* session JWK before approval; the signed result is verified against those
|
|
22170
|
+
* bytes before the network session or replica stamp is committed.
|
|
22171
|
+
*/
|
|
22172
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
22173
|
+
if (!this.signer || !this.tc || !this.auth) {
|
|
22174
|
+
throw new Error("Cannot signInWithOpenKey() in session-only mode. Provide a signer in config.");
|
|
22175
|
+
}
|
|
22176
|
+
await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
|
|
22177
|
+
}
|
|
22178
|
+
async signInWith(performSignIn) {
|
|
21756
22179
|
const epoch = this.sessionEpoch;
|
|
21757
22180
|
const attempt = ++this.latestSessionAttempt;
|
|
21758
22181
|
await this.signOutInFlight;
|
|
21759
22182
|
try {
|
|
21760
|
-
await this.establishSignIn(epoch,
|
|
22183
|
+
await this.establishSignIn(epoch, performSignIn);
|
|
21761
22184
|
} catch (error) {
|
|
21762
22185
|
if (epoch !== this.sessionEpoch && attempt === this.latestSessionAttempt) {
|
|
21763
22186
|
await this.endSession();
|
|
@@ -21765,7 +22188,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21765
22188
|
throw error;
|
|
21766
22189
|
}
|
|
21767
22190
|
}
|
|
21768
|
-
async establishSignIn(epoch,
|
|
22191
|
+
async establishSignIn(epoch, performSignIn) {
|
|
21769
22192
|
if (!this.signer || !this.tc) {
|
|
21770
22193
|
throw new Error("Not signed in. Call signIn() first.");
|
|
21771
22194
|
}
|
|
@@ -21775,6 +22198,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21775
22198
|
const address = canonicalizeAddress2(await this.signer.getAddress());
|
|
21776
22199
|
const chainId = await this.signer.getChainId();
|
|
21777
22200
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
22201
|
+
const nextPrincipal = pkhDid2(address, chainId);
|
|
22202
|
+
const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? pkhDid2(this.address, this._chainId) : void 0);
|
|
22203
|
+
if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
|
|
22204
|
+
const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
|
|
22205
|
+
this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
|
|
22206
|
+
}
|
|
21778
22207
|
if (address !== this._address || chainId !== this._chainId) this.storageFull.clear();
|
|
21779
22208
|
this._address = address;
|
|
21780
22209
|
this._chainId = chainId;
|
|
@@ -21792,7 +22221,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21792
22221
|
this.runtimePermissionGrants = [];
|
|
21793
22222
|
this.confirmedHostedSpaceIds.clear();
|
|
21794
22223
|
this.hostedOwnedSpaceIds.clear();
|
|
21795
|
-
|
|
22224
|
+
const signInAttempt = this.latestSessionAttempt;
|
|
22225
|
+
const previousReplicaGate = this.replicaRestoreGate;
|
|
22226
|
+
const previousReplicaStamp = this.replicaSessionStamp;
|
|
22227
|
+
this.replicaRestoreGate = void 0;
|
|
22228
|
+
this.replicaSessionStamp = void 0;
|
|
22229
|
+
try {
|
|
22230
|
+
await performSignIn();
|
|
22231
|
+
} catch (error) {
|
|
22232
|
+
if (epoch === this.sessionEpoch && signInAttempt === this.latestSessionAttempt) {
|
|
22233
|
+
this.replicaRestoreGate = previousReplicaGate;
|
|
22234
|
+
this.replicaSessionStamp = previousReplicaStamp;
|
|
22235
|
+
}
|
|
22236
|
+
throw error;
|
|
22237
|
+
}
|
|
21796
22238
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
21797
22239
|
this.cancelOwnedSpaceHosts();
|
|
21798
22240
|
this.syncResolvedHostFromAuth();
|
|
@@ -21800,8 +22242,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21800
22242
|
this.hostedOwnedSpaceIds.add(spaceId);
|
|
21801
22243
|
}
|
|
21802
22244
|
this.replicationDelegateSession = void 0;
|
|
22245
|
+
this.replicaSessionPrincipal = pkhDid2(address, chainId);
|
|
21803
22246
|
this.replicationScopeError = void 0;
|
|
21804
|
-
|
|
22247
|
+
await this.quiesceReplicaRuntime();
|
|
21805
22248
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
21806
22249
|
const oldGraph = this._serviceGraph;
|
|
21807
22250
|
this._serviceGraph = this.createServiceGraphLifetime();
|
|
@@ -22053,7 +22496,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22053
22496
|
);
|
|
22054
22497
|
}
|
|
22055
22498
|
}
|
|
22056
|
-
await new Promise((
|
|
22499
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
22057
22500
|
for (const step of steps) {
|
|
22058
22501
|
if (step.kind !== "activate") continue;
|
|
22059
22502
|
graph.assertActive();
|
|
@@ -22388,6 +22831,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22388
22831
|
if (this.ownStorageFull) {
|
|
22389
22832
|
return;
|
|
22390
22833
|
}
|
|
22834
|
+
if (!this.currentSessionCoversAccountRegistry()) {
|
|
22835
|
+
return;
|
|
22836
|
+
}
|
|
22391
22837
|
const controller = new AbortController();
|
|
22392
22838
|
const deadline = setTimeout(
|
|
22393
22839
|
() => controller.abort(new DOMException("Account registry deadline exceeded", "TimeoutError")),
|
|
@@ -22502,6 +22948,27 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22502
22948
|
(operation) => operation.service === "space" && this.actionContains(operation.action, "tinycloud.space/list")
|
|
22503
22949
|
);
|
|
22504
22950
|
}
|
|
22951
|
+
/**
|
|
22952
|
+
* Whether the current primary session's recap covers the account index read
|
|
22953
|
+
* (`tinycloud.sql/read` on the owned account space's `account` database),
|
|
22954
|
+
* the first call of every registry sync. As in
|
|
22955
|
+
* {@link currentSessionCanListSpaces}, a session with no parseable recap
|
|
22956
|
+
* keeps today's behavior.
|
|
22957
|
+
*/
|
|
22958
|
+
currentSessionCoversAccountRegistry() {
|
|
22959
|
+
const session = this.currentTinyCloudSession();
|
|
22960
|
+
const operations = session ? this.recapOperationsFromSession(session) : [];
|
|
22961
|
+
if (operations.length === 0) {
|
|
22962
|
+
return true;
|
|
22963
|
+
}
|
|
22964
|
+
const indexRead = {
|
|
22965
|
+
spaceId: this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2),
|
|
22966
|
+
service: "sql",
|
|
22967
|
+
path: "account",
|
|
22968
|
+
action: "tinycloud.sql/read"
|
|
22969
|
+
};
|
|
22970
|
+
return operations.some((granted) => this.operationCovers(granted, indexRead));
|
|
22971
|
+
}
|
|
22505
22972
|
async withAccountRegistryRetry(task, signal) {
|
|
22506
22973
|
const delays = [250, 1e3, 3e3];
|
|
22507
22974
|
const stopped = (detail) => {
|
|
@@ -22542,10 +23009,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22542
23009
|
}
|
|
22543
23010
|
lastError = error;
|
|
22544
23011
|
if (attempt < delays.length - 1) {
|
|
22545
|
-
await new Promise((
|
|
23012
|
+
await new Promise((resolve2, reject2) => {
|
|
22546
23013
|
const timer = setTimeout(() => {
|
|
22547
23014
|
signal.removeEventListener("abort", onAbort);
|
|
22548
|
-
|
|
23015
|
+
resolve2();
|
|
22549
23016
|
}, delays[attempt]);
|
|
22550
23017
|
const onAbort = () => {
|
|
22551
23018
|
clearTimeout(timer);
|
|
@@ -22785,7 +23252,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22785
23252
|
}
|
|
22786
23253
|
entry.waiters += 1;
|
|
22787
23254
|
const shared = entry;
|
|
22788
|
-
return new Promise((
|
|
23255
|
+
return new Promise((resolve2, reject2) => {
|
|
22789
23256
|
const onAbort = () => {
|
|
22790
23257
|
reject2(signal.reason);
|
|
22791
23258
|
shared.waiters -= 1;
|
|
@@ -22794,7 +23261,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22794
23261
|
if (this.ownedSpaceHosts.get(spaceId) === shared) this.ownedSpaceHosts.delete(spaceId);
|
|
22795
23262
|
};
|
|
22796
23263
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
22797
|
-
shared.promise.then(
|
|
23264
|
+
shared.promise.then(resolve2, reject2).finally(() => signal.removeEventListener("abort", onAbort));
|
|
22798
23265
|
});
|
|
22799
23266
|
}
|
|
22800
23267
|
/** Session replacement: cancel the replaced session's in-flight hosts and forget them. */
|
|
@@ -22968,11 +23435,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22968
23435
|
);
|
|
22969
23436
|
const stagedHost = resolvedHost ?? this.config.host;
|
|
22970
23437
|
let restoredNodeFeatures;
|
|
23438
|
+
let replicaProtocolMismatch = false;
|
|
22971
23439
|
if (this.config.replication?.enabled) {
|
|
22972
23440
|
try {
|
|
22973
23441
|
restoredNodeFeatures = (await checkNodeInfo2(stagedHost, this.wasmBindings.protocolVersion())).features;
|
|
22974
23442
|
} catch (error) {
|
|
22975
|
-
|
|
23443
|
+
replicaProtocolMismatch = error instanceof ProtocolMismatchError;
|
|
22976
23444
|
}
|
|
22977
23445
|
}
|
|
22978
23446
|
const stagedAddress = restoredAddress;
|
|
@@ -22980,28 +23448,55 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22980
23448
|
let replicationPrincipal;
|
|
22981
23449
|
let compactReplicationSession;
|
|
22982
23450
|
let replicationScopeError;
|
|
23451
|
+
let replicationScopeUnavailable = false;
|
|
22983
23452
|
if (this.config.replication?.enabled) {
|
|
22984
23453
|
if (stagedAddress !== void 0) {
|
|
22985
23454
|
replicationPrincipal = pkhDid2(stagedAddress, stagedChainId);
|
|
22986
23455
|
} 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 = {
|
|
23456
|
+
try {
|
|
23457
|
+
const { replicationScopeFromSignedSession: replicationScopeFromSignedSession2 } = await getNodeReplicationLoaders().authority();
|
|
23458
|
+
const signedScope = replicationScopeFromSignedSession2({
|
|
22999
23459
|
delegationHeader: sessionData.delegationHeader,
|
|
23000
23460
|
delegationCid: sessionData.delegationCid,
|
|
23001
|
-
spaceId: signedScope.space,
|
|
23002
23461
|
verificationMethod: restoredVerificationMethod,
|
|
23003
|
-
|
|
23004
|
-
};
|
|
23462
|
+
persistedSpace: sessionData.spaceId
|
|
23463
|
+
});
|
|
23464
|
+
if (!signedScope) {
|
|
23465
|
+
replicationScopeError = "Persisted space does not match the locally verified delegation scope.";
|
|
23466
|
+
} else {
|
|
23467
|
+
replicationPrincipal = signedScope.principal;
|
|
23468
|
+
compactReplicationSession = {
|
|
23469
|
+
delegationHeader: sessionData.delegationHeader,
|
|
23470
|
+
delegationCid: sessionData.delegationCid,
|
|
23471
|
+
spaceId: signedScope.space,
|
|
23472
|
+
verificationMethod: restoredVerificationMethod,
|
|
23473
|
+
jwk: stagedJwk
|
|
23474
|
+
};
|
|
23475
|
+
}
|
|
23476
|
+
} catch {
|
|
23477
|
+
replicationScopeUnavailable = true;
|
|
23478
|
+
replicationScopeError = "Replica scope could not be verified for this network session.";
|
|
23479
|
+
}
|
|
23480
|
+
}
|
|
23481
|
+
}
|
|
23482
|
+
let replicaRestoreGate;
|
|
23483
|
+
let restoredReplicaStamp;
|
|
23484
|
+
const replication = this.config.replication;
|
|
23485
|
+
if (replication?.enabled) {
|
|
23486
|
+
if (replicaProtocolMismatch) {
|
|
23487
|
+
replicaRestoreGate = "gate_unavailable";
|
|
23488
|
+
} else if (replicationPrincipal === void 0) {
|
|
23489
|
+
replicaRestoreGate = replicationScopeUnavailable ? "gate_unavailable" : "principal_mismatch";
|
|
23490
|
+
} else {
|
|
23491
|
+
try {
|
|
23492
|
+
const { restoreReplicaSessionStamp: restoreReplicaSessionStamp2 } = await getNodeReplicationLoaders().authority();
|
|
23493
|
+
restoredReplicaStamp = await restoreReplicaSessionStamp2(
|
|
23494
|
+
replication.storage,
|
|
23495
|
+
replicationPrincipal,
|
|
23496
|
+
sessionData.replicaStamp ?? void 0
|
|
23497
|
+
);
|
|
23498
|
+
} catch (error) {
|
|
23499
|
+
replicaRestoreGate = replicaRestoreRefusal(error);
|
|
23005
23500
|
}
|
|
23006
23501
|
}
|
|
23007
23502
|
}
|
|
@@ -23094,7 +23589,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23094
23589
|
}
|
|
23095
23590
|
this.replicationDelegateSession = compactReplicationSession;
|
|
23096
23591
|
this.replicationScopeError = replicationScopeError;
|
|
23097
|
-
|
|
23592
|
+
this.replicaRestoreGate = replicaRestoreGate;
|
|
23593
|
+
this.replicaSessionPrincipal = replicationPrincipal;
|
|
23594
|
+
this.replicaSessionStamp = restoredReplicaStamp;
|
|
23595
|
+
await this.quiesceReplicaRuntime();
|
|
23098
23596
|
if (epoch !== this.sessionEpoch) {
|
|
23099
23597
|
oldGraph.retire();
|
|
23100
23598
|
oldCore?.retireServices?.();
|
|
@@ -23109,7 +23607,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23109
23607
|
detail: replicationScopeError
|
|
23110
23608
|
});
|
|
23111
23609
|
}
|
|
23112
|
-
await this.
|
|
23610
|
+
await this.bindReplicaOrGate(stagedGraph.serviceContext, serviceSession, stagedGraph.kv, replicationPrincipal);
|
|
23113
23611
|
oldCore?.retireServices?.();
|
|
23114
23612
|
if (epoch !== this.sessionEpoch) {
|
|
23115
23613
|
if (attempt === this.latestSessionAttempt) await this.endSession();
|
|
@@ -23242,6 +23740,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23242
23740
|
session: input.serviceSession,
|
|
23243
23741
|
invoke: graph.invoke,
|
|
23244
23742
|
invokeAny: graph.invokeAny,
|
|
23743
|
+
signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
|
|
23245
23744
|
fetch: graph.fetch
|
|
23246
23745
|
});
|
|
23247
23746
|
const keyProvider = new WasmKeyProvider({ sessionManager: input.manager });
|
|
@@ -23585,6 +24084,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23585
24084
|
signer: this.signer,
|
|
23586
24085
|
signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
|
|
23587
24086
|
wasmBindings: this.wasmBindings,
|
|
24087
|
+
...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
23588
24088
|
sessionManager: this.sessionManager,
|
|
23589
24089
|
sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
|
|
23590
24090
|
domain: this.siweDomain,
|
|
@@ -23643,6 +24143,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23643
24143
|
signer: this.signer,
|
|
23644
24144
|
signStrategy: this.config.signStrategy ?? { type: "auto-sign" },
|
|
23645
24145
|
wasmBindings: this.wasmBindings,
|
|
24146
|
+
...this.config.replication?.enabled === true ? { establishReplicaStamp: this.establishReplicaStamp } : {},
|
|
23646
24147
|
sessionManager: this.sessionManager,
|
|
23647
24148
|
sessionStorage: options?.sessionStorage ?? this.config.sessionStorage ?? new MemorySessionStorage(),
|
|
23648
24149
|
domain: this.siweDomain,
|
|
@@ -23723,7 +24224,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23723
24224
|
jwk: session.jwk
|
|
23724
24225
|
};
|
|
23725
24226
|
this._serviceContext.setSession(serviceSession);
|
|
23726
|
-
await this.
|
|
24227
|
+
await this.bindReplicaOrGate(this._serviceContext, serviceSession, this._kv);
|
|
23727
24228
|
this.tc.serviceContext.setSession(serviceSession);
|
|
23728
24229
|
this._vault = this.createVaultService(session.spaceId, this._kv);
|
|
23729
24230
|
this._vault.initialize(this._serviceContext);
|
|
@@ -24117,6 +24618,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24117
24618
|
session: serviceSession,
|
|
24118
24619
|
invoke: graph.invoke,
|
|
24119
24620
|
invokeAny: graph.invokeAny,
|
|
24621
|
+
signOwnerRevocation: this.signer ? (cid) => this.signOwnerRevocation(cid) : void 0,
|
|
24120
24622
|
fetch: graph.fetch
|
|
24121
24623
|
});
|
|
24122
24624
|
this._spaceService = new SpaceService({
|
|
@@ -24252,11 +24754,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24252
24754
|
* Both SharingService (single-entry) and
|
|
24253
24755
|
* {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
|
|
24254
24756
|
* the same code path so there's exactly one place that touches the
|
|
24255
|
-
* WASM boundary.
|
|
24757
|
+
* WASM boundary. `terminal` adds the node's terminal marker, which the
|
|
24758
|
+
* WASM builder cannot emit.
|
|
24256
24759
|
*
|
|
24257
24760
|
* @internal
|
|
24258
24761
|
*/
|
|
24259
|
-
createDelegationWrapper(params) {
|
|
24762
|
+
createDelegationWrapper(params, terminal = false) {
|
|
24260
24763
|
const wasmSession = {
|
|
24261
24764
|
delegationHeader: params.session.delegationHeader,
|
|
24262
24765
|
delegationCid: params.session.delegationCid,
|
|
@@ -24276,8 +24779,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24276
24779
|
params.notBeforeSecs
|
|
24277
24780
|
);
|
|
24278
24781
|
return {
|
|
24279
|
-
delegation: result.delegation,
|
|
24280
|
-
cid: result.cid,
|
|
24782
|
+
...terminal ? markDelegationTerminal(result.delegation, params.session.jwk) : { delegation: result.delegation, cid: result.cid },
|
|
24281
24783
|
// Rust serde `rename_all = "camelCase"` emits `delegateDid`
|
|
24282
24784
|
// (lowercase d); the TypeScript interface uses `delegateDID`
|
|
24283
24785
|
// (historical, matches Delegation.delegateDID). Normalize here.
|
|
@@ -24419,7 +24921,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24419
24921
|
};
|
|
24420
24922
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalizeEncryptionJson(body)))));
|
|
24421
24923
|
const request = { ...body, requestBodyDigest };
|
|
24422
|
-
const authorization = authorizationHeader(this.
|
|
24924
|
+
const authorization = authorizationHeader(this.wasmBindings.invoke(serviceSession, "kv", input.resourcePath, "tinycloud.kv/get"));
|
|
24423
24925
|
const response = await fetch(`${this.config.host}/policy/v3/deliveries/authorize`, {
|
|
24424
24926
|
method: "POST",
|
|
24425
24927
|
headers: { accept: "application/json", "content-type": "application/json", authorization },
|
|
@@ -24734,6 +25236,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24734
25236
|
}
|
|
24735
25237
|
};
|
|
24736
25238
|
}
|
|
25239
|
+
if (result.status === "aad_mismatch") return { status: "read_failed" };
|
|
24737
25240
|
return { status: result.status };
|
|
24738
25241
|
}
|
|
24739
25242
|
if (!isSecretPayload(result.entry.value)) {
|
|
@@ -25409,7 +25912,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25409
25912
|
return this.delegationManager.create(params);
|
|
25410
25913
|
}
|
|
25411
25914
|
/**
|
|
25412
|
-
* Revoke a delegation
|
|
25915
|
+
* Revoke a delegation with the owner wallet when available. Session-only
|
|
25916
|
+
* clients continue to use their delegated revocation authority.
|
|
25413
25917
|
*
|
|
25414
25918
|
* @param cid - The CID of the delegation to revoke
|
|
25415
25919
|
* @returns Result indicating success or failure
|
|
@@ -25525,6 +26029,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25525
26029
|
effectiveExpiration = sessionExpiry;
|
|
25526
26030
|
}
|
|
25527
26031
|
if (options?.forceWalletSign) {
|
|
26032
|
+
if (options.disableSubDelegation) {
|
|
26033
|
+
throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
|
|
26034
|
+
}
|
|
25528
26035
|
if (expandedEntries.length > 1) {
|
|
25529
26036
|
throw new Error(
|
|
25530
26037
|
"delegateTo with forceWalletSign=true supports at most one PermissionEntry. Multi-entry requests must go through the session-key UCAN path (drop forceWalletSign) or the legacy createDelegation method."
|
|
@@ -25560,7 +26067,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25560
26067
|
plan.effectiveExpiration,
|
|
25561
26068
|
plan.grant,
|
|
25562
26069
|
plan.operations,
|
|
25563
|
-
onPrepared
|
|
26070
|
+
onPrepared,
|
|
26071
|
+
options?.disableSubDelegation === true
|
|
25564
26072
|
);
|
|
25565
26073
|
return { delegation: delegation2, prompted: false };
|
|
25566
26074
|
}
|
|
@@ -25569,7 +26077,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25569
26077
|
expandedEntries,
|
|
25570
26078
|
effectiveExpiration,
|
|
25571
26079
|
session,
|
|
25572
|
-
onPrepared
|
|
26080
|
+
onPrepared,
|
|
26081
|
+
options?.disableSubDelegation === true
|
|
25573
26082
|
);
|
|
25574
26083
|
return { delegation, prompted: false };
|
|
25575
26084
|
}
|
|
@@ -25684,34 +26193,67 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25684
26193
|
return auth.replicationSignInEntries();
|
|
25685
26194
|
}
|
|
25686
26195
|
async replicationAuthority() {
|
|
25687
|
-
const { createReplicationAuthority: createReplicationAuthority2 } = await getNodeReplicationLoaders().authority();
|
|
26196
|
+
const { createReplicationAuthority: createReplicationAuthority2, mintNodeReplicationGrant: mintNodeReplicationGrant2, verifiedOwnerConsentAnchor: verifiedOwnerConsentAnchor2 } = await getNodeReplicationLoaders().authority();
|
|
25688
26197
|
return createReplicationAuthority2({
|
|
25689
26198
|
replicationSession: () => this.currentTinyCloudSession() ?? this.replicationDelegateSession,
|
|
26199
|
+
verifiedConsentAnchor: (session) => {
|
|
26200
|
+
const current = this.currentTinyCloudSession();
|
|
26201
|
+
return session === current && current !== void 0 ? verifiedOwnerConsentAnchor2(current, this.wasmBindings, exactVerifiedRecap) : null;
|
|
26202
|
+
},
|
|
25690
26203
|
siweExpiration: (siwe) => extractSiweExpiration(siwe),
|
|
25691
26204
|
planDelegation: (entries, options) => this.planDelegation(entries, options),
|
|
25692
|
-
mintDelegation:
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
26205
|
+
mintDelegation: (deviceDid, entries, options) => mintNodeReplicationGrant2(
|
|
26206
|
+
deviceDid,
|
|
26207
|
+
entries,
|
|
26208
|
+
options,
|
|
26209
|
+
options?.renewalOnly ? this.currentTinyCloudSession() : void 0,
|
|
26210
|
+
this
|
|
26211
|
+
)
|
|
25699
26212
|
});
|
|
25700
26213
|
}
|
|
25701
|
-
async
|
|
26214
|
+
async quiesceReplicaRuntime() {
|
|
25702
26215
|
if (!this.replicationRuntime) return;
|
|
25703
|
-
|
|
25704
|
-
|
|
25705
|
-
|
|
25706
|
-
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
26216
|
+
try {
|
|
26217
|
+
await (await this.replicationRuntime).unbind();
|
|
26218
|
+
} catch {
|
|
26219
|
+
this.replicaRestoreGate = "gate_unavailable";
|
|
26220
|
+
}
|
|
26221
|
+
}
|
|
26222
|
+
async bindReplicaOrGate(context, session, kv, principal) {
|
|
26223
|
+
if (!this.replicationRuntime) return;
|
|
26224
|
+
const gate = this.replicaRestoreGate;
|
|
26225
|
+
const epoch = this.sessionEpoch;
|
|
26226
|
+
const attempt = this.latestSessionAttempt;
|
|
26227
|
+
try {
|
|
26228
|
+
const runtime = await this.replicationRuntime;
|
|
26229
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context || gate !== this.replicaRestoreGate) return;
|
|
26230
|
+
if (gate) {
|
|
26231
|
+
await runtime.bindGatedNetworkOnly({
|
|
26232
|
+
context,
|
|
26233
|
+
session,
|
|
26234
|
+
gate,
|
|
26235
|
+
primaryKV: [{ space: session.spaceId, kv }]
|
|
26236
|
+
}, getNodeReplicationLoaders().gatedNetwork);
|
|
26237
|
+
return;
|
|
26238
|
+
}
|
|
26239
|
+
const identityPrincipal = principal ?? (this._address === void 0 ? void 0 : pkhDid2(this._address, this._chainId));
|
|
26240
|
+
if (identityPrincipal === void 0) return;
|
|
26241
|
+
const authority = await this.replicationAuthority();
|
|
26242
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
|
|
26243
|
+
await runtime.bind({
|
|
26244
|
+
context,
|
|
26245
|
+
session,
|
|
26246
|
+
principal: identityPrincipal,
|
|
26247
|
+
stamp: this.replicaSessionStamp,
|
|
26248
|
+
authority,
|
|
26249
|
+
nodeFeatures: this.replicationNodeFeatures,
|
|
26250
|
+
primaryKV: [{ space: session.spaceId, kv }]
|
|
26251
|
+
});
|
|
26252
|
+
} catch {
|
|
26253
|
+
if (epoch !== this.sessionEpoch || attempt !== this.latestSessionAttempt || this._kv !== kv || this._serviceContext !== context) return;
|
|
26254
|
+
this.replicaRestoreGate = "gate_unavailable";
|
|
26255
|
+
await this.quiesceReplicaRuntime();
|
|
26256
|
+
}
|
|
25715
26257
|
}
|
|
25716
26258
|
/** Stable control facade for this node's optional replication runtime. */
|
|
25717
26259
|
get replication() {
|
|
@@ -25767,7 +26309,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25767
26309
|
*
|
|
25768
26310
|
* @internal
|
|
25769
26311
|
*/
|
|
25770
|
-
async createDelegationViaWasmPath(did, entries, expirationTime, session, onPrepared) {
|
|
26312
|
+
async createDelegationViaWasmPath(did, entries, expirationTime, session, onPrepared, disableSubDelegation = false) {
|
|
25771
26313
|
if (entries.length === 0) {
|
|
25772
26314
|
throw new Error(
|
|
25773
26315
|
"createDelegationViaWasmPath requires a non-empty entries array"
|
|
@@ -25827,7 +26369,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25827
26369
|
spaceId,
|
|
25828
26370
|
abilities,
|
|
25829
26371
|
expirationSecs
|
|
25830
|
-
});
|
|
26372
|
+
}, disableSubDelegation);
|
|
25831
26373
|
const primary = result.resources[0];
|
|
25832
26374
|
const delegationHeader = { Authorization: result.delegation };
|
|
25833
26375
|
const delegation = {
|
|
@@ -25837,7 +26379,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25837
26379
|
path: primary.path,
|
|
25838
26380
|
actions: primary.actions,
|
|
25839
26381
|
resources: result.resources,
|
|
25840
|
-
disableSubDelegation
|
|
26382
|
+
disableSubDelegation,
|
|
25841
26383
|
expiry: result.expiry,
|
|
25842
26384
|
delegateDID: did,
|
|
25843
26385
|
ownerAddress: session.address,
|
|
@@ -25859,7 +26401,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25859
26401
|
}
|
|
25860
26402
|
return delegation;
|
|
25861
26403
|
}
|
|
25862
|
-
async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations, onPrepared) {
|
|
26404
|
+
async createDelegationViaRuntimeGrant(did, entries, expirationTime, grant, requestedOperations, onPrepared, disableSubDelegation = false) {
|
|
25863
26405
|
this.assertRuntimeGrantCaveatsPreservable(entries, requestedOperations, grant);
|
|
25864
26406
|
this.assertDelegationCaveatsPreservable(entries);
|
|
25865
26407
|
const result = this.createDelegationWrapper({
|
|
@@ -25868,7 +26410,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25868
26410
|
spaceId: grant.session.spaceId,
|
|
25869
26411
|
abilities: this.permissionsToAbilities(entries),
|
|
25870
26412
|
expirationSecs: Math.floor(expirationTime.getTime() / 1e3)
|
|
25871
|
-
});
|
|
26413
|
+
}, disableSubDelegation);
|
|
25872
26414
|
const primary = result.resources[0];
|
|
25873
26415
|
const delegationHeader = { Authorization: result.delegation };
|
|
25874
26416
|
const targetHost = grant.delegation.host ?? this.config.host;
|
|
@@ -25879,7 +26421,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25879
26421
|
path: primary.path,
|
|
25880
26422
|
actions: primary.actions,
|
|
25881
26423
|
resources: result.resources,
|
|
25882
|
-
disableSubDelegation
|
|
26424
|
+
disableSubDelegation,
|
|
25883
26425
|
expiry: result.expiry,
|
|
25884
26426
|
delegateDID: did,
|
|
25885
26427
|
ownerAddress: grant.delegation.ownerAddress,
|
|
@@ -26463,11 +27005,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26463
27005
|
params.spaceIdOverride
|
|
26464
27006
|
);
|
|
26465
27007
|
try {
|
|
26466
|
-
const result = await this.delegateTo(
|
|
26467
|
-
|
|
26468
|
-
|
|
26469
|
-
|
|
26470
|
-
);
|
|
27008
|
+
const result = await this.delegateTo(resolvedDelegateDID, entries, {
|
|
27009
|
+
...params.expiryMs !== void 0 ? { expiry: params.expiryMs } : {},
|
|
27010
|
+
disableSubDelegation: params.disableSubDelegation
|
|
27011
|
+
});
|
|
26471
27012
|
return result.delegation;
|
|
26472
27013
|
} catch (err2) {
|
|
26473
27014
|
if (err2 instanceof PermissionNotInManifestError) {
|
|
@@ -26488,6 +27029,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26488
27029
|
* @internal
|
|
26489
27030
|
*/
|
|
26490
27031
|
async createDelegationWalletPath(params) {
|
|
27032
|
+
if (params.disableSubDelegation) {
|
|
27033
|
+
throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
|
|
27034
|
+
}
|
|
26491
27035
|
if (!this.signer) {
|
|
26492
27036
|
throw new Error("Cannot createDelegation() in session-only mode. Requires wallet mode.");
|
|
26493
27037
|
}
|
|
@@ -26543,7 +27087,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26543
27087
|
spaceId: params.spaceIdOverride ?? session.spaceId,
|
|
26544
27088
|
path: params.path,
|
|
26545
27089
|
actions: params.actions,
|
|
26546
|
-
disableSubDelegation:
|
|
27090
|
+
disableSubDelegation: false,
|
|
26547
27091
|
expiry: expirationTime,
|
|
26548
27092
|
delegateDID: params.delegateDID,
|
|
26549
27093
|
ownerAddress: session.address,
|
|
@@ -26586,7 +27130,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26586
27130
|
spaceId: publicSpaceId,
|
|
26587
27131
|
path: "",
|
|
26588
27132
|
actions: [KV3.GET, KV3.PUT, KV3.METADATA],
|
|
26589
|
-
disableSubDelegation:
|
|
27133
|
+
disableSubDelegation: false,
|
|
26590
27134
|
expiry: expirationTime,
|
|
26591
27135
|
delegateDID: params.delegateDID,
|
|
26592
27136
|
ownerAddress: session.address,
|
|
@@ -26753,6 +27297,9 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26753
27297
|
if (parentDelegation.disableSubDelegation) {
|
|
26754
27298
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
26755
27299
|
}
|
|
27300
|
+
if (params.disableSubDelegation) {
|
|
27301
|
+
throw new Error(TERMINAL_DELEGATION_REQUIRES_SESSION_KEY);
|
|
27302
|
+
}
|
|
26756
27303
|
const parentResources = parentDelegation.resources ?? [{
|
|
26757
27304
|
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
26758
27305
|
space: parentDelegation.spaceId,
|
|
@@ -26825,7 +27372,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
26825
27372
|
spaceId: parentDelegation.spaceId,
|
|
26826
27373
|
path: requestedResources[0].path,
|
|
26827
27374
|
actions: requestedResources[0].actions,
|
|
26828
|
-
disableSubDelegation:
|
|
27375
|
+
disableSubDelegation: false,
|
|
26829
27376
|
expiry: actualExpiry,
|
|
26830
27377
|
delegateDID: params.delegateDID,
|
|
26831
27378
|
ownerAddress: parentDelegation.ownerAddress,
|
|
@@ -26860,9 +27407,16 @@ TinyCloudNode.registerNodeDefaults({
|
|
|
26860
27407
|
});
|
|
26861
27408
|
|
|
26862
27409
|
// src/replication/node-loader.ts
|
|
27410
|
+
init_config();
|
|
26863
27411
|
registerReplicationLoaders({
|
|
27412
|
+
validateConfig: assertValidReplicationConfig,
|
|
26864
27413
|
runtime: () => Promise.resolve().then(() => (init_runtime(), runtime_exports)),
|
|
26865
|
-
authority: () => Promise.resolve().then(() => (init_authority(), authority_exports))
|
|
27414
|
+
authority: () => Promise.resolve().then(() => (init_authority(), authority_exports)),
|
|
27415
|
+
gatedNetwork: () => Promise.resolve().then(() => (init_gated_network(), gated_network_exports)),
|
|
27416
|
+
// Lazy by contract: replica loads only when replication is enabled. Literal specifier typed by
|
|
27417
|
+
// ProfileGateModule; TS2307 under moduleResolution "node" is suppressed (see replica-contract.ts).
|
|
27418
|
+
// @ts-ignore
|
|
27419
|
+
gate: () => import("@tinycloud/replica/profile-gate")
|
|
26866
27420
|
});
|
|
26867
27421
|
|
|
26868
27422
|
// src/index.ts
|
|
@@ -27178,8 +27732,8 @@ import {
|
|
|
27178
27732
|
Space
|
|
27179
27733
|
} from "@tinycloud/sdk-core";
|
|
27180
27734
|
import {
|
|
27181
|
-
ProtocolMismatchError,
|
|
27182
|
-
VersionCheckError
|
|
27735
|
+
ProtocolMismatchError as ProtocolMismatchError2,
|
|
27736
|
+
VersionCheckError,
|
|
27183
27737
|
UnsupportedFeatureError as UnsupportedFeatureError2,
|
|
27184
27738
|
checkNodeInfo as checkNodeInfo3
|
|
27185
27739
|
} from "@tinycloud/sdk-core";
|
|
@@ -27187,15 +27741,518 @@ import { ServiceContext as ServiceContext4 } from "@tinycloud/sdk-core";
|
|
|
27187
27741
|
|
|
27188
27742
|
// src/replication/sqlite.ts
|
|
27189
27743
|
init_authority();
|
|
27744
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
27190
27745
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
27191
|
-
import {
|
|
27192
|
-
import {
|
|
27746
|
+
import { lstatSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
27747
|
+
import { chmod, lstat, mkdir, open as openFile, readFile, readdir, realpath, rename, rm, rmdir, stat } from "fs/promises";
|
|
27748
|
+
import { basename, dirname as dirname2, join as join2, parse as parse2, resolve, sep } from "path";
|
|
27193
27749
|
import { KVService as KVService3, ServiceContext as ServiceContext3, principalDid as principalDid2 } from "@tinycloud/sdk-core";
|
|
27194
27750
|
import {
|
|
27195
27751
|
canonicalReplicationIdentity,
|
|
27196
27752
|
replicationIdentityKey,
|
|
27197
27753
|
requiresSecretsOptIn as requiresSecretsOptIn2
|
|
27198
27754
|
} from "@tinycloud/sdk-services";
|
|
27755
|
+
import { CONSENT_WINDOW_MS, MAX_GRANT_TTL_MS as MAX_GRANT_TTL_MS2, REPLICA_GATE_FILE } from "@tinycloud/sdk-services/kv/replication/contractConstants";
|
|
27756
|
+
var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
|
|
27757
|
+
function assertProfileLogoutClear(profileDir) {
|
|
27758
|
+
assertLogoutClear(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
27759
|
+
}
|
|
27760
|
+
function assertLogoutClear(path) {
|
|
27761
|
+
if (path === void 0) return;
|
|
27762
|
+
try {
|
|
27763
|
+
lstatSync(path);
|
|
27764
|
+
} catch (error) {
|
|
27765
|
+
if (isNotFound(error)) return;
|
|
27766
|
+
}
|
|
27767
|
+
throw gated("gate_unavailable");
|
|
27768
|
+
}
|
|
27769
|
+
function safeInventoryPath(path) {
|
|
27770
|
+
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/"));
|
|
27771
|
+
}
|
|
27772
|
+
function logoutPrincipalDigest(principal) {
|
|
27773
|
+
return createHash2("sha256").update("tinycloud-replica-logout\0").update(principal, "utf8").digest("hex");
|
|
27774
|
+
}
|
|
27775
|
+
function compareInventoryString(left, right) {
|
|
27776
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
27777
|
+
}
|
|
27778
|
+
function approvalEntries(report) {
|
|
27779
|
+
return [
|
|
27780
|
+
...report.unknownEntries,
|
|
27781
|
+
...report.knownEntries,
|
|
27782
|
+
...report.knownRoots.map((root) => ({ ...root, kind: "directory" })),
|
|
27783
|
+
...report.missingDatabases.map((path) => ({ path, kind: "missing", dev: "", ino: "" }))
|
|
27784
|
+
].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));
|
|
27785
|
+
}
|
|
27786
|
+
function inventoryApprovalToken(marker, principal, entries) {
|
|
27787
|
+
const markerId = marker.v === 2 ? [marker.v, marker.nonce, marker.principalDigest] : [marker.v, marker.incarnation, marker.lifecycleEpoch, marker.principalDigest];
|
|
27788
|
+
return createHash2("sha256").update("tc-logout-approve-v1\0").update(JSON.stringify([
|
|
27789
|
+
markerId,
|
|
27790
|
+
marker.approvalSalt,
|
|
27791
|
+
principal,
|
|
27792
|
+
marker.v === 2 ? null : marker.lifecycleEpoch,
|
|
27793
|
+
entries
|
|
27794
|
+
])).digest().subarray(0, 16).toString("hex");
|
|
27795
|
+
}
|
|
27796
|
+
function changedInventoryRefusal(marker, principal, report, entries = approvalEntries(report)) {
|
|
27797
|
+
const token = inventoryApprovalToken(marker, principal, entries);
|
|
27798
|
+
const error = localChangesRefusal(report.unknownPaths);
|
|
27799
|
+
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}.`;
|
|
27800
|
+
return Object.assign(error, { inventoryApprovalToken: token, inventoryApprovalEntries: entries });
|
|
27801
|
+
}
|
|
27802
|
+
function storedInventoryChanged(marker, report) {
|
|
27803
|
+
try {
|
|
27804
|
+
assertApprovedInventory(report, {
|
|
27805
|
+
unknownEntries: marker.approvedUnknown,
|
|
27806
|
+
knownRoots: marker.knownRoots,
|
|
27807
|
+
knownEntries: marker.knownEntries,
|
|
27808
|
+
missingDatabases: marker.missingDatabases
|
|
27809
|
+
});
|
|
27810
|
+
return false;
|
|
27811
|
+
} catch (error) {
|
|
27812
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
|
|
27813
|
+
return true;
|
|
27814
|
+
}
|
|
27815
|
+
throw error;
|
|
27816
|
+
}
|
|
27817
|
+
}
|
|
27818
|
+
function decimalIdentity(value) {
|
|
27819
|
+
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
27820
|
+
}
|
|
27821
|
+
function logoutMarkerOf(value) {
|
|
27822
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw gated("gate_unavailable");
|
|
27823
|
+
const row = value;
|
|
27824
|
+
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);
|
|
27825
|
+
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);
|
|
27826
|
+
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(
|
|
27827
|
+
(path) => typeof path === "string" && safeInventoryPath(path) && path.endsWith("/replica.db")
|
|
27828
|
+
) || 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");
|
|
27829
|
+
const optional = Number(row.discardLocalChanges === true) + Number(row.remotePending === true) + Number(row.remoteAbandoned === true);
|
|
27830
|
+
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;
|
|
27831
|
+
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) {
|
|
27832
|
+
return row;
|
|
27833
|
+
}
|
|
27834
|
+
const bound = Number(row.deniedEpoch !== void 0);
|
|
27835
|
+
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)) {
|
|
27836
|
+
throw gated("gate_unavailable");
|
|
27837
|
+
}
|
|
27838
|
+
return row;
|
|
27839
|
+
}
|
|
27840
|
+
async function readLogoutMarker(path) {
|
|
27841
|
+
const value = await readJsonFile(path);
|
|
27842
|
+
return value === void 0 ? null : logoutMarkerOf(value);
|
|
27843
|
+
}
|
|
27844
|
+
async function orphanRoot(profileDir, name) {
|
|
27845
|
+
let profile;
|
|
27846
|
+
try {
|
|
27847
|
+
profile = await lstat(profileDir);
|
|
27848
|
+
} catch {
|
|
27849
|
+
throw gated("gate_unavailable");
|
|
27850
|
+
}
|
|
27851
|
+
let pinned = false;
|
|
27852
|
+
if (process.platform === "linux" && /^\/proc\/self\/fd\/[1-9]\d*$/.test(profileDir) && profile.isSymbolicLink()) {
|
|
27853
|
+
try {
|
|
27854
|
+
pinned = (await stat(profileDir)).isDirectory();
|
|
27855
|
+
} catch {
|
|
27856
|
+
throw gated("gate_unavailable");
|
|
27857
|
+
}
|
|
27858
|
+
}
|
|
27859
|
+
if (!profile.isDirectory() && !pinned) throw gated("gate_unavailable");
|
|
27860
|
+
const root = join2(profileDir, name);
|
|
27861
|
+
let entry;
|
|
27862
|
+
try {
|
|
27863
|
+
entry = await lstat(root);
|
|
27864
|
+
} catch (error) {
|
|
27865
|
+
if (isNotFound(error)) return null;
|
|
27866
|
+
throw error;
|
|
27867
|
+
}
|
|
27868
|
+
if (!entry.isDirectory() || await realpath(root) !== join2(await realpath(profileDir), name)) {
|
|
27869
|
+
throw gated("gate_unavailable");
|
|
27870
|
+
}
|
|
27871
|
+
return root;
|
|
27872
|
+
}
|
|
27873
|
+
async function hasOrphanPending(profileDir) {
|
|
27874
|
+
const root = await orphanRoot(profileDir, "replication");
|
|
27875
|
+
if (root === null) return false;
|
|
27876
|
+
let entries;
|
|
27877
|
+
try {
|
|
27878
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
27879
|
+
} catch {
|
|
27880
|
+
return true;
|
|
27881
|
+
}
|
|
27882
|
+
for (const entry of entries) {
|
|
27883
|
+
if (!entry.isDirectory()) return true;
|
|
27884
|
+
const dir = join2(root, entry.name);
|
|
27885
|
+
try {
|
|
27886
|
+
if (!(await lstat(dir)).isDirectory()) return true;
|
|
27887
|
+
} catch {
|
|
27888
|
+
return true;
|
|
27889
|
+
}
|
|
27890
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) return true;
|
|
27891
|
+
let children;
|
|
27892
|
+
try {
|
|
27893
|
+
children = await readdir(dir, { withFileTypes: true });
|
|
27894
|
+
} catch {
|
|
27895
|
+
return true;
|
|
27896
|
+
}
|
|
27897
|
+
if (children.length !== 2 || children.some((child) => !child.isFile() || child.name !== "identity.json" && child.name !== "pending.json")) return true;
|
|
27898
|
+
const pendingFile = join2(dir, "pending.json");
|
|
27899
|
+
let text;
|
|
27900
|
+
try {
|
|
27901
|
+
const pending = await lstat(pendingFile);
|
|
27902
|
+
if (!pending.isFile()) return true;
|
|
27903
|
+
text = await readFile(pendingFile, "utf8");
|
|
27904
|
+
} catch (error) {
|
|
27905
|
+
if (isNotFound(error)) return true;
|
|
27906
|
+
return true;
|
|
27907
|
+
}
|
|
27908
|
+
try {
|
|
27909
|
+
const identityFile = join2(dir, "identity.json");
|
|
27910
|
+
if (!(await lstat(identityFile)).isFile()) return true;
|
|
27911
|
+
const identity = identityRecordOf(await readJsonFile(identityFile));
|
|
27912
|
+
if (identity === void 0 || parsePendingState(text, pendingFile, identity).records.length !== 0) {
|
|
27913
|
+
return true;
|
|
27914
|
+
}
|
|
27915
|
+
} catch {
|
|
27916
|
+
return true;
|
|
27917
|
+
}
|
|
27918
|
+
}
|
|
27919
|
+
return false;
|
|
27920
|
+
}
|
|
27921
|
+
function inventoryIdentity(path, dev, ino) {
|
|
27922
|
+
return { path, dev: dev.toString(), ino: ino.toString() };
|
|
27923
|
+
}
|
|
27924
|
+
async function inventoryUnknown(profileDir, relative, entries) {
|
|
27925
|
+
const path = join2(profileDir, relative);
|
|
27926
|
+
const info = await lstat(path, { bigint: true });
|
|
27927
|
+
const kind = info.isDirectory() ? "directory" : info.isSymbolicLink() ? "symlink" : "file";
|
|
27928
|
+
entries.push({ ...inventoryIdentity(relative, info.dev, info.ino), kind });
|
|
27929
|
+
if (kind === "directory") {
|
|
27930
|
+
for (const child of await readdir(path)) await inventoryUnknown(profileDir, `${relative}/${child}`, entries);
|
|
27931
|
+
}
|
|
27932
|
+
}
|
|
27933
|
+
async function inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly = false, diagnosticFiles = []) {
|
|
27934
|
+
const report = {
|
|
27935
|
+
knownLegacyReplicas: 0,
|
|
27936
|
+
unknownLegacyPaths: [],
|
|
27937
|
+
unknownPaths: [],
|
|
27938
|
+
unknownEntries: [],
|
|
27939
|
+
knownRoots: [],
|
|
27940
|
+
knownEntries: [],
|
|
27941
|
+
missingDatabases: []
|
|
27942
|
+
};
|
|
27943
|
+
const legacy = await orphanRoot(profileDir, "replicas");
|
|
27944
|
+
const active = legacyOnly ? null : await orphanRoot(profileDir, "replication");
|
|
27945
|
+
for (const [name, dir] of [["replicas", legacy], ["replication", active]]) {
|
|
27946
|
+
if (dir === null) continue;
|
|
27947
|
+
const info = await lstat(dir, { bigint: true });
|
|
27948
|
+
report.knownRoots.push(inventoryIdentity(name, info.dev, info.ino));
|
|
27949
|
+
}
|
|
27950
|
+
if (legacy !== null) {
|
|
27951
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
27952
|
+
for (const entry of await readdir(legacy, { withFileTypes: true })) {
|
|
27953
|
+
const relative = `replicas/${entry.name}`;
|
|
27954
|
+
const child = join2(legacy, entry.name);
|
|
27955
|
+
if (!entry.isDirectory()) {
|
|
27956
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27957
|
+
continue;
|
|
27958
|
+
}
|
|
27959
|
+
try {
|
|
27960
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(child);
|
|
27961
|
+
report.knownRoots.push({ path: relative, ...layout.rootIdentity });
|
|
27962
|
+
report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
|
|
27963
|
+
...item,
|
|
27964
|
+
path: `${relative}/${item.path}`
|
|
27965
|
+
})));
|
|
27966
|
+
report.knownEntries.push(...layout.knownEntries.map((item) => ({
|
|
27967
|
+
...item,
|
|
27968
|
+
path: `${relative}/${item.path}`
|
|
27969
|
+
})));
|
|
27970
|
+
if (layout.unknownPaths.length === 0) report.knownLegacyReplicas++;
|
|
27971
|
+
if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
27972
|
+
report.missingDatabases.push(`${relative}/replica.db`);
|
|
27973
|
+
}
|
|
27974
|
+
} catch {
|
|
27975
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27976
|
+
}
|
|
27977
|
+
}
|
|
27978
|
+
}
|
|
27979
|
+
if (active !== null) {
|
|
27980
|
+
const cleanOrphan = !gateReadable && (approvedMarker?.v === 2 || !await hasOrphanPending(profileDir));
|
|
27981
|
+
for (const entry of await readdir(active, { withFileTypes: true })) {
|
|
27982
|
+
const relative = `replication/${entry.name}`;
|
|
27983
|
+
const dir = join2(active, entry.name);
|
|
27984
|
+
if (approvedMarker?.approvedUnknown.some((item) => item.path === relative)) {
|
|
27985
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27986
|
+
continue;
|
|
27987
|
+
}
|
|
27988
|
+
if (gateReadable && diagnosticFiles.includes(entry.name)) {
|
|
27989
|
+
const info2 = await lstat(dir, { bigint: true });
|
|
27990
|
+
if (info2.isFile()) {
|
|
27991
|
+
report.knownEntries.push({ ...inventoryIdentity(relative, info2.dev, info2.ino), kind: "file" });
|
|
27992
|
+
continue;
|
|
27993
|
+
}
|
|
27994
|
+
}
|
|
27995
|
+
if (!gateReadable && !cleanOrphan || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
27996
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
27997
|
+
continue;
|
|
27998
|
+
}
|
|
27999
|
+
const info = await lstat(dir, { bigint: true });
|
|
28000
|
+
report.knownRoots.push(inventoryIdentity(relative, info.dev, info.ino));
|
|
28001
|
+
const identityPath = join2(dir, "identity.json");
|
|
28002
|
+
let identity;
|
|
28003
|
+
try {
|
|
28004
|
+
if ((await lstat(identityPath)).isFile()) identity = identityRecordOf(await readJsonFile(identityPath));
|
|
28005
|
+
} catch {
|
|
28006
|
+
}
|
|
28007
|
+
if (identity === void 0) {
|
|
28008
|
+
const approvedRoot = approvedMarker?.knownRoots.find((item) => item.path === relative);
|
|
28009
|
+
if (approvedRoot?.dev === info.dev.toString() && approvedRoot.ino === info.ino.toString()) {
|
|
28010
|
+
const children = await readdir(dir);
|
|
28011
|
+
if (children.length === 0) continue;
|
|
28012
|
+
if (children.length === 1 && children[0] === "replicas") {
|
|
28013
|
+
const replicasPath = `${relative}/replicas`;
|
|
28014
|
+
const replicasDir = join2(dir, "replicas");
|
|
28015
|
+
const replicasInfo = await lstat(replicasDir, { bigint: true });
|
|
28016
|
+
const approvedReplicas = approvedMarker.knownRoots.find((item) => item.path === replicasPath);
|
|
28017
|
+
if (replicasInfo.isDirectory() && !replicasInfo.isSymbolicLink() && approvedReplicas?.dev === replicasInfo.dev.toString() && approvedReplicas.ino === replicasInfo.ino.toString() && (await readdir(replicasDir)).length === 0) {
|
|
28018
|
+
report.knownRoots.push(inventoryIdentity(replicasPath, replicasInfo.dev, replicasInfo.ino));
|
|
28019
|
+
continue;
|
|
28020
|
+
}
|
|
28021
|
+
}
|
|
28022
|
+
}
|
|
28023
|
+
report.knownRoots.pop();
|
|
28024
|
+
await inventoryUnknown(profileDir, relative, report.unknownEntries);
|
|
28025
|
+
continue;
|
|
28026
|
+
}
|
|
28027
|
+
for (const child of await readdir(dir, { withFileTypes: true })) {
|
|
28028
|
+
const childRelative = `${relative}/${child.name}`;
|
|
28029
|
+
const childPath = join2(dir, child.name);
|
|
28030
|
+
if ((child.name === "identity.json" || child.name === "device.jwk") && child.isFile()) {
|
|
28031
|
+
const childInfo = await lstat(childPath, { bigint: true });
|
|
28032
|
+
report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
|
|
28033
|
+
continue;
|
|
28034
|
+
}
|
|
28035
|
+
if (child.name === "pending.json" && child.isFile()) {
|
|
28036
|
+
try {
|
|
28037
|
+
if (parsePendingState(await readFile(childPath, "utf8"), childPath, identity).records.length === 0) {
|
|
28038
|
+
const childInfo = await lstat(childPath, { bigint: true });
|
|
28039
|
+
report.knownEntries.push({ ...inventoryIdentity(childRelative, childInfo.dev, childInfo.ino), kind: "file" });
|
|
28040
|
+
continue;
|
|
28041
|
+
}
|
|
28042
|
+
} catch {
|
|
28043
|
+
}
|
|
28044
|
+
}
|
|
28045
|
+
if (child.name === "replicas" && child.isDirectory()) {
|
|
28046
|
+
const replicasInfo = await lstat(childPath, { bigint: true });
|
|
28047
|
+
report.knownRoots.push(inventoryIdentity(childRelative, replicasInfo.dev, replicasInfo.ino));
|
|
28048
|
+
for (const replicaChild of await readdir(childPath, { withFileTypes: true })) {
|
|
28049
|
+
const replicaRelative = `${childRelative}/${replicaChild.name}`;
|
|
28050
|
+
if (!replicaChild.isDirectory() || !/^[a-z2-7]{26}$/.test(replicaChild.name)) {
|
|
28051
|
+
await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
|
|
28052
|
+
continue;
|
|
28053
|
+
}
|
|
28054
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
28055
|
+
try {
|
|
28056
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(join2(childPath, replicaChild.name), {
|
|
28057
|
+
allowAdapterFence: true
|
|
28058
|
+
});
|
|
28059
|
+
report.knownRoots.push({ path: replicaRelative, ...layout.rootIdentity });
|
|
28060
|
+
report.unknownEntries.push(...layout.unknownEntries.map((item) => ({
|
|
28061
|
+
...item,
|
|
28062
|
+
path: `${replicaRelative}/${item.path}`
|
|
28063
|
+
})));
|
|
28064
|
+
report.knownEntries.push(...layout.knownEntries.map((item) => ({
|
|
28065
|
+
...item,
|
|
28066
|
+
path: `${replicaRelative}/${item.path}`
|
|
28067
|
+
})));
|
|
28068
|
+
if (layout.unknownPaths.includes("replica.db") && !layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
28069
|
+
report.missingDatabases.push(`${replicaRelative}/replica.db`);
|
|
28070
|
+
}
|
|
28071
|
+
} catch {
|
|
28072
|
+
await inventoryUnknown(profileDir, replicaRelative, report.unknownEntries);
|
|
28073
|
+
}
|
|
28074
|
+
}
|
|
28075
|
+
continue;
|
|
28076
|
+
}
|
|
28077
|
+
await inventoryUnknown(profileDir, childRelative, report.unknownEntries);
|
|
28078
|
+
}
|
|
28079
|
+
}
|
|
28080
|
+
}
|
|
28081
|
+
report.unknownEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
28082
|
+
report.knownEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
28083
|
+
report.knownRoots.sort((a, b) => a.path.localeCompare(b.path));
|
|
28084
|
+
report.missingDatabases.sort();
|
|
28085
|
+
report.unknownPaths = [...report.unknownEntries.map((entry) => entry.path), ...report.missingDatabases].sort();
|
|
28086
|
+
report.unknownLegacyPaths = [...report.unknownPaths];
|
|
28087
|
+
return report;
|
|
28088
|
+
}
|
|
28089
|
+
function assertApprovedInventory(current, approval, allowRemoved = false) {
|
|
28090
|
+
const approved = new Map(approval.unknownEntries.map((item) => [item.path, item]));
|
|
28091
|
+
const known = new Map(approval.knownRoots.map((item) => [item.path, item]));
|
|
28092
|
+
const knownFiles = new Map(approval.knownEntries.map((item) => [item.path, item]));
|
|
28093
|
+
for (const entry of current.unknownEntries) {
|
|
28094
|
+
const expected = approved.get(entry.path);
|
|
28095
|
+
if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
|
|
28096
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
28097
|
+
}
|
|
28098
|
+
}
|
|
28099
|
+
for (const root of current.knownRoots) {
|
|
28100
|
+
const expected = known.get(root.path);
|
|
28101
|
+
if (expected?.dev !== root.dev || expected.ino !== root.ino) throw localChangesRefusal([root.path]);
|
|
28102
|
+
}
|
|
28103
|
+
for (const entry of current.knownEntries) {
|
|
28104
|
+
if (allowRemoved && entry.kind === "file" && /^(?:replication\/[a-z2-7]{26}\/replicas\/[a-z2-7]{26}|replicas\/[^/]+)\/replica\.db-(?:wal|shm|journal)$/.test(entry.path)) {
|
|
28105
|
+
const database = entry.path.replace(/-(?:wal|shm|journal)$/, "");
|
|
28106
|
+
const original = knownFiles.get(database);
|
|
28107
|
+
const present = current.knownEntries.find((item) => item.path === database);
|
|
28108
|
+
if (!approved.has(entry.path) && original?.kind === "file" && present?.dev === original.dev && present.ino === original.ino) continue;
|
|
28109
|
+
}
|
|
28110
|
+
const expected = knownFiles.get(entry.path);
|
|
28111
|
+
if (expected?.kind !== entry.kind || expected.dev !== entry.dev || expected.ino !== entry.ino) {
|
|
28112
|
+
throw localChangesRefusal([entry.path]);
|
|
28113
|
+
}
|
|
28114
|
+
}
|
|
28115
|
+
for (const path of current.missingDatabases) {
|
|
28116
|
+
if (!approval.missingDatabases.includes(path) && !(allowRemoved && knownFiles.get(path)?.kind === "file")) throw localChangesRefusal([path]);
|
|
28117
|
+
}
|
|
28118
|
+
if (!allowRemoved && (current.unknownEntries.length !== approved.size || current.knownRoots.length !== known.size || current.knownEntries.length !== knownFiles.size || current.missingDatabases.length !== approval.missingDatabases.length)) {
|
|
28119
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
28120
|
+
}
|
|
28121
|
+
}
|
|
28122
|
+
async function assertApprovedParents(profileDir, marker, path) {
|
|
28123
|
+
if (process.platform !== "linux") {
|
|
28124
|
+
const absolute = resolve(profileDir);
|
|
28125
|
+
let ancestor = parse2(absolute).root;
|
|
28126
|
+
for (const part of absolute.slice(ancestor.length).split(sep).filter(Boolean)) {
|
|
28127
|
+
ancestor = join2(ancestor, part);
|
|
28128
|
+
const details = await lstat(ancestor, { bigint: true });
|
|
28129
|
+
if (!details.isDirectory() || details.isSymbolicLink()) throw localChangesRefusal([path]);
|
|
28130
|
+
}
|
|
28131
|
+
}
|
|
28132
|
+
let relativeParent = "";
|
|
28133
|
+
for (const part of path.split("/").slice(0, -1)) {
|
|
28134
|
+
relativeParent = relativeParent ? `${relativeParent}/${part}` : part;
|
|
28135
|
+
let details;
|
|
28136
|
+
try {
|
|
28137
|
+
details = await lstat(join2(profileDir, relativeParent), { bigint: true });
|
|
28138
|
+
} catch (error) {
|
|
28139
|
+
if (isNotFound(error)) return false;
|
|
28140
|
+
throw error;
|
|
28141
|
+
}
|
|
28142
|
+
const expected = marker.knownRoots.find((root) => root.path === relativeParent) ?? marker.approvedUnknown.find((item) => item.path === relativeParent && item.kind === "directory");
|
|
28143
|
+
if (!details.isDirectory() || details.isSymbolicLink() || expected?.dev !== details.dev.toString() || expected.ino !== details.ino.toString()) {
|
|
28144
|
+
throw localChangesRefusal([relativeParent]);
|
|
28145
|
+
}
|
|
28146
|
+
}
|
|
28147
|
+
return true;
|
|
28148
|
+
}
|
|
28149
|
+
async function eraseApprovedUnknown(profileDir, marker, prefix, diagnosticFiles) {
|
|
28150
|
+
const entries = marker.approvedUnknown.filter((entry) => entry.path.startsWith(`${prefix}/`));
|
|
28151
|
+
for (const entry of entries.sort((a, b) => b.path.length - a.path.length)) {
|
|
28152
|
+
assertApprovedInventory(await inspectProfileReplicaLayout(profileDir, marker.v !== 2, marker, false, diagnosticFiles), {
|
|
28153
|
+
unknownEntries: marker.approvedUnknown,
|
|
28154
|
+
knownRoots: marker.knownRoots,
|
|
28155
|
+
knownEntries: marker.knownEntries,
|
|
28156
|
+
missingDatabases: marker.missingDatabases
|
|
28157
|
+
}, true);
|
|
28158
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28159
|
+
const file = join2(profileDir, entry.path);
|
|
28160
|
+
let current;
|
|
28161
|
+
try {
|
|
28162
|
+
current = await lstat(file, { bigint: true });
|
|
28163
|
+
} catch (error) {
|
|
28164
|
+
if (isNotFound(error)) continue;
|
|
28165
|
+
throw error;
|
|
28166
|
+
}
|
|
28167
|
+
const kind = current.isDirectory() ? "directory" : current.isSymbolicLink() ? "symlink" : "file";
|
|
28168
|
+
if (kind !== entry.kind || current.dev.toString() !== entry.dev || current.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
|
|
28169
|
+
if (process.platform !== "linux" && kind === "symlink") throw localChangesRefusal([entry.path]);
|
|
28170
|
+
if (kind === "directory") await rmdir(file);
|
|
28171
|
+
else await rm(file);
|
|
28172
|
+
}
|
|
28173
|
+
}
|
|
28174
|
+
async function eraseKnownFlagResidue(profileDir, marker, diagnosticFiles) {
|
|
28175
|
+
const diagnostics = new Set(diagnosticFiles.map((name) => `replication/${name}`));
|
|
28176
|
+
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")))) {
|
|
28177
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28178
|
+
const file = join2(profileDir, entry.path);
|
|
28179
|
+
let details;
|
|
28180
|
+
try {
|
|
28181
|
+
details = await lstat(file, { bigint: true });
|
|
28182
|
+
} catch (error) {
|
|
28183
|
+
if (isNotFound(error)) continue;
|
|
28184
|
+
throw error;
|
|
28185
|
+
}
|
|
28186
|
+
if (!details.isFile() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) throw localChangesRefusal([entry.path]);
|
|
28187
|
+
await rm(file);
|
|
28188
|
+
}
|
|
28189
|
+
const roots = marker.knownRoots.filter((item) => item.path === "replication" || item.path.startsWith("replication/"));
|
|
28190
|
+
roots.sort((a, b) => b.path.split("/").length - a.path.split("/").length);
|
|
28191
|
+
for (const entry of roots) {
|
|
28192
|
+
if (!await assertApprovedParents(profileDir, marker, entry.path)) continue;
|
|
28193
|
+
const path = join2(profileDir, entry.path);
|
|
28194
|
+
let details;
|
|
28195
|
+
try {
|
|
28196
|
+
details = await lstat(path, { bigint: true });
|
|
28197
|
+
} catch (error) {
|
|
28198
|
+
if (isNotFound(error)) continue;
|
|
28199
|
+
throw error;
|
|
28200
|
+
}
|
|
28201
|
+
if (!details.isDirectory() || details.isSymbolicLink() || details.dev.toString() !== entry.dev || details.ino.toString() !== entry.ino) {
|
|
28202
|
+
throw localChangesRefusal([entry.path]);
|
|
28203
|
+
}
|
|
28204
|
+
await rmdir(path);
|
|
28205
|
+
}
|
|
28206
|
+
}
|
|
28207
|
+
function localChangesRefusal(unknownPaths = []) {
|
|
28208
|
+
const error = gated("gate_unavailable");
|
|
28209
|
+
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(", ")}.`);
|
|
28210
|
+
return error;
|
|
28211
|
+
}
|
|
28212
|
+
function gateOwnedByAnother(state, principalHash) {
|
|
28213
|
+
return state.principalHash !== null ? state.principalHash !== principalHash : state.eraseDeferred && state.deferredPrincipalHash !== principalHash;
|
|
28214
|
+
}
|
|
28215
|
+
function isGateUnavailable(error) {
|
|
28216
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable";
|
|
28217
|
+
}
|
|
28218
|
+
function restoredGate() {
|
|
28219
|
+
const error = gated("stamp_stale");
|
|
28220
|
+
error.message = RESTORED_GATE_MESSAGE;
|
|
28221
|
+
return Object.assign(error, { reportCode: "gate_restored" });
|
|
28222
|
+
}
|
|
28223
|
+
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.";
|
|
28224
|
+
function interruptedSignOut() {
|
|
28225
|
+
const error = gated("gate_unavailable");
|
|
28226
|
+
error.message = "A previous sign-out was interrupted; re-running `tc auth logout` completes it.";
|
|
28227
|
+
return error;
|
|
28228
|
+
}
|
|
28229
|
+
function assertSupportedProfileLayout(report) {
|
|
28230
|
+
if (process.platform === "linux") return;
|
|
28231
|
+
const link = report.unknownEntries.find((entry) => entry.kind === "symlink");
|
|
28232
|
+
if (link === void 0) return;
|
|
28233
|
+
const error = gated("gate_unavailable");
|
|
28234
|
+
error.message = `Replica symlink ${JSON.stringify(link.path)} cannot be removed safely on this platform.`;
|
|
28235
|
+
throw error;
|
|
28236
|
+
}
|
|
28237
|
+
async function fileGate() {
|
|
28238
|
+
return import("@tinycloud/replica/profile-gate");
|
|
28239
|
+
}
|
|
28240
|
+
function gated(reason) {
|
|
28241
|
+
return Object.assign(new Error(`Replica gate refused: ${reason}`), { code: "REPLICA_GATED", gate: reason });
|
|
28242
|
+
}
|
|
28243
|
+
async function requiredGuard(guard, section) {
|
|
28244
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
28245
|
+
let entered = false;
|
|
28246
|
+
try {
|
|
28247
|
+
return await guard(async () => {
|
|
28248
|
+
entered = true;
|
|
28249
|
+
return section();
|
|
28250
|
+
});
|
|
28251
|
+
} catch (error) {
|
|
28252
|
+
if (!entered) throw gated("gate_unavailable");
|
|
28253
|
+
throw error;
|
|
28254
|
+
}
|
|
28255
|
+
}
|
|
27199
28256
|
var ReplicaStorageError = class extends Error {
|
|
27200
28257
|
constructor(code, message, options) {
|
|
27201
28258
|
super(message);
|
|
@@ -27221,6 +28278,19 @@ async function pathExists(path) {
|
|
|
27221
28278
|
throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
|
|
27222
28279
|
}
|
|
27223
28280
|
}
|
|
28281
|
+
async function realDirectoryExists(path) {
|
|
28282
|
+
let stats;
|
|
28283
|
+
try {
|
|
28284
|
+
stats = await lstat(path);
|
|
28285
|
+
} catch (error) {
|
|
28286
|
+
if (isNotFound(error)) return false;
|
|
28287
|
+
throw storageError("STORAGE_ERROR", `Reading ${path}`, error);
|
|
28288
|
+
}
|
|
28289
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
28290
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Refusing the replaced replica path ${path}.`);
|
|
28291
|
+
}
|
|
28292
|
+
return true;
|
|
28293
|
+
}
|
|
27224
28294
|
function principalOf(did) {
|
|
27225
28295
|
return principalDid2(did);
|
|
27226
28296
|
}
|
|
@@ -27374,90 +28444,117 @@ function parsePendingState(text, file, identity) {
|
|
|
27374
28444
|
}
|
|
27375
28445
|
return state;
|
|
27376
28446
|
}
|
|
27377
|
-
var _dir, _guard, _dirSync,
|
|
28447
|
+
var _dir, _profileDir, _guard, _stamp, _dirSync, _logoutPendingFile, _generation, _canonicalDir, _FilePendingWriteStore_instances, withGate_fn, readFile_fn;
|
|
27378
28448
|
var FilePendingWriteStore = class {
|
|
27379
|
-
constructor(identity, dir, guard, dirSync) {
|
|
28449
|
+
constructor(identity, dir, profileDir, guard, stamp, dirSync, logoutPendingFile) {
|
|
27380
28450
|
__privateAdd(this, _FilePendingWriteStore_instances);
|
|
27381
28451
|
this.durable = true;
|
|
27382
28452
|
__privateAdd(this, _dir);
|
|
28453
|
+
__privateAdd(this, _profileDir);
|
|
27383
28454
|
__privateAdd(this, _guard);
|
|
28455
|
+
__privateAdd(this, _stamp);
|
|
27384
28456
|
__privateAdd(this, _dirSync);
|
|
27385
|
-
__privateAdd(this,
|
|
28457
|
+
__privateAdd(this, _logoutPendingFile);
|
|
28458
|
+
__privateAdd(this, _generation);
|
|
28459
|
+
__privateAdd(this, _canonicalDir);
|
|
27386
28460
|
this.identity = identity;
|
|
27387
28461
|
__privateSet(this, _dir, dir);
|
|
28462
|
+
__privateSet(this, _profileDir, profileDir);
|
|
27388
28463
|
__privateSet(this, _guard, guard);
|
|
28464
|
+
__privateSet(this, _stamp, stamp);
|
|
27389
28465
|
__privateSet(this, _dirSync, dirSync);
|
|
28466
|
+
__privateSet(this, _logoutPendingFile, logoutPendingFile);
|
|
27390
28467
|
}
|
|
27391
28468
|
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);
|
|
28469
|
+
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
28470
|
}
|
|
27404
28471
|
async readExisting() {
|
|
27405
|
-
|
|
27406
|
-
try {
|
|
27407
|
-
file = join2(await realpath(__privateGet(this, _dir)), "pending.json");
|
|
27408
|
-
} catch (error) {
|
|
27409
|
-
if (isNotFound(error)) return void 0;
|
|
27410
|
-
throw storageError("STORAGE_ERROR", `Resolving pending-write state in ${__privateGet(this, _dir)}`, error);
|
|
27411
|
-
}
|
|
27412
|
-
try {
|
|
27413
|
-
const text = await readFile(file, "utf8");
|
|
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);
|
|
27419
|
-
}
|
|
28472
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, (file) => __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, file));
|
|
27420
28473
|
}
|
|
27421
28474
|
async update(mutate) {
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
27427
|
-
}
|
|
27428
|
-
|
|
27429
|
-
|
|
27430
|
-
|
|
27431
|
-
|
|
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
|
-
});
|
|
28475
|
+
return __privateMethod(this, _FilePendingWriteStore_instances, withGate_fn).call(this, async (file) => {
|
|
28476
|
+
await ensurePartitionIdentity(__privateGet(this, _dir), this.identity, __privateGet(this, _dirSync));
|
|
28477
|
+
const currentFile = join2(await realpath(__privateGet(this, _dir)), "pending.json");
|
|
28478
|
+
if (__privateGet(this, _canonicalDir) !== void 0 && dirname2(currentFile) !== __privateGet(this, _canonicalDir)) {
|
|
28479
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
|
|
28480
|
+
}
|
|
28481
|
+
const state = await __privateMethod(this, _FilePendingWriteStore_instances, readFile_fn).call(this, currentFile) ?? { v: 2, identity: this.identity, committedEpoch: 0, seq: 0, records: [] };
|
|
28482
|
+
const result = await mutate(state);
|
|
28483
|
+
await writeJsonAtomic(currentFile, state, __privateGet(this, _dirSync));
|
|
28484
|
+
return result;
|
|
27442
28485
|
});
|
|
27443
28486
|
}
|
|
27444
28487
|
};
|
|
27445
28488
|
_dir = new WeakMap();
|
|
28489
|
+
_profileDir = new WeakMap();
|
|
27446
28490
|
_guard = new WeakMap();
|
|
28491
|
+
_stamp = new WeakMap();
|
|
27447
28492
|
_dirSync = new WeakMap();
|
|
27448
|
-
|
|
28493
|
+
_logoutPendingFile = new WeakMap();
|
|
28494
|
+
_generation = new WeakMap();
|
|
28495
|
+
_canonicalDir = new WeakMap();
|
|
27449
28496
|
_FilePendingWriteStore_instances = new WeakSet();
|
|
27450
|
-
|
|
27451
|
-
__privateGet(this,
|
|
27452
|
-
|
|
27453
|
-
|
|
28497
|
+
withGate_fn = async function(section) {
|
|
28498
|
+
if (__privateGet(this, _stamp) === void 0) throw gated("not_established");
|
|
28499
|
+
if (__privateGet(this, _guard) === void 0) throw gated("gate_unavailable");
|
|
28500
|
+
const gate = await fileGate();
|
|
28501
|
+
const snapshot = await gate.readGateSnapshot(__privateGet(this, _profileDir));
|
|
28502
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28503
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28504
|
+
gate.principalHashOf(snapshot.salt, this.identity.principal),
|
|
28505
|
+
gate.identityHashOf(snapshot.salt, replicationIdentityKey(this.identity))
|
|
28506
|
+
]);
|
|
28507
|
+
const access = {
|
|
28508
|
+
stamp: __privateGet(this, _stamp),
|
|
28509
|
+
incarnation: snapshot.incarnation,
|
|
28510
|
+
principalHash,
|
|
28511
|
+
identityKeyHash
|
|
28512
|
+
};
|
|
28513
|
+
let canonicalRoot;
|
|
28514
|
+
try {
|
|
28515
|
+
canonicalRoot = await realpath(dirname2(__privateGet(this, _dir)));
|
|
28516
|
+
} catch (error) {
|
|
28517
|
+
if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${dirname2(__privateGet(this, _dir))}`, error);
|
|
28518
|
+
canonicalRoot = join2(await realpath(dirname2(dirname2(__privateGet(this, _dir)))), basename(dirname2(__privateGet(this, _dir))));
|
|
28519
|
+
}
|
|
28520
|
+
const queueKey = join2(canonicalRoot, basename(__privateGet(this, _dir)), "pending.json");
|
|
28521
|
+
return serializePendingWrite(queueKey, () => requiredGuard(__privateGet(this, _guard), async () => {
|
|
28522
|
+
assertLogoutClear(__privateGet(this, _logoutPendingFile));
|
|
28523
|
+
const before = await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
|
|
28524
|
+
const generation = before.pendingGenerations[identityKeyHash] ?? 0;
|
|
28525
|
+
if (__privateGet(this, _generation) !== void 0 && __privateGet(this, _generation) !== generation) {
|
|
28526
|
+
throw new ReplicaStorageError("RESET_REQUIRED", "The pending identity was erased; reopen the store.");
|
|
28527
|
+
}
|
|
28528
|
+
let file = join2(__privateGet(this, _dir), "pending.json");
|
|
27454
28529
|
try {
|
|
27455
|
-
|
|
28530
|
+
file = join2(await realpath(__privateGet(this, _dir)), "pending.json");
|
|
28531
|
+
const recorded = identityRecordOf(await readJsonFile(join2(dirname2(file), "identity.json")));
|
|
28532
|
+
if (recorded === void 0 || !sameIdentity(recorded, this.identity)) {
|
|
28533
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending partition belongs to a different identity.");
|
|
28534
|
+
}
|
|
27456
28535
|
} catch (error) {
|
|
27457
|
-
|
|
28536
|
+
if (error instanceof ReplicaStorageError) throw error;
|
|
28537
|
+
if (!isNotFound(error)) throw storageError("STORAGE_ERROR", `Resolving ${__privateGet(this, _dir)}`, error);
|
|
27458
28538
|
}
|
|
27459
|
-
|
|
27460
|
-
|
|
28539
|
+
if (__privateGet(this, _canonicalDir) !== void 0 && dirname2(file) !== __privateGet(this, _canonicalDir)) {
|
|
28540
|
+
throw new ReplicaStorageError("STORAGE_ERROR", "The pending-write partition changed while open.");
|
|
28541
|
+
}
|
|
28542
|
+
const result = await section(file);
|
|
28543
|
+
await gate.assertGateAccess(__privateGet(this, _profileDir), access, snapshot.salt);
|
|
28544
|
+
assertLogoutClear(__privateGet(this, _logoutPendingFile));
|
|
28545
|
+
__privateSet(this, _generation, generation);
|
|
28546
|
+
if (await pathExists(__privateGet(this, _dir))) __privateSet(this, _canonicalDir, await realpath(__privateGet(this, _dir)));
|
|
28547
|
+
return result;
|
|
28548
|
+
}));
|
|
28549
|
+
};
|
|
28550
|
+
readFile_fn = async function(file) {
|
|
28551
|
+
try {
|
|
28552
|
+
return parsePendingState(await readFile(file, "utf8"), file, this.identity);
|
|
28553
|
+
} catch (error) {
|
|
28554
|
+
if (isNotFound(error)) return void 0;
|
|
28555
|
+
if (error instanceof ReplicaStorageError) throw error;
|
|
28556
|
+
throw storageError("STORAGE_ERROR", `Reading ${file}`, error);
|
|
28557
|
+
}
|
|
27461
28558
|
};
|
|
27462
28559
|
async function loadOrCreateDevice(idDir, guard, createDevice, dirSync) {
|
|
27463
28560
|
return guard(async () => {
|
|
@@ -27503,7 +28600,7 @@ async function readFence(replicaDir) {
|
|
|
27503
28600
|
const fence = await readJsonFile(join2(replicaDir, FENCE_FILE));
|
|
27504
28601
|
return typeof fence === "object" && fence !== null && "syncedThroughEpoch" in fence && typeof fence.syncedThroughEpoch === "number" ? fence.syncedThroughEpoch : 0;
|
|
27505
28602
|
}
|
|
27506
|
-
function newestGrantInfo(replica, record, pending, spec) {
|
|
28603
|
+
function newestGrantInfo(replica, record, pending, spec, install) {
|
|
27507
28604
|
if (record === null) return null;
|
|
27508
28605
|
const parsed = replica.parseUcanGrant(record.bytes);
|
|
27509
28606
|
return {
|
|
@@ -27511,10 +28608,11 @@ function newestGrantInfo(replica, record, pending, spec) {
|
|
|
27511
28608
|
parentCid: parsed.prf[0] ?? null,
|
|
27512
28609
|
expiresAt: record.expiresAt === null ? null : record.expiresAt * 1e3,
|
|
27513
28610
|
state: pending !== null && record === pending ? "pending" : "active",
|
|
27514
|
-
unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix)
|
|
28611
|
+
unconstrained: ucanAttUnconstrainedFor(parsed.att, spec.space, spec.prefix),
|
|
28612
|
+
...install === null ? {} : install
|
|
27515
28613
|
};
|
|
27516
28614
|
}
|
|
27517
|
-
var _device, _store, _replicaDir, _replica, _guard2, _dirSync2,
|
|
28615
|
+
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
28616
|
var SqliteReplicaHandle = class {
|
|
27519
28617
|
constructor(input) {
|
|
27520
28618
|
__privateAdd(this, _SqliteReplicaHandle_instances);
|
|
@@ -27523,9 +28621,12 @@ var SqliteReplicaHandle = class {
|
|
|
27523
28621
|
__privateAdd(this, _replicaDir);
|
|
27524
28622
|
__privateAdd(this, _replica);
|
|
27525
28623
|
__privateAdd(this, _guard2);
|
|
28624
|
+
__privateAdd(this, _read);
|
|
28625
|
+
__privateAdd(this, _policy);
|
|
28626
|
+
__privateAdd(this, _recordGrantInstall);
|
|
27526
28627
|
__privateAdd(this, _dirSync2);
|
|
27527
28628
|
/** The replica row generation this handle opened under — the durable fence a purge commits past (browser-parity wipe). */
|
|
27528
|
-
__privateAdd(this,
|
|
28629
|
+
__privateAdd(this, _generation2);
|
|
27529
28630
|
/** The inode of `replica.db` at open — a directory recreated in place is a different replica. */
|
|
27530
28631
|
__privateAdd(this, _ino);
|
|
27531
28632
|
__privateAdd(this, _transportOverride);
|
|
@@ -27533,64 +28634,76 @@ var SqliteReplicaHandle = class {
|
|
|
27533
28634
|
__privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
|
|
27534
28635
|
__privateAdd(this, _transportFactory);
|
|
27535
28636
|
this.spec = input.spec;
|
|
28637
|
+
this.replicaId = input.replicaId;
|
|
27536
28638
|
__privateSet(this, _device, input.device);
|
|
27537
28639
|
this.deviceDid = input.device.did;
|
|
27538
28640
|
__privateSet(this, _store, input.store);
|
|
27539
28641
|
__privateSet(this, _replicaDir, input.replicaDir);
|
|
27540
28642
|
__privateSet(this, _replica, input.replica);
|
|
27541
28643
|
__privateSet(this, _guard2, input.guard);
|
|
28644
|
+
__privateSet(this, _read, input.read);
|
|
28645
|
+
__privateSet(this, _policy, input.policy);
|
|
28646
|
+
__privateSet(this, _recordGrantInstall, input.recordGrantInstall);
|
|
27542
28647
|
__privateSet(this, _transportOverride, input.transportFor);
|
|
27543
28648
|
__privateSet(this, _dirSync2, input.dirSync);
|
|
27544
|
-
__privateSet(this,
|
|
28649
|
+
__privateSet(this, _generation2, input.generation);
|
|
27545
28650
|
__privateSet(this, _ino, input.ino);
|
|
27546
28651
|
}
|
|
27547
28652
|
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: {
|
|
28653
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28654
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28655
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28656
|
+
const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).get(key2);
|
|
28657
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28658
|
+
const meta = {
|
|
27588
28659
|
asOf: result.meta.asOf,
|
|
27589
28660
|
coverage: result.meta.coverage,
|
|
27590
28661
|
authority: result.meta.authority,
|
|
27591
|
-
syncedThroughEpoch
|
|
28662
|
+
syncedThroughEpoch
|
|
28663
|
+
};
|
|
28664
|
+
switch (result.status) {
|
|
28665
|
+
case "present":
|
|
28666
|
+
return {
|
|
28667
|
+
status: "present",
|
|
28668
|
+
key: result.key,
|
|
28669
|
+
value: result.value,
|
|
28670
|
+
etag: result.etag,
|
|
28671
|
+
metadata: result.metadata,
|
|
28672
|
+
meta
|
|
28673
|
+
};
|
|
28674
|
+
case "content_missing":
|
|
28675
|
+
return {
|
|
28676
|
+
status: "content_missing",
|
|
28677
|
+
key: result.key,
|
|
28678
|
+
etag: result.etag,
|
|
28679
|
+
metadata: result.metadata,
|
|
28680
|
+
meta
|
|
28681
|
+
};
|
|
28682
|
+
default:
|
|
28683
|
+
return { status: result.status, key: result.key, meta };
|
|
27592
28684
|
}
|
|
27593
|
-
};
|
|
28685
|
+
}, true);
|
|
28686
|
+
}
|
|
28687
|
+
async list(options) {
|
|
28688
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28689
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28690
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28691
|
+
const result = await new (__privateGet(this, _replica)).Replica({ store: snapshot }).list({
|
|
28692
|
+
prefix: options.prefix,
|
|
28693
|
+
...options.after === void 0 ? {} : { after: options.after },
|
|
28694
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
28695
|
+
});
|
|
28696
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertEffective_fn).call(this, snapshot);
|
|
28697
|
+
return {
|
|
28698
|
+
keys: result.entries.map((entry) => entry.key),
|
|
28699
|
+
meta: {
|
|
28700
|
+
asOf: result.meta.asOf,
|
|
28701
|
+
coverage: result.meta.coverage,
|
|
28702
|
+
authority: result.meta.authority,
|
|
28703
|
+
syncedThroughEpoch
|
|
28704
|
+
}
|
|
28705
|
+
};
|
|
28706
|
+
}, true);
|
|
27594
28707
|
}
|
|
27595
28708
|
/**
|
|
27596
28709
|
* The newest installed grant — the pending one when a not-yet-promoted
|
|
@@ -27600,43 +28713,59 @@ var SqliteReplicaHandle = class {
|
|
|
27600
28713
|
* grant is still reported (unconstrained: false) and never reused.
|
|
27601
28714
|
*/
|
|
27602
28715
|
async grant() {
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
|
|
27611
|
-
|
|
28716
|
+
return __privateGet(this, _read).call(this, async (snapshot) => {
|
|
28717
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28718
|
+
const state = await snapshot.open();
|
|
28719
|
+
if (state === null) return null;
|
|
28720
|
+
const selected = state.pendingGrant ?? state.grant;
|
|
28721
|
+
const install = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
|
|
28722
|
+
return newestGrantInfo(
|
|
28723
|
+
__privateGet(this, _replica),
|
|
28724
|
+
state.pendingGrant ?? state.grant,
|
|
28725
|
+
state.pendingGrant,
|
|
28726
|
+
this.spec,
|
|
28727
|
+
install
|
|
28728
|
+
);
|
|
28729
|
+
});
|
|
27612
28730
|
}
|
|
27613
28731
|
async installGrant(ucan) {
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
__privateGet(this, _replica).
|
|
27620
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
|
|
27624
|
-
|
|
27625
|
-
|
|
27626
|
-
|
|
27627
|
-
|
|
28732
|
+
return __privateGet(this, _guard2).call(this, async () => {
|
|
28733
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
28734
|
+
const parsed = __privateGet(this, _replica).parseUcanGrant(ucan);
|
|
28735
|
+
const state = await __privateGet(this, _store).open();
|
|
28736
|
+
if (state === null) {
|
|
28737
|
+
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
28738
|
+
__privateGet(this, _replica).ReplicaErrorCode.NOT_FOUND,
|
|
28739
|
+
"The replica has not been created."
|
|
28740
|
+
);
|
|
28741
|
+
}
|
|
28742
|
+
__privateGet(this, _replica).assertGrantInstallable(parsed, {
|
|
28743
|
+
deviceDid: principalOf(__privateGet(this, _device).did),
|
|
28744
|
+
space: this.spec.space,
|
|
28745
|
+
prefix: this.spec.prefix,
|
|
28746
|
+
now: Date.now()
|
|
28747
|
+
});
|
|
28748
|
+
if (!ucanAttUnconstrainedFor(parsed.att, this.spec.space, this.spec.prefix)) {
|
|
28749
|
+
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
28750
|
+
__privateGet(this, _replica).ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
28751
|
+
"The grant's get or sync coverage carries caveats; re-mint instead of installing it."
|
|
28752
|
+
);
|
|
28753
|
+
}
|
|
28754
|
+
const now = Date.now();
|
|
28755
|
+
const policy = await __privateGet(this, _policy).call(this, now, true);
|
|
28756
|
+
if (policy === null || now >= policy.consentDeadline) {
|
|
28757
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The replica's consent window has expired.");
|
|
28758
|
+
}
|
|
28759
|
+
const installedAt = await __privateGet(this, _recordGrantInstall).call(this, parsed.cid, now);
|
|
28760
|
+
const installed = await __privateGet(this, _store).installGatedGrant(parsed, { ...policy, at: installedAt });
|
|
28761
|
+
return newestGrantInfo(__privateGet(this, _replica), parsed, parsed, this.spec, installed);
|
|
27628
28762
|
});
|
|
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
28763
|
}
|
|
27638
28764
|
async sync(options) {
|
|
27639
|
-
await
|
|
28765
|
+
await __privateGet(this, _guard2).call(this, async () => {
|
|
28766
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertLive_fn).call(this);
|
|
28767
|
+
await __privateMethod(this, _SqliteReplicaHandle_instances, assertSyncEffective_fn).call(this);
|
|
28768
|
+
});
|
|
27640
28769
|
const transportFor = await __privateMethod(this, _SqliteReplicaHandle_instances, transports_fn).call(this);
|
|
27641
28770
|
const abort = new AbortController();
|
|
27642
28771
|
options.signal.throwIfAborted();
|
|
@@ -27677,27 +28806,33 @@ var SqliteReplicaHandle = class {
|
|
|
27677
28806
|
}
|
|
27678
28807
|
}
|
|
27679
28808
|
async status() {
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
state
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27700
|
-
|
|
28809
|
+
return __privateGet(this, _read).call(this, async (snapshot, syncedThroughEpoch) => {
|
|
28810
|
+
__privateMethod(this, _SqliteReplicaHandle_instances, assertOpen_fn).call(this);
|
|
28811
|
+
const engine = new (__privateGet(this, _replica)).Replica({ store: snapshot });
|
|
28812
|
+
const status = await engine.status();
|
|
28813
|
+
const state = await snapshot.open();
|
|
28814
|
+
const selected = state?.pendingGrant ?? state?.grant ?? null;
|
|
28815
|
+
const selectedInstall = selected === null ? null : await snapshot.getGrantInstall(selected.cid);
|
|
28816
|
+
const activeRecord = state?.grant ?? null;
|
|
28817
|
+
const activeInstall = activeRecord === null ? null : await snapshot.getGrantInstall(activeRecord.cid);
|
|
28818
|
+
const grant = state === null ? null : newestGrantInfo(
|
|
28819
|
+
__privateGet(this, _replica),
|
|
28820
|
+
state.pendingGrant ?? state.grant,
|
|
28821
|
+
state.pendingGrant,
|
|
28822
|
+
this.spec,
|
|
28823
|
+
selectedInstall
|
|
28824
|
+
);
|
|
28825
|
+
return {
|
|
28826
|
+
coverage: status.coverage,
|
|
28827
|
+
lastSyncAt: status.lastSyncAt,
|
|
28828
|
+
syncedThroughEpoch,
|
|
28829
|
+
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 },
|
|
28830
|
+
grant,
|
|
28831
|
+
counts: status.counts,
|
|
28832
|
+
bytes: status.bytes,
|
|
28833
|
+
lastError: status.lastError
|
|
28834
|
+
};
|
|
28835
|
+
}, true);
|
|
27701
28836
|
}
|
|
27702
28837
|
async close() {
|
|
27703
28838
|
if (__privateGet(this, _closed)) return;
|
|
@@ -27712,8 +28847,11 @@ _store = new WeakMap();
|
|
|
27712
28847
|
_replicaDir = new WeakMap();
|
|
27713
28848
|
_replica = new WeakMap();
|
|
27714
28849
|
_guard2 = new WeakMap();
|
|
28850
|
+
_read = new WeakMap();
|
|
28851
|
+
_policy = new WeakMap();
|
|
28852
|
+
_recordGrantInstall = new WeakMap();
|
|
27715
28853
|
_dirSync2 = new WeakMap();
|
|
27716
|
-
|
|
28854
|
+
_generation2 = new WeakMap();
|
|
27717
28855
|
_ino = new WeakMap();
|
|
27718
28856
|
_transportOverride = new WeakMap();
|
|
27719
28857
|
_closed = new WeakMap();
|
|
@@ -27748,7 +28886,7 @@ assertLive_fn = async function() {
|
|
|
27748
28886
|
throw error;
|
|
27749
28887
|
}
|
|
27750
28888
|
const state = await __privateGet(this, _store).open();
|
|
27751
|
-
if (state !== null && state.generation !== __privateGet(this,
|
|
28889
|
+
if (state !== null && state.generation !== __privateGet(this, _generation2)) {
|
|
27752
28890
|
throw new (__privateGet(this, _replica)).ReplicaError(
|
|
27753
28891
|
__privateGet(this, _replica).ReplicaErrorCode.RESET_REQUIRED,
|
|
27754
28892
|
"The replica was purged or reopened; open it again to continue."
|
|
@@ -27790,97 +28928,1099 @@ transports_fn = async function() {
|
|
|
27790
28928
|
})());
|
|
27791
28929
|
return __privateGet(this, _transportFactory);
|
|
27792
28930
|
};
|
|
28931
|
+
assertEffective_fn = async function(snapshot) {
|
|
28932
|
+
const state = await snapshot.open();
|
|
28933
|
+
if (state?.grant === void 0 || state.grant === null) return;
|
|
28934
|
+
const installed = await snapshot.getGrantInstall(state.grant.cid);
|
|
28935
|
+
if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || Date.now() >= installed.effectiveExpiresAt || Date.now() >= installed.consentDeadline) {
|
|
28936
|
+
throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
|
|
28937
|
+
}
|
|
28938
|
+
};
|
|
28939
|
+
assertSyncEffective_fn = async function() {
|
|
28940
|
+
const state = await __privateGet(this, _store).open();
|
|
28941
|
+
const selected = state?.pendingGrant ?? state?.grant;
|
|
28942
|
+
if (selected === void 0 || selected === null) return;
|
|
28943
|
+
const installed = await __privateGet(this, _store).getGrantInstall(selected.cid);
|
|
28944
|
+
const now = Date.now();
|
|
28945
|
+
if (installed === null || !Number.isSafeInteger(installed.effectiveExpiresAt) || now >= installed.effectiveExpiresAt || now >= installed.consentDeadline) {
|
|
28946
|
+
throw new ReplicaStorageError("GRANT_EXPIRED", "The replica grant's effective lifetime has expired.");
|
|
28947
|
+
}
|
|
28948
|
+
};
|
|
27793
28949
|
function createSqliteReplicaStorage(options) {
|
|
27794
28950
|
const root = options.dir;
|
|
27795
|
-
const
|
|
28951
|
+
const profileDir = dirname2(options.gateFile ?? join2(dirname2(root), REPLICA_GATE_FILE));
|
|
28952
|
+
const diagnosticFiles = [...options.diagnosticFiles ?? []];
|
|
28953
|
+
for (const name of diagnosticFiles) {
|
|
28954
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || /^[a-z2-7]{26}$/.test(name)) {
|
|
28955
|
+
throw new TypeError(`Invalid replication diagnostics file name ${JSON.stringify(name)}.`);
|
|
28956
|
+
}
|
|
28957
|
+
}
|
|
28958
|
+
const inspectLayout = (gateReadable, approvedMarker, legacyOnly = false) => inspectProfileReplicaLayout(profileDir, gateReadable, approvedMarker, legacyOnly, diagnosticFiles);
|
|
28959
|
+
const guard = options.guard;
|
|
27796
28960
|
const createDevice = options.createDevice ?? createDeviceKey;
|
|
27797
|
-
|
|
27798
|
-
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
28961
|
+
const heldLock = new AsyncLocalStorage();
|
|
28962
|
+
let visibleStamp;
|
|
28963
|
+
let visiblePrincipalHash;
|
|
28964
|
+
let checkedGate;
|
|
28965
|
+
async function profileSection(section) {
|
|
28966
|
+
if (heldLock.getStore() === true) return section();
|
|
28967
|
+
return requiredGuard(guard, () => heldLock.run(true, section));
|
|
28968
|
+
}
|
|
28969
|
+
async function guarded(principal, stamp, identity, partitionId, section) {
|
|
28970
|
+
if (stamp === void 0) throw gated("not_established");
|
|
28971
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
28972
|
+
const gate = await fileGate();
|
|
28973
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
28974
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28975
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28976
|
+
gate.principalHashOf(snapshot.salt, principal),
|
|
28977
|
+
identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, replicationIdentityKey(identity))
|
|
27803
28978
|
]);
|
|
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
|
|
28979
|
+
return profileSection(async () => {
|
|
28980
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28981
|
+
await gate.assertGateAccess(profileDir, {
|
|
28982
|
+
stamp,
|
|
28983
|
+
incarnation: snapshot.incarnation,
|
|
28984
|
+
principalHash,
|
|
28985
|
+
identityKeyHash,
|
|
28986
|
+
partitionId
|
|
28987
|
+
}, snapshot.salt);
|
|
28988
|
+
const result = await section();
|
|
28989
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
28990
|
+
return result;
|
|
27819
28991
|
});
|
|
27820
|
-
|
|
28992
|
+
}
|
|
28993
|
+
async function readGated(spec, partitionId, store, replicaDir, generation, section, withFence = false) {
|
|
28994
|
+
if (spec.stamp === void 0) throw gated("not_established");
|
|
28995
|
+
const gate = await fileGate();
|
|
28996
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
28997
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28998
|
+
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28999
|
+
gate.principalHashOf(snapshot.salt, spec.identity.principal),
|
|
29000
|
+
gate.identityHashOf(snapshot.salt, replicationIdentityKey(spec.identity))
|
|
29001
|
+
]);
|
|
29002
|
+
const access = {
|
|
29003
|
+
stamp: spec.stamp,
|
|
29004
|
+
incarnation: snapshot.incarnation,
|
|
29005
|
+
principalHash,
|
|
29006
|
+
identityKeyHash,
|
|
29007
|
+
partitionId
|
|
29008
|
+
};
|
|
29009
|
+
const path = options.gateFile ?? join2(profileDir, REPLICA_GATE_FILE);
|
|
29010
|
+
let checked;
|
|
29011
|
+
const checkGate = async () => {
|
|
29012
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29013
|
+
let before;
|
|
29014
|
+
try {
|
|
29015
|
+
before = await stat(path, { bigint: true });
|
|
29016
|
+
} catch {
|
|
29017
|
+
throw gated("gate_unavailable");
|
|
29018
|
+
}
|
|
29019
|
+
if (checked?.ino === before.ino && checked.size === before.size && checked.mtimeNs === before.mtimeNs) return;
|
|
29020
|
+
const checkedState = await gate.assertGateAccess(profileDir, access, snapshot.salt);
|
|
29021
|
+
if (spec.grantPolicy?.consentAnchor === null && checkedState.firstInstall[identityKeyHash] === void 0) {
|
|
29022
|
+
const installed = await store.open();
|
|
29023
|
+
if (installed !== null && (installed.grant !== null || installed.pendingGrant !== null)) throw gated("gate_unavailable");
|
|
29024
|
+
}
|
|
29025
|
+
let after;
|
|
29026
|
+
try {
|
|
29027
|
+
after = await stat(path, { bigint: true });
|
|
29028
|
+
} catch {
|
|
29029
|
+
throw gated("gate_unavailable");
|
|
29030
|
+
}
|
|
29031
|
+
if (before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
|
|
29032
|
+
throw gated("gate_unavailable");
|
|
29033
|
+
}
|
|
29034
|
+
checked = after;
|
|
29035
|
+
};
|
|
29036
|
+
if (withFence) await checkGate();
|
|
29037
|
+
const fence = withFence ? await readFence(replicaDir) : 0;
|
|
29038
|
+
return store.readTransaction(generation, checkGate, (sqliteSnapshot) => section(sqliteSnapshot, fence));
|
|
29039
|
+
}
|
|
29040
|
+
async function identityDirs() {
|
|
29041
|
+
let entries;
|
|
29042
|
+
try {
|
|
29043
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
29044
|
+
} catch (error) {
|
|
29045
|
+
if (isNotFound(error)) return [];
|
|
29046
|
+
throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
|
|
29047
|
+
}
|
|
29048
|
+
try {
|
|
29049
|
+
const marker = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29050
|
+
if (marker !== null) {
|
|
29051
|
+
const inventory = await inspectLayout(true);
|
|
29052
|
+
assertApprovedInventory(inventory, {
|
|
29053
|
+
unknownEntries: marker.approvedUnknown,
|
|
29054
|
+
knownRoots: marker.knownRoots,
|
|
29055
|
+
knownEntries: marker.knownEntries,
|
|
29056
|
+
missingDatabases: marker.missingDatabases
|
|
29057
|
+
}, true);
|
|
29058
|
+
}
|
|
29059
|
+
for (const entry of entries) {
|
|
29060
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
29061
|
+
if (marker?.approvedUnknown.some((item) => item.path === `replication/${entry.name}` && (entry.isDirectory() ? item.kind === "directory" : entry.isSymbolicLink() ? item.kind === "symlink" : item.kind === "file"))) continue;
|
|
29062
|
+
if (diagnosticFiles.includes(entry.name)) {
|
|
29063
|
+
const info = await lstat(join2(root, entry.name)).catch((error) => {
|
|
29064
|
+
if (isNotFound(error)) return null;
|
|
29065
|
+
throw error;
|
|
29066
|
+
});
|
|
29067
|
+
if (info === null || info.isFile()) continue;
|
|
29068
|
+
}
|
|
29069
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Unknown identity partition at ${root}.`);
|
|
29070
|
+
}
|
|
29071
|
+
}
|
|
29072
|
+
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));
|
|
29073
|
+
} catch (error) {
|
|
29074
|
+
throw storageError("STORAGE_ERROR", `Listing ${root}`, error);
|
|
29075
|
+
}
|
|
29076
|
+
}
|
|
29077
|
+
async function collectIntents(principalHash, salt) {
|
|
29078
|
+
const gate = await fileGate();
|
|
29079
|
+
const intents = [];
|
|
29080
|
+
for (const idDir of await identityDirs()) {
|
|
29081
|
+
const identity = identityRecordOf(await readJsonFile(join2(idDir, "identity.json")));
|
|
29082
|
+
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
|
|
29083
|
+
if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
|
|
29084
|
+
const identityKeyHash = await gate.identityHashOf(salt, replicationIdentityKey(identity));
|
|
29085
|
+
const recordedAt = Date.now();
|
|
29086
|
+
if (await pathExists(join2(idDir, "pending.json")) || await pathExists(join2(idDir, "device.jwk"))) {
|
|
29087
|
+
intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
|
|
29088
|
+
}
|
|
29089
|
+
const replicasDir = join2(idDir, "replicas");
|
|
29090
|
+
let entries;
|
|
29091
|
+
try {
|
|
29092
|
+
entries = await readdir(replicasDir, { withFileTypes: true });
|
|
29093
|
+
} catch (error) {
|
|
29094
|
+
if (isNotFound(error)) continue;
|
|
29095
|
+
throw storageError("STORAGE_ERROR", `Listing ${replicasDir}`, error);
|
|
29096
|
+
}
|
|
29097
|
+
for (const entry of entries) {
|
|
29098
|
+
if (!entry.isDirectory()) continue;
|
|
29099
|
+
if (!/^[a-z2-7]{26}$/.test(entry.name)) {
|
|
29100
|
+
throw new ReplicaStorageError("STORAGE_ERROR", `Unrecognized replica partition at ${replicasDir}.`);
|
|
29101
|
+
}
|
|
29102
|
+
const partitionId = `${identityKeyHash}:${entry.name}`;
|
|
29103
|
+
intents.push({ id: partitionId, kind: "partition", partitionId, principalHash, recordedAt });
|
|
29104
|
+
}
|
|
29105
|
+
}
|
|
29106
|
+
return intents;
|
|
29107
|
+
}
|
|
29108
|
+
async function retainGrantInstalls(gate, gateState, identityKeyHash, store) {
|
|
29109
|
+
for (const { cid, installedAt } of await store.grantInstallTimes()) {
|
|
29110
|
+
const key2 = await gate.grantInstallHashOf(gateState.salt, identityKeyHash, cid);
|
|
29111
|
+
await gate.recordGrantInstall(profileDir, key2, installedAt);
|
|
29112
|
+
}
|
|
29113
|
+
}
|
|
29114
|
+
async function removeCorruptOrphanGate(gate) {
|
|
29115
|
+
const path = join2(profileDir, REPLICA_GATE_FILE);
|
|
29116
|
+
const before = await lstat(path, { bigint: true });
|
|
29117
|
+
if (!before.isFile() || before.isSymbolicLink()) throw gated("gate_unavailable");
|
|
29118
|
+
let corrupt = false;
|
|
27821
29119
|
try {
|
|
29120
|
+
await gate.readGateSnapshot(profileDir);
|
|
29121
|
+
} catch (error) {
|
|
29122
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED" && "gate" in error && error.gate === "gate_unavailable") {
|
|
29123
|
+
corrupt = true;
|
|
29124
|
+
} else throw error;
|
|
29125
|
+
}
|
|
29126
|
+
if (!corrupt) throw gated("stamp_stale");
|
|
29127
|
+
const current = await lstat(path, { bigint: true });
|
|
29128
|
+
if (current.dev !== before.dev || current.ino !== before.ino || current.mtimeNs !== before.mtimeNs || current.size !== before.size) throw gated("stamp_stale");
|
|
29129
|
+
await rm(path);
|
|
29130
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29131
|
+
if (await gate.readGateSnapshot(profileDir) !== void 0) throw gated("stamp_stale");
|
|
29132
|
+
}
|
|
29133
|
+
async function readLogoutState(gate) {
|
|
29134
|
+
try {
|
|
29135
|
+
return { read: await gate.readLogoutGate(profileDir), corrupt: false };
|
|
29136
|
+
} catch (error) {
|
|
29137
|
+
if (isGateUnavailable(error)) return { read: void 0, corrupt: true };
|
|
29138
|
+
throw error;
|
|
29139
|
+
}
|
|
29140
|
+
}
|
|
29141
|
+
async function validateV1(gate, path, marker, read, mode) {
|
|
29142
|
+
if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
|
|
29143
|
+
const { state, floorLagging } = read;
|
|
29144
|
+
if (marker.deniedEpoch !== void 0) {
|
|
29145
|
+
if (floorLagging || !v1Denied(marker, state, marker.deniedEpoch)) throw restoredGate();
|
|
29146
|
+
return { marker, state, active: false };
|
|
29147
|
+
}
|
|
29148
|
+
if (!floorLagging && state.principalHash === null && state.lifecycleEpoch > marker.lifecycleEpoch && (marker.remotePending ? state.eraseDeferred && state.deferredPrincipalHash === marker.principalHash : !state.eraseDeferred && state.deferredPrincipalHash === null)) {
|
|
29149
|
+
return { marker: await bindDeniedEpoch(path, marker, state.lifecycleEpoch), state, active: false };
|
|
29150
|
+
}
|
|
29151
|
+
if (state.principalHash !== marker.principalHash || state.lifecycleEpoch !== marker.lifecycleEpoch) {
|
|
29152
|
+
throw gated("stamp_stale");
|
|
29153
|
+
}
|
|
29154
|
+
if (!floorLagging && !mode.denyActive) {
|
|
29155
|
+
if (!mode.active) throw gated("stamp_stale");
|
|
29156
|
+
return { marker, state, active: true };
|
|
29157
|
+
}
|
|
29158
|
+
if (mode.acceptLagging) return { marker, state, active: true };
|
|
29159
|
+
if (mode.deny === void 0) throw interruptedSignOut();
|
|
29160
|
+
const denied = await gate.denyGate(
|
|
29161
|
+
profileDir,
|
|
29162
|
+
{ reason: "sign_out", principal: "" },
|
|
29163
|
+
(principalHash) => collectIntents(principalHash, state.salt),
|
|
29164
|
+
{ salt: state.salt, principalHash: marker.principalHash },
|
|
29165
|
+
{ deferErase: mode.deny === "deferred" }
|
|
29166
|
+
);
|
|
29167
|
+
if (denied.status !== "denied") throw gated("gate_unavailable");
|
|
29168
|
+
const after = await gate.readLogoutGate(profileDir);
|
|
29169
|
+
if (after === void 0 || after.floorLagging || !v1Denied(marker, after.state, denied.lifecycleEpoch)) {
|
|
29170
|
+
throw gated("gate_unavailable");
|
|
29171
|
+
}
|
|
29172
|
+
return { marker: await bindDeniedEpoch(path, marker, denied.lifecycleEpoch), state: after.state, active: false };
|
|
29173
|
+
}
|
|
29174
|
+
function v1Denied(marker, state, epoch) {
|
|
29175
|
+
return state.incarnation === marker.incarnation && state.principalHash === null && state.lifecycleEpoch === epoch && (state.deferredPrincipalHash === marker.principalHash || !marker.remotePending && !state.eraseDeferred && state.deferredPrincipalHash === null);
|
|
29176
|
+
}
|
|
29177
|
+
async function bindDeniedEpoch(path, marker, deniedEpoch) {
|
|
29178
|
+
if (marker.deniedEpoch !== void 0) throw gated("stamp_stale");
|
|
29179
|
+
const bound = { ...marker, deniedEpoch };
|
|
29180
|
+
await writeJsonAtomic(path, bound, options.dirSync);
|
|
29181
|
+
return bound;
|
|
29182
|
+
}
|
|
29183
|
+
async function validatedRemoteMarker(path, marker, principal, mode) {
|
|
29184
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29185
|
+
const gate = await fileGate();
|
|
29186
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29187
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29188
|
+
if (marker.v === 2) {
|
|
29189
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29190
|
+
return { marker, state: void 0 };
|
|
29191
|
+
}
|
|
29192
|
+
if (read === void 0 || read.state.incarnation !== marker.incarnation) throw gated("stamp_stale");
|
|
29193
|
+
if (marker.v === 3) {
|
|
29194
|
+
if (read.floorLagging || read.state.lifecycleEpoch !== marker.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
|
|
29195
|
+
return { marker, state: read.state };
|
|
29196
|
+
}
|
|
29197
|
+
if (marker.principalHash !== await gate.principalHashOf(read.state.salt, principal)) {
|
|
29198
|
+
throw gated("principal_mismatch");
|
|
29199
|
+
}
|
|
29200
|
+
const checked = await validateV1(gate, path, marker, read, mode);
|
|
29201
|
+
return { marker: checked.marker, state: checked.state };
|
|
29202
|
+
}
|
|
29203
|
+
async function currentLayout() {
|
|
29204
|
+
const { read, corrupt } = await readLogoutState(await fileGate());
|
|
29205
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29206
|
+
const report = await inspectLayout(read !== void 0);
|
|
29207
|
+
assertSupportedProfileLayout(report);
|
|
29208
|
+
return report;
|
|
29209
|
+
}
|
|
29210
|
+
async function commitPlainAbandon(path, marker, report, discardLocalChanges) {
|
|
29211
|
+
const { remotePending: _, discardLocalChanges: _oldDiscard, ...previous } = marker;
|
|
29212
|
+
const abandoned = {
|
|
29213
|
+
...previous,
|
|
29214
|
+
remoteAbandoned: true,
|
|
29215
|
+
approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
|
|
29216
|
+
knownRoots: report.knownRoots,
|
|
29217
|
+
knownEntries: report.knownEntries,
|
|
29218
|
+
missingDatabases: report.missingDatabases,
|
|
29219
|
+
...discardLocalChanges ? { discardLocalChanges: true } : {}
|
|
29220
|
+
};
|
|
29221
|
+
await writeJsonAtomic(path, abandoned, options.dirSync);
|
|
29222
|
+
return abandoned;
|
|
29223
|
+
}
|
|
29224
|
+
function deny(input, denyOptions) {
|
|
29225
|
+
const deferErase = denyOptions?.deferErase === true;
|
|
29226
|
+
if (deferErase && input.reason !== "sign_out") {
|
|
29227
|
+
throw new TypeError("Deferred erase is only available for sign-out.");
|
|
29228
|
+
}
|
|
29229
|
+
visibleStamp = void 0;
|
|
29230
|
+
visiblePrincipalHash = void 0;
|
|
29231
|
+
checkedGate = void 0;
|
|
29232
|
+
return (async () => {
|
|
27822
29233
|
try {
|
|
27823
|
-
await
|
|
27824
|
-
|
|
27825
|
-
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
|
|
29234
|
+
const gate = await fileGate();
|
|
29235
|
+
return await profileSection(async () => {
|
|
29236
|
+
const snapshot = (await gate.readLogoutGate(profileDir))?.state;
|
|
29237
|
+
const precomputed = snapshot === void 0 ? void 0 : {
|
|
29238
|
+
salt: snapshot.salt,
|
|
29239
|
+
principalHash: await gate.principalHashOf(snapshot.salt, input.principal)
|
|
29240
|
+
};
|
|
29241
|
+
const result = await gate.denyGate(
|
|
29242
|
+
profileDir,
|
|
29243
|
+
input,
|
|
29244
|
+
async (principalHash) => {
|
|
29245
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29246
|
+
return collectIntents(principalHash, snapshot.salt);
|
|
29247
|
+
},
|
|
29248
|
+
precomputed,
|
|
29249
|
+
{ deferErase }
|
|
29250
|
+
);
|
|
29251
|
+
if (result.status !== "denied") return result;
|
|
29252
|
+
if (input.reason === "sign_out" && snapshot !== void 0 && precomputed !== void 0) {
|
|
29253
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29254
|
+
const marker = await readLogoutMarker(path);
|
|
29255
|
+
if (marker?.v === 1 && marker.deniedEpoch === void 0 && marker.incarnation === snapshot.incarnation && marker.principalHash === precomputed.principalHash && marker.remotePending === true === deferErase) {
|
|
29256
|
+
await bindDeniedEpoch(path, marker, result.lifecycleEpoch);
|
|
29257
|
+
}
|
|
29258
|
+
}
|
|
29259
|
+
if (!deferErase) return result;
|
|
29260
|
+
const resumeToken = await gate.deferredGateToken(profileDir);
|
|
29261
|
+
if (resumeToken === null || resumeToken.deniedEpoch !== result.lifecycleEpoch) {
|
|
29262
|
+
return { status: "unguarded", code: "gate_unavailable" };
|
|
29263
|
+
}
|
|
29264
|
+
return { ...result, resumeToken };
|
|
27833
29265
|
});
|
|
27834
29266
|
} catch (error) {
|
|
27835
|
-
|
|
27836
|
-
|
|
29267
|
+
return { status: "unguarded", code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "gate_unavailable" };
|
|
29268
|
+
}
|
|
29269
|
+
})();
|
|
29270
|
+
}
|
|
29271
|
+
const lifecycle = {
|
|
29272
|
+
kind: "file",
|
|
29273
|
+
async recover({ signal }) {
|
|
29274
|
+
try {
|
|
29275
|
+
return await lifecycle.erasePending({ signal });
|
|
29276
|
+
} catch (error) {
|
|
29277
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR";
|
|
29278
|
+
let remaining = 0;
|
|
29279
|
+
try {
|
|
29280
|
+
remaining = (await (await fileGate()).readGateSnapshot(profileDir))?.intents.length ?? 0;
|
|
29281
|
+
} catch {
|
|
27837
29282
|
}
|
|
29283
|
+
return { erased: [], failed: [{ id: "gate", code }], remaining };
|
|
27838
29284
|
}
|
|
27839
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
|
|
29285
|
+
},
|
|
29286
|
+
async establishSessionStamp(principal, { consentAnchor }) {
|
|
29287
|
+
if (consentAnchor?.source === "first_install" || !Number.isSafeInteger(consentAnchor?.at) || consentAnchor.at < 0) {
|
|
29288
|
+
throw gated("not_established");
|
|
29289
|
+
}
|
|
29290
|
+
const gate = await fileGate();
|
|
29291
|
+
const stamp = await profileSection(async () => {
|
|
29292
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29293
|
+
return gate.establishSessionStamp(profileDir, principal, { siweIssuedAt: consentAnchor.at });
|
|
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 restoreSessionStamp(principal, persisted) {
|
|
29303
|
+
const gate = await fileGate();
|
|
29304
|
+
const stamp = await profileSection(async () => {
|
|
29305
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29306
|
+
return gate.restoreSessionStamp(profileDir, principal, persisted);
|
|
29307
|
+
});
|
|
29308
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29309
|
+
if (state === void 0) throw gated("gate_unavailable");
|
|
29310
|
+
visiblePrincipalHash = await gate.principalHashOf(state.salt, principal);
|
|
29311
|
+
checkedGate = void 0;
|
|
29312
|
+
visibleStamp = stamp;
|
|
29313
|
+
return stamp;
|
|
29314
|
+
},
|
|
29315
|
+
async activationAllowed(next) {
|
|
29316
|
+
if (guard === void 0) return false;
|
|
29317
|
+
try {
|
|
29318
|
+
const gate = await fileGate();
|
|
29319
|
+
return await profileSection(async () => {
|
|
29320
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29321
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29322
|
+
return state !== void 0 && !state.eraseDeferred && state.intents.length === 0 && state.principalHash !== null && state.principalHash === await gate.principalHashOf(state.salt, next);
|
|
29323
|
+
});
|
|
29324
|
+
} catch {
|
|
29325
|
+
return false;
|
|
27845
29326
|
}
|
|
27846
|
-
|
|
27847
|
-
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
29327
|
+
},
|
|
29328
|
+
checkDelivery(stamp) {
|
|
29329
|
+
if (guard === void 0 || visiblePrincipalHash === void 0) return { status: "gated", gate: "gate_unavailable" };
|
|
29330
|
+
try {
|
|
29331
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
29332
|
+
} catch {
|
|
29333
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
27852
29334
|
}
|
|
27853
|
-
if (
|
|
27854
|
-
|
|
27855
|
-
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
27856
|
-
"The stored replica belongs to a different device key; purge the partition to rekey."
|
|
27857
|
-
);
|
|
29335
|
+
if (!stamp || visibleStamp?.incarnation !== stamp.incarnation || visibleStamp.lifecycleEpoch !== stamp.lifecycleEpoch) {
|
|
29336
|
+
return { status: "gated", gate: "stamp_stale" };
|
|
27858
29337
|
}
|
|
27859
|
-
|
|
27860
|
-
|
|
27861
|
-
|
|
27862
|
-
|
|
29338
|
+
try {
|
|
29339
|
+
const path = options.gateFile ?? join2(profileDir, REPLICA_GATE_FILE);
|
|
29340
|
+
const stats = statSync(path, { bigint: true });
|
|
29341
|
+
if (checkedGate === void 0 || checkedGate.ino !== stats.ino || checkedGate.mtimeNs !== stats.mtimeNs || checkedGate.size !== stats.size) {
|
|
29342
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
29343
|
+
if (typeof raw.incarnation !== "string" || !Number.isSafeInteger(raw.lifecycleEpoch) || raw.principalHash !== null && typeof raw.principalHash !== "string" || !Array.isArray(raw.intents)) {
|
|
29344
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
29345
|
+
}
|
|
29346
|
+
checkedGate = {
|
|
29347
|
+
ino: stats.ino,
|
|
29348
|
+
mtimeNs: stats.mtimeNs,
|
|
29349
|
+
size: stats.size,
|
|
29350
|
+
incarnation: raw.incarnation,
|
|
29351
|
+
epoch: raw.lifecycleEpoch,
|
|
29352
|
+
principalHash: raw.principalHash
|
|
29353
|
+
};
|
|
29354
|
+
}
|
|
29355
|
+
} catch {
|
|
29356
|
+
return { status: "gated", gate: "gate_unavailable" };
|
|
29357
|
+
}
|
|
29358
|
+
if (checkedGate.incarnation !== stamp.incarnation || checkedGate.epoch !== stamp.lifecycleEpoch) {
|
|
29359
|
+
return { status: "gated", gate: "stamp_stale" };
|
|
29360
|
+
}
|
|
29361
|
+
return checkedGate.principalHash === visiblePrincipalHash ? { status: "allow" } : { status: "gated", gate: "principal_mismatch" };
|
|
29362
|
+
},
|
|
29363
|
+
consentDeadline() {
|
|
29364
|
+
return null;
|
|
29365
|
+
},
|
|
29366
|
+
deliverable(stamp) {
|
|
29367
|
+
return lifecycle.checkDelivery(stamp).status === "allow";
|
|
29368
|
+
},
|
|
29369
|
+
deny,
|
|
29370
|
+
deferredErase() {
|
|
29371
|
+
return profileSection(async () => (await fileGate()).deferredGateToken(profileDir));
|
|
29372
|
+
},
|
|
29373
|
+
async resumeDeferredErase(token, { signal }) {
|
|
29374
|
+
signal.throwIfAborted();
|
|
29375
|
+
await profileSection(async () => (await fileGate()).resumeDeferredGate(profileDir, token));
|
|
29376
|
+
return lifecycle.erasePending({ signal });
|
|
29377
|
+
},
|
|
29378
|
+
async preflightLogout({ principal, discardLocalChanges = false }) {
|
|
29379
|
+
return profileSection(async () => {
|
|
29380
|
+
const marker = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29381
|
+
const gate = await fileGate();
|
|
29382
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29383
|
+
const state = read?.state;
|
|
29384
|
+
let otherAccount = false;
|
|
29385
|
+
if (state !== void 0) {
|
|
29386
|
+
const hash3 = await gate.principalHashOf(state.salt, principal);
|
|
29387
|
+
otherAccount = gateOwnedByAnother(state, hash3);
|
|
29388
|
+
if (marker?.v === 1 && marker.principalHash !== hash3) throw gated("principal_mismatch");
|
|
29389
|
+
}
|
|
29390
|
+
const report = await inspectLayout(state !== void 0, void 0, otherAccount);
|
|
29391
|
+
assertSupportedProfileLayout(report);
|
|
29392
|
+
if (!discardLocalChanges && (report.unknownPaths.length > 0 || corrupt && !otherAccount || !otherAccount && (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
|
|
29393
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29394
|
+
}
|
|
29395
|
+
return report;
|
|
29396
|
+
});
|
|
29397
|
+
},
|
|
29398
|
+
async prepareLogoutPending({ principal, approval, discardLocalChanges = false, remotePending }) {
|
|
29399
|
+
const gate = await fileGate();
|
|
29400
|
+
return profileSection(async () => {
|
|
29401
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29402
|
+
const previous = await readLogoutMarker(path);
|
|
29403
|
+
const { read, corrupt: corruptGate } = await readLogoutState(gate);
|
|
29404
|
+
let state = read?.state;
|
|
29405
|
+
const principalHash = state === void 0 ? void 0 : await gate.principalHashOf(state.salt, principal);
|
|
29406
|
+
const otherAccount = state !== void 0 && gateOwnedByAnother(state, principalHash);
|
|
29407
|
+
let report = await inspectLayout(state !== void 0, void 0, otherAccount);
|
|
29408
|
+
assertSupportedProfileLayout(report);
|
|
29409
|
+
const assertApproved = (current) => {
|
|
29410
|
+
assertApprovedInventory(current, approval);
|
|
29411
|
+
if (approval.knownLegacyReplicas !== current.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(current.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(current.unknownLegacyPaths)) {
|
|
29412
|
+
throw localChangesRefusal(current.unknownPaths);
|
|
29413
|
+
}
|
|
29414
|
+
};
|
|
29415
|
+
assertApproved(report);
|
|
29416
|
+
if (otherAccount) {
|
|
29417
|
+
if (previous !== null) throw gated("principal_mismatch");
|
|
29418
|
+
if (!discardLocalChanges && report.unknownPaths.length > 0) throw localChangesRefusal(report.unknownPaths);
|
|
29419
|
+
return null;
|
|
29420
|
+
}
|
|
29421
|
+
if (!discardLocalChanges && (report.unknownPaths.length > 0 || corruptGate || (state === void 0 || state.principalHash === null && !state.eraseDeferred) && await hasOrphanPending(profileDir))) {
|
|
29422
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29423
|
+
}
|
|
29424
|
+
if (previous !== null) {
|
|
29425
|
+
if (previous.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29426
|
+
if (corruptGate) throw gated("gate_unavailable");
|
|
29427
|
+
let current = previous;
|
|
29428
|
+
if (previous.v === 2) {
|
|
29429
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29430
|
+
} else if (read === void 0 || previous.incarnation !== read.state.incarnation) {
|
|
29431
|
+
throw gated("stamp_stale");
|
|
29432
|
+
} else if (previous.v === 3) {
|
|
29433
|
+
if (read.floorLagging || read.state.lifecycleEpoch !== previous.lifecycleEpoch || read.state.principalHash !== null || read.state.eraseDeferred) throw gated("stamp_stale");
|
|
29434
|
+
} else {
|
|
29435
|
+
if (previous.principalHash !== principalHash) throw gated("principal_mismatch");
|
|
29436
|
+
current = (await validateV1(gate, path, previous, read, { active: true, acceptLagging: true })).marker;
|
|
29437
|
+
}
|
|
29438
|
+
assertApprovedInventory(report, {
|
|
29439
|
+
unknownEntries: current.approvedUnknown,
|
|
29440
|
+
knownRoots: current.knownRoots,
|
|
29441
|
+
knownEntries: current.knownEntries,
|
|
29442
|
+
missingDatabases: current.missingDatabases
|
|
29443
|
+
});
|
|
29444
|
+
if (current.remotePending !== remotePending || current.remoteAbandoned) {
|
|
29445
|
+
throw gated("gate_unavailable");
|
|
29446
|
+
}
|
|
29447
|
+
return current;
|
|
29448
|
+
}
|
|
29449
|
+
if (state?.eraseDeferred) throw gated("stamp_stale");
|
|
29450
|
+
if (read?.floorLagging) {
|
|
29451
|
+
if (state.principalHash === null) {
|
|
29452
|
+
state = await gate.advanceInactiveGate(profileDir);
|
|
29453
|
+
} else {
|
|
29454
|
+
const salt = state.salt;
|
|
29455
|
+
const denied = await gate.denyGate(
|
|
29456
|
+
profileDir,
|
|
29457
|
+
{ reason: "sign_out", principal },
|
|
29458
|
+
(hash3) => collectIntents(hash3, salt),
|
|
29459
|
+
{ salt, principalHash }
|
|
29460
|
+
);
|
|
29461
|
+
if (denied.status !== "denied") throw gated("gate_unavailable");
|
|
29462
|
+
state = await gate.readGateSnapshot(profileDir);
|
|
29463
|
+
}
|
|
29464
|
+
report = await inspectLayout(true);
|
|
29465
|
+
assertSupportedProfileLayout(report);
|
|
29466
|
+
assertApproved(report);
|
|
29467
|
+
}
|
|
29468
|
+
if (state === void 0 && report.knownRoots.length === 0 && !corruptGate) return null;
|
|
29469
|
+
if (state !== void 0 && state.principalHash === null && !state.eraseDeferred && report.knownRoots.length === 0 && state.intents.length === 0) return null;
|
|
29470
|
+
const remoteState = remotePending ? { remotePending: true } : {};
|
|
29471
|
+
const approvalFields = {
|
|
29472
|
+
principalDigest: logoutPrincipalDigest(principal),
|
|
29473
|
+
approvalSalt: randomBytes2(16).toString("hex"),
|
|
29474
|
+
approvedUnknown: discardLocalChanges ? report.unknownEntries : [],
|
|
29475
|
+
knownRoots: report.knownRoots,
|
|
29476
|
+
knownEntries: report.knownEntries,
|
|
29477
|
+
missingDatabases: report.missingDatabases,
|
|
29478
|
+
...discardLocalChanges ? { discardLocalChanges: true } : {},
|
|
29479
|
+
...remoteState
|
|
29480
|
+
};
|
|
29481
|
+
let marker;
|
|
29482
|
+
if (state === void 0) {
|
|
29483
|
+
marker = {
|
|
29484
|
+
v: 2,
|
|
29485
|
+
scope: "orphan_profile",
|
|
29486
|
+
nonce: randomBytes2(16).toString("hex"),
|
|
29487
|
+
...approvalFields
|
|
29488
|
+
};
|
|
29489
|
+
} else if (state.principalHash === null) {
|
|
29490
|
+
marker = {
|
|
29491
|
+
v: 3,
|
|
29492
|
+
scope: "inactive_gate",
|
|
29493
|
+
incarnation: state.incarnation,
|
|
29494
|
+
lifecycleEpoch: state.lifecycleEpoch,
|
|
29495
|
+
...approvalFields
|
|
29496
|
+
};
|
|
29497
|
+
} else {
|
|
29498
|
+
marker = {
|
|
29499
|
+
v: 1,
|
|
29500
|
+
incarnation: state.incarnation,
|
|
29501
|
+
lifecycleEpoch: state.lifecycleEpoch,
|
|
29502
|
+
principalHash,
|
|
29503
|
+
...approvalFields
|
|
29504
|
+
};
|
|
29505
|
+
}
|
|
29506
|
+
await writeJsonAtomic(path, marker, options.dirSync);
|
|
29507
|
+
visibleStamp = void 0;
|
|
29508
|
+
visiblePrincipalHash = void 0;
|
|
29509
|
+
checkedGate = void 0;
|
|
29510
|
+
if (corruptGate) await removeCorruptOrphanGate(gate);
|
|
29511
|
+
return marker;
|
|
29512
|
+
});
|
|
29513
|
+
},
|
|
29514
|
+
async completeRemoteLogout({ principal }) {
|
|
29515
|
+
return profileSection(async () => {
|
|
29516
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29517
|
+
const marker = await readLogoutMarker(path);
|
|
29518
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29519
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: false });
|
|
29520
|
+
const { remotePending: _, ...complete } = checked.marker;
|
|
29521
|
+
await writeJsonAtomic(path, complete, options.dirSync);
|
|
29522
|
+
return complete;
|
|
29523
|
+
});
|
|
29524
|
+
},
|
|
29525
|
+
async remotePendingLogout({ principal }) {
|
|
29526
|
+
return profileSection(async () => {
|
|
29527
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29528
|
+
const marker = await readLogoutMarker(path);
|
|
29529
|
+
if (marker === null || marker.remotePending !== true) return false;
|
|
29530
|
+
await validatedRemoteMarker(path, marker, principal, { active: false });
|
|
29531
|
+
return true;
|
|
29532
|
+
});
|
|
29533
|
+
},
|
|
29534
|
+
async abandonRemoteLogout({ principal, approval, discardLocalChanges = false }) {
|
|
29535
|
+
return profileSection(async () => {
|
|
29536
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29537
|
+
const marker = await readLogoutMarker(path);
|
|
29538
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29539
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29540
|
+
const report = await currentLayout();
|
|
29541
|
+
if (storedInventoryChanged(marker, report)) throw changedInventoryRefusal(marker, principal, report);
|
|
29542
|
+
assertApprovedInventory(report, approval);
|
|
29543
|
+
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) {
|
|
29544
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29545
|
+
}
|
|
29546
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
|
|
29547
|
+
return commitPlainAbandon(path, checked.marker, report, discardLocalChanges);
|
|
29548
|
+
});
|
|
29549
|
+
},
|
|
29550
|
+
async approveChangedPlainLogout({ principal, approval, discardLocalChanges, inventoryApprovalToken: token }) {
|
|
29551
|
+
return profileSection(async () => {
|
|
29552
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29553
|
+
const marker = await readLogoutMarker(path);
|
|
29554
|
+
if (marker === null || marker.remotePending !== true) throw gated("gate_unavailable");
|
|
29555
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29556
|
+
const report = await currentLayout();
|
|
29557
|
+
if (!storedInventoryChanged(marker, report)) throw gated("gate_unavailable");
|
|
29558
|
+
const entries = approvalEntries(report);
|
|
29559
|
+
if (discardLocalChanges !== true || typeof token !== "string" || !/^[0-9a-f]{32}$/.test(token) || token !== inventoryApprovalToken(marker, principal, entries)) {
|
|
29560
|
+
throw changedInventoryRefusal(marker, principal, report, entries);
|
|
29561
|
+
}
|
|
29562
|
+
assertApprovedInventory(report, approval);
|
|
29563
|
+
if (approval.knownLegacyReplicas !== report.knownLegacyReplicas || JSON.stringify(approval.unknownPaths) !== JSON.stringify(report.unknownPaths) || JSON.stringify(approval.unknownLegacyPaths) !== JSON.stringify(report.unknownLegacyPaths)) {
|
|
29564
|
+
throw changedInventoryRefusal(marker, principal, report, entries);
|
|
29565
|
+
}
|
|
29566
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true, deny: "deferred" });
|
|
29567
|
+
return commitPlainAbandon(path, checked.marker, report, true);
|
|
29568
|
+
});
|
|
29569
|
+
},
|
|
29570
|
+
async reapproveLogoutPending({ principal, approval }) {
|
|
29571
|
+
return profileSection(async () => {
|
|
29572
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29573
|
+
const marker = await readLogoutMarker(path);
|
|
29574
|
+
if (marker === null || marker.remotePending) throw gated("gate_unavailable");
|
|
29575
|
+
if (marker.principalDigest !== logoutPrincipalDigest(principal)) throw gated("principal_mismatch");
|
|
29576
|
+
const report = await currentLayout();
|
|
29577
|
+
assertApprovedInventory(report, approval);
|
|
29578
|
+
if (JSON.stringify(report.unknownPaths) !== JSON.stringify(approval.unknownPaths)) {
|
|
29579
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29580
|
+
}
|
|
29581
|
+
const checked = await validatedRemoteMarker(path, marker, principal, { active: true });
|
|
29582
|
+
const amended = {
|
|
29583
|
+
...checked.marker,
|
|
29584
|
+
approvedUnknown: report.unknownEntries,
|
|
29585
|
+
knownRoots: report.knownRoots,
|
|
29586
|
+
knownEntries: report.knownEntries,
|
|
29587
|
+
missingDatabases: report.missingDatabases,
|
|
29588
|
+
...report.unknownPaths.length > 0 ? { discardLocalChanges: true } : {}
|
|
29589
|
+
};
|
|
29590
|
+
await writeJsonAtomic(path, amended, options.dirSync);
|
|
29591
|
+
return amended;
|
|
29592
|
+
});
|
|
29593
|
+
},
|
|
29594
|
+
async recoverLogoutPending({ signal, clearCredentials, eraseLegacyReplicas }) {
|
|
29595
|
+
const path = join2(profileDir, REPLICA_LOGOUT_PENDING_FILE);
|
|
29596
|
+
const failure = async (error) => ({
|
|
29597
|
+
erased: [],
|
|
29598
|
+
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" }],
|
|
29599
|
+
remaining: Math.max(1, (await lifecycle.pendingPurge().catch(() => [])).length)
|
|
29600
|
+
});
|
|
29601
|
+
try {
|
|
29602
|
+
const prepared = await profileSection(async () => {
|
|
29603
|
+
const marker2 = await readLogoutMarker(path);
|
|
29604
|
+
if (marker2 === null) {
|
|
29605
|
+
const state2 = await (await fileGate()).readGateSnapshot(profileDir);
|
|
29606
|
+
return { report: { erased: [], failed: [], remaining: state2?.intents.length ?? 0 } };
|
|
29607
|
+
}
|
|
29608
|
+
signal.throwIfAborted();
|
|
29609
|
+
if (marker2.remotePending) throw gated("gate_unavailable");
|
|
29610
|
+
const gate = await fileGate();
|
|
29611
|
+
const { read, corrupt } = await readLogoutState(gate);
|
|
29612
|
+
if (corrupt) throw gated("gate_unavailable");
|
|
29613
|
+
let current = marker2;
|
|
29614
|
+
let state = read?.state;
|
|
29615
|
+
if (marker2.v === 2) {
|
|
29616
|
+
if (read !== void 0) throw gated("stamp_stale");
|
|
29617
|
+
} else if (read === void 0 || read.state.incarnation !== marker2.incarnation) {
|
|
29618
|
+
throw gated("stamp_stale");
|
|
29619
|
+
} else if (marker2.v === 3) {
|
|
29620
|
+
if (read.floorLagging || read.state.principalHash !== null || read.state.eraseDeferred || read.state.lifecycleEpoch !== marker2.lifecycleEpoch) throw gated("stamp_stale");
|
|
29621
|
+
} else {
|
|
29622
|
+
const checked = await validateV1(
|
|
29623
|
+
gate,
|
|
29624
|
+
path,
|
|
29625
|
+
marker2,
|
|
29626
|
+
read,
|
|
29627
|
+
{ active: true, deny: "ordinary", denyActive: true }
|
|
29628
|
+
);
|
|
29629
|
+
current = checked.marker;
|
|
29630
|
+
state = checked.state;
|
|
29631
|
+
}
|
|
29632
|
+
await clearCredentials();
|
|
29633
|
+
for (const file of ["session.json", "key.json"]) {
|
|
29634
|
+
try {
|
|
29635
|
+
await lstat(join2(profileDir, file));
|
|
29636
|
+
throw gated("gate_unavailable");
|
|
29637
|
+
} catch (error) {
|
|
29638
|
+
if (!isNotFound(error)) throw error;
|
|
29639
|
+
}
|
|
29640
|
+
}
|
|
29641
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29642
|
+
const report = await inspectLayout(state !== void 0, marker2);
|
|
29643
|
+
assertApprovedInventory(report, {
|
|
29644
|
+
unknownEntries: marker2.approvedUnknown,
|
|
29645
|
+
knownRoots: marker2.knownRoots,
|
|
29646
|
+
knownEntries: marker2.knownEntries,
|
|
29647
|
+
missingDatabases: marker2.missingDatabases
|
|
29648
|
+
}, true);
|
|
29649
|
+
if (!marker2.discardLocalChanges && report.unknownEntries.length > 0) {
|
|
29650
|
+
throw localChangesRefusal(report.unknownPaths);
|
|
29651
|
+
}
|
|
29652
|
+
if (current.v === 1 && state?.eraseDeferred) {
|
|
29653
|
+
await gate.resumeDeferredGate(profileDir, {
|
|
29654
|
+
deniedEpoch: current.deniedEpoch,
|
|
29655
|
+
principalHash: current.principalHash
|
|
29656
|
+
});
|
|
29657
|
+
}
|
|
29658
|
+
await eraseLegacyReplicas({
|
|
29659
|
+
discardLocalChanges: marker2.discardLocalChanges === true,
|
|
29660
|
+
approvedUnknown: marker2.approvedUnknown,
|
|
29661
|
+
knownRoots: marker2.knownRoots,
|
|
29662
|
+
knownEntries: marker2.knownEntries,
|
|
29663
|
+
missingDatabases: marker2.missingDatabases
|
|
29664
|
+
});
|
|
29665
|
+
try {
|
|
29666
|
+
await lstat(join2(profileDir, "replicas"));
|
|
29667
|
+
throw gated("gate_unavailable");
|
|
29668
|
+
} catch (error) {
|
|
29669
|
+
if (!isNotFound(error)) throw error;
|
|
29670
|
+
}
|
|
29671
|
+
return { marker: current };
|
|
29672
|
+
});
|
|
29673
|
+
if ("report" in prepared) return prepared.report;
|
|
29674
|
+
const marker = prepared.marker;
|
|
29675
|
+
let erased = { erased: [], failed: [], remaining: 0 };
|
|
29676
|
+
if (marker.v !== 2) {
|
|
29677
|
+
erased = await eraseGateIntents({ signal }, marker);
|
|
29678
|
+
if (erased.failed.length > 0 || erased.remaining > 0) return erased;
|
|
29679
|
+
}
|
|
29680
|
+
await profileSection(async () => {
|
|
29681
|
+
const pending = await readLogoutMarker(path);
|
|
29682
|
+
if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
|
|
29683
|
+
throw gated("stamp_stale");
|
|
29684
|
+
}
|
|
29685
|
+
const gate = await fileGate();
|
|
29686
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29687
|
+
if (marker.v === 2 && state !== void 0) throw gated("stamp_stale");
|
|
29688
|
+
const report = await inspectLayout(state !== void 0, marker);
|
|
29689
|
+
assertApprovedInventory(report, {
|
|
29690
|
+
unknownEntries: marker.approvedUnknown,
|
|
29691
|
+
knownRoots: marker.knownRoots,
|
|
29692
|
+
knownEntries: marker.knownEntries,
|
|
29693
|
+
missingDatabases: marker.missingDatabases
|
|
29694
|
+
}, true);
|
|
29695
|
+
await eraseApprovedUnknown(profileDir, marker, "replication", diagnosticFiles);
|
|
29696
|
+
await eraseKnownFlagResidue(profileDir, marker, diagnosticFiles);
|
|
29697
|
+
const finalReport = await inspectLayout(state !== void 0, marker);
|
|
29698
|
+
assertApprovedInventory(finalReport, {
|
|
29699
|
+
unknownEntries: marker.approvedUnknown,
|
|
29700
|
+
knownRoots: marker.knownRoots,
|
|
29701
|
+
knownEntries: marker.knownEntries,
|
|
29702
|
+
missingDatabases: marker.missingDatabases
|
|
29703
|
+
}, true);
|
|
29704
|
+
if (finalReport.unknownPaths.length > 0) throw localChangesRefusal(finalReport.unknownPaths);
|
|
29705
|
+
if (finalReport.knownRoots.length > 0) throw gated("gate_unavailable");
|
|
29706
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29707
|
+
await rm(path);
|
|
29708
|
+
await (options.dirSync ?? fsyncDir)(profileDir);
|
|
29709
|
+
});
|
|
29710
|
+
return erased;
|
|
29711
|
+
} catch (error) {
|
|
29712
|
+
return failure(error);
|
|
29713
|
+
}
|
|
29714
|
+
},
|
|
29715
|
+
async pendingPurge() {
|
|
29716
|
+
const gate = await fileGate();
|
|
29717
|
+
return (await gate.readGateSnapshot(profileDir))?.intents ?? [];
|
|
29718
|
+
},
|
|
29719
|
+
erasePending({ signal }) {
|
|
29720
|
+
return eraseGateIntents({ signal });
|
|
27863
29721
|
}
|
|
27864
|
-
|
|
29722
|
+
};
|
|
29723
|
+
async function eraseGateIntents({ signal }, marker) {
|
|
29724
|
+
const gate = await fileGate();
|
|
29725
|
+
const erased = [];
|
|
29726
|
+
const failed = [];
|
|
29727
|
+
const intents = await lifecycle.pendingPurge();
|
|
27865
29728
|
try {
|
|
27866
|
-
|
|
27867
|
-
|
|
27868
|
-
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
|
|
27872
|
-
|
|
27873
|
-
|
|
27874
|
-
|
|
27875
|
-
|
|
27876
|
-
|
|
27877
|
-
|
|
27878
|
-
|
|
27879
|
-
|
|
27880
|
-
|
|
29729
|
+
if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
|
|
29730
|
+
else if (marker.remotePending || await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE)) === null) {
|
|
29731
|
+
throw gated("gate_unavailable");
|
|
29732
|
+
}
|
|
29733
|
+
} catch {
|
|
29734
|
+
return { erased, failed: [{ id: "logout", code: "REPLICA_GATED" }], remaining: Math.max(1, intents.length) };
|
|
29735
|
+
}
|
|
29736
|
+
const held = await profileSection(() => gate.readGateSnapshot(profileDir));
|
|
29737
|
+
if (held?.eraseDeferred) return { erased, failed, remaining: held.intents.length, deferred: true };
|
|
29738
|
+
for (const intent of intents) {
|
|
29739
|
+
signal.throwIfAborted();
|
|
29740
|
+
try {
|
|
29741
|
+
await profileSection(async () => {
|
|
29742
|
+
if (marker === void 0) assertLogoutClear(options.logoutPendingFile);
|
|
29743
|
+
else {
|
|
29744
|
+
const pending = await readLogoutMarker(join2(profileDir, REPLICA_LOGOUT_PENDING_FILE));
|
|
29745
|
+
if (pending === null || pending.remotePending || JSON.stringify(pending) !== JSON.stringify(marker)) {
|
|
29746
|
+
throw gated("gate_unavailable");
|
|
29747
|
+
}
|
|
29748
|
+
assertApprovedInventory(await inspectLayout(true), {
|
|
29749
|
+
unknownEntries: marker.approvedUnknown,
|
|
29750
|
+
knownRoots: marker.knownRoots,
|
|
29751
|
+
knownEntries: marker.knownEntries,
|
|
29752
|
+
missingDatabases: marker.missingDatabases
|
|
29753
|
+
}, true);
|
|
29754
|
+
}
|
|
29755
|
+
const state = await gate.readGateSnapshot(profileDir);
|
|
29756
|
+
if (state?.eraseDeferred) return;
|
|
29757
|
+
if (!state?.intents.some((queued) => queued.id === intent.id)) return;
|
|
29758
|
+
let idDir;
|
|
29759
|
+
for (const dir of await identityDirs()) {
|
|
29760
|
+
const identity = identityRecordOf(await readJsonFile(join2(dir, "identity.json")));
|
|
29761
|
+
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
|
|
29762
|
+
const hash3 = await gate.identityHashOf(state.salt, replicationIdentityKey(identity));
|
|
29763
|
+
if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
|
|
29764
|
+
idDir = dir;
|
|
29765
|
+
break;
|
|
29766
|
+
}
|
|
29767
|
+
}
|
|
29768
|
+
if (intent.kind === "pending") {
|
|
29769
|
+
if (intent.id !== `pending:${intent.identityKeyHash}`) throw gated("gate_unavailable");
|
|
29770
|
+
if (idDir !== void 0) {
|
|
29771
|
+
for (const name of ["pending.json", "device.jwk"]) {
|
|
29772
|
+
const file = join2(idDir, name);
|
|
29773
|
+
let info;
|
|
29774
|
+
try {
|
|
29775
|
+
info = await lstat(file, { bigint: true });
|
|
29776
|
+
} catch (error) {
|
|
29777
|
+
if (isNotFound(error)) continue;
|
|
29778
|
+
throw error;
|
|
29779
|
+
}
|
|
29780
|
+
if (!info.isFile()) throw gated("gate_unavailable");
|
|
29781
|
+
if (marker !== void 0) {
|
|
29782
|
+
const relative = `replication/${basename(idDir)}/${name}`;
|
|
29783
|
+
const expected = [...marker.knownEntries, ...marker.approvedUnknown].find(
|
|
29784
|
+
(item) => item.path === relative
|
|
29785
|
+
);
|
|
29786
|
+
if (expected?.kind !== "file" || expected.dev !== info.dev.toString() || expected.ino !== info.ino.toString()) throw localChangesRefusal([relative]);
|
|
29787
|
+
}
|
|
29788
|
+
await rm(file);
|
|
29789
|
+
}
|
|
29790
|
+
await fsyncDir(idDir);
|
|
29791
|
+
}
|
|
29792
|
+
} else {
|
|
29793
|
+
if (!/^[0-9a-f]{64}:[a-z2-7]{26}$/.test(intent.partitionId) || intent.id !== intent.partitionId) {
|
|
29794
|
+
throw gated("gate_unavailable");
|
|
29795
|
+
}
|
|
29796
|
+
if (idDir !== void 0) {
|
|
29797
|
+
const replicaDir = join2(idDir, "replicas", intent.partitionId.slice(65));
|
|
29798
|
+
let dirInfo;
|
|
29799
|
+
if (await realDirectoryExists(join2(idDir, "replicas"))) {
|
|
29800
|
+
try {
|
|
29801
|
+
dirInfo = await lstat(replicaDir, { bigint: true });
|
|
29802
|
+
} catch (error) {
|
|
29803
|
+
if (!isNotFound(error)) throw error;
|
|
29804
|
+
}
|
|
29805
|
+
}
|
|
29806
|
+
if (dirInfo !== void 0) {
|
|
29807
|
+
const relative = `replication/${basename(idDir)}/replicas/${basename(replicaDir)}`;
|
|
29808
|
+
const expectedRoot = marker?.knownRoots.find((item) => item.path === relative);
|
|
29809
|
+
if (!dirInfo.isDirectory() || marker !== void 0 && (expectedRoot?.dev !== dirInfo.dev.toString() || expectedRoot.ino !== dirInfo.ino.toString())) throw localChangesRefusal([relative]);
|
|
29810
|
+
const approvedUnknown = marker?.approvedUnknown.filter(
|
|
29811
|
+
(item) => item.path.startsWith(`${relative}/`)
|
|
29812
|
+
).map(
|
|
29813
|
+
(item) => ({ ...item, path: item.path.slice(relative.length + 1) })
|
|
29814
|
+
);
|
|
29815
|
+
const expectedKnown = marker?.knownEntries.filter(
|
|
29816
|
+
(item) => item.path.startsWith(`${relative}/`)
|
|
29817
|
+
).map(
|
|
29818
|
+
(item) => ({ ...item, path: item.path.slice(relative.length + 1) })
|
|
29819
|
+
);
|
|
29820
|
+
const sqlite = await import("@tinycloud/replica/sqlite");
|
|
29821
|
+
if (!await pathExists(join2(replicaDir, "replica.db"))) {
|
|
29822
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
|
|
29823
|
+
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))) {
|
|
29824
|
+
throw localChangesRefusal([relative]);
|
|
29825
|
+
}
|
|
29826
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29827
|
+
approvedUnknown,
|
|
29828
|
+
expectedKnown: layout.knownEntries,
|
|
29829
|
+
expectedRoot,
|
|
29830
|
+
allowAdapterFence: true,
|
|
29831
|
+
allowMissingDatabase: true
|
|
29832
|
+
});
|
|
29833
|
+
} else {
|
|
29834
|
+
let store;
|
|
29835
|
+
try {
|
|
29836
|
+
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
|
|
29837
|
+
const row = await store.open();
|
|
29838
|
+
if (row !== null) {
|
|
29839
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
|
|
29840
|
+
if (lease === null) throw gated("gate_unavailable");
|
|
29841
|
+
try {
|
|
29842
|
+
if (row.revoked === null) await store.reset(lease, "purged");
|
|
29843
|
+
let deletionKnown = expectedKnown;
|
|
29844
|
+
if (marker !== void 0) {
|
|
29845
|
+
const current = await sqlite.inspectSqliteReplicaLayout(replicaDir, {
|
|
29846
|
+
allowAdapterFence: true
|
|
29847
|
+
});
|
|
29848
|
+
if (current.rootIdentity.dev !== expectedRoot?.dev || current.rootIdentity.ino !== expectedRoot.ino) throw localChangesRefusal([relative]);
|
|
29849
|
+
const approved = new Map(expectedKnown?.map((entry) => [entry.path, entry]));
|
|
29850
|
+
for (const entry of current.knownEntries) {
|
|
29851
|
+
const prior = approved.get(entry.path);
|
|
29852
|
+
if (prior?.kind === entry.kind && prior.dev === entry.dev && prior.ino === entry.ino) continue;
|
|
29853
|
+
if (entry.kind !== "file" || !["replica.db-wal", "replica.db-shm", "replica.db-journal"].includes(entry.path)) {
|
|
29854
|
+
throw localChangesRefusal([`${relative}/${entry.path}`]);
|
|
29855
|
+
}
|
|
29856
|
+
}
|
|
29857
|
+
deletionKnown = current.knownEntries;
|
|
29858
|
+
}
|
|
29859
|
+
await store.destroy(lease, {
|
|
29860
|
+
approvedUnknown,
|
|
29861
|
+
expectedKnown: deletionKnown,
|
|
29862
|
+
expectedRoot,
|
|
29863
|
+
allowAdapterFence: true
|
|
29864
|
+
});
|
|
29865
|
+
} finally {
|
|
29866
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
29867
|
+
}
|
|
29868
|
+
} else {
|
|
29869
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
29870
|
+
approvedUnknown,
|
|
29871
|
+
expectedKnown,
|
|
29872
|
+
expectedRoot,
|
|
29873
|
+
allowAdapterFence: true
|
|
29874
|
+
});
|
|
29875
|
+
}
|
|
29876
|
+
} finally {
|
|
29877
|
+
await store?.close().catch(() => void 0);
|
|
29878
|
+
}
|
|
29879
|
+
}
|
|
29880
|
+
await fsyncDir(join2(idDir, "replicas"));
|
|
29881
|
+
}
|
|
29882
|
+
}
|
|
29883
|
+
}
|
|
29884
|
+
await gate.clearGateIntent(profileDir, intent.id);
|
|
29885
|
+
erased.push(intent.id);
|
|
29886
|
+
});
|
|
29887
|
+
} catch (error) {
|
|
29888
|
+
failed.push({ id: intent.id, code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "STORAGE_ERROR" });
|
|
29889
|
+
}
|
|
29890
|
+
}
|
|
29891
|
+
return { erased, failed, remaining: (await lifecycle.pendingPurge()).length };
|
|
29892
|
+
}
|
|
29893
|
+
async function openHandle(spec) {
|
|
29894
|
+
const [replica, sqlite] = await Promise.all([
|
|
29895
|
+
// @ts-ignore — TS2307 when replica's dist is absent (CI builds node-sdk first).
|
|
29896
|
+
import("@tinycloud/replica"),
|
|
29897
|
+
// @ts-ignore — same as above.
|
|
29898
|
+
import("@tinycloud/replica/sqlite")
|
|
29899
|
+
]);
|
|
29900
|
+
if (spec.stamp === void 0) throw gated("not_established");
|
|
29901
|
+
if (guard === void 0) throw gated("gate_unavailable");
|
|
29902
|
+
if (spec.grantPolicy === void 0) {
|
|
29903
|
+
throw new ReplicaStorageError("REPLICA_INVALID_ARGUMENT", "A replica grant policy is required.");
|
|
29904
|
+
}
|
|
29905
|
+
const gate = await fileGate();
|
|
29906
|
+
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
29907
|
+
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29908
|
+
const identityKeyHash = await gate.identityHashOf(snapshot.salt, replicationIdentityKey(spec.identity));
|
|
29909
|
+
const resolvePolicy = async (at, recordFirst) => {
|
|
29910
|
+
const policy = spec.grantPolicy;
|
|
29911
|
+
if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > MAX_GRANT_TTL_MS2) {
|
|
29912
|
+
throw new TypeError("A replica grant requires a finite TTL not exceeding seven days.");
|
|
29913
|
+
}
|
|
29914
|
+
const verified = policy.consentAnchor;
|
|
29915
|
+
if (verified !== null && (!Number.isSafeInteger(verified.at) || verified.at < 0 || verified.source === "first_install")) {
|
|
29916
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The verified consent anchor is unavailable.");
|
|
29917
|
+
}
|
|
29918
|
+
const first = recordFirst ? await gate.recordFirstInstall(profileDir, identityKeyHash, at) : await gate.readFirstInstall(profileDir, identityKeyHash);
|
|
29919
|
+
const anchor = verified === null ? first === void 0 ? null : { at: first, source: "first_install" } : { at: Math.min(verified.at, at), source: verified.source };
|
|
29920
|
+
if (anchor === null) return null;
|
|
29921
|
+
const deadline = anchor.at + CONSENT_WINDOW_MS;
|
|
29922
|
+
if (!Number.isSafeInteger(anchor.at) || anchor.at < 0 || !Number.isSafeInteger(deadline)) {
|
|
29923
|
+
throw new ReplicaStorageError("CONSENT_EXPIRED", "The durable consent anchor is invalid.");
|
|
29924
|
+
}
|
|
29925
|
+
return { at, ttlMs: policy.grantTtlMs, consentDeadline: deadline, consentAnchor: anchor };
|
|
29926
|
+
};
|
|
29927
|
+
let partitionId;
|
|
29928
|
+
const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
|
|
29929
|
+
return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
|
|
29930
|
+
if (requiresSecretsOptIn2(spec.space, spec.prefix) && spec.allowSecrets !== true) {
|
|
29931
|
+
throw new replica.ReplicaError(
|
|
29932
|
+
replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
29933
|
+
`Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
|
|
29934
|
+
);
|
|
29935
|
+
}
|
|
29936
|
+
const idDir = partitionDirOf(root, spec.identity);
|
|
29937
|
+
await ensurePartitionIdentity(idDir, spec.identity, options.dirSync);
|
|
29938
|
+
const createdDevice = spec.device ?? await loadOrCreateDevice(idDir, profileSection, createDevice, options.dirSync);
|
|
29939
|
+
const device = { ...createdDevice, did: principalOf(createdDevice.did) };
|
|
29940
|
+
const replicaHash = replicaHashOf(spec.prefix, device.did);
|
|
29941
|
+
partitionId = `${identityKeyHash}:${replicaHash}`;
|
|
29942
|
+
const replicaDir = join2(idDir, "replicas", replicaHash);
|
|
29943
|
+
const store = await sqlite.SqliteReplicaStore.open(replicaDir, {
|
|
29944
|
+
create: true,
|
|
29945
|
+
guard: storeGuard
|
|
29946
|
+
});
|
|
29947
|
+
let generation;
|
|
29948
|
+
try {
|
|
29949
|
+
try {
|
|
29950
|
+
await store.init({
|
|
29951
|
+
name: spec.prefix.replace(/\/+$/, "") || "replica",
|
|
29952
|
+
replicaId: replicaHash,
|
|
29953
|
+
host: spec.identity.host,
|
|
29954
|
+
space: spec.space,
|
|
29955
|
+
prefix: spec.prefix,
|
|
29956
|
+
deviceDid: device.did,
|
|
29957
|
+
allowSecrets: spec.allowSecrets,
|
|
29958
|
+
localReadPolicy: "whileGrantValid",
|
|
29959
|
+
retentionGrantCid: null
|
|
29960
|
+
});
|
|
29961
|
+
} catch (error) {
|
|
29962
|
+
if (!replica.isReplicaError(error, replica.ReplicaErrorCode.CONFIG_MISMATCH)) {
|
|
29963
|
+
throw error;
|
|
29964
|
+
}
|
|
29965
|
+
}
|
|
29966
|
+
const state = await store.open();
|
|
29967
|
+
if (state === null) {
|
|
29968
|
+
throw new replica.ReplicaError(
|
|
29969
|
+
replica.ReplicaErrorCode.NOT_FOUND,
|
|
29970
|
+
"The replica row vanished during init."
|
|
29971
|
+
);
|
|
29972
|
+
}
|
|
29973
|
+
const config = state.config;
|
|
29974
|
+
if (config.host !== spec.identity.host || config.space !== spec.space || config.prefix !== spec.prefix) {
|
|
29975
|
+
throw new replica.ReplicaError(
|
|
29976
|
+
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
29977
|
+
`This replica follows ${config.host} ${config.space}/kv/${config.prefix}, not ${spec.identity.host} ${spec.space}/kv/${spec.prefix}.`
|
|
29978
|
+
);
|
|
29979
|
+
}
|
|
29980
|
+
if (principalOf(config.deviceDid) !== principalOf(device.did)) {
|
|
29981
|
+
throw new replica.ReplicaError(
|
|
29982
|
+
replica.ReplicaErrorCode.CONFIG_MISMATCH,
|
|
29983
|
+
"The stored replica belongs to a different device key; purge the partition to rekey."
|
|
29984
|
+
);
|
|
29985
|
+
}
|
|
29986
|
+
await retainGrantInstalls(gate, snapshot, identityKeyHash, store);
|
|
29987
|
+
if (state.grant !== null || state.pendingGrant !== null) {
|
|
29988
|
+
const policy = await resolvePolicy(Date.now(), false);
|
|
29989
|
+
if (policy === null) throw gated("gate_unavailable");
|
|
29990
|
+
if (state.grant !== null) await store.recomputeGrantInstall(state.grant, policy);
|
|
29991
|
+
if (state.pendingGrant !== null) await store.recomputeGrantInstall(state.pendingGrant, policy);
|
|
29992
|
+
}
|
|
29993
|
+
generation = state.generation;
|
|
29994
|
+
} catch (error) {
|
|
29995
|
+
await store.close().catch(() => void 0);
|
|
29996
|
+
throw error;
|
|
29997
|
+
}
|
|
29998
|
+
let ino;
|
|
29999
|
+
try {
|
|
30000
|
+
ino = (await stat(join2(replicaDir, "replica.db"))).ino;
|
|
30001
|
+
} catch (error) {
|
|
30002
|
+
if (!isNotFound(error)) throw error;
|
|
30003
|
+
}
|
|
30004
|
+
return new SqliteReplicaHandle({
|
|
30005
|
+
spec,
|
|
30006
|
+
replicaId: replicaHash,
|
|
30007
|
+
device,
|
|
30008
|
+
store,
|
|
30009
|
+
replicaDir,
|
|
30010
|
+
replica,
|
|
30011
|
+
guard: storeGuard,
|
|
30012
|
+
read: (section, withFence) => readGated(spec, partitionId, store, replicaDir, generation, section, withFence),
|
|
30013
|
+
policy: resolvePolicy,
|
|
30014
|
+
recordGrantInstall: async (cid, at) => gate.recordGrantInstall(profileDir, await gate.grantInstallHashOf(snapshot.salt, identityKeyHash, cid), at),
|
|
30015
|
+
transportFor: options.transportFor,
|
|
30016
|
+
dirSync: options.dirSync,
|
|
30017
|
+
generation,
|
|
30018
|
+
ino
|
|
30019
|
+
});
|
|
27881
30020
|
});
|
|
27882
30021
|
}
|
|
27883
30022
|
async function inspectStatus(spec, { signal }) {
|
|
30023
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
27884
30024
|
const checkAbort = () => {
|
|
27885
30025
|
if (signal.aborted) throw signal.reason ?? Object.assign(new Error("Replica status inspection was aborted"), { code: "ABORTED" });
|
|
27886
30026
|
};
|
|
@@ -27913,6 +30053,8 @@ function createSqliteReplicaStorage(options) {
|
|
|
27913
30053
|
import("@tinycloud/replica/sqlite")
|
|
27914
30054
|
]);
|
|
27915
30055
|
checkAbort();
|
|
30056
|
+
const syncedThroughEpoch = await readFence(replicaDir);
|
|
30057
|
+
checkAbort();
|
|
27916
30058
|
const inspected = await sqlite.SqliteReplicaStore.inspect(replicaDir, { signal });
|
|
27917
30059
|
checkAbort();
|
|
27918
30060
|
if (inspected === null) return void 0;
|
|
@@ -27920,14 +30062,17 @@ function createSqliteReplicaStorage(options) {
|
|
|
27920
30062
|
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
30063
|
throw new ReplicaStorageError("STORAGE_ERROR", `The replica at ${replicaDir} does not match the requested identity, space, prefix, or device.`);
|
|
27922
30064
|
}
|
|
27923
|
-
const syncedThroughEpoch = await readFence(replicaDir);
|
|
27924
30065
|
checkAbort();
|
|
30066
|
+
assertLogoutClear(options.logoutPendingFile);
|
|
30067
|
+
const gateState = await (await fileGate()).readGateSnapshot(profileDir);
|
|
30068
|
+
const purgePending = gateState !== void 0 && (gateState.eraseDeferred || gateState.intents.length > 0);
|
|
27925
30069
|
return {
|
|
30070
|
+
purgePending,
|
|
27926
30071
|
coverage: status.coverage,
|
|
27927
30072
|
lastSyncAt: status.lastSyncAt,
|
|
27928
30073
|
syncedThroughEpoch,
|
|
27929
|
-
authority: { state: status.authority.state, expiresAt: status.authority.expiresAt },
|
|
27930
|
-
grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec),
|
|
30074
|
+
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 },
|
|
30075
|
+
grant: newestGrantInfo(replica, state.pendingGrant ?? state.grant, state.pendingGrant, spec, inspected.grantInstall),
|
|
27931
30076
|
counts: status.counts,
|
|
27932
30077
|
bytes: status.bytes,
|
|
27933
30078
|
lastError: status.lastError
|
|
@@ -27940,89 +30085,124 @@ function createSqliteReplicaStorage(options) {
|
|
|
27940
30085
|
// @ts-ignore — same as above.
|
|
27941
30086
|
import("@tinycloud/replica/sqlite")
|
|
27942
30087
|
]);
|
|
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;
|
|
30088
|
+
return profileSection(async () => {
|
|
30089
|
+
const idDir = partitionDirOf(root, target.identity);
|
|
30090
|
+
if (!await realDirectoryExists(idDir)) return;
|
|
30091
|
+
const hasReplicas = await realDirectoryExists(join2(idDir, "replicas"));
|
|
30092
|
+
const deviceDids = [];
|
|
30093
|
+
const deviceFile = join2(idDir, "device.jwk");
|
|
30094
|
+
const stored = await readJsonFile(deviceFile);
|
|
30095
|
+
if (stored !== void 0) {
|
|
30096
|
+
const device = deviceRecordOf(stored);
|
|
30097
|
+
if (device === void 0) {
|
|
30098
|
+
throw new ReplicaStorageError(
|
|
30099
|
+
"STORAGE_ERROR",
|
|
30100
|
+
`The replica device key at ${deviceFile} is unreadable.`
|
|
30101
|
+
);
|
|
30102
|
+
}
|
|
30103
|
+
deviceDids.push(device.did);
|
|
30104
|
+
}
|
|
30105
|
+
if (target.sessionDeviceDid !== void 0) deviceDids.push(target.sessionDeviceDid);
|
|
30106
|
+
const devices = [...new Map(deviceDids.map((did) => [principalOf(did), did])).values()];
|
|
30107
|
+
const removeLeftover = async (replicaDir) => {
|
|
30108
|
+
const layout = await sqlite.inspectSqliteReplicaLayout(replicaDir, { allowAdapterFence: true });
|
|
30109
|
+
const missingDatabase = !await pathExists(join2(replicaDir, "replica.db"));
|
|
30110
|
+
const unexpected = layout.unknownPaths.filter((path) => path !== "replica.db");
|
|
30111
|
+
if (unexpected.length !== 0 || layout.unknownPaths.includes("replica.db") !== missingDatabase || layout.unknownEntries.some((item) => item.path === "replica.db")) {
|
|
30112
|
+
throw new ReplicaStorageError(
|
|
30113
|
+
"STORAGE_ERROR",
|
|
30114
|
+
`Refusing to remove unexpected replica path ${join2(replicaDir, unexpected[0] ?? "replica.db")}.`
|
|
30115
|
+
);
|
|
27974
30116
|
}
|
|
30117
|
+
await sqlite.removeSqliteReplicaLayout(replicaDir, {
|
|
30118
|
+
expectedRoot: layout.rootIdentity,
|
|
30119
|
+
expectedKnown: layout.knownEntries,
|
|
30120
|
+
allowAdapterFence: true,
|
|
30121
|
+
allowMissingDatabase: missingDatabase
|
|
30122
|
+
});
|
|
30123
|
+
};
|
|
30124
|
+
let firstError;
|
|
30125
|
+
for (const deviceDid of devices) {
|
|
30126
|
+
const replicaDir = join2(idDir, "replicas", replicaHashOf(target.prefix, deviceDid));
|
|
30127
|
+
if (!hasReplicas || !await realDirectoryExists(replicaDir)) continue;
|
|
27975
30128
|
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
|
-
);
|
|
30129
|
+
let store;
|
|
30130
|
+
try {
|
|
30131
|
+
store = await sqlite.SqliteReplicaStore.open(replicaDir, { create: false, guard: profileSection });
|
|
30132
|
+
} catch (error) {
|
|
30133
|
+
if (replica.isReplicaError(error, replica.ReplicaErrorCode.NOT_FOUND)) {
|
|
30134
|
+
await removeLeftover(replicaDir);
|
|
30135
|
+
continue;
|
|
30136
|
+
}
|
|
30137
|
+
throw error;
|
|
27988
30138
|
}
|
|
27989
30139
|
try {
|
|
27990
|
-
|
|
27991
|
-
|
|
30140
|
+
const state = await store.open();
|
|
30141
|
+
if (state === null) {
|
|
30142
|
+
await store.close().catch(() => void 0);
|
|
30143
|
+
await removeLeftover(replicaDir);
|
|
30144
|
+
continue;
|
|
30145
|
+
}
|
|
30146
|
+
const lease = await store.acquireSyncLease(SYNC_LEASE_TTL_MS);
|
|
30147
|
+
if (lease === null) {
|
|
30148
|
+
throw new replica.ReplicaError(
|
|
30149
|
+
replica.ReplicaErrorCode.BUSY,
|
|
30150
|
+
"Another process holds this replica's sync lease."
|
|
30151
|
+
);
|
|
30152
|
+
}
|
|
30153
|
+
try {
|
|
30154
|
+
const gate = await fileGate();
|
|
30155
|
+
const currentGate = await gate.readGateSnapshot(profileDir);
|
|
30156
|
+
if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
|
|
30157
|
+
const identityKeyHash = await gate.identityHashOf(currentGate.salt, replicationIdentityKey(target.identity));
|
|
30158
|
+
await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
|
|
30159
|
+
}
|
|
30160
|
+
if (state.revoked === null) {
|
|
30161
|
+
await store.reset(lease, "purged");
|
|
30162
|
+
}
|
|
30163
|
+
await store.destroy(lease, { allowAdapterFence: true });
|
|
30164
|
+
} finally {
|
|
30165
|
+
await store.releaseLease(lease).catch(() => void 0);
|
|
27992
30166
|
}
|
|
27993
|
-
await store.destroy(lease);
|
|
27994
30167
|
} finally {
|
|
27995
|
-
await store.
|
|
30168
|
+
await store.close().catch(() => void 0);
|
|
27996
30169
|
}
|
|
27997
|
-
}
|
|
27998
|
-
|
|
30170
|
+
} catch (error) {
|
|
30171
|
+
firstError ?? (firstError = error);
|
|
27999
30172
|
}
|
|
28000
|
-
} catch (error) {
|
|
28001
|
-
firstError ?? (firstError = error);
|
|
28002
30173
|
}
|
|
28003
|
-
|
|
28004
|
-
|
|
30174
|
+
if (firstError !== void 0) throw firstError;
|
|
30175
|
+
});
|
|
28005
30176
|
}
|
|
28006
30177
|
return {
|
|
28007
30178
|
kind: "sqlite",
|
|
30179
|
+
lifecycle,
|
|
28008
30180
|
open: openHandle,
|
|
28009
30181
|
inspectStatus,
|
|
28010
30182
|
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) {
|
|
30183
|
+
// File-backed pending writes are always bound to a stamped gate.
|
|
30184
|
+
pendingWrites(identity, opts) {
|
|
28017
30185
|
return new FilePendingWriteStore(
|
|
28018
30186
|
identity,
|
|
28019
30187
|
partitionDirOf(root, identity),
|
|
30188
|
+
profileDir,
|
|
28020
30189
|
guard,
|
|
28021
|
-
|
|
30190
|
+
opts?.stamp,
|
|
30191
|
+
options.dirSync,
|
|
30192
|
+
options.logoutPendingFile
|
|
28022
30193
|
);
|
|
28023
30194
|
}
|
|
28024
30195
|
};
|
|
28025
30196
|
}
|
|
30197
|
+
function createProfileReplicaPurge(options) {
|
|
30198
|
+
return createSqliteReplicaStorage({
|
|
30199
|
+
dir: join2(options.profileDir, "replication"),
|
|
30200
|
+
guard: options.guard,
|
|
30201
|
+
dirSync: options.dirSync,
|
|
30202
|
+
logoutPendingFile: join2(options.profileDir, REPLICA_LOGOUT_PENDING_FILE),
|
|
30203
|
+
diagnosticFiles: options.diagnosticFiles
|
|
30204
|
+
}).lifecycle;
|
|
30205
|
+
}
|
|
28026
30206
|
function sqliteReplicaStorage(options) {
|
|
28027
30207
|
return createSqliteReplicaStorage(options);
|
|
28028
30208
|
}
|
|
@@ -28074,7 +30254,9 @@ export {
|
|
|
28074
30254
|
PermissionNotInManifestError2 as PermissionNotInManifestError,
|
|
28075
30255
|
PrefixedKVService,
|
|
28076
30256
|
PrivateKeySigner,
|
|
28077
|
-
ProtocolMismatchError,
|
|
30257
|
+
ProtocolMismatchError2 as ProtocolMismatchError,
|
|
30258
|
+
REPLICA_LOGOUT_PENDING_FILE,
|
|
30259
|
+
RESTORED_GATE_MESSAGE,
|
|
28078
30260
|
RUNTIME_DELEGATION_REJECTIONS,
|
|
28079
30261
|
RuntimeDelegationRejectedError,
|
|
28080
30262
|
SECRET_NAME_RE,
|
|
@@ -28101,12 +30283,13 @@ export {
|
|
|
28101
30283
|
VAULT_PERMISSION_SERVICE,
|
|
28102
30284
|
VaultHeaders,
|
|
28103
30285
|
VaultPublicSpaceKVActions,
|
|
28104
|
-
|
|
30286
|
+
VersionCheckError,
|
|
28105
30287
|
WasmKeyProvider,
|
|
28106
30288
|
activateCompactRuntimeDelegation,
|
|
28107
30289
|
activateValidatedRuntimeDelegation,
|
|
28108
30290
|
addressStorageKey,
|
|
28109
30291
|
appSecretScope,
|
|
30292
|
+
assertProfileLogoutClear,
|
|
28110
30293
|
buildCanonicalDecryptRequest,
|
|
28111
30294
|
buildDecryptAttenuation,
|
|
28112
30295
|
buildDecryptFacts,
|
|
@@ -28127,6 +30310,7 @@ export {
|
|
|
28127
30310
|
composeManifestRequest2 as composeManifestRequest,
|
|
28128
30311
|
createCapabilityKeyRegistry,
|
|
28129
30312
|
createOpenKeyCallbackSigningStrategy,
|
|
30313
|
+
createProfileReplicaPurge,
|
|
28130
30314
|
createSharingService,
|
|
28131
30315
|
createSpaceService,
|
|
28132
30316
|
createVaultCrypto2 as createVaultCrypto,
|
|
@@ -28163,6 +30347,7 @@ export {
|
|
|
28163
30347
|
isNetworkId,
|
|
28164
30348
|
isStorageFullError2 as isStorageFullError,
|
|
28165
30349
|
loadManifest,
|
|
30350
|
+
logoutPrincipalDigest,
|
|
28166
30351
|
makePkhSpaceId2 as makePkhSpaceId,
|
|
28167
30352
|
makePublicSpaceId2 as makePublicSpaceId,
|
|
28168
30353
|
networkDiscoveryKey,
|