@mirrowel/opencode-openshare 0.2.1 → 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/ci/openshare.js CHANGED
@@ -35,11 +35,13 @@ var DEFAULTS = {
35
35
  debounceMs: 1e4,
36
36
  completedDelayMs: 5000,
37
37
  modelLabel: "$Provider/$Model",
38
- ttlDays: 90,
38
+ ttlHours: 90 * 24,
39
39
  authLogin: "",
40
40
  authPassword: "",
41
41
  spoolDir: "",
42
42
  urlFile: "",
43
+ urlFileFormat: "url",
44
+ configSet: "",
43
45
  announceStderr: true
44
46
  };
45
47
  var CONFIG_NAME = "openshare.config.json";
@@ -78,6 +80,76 @@ function configPaths(pluginDir, env = process.env) {
78
80
  function configHomePath(env = process.env) {
79
81
  return join(globalConfigDir(env), CONFIG_NAME);
80
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
+ }
81
153
  function num(value, fallback) {
82
154
  const parsed = Number(value);
83
155
  return Number.isFinite(parsed) ? parsed : fallback;
@@ -104,6 +176,8 @@ function normalizeServers(raw) {
104
176
  entry.adminToken = e.adminToken;
105
177
  if (e.active === true)
106
178
  entry.active = true;
179
+ if (e.anonymous === true)
180
+ entry.anonymous = true;
107
181
  out[url] = entry;
108
182
  }
109
183
  return Object.keys(out).length ? out : undefined;
@@ -155,26 +229,53 @@ function loadConfig(pluginDir, options) {
155
229
  const cfg = { ...DEFAULTS };
156
230
  const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
157
231
  const known = new Set([...Object.keys(DEFAULTS), "shareMode"]);
158
- for (const path of [...configPaths(pluginDir)].reverse()) {
159
- try {
160
- const raw = JSON.parse(readFileSync(path, "utf8"));
161
- if (!raw || typeof raw !== "object")
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);
162
240
  continue;
163
- for (const [key, value] of Object.entries(raw)) {
164
- if (key === "servers") {
165
- const servers = normalizeServers(value);
166
- if (servers)
167
- cfg.servers = mergeServerGroups(cfg.servers, servers);
168
- 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 };
169
246
  }
170
- if (!known.has(key))
171
- continue;
172
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
173
- continue;
174
- if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
175
- continue;
176
- cfg[key] = value;
247
+ continue;
248
+ }
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;
177
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")));
178
279
  } catch (e) {
179
280
  if (existsSync(path))
180
281
  console.error(`[openshare] config file ${path} could not be parsed - ignored:`, e?.message ?? e);
@@ -197,6 +298,13 @@ function loadConfig(pluginDir, options) {
197
298
  cfg.shareMode = mode;
198
299
  }
199
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
+ }
200
308
  if (env.OPENSHARE_WORKER_URL)
201
309
  cfg.workerUrl = env.OPENSHARE_WORKER_URL;
202
310
  if (env.OPENSHARE_CREATE_TOKEN)
@@ -207,14 +315,20 @@ function loadConfig(pluginDir, options) {
207
315
  cfg.completedDelayMs = num(env.OPENSHARE_COMPLETED_DELAY_MS, cfg.completedDelayMs);
208
316
  if (env.OPENSHARE_MODEL_LABEL !== undefined)
209
317
  cfg.modelLabel = env.OPENSHARE_MODEL_LABEL;
318
+ if (env.OPENSHARE_TTL_HOURS)
319
+ cfg.ttlHours = num(env.OPENSHARE_TTL_HOURS, cfg.ttlHours);
210
320
  if (env.OPENSHARE_TTL_DAYS)
211
- cfg.ttlDays = num(env.OPENSHARE_TTL_DAYS, cfg.ttlDays);
321
+ cfg.ttlHours = Math.max(1, Math.floor(num(env.OPENSHARE_TTL_DAYS, 90) * 24));
212
322
  if (env.OPENSHARE_AUTH_LOGIN !== undefined)
213
323
  cfg.authLogin = env.OPENSHARE_AUTH_LOGIN;
214
324
  if (env.OPENSHARE_AUTH_PASSWORD !== undefined)
215
325
  cfg.authPassword = env.OPENSHARE_AUTH_PASSWORD;
216
326
  if (env.OPENSHARE_URL_FILE !== undefined)
217
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;
218
332
  if (env.OPENSHARE_SPOOL_DIR)
219
333
  cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
220
334
  if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
@@ -355,6 +469,7 @@ function resolveShareMode(cfg, input) {
355
469
 
356
470
  // src/core.ts
357
471
  import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
472
+ import { homedir } from "node:os";
358
473
  import { createHash } from "node:crypto";
359
474
  import { isAbsolute, join as join2 } from "node:path";
360
475
 
@@ -574,6 +689,109 @@ function queueBindingWrite(sid, fn) {
574
689
  return run;
575
690
  }
576
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
+
577
795
  // src/core.ts
578
796
  var DEFAULT_POLICY = {
579
797
  mode: "persistent",
@@ -626,6 +844,19 @@ function redact(info) {
626
844
  delete metadata[METADATA_KEY];
627
845
  return { ...info, metadata };
628
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
+ }
629
860
  function shareFlag(info) {
630
861
  const raw = info?.metadata?.[METADATA_KEY];
631
862
  if (!raw || typeof raw !== "object")
@@ -637,8 +868,10 @@ function shareFlag(info) {
637
868
  const opts = {};
638
869
  if (optsRaw && typeof optsRaw === "object") {
639
870
  const o = optsRaw;
640
- if (typeof o.ttlDays === "number" && o.ttlDays > 0)
641
- opts.ttlDays = o.ttlDays;
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);
642
875
  if (typeof o.authLogin === "string")
643
876
  opts.authLogin = o.authLogin;
644
877
  if (typeof o.authPassword === "string")
@@ -649,6 +882,8 @@ function shareFlag(info) {
649
882
  return { enabled: true, opts: Object.keys(opts).length ? opts : undefined };
650
883
  }
651
884
  var liveTargets = [];
885
+ var statsProvider = null;
886
+ var redactionLedger = new Map;
652
887
  function createEngine(deps) {
653
888
  let cfg = loadConfig(deps.pluginDir, deps.options);
654
889
  const mode = resolveShareMode(cfg, deps);
@@ -721,40 +956,98 @@ function createEngine(deps) {
721
956
  };
722
957
  seedCredentials();
723
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
+ };
724
1008
  const ensureCredential = async (url) => {
725
1009
  const c = credOf(url);
726
1010
  const target = targets.find((t) => t.url === url);
727
- if (c.credential)
1011
+ if (c.credential) {
1012
+ adoptServerSuggestions(url, c.credential);
728
1013
  return c.credential;
1014
+ }
729
1015
  if (Date.now() - c.lastEnrollAt < ENROLL_RETRY_MS)
730
1016
  return "";
731
1017
  c.lastEnrollAt = Date.now();
732
- try {
733
- const h = await fetch(`${url}/health`, { signal: AbortSignal.timeout(8000) });
734
- const hb = h.ok ? await h.json().catch(() => null) : null;
735
- const authMode2 = hb?.auth?.mode;
736
- const code = target?.entry.code ?? "";
737
- if (authMode2 === "public" || code) {
738
- const r = await fetch(`${url}/api/enroll`, {
739
- method: "POST",
740
- headers: { "content-type": "application/json" },
741
- body: JSON.stringify({ code }),
742
- signal: AbortSignal.timeout(1e4)
743
- });
744
- const b = r.ok ? await r.json().catch(() => null) : null;
745
- if (b?.ok && typeof b.token === "string") {
746
- 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") {
747
1024
  c.error = "";
748
- const savedTo = saveServerToken(url, c.credential);
749
- log(`enrolled at ${url} (user ${b.userId} "${b.label}")${savedTo ? " - token saved" : ""}`);
750
- return c.credential;
1025
+ return "";
751
1026
  }
752
- c.error = `enroll refused: ${b?.error ?? `HTTP ${r.status}`}`;
753
- } else if (authMode2 === "private") {
754
- c.error = "private server: add servers.{url}.code (or token) to enroll";
755
- } else {
756
- 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}`;
757
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}`}`;
758
1051
  } catch (e) {
759
1052
  c.error = `enrollment failed: ${e?.message || e}`;
760
1053
  }
@@ -1250,8 +1543,15 @@ function createEngine(deps) {
1250
1543
  if (mode === "all" && cfg.urlFile !== URL_FILE_NONE) {
1251
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");
1252
1545
  try {
1253
- appendFileSync(file, url + `
1254
- `);
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);
1255
1555
  if (cfg.announceStderr && (file !== lastUrlFile || !urlFileAnnounced)) {
1256
1556
  urlFileAnnounced = true;
1257
1557
  lastUrlFile = file;
@@ -1275,7 +1575,20 @@ function createEngine(deps) {
1275
1575
  }
1276
1576
  return { authLogin: login, authPassword: password };
1277
1577
  };
1278
- const ttlFor = (st) => st.opts?.ttlDays ?? cfg.ttlDays;
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
+ };
1279
1592
  const applyOpts = (st, opts) => {
1280
1593
  if (!opts)
1281
1594
  return;
@@ -1287,11 +1600,11 @@ function createEngine(deps) {
1287
1600
  if (!r.shareId || !r.secret || !r.wsReady)
1288
1601
  continue;
1289
1602
  const attach = { id: r.shareId, secret: r.secret, ...authFrame(st) };
1290
- if (st.opts?.ttlDays)
1291
- attach.ttlDays = st.opts.ttlDays;
1603
+ if (st.opts?.ttlHours)
1604
+ attach.ttlHours = st.opts.ttlHours;
1292
1605
  const sent = wsSend(r, { op: "init", attach });
1293
1606
  if (sent)
1294
- log(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlDays ? ` (ttl ${st.opts.ttlDays}d)` : ""}`);
1607
+ log(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlHours ? ` (ttl ${st.opts.ttlHours}h)` : ""}`);
1295
1608
  }
1296
1609
  };
1297
1610
  const deleteAbandoned = (r) => {
@@ -1487,7 +1800,7 @@ function createEngine(deps) {
1487
1800
  if (r.initWasAttach) {
1488
1801
  wsSend(r, { op: "init", attach: { id: r.shareId, secret: r.secret, ...authFrame(st) } });
1489
1802
  } else {
1490
- wsSend(r, { op: "init", create: { sessionID: st.sid, ttlDays: ttlFor(st), ...authFrame(st) } });
1803
+ wsSend(r, { op: "init", create: { sessionID: st.sid, ttlHours: ttlFor(st), ...authFrame(st), ...cfg.configSet ? { set: cfg.configSet } : {}, ...metaFor() ? { meta: metaFor() } : {} } });
1491
1804
  }
1492
1805
  r.initTimer = setTimeout(() => {
1493
1806
  if (seq !== r.socketSeq)
@@ -1658,7 +1971,7 @@ function createEngine(deps) {
1658
1971
  r.shareId = null;
1659
1972
  r.secret = null;
1660
1973
  r.ackedRev = 0;
1661
- wsSend(r, { op: "init", create: { sessionID: st.sid, ttlDays: ttlFor(st), ...authFrame(st) } });
1974
+ wsSend(r, { op: "init", create: { sessionID: st.sid, ttlHours: ttlFor(st), ...authFrame(st), ...cfg.configSet ? { set: cfg.configSet } : {}, ...metaFor() ? { meta: metaFor() } : {} } });
1662
1975
  return;
1663
1976
  }
1664
1977
  if (r.initWasAttach && typeof frame.rev === "number" && frame.rev < r.ackedRev) {
@@ -1667,7 +1980,7 @@ function createEngine(deps) {
1667
1980
  r.shareId = null;
1668
1981
  r.secret = null;
1669
1982
  r.ackedRev = 0;
1670
- wsSend(r, { op: "init", create: { sessionID: st.sid, ttlDays: ttlFor(st), ...authFrame(st) } });
1983
+ wsSend(r, { op: "init", create: { sessionID: st.sid, ttlHours: ttlFor(st), ...authFrame(st), ...cfg.configSet ? { set: cfg.configSet } : {}, ...metaFor() ? { meta: metaFor() } : {} } });
1671
1984
  return;
1672
1985
  }
1673
1986
  const wasFanoutCreate = !r.initWasAttach && st.everConnected;
@@ -1699,9 +2012,10 @@ function createEngine(deps) {
1699
2012
  applyPolicy(frame.policy, r.url);
1700
2013
  r.intentionalClose = false;
1701
2014
  touch(st);
1702
- if (typeof frame.ttlDays === "number") {
2015
+ const ackTtl = typeof frame.ttlHours === "number" ? frame.ttlHours : typeof frame.ttlDays === "number" ? frame.ttlDays * 24 : undefined;
2016
+ if (typeof ackTtl === "number") {
1703
2017
  const wanted = ttlFor(st);
1704
- const clamp = wanted != null && frame.ttlDays !== wanted ? { requested: wanted, effective: frame.ttlDays } : null;
2018
+ const clamp = wanted != null && ackTtl !== wanted ? { requested: wanted, effective: ackTtl } : null;
1705
2019
  if (JSON.stringify(clamp) !== JSON.stringify(st.ttlClamped)) {
1706
2020
  st.ttlClamped = clamp;
1707
2021
  scheduleBindingWrite(st);
@@ -1711,6 +2025,28 @@ function createEngine(deps) {
1711
2025
  st.lastError = null;
1712
2026
  scheduleBindingWrite(st);
1713
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
+ }
1714
2050
  announceUrl(st, r);
1715
2051
  if (!st.wroteBinding) {
1716
2052
  chain = chain.then(() => writeBinding(st)).catch(() => {});
@@ -1921,6 +2257,15 @@ function createEngine(deps) {
1921
2257
  }).catch((e) => logError("flush error", e?.message || e));
1922
2258
  }, wait);
1923
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);
1924
2269
  const enqueue = (st, entry) => {
1925
2270
  if (!st || !entry)
1926
2271
  return;
@@ -1937,6 +2282,22 @@ function createEngine(deps) {
1937
2282
  }
1938
2283
  if (h)
1939
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
+ }
1940
2301
  st.queue.set(entry.key, entry.item);
1941
2302
  indexMessage(entry);
1942
2303
  if (debugEvents)
@@ -1945,6 +2306,24 @@ function createEngine(deps) {
1945
2306
  ensureConnected(st);
1946
2307
  scheduleFlush(st);
1947
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
+ };
1948
2327
  const msgIndex = new Map;
1949
2328
  const tombstoned = new Set;
1950
2329
  const indexMessage = (entry) => {
@@ -2367,7 +2746,7 @@ function createEngine(deps) {
2367
2746
  const backfillSession = async (st, client, sid, info) => {
2368
2747
  const sessionInfoValue = info ?? await sessionInfo(client, sid);
2369
2748
  if (sessionInfoValue)
2370
- enqueue(st, itemFor("session", redact(sessionInfoValue), sid));
2749
+ enqueue(st, itemFor("session", stripLocalFacts(redact(sessionInfoValue)), sid));
2371
2750
  let modelSeen = false;
2372
2751
  const consume = (messages) => {
2373
2752
  for (const message of messages) {
@@ -2407,7 +2786,7 @@ function createEngine(deps) {
2407
2786
  if (messages.length === 0 && !info) {
2408
2787
  const fetched = await sessionInfo(client, st.sid);
2409
2788
  if (fetched)
2410
- enqueue(st, itemFor("session", redact(fetched), st.sid));
2789
+ enqueue(st, itemFor("session", stripLocalFacts(redact(fetched)), st.sid));
2411
2790
  }
2412
2791
  if (typeof client.session.list === "function") {
2413
2792
  try {
@@ -2485,7 +2864,7 @@ function createEngine(deps) {
2485
2864
  const st = sessions.get(parent);
2486
2865
  if (st?.activated && !st.closed) {
2487
2866
  if (info)
2488
- enqueue(st, itemFor("session", redact(info), sid));
2867
+ enqueue(st, itemFor("session", stripLocalFacts(redact(info)), sid));
2489
2868
  }
2490
2869
  return;
2491
2870
  }
@@ -2496,7 +2875,7 @@ function createEngine(deps) {
2496
2875
  if (flag.opts)
2497
2876
  applyOpts(st, flag.opts);
2498
2877
  if (info)
2499
- enqueue(st, itemFor("session", redact(info), sid));
2878
+ enqueue(st, itemFor("session", stripLocalFacts(redact(info)), sid));
2500
2879
  if (t === "session.updated")
2501
2880
  chain = chain.then(() => refreshOptOuts(st)).catch(() => {});
2502
2881
  return;
@@ -2548,7 +2927,7 @@ function createEngine(deps) {
2548
2927
  if (!st)
2549
2928
  return;
2550
2929
  if (info)
2551
- enqueue(st, itemFor("session", redact(info), sid));
2930
+ enqueue(st, itemFor("session", stripLocalFacts(redact(info)), sid));
2552
2931
  if (t === "session.updated" && st.activated && !st.closed) {
2553
2932
  chain = chain.then(() => refreshOptOuts(st)).catch(() => {});
2554
2933
  }