@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/dist/binding-queue.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* the servers map) and the TUI (`patchSessionBinding` in share-flow.ts —
|
|
6
6
|
* opt-outs, collision resolution). Their PATCHes interleave at the host:
|
|
7
7
|
* writer A reads at T0, writer B reads at T1, A PATCHes at T2, B PATCHes
|
|
8
|
-
* at T3
|
|
9
|
-
*
|
|
8
|
+
* at T3, and A's change is silently lost (a TUI opt-out erased by the
|
|
9
|
+
* engine's binding refresh).
|
|
10
10
|
*
|
|
11
11
|
* Both route their whole read-modify-write through this per-session FIFO.
|
|
12
12
|
* The queue never breaks on errors; entries drop when the process exits.
|
package/dist/commands.js
CHANGED
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
// auth — one surface, no duplicated manager); on an unshared session it
|
|
6
6
|
// asks consent and creates the share. /openshare opens settings.
|
|
7
7
|
//
|
|
8
|
-
// LAYER SHAPE
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
8
|
+
// LAYER SHAPE: the v1 host keymap gates layers that declare `mode` on the
|
|
9
|
+
// mode stack (resting mode is "base"), so a plugin layer MUST NOT pass mode
|
|
10
|
+
// ("global" made the whole layer unreachable: no palette entries, no slash
|
|
11
|
+
// commands, no shadow) and must not carry undefined-valued command fields.
|
|
12
|
+
// The registration shape is exactly what the proven family plugins use:
|
|
13
|
+
// {commands, bindings}, commands with only defined fields, category
|
|
14
|
+
// "OpenShare" (own group, deliberately NOT joined into the shared family
|
|
15
|
+
// palette category).
|
|
16
16
|
import { clearGoneBindingEntries, currentConfig, hostOf, isConfigured, patchSessionBinding, readBindings, resolveRoot, shareWithConsent, verifyShare, } from "./share-flow.js";
|
|
17
17
|
import { onboardingDialog, settingsDialog } from "./settings.js";
|
|
18
|
+
import { engineTargetUrls } from "./core.js";
|
|
18
19
|
import { dbgLog } from "./sidebar.js";
|
|
19
20
|
export const PALETTE_CATEGORY = "OpenShare";
|
|
20
21
|
function finish(host, result) {
|
|
@@ -135,7 +136,9 @@ export function buildShareCommand(host, sessionID) {
|
|
|
135
136
|
dbgLog(`/share run: entered (sid=${sessionID() ?? "none"})`);
|
|
136
137
|
host.ui.dialog.clear();
|
|
137
138
|
const cfg = currentConfig();
|
|
138
|
-
|
|
139
|
+
// engine-aware gate: project-level plugin options can configure
|
|
140
|
+
// targets the TUI's file chain cannot see - trust the live engine
|
|
141
|
+
if (!isConfigured(cfg) && !engineTargetUrls().length) {
|
|
139
142
|
void onboardingDialog(host);
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
@@ -155,12 +158,12 @@ export function buildShareCommand(host, sessionID) {
|
|
|
155
158
|
// enabled:true; legacy all-mode bindings without the field are live
|
|
156
159
|
// too — only an explicit false is dormant)
|
|
157
160
|
if (all.some((b) => b.url && b.enabled !== false)) {
|
|
158
|
-
// THE session menu
|
|
159
|
-
// construction (one surface, no duplicated manager)
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
await settingsDialog(host, sid);
|
|
161
|
+
// THE session menu - identical to /openshare This session by
|
|
162
|
+
// construction (one surface, no duplicated manager), opened
|
|
163
|
+
// DIRECTLY (owner: "/share is supposed to be the same menu, not
|
|
164
|
+
// the settings root one hop away"). No dialog.clear() after:
|
|
165
|
+
// settings owns its chain end-to-end.
|
|
166
|
+
await settingsDialog(host, sid, "session");
|
|
164
167
|
return;
|
|
165
168
|
}
|
|
166
169
|
// everything was just cleared by the gone modal and the user chose
|
package/dist/config.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type ServerEntry = {
|
|
|
12
12
|
label?: string;
|
|
13
13
|
active?: boolean;
|
|
14
14
|
adminToken?: string;
|
|
15
|
+
/** The user DECLINED onboarding for this server (asked once, remembered):
|
|
16
|
+
* shares go out anonymously (public servers only). Cleared by enrolling. */
|
|
17
|
+
anonymous?: boolean;
|
|
15
18
|
};
|
|
16
19
|
export type ShareConfig = {
|
|
17
20
|
workerUrl: string;
|
|
@@ -22,15 +25,33 @@ export type ShareConfig = {
|
|
|
22
25
|
debounceMs: number;
|
|
23
26
|
completedDelayMs: number;
|
|
24
27
|
modelLabel: string;
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
/** Default share lifetime in HOURS (Wave 3; default 2160 = 90d).
|
|
29
|
+
* Legacy ttlDays config/env is accepted and converted x24. */
|
|
30
|
+
ttlHours: number;
|
|
27
31
|
authLogin: string;
|
|
28
32
|
authPassword: string;
|
|
29
33
|
spoolDir: string;
|
|
30
34
|
urlFile: string;
|
|
35
|
+
/** urlFile line format: "url" (default, one bare URL per line) or
|
|
36
|
+
* "json" (one JSON object per line: id, url, status, gated) for CI
|
|
37
|
+
* consumers that want machine-readable lines without parsing URLs. */
|
|
38
|
+
urlFileFormat: "url" | "json";
|
|
31
39
|
/** false silences the `[openshare] live:` (and `url file:`) stderr
|
|
32
40
|
* lines. The share itself is unaffected. */
|
|
33
41
|
announceStderr: boolean;
|
|
42
|
+
/** Wave 3 redaction: user-supplied regex rules (client-side only)
|
|
43
|
+
* applied beside the built-in detectors on UNGATED shares. */
|
|
44
|
+
redact?: {
|
|
45
|
+
patterns?: string[];
|
|
46
|
+
};
|
|
47
|
+
/** B4: which named config set on the server token this agent uses
|
|
48
|
+
* (unset = the token's first/default set). Rides the create frame. */
|
|
49
|
+
configSet?: string;
|
|
50
|
+
/** Free-form string metadata stamped onto every share at create
|
|
51
|
+
* (labels for team directories/admin listings). Values may reference
|
|
52
|
+
* env vars as ${NAME} - interpolated at send time; unresolved refs
|
|
53
|
+
* stay literal so misconfiguration is visible. */
|
|
54
|
+
meta?: Record<string, string>;
|
|
34
55
|
shareMode?: ShareMode;
|
|
35
56
|
};
|
|
36
57
|
export declare const METADATA_KEY = "openshare";
|
|
@@ -65,9 +86,8 @@ export type WriteCandidate = {
|
|
|
65
86
|
};
|
|
66
87
|
/** Write-target candidates for the TUI picker: the global settings home
|
|
67
88
|
* (always writable) plus every OTHER file in the read chain that already
|
|
68
|
-
* exists on disk
|
|
69
|
-
*
|
|
70
|
-
* precedence (highest first). */
|
|
89
|
+
* exists on disk, since the TUI never creates deploy-time overrides. Order:
|
|
90
|
+
* global first (default), then the rest by read precedence (highest first). */
|
|
71
91
|
export declare function writeCandidates(pluginDir: string, env?: NodeJS.ProcessEnv): WriteCandidate[];
|
|
72
92
|
/** Per-key source attribution mirroring loadConfig precedence exactly
|
|
73
93
|
* (env > adjacent > package root > local walks > global > default).
|
|
@@ -78,15 +98,14 @@ export declare function configSourceMap(pluginDir: string, env?: NodeJS.ProcessE
|
|
|
78
98
|
export declare function normalizeServers(raw: unknown): Record<string, ServerEntry> | undefined;
|
|
79
99
|
/** Field-level merge of servers groups: entries only carry present fields
|
|
80
100
|
* (normalizeServers), so a static `code` in plugin options and a persisted
|
|
81
|
-
* `token` in the settings home combine instead of shadowing
|
|
82
|
-
* wholesale replacement would re-enroll on every boot
|
|
83
|
-
* suite design review). */
|
|
101
|
+
* `token` in the settings home combine instead of shadowing. Per-URL
|
|
102
|
+
* wholesale replacement would re-enroll on every boot. */
|
|
84
103
|
export declare function mergeServerGroups(base: Record<string, ServerEntry> | undefined, add: Record<string, ServerEntry>): Record<string, ServerEntry>;
|
|
85
104
|
/** The server new sessions share to: the `active` entry (or the first).
|
|
86
|
-
* Legacy workerUrl/createToken merge in as the implicit first entry
|
|
87
|
-
* the root token ALSO filed as adminToken
|
|
88
|
-
* it only as `token`
|
|
89
|
-
* holder
|
|
105
|
+
* Legacy workerUrl/createToken merge in as the implicit first entry, with
|
|
106
|
+
* the root token ALSO filed as adminToken: the createToken IS root, and
|
|
107
|
+
* filing it only as `token` offers user-actions like rotate to a root
|
|
108
|
+
* holder, while an identity is either admin or user, not both. */
|
|
90
109
|
export declare function resolveServer(cfg: ShareConfig): {
|
|
91
110
|
url: string;
|
|
92
111
|
token: string;
|
|
@@ -111,9 +130,9 @@ export declare function loadConfig(pluginDir: string, options?: Record<string, u
|
|
|
111
130
|
export declare function spoolDirFor(cfg: ShareConfig): string;
|
|
112
131
|
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
113
132
|
* Accepts the bare npm name, name@version, and the /tui (or any) subpath
|
|
114
|
-
* form
|
|
115
|
-
* never silently miss it
|
|
116
|
-
* believes they installed manual mode
|
|
133
|
+
* form. Older docs shipped the subpath spec, and presence detection must
|
|
134
|
+
* never silently miss it: all-mode would keep auto-sharing while the user
|
|
135
|
+
* believes they installed manual mode. */
|
|
117
136
|
export declare function isOpenShareSpec(spec: unknown): spec is string;
|
|
118
137
|
/** tui.json layers the TUI reads (mirrors the family selfwire path rules,
|
|
119
138
|
* read-only). The walk runs from the session directory up to the worktree
|
package/dist/config.js
CHANGED
|
@@ -35,12 +35,13 @@ export const DEFAULTS = {
|
|
|
35
35
|
// pending flip, so CI runs still land their final status.
|
|
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
|
const CONFIG_NAME = "openshare.config.json";
|
|
@@ -98,9 +99,8 @@ export function configHomePath(env = process.env) {
|
|
|
98
99
|
}
|
|
99
100
|
/** Write-target candidates for the TUI picker: the global settings home
|
|
100
101
|
* (always writable) plus every OTHER file in the read chain that already
|
|
101
|
-
* exists on disk
|
|
102
|
-
*
|
|
103
|
-
* precedence (highest first). */
|
|
102
|
+
* exists on disk, since the TUI never creates deploy-time overrides. Order:
|
|
103
|
+
* global first (default), then the rest by read precedence (highest first). */
|
|
104
104
|
export function writeCandidates(pluginDir, env = process.env) {
|
|
105
105
|
const candidates = [{ path: configHomePath(env), label: "global settings (~/.config/opencode)", always: true }];
|
|
106
106
|
const labels = [
|
|
@@ -136,10 +136,13 @@ export function configSourceMap(pluginDir, env = process.env) {
|
|
|
136
136
|
debounceMs: env.OPENSHARE_DEBOUNCE_MS,
|
|
137
137
|
completedDelayMs: env.OPENSHARE_COMPLETED_DELAY_MS,
|
|
138
138
|
modelLabel: env.OPENSHARE_MODEL_LABEL,
|
|
139
|
+
ttlHours: env.OPENSHARE_TTL_HOURS,
|
|
139
140
|
ttlDays: env.OPENSHARE_TTL_DAYS,
|
|
140
141
|
authLogin: env.OPENSHARE_AUTH_LOGIN,
|
|
141
142
|
authPassword: env.OPENSHARE_AUTH_PASSWORD,
|
|
142
143
|
urlFile: env.OPENSHARE_URL_FILE,
|
|
144
|
+
urlFileFormat: env.OPENSHARE_URL_FILE_FORMAT,
|
|
145
|
+
configSet: env.OPENSHARE_CONFIG_SET,
|
|
143
146
|
spoolDir: env.OPENSHARE_SPOOL_DIR,
|
|
144
147
|
announceStderr: env.OPENSHARE_ANNOUNCE_STDERR,
|
|
145
148
|
shareMode: env.OPENSHARE_SHARE_MODE,
|
|
@@ -148,6 +151,28 @@ export function configSourceMap(pluginDir, env = process.env) {
|
|
|
148
151
|
if (value !== undefined && value !== "")
|
|
149
152
|
sources[key] = "environment";
|
|
150
153
|
}
|
|
154
|
+
// legacy days-shaped env feeds the same knob as ttlHours
|
|
155
|
+
if (sources.ttlDays && !sources.ttlHours)
|
|
156
|
+
sources.ttlHours = sources.ttlDays;
|
|
157
|
+
delete sources.ttlDays;
|
|
158
|
+
// OPENSHARE_CONFIG_JSON sits between env and the files: it loses only
|
|
159
|
+
// to a specific env var, so it overrides any file's key
|
|
160
|
+
if (env.OPENSHARE_CONFIG_JSON) {
|
|
161
|
+
try {
|
|
162
|
+
const raw = JSON.parse(env.OPENSHARE_CONFIG_JSON);
|
|
163
|
+
if (raw && typeof raw === "object") {
|
|
164
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
165
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
166
|
+
continue;
|
|
167
|
+
if (sources[key] === "environment")
|
|
168
|
+
continue;
|
|
169
|
+
if (key === "servers" || known.has(key))
|
|
170
|
+
sources[key] = "OPENSHARE_CONFIG_JSON";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
}
|
|
151
176
|
// configPaths() is ordered lowest-priority-first? No: index 0 is the
|
|
152
177
|
// ADJACENT file (highest). Merge order reverses it; attribution walks
|
|
153
178
|
// it as-is so the first hit is the winning (highest) source.
|
|
@@ -207,15 +232,16 @@ export function normalizeServers(raw) {
|
|
|
207
232
|
entry.adminToken = e.adminToken;
|
|
208
233
|
if (e.active === true)
|
|
209
234
|
entry.active = true;
|
|
235
|
+
if (e.anonymous === true)
|
|
236
|
+
entry.anonymous = true;
|
|
210
237
|
out[url] = entry;
|
|
211
238
|
}
|
|
212
239
|
return Object.keys(out).length ? out : undefined;
|
|
213
240
|
}
|
|
214
241
|
/** Field-level merge of servers groups: entries only carry present fields
|
|
215
242
|
* (normalizeServers), so a static `code` in plugin options and a persisted
|
|
216
|
-
* `token` in the settings home combine instead of shadowing
|
|
217
|
-
* wholesale replacement would re-enroll on every boot
|
|
218
|
-
* suite design review). */
|
|
243
|
+
* `token` in the settings home combine instead of shadowing. Per-URL
|
|
244
|
+
* wholesale replacement would re-enroll on every boot. */
|
|
219
245
|
export function mergeServerGroups(base, add) {
|
|
220
246
|
const merged = { ...(base ?? {}) };
|
|
221
247
|
for (const [url, entry] of Object.entries(add))
|
|
@@ -223,10 +249,10 @@ export function mergeServerGroups(base, add) {
|
|
|
223
249
|
return merged;
|
|
224
250
|
}
|
|
225
251
|
/** The server new sessions share to: the `active` entry (or the first).
|
|
226
|
-
* Legacy workerUrl/createToken merge in as the implicit first entry
|
|
227
|
-
* the root token ALSO filed as adminToken
|
|
228
|
-
* it only as `token`
|
|
229
|
-
* holder
|
|
252
|
+
* Legacy workerUrl/createToken merge in as the implicit first entry, with
|
|
253
|
+
* the root token ALSO filed as adminToken: the createToken IS root, and
|
|
254
|
+
* filing it only as `token` offers user-actions like rotate to a root
|
|
255
|
+
* holder, while an identity is either admin or user, not both. */
|
|
230
256
|
export function resolveServer(cfg) {
|
|
231
257
|
const servers = {};
|
|
232
258
|
if (cfg.workerUrl)
|
|
@@ -303,31 +329,67 @@ export function loadConfig(pluginDir, options) {
|
|
|
303
329
|
const cfg = { ...DEFAULTS };
|
|
304
330
|
const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
|
|
305
331
|
const known = new Set([...Object.keys(DEFAULTS), "shareMode"]);
|
|
332
|
+
const applyObject = (raw) => {
|
|
333
|
+
if (!raw || typeof raw !== "object")
|
|
334
|
+
return;
|
|
335
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
336
|
+
if (key === "servers") {
|
|
337
|
+
const servers = normalizeServers(value);
|
|
338
|
+
if (servers)
|
|
339
|
+
cfg.servers = mergeServerGroups(cfg.servers, servers);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (key === "redact") {
|
|
343
|
+
if (value && typeof value === "object" && Array.isArray(value.patterns)) {
|
|
344
|
+
const pats = value.patterns.filter((x) => typeof x === "string" && x.length > 0 && x.length < 500).slice(0, 50);
|
|
345
|
+
cfg.redact = { patterns: pats };
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (key === "meta") {
|
|
350
|
+
// string-to-string only: meta rides the create frame verbatim
|
|
351
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
352
|
+
const clean = {};
|
|
353
|
+
for (const [mk, mv] of Object.entries(value)) {
|
|
354
|
+
if (typeof mv === "string" && mv.length <= 500 && mk.length <= 64)
|
|
355
|
+
clean[mk] = mv;
|
|
356
|
+
}
|
|
357
|
+
if (Object.keys(clean).length)
|
|
358
|
+
cfg.meta = { ...cfg.meta, ...clean };
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (key === "ttlDays") {
|
|
363
|
+
// legacy days-shaped file/options value converts to hours once
|
|
364
|
+
const d = Math.floor(Number(value));
|
|
365
|
+
if (Number.isFinite(d) && d > 0)
|
|
366
|
+
cfg.ttlHours = d * 24;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (!known.has(key))
|
|
370
|
+
continue;
|
|
371
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
372
|
+
continue;
|
|
373
|
+
if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
|
|
374
|
+
continue;
|
|
375
|
+
cfg[key] = value;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
306
378
|
for (const path of [...configPaths(pluginDir)].reverse()) {
|
|
307
379
|
try {
|
|
308
380
|
// strict JSON: the $comment key (what our templates use) is valid
|
|
309
381
|
// JSON; comment-stripping is deliberately avoided because a naive
|
|
310
382
|
// stripper eats "//" inside workerUrl values
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
if (!known.has(key))
|
|
322
|
-
continue;
|
|
323
|
-
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
324
|
-
continue;
|
|
325
|
-
if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
|
|
326
|
-
continue;
|
|
327
|
-
cfg[key] = value;
|
|
328
|
-
}
|
|
383
|
+
applyObject(JSON.parse(readFileSync(path, "utf8")));
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
// a malformed config file is a real setup error the user must hear
|
|
387
|
+
// about - silence here looks like "no share server configured".
|
|
388
|
+
// (Absent files are normal chain candidates - only broken content
|
|
389
|
+
// is worth a line.)
|
|
390
|
+
if (existsSync(path))
|
|
391
|
+
console.error(`[openshare] config file ${path} could not be parsed - ignored:`, e?.message ?? e);
|
|
329
392
|
}
|
|
330
|
-
catch { }
|
|
331
393
|
}
|
|
332
394
|
if (options && typeof options === "object") {
|
|
333
395
|
const serversOpt = normalizeServers(options.servers);
|
|
@@ -347,6 +409,19 @@ export function loadConfig(pluginDir, options) {
|
|
|
347
409
|
cfg.shareMode = mode;
|
|
348
410
|
}
|
|
349
411
|
const env = process.env;
|
|
412
|
+
// OPENSHARE_CONFIG_JSON: a complete config object in one env var. Sits
|
|
413
|
+
// between the file/options layers and the per-key OPENSHARE_* env (a
|
|
414
|
+
// specific env var still wins) - CI sets one variable instead of nine,
|
|
415
|
+
// and secrets never touch a file. Malformed JSON is a loud error, not
|
|
416
|
+
// silence (same rule as files).
|
|
417
|
+
if (env.OPENSHARE_CONFIG_JSON) {
|
|
418
|
+
try {
|
|
419
|
+
applyObject(JSON.parse(env.OPENSHARE_CONFIG_JSON));
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
console.error("[openshare] OPENSHARE_CONFIG_JSON could not be parsed - ignored:", e?.message ?? e);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
350
425
|
if (env.OPENSHARE_WORKER_URL)
|
|
351
426
|
cfg.workerUrl = env.OPENSHARE_WORKER_URL;
|
|
352
427
|
if (env.OPENSHARE_CREATE_TOKEN)
|
|
@@ -357,14 +432,20 @@ export function loadConfig(pluginDir, options) {
|
|
|
357
432
|
cfg.completedDelayMs = num(env.OPENSHARE_COMPLETED_DELAY_MS, cfg.completedDelayMs);
|
|
358
433
|
if (env.OPENSHARE_MODEL_LABEL !== undefined)
|
|
359
434
|
cfg.modelLabel = env.OPENSHARE_MODEL_LABEL;
|
|
435
|
+
if (env.OPENSHARE_TTL_HOURS)
|
|
436
|
+
cfg.ttlHours = num(env.OPENSHARE_TTL_HOURS, cfg.ttlHours);
|
|
360
437
|
if (env.OPENSHARE_TTL_DAYS)
|
|
361
|
-
cfg.
|
|
438
|
+
cfg.ttlHours = Math.max(1, Math.floor(num(env.OPENSHARE_TTL_DAYS, 90) * 24));
|
|
362
439
|
if (env.OPENSHARE_AUTH_LOGIN !== undefined)
|
|
363
440
|
cfg.authLogin = env.OPENSHARE_AUTH_LOGIN;
|
|
364
441
|
if (env.OPENSHARE_AUTH_PASSWORD !== undefined)
|
|
365
442
|
cfg.authPassword = env.OPENSHARE_AUTH_PASSWORD;
|
|
366
443
|
if (env.OPENSHARE_URL_FILE !== undefined)
|
|
367
444
|
cfg.urlFile = env.OPENSHARE_URL_FILE;
|
|
445
|
+
if (env.OPENSHARE_URL_FILE_FORMAT === "json" || env.OPENSHARE_URL_FILE_FORMAT === "url")
|
|
446
|
+
cfg.urlFileFormat = env.OPENSHARE_URL_FILE_FORMAT;
|
|
447
|
+
if (env.OPENSHARE_CONFIG_SET)
|
|
448
|
+
cfg.configSet = env.OPENSHARE_CONFIG_SET;
|
|
368
449
|
if (env.OPENSHARE_SPOOL_DIR)
|
|
369
450
|
cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
|
|
370
451
|
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
|
|
@@ -374,6 +455,17 @@ export function loadConfig(pluginDir, options) {
|
|
|
374
455
|
// normalize the disable sentinel (case-insensitive from any source)
|
|
375
456
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
376
457
|
cfg.urlFile = URL_FILE_NONE;
|
|
458
|
+
// URL hygiene: a trailing slash on a server url becomes
|
|
459
|
+
// wss://host//api/ws and https://host//s/<id> downstream - strip once,
|
|
460
|
+
// at the source, so every consumer agrees
|
|
461
|
+
if (cfg.workerUrl)
|
|
462
|
+
cfg.workerUrl = cfg.workerUrl.trim().replace(/\/+$/, "");
|
|
463
|
+
if (cfg.servers) {
|
|
464
|
+
const fixed = {};
|
|
465
|
+
for (const [u, e] of Object.entries(cfg.servers))
|
|
466
|
+
fixed[u.trim().replace(/\/+$/, "")] = e;
|
|
467
|
+
cfg.servers = fixed;
|
|
468
|
+
}
|
|
377
469
|
return cfg;
|
|
378
470
|
}
|
|
379
471
|
export function spoolDirFor(cfg) {
|
|
@@ -404,9 +496,9 @@ function specPath(spec) {
|
|
|
404
496
|
}
|
|
405
497
|
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
406
498
|
* Accepts the bare npm name, name@version, and the /tui (or any) subpath
|
|
407
|
-
* form
|
|
408
|
-
* never silently miss it
|
|
409
|
-
* believes they installed manual mode
|
|
499
|
+
* form. Older docs shipped the subpath spec, and presence detection must
|
|
500
|
+
* never silently miss it: all-mode would keep auto-sharing while the user
|
|
501
|
+
* believes they installed manual mode. */
|
|
410
502
|
export function isOpenShareSpec(spec) {
|
|
411
503
|
if (typeof spec !== "string" || spec.length === 0)
|
|
412
504
|
return false;
|
package/dist/contract.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export declare const CONTRACT_VERSION = 2;
|
|
2
2
|
/** Message tombstone item. sessionID MUST ride along: viewers key
|
|
3
|
-
* messages as message/<sessionID>/<id
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* messages as message/<sessionID>/<id>, so an unstamped del misses the key
|
|
4
|
+
* and undone turns resurrect on the live page (a refresh hides them because
|
|
5
|
+
* the worker keys items differently). */
|
|
6
6
|
export declare function messageTombstone(sessionID: string, messageID: string): {
|
|
7
7
|
readonly type: "message";
|
|
8
8
|
readonly del: true;
|
|
@@ -151,6 +151,6 @@ export declare function normalizeV1Message(info: Record<string, unknown>, parts:
|
|
|
151
151
|
export declare function normalizeV1Diff(diff: unknown): unknown;
|
|
152
152
|
/** v1 hosts paginate messages with an opaque cursor: base64url of
|
|
153
153
|
* {id, time} where time = the page tail's time_created (message-v2.ts
|
|
154
|
-
* cursor codec). A bare message id is rejected with 400
|
|
155
|
-
*
|
|
154
|
+
* cursor codec). A bare message id is rejected with 400, which silently
|
|
155
|
+
* truncates a v1 backfill to one page. */
|
|
156
156
|
export declare function cursorFor(id: string, time: number): string;
|
package/dist/contract.js
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// the whole message (parts are embedded; no cascade needed).
|
|
20
20
|
export const CONTRACT_VERSION = 2;
|
|
21
21
|
/** Message tombstone item. sessionID MUST ride along: viewers key
|
|
22
|
-
* messages as message/<sessionID>/<id
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* messages as message/<sessionID>/<id>, so an unstamped del misses the key
|
|
23
|
+
* and undone turns resurrect on the live page (a refresh hides them because
|
|
24
|
+
* the worker keys items differently). */
|
|
25
25
|
export function messageTombstone(sessionID, messageID) {
|
|
26
26
|
return { type: "message", del: true, sessionID, data: { id: messageID } };
|
|
27
27
|
}
|
|
@@ -256,8 +256,8 @@ export function normalizeV1Diff(diff) {
|
|
|
256
256
|
}
|
|
257
257
|
/** v1 hosts paginate messages with an opaque cursor: base64url of
|
|
258
258
|
* {id, time} where time = the page tail's time_created (message-v2.ts
|
|
259
|
-
* cursor codec). A bare message id is rejected with 400
|
|
260
|
-
*
|
|
259
|
+
* cursor codec). A bare message id is rejected with 400, which silently
|
|
260
|
+
* truncates a v1 backfill to one page. */
|
|
261
261
|
export function cursorFor(id, time) {
|
|
262
262
|
return Buffer.from(JSON.stringify({ id, time })).toString("base64url");
|
|
263
263
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ShareConfig, type ShareMode } from "./config.js";
|
|
2
|
+
import { type RedactionHit } from "./redact.js";
|
|
2
3
|
export type SessionModel = {
|
|
3
4
|
providerID?: string;
|
|
4
5
|
modelID?: string;
|
|
@@ -55,7 +56,7 @@ export type ShareEvent = {
|
|
|
55
56
|
sessionID: string;
|
|
56
57
|
};
|
|
57
58
|
export type SessionOpts = {
|
|
58
|
-
|
|
59
|
+
ttlHours?: number;
|
|
59
60
|
authLogin?: string;
|
|
60
61
|
authPassword?: string;
|
|
61
62
|
modelLabel?: string;
|
|
@@ -114,4 +115,15 @@ export type Engine = {
|
|
|
114
115
|
mode: ShareMode;
|
|
115
116
|
config: () => ShareConfig;
|
|
116
117
|
};
|
|
118
|
+
/** Preflight stats for the consent dialog: message/tool counts plus the
|
|
119
|
+
* redaction ledger (what would be stripped before publishing). */
|
|
120
|
+
export declare function sessionStatsFor(sid: string): {
|
|
121
|
+
messages: number;
|
|
122
|
+
toolCalls: number;
|
|
123
|
+
redaction: {
|
|
124
|
+
count: number;
|
|
125
|
+
samples: RedactionHit[];
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
export declare function engineTargetUrls(): string[];
|
|
117
129
|
export declare function createEngine(deps: EngineDeps): Engine;
|