@mirrowel/opencode-openshare 0.2.0 → 0.3.0-dev.1
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/README.md +48 -26
- package/ci/README.md +36 -9
- package/ci/openshare.js +547 -82
- package/dist/binding-queue.js +2 -2
- package/dist/commands.js +18 -15
- package/dist/config.d.ts +34 -15
- package/dist/config.js +127 -35
- package/dist/contract.d.ts +5 -5
- package/dist/contract.js +5 -5
- package/dist/core.d.ts +13 -1
- package/dist/core.js +491 -157
- package/dist/dialogs.d.ts +9 -10
- package/dist/dialogs.js +25 -28
- package/dist/redact.d.ts +46 -0
- package/dist/redact.js +130 -0
- package/dist/settings.d.ts +11 -8
- package/dist/settings.js +233 -91
- package/dist/share-flow.d.ts +89 -9
- package/dist/share-flow.js +172 -22
- package/dist/tui.js +449 -115
- package/dist/ui-prefs.js +6 -6
- package/dist/v2-server.js +5 -6
- package/openshare.config.json +1 -2
- package/package.json +1 -1
- package/src/banner.txt +1 -1
- package/src/binding-queue.ts +2 -2
- package/src/commands.ts +18 -15
- package/src/config.ts +129 -32
- package/src/contract.ts +5 -5
- package/src/core.ts +468 -158
- package/src/dialogs.tsx +29 -32
- package/src/redact.ts +134 -0
- package/src/settings.ts +225 -83
- package/src/share-flow.ts +180 -23
- package/src/sidebar.tsx +7 -7
- package/src/tui.tsx +22 -24
- package/src/ui-prefs.ts +6 -6
- package/src/v2-server.ts +5 -6
package/ci/openshare.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// > code defaults. Keys: servers (multi-server: url -> {token, code,
|
|
20
20
|
// label, active, adminToken}), workerUrl, createToken (legacy pair),
|
|
21
21
|
// shareMode ("all" | "manual"; default = TUI presence), debounceMs,
|
|
22
|
-
// completedDelayMs, modelLabel, ttlDays,
|
|
22
|
+
// completedDelayMs, modelLabel, ttlDays,
|
|
23
23
|
// authLogin, authPassword, spoolDir, urlFile ("none" = off),
|
|
24
24
|
// announceStderr.
|
|
25
25
|
//
|
|
@@ -35,12 +35,13 @@ var DEFAULTS = {
|
|
|
35
35
|
debounceMs: 1e4,
|
|
36
36
|
completedDelayMs: 5000,
|
|
37
37
|
modelLabel: "$Provider/$Model",
|
|
38
|
-
|
|
39
|
-
openTasksInNewTab: true,
|
|
38
|
+
ttlHours: 90 * 24,
|
|
40
39
|
authLogin: "",
|
|
41
40
|
authPassword: "",
|
|
42
41
|
spoolDir: "",
|
|
43
42
|
urlFile: "",
|
|
43
|
+
urlFileFormat: "url",
|
|
44
|
+
configSet: "",
|
|
44
45
|
announceStderr: true
|
|
45
46
|
};
|
|
46
47
|
var CONFIG_NAME = "openshare.config.json";
|
|
@@ -79,6 +80,76 @@ function configPaths(pluginDir, env = process.env) {
|
|
|
79
80
|
function configHomePath(env = process.env) {
|
|
80
81
|
return join(globalConfigDir(env), CONFIG_NAME);
|
|
81
82
|
}
|
|
83
|
+
function configSourceMap(pluginDir, env = process.env) {
|
|
84
|
+
const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
|
|
85
|
+
const known = new Set([...Object.keys(DEFAULTS), "shareMode", "servers"]);
|
|
86
|
+
const sources = {};
|
|
87
|
+
const envKeys = {
|
|
88
|
+
workerUrl: env.OPENSHARE_WORKER_URL,
|
|
89
|
+
createToken: env.OPENSHARE_CREATE_TOKEN,
|
|
90
|
+
debounceMs: env.OPENSHARE_DEBOUNCE_MS,
|
|
91
|
+
completedDelayMs: env.OPENSHARE_COMPLETED_DELAY_MS,
|
|
92
|
+
modelLabel: env.OPENSHARE_MODEL_LABEL,
|
|
93
|
+
ttlHours: env.OPENSHARE_TTL_HOURS,
|
|
94
|
+
ttlDays: env.OPENSHARE_TTL_DAYS,
|
|
95
|
+
authLogin: env.OPENSHARE_AUTH_LOGIN,
|
|
96
|
+
authPassword: env.OPENSHARE_AUTH_PASSWORD,
|
|
97
|
+
urlFile: env.OPENSHARE_URL_FILE,
|
|
98
|
+
urlFileFormat: env.OPENSHARE_URL_FILE_FORMAT,
|
|
99
|
+
configSet: env.OPENSHARE_CONFIG_SET,
|
|
100
|
+
spoolDir: env.OPENSHARE_SPOOL_DIR,
|
|
101
|
+
announceStderr: env.OPENSHARE_ANNOUNCE_STDERR,
|
|
102
|
+
shareMode: env.OPENSHARE_SHARE_MODE
|
|
103
|
+
};
|
|
104
|
+
for (const [key, value] of Object.entries(envKeys)) {
|
|
105
|
+
if (value !== undefined && value !== "")
|
|
106
|
+
sources[key] = "environment";
|
|
107
|
+
}
|
|
108
|
+
if (sources.ttlDays && !sources.ttlHours)
|
|
109
|
+
sources.ttlHours = sources.ttlDays;
|
|
110
|
+
delete sources.ttlDays;
|
|
111
|
+
if (env.OPENSHARE_CONFIG_JSON) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = JSON.parse(env.OPENSHARE_CONFIG_JSON);
|
|
114
|
+
if (raw && typeof raw === "object") {
|
|
115
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
116
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
117
|
+
continue;
|
|
118
|
+
if (sources[key] === "environment")
|
|
119
|
+
continue;
|
|
120
|
+
if (key === "servers" || known.has(key))
|
|
121
|
+
sources[key] = "OPENSHARE_CONFIG_JSON";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
for (const path of configPaths(pluginDir, env)) {
|
|
127
|
+
let raw;
|
|
128
|
+
try {
|
|
129
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
130
|
+
if (!raw || typeof raw !== "object")
|
|
131
|
+
continue;
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const label = basename(path) === LOCAL_NAME ? `openshare.local.json (${dirname(path).split(/[\\/]/).pop()})` : `${basename(path)} (${dirname(path).split(/[\\/]/).pop()})`;
|
|
136
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
137
|
+
if (key === "servers") {
|
|
138
|
+
if (sources.servers === undefined && normalizeServers(value))
|
|
139
|
+
sources.servers = label;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (sources[key] !== undefined || !known.has(key))
|
|
143
|
+
continue;
|
|
144
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
145
|
+
continue;
|
|
146
|
+
if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
|
|
147
|
+
continue;
|
|
148
|
+
sources[key] = label;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return sources;
|
|
152
|
+
}
|
|
82
153
|
function num(value, fallback) {
|
|
83
154
|
const parsed = Number(value);
|
|
84
155
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -105,6 +176,8 @@ function normalizeServers(raw) {
|
|
|
105
176
|
entry.adminToken = e.adminToken;
|
|
106
177
|
if (e.active === true)
|
|
107
178
|
entry.active = true;
|
|
179
|
+
if (e.anonymous === true)
|
|
180
|
+
entry.anonymous = true;
|
|
108
181
|
out[url] = entry;
|
|
109
182
|
}
|
|
110
183
|
return Object.keys(out).length ? out : undefined;
|
|
@@ -156,27 +229,57 @@ function loadConfig(pluginDir, options) {
|
|
|
156
229
|
const cfg = { ...DEFAULTS };
|
|
157
230
|
const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
|
|
158
231
|
const known = new Set([...Object.keys(DEFAULTS), "shareMode"]);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
232
|
+
const applyObject = (raw) => {
|
|
233
|
+
if (!raw || typeof raw !== "object")
|
|
234
|
+
return;
|
|
235
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
236
|
+
if (key === "servers") {
|
|
237
|
+
const servers = normalizeServers(value);
|
|
238
|
+
if (servers)
|
|
239
|
+
cfg.servers = mergeServerGroups(cfg.servers, servers);
|
|
163
240
|
continue;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
continue;
|
|
241
|
+
}
|
|
242
|
+
if (key === "redact") {
|
|
243
|
+
if (value && typeof value === "object" && Array.isArray(value.patterns)) {
|
|
244
|
+
const pats = value.patterns.filter((x) => typeof x === "string" && x.length > 0 && x.length < 500).slice(0, 50);
|
|
245
|
+
cfg.redact = { patterns: pats };
|
|
170
246
|
}
|
|
171
|
-
|
|
172
|
-
continue;
|
|
173
|
-
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
174
|
-
continue;
|
|
175
|
-
if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
|
|
176
|
-
continue;
|
|
177
|
-
cfg[key] = value;
|
|
247
|
+
continue;
|
|
178
248
|
}
|
|
179
|
-
|
|
249
|
+
if (key === "meta") {
|
|
250
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
251
|
+
const clean = {};
|
|
252
|
+
for (const [mk, mv] of Object.entries(value)) {
|
|
253
|
+
if (typeof mv === "string" && mv.length <= 500 && mk.length <= 64)
|
|
254
|
+
clean[mk] = mv;
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(clean).length)
|
|
257
|
+
cfg.meta = { ...cfg.meta, ...clean };
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (key === "ttlDays") {
|
|
262
|
+
const d = Math.floor(Number(value));
|
|
263
|
+
if (Number.isFinite(d) && d > 0)
|
|
264
|
+
cfg.ttlHours = d * 24;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (!known.has(key))
|
|
268
|
+
continue;
|
|
269
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
270
|
+
continue;
|
|
271
|
+
if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
|
|
272
|
+
continue;
|
|
273
|
+
cfg[key] = value;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
for (const path of [...configPaths(pluginDir)].reverse()) {
|
|
277
|
+
try {
|
|
278
|
+
applyObject(JSON.parse(readFileSync(path, "utf8")));
|
|
279
|
+
} catch (e) {
|
|
280
|
+
if (existsSync(path))
|
|
281
|
+
console.error(`[openshare] config file ${path} could not be parsed - ignored:`, e?.message ?? e);
|
|
282
|
+
}
|
|
180
283
|
}
|
|
181
284
|
if (options && typeof options === "object") {
|
|
182
285
|
const serversOpt = normalizeServers(options.servers);
|
|
@@ -195,6 +298,13 @@ function loadConfig(pluginDir, options) {
|
|
|
195
298
|
cfg.shareMode = mode;
|
|
196
299
|
}
|
|
197
300
|
const env = process.env;
|
|
301
|
+
if (env.OPENSHARE_CONFIG_JSON) {
|
|
302
|
+
try {
|
|
303
|
+
applyObject(JSON.parse(env.OPENSHARE_CONFIG_JSON));
|
|
304
|
+
} catch (e) {
|
|
305
|
+
console.error("[openshare] OPENSHARE_CONFIG_JSON could not be parsed - ignored:", e?.message ?? e);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
198
308
|
if (env.OPENSHARE_WORKER_URL)
|
|
199
309
|
cfg.workerUrl = env.OPENSHARE_WORKER_URL;
|
|
200
310
|
if (env.OPENSHARE_CREATE_TOKEN)
|
|
@@ -205,14 +315,20 @@ function loadConfig(pluginDir, options) {
|
|
|
205
315
|
cfg.completedDelayMs = num(env.OPENSHARE_COMPLETED_DELAY_MS, cfg.completedDelayMs);
|
|
206
316
|
if (env.OPENSHARE_MODEL_LABEL !== undefined)
|
|
207
317
|
cfg.modelLabel = env.OPENSHARE_MODEL_LABEL;
|
|
318
|
+
if (env.OPENSHARE_TTL_HOURS)
|
|
319
|
+
cfg.ttlHours = num(env.OPENSHARE_TTL_HOURS, cfg.ttlHours);
|
|
208
320
|
if (env.OPENSHARE_TTL_DAYS)
|
|
209
|
-
cfg.
|
|
321
|
+
cfg.ttlHours = Math.max(1, Math.floor(num(env.OPENSHARE_TTL_DAYS, 90) * 24));
|
|
210
322
|
if (env.OPENSHARE_AUTH_LOGIN !== undefined)
|
|
211
323
|
cfg.authLogin = env.OPENSHARE_AUTH_LOGIN;
|
|
212
324
|
if (env.OPENSHARE_AUTH_PASSWORD !== undefined)
|
|
213
325
|
cfg.authPassword = env.OPENSHARE_AUTH_PASSWORD;
|
|
214
326
|
if (env.OPENSHARE_URL_FILE !== undefined)
|
|
215
327
|
cfg.urlFile = env.OPENSHARE_URL_FILE;
|
|
328
|
+
if (env.OPENSHARE_URL_FILE_FORMAT === "json" || env.OPENSHARE_URL_FILE_FORMAT === "url")
|
|
329
|
+
cfg.urlFileFormat = env.OPENSHARE_URL_FILE_FORMAT;
|
|
330
|
+
if (env.OPENSHARE_CONFIG_SET)
|
|
331
|
+
cfg.configSet = env.OPENSHARE_CONFIG_SET;
|
|
216
332
|
if (env.OPENSHARE_SPOOL_DIR)
|
|
217
333
|
cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
|
|
218
334
|
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
|
|
@@ -221,6 +337,14 @@ function loadConfig(pluginDir, options) {
|
|
|
221
337
|
cfg.shareMode = env.OPENSHARE_SHARE_MODE;
|
|
222
338
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
223
339
|
cfg.urlFile = URL_FILE_NONE;
|
|
340
|
+
if (cfg.workerUrl)
|
|
341
|
+
cfg.workerUrl = cfg.workerUrl.trim().replace(/\/+$/, "");
|
|
342
|
+
if (cfg.servers) {
|
|
343
|
+
const fixed = {};
|
|
344
|
+
for (const [u, e] of Object.entries(cfg.servers))
|
|
345
|
+
fixed[u.trim().replace(/\/+$/, "")] = e;
|
|
346
|
+
cfg.servers = fixed;
|
|
347
|
+
}
|
|
224
348
|
return cfg;
|
|
225
349
|
}
|
|
226
350
|
function spoolDirFor(cfg) {
|
|
@@ -345,8 +469,9 @@ function resolveShareMode(cfg, input) {
|
|
|
345
469
|
|
|
346
470
|
// src/core.ts
|
|
347
471
|
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
472
|
+
import { homedir } from "node:os";
|
|
348
473
|
import { createHash } from "node:crypto";
|
|
349
|
-
import { join as join2 } from "node:path";
|
|
474
|
+
import { isAbsolute, join as join2 } from "node:path";
|
|
350
475
|
|
|
351
476
|
// src/contract.ts
|
|
352
477
|
function messageTombstone(sessionID, messageID) {
|
|
@@ -564,6 +689,109 @@ function queueBindingWrite(sid, fn) {
|
|
|
564
689
|
return run;
|
|
565
690
|
}
|
|
566
691
|
|
|
692
|
+
// src/redact.ts
|
|
693
|
+
var BUILTINS = [
|
|
694
|
+
{ kind: "aws-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
695
|
+
{ kind: "gh-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g },
|
|
696
|
+
{ kind: "gh-token", re: /\bgithub_pat_[A-Za-z0-9_]{22,255}\b/g },
|
|
697
|
+
{ kind: "api-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
698
|
+
{ kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
699
|
+
{ kind: "google-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
700
|
+
{ kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/gi },
|
|
701
|
+
{ kind: "private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
702
|
+
{ kind: "env-secret", re: /(?<![A-Za-z0-9_])(?:export\s+)?[A-Z0-9_]*(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIALS?)[A-Z0-9_]*\s*=\s*\S+/g },
|
|
703
|
+
{ kind: "conn-string", re: /\b[a-z][a-z0-9+.-]*:\/\/[^:@/\s"']+:[^@/\s"']{4,}@/gi }
|
|
704
|
+
];
|
|
705
|
+
function compile(patterns) {
|
|
706
|
+
if (!Array.isArray(patterns))
|
|
707
|
+
return [];
|
|
708
|
+
const out = [];
|
|
709
|
+
for (const p of patterns) {
|
|
710
|
+
if (typeof p !== "string" || !p)
|
|
711
|
+
continue;
|
|
712
|
+
try {
|
|
713
|
+
out.push({ kind: "user", re: new RegExp(p, "g") });
|
|
714
|
+
} catch {}
|
|
715
|
+
}
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
function tildePath(p, home) {
|
|
719
|
+
if (!p || !home || p.length <= home.length)
|
|
720
|
+
return p;
|
|
721
|
+
const norm = (s) => s.replace(/[\\/]+/g, "/").replace(/\/$/, "");
|
|
722
|
+
const np = norm(p);
|
|
723
|
+
const nh = norm(home);
|
|
724
|
+
if (np === nh)
|
|
725
|
+
return "~";
|
|
726
|
+
if (np.startsWith(nh + "/"))
|
|
727
|
+
return "~" + np.slice(nh.length);
|
|
728
|
+
return p;
|
|
729
|
+
}
|
|
730
|
+
var PATH_KEYS = new Set(["file", "path", "filePath", "cwd", "directory", "root"]);
|
|
731
|
+
function redactItem(item, opts) {
|
|
732
|
+
const flat = JSON.stringify(item) ?? "null";
|
|
733
|
+
const needRedact = opts.enabled && opts.rules.some((r) => {
|
|
734
|
+
r.re.lastIndex = 0;
|
|
735
|
+
return r.re.test(flat);
|
|
736
|
+
});
|
|
737
|
+
const needPaths = !!opts.home && flat.includes(opts.home);
|
|
738
|
+
if (!needRedact && !needPaths)
|
|
739
|
+
return { item, hits: [] };
|
|
740
|
+
const clone = JSON.parse(flat);
|
|
741
|
+
const stats = { hits: [], changed: false };
|
|
742
|
+
walk(clone, opts, stats);
|
|
743
|
+
return { item: clone, hits: stats.hits };
|
|
744
|
+
}
|
|
745
|
+
function walk(node, opts, stats, key) {
|
|
746
|
+
if (Array.isArray(node)) {
|
|
747
|
+
for (let i = 0;i < node.length; i++) {
|
|
748
|
+
const v = node[i];
|
|
749
|
+
if (typeof v === "string")
|
|
750
|
+
node[i] = applyString(v, opts, stats, key);
|
|
751
|
+
else
|
|
752
|
+
walk(v, opts, stats, key);
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (node && typeof node === "object") {
|
|
757
|
+
for (const [k, v] of Object.entries(node)) {
|
|
758
|
+
if (typeof v === "string")
|
|
759
|
+
node[k] = applyString(v, opts, stats, k);
|
|
760
|
+
else
|
|
761
|
+
walk(v, opts, stats, k);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function applyString(v, opts, stats, key) {
|
|
766
|
+
let out = v;
|
|
767
|
+
if (opts.enabled) {
|
|
768
|
+
for (const r of opts.rules) {
|
|
769
|
+
r.re.lastIndex = 0;
|
|
770
|
+
out = out.replace(r.re, (full) => {
|
|
771
|
+
if (stats.hits.length < 8 && !stats.hits.some((h) => h.kind === r.kind)) {
|
|
772
|
+
stats.hits.push({ kind: r.kind, sample: full.slice(0, 60) });
|
|
773
|
+
}
|
|
774
|
+
stats.changed = true;
|
|
775
|
+
if (r.kind === "conn-string") {
|
|
776
|
+
const at = full.lastIndexOf("@");
|
|
777
|
+
const colon = full.indexOf(":", full.indexOf("://") + 3) + 1;
|
|
778
|
+
if (at > colon && colon > 0)
|
|
779
|
+
return full.slice(0, colon) + "[redacted:conn-string]" + full.slice(at);
|
|
780
|
+
}
|
|
781
|
+
return `[redacted:${r.kind}]`;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (key && PATH_KEYS.has(key) && opts.home && out.includes(opts.home)) {
|
|
786
|
+
out = tildePath(out, opts.home);
|
|
787
|
+
stats.changed = true;
|
|
788
|
+
}
|
|
789
|
+
return out;
|
|
790
|
+
}
|
|
791
|
+
function activeRules(userPatterns) {
|
|
792
|
+
return [...BUILTINS, ...compile(userPatterns)];
|
|
793
|
+
}
|
|
794
|
+
|
|
567
795
|
// src/core.ts
|
|
568
796
|
var DEFAULT_POLICY = {
|
|
569
797
|
mode: "persistent",
|
|
@@ -616,6 +844,19 @@ function redact(info) {
|
|
|
616
844
|
delete metadata[METADATA_KEY];
|
|
617
845
|
return { ...info, metadata };
|
|
618
846
|
}
|
|
847
|
+
function stripLocalFacts(info) {
|
|
848
|
+
if (!info || typeof info !== "object")
|
|
849
|
+
return info;
|
|
850
|
+
const out = { ...info };
|
|
851
|
+
let changed = false;
|
|
852
|
+
for (const k of ["directory", "worktree"]) {
|
|
853
|
+
if (out[k] !== undefined) {
|
|
854
|
+
delete out[k];
|
|
855
|
+
changed = true;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return changed ? out : info;
|
|
859
|
+
}
|
|
619
860
|
function shareFlag(info) {
|
|
620
861
|
const raw = info?.metadata?.[METADATA_KEY];
|
|
621
862
|
if (!raw || typeof raw !== "object")
|
|
@@ -627,8 +868,10 @@ function shareFlag(info) {
|
|
|
627
868
|
const opts = {};
|
|
628
869
|
if (optsRaw && typeof optsRaw === "object") {
|
|
629
870
|
const o = optsRaw;
|
|
630
|
-
if (typeof o.
|
|
631
|
-
opts.
|
|
871
|
+
if (typeof o.ttlHours === "number" && o.ttlHours > 0)
|
|
872
|
+
opts.ttlHours = Math.floor(o.ttlHours);
|
|
873
|
+
else if (typeof o.ttlDays === "number" && o.ttlDays > 0)
|
|
874
|
+
opts.ttlHours = Math.floor(o.ttlDays * 24);
|
|
632
875
|
if (typeof o.authLogin === "string")
|
|
633
876
|
opts.authLogin = o.authLogin;
|
|
634
877
|
if (typeof o.authPassword === "string")
|
|
@@ -638,6 +881,9 @@ function shareFlag(info) {
|
|
|
638
881
|
}
|
|
639
882
|
return { enabled: true, opts: Object.keys(opts).length ? opts : undefined };
|
|
640
883
|
}
|
|
884
|
+
var liveTargets = [];
|
|
885
|
+
var statsProvider = null;
|
|
886
|
+
var redactionLedger = new Map;
|
|
641
887
|
function createEngine(deps) {
|
|
642
888
|
let cfg = loadConfig(deps.pluginDir, deps.options);
|
|
643
889
|
const mode = resolveShareMode(cfg, deps);
|
|
@@ -669,8 +915,13 @@ function createEngine(deps) {
|
|
|
669
915
|
log(`share mode: ${mode} (directory: ${deps.directory ?? "?"} worktree: ${deps.worktree ?? "?"})`);
|
|
670
916
|
const resolveTargets = () => {
|
|
671
917
|
const merged = {};
|
|
672
|
-
|
|
918
|
+
const hasServers = !!cfg.servers && Object.keys(cfg.servers).length > 0;
|
|
919
|
+
const envWorker = !!process.env.OPENSHARE_WORKER_URL;
|
|
920
|
+
if (cfg.workerUrl && (!hasServers || envWorker))
|
|
673
921
|
merged[cfg.workerUrl] = { token: cfg.createToken || "", active: true };
|
|
922
|
+
if (cfg.workerUrl && hasServers && !envWorker && !cfg.servers[cfg.workerUrl]) {
|
|
923
|
+
log(`legacy workerUrl ${cfg.workerUrl} ignored: a servers group is configured - remove one of the two`);
|
|
924
|
+
}
|
|
674
925
|
if (cfg.servers)
|
|
675
926
|
for (const [u, e] of Object.entries(cfg.servers))
|
|
676
927
|
merged[u] = { ...merged[u], ...e };
|
|
@@ -680,6 +931,7 @@ function createEngine(deps) {
|
|
|
680
931
|
return chosen.map((url) => ({ url, entry: merged[url] }));
|
|
681
932
|
};
|
|
682
933
|
let targets = resolveTargets();
|
|
934
|
+
liveTargets = targets.map((t) => t.url);
|
|
683
935
|
const credState = new Map;
|
|
684
936
|
const credOf = (url) => {
|
|
685
937
|
let s = credState.get(url);
|
|
@@ -704,40 +956,98 @@ function createEngine(deps) {
|
|
|
704
956
|
};
|
|
705
957
|
seedCredentials();
|
|
706
958
|
const ENROLL_RETRY_MS = 60000;
|
|
959
|
+
const setsFetched = new Set;
|
|
960
|
+
const adoptServerSuggestions = async (url, token) => {
|
|
961
|
+
if (setsFetched.has(url))
|
|
962
|
+
return;
|
|
963
|
+
setsFetched.add(url);
|
|
964
|
+
try {
|
|
965
|
+
const r = await fetch(`${url}/api/token/sets`, {
|
|
966
|
+
headers: { authorization: `Bearer ${token}` },
|
|
967
|
+
signal: AbortSignal.timeout(8000)
|
|
968
|
+
});
|
|
969
|
+
const body = r.ok ? await r.json().catch(() => null) : null;
|
|
970
|
+
const sets = body?.sets ?? {};
|
|
971
|
+
const pick = sets[cfg.configSet ?? ""] ?? Object.values(sets)[0];
|
|
972
|
+
if (!pick)
|
|
973
|
+
return;
|
|
974
|
+
const wantModel = typeof pick.modelLabel === "string" && !pick.modelLabel ? undefined : pick.modelLabel;
|
|
975
|
+
const wantMode = pick.shareMode === "all" || pick.shareMode === "manual" ? pick.shareMode : undefined;
|
|
976
|
+
const src = configSourceMap(deps.pluginDir ?? "");
|
|
977
|
+
const modelUnset = cfg.modelLabel === DEFAULTS.modelLabel && !src.modelLabel;
|
|
978
|
+
const modeUnset = cfg.shareMode === undefined && !src.shareMode;
|
|
979
|
+
const adoptModel = wantModel && modelUnset;
|
|
980
|
+
const adoptMode = wantMode && modeUnset;
|
|
981
|
+
if (!adoptModel && !adoptMode)
|
|
982
|
+
return;
|
|
983
|
+
const home = configHomePath();
|
|
984
|
+
if (!home)
|
|
985
|
+
return;
|
|
986
|
+
let doc = {};
|
|
987
|
+
try {
|
|
988
|
+
doc = JSON.parse(readFileSync2(home, "utf8"));
|
|
989
|
+
if (!doc || typeof doc !== "object")
|
|
990
|
+
doc = {};
|
|
991
|
+
} catch {}
|
|
992
|
+
let dirty = false;
|
|
993
|
+
if (adoptModel && doc.modelLabel === undefined) {
|
|
994
|
+
doc.modelLabel = wantModel;
|
|
995
|
+
dirty = true;
|
|
996
|
+
log(`${url}: adopted modelLabel from the server config set`);
|
|
997
|
+
}
|
|
998
|
+
if (adoptMode && doc.shareMode === undefined) {
|
|
999
|
+
doc.shareMode = wantMode;
|
|
1000
|
+
dirty = true;
|
|
1001
|
+
log(`${url}: adopted shareMode from the server config set`);
|
|
1002
|
+
}
|
|
1003
|
+
if (dirty)
|
|
1004
|
+
writeFileSync2(home, JSON.stringify(doc, null, 2) + `
|
|
1005
|
+
`);
|
|
1006
|
+
} catch {}
|
|
1007
|
+
};
|
|
707
1008
|
const ensureCredential = async (url) => {
|
|
708
1009
|
const c = credOf(url);
|
|
709
1010
|
const target = targets.find((t) => t.url === url);
|
|
710
|
-
if (c.credential)
|
|
1011
|
+
if (c.credential) {
|
|
1012
|
+
adoptServerSuggestions(url, c.credential);
|
|
711
1013
|
return c.credential;
|
|
1014
|
+
}
|
|
712
1015
|
if (Date.now() - c.lastEnrollAt < ENROLL_RETRY_MS)
|
|
713
1016
|
return "";
|
|
714
1017
|
c.lastEnrollAt = Date.now();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const r = await fetch(`${url}/api/enroll`, {
|
|
722
|
-
method: "POST",
|
|
723
|
-
headers: { "content-type": "application/json" },
|
|
724
|
-
body: JSON.stringify({ code }),
|
|
725
|
-
signal: AbortSignal.timeout(1e4)
|
|
726
|
-
});
|
|
727
|
-
const b = r.ok ? await r.json().catch(() => null) : null;
|
|
728
|
-
if (b?.ok && typeof b.token === "string") {
|
|
729
|
-
c.credential = b.token;
|
|
1018
|
+
const code = target?.entry.code ?? "";
|
|
1019
|
+
if (!code) {
|
|
1020
|
+
try {
|
|
1021
|
+
const h = await fetch(`${url}/health`, { signal: AbortSignal.timeout(8000) });
|
|
1022
|
+
const hb = h.ok ? await h.json().catch(() => null) : null;
|
|
1023
|
+
if (hb?.auth?.mode === "public") {
|
|
730
1024
|
c.error = "";
|
|
731
|
-
|
|
732
|
-
log(`enrolled at ${url} (user ${b.userId} "${b.label}")${savedTo ? " - token saved" : ""}`);
|
|
733
|
-
return c.credential;
|
|
1025
|
+
return "";
|
|
734
1026
|
}
|
|
735
|
-
c.error =
|
|
736
|
-
}
|
|
737
|
-
c.error =
|
|
738
|
-
} else {
|
|
739
|
-
c.error = `server health unreachable (HTTP ${h.status})`;
|
|
1027
|
+
c.error = "private server - onboard via /openshare (needs an access code)";
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
c.error = `server unreachable: ${e?.message || e}`;
|
|
740
1030
|
}
|
|
1031
|
+
logError(`no credential for ${url}: ${c.error}`);
|
|
1032
|
+
return "";
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
const r = await fetch(`${url}/api/enroll`, {
|
|
1036
|
+
method: "POST",
|
|
1037
|
+
headers: { "content-type": "application/json" },
|
|
1038
|
+
body: JSON.stringify({ code }),
|
|
1039
|
+
signal: AbortSignal.timeout(1e4)
|
|
1040
|
+
});
|
|
1041
|
+
const b = r.ok ? await r.json().catch(() => null) : null;
|
|
1042
|
+
if (b?.ok && typeof b.token === "string") {
|
|
1043
|
+
c.credential = b.token;
|
|
1044
|
+
c.error = "";
|
|
1045
|
+
const savedTo = saveServerToken(url, c.credential);
|
|
1046
|
+
log(`enrolled at ${url} (user ${b.userId} "${b.label}")${savedTo ? " - token saved" : ""}`);
|
|
1047
|
+
adoptServerSuggestions(url, c.credential);
|
|
1048
|
+
return c.credential;
|
|
1049
|
+
}
|
|
1050
|
+
c.error = `enroll refused: ${b?.error ?? `HTTP ${r.status}`}`;
|
|
741
1051
|
} catch (e) {
|
|
742
1052
|
c.error = `enrollment failed: ${e?.message || e}`;
|
|
743
1053
|
}
|
|
@@ -853,7 +1163,8 @@ function createEngine(deps) {
|
|
|
853
1163
|
badSignal: false,
|
|
854
1164
|
lastSentStatus: null,
|
|
855
1165
|
closed: false,
|
|
856
|
-
|
|
1166
|
+
lastError: null,
|
|
1167
|
+
ttlClamped: null,
|
|
857
1168
|
binding: null,
|
|
858
1169
|
bindingQueried: false,
|
|
859
1170
|
optOuts: new Set,
|
|
@@ -872,6 +1183,7 @@ function createEngine(deps) {
|
|
|
872
1183
|
spoolRoot = spoolDirFor(cfg);
|
|
873
1184
|
const before = new Set(targets.map((t) => t.url));
|
|
874
1185
|
targets = resolveTargets();
|
|
1186
|
+
liveTargets = targets.map((t) => t.url);
|
|
875
1187
|
seedCredentials();
|
|
876
1188
|
clampToPolicy();
|
|
877
1189
|
if (debugEvents) {
|
|
@@ -1045,6 +1357,10 @@ function createEngine(deps) {
|
|
|
1045
1357
|
const value = { v: 2, servers, enabled: true };
|
|
1046
1358
|
if (mode === "manual" && st.opts)
|
|
1047
1359
|
value.opts = st.opts;
|
|
1360
|
+
if (st.lastError)
|
|
1361
|
+
value.lastError = st.lastError;
|
|
1362
|
+
if (st.ttlClamped)
|
|
1363
|
+
value.ttlClamped = st.ttlClamped;
|
|
1048
1364
|
st.binding = value;
|
|
1049
1365
|
st.wroteBinding = true;
|
|
1050
1366
|
await writeMetadata(st, value);
|
|
@@ -1225,24 +1541,72 @@ function createEngine(deps) {
|
|
|
1225
1541
|
if (cfg.announceStderr)
|
|
1226
1542
|
log(`live: ${url}`);
|
|
1227
1543
|
if (mode === "all" && cfg.urlFile !== URL_FILE_NONE) {
|
|
1228
|
-
const file = cfg.urlFile
|
|
1544
|
+
const file = cfg.urlFile ? isAbsolute(cfg.urlFile) ? cfg.urlFile : join2(deps.directory ?? process.cwd(), cfg.urlFile) : join2(deps.directory ?? process.cwd(), "openshare.urls.txt");
|
|
1229
1545
|
try {
|
|
1230
|
-
|
|
1231
|
-
|
|
1546
|
+
const line = cfg.urlFileFormat === "json" ? JSON.stringify({
|
|
1547
|
+
id: r.shareId,
|
|
1548
|
+
url,
|
|
1549
|
+
status: st.lastSentStatus ?? "waiting",
|
|
1550
|
+
gated: (st.opts?.authLogin ?? cfg.authLogin ?? "") !== ""
|
|
1551
|
+
}) + `
|
|
1552
|
+
` : url + `
|
|
1553
|
+
`;
|
|
1554
|
+
appendFileSync(file, line);
|
|
1232
1555
|
if (cfg.announceStderr && (file !== lastUrlFile || !urlFileAnnounced)) {
|
|
1233
1556
|
urlFileAnnounced = true;
|
|
1234
1557
|
lastUrlFile = file;
|
|
1235
1558
|
log(`url file: ${file}`);
|
|
1236
1559
|
}
|
|
1237
|
-
} catch {
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
logError(`url file write failed (${file}):`, e?.message || e);
|
|
1562
|
+
}
|
|
1238
1563
|
}
|
|
1239
1564
|
};
|
|
1565
|
+
let halfAuthWarned = false;
|
|
1240
1566
|
const authFrame = (st) => {
|
|
1241
1567
|
const login = st.opts?.authLogin ?? cfg.authLogin ?? "";
|
|
1242
1568
|
const password = st.opts?.authPassword ?? cfg.authPassword ?? "";
|
|
1569
|
+
if (login === "" !== (password === "")) {
|
|
1570
|
+
if (!halfAuthWarned) {
|
|
1571
|
+
halfAuthWarned = true;
|
|
1572
|
+
logError("read auth needs BOTH a login and a password - gate disabled until fixed");
|
|
1573
|
+
}
|
|
1574
|
+
return { authLogin: "", authPassword: "" };
|
|
1575
|
+
}
|
|
1243
1576
|
return { authLogin: login, authPassword: password };
|
|
1244
1577
|
};
|
|
1245
|
-
const ttlFor = (st) => st.opts?.
|
|
1578
|
+
const ttlFor = (st) => st.opts?.ttlHours ?? cfg.ttlHours;
|
|
1579
|
+
const metaFor = () => {
|
|
1580
|
+
if (!cfg.meta || typeof cfg.meta !== "object")
|
|
1581
|
+
return;
|
|
1582
|
+
const out = {};
|
|
1583
|
+
let n = 0;
|
|
1584
|
+
for (const [k, v] of Object.entries(cfg.meta)) {
|
|
1585
|
+
if (n >= 20)
|
|
1586
|
+
break;
|
|
1587
|
+
out[k] = v.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (m, name) => process.env[name] ?? m);
|
|
1588
|
+
n++;
|
|
1589
|
+
}
|
|
1590
|
+
return Object.keys(out).length ? out : undefined;
|
|
1591
|
+
};
|
|
1592
|
+
const applyOpts = (st, opts) => {
|
|
1593
|
+
if (!opts)
|
|
1594
|
+
return;
|
|
1595
|
+
const prev = st.opts ? JSON.stringify(st.opts) : "";
|
|
1596
|
+
st.opts = opts;
|
|
1597
|
+
if (prev === JSON.stringify(opts))
|
|
1598
|
+
return;
|
|
1599
|
+
for (const [url, r] of st.remotes) {
|
|
1600
|
+
if (!r.shareId || !r.secret || !r.wsReady)
|
|
1601
|
+
continue;
|
|
1602
|
+
const attach = { id: r.shareId, secret: r.secret, ...authFrame(st) };
|
|
1603
|
+
if (st.opts?.ttlHours)
|
|
1604
|
+
attach.ttlHours = st.opts.ttlHours;
|
|
1605
|
+
const sent = wsSend(r, { op: "init", attach });
|
|
1606
|
+
if (sent)
|
|
1607
|
+
log(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlHours ? ` (ttl ${st.opts.ttlHours}h)` : ""}`);
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1246
1610
|
const deleteAbandoned = (r) => {
|
|
1247
1611
|
const id = r.shareId;
|
|
1248
1612
|
const secret = r.secret;
|
|
@@ -1326,6 +1690,25 @@ function createEngine(deps) {
|
|
|
1326
1690
|
r.announced = false;
|
|
1327
1691
|
queueConnect(st, r);
|
|
1328
1692
|
}
|
|
1693
|
+
let rearmed = false;
|
|
1694
|
+
for (const [url, e] of Object.entries(servers)) {
|
|
1695
|
+
if (st.optOuts.has(url) || st.remotes.has(url))
|
|
1696
|
+
continue;
|
|
1697
|
+
const ee = e;
|
|
1698
|
+
if (typeof ee.id !== "string" || typeof ee.secret !== "string" || !ee.id)
|
|
1699
|
+
continue;
|
|
1700
|
+
const r = remoteFor(st, url);
|
|
1701
|
+
r.shareId = ee.id;
|
|
1702
|
+
r.secret = ee.secret;
|
|
1703
|
+
r.ackedRev = typeof ee.rev === "number" ? ee.rev : 0;
|
|
1704
|
+
log(`re-share: re-arming ${url.slice(0, 24)} on share ${ee.id}`);
|
|
1705
|
+
queueConnect(st, r);
|
|
1706
|
+
rearmed = true;
|
|
1707
|
+
}
|
|
1708
|
+
if (rearmed) {
|
|
1709
|
+
st.sentHash.clear();
|
|
1710
|
+
chain = chain.then(() => backfill(st)).catch((e) => logError("re-share backfill error", e?.message || e));
|
|
1711
|
+
}
|
|
1329
1712
|
} catch {}
|
|
1330
1713
|
};
|
|
1331
1714
|
const connect = async (st, r) => {
|
|
@@ -1417,7 +1800,7 @@ function createEngine(deps) {
|
|
|
1417
1800
|
if (r.initWasAttach) {
|
|
1418
1801
|
wsSend(r, { op: "init", attach: { id: r.shareId, secret: r.secret, ...authFrame(st) } });
|
|
1419
1802
|
} else {
|
|
1420
|
-
wsSend(r, { op: "init", create: { sessionID: st.sid,
|
|
1803
|
+
wsSend(r, { op: "init", create: { sessionID: st.sid, ttlHours: ttlFor(st), ...authFrame(st), ...cfg.configSet ? { set: cfg.configSet } : {}, ...metaFor() ? { meta: metaFor() } : {} } });
|
|
1421
1804
|
}
|
|
1422
1805
|
r.initTimer = setTimeout(() => {
|
|
1423
1806
|
if (seq !== r.socketSeq)
|
|
@@ -1534,6 +1917,8 @@ function createEngine(deps) {
|
|
|
1534
1917
|
const err = String(frame.error ?? "");
|
|
1535
1918
|
if (/frozen/i.test(err)) {
|
|
1536
1919
|
r.authBlocked = true;
|
|
1920
|
+
st.lastError = `share frozen by the hoster (owner frozen) - contact the server admin`;
|
|
1921
|
+
scheduleBindingWrite(st);
|
|
1537
1922
|
logError(`${r.url}: share frozen by the hoster (owner frozen) - edits refused; contact the server admin`);
|
|
1538
1923
|
try {
|
|
1539
1924
|
r.ws?.close(4000, "frozen");
|
|
@@ -1568,6 +1953,8 @@ function createEngine(deps) {
|
|
|
1568
1953
|
return;
|
|
1569
1954
|
}
|
|
1570
1955
|
if (/limit|too many|rate/i.test(err)) {
|
|
1956
|
+
st.lastError = `create refused: ${frame.error ?? "rate limited"}`;
|
|
1957
|
+
scheduleBindingWrite(st);
|
|
1571
1958
|
logError(`${r.url}: create refused:`, frame.error, "- retrying with backoff");
|
|
1572
1959
|
r.connecting = false;
|
|
1573
1960
|
try {
|
|
@@ -1578,11 +1965,13 @@ function createEngine(deps) {
|
|
|
1578
1965
|
return;
|
|
1579
1966
|
}
|
|
1580
1967
|
logError(`${r.url}: init refused:`, frame.error, "- creating fresh share");
|
|
1968
|
+
st.lastError = `init refused: ${frame.error ?? "unknown"}`;
|
|
1969
|
+
scheduleBindingWrite(st);
|
|
1581
1970
|
deleteAbandoned(r);
|
|
1582
1971
|
r.shareId = null;
|
|
1583
1972
|
r.secret = null;
|
|
1584
1973
|
r.ackedRev = 0;
|
|
1585
|
-
wsSend(r, { op: "init", create: { sessionID: st.sid,
|
|
1974
|
+
wsSend(r, { op: "init", create: { sessionID: st.sid, ttlHours: ttlFor(st), ...authFrame(st), ...cfg.configSet ? { set: cfg.configSet } : {}, ...metaFor() ? { meta: metaFor() } : {} } });
|
|
1586
1975
|
return;
|
|
1587
1976
|
}
|
|
1588
1977
|
if (r.initWasAttach && typeof frame.rev === "number" && frame.rev < r.ackedRev) {
|
|
@@ -1591,7 +1980,7 @@ function createEngine(deps) {
|
|
|
1591
1980
|
r.shareId = null;
|
|
1592
1981
|
r.secret = null;
|
|
1593
1982
|
r.ackedRev = 0;
|
|
1594
|
-
wsSend(r, { op: "init", create: { sessionID: st.sid,
|
|
1983
|
+
wsSend(r, { op: "init", create: { sessionID: st.sid, ttlHours: ttlFor(st), ...authFrame(st), ...cfg.configSet ? { set: cfg.configSet } : {}, ...metaFor() ? { meta: metaFor() } : {} } });
|
|
1595
1984
|
return;
|
|
1596
1985
|
}
|
|
1597
1986
|
const wasFanoutCreate = !r.initWasAttach && st.everConnected;
|
|
@@ -1623,6 +2012,41 @@ function createEngine(deps) {
|
|
|
1623
2012
|
applyPolicy(frame.policy, r.url);
|
|
1624
2013
|
r.intentionalClose = false;
|
|
1625
2014
|
touch(st);
|
|
2015
|
+
const ackTtl = typeof frame.ttlHours === "number" ? frame.ttlHours : typeof frame.ttlDays === "number" ? frame.ttlDays * 24 : undefined;
|
|
2016
|
+
if (typeof ackTtl === "number") {
|
|
2017
|
+
const wanted = ttlFor(st);
|
|
2018
|
+
const clamp = wanted != null && ackTtl !== wanted ? { requested: wanted, effective: ackTtl } : null;
|
|
2019
|
+
if (JSON.stringify(clamp) !== JSON.stringify(st.ttlClamped)) {
|
|
2020
|
+
st.ttlClamped = clamp;
|
|
2021
|
+
scheduleBindingWrite(st);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (st.lastError) {
|
|
2025
|
+
st.lastError = null;
|
|
2026
|
+
scheduleBindingWrite(st);
|
|
2027
|
+
}
|
|
2028
|
+
const wantedGate = (st.opts?.authLogin ?? cfg.authLogin ?? "") !== "" && (st.opts?.authPassword ?? cfg.authPassword ?? "") !== "";
|
|
2029
|
+
if (wantedGate && frame.auth === false && !r.warnedNoGate) {
|
|
2030
|
+
r.warnedNoGate = true;
|
|
2031
|
+
logError(`${r.url}: read auth requested but the page has NO gate - check the server's auth handling`);
|
|
2032
|
+
} else if (frame.auth === true) {
|
|
2033
|
+
r.warnedNoGate = false;
|
|
2034
|
+
}
|
|
2035
|
+
if (credOf(r.url).credential) {
|
|
2036
|
+
const priv = {};
|
|
2037
|
+
const dir = deps.directory ?? "";
|
|
2038
|
+
if (dir)
|
|
2039
|
+
priv.directory = dir;
|
|
2040
|
+
if (deps.worktree)
|
|
2041
|
+
priv.worktree = String(deps.worktree);
|
|
2042
|
+
try {
|
|
2043
|
+
priv.home = homedir();
|
|
2044
|
+
} catch {}
|
|
2045
|
+
if (Object.keys(priv).length) {
|
|
2046
|
+
const seq = ++nextSeq;
|
|
2047
|
+
wsSend(r, { op: "sync", items: [{ type: "pmeta", sessionID: st.sid, data: priv }], status: undefined, seq });
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1626
2050
|
announceUrl(st, r);
|
|
1627
2051
|
if (!st.wroteBinding) {
|
|
1628
2052
|
chain = chain.then(() => writeBinding(st)).catch(() => {});
|
|
@@ -1833,6 +2257,15 @@ function createEngine(deps) {
|
|
|
1833
2257
|
}).catch((e) => logError("flush error", e?.message || e));
|
|
1834
2258
|
}, wait);
|
|
1835
2259
|
};
|
|
2260
|
+
const homeDir = (() => {
|
|
2261
|
+
try {
|
|
2262
|
+
return homedir();
|
|
2263
|
+
} catch {
|
|
2264
|
+
return "";
|
|
2265
|
+
}
|
|
2266
|
+
})();
|
|
2267
|
+
const userPatterns = cfg.redact?.patterns;
|
|
2268
|
+
const redactionRules = activeRules(userPatterns);
|
|
1836
2269
|
const enqueue = (st, entry) => {
|
|
1837
2270
|
if (!st || !entry)
|
|
1838
2271
|
return;
|
|
@@ -1849,6 +2282,22 @@ function createEngine(deps) {
|
|
|
1849
2282
|
}
|
|
1850
2283
|
if (h)
|
|
1851
2284
|
st.sentHash.set(entry.key, h);
|
|
2285
|
+
{
|
|
2286
|
+
const gate = authFrame(st);
|
|
2287
|
+
const gated = gate.authLogin !== "" && gate.authPassword !== "";
|
|
2288
|
+
const pass = redactItem(entry.item, { rules: redactionRules, home: homeDir, enabled: !gated });
|
|
2289
|
+
if (pass.item !== entry.item)
|
|
2290
|
+
entry = { ...entry, item: pass.item };
|
|
2291
|
+
if (pass.hits.length) {
|
|
2292
|
+
const led = redactionLedger.get(st.sid) ?? { count: 0, samples: [] };
|
|
2293
|
+
led.count += pass.hits.length;
|
|
2294
|
+
for (const hit of pass.hits) {
|
|
2295
|
+
if (led.samples.length < 5 && !led.samples.some((x) => x.kind === hit.kind))
|
|
2296
|
+
led.samples.push(hit);
|
|
2297
|
+
}
|
|
2298
|
+
redactionLedger.set(st.sid, led);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
1852
2301
|
st.queue.set(entry.key, entry.item);
|
|
1853
2302
|
indexMessage(entry);
|
|
1854
2303
|
if (debugEvents)
|
|
@@ -1857,6 +2306,24 @@ function createEngine(deps) {
|
|
|
1857
2306
|
ensureConnected(st);
|
|
1858
2307
|
scheduleFlush(st);
|
|
1859
2308
|
};
|
|
2309
|
+
statsProvider = (sid) => {
|
|
2310
|
+
const st = sessions.get(sid);
|
|
2311
|
+
let messages = 0;
|
|
2312
|
+
let toolCalls = 0;
|
|
2313
|
+
if (st) {
|
|
2314
|
+
for (const [, merged] of st.merged) {
|
|
2315
|
+
messages++;
|
|
2316
|
+
const content = merged.content;
|
|
2317
|
+
if (Array.isArray(content))
|
|
2318
|
+
toolCalls += content.filter((c) => !!c && c.type === "tool").length;
|
|
2319
|
+
}
|
|
2320
|
+
for (const it of st.queue.values()) {
|
|
2321
|
+
if (it?.type === "message")
|
|
2322
|
+
messages++;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return { messages, toolCalls };
|
|
2326
|
+
};
|
|
1860
2327
|
const msgIndex = new Map;
|
|
1861
2328
|
const tombstoned = new Set;
|
|
1862
2329
|
const indexMessage = (entry) => {
|
|
@@ -2279,7 +2746,7 @@ function createEngine(deps) {
|
|
|
2279
2746
|
const backfillSession = async (st, client, sid, info) => {
|
|
2280
2747
|
const sessionInfoValue = info ?? await sessionInfo(client, sid);
|
|
2281
2748
|
if (sessionInfoValue)
|
|
2282
|
-
enqueue(st, itemFor("session", redact(sessionInfoValue), sid));
|
|
2749
|
+
enqueue(st, itemFor("session", stripLocalFacts(redact(sessionInfoValue)), sid));
|
|
2283
2750
|
let modelSeen = false;
|
|
2284
2751
|
const consume = (messages) => {
|
|
2285
2752
|
for (const message of messages) {
|
|
@@ -2319,7 +2786,7 @@ function createEngine(deps) {
|
|
|
2319
2786
|
if (messages.length === 0 && !info) {
|
|
2320
2787
|
const fetched = await sessionInfo(client, st.sid);
|
|
2321
2788
|
if (fetched)
|
|
2322
|
-
enqueue(st, itemFor("session", redact(fetched), st.sid));
|
|
2789
|
+
enqueue(st, itemFor("session", stripLocalFacts(redact(fetched)), st.sid));
|
|
2323
2790
|
}
|
|
2324
2791
|
if (typeof client.session.list === "function") {
|
|
2325
2792
|
try {
|
|
@@ -2396,12 +2863,8 @@ function createEngine(deps) {
|
|
|
2396
2863
|
if (parent) {
|
|
2397
2864
|
const st = sessions.get(parent);
|
|
2398
2865
|
if (st?.activated && !st.closed) {
|
|
2399
|
-
if (!st.prefsSent) {
|
|
2400
|
-
st.prefsSent = true;
|
|
2401
|
-
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2402
|
-
}
|
|
2403
2866
|
if (info)
|
|
2404
|
-
enqueue(st, itemFor("session", redact(info), sid));
|
|
2867
|
+
enqueue(st, itemFor("session", stripLocalFacts(redact(info)), sid));
|
|
2405
2868
|
}
|
|
2406
2869
|
return;
|
|
2407
2870
|
}
|
|
@@ -2410,23 +2873,14 @@ function createEngine(deps) {
|
|
|
2410
2873
|
if (flag.enabled) {
|
|
2411
2874
|
if (st?.activated && !st.closed) {
|
|
2412
2875
|
if (flag.opts)
|
|
2413
|
-
st
|
|
2414
|
-
if (!st.prefsSent) {
|
|
2415
|
-
st.prefsSent = true;
|
|
2416
|
-
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2417
|
-
}
|
|
2876
|
+
applyOpts(st, flag.opts);
|
|
2418
2877
|
if (info)
|
|
2419
|
-
enqueue(st, itemFor("session", redact(info), sid));
|
|
2878
|
+
enqueue(st, itemFor("session", stripLocalFacts(redact(info)), sid));
|
|
2420
2879
|
if (t === "session.updated")
|
|
2421
2880
|
chain = chain.then(() => refreshOptOuts(st)).catch(() => {});
|
|
2422
2881
|
return;
|
|
2423
2882
|
}
|
|
2424
2883
|
activate(sid, flag.opts, info);
|
|
2425
|
-
const fresh = sessions.get(sid);
|
|
2426
|
-
if (fresh && !fresh.prefsSent) {
|
|
2427
|
-
fresh.prefsSent = true;
|
|
2428
|
-
enqueue(fresh, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2429
|
-
}
|
|
2430
2884
|
return;
|
|
2431
2885
|
}
|
|
2432
2886
|
if (st?.activated && !st.closed && t === "session.updated") {
|
|
@@ -2472,12 +2926,8 @@ function createEngine(deps) {
|
|
|
2472
2926
|
}
|
|
2473
2927
|
if (!st)
|
|
2474
2928
|
return;
|
|
2475
|
-
if (!st.prefsSent) {
|
|
2476
|
-
st.prefsSent = true;
|
|
2477
|
-
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2478
|
-
}
|
|
2479
2929
|
if (info)
|
|
2480
|
-
enqueue(st, itemFor("session", redact(info), sid));
|
|
2930
|
+
enqueue(st, itemFor("session", stripLocalFacts(redact(info)), sid));
|
|
2481
2931
|
if (t === "session.updated" && st.activated && !st.closed) {
|
|
2482
2932
|
chain = chain.then(() => refreshOptOuts(st)).catch(() => {});
|
|
2483
2933
|
}
|
|
@@ -2685,6 +3135,21 @@ function createEngine(deps) {
|
|
|
2685
3135
|
st.metaTimer = null;
|
|
2686
3136
|
await writeBinding(st).catch(() => {});
|
|
2687
3137
|
}
|
|
3138
|
+
const barrierStart = Date.now();
|
|
3139
|
+
while (Date.now() - barrierStart < 5000) {
|
|
3140
|
+
let pendingAnnounce = false;
|
|
3141
|
+
for (const st of sessions.values()) {
|
|
3142
|
+
for (const r of st.remotes.values()) {
|
|
3143
|
+
if (r.authBlocked || r.collided)
|
|
3144
|
+
continue;
|
|
3145
|
+
if (r.shareId && !r.announced || !r.shareId && (r.ws || r.connecting))
|
|
3146
|
+
pendingAnnounce = true;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
if (!pendingAnnounce)
|
|
3150
|
+
break;
|
|
3151
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
3152
|
+
}
|
|
2688
3153
|
for (const st of sessions.values()) {
|
|
2689
3154
|
for (const r of st.remotes.values()) {
|
|
2690
3155
|
try {
|