@volter/teams 0.2.19 → 0.2.21

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.
@@ -0,0 +1,80 @@
1
+ // A home's board owner (`supercode workflow serve`, one per install: D152) answers every card verb on that home's boards
2
+ // over one local socket. The CLI and the machine daemon's board door hold nothing: each sends the verb here and renders
3
+ // the answer. The socket is the home's own (`<home>/board.sock`, beside the owner's lock `<home>/board.lock`), by the
4
+ // home's real path, so every process naming the home reaches the one owner whatever its environment.
5
+ //
6
+ // request: one JSON line { v: 1, argv | tool, board, caller, session, facts, cwd, stdin }
7
+ // argv the verb and its arguments, without --root or --board
8
+ // tool a dispatched worker's board tool instead of a verb: { name, args, env } (its HERMES_KANBAN_*
9
+ // task, run and board, and HERMES_PROFILE); answered with the tool's JSON as stdout
10
+ // board the board's slug
11
+ // caller the calling session's address when its side named it (`sc:<machine>:<harness>:<id>`), or null
12
+ // session { id, harness } when the caller is a session on the owner's machine that the CLI named
13
+ // facts what the caller's environment says of it (SUPERCODE_AGENT_ID, SUPERCODE_AUTOMATION,
14
+ // SUPERCODE_CONTEXT, HERMES_PROFILE, USER, SUPERCODE_CALLER, and a launched worker's
15
+ // SUPERCODE_RUN_LAUNCH: the owner's secret for its run, which only run-session reads)
16
+ // cwd the caller's working directory, against which a path it names is read
17
+ // stdin the caller's standard input, when it names `-` as a file (else null)
18
+ // answer: one JSON line { code, stdout, stderr }
19
+ import { realpathSync } from 'node:fs';
20
+ import { connect } from 'node:net';
21
+ import { join, resolve } from 'node:path';
22
+
23
+ /** A Unix socket's path is at most 104 bytes on macOS (108 on Linux), terminator included. */
24
+ const SOCKET_PATH_MAX = 103;
25
+
26
+ /** The home's real path. */
27
+ export function boardHome(root) {
28
+ try { return realpathSync(resolve(root)); } catch { return resolve(root); }
29
+ }
30
+
31
+ /** The owner's socket for the home at `root`: the home's own, by its real path. */
32
+ export function boardOwnerSocket(root) {
33
+ const path = join(boardHome(root), 'board.sock');
34
+ if (Buffer.byteLength(path) > SOCKET_PATH_MAX) throw new Error(`the home's board socket ${path} is longer than a Unix socket path may be (${SOCKET_PATH_MAX} bytes); keep the home at a shorter path`);
35
+ return path;
36
+ }
37
+
38
+ /**
39
+ * The verbs that are a board's own operation, run on its machine as acts of their own and never sent to its owner or
40
+ * through the board door: the owner itself and its one-shot round, init, adoption, the board list, housekeeping and
41
+ * repair, migration, the dispatching switch, and the verbs that hold the call open (watch, tail, attach, events).
42
+ */
43
+ export const BOARD_OPERATION_VERBS = new Set(['serve', 'daemon', 'dispatch', 'dispatching', 'init', 'boards', 'gc', 'repair', 'migrate', 'watch', 'tail', 'events']);
44
+
45
+ /** The facts of a caller's environment the owner reads, and nothing else of it. */
46
+ export const CALLER_FACTS = ['SUPERCODE_AGENT_ID', 'SUPERCODE_AUTOMATION', 'SUPERCODE_CONTEXT', 'HERMES_PROFILE', 'USER', 'SUPERCODE_CALLER', 'SUPERCODE_RUN_LAUNCH'];
47
+ export const callerFacts = (env = process.env) => Object.fromEntries(CALLER_FACTS.filter((key) => env[key]).map((key) => [key, env[key]]));
48
+
49
+ /**
50
+ * Send one verb to the owner of the home at `root`: answers `{ code, stdout, stderr }`. A home whose owner is not
51
+ * running answers that, with what starts it; nothing else runs the verb.
52
+ */
53
+ export function askBoardOwner(root, request) {
54
+ let path;
55
+ try { path = boardOwnerSocket(root); } catch (error) { return Promise.resolve({ code: 1, stdout: '', stderr: `${error.message}\n` }); }
56
+ return new Promise((done) => {
57
+ let answer = '';
58
+ let settled = false;
59
+ const finish = (value) => { if (!settled) { settled = true; done(value); } };
60
+ const socket = connect(path);
61
+ socket.setEncoding('utf8');
62
+ // The request is one line, and the connection stays open both ways until the answer's line has come: an answer that
63
+ // takes its time (a Teams lookup, a reread home) is never lost to a closed side.
64
+ socket.on('connect', () => socket.write(`${JSON.stringify({ v: 1, ...request })}\n`));
65
+ socket.on('data', (chunk) => {
66
+ answer += chunk;
67
+ const end = answer.indexOf('\n');
68
+ if (end < 0) return;
69
+ socket.end();
70
+ try { finish(JSON.parse(answer.slice(0, end))); } catch { finish({ code: 1, stdout: '', stderr: `the board owner of ${root} answered nothing readable\n` }); }
71
+ });
72
+ socket.on('close', () => finish({ code: 1, stdout: '', stderr: `the board owner of ${root} closed the call before answering\n` }));
73
+ socket.on('error', (error) => {
74
+ const down = ['ENOENT', 'ECONNREFUSED'].includes(error.code);
75
+ finish({ code: 1, stdout: '', stderr: down
76
+ ? `the boards of ${root} have no running owner: \`supercode workflow serve --root ${root}\` is it (an install's keeper runs it)\n`
77
+ : `the board owner of ${root} could not be reached: ${error.message}\n` });
78
+ });
79
+ });
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
@@ -54,7 +54,8 @@
54
54
  "default": "./redact.mjs"
55
55
  },
56
56
  "./checkout": "./checkout.mjs",
57
- "./board-name": "./board-name.mjs"
57
+ "./board-name": "./board-name.mjs",
58
+ "./board-owner": "./board-owner.mjs"
58
59
  },
59
60
  "files": [
60
61
  "LICENSE-MIT",
package/redact.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  // - the values this machine keeps in its custody files (KEY=value lines), which no detector knows; the files are named
4
4
  // in `<supercode home>/custody.json` ({ "files": [...] }) and in SUPERCODE_CUSTODY_FILES (path-separated);
5
5
  // - gitleaks' default rule set, vendored unchanged as redact/gitleaks-rules.json (its source and sha256 inside): each
6
- // rule's keywords gate it, its secret group is what is replaced, and its entropy floor is kept.
6
+ // rule's keywords gate it, its secret group is what is replaced, and its entropy floor is kept;
7
+ // - the tokens a Teams server mints itself (TEAM_TOKENS), which no gitleaks rule knows.
7
8
  // Local files are never touched; only what is sent is redacted.
8
9
  import { readFileSync, existsSync, statSync } from 'node:fs';
9
10
  import { createHash } from 'node:crypto';
@@ -103,6 +104,13 @@ function longestMatch(source) {
103
104
  const RULES = vendored.rules.map((rule) => { const regex = jsPattern(rule.regex); return { id: rule.id, regex, longest: regex ? longestMatch(regex.source) : Infinity, group: rule.secretGroup ?? null, keywords: (rule.keywords ?? []).map((k) => k.toLowerCase()), entropy: rule.entropy ?? null }; }).filter((rule) => rule.regex);
104
105
  export const RULESET = { source: vendored.source, sha256: vendored.sha256, rules: RULES.length };
105
106
 
107
+ // The tokens a Teams server mints (sdk/volter-teams/server, sdk/teams/server): a credential `sct_<id>.<32 bytes>`
108
+ // (`sct_cred_<hex>.…`, the one three Codex session copies uploaded on 2026-09-30/10-01 carried, purged under D126), an
109
+ // invitation or install code `sci_[<id>.]<32 bytes>`, an app secret `sca_<32 bytes>`, an attachment token
110
+ // `sat_<id>.<32 bytes>`, each base64url, and a webhook signing secret `whsec_<32 bytes, base64>`. Each is replaced
111
+ // whole, by a marker naming its kind.
112
+ const TEAM_TOKENS = /\b(sct|sci|sca|sat)_(?:[A-Za-z]+_[0-9a-f]{32}\.)?[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])|\b(whsec)_[A-Za-z0-9+/]{43}=/g;
113
+
106
114
  // Where each of many literals occurs, in one pass over the text (gitleaks finds its keywords the same way, with one
107
115
  // automaton): a lookahead finds every position some literal starts at, and only those are compared literal by literal.
108
116
  // Hundreds of separate includes() read a 5 MB tool result hundreds of times.
@@ -182,6 +190,7 @@ export function redactText(text, env = process.env) {
182
190
  // Custody values are random, so one alternation of them gives the regex engine nothing to skip on: each is looked
183
191
  // for on its own, which reads a 5 MB text in a few dozen milliseconds.
184
192
  for (const { value, name } of custodyValues(env)) if (out.includes(value)) out = out.split(value).join(`[redacted:${name}]`);
193
+ if (/(?:sct|sci|sca|sat|whsec)_/.test(out)) out = out.replace(TEAM_TOKENS, (_, kind, hook) => `[redacted:teams-${kind ?? hook}-token]`);
185
194
  // Every rule has keywords, so a text holding none of them is done (the one test is cheap; finding where each keyword
186
195
  // occurs is not, and most texts are short and hold none).
187
196
  let lower = out.toLowerCase();