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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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) =>
|
|
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])),
|
|
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(
|
|
23
|
-
|
|
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)
|
|
26
|
-
let
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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(),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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({
|
|
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 = [],
|
|
118
|
+
const changed = [],
|
|
119
|
+
removed = [];
|
|
59
120
|
for (const line of beforeLines) {
|
|
60
|
-
const same = afterLines.findIndex(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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] = [
|
|
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)
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
94
|
-
|
|
95
|
-
|
|
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) =>
|
|
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(
|
|
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")
|
|
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(
|
|
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(
|
|
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(
|
|
47
|
-
|
|
48
|
-
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
{
|
|
57
|
-
|
|
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) =>
|
|
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(
|
|
66
|
-
|
|
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
|
-
{
|
|
73
|
-
|
|
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)),
|
|
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(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
17
|
-
|
|
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)
|
|
35
|
+
for (const pattern of PATTERNS)
|
|
36
|
+
if (pattern.test.test(value)) return pattern.reason;
|
|
24
37
|
return null;
|
|
25
38
|
}
|