tryharness 0.7.22 → 0.7.25

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 (64) hide show
  1. package/README.md +48 -8
  2. package/assets/opencode-plugin.js +48 -10
  3. package/assets/pi-extension.ts +106 -26
  4. package/cli.mjs +1281 -289
  5. package/lib/activity.mjs +197 -40
  6. package/lib/adapters.mjs +337 -54
  7. package/lib/agentnode.mjs +175 -35
  8. package/lib/atlas.mjs +604 -0
  9. package/lib/bip39-english.txt +2048 -0
  10. package/lib/cards.mjs +177 -39
  11. package/lib/claudemem.mjs +74 -18
  12. package/lib/commands.mjs +13 -5
  13. package/lib/context.mjs +628 -0
  14. package/lib/control-client.mjs +5 -2
  15. package/lib/coordinator.mjs +281 -56
  16. package/lib/daemon.mjs +5590 -838
  17. package/lib/desktop.mjs +149 -35
  18. package/lib/digest.mjs +81 -0
  19. package/lib/environment.mjs +180 -0
  20. package/lib/files.mjs +246 -13
  21. package/lib/firewall.mjs +288 -0
  22. package/lib/folders.mjs +223 -57
  23. package/lib/frames.mjs +333 -0
  24. package/lib/inference.mjs +663 -0
  25. package/lib/learn.mjs +182 -0
  26. package/lib/live.mjs +286 -61
  27. package/lib/localmodels.mjs +571 -0
  28. package/lib/mcp-stdio.mjs +912 -103
  29. package/lib/media.mjs +228 -0
  30. package/lib/members.json +1 -1
  31. package/lib/members.mjs +35 -9
  32. package/lib/models.mjs +342 -80
  33. package/lib/network.mjs +201 -42
  34. package/lib/notify.mjs +49 -12
  35. package/lib/nudge.mjs +190 -32
  36. package/lib/ocr.mjs +380 -0
  37. package/lib/onboard.mjs +321 -70
  38. package/lib/parsers.mjs +132 -41
  39. package/lib/paths.mjs +33 -10
  40. package/lib/personal.mjs +77 -0
  41. package/lib/phones.mjs +398 -51
  42. package/lib/places.mjs +566 -102
  43. package/lib/provenance.mjs +19 -4
  44. package/lib/receipts.mjs +82 -0
  45. package/lib/redact.mjs +730 -0
  46. package/lib/runner.mjs +347 -55
  47. package/lib/runtime-install.mjs +59 -17
  48. package/lib/screens.mjs +212 -0
  49. package/lib/sealed.mjs +160 -28
  50. package/lib/secrets.mjs +18 -5
  51. package/lib/service.mjs +173 -35
  52. package/lib/snippets.mjs +140 -0
  53. package/lib/speech.mjs +993 -0
  54. package/lib/store.mjs +1678 -301
  55. package/lib/swarm.mjs +598 -117
  56. package/lib/t3.mjs +400 -79
  57. package/lib/trails.mjs +401 -0
  58. package/lib/ui.mjs +95 -23
  59. package/lib/vectors.mjs +5 -2
  60. package/lib/wakes.mjs +284 -68
  61. package/lib/watches.mjs +362 -0
  62. package/lib/worktrees.mjs +186 -38
  63. package/package.json +6 -4
  64. package/lib/embedder.mjs +0 -156
@@ -0,0 +1,212 @@
1
+ import { decodeImage } from "./ocr.mjs";
2
+
3
+ // Two screenshots of the same screen, before and after an agent's UI change: which parts of the picture changed, and
4
+ // what the text on screen says now that it did not before. The pixels find where; the text read on this computer
5
+ // (ocr.mjs) finds what, down to a lost space ("yet OPEN" becoming "yetOPEN"). Code only, no model judges: the agent
6
+ // that made the change reads the list against what it was asked to do.
7
+
8
+ const CELL = 16;
9
+ // A cell counts as changed when its pixels differ by more than this on average (0 to 255): re-rendering the same page
10
+ // moves anti-aliasing by a little; the ui-checks eval set it against identical renders.
11
+ const CELL_DIFF = 6;
12
+ const MIN_OVERLAP = 0.3;
13
+
14
+ const luminance = (data, i) =>
15
+ 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
16
+ const overlap = (a, b) => {
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]));
19
+ const area = (box) => Math.max(1, (box[2] - box[0]) * (box[3] - box[1]));
20
+ return (w * h) / Math.min(area(a), area(b));
21
+ };
22
+
23
+ /** Changed regions of two same-size images: [{ box: [x0, y0, x1, y1], share }], largest first. */
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);
31
+ const changed = new Uint8Array(cols * rows);
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;
54
+ }
55
+ // Touching changed cells become one region.
56
+ const regions = [];
57
+ const seen = new Uint8Array(cols * rows);
58
+ for (let start = 0; start < changed.length; start += 1) {
59
+ if (!changed[start] || seen[start]) continue;
60
+ const stack = [start];
61
+ seen[start] = 1;
62
+ let x0 = Infinity,
63
+ y0 = Infinity,
64
+ x1 = -1,
65
+ y1 = -1,
66
+ cells = 0;
67
+ while (stack.length) {
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
+ }
100
+ }
101
+ }
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
+ });
111
+ }
112
+ return regions.sort((a, b) => b.share - a.share);
113
+ }
114
+
115
+ /** Lines of text read before and after, paired by where they sit: { changed: [{ before, after, box }], moved, removed, added }. */
116
+ export function textChanges(beforeLines, afterLines) {
117
+ const used = new Set();
118
+ const changed = [],
119
+ removed = [];
120
+ for (const line of beforeLines) {
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 });
148
+ }
149
+ const added = afterLines
150
+ .filter((_, i) => !used.has(i))
151
+ .map((line) => ({ text: line.text, box: line.box }));
152
+ // The same words gone from one place and new in another moved.
153
+ const moved = [];
154
+ for (const line of [...removed]) {
155
+ const to = added.findIndex((other) => other.text === line.text);
156
+ if (to < 0) continue;
157
+ moved.push({ text: line.text, from: line.box, to: added[to].box });
158
+ removed.splice(removed.indexOf(line), 1);
159
+ added.splice(to, 1);
160
+ }
161
+ return { changed, moved, removed, added };
162
+ }
163
+
164
+ /** Compares two screenshots (PNG or JPEG bytes): { size, regions, text, summary }. */
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;
176
+ const regions = sameSize ? changedRegions(before, after) : [];
177
+ const [readBefore, readAfter] = [
178
+ await ocr.read(before, { lane }),
179
+ await ocr.read(after, { lane }),
180
+ ];
181
+ const text = textChanges(readBefore.lines, readAfter.lines);
182
+ const parts = [];
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}"`);
195
+ for (const line of text.moved) parts.push(`"${line.text}" moved`);
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
+ };
212
+ }
package/lib/sealed.mjs CHANGED
@@ -13,6 +13,9 @@ import { timingSafeEqual, webcrypto } from "node:crypto";
13
13
  // - Every message: AES-256-GCM under HKDF-SHA256(ECDH(device, daemon)), a fresh 96-bit nonce,
14
14
  // and the direction, device and request id bound as associated data. Requests carry a
15
15
  // timestamp; the daemon refuses stale ones and any request id it has already seen.
16
+ // - The other way round, the daemon asks a paired browser (its screen history): the question goes
17
+ // "d2b" and the answer "b2d" under the same key, so neither opens as a message of the device's
18
+ // own calls. The browser applies the same freshness and replay rules (phones.mjs ask).
16
19
  // Not provided: forward secrecy (both keys are static). Revoking a device ends its key.
17
20
 
18
21
  const subtle = webcrypto.subtle;
@@ -23,57 +26,135 @@ export const SEALED_VERSION = 1;
23
26
  export const MAX_SKEW_MS = 5 * 60_000;
24
27
 
25
28
  export const b64u = (bytes) => Buffer.from(bytes).toString("base64url");
26
- export const fromB64u = (text) => new Uint8Array(Buffer.from(String(text), "base64url"));
29
+ export const fromB64u = (text) =>
30
+ new Uint8Array(Buffer.from(String(text), "base64url"));
27
31
 
28
32
  export async function loadOrCreateIdentity(file) {
29
33
  try {
30
34
  const saved = JSON.parse(fs.readFileSync(file, "utf8"));
31
- 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
+ );
32
42
  return { privateKey, publicKey: saved.publicKey };
33
43
  } catch (error) {
34
- 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
+ );
35
48
  }
36
49
  const pair = await subtle.generateKey(CURVE, true, ["deriveBits"]);
37
- 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
+ );
38
53
  const privateJwk = await subtle.exportKey("jwk", pair.privateKey);
39
- 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
+ );
40
59
  return loadOrCreateIdentity(file);
41
60
  }
42
61
 
43
- export async function pairProof(secret, offerId, devicePublicKey, daemonPublicKey) {
44
- const key = await subtle.importKey("raw", fromB64u(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
45
- 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
+ );
46
86
  }
47
87
 
48
88
  export async function channelKey(privateKey, peerPublicKey, deviceId) {
49
- const peer = await subtle.importKey("raw", fromB64u(peerPublicKey), CURVE, false, []);
50
- const shared = await subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
51
- 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
+ ]);
52
104
  return subtle.deriveKey(
53
- { name: "HKDF", hash: "SHA-256", salt: encoder.encode("HSC1"), info: encoder.encode(`HSC1 channel ${deviceId}`) },
54
- 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"]
55
115
  );
56
116
  }
57
117
 
58
- const aad = (direction, deviceId, requestId) => encoder.encode(`HSC1|${direction}|${deviceId}|${requestId}`);
118
+ const aad = (direction, deviceId, requestId) =>
119
+ encoder.encode(`HSC1|${direction}|${deviceId}|${requestId}`);
59
120
 
60
121
  export async function seal(key, direction, deviceId, requestId, value) {
61
122
  const iv = webcrypto.getRandomValues(new Uint8Array(12));
62
- const ct = await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad(direction, deviceId, requestId) }, key, encoder.encode(JSON.stringify(value)));
63
- 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
+ };
64
139
  }
65
140
 
66
141
  // Throws on a wrong key, a wrong direction, device or request id, or any changed byte.
67
142
  export async function open(key, direction, envelope) {
68
143
  const plain = await subtle.decrypt(
69
- { name: "AES-GCM", iv: fromB64u(envelope.iv), additionalData: aad(direction, envelope.deviceId, envelope.requestId) },
70
- 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)
71
151
  );
72
152
  return JSON.parse(decoder.decode(plain));
73
153
  }
74
154
 
75
155
  export function constantTimeEqual(a, b) {
76
- const left = Buffer.from(String(a)), right = Buffer.from(String(b));
156
+ const left = Buffer.from(String(a)),
157
+ right = Buffer.from(String(b));
77
158
  return left.length === right.length && timingSafeEqual(left, right);
78
159
  }
79
160
 
@@ -82,31 +163,82 @@ export function constantTimeEqual(a, b) {
82
163
  // associated data. A peer's key comes from Harness's directory the first time and is pinned by
83
164
  // handle on this machine; a different key later is refused until the person trusts it.
84
165
  export async function relayKey(privateKey, peerPublicKey) {
85
- const peer = await subtle.importKey("raw", fromB64u(peerPublicKey), CURVE, false, []);
86
- const shared = await subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
87
- const material = await subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
88
- 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
+ );
89
193
  }
90
194
  export async function sealBound(key, bound, value) {
91
195
  const iv = webcrypto.getRandomValues(new Uint8Array(12));
92
- 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
+ );
93
201
  return { iv: b64u(iv), ct: b64u(new Uint8Array(ct)) };
94
202
  }
95
203
  export async function openBound(key, bound, sealed) {
96
- 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
+ );
97
213
  return JSON.parse(decoder.decode(plain));
98
214
  }
99
215
  // A file's bytes, sealed as they are (no JSON or base64 inside): bound to its thread and file id like a message.
100
216
  export async function sealBytes(key, bound, bytes) {
101
217
  const iv = webcrypto.getRandomValues(new Uint8Array(12));
102
- 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
+ );
103
223
  return { iv: b64u(iv), ct: b64u(new Uint8Array(ct)) };
104
224
  }
105
225
  export async function openBytes(key, bound, sealed) {
106
- 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
+ );
107
237
  }
108
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.
109
239
  export async function fingerprint(publicKey) {
110
- 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");
111
243
  return digest.slice(0, 20).match(/.{4}/g).join(" ");
112
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
  }