sessionpipe-core 0.7.0 → 0.10.0
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/README.md +1 -1
- package/dist/control.js +172 -12
- package/dist/control.js.map +2 -2
- package/dist/index.js +1508 -431
- package/dist/index.js.map +4 -4
- package/dist/schema/v1.d.ts +405 -104
- package/dist/schema/v1.d.ts.map +1 -1
- package/dist/schema.js +174 -28
- package/dist/schema.js.map +2 -2
- package/dist/service.js +155 -0
- package/dist/service.js.map +7 -0
- package/dist/src/adapters/acp-control.d.ts +87 -0
- package/dist/src/adapters/acp-control.d.ts.map +1 -0
- package/dist/src/adapters/antigravity-control.d.ts +15 -5
- package/dist/src/adapters/antigravity-control.d.ts.map +1 -1
- package/dist/src/adapters/antigravity.d.ts +1 -2
- package/dist/src/adapters/antigravity.d.ts.map +1 -1
- package/dist/src/adapters/claude-code-control.d.ts +41 -4
- package/dist/src/adapters/claude-code-control.d.ts.map +1 -1
- package/dist/src/adapters/claude-code.d.ts +10 -0
- package/dist/src/adapters/claude-code.d.ts.map +1 -1
- package/dist/src/adapters/claude-shaped.d.ts.map +1 -1
- package/dist/src/adapters/codex-config.d.ts +2 -2
- package/dist/src/adapters/codex-config.d.ts.map +1 -1
- package/dist/src/adapters/codex-control.d.ts +23 -7
- package/dist/src/adapters/codex-control.d.ts.map +1 -1
- package/dist/src/adapters/codex-rollout.d.ts +8 -0
- package/dist/src/adapters/codex-rollout.d.ts.map +1 -0
- package/dist/src/adapters/codex.d.ts +1 -1
- package/dist/src/adapters/codex.d.ts.map +1 -1
- package/dist/src/adapters/devin.d.ts +32 -0
- package/dist/src/adapters/devin.d.ts.map +1 -0
- package/dist/src/adapters/gemini-cli.d.ts.map +1 -1
- package/dist/src/adapters/headless.d.ts +4 -1
- package/dist/src/adapters/headless.d.ts.map +1 -1
- package/dist/src/adapters/index.d.ts +11 -3
- package/dist/src/adapters/index.d.ts.map +1 -1
- package/dist/src/adapters/types.d.ts +9 -1
- package/dist/src/adapters/types.d.ts.map +1 -1
- package/dist/src/control/verify.d.ts +96 -13
- package/dist/src/control/verify.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/launched-by.d.ts +12 -0
- package/dist/src/launched-by.d.ts.map +1 -0
- package/dist/src/outbox.d.ts +13 -2
- package/dist/src/outbox.d.ts.map +1 -1
- package/dist/src/paths.d.ts +7 -1
- package/dist/src/paths.d.ts.map +1 -1
- package/dist/src/privacy/filter.d.ts +2 -0
- package/dist/src/privacy/filter.d.ts.map +1 -1
- package/dist/src/privacy/index.d.ts +1 -1
- package/dist/src/privacy/index.d.ts.map +1 -1
- package/dist/src/readers/files.d.ts.map +1 -1
- package/dist/src/readers/git.d.ts.map +1 -1
- package/dist/src/readers/sqlite.d.ts +23 -0
- package/dist/src/readers/sqlite.d.ts.map +1 -0
- package/dist/src/readers/transcripts.d.ts +5 -2
- package/dist/src/readers/transcripts.d.ts.map +1 -1
- package/dist/src/service/index.d.ts +83 -0
- package/dist/src/service/index.d.ts.map +1 -0
- package/dist/src/short-id.d.ts +2 -0
- package/dist/src/short-id.d.ts.map +1 -0
- package/dist/src/sinks/file.d.ts.map +1 -1
- package/dist/src/sinks/stdout.d.ts.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/ulid.d.ts.map +1 -1
- package/dist/src/version.d.ts +39 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/version.js +75 -0
- package/dist/version.js.map +7 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
sessionpipe core: protocol types, harness adapters, privacy filter and redaction rulesets, outbox and sinks. Zero runtime dependencies.
|
|
4
4
|
|
|
5
|
-
Part of [sessionpipe](https://github.com/
|
|
5
|
+
Part of [sessionpipe](https://github.com/spacesheep-dev/sessionpipe) · docs at [sessionpipe.org](https://sessionpipe.org). Apache-2.0.
|
package/dist/control.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// sessionpipe-core 0.
|
|
1
|
+
// sessionpipe-core 0.10.0 · Apache-2.0 · https://sessionpipe.org
|
|
2
2
|
|
|
3
3
|
// src/control/verify.ts
|
|
4
4
|
var GRANT_MAX_MS = 24 * 36e5;
|
|
@@ -10,20 +10,42 @@ var CMD_MAX = 48e3;
|
|
|
10
10
|
var TEXT_MAX = 2e4;
|
|
11
11
|
var NOTE_MAX = 2e3;
|
|
12
12
|
var GRANT_STR_MAX = 2e3;
|
|
13
|
+
var FILES_MAX = 10;
|
|
14
|
+
var FILE_SIZE_MAX = 26214400;
|
|
15
|
+
var FILES_TOTAL_MAX = 52428800;
|
|
16
|
+
var FILE_NAME_MAX = 200;
|
|
17
|
+
var FILE_TYPE_MAX = 100;
|
|
18
|
+
var MODEL_MAX = 100;
|
|
19
|
+
var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
|
|
20
|
+
var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
|
|
21
|
+
var levelRank = (l) => START_PERMISSIONS.indexOf(l);
|
|
22
|
+
var narrower = (a, b) => levelRank(a) <= levelRank(b) ? a : b;
|
|
13
23
|
var COMMAND_TYPE = "sessionpipe.control";
|
|
14
24
|
var GRANT_TYPE = "sessionpipe.grant";
|
|
15
25
|
var CONTROL_KINDS = ["prompt", "permission.answer", "cancel", "start", "key.add"];
|
|
16
26
|
var KEY_STORE_SESSION = "sessionpipe:keys";
|
|
17
27
|
var ES256 = -7;
|
|
18
28
|
var RS256 = -257;
|
|
29
|
+
var KEY_TYPES = ["passkey", "device"];
|
|
19
30
|
var NONCE_RE = /^[A-Za-z0-9_-]{22,64}$/;
|
|
20
31
|
var MACHINE_RE = /^m_[A-Za-z0-9_-]{16,40}$/;
|
|
21
32
|
var HARNESS_RE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
22
33
|
var SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
23
34
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
35
|
var B64URL_RE = /^[A-Za-z0-9_-]*$/;
|
|
36
|
+
var SHA256_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
37
|
+
var MEDIA_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
|
|
38
|
+
var BAD_NAME_CHAR_RE = /[/\\\x00-\x1f\x7f]/;
|
|
25
39
|
var UP = 1;
|
|
26
40
|
var UV = 4;
|
|
41
|
+
var COMMON_FIELDS = ["v", "t", "machine", "kind", "session", "nonce", "iat", "note"];
|
|
42
|
+
var FIELDS = {
|
|
43
|
+
prompt: ["text", "files"],
|
|
44
|
+
"permission.answer": ["for", "decision"],
|
|
45
|
+
cancel: [],
|
|
46
|
+
start: ["text", "cwd", "files", "permission", "model"],
|
|
47
|
+
"key.add": ["key", "name"]
|
|
48
|
+
};
|
|
27
49
|
var enc = new TextEncoder();
|
|
28
50
|
var dec = new TextDecoder();
|
|
29
51
|
function b64url(bytes) {
|
|
@@ -92,15 +114,33 @@ async function importKey(key) {
|
|
|
92
114
|
const k = await crypto.subtle.importKey("spki", u8(spki), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, [
|
|
93
115
|
"verify"
|
|
94
116
|
]);
|
|
95
|
-
const bits = k.algorithm.modulusLength
|
|
117
|
+
const bits = k.algorithm.modulusLength;
|
|
96
118
|
return bits >= 2048 ? k : null;
|
|
97
119
|
}
|
|
98
120
|
} catch {
|
|
99
121
|
}
|
|
100
122
|
return null;
|
|
101
123
|
}
|
|
124
|
+
var PASSKEY_FIELDS = /* @__PURE__ */ new Set(["id", "alg", "spki"]);
|
|
125
|
+
var DEVICE_FIELDS = /* @__PURE__ */ new Set(["id", "alg", "spki", "type", "store", "uv"]);
|
|
126
|
+
var DEVICE_SIG_FIELDS = ["cred", "sig", "str"];
|
|
127
|
+
async function deviceKeyId(spki) {
|
|
128
|
+
return b64url(await sha256(fromB64url(spki) ?? new Uint8Array()));
|
|
129
|
+
}
|
|
130
|
+
async function deviceRecordValid(key) {
|
|
131
|
+
if (key.type !== "device" || key.alg !== ES256 || key.store !== "hardware" || key.uv !== "each-use") return false;
|
|
132
|
+
if (typeof key.id !== "string" || !SHA256_RE.test(key.id) || key.id !== await deviceKeyId(key.spki)) return false;
|
|
133
|
+
return !!await importKey(key);
|
|
134
|
+
}
|
|
102
135
|
async function keyUsable(key) {
|
|
103
|
-
|
|
136
|
+
if (!key || typeof key !== "object" || Array.isArray(key)) return false;
|
|
137
|
+
const fields = Object.keys(key).filter((k) => key[k] !== void 0);
|
|
138
|
+
if ("type" in key && key.type !== void 0) {
|
|
139
|
+
if (!fields.every((f) => DEVICE_FIELDS.has(f))) return false;
|
|
140
|
+
return deviceRecordValid(key);
|
|
141
|
+
}
|
|
142
|
+
if (!fields.every((f) => PASSKEY_FIELDS.has(f))) return false;
|
|
143
|
+
return typeof key.id === "string" && /^[A-Za-z0-9_-]{16,1400}$/.test(key.id) && (key.alg === ES256 || key.alg === RS256) && !!await importKey(key);
|
|
104
144
|
}
|
|
105
145
|
function originAllowed(origin, rpId) {
|
|
106
146
|
if (typeof origin !== "string" || !rpId) return false;
|
|
@@ -116,8 +156,47 @@ function originAllowed(origin, rpId) {
|
|
|
116
156
|
return u.protocol === "http:" && rpId === "localhost";
|
|
117
157
|
}
|
|
118
158
|
var no = (why) => ({ ok: false, why });
|
|
119
|
-
|
|
159
|
+
var DEVICE_CONTEXT = "sessionpipe.device.v1";
|
|
160
|
+
function deviceMessage(purpose, rpId, challenge) {
|
|
161
|
+
return enc.encode(`${DEVICE_CONTEXT}
|
|
162
|
+
${purpose}
|
|
163
|
+
${rpId}
|
|
164
|
+
${b64url(challenge)}`);
|
|
165
|
+
}
|
|
166
|
+
async function verifyDeviceSignature(challenge, a, key, rpId, purpose) {
|
|
167
|
+
const sig = a;
|
|
168
|
+
for (const f of Object.keys(sig)) {
|
|
169
|
+
if (f === "str" && purpose === "grant") continue;
|
|
170
|
+
if (f !== "cred" && f !== "sig") return no(`a device key's signature carries no ${f}`);
|
|
171
|
+
}
|
|
172
|
+
if (sig.cred !== key.id) return no("signed with a key this machine doesn't trust");
|
|
173
|
+
if (!await deviceRecordValid(key)) return no("a trusted key is unusable");
|
|
174
|
+
const raw = fromB64url(sig.sig);
|
|
175
|
+
if (raw?.length !== 64) return no("a device key's signature is 64 bytes, r\u2016s");
|
|
176
|
+
const pub = await importKey(key);
|
|
177
|
+
if (!pub) return no("a trusted key is unusable");
|
|
178
|
+
let good = false;
|
|
179
|
+
try {
|
|
180
|
+
good = await crypto.subtle.verify(
|
|
181
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
182
|
+
pub,
|
|
183
|
+
u8(raw),
|
|
184
|
+
u8(deviceMessage(purpose, rpId, challenge))
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
good = false;
|
|
188
|
+
}
|
|
189
|
+
return good ? { ok: true } : no("the device key's signature doesn't verify");
|
|
190
|
+
}
|
|
191
|
+
async function verifyAssertion(challenge, a, key, rpId, purpose) {
|
|
120
192
|
if (!a || typeof a !== "object") return no("no passkey signature");
|
|
193
|
+
if (key.type !== void 0) {
|
|
194
|
+
if (key.type !== "device") return no("a trusted key is unusable");
|
|
195
|
+
return verifyDeviceSignature(challenge, a, key, rpId, purpose);
|
|
196
|
+
}
|
|
197
|
+
return verifyPasskeyAssertion(challenge, a, key, rpId);
|
|
198
|
+
}
|
|
199
|
+
async function verifyPasskeyAssertion(challenge, a, key, rpId) {
|
|
121
200
|
if (a.cred !== key.id) return no("signed with a key this machine doesn't trust");
|
|
122
201
|
const cdBytes = fromB64url(a.cd);
|
|
123
202
|
const ad = fromB64url(a.ad);
|
|
@@ -159,7 +238,7 @@ function pairChallenge(machine, code) {
|
|
|
159
238
|
}
|
|
160
239
|
async function verifyEnrollment(o) {
|
|
161
240
|
if (!await keyUsable(o.key)) return no("the key is in a shape this machine can't use");
|
|
162
|
-
return verifyAssertion(await pairChallenge(o.machine, o.code), o.proof, o.key, o.rpId);
|
|
241
|
+
return verifyAssertion(await pairChallenge(o.machine, o.code), o.proof, o.key, o.rpId, "pair");
|
|
163
242
|
}
|
|
164
243
|
var bad = (code, why) => ({
|
|
165
244
|
ok: false,
|
|
@@ -205,13 +284,26 @@ function parseCommand(str) {
|
|
|
205
284
|
switch (c.kind) {
|
|
206
285
|
case "prompt":
|
|
207
286
|
if (!text()) return bad("bad_fields", `a prompt needs text, at most ${TEXT_MAX} characters`);
|
|
208
|
-
if (has("for") || has("decision") || has("cwd")) return bad("bad_fields", "a prompt carries
|
|
287
|
+
if (has("for") || has("decision") || has("cwd")) return bad("bad_fields", "a prompt carries text and files");
|
|
288
|
+
if (has("files")) {
|
|
289
|
+
const why = filesProblem(c.files);
|
|
290
|
+
if (why) return bad("bad_fields", why);
|
|
291
|
+
}
|
|
209
292
|
break;
|
|
210
293
|
case "start":
|
|
211
294
|
if (!text()) return bad("bad_fields", `a new session needs text, at most ${TEXT_MAX} characters`);
|
|
212
|
-
if (typeof c.cwd !== "string" || !isAbsolute(c.cwd) || c.cwd.length > 1024)
|
|
213
|
-
return bad("bad_fields", "a new session needs an absolute folder");
|
|
214
|
-
if (has("for") || has("decision"))
|
|
295
|
+
if (typeof c.cwd !== "string" || !(isAbsolute(c.cwd) || HOME_RELATIVE_RE.test(c.cwd)) || c.cwd.length > 1024)
|
|
296
|
+
return bad("bad_fields", "a new session needs an absolute folder, or one under ~");
|
|
297
|
+
if (has("for") || has("decision"))
|
|
298
|
+
return bad("bad_fields", "a start carries text, cwd, files, a permission and a model");
|
|
299
|
+
if (has("files")) {
|
|
300
|
+
const why = filesProblem(c.files);
|
|
301
|
+
if (why) return bad("bad_fields", why);
|
|
302
|
+
}
|
|
303
|
+
if (has("permission") && levelRank(c.permission) < 0)
|
|
304
|
+
return bad("bad_fields", `a permission is one of ${START_PERMISSIONS.join(", ")}`);
|
|
305
|
+
if (has("model") && (typeof c.model !== "string" || c.model.length > MODEL_MAX || !MODEL_RE.test(c.model)))
|
|
306
|
+
return bad("bad_fields", `a model is 1 to ${MODEL_MAX} letters, digits and ._:/@[]-, not starting with a dash`);
|
|
215
307
|
break;
|
|
216
308
|
case "permission.answer":
|
|
217
309
|
if (typeof c.for !== "string" || !c.for || c.for.length > 128) return bad("bad_fields", "which attention?");
|
|
@@ -226,8 +318,16 @@ function parseCommand(str) {
|
|
|
226
318
|
if (c.session !== KEY_STORE_SESSION) return bad("bad_session", `a key.add names ${KEY_STORE_SESSION}`);
|
|
227
319
|
const k = c.key;
|
|
228
320
|
const b64 = (v, min, max) => typeof v === "string" && v.length >= min && v.length <= max && /^[A-Za-z0-9_-]+$/.test(v);
|
|
229
|
-
if (!k || typeof k !== "object" || !b64(k.id, 16, 1400) || k.alg !== -7 && k.alg !== -257 || !b64(k.spki, 40, 2e3))
|
|
321
|
+
if (!k || typeof k !== "object" || Array.isArray(k) || !b64(k.id, 16, 1400) || k.alg !== -7 && k.alg !== -257 || !b64(k.spki, 40, 2e3))
|
|
230
322
|
return bad("bad_fields", "a key.add carries the key: id, alg (-7 or -257) and spki");
|
|
323
|
+
if (k.type !== void 0) {
|
|
324
|
+
if (k.type !== "device") return bad("bad_fields", "a key's type is device, or absent for a passkey");
|
|
325
|
+
if (k.alg !== ES256 || k.store !== "hardware" || k.uv !== "each-use" || !SHA256_RE.test(k.id))
|
|
326
|
+
return bad("bad_fields", "a device key is ES256, store hardware, uv each-use, named by its SPKI's hash");
|
|
327
|
+
if (Object.keys(k).some((f) => !DEVICE_FIELDS.has(f)))
|
|
328
|
+
return bad("bad_fields", "a device key carries id, alg, spki, type, store and uv");
|
|
329
|
+
} else if (Object.keys(k).some((f) => !PASSKEY_FIELDS.has(f)))
|
|
330
|
+
return bad("bad_fields", "a passkey carries id, alg and spki: store and uv belong to a device key");
|
|
231
331
|
if (has("name") && (typeof c.name !== "string" || !c.name.trim() || c.name.length > 80))
|
|
232
332
|
return bad("bad_fields", "a device name is text, at most 80 characters");
|
|
233
333
|
if (has("text") || has("for") || has("decision") || has("cwd"))
|
|
@@ -236,10 +336,50 @@ function parseCommand(str) {
|
|
|
236
336
|
}
|
|
237
337
|
}
|
|
238
338
|
if (c.kind !== "key.add" && (has("key") || has("name"))) return bad("bad_fields", "only a key.add carries a key");
|
|
339
|
+
if (c.kind !== "start" && c.kind !== "prompt" && has("files"))
|
|
340
|
+
return bad("bad_fields", "only a start or a prompt carries files");
|
|
341
|
+
if (c.kind !== "start" && (has("permission") || has("model")))
|
|
342
|
+
return bad("bad_fields", "only a start carries a model or a permission");
|
|
343
|
+
const allowed = FIELDS[c.kind];
|
|
344
|
+
for (const k of Object.keys(c))
|
|
345
|
+
if (!COMMON_FIELDS.includes(k) && !allowed.includes(k))
|
|
346
|
+
return bad("bad_fields", `the command carries a field its kind doesn't have: ${k.slice(0, 40)}`);
|
|
239
347
|
if (has("note") && (typeof c.note !== "string" || c.note.length > NOTE_MAX))
|
|
240
348
|
return bad("bad_fields", `a note is text, at most ${NOTE_MAX} characters`);
|
|
241
349
|
return { ok: true, cmd: c };
|
|
242
350
|
}
|
|
351
|
+
function filesProblem(files) {
|
|
352
|
+
if (!Array.isArray(files) || files.length < 1 || files.length > FILES_MAX)
|
|
353
|
+
return `files is a list of 1 to ${FILES_MAX} files`;
|
|
354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
355
|
+
let total = 0;
|
|
356
|
+
for (const f of files) {
|
|
357
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) return "each file is an object";
|
|
358
|
+
const { name, type, size, sha256: hash } = f;
|
|
359
|
+
const why = fileNameProblem(name);
|
|
360
|
+
if (why) return why;
|
|
361
|
+
const key = name.normalize("NFC").toLowerCase();
|
|
362
|
+
if (seen.has(key)) return "two files have the same name";
|
|
363
|
+
seen.add(key);
|
|
364
|
+
if (typeof type !== "string" || type.length > FILE_TYPE_MAX || !MEDIA_TYPE_RE.test(type))
|
|
365
|
+
return "a file's type isn't a media type";
|
|
366
|
+
if (!isNum(size) || !Number.isInteger(size) || size < 1 || size > FILE_SIZE_MAX)
|
|
367
|
+
return `a file is 1 byte to ${FILE_SIZE_MAX / 1048576} MiB`;
|
|
368
|
+
total += size;
|
|
369
|
+
if (typeof hash !== "string" || !SHA256_RE.test(hash)) return "a file's sha256 isn't 43 base64url characters";
|
|
370
|
+
}
|
|
371
|
+
if (total > FILES_TOTAL_MAX) return `the files add up to more than ${FILES_TOTAL_MAX / 1048576} MiB`;
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
function fileNameProblem(name) {
|
|
375
|
+
if (typeof name !== "string") return "a file has no name";
|
|
376
|
+
const n = name.normalize("NFC");
|
|
377
|
+
if (n.length < 1 || n.length > FILE_NAME_MAX) return `a file name is 1 to ${FILE_NAME_MAX} characters`;
|
|
378
|
+
if (BAD_NAME_CHAR_RE.test(n)) return "a file name has a slash, a backslash or a control character";
|
|
379
|
+
if (n.startsWith(".")) return "a file name can't start with a dot";
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
var HOME_RELATIVE_RE = /^~(?:[\\/]|$)/;
|
|
243
383
|
function isAbsolute(p) {
|
|
244
384
|
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
245
385
|
}
|
|
@@ -294,7 +434,7 @@ async function verifyCommand(s, o) {
|
|
|
294
434
|
const key = o.trusted.find((k) => k && k.id === s.confirm?.cred);
|
|
295
435
|
if (!key) refusal = { code: "untrusted_key", why: "confirmed with a key this machine doesn't trust" };
|
|
296
436
|
else {
|
|
297
|
-
const a = await verifyAssertion(await sha256(s.cmd), s.confirm, key, o.rpId);
|
|
437
|
+
const a = await verifyAssertion(await sha256(s.cmd), s.confirm, key, o.rpId, "confirm");
|
|
298
438
|
if (a.ok) {
|
|
299
439
|
via = "confirm";
|
|
300
440
|
keyId = key.id;
|
|
@@ -305,6 +445,8 @@ async function verifyCommand(s, o) {
|
|
|
305
445
|
if (refusal) return bad(refusal.code, refusal.why);
|
|
306
446
|
return bad("unsigned", "the command isn't signed");
|
|
307
447
|
}
|
|
448
|
+
if (c.kind === "key.add" && !await keyUsable(c.key))
|
|
449
|
+
return bad("bad_fields", "the key a key.add brings is in a shape this machine can't use");
|
|
308
450
|
if (c.iat > o.now + SKEW_MS) return bad("future", "the command is dated in the future (is this clock right?)");
|
|
309
451
|
if (o.now - c.iat > (o.maxAgeMs ?? MACHINE_MAX_AGE_MS)) return bad("too_old", "the command was signed too long ago");
|
|
310
452
|
if (o.seenNonce) {
|
|
@@ -324,7 +466,7 @@ async function checkGrantPath(s, c, o) {
|
|
|
324
466
|
if (!g.ok) return bad("bad_grant", g.why);
|
|
325
467
|
const key = o.trusted.find((k) => k && k.id === bundle.cred);
|
|
326
468
|
if (!key) return bad("untrusted_key", "the grant was signed with a key this machine doesn't trust");
|
|
327
|
-
const a = await verifyAssertion(await sha256(bundle.str), bundle, key, o.rpId);
|
|
469
|
+
const a = await verifyAssertion(await sha256(bundle.str), bundle, key, o.rpId, "grant");
|
|
328
470
|
if (!a.ok) return bad("bad_assertion", a.why);
|
|
329
471
|
if (!await verifyCsig(s.cmd, s.csig, g.grant.pub))
|
|
330
472
|
return bad("bad_csig", "the command's signature doesn't verify \u2014 it was changed after it was signed");
|
|
@@ -336,27 +478,45 @@ export {
|
|
|
336
478
|
CMD_MAX,
|
|
337
479
|
COMMAND_TYPE,
|
|
338
480
|
CONTROL_KINDS,
|
|
481
|
+
DEVICE_CONTEXT,
|
|
482
|
+
DEVICE_SIG_FIELDS,
|
|
339
483
|
ES256,
|
|
484
|
+
FILES_MAX,
|
|
485
|
+
FILES_TOTAL_MAX,
|
|
486
|
+
FILE_NAME_MAX,
|
|
487
|
+
FILE_SIZE_MAX,
|
|
488
|
+
FILE_TYPE_MAX,
|
|
340
489
|
GRANT_MAX_MS,
|
|
341
490
|
GRANT_STR_MAX,
|
|
342
491
|
GRANT_TYPE,
|
|
492
|
+
HOME_RELATIVE_RE,
|
|
343
493
|
KEY_STORE_SESSION,
|
|
494
|
+
KEY_TYPES,
|
|
344
495
|
MACHINE_MAX_AGE_MS,
|
|
496
|
+
MODEL_MAX,
|
|
497
|
+
MODEL_RE,
|
|
345
498
|
NONCE_TTL_MS,
|
|
346
499
|
NOTE_MAX,
|
|
347
500
|
RECEIVER_MAX_AGE_MS,
|
|
348
501
|
RS256,
|
|
349
502
|
SKEW_MS,
|
|
503
|
+
START_PERMISSIONS,
|
|
350
504
|
TEXT_MAX,
|
|
351
505
|
b64url,
|
|
352
506
|
derToRaw,
|
|
507
|
+
deviceKeyId,
|
|
508
|
+
deviceMessage,
|
|
509
|
+
filesProblem,
|
|
353
510
|
fromB64url,
|
|
354
511
|
keyUsable,
|
|
512
|
+
levelRank,
|
|
513
|
+
narrower,
|
|
355
514
|
originAllowed,
|
|
356
515
|
pairChallenge,
|
|
357
516
|
parseCommand,
|
|
358
517
|
parseGrant,
|
|
359
518
|
parseSessionRef,
|
|
519
|
+
sameBytes,
|
|
360
520
|
sessionIdOk,
|
|
361
521
|
sha256,
|
|
362
522
|
verifyAssertion,
|
package/dist/control.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/control/verify.ts"],
|
|
4
|
-
"sourcesContent": ["// SPDX-License-Identifier: Apache-2.0\n// The one verifier for signed control commands (spec/CONTROL.md \u00A74\u2013\u00A75). A machine\n// runs a command only when this says yes; a receiver MAY run it too, as an early\n// word, so a bad command is refused where the person can see why.\n//\n// WebCrypto only (globalThis.crypto.subtle), no node: imports, no clock of its own\n// (`now` is passed in): the same file runs in a Cloudflare Worker, in a browser and\n// in Node \u2265 20, and conformance/control-vectors/ pins its answers.\n//\n// Every signature covers the EXACT bytes as transmitted (`cmd`, `grant.str`): the\n// strings are parsed to read their fields and never re-serialized to verify.\n\n/** One passkey tap vouches for a day key for at most this long. */\nexport const GRANT_MAX_MS = 24 * 3600_000;\n/** Clock slack between the signing device, the receiver and the machine. */\nexport const SKEW_MS = 5 * 60_000;\n/** The machine judges a command by when it was SIGNED: a message sent while the lid\n * was shut still runs when the machine wakes, up to a day later. */\nexport const MACHINE_MAX_AGE_MS = 24 * 3600_000;\n/** A receiver's early check is stricter: a command is carried within ten minutes of\n * being signed, or not at all. */\nexport const RECEIVER_MAX_AGE_MS = 10 * 60_000;\n/** How long a machine remembers a nonce: longer than MACHINE_MAX_AGE_MS + SKEW_MS. */\nexport const NONCE_TTL_MS = 25 * 3600_000;\n/** The whole command string, in UTF-16 code units (what JSON.parse sees). */\nexport const CMD_MAX = 48_000;\n/** A prompt's text, in characters. */\nexport const TEXT_MAX = 20_000;\n/** A permission answer's note. */\nexport const NOTE_MAX = 2_000;\n/** A grant string. */\nexport const GRANT_STR_MAX = 2_000;\n\nexport const COMMAND_TYPE = \"sessionpipe.control\";\nexport const GRANT_TYPE = \"sessionpipe.grant\";\nexport const CONTROL_KINDS = [\"prompt\", \"permission.answer\", \"cancel\", \"start\", \"key.add\"] as const;\n/** The session a `key.add` names: the machine's own key store, not a coding session. */\nexport const KEY_STORE_SESSION = \"sessionpipe:keys\";\nexport type ControlKind = (typeof CONTROL_KINDS)[number];\n\n/** COSE algorithm ids a trusted key may use. */\nexport const ES256 = -7;\nexport const RS256 = -257;\n\n/** A key enrolled on the machine: a passkey's credential id, COSE alg and SPKI (base64url). */\nexport interface TrustedKey {\n id: string;\n alg: number;\n spki: string;\n}\n/** A WebAuthn assertion: credential id, authenticatorData, clientDataJSON, signature (base64url). */\nexport interface Assertion {\n cred: string;\n ad: string;\n cd: string;\n sig: string;\n}\n/** The day's grant: a passkey's assertion over sha256(str), where str names the day key. */\nexport interface GrantBundle extends Assertion {\n str: string;\n}\n/** What a receiver carries for one command (control.json minus the receiver's envelope). */\nexport interface SignedCommand {\n cmd: string;\n csig?: string | null;\n grant?: GrantBundle | null;\n confirm?: Assertion | null;\n}\nexport interface Grant {\n v: 1;\n t: typeof GRANT_TYPE;\n pub: string;\n iat: number;\n exp: number;\n rp: string;\n}\nexport interface ControlCommand {\n v: 1;\n t: typeof COMMAND_TYPE;\n machine: string;\n kind: ControlKind;\n /** `<harness>:<id>`, e.g. `claude-code:8c132906-\u2026`. */\n session: string;\n text?: string;\n for?: string;\n decision?: \"allow\" | \"deny\";\n note?: string;\n cwd?: string;\n /** key.add only: the passkey to trust from now on, as the receiver holds it. */\n key?: { id: string; alg: -7 | -257; spki: string };\n /** key.add only: what the person calls the device (\"iPhone\"), for `control keys`. */\n name?: string;\n nonce: string;\n iat: number;\n}\n\n/** Why a command was refused. Stable: conformance vectors and acks name these. */\nexport type RefusalCode =\n | \"malformed\"\n | \"not_a_command\"\n | \"wrong_machine\"\n | \"unknown_kind\"\n | \"bad_session\"\n | \"bad_fields\"\n | \"bad_nonce\"\n | \"too_old\"\n | \"future\"\n | \"unsigned\"\n | \"untrusted_key\"\n | \"bad_assertion\"\n | \"bad_grant\"\n | \"outside_grant\"\n | \"bad_csig\"\n | \"replay\"\n | \"nonce_store\";\n\nexport type Verdict =\n | { ok: true; cmd: ControlCommand; via: \"grant\" | \"confirm\"; key: string }\n | { ok: false; code: RefusalCode; why: string };\n\nexport interface VerifyOptions {\n /** This machine's id; a command for another machine is refused. */\n machine: string;\n /** The receiver's WebAuthn rp id the keys were enrolled for (well-known control.signing.rp_id). */\n rpId: string;\n /** The keys enrolled on this machine (or, for a receiver's early check, the account's). */\n trusted: TrustedKey[];\n /** Epoch ms. */\n now: number;\n /** MACHINE_MAX_AGE_MS on a machine (default), RECEIVER_MAX_AGE_MS on a receiver. */\n maxAgeMs?: number;\n /** Called once, last, only for a command whose signatures all verified: answer\n * true when the nonce was seen before, otherwise record it (durably) and answer\n * false. A throw refuses the command (`nonce_store`): never run what could not\n * be remembered. Omit it on a receiver's early check. */\n seenNonce?: (nonce: string) => boolean | Promise<boolean>;\n}\n\nconst NONCE_RE = /^[A-Za-z0-9_-]{22,64}$/;\nconst MACHINE_RE = /^m_[A-Za-z0-9_-]{16,40}$/;\nconst HARNESS_RE = /^[a-z][a-z0-9-]{0,39}$/;\nconst SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst B64URL_RE = /^[A-Za-z0-9_-]*$/;\nconst UP = 0x01;\nconst UV = 0x04;\n\n// --- encodings ------------------------------------------------------------------\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nexport function b64url(bytes: Uint8Array | ArrayBuffer): string {\n const u = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let s = \"\";\n for (const b of u) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** base64url without padding, strictly; null for anything else. */\nexport function fromB64url(s: unknown): Uint8Array | null {\n if (typeof s !== \"string\" || !B64URL_RE.test(s) || s.length % 4 === 1) return null;\n try {\n const bin = atob(s.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((s.length + 3) % 4));\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n } catch {\n return null;\n }\n}\n\nexport async function sha256(data: string | Uint8Array): Promise<Uint8Array> {\n const bytes = typeof data === \"string\" ? enc.encode(data) : data;\n return new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes as Uint8Array<ArrayBuffer>));\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n}\n\nfunction sameBytes(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n let d = 0;\n for (let i = 0; i < a.length; i++) d |= (a[i] as number) ^ (b[i] as number);\n return d === 0;\n}\n\n/** A DER ECDSA signature (what WebAuthn returns) as the 64-byte r\u2016s WebCrypto verifies.\n * Strict: every length must match the bytes, so one signature has one encoding (a\n * P-256 signature is at most 72 bytes, so the short length form is the only one). */\nexport function derToRaw(der: Uint8Array): Uint8Array | null {\n if (der[0] !== 0x30 || der.length < 8 || der[1] !== der.length - 2) return null;\n let i = 2;\n const part = (): Uint8Array | null => {\n if (der[i] !== 0x02) return null;\n const len = der[i + 1] as number;\n if (len < 1 || len > 33) return null;\n let v = der.slice(i + 2, i + 2 + len);\n if (v.length !== len) return null;\n // Minimal integers only: no needless leading zero, and never negative.\n if ((v[0] as number) & 0x80) return null;\n if (len > 1 && v[0] === 0 && !((v[1] as number) & 0x80)) return null;\n i += 2 + len;\n while (v.length > 32 && v[0] === 0) v = v.slice(1);\n if (v.length > 32) return null;\n const out = new Uint8Array(32);\n out.set(v, 32 - v.length);\n return out;\n };\n const r = part();\n const s = part();\n return r && s && i === der.length ? concat(r, s) : null;\n}\n\nconst u8 = (b: Uint8Array) => b as Uint8Array<ArrayBuffer>;\n/** WebCrypto's key type, named without the DOM lib (Workers, Node and browsers all have it). */\ntype CryptoKey = Awaited<ReturnType<typeof crypto.subtle.importKey>>;\n\nasync function importKey(key: TrustedKey): Promise<CryptoKey | null> {\n const spki = fromB64url(key.spki);\n if (!spki?.length) return null;\n try {\n if (key.alg === ES256)\n return await crypto.subtle.importKey(\"spki\", u8(spki), { name: \"ECDSA\", namedCurve: \"P-256\" }, false, [\"verify\"]);\n if (key.alg === RS256) {\n const k = await crypto.subtle.importKey(\"spki\", u8(spki), { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" }, false, [\n \"verify\",\n ]);\n const bits = (k.algorithm as { modulusLength?: number }).modulusLength ?? 0;\n return bits >= 2048 ? k : null;\n }\n } catch {}\n return null;\n}\n\n/** Can this key be enrolled at all? A machine refuses to trust a key it could never verify with. */\nexport async function keyUsable(key: TrustedKey): Promise<boolean> {\n return (\n !!key &&\n typeof key.id === \"string\" &&\n /^[A-Za-z0-9_-]{16,1400}$/.test(key.id) &&\n (key.alg === ES256 || key.alg === RS256) &&\n !!(await importKey(key))\n );\n}\n\n// --- WebAuthn -------------------------------------------------------------------\n\n/** A WebAuthn origin a key may have been used from: https on the rp id or a subdomain;\n * for rp id `localhost`, plain http on any port too. A bare origin, as a browser writes it. */\nexport function originAllowed(origin: unknown, rpId: string): boolean {\n if (typeof origin !== \"string\" || !rpId) return false;\n let u: URL;\n try {\n u = new URL(origin);\n } catch {\n return false;\n }\n if (u.origin !== origin) return false;\n if (u.hostname !== rpId && !u.hostname.endsWith(`.${rpId}`)) return false;\n if (u.protocol === \"https:\") return true;\n return u.protocol === \"http:\" && rpId === \"localhost\";\n}\n\ntype Check = { ok: true } | { ok: false; why: string };\nconst no = (why: string): Check => ({ ok: false, why });\n\n/** One passkey assertion over `challenge`, made with `key`, by a verified person. */\nexport async function verifyAssertion(\n challenge: Uint8Array,\n a: Assertion | null | undefined,\n key: TrustedKey,\n rpId: string,\n): Promise<Check> {\n if (!a || typeof a !== \"object\") return no(\"no passkey signature\");\n if (a.cred !== key.id) return no(\"signed with a key this machine doesn't trust\");\n const cdBytes = fromB64url(a.cd);\n const ad = fromB64url(a.ad);\n const sig = fromB64url(a.sig);\n if (!cdBytes || !ad || !sig?.length) return no(\"the passkey signature is malformed\");\n let cd: { type?: unknown; challenge?: unknown; origin?: unknown; crossOrigin?: unknown };\n try {\n cd = JSON.parse(dec.decode(cdBytes));\n } catch {\n return no(\"the passkey signature is malformed\");\n }\n if (!cd || typeof cd !== \"object\") return no(\"the passkey signature is malformed\");\n if (cd.type !== \"webauthn.get\") return no(\"the passkey answered a different question\");\n if (cd.challenge !== b64url(challenge)) return no(\"the passkey signed something else\");\n if (!originAllowed(cd.origin, rpId)) return no(`the passkey was used on another site, not ${rpId}`);\n if (cd.crossOrigin === true) return no(\"the passkey was used inside another site's frame\");\n if (ad.length < 37) return no(\"the passkey signature is malformed\");\n if (!sameBytes(ad.slice(0, 32), await sha256(rpId))) return no(`the passkey belongs to another site, not ${rpId}`);\n const flags = ad[32] as number;\n if (!(flags & UP) || !(flags & UV)) return no(\"the passkey signed without verifying the person\");\n const pub = await importKey(key);\n if (!pub) return no(\"a trusted key is unusable\");\n const signed = concat(ad, await sha256(cdBytes));\n let good = false;\n try {\n if (key.alg === ES256) {\n const raw = derToRaw(sig);\n good = !!raw && (await crypto.subtle.verify({ name: \"ECDSA\", hash: \"SHA-256\" }, pub, u8(raw), u8(signed)));\n } else {\n good = await crypto.subtle.verify({ name: \"RSASSA-PKCS1-v1_5\" }, pub, u8(sig), u8(signed));\n }\n } catch {\n good = false;\n }\n return good ? { ok: true } : no(\"the passkey signature doesn't verify\");\n}\n\n/** What a passkey signs to enroll on a machine: bound to that machine and that pairing code. */\nexport function pairChallenge(machine: string, code: string): Promise<Uint8Array> {\n return sha256(`sessionpipe.pair:${machine}:${code}`);\n}\n\n/** The enrollment proof: the new key's assertion over pairChallenge(machine, code).\n * Only after this verifies does the key go into the machine's trust store. */\nexport async function verifyEnrollment(o: {\n machine: string;\n code: string;\n key: TrustedKey;\n proof: Assertion;\n rpId: string;\n}): Promise<Check> {\n if (!(await keyUsable(o.key))) return no(\"the key is in a shape this machine can't use\");\n return verifyAssertion(await pairChallenge(o.machine, o.code), o.proof, o.key, o.rpId);\n}\n\n// --- the command ------------------------------------------------------------------\n\ntype Parsed = { ok: true; cmd: ControlCommand } | { ok: false; code: RefusalCode; why: string };\nconst bad = (code: RefusalCode, why: string): { ok: false; code: RefusalCode; why: string } => ({\n ok: false,\n code,\n why,\n});\nconst isNum = (n: unknown): n is number => typeof n === \"number\" && Number.isFinite(n);\n\n/** The session id rule per harness (ADAPTERS.md): Claude Code ids are UUIDs. */\nexport function sessionIdOk(harness: string, id: string): boolean {\n if (!SESSION_ID_RE.test(id)) return false;\n if (harness === \"claude-code\") return UUID_RE.test(id);\n return true;\n}\n\n/** Split `<harness>:<id>`; null when either half breaks its rule. */\nexport function parseSessionRef(ref: unknown): { harness: string; id: string } | null {\n if (typeof ref !== \"string\") return null;\n const i = ref.indexOf(\":\");\n if (i <= 0) return null;\n const harness = ref.slice(0, i);\n const id = ref.slice(i + 1);\n return HARNESS_RE.test(harness) && sessionIdOk(harness, id) ? { harness, id } : null;\n}\n\n/** Read the command's fields from the exact string. Structure only; no clock, no keys. */\nexport function parseCommand(str: unknown): Parsed {\n if (typeof str !== \"string\" || !str || str.length > CMD_MAX)\n return bad(\"malformed\", \"the command is missing or too long\");\n let c: Record<string, unknown>;\n try {\n c = JSON.parse(str);\n } catch {\n return bad(\"malformed\", \"the command is not JSON\");\n }\n if (!c || typeof c !== \"object\" || Array.isArray(c)) return bad(\"malformed\", \"the command is not an object\");\n if (c.v !== 1 || c.t !== COMMAND_TYPE) return bad(\"not_a_command\", \"not a sessionpipe control command\");\n if (typeof c.machine !== \"string\" || !MACHINE_RE.test(c.machine)) return bad(\"malformed\", \"which machine?\");\n if (!CONTROL_KINDS.includes(c.kind as ControlKind))\n return bad(\"unknown_kind\", \"the command asks for something unknown\");\n const ref = parseSessionRef(c.session);\n if (!ref) return bad(\"bad_session\", \"which session?\");\n if (typeof c.nonce !== \"string\" || !NONCE_RE.test(c.nonce))\n return bad(\"bad_nonce\", \"the nonce is missing or malformed\");\n if (!isNum(c.iat) || !Number.isInteger(c.iat) || c.iat <= 0)\n return bad(\"malformed\", \"the command has no signing time\");\n const has = (k: string) => c[k] !== undefined;\n const text = () => typeof c.text === \"string\" && c.text.trim().length > 0 && c.text.length <= TEXT_MAX;\n switch (c.kind as ControlKind) {\n case \"prompt\":\n if (!text()) return bad(\"bad_fields\", `a prompt needs text, at most ${TEXT_MAX} characters`);\n if (has(\"for\") || has(\"decision\") || has(\"cwd\")) return bad(\"bad_fields\", \"a prompt carries only text\");\n break;\n case \"start\":\n if (!text()) return bad(\"bad_fields\", `a new session needs text, at most ${TEXT_MAX} characters`);\n if (typeof c.cwd !== \"string\" || !isAbsolute(c.cwd) || c.cwd.length > 1024)\n return bad(\"bad_fields\", \"a new session needs an absolute folder\");\n if (has(\"for\") || has(\"decision\")) return bad(\"bad_fields\", \"a start carries text and cwd\");\n break;\n case \"permission.answer\":\n if (typeof c.for !== \"string\" || !c.for || c.for.length > 128) return bad(\"bad_fields\", \"which attention?\");\n if (c.decision !== \"allow\" && c.decision !== \"deny\") return bad(\"bad_fields\", \"allow or deny?\");\n if (has(\"text\") || has(\"cwd\")) return bad(\"bad_fields\", \"a permission answer carries for, decision and note\");\n break;\n case \"cancel\":\n if (has(\"text\") || has(\"for\") || has(\"decision\") || has(\"cwd\"))\n return bad(\"bad_fields\", \"a cancel carries nothing\");\n break;\n case \"key.add\": {\n // A device the machine already trusts vouches for a new one (CONTROL.md \u00A72): the\n // new passkey's id, algorithm and public key are inside the signed bytes.\n if (c.session !== KEY_STORE_SESSION) return bad(\"bad_session\", `a key.add names ${KEY_STORE_SESSION}`);\n const k = c.key as { id?: unknown; alg?: unknown; spki?: unknown } | undefined;\n const b64 = (v: unknown, min: number, max: number) =>\n typeof v === \"string\" && v.length >= min && v.length <= max && /^[A-Za-z0-9_-]+$/.test(v);\n if (\n !k ||\n typeof k !== \"object\" ||\n !b64(k.id, 16, 1400) ||\n (k.alg !== -7 && k.alg !== -257) ||\n !b64(k.spki, 40, 2000)\n )\n return bad(\"bad_fields\", \"a key.add carries the key: id, alg (-7 or -257) and spki\");\n if (has(\"name\") && (typeof c.name !== \"string\" || !c.name.trim() || c.name.length > 80))\n return bad(\"bad_fields\", \"a device name is text, at most 80 characters\");\n if (has(\"text\") || has(\"for\") || has(\"decision\") || has(\"cwd\"))\n return bad(\"bad_fields\", \"a key.add carries the key and a name\");\n break;\n }\n }\n if (c.kind !== \"key.add\" && (has(\"key\") || has(\"name\"))) return bad(\"bad_fields\", \"only a key.add carries a key\");\n if (has(\"note\") && (typeof c.note !== \"string\" || c.note.length > NOTE_MAX))\n return bad(\"bad_fields\", `a note is text, at most ${NOTE_MAX} characters`);\n return { ok: true, cmd: c as unknown as ControlCommand };\n}\n\nfunction isAbsolute(p: string): boolean {\n return p.startsWith(\"/\") || /^[A-Za-z]:[\\\\/]/.test(p);\n}\n\n/** The grant's fields, read from its exact string. */\nexport function parseGrant(str: unknown, rpId: string): { ok: true; grant: Grant } | { ok: false; why: string } {\n if (typeof str !== \"string\" || !str || str.length > GRANT_STR_MAX)\n return { ok: false, why: \"the grant is missing or too long\" };\n let g: Record<string, unknown>;\n try {\n g = JSON.parse(str);\n } catch {\n return { ok: false, why: \"the grant is not JSON\" };\n }\n if (!g || typeof g !== \"object\" || Array.isArray(g)) return { ok: false, why: \"the grant is not an object\" };\n if (g.v !== 1 || g.t !== GRANT_TYPE) return { ok: false, why: \"not a sessionpipe grant\" };\n if (g.rp !== rpId) return { ok: false, why: `the grant is for another site, not ${rpId}` };\n if (typeof g.pub !== \"string\" || !fromB64url(g.pub)?.length || !isNum(g.iat) || !isNum(g.exp))\n return { ok: false, why: \"the grant is missing parts\" };\n if (g.exp <= g.iat) return { ok: false, why: \"the grant ends before it starts\" };\n if (g.exp - g.iat > GRANT_MAX_MS + SKEW_MS) return { ok: false, why: \"the grant lasts longer than a day\" };\n return { ok: true, grant: g as unknown as Grant };\n}\n\nasync function verifyCsig(cmd: string, csig: unknown, pub: string): Promise<boolean> {\n const sig = fromB64url(csig);\n const spki = fromB64url(pub);\n if (sig?.length !== 64 || !spki) return false;\n try {\n const key = await crypto.subtle.importKey(\"spki\", u8(spki), { name: \"ECDSA\", namedCurve: \"P-256\" }, false, [\n \"verify\",\n ]);\n return await crypto.subtle.verify({ name: \"ECDSA\", hash: \"SHA-256\" }, key, u8(sig), u8(enc.encode(cmd)));\n } catch {\n return false;\n }\n}\n\n/**\n * Is this a command this machine may run? The checks, in the spec's order\n * (CONTROL.md \u00A75): the exact bytes parse; the command is for this machine; it is\n * signed \u2014 a grant from a trusted key plus the day key's signature, or a confirm\n * over sha256(cmd); it was signed within the age window; its nonce is new.\n * The folder and open-attention checks that follow are the daemon's (they read\n * the machine, not the command).\n */\nexport async function verifyCommand(s: SignedCommand | null | undefined, o: VerifyOptions): Promise<Verdict> {\n if (!s || typeof s !== \"object\") return bad(\"malformed\", \"nothing to verify\");\n const p = parseCommand(s.cmd);\n if (!p.ok) return p;\n const c = p.cmd;\n if (c.machine !== o.machine) return bad(\"wrong_machine\", \"the command is for another machine\");\n\n let via: \"grant\" | \"confirm\" | null = null;\n let keyId = \"\";\n let refusal: { code: RefusalCode; why: string } | null = null;\n if (s.grant && s.csig) {\n const r = await checkGrantPath(s, c, o);\n if (r.ok) {\n via = \"grant\";\n keyId = r.key;\n } else refusal = r;\n }\n if (!via && s.confirm) {\n const key = o.trusted.find((k) => k && k.id === s.confirm?.cred);\n if (!key) refusal = { code: \"untrusted_key\", why: \"confirmed with a key this machine doesn't trust\" };\n else {\n const a = await verifyAssertion(await sha256(s.cmd), s.confirm, key, o.rpId);\n if (a.ok) {\n via = \"confirm\";\n keyId = key.id;\n } else refusal = { code: \"bad_assertion\", why: a.why };\n }\n }\n if (!via) {\n if (refusal) return bad(refusal.code, refusal.why);\n return bad(\"unsigned\", \"the command isn't signed\");\n }\n if (c.iat > o.now + SKEW_MS) return bad(\"future\", \"the command is dated in the future (is this clock right?)\");\n if (o.now - c.iat > (o.maxAgeMs ?? MACHINE_MAX_AGE_MS)) return bad(\"too_old\", \"the command was signed too long ago\");\n if (o.seenNonce) {\n let seen: boolean;\n try {\n seen = await o.seenNonce(c.nonce);\n } catch {\n return bad(\"nonce_store\", \"the nonce couldn't be recorded, so the command didn't run\");\n }\n if (seen) return bad(\"replay\", \"this command was already used once\");\n }\n return { ok: true, cmd: c, via, key: keyId };\n}\n\nasync function checkGrantPath(\n s: SignedCommand,\n c: ControlCommand,\n o: VerifyOptions,\n): Promise<{ ok: true; key: string } | { ok: false; code: RefusalCode; why: string }> {\n const bundle = s.grant as GrantBundle;\n const g = parseGrant(bundle.str, o.rpId);\n if (!g.ok) return bad(\"bad_grant\", g.why);\n const key = o.trusted.find((k) => k && k.id === bundle.cred);\n if (!key) return bad(\"untrusted_key\", \"the grant was signed with a key this machine doesn't trust\");\n const a = await verifyAssertion(await sha256(bundle.str), bundle, key, o.rpId);\n if (!a.ok) return bad(\"bad_assertion\", a.why);\n if (!(await verifyCsig(s.cmd, s.csig, g.grant.pub)))\n return bad(\"bad_csig\", \"the command's signature doesn't verify \u2014 it was changed after it was signed\");\n if (c.iat < g.grant.iat - SKEW_MS || c.iat > g.grant.exp)\n return bad(\"outside_grant\", \"the command was signed outside its grant's window\");\n return { ok: true, key: key.id };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["// SPDX-License-Identifier: Apache-2.0\n// The one verifier for signed control commands (spec/CONTROL.md \u00A74\u2013\u00A75). A machine\n// runs a command only when this says yes; a receiver MAY run it too, as an early\n// word, so a bad command is refused where the person can see why.\n//\n// WebCrypto only (globalThis.crypto.subtle), no node: imports, no clock of its own\n// (`now` is passed in): the same file runs in a Cloudflare Worker, in a browser and\n// in Node \u2265 20, and conformance/control-vectors/ pins its answers.\n//\n// Every signature covers the EXACT bytes as transmitted (`cmd`, `grant.str`): the\n// strings are parsed to read their fields and never re-serialized to verify.\n\n/** One passkey tap vouches for a day key for at most this long. */\nexport const GRANT_MAX_MS = 24 * 3600_000;\n/** Clock slack between the signing device, the receiver and the machine. */\nexport const SKEW_MS = 5 * 60_000;\n/** The machine judges a command by when it was SIGNED: a message sent while the lid\n * was shut still runs when the machine wakes, up to a day later. */\nexport const MACHINE_MAX_AGE_MS = 24 * 3600_000;\n/** A receiver's early check is stricter: a command is carried within ten minutes of\n * being signed, or not at all. */\nexport const RECEIVER_MAX_AGE_MS = 10 * 60_000;\n/** How long a machine remembers a nonce: longer than MACHINE_MAX_AGE_MS + SKEW_MS. */\nexport const NONCE_TTL_MS = 25 * 3600_000;\n/** The whole command string, in UTF-16 code units (what JSON.parse sees). */\nexport const CMD_MAX = 48_000;\n/** A prompt's text, in characters. */\nexport const TEXT_MAX = 20_000;\n/** A permission answer's note. */\nexport const NOTE_MAX = 2_000;\n/** A grant string. */\nexport const GRANT_STR_MAX = 2_000;\n/** Attachments on a `start` or a `prompt` (CONTROL.md \u00A73.1): at most this many files, */\nexport const FILES_MAX = 10;\n/** each at most this many bytes, */\nexport const FILE_SIZE_MAX = 26_214_400;\n/** together at most this many, */\nexport const FILES_TOTAL_MAX = 52_428_800;\n/** with a name of at most this many characters (after NFC) */\nexport const FILE_NAME_MAX = 200;\n/** and a media type of at most this many. */\nexport const FILE_TYPE_MAX = 100;\n/** A start's `model` (CONTROL.md \u00A73.1): at most this many characters, */\nexport const MODEL_MAX = 100;\n/** in this alphabet, never starting with a dash, so it can never read as a flag. */\nexport const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\\]-]*$/;\n/** The levels a start may ask for (CONTROL.md \u00A73.1), narrowest first. Closed: no level\n * is wider than `auto`, and none turns a harness's approval off. */\nexport const START_PERMISSIONS = [\"read\", \"safe\", \"edits\", \"auto\"] as const;\nexport type StartPermission = (typeof START_PERMISSIONS)[number];\n/** What a run is started with: a level, or `inherit` (CONTROL.md \u00A72) \u2014 no permission\n * flag at all, so the harness starts in its own default on the machine. Only a machine\n * the person put in inherit mode runs that way; a command never names it. */\nexport type RunLevel = StartPermission | \"inherit\";\n\n/** Where a level sits, narrowest (0) to widest; -1 for anything that isn't one. */\nexport const levelRank = (l: unknown): number => START_PERMISSIONS.indexOf(l as StartPermission);\n/** The narrower of two levels: what a later run of a session gets, its recorded level\n * against what the machine's mode allows now. */\nexport const narrower = (a: StartPermission, b: StartPermission): StartPermission =>\n levelRank(a) <= levelRank(b) ? a : b;\n\nexport const COMMAND_TYPE = \"sessionpipe.control\";\nexport const GRANT_TYPE = \"sessionpipe.grant\";\nexport const CONTROL_KINDS = [\"prompt\", \"permission.answer\", \"cancel\", \"start\", \"key.add\"] as const;\n/** The session a `key.add` names: the machine's own key store, not a coding session. */\nexport const KEY_STORE_SESSION = \"sessionpipe:keys\";\nexport type ControlKind = (typeof CONTROL_KINDS)[number];\n\n/** COSE algorithm ids a trusted key may use. */\nexport const ES256 = -7;\nexport const RS256 = -257;\n\n/** The kinds of key a machine may trust (CONTROL.md \u00A72): a WebAuthn passkey, or a\n * device key an app holds in hardware where it has no passkey to offer. */\nexport const KEY_TYPES = [\"passkey\", \"device\"] as const;\nexport type KeyType = (typeof KEY_TYPES)[number];\n\n/** A key enrolled on the machine: a passkey's credential id, COSE alg and SPKI\n * (base64url); a device key also says so (`type`), and what its app declared about\n * it (`store`, `uv`). The machine verifies by its own record, never by a message. */\nexport interface TrustedKey {\n id: string;\n alg: number;\n spki: string;\n /** Present only on a device key (CONTROL.md \u00A72 \"Device keys\"). */\n type?: \"device\";\n store?: \"hardware\";\n uv?: \"each-use\";\n}\n/** A WebAuthn assertion: credential id, authenticatorData, clientDataJSON, signature (base64url). */\nexport interface Assertion {\n cred: string;\n ad: string;\n cd: string;\n sig: string;\n}\n/** A device key's signature (CONTROL.md \u00A73.2): its id and 64 bytes r\u2016s over deviceMessage(). */\nexport interface DeviceSignature {\n cred: string;\n sig: string;\n}\n/** The day's grant: a passkey's assertion over sha256(str), where str names the day key. */\nexport interface GrantBundle extends Assertion {\n str: string;\n}\n/** The day's grant from a device key: its signature over deviceMessage(\"grant\", rp, sha256(str)). */\nexport interface DeviceGrant extends DeviceSignature {\n str: string;\n}\n/** What a receiver carries for one command (control.json minus the receiver's envelope). */\nexport interface SignedCommand {\n cmd: string;\n csig?: string | null;\n grant?: GrantBundle | DeviceGrant | null;\n confirm?: Assertion | DeviceSignature | null;\n}\nexport interface Grant {\n v: 1;\n t: typeof GRANT_TYPE;\n pub: string;\n iat: number;\n exp: number;\n rp: string;\n}\n/** One file attached to a `start`: the bytes live on the receiver, named by their hash. */\nexport interface ControlFile {\n /** The file name the machine writes (NFC). */\n name: string;\n /** Media type, `type/subtype`. */\n type: string;\n /** Bytes. */\n size: number;\n /** SHA-256 of the bytes, base64url without padding (43 characters). */\n sha256: string;\n}\nexport interface ControlCommand {\n v: 1;\n t: typeof COMMAND_TYPE;\n machine: string;\n kind: ControlKind;\n /** `<harness>:<id>`, e.g. `claude-code:8c132906-\u2026`. */\n session: string;\n text?: string;\n for?: string;\n decision?: \"allow\" | \"deny\";\n note?: string;\n cwd?: string;\n /** key.add only: the key to trust from now on, as the receiver holds it (a passkey,\n * or a device key with its `type`, `store` and `uv`). */\n key?: TrustedKey;\n /** key.add only: what the person calls the device (\"iPhone\"), for `control keys`. */\n name?: string;\n /** start, prompt: files to download, verify and write into the session's folder. */\n files?: ControlFile[];\n /** start only: the level the new session runs at (never above the machine's mode). */\n permission?: StartPermission;\n /** start only: the model the harness runs, as the harness names it. */\n model?: string;\n nonce: string;\n iat: number;\n}\n\n/** Why a command was refused. Stable: conformance vectors and acks name these. */\nexport type RefusalCode =\n | \"malformed\"\n | \"not_a_command\"\n | \"wrong_machine\"\n | \"unknown_kind\"\n | \"bad_session\"\n | \"bad_fields\"\n | \"bad_nonce\"\n | \"too_old\"\n | \"future\"\n | \"unsigned\"\n | \"untrusted_key\"\n | \"bad_assertion\"\n | \"bad_grant\"\n | \"outside_grant\"\n | \"bad_csig\"\n | \"replay\"\n | \"nonce_store\";\n\nexport type Verdict =\n | { ok: true; cmd: ControlCommand; via: \"grant\" | \"confirm\"; key: string }\n | { ok: false; code: RefusalCode; why: string };\n\nexport interface VerifyOptions {\n /** This machine's id; a command for another machine is refused. */\n machine: string;\n /** The receiver's WebAuthn rp id the keys were enrolled for (well-known control.signing.rp_id). */\n rpId: string;\n /** The keys enrolled on this machine (or, for a receiver's early check, the account's). */\n trusted: TrustedKey[];\n /** Epoch ms. */\n now: number;\n /** MACHINE_MAX_AGE_MS on a machine (default), RECEIVER_MAX_AGE_MS on a receiver. */\n maxAgeMs?: number;\n /** Called once, last, only for a command whose signatures all verified: answer\n * true when the nonce was seen before, otherwise record it (durably) and answer\n * false. A throw refuses the command (`nonce_store`): never run what could not\n * be remembered. Omit it on a receiver's early check. */\n seenNonce?: (nonce: string) => boolean | Promise<boolean>;\n}\n\nconst NONCE_RE = /^[A-Za-z0-9_-]{22,64}$/;\nconst MACHINE_RE = /^m_[A-Za-z0-9_-]{16,40}$/;\nconst HARNESS_RE = /^[a-z][a-z0-9-]{0,39}$/;\nconst SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst B64URL_RE = /^[A-Za-z0-9_-]*$/;\nconst SHA256_RE = /^[A-Za-z0-9_-]{43}$/;\nconst MEDIA_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;\n// biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are exactly what it rejects\nconst BAD_NAME_CHAR_RE = /[/\\\\\\x00-\\x1f\\x7f]/;\nconst UP = 0x01;\nconst UV = 0x04;\n\n/** The fields each kind carries besides the common ones (CONTROL.md \u00A73.1). The set is\n * closed: a field it doesn't list is `bad_fields`, so a field added later fails closed\n * on a machine that predates it instead of being ignored. */\nconst COMMON_FIELDS: readonly string[] = [\"v\", \"t\", \"machine\", \"kind\", \"session\", \"nonce\", \"iat\", \"note\"];\nconst FIELDS: Record<ControlKind, readonly string[]> = {\n prompt: [\"text\", \"files\"],\n \"permission.answer\": [\"for\", \"decision\"],\n cancel: [],\n start: [\"text\", \"cwd\", \"files\", \"permission\", \"model\"],\n \"key.add\": [\"key\", \"name\"],\n};\n\n// --- encodings ------------------------------------------------------------------\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nexport function b64url(bytes: Uint8Array | ArrayBuffer): string {\n const u = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let s = \"\";\n for (const b of u) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** base64url without padding, strictly; null for anything else. */\nexport function fromB64url(s: unknown): Uint8Array | null {\n if (typeof s !== \"string\" || !B64URL_RE.test(s) || s.length % 4 === 1) return null;\n try {\n const bin = atob(s.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((s.length + 3) % 4));\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n } catch {\n return null;\n }\n}\n\nexport async function sha256(data: string | Uint8Array): Promise<Uint8Array> {\n const bytes = typeof data === \"string\" ? enc.encode(data) : data;\n return new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes as Uint8Array<ArrayBuffer>));\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n}\n\n/** Equal, compared whole: a mismatch costs the same as a match. */\nexport function sameBytes(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n let d = 0;\n for (let i = 0; i < a.length; i++) d |= (a[i] as number) ^ (b[i] as number);\n return d === 0;\n}\n\n/** A DER ECDSA signature (what WebAuthn returns) as the 64-byte r\u2016s WebCrypto verifies.\n * Strict: every length must match the bytes, so one signature has one encoding (a\n * P-256 signature is at most 72 bytes, so the short length form is the only one). */\nexport function derToRaw(der: Uint8Array): Uint8Array | null {\n if (der[0] !== 0x30 || der.length < 8 || der[1] !== der.length - 2) return null;\n let i = 2;\n const part = (): Uint8Array | null => {\n if (der[i] !== 0x02) return null;\n const len = der[i + 1] as number;\n if (len < 1 || len > 33) return null;\n let v = der.slice(i + 2, i + 2 + len);\n if (v.length !== len) return null;\n // Minimal integers only: no needless leading zero, and never negative.\n if ((v[0] as number) & 0x80) return null;\n if (len > 1 && v[0] === 0 && !((v[1] as number) & 0x80)) return null;\n i += 2 + len;\n while (v.length > 32 && v[0] === 0) v = v.slice(1);\n if (v.length > 32) return null;\n const out = new Uint8Array(32);\n out.set(v, 32 - v.length);\n return out;\n };\n const r = part();\n const s = part();\n return r && s && i === der.length ? concat(r, s) : null;\n}\n\nconst u8 = (b: Uint8Array) => b as Uint8Array<ArrayBuffer>;\n/** WebCrypto's key type, named without the DOM lib (Workers, Node and browsers all have it). */\ntype CryptoKey = Awaited<ReturnType<typeof crypto.subtle.importKey>>;\n\nasync function importKey(key: TrustedKey): Promise<CryptoKey | null> {\n const spki = fromB64url(key.spki);\n if (!spki?.length) return null;\n try {\n if (key.alg === ES256)\n return await crypto.subtle.importKey(\"spki\", u8(spki), { name: \"ECDSA\", namedCurve: \"P-256\" }, false, [\"verify\"]);\n if (key.alg === RS256) {\n const k = await crypto.subtle.importKey(\"spki\", u8(spki), { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" }, false, [\n \"verify\",\n ]);\n const bits = (k.algorithm as { modulusLength?: number }).modulusLength as number; // every RSA key has one\n return bits >= 2048 ? k : null;\n }\n } catch {}\n return null;\n}\n\nconst PASSKEY_FIELDS = new Set([\"id\", \"alg\", \"spki\"]);\nconst DEVICE_FIELDS = new Set([\"id\", \"alg\", \"spki\", \"type\", \"store\", \"uv\"]);\n/** The only fields a device signature has (`str` only in a grant). */\nexport const DEVICE_SIG_FIELDS = [\"cred\", \"sig\", \"str\"] as const;\n\n/** A device key's id: the SHA-256 of its SPKI bytes, base64url without padding. */\nexport async function deviceKeyId(spki: string): Promise<string> {\n return b64url(await sha256(fromB64url(spki) ?? new Uint8Array()));\n}\n\n/** A device key's declared rules, ignoring any bookkeeping fields a store keeps beside\n * them (a name, when it was added): ES256, hardware, verified each use, named by its\n * SPKI's hash, and a P-256 key WebCrypto can import. */\nasync function deviceRecordValid(key: TrustedKey): Promise<boolean> {\n if (key.type !== \"device\" || key.alg !== ES256 || key.store !== \"hardware\" || key.uv !== \"each-use\") return false;\n if (typeof key.id !== \"string\" || !SHA256_RE.test(key.id) || key.id !== (await deviceKeyId(key.spki))) return false;\n return !!(await importKey(key));\n}\n\n/** Can this key be enrolled at all? A machine refuses to trust a key it could never\n * verify with. The key's fields are closed: a passkey is `id`, `alg`, `spki` and\n * nothing else; a device key adds exactly `type`, `store` and `uv` (CONTROL.md \u00A72),\n * so a kind added later is refused by every machine that predates it. */\nexport async function keyUsable(key: TrustedKey): Promise<boolean> {\n if (!key || typeof key !== \"object\" || Array.isArray(key)) return false;\n const fields = Object.keys(key).filter((k) => (key as unknown as Record<string, unknown>)[k] !== undefined);\n if (\"type\" in key && key.type !== undefined) {\n if (!fields.every((f) => DEVICE_FIELDS.has(f))) return false;\n return deviceRecordValid(key);\n }\n if (!fields.every((f) => PASSKEY_FIELDS.has(f))) return false;\n return (\n typeof key.id === \"string\" &&\n /^[A-Za-z0-9_-]{16,1400}$/.test(key.id) &&\n (key.alg === ES256 || key.alg === RS256) &&\n !!(await importKey(key))\n );\n}\n\n// --- WebAuthn -------------------------------------------------------------------\n\n/** A WebAuthn origin a key may have been used from: https on the rp id or a subdomain;\n * for rp id `localhost`, plain http on any port too. A bare origin, as a browser writes it. */\nexport function originAllowed(origin: unknown, rpId: string): boolean {\n if (typeof origin !== \"string\" || !rpId) return false;\n let u: URL;\n try {\n u = new URL(origin);\n } catch {\n return false;\n }\n if (u.origin !== origin) return false;\n if (u.hostname !== rpId && !u.hostname.endsWith(`.${rpId}`)) return false;\n if (u.protocol === \"https:\") return true;\n return u.protocol === \"http:\" && rpId === \"localhost\";\n}\n\ntype Check = { ok: true } | { ok: false; why: string };\nconst no = (why: string): Check => ({ ok: false, why });\n\n// --- device keys (CONTROL.md \u00A73.2) ------------------------------------------------\n\n/** The first line of every message a device key signs. */\nexport const DEVICE_CONTEXT = \"sessionpipe.device.v1\";\n/** What a device signature is for: one use can never answer for another. */\nexport type DevicePurpose = \"grant\" | \"confirm\" | \"pair\";\n\n/** The exact bytes a device key signs: the context, the purpose, the rp id as the\n * machine recorded it, and the base64url (no padding) of the 32-byte challenge a\n * passkey would sign for that purpose, joined by single line feeds, none at the end. */\nexport function deviceMessage(purpose: DevicePurpose, rpId: string, challenge: Uint8Array): Uint8Array {\n return enc.encode(`${DEVICE_CONTEXT}\\n${purpose}\\n${rpId}\\n${b64url(challenge)}`);\n}\n\n/** One device signature over `challenge` for `purpose`, made with the device key `key`. */\nasync function verifyDeviceSignature(\n challenge: Uint8Array,\n a: object,\n key: TrustedKey,\n rpId: string,\n purpose: DevicePurpose,\n): Promise<Check> {\n const sig = a as Record<string, unknown>;\n for (const f of Object.keys(sig)) {\n if (f === \"str\" && purpose === \"grant\") continue;\n if (f !== \"cred\" && f !== \"sig\") return no(`a device key's signature carries no ${f}`);\n }\n if (sig.cred !== key.id) return no(\"signed with a key this machine doesn't trust\");\n if (!(await deviceRecordValid(key))) return no(\"a trusted key is unusable\");\n const raw = fromB64url(sig.sig);\n if (raw?.length !== 64) return no(\"a device key's signature is 64 bytes, r\u2016s\");\n const pub = await importKey(key);\n /* v8 ignore next -- deviceRecordValid imported it already */\n if (!pub) return no(\"a trusted key is unusable\");\n let good = false;\n try {\n good = await crypto.subtle.verify(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n pub,\n u8(raw),\n u8(deviceMessage(purpose, rpId, challenge)),\n );\n } catch {\n /* v8 ignore next -- WebCrypto answers false for a bad signature; a throw is a broken runtime */\n good = false;\n }\n return good ? { ok: true } : no(\"the device key's signature doesn't verify\");\n}\n\n/** One signature over `challenge` by `key`, chosen from the machine's own record of\n * the key, never from the signature: a device key's signature for `purpose`\n * (CONTROL.md \u00A73.2), or a passkey assertion made by a verified person. */\nexport async function verifyAssertion(\n challenge: Uint8Array,\n a: Assertion | DeviceSignature | null | undefined,\n key: TrustedKey,\n rpId: string,\n purpose: DevicePurpose,\n): Promise<Check> {\n if (!a || typeof a !== \"object\") return no(\"no passkey signature\");\n if (key.type !== undefined) {\n if (key.type !== \"device\") return no(\"a trusted key is unusable\");\n return verifyDeviceSignature(challenge, a, key, rpId, purpose);\n }\n return verifyPasskeyAssertion(challenge, a as Assertion, key, rpId);\n}\n\nasync function verifyPasskeyAssertion(\n challenge: Uint8Array,\n a: Assertion,\n key: TrustedKey,\n rpId: string,\n): Promise<Check> {\n if (a.cred !== key.id) return no(\"signed with a key this machine doesn't trust\");\n const cdBytes = fromB64url(a.cd);\n const ad = fromB64url(a.ad);\n const sig = fromB64url(a.sig);\n if (!cdBytes || !ad || !sig?.length) return no(\"the passkey signature is malformed\");\n let cd: { type?: unknown; challenge?: unknown; origin?: unknown; crossOrigin?: unknown };\n try {\n cd = JSON.parse(dec.decode(cdBytes));\n } catch {\n return no(\"the passkey signature is malformed\");\n }\n if (!cd || typeof cd !== \"object\") return no(\"the passkey signature is malformed\");\n if (cd.type !== \"webauthn.get\") return no(\"the passkey answered a different question\");\n if (cd.challenge !== b64url(challenge)) return no(\"the passkey signed something else\");\n if (!originAllowed(cd.origin, rpId)) return no(`the passkey was used on another site, not ${rpId}`);\n if (cd.crossOrigin === true) return no(\"the passkey was used inside another site's frame\");\n if (ad.length < 37) return no(\"the passkey signature is malformed\");\n if (!sameBytes(ad.slice(0, 32), await sha256(rpId))) return no(`the passkey belongs to another site, not ${rpId}`);\n const flags = ad[32] as number;\n if (!(flags & UP) || !(flags & UV)) return no(\"the passkey signed without verifying the person\");\n const pub = await importKey(key);\n if (!pub) return no(\"a trusted key is unusable\");\n const signed = concat(ad, await sha256(cdBytes));\n let good = false;\n try {\n if (key.alg === ES256) {\n const raw = derToRaw(sig);\n good = !!raw && (await crypto.subtle.verify({ name: \"ECDSA\", hash: \"SHA-256\" }, pub, u8(raw), u8(signed)));\n } else {\n good = await crypto.subtle.verify({ name: \"RSASSA-PKCS1-v1_5\" }, pub, u8(sig), u8(signed));\n }\n } catch {\n good = false;\n }\n return good ? { ok: true } : no(\"the passkey signature doesn't verify\");\n}\n\n/** What a passkey signs to enroll on a machine: bound to that machine and that pairing code. */\nexport function pairChallenge(machine: string, code: string): Promise<Uint8Array> {\n return sha256(`sessionpipe.pair:${machine}:${code}`);\n}\n\n/** The enrollment proof: the new key's assertion over pairChallenge(machine, code).\n * Only after this verifies does the key go into the machine's trust store. */\nexport async function verifyEnrollment(o: {\n machine: string;\n code: string;\n key: TrustedKey;\n proof: Assertion | DeviceSignature;\n rpId: string;\n}): Promise<Check> {\n if (!(await keyUsable(o.key))) return no(\"the key is in a shape this machine can't use\");\n return verifyAssertion(await pairChallenge(o.machine, o.code), o.proof, o.key, o.rpId, \"pair\");\n}\n\n// --- the command ------------------------------------------------------------------\n\ntype Parsed = { ok: true; cmd: ControlCommand } | { ok: false; code: RefusalCode; why: string };\nconst bad = (code: RefusalCode, why: string): { ok: false; code: RefusalCode; why: string } => ({\n ok: false,\n code,\n why,\n});\nconst isNum = (n: unknown): n is number => typeof n === \"number\" && Number.isFinite(n);\n\n/** The session id rule per harness (ADAPTERS.md): Claude Code ids are UUIDs. */\nexport function sessionIdOk(harness: string, id: string): boolean {\n if (!SESSION_ID_RE.test(id)) return false;\n if (harness === \"claude-code\") return UUID_RE.test(id);\n return true;\n}\n\n/** Split `<harness>:<id>`; null when either half breaks its rule. */\nexport function parseSessionRef(ref: unknown): { harness: string; id: string } | null {\n if (typeof ref !== \"string\") return null;\n const i = ref.indexOf(\":\");\n if (i <= 0) return null;\n const harness = ref.slice(0, i);\n const id = ref.slice(i + 1);\n return HARNESS_RE.test(harness) && sessionIdOk(harness, id) ? { harness, id } : null;\n}\n\n/** Read the command's fields from the exact string. Structure only; no clock, no keys. */\nexport function parseCommand(str: unknown): Parsed {\n if (typeof str !== \"string\" || !str || str.length > CMD_MAX)\n return bad(\"malformed\", \"the command is missing or too long\");\n let c: Record<string, unknown>;\n try {\n c = JSON.parse(str);\n } catch {\n return bad(\"malformed\", \"the command is not JSON\");\n }\n if (!c || typeof c !== \"object\" || Array.isArray(c)) return bad(\"malformed\", \"the command is not an object\");\n if (c.v !== 1 || c.t !== COMMAND_TYPE) return bad(\"not_a_command\", \"not a sessionpipe control command\");\n if (typeof c.machine !== \"string\" || !MACHINE_RE.test(c.machine)) return bad(\"malformed\", \"which machine?\");\n if (!CONTROL_KINDS.includes(c.kind as ControlKind))\n return bad(\"unknown_kind\", \"the command asks for something unknown\");\n const ref = parseSessionRef(c.session);\n if (!ref) return bad(\"bad_session\", \"which session?\");\n if (typeof c.nonce !== \"string\" || !NONCE_RE.test(c.nonce))\n return bad(\"bad_nonce\", \"the nonce is missing or malformed\");\n if (!isNum(c.iat) || !Number.isInteger(c.iat) || c.iat <= 0)\n return bad(\"malformed\", \"the command has no signing time\");\n const has = (k: string) => c[k] !== undefined;\n const text = () => typeof c.text === \"string\" && c.text.trim().length > 0 && c.text.length <= TEXT_MAX;\n switch (c.kind as ControlKind) {\n case \"prompt\":\n if (!text()) return bad(\"bad_fields\", `a prompt needs text, at most ${TEXT_MAX} characters`);\n if (has(\"for\") || has(\"decision\") || has(\"cwd\")) return bad(\"bad_fields\", \"a prompt carries text and files\");\n if (has(\"files\")) {\n const why = filesProblem(c.files);\n if (why) return bad(\"bad_fields\", why);\n }\n break;\n case \"start\":\n if (!text()) return bad(\"bad_fields\", `a new session needs text, at most ${TEXT_MAX} characters`);\n if (typeof c.cwd !== \"string\" || !(isAbsolute(c.cwd) || HOME_RELATIVE_RE.test(c.cwd)) || c.cwd.length > 1024)\n return bad(\"bad_fields\", \"a new session needs an absolute folder, or one under ~\");\n if (has(\"for\") || has(\"decision\"))\n return bad(\"bad_fields\", \"a start carries text, cwd, files, a permission and a model\");\n if (has(\"files\")) {\n const why = filesProblem(c.files);\n if (why) return bad(\"bad_fields\", why);\n }\n if (has(\"permission\") && levelRank(c.permission) < 0)\n return bad(\"bad_fields\", `a permission is one of ${START_PERMISSIONS.join(\", \")}`);\n if (has(\"model\") && (typeof c.model !== \"string\" || c.model.length > MODEL_MAX || !MODEL_RE.test(c.model)))\n return bad(\"bad_fields\", `a model is 1 to ${MODEL_MAX} letters, digits and ._:/@[]-, not starting with a dash`);\n break;\n case \"permission.answer\":\n if (typeof c.for !== \"string\" || !c.for || c.for.length > 128) return bad(\"bad_fields\", \"which attention?\");\n if (c.decision !== \"allow\" && c.decision !== \"deny\") return bad(\"bad_fields\", \"allow or deny?\");\n if (has(\"text\") || has(\"cwd\")) return bad(\"bad_fields\", \"a permission answer carries for, decision and note\");\n break;\n case \"cancel\":\n if (has(\"text\") || has(\"for\") || has(\"decision\") || has(\"cwd\"))\n return bad(\"bad_fields\", \"a cancel carries nothing\");\n break;\n case \"key.add\": {\n // A device the machine already trusts vouches for a new one (CONTROL.md \u00A72): the\n // new key's id, algorithm and public key (and, for a device key, its type, store\n // and uv) are inside the signed bytes.\n if (c.session !== KEY_STORE_SESSION) return bad(\"bad_session\", `a key.add names ${KEY_STORE_SESSION}`);\n const k = c.key as Record<string, unknown> | undefined;\n const b64 = (v: unknown, min: number, max: number) =>\n typeof v === \"string\" && v.length >= min && v.length <= max && /^[A-Za-z0-9_-]+$/.test(v);\n if (\n !k ||\n typeof k !== \"object\" ||\n Array.isArray(k) ||\n !b64(k.id, 16, 1400) ||\n (k.alg !== -7 && k.alg !== -257) ||\n !b64(k.spki, 40, 2000)\n )\n return bad(\"bad_fields\", \"a key.add carries the key: id, alg (-7 or -257) and spki\");\n if (k.type !== undefined) {\n if (k.type !== \"device\") return bad(\"bad_fields\", \"a key's type is device, or absent for a passkey\");\n if (k.alg !== ES256 || k.store !== \"hardware\" || k.uv !== \"each-use\" || !SHA256_RE.test(k.id as string))\n return bad(\"bad_fields\", \"a device key is ES256, store hardware, uv each-use, named by its SPKI's hash\");\n if (Object.keys(k).some((f) => !DEVICE_FIELDS.has(f)))\n return bad(\"bad_fields\", \"a device key carries id, alg, spki, type, store and uv\");\n } else if (Object.keys(k).some((f) => !PASSKEY_FIELDS.has(f)))\n return bad(\"bad_fields\", \"a passkey carries id, alg and spki: store and uv belong to a device key\");\n if (has(\"name\") && (typeof c.name !== \"string\" || !c.name.trim() || c.name.length > 80))\n return bad(\"bad_fields\", \"a device name is text, at most 80 characters\");\n if (has(\"text\") || has(\"for\") || has(\"decision\") || has(\"cwd\"))\n return bad(\"bad_fields\", \"a key.add carries the key and a name\");\n break;\n }\n }\n if (c.kind !== \"key.add\" && (has(\"key\") || has(\"name\"))) return bad(\"bad_fields\", \"only a key.add carries a key\");\n if (c.kind !== \"start\" && c.kind !== \"prompt\" && has(\"files\"))\n return bad(\"bad_fields\", \"only a start or a prompt carries files\");\n if (c.kind !== \"start\" && (has(\"permission\") || has(\"model\")))\n return bad(\"bad_fields\", \"only a start carries a model or a permission\");\n const allowed = FIELDS[c.kind as ControlKind];\n for (const k of Object.keys(c))\n if (!COMMON_FIELDS.includes(k) && !allowed.includes(k))\n return bad(\"bad_fields\", `the command carries a field its kind doesn't have: ${k.slice(0, 40)}`);\n if (has(\"note\") && (typeof c.note !== \"string\" || c.note.length > NOTE_MAX))\n return bad(\"bad_fields\", `a note is text, at most ${NOTE_MAX} characters`);\n return { ok: true, cmd: c as unknown as ControlCommand };\n}\n\n/** Why a start's or a prompt's `files` breaks the rules (CONTROL.md \u00A73.1), or null when it doesn't. */\nexport function filesProblem(files: unknown): string | null {\n if (!Array.isArray(files) || files.length < 1 || files.length > FILES_MAX)\n return `files is a list of 1 to ${FILES_MAX} files`;\n const seen = new Set<string>();\n let total = 0;\n for (const f of files as unknown[]) {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return \"each file is an object\";\n const { name, type, size, sha256: hash } = f as Record<string, unknown>;\n const why = fileNameProblem(name);\n if (why) return why;\n const key = (name as string).normalize(\"NFC\").toLowerCase();\n if (seen.has(key)) return \"two files have the same name\";\n seen.add(key);\n if (typeof type !== \"string\" || type.length > FILE_TYPE_MAX || !MEDIA_TYPE_RE.test(type))\n return \"a file's type isn't a media type\";\n if (!isNum(size) || !Number.isInteger(size) || size < 1 || size > FILE_SIZE_MAX)\n return `a file is 1 byte to ${FILE_SIZE_MAX / 1_048_576} MiB`;\n total += size;\n if (typeof hash !== \"string\" || !SHA256_RE.test(hash)) return \"a file's sha256 isn't 43 base64url characters\";\n }\n if (total > FILES_TOTAL_MAX) return `the files add up to more than ${FILES_TOTAL_MAX / 1_048_576} MiB`;\n return null;\n}\n\n/** A file name the machine can write inside the session's folder and nowhere else. */\nfunction fileNameProblem(name: unknown): string | null {\n if (typeof name !== \"string\") return \"a file has no name\";\n const n = name.normalize(\"NFC\");\n if (n.length < 1 || n.length > FILE_NAME_MAX) return `a file name is 1 to ${FILE_NAME_MAX} characters`;\n if (BAD_NAME_CHAR_RE.test(n)) return \"a file name has a slash, a backslash or a control character\";\n if (n.startsWith(\".\")) return \"a file name can't start with a dot\";\n return null;\n}\n\n/** A `start`'s `cwd` under the machine's home: `~` alone, or `~/\u2026` (`~\\\u2026`). Never `~user`. */\nexport const HOME_RELATIVE_RE = /^~(?:[\\\\/]|$)/;\n\nfunction isAbsolute(p: string): boolean {\n return p.startsWith(\"/\") || /^[A-Za-z]:[\\\\/]/.test(p);\n}\n\n/** The grant's fields, read from its exact string. */\nexport function parseGrant(str: unknown, rpId: string): { ok: true; grant: Grant } | { ok: false; why: string } {\n if (typeof str !== \"string\" || !str || str.length > GRANT_STR_MAX)\n return { ok: false, why: \"the grant is missing or too long\" };\n let g: Record<string, unknown>;\n try {\n g = JSON.parse(str);\n } catch {\n return { ok: false, why: \"the grant is not JSON\" };\n }\n if (!g || typeof g !== \"object\" || Array.isArray(g)) return { ok: false, why: \"the grant is not an object\" };\n if (g.v !== 1 || g.t !== GRANT_TYPE) return { ok: false, why: \"not a sessionpipe grant\" };\n if (g.rp !== rpId) return { ok: false, why: `the grant is for another site, not ${rpId}` };\n if (typeof g.pub !== \"string\" || !fromB64url(g.pub)?.length || !isNum(g.iat) || !isNum(g.exp))\n return { ok: false, why: \"the grant is missing parts\" };\n if (g.exp <= g.iat) return { ok: false, why: \"the grant ends before it starts\" };\n if (g.exp - g.iat > GRANT_MAX_MS + SKEW_MS) return { ok: false, why: \"the grant lasts longer than a day\" };\n return { ok: true, grant: g as unknown as Grant };\n}\n\nasync function verifyCsig(cmd: string, csig: unknown, pub: string): Promise<boolean> {\n const sig = fromB64url(csig);\n const spki = fromB64url(pub);\n if (sig?.length !== 64 || !spki) return false;\n try {\n const key = await crypto.subtle.importKey(\"spki\", u8(spki), { name: \"ECDSA\", namedCurve: \"P-256\" }, false, [\n \"verify\",\n ]);\n return await crypto.subtle.verify({ name: \"ECDSA\", hash: \"SHA-256\" }, key, u8(sig), u8(enc.encode(cmd)));\n } catch {\n return false;\n }\n}\n\n/**\n * Is this a command this machine may run? The checks, in the spec's order\n * (CONTROL.md \u00A75): the exact bytes parse; the command is for this machine; it is\n * signed \u2014 a grant from a trusted key plus the day key's signature, or a confirm\n * over sha256(cmd); it was signed within the age window; its nonce is new.\n * The folder and open-attention checks that follow are the daemon's (they read\n * the machine, not the command).\n */\nexport async function verifyCommand(s: SignedCommand | null | undefined, o: VerifyOptions): Promise<Verdict> {\n if (!s || typeof s !== \"object\") return bad(\"malformed\", \"nothing to verify\");\n const p = parseCommand(s.cmd);\n if (!p.ok) return p;\n const c = p.cmd;\n if (c.machine !== o.machine) return bad(\"wrong_machine\", \"the command is for another machine\");\n\n let via: \"grant\" | \"confirm\" | null = null;\n let keyId = \"\";\n let refusal: { code: RefusalCode; why: string } | null = null;\n if (s.grant && s.csig) {\n const r = await checkGrantPath(s, c, o);\n if (r.ok) {\n via = \"grant\";\n keyId = r.key;\n } else refusal = r;\n }\n if (!via && s.confirm) {\n const key = o.trusted.find((k) => k && k.id === s.confirm?.cred);\n if (!key) refusal = { code: \"untrusted_key\", why: \"confirmed with a key this machine doesn't trust\" };\n else {\n const a = await verifyAssertion(await sha256(s.cmd), s.confirm, key, o.rpId, \"confirm\");\n if (a.ok) {\n via = \"confirm\";\n keyId = key.id;\n } else refusal = { code: \"bad_assertion\", why: a.why };\n }\n }\n if (!via) {\n if (refusal) return bad(refusal.code, refusal.why);\n return bad(\"unsigned\", \"the command isn't signed\");\n }\n // The key a key.add brings must be one this machine could ever verify with: an\n // SPKI it can import for that alg, and every rule of a device key (CONTROL.md \u00A72).\n if (c.kind === \"key.add\" && !(await keyUsable(c.key as TrustedKey)))\n return bad(\"bad_fields\", \"the key a key.add brings is in a shape this machine can't use\");\n if (c.iat > o.now + SKEW_MS) return bad(\"future\", \"the command is dated in the future (is this clock right?)\");\n if (o.now - c.iat > (o.maxAgeMs ?? MACHINE_MAX_AGE_MS)) return bad(\"too_old\", \"the command was signed too long ago\");\n if (o.seenNonce) {\n let seen: boolean;\n try {\n seen = await o.seenNonce(c.nonce);\n } catch {\n return bad(\"nonce_store\", \"the nonce couldn't be recorded, so the command didn't run\");\n }\n if (seen) return bad(\"replay\", \"this command was already used once\");\n }\n return { ok: true, cmd: c, via, key: keyId };\n}\n\nasync function checkGrantPath(\n s: SignedCommand,\n c: ControlCommand,\n o: VerifyOptions,\n): Promise<{ ok: true; key: string } | { ok: false; code: RefusalCode; why: string }> {\n const bundle = s.grant as GrantBundle | DeviceGrant;\n const g = parseGrant(bundle.str, o.rpId);\n if (!g.ok) return bad(\"bad_grant\", g.why);\n const key = o.trusted.find((k) => k && k.id === bundle.cred);\n if (!key) return bad(\"untrusted_key\", \"the grant was signed with a key this machine doesn't trust\");\n const a = await verifyAssertion(await sha256(bundle.str), bundle, key, o.rpId, \"grant\");\n if (!a.ok) return bad(\"bad_assertion\", a.why);\n if (!(await verifyCsig(s.cmd, s.csig, g.grant.pub)))\n return bad(\"bad_csig\", \"the command's signature doesn't verify \u2014 it was changed after it was signed\");\n if (c.iat < g.grant.iat - SKEW_MS || c.iat > g.grant.exp)\n return bad(\"outside_grant\", \"the command was signed outside its grant's window\");\n return { ok: true, key: key.id };\n}\n"],
|
|
5
|
+
"mappings": ";;;AAaO,IAAM,eAAe,KAAK;AAE1B,IAAM,UAAU,IAAI;AAGpB,IAAM,qBAAqB,KAAK;AAGhC,IAAM,sBAAsB,KAAK;AAEjC,IAAM,eAAe,KAAK;AAE1B,IAAM,UAAU;AAEhB,IAAM,WAAW;AAEjB,IAAM,WAAW;AAEjB,IAAM,gBAAgB;AAEtB,IAAM,YAAY;AAElB,IAAM,gBAAgB;AAEtB,IAAM,kBAAkB;AAExB,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAEtB,IAAM,YAAY;AAElB,IAAM,WAAW;AAGjB,IAAM,oBAAoB,CAAC,QAAQ,QAAQ,SAAS,MAAM;AAQ1D,IAAM,YAAY,CAAC,MAAuB,kBAAkB,QAAQ,CAAoB;AAGxF,IAAM,WAAW,CAAC,GAAoB,MAC3C,UAAU,CAAC,KAAK,UAAU,CAAC,IAAI,IAAI;AAE9B,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,gBAAgB,CAAC,UAAU,qBAAqB,UAAU,SAAS,SAAS;AAElF,IAAM,oBAAoB;AAI1B,IAAM,QAAQ;AACd,IAAM,QAAQ;AAId,IAAM,YAAY,CAAC,WAAW,QAAQ;AAkI7C,IAAM,WAAW;AACjB,IAAM,aAAa;AACnB,IAAM,aAAa;AACnB,IAAM,gBAAgB;AACtB,IAAM,UAAU;AAChB,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,gBAAgB;AAEtB,IAAM,mBAAmB;AACzB,IAAM,KAAK;AACX,IAAM,KAAK;AAKX,IAAM,gBAAmC,CAAC,KAAK,KAAK,WAAW,QAAQ,WAAW,SAAS,OAAO,MAAM;AACxG,IAAM,SAAiD;AAAA,EACrD,QAAQ,CAAC,QAAQ,OAAO;AAAA,EACxB,qBAAqB,CAAC,OAAO,UAAU;AAAA,EACvC,QAAQ,CAAC;AAAA,EACT,OAAO,CAAC,QAAQ,OAAO,SAAS,cAAc,OAAO;AAAA,EACrD,WAAW,CAAC,OAAO,MAAM;AAC3B;AAIA,IAAM,MAAM,IAAI,YAAY;AAC5B,IAAM,MAAM,IAAI,YAAY;AAErB,SAAS,OAAO,OAAyC;AAC9D,QAAM,IAAI,iBAAiB,aAAa,QAAQ,IAAI,WAAW,KAAK;AACpE,MAAI,IAAI;AACR,aAAW,KAAK,EAAG,MAAK,OAAO,aAAa,CAAC;AAC7C,SAAO,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC1E;AAGO,SAAS,WAAW,GAA+B;AACxD,MAAI,OAAO,MAAM,YAAY,CAAC,UAAU,KAAK,CAAC,KAAK,EAAE,SAAS,MAAM,EAAG,QAAO;AAC9E,MAAI;AACF,UAAM,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,IAAI,MAAM,OAAO,EAAE,SAAS,KAAK,CAAC,CAAC;AAC1F,UAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AACrC,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,KAAI,CAAC,IAAI,IAAI,WAAW,CAAC;AAC9D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,OAAO,MAAgD;AAC3E,QAAM,QAAQ,OAAO,SAAS,WAAW,IAAI,OAAO,IAAI,IAAI;AAC5D,SAAO,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,KAAgC,CAAC;AAC/F;AAEA,SAAS,OAAO,GAAe,GAA2B;AACxD,QAAM,MAAM,IAAI,WAAW,EAAE,SAAS,EAAE,MAAM;AAC9C,MAAI,IAAI,GAAG,CAAC;AACZ,MAAI,IAAI,GAAG,EAAE,MAAM;AACnB,SAAO;AACT;AAGO,SAAS,UAAU,GAAe,GAAwB;AAC/D,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,MAAM,EAAE,CAAC,IAAgB,EAAE,CAAC;AAC/D,SAAO,MAAM;AACf;AAKO,SAAS,SAAS,KAAoC;AAC3D,MAAI,IAAI,CAAC,MAAM,MAAQ,IAAI,SAAS,KAAK,IAAI,CAAC,MAAM,IAAI,SAAS,EAAG,QAAO;AAC3E,MAAI,IAAI;AACR,QAAM,OAAO,MAAyB;AACpC,QAAI,IAAI,CAAC,MAAM,EAAM,QAAO;AAC5B,UAAM,MAAM,IAAI,IAAI,CAAC;AACrB,QAAI,MAAM,KAAK,MAAM,GAAI,QAAO;AAChC,QAAI,IAAI,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,GAAG;AACpC,QAAI,EAAE,WAAW,IAAK,QAAO;AAE7B,QAAK,EAAE,CAAC,IAAe,IAAM,QAAO;AACpC,QAAI,MAAM,KAAK,EAAE,CAAC,MAAM,KAAK,EAAG,EAAE,CAAC,IAAe,KAAO,QAAO;AAChE,SAAK,IAAI;AACT,WAAO,EAAE,SAAS,MAAM,EAAE,CAAC,MAAM,EAAG,KAAI,EAAE,MAAM,CAAC;AACjD,QAAI,EAAE,SAAS,GAAI,QAAO;AAC1B,UAAM,MAAM,IAAI,WAAW,EAAE;AAC7B,QAAI,IAAI,GAAG,KAAK,EAAE,MAAM;AACxB,WAAO;AAAA,EACT;AACA,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,KAAK;AACf,SAAO,KAAK,KAAK,MAAM,IAAI,SAAS,OAAO,GAAG,CAAC,IAAI;AACrD;AAEA,IAAM,KAAK,CAAC,MAAkB;AAI9B,eAAe,UAAU,KAA4C;AACnE,QAAM,OAAO,WAAW,IAAI,IAAI;AAChC,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,MAAI;AACF,QAAI,IAAI,QAAQ;AACd,aAAO,MAAM,OAAO,OAAO,UAAU,QAAQ,GAAG,IAAI,GAAG,EAAE,MAAM,SAAS,YAAY,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AAClH,QAAI,IAAI,QAAQ,OAAO;AACrB,YAAM,IAAI,MAAM,OAAO,OAAO,UAAU,QAAQ,GAAG,IAAI,GAAG,EAAE,MAAM,qBAAqB,MAAM,UAAU,GAAG,OAAO;AAAA,QAC/G;AAAA,MACF,CAAC;AACD,YAAM,OAAQ,EAAE,UAAyC;AACzD,aAAO,QAAQ,OAAO,IAAI;AAAA,IAC5B;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,SAAO;AACT;AAEA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,MAAM,OAAO,MAAM,CAAC;AACpD,IAAM,gBAAgB,oBAAI,IAAI,CAAC,MAAM,OAAO,QAAQ,QAAQ,SAAS,IAAI,CAAC;AAEnE,IAAM,oBAAoB,CAAC,QAAQ,OAAO,KAAK;AAGtD,eAAsB,YAAY,MAA+B;AAC/D,SAAO,OAAO,MAAM,OAAO,WAAW,IAAI,KAAK,IAAI,WAAW,CAAC,CAAC;AAClE;AAKA,eAAe,kBAAkB,KAAmC;AAClE,MAAI,IAAI,SAAS,YAAY,IAAI,QAAQ,SAAS,IAAI,UAAU,cAAc,IAAI,OAAO,WAAY,QAAO;AAC5G,MAAI,OAAO,IAAI,OAAO,YAAY,CAAC,UAAU,KAAK,IAAI,EAAE,KAAK,IAAI,OAAQ,MAAM,YAAY,IAAI,IAAI,EAAI,QAAO;AAC9G,SAAO,CAAC,CAAE,MAAM,UAAU,GAAG;AAC/B;AAMA,eAAsB,UAAU,KAAmC;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,EAAG,QAAO;AAClE,QAAM,SAAS,OAAO,KAAK,GAAG,EAAE,OAAO,CAAC,MAAO,IAA2C,CAAC,MAAM,MAAS;AAC1G,MAAI,UAAU,OAAO,IAAI,SAAS,QAAW;AAC3C,QAAI,CAAC,OAAO,MAAM,CAAC,MAAM,cAAc,IAAI,CAAC,CAAC,EAAG,QAAO;AACvD,WAAO,kBAAkB,GAAG;AAAA,EAC9B;AACA,MAAI,CAAC,OAAO,MAAM,CAAC,MAAM,eAAe,IAAI,CAAC,CAAC,EAAG,QAAO;AACxD,SACE,OAAO,IAAI,OAAO,YAClB,2BAA2B,KAAK,IAAI,EAAE,MACrC,IAAI,QAAQ,SAAS,IAAI,QAAQ,UAClC,CAAC,CAAE,MAAM,UAAU,GAAG;AAE1B;AAMO,SAAS,cAAc,QAAiB,MAAuB;AACpE,MAAI,OAAO,WAAW,YAAY,CAAC,KAAM,QAAO;AAChD,MAAI;AACJ,MAAI;AACF,QAAI,IAAI,IAAI,MAAM;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,EAAE,WAAW,OAAQ,QAAO;AAChC,MAAI,EAAE,aAAa,QAAQ,CAAC,EAAE,SAAS,SAAS,IAAI,IAAI,EAAE,EAAG,QAAO;AACpE,MAAI,EAAE,aAAa,SAAU,QAAO;AACpC,SAAO,EAAE,aAAa,WAAW,SAAS;AAC5C;AAGA,IAAM,KAAK,CAAC,SAAwB,EAAE,IAAI,OAAO,IAAI;AAK9C,IAAM,iBAAiB;AAOvB,SAAS,cAAc,SAAwB,MAAc,WAAmC;AACrG,SAAO,IAAI,OAAO,GAAG,cAAc;AAAA,EAAK,OAAO;AAAA,EAAK,IAAI;AAAA,EAAK,OAAO,SAAS,CAAC,EAAE;AAClF;AAGA,eAAe,sBACb,WACA,GACA,KACA,MACA,SACgB;AAChB,QAAM,MAAM;AACZ,aAAW,KAAK,OAAO,KAAK,GAAG,GAAG;AAChC,QAAI,MAAM,SAAS,YAAY,QAAS;AACxC,QAAI,MAAM,UAAU,MAAM,MAAO,QAAO,GAAG,uCAAuC,CAAC,EAAE;AAAA,EACvF;AACA,MAAI,IAAI,SAAS,IAAI,GAAI,QAAO,GAAG,8CAA8C;AACjF,MAAI,CAAE,MAAM,kBAAkB,GAAG,EAAI,QAAO,GAAG,2BAA2B;AAC1E,QAAM,MAAM,WAAW,IAAI,GAAG;AAC9B,MAAI,KAAK,WAAW,GAAI,QAAO,GAAG,gDAA2C;AAC7E,QAAM,MAAM,MAAM,UAAU,GAAG;AAE/B,MAAI,CAAC,IAAK,QAAO,GAAG,2BAA2B;AAC/C,MAAI,OAAO;AACX,MAAI;AACF,WAAO,MAAM,OAAO,OAAO;AAAA,MACzB,EAAE,MAAM,SAAS,MAAM,UAAU;AAAA,MACjC;AAAA,MACA,GAAG,GAAG;AAAA,MACN,GAAG,cAAc,SAAS,MAAM,SAAS,CAAC;AAAA,IAC5C;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACA,SAAO,OAAO,EAAE,IAAI,KAAK,IAAI,GAAG,2CAA2C;AAC7E;AAKA,eAAsB,gBACpB,WACA,GACA,KACA,MACA,SACgB;AAChB,MAAI,CAAC,KAAK,OAAO,MAAM,SAAU,QAAO,GAAG,sBAAsB;AACjE,MAAI,IAAI,SAAS,QAAW;AAC1B,QAAI,IAAI,SAAS,SAAU,QAAO,GAAG,2BAA2B;AAChE,WAAO,sBAAsB,WAAW,GAAG,KAAK,MAAM,OAAO;AAAA,EAC/D;AACA,SAAO,uBAAuB,WAAW,GAAgB,KAAK,IAAI;AACpE;AAEA,eAAe,uBACb,WACA,GACA,KACA,MACgB;AAChB,MAAI,EAAE,SAAS,IAAI,GAAI,QAAO,GAAG,8CAA8C;AAC/E,QAAM,UAAU,WAAW,EAAE,EAAE;AAC/B,QAAM,KAAK,WAAW,EAAE,EAAE;AAC1B,QAAM,MAAM,WAAW,EAAE,GAAG;AAC5B,MAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,OAAQ,QAAO,GAAG,oCAAoC;AACnF,MAAI;AACJ,MAAI;AACF,SAAK,KAAK,MAAM,IAAI,OAAO,OAAO,CAAC;AAAA,EACrC,QAAQ;AACN,WAAO,GAAG,oCAAoC;AAAA,EAChD;AACA,MAAI,CAAC,MAAM,OAAO,OAAO,SAAU,QAAO,GAAG,oCAAoC;AACjF,MAAI,GAAG,SAAS,eAAgB,QAAO,GAAG,2CAA2C;AACrF,MAAI,GAAG,cAAc,OAAO,SAAS,EAAG,QAAO,GAAG,mCAAmC;AACrF,MAAI,CAAC,cAAc,GAAG,QAAQ,IAAI,EAAG,QAAO,GAAG,6CAA6C,IAAI,EAAE;AAClG,MAAI,GAAG,gBAAgB,KAAM,QAAO,GAAG,kDAAkD;AACzF,MAAI,GAAG,SAAS,GAAI,QAAO,GAAG,oCAAoC;AAClE,MAAI,CAAC,UAAU,GAAG,MAAM,GAAG,EAAE,GAAG,MAAM,OAAO,IAAI,CAAC,EAAG,QAAO,GAAG,4CAA4C,IAAI,EAAE;AACjH,QAAM,QAAQ,GAAG,EAAE;AACnB,MAAI,EAAE,QAAQ,OAAO,EAAE,QAAQ,IAAK,QAAO,GAAG,iDAAiD;AAC/F,QAAM,MAAM,MAAM,UAAU,GAAG;AAC/B,MAAI,CAAC,IAAK,QAAO,GAAG,2BAA2B;AAC/C,QAAM,SAAS,OAAO,IAAI,MAAM,OAAO,OAAO,CAAC;AAC/C,MAAI,OAAO;AACX,MAAI;AACF,QAAI,IAAI,QAAQ,OAAO;AACrB,YAAM,MAAM,SAAS,GAAG;AACxB,aAAO,CAAC,CAAC,OAAQ,MAAM,OAAO,OAAO,OAAO,EAAE,MAAM,SAAS,MAAM,UAAU,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,MAAM,CAAC;AAAA,IAC1G,OAAO;AACL,aAAO,MAAM,OAAO,OAAO,OAAO,EAAE,MAAM,oBAAoB,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,MAAM,CAAC;AAAA,IAC3F;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO,OAAO,EAAE,IAAI,KAAK,IAAI,GAAG,sCAAsC;AACxE;AAGO,SAAS,cAAc,SAAiB,MAAmC;AAChF,SAAO,OAAO,oBAAoB,OAAO,IAAI,IAAI,EAAE;AACrD;AAIA,eAAsB,iBAAiB,GAMpB;AACjB,MAAI,CAAE,MAAM,UAAU,EAAE,GAAG,EAAI,QAAO,GAAG,8CAA8C;AACvF,SAAO,gBAAgB,MAAM,cAAc,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM;AAC/F;AAKA,IAAM,MAAM,CAAC,MAAmB,SAAgE;AAAA,EAC9F,IAAI;AAAA,EACJ;AAAA,EACA;AACF;AACA,IAAM,QAAQ,CAAC,MAA4B,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC;AAG9E,SAAS,YAAY,SAAiB,IAAqB;AAChE,MAAI,CAAC,cAAc,KAAK,EAAE,EAAG,QAAO;AACpC,MAAI,YAAY,cAAe,QAAO,QAAQ,KAAK,EAAE;AACrD,SAAO;AACT;AAGO,SAAS,gBAAgB,KAAsD;AACpF,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAM,IAAI,IAAI,QAAQ,GAAG;AACzB,MAAI,KAAK,EAAG,QAAO;AACnB,QAAM,UAAU,IAAI,MAAM,GAAG,CAAC;AAC9B,QAAM,KAAK,IAAI,MAAM,IAAI,CAAC;AAC1B,SAAO,WAAW,KAAK,OAAO,KAAK,YAAY,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;AAClF;AAGO,SAAS,aAAa,KAAsB;AACjD,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,IAAI,SAAS;AAClD,WAAO,IAAI,aAAa,oCAAoC;AAC9D,MAAI;AACJ,MAAI;AACF,QAAI,KAAK,MAAM,GAAG;AAAA,EACpB,QAAQ;AACN,WAAO,IAAI,aAAa,yBAAyB;AAAA,EACnD;AACA,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO,IAAI,aAAa,8BAA8B;AAC3G,MAAI,EAAE,MAAM,KAAK,EAAE,MAAM,aAAc,QAAO,IAAI,iBAAiB,mCAAmC;AACtG,MAAI,OAAO,EAAE,YAAY,YAAY,CAAC,WAAW,KAAK,EAAE,OAAO,EAAG,QAAO,IAAI,aAAa,gBAAgB;AAC1G,MAAI,CAAC,cAAc,SAAS,EAAE,IAAmB;AAC/C,WAAO,IAAI,gBAAgB,wCAAwC;AACrE,QAAM,MAAM,gBAAgB,EAAE,OAAO;AACrC,MAAI,CAAC,IAAK,QAAO,IAAI,eAAe,gBAAgB;AACpD,MAAI,OAAO,EAAE,UAAU,YAAY,CAAC,SAAS,KAAK,EAAE,KAAK;AACvD,WAAO,IAAI,aAAa,mCAAmC;AAC7D,MAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,UAAU,EAAE,GAAG,KAAK,EAAE,OAAO;AACxD,WAAO,IAAI,aAAa,iCAAiC;AAC3D,QAAM,MAAM,CAAC,MAAc,EAAE,CAAC,MAAM;AACpC,QAAM,OAAO,MAAM,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,KAAK,EAAE,SAAS,KAAK,EAAE,KAAK,UAAU;AAC9F,UAAQ,EAAE,MAAqB;AAAA,IAC7B,KAAK;AACH,UAAI,CAAC,KAAK,EAAG,QAAO,IAAI,cAAc,gCAAgC,QAAQ,aAAa;AAC3F,UAAI,IAAI,KAAK,KAAK,IAAI,UAAU,KAAK,IAAI,KAAK,EAAG,QAAO,IAAI,cAAc,iCAAiC;AAC3G,UAAI,IAAI,OAAO,GAAG;AAChB,cAAM,MAAM,aAAa,EAAE,KAAK;AAChC,YAAI,IAAK,QAAO,IAAI,cAAc,GAAG;AAAA,MACvC;AACA;AAAA,IACF,KAAK;AACH,UAAI,CAAC,KAAK,EAAG,QAAO,IAAI,cAAc,qCAAqC,QAAQ,aAAa;AAChG,UAAI,OAAO,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,GAAG,KAAK,iBAAiB,KAAK,EAAE,GAAG,MAAM,EAAE,IAAI,SAAS;AACtG,eAAO,IAAI,cAAc,wDAAwD;AACnF,UAAI,IAAI,KAAK,KAAK,IAAI,UAAU;AAC9B,eAAO,IAAI,cAAc,4DAA4D;AACvF,UAAI,IAAI,OAAO,GAAG;AAChB,cAAM,MAAM,aAAa,EAAE,KAAK;AAChC,YAAI,IAAK,QAAO,IAAI,cAAc,GAAG;AAAA,MACvC;AACA,UAAI,IAAI,YAAY,KAAK,UAAU,EAAE,UAAU,IAAI;AACjD,eAAO,IAAI,cAAc,0BAA0B,kBAAkB,KAAK,IAAI,CAAC,EAAE;AACnF,UAAI,IAAI,OAAO,MAAM,OAAO,EAAE,UAAU,YAAY,EAAE,MAAM,SAAS,aAAa,CAAC,SAAS,KAAK,EAAE,KAAK;AACtG,eAAO,IAAI,cAAc,mBAAmB,SAAS,yDAAyD;AAChH;AAAA,IACF,KAAK;AACH,UAAI,OAAO,EAAE,QAAQ,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,SAAS,IAAK,QAAO,IAAI,cAAc,kBAAkB;AAC1G,UAAI,EAAE,aAAa,WAAW,EAAE,aAAa,OAAQ,QAAO,IAAI,cAAc,gBAAgB;AAC9F,UAAI,IAAI,MAAM,KAAK,IAAI,KAAK,EAAG,QAAO,IAAI,cAAc,oDAAoD;AAC5G;AAAA,IACF,KAAK;AACH,UAAI,IAAI,MAAM,KAAK,IAAI,KAAK,KAAK,IAAI,UAAU,KAAK,IAAI,KAAK;AAC3D,eAAO,IAAI,cAAc,0BAA0B;AACrD;AAAA,IACF,KAAK,WAAW;AAId,UAAI,EAAE,YAAY,kBAAmB,QAAO,IAAI,eAAe,mBAAmB,iBAAiB,EAAE;AACrG,YAAM,IAAI,EAAE;AACZ,YAAM,MAAM,CAAC,GAAY,KAAa,QACpC,OAAO,MAAM,YAAY,EAAE,UAAU,OAAO,EAAE,UAAU,OAAO,mBAAmB,KAAK,CAAC;AAC1F,UACE,CAAC,KACD,OAAO,MAAM,YACb,MAAM,QAAQ,CAAC,KACf,CAAC,IAAI,EAAE,IAAI,IAAI,IAAI,KAClB,EAAE,QAAQ,MAAM,EAAE,QAAQ,QAC3B,CAAC,IAAI,EAAE,MAAM,IAAI,GAAI;AAErB,eAAO,IAAI,cAAc,0DAA0D;AACrF,UAAI,EAAE,SAAS,QAAW;AACxB,YAAI,EAAE,SAAS,SAAU,QAAO,IAAI,cAAc,iDAAiD;AACnG,YAAI,EAAE,QAAQ,SAAS,EAAE,UAAU,cAAc,EAAE,OAAO,cAAc,CAAC,UAAU,KAAK,EAAE,EAAY;AACpG,iBAAO,IAAI,cAAc,8EAA8E;AACzG,YAAI,OAAO,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC;AAClD,iBAAO,IAAI,cAAc,wDAAwD;AAAA,MACrF,WAAW,OAAO,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,CAAC;AAC1D,eAAO,IAAI,cAAc,yEAAyE;AACpG,UAAI,IAAI,MAAM,MAAM,OAAO,EAAE,SAAS,YAAY,CAAC,EAAE,KAAK,KAAK,KAAK,EAAE,KAAK,SAAS;AAClF,eAAO,IAAI,cAAc,8CAA8C;AACzE,UAAI,IAAI,MAAM,KAAK,IAAI,KAAK,KAAK,IAAI,UAAU,KAAK,IAAI,KAAK;AAC3D,eAAO,IAAI,cAAc,sCAAsC;AACjE;AAAA,IACF;AAAA,EACF;AACA,MAAI,EAAE,SAAS,cAAc,IAAI,KAAK,KAAK,IAAI,MAAM,GAAI,QAAO,IAAI,cAAc,8BAA8B;AAChH,MAAI,EAAE,SAAS,WAAW,EAAE,SAAS,YAAY,IAAI,OAAO;AAC1D,WAAO,IAAI,cAAc,wCAAwC;AACnE,MAAI,EAAE,SAAS,YAAY,IAAI,YAAY,KAAK,IAAI,OAAO;AACzD,WAAO,IAAI,cAAc,8CAA8C;AACzE,QAAM,UAAU,OAAO,EAAE,IAAmB;AAC5C,aAAW,KAAK,OAAO,KAAK,CAAC;AAC3B,QAAI,CAAC,cAAc,SAAS,CAAC,KAAK,CAAC,QAAQ,SAAS,CAAC;AACnD,aAAO,IAAI,cAAc,sDAAsD,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE;AACnG,MAAI,IAAI,MAAM,MAAM,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,SAAS;AAChE,WAAO,IAAI,cAAc,2BAA2B,QAAQ,aAAa;AAC3E,SAAO,EAAE,IAAI,MAAM,KAAK,EAA+B;AACzD;AAGO,SAAS,aAAa,OAA+B;AAC1D,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,KAAK,MAAM,SAAS;AAC9D,WAAO,2BAA2B,SAAS;AAC7C,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,QAAQ;AACZ,aAAW,KAAK,OAAoB;AAClC,QAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC5D,UAAM,EAAE,MAAM,MAAM,MAAM,QAAQ,KAAK,IAAI;AAC3C,UAAM,MAAM,gBAAgB,IAAI;AAChC,QAAI,IAAK,QAAO;AAChB,UAAM,MAAO,KAAgB,UAAU,KAAK,EAAE,YAAY;AAC1D,QAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAC1B,SAAK,IAAI,GAAG;AACZ,QAAI,OAAO,SAAS,YAAY,KAAK,SAAS,iBAAiB,CAAC,cAAc,KAAK,IAAI;AACrF,aAAO;AACT,QAAI,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO;AAChE,aAAO,uBAAuB,gBAAgB,OAAS;AACzD,aAAS;AACT,QAAI,OAAO,SAAS,YAAY,CAAC,UAAU,KAAK,IAAI,EAAG,QAAO;AAAA,EAChE;AACA,MAAI,QAAQ,gBAAiB,QAAO,iCAAiC,kBAAkB,OAAS;AAChG,SAAO;AACT;AAGA,SAAS,gBAAgB,MAA8B;AACrD,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAM,IAAI,KAAK,UAAU,KAAK;AAC9B,MAAI,EAAE,SAAS,KAAK,EAAE,SAAS,cAAe,QAAO,uBAAuB,aAAa;AACzF,MAAI,iBAAiB,KAAK,CAAC,EAAG,QAAO;AACrC,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,SAAO;AACT;AAGO,IAAM,mBAAmB;AAEhC,SAAS,WAAW,GAAoB;AACtC,SAAO,EAAE,WAAW,GAAG,KAAK,kBAAkB,KAAK,CAAC;AACtD;AAGO,SAAS,WAAW,KAAc,MAAuE;AAC9G,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,IAAI,SAAS;AAClD,WAAO,EAAE,IAAI,OAAO,KAAK,mCAAmC;AAC9D,MAAI;AACJ,MAAI;AACF,QAAI,KAAK,MAAM,GAAG;AAAA,EACpB,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,KAAK,wBAAwB;AAAA,EACnD;AACA,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,IAAI,OAAO,KAAK,6BAA6B;AAC3G,MAAI,EAAE,MAAM,KAAK,EAAE,MAAM,WAAY,QAAO,EAAE,IAAI,OAAO,KAAK,0BAA0B;AACxF,MAAI,EAAE,OAAO,KAAM,QAAO,EAAE,IAAI,OAAO,KAAK,sCAAsC,IAAI,GAAG;AACzF,MAAI,OAAO,EAAE,QAAQ,YAAY,CAAC,WAAW,EAAE,GAAG,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG;AAC1F,WAAO,EAAE,IAAI,OAAO,KAAK,6BAA6B;AACxD,MAAI,EAAE,OAAO,EAAE,IAAK,QAAO,EAAE,IAAI,OAAO,KAAK,kCAAkC;AAC/E,MAAI,EAAE,MAAM,EAAE,MAAM,eAAe,QAAS,QAAO,EAAE,IAAI,OAAO,KAAK,oCAAoC;AACzG,SAAO,EAAE,IAAI,MAAM,OAAO,EAAsB;AAClD;AAEA,eAAe,WAAW,KAAa,MAAe,KAA+B;AACnF,QAAM,MAAM,WAAW,IAAI;AAC3B,QAAM,OAAO,WAAW,GAAG;AAC3B,MAAI,KAAK,WAAW,MAAM,CAAC,KAAM,QAAO;AACxC,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,OAAO,UAAU,QAAQ,GAAG,IAAI,GAAG,EAAE,MAAM,SAAS,YAAY,QAAQ,GAAG,OAAO;AAAA,MACzG;AAAA,IACF,CAAC;AACD,WAAO,MAAM,OAAO,OAAO,OAAO,EAAE,MAAM,SAAS,MAAM,UAAU,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,IAAI,OAAO,GAAG,CAAC,CAAC;AAAA,EACzG,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUA,eAAsB,cAAc,GAAqC,GAAoC;AAC3G,MAAI,CAAC,KAAK,OAAO,MAAM,SAAU,QAAO,IAAI,aAAa,mBAAmB;AAC5E,QAAM,IAAI,aAAa,EAAE,GAAG;AAC5B,MAAI,CAAC,EAAE,GAAI,QAAO;AAClB,QAAM,IAAI,EAAE;AACZ,MAAI,EAAE,YAAY,EAAE,QAAS,QAAO,IAAI,iBAAiB,oCAAoC;AAE7F,MAAI,MAAkC;AACtC,MAAI,QAAQ;AACZ,MAAI,UAAqD;AACzD,MAAI,EAAE,SAAS,EAAE,MAAM;AACrB,UAAM,IAAI,MAAM,eAAe,GAAG,GAAG,CAAC;AACtC,QAAI,EAAE,IAAI;AACR,YAAM;AACN,cAAQ,EAAE;AAAA,IACZ,MAAO,WAAU;AAAA,EACnB;AACA,MAAI,CAAC,OAAO,EAAE,SAAS;AACrB,UAAM,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,KAAK,EAAE,OAAO,EAAE,SAAS,IAAI;AAC/D,QAAI,CAAC,IAAK,WAAU,EAAE,MAAM,iBAAiB,KAAK,kDAAkD;AAAA,SAC/F;AACH,YAAM,IAAI,MAAM,gBAAgB,MAAM,OAAO,EAAE,GAAG,GAAG,EAAE,SAAS,KAAK,EAAE,MAAM,SAAS;AACtF,UAAI,EAAE,IAAI;AACR,cAAM;AACN,gBAAQ,IAAI;AAAA,MACd,MAAO,WAAU,EAAE,MAAM,iBAAiB,KAAK,EAAE,IAAI;AAAA,IACvD;AAAA,EACF;AACA,MAAI,CAAC,KAAK;AACR,QAAI,QAAS,QAAO,IAAI,QAAQ,MAAM,QAAQ,GAAG;AACjD,WAAO,IAAI,YAAY,0BAA0B;AAAA,EACnD;AAGA,MAAI,EAAE,SAAS,aAAa,CAAE,MAAM,UAAU,EAAE,GAAiB;AAC/D,WAAO,IAAI,cAAc,+DAA+D;AAC1F,MAAI,EAAE,MAAM,EAAE,MAAM,QAAS,QAAO,IAAI,UAAU,2DAA2D;AAC7G,MAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,oBAAqB,QAAO,IAAI,WAAW,qCAAqC;AACnH,MAAI,EAAE,WAAW;AACf,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,EAAE,UAAU,EAAE,KAAK;AAAA,IAClC,QAAQ;AACN,aAAO,IAAI,eAAe,2DAA2D;AAAA,IACvF;AACA,QAAI,KAAM,QAAO,IAAI,UAAU,oCAAoC;AAAA,EACrE;AACA,SAAO,EAAE,IAAI,MAAM,KAAK,GAAG,KAAK,KAAK,MAAM;AAC7C;AAEA,eAAe,eACb,GACA,GACA,GACoF;AACpF,QAAM,SAAS,EAAE;AACjB,QAAM,IAAI,WAAW,OAAO,KAAK,EAAE,IAAI;AACvC,MAAI,CAAC,EAAE,GAAI,QAAO,IAAI,aAAa,EAAE,GAAG;AACxC,QAAM,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,KAAK,EAAE,OAAO,OAAO,IAAI;AAC3D,MAAI,CAAC,IAAK,QAAO,IAAI,iBAAiB,4DAA4D;AAClG,QAAM,IAAI,MAAM,gBAAgB,MAAM,OAAO,OAAO,GAAG,GAAG,QAAQ,KAAK,EAAE,MAAM,OAAO;AACtF,MAAI,CAAC,EAAE,GAAI,QAAO,IAAI,iBAAiB,EAAE,GAAG;AAC5C,MAAI,CAAE,MAAM,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG;AAC/C,WAAO,IAAI,YAAY,kFAA6E;AACtG,MAAI,EAAE,MAAM,EAAE,MAAM,MAAM,WAAW,EAAE,MAAM,EAAE,MAAM;AACnD,WAAO,IAAI,iBAAiB,mDAAmD;AACjF,SAAO,EAAE,IAAI,MAAM,KAAK,IAAI,GAAG;AACjC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|