@mirrowel/opencode-openshare 0.2.1 → 0.3.0-dev.10

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,14 +5,14 @@
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
18
  import { engineTargetUrls } from "./core.js";
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,14 +25,33 @@ export type ShareConfig = {
22
25
  debounceMs: number;
23
26
  completedDelayMs: number;
24
27
  modelLabel: string;
25
- ttlDays: number;
28
+ /** Default share lifetime in HOURS (Wave 3; default 2160 = 90d).
29
+ * Legacy ttlDays config/env is accepted and converted x24. */
30
+ ttlHours: number;
26
31
  authLogin: string;
27
32
  authPassword: string;
28
33
  spoolDir: string;
29
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";
30
39
  /** false silences the `[openshare] live:` (and `url file:`) stderr
31
40
  * lines. The share itself is unaffected. */
32
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>;
33
55
  shareMode?: ShareMode;
34
56
  };
35
57
  export declare const METADATA_KEY = "openshare";
@@ -40,6 +62,9 @@ export declare const DEFAULTS: ShareConfig;
40
62
  /** Directory of this module file (bundled standalone: the single file's
41
63
  * directory; package: dist/). */
42
64
  export declare function moduleDir(): string;
65
+ /** Version of the running plugin, from the package it was loaded from.
66
+ * "?" outside a package (single-file CI bundle with no sibling manifest). */
67
+ export declare function pluginVersion(): string;
43
68
  /** Nearest ancestor (starting at `start`) containing package.json. */
44
69
  export declare function packageRoot(start: string): string;
45
70
  /** Config file search paths for a plugin living at `pluginDir`.
@@ -64,9 +89,8 @@ export type WriteCandidate = {
64
89
  };
65
90
  /** Write-target candidates for the TUI picker: the global settings home
66
91
  * (always writable) plus every OTHER file in the read chain that already
67
- * exists on disk (deploy-time overrides are never created from the TUI —
68
- * owner rule). Order: global first (default), then the rest by read
69
- * precedence (highest first). */
92
+ * exists on disk, since the TUI never creates deploy-time overrides. Order:
93
+ * global first (default), then the rest by read precedence (highest first). */
70
94
  export declare function writeCandidates(pluginDir: string, env?: NodeJS.ProcessEnv): WriteCandidate[];
71
95
  /** Per-key source attribution mirroring loadConfig precedence exactly
72
96
  * (env > adjacent > package root > local walks > global > default).
@@ -77,15 +101,14 @@ export declare function configSourceMap(pluginDir: string, env?: NodeJS.ProcessE
77
101
  export declare function normalizeServers(raw: unknown): Record<string, ServerEntry> | undefined;
78
102
  /** Field-level merge of servers groups: entries only carry present fields
79
103
  * (normalizeServers), so a static `code` in plugin options and a persisted
80
- * `token` in the settings home combine instead of shadowing — per-URL
81
- * wholesale replacement would re-enroll on every boot (live-caught in
82
- * suite design review). */
104
+ * `token` in the settings home combine instead of shadowing. Per-URL
105
+ * wholesale replacement would re-enroll on every boot. */
83
106
  export declare function mergeServerGroups(base: Record<string, ServerEntry> | undefined, add: Record<string, ServerEntry>): Record<string, ServerEntry>;
84
107
  /** The server new sessions share to: the `active` entry (or the first).
85
- * Legacy workerUrl/createToken merge in as the implicit first entry — with
86
- * the root token ALSO filed as adminToken (the createToken IS root; filing
87
- * it only as `token` made the TUI offer user-actions like rotate to a root
88
- * holder — live-caught "can either be admin or user but not both"). */
108
+ * Legacy workerUrl/createToken merge in as the implicit first entry, with
109
+ * the root token ALSO filed as adminToken: the createToken IS root, and
110
+ * filing it only as `token` offers user-actions like rotate to a root
111
+ * holder, while an identity is either admin or user, not both. */
89
112
  export declare function resolveServer(cfg: ShareConfig): {
90
113
  url: string;
91
114
  token: string;
@@ -110,9 +133,9 @@ export declare function loadConfig(pluginDir: string, options?: Record<string, u
110
133
  export declare function spoolDirFor(cfg: ShareConfig): string;
111
134
  /** Does this plugin spec (tui.json entry) point at the openshare package?
112
135
  * Accepts the bare npm name, name@version, and the /tui (or any) subpath
113
- * form — older docs shipped the subpath spec and presence detection must
114
- * never silently miss it (all-mode would keep auto-sharing while the user
115
- * believes they installed manual mode — live-caught in review). */
136
+ * form. Older docs shipped the subpath spec, and presence detection must
137
+ * never silently miss it: all-mode would keep auto-sharing while the user
138
+ * believes they installed manual mode. */
116
139
  export declare function isOpenShareSpec(spec: unknown): spec is string;
117
140
  /** tui.json layers the TUI reads (mirrors the family selfwire path rules,
118
141
  * read-only). The walk runs from the session directory up to the worktree
package/dist/config.js CHANGED
@@ -35,11 +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,
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
  const CONFIG_NAME = "openshare.config.json";
@@ -49,6 +51,17 @@ const LOCAL_NAME = "openshare.local.json";
49
51
  export function moduleDir() {
50
52
  return dirname(fileURLToPath(import.meta.pathname ?? import.meta.url));
51
53
  }
54
+ /** Version of the running plugin, from the package it was loaded from.
55
+ * "?" outside a package (single-file CI bundle with no sibling manifest). */
56
+ export function pluginVersion() {
57
+ try {
58
+ const pkg = JSON.parse(readFileSync(join(packageRoot(moduleDir()), "package.json"), "utf8"));
59
+ return pkg.version ?? "?";
60
+ }
61
+ catch {
62
+ return "?";
63
+ }
64
+ }
52
65
  /** Nearest ancestor (starting at `start`) containing package.json. */
53
66
  export function packageRoot(start) {
54
67
  let current = start;
@@ -97,9 +110,8 @@ export function configHomePath(env = process.env) {
97
110
  }
98
111
  /** Write-target candidates for the TUI picker: the global settings home
99
112
  * (always writable) plus every OTHER file in the read chain that already
100
- * exists on disk (deploy-time overrides are never created from the TUI —
101
- * owner rule). Order: global first (default), then the rest by read
102
- * precedence (highest first). */
113
+ * exists on disk, since the TUI never creates deploy-time overrides. Order:
114
+ * global first (default), then the rest by read precedence (highest first). */
103
115
  export function writeCandidates(pluginDir, env = process.env) {
104
116
  const candidates = [{ path: configHomePath(env), label: "global settings (~/.config/opencode)", always: true }];
105
117
  const labels = [
@@ -135,10 +147,13 @@ export function configSourceMap(pluginDir, env = process.env) {
135
147
  debounceMs: env.OPENSHARE_DEBOUNCE_MS,
136
148
  completedDelayMs: env.OPENSHARE_COMPLETED_DELAY_MS,
137
149
  modelLabel: env.OPENSHARE_MODEL_LABEL,
150
+ ttlHours: env.OPENSHARE_TTL_HOURS,
138
151
  ttlDays: env.OPENSHARE_TTL_DAYS,
139
152
  authLogin: env.OPENSHARE_AUTH_LOGIN,
140
153
  authPassword: env.OPENSHARE_AUTH_PASSWORD,
141
154
  urlFile: env.OPENSHARE_URL_FILE,
155
+ urlFileFormat: env.OPENSHARE_URL_FILE_FORMAT,
156
+ configSet: env.OPENSHARE_CONFIG_SET,
142
157
  spoolDir: env.OPENSHARE_SPOOL_DIR,
143
158
  announceStderr: env.OPENSHARE_ANNOUNCE_STDERR,
144
159
  shareMode: env.OPENSHARE_SHARE_MODE,
@@ -147,6 +162,28 @@ export function configSourceMap(pluginDir, env = process.env) {
147
162
  if (value !== undefined && value !== "")
148
163
  sources[key] = "environment";
149
164
  }
165
+ // legacy days-shaped env feeds the same knob as ttlHours
166
+ if (sources.ttlDays && !sources.ttlHours)
167
+ sources.ttlHours = sources.ttlDays;
168
+ delete sources.ttlDays;
169
+ // OPENSHARE_CONFIG_JSON sits between env and the files: it loses only
170
+ // to a specific env var, so it overrides any file's key
171
+ if (env.OPENSHARE_CONFIG_JSON) {
172
+ try {
173
+ const raw = JSON.parse(env.OPENSHARE_CONFIG_JSON);
174
+ if (raw && typeof raw === "object") {
175
+ for (const [key, value] of Object.entries(raw)) {
176
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
177
+ continue;
178
+ if (sources[key] === "environment")
179
+ continue;
180
+ if (key === "servers" || known.has(key))
181
+ sources[key] = "OPENSHARE_CONFIG_JSON";
182
+ }
183
+ }
184
+ }
185
+ catch { }
186
+ }
150
187
  // configPaths() is ordered lowest-priority-first? No: index 0 is the
151
188
  // ADJACENT file (highest). Merge order reverses it; attribution walks
152
189
  // it as-is so the first hit is the winning (highest) source.
@@ -206,15 +243,16 @@ export function normalizeServers(raw) {
206
243
  entry.adminToken = e.adminToken;
207
244
  if (e.active === true)
208
245
  entry.active = true;
246
+ if (e.anonymous === true)
247
+ entry.anonymous = true;
209
248
  out[url] = entry;
210
249
  }
211
250
  return Object.keys(out).length ? out : undefined;
212
251
  }
213
252
  /** Field-level merge of servers groups: entries only carry present fields
214
253
  * (normalizeServers), so a static `code` in plugin options and a persisted
215
- * `token` in the settings home combine instead of shadowing — per-URL
216
- * wholesale replacement would re-enroll on every boot (live-caught in
217
- * suite design review). */
254
+ * `token` in the settings home combine instead of shadowing. Per-URL
255
+ * wholesale replacement would re-enroll on every boot. */
218
256
  export function mergeServerGroups(base, add) {
219
257
  const merged = { ...(base ?? {}) };
220
258
  for (const [url, entry] of Object.entries(add))
@@ -222,10 +260,10 @@ export function mergeServerGroups(base, add) {
222
260
  return merged;
223
261
  }
224
262
  /** The server new sessions share to: the `active` entry (or the first).
225
- * Legacy workerUrl/createToken merge in as the implicit first entry — with
226
- * the root token ALSO filed as adminToken (the createToken IS root; filing
227
- * it only as `token` made the TUI offer user-actions like rotate to a root
228
- * holder — live-caught "can either be admin or user but not both"). */
263
+ * Legacy workerUrl/createToken merge in as the implicit first entry, with
264
+ * the root token ALSO filed as adminToken: the createToken IS root, and
265
+ * filing it only as `token` offers user-actions like rotate to a root
266
+ * holder, while an identity is either admin or user, not both. */
229
267
  export function resolveServer(cfg) {
230
268
  const servers = {};
231
269
  if (cfg.workerUrl)
@@ -302,29 +340,58 @@ export function loadConfig(pluginDir, options) {
302
340
  const cfg = { ...DEFAULTS };
303
341
  const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
304
342
  const known = new Set([...Object.keys(DEFAULTS), "shareMode"]);
343
+ const applyObject = (raw) => {
344
+ if (!raw || typeof raw !== "object")
345
+ return;
346
+ for (const [key, value] of Object.entries(raw)) {
347
+ if (key === "servers") {
348
+ const servers = normalizeServers(value);
349
+ if (servers)
350
+ cfg.servers = mergeServerGroups(cfg.servers, servers);
351
+ continue;
352
+ }
353
+ if (key === "redact") {
354
+ if (value && typeof value === "object" && Array.isArray(value.patterns)) {
355
+ const pats = value.patterns.filter((x) => typeof x === "string" && x.length > 0 && x.length < 500).slice(0, 50);
356
+ cfg.redact = { patterns: pats };
357
+ }
358
+ continue;
359
+ }
360
+ if (key === "meta") {
361
+ // string-to-string only: meta rides the create frame verbatim
362
+ if (value && typeof value === "object" && !Array.isArray(value)) {
363
+ const clean = {};
364
+ for (const [mk, mv] of Object.entries(value)) {
365
+ if (typeof mv === "string" && mv.length <= 500 && mk.length <= 64)
366
+ clean[mk] = mv;
367
+ }
368
+ if (Object.keys(clean).length)
369
+ cfg.meta = { ...cfg.meta, ...clean };
370
+ }
371
+ continue;
372
+ }
373
+ if (key === "ttlDays") {
374
+ // legacy days-shaped file/options value converts to hours once
375
+ const d = Math.floor(Number(value));
376
+ if (Number.isFinite(d) && d > 0)
377
+ cfg.ttlHours = d * 24;
378
+ continue;
379
+ }
380
+ if (!known.has(key))
381
+ continue;
382
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
383
+ continue;
384
+ if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
385
+ continue;
386
+ cfg[key] = value;
387
+ }
388
+ };
305
389
  for (const path of [...configPaths(pluginDir)].reverse()) {
306
390
  try {
307
391
  // strict JSON: the $comment key (what our templates use) is valid
308
392
  // JSON; comment-stripping is deliberately avoided because a naive
309
393
  // stripper eats "//" inside workerUrl values
310
- const raw = JSON.parse(readFileSync(path, "utf8"));
311
- if (!raw || typeof raw !== "object")
312
- continue;
313
- for (const [key, value] of Object.entries(raw)) {
314
- if (key === "servers") {
315
- const servers = normalizeServers(value);
316
- if (servers)
317
- cfg.servers = mergeServerGroups(cfg.servers, servers);
318
- continue;
319
- }
320
- if (!known.has(key))
321
- continue;
322
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
323
- continue;
324
- if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
325
- continue;
326
- cfg[key] = value;
327
- }
394
+ applyObject(JSON.parse(readFileSync(path, "utf8")));
328
395
  }
329
396
  catch (e) {
330
397
  // a malformed config file is a real setup error the user must hear
@@ -353,6 +420,19 @@ export function loadConfig(pluginDir, options) {
353
420
  cfg.shareMode = mode;
354
421
  }
355
422
  const env = process.env;
423
+ // OPENSHARE_CONFIG_JSON: a complete config object in one env var. Sits
424
+ // between the file/options layers and the per-key OPENSHARE_* env (a
425
+ // specific env var still wins) - CI sets one variable instead of nine,
426
+ // and secrets never touch a file. Malformed JSON is a loud error, not
427
+ // silence (same rule as files).
428
+ if (env.OPENSHARE_CONFIG_JSON) {
429
+ try {
430
+ applyObject(JSON.parse(env.OPENSHARE_CONFIG_JSON));
431
+ }
432
+ catch (e) {
433
+ console.error("[openshare] OPENSHARE_CONFIG_JSON could not be parsed - ignored:", e?.message ?? e);
434
+ }
435
+ }
356
436
  if (env.OPENSHARE_WORKER_URL)
357
437
  cfg.workerUrl = env.OPENSHARE_WORKER_URL;
358
438
  if (env.OPENSHARE_CREATE_TOKEN)
@@ -363,14 +443,20 @@ export function loadConfig(pluginDir, options) {
363
443
  cfg.completedDelayMs = num(env.OPENSHARE_COMPLETED_DELAY_MS, cfg.completedDelayMs);
364
444
  if (env.OPENSHARE_MODEL_LABEL !== undefined)
365
445
  cfg.modelLabel = env.OPENSHARE_MODEL_LABEL;
446
+ if (env.OPENSHARE_TTL_HOURS)
447
+ cfg.ttlHours = num(env.OPENSHARE_TTL_HOURS, cfg.ttlHours);
366
448
  if (env.OPENSHARE_TTL_DAYS)
367
- cfg.ttlDays = num(env.OPENSHARE_TTL_DAYS, cfg.ttlDays);
449
+ cfg.ttlHours = Math.max(1, Math.floor(num(env.OPENSHARE_TTL_DAYS, 90) * 24));
368
450
  if (env.OPENSHARE_AUTH_LOGIN !== undefined)
369
451
  cfg.authLogin = env.OPENSHARE_AUTH_LOGIN;
370
452
  if (env.OPENSHARE_AUTH_PASSWORD !== undefined)
371
453
  cfg.authPassword = env.OPENSHARE_AUTH_PASSWORD;
372
454
  if (env.OPENSHARE_URL_FILE !== undefined)
373
455
  cfg.urlFile = env.OPENSHARE_URL_FILE;
456
+ if (env.OPENSHARE_URL_FILE_FORMAT === "json" || env.OPENSHARE_URL_FILE_FORMAT === "url")
457
+ cfg.urlFileFormat = env.OPENSHARE_URL_FILE_FORMAT;
458
+ if (env.OPENSHARE_CONFIG_SET)
459
+ cfg.configSet = env.OPENSHARE_CONFIG_SET;
374
460
  if (env.OPENSHARE_SPOOL_DIR)
375
461
  cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
376
462
  if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
@@ -421,9 +507,9 @@ function specPath(spec) {
421
507
  }
422
508
  /** Does this plugin spec (tui.json entry) point at the openshare package?
423
509
  * Accepts the bare npm name, name@version, and the /tui (or any) subpath
424
- * form — older docs shipped the subpath spec and presence detection must
425
- * never silently miss it (all-mode would keep auto-sharing while the user
426
- * believes they installed manual mode — live-caught in review). */
510
+ * form. Older docs shipped the subpath spec, and presence detection must
511
+ * never silently miss it: all-mode would keep auto-sharing while the user
512
+ * believes they installed manual mode. */
427
513
  export function isOpenShareSpec(spec) {
428
514
  if (typeof spec !== "string" || spec.length === 0)
429
515
  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,5 +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
+ };
117
128
  export declare function engineTargetUrls(): string[];
118
129
  export declare function createEngine(deps: EngineDeps): Engine;