@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.
package/dist/dialogs.d.ts CHANGED
@@ -11,11 +11,11 @@ export declare function setDialogHeightPercent(api: TuiPluginApi, value: number)
11
11
  * accounted); the estimator and the pre-wrapper both use this so heights
12
12
  * match what actually renders. */
13
13
  export declare function dialogContentWidth(api: TuiPluginApi): number;
14
- /** Deterministic character-level pre-wrap (parity lesson: opentui's
15
- * word wrap cannot break unbreakable tokens like a JSON policy blob, so
16
- * "wrapped" text still escaped the dialog border — live-caught). Both the
17
- * [i] pane and InfoDialog render THESE lines; the row estimate uses the
18
- * same output, so heights match what renders. */
14
+ /** Deterministic character-level pre-wrap: opentui's word wrap cannot
15
+ * break unbreakable tokens like a JSON policy blob, so "wrapped" text still
16
+ * escapes the dialog border. Both the [i] pane and InfoDialog render THESE
17
+ * lines; the row estimate uses the same output, so heights match what
18
+ * renders. */
19
19
  export declare function preWrap(text: string, width: number): string[];
20
20
  export type MenuOption<Value = string> = {
21
21
  title: string;
@@ -41,11 +41,10 @@ export declare function MenuDialog<Value>(props: {
41
41
  hint?: string;
42
42
  onDone: (value: Value | undefined) => void;
43
43
  }): JSX.Element;
44
- /** showMenuOnce, family discipline with the flicker fix (2026-10-19):
45
- * done() does NOT clear the host stack — the next dialog's replace() is an
46
- * atomic swap, so clearing first paints one empty frame (the esc-to-parent
47
- * flicker, live-caught). Chain ends clear explicitly (command exits,
48
- * settings exit). */
44
+ /** showMenuOnce, family discipline: done() does NOT clear the host stack,
45
+ * because the next dialog's replace() is an atomic swap and clearing first
46
+ * paints one empty frame (the esc-to-parent flicker). Chain ends clear
47
+ * explicitly (command exits, settings exit). */
49
48
  export declare function showMenuOnce<Value>(api: TuiPluginApi, props: {
50
49
  title: string;
51
50
  options: MenuOption<Value>[];
package/dist/dialogs.js CHANGED
@@ -142,11 +142,11 @@ function cappedHeight(count, max, min = 1) {
142
142
  function estimatedVisualRows(message, width) {
143
143
  return message.split(/\r?\n/).reduce((rows, line) => rows + Math.max(1, Math.ceil(line.length / Math.max(1, width))), 0);
144
144
  }
145
- /** Deterministic character-level pre-wrap (parity lesson: opentui's
146
- * word wrap cannot break unbreakable tokens like a JSON policy blob, so
147
- * "wrapped" text still escaped the dialog border — live-caught). Both the
148
- * [i] pane and InfoDialog render THESE lines; the row estimate uses the
149
- * same output, so heights match what renders. */
145
+ /** Deterministic character-level pre-wrap: opentui's word wrap cannot
146
+ * break unbreakable tokens like a JSON policy blob, so "wrapped" text still
147
+ * escapes the dialog border. Both the [i] pane and InfoDialog render THESE
148
+ * lines; the row estimate uses the same output, so heights match what
149
+ * renders. */
150
150
  export function preWrap(text, width) {
151
151
  const out = [];
152
152
  for (const raw of text.split(/\r?\n/)) {
@@ -200,10 +200,9 @@ function InfoLines(props) {
200
200
  return line.length === 0 ? (_jsx("text", { children: " " })) : (_jsx("text", { fg: colorOf(attrs.fg), wrapMode: "none", children: attrs.bold ? _jsx("b", { children: line }) : line }));
201
201
  } }) }));
202
202
  }
203
- /** title column cap (family menuTitleWidth, live-caught need: a server URL
204
- * as a row title stretched the column toward half the dialog — the owner
205
- * capped it further: full URLs belong in the [i] pane, not the row).
206
- * Capped + ellipsized. */
203
+ /** Title column cap (family menuTitleWidth): a server URL as a row title
204
+ * would otherwise stretch the column toward half the dialog. Full URLs
205
+ * belong in the [i] pane, not the row. Capped and ellipsized. */
207
206
  function menuTitleWidth(width, options) {
208
207
  const longest = Math.max(0, ...options.map((option) => option.title.length)) + 2;
209
208
  if (width === "xlarge")
@@ -217,13 +216,12 @@ function ellipsize(text, width) {
217
216
  }
218
217
  /** OpenShare visual frame: rounded accent border + accent title row. The
219
218
  * logic inside the menus stays the ported family pattern; this wrapper is
220
- * purely presentation (owner-preferred styling). Natural width — the host
221
- * dialog panel provides the container; the border hugs the content. */
222
- // Frame note (live-caught): the HOST dialog panel renders plugin content
223
- // with paddingTop=1 (opencode dialog.tsx), so a BORDERED frame always
224
- // showed a dead panel strip above its top border. Borderless family
225
- // shape + a solid accent rule under the title: zero dead space, accent
226
- // identity kept.
219
+ * purely presentation. Natural width: the host dialog panel provides the
220
+ * container and the border hugs the content. */
221
+ // Frame note: the HOST dialog panel renders plugin content with
222
+ // paddingTop=1 (opencode dialog.tsx), so a BORDERED frame always shows a
223
+ // dead panel strip above its top border. Borderless family shape plus a
224
+ // solid accent rule under the title: zero dead space, accent identity kept.
227
225
  function DialogFrame(props) {
228
226
  const theme = () => props.api.theme.current;
229
227
  return (_jsxs("box", { flexDirection: "column", width: "100%", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { width: "100%", marginBottom: 1, children: _jsx("text", { fg: theme().accent, children: _jsx("b", { children: props.title }) }) }), _jsx("box", { width: "100%", height: 1, backgroundColor: theme().accent, marginBottom: 1 }), props.children] }));
@@ -243,8 +241,8 @@ export function MenuDialog(props) {
243
241
  const dimensions = useTerminalDimensions();
244
242
  const listHeight = createMemo(() => cappedHeight(props.options.length, maxRows(props.api, dimensions().height, 12, 4), 4));
245
243
  // fixed title column shared by every row (family menuTitleWidth): flex
246
- // columns stagger per-row because flex splits by content — a computed
247
- // width is what makes the value/description column align (live-caught).
244
+ // columns stagger per row because flex splits by content, so a computed
245
+ // width is what makes the value/description column align.
248
246
  const width = dialogWidthOf(props.api);
249
247
  const titleWidth = createMemo(() => menuTitleWidth(width, props.options));
250
248
  const hasHelp = createMemo(() => props.options.some((option) => option.help));
@@ -272,8 +270,8 @@ export function MenuDialog(props) {
272
270
  });
273
271
  // [i] pane sections: "## Header" lines in help split sections; the pane
274
272
  // height comes from the CONTENT (pre-wrapped rows), capped by the same
275
- // dialog metrics — no longer bound to the spawning menu's list height
276
- // (live-caught: a 3-row menu gave a 3-row info pane).
273
+ // dialog metrics rather than the spawning menu's list height (which gave
274
+ // a 3-row menu a 3-row info pane).
277
275
  const helpSections = createMemo(() => {
278
276
  const raw = current()?.help ?? "";
279
277
  const sections = [{ title: "", lines: [] }];
@@ -465,11 +463,10 @@ function claimDialog(api) {
465
463
  pendingDialog = release;
466
464
  return release;
467
465
  }
468
- /** showMenuOnce, family discipline with the flicker fix (2026-10-19):
469
- * done() does NOT clear the host stack — the next dialog's replace() is an
470
- * atomic swap, so clearing first paints one empty frame (the esc-to-parent
471
- * flicker, live-caught). Chain ends clear explicitly (command exits,
472
- * settings exit). */
466
+ /** showMenuOnce, family discipline: done() does NOT clear the host stack,
467
+ * because the next dialog's replace() is an atomic swap and clearing first
468
+ * paints one empty frame (the esc-to-parent flicker). Chain ends clear
469
+ * explicitly (command exits, settings exit). */
473
470
  export function showMenuOnce(api, props) {
474
471
  return new Promise((resolve) => {
475
472
  let settled = false;
@@ -579,9 +576,9 @@ export function showAlertMenu(api, props) {
579
576
  return showInfoDialog(api, { title: props.title, message: props.message }).then(() => undefined);
580
577
  }
581
578
  // --- prompt (text entry — the one custom piece) -------------------------------
582
- // Full printable ASCII (parity fix, live-caught class: the lowercase-only
583
- // set made uppercase CREATE_TOKENs and codes UNTYPEABLE). Space is bound
584
- // separately below; escape is the back binding, so it's excluded.
579
+ // Full printable ASCII: a lowercase-only set makes uppercase
580
+ // CREATE_TOKENs and codes UNTYPEABLE. Space is bound separately below;
581
+ // escape is the back binding, so it is excluded.
585
582
  const PROMPT_CHARS = [
586
583
  ..."abcdefghijklmnopqrstuvwxyz",
587
584
  ..."ABCDEFGHIJKLMNOPQRSTUVWXYZ",
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Client-only redaction + light path transform (Wave 3 trust).
3
+ *
4
+ * Owner verdict: the server stays dumb - the client controls what leaves
5
+ * the machine. This pass runs once at enqueue (the single choke point
6
+ * every item crosses before any transport) on UNGATED shares only: a
7
+ * share with login+pass is private from casual observers already, and
8
+ * redacting there would corrupt content the server owner can see anyway
9
+ * (documented caveat: NOT private from the server owner until Wave 3b
10
+ * encryption lands).
11
+ *
12
+ * Extending: add a detector to BUILTINS (fork-friendly registry) or set
13
+ * `redact.patterns` in the config (regex source strings, global flag).
14
+ */
15
+ /** A detector: stable `kind` names the marker, e.g. `[redacted:aws-key]`. */
16
+ export type RedactRule = {
17
+ kind: string;
18
+ re: RegExp;
19
+ };
20
+ export declare const BUILTINS: RedactRule[];
21
+ export type RedactionHit = {
22
+ kind: string;
23
+ sample: string;
24
+ };
25
+ /** Home-directory prefix -> `~`; applied consistently to session.directory
26
+ * AND tool path fields so viewer-side relativization still works (both
27
+ * ends share the ~-prefixed form). */
28
+ export declare function tildePath(p: string, home: string): string;
29
+ type Opts = {
30
+ rules: RedactRule[];
31
+ home: string;
32
+ enabled: boolean;
33
+ };
34
+ /**
35
+ * Apply redaction (when enabled) + the light path transform to one
36
+ * contract item. Returns the ORIGINAL untouched when nothing matched
37
+ * (zero-copy fast path for clean sessions); otherwise a mutated deep
38
+ * clone - the host's live object is never touched.
39
+ */
40
+ export declare function redactItem(item: unknown, opts: Opts): {
41
+ item: unknown;
42
+ hits: RedactionHit[];
43
+ };
44
+ /** All active rules (built-ins + compiled user patterns). */
45
+ export declare function activeRules(userPatterns: unknown): RedactRule[];
46
+ export {};
package/dist/redact.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Client-only redaction + light path transform (Wave 3 trust).
3
+ *
4
+ * Owner verdict: the server stays dumb - the client controls what leaves
5
+ * the machine. This pass runs once at enqueue (the single choke point
6
+ * every item crosses before any transport) on UNGATED shares only: a
7
+ * share with login+pass is private from casual observers already, and
8
+ * redacting there would corrupt content the server owner can see anyway
9
+ * (documented caveat: NOT private from the server owner until Wave 3b
10
+ * encryption lands).
11
+ *
12
+ * Extending: add a detector to BUILTINS (fork-friendly registry) or set
13
+ * `redact.patterns` in the config (regex source strings, global flag).
14
+ */
15
+ export const BUILTINS = [
16
+ { kind: "aws-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
17
+ { kind: "gh-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g },
18
+ { kind: "gh-token", re: /\bgithub_pat_[A-Za-z0-9_]{22,255}\b/g },
19
+ { kind: "api-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
20
+ { kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
21
+ { kind: "google-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
22
+ { kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/gi },
23
+ // PEM private-key blocks (whole block)
24
+ { kind: "private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
25
+ // .env style assignments for secret-shaped names
26
+ { 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 },
27
+ // URL connection strings with embedded passwords (password masked)
28
+ { kind: "conn-string", re: /\b[a-z][a-z0-9+.-]*:\/\/[^:@/\s"']+:[^@/\s"']{4,}@/gi },
29
+ ];
30
+ /** Config-supplied rules (regex source strings, applied globally). */
31
+ function compile(patterns) {
32
+ if (!Array.isArray(patterns))
33
+ return [];
34
+ const out = [];
35
+ for (const p of patterns) {
36
+ if (typeof p !== "string" || !p)
37
+ continue;
38
+ try {
39
+ out.push({ kind: "user", re: new RegExp(p, "g") });
40
+ }
41
+ catch {
42
+ // a broken user regex must never kill sharing - skip it
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ /** Home-directory prefix -> `~`; applied consistently to session.directory
48
+ * AND tool path fields so viewer-side relativization still works (both
49
+ * ends share the ~-prefixed form). */
50
+ export function tildePath(p, home) {
51
+ if (!p || !home || p.length <= home.length)
52
+ return p;
53
+ const norm = (s) => s.replace(/[\\/]+/g, "/").replace(/\/$/, "");
54
+ const np = norm(p);
55
+ const nh = norm(home);
56
+ if (np === nh)
57
+ return "~";
58
+ if (np.startsWith(nh + "/"))
59
+ return "~" + np.slice(nh.length);
60
+ return p;
61
+ }
62
+ const PATH_KEYS = new Set(["file", "path", "filePath", "cwd", "directory", "root"]);
63
+ /**
64
+ * Apply redaction (when enabled) + the light path transform to one
65
+ * contract item. Returns the ORIGINAL untouched when nothing matched
66
+ * (zero-copy fast path for clean sessions); otherwise a mutated deep
67
+ * clone - the host's live object is never touched.
68
+ */
69
+ export function redactItem(item, opts) {
70
+ const flat = JSON.stringify(item) ?? "null";
71
+ const needRedact = opts.enabled && opts.rules.some((r) => { r.re.lastIndex = 0; return r.re.test(flat); });
72
+ const needPaths = !!opts.home && flat.includes(opts.home);
73
+ if (!needRedact && !needPaths)
74
+ return { item, hits: [] };
75
+ const clone = JSON.parse(flat);
76
+ const stats = { hits: [], changed: false };
77
+ walk(clone, opts, stats);
78
+ return { item: clone, hits: stats.hits };
79
+ }
80
+ function walk(node, opts, stats, key) {
81
+ if (Array.isArray(node)) {
82
+ for (let i = 0; i < node.length; i++) {
83
+ const v = node[i];
84
+ if (typeof v === "string")
85
+ node[i] = applyString(v, opts, stats, key);
86
+ else
87
+ walk(v, opts, stats, key);
88
+ }
89
+ return;
90
+ }
91
+ if (node && typeof node === "object") {
92
+ for (const [k, v] of Object.entries(node)) {
93
+ if (typeof v === "string")
94
+ node[k] = applyString(v, opts, stats, k);
95
+ else
96
+ walk(v, opts, stats, k);
97
+ }
98
+ }
99
+ }
100
+ function applyString(v, opts, stats, key) {
101
+ let out = v;
102
+ if (opts.enabled) {
103
+ for (const r of opts.rules) {
104
+ r.re.lastIndex = 0;
105
+ out = out.replace(r.re, (full) => {
106
+ if (stats.hits.length < 8 && !stats.hits.some((h) => h.kind === r.kind)) {
107
+ stats.hits.push({ kind: r.kind, sample: full.slice(0, 60) });
108
+ }
109
+ stats.changed = true;
110
+ if (r.kind === "conn-string") {
111
+ // keep scheme://user, mask the password, keep @host
112
+ const at = full.lastIndexOf("@");
113
+ const colon = full.indexOf(":", full.indexOf("://") + 3) + 1;
114
+ if (at > colon && colon > 0)
115
+ return full.slice(0, colon) + "[redacted:conn-string]" + full.slice(at);
116
+ }
117
+ return `[redacted:${r.kind}]`;
118
+ });
119
+ }
120
+ }
121
+ if (key && PATH_KEYS.has(key) && opts.home && out.includes(opts.home)) {
122
+ out = tildePath(out, opts.home);
123
+ stats.changed = true;
124
+ }
125
+ return out;
126
+ }
127
+ /** All active rules (built-ins + compiled user patterns). */
128
+ export function activeRules(userPatterns) {
129
+ return [...BUILTINS, ...compile(userPatterns)];
130
+ }
@@ -1,8 +1,12 @@
1
1
  import type { ShareHostApi } from "./tui-host.js";
2
- /** Connect (add + enroll) a server — the /share first-run flow and the
2
+ /** Connect (add + enroll) a server - the /share first-run flow and the
3
3
  * settings "Connect to a server" row share it. Prompts the URL, probes
4
- * the auth mode, prompts a code for private servers, enrolls, persists
5
- * the token and makes it the active server. */
4
+ * the auth mode, then ASKS ONCE: onboard to an account, or stay anonymous
5
+ * (public servers only; anonymous shares sit under the server's global
6
+ * caps). Onboarding detects whether a code is needed and only then prompts
7
+ * for one (a code already in config is used without asking). The outcome is
8
+ * remembered per server: a token means onboarded, an anonymous flag means
9
+ * declined. */
6
10
  export declare function connectServerFlow(host: ShareHostApi): Promise<{
7
11
  ok: boolean;
8
12
  message: string;
@@ -13,10 +17,9 @@ export declare function connectServerFlow(host: ShareHostApi): Promise<{
13
17
  * per-server submenu (Admin panel); pinnedUrl skips the server picker. */
14
18
  export declare function adminConsole(host: ShareHostApi, pinnedUrl?: string): Promise<void>;
15
19
  /** The settings dialog: a compact ROOT of group entries (with live
16
- * summaries) opening focused submenus. Two-level by design — one flat
17
- * ~25-row dump was unreadable (owner-directed regroup 2026-10-19).
18
- * Save semantics unchanged: root Exit saves when dirty; submenu Back
19
- * just returns. */
20
+ * summaries) opening focused submenus. Two-level by design: one flat ~25-row
21
+ * dump is unreadable. Save semantics unchanged: root Exit saves when dirty;
22
+ * submenu Back just returns. */
20
23
  export declare function settingsDialog(host: ShareHostApi, startAtSession?: string, startGroup?: "session"): Promise<void>;
21
24
  /** Onboarding shown by /share when no server is connected: connect flow
22
25
  * first (URL + optional code = enrolled + active), full settings as the