agents-city 0.4.0 → 0.5.2
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/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +240 -11
- package/README.md +238 -11
- package/benchmarks/latency/fake-claude-cli.mjs +2 -1
- package/benchmarks/reception/README.md +16 -0
- package/benchmarks/reception/run.mjs +84 -0
- package/bin/agents-city.js +1 -0
- package/bin/connect +5 -0
- package/bin/hall.html +117 -18
- package/bin/navegador.mjs +170 -1
- package/bin/serve.py +201 -101
- package/bin/test +15 -3
- package/bin/test-arnes.py +192 -0
- package/bin/test-cage.py +9 -0
- package/bin/test-channel.py +178 -12
- package/bin/test-claude-runtime.py +132 -4
- package/bin/test-connect-client.mjs +609 -0
- package/bin/test-connect.py +52 -0
- package/bin/test-contracts.py +214 -0
- package/bin/test-diario.py +185 -0
- package/bin/test-doctor.py +31 -0
- package/bin/test-i18n.py +189 -0
- package/bin/test-navegador.py +61 -0
- package/bin/test-seat.py +78 -20
- package/bin/test-security.py +2 -0
- package/bin/test-serve.py +366 -0
- package/city/web/dist-hall/hall.js +789 -214
- package/city/web/src/bienvenida.ts +14 -13
- package/city/web/src/casa.ts +143 -56
- package/city/web/src/demo.ts +61 -31
- package/city/web/src/es.ts +187 -27
- package/city/web/src/explorador.ts +70 -43
- package/city/web/src/hall.ts +520 -103
- package/city/web/src/idioma.ts +21 -1
- package/city/web/src/motores.ts +48 -1
- package/city/web/src/vista.ts +58 -0
- package/demo/graba.py +5 -10
- package/docs/managed-connect.md +237 -0
- package/docs/security.md +41 -2
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +10 -0
- package/plugin/channel/adapter.js +78 -31
- package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
- package/plugin/channel/bus.js +109 -65
- package/plugin/channel/bus.ts +11 -1
- package/plugin/channel/client.js +67 -30
- package/plugin/channel/delivery-queue.ts +160 -21
- package/plugin/channel/hub/remote-roads.ts +32 -1
- package/plugin/channel/hub/road-controller.ts +116 -16
- package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
- package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
- package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
- package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
- package/plugin/channel/licenses/keyring-MIT.txt +21 -0
- package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
- package/plugin/channel/local-hub.js +2080 -97
- package/plugin/channel/local-hub.ts +5 -4
- package/plugin/channel/managed-connect/bridge.ts +208 -0
- package/plugin/channel/managed-connect/cli.ts +416 -0
- package/plugin/channel/managed-connect/device.ts +15 -0
- package/plugin/channel/managed-connect/local-cities.ts +94 -0
- package/plugin/channel/managed-connect/person-message.ts +74 -0
- package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
- package/plugin/channel/managed-connect/relay-session.ts +7 -0
- package/plugin/channel/managed-connect/storage.ts +653 -0
- package/plugin/channel/managed-connect/transport.ts +87 -0
- package/plugin/channel/managed-connect-cli.js +4777 -0
- package/plugin/channel/managed-connect-cli.ts +7 -0
- package/plugin/channel/managed-connect-client.d.ts +261 -0
- package/plugin/channel/managed-connect-client.js +6572 -0
- package/plugin/channel/managed-connect-client.manifest.json +38 -0
- package/plugin/channel/package-lock.json +123 -105
- package/plugin/channel/package.json +4 -4
- package/plugin/channel/protocol.ts +4 -0
- package/plugin/channel/reception.ts +896 -0
- package/plugin/channel/road-cli.ts +1 -1
- package/plugin/channel/runtime/arnes.json +189 -0
- package/plugin/channel/runtime/arnes.ts +47 -0
- package/plugin/channel/runtime/claude.ts +18 -0
- package/plugin/channel/runtime/codex-config.ts +51 -1
- package/plugin/channel/runtime/codex.ts +31 -11
- package/plugin/channel/runtime/kimi.ts +6 -4
- package/plugin/channel/runtime-files.ts +39 -5
- package/plugin/channel/runtime-gateway.js +426 -100
- package/plugin/channel/runtime-gateway.ts +12 -0
- package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
- package/plugin/scripts/arnes.py +271 -0
- package/plugin/scripts/busca.py +46 -10
- package/plugin/scripts/cage.py +5 -0
- package/plugin/scripts/city-session.sh +144 -28
- package/plugin/scripts/crecimiento.py +4 -1
- package/plugin/scripts/demos.py +58 -43
- package/plugin/scripts/desinstala.py +36 -21
- package/plugin/scripts/diario.py +119 -0
- package/plugin/scripts/doctor.py +90 -15
- package/plugin/scripts/read-card.py +48 -8
- package/plugin/scripts/reception.py +665 -0
|
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
13
13
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
14
|
});
|
|
15
15
|
var __commonJS = (cb, mod) => function __require2() {
|
|
16
|
-
|
|
16
|
+
try {
|
|
17
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
throw mod = 0, e;
|
|
20
|
+
}
|
|
17
21
|
};
|
|
18
22
|
var __copyProps = (to, from, except, desc) => {
|
|
19
23
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
@@ -45,10 +49,10 @@ var require_constants = __commonJS({
|
|
|
45
49
|
EMPTY_BUFFER: Buffer.alloc(0),
|
|
46
50
|
GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
|
|
47
51
|
hasBlob,
|
|
48
|
-
kForOnEventAttribute: Symbol("kIsForOnEventAttribute"),
|
|
49
|
-
kListener: Symbol("kListener"),
|
|
50
|
-
kStatusCode: Symbol("status-code"),
|
|
51
|
-
kWebSocket: Symbol("websocket"),
|
|
52
|
+
kForOnEventAttribute: /* @__PURE__ */ Symbol("kIsForOnEventAttribute"),
|
|
53
|
+
kListener: /* @__PURE__ */ Symbol("kListener"),
|
|
54
|
+
kStatusCode: /* @__PURE__ */ Symbol("status-code"),
|
|
55
|
+
kWebSocket: /* @__PURE__ */ Symbol("websocket"),
|
|
52
56
|
NOOP: () => {
|
|
53
57
|
}
|
|
54
58
|
};
|
|
@@ -134,8 +138,8 @@ var require_buffer_util = __commonJS({
|
|
|
134
138
|
var require_limiter = __commonJS({
|
|
135
139
|
"node_modules/ws/lib/limiter.js"(exports, module) {
|
|
136
140
|
"use strict";
|
|
137
|
-
var kDone = Symbol("kDone");
|
|
138
|
-
var kRun = Symbol("kRun");
|
|
141
|
+
var kDone = /* @__PURE__ */ Symbol("kDone");
|
|
142
|
+
var kRun = /* @__PURE__ */ Symbol("kRun");
|
|
139
143
|
var Limiter = class {
|
|
140
144
|
/**
|
|
141
145
|
* Creates a new `Limiter`.
|
|
@@ -190,11 +194,11 @@ var require_permessage_deflate = __commonJS({
|
|
|
190
194
|
var { kStatusCode } = require_constants();
|
|
191
195
|
var FastBuffer = Buffer[Symbol.species];
|
|
192
196
|
var TRAILER = Buffer.from([0, 0, 255, 255]);
|
|
193
|
-
var kPerMessageDeflate = Symbol("permessage-deflate");
|
|
194
|
-
var kTotalLength = Symbol("total-length");
|
|
195
|
-
var kCallback = Symbol("callback");
|
|
196
|
-
var kBuffers = Symbol("buffers");
|
|
197
|
-
var kError = Symbol("error");
|
|
197
|
+
var kPerMessageDeflate = /* @__PURE__ */ Symbol("permessage-deflate");
|
|
198
|
+
var kTotalLength = /* @__PURE__ */ Symbol("total-length");
|
|
199
|
+
var kCallback = /* @__PURE__ */ Symbol("callback");
|
|
200
|
+
var kBuffers = /* @__PURE__ */ Symbol("buffers");
|
|
201
|
+
var kError = /* @__PURE__ */ Symbol("error");
|
|
198
202
|
var zlibLimiter;
|
|
199
203
|
var PerMessageDeflate2 = class {
|
|
200
204
|
/**
|
|
@@ -1400,7 +1404,7 @@ var require_sender = __commonJS({
|
|
|
1400
1404
|
var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
|
|
1401
1405
|
var { isBlob, isValidStatusCode } = require_validation();
|
|
1402
1406
|
var { mask: applyMask, toBuffer } = require_buffer_util();
|
|
1403
|
-
var kByteLength = Symbol("kByteLength");
|
|
1407
|
+
var kByteLength = /* @__PURE__ */ Symbol("kByteLength");
|
|
1404
1408
|
var maskBuffer = Buffer.alloc(4);
|
|
1405
1409
|
var RANDOM_POOL_SIZE = 8 * 1024;
|
|
1406
1410
|
var randomPool;
|
|
@@ -1885,14 +1889,14 @@ var require_event_target = __commonJS({
|
|
|
1885
1889
|
"node_modules/ws/lib/event-target.js"(exports, module) {
|
|
1886
1890
|
"use strict";
|
|
1887
1891
|
var { kForOnEventAttribute, kListener } = require_constants();
|
|
1888
|
-
var kCode = Symbol("kCode");
|
|
1889
|
-
var kData = Symbol("kData");
|
|
1890
|
-
var kError = Symbol("kError");
|
|
1891
|
-
var kMessage = Symbol("kMessage");
|
|
1892
|
-
var kReason = Symbol("kReason");
|
|
1893
|
-
var kTarget = Symbol("kTarget");
|
|
1894
|
-
var kType = Symbol("kType");
|
|
1895
|
-
var kWasClean = Symbol("kWasClean");
|
|
1892
|
+
var kCode = /* @__PURE__ */ Symbol("kCode");
|
|
1893
|
+
var kData = /* @__PURE__ */ Symbol("kData");
|
|
1894
|
+
var kError = /* @__PURE__ */ Symbol("kError");
|
|
1895
|
+
var kMessage = /* @__PURE__ */ Symbol("kMessage");
|
|
1896
|
+
var kReason = /* @__PURE__ */ Symbol("kReason");
|
|
1897
|
+
var kTarget = /* @__PURE__ */ Symbol("kTarget");
|
|
1898
|
+
var kType = /* @__PURE__ */ Symbol("kType");
|
|
1899
|
+
var kWasClean = /* @__PURE__ */ Symbol("kWasClean");
|
|
1896
1900
|
var Event = class {
|
|
1897
1901
|
/**
|
|
1898
1902
|
* Create a new `Event`.
|
|
@@ -2271,7 +2275,7 @@ var require_websocket = __commonJS({
|
|
|
2271
2275
|
var http = __require("http");
|
|
2272
2276
|
var net = __require("net");
|
|
2273
2277
|
var tls = __require("tls");
|
|
2274
|
-
var { randomBytes: randomBytes2, createHash } = __require("crypto");
|
|
2278
|
+
var { randomBytes: randomBytes2, createHash: createHash3 } = __require("crypto");
|
|
2275
2279
|
var { Duplex, Readable } = __require("stream");
|
|
2276
2280
|
var { URL: URL2 } = __require("url");
|
|
2277
2281
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2294,7 +2298,7 @@ var require_websocket = __commonJS({
|
|
|
2294
2298
|
} = require_event_target();
|
|
2295
2299
|
var { format, parse } = require_extension();
|
|
2296
2300
|
var { toBuffer } = require_buffer_util();
|
|
2297
|
-
var kAborted = Symbol("kAborted");
|
|
2301
|
+
var kAborted = /* @__PURE__ */ Symbol("kAborted");
|
|
2298
2302
|
var protocolVersions = [8, 13];
|
|
2299
2303
|
var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
2300
2304
|
var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
|
|
@@ -2939,7 +2943,7 @@ var require_websocket = __commonJS({
|
|
|
2939
2943
|
abortHandshake(websocket2, socket, "Invalid Upgrade header");
|
|
2940
2944
|
return;
|
|
2941
2945
|
}
|
|
2942
|
-
const digest =
|
|
2946
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
2943
2947
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2944
2948
|
abortHandshake(websocket2, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2945
2949
|
return;
|
|
@@ -3308,7 +3312,7 @@ var require_websocket_server = __commonJS({
|
|
|
3308
3312
|
var EventEmitter = __require("events");
|
|
3309
3313
|
var http = __require("http");
|
|
3310
3314
|
var { Duplex } = __require("stream");
|
|
3311
|
-
var { createHash } = __require("crypto");
|
|
3315
|
+
var { createHash: createHash3 } = __require("crypto");
|
|
3312
3316
|
var extension2 = require_extension();
|
|
3313
3317
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3314
3318
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3615,7 +3619,7 @@ var require_websocket_server = __commonJS({
|
|
|
3615
3619
|
);
|
|
3616
3620
|
}
|
|
3617
3621
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3618
|
-
const digest =
|
|
3622
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3619
3623
|
const headers = [
|
|
3620
3624
|
"HTTP/1.1 101 Switching Protocols",
|
|
3621
3625
|
"Upgrade: websocket",
|
|
@@ -3705,7 +3709,7 @@ var require_websocket_server = __commonJS({
|
|
|
3705
3709
|
// local-hub.ts
|
|
3706
3710
|
import { timingSafeEqual } from "crypto";
|
|
3707
3711
|
import { createServer } from "http";
|
|
3708
|
-
import { readFileSync as
|
|
3712
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3709
3713
|
|
|
3710
3714
|
// node_modules/ws/wrapper.mjs
|
|
3711
3715
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -4546,22 +4550,63 @@ function escapeRegExp(value) {
|
|
|
4546
4550
|
}
|
|
4547
4551
|
|
|
4548
4552
|
// delivery-queue.ts
|
|
4549
|
-
import {
|
|
4553
|
+
import {
|
|
4554
|
+
appendFileSync as appendFileSync2,
|
|
4555
|
+
existsSync as existsSync4,
|
|
4556
|
+
mkdirSync as mkdirSync3,
|
|
4557
|
+
readFileSync as readFileSync4,
|
|
4558
|
+
readdirSync as readdirSync2,
|
|
4559
|
+
statSync,
|
|
4560
|
+
unlinkSync as unlinkSync2
|
|
4561
|
+
} from "fs";
|
|
4550
4562
|
import { join as join5 } from "path";
|
|
4551
4563
|
|
|
4552
4564
|
// runtime-files.ts
|
|
4553
4565
|
import { randomBytes } from "crypto";
|
|
4554
|
-
import {
|
|
4566
|
+
import {
|
|
4567
|
+
chmodSync as chmodSync2,
|
|
4568
|
+
closeSync,
|
|
4569
|
+
constants,
|
|
4570
|
+
existsSync as existsSync3,
|
|
4571
|
+
fsyncSync,
|
|
4572
|
+
mkdirSync as mkdirSync2,
|
|
4573
|
+
openSync,
|
|
4574
|
+
readFileSync as readFileSync3,
|
|
4575
|
+
renameSync as renameSync3,
|
|
4576
|
+
unlinkSync,
|
|
4577
|
+
writeFileSync as writeFileSync3
|
|
4578
|
+
} from "fs";
|
|
4555
4579
|
import { dirname, join as join4 } from "path";
|
|
4556
4580
|
var counter = 0;
|
|
4557
4581
|
function atomicJson(path, value) {
|
|
4558
|
-
|
|
4582
|
+
const directory = dirname(path);
|
|
4583
|
+
mkdirSync2(directory, { recursive: true, mode: 448 });
|
|
4559
4584
|
const tmp = `${path}.tmp-${process.pid}-${counter++}`;
|
|
4560
|
-
writeFileSync3(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
4561
|
-
renameSync3(tmp, path);
|
|
4562
4585
|
try {
|
|
4586
|
+
const fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
4587
|
+
try {
|
|
4588
|
+
writeFileSync3(fd, JSON.stringify(value, null, 2) + "\n");
|
|
4589
|
+
fsyncSync(fd);
|
|
4590
|
+
} finally {
|
|
4591
|
+
closeSync(fd);
|
|
4592
|
+
}
|
|
4593
|
+
renameSync3(tmp, path);
|
|
4563
4594
|
chmodSync2(path, 384);
|
|
4564
|
-
|
|
4595
|
+
try {
|
|
4596
|
+
const dirFd = openSync(directory, constants.O_RDONLY);
|
|
4597
|
+
try {
|
|
4598
|
+
fsyncSync(dirFd);
|
|
4599
|
+
} finally {
|
|
4600
|
+
closeSync(dirFd);
|
|
4601
|
+
}
|
|
4602
|
+
} catch {
|
|
4603
|
+
}
|
|
4604
|
+
} catch (error) {
|
|
4605
|
+
try {
|
|
4606
|
+
unlinkSync(tmp);
|
|
4607
|
+
} catch {
|
|
4608
|
+
}
|
|
4609
|
+
throw error;
|
|
4565
4610
|
}
|
|
4566
4611
|
}
|
|
4567
4612
|
function actorCredential(context2, actor) {
|
|
@@ -4604,11 +4649,16 @@ function endpointPath(context2) {
|
|
|
4604
4649
|
}
|
|
4605
4650
|
|
|
4606
4651
|
// delivery-queue.ts
|
|
4652
|
+
var ROAD_INBOX_BATCH_SIZE = 20;
|
|
4653
|
+
var DEFAULT_ROAD_INBOX_LIMIT = 500;
|
|
4654
|
+
var MAX_ROAD_INBOX_LIMIT = 1e4;
|
|
4655
|
+
var ROAD_INBOX_WAKE_FILE = "road-inbox-wakeup.json";
|
|
4607
4656
|
function enqueueForActor(runtimeDir, envelope) {
|
|
4608
4657
|
const directory = join5(runtimeDir, "outbox", safeSegment(envelope.to.actor));
|
|
4609
4658
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
4610
|
-
|
|
4611
|
-
|
|
4659
|
+
const path = join5(directory, `${fileKey(envelope.id)}.json`);
|
|
4660
|
+
requireCapacity(directory, path, MAX_PENDING, "actor_outbox_full");
|
|
4661
|
+
atomicJson(path, envelope);
|
|
4612
4662
|
}
|
|
4613
4663
|
function pendingForActor(runtimeDir, actor) {
|
|
4614
4664
|
const directory = join5(runtimeDir, "outbox", safeSegment(actor));
|
|
@@ -4617,13 +4667,13 @@ function pendingForActor(runtimeDir, actor) {
|
|
|
4617
4667
|
try {
|
|
4618
4668
|
const envelope = JSON.parse(readFileSync4(path, "utf8"));
|
|
4619
4669
|
if (now - Date.parse(envelope.createdAt) > MESSAGE_TTL_MS) {
|
|
4620
|
-
|
|
4670
|
+
unlinkSync2(path);
|
|
4621
4671
|
return null;
|
|
4622
4672
|
}
|
|
4623
4673
|
return envelope;
|
|
4624
4674
|
} catch {
|
|
4625
4675
|
try {
|
|
4626
|
-
|
|
4676
|
+
unlinkSync2(path);
|
|
4627
4677
|
} catch {
|
|
4628
4678
|
}
|
|
4629
4679
|
return null;
|
|
@@ -4633,7 +4683,7 @@ function pendingForActor(runtimeDir, actor) {
|
|
|
4633
4683
|
function acknowledge(runtimeDir, actor, envelopeId) {
|
|
4634
4684
|
const path = join5(runtimeDir, "outbox", safeSegment(actor), `${fileKey(envelopeId)}.json`);
|
|
4635
4685
|
try {
|
|
4636
|
-
|
|
4686
|
+
unlinkSync2(path);
|
|
4637
4687
|
return true;
|
|
4638
4688
|
} catch {
|
|
4639
4689
|
return false;
|
|
@@ -4642,48 +4692,107 @@ function acknowledge(runtimeDir, actor, envelopeId) {
|
|
|
4642
4692
|
function queueRoad(runtimeDir, envelope) {
|
|
4643
4693
|
const directory = join5(runtimeDir, "road-queue");
|
|
4644
4694
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
4645
|
-
|
|
4646
|
-
|
|
4695
|
+
const path = join5(directory, `${fileKey(envelope.id)}.json`);
|
|
4696
|
+
requireCapacity(directory, path, MAX_PENDING, "road_queue_full");
|
|
4697
|
+
atomicJson(path, envelope);
|
|
4647
4698
|
}
|
|
4648
|
-
function
|
|
4699
|
+
function pendingRoadQueue(runtimeDir) {
|
|
4649
4700
|
const directory = join5(runtimeDir, "road-queue");
|
|
4650
4701
|
const out = [];
|
|
4651
|
-
for (const path of
|
|
4702
|
+
for (const path of jsonFilesByAge(directory)) {
|
|
4652
4703
|
try {
|
|
4653
4704
|
const envelope = JSON.parse(readFileSync4(path, "utf8"));
|
|
4654
|
-
if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS)
|
|
4705
|
+
if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS) {
|
|
4706
|
+
out.push({ envelope, queueFile: path });
|
|
4707
|
+
continue;
|
|
4708
|
+
}
|
|
4655
4709
|
} catch {
|
|
4656
4710
|
}
|
|
4657
4711
|
try {
|
|
4658
|
-
|
|
4712
|
+
unlinkSync2(path);
|
|
4659
4713
|
} catch {
|
|
4660
4714
|
}
|
|
4661
4715
|
}
|
|
4662
4716
|
return out;
|
|
4663
4717
|
}
|
|
4718
|
+
function acknowledgeRoadQueue(queueFile) {
|
|
4719
|
+
try {
|
|
4720
|
+
unlinkSync2(queueFile);
|
|
4721
|
+
} catch {
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4664
4724
|
function recordRoadInbox(runtimeDir, envelope) {
|
|
4665
4725
|
const directory = join5(runtimeDir, "road-inbox");
|
|
4726
|
+
const receipts = join5(runtimeDir, "road-receipts");
|
|
4666
4727
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
});
|
|
4728
|
+
mkdirSync3(receipts, { recursive: true, mode: 448 });
|
|
4729
|
+
const key = fileKey(envelope.id);
|
|
4730
|
+
const receipt = join5(receipts, `${key}.json`);
|
|
4731
|
+
if (existsSync4(receipt)) return false;
|
|
4732
|
+
const inbox = join5(directory, `${key}.json`);
|
|
4733
|
+
const recovered = existsSync4(inbox);
|
|
4734
|
+
if (!recovered) {
|
|
4735
|
+
requireCapacity(directory, inbox, roadInboxLimit(), "road_inbox_full");
|
|
4736
|
+
atomicJson(inbox, envelope);
|
|
4737
|
+
appendFileSync2(join5(runtimeDir, "road-history.jsonl"), JSON.stringify(envelope) + "\n", {
|
|
4738
|
+
mode: 384
|
|
4739
|
+
});
|
|
4740
|
+
}
|
|
4741
|
+
return true;
|
|
4672
4742
|
}
|
|
4673
|
-
function
|
|
4743
|
+
function markRoadInboxAccepted(runtimeDir, envelopeId) {
|
|
4744
|
+
const receipts = join5(runtimeDir, "road-receipts");
|
|
4745
|
+
mkdirSync3(receipts, { recursive: true, mode: 448 });
|
|
4746
|
+
const key = fileKey(envelopeId);
|
|
4747
|
+
const receipt = join5(receipts, `${key}.json`);
|
|
4748
|
+
if (existsSync4(receipt)) return;
|
|
4749
|
+
trimTo(receipts, 1e3);
|
|
4750
|
+
atomicJson(receipt, { id: envelopeId, acceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4751
|
+
}
|
|
4752
|
+
function roadInboxStatus(runtimeDir) {
|
|
4753
|
+
const directory = join5(runtimeDir, "road-inbox");
|
|
4754
|
+
const paths = jsonFilesByAge(directory);
|
|
4755
|
+
let notifiedAt = 0;
|
|
4756
|
+
try {
|
|
4757
|
+
const state = JSON.parse(readFileSync4(join5(runtimeDir, ROAD_INBOX_WAKE_FILE), "utf8"));
|
|
4758
|
+
if (Number.isSafeInteger(state.notifiedAt) && Number(state.notifiedAt) > 0) {
|
|
4759
|
+
notifiedAt = Number(state.notifiedAt);
|
|
4760
|
+
}
|
|
4761
|
+
} catch {
|
|
4762
|
+
}
|
|
4763
|
+
return {
|
|
4764
|
+
pending: paths.length,
|
|
4765
|
+
oldestAt: oldestTimestamp(paths),
|
|
4766
|
+
notifiedAt
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
function markRoadInboxNotified(runtimeDir, notifiedAt = Date.now()) {
|
|
4770
|
+
atomicJson(join5(runtimeDir, ROAD_INBOX_WAKE_FILE), { notifiedAt });
|
|
4771
|
+
}
|
|
4772
|
+
function takeRoadInbox(runtimeDir, limit = ROAD_INBOX_BATCH_SIZE) {
|
|
4773
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > ROAD_INBOX_BATCH_SIZE) {
|
|
4774
|
+
throw new Error("invalid_road_inbox_batch_size");
|
|
4775
|
+
}
|
|
4674
4776
|
const directory = join5(runtimeDir, "road-inbox");
|
|
4675
4777
|
const out = [];
|
|
4676
|
-
for (const path of
|
|
4778
|
+
for (const path of jsonFilesByAge(directory).slice(0, limit)) {
|
|
4677
4779
|
try {
|
|
4678
4780
|
out.push(JSON.parse(readFileSync4(path, "utf8")));
|
|
4679
4781
|
} catch {
|
|
4680
4782
|
}
|
|
4681
4783
|
try {
|
|
4682
|
-
|
|
4784
|
+
unlinkSync2(path);
|
|
4683
4785
|
} catch {
|
|
4684
4786
|
}
|
|
4685
4787
|
}
|
|
4686
|
-
|
|
4788
|
+
const remaining = jsonFiles(directory).length;
|
|
4789
|
+
if (!remaining) {
|
|
4790
|
+
try {
|
|
4791
|
+
unlinkSync2(join5(runtimeDir, ROAD_INBOX_WAKE_FILE));
|
|
4792
|
+
} catch {
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
return { messages: out, remaining };
|
|
4687
4796
|
}
|
|
4688
4797
|
function jsonFiles(directory) {
|
|
4689
4798
|
if (!existsSync4(directory)) return [];
|
|
@@ -4693,16 +4802,50 @@ function jsonFiles(directory) {
|
|
|
4693
4802
|
return [];
|
|
4694
4803
|
}
|
|
4695
4804
|
}
|
|
4805
|
+
function jsonFilesByAge(directory) {
|
|
4806
|
+
return jsonFiles(directory).sort((left, right) => {
|
|
4807
|
+
try {
|
|
4808
|
+
const delta = statSync(left).mtimeMs - statSync(right).mtimeMs;
|
|
4809
|
+
return delta || left.localeCompare(right);
|
|
4810
|
+
} catch {
|
|
4811
|
+
return left.localeCompare(right);
|
|
4812
|
+
}
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
function oldestTimestamp(paths) {
|
|
4816
|
+
for (const path of paths) {
|
|
4817
|
+
try {
|
|
4818
|
+
return new Date(statSync(path).mtimeMs).toISOString();
|
|
4819
|
+
} catch {
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
return null;
|
|
4823
|
+
}
|
|
4696
4824
|
function fileKey(value) {
|
|
4697
4825
|
const out = String(value).replace(/[^a-zA-Z0-9_.-]+/g, "-").slice(0, 160);
|
|
4698
4826
|
if (!out) throw new Error("invalid message id");
|
|
4699
4827
|
return out;
|
|
4700
4828
|
}
|
|
4701
|
-
function
|
|
4702
|
-
const
|
|
4703
|
-
|
|
4829
|
+
function roadInboxLimit() {
|
|
4830
|
+
const configured = Number(process.env.CITY_ROAD_INBOX_MAX_PENDING);
|
|
4831
|
+
return Number.isSafeInteger(configured) && configured >= ROAD_INBOX_BATCH_SIZE ? Math.min(configured, MAX_ROAD_INBOX_LIMIT) : DEFAULT_ROAD_INBOX_LIMIT;
|
|
4832
|
+
}
|
|
4833
|
+
function requireCapacity(directory, target, maximum, code) {
|
|
4834
|
+
if (existsSync4(target)) return;
|
|
4835
|
+
if (jsonFiles(directory).length >= maximum) throw new Error(code);
|
|
4836
|
+
}
|
|
4837
|
+
function trimTo(directory, maximum) {
|
|
4838
|
+
const files2 = jsonFiles(directory).sort((left, right) => {
|
|
4839
|
+
try {
|
|
4840
|
+
const delta = statSync(left).mtimeMs - statSync(right).mtimeMs;
|
|
4841
|
+
return delta || left.localeCompare(right);
|
|
4842
|
+
} catch {
|
|
4843
|
+
return left.localeCompare(right);
|
|
4844
|
+
}
|
|
4845
|
+
});
|
|
4846
|
+
for (const path of files2.slice(0, Math.max(0, files2.length - maximum + 1))) {
|
|
4704
4847
|
try {
|
|
4705
|
-
|
|
4848
|
+
unlinkSync2(path);
|
|
4706
4849
|
} catch {
|
|
4707
4850
|
}
|
|
4708
4851
|
}
|
|
@@ -4962,7 +5105,7 @@ import {
|
|
|
4962
5105
|
mkdirSync as mkdirSync4,
|
|
4963
5106
|
readFileSync as readFileSync5,
|
|
4964
5107
|
renameSync as renameSync4,
|
|
4965
|
-
statSync,
|
|
5108
|
+
statSync as statSync2,
|
|
4966
5109
|
writeFileSync as writeFileSync4
|
|
4967
5110
|
} from "fs";
|
|
4968
5111
|
import { join as join6 } from "path";
|
|
@@ -5049,7 +5192,7 @@ function readRecent(path, limit) {
|
|
|
5049
5192
|
}
|
|
5050
5193
|
function fileIsLarge(path) {
|
|
5051
5194
|
try {
|
|
5052
|
-
return
|
|
5195
|
+
return statSync2(path).size > 2e6;
|
|
5053
5196
|
} catch {
|
|
5054
5197
|
return false;
|
|
5055
5198
|
}
|
|
@@ -5308,12 +5451,12 @@ function envelopeRouter(context2, connections2, options = {}) {
|
|
|
5308
5451
|
|
|
5309
5452
|
// hub/lifecycle.ts
|
|
5310
5453
|
import {
|
|
5311
|
-
closeSync,
|
|
5454
|
+
closeSync as closeSync2,
|
|
5312
5455
|
mkdirSync as mkdirSync6,
|
|
5313
|
-
openSync,
|
|
5456
|
+
openSync as openSync2,
|
|
5314
5457
|
readFileSync as readFileSync6,
|
|
5315
|
-
statSync as
|
|
5316
|
-
unlinkSync as
|
|
5458
|
+
statSync as statSync3,
|
|
5459
|
+
unlinkSync as unlinkSync3,
|
|
5317
5460
|
writeFileSync as writeFileSync5
|
|
5318
5461
|
} from "fs";
|
|
5319
5462
|
import { join as join8 } from "path";
|
|
@@ -5324,11 +5467,11 @@ function acquireHub(context2) {
|
|
|
5324
5467
|
let acquired = false;
|
|
5325
5468
|
for (let attempt = 0; attempt < 2 && !acquired; attempt += 1) {
|
|
5326
5469
|
try {
|
|
5327
|
-
const fd =
|
|
5470
|
+
const fd = openSync2(lock, "wx", 384);
|
|
5328
5471
|
try {
|
|
5329
5472
|
writeFileSync5(fd, String(owner) + "\n");
|
|
5330
5473
|
} finally {
|
|
5331
|
-
|
|
5474
|
+
closeSync2(fd);
|
|
5332
5475
|
}
|
|
5333
5476
|
acquired = true;
|
|
5334
5477
|
} catch (error) {
|
|
@@ -5342,7 +5485,7 @@ function acquireHub(context2) {
|
|
|
5342
5485
|
throw new Error("city bus is already starting");
|
|
5343
5486
|
}
|
|
5344
5487
|
try {
|
|
5345
|
-
|
|
5488
|
+
unlinkSync3(lock);
|
|
5346
5489
|
} catch (unlinkError) {
|
|
5347
5490
|
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
5348
5491
|
}
|
|
@@ -5354,11 +5497,11 @@ function acquireHub(context2) {
|
|
|
5354
5497
|
const endpoint = endpointPath(context2);
|
|
5355
5498
|
try {
|
|
5356
5499
|
const published = JSON.parse(readFileSync6(endpoint, "utf8"));
|
|
5357
|
-
if (published.pid === owner)
|
|
5500
|
+
if (published.pid === owner) unlinkSync3(endpoint);
|
|
5358
5501
|
} catch {
|
|
5359
5502
|
}
|
|
5360
5503
|
try {
|
|
5361
|
-
|
|
5504
|
+
unlinkSync3(lock);
|
|
5362
5505
|
} catch {
|
|
5363
5506
|
}
|
|
5364
5507
|
};
|
|
@@ -5372,7 +5515,7 @@ function lockOwner(path) {
|
|
|
5372
5515
|
}
|
|
5373
5516
|
function lockIsFresh(path) {
|
|
5374
5517
|
try {
|
|
5375
|
-
return Date.now() -
|
|
5518
|
+
return Date.now() - statSync3(path).mtimeMs < 5e3;
|
|
5376
5519
|
} catch {
|
|
5377
5520
|
return false;
|
|
5378
5521
|
}
|
|
@@ -5389,6 +5532,12 @@ function publishEndpoint(context2, endpoint) {
|
|
|
5389
5532
|
atomicJson(endpointPath(context2), endpoint);
|
|
5390
5533
|
}
|
|
5391
5534
|
|
|
5535
|
+
// reception.ts
|
|
5536
|
+
import { createHash } from "node:crypto";
|
|
5537
|
+
import { chmodSync as chmodSync3, existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync7, realpathSync } from "node:fs";
|
|
5538
|
+
import { join as join9, resolve as resolve2 } from "node:path";
|
|
5539
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5540
|
+
|
|
5392
5541
|
// untrusted.ts
|
|
5393
5542
|
var SPECIAL_TOKEN = /<\|[a-zA-Z0-9_]+\|>|<\/?s>|\[INST\]|\[\/INST\]|<<SYS>>|<<\/SYS>>|<start_of_turn>|<end_of_turn>/g;
|
|
5394
5543
|
function stripSpecialTokens(input) {
|
|
@@ -5407,15 +5556,691 @@ ${cleanBody}
|
|
|
5407
5556
|
${close2}`, markerId };
|
|
5408
5557
|
}
|
|
5409
5558
|
|
|
5559
|
+
// reception.ts
|
|
5560
|
+
var RECEPTION_PROTOCOL = "agents-city-reception/1";
|
|
5561
|
+
var RECEPTION_SCHEMA_VERSION = 3;
|
|
5562
|
+
var AUTO_ROUTER_PROFILE = "deterministic-rules/1";
|
|
5563
|
+
var DEFAULT_PENDING_MESSAGES = 1e4;
|
|
5564
|
+
var MAX_PENDING_MESSAGES = 1e5;
|
|
5565
|
+
var DEFAULT_PENDING_BYTES = 64 * 1024 * 1024;
|
|
5566
|
+
var MAX_PENDING_BYTES = 512 * 1024 * 1024;
|
|
5567
|
+
var DELIVERY_BATCH = 20;
|
|
5568
|
+
var databases = /* @__PURE__ */ new Map();
|
|
5569
|
+
function receptionDatabasePath(appHome) {
|
|
5570
|
+
return join9(resolve2(appHome), ".runtime", "reception", "reception.sqlite3");
|
|
5571
|
+
}
|
|
5572
|
+
function recordReceptionMessage(context2, envelope) {
|
|
5573
|
+
return recordReceptionMessages(context2, [envelope]);
|
|
5574
|
+
}
|
|
5575
|
+
function recordReceptionMessages(context2, envelopes) {
|
|
5576
|
+
if (!envelopes.length || envelopes.length > 32) {
|
|
5577
|
+
throw new Error("invalid_reception_message_batch");
|
|
5578
|
+
}
|
|
5579
|
+
const rows = envelopes.map(validateReceptionEnvelope);
|
|
5580
|
+
if (new Set(rows.map((row) => row.envelope.id)).size !== rows.length) {
|
|
5581
|
+
throw new Error("duplicate_reception_message_batch");
|
|
5582
|
+
}
|
|
5583
|
+
const database = receptionDatabase(context2.appHome);
|
|
5584
|
+
const maximumMessages = boundedInteger(
|
|
5585
|
+
process.env.CITY_RECEPTION_MAX_PENDING,
|
|
5586
|
+
DEFAULT_PENDING_MESSAGES,
|
|
5587
|
+
100,
|
|
5588
|
+
MAX_PENDING_MESSAGES
|
|
5589
|
+
);
|
|
5590
|
+
const maximumBytes = boundedInteger(
|
|
5591
|
+
process.env.CITY_RECEPTION_MAX_BYTES,
|
|
5592
|
+
DEFAULT_PENDING_BYTES,
|
|
5593
|
+
1024 * 1024,
|
|
5594
|
+
MAX_PENDING_BYTES
|
|
5595
|
+
);
|
|
5596
|
+
let committed = false;
|
|
5597
|
+
try {
|
|
5598
|
+
database.exec("BEGIN IMMEDIATE");
|
|
5599
|
+
expireOldReception(database);
|
|
5600
|
+
const fresh = rows.filter((row) => {
|
|
5601
|
+
const existing = database.prepare("SELECT 1 AS found FROM reception_messages WHERE message_id = ? LIMIT 1").get(row.envelope.id);
|
|
5602
|
+
return !existing;
|
|
5603
|
+
});
|
|
5604
|
+
if (!fresh.length) {
|
|
5605
|
+
const current = receptionCounters(database);
|
|
5606
|
+
database.exec("COMMIT");
|
|
5607
|
+
committed = true;
|
|
5608
|
+
return { inserted: false, ...current };
|
|
5609
|
+
}
|
|
5610
|
+
const counters = receptionCounters(database);
|
|
5611
|
+
const bytes = fresh.reduce((sum, row) => sum + row.bytes, 0);
|
|
5612
|
+
if (counters.pending + fresh.length > maximumMessages || counters.pendingBytes + bytes > maximumBytes) {
|
|
5613
|
+
throw new Error("reception_inbox_full");
|
|
5614
|
+
}
|
|
5615
|
+
const insert = database.prepare(`
|
|
5616
|
+
INSERT INTO reception_messages (
|
|
5617
|
+
message_id, protocol, state, source_city, source_created_at,
|
|
5618
|
+
source_name, message_kind, in_reply_to,
|
|
5619
|
+
received_city_id, received_city_address, body, body_sha256,
|
|
5620
|
+
connection_id, road_id, remote_message_id, received_at
|
|
5621
|
+
) VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5622
|
+
`);
|
|
5623
|
+
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5624
|
+
for (const row of fresh) {
|
|
5625
|
+
const envelope = row.envelope;
|
|
5626
|
+
insert.run(
|
|
5627
|
+
envelope.id,
|
|
5628
|
+
RECEPTION_PROTOCOL,
|
|
5629
|
+
envelope.from.city,
|
|
5630
|
+
envelope.createdAt,
|
|
5631
|
+
row.sourceName,
|
|
5632
|
+
row.kind,
|
|
5633
|
+
row.inReplyTo,
|
|
5634
|
+
context2.city.id,
|
|
5635
|
+
context2.city.address,
|
|
5636
|
+
row.body,
|
|
5637
|
+
sha256(row.body),
|
|
5638
|
+
optionalText(envelope.payload?.connectionId, 160),
|
|
5639
|
+
optionalText(envelope.payload?.roadId, 160),
|
|
5640
|
+
optionalText(envelope.payload?.remoteMessageId, 160),
|
|
5641
|
+
receivedAt
|
|
5642
|
+
);
|
|
5643
|
+
}
|
|
5644
|
+
applyAutomaticRouting(
|
|
5645
|
+
database,
|
|
5646
|
+
fresh.map((row) => row.envelope.id)
|
|
5647
|
+
);
|
|
5648
|
+
const updated = receptionCounters(database);
|
|
5649
|
+
database.exec("COMMIT");
|
|
5650
|
+
committed = true;
|
|
5651
|
+
return { inserted: true, ...updated };
|
|
5652
|
+
} finally {
|
|
5653
|
+
if (!committed) {
|
|
5654
|
+
try {
|
|
5655
|
+
database.exec("ROLLBACK");
|
|
5656
|
+
} catch {
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
function validateReceptionEnvelope(envelope) {
|
|
5662
|
+
const body = envelope.payload?.text;
|
|
5663
|
+
if (typeof body !== "string" || !body || body.length > MAX_BODY) {
|
|
5664
|
+
throw new Error("invalid_reception_message_body");
|
|
5665
|
+
}
|
|
5666
|
+
if (envelope.payload?.transport !== "managed-e2ee") {
|
|
5667
|
+
throw new Error("reception_accepts_managed_messages_only");
|
|
5668
|
+
}
|
|
5669
|
+
const kind = envelope.payload?.messageKind === "rejection" ? "rejection" : "message";
|
|
5670
|
+
const sourceName = optionalText(envelope.payload?.sourceName, 100) ?? envelope.from.city;
|
|
5671
|
+
const inReplyTo = optionalText(envelope.payload?.inReplyTo, 180);
|
|
5672
|
+
return { envelope, body, kind, sourceName, inReplyTo, bytes: Buffer.byteLength(body, "utf8") };
|
|
5673
|
+
}
|
|
5674
|
+
function applyAutomaticRouting(database, messageIds) {
|
|
5675
|
+
if (!messageIds.length) return;
|
|
5676
|
+
const settings = database.prepare(
|
|
5677
|
+
`
|
|
5678
|
+
SELECT routing_mode, router_profile FROM reception_settings WHERE singleton = 1
|
|
5679
|
+
`
|
|
5680
|
+
).get();
|
|
5681
|
+
if (settings?.routing_mode !== "auto" || settings.router_profile !== AUTO_ROUTER_PROFILE) return;
|
|
5682
|
+
const rawRules = database.prepare(
|
|
5683
|
+
`
|
|
5684
|
+
SELECT rule_id, target_city_id, target_city_address, keywords_json, priority
|
|
5685
|
+
FROM reception_auto_rules WHERE enabled = 1
|
|
5686
|
+
ORDER BY priority DESC, rule_id
|
|
5687
|
+
`
|
|
5688
|
+
).all();
|
|
5689
|
+
const rules = rawRules.map(parseAutomaticRule).filter((rule) => Boolean(rule));
|
|
5690
|
+
if (!rules.length) return;
|
|
5691
|
+
const route = database.prepare(`
|
|
5692
|
+
INSERT INTO reception_routes (
|
|
5693
|
+
message_id, target_city_id, target_city_address, state, approved_by, approved_at
|
|
5694
|
+
) VALUES (?, ?, ?, 'queued', 'auto', ?)
|
|
5695
|
+
`);
|
|
5696
|
+
const decide2 = database.prepare(`
|
|
5697
|
+
UPDATE reception_messages
|
|
5698
|
+
SET state = 'routed', decided_at = ?, decision_reason = ?
|
|
5699
|
+
WHERE message_id = ? AND state = 'pending' AND message_kind = 'message'
|
|
5700
|
+
`);
|
|
5701
|
+
for (const messageId of messageIds) {
|
|
5702
|
+
const message = database.prepare(
|
|
5703
|
+
`
|
|
5704
|
+
SELECT body, message_kind FROM reception_messages
|
|
5705
|
+
WHERE message_id = ? AND state = 'pending' LIMIT 1
|
|
5706
|
+
`
|
|
5707
|
+
).get(messageId);
|
|
5708
|
+
if (message?.message_kind !== "message" || typeof message.body !== "string") continue;
|
|
5709
|
+
const selected = automaticDestination(message.body, rules);
|
|
5710
|
+
if (!selected) continue;
|
|
5711
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5712
|
+
route.run(messageId, selected.targetCityId, selected.targetCityAddress, now);
|
|
5713
|
+
const result2 = decide2.run(now, `auto:${selected.ruleId}`, messageId);
|
|
5714
|
+
if (Number(result2.changes) !== 1) throw new Error("automatic_reception_decision_conflict");
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
function parseAutomaticRule(value) {
|
|
5718
|
+
let keywords;
|
|
5719
|
+
try {
|
|
5720
|
+
keywords = JSON.parse(String(value.keywords_json ?? ""));
|
|
5721
|
+
} catch {
|
|
5722
|
+
return null;
|
|
5723
|
+
}
|
|
5724
|
+
if (typeof value.rule_id !== "string" || typeof value.target_city_id !== "string" || typeof value.target_city_address !== "string" || !Array.isArray(keywords) || keywords.length < 1 || keywords.length > 20 || !keywords.every((keyword) => typeof keyword === "string" && keyword.length <= 80))
|
|
5725
|
+
return null;
|
|
5726
|
+
return {
|
|
5727
|
+
ruleId: value.rule_id,
|
|
5728
|
+
targetCityId: value.target_city_id,
|
|
5729
|
+
targetCityAddress: value.target_city_address,
|
|
5730
|
+
keywords: keywords.map((keyword) => normalizeForRouting(keyword)).filter(Boolean),
|
|
5731
|
+
priority: Number(value.priority) || 0
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
function automaticDestination(body, rules) {
|
|
5735
|
+
const normalized = normalizeForRouting(body);
|
|
5736
|
+
if (!normalized || riskyAutomaticText(normalized)) return null;
|
|
5737
|
+
const matches = rules.map((rule) => ({
|
|
5738
|
+
rule,
|
|
5739
|
+
score: rule.keywords.reduce(
|
|
5740
|
+
(score, keyword) => score + (keyword === "*" || normalized.includes(keyword) ? 1 : 0),
|
|
5741
|
+
0
|
|
5742
|
+
)
|
|
5743
|
+
})).filter((candidate) => candidate.score > 0);
|
|
5744
|
+
if (!matches.length) return null;
|
|
5745
|
+
matches.sort(
|
|
5746
|
+
(left, right) => right.score - left.score || right.rule.priority - left.rule.priority || left.rule.ruleId.localeCompare(right.rule.ruleId)
|
|
5747
|
+
);
|
|
5748
|
+
const winner = matches[0];
|
|
5749
|
+
const runnerUp = matches[1];
|
|
5750
|
+
if (runnerUp && runnerUp.score === winner.score && runnerUp.rule.priority === winner.rule.priority)
|
|
5751
|
+
return null;
|
|
5752
|
+
return winner.rule;
|
|
5753
|
+
}
|
|
5754
|
+
function normalizeForRouting(value) {
|
|
5755
|
+
return value.normalize("NFKC").toLocaleLowerCase("en").replace(/\s+/g, " ").trim();
|
|
5756
|
+
}
|
|
5757
|
+
function riskyAutomaticText(value) {
|
|
5758
|
+
return [
|
|
5759
|
+
/<\|(?:im_start|im_end|system|developer)\|>/i,
|
|
5760
|
+
/\[(?:system|inst|\/inst)\]/i,
|
|
5761
|
+
/\b(?:ignore|disregard|override)\b.{0,40}\b(?:previous|system|developer|instructions?)\b/i,
|
|
5762
|
+
/\b(?:system|developer)\s+(?:prompt|message)\b/i,
|
|
5763
|
+
/\b(?:reveal|print|send|exfiltrate)\b.{0,50}\b(?:password|secret|api[ -]?key|token|credential)\b/i,
|
|
5764
|
+
/\b(?:run|execute)\b.{0,30}\b(?:shell|command|terminal|tool)\b/i
|
|
5765
|
+
].some((pattern) => pattern.test(value));
|
|
5766
|
+
}
|
|
5767
|
+
function syncReceptionConnections(appHome, connections2) {
|
|
5768
|
+
const database = receptionDatabase(appHome);
|
|
5769
|
+
let committed = false;
|
|
5770
|
+
try {
|
|
5771
|
+
database.exec("BEGIN IMMEDIATE");
|
|
5772
|
+
database.prepare("UPDATE reception_connections SET status = 'inactive'").run();
|
|
5773
|
+
const upsert = database.prepare(`
|
|
5774
|
+
INSERT INTO reception_connections (
|
|
5775
|
+
road_id, connection_id, peer_name, peer_endpoint, status, updated_at
|
|
5776
|
+
) VALUES (?, ?, ?, ?, 'active', ?)
|
|
5777
|
+
ON CONFLICT(road_id) DO UPDATE SET
|
|
5778
|
+
connection_id = excluded.connection_id,
|
|
5779
|
+
peer_name = excluded.peer_name,
|
|
5780
|
+
peer_endpoint = excluded.peer_endpoint,
|
|
5781
|
+
status = 'active',
|
|
5782
|
+
updated_at = excluded.updated_at
|
|
5783
|
+
`);
|
|
5784
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5785
|
+
for (const connection of connections2) {
|
|
5786
|
+
if (!connection.roadId || !connection.connectionId || !connection.peerName.trim() || connection.peerName.length > 100 || !connection.peerEndpoint)
|
|
5787
|
+
throw new Error("invalid_reception_connection");
|
|
5788
|
+
upsert.run(
|
|
5789
|
+
connection.roadId,
|
|
5790
|
+
connection.connectionId,
|
|
5791
|
+
connection.peerName.trim(),
|
|
5792
|
+
connection.peerEndpoint,
|
|
5793
|
+
now
|
|
5794
|
+
);
|
|
5795
|
+
}
|
|
5796
|
+
database.exec("COMMIT");
|
|
5797
|
+
committed = true;
|
|
5798
|
+
} finally {
|
|
5799
|
+
if (!committed) {
|
|
5800
|
+
try {
|
|
5801
|
+
database.exec("ROLLBACK");
|
|
5802
|
+
} catch {
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
function pendingReceptionOutbox(appHome, limit = 20) {
|
|
5808
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 20) {
|
|
5809
|
+
throw new Error("invalid_reception_outbox_batch");
|
|
5810
|
+
}
|
|
5811
|
+
return receptionDatabase(appHome).prepare(
|
|
5812
|
+
`
|
|
5813
|
+
SELECT o.message_id, o.road_id, o.connection_id, o.kind, o.body,
|
|
5814
|
+
o.in_reply_to, o.attempt_count
|
|
5815
|
+
FROM reception_outbox o
|
|
5816
|
+
JOIN reception_connections c ON c.road_id = o.road_id
|
|
5817
|
+
AND c.connection_id = o.connection_id
|
|
5818
|
+
WHERE o.state = 'queued' AND o.body IS NOT NULL AND c.status = 'active'
|
|
5819
|
+
AND (o.next_attempt_at IS NULL OR o.next_attempt_at <= ?)
|
|
5820
|
+
ORDER BY o.created_at, o.message_id
|
|
5821
|
+
LIMIT ?
|
|
5822
|
+
`
|
|
5823
|
+
).all(Date.now(), limit).map((row) => {
|
|
5824
|
+
const value = row;
|
|
5825
|
+
return {
|
|
5826
|
+
messageId: String(value.message_id),
|
|
5827
|
+
roadId: String(value.road_id),
|
|
5828
|
+
connectionId: String(value.connection_id),
|
|
5829
|
+
kind: value.kind === "rejection" ? "rejection" : "message",
|
|
5830
|
+
body: String(value.body),
|
|
5831
|
+
inReplyTo: typeof value.in_reply_to === "string" ? value.in_reply_to : null,
|
|
5832
|
+
attemptCount: Number(value.attempt_count)
|
|
5833
|
+
};
|
|
5834
|
+
});
|
|
5835
|
+
}
|
|
5836
|
+
function markReceptionOutboxSent(appHome, messageId) {
|
|
5837
|
+
receptionDatabase(appHome).prepare(
|
|
5838
|
+
`
|
|
5839
|
+
UPDATE reception_outbox
|
|
5840
|
+
SET state = 'sent', body = NULL, sent_at = ?, next_attempt_at = NULL, error = NULL
|
|
5841
|
+
WHERE message_id = ? AND state = 'queued'
|
|
5842
|
+
`
|
|
5843
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), messageId);
|
|
5844
|
+
}
|
|
5845
|
+
function markReceptionOutboxFailed(appHome, messageId, attemptCount, error) {
|
|
5846
|
+
const attempts = Math.max(1, attemptCount + 1);
|
|
5847
|
+
const retryAt = Date.now() + Math.min(3e5, 1e3 * 2 ** Math.min(attempts - 1, 8));
|
|
5848
|
+
receptionDatabase(appHome).prepare(
|
|
5849
|
+
`
|
|
5850
|
+
UPDATE reception_outbox
|
|
5851
|
+
SET attempt_count = ?, last_attempt_at = ?, next_attempt_at = ?, error = ?
|
|
5852
|
+
WHERE message_id = ? AND state = 'queued'
|
|
5853
|
+
`
|
|
5854
|
+
).run(
|
|
5855
|
+
attempts,
|
|
5856
|
+
Date.now(),
|
|
5857
|
+
retryAt,
|
|
5858
|
+
String(error instanceof Error ? error.message : error).slice(0, 300),
|
|
5859
|
+
messageId
|
|
5860
|
+
);
|
|
5861
|
+
}
|
|
5862
|
+
function deliverApprovedReception(context2, limit = DELIVERY_BATCH) {
|
|
5863
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > DELIVERY_BATCH) {
|
|
5864
|
+
throw new Error("invalid_reception_delivery_batch");
|
|
5865
|
+
}
|
|
5866
|
+
const database = receptionDatabase(context2.appHome);
|
|
5867
|
+
const routes = database.prepare(
|
|
5868
|
+
`
|
|
5869
|
+
SELECT m.message_id, m.source_city, m.source_created_at, m.body,
|
|
5870
|
+
m.connection_id, m.road_id, r.approved_at, r.approved_by,
|
|
5871
|
+
r.attempt_count
|
|
5872
|
+
FROM reception_routes r
|
|
5873
|
+
JOIN reception_messages m ON m.message_id = r.message_id
|
|
5874
|
+
WHERE r.target_city_id = ? AND r.target_city_address = ?
|
|
5875
|
+
AND r.state = 'queued' AND m.state = 'routed' AND m.body IS NOT NULL
|
|
5876
|
+
AND (r.next_attempt_at IS NULL OR r.next_attempt_at <= ?)
|
|
5877
|
+
ORDER BY r.approved_at, m.received_at, m.message_id
|
|
5878
|
+
LIMIT ?
|
|
5879
|
+
`
|
|
5880
|
+
).all(context2.city.id, context2.city.address, Date.now(), limit);
|
|
5881
|
+
let delivered = 0;
|
|
5882
|
+
let failed = 0;
|
|
5883
|
+
for (const route of routes) {
|
|
5884
|
+
try {
|
|
5885
|
+
const envelope = {
|
|
5886
|
+
protocol: BUS_PROTOCOL,
|
|
5887
|
+
id: route.message_id,
|
|
5888
|
+
kind: "road.message",
|
|
5889
|
+
scope: "road",
|
|
5890
|
+
thread: null,
|
|
5891
|
+
from: { city: route.source_city, actor: "seat", role: "external-seat" },
|
|
5892
|
+
to: { city: context2.city.address, actor: "seat" },
|
|
5893
|
+
createdAt: route.source_created_at,
|
|
5894
|
+
payload: {
|
|
5895
|
+
text: wrapUntrusted(route.body, route.source_city).text,
|
|
5896
|
+
trust: "information-not-authority",
|
|
5897
|
+
transport: "reception-approved",
|
|
5898
|
+
reception: {
|
|
5899
|
+
approvedAt: route.approved_at,
|
|
5900
|
+
approvedBy: route.approved_by,
|
|
5901
|
+
sourceMessageId: route.message_id,
|
|
5902
|
+
...route.connection_id ? { connectionId: route.connection_id } : {},
|
|
5903
|
+
...route.road_id ? { roadId: route.road_id } : {}
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
};
|
|
5907
|
+
const accepted = recordRoadInbox(context2.runtimeDir, envelope);
|
|
5908
|
+
if (accepted) markRoadInboxAccepted(context2.runtimeDir, envelope.id);
|
|
5909
|
+
markRouteDelivered(database, route.message_id, context2.city.id);
|
|
5910
|
+
delivered += 1;
|
|
5911
|
+
} catch (error) {
|
|
5912
|
+
if (error instanceof Error && error.message === "road_inbox_full") break;
|
|
5913
|
+
markRouteFailed(database, route.message_id, context2.city.id, error);
|
|
5914
|
+
failed += 1;
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
const remaining = Number(
|
|
5918
|
+
database.prepare(
|
|
5919
|
+
`
|
|
5920
|
+
SELECT COUNT(*) AS count FROM reception_routes
|
|
5921
|
+
WHERE target_city_id = ? AND target_city_address = ? AND state = 'queued'
|
|
5922
|
+
`
|
|
5923
|
+
).get(context2.city.id, context2.city.address)?.count ?? 0
|
|
5924
|
+
);
|
|
5925
|
+
return { delivered, failed, remaining };
|
|
5926
|
+
}
|
|
5927
|
+
function receptionDatabase(appHome) {
|
|
5928
|
+
const path = receptionDatabasePath(appHome);
|
|
5929
|
+
const cached = databases.get(path);
|
|
5930
|
+
if (cached) return cached;
|
|
5931
|
+
const directory = preparePrivateReceptionDirectory(appHome);
|
|
5932
|
+
if (existsSync6(path)) assertRegularPrivateDatabase(path);
|
|
5933
|
+
const database = new DatabaseSync(path);
|
|
5934
|
+
chmodSync3(path, 384);
|
|
5935
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
5936
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
5937
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
5938
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
5939
|
+
initializeSchema(database);
|
|
5940
|
+
chmodSync3(directory, 448);
|
|
5941
|
+
databases.set(path, database);
|
|
5942
|
+
return database;
|
|
5943
|
+
}
|
|
5944
|
+
function preparePrivateReceptionDirectory(appHome) {
|
|
5945
|
+
const home = realpathSync(resolve2(appHome));
|
|
5946
|
+
const runtime = join9(home, ".runtime");
|
|
5947
|
+
const reception = join9(runtime, "reception");
|
|
5948
|
+
for (const path of [runtime, reception]) {
|
|
5949
|
+
if (!existsSync6(path)) mkdirSync7(path, { mode: 448 });
|
|
5950
|
+
const info = lstatSync(path);
|
|
5951
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
5952
|
+
throw new Error(`unsafe_reception_directory:${path}`);
|
|
5953
|
+
}
|
|
5954
|
+
chmodSync3(path, 448);
|
|
5955
|
+
}
|
|
5956
|
+
return reception;
|
|
5957
|
+
}
|
|
5958
|
+
function assertRegularPrivateDatabase(path) {
|
|
5959
|
+
const info = lstatSync(path);
|
|
5960
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe_reception_database");
|
|
5961
|
+
chmodSync3(path, 384);
|
|
5962
|
+
}
|
|
5963
|
+
function initializeSchema(database) {
|
|
5964
|
+
database.exec(`
|
|
5965
|
+
CREATE TABLE IF NOT EXISTS reception_meta (
|
|
5966
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
5967
|
+
schema_version INTEGER NOT NULL,
|
|
5968
|
+
created_at TEXT NOT NULL
|
|
5969
|
+
);
|
|
5970
|
+
CREATE TABLE IF NOT EXISTS reception_settings (
|
|
5971
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
5972
|
+
routing_mode TEXT NOT NULL DEFAULT 'manual' CHECK (routing_mode IN ('manual', 'auto')),
|
|
5973
|
+
review_policy TEXT NOT NULL DEFAULT 'every_message' CHECK (review_policy IN ('every_message', 'new_thread')),
|
|
5974
|
+
router_profile TEXT CHECK (router_profile IS NULL OR length(router_profile) <= 160),
|
|
5975
|
+
updated_at TEXT NOT NULL,
|
|
5976
|
+
CHECK (routing_mode = 'manual' OR router_profile IS NOT NULL)
|
|
5977
|
+
);
|
|
5978
|
+
CREATE TABLE IF NOT EXISTS reception_messages (
|
|
5979
|
+
message_id TEXT PRIMARY KEY CHECK (length(message_id) BETWEEN 1 AND 180),
|
|
5980
|
+
protocol TEXT NOT NULL CHECK (protocol = '${RECEPTION_PROTOCOL}'),
|
|
5981
|
+
state TEXT NOT NULL CHECK (state IN ('pending', 'routed', 'rejected', 'expired')),
|
|
5982
|
+
source_city TEXT NOT NULL CHECK (length(source_city) BETWEEN 3 AND 160),
|
|
5983
|
+
source_created_at TEXT NOT NULL,
|
|
5984
|
+
source_name TEXT CHECK (source_name IS NULL OR length(source_name) <= 100),
|
|
5985
|
+
message_kind TEXT NOT NULL DEFAULT 'message' CHECK (message_kind IN ('message', 'rejection')),
|
|
5986
|
+
in_reply_to TEXT CHECK (in_reply_to IS NULL OR length(in_reply_to) <= 180),
|
|
5987
|
+
received_city_id TEXT NOT NULL CHECK (length(received_city_id) BETWEEN 1 AND 160),
|
|
5988
|
+
received_city_address TEXT NOT NULL CHECK (length(received_city_address) BETWEEN 3 AND 160),
|
|
5989
|
+
body TEXT,
|
|
5990
|
+
body_sha256 TEXT NOT NULL CHECK (length(body_sha256) = 64),
|
|
5991
|
+
connection_id TEXT,
|
|
5992
|
+
road_id TEXT,
|
|
5993
|
+
remote_message_id TEXT,
|
|
5994
|
+
received_at TEXT NOT NULL,
|
|
5995
|
+
decided_at TEXT,
|
|
5996
|
+
decision_reason TEXT CHECK (decision_reason IS NULL OR length(decision_reason) <= 500),
|
|
5997
|
+
CHECK (state = 'pending' OR decided_at IS NOT NULL),
|
|
5998
|
+
CHECK (state <> 'pending' OR body IS NOT NULL)
|
|
5999
|
+
);
|
|
6000
|
+
CREATE INDEX IF NOT EXISTS idx_reception_messages_state_age
|
|
6001
|
+
ON reception_messages (state, received_at, message_id);
|
|
6002
|
+
CREATE TABLE IF NOT EXISTS reception_routes (
|
|
6003
|
+
message_id TEXT NOT NULL REFERENCES reception_messages(message_id) ON DELETE CASCADE,
|
|
6004
|
+
target_city_id TEXT NOT NULL CHECK (length(target_city_id) BETWEEN 1 AND 160),
|
|
6005
|
+
target_city_address TEXT NOT NULL CHECK (length(target_city_address) BETWEEN 3 AND 160),
|
|
6006
|
+
state TEXT NOT NULL DEFAULT 'queued' CHECK (state IN ('queued', 'delivered', 'failed')),
|
|
6007
|
+
approved_by TEXT NOT NULL CHECK (approved_by IN ('human', 'auto')),
|
|
6008
|
+
approved_at TEXT NOT NULL,
|
|
6009
|
+
delivered_at TEXT,
|
|
6010
|
+
error TEXT CHECK (error IS NULL OR length(error) <= 300),
|
|
6011
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
|
6012
|
+
last_attempt_at INTEGER,
|
|
6013
|
+
next_attempt_at INTEGER,
|
|
6014
|
+
PRIMARY KEY (message_id, target_city_id)
|
|
6015
|
+
);
|
|
6016
|
+
CREATE INDEX IF NOT EXISTS idx_reception_routes_city_state
|
|
6017
|
+
ON reception_routes (target_city_id, state, approved_at);
|
|
6018
|
+
CREATE TABLE IF NOT EXISTS reception_counters (
|
|
6019
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
6020
|
+
pending_count INTEGER NOT NULL DEFAULT 0 CHECK (pending_count >= 0),
|
|
6021
|
+
pending_bytes INTEGER NOT NULL DEFAULT 0 CHECK (pending_bytes >= 0)
|
|
6022
|
+
);
|
|
6023
|
+
CREATE TABLE IF NOT EXISTS reception_connections (
|
|
6024
|
+
road_id TEXT PRIMARY KEY,
|
|
6025
|
+
connection_id TEXT NOT NULL UNIQUE,
|
|
6026
|
+
peer_name TEXT NOT NULL CHECK (length(peer_name) BETWEEN 1 AND 100),
|
|
6027
|
+
peer_endpoint TEXT NOT NULL CHECK (length(peer_endpoint) BETWEEN 3 AND 160),
|
|
6028
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'inactive')),
|
|
6029
|
+
updated_at TEXT NOT NULL
|
|
6030
|
+
);
|
|
6031
|
+
CREATE TABLE IF NOT EXISTS reception_outbox (
|
|
6032
|
+
message_id TEXT PRIMARY KEY CHECK (length(message_id) = 36),
|
|
6033
|
+
road_id TEXT NOT NULL,
|
|
6034
|
+
connection_id TEXT NOT NULL,
|
|
6035
|
+
kind TEXT NOT NULL CHECK (kind IN ('message', 'rejection')),
|
|
6036
|
+
body TEXT,
|
|
6037
|
+
in_reply_to TEXT CHECK (in_reply_to IS NULL OR length(in_reply_to) <= 180),
|
|
6038
|
+
state TEXT NOT NULL DEFAULT 'queued' CHECK (state IN ('queued', 'sent')),
|
|
6039
|
+
created_at TEXT NOT NULL,
|
|
6040
|
+
sent_at TEXT,
|
|
6041
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
|
6042
|
+
last_attempt_at INTEGER,
|
|
6043
|
+
next_attempt_at INTEGER,
|
|
6044
|
+
error TEXT CHECK (error IS NULL OR length(error) <= 300)
|
|
6045
|
+
);
|
|
6046
|
+
CREATE INDEX IF NOT EXISTS idx_reception_outbox_state_age
|
|
6047
|
+
ON reception_outbox (state, next_attempt_at, created_at);
|
|
6048
|
+
CREATE TABLE IF NOT EXISTS reception_auto_rules (
|
|
6049
|
+
rule_id TEXT PRIMARY KEY CHECK (length(rule_id) = 36),
|
|
6050
|
+
target_city_id TEXT NOT NULL UNIQUE CHECK (length(target_city_id) BETWEEN 1 AND 160),
|
|
6051
|
+
target_city_address TEXT NOT NULL CHECK (length(target_city_address) BETWEEN 3 AND 160),
|
|
6052
|
+
keywords_json TEXT NOT NULL CHECK (length(keywords_json) BETWEEN 5 AND 2000),
|
|
6053
|
+
priority INTEGER NOT NULL DEFAULT 0 CHECK (priority BETWEEN 0 AND 1000),
|
|
6054
|
+
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
6055
|
+
updated_at TEXT NOT NULL
|
|
6056
|
+
);
|
|
6057
|
+
CREATE TRIGGER IF NOT EXISTS reception_message_count_after_insert
|
|
6058
|
+
AFTER INSERT ON reception_messages WHEN NEW.state = 'pending'
|
|
6059
|
+
BEGIN
|
|
6060
|
+
UPDATE reception_counters
|
|
6061
|
+
SET pending_count = pending_count + 1,
|
|
6062
|
+
pending_bytes = pending_bytes + length(CAST(NEW.body AS BLOB))
|
|
6063
|
+
WHERE singleton = 1;
|
|
6064
|
+
END;
|
|
6065
|
+
CREATE TRIGGER IF NOT EXISTS reception_message_count_after_decision
|
|
6066
|
+
AFTER UPDATE OF state ON reception_messages
|
|
6067
|
+
WHEN OLD.state = 'pending' AND NEW.state <> 'pending'
|
|
6068
|
+
BEGIN
|
|
6069
|
+
UPDATE reception_counters
|
|
6070
|
+
SET pending_count = MAX(0, pending_count - 1),
|
|
6071
|
+
pending_bytes = MAX(0, pending_bytes - length(CAST(OLD.body AS BLOB)))
|
|
6072
|
+
WHERE singleton = 1;
|
|
6073
|
+
END;
|
|
6074
|
+
CREATE TRIGGER IF NOT EXISTS reception_message_count_after_delete
|
|
6075
|
+
AFTER DELETE ON reception_messages WHEN OLD.state = 'pending'
|
|
6076
|
+
BEGIN
|
|
6077
|
+
UPDATE reception_counters
|
|
6078
|
+
SET pending_count = MAX(0, pending_count - 1),
|
|
6079
|
+
pending_bytes = MAX(0, pending_bytes - length(CAST(OLD.body AS BLOB)))
|
|
6080
|
+
WHERE singleton = 1;
|
|
6081
|
+
END;
|
|
6082
|
+
`);
|
|
6083
|
+
const meta = database.prepare("SELECT schema_version FROM reception_meta WHERE singleton = 1").get();
|
|
6084
|
+
if (meta?.schema_version === 1) {
|
|
6085
|
+
database.exec("BEGIN IMMEDIATE");
|
|
6086
|
+
try {
|
|
6087
|
+
const current = database.prepare("SELECT schema_version FROM reception_meta WHERE singleton = 1").get();
|
|
6088
|
+
if (current?.schema_version === 1) {
|
|
6089
|
+
database.exec(`
|
|
6090
|
+
ALTER TABLE reception_messages ADD COLUMN source_name TEXT
|
|
6091
|
+
CHECK (source_name IS NULL OR length(source_name) <= 100);
|
|
6092
|
+
ALTER TABLE reception_messages ADD COLUMN message_kind TEXT NOT NULL DEFAULT 'message'
|
|
6093
|
+
CHECK (message_kind IN ('message', 'rejection'));
|
|
6094
|
+
ALTER TABLE reception_messages ADD COLUMN in_reply_to TEXT
|
|
6095
|
+
CHECK (in_reply_to IS NULL OR length(in_reply_to) <= 180);
|
|
6096
|
+
UPDATE reception_meta SET schema_version = ${RECEPTION_SCHEMA_VERSION} WHERE singleton = 1;
|
|
6097
|
+
`);
|
|
6098
|
+
} else if (current?.schema_version === 2) {
|
|
6099
|
+
database.prepare("UPDATE reception_meta SET schema_version = ? WHERE singleton = 1").run(RECEPTION_SCHEMA_VERSION);
|
|
6100
|
+
} else if (current?.schema_version !== RECEPTION_SCHEMA_VERSION) {
|
|
6101
|
+
throw new Error(`unsupported_reception_schema:${current?.schema_version ?? "missing"}`);
|
|
6102
|
+
}
|
|
6103
|
+
database.exec("COMMIT");
|
|
6104
|
+
} catch (error) {
|
|
6105
|
+
try {
|
|
6106
|
+
database.exec("ROLLBACK");
|
|
6107
|
+
} catch {
|
|
6108
|
+
}
|
|
6109
|
+
throw error;
|
|
6110
|
+
}
|
|
6111
|
+
} else if (meta && meta.schema_version !== RECEPTION_SCHEMA_VERSION) {
|
|
6112
|
+
throw new Error(`unsupported_reception_schema:${meta.schema_version}`);
|
|
6113
|
+
}
|
|
6114
|
+
database.prepare(
|
|
6115
|
+
`
|
|
6116
|
+
INSERT OR IGNORE INTO reception_meta (singleton, schema_version, created_at)
|
|
6117
|
+
VALUES (1, ?, ?)
|
|
6118
|
+
`
|
|
6119
|
+
).run(RECEPTION_SCHEMA_VERSION, (/* @__PURE__ */ new Date()).toISOString());
|
|
6120
|
+
database.prepare(
|
|
6121
|
+
`
|
|
6122
|
+
INSERT OR IGNORE INTO reception_settings (
|
|
6123
|
+
singleton, routing_mode, review_policy, router_profile, updated_at
|
|
6124
|
+
) VALUES (1, 'manual', 'every_message', NULL, ?)
|
|
6125
|
+
`
|
|
6126
|
+
).run((/* @__PURE__ */ new Date()).toISOString());
|
|
6127
|
+
database.prepare(
|
|
6128
|
+
`
|
|
6129
|
+
INSERT OR IGNORE INTO reception_counters (singleton, pending_count, pending_bytes)
|
|
6130
|
+
VALUES (1, 0, 0)
|
|
6131
|
+
`
|
|
6132
|
+
).run();
|
|
6133
|
+
}
|
|
6134
|
+
function receptionCounters(database) {
|
|
6135
|
+
const row = database.prepare("SELECT pending_count, pending_bytes FROM reception_counters WHERE singleton = 1").get();
|
|
6136
|
+
return {
|
|
6137
|
+
pending: Number(row?.pending_count ?? 0),
|
|
6138
|
+
pendingBytes: Number(row?.pending_bytes ?? 0)
|
|
6139
|
+
};
|
|
6140
|
+
}
|
|
6141
|
+
function expireOldReception(database) {
|
|
6142
|
+
const retentionDays = boundedInteger(process.env.CITY_RECEPTION_PENDING_DAYS, 30, 1, 90);
|
|
6143
|
+
database.prepare(
|
|
6144
|
+
`
|
|
6145
|
+
UPDATE reception_messages
|
|
6146
|
+
SET state = 'expired', body = NULL, decided_at = ?, decision_reason = 'expired locally'
|
|
6147
|
+
WHERE state = 'pending' AND received_at < ?
|
|
6148
|
+
`
|
|
6149
|
+
).run(
|
|
6150
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
6151
|
+
new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3).toISOString()
|
|
6152
|
+
);
|
|
6153
|
+
database.prepare(
|
|
6154
|
+
`
|
|
6155
|
+
DELETE FROM reception_messages
|
|
6156
|
+
WHERE state IN ('rejected', 'expired') AND decided_at < ?
|
|
6157
|
+
`
|
|
6158
|
+
).run(new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString());
|
|
6159
|
+
database.prepare(
|
|
6160
|
+
`
|
|
6161
|
+
DELETE FROM reception_messages
|
|
6162
|
+
WHERE state = 'routed' AND body IS NULL AND decided_at < ?
|
|
6163
|
+
`
|
|
6164
|
+
).run(new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString());
|
|
6165
|
+
}
|
|
6166
|
+
function markRouteDelivered(database, messageId, cityId) {
|
|
6167
|
+
let committed = false;
|
|
6168
|
+
try {
|
|
6169
|
+
database.exec("BEGIN IMMEDIATE");
|
|
6170
|
+
database.prepare(
|
|
6171
|
+
`
|
|
6172
|
+
UPDATE reception_routes
|
|
6173
|
+
SET state = 'delivered', delivered_at = ?, error = NULL,
|
|
6174
|
+
next_attempt_at = NULL
|
|
6175
|
+
WHERE message_id = ? AND target_city_id = ? AND state = 'queued'
|
|
6176
|
+
`
|
|
6177
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), messageId, cityId);
|
|
6178
|
+
const waiting = Number(
|
|
6179
|
+
database.prepare(
|
|
6180
|
+
`
|
|
6181
|
+
SELECT COUNT(*) AS count FROM reception_routes
|
|
6182
|
+
WHERE message_id = ? AND state <> 'delivered'
|
|
6183
|
+
`
|
|
6184
|
+
).get(messageId)?.count ?? 0
|
|
6185
|
+
);
|
|
6186
|
+
if (!waiting) {
|
|
6187
|
+
database.prepare(
|
|
6188
|
+
`
|
|
6189
|
+
UPDATE reception_messages SET body = NULL
|
|
6190
|
+
WHERE message_id = ? AND state = 'routed'
|
|
6191
|
+
`
|
|
6192
|
+
).run(messageId);
|
|
6193
|
+
}
|
|
6194
|
+
database.exec("COMMIT");
|
|
6195
|
+
committed = true;
|
|
6196
|
+
} finally {
|
|
6197
|
+
if (!committed) {
|
|
6198
|
+
try {
|
|
6199
|
+
database.exec("ROLLBACK");
|
|
6200
|
+
} catch {
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
function markRouteFailed(database, messageId, cityId, error) {
|
|
6206
|
+
const reason = String(error instanceof Error ? error.message : error).slice(0, 300);
|
|
6207
|
+
const row = database.prepare(
|
|
6208
|
+
`
|
|
6209
|
+
SELECT attempt_count FROM reception_routes
|
|
6210
|
+
WHERE message_id = ? AND target_city_id = ? AND state = 'queued'
|
|
6211
|
+
`
|
|
6212
|
+
).get(messageId, cityId);
|
|
6213
|
+
const attempts = Number(row?.attempt_count ?? 0) + 1;
|
|
6214
|
+
const now = Date.now();
|
|
6215
|
+
const retryAt = now + Math.min(3e5, 1e3 * 2 ** Math.min(attempts - 1, 8));
|
|
6216
|
+
database.prepare(
|
|
6217
|
+
`
|
|
6218
|
+
UPDATE reception_routes
|
|
6219
|
+
SET attempt_count = ?, last_attempt_at = ?, next_attempt_at = ?, error = ?
|
|
6220
|
+
WHERE message_id = ? AND target_city_id = ? AND state = 'queued'
|
|
6221
|
+
`
|
|
6222
|
+
).run(attempts, now, retryAt, reason, messageId, cityId);
|
|
6223
|
+
}
|
|
6224
|
+
function optionalText(value, maximum) {
|
|
6225
|
+
return typeof value === "string" && value ? value.slice(0, maximum) : null;
|
|
6226
|
+
}
|
|
6227
|
+
function sha256(value) {
|
|
6228
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
6229
|
+
}
|
|
6230
|
+
function boundedInteger(raw, fallback, minimum, maximum) {
|
|
6231
|
+
const value = Number(raw);
|
|
6232
|
+
return Number.isSafeInteger(value) && value >= minimum && value <= maximum ? value : fallback;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
5410
6235
|
// hub/local-roads.ts
|
|
5411
6236
|
import { readFileSync as readFileSync7 } from "fs";
|
|
5412
|
-
import { join as
|
|
6237
|
+
import { join as join10 } from "path";
|
|
5413
6238
|
async function sendLocalRoad(context2, road, envelope) {
|
|
5414
6239
|
const destinationRuntime = runtimeDirForCity(context2.appHome, road.id);
|
|
5415
6240
|
let endpoint;
|
|
5416
6241
|
try {
|
|
5417
6242
|
endpoint = JSON.parse(
|
|
5418
|
-
readFileSync7(
|
|
6243
|
+
readFileSync7(join10(destinationRuntime, "endpoint.json"), "utf8")
|
|
5419
6244
|
);
|
|
5420
6245
|
if (endpoint.cityId !== road.id || endpoint.cityAddress !== road.address)
|
|
5421
6246
|
throw new Error("identity mismatch");
|
|
@@ -5433,7 +6258,7 @@ async function sendLocalRoad(context2, road, envelope) {
|
|
|
5433
6258
|
function localRoadOnline(context2, road) {
|
|
5434
6259
|
try {
|
|
5435
6260
|
const endpoint = JSON.parse(
|
|
5436
|
-
readFileSync7(
|
|
6261
|
+
readFileSync7(join10(runtimeDirForCity(context2.appHome, road.id), "endpoint.json"), "utf8")
|
|
5437
6262
|
);
|
|
5438
6263
|
if (endpoint.cityId !== road.id || endpoint.cityAddress !== road.address || endpoint.pid <= 0)
|
|
5439
6264
|
return false;
|
|
@@ -5444,7 +6269,7 @@ function localRoadOnline(context2, road) {
|
|
|
5444
6269
|
}
|
|
5445
6270
|
}
|
|
5446
6271
|
function deliver(endpoint, from, envelope) {
|
|
5447
|
-
return new Promise((
|
|
6272
|
+
return new Promise((resolve4, reject) => {
|
|
5448
6273
|
const url = new URL(endpoint.url);
|
|
5449
6274
|
url.searchParams.set("mode", "road");
|
|
5450
6275
|
url.searchParams.set("from", from);
|
|
@@ -5463,7 +6288,7 @@ function deliver(endpoint, from, envelope) {
|
|
|
5463
6288
|
} catch {
|
|
5464
6289
|
}
|
|
5465
6290
|
if (error) reject(error);
|
|
5466
|
-
else
|
|
6291
|
+
else resolve4(`delivered locally to ${endpoint.cityAddress}`);
|
|
5467
6292
|
};
|
|
5468
6293
|
ws.on("message", (raw) => {
|
|
5469
6294
|
let message;
|
|
@@ -5487,8 +6312,692 @@ function deliver(endpoint, from, envelope) {
|
|
|
5487
6312
|
});
|
|
5488
6313
|
}
|
|
5489
6314
|
|
|
6315
|
+
// managed-connect/storage.ts
|
|
6316
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6317
|
+
import {
|
|
6318
|
+
chmodSync as chmodSync4,
|
|
6319
|
+
closeSync as closeSync3,
|
|
6320
|
+
constants as constants2,
|
|
6321
|
+
existsSync as existsSync7,
|
|
6322
|
+
fstatSync,
|
|
6323
|
+
fsyncSync as fsyncSync2,
|
|
6324
|
+
lstatSync as lstatSync2,
|
|
6325
|
+
mkdirSync as mkdirSync8,
|
|
6326
|
+
openSync as openSync3,
|
|
6327
|
+
readFileSync as readFileSync8,
|
|
6328
|
+
realpathSync as realpathSync2,
|
|
6329
|
+
renameSync as renameSync5,
|
|
6330
|
+
unlinkSync as unlinkSync4,
|
|
6331
|
+
writeFileSync as writeFileSync6
|
|
6332
|
+
} from "node:fs";
|
|
6333
|
+
import { homedir as homedir2 } from "node:os";
|
|
6334
|
+
import { dirname as dirname2, join as join11, resolve as resolve3 } from "node:path";
|
|
6335
|
+
import {
|
|
6336
|
+
KEY_TRANSPARENCY_ROOT_CHAIN_PROTOCOL,
|
|
6337
|
+
createOsProtectedDeviceVault,
|
|
6338
|
+
initializeHybridCrypto,
|
|
6339
|
+
parseKeyTransparencyRootChain,
|
|
6340
|
+
resolveKeyTransparencyRootChain
|
|
6341
|
+
} from "./managed-connect-client.js";
|
|
6342
|
+
var CONNECT_STATE_PROTOCOL = "agents-city-connect-state/3";
|
|
6343
|
+
var PLAINTEXT_KEY_CONNECT_STATE_PROTOCOL = "agents-city-connect-state/1";
|
|
6344
|
+
var UNVERSIONED_TRUST_CONNECT_STATE_PROTOCOL = "agents-city-connect-state/2";
|
|
6345
|
+
var MAX_STATE_BYTES = 128 * 1024;
|
|
6346
|
+
var MAX_ROOT_CHAIN_BYTES = 128 * 1024;
|
|
6347
|
+
var CITY_ADDRESS_RE = /^[a-z0-9][a-z0-9_-]{0,31}\/[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
6348
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6349
|
+
var OWNER_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
6350
|
+
function agentsCityHome(explicit = "") {
|
|
6351
|
+
const requested = resolve3(
|
|
6352
|
+
explicit || process.env.AGENTS_CITY_HOME || join11(homedir2(), ".agents-city")
|
|
6353
|
+
);
|
|
6354
|
+
mkdirSync8(requested, { recursive: true, mode: 448 });
|
|
6355
|
+
return realpathSync2(requested);
|
|
6356
|
+
}
|
|
6357
|
+
function connectStateDirectory(appHome = "") {
|
|
6358
|
+
return join11(agentsCityHome(appHome), ".runtime", "connect");
|
|
6359
|
+
}
|
|
6360
|
+
function connectStatePath(appHome = "") {
|
|
6361
|
+
return join11(connectStateDirectory(appHome), "device.json");
|
|
6362
|
+
}
|
|
6363
|
+
function connectVaultDirectory(appHome = "") {
|
|
6364
|
+
return join11(connectStateDirectory(appHome), "vault");
|
|
6365
|
+
}
|
|
6366
|
+
var vaultAccount = (appHome = "") => {
|
|
6367
|
+
const digest = createHash2("sha256").update(agentsCityHome(appHome)).digest("hex");
|
|
6368
|
+
return `agents-city-connect-${digest}`;
|
|
6369
|
+
};
|
|
6370
|
+
function privateDirectory(path) {
|
|
6371
|
+
if (!existsSync7(path)) mkdirSync8(path, { mode: 448 });
|
|
6372
|
+
const info = lstatSync2(path);
|
|
6373
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
6374
|
+
throw new Error(`unsafe_connect_state_directory:${path}`);
|
|
6375
|
+
}
|
|
6376
|
+
chmodSync4(path, 448);
|
|
6377
|
+
}
|
|
6378
|
+
function prepareStateDirectory(appHome = "") {
|
|
6379
|
+
const home = agentsCityHome(appHome);
|
|
6380
|
+
const runtime = join11(home, ".runtime");
|
|
6381
|
+
privateDirectory(runtime);
|
|
6382
|
+
const connect = join11(runtime, "connect");
|
|
6383
|
+
privateDirectory(connect);
|
|
6384
|
+
return connect;
|
|
6385
|
+
}
|
|
6386
|
+
function assertSafeStateDirectory(appHome = "") {
|
|
6387
|
+
const home = agentsCityHome(appHome);
|
|
6388
|
+
const runtime = join11(home, ".runtime");
|
|
6389
|
+
const connect = join11(runtime, "connect");
|
|
6390
|
+
for (const path of [runtime, connect]) {
|
|
6391
|
+
const info = lstatSync2(path);
|
|
6392
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
6393
|
+
throw new Error(`unsafe_connect_state_directory:${path}`);
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
if ((lstatSync2(connect).mode & 63) !== 0) {
|
|
6397
|
+
throw new Error("connect_state_directory_permissions_too_open");
|
|
6398
|
+
}
|
|
6399
|
+
return connect;
|
|
6400
|
+
}
|
|
6401
|
+
function assertPrivateFile(path) {
|
|
6402
|
+
const info = lstatSync2(path);
|
|
6403
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe_connect_state_file");
|
|
6404
|
+
if ((info.mode & 63) !== 0) throw new Error("connect_state_permissions_too_open");
|
|
6405
|
+
if (info.size < 2 || info.size > MAX_STATE_BYTES) throw new Error("invalid_connect_state_size");
|
|
6406
|
+
}
|
|
6407
|
+
var noFollowFlag = () => typeof constants2.O_NOFOLLOW === "number" ? constants2.O_NOFOLLOW : 0;
|
|
6408
|
+
function writeConnectState(state, appHome = "") {
|
|
6409
|
+
const checked = validateConnectState(state);
|
|
6410
|
+
const directory = prepareStateDirectory(appHome);
|
|
6411
|
+
const destination = join11(directory, "device.json");
|
|
6412
|
+
const temporary = join11(directory, `.device-${process.pid}-${crypto.randomUUID()}.tmp`);
|
|
6413
|
+
const fd = openSync3(
|
|
6414
|
+
temporary,
|
|
6415
|
+
constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | noFollowFlag(),
|
|
6416
|
+
384
|
|
6417
|
+
);
|
|
6418
|
+
try {
|
|
6419
|
+
writeFileSync6(fd, `${JSON.stringify(checked, null, 2)}
|
|
6420
|
+
`, { encoding: "utf8" });
|
|
6421
|
+
fsyncSync2(fd);
|
|
6422
|
+
} finally {
|
|
6423
|
+
closeSync3(fd);
|
|
6424
|
+
}
|
|
6425
|
+
renameSync5(temporary, destination);
|
|
6426
|
+
chmodSync4(destination, 384);
|
|
6427
|
+
try {
|
|
6428
|
+
const directoryFd = openSync3(directory, constants2.O_RDONLY);
|
|
6429
|
+
try {
|
|
6430
|
+
fsyncSync2(directoryFd);
|
|
6431
|
+
} finally {
|
|
6432
|
+
closeSync3(directoryFd);
|
|
6433
|
+
}
|
|
6434
|
+
} catch {
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
function readConnectState(appHome = "") {
|
|
6438
|
+
const path = connectStatePath(appHome);
|
|
6439
|
+
if (!existsSync7(path)) return null;
|
|
6440
|
+
assertSafeStateDirectory(appHome);
|
|
6441
|
+
assertPrivateFile(path);
|
|
6442
|
+
const fd = openSync3(path, constants2.O_RDONLY | noFollowFlag());
|
|
6443
|
+
try {
|
|
6444
|
+
const info = fstatSync(fd);
|
|
6445
|
+
if (!info.isFile() || info.size > MAX_STATE_BYTES) {
|
|
6446
|
+
throw new Error("invalid_connect_state_size");
|
|
6447
|
+
}
|
|
6448
|
+
const value = JSON.parse(readFileSync8(fd, "utf8"));
|
|
6449
|
+
if (value.protocol === PLAINTEXT_KEY_CONNECT_STATE_PROTOCOL) {
|
|
6450
|
+
throw new Error("legacy_connect_state_contains_plaintext_keys");
|
|
6451
|
+
}
|
|
6452
|
+
if (value.protocol === UNVERSIONED_TRUST_CONNECT_STATE_PROTOCOL) {
|
|
6453
|
+
throw new Error("connect_state_requires_versioned_trust_repairing");
|
|
6454
|
+
}
|
|
6455
|
+
return validateConnectState(value);
|
|
6456
|
+
} catch (error) {
|
|
6457
|
+
if (error instanceof SyntaxError) throw new Error("invalid_connect_state_json");
|
|
6458
|
+
throw error;
|
|
6459
|
+
} finally {
|
|
6460
|
+
closeSync3(fd);
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
function secureWebOrigin(value) {
|
|
6464
|
+
let url;
|
|
6465
|
+
try {
|
|
6466
|
+
url = new URL(String(value ?? ""));
|
|
6467
|
+
} catch {
|
|
6468
|
+
throw new Error("invalid_connect_service_url");
|
|
6469
|
+
}
|
|
6470
|
+
const local = ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
|
|
6471
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
|
|
6472
|
+
throw new Error("connect_service_requires_https");
|
|
6473
|
+
}
|
|
6474
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
6475
|
+
throw new Error("invalid_connect_service_url");
|
|
6476
|
+
}
|
|
6477
|
+
if (url.pathname !== "/" && url.pathname !== "") {
|
|
6478
|
+
throw new Error("connect_service_must_be_an_origin");
|
|
6479
|
+
}
|
|
6480
|
+
url.pathname = "/";
|
|
6481
|
+
return url.toString().replace(/\/$/, "");
|
|
6482
|
+
}
|
|
6483
|
+
var normalizeConnectServiceUrl = (value) => secureWebOrigin(value);
|
|
6484
|
+
function transparencyResult(serviceUrl, root, trust) {
|
|
6485
|
+
const normalized = normalizeConnectServiceUrl(serviceUrl);
|
|
6486
|
+
if (root.signed.controlPlaneUrl !== normalized) {
|
|
6487
|
+
throw new Error("key_transparency_origin_mismatch");
|
|
6488
|
+
}
|
|
6489
|
+
return {
|
|
6490
|
+
stored: { root },
|
|
6491
|
+
runtime: { controlPlaneUrl: normalized, trust }
|
|
6492
|
+
};
|
|
6493
|
+
}
|
|
6494
|
+
async function resolveStoredTransparency(state) {
|
|
6495
|
+
const chain = {
|
|
6496
|
+
protocol: KEY_TRANSPARENCY_ROOT_CHAIN_PROTOCOL,
|
|
6497
|
+
roots: [state.keyTransparency.root]
|
|
6498
|
+
};
|
|
6499
|
+
const resolvedProfile = await resolveKeyTransparencyRootChain(chain, state.keyTransparency.root);
|
|
6500
|
+
const resolved = transparencyResult(
|
|
6501
|
+
state.serviceUrl,
|
|
6502
|
+
resolvedProfile.root,
|
|
6503
|
+
resolvedProfile.trust
|
|
6504
|
+
);
|
|
6505
|
+
if (state.status === "connected" && resolvedProfile.root.signed.relayUrl !== state.device.relayUrl)
|
|
6506
|
+
throw new Error("key_transparency_relay_mismatch");
|
|
6507
|
+
return resolved.runtime;
|
|
6508
|
+
}
|
|
6509
|
+
async function readRootChainResponse(response) {
|
|
6510
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
6511
|
+
if (Number.isFinite(declared) && declared > MAX_ROOT_CHAIN_BYTES) {
|
|
6512
|
+
throw new Error("key_transparency_root_chain_too_large");
|
|
6513
|
+
}
|
|
6514
|
+
if (!response.body) throw new Error("empty_key_transparency_root_chain");
|
|
6515
|
+
const reader = response.body.getReader();
|
|
6516
|
+
const decoder = new TextDecoder();
|
|
6517
|
+
let total = 0;
|
|
6518
|
+
let body = "";
|
|
6519
|
+
while (true) {
|
|
6520
|
+
const { done, value } = await reader.read();
|
|
6521
|
+
if (done) break;
|
|
6522
|
+
total += value.byteLength;
|
|
6523
|
+
if (total > MAX_ROOT_CHAIN_BYTES) {
|
|
6524
|
+
await reader.cancel();
|
|
6525
|
+
throw new Error("key_transparency_root_chain_too_large");
|
|
6526
|
+
}
|
|
6527
|
+
body += decoder.decode(value, { stream: true });
|
|
6528
|
+
}
|
|
6529
|
+
body += decoder.decode();
|
|
6530
|
+
try {
|
|
6531
|
+
return parseKeyTransparencyRootChain(JSON.parse(body));
|
|
6532
|
+
} catch {
|
|
6533
|
+
throw new Error("invalid_key_transparency_root_chain_response");
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
async function refreshStoredTransparency(appHome = "", fetcher = fetch) {
|
|
6537
|
+
const state = readConnectState(appHome);
|
|
6538
|
+
if (!state) throw new Error("connect_state_missing");
|
|
6539
|
+
let cached = null;
|
|
6540
|
+
let cachedError = null;
|
|
6541
|
+
try {
|
|
6542
|
+
cached = await resolveStoredTransparency(state);
|
|
6543
|
+
} catch (error) {
|
|
6544
|
+
cachedError = error;
|
|
6545
|
+
}
|
|
6546
|
+
const endpoint = new URL("/api/key-transparency/roots", state.serviceUrl);
|
|
6547
|
+
endpoint.searchParams.set("from", String(state.keyTransparency.root.signed.version));
|
|
6548
|
+
let response;
|
|
6549
|
+
try {
|
|
6550
|
+
response = await fetcher(endpoint, {
|
|
6551
|
+
headers: { accept: "application/json" },
|
|
6552
|
+
redirect: "error",
|
|
6553
|
+
signal: AbortSignal.timeout(5e3)
|
|
6554
|
+
});
|
|
6555
|
+
} catch {
|
|
6556
|
+
if (!cached) throw cachedError;
|
|
6557
|
+
return {
|
|
6558
|
+
state,
|
|
6559
|
+
runtime: cached,
|
|
6560
|
+
updated: false,
|
|
6561
|
+
refreshWarning: "key_transparency_root_refresh_unavailable"
|
|
6562
|
+
};
|
|
6563
|
+
}
|
|
6564
|
+
if (!response.ok) {
|
|
6565
|
+
if (!cached) throw cachedError;
|
|
6566
|
+
return {
|
|
6567
|
+
state,
|
|
6568
|
+
runtime: cached,
|
|
6569
|
+
updated: false,
|
|
6570
|
+
refreshWarning: `key_transparency_root_refresh_http_${response.status}`
|
|
6571
|
+
};
|
|
6572
|
+
}
|
|
6573
|
+
const chain = await readRootChainResponse(response);
|
|
6574
|
+
const latestState = readConnectState(appHome);
|
|
6575
|
+
if (!latestState) throw new Error("connect_state_missing");
|
|
6576
|
+
const resolvedProfile = await resolveKeyTransparencyRootChain(
|
|
6577
|
+
chain,
|
|
6578
|
+
latestState.keyTransparency.root
|
|
6579
|
+
);
|
|
6580
|
+
const resolved = transparencyResult(
|
|
6581
|
+
latestState.serviceUrl,
|
|
6582
|
+
resolvedProfile.root,
|
|
6583
|
+
resolvedProfile.trust
|
|
6584
|
+
);
|
|
6585
|
+
if (latestState.status === "connected" && resolvedProfile.root.signed.relayUrl !== latestState.device.relayUrl)
|
|
6586
|
+
throw new Error("key_transparency_relay_mismatch");
|
|
6587
|
+
const updated = resolvedProfile.root.signed.version > latestState.keyTransparency.root.signed.version;
|
|
6588
|
+
const nextState = updated ? {
|
|
6589
|
+
...latestState,
|
|
6590
|
+
...latestState.status === "connected" ? { updatedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
|
|
6591
|
+
keyTransparency: resolved.stored
|
|
6592
|
+
} : latestState;
|
|
6593
|
+
if (updated) writeConnectState(nextState, appHome);
|
|
6594
|
+
return {
|
|
6595
|
+
state: nextState,
|
|
6596
|
+
runtime: resolved.runtime,
|
|
6597
|
+
updated,
|
|
6598
|
+
refreshWarning: null
|
|
6599
|
+
};
|
|
6600
|
+
}
|
|
6601
|
+
function secureRelayUrl(value) {
|
|
6602
|
+
let url;
|
|
6603
|
+
try {
|
|
6604
|
+
url = new URL(String(value ?? ""));
|
|
6605
|
+
} catch {
|
|
6606
|
+
throw new Error("invalid_relay_url");
|
|
6607
|
+
}
|
|
6608
|
+
const local = ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
|
|
6609
|
+
if (url.protocol !== "wss:" && !(local && url.protocol === "ws:")) {
|
|
6610
|
+
throw new Error("relay_requires_wss");
|
|
6611
|
+
}
|
|
6612
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/v1/connect")
|
|
6613
|
+
throw new Error("invalid_relay_url");
|
|
6614
|
+
return url.toString();
|
|
6615
|
+
}
|
|
6616
|
+
var decodedLength = (value) => {
|
|
6617
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) return -1;
|
|
6618
|
+
try {
|
|
6619
|
+
return Buffer.from(value, "base64url").byteLength;
|
|
6620
|
+
} catch {
|
|
6621
|
+
return -1;
|
|
6622
|
+
}
|
|
6623
|
+
};
|
|
6624
|
+
function validateStoredTransparency(value, serviceUrl) {
|
|
6625
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6626
|
+
throw new Error("invalid_key_transparency_profile");
|
|
6627
|
+
}
|
|
6628
|
+
const profile = value;
|
|
6629
|
+
if (Object.keys(profile).length !== 1 || !Object.hasOwn(profile, "root")) {
|
|
6630
|
+
throw new Error("invalid_key_transparency_profile");
|
|
6631
|
+
}
|
|
6632
|
+
const parsed = parseKeyTransparencyRootChain({
|
|
6633
|
+
protocol: KEY_TRANSPARENCY_ROOT_CHAIN_PROTOCOL,
|
|
6634
|
+
roots: [profile.root]
|
|
6635
|
+
});
|
|
6636
|
+
const root = parsed.roots[0];
|
|
6637
|
+
if (root.signed.controlPlaneUrl !== serviceUrl) {
|
|
6638
|
+
throw new Error("key_transparency_origin_mismatch");
|
|
6639
|
+
}
|
|
6640
|
+
return { root };
|
|
6641
|
+
}
|
|
6642
|
+
function validateAuthorization(value, serviceUrl) {
|
|
6643
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6644
|
+
throw new Error("invalid_device_authorization");
|
|
6645
|
+
}
|
|
6646
|
+
const authorization = value;
|
|
6647
|
+
if (typeof authorization.device_code !== "string" || !authorization.device_code.startsWith("pasco_") || typeof authorization.user_code !== "string" || !/^PASCO-[A-Z0-9-]{8,20}$/.test(authorization.user_code) || !Number.isSafeInteger(authorization.expires_in) || authorization.expires_in < 30 || authorization.expires_in > 3600 || !Number.isSafeInteger(authorization.interval) || authorization.interval < 1 || authorization.interval > 60 || typeof authorization.signing_key_thumbprint !== "string" || decodedLength(authorization.signing_key_thumbprint) !== 32)
|
|
6648
|
+
throw new Error("invalid_device_authorization");
|
|
6649
|
+
const verification = new URL(authorization.verification_uri);
|
|
6650
|
+
if (verification.origin !== new URL(serviceUrl).origin) {
|
|
6651
|
+
throw new Error("verification_origin_mismatch");
|
|
6652
|
+
}
|
|
6653
|
+
return { ...authorization };
|
|
6654
|
+
}
|
|
6655
|
+
function validateAssignment(value) {
|
|
6656
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6657
|
+
throw new Error("invalid_device_assignment");
|
|
6658
|
+
}
|
|
6659
|
+
const assignment = value;
|
|
6660
|
+
if (!UUID_RE.test(String(assignment.deviceId ?? "")) || !OWNER_RE.test(String(assignment.ownerPrefix ?? "")) || !Number.isSafeInteger(assignment.keyVersion) || assignment.keyVersion < 1)
|
|
6661
|
+
throw new Error("invalid_device_assignment");
|
|
6662
|
+
return {
|
|
6663
|
+
deviceId: assignment.deviceId,
|
|
6664
|
+
ownerPrefix: assignment.ownerPrefix,
|
|
6665
|
+
relayUrl: secureRelayUrl(assignment.relayUrl),
|
|
6666
|
+
keyVersion: assignment.keyVersion
|
|
6667
|
+
};
|
|
6668
|
+
}
|
|
6669
|
+
function validateBinding(value) {
|
|
6670
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6671
|
+
throw new Error("invalid_connected_city");
|
|
6672
|
+
}
|
|
6673
|
+
const city = value;
|
|
6674
|
+
const rawDataDir = String(city.dataDir ?? "");
|
|
6675
|
+
const dataDir2 = resolve3(rawDataDir);
|
|
6676
|
+
if (typeof city.localCityId !== "string" || !/^[A-Za-z0-9_-]{4,160}$/.test(city.localCityId) || !OWNER_RE.test(String(city.slug ?? "")) || typeof city.name !== "string" || !city.name.trim() || city.name.length > 100 || !CITY_ADDRESS_RE.test(String(city.remoteAddress ?? "")) || decodedLength(city.encryptionKeyId) !== 32 || typeof city.connected !== "boolean" || !rawDataDir.startsWith("/") || !dataDir2.startsWith("/"))
|
|
6677
|
+
throw new Error("invalid_connected_city");
|
|
6678
|
+
return { ...city, dataDir: dataDir2 };
|
|
6679
|
+
}
|
|
6680
|
+
function validateConnectState(value) {
|
|
6681
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6682
|
+
throw new Error("invalid_connect_state");
|
|
6683
|
+
}
|
|
6684
|
+
const state = value;
|
|
6685
|
+
if (state.protocol !== CONNECT_STATE_PROTOCOL) throw new Error("invalid_connect_state_protocol");
|
|
6686
|
+
const serviceUrl = normalizeConnectServiceUrl(state.serviceUrl);
|
|
6687
|
+
const keyTransparency = validateStoredTransparency(state.keyTransparency, serviceUrl);
|
|
6688
|
+
if (state.status === "pending") {
|
|
6689
|
+
if (typeof state.machineName !== "string" || !state.machineName.trim() || state.machineName.length > 100 || typeof state.createdAt !== "string" || !Number.isFinite(Date.parse(state.createdAt)))
|
|
6690
|
+
throw new Error("invalid_connect_state");
|
|
6691
|
+
return {
|
|
6692
|
+
protocol: CONNECT_STATE_PROTOCOL,
|
|
6693
|
+
status: "pending",
|
|
6694
|
+
serviceUrl,
|
|
6695
|
+
machineName: state.machineName,
|
|
6696
|
+
createdAt: state.createdAt,
|
|
6697
|
+
authorization: validateAuthorization(state.authorization, serviceUrl),
|
|
6698
|
+
keyTransparency
|
|
6699
|
+
};
|
|
6700
|
+
}
|
|
6701
|
+
if (state.status !== "connected" || typeof state.connectedAt !== "string" || !Number.isFinite(Date.parse(state.connectedAt)) || typeof state.updatedAt !== "string" || !Number.isFinite(Date.parse(state.updatedAt)) || !Array.isArray(state.cities) || state.cities.length > 100)
|
|
6702
|
+
throw new Error("invalid_connect_state");
|
|
6703
|
+
const cities = state.cities.map(validateBinding);
|
|
6704
|
+
if (new Set(cities.map((city) => city.localCityId)).size !== cities.length || new Set(cities.map((city) => city.remoteAddress)).size !== cities.length)
|
|
6705
|
+
throw new Error("duplicate_connected_city");
|
|
6706
|
+
const device = validateAssignment(state.device);
|
|
6707
|
+
if (keyTransparency.root.signed.relayUrl !== device.relayUrl) {
|
|
6708
|
+
throw new Error("key_transparency_relay_mismatch");
|
|
6709
|
+
}
|
|
6710
|
+
return {
|
|
6711
|
+
protocol: CONNECT_STATE_PROTOCOL,
|
|
6712
|
+
status: "connected",
|
|
6713
|
+
serviceUrl,
|
|
6714
|
+
connectedAt: state.connectedAt,
|
|
6715
|
+
updatedAt: state.updatedAt,
|
|
6716
|
+
device,
|
|
6717
|
+
keyTransparency,
|
|
6718
|
+
cities
|
|
6719
|
+
};
|
|
6720
|
+
}
|
|
6721
|
+
async function openConnectDeviceVault(appHome = "") {
|
|
6722
|
+
prepareStateDirectory(appHome);
|
|
6723
|
+
await initializeHybridCrypto();
|
|
6724
|
+
return createOsProtectedDeviceVault({
|
|
6725
|
+
directory: connectVaultDirectory(appHome),
|
|
6726
|
+
service: "agents-city-private-device",
|
|
6727
|
+
account: vaultAccount(appHome)
|
|
6728
|
+
});
|
|
6729
|
+
}
|
|
6730
|
+
async function loadConnectIdentity(state, appHome = "") {
|
|
6731
|
+
const vault = await openConnectDeviceVault(appHome);
|
|
6732
|
+
const identity = await vault.loadIdentity();
|
|
6733
|
+
if (!identity) throw new Error("connect_device_identity_missing");
|
|
6734
|
+
if (identity.deviceId !== state.device.deviceId || identity.ownerPrefix !== state.device.ownerPrefix || identity.relayUrl !== state.device.relayUrl || identity.keyVersion !== state.device.keyVersion)
|
|
6735
|
+
throw new Error("connect_device_identity_mismatch");
|
|
6736
|
+
return identity;
|
|
6737
|
+
}
|
|
6738
|
+
function connectedStateForCity(localCityId, appHome = "") {
|
|
6739
|
+
const state = readConnectState(appHome);
|
|
6740
|
+
if (!state || state.status !== "connected") return null;
|
|
6741
|
+
const binding = state.cities.find((city) => city.localCityId === localCityId && city.connected);
|
|
6742
|
+
return binding ? { state, binding } : null;
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6745
|
+
// managed-connect/transport.ts
|
|
6746
|
+
import {
|
|
6747
|
+
ManagedRelaySession,
|
|
6748
|
+
initializeHybridCrypto as initializeHybridCrypto2,
|
|
6749
|
+
signedRelayHeaders
|
|
6750
|
+
} from "./managed-connect-client.js";
|
|
6751
|
+
var CITY_ADDRESS_RE2 = /^[a-z0-9][a-z0-9_-]{0,31}\/[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
6752
|
+
var MAX_SERVER_FRAME_BYTES = 262144;
|
|
6753
|
+
async function openManagedRelaySession(identity, city, options) {
|
|
6754
|
+
if (!CITY_ADDRESS_RE2.test(city)) throw new Error("invalid_city_address");
|
|
6755
|
+
if (!options.keyTransparency) throw new Error("key_transparency_required");
|
|
6756
|
+
await initializeHybridCrypto2();
|
|
6757
|
+
const headers = await signedRelayHeaders(identity, city);
|
|
6758
|
+
const url = new URL(identity.relayUrl);
|
|
6759
|
+
const local = ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
|
|
6760
|
+
if (url.protocol !== "wss:" && !(local && url.protocol === "ws:") || url.pathname !== "/v1/connect" || url.username || url.password || url.search || url.hash)
|
|
6761
|
+
throw new Error("invalid_relay_url");
|
|
6762
|
+
url.searchParams.set("city", city);
|
|
6763
|
+
const socket = new wrapper_default(url, {
|
|
6764
|
+
headers,
|
|
6765
|
+
handshakeTimeout: 1e4,
|
|
6766
|
+
maxPayload: MAX_SERVER_FRAME_BYTES,
|
|
6767
|
+
perMessageDeflate: false,
|
|
6768
|
+
followRedirects: false
|
|
6769
|
+
});
|
|
6770
|
+
socket.on("error", () => {
|
|
6771
|
+
});
|
|
6772
|
+
const transport = {
|
|
6773
|
+
send: (raw) => {
|
|
6774
|
+
if (socket.readyState !== wrapper_default.OPEN) throw new Error("relay_connection_closed");
|
|
6775
|
+
socket.send(raw);
|
|
6776
|
+
},
|
|
6777
|
+
close: (code, reason) => socket.close(code, reason),
|
|
6778
|
+
onMessage: (handler) => {
|
|
6779
|
+
socket.on("message", (raw, isBinary) => {
|
|
6780
|
+
handler(isBinary ? "" : String(raw));
|
|
6781
|
+
});
|
|
6782
|
+
},
|
|
6783
|
+
onClose: (handler) => {
|
|
6784
|
+
socket.on("close", handler);
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
6787
|
+
const session = new ManagedRelaySession(identity, city, transport, {
|
|
6788
|
+
...options,
|
|
6789
|
+
sealedSender: options.sealedSender ?? {}
|
|
6790
|
+
});
|
|
6791
|
+
try {
|
|
6792
|
+
await new Promise((resolve4, reject) => {
|
|
6793
|
+
const timer = setTimeout(() => reject(new Error("relay_connection_timeout")), 1e4);
|
|
6794
|
+
socket.once("open", () => {
|
|
6795
|
+
clearTimeout(timer);
|
|
6796
|
+
resolve4();
|
|
6797
|
+
});
|
|
6798
|
+
socket.once("error", () => {
|
|
6799
|
+
clearTimeout(timer);
|
|
6800
|
+
reject(new Error("relay_connection_failed"));
|
|
6801
|
+
});
|
|
6802
|
+
});
|
|
6803
|
+
await session.ready();
|
|
6804
|
+
return { session, socket };
|
|
6805
|
+
} catch (error) {
|
|
6806
|
+
try {
|
|
6807
|
+
socket.close(1e3, "connection failed");
|
|
6808
|
+
} catch {
|
|
6809
|
+
}
|
|
6810
|
+
throw error;
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
|
|
6814
|
+
// managed-connect/bridge.ts
|
|
6815
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
6816
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
6817
|
+
function managedRoadBridge(context2, receive, receiveManaged) {
|
|
6818
|
+
let session = null;
|
|
6819
|
+
let socket = null;
|
|
6820
|
+
let stopped = false;
|
|
6821
|
+
let connecting = false;
|
|
6822
|
+
let timer = null;
|
|
6823
|
+
let heartbeat = null;
|
|
6824
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
6825
|
+
let cachedRoads = [];
|
|
6826
|
+
let configured = false;
|
|
6827
|
+
let warnedState = false;
|
|
6828
|
+
const stateForCity = () => {
|
|
6829
|
+
try {
|
|
6830
|
+
const found = connectedStateForCity(context2.city.id, context2.appHome);
|
|
6831
|
+
configured = Boolean(found);
|
|
6832
|
+
return found;
|
|
6833
|
+
} catch (error) {
|
|
6834
|
+
configured = false;
|
|
6835
|
+
if (!warnedState) {
|
|
6836
|
+
warnedState = true;
|
|
6837
|
+
console.error(`[city-bus] managed Connect unavailable: ${error.message}`);
|
|
6838
|
+
}
|
|
6839
|
+
return null;
|
|
6840
|
+
}
|
|
6841
|
+
};
|
|
6842
|
+
const updateCache = () => {
|
|
6843
|
+
if (session) {
|
|
6844
|
+
cachedRoads = session.roads().map((road) => ({
|
|
6845
|
+
id: road.id,
|
|
6846
|
+
name: road.peerCity.split("/")[1] || road.peerCity,
|
|
6847
|
+
owner: road.peerCity.split("/")[0] || "remote",
|
|
6848
|
+
address: road.peerCity,
|
|
6849
|
+
local: false,
|
|
6850
|
+
managed: true,
|
|
6851
|
+
revision: road.revision
|
|
6852
|
+
}));
|
|
6853
|
+
}
|
|
6854
|
+
return cachedRoads.map((road) => ({ ...road }));
|
|
6855
|
+
};
|
|
6856
|
+
const schedule = () => {
|
|
6857
|
+
if (stopped || timer) return;
|
|
6858
|
+
timer = setTimeout(() => {
|
|
6859
|
+
timer = null;
|
|
6860
|
+
void connect();
|
|
6861
|
+
}, backoff);
|
|
6862
|
+
timer.unref();
|
|
6863
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
6864
|
+
};
|
|
6865
|
+
const disconnected = () => {
|
|
6866
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
6867
|
+
heartbeat = null;
|
|
6868
|
+
session = null;
|
|
6869
|
+
socket = null;
|
|
6870
|
+
connecting = false;
|
|
6871
|
+
if (!stopped) schedule();
|
|
6872
|
+
};
|
|
6873
|
+
const connect = async () => {
|
|
6874
|
+
if (stopped || connecting || session) return;
|
|
6875
|
+
const found = stateForCity();
|
|
6876
|
+
if (!found) {
|
|
6877
|
+
schedule();
|
|
6878
|
+
return;
|
|
6879
|
+
}
|
|
6880
|
+
connecting = true;
|
|
6881
|
+
try {
|
|
6882
|
+
const refreshed = await refreshStoredTransparency(context2.appHome);
|
|
6883
|
+
if (refreshed.state.status !== "connected") throw new Error("connect_state_not_connected");
|
|
6884
|
+
if (refreshed.refreshWarning && process.env.CITY_BUS_DEBUG === "1") {
|
|
6885
|
+
console.error(`[city-bus] ${refreshed.refreshWarning}; using unexpired cached root`);
|
|
6886
|
+
}
|
|
6887
|
+
const opened = await openManagedRelaySession(
|
|
6888
|
+
await loadConnectIdentity(refreshed.state, context2.appHome),
|
|
6889
|
+
found.binding.remoteAddress,
|
|
6890
|
+
{
|
|
6891
|
+
onText: async (message) => {
|
|
6892
|
+
const envelope = managedEnvelope(context2, message);
|
|
6893
|
+
if (!receiveManaged) throw new Error("managed_reception_receipt_unavailable");
|
|
6894
|
+
const result2 = receiveManaged(envelope);
|
|
6895
|
+
return {
|
|
6896
|
+
messageId: message.messageId,
|
|
6897
|
+
status: result2.inserted ? "inserted" : "duplicate"
|
|
6898
|
+
};
|
|
6899
|
+
},
|
|
6900
|
+
keyTransparency: refreshed.runtime,
|
|
6901
|
+
onSecurityError: (error) => {
|
|
6902
|
+
console.error(`[city-bus] managed Road frame rejected: ${error.message}`);
|
|
6903
|
+
},
|
|
6904
|
+
onLocalError: (error) => {
|
|
6905
|
+
console.error(`[city-bus] managed Road local handoff failed: ${error.message}`);
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
);
|
|
6909
|
+
if (stopped) {
|
|
6910
|
+
opened.session.close();
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
session = opened.session;
|
|
6914
|
+
socket = opened.socket;
|
|
6915
|
+
updateCache();
|
|
6916
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
6917
|
+
warnedState = false;
|
|
6918
|
+
connecting = false;
|
|
6919
|
+
opened.socket.once("close", disconnected);
|
|
6920
|
+
heartbeat = setInterval(() => {
|
|
6921
|
+
try {
|
|
6922
|
+
session?.ping();
|
|
6923
|
+
} catch {
|
|
6924
|
+
}
|
|
6925
|
+
}, 3e4);
|
|
6926
|
+
heartbeat.unref();
|
|
6927
|
+
} catch (error) {
|
|
6928
|
+
connecting = false;
|
|
6929
|
+
if (process.env.CITY_BUS_DEBUG === "1") {
|
|
6930
|
+
console.error(`[city-bus] managed Connect retry: ${error.message}`);
|
|
6931
|
+
}
|
|
6932
|
+
schedule();
|
|
6933
|
+
}
|
|
6934
|
+
};
|
|
6935
|
+
const send = async (to, envelope) => {
|
|
6936
|
+
const active = session;
|
|
6937
|
+
if (!active) throw new Error("the managed Road is not connected");
|
|
6938
|
+
const matches = active.roads().filter((road) => road.peerCity === to);
|
|
6939
|
+
if (matches.length !== 1) {
|
|
6940
|
+
throw new Error(
|
|
6941
|
+
matches.length ? "multiple managed Roads share that address" : "managed Road not available"
|
|
6942
|
+
);
|
|
6943
|
+
}
|
|
6944
|
+
const body = envelope.payload?.text;
|
|
6945
|
+
if (typeof body !== "string") throw new Error("managed Roads carry text only");
|
|
6946
|
+
const result2 = await active.sendRoadText(matches[0].id, body);
|
|
6947
|
+
if (result2.status === "duplicate") return `duplicate already accepted by ${to}`;
|
|
6948
|
+
return `encrypted message durably queued for ${to}`;
|
|
6949
|
+
};
|
|
6950
|
+
const close2 = () => {
|
|
6951
|
+
stopped = true;
|
|
6952
|
+
if (timer) clearTimeout(timer);
|
|
6953
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
6954
|
+
timer = null;
|
|
6955
|
+
heartbeat = null;
|
|
6956
|
+
try {
|
|
6957
|
+
session?.close();
|
|
6958
|
+
} catch {
|
|
6959
|
+
}
|
|
6960
|
+
try {
|
|
6961
|
+
socket?.close();
|
|
6962
|
+
} catch {
|
|
6963
|
+
}
|
|
6964
|
+
session = null;
|
|
6965
|
+
socket = null;
|
|
6966
|
+
};
|
|
6967
|
+
return {
|
|
6968
|
+
start: () => {
|
|
6969
|
+
void connect();
|
|
6970
|
+
},
|
|
6971
|
+
close: close2,
|
|
6972
|
+
send,
|
|
6973
|
+
roads: updateCache,
|
|
6974
|
+
hasRoad: (address) => updateCache().some((road) => road.address === address),
|
|
6975
|
+
enabled: () => configured || Boolean(stateForCity()),
|
|
6976
|
+
online: (address) => Boolean(session) && updateCache().some((road) => road.address === address)
|
|
6977
|
+
};
|
|
6978
|
+
}
|
|
6979
|
+
function managedEnvelope(context2, message) {
|
|
6980
|
+
return {
|
|
6981
|
+
protocol: BUS_PROTOCOL,
|
|
6982
|
+
id: `managed_${message.messageId.replaceAll("-", "")}`,
|
|
6983
|
+
kind: "road.message",
|
|
6984
|
+
scope: "road",
|
|
6985
|
+
thread: null,
|
|
6986
|
+
from: { city: message.from, actor: "seat", role: "external-seat" },
|
|
6987
|
+
to: { city: context2.city.address, actor: "seat" },
|
|
6988
|
+
createdAt: isoNow(),
|
|
6989
|
+
payload: {
|
|
6990
|
+
text: message.text,
|
|
6991
|
+
trust: "information-not-authority",
|
|
6992
|
+
transport: "managed-e2ee",
|
|
6993
|
+
remoteMessageId: message.messageId,
|
|
6994
|
+
roadId: message.roadId
|
|
6995
|
+
}
|
|
6996
|
+
};
|
|
6997
|
+
}
|
|
6998
|
+
|
|
5490
6999
|
// hub/remote-roads.ts
|
|
5491
|
-
function
|
|
7000
|
+
function legacyRemoteRoadBridge(context2, receive) {
|
|
5492
7001
|
const base = process.env.CITY_BUS_URL || "";
|
|
5493
7002
|
const token = process.env.CITY_BUS_TOKEN || "";
|
|
5494
7003
|
const enabled = Boolean(base && token);
|
|
@@ -5558,7 +7067,7 @@ function remoteRoadBridge(context2, receive) {
|
|
|
5558
7067
|
}
|
|
5559
7068
|
if (envelope.scope === "road" && envelope.to.city === context2.city.address) receive(envelope);
|
|
5560
7069
|
};
|
|
5561
|
-
const sendRaw = (to, envelope) => new Promise((
|
|
7070
|
+
const sendRaw = (to, envelope) => new Promise((resolve4, reject) => {
|
|
5562
7071
|
if (!enabled) return reject(new Error("no remote road transport is configured"));
|
|
5563
7072
|
if (!online || !ws || ws.readyState !== wrapper_default.OPEN) {
|
|
5564
7073
|
return reject(new Error("the remote road is not connected"));
|
|
@@ -5576,8 +7085,8 @@ function remoteRoadBridge(context2, receive) {
|
|
|
5576
7085
|
cleanup();
|
|
5577
7086
|
if (message.type === "error")
|
|
5578
7087
|
reject(new Error(String(message.error || "remote road refused the message")));
|
|
5579
|
-
else if (message.type === "queued")
|
|
5580
|
-
else
|
|
7088
|
+
else if (message.type === "queued") resolve4(`${to} is offline: queued remotely`);
|
|
7089
|
+
else resolve4(`delivered remotely to ${to}`);
|
|
5581
7090
|
};
|
|
5582
7091
|
const cleanup = () => {
|
|
5583
7092
|
clearTimeout(timer);
|
|
@@ -5621,6 +7130,30 @@ function remoteRoadBridge(context2, receive) {
|
|
|
5621
7130
|
online: (address) => roster.has(address)
|
|
5622
7131
|
};
|
|
5623
7132
|
}
|
|
7133
|
+
function remoteRoadBridge(context2, receive, receiveManaged) {
|
|
7134
|
+
const legacy = legacyRemoteRoadBridge(context2, (envelope) => {
|
|
7135
|
+
try {
|
|
7136
|
+
receive(envelope);
|
|
7137
|
+
} catch (error) {
|
|
7138
|
+
console.error(`[city-bus] dropped remote envelope: ${error.message}`);
|
|
7139
|
+
}
|
|
7140
|
+
});
|
|
7141
|
+
const managed = managedRoadBridge(context2, receive, receiveManaged);
|
|
7142
|
+
return {
|
|
7143
|
+
start: () => {
|
|
7144
|
+
legacy.start();
|
|
7145
|
+
managed.start();
|
|
7146
|
+
},
|
|
7147
|
+
close: () => {
|
|
7148
|
+
legacy.close();
|
|
7149
|
+
managed.close();
|
|
7150
|
+
},
|
|
7151
|
+
send: (to, envelope) => managed.hasRoad(to) ? managed.send(to, envelope) : legacy.send(to, envelope),
|
|
7152
|
+
enabled: () => legacy.enabled() || managed.enabled(),
|
|
7153
|
+
online: (address) => managed.online(address) || legacy.online(address),
|
|
7154
|
+
roads: managed.roads
|
|
7155
|
+
};
|
|
7156
|
+
}
|
|
5624
7157
|
function legacyEnvelope(context2, from, body, id) {
|
|
5625
7158
|
return {
|
|
5626
7159
|
protocol: BUS_PROTOCOL,
|
|
@@ -5635,14 +7168,413 @@ function legacyEnvelope(context2, from, body, id) {
|
|
|
5635
7168
|
};
|
|
5636
7169
|
}
|
|
5637
7170
|
|
|
7171
|
+
// managed-connect/reception-bridge.ts
|
|
7172
|
+
import {
|
|
7173
|
+
closeSync as closeSync4,
|
|
7174
|
+
existsSync as existsSync8,
|
|
7175
|
+
lstatSync as lstatSync3,
|
|
7176
|
+
openSync as openSync4,
|
|
7177
|
+
readFileSync as readFileSync9,
|
|
7178
|
+
statSync as statSync4,
|
|
7179
|
+
unlinkSync as unlinkSync5,
|
|
7180
|
+
writeFileSync as writeFileSync7
|
|
7181
|
+
} from "node:fs";
|
|
7182
|
+
import { join as join12 } from "node:path";
|
|
7183
|
+
|
|
7184
|
+
// managed-connect/device.ts
|
|
7185
|
+
import {
|
|
7186
|
+
ConnectApiError,
|
|
7187
|
+
beginDeviceAuthorization,
|
|
7188
|
+
listDeviceRoads,
|
|
7189
|
+
pollDeviceAuthorization,
|
|
7190
|
+
signedRelayHeaders as signedRelayHeaders2,
|
|
7191
|
+
syncDeviceCities
|
|
7192
|
+
} from "./managed-connect-client.js";
|
|
7193
|
+
|
|
7194
|
+
// managed-connect/person-message.ts
|
|
7195
|
+
var PERSON_MESSAGE_PROTOCOL = "agents-city-person-message/1";
|
|
7196
|
+
var MAX_PERSON_TEXT_BYTES = 11500;
|
|
7197
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7198
|
+
var textEncoder = new TextEncoder();
|
|
7199
|
+
var textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
7200
|
+
var utf8Length = (value) => textEncoder.encode(value).byteLength;
|
|
7201
|
+
function encodePersonMessage(message) {
|
|
7202
|
+
validateText(message.text);
|
|
7203
|
+
if (!["message", "rejection"].includes(message.kind)) {
|
|
7204
|
+
throw new Error("invalid_person_message_kind");
|
|
7205
|
+
}
|
|
7206
|
+
if (message.inReplyTo !== null && !UUID_RE2.test(message.inReplyTo)) {
|
|
7207
|
+
throw new Error("invalid_person_reply_reference");
|
|
7208
|
+
}
|
|
7209
|
+
return textDecoder.decode(
|
|
7210
|
+
textEncoder.encode(
|
|
7211
|
+
JSON.stringify({
|
|
7212
|
+
protocol: PERSON_MESSAGE_PROTOCOL,
|
|
7213
|
+
kind: message.kind,
|
|
7214
|
+
text: message.text,
|
|
7215
|
+
...message.inReplyTo ? { inReplyTo: message.inReplyTo } : {}
|
|
7216
|
+
})
|
|
7217
|
+
)
|
|
7218
|
+
);
|
|
7219
|
+
}
|
|
7220
|
+
function decodePersonMessage(value) {
|
|
7221
|
+
validateText(value);
|
|
7222
|
+
let parsed;
|
|
7223
|
+
try {
|
|
7224
|
+
parsed = JSON.parse(value);
|
|
7225
|
+
} catch {
|
|
7226
|
+
return { kind: "message", text: value, inReplyTo: null };
|
|
7227
|
+
}
|
|
7228
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7229
|
+
return { kind: "message", text: value, inReplyTo: null };
|
|
7230
|
+
}
|
|
7231
|
+
const record2 = parsed;
|
|
7232
|
+
const allowed = /* @__PURE__ */ new Set(["protocol", "kind", "text", "inReplyTo"]);
|
|
7233
|
+
if (record2.protocol !== PERSON_MESSAGE_PROTOCOL || !["message", "rejection"].includes(String(record2.kind)) || typeof record2.text !== "string" || !record2.text.trim() || utf8Length(record2.text) > MAX_PERSON_TEXT_BYTES || record2.inReplyTo !== void 0 && (typeof record2.inReplyTo !== "string" || !UUID_RE2.test(record2.inReplyTo)) || Object.keys(record2).some((key) => !allowed.has(key))) {
|
|
7234
|
+
return { kind: "message", text: value, inReplyTo: null };
|
|
7235
|
+
}
|
|
7236
|
+
return {
|
|
7237
|
+
kind: record2.kind,
|
|
7238
|
+
text: record2.text,
|
|
7239
|
+
inReplyTo: typeof record2.inReplyTo === "string" ? record2.inReplyTo : null
|
|
7240
|
+
};
|
|
7241
|
+
}
|
|
7242
|
+
function validateText(value) {
|
|
7243
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("person_message_required");
|
|
7244
|
+
if (utf8Length(value) > MAX_PERSON_TEXT_BYTES) throw new Error("person_message_too_large");
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7247
|
+
// managed-connect/reception-bridge.ts
|
|
7248
|
+
var RETRY_START_MS = 1e3;
|
|
7249
|
+
var RETRY_MAX_MS = 3e4;
|
|
7250
|
+
var DIRECTORY_REFRESH_MS = 3e4;
|
|
7251
|
+
var OUTBOX_INTERVAL_MS = 250;
|
|
7252
|
+
function managedReceptionBridge(context2) {
|
|
7253
|
+
let releaseLease = null;
|
|
7254
|
+
let session = null;
|
|
7255
|
+
let socket = null;
|
|
7256
|
+
let stopped = false;
|
|
7257
|
+
let connecting = false;
|
|
7258
|
+
let retryTimer = null;
|
|
7259
|
+
let refreshTimer = null;
|
|
7260
|
+
let outboxTimer = null;
|
|
7261
|
+
let heartbeat = null;
|
|
7262
|
+
let backoff = RETRY_START_MS;
|
|
7263
|
+
let draining = false;
|
|
7264
|
+
let metadata2 = /* @__PURE__ */ new Map();
|
|
7265
|
+
let identity = null;
|
|
7266
|
+
const debug2 = (message) => {
|
|
7267
|
+
if (process.env.CITY_BUS_DEBUG === "1") console.error(`[reception] ${message}`);
|
|
7268
|
+
};
|
|
7269
|
+
const schedule = (delay = backoff) => {
|
|
7270
|
+
if (stopped || retryTimer) return;
|
|
7271
|
+
retryTimer = setTimeout(() => {
|
|
7272
|
+
retryTimer = null;
|
|
7273
|
+
void connect();
|
|
7274
|
+
}, delay);
|
|
7275
|
+
retryTimer.unref();
|
|
7276
|
+
backoff = Math.min(RETRY_MAX_MS, Math.max(RETRY_START_MS, backoff * 2));
|
|
7277
|
+
};
|
|
7278
|
+
const clearConnection = () => {
|
|
7279
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
7280
|
+
heartbeat = null;
|
|
7281
|
+
session = null;
|
|
7282
|
+
socket = null;
|
|
7283
|
+
connecting = false;
|
|
7284
|
+
};
|
|
7285
|
+
const identityFor = async (state) => {
|
|
7286
|
+
if (identity?.deviceId === state.device.deviceId) return identity;
|
|
7287
|
+
identity = await loadConnectIdentity(state, context2.appHome);
|
|
7288
|
+
return identity;
|
|
7289
|
+
};
|
|
7290
|
+
const refreshDirectory = async () => {
|
|
7291
|
+
const state = readConnectState(context2.appHome);
|
|
7292
|
+
if (!state || state.status !== "connected") return [];
|
|
7293
|
+
const activeIdentity = await identityFor(state);
|
|
7294
|
+
const endpoint = receptionEndpoint(activeIdentity.ownerPrefix, activeIdentity.deviceId);
|
|
7295
|
+
const directory = await listDeviceRoads(state.serviceUrl, activeIdentity);
|
|
7296
|
+
const roads2 = directory.roads.filter(
|
|
7297
|
+
(road) => road.kind === "connection" && Boolean(road.connectionId) && road.localCity === endpoint
|
|
7298
|
+
);
|
|
7299
|
+
metadata2 = new Map(roads2.map((road) => [road.id, road]));
|
|
7300
|
+
syncReceptionConnections(
|
|
7301
|
+
context2.appHome,
|
|
7302
|
+
roads2.map((road) => ({
|
|
7303
|
+
roadId: road.id,
|
|
7304
|
+
connectionId: road.connectionId,
|
|
7305
|
+
peerName: road.peerName,
|
|
7306
|
+
peerEndpoint: road.peerCity
|
|
7307
|
+
}))
|
|
7308
|
+
);
|
|
7309
|
+
return roads2;
|
|
7310
|
+
};
|
|
7311
|
+
const connect = async () => {
|
|
7312
|
+
if (stopped || connecting || session) return;
|
|
7313
|
+
const state = readConnectState(context2.appHome);
|
|
7314
|
+
if (!state || state.status !== "connected") return schedule(DIRECTORY_REFRESH_MS);
|
|
7315
|
+
if (!releaseLease) {
|
|
7316
|
+
releaseLease = tryAcquireReceptionLease(context2.appHome);
|
|
7317
|
+
if (!releaseLease) return schedule(5e3);
|
|
7318
|
+
}
|
|
7319
|
+
connecting = true;
|
|
7320
|
+
try {
|
|
7321
|
+
const refreshed = await refreshStoredTransparency(context2.appHome);
|
|
7322
|
+
if (refreshed.state.status !== "connected") throw new Error("connect_state_not_connected");
|
|
7323
|
+
if (refreshed.refreshWarning) {
|
|
7324
|
+
debug2(`${refreshed.refreshWarning}; using unexpired cached root`);
|
|
7325
|
+
}
|
|
7326
|
+
const activeIdentity = await identityFor(refreshed.state);
|
|
7327
|
+
const roads2 = await refreshDirectory();
|
|
7328
|
+
if (!roads2.length) {
|
|
7329
|
+
connecting = false;
|
|
7330
|
+
backoff = RETRY_START_MS;
|
|
7331
|
+
return schedule(DIRECTORY_REFRESH_MS);
|
|
7332
|
+
}
|
|
7333
|
+
const endpoint = receptionEndpoint(activeIdentity.ownerPrefix, activeIdentity.deviceId);
|
|
7334
|
+
const opened = await openManagedRelaySession(activeIdentity, endpoint, {
|
|
7335
|
+
onText: async (message) => {
|
|
7336
|
+
const recorded = recordReceptionMessage(
|
|
7337
|
+
{
|
|
7338
|
+
appHome: context2.appHome,
|
|
7339
|
+
city: { id: `device_${activeIdentity.deviceId}`, address: endpoint }
|
|
7340
|
+
},
|
|
7341
|
+
receptionEnvelope(
|
|
7342
|
+
activeIdentity.deviceId,
|
|
7343
|
+
endpoint,
|
|
7344
|
+
message,
|
|
7345
|
+
metadata2.get(message.roadId)
|
|
7346
|
+
)
|
|
7347
|
+
);
|
|
7348
|
+
return {
|
|
7349
|
+
messageId: message.messageId,
|
|
7350
|
+
status: recorded.inserted ? "inserted" : "duplicate"
|
|
7351
|
+
};
|
|
7352
|
+
},
|
|
7353
|
+
keyTransparency: refreshed.runtime,
|
|
7354
|
+
onSecurityError: (error) => debug2(`security refusal: ${error.message}`),
|
|
7355
|
+
onLocalError: (error) => debug2(`local handoff failed: ${error.message}`)
|
|
7356
|
+
});
|
|
7357
|
+
if (stopped) {
|
|
7358
|
+
opened.session.close();
|
|
7359
|
+
return;
|
|
7360
|
+
}
|
|
7361
|
+
session = opened.session;
|
|
7362
|
+
socket = opened.socket;
|
|
7363
|
+
connecting = false;
|
|
7364
|
+
backoff = RETRY_START_MS;
|
|
7365
|
+
opened.socket.once("close", () => {
|
|
7366
|
+
clearConnection();
|
|
7367
|
+
schedule();
|
|
7368
|
+
});
|
|
7369
|
+
heartbeat = setInterval(() => {
|
|
7370
|
+
try {
|
|
7371
|
+
session?.ping();
|
|
7372
|
+
} catch {
|
|
7373
|
+
}
|
|
7374
|
+
}, 3e4);
|
|
7375
|
+
heartbeat.unref();
|
|
7376
|
+
void drainOutbox();
|
|
7377
|
+
} catch (error) {
|
|
7378
|
+
connecting = false;
|
|
7379
|
+
debug2(`retry: ${error.message}`);
|
|
7380
|
+
schedule();
|
|
7381
|
+
}
|
|
7382
|
+
};
|
|
7383
|
+
const drainOutbox = async () => {
|
|
7384
|
+
if (draining || !session) return;
|
|
7385
|
+
draining = true;
|
|
7386
|
+
try {
|
|
7387
|
+
const rows = pendingReceptionOutbox(context2.appHome);
|
|
7388
|
+
await Promise.all(
|
|
7389
|
+
rows.map(async (row) => {
|
|
7390
|
+
try {
|
|
7391
|
+
const active = session;
|
|
7392
|
+
if (!active || !metadata2.has(row.roadId)) throw new Error("connection_not_available");
|
|
7393
|
+
await active.sendRoadText(
|
|
7394
|
+
row.roadId,
|
|
7395
|
+
encodePersonMessage({
|
|
7396
|
+
kind: row.kind,
|
|
7397
|
+
text: row.body,
|
|
7398
|
+
inReplyTo: row.inReplyTo
|
|
7399
|
+
}),
|
|
7400
|
+
{
|
|
7401
|
+
messageId: row.messageId,
|
|
7402
|
+
onAccepted: () => {
|
|
7403
|
+
markReceptionOutboxSent(context2.appHome, row.messageId);
|
|
7404
|
+
}
|
|
7405
|
+
}
|
|
7406
|
+
);
|
|
7407
|
+
} catch (error) {
|
|
7408
|
+
markReceptionOutboxFailed(context2.appHome, row.messageId, row.attemptCount, error);
|
|
7409
|
+
}
|
|
7410
|
+
})
|
|
7411
|
+
);
|
|
7412
|
+
} finally {
|
|
7413
|
+
draining = false;
|
|
7414
|
+
}
|
|
7415
|
+
};
|
|
7416
|
+
return {
|
|
7417
|
+
start: () => {
|
|
7418
|
+
void connect();
|
|
7419
|
+
refreshTimer = setInterval(() => {
|
|
7420
|
+
if (!releaseLease) {
|
|
7421
|
+
void connect();
|
|
7422
|
+
return;
|
|
7423
|
+
}
|
|
7424
|
+
void refreshDirectory().then((roads2) => {
|
|
7425
|
+
if (roads2.length && !session) void connect();
|
|
7426
|
+
}).catch((error) => debug2(`directory refresh failed: ${error.message}`));
|
|
7427
|
+
}, DIRECTORY_REFRESH_MS);
|
|
7428
|
+
refreshTimer.unref();
|
|
7429
|
+
outboxTimer = setInterval(() => void drainOutbox(), OUTBOX_INTERVAL_MS);
|
|
7430
|
+
outboxTimer.unref();
|
|
7431
|
+
},
|
|
7432
|
+
close: () => {
|
|
7433
|
+
stopped = true;
|
|
7434
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
7435
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
7436
|
+
if (outboxTimer) clearInterval(outboxTimer);
|
|
7437
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
7438
|
+
retryTimer = null;
|
|
7439
|
+
refreshTimer = null;
|
|
7440
|
+
outboxTimer = null;
|
|
7441
|
+
heartbeat = null;
|
|
7442
|
+
try {
|
|
7443
|
+
session?.close();
|
|
7444
|
+
} catch {
|
|
7445
|
+
}
|
|
7446
|
+
try {
|
|
7447
|
+
socket?.close();
|
|
7448
|
+
} catch {
|
|
7449
|
+
}
|
|
7450
|
+
clearConnection();
|
|
7451
|
+
releaseLease?.();
|
|
7452
|
+
releaseLease = null;
|
|
7453
|
+
}
|
|
7454
|
+
};
|
|
7455
|
+
}
|
|
7456
|
+
function receptionEndpoint(ownerPrefix, deviceId) {
|
|
7457
|
+
return `${ownerPrefix}/rx-${deviceId.replaceAll("-", "").slice(0, 12)}`;
|
|
7458
|
+
}
|
|
7459
|
+
function receptionEnvelope(deviceId, endpoint, message, road) {
|
|
7460
|
+
if (!road?.connectionId || road.kind !== "connection") {
|
|
7461
|
+
throw new Error("unknown_connection_road");
|
|
7462
|
+
}
|
|
7463
|
+
const person = decodePersonMessage(message.text);
|
|
7464
|
+
return {
|
|
7465
|
+
protocol: BUS_PROTOCOL,
|
|
7466
|
+
id: `managed_${message.messageId.replaceAll("-", "")}`,
|
|
7467
|
+
kind: "road.message",
|
|
7468
|
+
scope: "road",
|
|
7469
|
+
thread: null,
|
|
7470
|
+
from: { city: message.from, actor: "seat", role: "external-seat" },
|
|
7471
|
+
to: { city: endpoint, actor: "seat" },
|
|
7472
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7473
|
+
payload: {
|
|
7474
|
+
text: person.text,
|
|
7475
|
+
trust: "information-not-authority",
|
|
7476
|
+
transport: "managed-e2ee",
|
|
7477
|
+
messageKind: person.kind,
|
|
7478
|
+
inReplyTo: person.inReplyTo,
|
|
7479
|
+
sourceName: road.peerName,
|
|
7480
|
+
connectionId: road.connectionId,
|
|
7481
|
+
roadId: road.id,
|
|
7482
|
+
remoteMessageId: message.messageId,
|
|
7483
|
+
receiverDeviceId: deviceId
|
|
7484
|
+
}
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7487
|
+
function tryAcquireReceptionLease(appHome) {
|
|
7488
|
+
const lock = join12(connectStateDirectory(appHome), "reception.lock");
|
|
7489
|
+
const owner = process.pid;
|
|
7490
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
7491
|
+
try {
|
|
7492
|
+
const fd = openSync4(lock, "wx", 384);
|
|
7493
|
+
try {
|
|
7494
|
+
writeFileSync7(fd, `${owner}
|
|
7495
|
+
`);
|
|
7496
|
+
} finally {
|
|
7497
|
+
closeSync4(fd);
|
|
7498
|
+
}
|
|
7499
|
+
return () => {
|
|
7500
|
+
if (lockOwner2(lock) !== owner) return;
|
|
7501
|
+
try {
|
|
7502
|
+
unlinkSync5(lock);
|
|
7503
|
+
} catch {
|
|
7504
|
+
}
|
|
7505
|
+
};
|
|
7506
|
+
} catch (error) {
|
|
7507
|
+
if (error.code !== "EEXIST") throw error;
|
|
7508
|
+
const info = lstatSync3(lock);
|
|
7509
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe_reception_lock");
|
|
7510
|
+
const old = lockOwner2(lock);
|
|
7511
|
+
if (old > 0 && processAlive2(old) || !old && Date.now() - statSync4(lock).mtimeMs < 5e3) {
|
|
7512
|
+
return null;
|
|
7513
|
+
}
|
|
7514
|
+
try {
|
|
7515
|
+
unlinkSync5(lock);
|
|
7516
|
+
} catch (unlinkError) {
|
|
7517
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
return null;
|
|
7522
|
+
}
|
|
7523
|
+
function lockOwner2(path) {
|
|
7524
|
+
if (!existsSync8(path)) return 0;
|
|
7525
|
+
try {
|
|
7526
|
+
return Number(readFileSync9(path, "utf8").trim()) || 0;
|
|
7527
|
+
} catch {
|
|
7528
|
+
return 0;
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
function processAlive2(pid) {
|
|
7532
|
+
try {
|
|
7533
|
+
process.kill(pid, 0);
|
|
7534
|
+
return true;
|
|
7535
|
+
} catch {
|
|
7536
|
+
return false;
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
|
|
5638
7540
|
// hub/road-controller.ts
|
|
5639
7541
|
function roadController(context2, router2) {
|
|
5640
|
-
const
|
|
5641
|
-
|
|
7542
|
+
const wakeIntervalMs = duration(
|
|
7543
|
+
process.env.CITY_ROAD_INBOX_WAKE_INTERVAL_MS,
|
|
7544
|
+
5 * 6e4,
|
|
7545
|
+
3e4,
|
|
7546
|
+
60 * 6e4
|
|
7547
|
+
);
|
|
7548
|
+
const wakeCheckMs = Math.min(3e4, wakeIntervalMs);
|
|
7549
|
+
const receptionCheckMs = duration(
|
|
7550
|
+
process.env.CITY_RECEPTION_DELIVERY_INTERVAL_MS,
|
|
7551
|
+
1e3,
|
|
7552
|
+
250,
|
|
7553
|
+
3e4
|
|
7554
|
+
);
|
|
7555
|
+
let wakeTimer = null;
|
|
7556
|
+
let receptionTimer = null;
|
|
7557
|
+
let remote;
|
|
7558
|
+
const reception = managedReceptionBridge(context2);
|
|
7559
|
+
const roads2 = () => {
|
|
7560
|
+
const merged = [...context2.roads, ...remote?.roads() ?? []];
|
|
7561
|
+
return merged.filter(
|
|
7562
|
+
(road, index) => merged.findIndex((candidate) => candidate.address === road.address) === index
|
|
7563
|
+
);
|
|
7564
|
+
};
|
|
7565
|
+
const validateInbound = (envelope) => {
|
|
7566
|
+
const road = roads2().find((candidate) => candidate.address === envelope.from.city);
|
|
5642
7567
|
if (!road) throw new Error(`there is no road from ${envelope.from.city}`);
|
|
5643
7568
|
if (envelope.protocol !== BUS_PROTOCOL || envelope.scope !== "road" || envelope.from.actor !== "seat" || envelope.to.city !== context2.city.address || envelope.to.actor !== "seat") {
|
|
5644
7569
|
throw new Error("invalid road envelope");
|
|
5645
7570
|
}
|
|
7571
|
+
};
|
|
7572
|
+
const inbound = (envelope) => {
|
|
7573
|
+
validateInbound(envelope);
|
|
7574
|
+
if (envelope.payload?.transport === "managed-e2ee") {
|
|
7575
|
+
recordReceptionMessage(context2, envelope);
|
|
7576
|
+
return;
|
|
7577
|
+
}
|
|
5646
7578
|
const rawBody = envelope.payload?.text;
|
|
5647
7579
|
const guarded = typeof rawBody === "string" ? {
|
|
5648
7580
|
...envelope,
|
|
@@ -5652,18 +7584,31 @@ function roadController(context2, router2) {
|
|
|
5652
7584
|
textRaw: void 0
|
|
5653
7585
|
}
|
|
5654
7586
|
} : envelope;
|
|
5655
|
-
recordRoadInbox(context2.runtimeDir, guarded);
|
|
5656
|
-
|
|
7587
|
+
const newlyRecorded = recordRoadInbox(context2.runtimeDir, guarded);
|
|
7588
|
+
notifyBacklog();
|
|
7589
|
+
if (!newlyRecorded) return;
|
|
7590
|
+
markRoadInboxAccepted(context2.runtimeDir, guarded.id);
|
|
5657
7591
|
};
|
|
5658
|
-
const
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
console.error(`[city-bus] dropped remote envelope: ${error.message}`);
|
|
7592
|
+
const inboundManaged = (envelope) => {
|
|
7593
|
+
validateInbound(envelope);
|
|
7594
|
+
if (envelope.payload?.transport !== "managed-e2ee") {
|
|
7595
|
+
throw new Error("managed delivery contains a non-managed envelope");
|
|
5663
7596
|
}
|
|
5664
|
-
|
|
7597
|
+
return recordReceptionMessage(context2, envelope);
|
|
7598
|
+
};
|
|
7599
|
+
remote = remoteRoadBridge(context2, inbound, inboundManaged);
|
|
7600
|
+
const notifyBacklog = () => {
|
|
7601
|
+
const status = roadInboxStatus(context2.runtimeDir);
|
|
7602
|
+
if (!status.pending || Date.now() - status.notifiedAt < wakeIntervalMs) return;
|
|
7603
|
+
router2.internal("road.inbox.ready", "seat", "chair", "seat", null, {
|
|
7604
|
+
pending: status.pending,
|
|
7605
|
+
oldestAt: status.oldestAt,
|
|
7606
|
+
batchSize: ROAD_INBOX_BATCH_SIZE
|
|
7607
|
+
});
|
|
7608
|
+
markRoadInboxNotified(context2.runtimeDir);
|
|
7609
|
+
};
|
|
5665
7610
|
const sendOne = async (to, body) => {
|
|
5666
|
-
const road =
|
|
7611
|
+
const road = roads2().find((candidate) => candidate.address === to);
|
|
5667
7612
|
if (!road) throw new Error(`no road from ${context2.city.address} to ${to}`);
|
|
5668
7613
|
const envelope = {
|
|
5669
7614
|
protocol: BUS_PROTOCOL,
|
|
@@ -5681,7 +7626,7 @@ function roadController(context2, router2) {
|
|
|
5681
7626
|
const command = async (name, payload, actor, role) => {
|
|
5682
7627
|
if (name === "road.roster") {
|
|
5683
7628
|
requireChair(actor, role);
|
|
5684
|
-
return
|
|
7629
|
+
return roads2().map((road) => ({
|
|
5685
7630
|
...road,
|
|
5686
7631
|
online: road.local ? localRoadOnline(context2, road) : remote.online(road.address)
|
|
5687
7632
|
}));
|
|
@@ -5695,12 +7640,49 @@ function roadController(context2, router2) {
|
|
|
5695
7640
|
const to = text(payload.to, "to");
|
|
5696
7641
|
const body = text(payload.text, "text");
|
|
5697
7642
|
if (to !== "*") return { results: [await sendOne(to, body)] };
|
|
5698
|
-
|
|
7643
|
+
const currentRoads = roads2();
|
|
7644
|
+
if (!currentRoads.length) throw new Error("this city has no roads");
|
|
5699
7645
|
const results = [];
|
|
5700
|
-
for (const road of
|
|
7646
|
+
for (const road of currentRoads) results.push(await sendOne(road.address, body));
|
|
5701
7647
|
return { results };
|
|
5702
7648
|
};
|
|
5703
|
-
|
|
7649
|
+
const start = () => {
|
|
7650
|
+
remote.start();
|
|
7651
|
+
reception.start();
|
|
7652
|
+
deliverReception();
|
|
7653
|
+
notifyBacklog();
|
|
7654
|
+
receptionTimer = setInterval(deliverReception, receptionCheckMs);
|
|
7655
|
+
receptionTimer.unref();
|
|
7656
|
+
wakeTimer = setInterval(() => {
|
|
7657
|
+
try {
|
|
7658
|
+
notifyBacklog();
|
|
7659
|
+
} catch (error) {
|
|
7660
|
+
console.error(`[city-bus] Road inbox wake-up failed: ${error.message}`);
|
|
7661
|
+
}
|
|
7662
|
+
}, wakeCheckMs);
|
|
7663
|
+
wakeTimer.unref();
|
|
7664
|
+
};
|
|
7665
|
+
const close2 = () => {
|
|
7666
|
+
if (wakeTimer) clearInterval(wakeTimer);
|
|
7667
|
+
if (receptionTimer) clearInterval(receptionTimer);
|
|
7668
|
+
wakeTimer = null;
|
|
7669
|
+
receptionTimer = null;
|
|
7670
|
+
remote.close();
|
|
7671
|
+
reception.close();
|
|
7672
|
+
};
|
|
7673
|
+
return { command, inbound, start, close: close2 };
|
|
7674
|
+
function deliverReception() {
|
|
7675
|
+
try {
|
|
7676
|
+
const result2 = deliverApprovedReception(context2);
|
|
7677
|
+
if (result2.delivered) notifyBacklog();
|
|
7678
|
+
} catch (error) {
|
|
7679
|
+
console.error(`[city-bus] Reception delivery failed: ${error.message}`);
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
function duration(raw, fallback, minimum, maximum) {
|
|
7684
|
+
const value = Number(raw);
|
|
7685
|
+
return Number.isSafeInteger(value) && value >= minimum && value <= maximum ? value : fallback;
|
|
5704
7686
|
}
|
|
5705
7687
|
|
|
5706
7688
|
// local-hub.ts
|
|
@@ -5827,11 +7809,12 @@ server.listen(0, "127.0.0.1", () => {
|
|
|
5827
7809
|
};
|
|
5828
7810
|
publishEndpoint(context, endpoint);
|
|
5829
7811
|
diagnostics("hub.listening", { outcome: "ready", message: endpoint.url });
|
|
5830
|
-
for (const
|
|
7812
|
+
for (const queued of pendingRoadQueue(context.runtimeDir)) {
|
|
5831
7813
|
try {
|
|
5832
|
-
roads.inbound(envelope);
|
|
7814
|
+
roads.inbound(queued.envelope);
|
|
7815
|
+
acknowledgeRoadQueue(queued.queueFile);
|
|
5833
7816
|
} catch (error) {
|
|
5834
|
-
console.error(`[city-bus]
|
|
7817
|
+
console.error(`[city-bus] kept queued Road envelope: ${error.message}`);
|
|
5835
7818
|
}
|
|
5836
7819
|
}
|
|
5837
7820
|
roads.start();
|
|
@@ -5942,7 +7925,7 @@ function authenticateActor(url, mode) {
|
|
|
5942
7925
|
const actor = url.searchParams.get("actor") || "";
|
|
5943
7926
|
if (!ACTOR_RE.test(actor) || !context.actors[actor]) throw new Error("unknown city actor");
|
|
5944
7927
|
const credential = JSON.parse(
|
|
5945
|
-
|
|
7928
|
+
readFileSync10(credentialPath(context, actor), "utf8")
|
|
5946
7929
|
);
|
|
5947
7930
|
if (!sameSecret(url.searchParams.get("token") || "", credential.token))
|
|
5948
7931
|
throw new Error("invalid actor token");
|