@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.
@@ -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 — A's change is silently lost (live-caught class: a TUI opt-out
9
- * erased by the engine's binding refresh).
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 (live-caught 2026-10-19): the v1 host keymap gates layers
9
- // that declare `mode` on the mode stack (resting mode is "base"), so a
10
- // plugin layer MUST NOT pass mode ("global" made the whole layer
11
- // unreachable — no palette entries, no slash commands, no shadow) and
12
- // must not carry undefined-valued command fields. The registration shape
13
- // is exactly what the proven family plugins use: {commands, bindings},
14
- // commands with only defined fields, category "OpenShare" (own group,
15
- // deliberately NOT joined into the shared family palette category).
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
- if (!isConfigured(cfg)) {
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 — identical to /openshare → This session by
159
- // construction (one surface, no duplicated manager). No
160
- // dialog.clear() after: settings owns its chain end-to-end (the
161
- // old fire-and-forget menu + immediate clear wiped it instantly —
162
- // live-caught as "/share did nothing" on a shared session).
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
- ttlDays: number;
26
- openTasksInNewTab: boolean;
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 (deploy-time overrides are never created from the TUI —
69
- * owner rule). Order: global first (default), then the rest by read
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 — per-URL
82
- * wholesale replacement would re-enroll on every boot (live-caught in
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 — with
87
- * the root token ALSO filed as adminToken (the createToken IS root; filing
88
- * it only as `token` made the TUI offer user-actions like rotate to a root
89
- * holder — live-caught "can either be admin or user but not both"). */
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 — older docs shipped the subpath spec and presence detection must
115
- * never silently miss it (all-mode would keep auto-sharing while the user
116
- * believes they installed manual mode — live-caught in review). */
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
- ttlDays: 90,
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 (deploy-time overrides are never created from the TUI —
102
- * owner rule). Order: global first (default), then the rest by read
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 — per-URL
217
- * wholesale replacement would re-enroll on every boot (live-caught in
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 — with
227
- * the root token ALSO filed as adminToken (the createToken IS root; filing
228
- * it only as `token` made the TUI offer user-actions like rotate to a root
229
- * holder — live-caught "can either be admin or user but not both"). */
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
- const raw = JSON.parse(readFileSync(path, "utf8"));
312
- if (!raw || typeof raw !== "object")
313
- continue;
314
- for (const [key, value] of Object.entries(raw)) {
315
- if (key === "servers") {
316
- const servers = normalizeServers(value);
317
- if (servers)
318
- cfg.servers = mergeServerGroups(cfg.servers, servers);
319
- continue;
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.ttlDays = num(env.OPENSHARE_TTL_DAYS, cfg.ttlDays);
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 — older docs shipped the subpath spec and presence detection must
408
- * never silently miss it (all-mode would keep auto-sharing while the user
409
- * believes they installed manual mode — live-caught in review). */
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;
@@ -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> — an unstamped del misses the
4
- * key and undone turns resurrect on the live page (live-caught
5
- * 2026-10-21, refresh-correct because the worker keys differently). */
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 — that silently
155
- * truncated every v1 backfill to one page (live-caught). */
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> — an unstamped del misses the
23
- * key and undone turns resurrect on the live page (live-caught
24
- * 2026-10-21, refresh-correct because the worker keys differently). */
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 — that silently
260
- * truncated every v1 backfill to one page (live-caught). */
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
- ttlDays?: number;
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;