tryharness 0.7.23 → 0.7.26

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.
Files changed (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/screens.mjs CHANGED
@@ -11,26 +11,47 @@ const CELL = 16;
11
11
  const CELL_DIFF = 6;
12
12
  const MIN_OVERLAP = 0.3;
13
13
 
14
- const luminance = (data, i) => 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
14
+ const luminance = (data, i) =>
15
+ 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
15
16
  const overlap = (a, b) => {
16
- const w = Math.max(0, Math.min(a[2], b[2]) - Math.max(a[0], b[0])), h = Math.max(0, Math.min(a[3], b[3]) - Math.max(a[1], b[1]));
17
+ const w = Math.max(0, Math.min(a[2], b[2]) - Math.max(a[0], b[0])),
18
+ h = Math.max(0, Math.min(a[3], b[3]) - Math.max(a[1], b[1]));
17
19
  const area = (box) => Math.max(1, (box[2] - box[0]) * (box[3] - box[1]));
18
20
  return (w * h) / Math.min(area(a), area(b));
19
21
  };
20
22
 
21
23
  /** Changed regions of two same-size images: [{ box: [x0, y0, x1, y1], share }], largest first. */
22
- export function changedRegions(before, after, { cell = CELL, threshold = CELL_DIFF } = {}) {
23
- const cols = Math.ceil(before.width / cell), rows = Math.ceil(before.height / cell);
24
+ export function changedRegions(
25
+ before,
26
+ after,
27
+ { cell = CELL, threshold = CELL_DIFF } = {}
28
+ ) {
29
+ const cols = Math.ceil(before.width / cell),
30
+ rows = Math.ceil(before.height / cell);
24
31
  const changed = new Uint8Array(cols * rows);
25
- for (let cy = 0; cy < rows; cy += 1) for (let cx = 0; cx < cols; cx += 1) {
26
- let sum = 0, count = 0;
27
- for (let y = cy * cell; y < Math.min(before.height, (cy + 1) * cell); y += 1) for (let x = cx * cell; x < Math.min(before.width, (cx + 1) * cell); x += 1) {
28
- const i = (y * before.width + x) * 4;
29
- sum += Math.abs(luminance(before.data, i) - luminance(after.data, i)) + Math.abs(before.data[i] - after.data[i]) / 3 + Math.abs(before.data[i + 2] - after.data[i + 2]) / 3;
30
- count += 1;
32
+ for (let cy = 0; cy < rows; cy += 1)
33
+ for (let cx = 0; cx < cols; cx += 1) {
34
+ let sum = 0,
35
+ count = 0;
36
+ for (
37
+ let y = cy * cell;
38
+ y < Math.min(before.height, (cy + 1) * cell);
39
+ y += 1
40
+ )
41
+ for (
42
+ let x = cx * cell;
43
+ x < Math.min(before.width, (cx + 1) * cell);
44
+ x += 1
45
+ ) {
46
+ const i = (y * before.width + x) * 4;
47
+ sum +=
48
+ Math.abs(luminance(before.data, i) - luminance(after.data, i)) +
49
+ Math.abs(before.data[i] - after.data[i]) / 3 +
50
+ Math.abs(before.data[i + 2] - after.data[i + 2]) / 3;
51
+ count += 1;
52
+ }
53
+ if (sum / count > threshold) changed[cy * cols + cx] = 1;
31
54
  }
32
- if (sum / count > threshold) changed[cy * cols + cx] = 1;
33
- }
34
55
  // Touching changed cells become one region.
35
56
  const regions = [];
36
57
  const seen = new Uint8Array(cols * rows);
@@ -38,16 +59,55 @@ export function changedRegions(before, after, { cell = CELL, threshold = CELL_DI
38
59
  if (!changed[start] || seen[start]) continue;
39
60
  const stack = [start];
40
61
  seen[start] = 1;
41
- let x0 = Infinity, y0 = Infinity, x1 = -1, y1 = -1, cells = 0;
62
+ let x0 = Infinity,
63
+ y0 = Infinity,
64
+ x1 = -1,
65
+ y1 = -1,
66
+ cells = 0;
42
67
  while (stack.length) {
43
- const at = stack.pop(), cx = at % cols, cy = (at - cx) / cols;
44
- cells += 1; x0 = Math.min(x0, cx); y0 = Math.min(y0, cy); x1 = Math.max(x1, cx); y1 = Math.max(y1, cy);
45
- for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [-1, -1], [1, -1], [-1, 1]]) {
46
- const nx = cx + dx, ny = cy + dy, next = ny * cols + nx;
47
- if (nx >= 0 && ny >= 0 && nx < cols && ny < rows && changed[next] && !seen[next]) { seen[next] = 1; stack.push(next); }
68
+ const at = stack.pop(),
69
+ cx = at % cols,
70
+ cy = (at - cx) / cols;
71
+ cells += 1;
72
+ x0 = Math.min(x0, cx);
73
+ y0 = Math.min(y0, cy);
74
+ x1 = Math.max(x1, cx);
75
+ y1 = Math.max(y1, cy);
76
+ for (const [dx, dy] of [
77
+ [1, 0],
78
+ [-1, 0],
79
+ [0, 1],
80
+ [0, -1],
81
+ [1, 1],
82
+ [-1, -1],
83
+ [1, -1],
84
+ [-1, 1],
85
+ ]) {
86
+ const nx = cx + dx,
87
+ ny = cy + dy,
88
+ next = ny * cols + nx;
89
+ if (
90
+ nx >= 0 &&
91
+ ny >= 0 &&
92
+ nx < cols &&
93
+ ny < rows &&
94
+ changed[next] &&
95
+ !seen[next]
96
+ ) {
97
+ seen[next] = 1;
98
+ stack.push(next);
99
+ }
48
100
  }
49
101
  }
50
- regions.push({ box: [x0 * cell, y0 * cell, Math.min(before.width, (x1 + 1) * cell), Math.min(before.height, (y1 + 1) * cell)], share: Math.round((cells / (cols * rows)) * 10_000) / 10_000 });
102
+ regions.push({
103
+ box: [
104
+ x0 * cell,
105
+ y0 * cell,
106
+ Math.min(before.width, (x1 + 1) * cell),
107
+ Math.min(before.height, (y1 + 1) * cell),
108
+ ],
109
+ share: Math.round((cells / (cols * rows)) * 10_000) / 10_000,
110
+ });
51
111
  }
52
112
  return regions.sort((a, b) => b.share - a.share);
53
113
  }
@@ -55,16 +115,40 @@ export function changedRegions(before, after, { cell = CELL, threshold = CELL_DI
55
115
  /** Lines of text read before and after, paired by where they sit: { changed: [{ before, after, box }], moved, removed, added }. */
56
116
  export function textChanges(beforeLines, afterLines) {
57
117
  const used = new Set();
58
- const changed = [], removed = [];
118
+ const changed = [],
119
+ removed = [];
59
120
  for (const line of beforeLines) {
60
- const same = afterLines.findIndex((other, i) => !used.has(i) && other.text === line.text && overlap(line.box, other.box) >= MIN_OVERLAP);
61
- if (same >= 0) { used.add(same); continue; }
62
- let best = -1, bestOverlap = MIN_OVERLAP;
63
- afterLines.forEach((other, i) => { const o = overlap(line.box, other.box); if (!used.has(i) && o >= bestOverlap) { best = i; bestOverlap = o; } });
64
- if (best >= 0) { used.add(best); changed.push({ before: line.text, after: afterLines[best].text, box: afterLines[best].box }); }
65
- else removed.push({ text: line.text, box: line.box });
121
+ const same = afterLines.findIndex(
122
+ (other, i) =>
123
+ !used.has(i) &&
124
+ other.text === line.text &&
125
+ overlap(line.box, other.box) >= MIN_OVERLAP
126
+ );
127
+ if (same >= 0) {
128
+ used.add(same);
129
+ continue;
130
+ }
131
+ let best = -1,
132
+ bestOverlap = MIN_OVERLAP;
133
+ afterLines.forEach((other, i) => {
134
+ const o = overlap(line.box, other.box);
135
+ if (!used.has(i) && o >= bestOverlap) {
136
+ best = i;
137
+ bestOverlap = o;
138
+ }
139
+ });
140
+ if (best >= 0) {
141
+ used.add(best);
142
+ changed.push({
143
+ before: line.text,
144
+ after: afterLines[best].text,
145
+ box: afterLines[best].box,
146
+ });
147
+ } else removed.push({ text: line.text, box: line.box });
66
148
  }
67
- const added = afterLines.filter((_, i) => !used.has(i)).map((line) => ({ text: line.text, box: line.box }));
149
+ const added = afterLines
150
+ .filter((_, i) => !used.has(i))
151
+ .map((line) => ({ text: line.text, box: line.box }));
68
152
  // The same words gone from one place and new in another moved.
69
153
  const moved = [];
70
154
  for (const line of [...removed]) {
@@ -78,19 +162,51 @@ export function textChanges(beforeLines, afterLines) {
78
162
  }
79
163
 
80
164
  /** Compares two screenshots (PNG or JPEG bytes): { size, regions, text, summary }. */
81
- export async function compareScreens(beforeBytes, afterBytes, { ocr, lane = "interactive" }) {
82
- const before = decodeImage(beforeBytes), after = decodeImage(afterBytes);
83
- if (!before || !after) throw new Error("both screenshots must be PNG or JPEG");
84
- const sameSize = before.width === after.width && before.height === after.height;
165
+ export async function compareScreens(
166
+ beforeBytes,
167
+ afterBytes,
168
+ { ocr, lane = "interactive" }
169
+ ) {
170
+ const before = decodeImage(beforeBytes),
171
+ after = decodeImage(afterBytes);
172
+ if (!before || !after)
173
+ throw new Error("both screenshots must be PNG or JPEG");
174
+ const sameSize =
175
+ before.width === after.width && before.height === after.height;
85
176
  const regions = sameSize ? changedRegions(before, after) : [];
86
- const [readBefore, readAfter] = [await ocr.read(before, { lane }), await ocr.read(after, { lane })];
177
+ const [readBefore, readAfter] = [
178
+ await ocr.read(before, { lane }),
179
+ await ocr.read(after, { lane }),
180
+ ];
87
181
  const text = textChanges(readBefore.lines, readAfter.lines);
88
182
  const parts = [];
89
- if (!sameSize) parts.push(`the screenshots differ in size (${before.width}x${before.height} and ${after.width}x${after.height}), so only their text was compared`);
90
- else parts.push(regions.length ? `${regions.length} region${regions.length === 1 ? "" : "s"} of the picture changed` : "no part of the picture changed");
91
- for (const change of text.changed) parts.push(`"${change.before}" now reads "${change.after}"`);
183
+ if (!sameSize)
184
+ parts.push(
185
+ `the screenshots differ in size (${before.width}x${before.height} and ${after.width}x${after.height}), so only their text was compared`
186
+ );
187
+ else
188
+ parts.push(
189
+ regions.length
190
+ ? `${regions.length} region${regions.length === 1 ? "" : "s"} of the picture changed`
191
+ : "no part of the picture changed"
192
+ );
193
+ for (const change of text.changed)
194
+ parts.push(`"${change.before}" now reads "${change.after}"`);
92
195
  for (const line of text.moved) parts.push(`"${line.text}" moved`);
93
- if (text.removed.length) parts.push(`gone: ${text.removed.map((line) => `"${line.text}"`).join(", ")}`);
94
- if (text.added.length) parts.push(`new: ${text.added.map((line) => `"${line.text}"`).join(", ")}`);
95
- return { size: { before: [before.width, before.height], after: [after.width, after.height] }, regions: regions.slice(0, 20), text, summary: `${parts.join("; ")}.`, ms: readBefore.ms + readAfter.ms };
196
+ if (text.removed.length)
197
+ parts.push(
198
+ `gone: ${text.removed.map((line) => `"${line.text}"`).join(", ")}`
199
+ );
200
+ if (text.added.length)
201
+ parts.push(`new: ${text.added.map((line) => `"${line.text}"`).join(", ")}`);
202
+ return {
203
+ size: {
204
+ before: [before.width, before.height],
205
+ after: [after.width, after.height],
206
+ },
207
+ regions: regions.slice(0, 20),
208
+ text,
209
+ summary: `${parts.join("; ")}.`,
210
+ ms: readBefore.ms + readAfter.ms,
211
+ };
96
212
  }
package/lib/sealed.mjs CHANGED
@@ -26,57 +26,135 @@ export const SEALED_VERSION = 1;
26
26
  export const MAX_SKEW_MS = 5 * 60_000;
27
27
 
28
28
  export const b64u = (bytes) => Buffer.from(bytes).toString("base64url");
29
- export const fromB64u = (text) => new Uint8Array(Buffer.from(String(text), "base64url"));
29
+ export const fromB64u = (text) =>
30
+ new Uint8Array(Buffer.from(String(text), "base64url"));
30
31
 
31
32
  export async function loadOrCreateIdentity(file) {
32
33
  try {
33
34
  const saved = JSON.parse(fs.readFileSync(file, "utf8"));
34
- const privateKey = await subtle.importKey("jwk", saved.privateJwk, CURVE, false, ["deriveBits"]);
35
+ const privateKey = await subtle.importKey(
36
+ "jwk",
37
+ saved.privateJwk,
38
+ CURVE,
39
+ false,
40
+ ["deriveBits"]
41
+ );
35
42
  return { privateKey, publicKey: saved.publicKey };
36
43
  } catch (error) {
37
- if (error.code !== "ENOENT") throw new Error(`the phone identity at ${file} is unreadable: ${error.message}`);
44
+ if (error.code !== "ENOENT")
45
+ throw new Error(
46
+ `the phone identity at ${file} is unreadable: ${error.message}`
47
+ );
38
48
  }
39
49
  const pair = await subtle.generateKey(CURVE, true, ["deriveBits"]);
40
- const publicKey = b64u(new Uint8Array(await subtle.exportKey("raw", pair.publicKey)));
50
+ const publicKey = b64u(
51
+ new Uint8Array(await subtle.exportKey("raw", pair.publicKey))
52
+ );
41
53
  const privateJwk = await subtle.exportKey("jwk", pair.privateKey);
42
- fs.writeFileSync(file, JSON.stringify({ version: SEALED_VERSION, publicKey, privateJwk }), { mode: 0o600, flag: "wx" });
54
+ fs.writeFileSync(
55
+ file,
56
+ JSON.stringify({ version: SEALED_VERSION, publicKey, privateJwk }),
57
+ { mode: 0o600, flag: "wx" }
58
+ );
43
59
  return loadOrCreateIdentity(file);
44
60
  }
45
61
 
46
- export async function pairProof(secret, offerId, devicePublicKey, daemonPublicKey) {
47
- const key = await subtle.importKey("raw", fromB64u(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
48
- return b64u(new Uint8Array(await subtle.sign("HMAC", key, encoder.encode(`HSC1-pair\n${offerId}\n${devicePublicKey}\n${daemonPublicKey}`))));
62
+ export async function pairProof(
63
+ secret,
64
+ offerId,
65
+ devicePublicKey,
66
+ daemonPublicKey
67
+ ) {
68
+ const key = await subtle.importKey(
69
+ "raw",
70
+ fromB64u(secret),
71
+ { name: "HMAC", hash: "SHA-256" },
72
+ false,
73
+ ["sign"]
74
+ );
75
+ return b64u(
76
+ new Uint8Array(
77
+ await subtle.sign(
78
+ "HMAC",
79
+ key,
80
+ encoder.encode(
81
+ `HSC1-pair\n${offerId}\n${devicePublicKey}\n${daemonPublicKey}`
82
+ )
83
+ )
84
+ )
85
+ );
49
86
  }
50
87
 
51
88
  export async function channelKey(privateKey, peerPublicKey, deviceId) {
52
- const peer = await subtle.importKey("raw", fromB64u(peerPublicKey), CURVE, false, []);
53
- const shared = await subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
54
- const material = await subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
89
+ const peer = await subtle.importKey(
90
+ "raw",
91
+ fromB64u(peerPublicKey),
92
+ CURVE,
93
+ false,
94
+ []
95
+ );
96
+ const shared = await subtle.deriveBits(
97
+ { name: "ECDH", public: peer },
98
+ privateKey,
99
+ 256
100
+ );
101
+ const material = await subtle.importKey("raw", shared, "HKDF", false, [
102
+ "deriveKey",
103
+ ]);
55
104
  return subtle.deriveKey(
56
- { name: "HKDF", hash: "SHA-256", salt: encoder.encode("HSC1"), info: encoder.encode(`HSC1 channel ${deviceId}`) },
57
- material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"],
105
+ {
106
+ name: "HKDF",
107
+ hash: "SHA-256",
108
+ salt: encoder.encode("HSC1"),
109
+ info: encoder.encode(`HSC1 channel ${deviceId}`),
110
+ },
111
+ material,
112
+ { name: "AES-GCM", length: 256 },
113
+ false,
114
+ ["encrypt", "decrypt"]
58
115
  );
59
116
  }
60
117
 
61
- const aad = (direction, deviceId, requestId) => encoder.encode(`HSC1|${direction}|${deviceId}|${requestId}`);
118
+ const aad = (direction, deviceId, requestId) =>
119
+ encoder.encode(`HSC1|${direction}|${deviceId}|${requestId}`);
62
120
 
63
121
  export async function seal(key, direction, deviceId, requestId, value) {
64
122
  const iv = webcrypto.getRandomValues(new Uint8Array(12));
65
- const ct = await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad(direction, deviceId, requestId) }, key, encoder.encode(JSON.stringify(value)));
66
- return { v: SEALED_VERSION, deviceId, requestId, iv: b64u(iv), ct: b64u(new Uint8Array(ct)) };
123
+ const ct = await subtle.encrypt(
124
+ {
125
+ name: "AES-GCM",
126
+ iv,
127
+ additionalData: aad(direction, deviceId, requestId),
128
+ },
129
+ key,
130
+ encoder.encode(JSON.stringify(value))
131
+ );
132
+ return {
133
+ v: SEALED_VERSION,
134
+ deviceId,
135
+ requestId,
136
+ iv: b64u(iv),
137
+ ct: b64u(new Uint8Array(ct)),
138
+ };
67
139
  }
68
140
 
69
141
  // Throws on a wrong key, a wrong direction, device or request id, or any changed byte.
70
142
  export async function open(key, direction, envelope) {
71
143
  const plain = await subtle.decrypt(
72
- { name: "AES-GCM", iv: fromB64u(envelope.iv), additionalData: aad(direction, envelope.deviceId, envelope.requestId) },
73
- key, fromB64u(envelope.ct),
144
+ {
145
+ name: "AES-GCM",
146
+ iv: fromB64u(envelope.iv),
147
+ additionalData: aad(direction, envelope.deviceId, envelope.requestId),
148
+ },
149
+ key,
150
+ fromB64u(envelope.ct)
74
151
  );
75
152
  return JSON.parse(decoder.decode(plain));
76
153
  }
77
154
 
78
155
  export function constantTimeEqual(a, b) {
79
- const left = Buffer.from(String(a)), right = Buffer.from(String(b));
156
+ const left = Buffer.from(String(a)),
157
+ right = Buffer.from(String(b));
80
158
  return left.length === right.length && timingSafeEqual(left, right);
81
159
  }
82
160
 
@@ -85,31 +163,82 @@ export function constantTimeEqual(a, b) {
85
163
  // associated data. A peer's key comes from Harness's directory the first time and is pinned by
86
164
  // handle on this machine; a different key later is refused until the person trusts it.
87
165
  export async function relayKey(privateKey, peerPublicKey) {
88
- const peer = await subtle.importKey("raw", fromB64u(peerPublicKey), CURVE, false, []);
89
- const shared = await subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
90
- const material = await subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
91
- return subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: encoder.encode("HSC1"), info: encoder.encode("HSC1 relay") }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
166
+ const peer = await subtle.importKey(
167
+ "raw",
168
+ fromB64u(peerPublicKey),
169
+ CURVE,
170
+ false,
171
+ []
172
+ );
173
+ const shared = await subtle.deriveBits(
174
+ { name: "ECDH", public: peer },
175
+ privateKey,
176
+ 256
177
+ );
178
+ const material = await subtle.importKey("raw", shared, "HKDF", false, [
179
+ "deriveKey",
180
+ ]);
181
+ return subtle.deriveKey(
182
+ {
183
+ name: "HKDF",
184
+ hash: "SHA-256",
185
+ salt: encoder.encode("HSC1"),
186
+ info: encoder.encode("HSC1 relay"),
187
+ },
188
+ material,
189
+ { name: "AES-GCM", length: 256 },
190
+ false,
191
+ ["encrypt", "decrypt"]
192
+ );
92
193
  }
93
194
  export async function sealBound(key, bound, value) {
94
195
  const iv = webcrypto.getRandomValues(new Uint8Array(12));
95
- const ct = await subtle.encrypt({ name: "AES-GCM", iv, additionalData: encoder.encode(`HSC1|${bound}`) }, key, encoder.encode(JSON.stringify(value)));
196
+ const ct = await subtle.encrypt(
197
+ { name: "AES-GCM", iv, additionalData: encoder.encode(`HSC1|${bound}`) },
198
+ key,
199
+ encoder.encode(JSON.stringify(value))
200
+ );
96
201
  return { iv: b64u(iv), ct: b64u(new Uint8Array(ct)) };
97
202
  }
98
203
  export async function openBound(key, bound, sealed) {
99
- const plain = await subtle.decrypt({ name: "AES-GCM", iv: fromB64u(sealed.iv), additionalData: encoder.encode(`HSC1|${bound}`) }, key, fromB64u(sealed.ct));
204
+ const plain = await subtle.decrypt(
205
+ {
206
+ name: "AES-GCM",
207
+ iv: fromB64u(sealed.iv),
208
+ additionalData: encoder.encode(`HSC1|${bound}`),
209
+ },
210
+ key,
211
+ fromB64u(sealed.ct)
212
+ );
100
213
  return JSON.parse(decoder.decode(plain));
101
214
  }
102
215
  // A file's bytes, sealed as they are (no JSON or base64 inside): bound to its thread and file id like a message.
103
216
  export async function sealBytes(key, bound, bytes) {
104
217
  const iv = webcrypto.getRandomValues(new Uint8Array(12));
105
- const ct = await subtle.encrypt({ name: "AES-GCM", iv, additionalData: encoder.encode(`HSC1|${bound}`) }, key, bytes);
218
+ const ct = await subtle.encrypt(
219
+ { name: "AES-GCM", iv, additionalData: encoder.encode(`HSC1|${bound}`) },
220
+ key,
221
+ bytes
222
+ );
106
223
  return { iv: b64u(iv), ct: b64u(new Uint8Array(ct)) };
107
224
  }
108
225
  export async function openBytes(key, bound, sealed) {
109
- return Buffer.from(await subtle.decrypt({ name: "AES-GCM", iv: fromB64u(sealed.iv), additionalData: encoder.encode(`HSC1|${bound}`) }, key, fromB64u(sealed.ct)));
226
+ return Buffer.from(
227
+ await subtle.decrypt(
228
+ {
229
+ name: "AES-GCM",
230
+ iv: fromB64u(sealed.iv),
231
+ additionalData: encoder.encode(`HSC1|${bound}`),
232
+ },
233
+ key,
234
+ fromB64u(sealed.ct)
235
+ )
236
+ );
110
237
  }
111
238
  // What two people read to each other to check they hold each other's key: SHA-256 of the key, 20 hex digits in groups of four.
112
239
  export async function fingerprint(publicKey) {
113
- const digest = Buffer.from(await subtle.digest("SHA-256", fromB64u(publicKey))).toString("hex");
240
+ const digest = Buffer.from(
241
+ await subtle.digest("SHA-256", fromB64u(publicKey))
242
+ ).toString("hex");
114
243
  return digest.slice(0, 20).match(/.{4}/g).join(" ");
115
244
  }
package/lib/secrets.mjs CHANGED
@@ -8,18 +8,31 @@ const PATTERNS = [
8
8
  { reason: "a private key block", test: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
9
9
  { reason: "an OpenAI-style API key", test: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
10
10
  { reason: "an AWS access key", test: /\bAKIA[0-9A-Z]{16}\b/ },
11
- { reason: "a GitHub token", test: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}\b|\bgithub_pat_[A-Za-z0-9_]{40,}\b/ },
11
+ {
12
+ reason: "a GitHub token",
13
+ test: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}\b|\bgithub_pat_[A-Za-z0-9_]{40,}\b/,
14
+ },
12
15
  { reason: "a Slack token", test: /\bxox[abpr]-[A-Za-z0-9-]{10,}\b/ },
13
- { reason: "a JSON web token", test: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
16
+ {
17
+ reason: "a JSON web token",
18
+ test: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
19
+ },
14
20
  { reason: "a 32-byte hex key", test: /\b(0x)?[0-9a-fA-F]{64}\b/ },
15
21
  { reason: "a Harness connection token", test: /\bhmcp_[A-Za-z0-9]{12,}\b/ },
16
- { reason: "a secret assigned in env style", test: /\b[A-Z0-9_]*(SECRET|TOKEN|PASSWORD|PRIVATE_KEY|API_KEY)[A-Z0-9_]*\s*[=:]\s*['"]?[^\s'"]{8,}/ },
17
- { reason: "a seed phrase", test: /\b(seed phrase|mnemonic|recovery phrase)\b[\s\S]{0,40}?(\b[a-z]{3,8}\b[\s,]+){11,}/i },
22
+ {
23
+ reason: "a secret assigned in env style",
24
+ test: /\b[A-Z0-9_]*(SECRET|TOKEN|PASSWORD|PRIVATE_KEY|API_KEY)[A-Z0-9_]*\s*[=:]\s*['"]?[^\s'"]{8,}/,
25
+ },
26
+ {
27
+ reason: "a seed phrase",
28
+ test: /\b(seed phrase|mnemonic|recovery phrase)\b[\s\S]{0,40}?(\b[a-z]{3,8}\b[\s,]+){11,}/i,
29
+ },
18
30
  ];
19
31
 
20
32
  /** The reason a text must be held, or null when nothing in it looks like a secret. */
21
33
  export function secretReason(text) {
22
34
  const value = String(text || "");
23
- for (const pattern of PATTERNS) if (pattern.test.test(value)) return pattern.reason;
35
+ for (const pattern of PATTERNS)
36
+ if (pattern.test.test(value)) return pattern.reason;
24
37
  return null;
25
38
  }