@mcp-index/sdk 0.18.1 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +178 -0
  3. package/dist/src/ai.d.ts +96 -0
  4. package/dist/src/ai.d.ts.map +1 -0
  5. package/dist/src/ai.js +151 -0
  6. package/dist/src/ai.js.map +1 -0
  7. package/dist/src/gate.d.ts +7 -0
  8. package/dist/src/gate.d.ts.map +1 -1
  9. package/dist/src/gate.js +36 -2
  10. package/dist/src/gate.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -1
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/lock/check.d.ts +143 -0
  16. package/dist/src/lock/check.d.ts.map +1 -0
  17. package/dist/src/lock/check.js +274 -0
  18. package/dist/src/lock/check.js.map +1 -0
  19. package/dist/src/lock/digest.d.ts +48 -0
  20. package/dist/src/lock/digest.d.ts.map +1 -0
  21. package/dist/src/lock/digest.js +234 -0
  22. package/dist/src/lock/digest.js.map +1 -0
  23. package/dist/src/lock/events.d.ts +82 -0
  24. package/dist/src/lock/events.d.ts.map +1 -0
  25. package/dist/src/lock/events.js +80 -0
  26. package/dist/src/lock/events.js.map +1 -0
  27. package/dist/src/lock/index.d.ts +20 -0
  28. package/dist/src/lock/index.d.ts.map +1 -0
  29. package/dist/src/lock/index.js +14 -0
  30. package/dist/src/lock/index.js.map +1 -0
  31. package/dist/src/lock/lock.d.ts +195 -0
  32. package/dist/src/lock/lock.d.ts.map +1 -0
  33. package/dist/src/lock/lock.js +1075 -0
  34. package/dist/src/lock/lock.js.map +1 -0
  35. package/dist/src/lock/pg.d.ts +150 -0
  36. package/dist/src/lock/pg.d.ts.map +1 -0
  37. package/dist/src/lock/pg.js +539 -0
  38. package/dist/src/lock/pg.js.map +1 -0
  39. package/dist/src/lock/rawList.d.ts +127 -0
  40. package/dist/src/lock/rawList.d.ts.map +1 -0
  41. package/dist/src/lock/rawList.js +561 -0
  42. package/dist/src/lock/rawList.js.map +1 -0
  43. package/dist/src/lock/signing.d.ts +35 -0
  44. package/dist/src/lock/signing.d.ts.map +1 -0
  45. package/dist/src/lock/signing.js +85 -0
  46. package/dist/src/lock/signing.js.map +1 -0
  47. package/dist/src/lock/store.d.ts +149 -0
  48. package/dist/src/lock/store.d.ts.map +1 -0
  49. package/dist/src/lock/store.js +459 -0
  50. package/dist/src/lock/store.js.map +1 -0
  51. package/dist/src/schemaDiff.d.ts +21 -2
  52. package/dist/src/schemaDiff.d.ts.map +1 -1
  53. package/dist/src/schemaDiff.js +79 -1
  54. package/dist/src/schemaDiff.js.map +1 -1
  55. package/package.json +31 -2
@@ -0,0 +1,1075 @@
1
+ /**
2
+ * Lock format draft 1 (spec/mcp-lock-draft-1.md in mcp-tool-pinning): server
3
+ * identity, the strict reader for `.mcp/lock` and `.mcp/contracts`, and a
4
+ * writer whose bytes match the Python generator (json.dumps indent=2 with
5
+ * ensure_ascii, trailing newline).
6
+ *
7
+ * The reader mirrors mcp_tool_pinning.lockref check for check and in the same
8
+ * order, so a lock with one fault gives the same reason code in both. Every
9
+ * file is read once; the manifest, signature and parse all use those bytes.
10
+ */
11
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync, realpathSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { canonicalize, compareCodePoints, hasLoneSurrogate, sha256Hex } from "./digest.js";
14
+ export const LOCK_VERSION = "1-draft";
15
+ export const POSTURES = ["monitor", "guard", "strict"];
16
+ export const REGISTRIES = ["npm", "pypi", "oci"];
17
+ export const MANIFEST = "MANIFEST.json";
18
+ export const SIGNATURE = "MANIFEST.json.sigstore";
19
+ export const ALIASES = "aliases.json";
20
+ export const SLUG_MAX = 48;
21
+ /** Largest manifest sequence; every reader parses it exactly. */
22
+ export const SEQUENCE_MAX = Number.MAX_SAFE_INTEGER;
23
+ export const SHARD_NAME = /^([a-z0-9]+(?:-[a-z0-9]+)*)--([0-9a-f]{16})\.json$/;
24
+ export const CONTRACT_NAME = /^sha256-([0-9a-f]{64})\.json$/;
25
+ export const DIGEST_FORM = /^sha256:([0-9a-f]{64})$/;
26
+ // ASCII digits only. lockref.py uses \d, which in a Python str pattern also
27
+ // matches non-ASCII digits, and its `$` also matches before a final newline.
28
+ const RFC3339 = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$/;
29
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
30
+ const SHARD_KEYS = new Set(["lockVersion", "identity", "attributes", "instructions", "tools"]);
31
+ const ATTRIBUTE_KEYS = new Set(["version", "audience", "command", "args", "env"]);
32
+ const ENTRY_KEYS = ["digest", "contract", "approval"];
33
+ /** Optional tool entry members (draft 1c). */
34
+ const ENTRY_OPTIONAL = ["metaDigest"];
35
+ const APPROVAL_KEYS = new Set(["subject", "at", "via", "ref"]);
36
+ export const VIA = ["pr", "runtime", "api", "migrated"];
37
+ export class LockError extends Error {
38
+ reason;
39
+ detail;
40
+ constructor(reason, detail) {
41
+ super(`${reason}: ${detail}`);
42
+ this.name = "LockError";
43
+ this.reason = reason;
44
+ this.detail = detail;
45
+ }
46
+ }
47
+ const unreadable = (detail) => new LockError("lock-unreadable", detail);
48
+ const unsupported = (detail) => new LockError("lock-unsupported", detail);
49
+ // Small helpers ------------------------------------------------------------
50
+ /** Read-only Map view. Snapshots hand these out so a holder cannot edit them. */
51
+ export class FrozenMap {
52
+ #inner;
53
+ constructor(entries) {
54
+ this.#inner = new Map(entries);
55
+ Object.freeze(this);
56
+ }
57
+ get size() {
58
+ return this.#inner.size;
59
+ }
60
+ get(key) {
61
+ return this.#inner.get(key);
62
+ }
63
+ has(key) {
64
+ return this.#inner.has(key);
65
+ }
66
+ forEach(fn) {
67
+ this.#inner.forEach((v, k) => fn(v, k, this));
68
+ }
69
+ entries() {
70
+ return this.#inner.entries();
71
+ }
72
+ keys() {
73
+ return this.#inner.keys();
74
+ }
75
+ values() {
76
+ return this.#inner.values();
77
+ }
78
+ [Symbol.iterator]() {
79
+ return this.#inner[Symbol.iterator]();
80
+ }
81
+ }
82
+ function isObject(value) {
83
+ return value !== null && typeof value === "object" && !Array.isArray(value);
84
+ }
85
+ function own(obj, key) {
86
+ return Object.getOwnPropertyDescriptor(obj, key)?.value;
87
+ }
88
+ function hasOwn(obj, key) {
89
+ return Object.prototype.hasOwnProperty.call(obj, key);
90
+ }
91
+ function isAscii(text) {
92
+ return /^[\x00-\x7f]*$/.test(text);
93
+ }
94
+ function sortedKeys(obj) {
95
+ return Object.keys(obj).sort(compareCodePoints);
96
+ }
97
+ function repr(value) {
98
+ try {
99
+ return JSON.stringify(value) ?? String(value);
100
+ }
101
+ catch {
102
+ return String(value);
103
+ }
104
+ }
105
+ const DEFAULT_PORTS = { http: 80, https: 443 };
106
+ /** The parts of Python's urllib.parse.urlsplit that section 3.1 needs. */
107
+ function splitUrl(raw) {
108
+ // urlsplit strips leading C0 controls and spaces and drops tab, CR and LF.
109
+ const url = raw.replace(/^[\x00-\x20]+/, "").replace(/[\t\r\n]/g, "");
110
+ let rest = url;
111
+ let scheme = "";
112
+ const colon = url.indexOf(":");
113
+ if (colon > 0 && /^[A-Za-z][A-Za-z0-9+.-]*$/.test(url.slice(0, colon))) {
114
+ scheme = url.slice(0, colon).toLowerCase();
115
+ rest = url.slice(colon + 1);
116
+ }
117
+ let netloc = "";
118
+ if (rest.startsWith("//")) {
119
+ const after = rest.slice(2);
120
+ const end = after.search(/[/?#]/);
121
+ netloc = end === -1 ? after : after.slice(0, end);
122
+ rest = end === -1 ? "" : after.slice(end);
123
+ if (netloc.includes("[") !== netloc.includes("]"))
124
+ throw new Error("invalid IPv6 URL");
125
+ }
126
+ const cut = rest.search(/[?#]/);
127
+ const path = cut === -1 ? rest : rest.slice(0, cut);
128
+ return { scheme, netloc, path };
129
+ }
130
+ function hostAndPort(netloc) {
131
+ const hostinfo = netloc.slice(netloc.lastIndexOf("@") + 1);
132
+ let host;
133
+ let portText;
134
+ if (hostinfo.startsWith("[")) {
135
+ const close = hostinfo.indexOf("]");
136
+ if (close === -1)
137
+ throw new Error("invalid IPv6 URL");
138
+ host = hostinfo.slice(1, close);
139
+ if (!/^[0-9A-Fa-f:.]+(%.+)?$/.test(host) || !host.includes(":")) {
140
+ throw new Error("invalid IPv6 literal");
141
+ }
142
+ const tail = hostinfo.slice(close + 1);
143
+ portText = tail.startsWith(":") ? tail.slice(1) : "";
144
+ }
145
+ else {
146
+ const c = hostinfo.indexOf(":");
147
+ host = c === -1 ? hostinfo : hostinfo.slice(0, c);
148
+ portText = c === -1 ? "" : hostinfo.slice(c + 1);
149
+ }
150
+ host = host.toLowerCase();
151
+ if (portText === "")
152
+ return { host, port: null };
153
+ if (!/^[0-9]+$/.test(portText))
154
+ throw new Error(`invalid port ${repr(portText)}`);
155
+ const port = Number(portText);
156
+ if (port > 65535)
157
+ throw new Error("port out of range");
158
+ return { host, port };
159
+ }
160
+ /** Section 3.1. Throws Error on a URL that cannot be an identity. */
161
+ export function normalizeUrl(url) {
162
+ if (!isAscii(url))
163
+ throw new Error("URL must be ASCII; convert the host to its A-label first");
164
+ const parts = splitUrl(url);
165
+ const scheme = parts.scheme;
166
+ if (!(scheme in DEFAULT_PORTS))
167
+ throw new Error(`unsupported scheme ${repr(scheme)}`);
168
+ const { host, port } = hostAndPort(parts.netloc);
169
+ if (host === "")
170
+ throw new Error("URL has no host");
171
+ const shownHost = host.includes(":") ? `[${host}]` : host;
172
+ const netloc = port === null || port === DEFAULT_PORTS[scheme] ? shownHost : `${shownHost}:${port}`;
173
+ return `${scheme}://${netloc}${parts.path}`;
174
+ }
175
+ function validPackageName(name) {
176
+ return name.length > 0 && /^[\x21-\x7e]+$/.test(name);
177
+ }
178
+ function validCommand(name) {
179
+ return validPackageName(name) && !name.includes("/") && !name.includes("\\") && name !== "." && name !== "..";
180
+ }
181
+ export function remoteIdentity(url) {
182
+ return { kind: "remote", url: normalizeUrl(url) };
183
+ }
184
+ export function packageIdentity(registry, name) {
185
+ if (!REGISTRIES.includes(registry))
186
+ throw new Error(`unknown registry ${repr(registry)}`);
187
+ let n = name;
188
+ if (registry === "pypi") {
189
+ n = n.replace(/[-_.]+/g, "-").toLowerCase();
190
+ }
191
+ else if (registry === "oci") {
192
+ const at = n.indexOf("@");
193
+ if (at !== -1)
194
+ n = n.slice(0, at);
195
+ const slash = n.lastIndexOf("/");
196
+ const head = slash === -1 ? "" : n.slice(0, slash);
197
+ let last = slash === -1 ? n : n.slice(slash + 1);
198
+ const colon = last.indexOf(":");
199
+ if (colon !== -1)
200
+ last = last.slice(0, colon);
201
+ n = ((head ? `${head}/` : "") + last).toLowerCase();
202
+ }
203
+ if (!validPackageName(n))
204
+ throw new Error(`invalid package name ${repr(n)}`);
205
+ return { kind: "stdio", package: `${registry}:${n}` };
206
+ }
207
+ /**
208
+ * Section 3.2 command form: the basename of the command and every argument
209
+ * verbatim. Throws for a command that is not a basename once its path is
210
+ * removed, and for secret-looking arguments (section 3.2.1), which an identity
211
+ * can neither keep nor strip. The message names the rule, never the value.
212
+ */
213
+ export function commandIdentity(command, args = []) {
214
+ const parts = command.replace(/\\/g, "/").split("/");
215
+ const base = parts[parts.length - 1] ?? "";
216
+ if (!validCommand(base))
217
+ throw new Error(`invalid command ${repr(command)}`);
218
+ if (!Array.isArray(args) || !args.every((a) => typeof a === "string" && !hasLoneSurrogate(a))) {
219
+ throw new Error("args must be strings without unpaired surrogates");
220
+ }
221
+ const problem = secretArgProblem(args);
222
+ if (problem !== null) {
223
+ throw new Error(`cannot form a server identity: ${problem}; move it to an env var or configure an alias`);
224
+ }
225
+ return Object.freeze({ kind: "stdio", command: base, args: Object.freeze([...args]) });
226
+ }
227
+ // Section 3.2.1. ASCII-only and case-insensitive; without the u flag, \b and
228
+ // the i flag work on ASCII only, as in the Python port with re.ASCII.
229
+ const SECRET_SEGMENTS = new Set([
230
+ "token", "secret", "password", "passwd", "pwd", "apikey", "auth", "authorization",
231
+ "credential", "credentials", "bearer", "cookie", "key", "pat", "privatekey", "session",
232
+ ]);
233
+ const SECRET_VALUE = new RegExp([
234
+ "^(bearer|basic|token)[ \\t]+[^ \\t]",
235
+ "\\bsk-[A-Za-z0-9_-]{16,}",
236
+ "\\bsk_(live|test)_[A-Za-z0-9]{10,}",
237
+ "\\bgh[pousr]_[A-Za-z0-9]{20,}",
238
+ "\\bgithub_pat_[A-Za-z0-9_]{20,}",
239
+ "\\bglpat-[A-Za-z0-9_-]{16,}",
240
+ "\\bxox[abprs]-[A-Za-z0-9-]{10,}",
241
+ "\\bAKIA[0-9A-Z]{16}\\b",
242
+ "\\bAIza[0-9A-Za-z_-]{30,}",
243
+ "\\bnpm_[A-Za-z0-9]{30,}",
244
+ "-----BEGIN [A-Z ]*PRIVATE KEY",
245
+ "\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.",
246
+ ].join("|"), "i");
247
+ function secretName(name) {
248
+ const segments = name.replace(/^-+/, "").toLowerCase().split(/[-_.]+/);
249
+ return segments.some((s) => SECRET_SEGMENTS.has(s)) || SECRET_SEGMENTS.has(segments.join(""));
250
+ }
251
+ function urlProblem(arg) {
252
+ const at = arg.indexOf("://");
253
+ if (at === -1)
254
+ return null;
255
+ const rest = arg.slice(at + 3);
256
+ const end = rest.search(/[/?#]/);
257
+ const authority = end === -1 ? rest : rest.slice(0, end);
258
+ const lastAt = authority.lastIndexOf("@");
259
+ if (lastAt !== -1 && authority.slice(0, lastAt).includes(":"))
260
+ return "a URL argument carries a password in its userinfo";
261
+ const q = rest.indexOf("?");
262
+ if (q !== -1) {
263
+ const hash = rest.indexOf("#", q);
264
+ const query = rest.slice(q + 1, hash === -1 ? undefined : hash);
265
+ for (const part of query.split("&")) {
266
+ const eq = part.indexOf("=");
267
+ if (secretName(eq === -1 ? part : part.slice(0, eq)))
268
+ return "a URL argument carries a credential in its query";
269
+ }
270
+ }
271
+ return null;
272
+ }
273
+ /** null if no argument is secret-looking under section 3.2.1, else what was found. */
274
+ export function secretArgProblem(args) {
275
+ let valueOf = null;
276
+ for (const arg of args) {
277
+ if (valueOf !== null && !arg.startsWith("-"))
278
+ return `the value of ${valueOf} looks like a credential`;
279
+ valueOf = null;
280
+ if (SECRET_VALUE.test(arg))
281
+ return "an argument matches a credential pattern";
282
+ const url = urlProblem(arg);
283
+ if (url !== null)
284
+ return url;
285
+ const eq = arg.indexOf("=");
286
+ const head = eq === -1 ? arg : arg.slice(0, eq);
287
+ if (arg.startsWith("-")) {
288
+ if (secretName(head)) {
289
+ if (eq !== -1 && arg.length > eq + 1)
290
+ return `${head} carries a credential`;
291
+ if (eq === -1)
292
+ valueOf = head;
293
+ }
294
+ continue;
295
+ }
296
+ if (eq !== -1 && ENV_NAME.test(head) && secretName(head))
297
+ return `${head}= carries a credential`;
298
+ }
299
+ return null;
300
+ }
301
+ /** Validate a parsed identity object. Returns a fresh frozen copy or throws LockError. */
302
+ export function validateIdentity(value) {
303
+ if (!isObject(value))
304
+ throw unreadable("identity must be an object");
305
+ const keys = Object.keys(value);
306
+ if (!keys.every((k) => k === "args" || typeof own(value, k) === "string")) {
307
+ throw unreadable("identity members must be strings, args an array of strings");
308
+ }
309
+ const has = (...names) => keys.length === names.length && names.every((n) => keys.includes(n));
310
+ const kind = own(value, "kind");
311
+ if (kind === "remote") {
312
+ if (!has("kind", "url"))
313
+ throw unsupported(`remote identity members ${repr(keys.sort())}`);
314
+ const url = own(value, "url");
315
+ let ok;
316
+ try {
317
+ ok = normalizeUrl(url) === url;
318
+ }
319
+ catch {
320
+ ok = false;
321
+ }
322
+ if (!ok)
323
+ throw unreadable(`identity url is not normalized: ${repr(url)}`);
324
+ return Object.freeze({ kind: "remote", url });
325
+ }
326
+ if (kind === "stdio") {
327
+ if (has("kind", "package")) {
328
+ const pkg = own(value, "package");
329
+ const colon = pkg.indexOf(":");
330
+ const registry = colon === -1 ? pkg : pkg.slice(0, colon);
331
+ const name = colon === -1 ? "" : pkg.slice(colon + 1);
332
+ if (!REGISTRIES.includes(registry) || !validPackageName(name)) {
333
+ throw unreadable(`invalid package ${repr(pkg)}`);
334
+ }
335
+ return Object.freeze({ kind: "stdio", package: pkg });
336
+ }
337
+ if (has("kind", "command", "args")) {
338
+ const command = own(value, "command");
339
+ const args = own(value, "args");
340
+ if (!Array.isArray(args) || !args.every((a) => typeof a === "string")) {
341
+ throw unreadable("identity args must be an array of strings");
342
+ }
343
+ let derived;
344
+ try {
345
+ derived = commandIdentity(command, args);
346
+ }
347
+ catch {
348
+ throw unreadable("identity command is not a basename, or its args cannot form an identity");
349
+ }
350
+ if (derived.command !== command)
351
+ throw unreadable(`command must be a basename: ${repr(command)}`);
352
+ return derived;
353
+ }
354
+ throw unsupported(`stdio identity members ${repr(keys.sort())}`);
355
+ }
356
+ throw unsupported(`identity kind ${repr(kind)}`);
357
+ }
358
+ function identityFields(identity) {
359
+ if (identity.kind === "remote")
360
+ return [["kind", "remote"], ["url", identity.url]];
361
+ if ("package" in identity)
362
+ return [["kind", "stdio"], ["package", identity.package]];
363
+ return [["kind", "stdio"], ["command", identity.command], ["args", identity.args]];
364
+ }
365
+ /** RFC 8785 text of the identity (section 3.3). Compare identities by this string. */
366
+ export function identityKey(identity) {
367
+ const obj = Object.create(null);
368
+ for (const [k, v] of identityFields(identity))
369
+ obj[k] = v;
370
+ return canonicalize(obj);
371
+ }
372
+ export function identityH16(identity) {
373
+ return sha256Hex(identityKey(identity)).slice(0, 16);
374
+ }
375
+ export function slug(identity) {
376
+ let source;
377
+ if (identity.kind === "remote") {
378
+ const parts = splitUrl(identity.url);
379
+ let host = "";
380
+ try {
381
+ host = hostAndPort(parts.netloc).host;
382
+ }
383
+ catch {
384
+ host = "";
385
+ }
386
+ source = host + parts.path;
387
+ }
388
+ else if ("package" in identity) {
389
+ const pkg = identity.package;
390
+ const colon = pkg.indexOf(":");
391
+ source = colon === -1 ? "" : pkg.slice(colon + 1);
392
+ }
393
+ else {
394
+ source = identity.command;
395
+ }
396
+ // Python str.lower() also folds non-ASCII letters, but they are then replaced
397
+ // by `-` with everything else outside a-z0-9, so the result is the same.
398
+ const text = source.toLowerCase().replace(/[^a-z0-9]+/g, "-");
399
+ const cut = text.replace(/^-+|-+$/g, "").slice(0, SLUG_MAX).replace(/-+$/, "");
400
+ return cut || "server";
401
+ }
402
+ export function shardFilename(identity) {
403
+ return `${slug(identity)}--${identityH16(identity)}.json`;
404
+ }
405
+ // Strict JSON --------------------------------------------------------------
406
+ const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
407
+ /**
408
+ * Find a member name repeated within one object. JSON.parse keeps the last
409
+ * value silently, so this scans the text, which JSON.parse has already
410
+ * accepted, and compares decoded names (so "a" and "a" collide).
411
+ */
412
+ /**
413
+ * Section 4.3: every number in a lock file is an integer token. JSON.parse
414
+ * turns 1.0 and 1e0 into 1, so the raw text is checked. Returns the first
415
+ * offending token, or null. Outside strings, a number is the only token that
416
+ * starts with `-` or a digit.
417
+ */
418
+ export function findNonIntegerNumber(text) {
419
+ let i = 0;
420
+ while (i < text.length) {
421
+ const ch = text[i];
422
+ if (ch === '"') {
423
+ let j = i + 1;
424
+ while (j < text.length && text[j] !== '"')
425
+ j += text[j] === "\\" ? 2 : 1;
426
+ i = j + 1;
427
+ continue;
428
+ }
429
+ if (ch === "-" || (ch >= "0" && ch <= "9")) {
430
+ let j = i + 1;
431
+ while (j < text.length && /[0-9.eE+-]/.test(text[j]))
432
+ j++;
433
+ const token = text.slice(i, j);
434
+ if (!/^-?[0-9]+$/.test(token))
435
+ return token;
436
+ i = j;
437
+ continue;
438
+ }
439
+ i++;
440
+ }
441
+ return null;
442
+ }
443
+ export function findDuplicateKey(text) {
444
+ const stack = [];
445
+ let i = 0;
446
+ while (i < text.length) {
447
+ const ch = text[i];
448
+ if (ch === '"') {
449
+ let j = i + 1;
450
+ while (j < text.length && text[j] !== '"')
451
+ j += text[j] === "\\" ? 2 : 1;
452
+ const top = stack[stack.length - 1];
453
+ if (top !== undefined && top.keys !== null && top.expectKey) {
454
+ const key = JSON.parse(text.slice(i, j + 1));
455
+ if (top.keys.has(key))
456
+ return key;
457
+ top.keys.add(key);
458
+ top.expectKey = false;
459
+ }
460
+ i = j + 1;
461
+ continue;
462
+ }
463
+ if (ch === "{")
464
+ stack.push({ keys: new Set(), expectKey: true });
465
+ else if (ch === "[")
466
+ stack.push({ keys: null, expectKey: false });
467
+ else if (ch === "}" || ch === "]")
468
+ stack.pop();
469
+ else if (ch === ",") {
470
+ const top = stack[stack.length - 1];
471
+ if (top !== undefined && top.keys !== null)
472
+ top.expectKey = true;
473
+ }
474
+ i++;
475
+ }
476
+ return null;
477
+ }
478
+ /**
479
+ * Parse lock file bytes: UTF-8 (a BOM is kept and so rejected), JSON, no repeated
480
+ * member names, integer number tokens only. JSON.parse has no NaN or Infinity,
481
+ * so that rule needs no code.
482
+ */
483
+ export function strictJson(data, what) {
484
+ let text;
485
+ try {
486
+ text = UTF8.decode(data);
487
+ }
488
+ catch {
489
+ throw unreadable(`${what}: not UTF-8`);
490
+ }
491
+ let value;
492
+ try {
493
+ value = JSON.parse(text);
494
+ }
495
+ catch (exc) {
496
+ throw unreadable(`${what}: ${exc.message}`);
497
+ }
498
+ const dup = findDuplicateKey(text);
499
+ if (dup !== null)
500
+ throw unreadable(`${what}: duplicate member name ${repr(dup)}`);
501
+ const num = findNonIntegerNumber(text);
502
+ if (num !== null)
503
+ throw unreadable(`${what}: number ${num} is not an integer token`);
504
+ return value;
505
+ }
506
+ // File access --------------------------------------------------------------
507
+ function errCode(exc) {
508
+ return exc !== null && typeof exc === "object" && "code" in exc ? String(exc.code) : "";
509
+ }
510
+ function checkDir(path, name, required) {
511
+ let st;
512
+ try {
513
+ st = lstatSync(path);
514
+ }
515
+ catch (exc) {
516
+ if (errCode(exc) === "ENOENT") {
517
+ if (required)
518
+ throw unreadable(`${name} is missing`);
519
+ return null;
520
+ }
521
+ throw unreadable(`${name}: ${errCode(exc) || "unreadable"}`);
522
+ }
523
+ if (st.isSymbolicLink() || !st.isDirectory())
524
+ throw unreadable(`${name} is not a plain directory`);
525
+ let real;
526
+ try {
527
+ real = realpathSync(path);
528
+ }
529
+ catch (exc) {
530
+ throw unreadable(`${name}: ${errCode(exc) || "unreadable"}`);
531
+ }
532
+ return { path, real, dev: st.dev, ino: st.ino };
533
+ }
534
+ /**
535
+ * O_NOFOLLOW guards only the last path component. After a file is open, check
536
+ * that its directory is still the plain directory that was checked (same
537
+ * dev/ino, same real path) and that the open file is the entry of that name
538
+ * in it. A directory swapped for a symlink between the check and the open, or
539
+ * swapped back after it, is refused.
540
+ */
541
+ function confirmInDir(fd, dir, entry, name) {
542
+ const moved = () => unreadable(`${name}: its directory changed while it was read`);
543
+ let st;
544
+ let real;
545
+ let inDir;
546
+ try {
547
+ st = lstatSync(dir.path);
548
+ real = realpathSync(dir.path);
549
+ inDir = lstatSync(join(real, entry));
550
+ }
551
+ catch {
552
+ throw moved();
553
+ }
554
+ if (st.isSymbolicLink() || !st.isDirectory() || st.dev !== dir.dev || st.ino !== dir.ino || real !== dir.real) {
555
+ throw moved();
556
+ }
557
+ const fst = fstatSync(fd);
558
+ if (inDir.dev !== fst.dev || inDir.ino !== fst.ino)
559
+ throw moved();
560
+ }
561
+ class MissingFile extends Error {
562
+ }
563
+ /**
564
+ * lstat, then open with O_NOFOLLOW, check the directory (confirmInDir) and
565
+ * fstat the descriptor, then read it once.
566
+ */
567
+ function readRegular(dir, entry, name) {
568
+ const path = join(dir.path, entry);
569
+ let st;
570
+ try {
571
+ st = lstatSync(path);
572
+ }
573
+ catch (exc) {
574
+ if (errCode(exc) === "ENOENT")
575
+ throw new MissingFile(name);
576
+ throw unreadable(`${name}: ${errCode(exc) || "unreadable"}`);
577
+ }
578
+ if (st.isSymbolicLink() || !st.isFile())
579
+ throw unreadable(`${name} is not a regular file`);
580
+ let flags = constants.O_RDONLY;
581
+ if (typeof constants.O_NOFOLLOW === "number")
582
+ flags |= constants.O_NOFOLLOW;
583
+ let fd;
584
+ try {
585
+ fd = openSync(path, flags);
586
+ }
587
+ catch (exc) {
588
+ if (errCode(exc) === "ENOENT")
589
+ throw new MissingFile(name);
590
+ throw unreadable(`${name} is not a regular file (${errCode(exc)})`);
591
+ }
592
+ try {
593
+ const fst = fstatSync(fd);
594
+ if (!fst.isFile())
595
+ throw unreadable(`${name} is not a regular file`);
596
+ confirmInDir(fd, dir, entry, name);
597
+ const chunks = [];
598
+ const buf = Buffer.alloc(64 * 1024);
599
+ for (;;) {
600
+ const n = readSync(fd, buf, 0, buf.length, null);
601
+ if (n === 0)
602
+ break;
603
+ chunks.push(Buffer.from(buf.subarray(0, n)));
604
+ }
605
+ return Buffer.concat(chunks);
606
+ }
607
+ finally {
608
+ closeSync(fd);
609
+ }
610
+ }
611
+ /** Every file in `.mcp/lock`, read once. Names are checked as raw bytes for ASCII. */
612
+ export function readLockDir(lockDir) {
613
+ return readLockFiles(checkDir(lockDir, ".mcp/lock", true));
614
+ }
615
+ function readLockFiles(dir) {
616
+ const lockDir = dir.path;
617
+ const names = readdirSync(lockDir, { encoding: "buffer" });
618
+ for (const raw of names) {
619
+ if (raw.some((b) => b > 0x7f))
620
+ throw unreadable(`non-ASCII filename ${repr(raw.toString("utf8"))}`);
621
+ }
622
+ const ascii = names.map((b) => b.toString("latin1")).sort(compareCodePoints);
623
+ const files = new Map();
624
+ for (const name of ascii) {
625
+ const shard = SHARD_NAME.exec(name);
626
+ if (!(name === MANIFEST || name === SIGNATURE || name === ALIASES || (shard !== null && shard[1].length <= SLUG_MAX))) {
627
+ throw unreadable(`unexpected file ${repr(name)}`);
628
+ }
629
+ try {
630
+ files.set(name, readRegular(dir, name, name));
631
+ }
632
+ catch (exc) {
633
+ if (exc instanceof MissingFile)
634
+ throw unreadable(`${name} vanished while reading`);
635
+ throw exc;
636
+ }
637
+ }
638
+ return files;
639
+ }
640
+ // Checks -------------------------------------------------------------------
641
+ /** Section 8.2 steps 1 and 2: with a signer configured, both files and a good signature. */
642
+ async function verifySignature(files, config) {
643
+ const signer = config.signer ?? null;
644
+ if (signer === null)
645
+ return false;
646
+ if (config.sequenceStore !== true) {
647
+ throw new LockError("lock-signature-invalid", "a signer is configured but the host config names no sequence store");
648
+ }
649
+ const raw = files.get(MANIFEST);
650
+ if (raw === undefined)
651
+ throw new LockError("lock-signature-invalid", "signer configured but no MANIFEST.json");
652
+ const bundle = files.get(SIGNATURE);
653
+ const verifier = config.verifier ?? null;
654
+ let ok = false;
655
+ if (bundle !== undefined && verifier !== null) {
656
+ try {
657
+ ok = (await verifier.verify(raw, bundle, signer)) === true;
658
+ }
659
+ catch {
660
+ ok = false;
661
+ }
662
+ }
663
+ if (!ok)
664
+ throw new LockError("lock-signature-invalid", "MANIFEST.json signature missing or invalid");
665
+ return true;
666
+ }
667
+ /** Section 8.1 and 8.2 step 3. `signed` says the signature over these bytes was verified. */
668
+ function checkManifest(files, config, signed) {
669
+ const raw = files.get(MANIFEST);
670
+ if (raw === undefined) {
671
+ if (config.requireManifest)
672
+ throw unreadable("MANIFEST.json required by host configuration");
673
+ return null;
674
+ }
675
+ const manifest = strictJson(raw, MANIFEST);
676
+ if (!isObject(manifest))
677
+ throw unreadable("MANIFEST.json is not an object");
678
+ if (own(manifest, "lockVersion") !== LOCK_VERSION) {
679
+ throw unsupported(`MANIFEST.json lockVersion ${repr(own(manifest, "lockVersion"))}`);
680
+ }
681
+ if (Object.keys(manifest).some((k) => !["lockVersion", "sequence", "files"].includes(k))) {
682
+ throw unsupported(`MANIFEST.json members ${repr(sortedKeys(manifest))}`);
683
+ }
684
+ const sequence = own(manifest, "sequence");
685
+ const listed = own(manifest, "files");
686
+ // strictJson already refused 1.0 and 1e0 in the raw text (section 4.3).
687
+ if (typeof sequence !== "number" || !Number.isSafeInteger(sequence) || sequence < 0 || sequence > SEQUENCE_MAX || !isObject(listed)) {
688
+ throw unreadable("MANIFEST.json sequence or files malformed");
689
+ }
690
+ const last = config.lastSequence ?? null;
691
+ if (signed && last !== null && sequence < last) {
692
+ throw new LockError("lock-signature-invalid", `sequence ${sequence} < ${last}`);
693
+ }
694
+ const onDisk = [...files.keys()].filter((n) => n !== MANIFEST && n !== SIGNATURE);
695
+ const listedNames = Object.keys(listed);
696
+ const diff = [
697
+ ...onDisk.filter((n) => !hasOwn(listed, n)),
698
+ ...listedNames.filter((n) => !files.has(n) || n === MANIFEST || n === SIGNATURE),
699
+ ];
700
+ if (diff.length > 0)
701
+ throw unreadable(`file set differs from MANIFEST.json: ${repr(diff.sort(compareCodePoints))}`);
702
+ for (const name of onDisk.sort(compareCodePoints)) {
703
+ if (own(listed, name) !== "sha256:" + sha256Hex(files.get(name))) {
704
+ throw unreadable(`${name} does not match MANIFEST.json`);
705
+ }
706
+ }
707
+ return sequence;
708
+ }
709
+ function checkAliases(raw) {
710
+ const doc = strictJson(raw, ALIASES);
711
+ if (!isObject(doc))
712
+ throw unreadable("aliases.json is not an object");
713
+ if (own(doc, "lockVersion") !== LOCK_VERSION) {
714
+ throw unsupported(`aliases.json lockVersion ${repr(own(doc, "lockVersion"))}`);
715
+ }
716
+ const keys = Object.keys(doc);
717
+ if (keys.length !== 2 || !keys.includes("lockVersion") || !keys.includes("aliases")) {
718
+ throw unsupported(`aliases.json members ${repr(sortedKeys(doc))}`);
719
+ }
720
+ const aliases = own(doc, "aliases");
721
+ if (!isObject(aliases))
722
+ throw unreadable("aliases must be an object");
723
+ const out = new Map();
724
+ for (const name of Object.keys(aliases))
725
+ out.set(name, validateIdentity(own(aliases, name)));
726
+ return out;
727
+ }
728
+ function checkAttributes(attributes) {
729
+ if (!isObject(attributes))
730
+ throw unreadable("attributes must be an object");
731
+ const keys = Object.keys(attributes);
732
+ if (keys.some((k) => !ATTRIBUTE_KEYS.has(k)))
733
+ throw unsupported(`attributes members ${repr(sortedKeys(attributes))}`);
734
+ const out = {};
735
+ for (const key of ["version", "audience", "command"]) {
736
+ if (hasOwn(attributes, key)) {
737
+ const v = own(attributes, key);
738
+ if (typeof v !== "string")
739
+ throw unreadable(`attributes.${key} must be a string`);
740
+ out[key] = v;
741
+ }
742
+ }
743
+ for (const key of ["args", "env"]) {
744
+ const v = hasOwn(attributes, key) ? own(attributes, key) : [];
745
+ if (!Array.isArray(v) || !v.every((s) => typeof s === "string")) {
746
+ throw unreadable(`attributes.${key} must be an array of strings`);
747
+ }
748
+ if (hasOwn(attributes, key))
749
+ out[key] = Object.freeze([...v]);
750
+ }
751
+ if (!(out.env ?? []).every((v) => ENV_NAME.test(v)))
752
+ throw unreadable("attributes.env holds names only");
753
+ return Object.freeze(out);
754
+ }
755
+ function checkEntry(name, entry, readContract) {
756
+ const label = repr(name);
757
+ if (!isObject(entry))
758
+ throw unreadable(`tool ${label} entry is not an object`);
759
+ const keys = Object.keys(entry);
760
+ if (keys.some((k) => !ENTRY_KEYS.includes(k) && !ENTRY_OPTIONAL.includes(k)))
761
+ throw unsupported(`tool ${label} members ${repr(sortedKeys(entry))}`);
762
+ if (!ENTRY_KEYS.every((k) => hasOwn(entry, k))) {
763
+ throw unreadable(`tool ${label} lacks ${repr(ENTRY_KEYS.filter((k) => !hasOwn(entry, k)).sort())}`);
764
+ }
765
+ const approval = own(entry, "approval");
766
+ if (!isObject(approval))
767
+ throw unreadable(`tool ${label} approval is not an object`);
768
+ if (Object.keys(approval).some((k) => !APPROVAL_KEYS.has(k))) {
769
+ throw unsupported(`tool ${label} approval members ${repr(sortedKeys(approval))}`);
770
+ }
771
+ const subject = own(approval, "subject");
772
+ const at = own(approval, "at");
773
+ const via = own(approval, "via");
774
+ if (typeof subject !== "string" || typeof at !== "string" || typeof via !== "string") {
775
+ throw unreadable(`tool ${label} approval lacks subject, at or via`);
776
+ }
777
+ const ref = own(approval, "ref");
778
+ if (hasOwn(approval, "ref") && typeof ref !== "string")
779
+ throw unreadable(`tool ${label} approval ref must be a string`);
780
+ if (!RFC3339.test(at))
781
+ throw unreadable(`tool ${label} approval time is not RFC 3339`);
782
+ if (!VIA.includes(via))
783
+ throw unsupported(`tool ${label} approval via ${repr(via)}`);
784
+ const digest = own(entry, "digest");
785
+ const contract = own(entry, "contract");
786
+ const m = typeof digest === "string" ? DIGEST_FORM.exec(digest) : null;
787
+ if (m === null)
788
+ throw unreadable(`tool ${label} digest malformed`);
789
+ if (typeof contract !== "string" || !CONTRACT_NAME.test(contract)) {
790
+ throw unreadable(`tool ${label} contract is not a contract filename: ${repr(contract)}`);
791
+ }
792
+ const hex = m[1];
793
+ if (contract !== `sha256-${hex}.json`) {
794
+ throw new LockError("contract-mismatch", `tool ${label} contract filename differs from digest`);
795
+ }
796
+ const meta = own(entry, "metaDigest");
797
+ if (hasOwn(entry, "metaDigest") && (typeof meta !== "string" || !DIGEST_FORM.test(meta))) {
798
+ throw unreadable(`tool ${label} metaDigest malformed`);
799
+ }
800
+ const data = readContract(contract);
801
+ if (data === null)
802
+ throw new LockError("contract-mismatch", `tool ${label} contract file missing`);
803
+ if (sha256Hex(data) !== hex)
804
+ throw new LockError("contract-mismatch", `tool ${label} contract bytes differ from digest`);
805
+ const appr = typeof ref === "string"
806
+ ? Object.freeze({ subject, at, via: via, ref })
807
+ : Object.freeze({ subject, at, via: via });
808
+ return typeof meta === "string"
809
+ ? Object.freeze({ digest: digest, contract, metaDigest: meta, approval: appr })
810
+ : Object.freeze({ digest: digest, contract, approval: appr });
811
+ }
812
+ /** Parse and check one shard (section 5). `filename` must already match SHARD_NAME. */
813
+ export function parseShard(filename, raw, readContract) {
814
+ const doc = strictJson(raw, filename);
815
+ if (!isObject(doc))
816
+ throw unreadable(`${filename} is not an object`);
817
+ if (!hasOwn(doc, "lockVersion"))
818
+ throw unreadable(`${filename} has no lockVersion`);
819
+ if (own(doc, "lockVersion") !== LOCK_VERSION) {
820
+ throw unsupported(`${filename} lockVersion ${repr(own(doc, "lockVersion"))}`);
821
+ }
822
+ const extra = Object.keys(doc).filter((k) => !SHARD_KEYS.has(k));
823
+ if (extra.length > 0)
824
+ throw unsupported(`${filename} members ${repr(extra.sort(compareCodePoints))}`);
825
+ if (!hasOwn(doc, "identity") || !hasOwn(doc, "tools"))
826
+ throw unreadable(`${filename} lacks identity or tools`);
827
+ const identity = validateIdentity(own(doc, "identity"));
828
+ const match = SHARD_NAME.exec(filename);
829
+ if (match === null)
830
+ throw unreadable(`${filename} is not a shard filename`);
831
+ if (identityH16(identity) !== match[2])
832
+ throw unreadable(`${filename} identity does not hash to its filename`);
833
+ const attributes = hasOwn(doc, "attributes") ? checkAttributes(own(doc, "attributes")) : null;
834
+ let instructions = null;
835
+ if (hasOwn(doc, "instructions")) {
836
+ const block = own(doc, "instructions");
837
+ if (!isObject(block))
838
+ throw unreadable(`${filename} instructions is not an object`);
839
+ if (Object.keys(block).some((k) => k !== "digest")) {
840
+ throw unsupported(`${filename} instructions members ${repr(sortedKeys(block))}`);
841
+ }
842
+ const d = own(block, "digest");
843
+ if (typeof d !== "string" || !DIGEST_FORM.test(d))
844
+ throw unreadable(`${filename} instructions digest malformed`);
845
+ instructions = d;
846
+ }
847
+ const tools = own(doc, "tools");
848
+ if (!isObject(tools))
849
+ throw unreadable(`${filename} tools is not an object`);
850
+ const entries = [];
851
+ for (const name of Object.keys(tools))
852
+ entries.push([name, checkEntry(name, own(tools, name), readContract)]);
853
+ return Object.freeze({ identity, attributes, instructions, tools: new FrozenMap(entries) });
854
+ }
855
+ /** Hash of the lock directory contents; contracts are bound through the shards. */
856
+ export function lockFilesId(files) {
857
+ let text = "";
858
+ for (const name of [...files.keys()].sort(compareCodePoints)) {
859
+ text += `${name}\u0000${sha256Hex(files.get(name))}\n`;
860
+ }
861
+ return "sha256:" + sha256Hex(text);
862
+ }
863
+ /** Directory checks and one read of every file in `.mcp/lock`. */
864
+ function readLockBytes(root) {
865
+ const mcp = join(root, ".mcp");
866
+ checkDir(mcp, ".mcp", true);
867
+ const lockDir = checkDir(join(mcp, "lock"), ".mcp/lock", true);
868
+ const contractsDir = checkDir(join(mcp, "contracts"), ".mcp/contracts", false);
869
+ const files = readLockFiles(lockDir);
870
+ const readContract = (filename) => {
871
+ if (contractsDir === null)
872
+ return null;
873
+ try {
874
+ return readRegular(contractsDir, filename, filename);
875
+ }
876
+ catch (exc) {
877
+ if (exc instanceof MissingFile)
878
+ return null;
879
+ throw exc;
880
+ }
881
+ };
882
+ return { files, readContract };
883
+ }
884
+ function parseLock(bytes, config, signed) {
885
+ const { files, readContract } = bytes;
886
+ const sequence = checkManifest(files, config, signed);
887
+ const aliasBytes = files.get(ALIASES);
888
+ const aliases = aliasBytes !== undefined ? checkAliases(aliasBytes) : new Map();
889
+ const shards = new Map();
890
+ for (const name of [...files.keys()].sort(compareCodePoints)) {
891
+ if (!SHARD_NAME.test(name))
892
+ continue;
893
+ const shard = parseShard(name, files.get(name), readContract);
894
+ const key = identityKey(shard.identity);
895
+ if (shards.has(key))
896
+ throw unreadable(`two shards for one identity: ${name}`);
897
+ shards.set(key, shard);
898
+ }
899
+ const lock = Object.freeze({
900
+ shards: new FrozenMap(shards),
901
+ aliases: new FrozenMap(aliases),
902
+ sequence,
903
+ });
904
+ return Object.freeze({ lock, snapshotId: lockFilesId(files) });
905
+ }
906
+ /** Read and verify the whole lock under `root`. Any failure throws LockError. */
907
+ export async function loadLock(root, config = {}) {
908
+ const bytes = readLockBytes(root);
909
+ const signed = await verifySignature(bytes.files, config);
910
+ return parseLock(bytes, config, signed);
911
+ }
912
+ /**
913
+ * Synchronous read for writers holding the file lock. Refuses a config with a
914
+ * signer, since signature checks are asynchronous; a writer does not need them.
915
+ */
916
+ export function loadLockSync(root, config = {}) {
917
+ if ((config.signer ?? null) !== null)
918
+ throw new Error("loadLockSync does not verify signatures");
919
+ return parseLock(readLockBytes(root), config, false);
920
+ }
921
+ /** The identity an alias names, if the lock maps it. */
922
+ export function resolveAlias(lock, alias) {
923
+ return lock.aliases.get(alias) ?? null;
924
+ }
925
+ function ordered(entries) {
926
+ return { entries };
927
+ }
928
+ function isOrdered(v) {
929
+ return v !== null && typeof v === "object" && !Array.isArray(v);
930
+ }
931
+ /** Python json.dumps string form with ensure_ascii=True. */
932
+ function pyString(text) {
933
+ if (hasLoneSurrogate(text))
934
+ throw new Error("unpaired surrogate in lock text");
935
+ let out = '"';
936
+ for (let i = 0; i < text.length; i++) {
937
+ const c = text.charCodeAt(i);
938
+ const ch = text[i];
939
+ if (ch === '"')
940
+ out += '\\"';
941
+ else if (ch === "\\")
942
+ out += "\\\\";
943
+ else if (ch === "\n")
944
+ out += "\\n";
945
+ else if (ch === "\r")
946
+ out += "\\r";
947
+ else if (ch === "\t")
948
+ out += "\\t";
949
+ else if (ch === "\b")
950
+ out += "\\b";
951
+ else if (ch === "\f")
952
+ out += "\\f";
953
+ else if (c < 0x20 || c > 0x7e)
954
+ out += "\\u" + c.toString(16).padStart(4, "0");
955
+ else
956
+ out += ch;
957
+ }
958
+ return out + '"';
959
+ }
960
+ function pyDump(value, indent) {
961
+ if (value === null)
962
+ return "null";
963
+ if (value === true)
964
+ return "true";
965
+ if (value === false)
966
+ return "false";
967
+ if (typeof value === "number") {
968
+ if (!Number.isSafeInteger(value))
969
+ throw new Error("lock files hold integers only");
970
+ return String(value);
971
+ }
972
+ if (typeof value === "string")
973
+ return pyString(value);
974
+ const inner = indent + " ";
975
+ if (Array.isArray(value)) {
976
+ const arr = value;
977
+ if (arr.length === 0)
978
+ return "[]";
979
+ return "[\n" + arr.map((v) => inner + pyDump(v, inner)).join(",\n") + "\n" + indent + "]";
980
+ }
981
+ if (!isOrdered(value))
982
+ throw new Error("unsupported writer value");
983
+ if (value.entries.length === 0)
984
+ return "{}";
985
+ return ("{\n" +
986
+ value.entries.map(([k, v]) => inner + pyString(k) + ": " + pyDump(v, inner)).join(",\n") +
987
+ "\n" +
988
+ indent +
989
+ "}");
990
+ }
991
+ /** Bytes for a lock file: json.dumps(doc, indent=2) + "\n". */
992
+ export function formatLockFile(doc) {
993
+ return Buffer.from(pyDump(doc, "") + "\n", "utf8");
994
+ }
995
+ function identityValue(identity) {
996
+ return ordered(identityFields(identity).map(([k, v]) => [k, typeof v === "string" ? v : [...v]]));
997
+ }
998
+ function attributesValue(a) {
999
+ const e = [];
1000
+ if (a.version !== undefined)
1001
+ e.push(["version", a.version]);
1002
+ if (a.audience !== undefined)
1003
+ e.push(["audience", a.audience]);
1004
+ if (a.command !== undefined)
1005
+ e.push(["command", a.command]);
1006
+ if (a.args !== undefined)
1007
+ e.push(["args", [...a.args]]);
1008
+ if (a.env !== undefined)
1009
+ e.push(["env", [...a.env]]);
1010
+ return ordered(e);
1011
+ }
1012
+ function approvalValue(a) {
1013
+ const e = [
1014
+ ["subject", a.subject],
1015
+ ["at", a.at],
1016
+ ["via", a.via],
1017
+ ];
1018
+ if (a.ref !== undefined)
1019
+ e.push(["ref", a.ref]);
1020
+ return ordered(e);
1021
+ }
1022
+ /**
1023
+ * Shard bytes. Member order is lockVersion, identity, attributes, instructions,
1024
+ * tools; tools keep the order of `shard.tools` (a writer appends a newly approved
1025
+ * name at the end), which is the order the Python generator writes.
1026
+ */
1027
+ export function formatShard(shard) {
1028
+ const e = [
1029
+ ["lockVersion", LOCK_VERSION],
1030
+ ["identity", identityValue(shard.identity)],
1031
+ ];
1032
+ if (shard.attributes !== null)
1033
+ e.push(["attributes", attributesValue(shard.attributes)]);
1034
+ if (shard.instructions !== null)
1035
+ e.push(["instructions", ordered([["digest", shard.instructions]])]);
1036
+ const tools = [];
1037
+ for (const [name, entry] of shard.tools) {
1038
+ tools.push([
1039
+ name,
1040
+ ordered([
1041
+ ["digest", entry.digest],
1042
+ ["contract", entry.contract],
1043
+ ["approval", approvalValue(entry.approval)],
1044
+ // After approval, where the reference generator writes it.
1045
+ ...(entry.metaDigest !== undefined ? [["metaDigest", entry.metaDigest]] : []),
1046
+ ]),
1047
+ ]);
1048
+ }
1049
+ e.push(["tools", ordered(tools)]);
1050
+ return formatLockFile(ordered(e));
1051
+ }
1052
+ export function formatAliases(aliases) {
1053
+ const e = [];
1054
+ for (const [name, identity] of aliases)
1055
+ e.push([name, identityValue(identity)]);
1056
+ return formatLockFile(ordered([
1057
+ ["lockVersion", LOCK_VERSION],
1058
+ ["aliases", ordered(e)],
1059
+ ]));
1060
+ }
1061
+ /** Manifest bytes over `files` (name to bytes), names sorted. */
1062
+ export function formatManifest(sequence, files) {
1063
+ const e = [];
1064
+ for (const name of [...files.keys()].sort(compareCodePoints)) {
1065
+ if (name === MANIFEST || name === SIGNATURE)
1066
+ continue;
1067
+ e.push([name, "sha256:" + sha256Hex(files.get(name))]);
1068
+ }
1069
+ return formatLockFile(ordered([
1070
+ ["lockVersion", LOCK_VERSION],
1071
+ ["sequence", sequence],
1072
+ ["files", ordered(e)],
1073
+ ]));
1074
+ }
1075
+ //# sourceMappingURL=lock.js.map