@codai/axiom-mcp 2.0.0 → 2.1.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 +57 -6
- package/dist/axm-lazy-CR-FmFVd.js +6255 -0
- package/dist/axm-lazy.js +81 -2
- package/dist/cli-main.js +2315 -264
- package/dist/dist-FFqnyzCe.js +6141 -0
- package/dist/gate-lazy.js +77 -1
- package/dist/http-lazy.js +15154 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +411 -21
- package/dist/lib-CqHwM4m_.js +3884 -0
- package/dist/migrate-lazy.js +6472 -0
- package/package.json +9 -7
- package/spec/codai-tools.json +65 -2
- package/spec/tools.json +324 -2
package/dist/cli-main.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import * as fs$1 from "node:fs/promises";
|
|
3
|
-
import fs, { access, constants, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import fs, { access, chmod, constants, lstat, mkdir, open, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
6
6
|
import { parseArgs, promisify } from "node:util";
|
|
7
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
-
import { realpath } from "node:fs";
|
|
7
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify } from "node:crypto";
|
|
8
|
+
import { createReadStream, realpath as realpath$1 } from "node:fs";
|
|
9
9
|
import childProcess, { spawn } from "node:child_process";
|
|
10
10
|
import { cpus, hostname } from "node:os";
|
|
11
11
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
|
-
import { performance } from "node:perf_hooks";
|
|
12
|
+
import { performance as performance$1 } from "node:perf_hooks";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
import process$1 from "node:process";
|
|
14
15
|
var __create = Object.create;
|
|
15
16
|
var __defProp = Object.defineProperty;
|
|
@@ -80,13 +81,13 @@ function canonicalize$1(value) {
|
|
|
80
81
|
}
|
|
81
82
|
return `{${parts.join(",")}}`;
|
|
82
83
|
}
|
|
83
|
-
const HEX64 = /^[0-9a-f]{64}$/;
|
|
84
|
+
const HEX64$1 = /^[0-9a-f]{64}$/;
|
|
84
85
|
const REF = /^sha256:([0-9a-f]{64})$/;
|
|
85
86
|
function sha256Hex(data) {
|
|
86
87
|
return createHash("sha256").update(data).digest("hex");
|
|
87
88
|
}
|
|
88
89
|
function digestRef(hex) {
|
|
89
|
-
if (!HEX64.test(hex)) throw new TypeError(`invalid sha256 hex: ${hex}`);
|
|
90
|
+
if (!HEX64$1.test(hex)) throw new TypeError(`invalid sha256 hex: ${hex}`);
|
|
90
91
|
return `sha256:${hex}`;
|
|
91
92
|
}
|
|
92
93
|
function parseDigestRef(ref) {
|
|
@@ -100,7 +101,200 @@ function canonicalHash(value) {
|
|
|
100
101
|
function canonicalDigestRef(value) {
|
|
101
102
|
return digestRef(canonicalHash(value));
|
|
102
103
|
}
|
|
103
|
-
new TextEncoder();
|
|
104
|
+
const utf8 = new TextEncoder();
|
|
105
|
+
function pae(payloadType, payload) {
|
|
106
|
+
const typeBytes = utf8.encode(payloadType);
|
|
107
|
+
const head = utf8.encode(`DSSEv1 ${typeBytes.length} ${payloadType} ${payload.length} `);
|
|
108
|
+
const out = new Uint8Array(head.length + payload.length);
|
|
109
|
+
out.set(head, 0);
|
|
110
|
+
out.set(payload, head.length);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
const AXIOM_MANIFEST_PAYLOAD_TYPE$1 = "application/vnd.axiom.manifest+json";
|
|
114
|
+
const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
115
|
+
const PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
116
|
+
function toBuffer(u8) {
|
|
117
|
+
return Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength);
|
|
118
|
+
}
|
|
119
|
+
function rawPublicKeyOf(key) {
|
|
120
|
+
const spki = key.export({
|
|
121
|
+
type: "spki",
|
|
122
|
+
format: "der"
|
|
123
|
+
});
|
|
124
|
+
return Buffer.from(spki.subarray(spki.length - 32));
|
|
125
|
+
}
|
|
126
|
+
function keyidFor(key) {
|
|
127
|
+
let raw;
|
|
128
|
+
if (typeof key === "string") raw = Buffer.from(key, "base64");
|
|
129
|
+
else if (key instanceof Uint8Array) raw = toBuffer(key);
|
|
130
|
+
else raw = rawPublicKeyOf(key.type === "private" ? createPublicKey(key) : key);
|
|
131
|
+
if (raw.length !== 32) throw new Error(`ed25519 public key must be 32 raw bytes`);
|
|
132
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
133
|
+
}
|
|
134
|
+
function publicKeyBase64(key) {
|
|
135
|
+
return rawPublicKeyOf(key.type === "private" ? createPublicKey(key) : key).toString("base64");
|
|
136
|
+
}
|
|
137
|
+
function privateKeyFrom(material) {
|
|
138
|
+
const text = material.trim();
|
|
139
|
+
if (text.includes("-----BEGIN")) {
|
|
140
|
+
const key = createPrivateKey(text);
|
|
141
|
+
assertEd25519(key);
|
|
142
|
+
return key;
|
|
143
|
+
}
|
|
144
|
+
let der;
|
|
145
|
+
try {
|
|
146
|
+
der = Buffer.from(text, "base64");
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error("signing key is not valid base64");
|
|
149
|
+
}
|
|
150
|
+
if (der.length === 0) throw new Error("signing key is empty");
|
|
151
|
+
if (der.length === 32) return createPrivateKey({
|
|
152
|
+
key: Buffer.concat([PKCS8_PREFIX, der]),
|
|
153
|
+
format: "der",
|
|
154
|
+
type: "pkcs8"
|
|
155
|
+
});
|
|
156
|
+
const key = createPrivateKey({
|
|
157
|
+
key: der,
|
|
158
|
+
format: "der",
|
|
159
|
+
type: "pkcs8"
|
|
160
|
+
});
|
|
161
|
+
assertEd25519(key);
|
|
162
|
+
return key;
|
|
163
|
+
}
|
|
164
|
+
function publicKeyFrom(material) {
|
|
165
|
+
const text = material.trim();
|
|
166
|
+
if (text.includes("-----BEGIN")) {
|
|
167
|
+
const key = createPublicKey(text);
|
|
168
|
+
assertEd25519(key);
|
|
169
|
+
return key;
|
|
170
|
+
}
|
|
171
|
+
const der = Buffer.from(text, "base64");
|
|
172
|
+
if (der.length === 32) return createPublicKey({
|
|
173
|
+
key: Buffer.concat([SPKI_PREFIX, der]),
|
|
174
|
+
format: "der",
|
|
175
|
+
type: "spki"
|
|
176
|
+
});
|
|
177
|
+
const key = createPublicKey({
|
|
178
|
+
key: der,
|
|
179
|
+
format: "der",
|
|
180
|
+
type: "spki"
|
|
181
|
+
});
|
|
182
|
+
assertEd25519(key);
|
|
183
|
+
return key;
|
|
184
|
+
}
|
|
185
|
+
function assertEd25519(key) {
|
|
186
|
+
if (key.asymmetricKeyType !== "ed25519") throw new Error(`expected an ed25519 key, got ${String(key.asymmetricKeyType)}`);
|
|
187
|
+
}
|
|
188
|
+
function generateKeyPair() {
|
|
189
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
190
|
+
return {
|
|
191
|
+
keyid: keyidFor(publicKey),
|
|
192
|
+
privateKey,
|
|
193
|
+
publicKey,
|
|
194
|
+
privateKeyBase64: Buffer.from(privateKey.export({
|
|
195
|
+
type: "pkcs8",
|
|
196
|
+
format: "der"
|
|
197
|
+
})).toString("base64"),
|
|
198
|
+
publicKeyBase64: publicKeyBase64(publicKey)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function signEnvelope(body, privateKey, keyid, payloadType = AXIOM_MANIFEST_PAYLOAD_TYPE$1) {
|
|
202
|
+
assertEd25519(privateKey);
|
|
203
|
+
const payloadBytes = new TextEncoder().encode(canonicalize$1(body));
|
|
204
|
+
const message = toBuffer(pae(payloadType, payloadBytes));
|
|
205
|
+
const sig = sign(null, message, privateKey);
|
|
206
|
+
return {
|
|
207
|
+
payloadType,
|
|
208
|
+
payload: toBuffer(payloadBytes).toString("base64"),
|
|
209
|
+
signatures: [{
|
|
210
|
+
keyid: keyid ?? keyidFor(privateKey),
|
|
211
|
+
sig: sig.toString("base64")
|
|
212
|
+
}]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function eqKeyid(a, b) {
|
|
216
|
+
if (a.length !== b.length) return false;
|
|
217
|
+
return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
218
|
+
}
|
|
219
|
+
function verifyEnvelope(env, trustedKeys, payloadType = AXIOM_MANIFEST_PAYLOAD_TYPE$1) {
|
|
220
|
+
if (env.payloadType !== payloadType) return {
|
|
221
|
+
ok: false,
|
|
222
|
+
keyids: [],
|
|
223
|
+
reason: "BAD_PAYLOAD_TYPE"
|
|
224
|
+
};
|
|
225
|
+
if (!Array.isArray(env.signatures) || env.signatures.length === 0) return {
|
|
226
|
+
ok: false,
|
|
227
|
+
keyids: [],
|
|
228
|
+
reason: "NO_SIGNATURES"
|
|
229
|
+
};
|
|
230
|
+
const payloadBytes = Buffer.from(env.payload, "base64");
|
|
231
|
+
const text = payloadBytes.toString("utf8");
|
|
232
|
+
let parsed;
|
|
233
|
+
try {
|
|
234
|
+
parsed = JSON.parse(text);
|
|
235
|
+
} catch {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
keyids: [],
|
|
239
|
+
reason: "BAD_PAYLOAD"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
let canon;
|
|
243
|
+
try {
|
|
244
|
+
canon = canonicalize$1(parsed);
|
|
245
|
+
} catch {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
keyids: [],
|
|
249
|
+
reason: "NOT_CANONICAL"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (canon !== text) return {
|
|
253
|
+
ok: false,
|
|
254
|
+
keyids: [],
|
|
255
|
+
reason: "NOT_CANONICAL"
|
|
256
|
+
};
|
|
257
|
+
const message = toBuffer(pae(env.payloadType, payloadBytes));
|
|
258
|
+
const keys = [];
|
|
259
|
+
for (const t of trustedKeys) {
|
|
260
|
+
if (t.alg !== "ed25519") continue;
|
|
261
|
+
try {
|
|
262
|
+
keys.push({
|
|
263
|
+
keyid: t.keyid,
|
|
264
|
+
key: publicKeyFrom(t.publicKey)
|
|
265
|
+
});
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
if (keys.length === 0) return {
|
|
269
|
+
ok: false,
|
|
270
|
+
keyids: [],
|
|
271
|
+
reason: "UNKNOWN_KEY"
|
|
272
|
+
};
|
|
273
|
+
const verified = /* @__PURE__ */ new Set();
|
|
274
|
+
let sawKnownKeyid = false;
|
|
275
|
+
for (const s of env.signatures) {
|
|
276
|
+
if (s.keyid !== void 0 && keys.some((k) => eqKeyid(k.keyid, s.keyid))) sawKnownKeyid = true;
|
|
277
|
+
let sig;
|
|
278
|
+
try {
|
|
279
|
+
sig = Buffer.from(s.sig, "base64");
|
|
280
|
+
} catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
for (const k of keys) try {
|
|
284
|
+
if (verify(null, message, k.key, sig)) verified.add(k.keyid);
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
if (verified.size === 0) return {
|
|
288
|
+
ok: false,
|
|
289
|
+
keyids: [],
|
|
290
|
+
reason: sawKnownKeyid ? "BAD_SIGNATURE" : "UNKNOWN_KEY"
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
keyids: [...verified].sort(),
|
|
295
|
+
payload: text
|
|
296
|
+
};
|
|
297
|
+
}
|
|
104
298
|
const IN_TOTO_STATEMENT_V1 = "https://in-toto.io/Statement/v1";
|
|
105
299
|
const SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1";
|
|
106
300
|
const AXIOM_BUILD_TYPE = "https://axiom.dev/build/plan@v2";
|
|
@@ -6025,6 +6219,12 @@ const ERROR_CODES = [
|
|
|
6025
6219
|
"ERR_PROVIDER_FAILED",
|
|
6026
6220
|
"ERR_GUARD_TIMEOUT",
|
|
6027
6221
|
"ERR_GUARD_OUTPUT",
|
|
6222
|
+
"ERR_SIGNATURE_MISSING",
|
|
6223
|
+
"ERR_SIGNATURE_INVALID",
|
|
6224
|
+
"ERR_ROLLBACK",
|
|
6225
|
+
"ERR_EMITTER_UNKNOWN",
|
|
6226
|
+
"ERR_TEMPLATE_UNKNOWN",
|
|
6227
|
+
"ERR_TEMPLATE_PARAMS",
|
|
6028
6228
|
"ERR_GIT_NOT_FOUND",
|
|
6029
6229
|
"ERR_GIT_NOT_REPO",
|
|
6030
6230
|
"ERR_GIT_DIRTY",
|
|
@@ -6032,6 +6232,9 @@ const ERROR_CODES = [
|
|
|
6032
6232
|
"ERR_GIT_BRANCH_INVALID",
|
|
6033
6233
|
"ERR_GIT_FAILED",
|
|
6034
6234
|
"ERR_REF_OFFLINE",
|
|
6235
|
+
"ERR_NET_DISABLED",
|
|
6236
|
+
"ERR_NET_DENIED",
|
|
6237
|
+
"ERR_NET_FAILED",
|
|
6035
6238
|
"ERR_NOT_CANONICAL",
|
|
6036
6239
|
"ERR_UNSUPPORTED_OP",
|
|
6037
6240
|
"ERR_INTERNAL"
|
|
@@ -6208,6 +6411,7 @@ const PlanSchema = object$1({
|
|
|
6208
6411
|
capabilities: array(CapabilitySchema).default([]),
|
|
6209
6412
|
artifacts: array(PlanArtifactSchema).min(1).max(2e3),
|
|
6210
6413
|
checks: array(CheckRefSchema).default([]),
|
|
6414
|
+
counter: int().nonnegative().optional(),
|
|
6211
6415
|
metadata: record(string(), json$1()).default({})
|
|
6212
6416
|
}).strict();
|
|
6213
6417
|
const ErrorCodeSchema = _enum(ERROR_CODES);
|
|
@@ -6335,7 +6539,8 @@ const ManifestBodySchema = object$1({
|
|
|
6335
6539
|
planDigest: DigestRefSchema,
|
|
6336
6540
|
artifacts: array(ManifestArtifactSchema).min(1).max(2e3),
|
|
6337
6541
|
checks: array(CheckRefSchema),
|
|
6338
|
-
toolchain: ToolchainSchema
|
|
6542
|
+
toolchain: ToolchainSchema,
|
|
6543
|
+
counter: int().nonnegative().optional()
|
|
6339
6544
|
}).strict().superRefine((m, ctx) => {
|
|
6340
6545
|
if (!isSortedUnique(m.artifacts.map((a) => a.path))) ctx.addIssue({
|
|
6341
6546
|
code: "custom",
|
|
@@ -6376,11 +6581,37 @@ const DsseEnvelopeSchema = object$1({
|
|
|
6376
6581
|
sig: base64()
|
|
6377
6582
|
}).strict())
|
|
6378
6583
|
}).strict();
|
|
6584
|
+
const ManifestSignatureSchema = object$1({
|
|
6585
|
+
payloadType: literal("application/vnd.axiom.manifest+json"),
|
|
6586
|
+
payload: base64(),
|
|
6587
|
+
signatures: array(object$1({
|
|
6588
|
+
keyid: string().optional(),
|
|
6589
|
+
sig: base64()
|
|
6590
|
+
}).strict()).min(1)
|
|
6591
|
+
}).strict();
|
|
6592
|
+
const TrustedKeySchema = object$1({
|
|
6593
|
+
keyid: string().regex(/^[0-9a-f]{64}$/),
|
|
6594
|
+
alg: literal("ed25519"),
|
|
6595
|
+
publicKey: base64(),
|
|
6596
|
+
name: string().min(1).max(200).optional(),
|
|
6597
|
+
notBefore: int().nonnegative().optional()
|
|
6598
|
+
}).strict();
|
|
6599
|
+
const TrustStoreSchema = object$1({
|
|
6600
|
+
version: literal(1),
|
|
6601
|
+
keys: array(TrustedKeySchema),
|
|
6602
|
+
minCounter: int().nonnegative().optional()
|
|
6603
|
+
}).strict();
|
|
6604
|
+
const TrustStateSchema = object$1({
|
|
6605
|
+
version: literal(1),
|
|
6606
|
+
lastCounter: int().nonnegative(),
|
|
6607
|
+
manifestDigest: DigestRefSchema.optional()
|
|
6608
|
+
}).strict();
|
|
6379
6609
|
const ManifestBundleSchema = object$1({
|
|
6380
6610
|
manifest: ManifestBodySchema,
|
|
6381
6611
|
manifestDigest: DigestRefSchema,
|
|
6382
6612
|
attestation: InTotoStatementLooseSchema.optional(),
|
|
6383
6613
|
envelope: DsseEnvelopeSchema.optional(),
|
|
6614
|
+
signatures: array(ManifestSignatureSchema).optional(),
|
|
6384
6615
|
blobs: record(DigestRefSchema, BlobSchema).default({})
|
|
6385
6616
|
}).strict().superRefine((b, ctx) => {
|
|
6386
6617
|
let total = 0;
|
|
@@ -6415,6 +6646,28 @@ const ProfileSchema = object$1({
|
|
|
6415
6646
|
limits: ProfileLimitsSchema.default({}),
|
|
6416
6647
|
facts: ProfileFactsSchema.prefault({})
|
|
6417
6648
|
}).strict();
|
|
6649
|
+
const RepoSnapshotEntryKindSchema = _enum(["file", "symlink"]);
|
|
6650
|
+
const RepoSnapshotBodySchema = object$1({
|
|
6651
|
+
files: array(object$1({
|
|
6652
|
+
path: RelPathSchema,
|
|
6653
|
+
bytes: int().nonnegative(),
|
|
6654
|
+
sha256: Sha256HexSchema.optional(),
|
|
6655
|
+
mode: ArtifactModeSchema,
|
|
6656
|
+
kind: RepoSnapshotEntryKindSchema
|
|
6657
|
+
}).strict()),
|
|
6658
|
+
truncated: boolean(),
|
|
6659
|
+
counts: object$1({
|
|
6660
|
+
files: int().nonnegative(),
|
|
6661
|
+
bytes: int().nonnegative()
|
|
6662
|
+
}).strict()
|
|
6663
|
+
}).strict();
|
|
6664
|
+
const RepoSnapshotSchema = object$1({
|
|
6665
|
+
apiVersion: ApiVersionSchema,
|
|
6666
|
+
kind: literal("RepoSnapshot"),
|
|
6667
|
+
root: object$1({ kind: literal("relative") }).strict(),
|
|
6668
|
+
snapshotDigest: DigestRefSchema,
|
|
6669
|
+
body: RepoSnapshotBodySchema
|
|
6670
|
+
}).strict();
|
|
6418
6671
|
var Diff = class {
|
|
6419
6672
|
diff(oldStr, newStr, options = {}) {
|
|
6420
6673
|
let callback;
|
|
@@ -7198,7 +7451,7 @@ async function resolveContent(bundle, artifact, casDir) {
|
|
|
7198
7451
|
});
|
|
7199
7452
|
return bytes;
|
|
7200
7453
|
}
|
|
7201
|
-
const realpathNative$2 = promisify(realpath.native);
|
|
7454
|
+
const realpathNative$2 = promisify(realpath$1.native);
|
|
7202
7455
|
function validateArtifactPaths(artifacts) {
|
|
7203
7456
|
const seen = /* @__PURE__ */ new Map();
|
|
7204
7457
|
for (const a of artifacts) {
|
|
@@ -9990,7 +10243,7 @@ function deriveManifestFacts(bundle) {
|
|
|
9990
10243
|
const ext = extOf(a.path);
|
|
9991
10244
|
byExt[ext] = (byExt[ext] ?? 0) + 1;
|
|
9992
10245
|
}
|
|
9993
|
-
const signed = bundle.envelope !== void 0 && bundle.envelope.signatures.length > 0;
|
|
10246
|
+
const signed = bundle.signatures !== void 0 && bundle.signatures.length > 0 || bundle.envelope !== void 0 && bundle.envelope.signatures.length > 0;
|
|
9994
10247
|
return {
|
|
9995
10248
|
artifactCount: bundle.manifest.artifacts.length,
|
|
9996
10249
|
totalBytes,
|
|
@@ -10164,6 +10417,292 @@ function parseJsonObject(bytes) {
|
|
|
10164
10417
|
return;
|
|
10165
10418
|
}
|
|
10166
10419
|
}
|
|
10420
|
+
const EXPRESSION_MAX_CHARS = 4096;
|
|
10421
|
+
const CEL_LIMITS = {
|
|
10422
|
+
maxDepth: 24,
|
|
10423
|
+
maxAstNodes: 2e3,
|
|
10424
|
+
maxListElements: 256,
|
|
10425
|
+
maxMapEntries: 256,
|
|
10426
|
+
maxCallArguments: 8
|
|
10427
|
+
};
|
|
10428
|
+
const CEL_ALLOWED_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
10429
|
+
"has",
|
|
10430
|
+
"all",
|
|
10431
|
+
"exists",
|
|
10432
|
+
"exists_one",
|
|
10433
|
+
"map",
|
|
10434
|
+
"filter",
|
|
10435
|
+
"size",
|
|
10436
|
+
"contains",
|
|
10437
|
+
"startsWith",
|
|
10438
|
+
"endsWith",
|
|
10439
|
+
"matches",
|
|
10440
|
+
"lowerAscii",
|
|
10441
|
+
"upperAscii",
|
|
10442
|
+
"trim",
|
|
10443
|
+
"split",
|
|
10444
|
+
"join",
|
|
10445
|
+
"indexOf",
|
|
10446
|
+
"lastIndexOf",
|
|
10447
|
+
"substring",
|
|
10448
|
+
"string",
|
|
10449
|
+
"int",
|
|
10450
|
+
"uint",
|
|
10451
|
+
"double",
|
|
10452
|
+
"bool",
|
|
10453
|
+
"bytes",
|
|
10454
|
+
"dyn",
|
|
10455
|
+
"type"
|
|
10456
|
+
]);
|
|
10457
|
+
const CelParams = object$1({
|
|
10458
|
+
expression: string().min(1).max(EXPRESSION_MAX_CHARS),
|
|
10459
|
+
message: string().max(2e3).optional(),
|
|
10460
|
+
severity: _enum([
|
|
10461
|
+
"error",
|
|
10462
|
+
"warn",
|
|
10463
|
+
"info"
|
|
10464
|
+
]).optional()
|
|
10465
|
+
}).strict();
|
|
10466
|
+
const PREDICATE$1 = "expr.cel";
|
|
10467
|
+
const UNSAFE_REGEX = /\(\?<?[=!]|\\[1-9]|\\k</;
|
|
10468
|
+
let envPromise;
|
|
10469
|
+
function celEnvironment() {
|
|
10470
|
+
if (envPromise === void 0) envPromise = import("./lib-CqHwM4m_.js").then((m) => {
|
|
10471
|
+
let env = new m.Environment({
|
|
10472
|
+
limits: CEL_LIMITS,
|
|
10473
|
+
unlistedVariablesAreDyn: false
|
|
10474
|
+
});
|
|
10475
|
+
env = env.registerVariable("manifest", "map");
|
|
10476
|
+
env = env.registerVariable("artifacts", "list");
|
|
10477
|
+
env = env.registerVariable("content", "map");
|
|
10478
|
+
env = env.registerVariable("repo", "map");
|
|
10479
|
+
return env;
|
|
10480
|
+
});
|
|
10481
|
+
return envPromise;
|
|
10482
|
+
}
|
|
10483
|
+
function isNode(v) {
|
|
10484
|
+
return typeof v === "object" && v !== null && "op" in v && "args" in v;
|
|
10485
|
+
}
|
|
10486
|
+
function children(n) {
|
|
10487
|
+
const a = n.args;
|
|
10488
|
+
switch (n.op) {
|
|
10489
|
+
case "value":
|
|
10490
|
+
case "id": return [];
|
|
10491
|
+
case ".":
|
|
10492
|
+
case ".?": return Array.isArray(a) && isNode(a[0]) ? [a[0]] : [];
|
|
10493
|
+
case "call": return Array.isArray(a) && Array.isArray(a[1]) ? a[1].filter(isNode) : [];
|
|
10494
|
+
case "rcall": {
|
|
10495
|
+
if (!Array.isArray(a)) return [];
|
|
10496
|
+
const out = [];
|
|
10497
|
+
if (isNode(a[1])) out.push(a[1]);
|
|
10498
|
+
if (Array.isArray(a[2])) out.push(...a[2].filter(isNode));
|
|
10499
|
+
return out;
|
|
10500
|
+
}
|
|
10501
|
+
case "map": return Array.isArray(a) ? a.flatMap((e) => Array.isArray(e) ? e.filter(isNode) : isNode(e) ? [e] : []) : [];
|
|
10502
|
+
default:
|
|
10503
|
+
if (isNode(a)) return [a];
|
|
10504
|
+
return Array.isArray(a) ? a.filter(isNode) : [];
|
|
10505
|
+
}
|
|
10506
|
+
}
|
|
10507
|
+
function analyzeAst(root) {
|
|
10508
|
+
const out = {
|
|
10509
|
+
deniedCalls: [],
|
|
10510
|
+
unsafeRegex: [],
|
|
10511
|
+
variables: /* @__PURE__ */ new Set()
|
|
10512
|
+
};
|
|
10513
|
+
const stack = [root];
|
|
10514
|
+
while (stack.length > 0) {
|
|
10515
|
+
const n = stack.pop();
|
|
10516
|
+
if (n === void 0) break;
|
|
10517
|
+
if (n.op === "id" && typeof n.args === "string") out.variables.add(n.args);
|
|
10518
|
+
if (n.op === "call" || n.op === "rcall") {
|
|
10519
|
+
const args = n.args;
|
|
10520
|
+
const name = typeof args[0] === "string" ? args[0] : "";
|
|
10521
|
+
if (!CEL_ALLOWED_FUNCTIONS.has(name)) out.deniedCalls.push(name);
|
|
10522
|
+
if (name === "matches") {
|
|
10523
|
+
const params = n.op === "rcall" ? args[2] : args[1].slice(1);
|
|
10524
|
+
const pat = Array.isArray(params) ? params[0] : void 0;
|
|
10525
|
+
if (!isNode(pat) || pat.op !== "value" || typeof pat.args !== "string") out.unsafeRegex.push("<non-literal pattern>");
|
|
10526
|
+
else if (UNSAFE_REGEX.test(pat.args)) out.unsafeRegex.push(pat.args);
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
const kids = children(n);
|
|
10530
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
10531
|
+
const k = kids[i];
|
|
10532
|
+
if (k !== void 0) stack.push(k);
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10535
|
+
return out;
|
|
10536
|
+
}
|
|
10537
|
+
function toCelValue(v) {
|
|
10538
|
+
if (typeof v === "number") return Number.isInteger(v) ? BigInt(v) : v;
|
|
10539
|
+
if (Array.isArray(v)) return v.map(toCelValue);
|
|
10540
|
+
if (v instanceof Uint8Array) return v;
|
|
10541
|
+
if (typeof v === "object" && v !== null) {
|
|
10542
|
+
const out = {};
|
|
10543
|
+
for (const [k, val] of Object.entries(v)) out[k] = toCelValue(val);
|
|
10544
|
+
return out;
|
|
10545
|
+
}
|
|
10546
|
+
return v;
|
|
10547
|
+
}
|
|
10548
|
+
async function buildContentMap(ctx) {
|
|
10549
|
+
const out = {};
|
|
10550
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
10551
|
+
for (const a of ctx.manifest.artifacts) {
|
|
10552
|
+
if (a.op === "delete" || a.digest === void 0) continue;
|
|
10553
|
+
const bytes = await ctx.facts.content(a.path);
|
|
10554
|
+
if (bytes === void 0) continue;
|
|
10555
|
+
const entry = {
|
|
10556
|
+
bytes: BigInt(bytes.length),
|
|
10557
|
+
sha256: a.digest.sha256
|
|
10558
|
+
};
|
|
10559
|
+
if (bytes.length <= 262144 && isUtf8(bytes)) entry.text = decoder.decode(bytes);
|
|
10560
|
+
out[a.path] = entry;
|
|
10561
|
+
}
|
|
10562
|
+
return out;
|
|
10563
|
+
}
|
|
10564
|
+
async function buildRepoMap(ctx) {
|
|
10565
|
+
const repo = ctx.facts.repo;
|
|
10566
|
+
if (repo === void 0) return void 0;
|
|
10567
|
+
const exists = {};
|
|
10568
|
+
for (const a of ctx.manifest.artifacts) exists[a.path] = await repo.exists(a.path);
|
|
10569
|
+
const out = { exists };
|
|
10570
|
+
if (repo.packageJson !== void 0) out.packageJson = toCelValue(repo.packageJson);
|
|
10571
|
+
if (repo.gitHead !== void 0) out.gitHead = repo.gitHead;
|
|
10572
|
+
if (repo.gitDirty !== void 0) out.gitDirty = repo.gitDirty;
|
|
10573
|
+
return out;
|
|
10574
|
+
}
|
|
10575
|
+
function manifestActivation(manifest) {
|
|
10576
|
+
const m = toCelValue(manifest);
|
|
10577
|
+
return {
|
|
10578
|
+
manifest: m,
|
|
10579
|
+
artifacts: m.artifacts
|
|
10580
|
+
};
|
|
10581
|
+
}
|
|
10582
|
+
function errorMessage$1(e) {
|
|
10583
|
+
if (e instanceof Error) {
|
|
10584
|
+
const code = e.code;
|
|
10585
|
+
const first = e.message.split("\n")[0] ?? e.message;
|
|
10586
|
+
return typeof code === "string" ? `${code}: ${first}` : first;
|
|
10587
|
+
}
|
|
10588
|
+
return String(e);
|
|
10589
|
+
}
|
|
10590
|
+
async function evaluateCel(expression, activation, budgetMs = 100) {
|
|
10591
|
+
if (expression.length > 4096) return {
|
|
10592
|
+
kind: "error",
|
|
10593
|
+
code: "ERR_PREDICATE_PARAMS",
|
|
10594
|
+
message: `expression exceeds ${EXPRESSION_MAX_CHARS} characters`
|
|
10595
|
+
};
|
|
10596
|
+
const env = await celEnvironment();
|
|
10597
|
+
let compiled;
|
|
10598
|
+
try {
|
|
10599
|
+
compiled = env.parse(expression);
|
|
10600
|
+
} catch (e) {
|
|
10601
|
+
return {
|
|
10602
|
+
kind: "error",
|
|
10603
|
+
code: "ERR_PREDICATE_PARAMS",
|
|
10604
|
+
message: `parse: ${errorMessage$1(e)}`
|
|
10605
|
+
};
|
|
10606
|
+
}
|
|
10607
|
+
const analysis = analyzeAst(compiled.ast);
|
|
10608
|
+
if (analysis.deniedCalls.length > 0) return {
|
|
10609
|
+
kind: "error",
|
|
10610
|
+
code: "ERR_PREDICATE_PARAMS",
|
|
10611
|
+
message: `function not allowed: ${[...new Set(analysis.deniedCalls)].join(", ")}`
|
|
10612
|
+
};
|
|
10613
|
+
if (analysis.unsafeRegex.length > 0) return {
|
|
10614
|
+
kind: "error",
|
|
10615
|
+
code: "ERR_PREDICATE_PARAMS",
|
|
10616
|
+
message: `matches() pattern must be a literal RE2-safe regex: ${analysis.unsafeRegex.join(", ")}`
|
|
10617
|
+
};
|
|
10618
|
+
if (analysis.variables.has("repo") && activation.repo === void 0) return {
|
|
10619
|
+
kind: "error",
|
|
10620
|
+
code: "ERR_PROVIDER_FAILED",
|
|
10621
|
+
message: "expression references `repo` but repo facts are unavailable (no authorised root)"
|
|
10622
|
+
};
|
|
10623
|
+
const context = {
|
|
10624
|
+
manifest: activation.manifest,
|
|
10625
|
+
artifacts: activation.artifacts,
|
|
10626
|
+
content: activation.content
|
|
10627
|
+
};
|
|
10628
|
+
if (activation.repo !== void 0) context.repo = activation.repo;
|
|
10629
|
+
const t0 = performance.now();
|
|
10630
|
+
let value;
|
|
10631
|
+
try {
|
|
10632
|
+
value = compiled(context);
|
|
10633
|
+
} catch (e) {
|
|
10634
|
+
return {
|
|
10635
|
+
kind: "error",
|
|
10636
|
+
code: "ERR_PROVIDER_FAILED",
|
|
10637
|
+
message: `eval: ${errorMessage$1(e)}`
|
|
10638
|
+
};
|
|
10639
|
+
}
|
|
10640
|
+
const ms = performance.now() - t0;
|
|
10641
|
+
if (ms > budgetMs) return {
|
|
10642
|
+
kind: "error",
|
|
10643
|
+
code: "ERR_PROVIDER_FAILED",
|
|
10644
|
+
message: `evaluation took ${Math.round(ms)} ms (budget ${budgetMs} ms)`
|
|
10645
|
+
};
|
|
10646
|
+
if (typeof value !== "boolean") return {
|
|
10647
|
+
kind: "error",
|
|
10648
|
+
code: "ERR_PROVIDER_FAILED",
|
|
10649
|
+
message: `expression must evaluate to bool, got ${celTypeName(value)}`
|
|
10650
|
+
};
|
|
10651
|
+
return {
|
|
10652
|
+
kind: "value",
|
|
10653
|
+
value,
|
|
10654
|
+
ms
|
|
10655
|
+
};
|
|
10656
|
+
}
|
|
10657
|
+
function celTypeName(v) {
|
|
10658
|
+
if (v === null) return "null";
|
|
10659
|
+
if (typeof v === "bigint") return "int";
|
|
10660
|
+
if (typeof v === "number") return "double";
|
|
10661
|
+
if (Array.isArray(v)) return "list";
|
|
10662
|
+
if (v instanceof Uint8Array) return "bytes";
|
|
10663
|
+
return typeof v;
|
|
10664
|
+
}
|
|
10665
|
+
function providerError$2(code, message, expression) {
|
|
10666
|
+
return finding({
|
|
10667
|
+
id: PREDICATE$1,
|
|
10668
|
+
predicate: PREDICATE$1,
|
|
10669
|
+
message,
|
|
10670
|
+
facts: {
|
|
10671
|
+
code,
|
|
10672
|
+
__provider: true,
|
|
10673
|
+
expression
|
|
10674
|
+
}
|
|
10675
|
+
});
|
|
10676
|
+
}
|
|
10677
|
+
const exprCel = definePredicate({
|
|
10678
|
+
id: PREDICATE$1,
|
|
10679
|
+
params: CelParams,
|
|
10680
|
+
requires: ["manifest", "content"],
|
|
10681
|
+
async run(ctx, { expression, message, severity }) {
|
|
10682
|
+
const { manifest, artifacts } = manifestActivation(ctx.manifest);
|
|
10683
|
+
const mentions = (name) => new RegExp(`\\b${name}\\b`).test(expression);
|
|
10684
|
+
const activation = {
|
|
10685
|
+
manifest,
|
|
10686
|
+
artifacts,
|
|
10687
|
+
content: mentions("content") ? await buildContentMap(ctx) : {}
|
|
10688
|
+
};
|
|
10689
|
+
if (mentions("repo")) {
|
|
10690
|
+
const repo = await buildRepoMap(ctx);
|
|
10691
|
+
if (repo !== void 0) activation.repo = repo;
|
|
10692
|
+
}
|
|
10693
|
+
const r = await evaluateCel(expression, activation);
|
|
10694
|
+
if (r.kind === "error") return [providerError$2(r.code, r.message, expression)];
|
|
10695
|
+
if (r.value) return [];
|
|
10696
|
+
const f = finding({
|
|
10697
|
+
id: PREDICATE$1,
|
|
10698
|
+
predicate: PREDICATE$1,
|
|
10699
|
+
message: message ?? `expression evaluated to false: ${expression}`,
|
|
10700
|
+
facts: { expression }
|
|
10701
|
+
});
|
|
10702
|
+
if (severity !== void 0) f.severity = severity;
|
|
10703
|
+
return [f];
|
|
10704
|
+
}
|
|
10705
|
+
});
|
|
10167
10706
|
const SECRET_PATTERNS = [
|
|
10168
10707
|
{
|
|
10169
10708
|
name: "cnp",
|
|
@@ -10390,7 +10929,7 @@ const GuardOutputSchema = object$1({
|
|
|
10390
10929
|
const PREDICATE = "guard.external";
|
|
10391
10930
|
const OUTPUT_MAX = 8388608;
|
|
10392
10931
|
const IS_WIN32$1 = process.platform === "win32";
|
|
10393
|
-
const realpathNative$1 = promisify(realpath.native);
|
|
10932
|
+
const realpathNative$1 = promisify(realpath$1.native);
|
|
10394
10933
|
const ENV_WHITELIST = [
|
|
10395
10934
|
"PATH",
|
|
10396
10935
|
"HOME",
|
|
@@ -10423,7 +10962,7 @@ function isInside(parent, child) {
|
|
|
10423
10962
|
const c = norm$1(child);
|
|
10424
10963
|
return c !== p && c.startsWith(p + path.sep);
|
|
10425
10964
|
}
|
|
10426
|
-
function providerError(code, message, extra = {}) {
|
|
10965
|
+
function providerError$1(code, message, extra = {}) {
|
|
10427
10966
|
return finding({
|
|
10428
10967
|
id: PREDICATE,
|
|
10429
10968
|
predicate: PREDICATE,
|
|
@@ -10449,22 +10988,22 @@ function guardEnv(extra, digest, root, base = process.env) {
|
|
|
10449
10988
|
}
|
|
10450
10989
|
async function resolveGuardCommand(command, args, root, allowlist) {
|
|
10451
10990
|
const segments = command.split(/[\\/]+/);
|
|
10452
|
-
if (segments.includes("..")) return providerError("ERR_PREDICATE_PARAMS", `guard command must not contain "..": ${command}`, { command });
|
|
10991
|
+
if (segments.includes("..")) return providerError$1("ERR_PREDICATE_PARAMS", `guard command must not contain "..": ${command}`, { command });
|
|
10453
10992
|
let real;
|
|
10454
10993
|
if (path.isAbsolute(command)) {
|
|
10455
10994
|
real = await realpathSafe(command);
|
|
10456
|
-
if (real === void 0) return providerError("ERR_PREDICATE_PARAMS", `guard command not found: ${command}`, { command });
|
|
10995
|
+
if (real === void 0) return providerError$1("ERR_PREDICATE_PARAMS", `guard command not found: ${command}`, { command });
|
|
10457
10996
|
const allowed = /* @__PURE__ */ new Set();
|
|
10458
10997
|
for (const a of allowlist) {
|
|
10459
10998
|
const r = await realpathSafe(a);
|
|
10460
10999
|
if (r !== void 0) allowed.add(norm$1(r));
|
|
10461
11000
|
}
|
|
10462
|
-
if (!allowed.has(norm$1(real))) return providerError("ERR_PREDICATE_PARAMS", `absolute guard command is not in --guard-allowlist: ${command}`, { command });
|
|
11001
|
+
if (!allowed.has(norm$1(real))) return providerError$1("ERR_PREDICATE_PARAMS", `absolute guard command is not in --guard-allowlist: ${command}`, { command });
|
|
10463
11002
|
} else {
|
|
10464
11003
|
const scriptsDir = path.join(root, "scripts");
|
|
10465
11004
|
real = await realpathSafe(segments[0] === "scripts" ? path.resolve(root, command) : path.resolve(scriptsDir, command));
|
|
10466
11005
|
const scriptsReal = await realpathSafe(scriptsDir);
|
|
10467
|
-
if (real === void 0 || scriptsReal === void 0 || !isInside(scriptsReal, real)) return providerError("ERR_PREDICATE_PARAMS", `relative guard command must resolve inside <root>/scripts/: ${command}`, { command });
|
|
11006
|
+
if (real === void 0 || scriptsReal === void 0 || !isInside(scriptsReal, real)) return providerError$1("ERR_PREDICATE_PARAMS", `relative guard command must resolve inside <root>/scripts/: ${command}`, { command });
|
|
10468
11007
|
}
|
|
10469
11008
|
const ext = path.extname(real).toLowerCase();
|
|
10470
11009
|
if (ext === ".mjs" || ext === ".js" || ext === ".cjs") return {
|
|
@@ -10482,7 +11021,7 @@ async function resolveGuardCommand(command, args, root, allowlist) {
|
|
|
10482
11021
|
...args
|
|
10483
11022
|
]
|
|
10484
11023
|
};
|
|
10485
|
-
if (!path.isAbsolute(command)) return providerError("ERR_PREDICATE_PARAMS", `relative guard command must be .mjs/.js/.cjs/.ps1: ${command}`, { command });
|
|
11024
|
+
if (!path.isAbsolute(command)) return providerError$1("ERR_PREDICATE_PARAMS", `relative guard command must be .mjs/.js/.cjs/.ps1: ${command}`, { command });
|
|
10486
11025
|
return {
|
|
10487
11026
|
file: real,
|
|
10488
11027
|
argv: [...args]
|
|
@@ -10620,25 +11159,25 @@ function mapOutput(out, command) {
|
|
|
10620
11159
|
}
|
|
10621
11160
|
async function runGuard(ctx, params) {
|
|
10622
11161
|
const guard = ctx.facts.guard;
|
|
10623
|
-
if (guard === void 0 || !guard.enabled || !ctx.facts.profile.allowGuards) return [providerError("ERR_UNSUPPORTED_OP", "external guards disabled (needs profile facts.allowGuards and --allow-guards)", { command: params.command })];
|
|
11162
|
+
if (guard === void 0 || !guard.enabled || !ctx.facts.profile.allowGuards) return [providerError$1("ERR_UNSUPPORTED_OP", "external guards disabled (needs profile facts.allowGuards and --allow-guards)", { command: params.command })];
|
|
10624
11163
|
const resolved = await resolveGuardCommand(params.command, params.args, guard.root, guard.allowlist);
|
|
10625
11164
|
if ("id" in resolved) return [resolved];
|
|
10626
11165
|
let cwd = guard.root;
|
|
10627
11166
|
if (params.cwd === "staging") {
|
|
10628
|
-
if (guard.stagingDir === void 0) return [providerError("ERR_PREDICATE_PARAMS", "cwd \"staging\" requested but no staging dir", { command: params.command })];
|
|
11167
|
+
if (guard.stagingDir === void 0) return [providerError$1("ERR_PREDICATE_PARAMS", "cwd \"staging\" requested but no staging dir", { command: params.command })];
|
|
10629
11168
|
cwd = guard.stagingDir;
|
|
10630
11169
|
}
|
|
10631
11170
|
const input = params.stdin === "none" ? void 0 : canonicalize$1(params.stdin === "bundle" ? ctx.bundle : ctx.manifest);
|
|
10632
11171
|
const env = guardEnv(params.env, ctx.bundle.manifestDigest, guard.root);
|
|
10633
11172
|
const r = await runChild(resolved, cwd, env, input, params.timeoutMs);
|
|
10634
|
-
if (r.timedOut) return [providerError("ERR_GUARD_TIMEOUT", `guard timed out after ${params.timeoutMs} ms`, {
|
|
11173
|
+
if (r.timedOut) return [providerError$1("ERR_GUARD_TIMEOUT", `guard timed out after ${params.timeoutMs} ms`, {
|
|
10635
11174
|
command: params.command,
|
|
10636
11175
|
timeoutMs: params.timeoutMs,
|
|
10637
11176
|
stderr: r.stderr
|
|
10638
11177
|
})];
|
|
10639
|
-
if (r.spawnError !== void 0) return [providerError("ERR_GUARD_OUTPUT", `guard could not be spawned: ${r.spawnError}`, { command: params.command })];
|
|
11178
|
+
if (r.spawnError !== void 0) return [providerError$1("ERR_GUARD_OUTPUT", `guard could not be spawned: ${r.spawnError}`, { command: params.command })];
|
|
10640
11179
|
const out = parseJsonOutput(r.stdout) ?? (params.legacyText ? parseLegacyText(r.stdout) : void 0);
|
|
10641
|
-
if (out === void 0) return [providerError("ERR_GUARD_OUTPUT", `guard stdout is not a GuardOutput JSON object (exit ${r.code ?? "null"})`, {
|
|
11180
|
+
if (out === void 0) return [providerError$1("ERR_GUARD_OUTPUT", `guard stdout is not a GuardOutput JSON object (exit ${r.code ?? "null"})`, {
|
|
10642
11181
|
command: params.command,
|
|
10643
11182
|
exitCode: r.code,
|
|
10644
11183
|
stdout: r.stdout.slice(-4096),
|
|
@@ -10690,20 +11229,6 @@ const manifestMaxTotalBytes = definePredicate({
|
|
|
10690
11229
|
})];
|
|
10691
11230
|
}
|
|
10692
11231
|
});
|
|
10693
|
-
const manifestRequireSigned = definePredicate({
|
|
10694
|
-
id: "manifest.requireSigned",
|
|
10695
|
-
params: Empty,
|
|
10696
|
-
requires: ["manifest"],
|
|
10697
|
-
async run(ctx) {
|
|
10698
|
-
if (ctx.facts.manifest.signed) return [];
|
|
10699
|
-
return [finding({
|
|
10700
|
-
id: "manifest.requireSigned",
|
|
10701
|
-
predicate: "manifest.requireSigned",
|
|
10702
|
-
message: "bundle has no DSSE signature",
|
|
10703
|
-
facts: { signed: false }
|
|
10704
|
-
})];
|
|
10705
|
-
}
|
|
10706
|
-
});
|
|
10707
11232
|
const manifestNoDeletes = definePredicate({
|
|
10708
11233
|
id: "manifest.noDeletes",
|
|
10709
11234
|
params: Empty,
|
|
@@ -10718,59 +11243,283 @@ const manifestNoDeletes = definePredicate({
|
|
|
10718
11243
|
}
|
|
10719
11244
|
});
|
|
10720
11245
|
const Globs = object$1({ globs: array(string().min(1)).min(1) }).strict();
|
|
10721
|
-
const
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
11246
|
+
const pathAllow = definePredicate({
|
|
11247
|
+
id: "path.allow",
|
|
11248
|
+
params: Globs,
|
|
11249
|
+
requires: ["manifest"],
|
|
11250
|
+
async run(ctx, { globs }) {
|
|
11251
|
+
const allowed = globMatcher(globs, false);
|
|
11252
|
+
return ctx.facts.manifest.paths.filter((p) => !allowed(p)).map((p) => finding({
|
|
11253
|
+
id: "path.allow",
|
|
11254
|
+
predicate: "path.allow",
|
|
11255
|
+
path: p,
|
|
11256
|
+
message: `path is outside the allowed globs`,
|
|
11257
|
+
facts: { globs }
|
|
11258
|
+
}));
|
|
11259
|
+
}
|
|
11260
|
+
});
|
|
11261
|
+
const pathDeny = definePredicate({
|
|
11262
|
+
id: "path.deny",
|
|
11263
|
+
params: Globs,
|
|
11264
|
+
requires: ["manifest"],
|
|
11265
|
+
async run(ctx, { globs }) {
|
|
11266
|
+
const denied = globMatcher(globs, false);
|
|
11267
|
+
return ctx.facts.manifest.paths.filter(denied).map((p) => finding({
|
|
11268
|
+
id: "path.deny",
|
|
11269
|
+
predicate: "path.deny",
|
|
11270
|
+
path: p,
|
|
11271
|
+
message: `path matches a denied glob`,
|
|
11272
|
+
facts: { globs }
|
|
11273
|
+
}));
|
|
11274
|
+
}
|
|
11275
|
+
});
|
|
11276
|
+
const pathReservedNames = definePredicate({
|
|
11277
|
+
id: "path.reservedNames",
|
|
11278
|
+
params: object$1({}).strict(),
|
|
11279
|
+
requires: ["manifest"],
|
|
11280
|
+
async run(ctx) {
|
|
11281
|
+
const out = [];
|
|
11282
|
+
for (const p of ctx.facts.manifest.paths) {
|
|
11283
|
+
const issues = relPathIssues(p);
|
|
11284
|
+
if (issues.length === 0) continue;
|
|
11285
|
+
out.push(finding({
|
|
11286
|
+
id: "path.reservedNames",
|
|
11287
|
+
predicate: "path.reservedNames",
|
|
11288
|
+
message: `path violates relative-POSIX rules: ${issues.join(", ")}`,
|
|
11289
|
+
facts: {
|
|
11290
|
+
path: p,
|
|
11291
|
+
issues
|
|
11292
|
+
}
|
|
10734
11293
|
}));
|
|
10735
11294
|
}
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
11295
|
+
return out;
|
|
11296
|
+
}
|
|
11297
|
+
});
|
|
11298
|
+
const repoNoOverwriteOf = definePredicate({
|
|
11299
|
+
id: "repo.noOverwriteOf",
|
|
11300
|
+
params: object$1({ globs: array(string().min(1)).min(1) }).strict(),
|
|
11301
|
+
requires: ["manifest", "repo"],
|
|
11302
|
+
async run(ctx, { globs }) {
|
|
11303
|
+
const repo = ctx.facts.repo;
|
|
11304
|
+
if (repo === void 0) return [];
|
|
11305
|
+
const protectedPath = globMatcher(globs, false);
|
|
11306
|
+
const out = [];
|
|
11307
|
+
for (const a of ctx.manifest.artifacts) {
|
|
11308
|
+
if (a.op === "create" || !protectedPath(a.path)) continue;
|
|
11309
|
+
if (!await repo.exists(a.path)) continue;
|
|
11310
|
+
out.push(finding({
|
|
11311
|
+
id: "repo.noOverwriteOf",
|
|
11312
|
+
predicate: "repo.noOverwriteOf",
|
|
11313
|
+
path: a.path,
|
|
11314
|
+
message: `${a.op} of protected existing file`,
|
|
11315
|
+
facts: {
|
|
11316
|
+
op: a.op,
|
|
11317
|
+
globs
|
|
11318
|
+
}
|
|
10749
11319
|
}));
|
|
10750
11320
|
}
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
11321
|
+
return out;
|
|
11322
|
+
}
|
|
11323
|
+
});
|
|
11324
|
+
const repoRequireCompanion = definePredicate({
|
|
11325
|
+
id: "repo.requireCompanion",
|
|
11326
|
+
params: object$1({ rules: array(object$1({
|
|
11327
|
+
when: string().min(1),
|
|
11328
|
+
expect: array(object$1({
|
|
11329
|
+
name: string().min(1),
|
|
11330
|
+
match: string().min(1)
|
|
11331
|
+
}).strict()).min(1)
|
|
11332
|
+
}).strict()).min(1) }).strict(),
|
|
11333
|
+
requires: ["manifest", "repo"],
|
|
11334
|
+
async run(ctx, { rules }) {
|
|
11335
|
+
const paths = ctx.facts.manifest.paths;
|
|
11336
|
+
const out = [];
|
|
11337
|
+
for (const rule of rules) {
|
|
11338
|
+
const when = globMatcher([rule.when], false);
|
|
11339
|
+
const triggers = paths.filter(when);
|
|
11340
|
+
if (triggers.length === 0) continue;
|
|
11341
|
+
for (const exp of rule.expect) {
|
|
11342
|
+
const match = globMatcher([exp.match], false);
|
|
11343
|
+
if (paths.some(match)) continue;
|
|
11344
|
+
if ((ctx.facts.repo ? await ctx.facts.repo.glob(exp.match) : []).length > 0) continue;
|
|
10761
11345
|
out.push(finding({
|
|
10762
|
-
id:
|
|
10763
|
-
predicate: "
|
|
10764
|
-
message: `
|
|
11346
|
+
id: `repo.requireCompanion.${exp.name}`,
|
|
11347
|
+
predicate: "repo.requireCompanion",
|
|
11348
|
+
message: `"${rule.when}" changed but no companion matches "${exp.match}" (${exp.name})`,
|
|
10765
11349
|
facts: {
|
|
10766
|
-
|
|
10767
|
-
|
|
11350
|
+
when: rule.when,
|
|
11351
|
+
expect: exp.match,
|
|
11352
|
+
name: exp.name,
|
|
11353
|
+
triggers
|
|
10768
11354
|
}
|
|
10769
11355
|
}));
|
|
10770
11356
|
}
|
|
10771
|
-
return out;
|
|
10772
11357
|
}
|
|
10773
|
-
|
|
11358
|
+
return out;
|
|
11359
|
+
}
|
|
11360
|
+
});
|
|
11361
|
+
const PREDICATE_ID = "manifest.requireSigned";
|
|
11362
|
+
const TRUST_FILE_DEFAULT = ".axiom/trust/keys.json";
|
|
11363
|
+
const TRUST_STATE_FILE = ".axiom/trust/state.json";
|
|
11364
|
+
const TRUST_FILE_MAX = 262144;
|
|
11365
|
+
const RequireSignedParams = object$1({
|
|
11366
|
+
minSignatures: int().min(1).max(16).default(1),
|
|
11367
|
+
antiRollback: boolean().default(false),
|
|
11368
|
+
trustFile: string().min(1).default(TRUST_FILE_DEFAULT)
|
|
11369
|
+
}).strict();
|
|
11370
|
+
function providerError(code, message, extra = {}) {
|
|
11371
|
+
return finding({
|
|
11372
|
+
id: PREDICATE_ID,
|
|
11373
|
+
predicate: PREDICATE_ID,
|
|
11374
|
+
message,
|
|
11375
|
+
facts: {
|
|
11376
|
+
code,
|
|
11377
|
+
__provider: true,
|
|
11378
|
+
...extra
|
|
11379
|
+
}
|
|
11380
|
+
});
|
|
11381
|
+
}
|
|
11382
|
+
function fail(id, message, facts = {}) {
|
|
11383
|
+
return finding({
|
|
11384
|
+
id,
|
|
11385
|
+
predicate: PREDICATE_ID,
|
|
11386
|
+
message,
|
|
11387
|
+
facts
|
|
11388
|
+
});
|
|
11389
|
+
}
|
|
11390
|
+
async function readJsonFile(ctx, rel) {
|
|
11391
|
+
const repo = ctx.facts.repo;
|
|
11392
|
+
if (repo === void 0) return { error: "no root" };
|
|
11393
|
+
let bytes;
|
|
11394
|
+
try {
|
|
11395
|
+
bytes = await repo.read(rel, TRUST_FILE_MAX);
|
|
11396
|
+
} catch (e) {
|
|
11397
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
11398
|
+
}
|
|
11399
|
+
if (bytes === void 0) return { missing: true };
|
|
11400
|
+
try {
|
|
11401
|
+
return { value: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) };
|
|
11402
|
+
} catch (e) {
|
|
11403
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
async function loadTrustStore$1(ctx, trustFile) {
|
|
11407
|
+
if (ctx.facts.repo === void 0) return { finding: providerError("ERR_PROVIDER_FAILED", "manifest.requireSigned needs a repo root to read the trust store") };
|
|
11408
|
+
const r = await readJsonFile(ctx, trustFile);
|
|
11409
|
+
if ("missing" in r) return { finding: providerError("ERR_NOT_FOUND", `trust store ${trustFile} not found`, { trustFile }) };
|
|
11410
|
+
if ("error" in r) return { finding: providerError("ERR_PROVIDER_FAILED", `trust store ${trustFile}: ${r.error}`, { trustFile }) };
|
|
11411
|
+
const parsed = TrustStoreSchema.safeParse(r.value);
|
|
11412
|
+
if (!parsed.success) return { finding: providerError("ERR_PROVIDER_FAILED", `trust store ${trustFile} is invalid`, {
|
|
11413
|
+
trustFile,
|
|
11414
|
+
issues: parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
11415
|
+
}) };
|
|
11416
|
+
return { store: parsed.data };
|
|
11417
|
+
}
|
|
11418
|
+
async function loadTrustState$1(ctx) {
|
|
11419
|
+
const r = await readJsonFile(ctx, TRUST_STATE_FILE);
|
|
11420
|
+
if ("missing" in r) return { state: void 0 };
|
|
11421
|
+
if ("error" in r) return { finding: providerError("ERR_PROVIDER_FAILED", `trust state ${TRUST_STATE_FILE}: ${r.error}`) };
|
|
11422
|
+
const parsed = TrustStateSchema.safeParse(r.value);
|
|
11423
|
+
if (!parsed.success) return { finding: providerError("ERR_JOURNAL_CORRUPT", `trust state ${TRUST_STATE_FILE} is invalid`) };
|
|
11424
|
+
return { state: parsed.data };
|
|
11425
|
+
}
|
|
11426
|
+
function verifyBundleSignatures(bundle, store, counter) {
|
|
11427
|
+
const findings = [];
|
|
11428
|
+
const envs = bundle.signatures ?? [];
|
|
11429
|
+
if (envs.length === 0) return {
|
|
11430
|
+
keyids: [],
|
|
11431
|
+
findings: [fail("signature.missing", "bundle carries no signatures")]
|
|
11432
|
+
};
|
|
11433
|
+
const keyids = /* @__PURE__ */ new Set();
|
|
11434
|
+
const eligible = store.keys.filter((k) => k.notBefore === void 0 || counter !== void 0 && counter >= k.notBefore);
|
|
11435
|
+
for (const [i, env] of envs.entries()) {
|
|
11436
|
+
const payloadDigest = `sha256:${sha256Hex(Buffer.from(env.payload, "base64"))}`;
|
|
11437
|
+
if (payloadDigest !== bundle.manifestDigest) {
|
|
11438
|
+
findings.push(fail("signature.bad", `signatures[${i}] signs a different manifest`, {
|
|
11439
|
+
reason: "PAYLOAD_MISMATCH",
|
|
11440
|
+
payloadDigest
|
|
11441
|
+
}));
|
|
11442
|
+
continue;
|
|
11443
|
+
}
|
|
11444
|
+
const r = verifyEnvelope(env, eligible);
|
|
11445
|
+
if (r.ok) {
|
|
11446
|
+
for (const k of r.keyids) keyids.add(k);
|
|
11447
|
+
continue;
|
|
11448
|
+
}
|
|
11449
|
+
const detail = {
|
|
11450
|
+
reason: r.reason,
|
|
11451
|
+
index: i
|
|
11452
|
+
};
|
|
11453
|
+
switch (r.reason) {
|
|
11454
|
+
case "NOT_CANONICAL":
|
|
11455
|
+
findings.push(fail("signature.notCanonical", `signatures[${i}] payload is not JCS-canonical`, detail));
|
|
11456
|
+
break;
|
|
11457
|
+
case "UNKNOWN_KEY":
|
|
11458
|
+
findings.push(fail("signature.unknownKey", `signatures[${i}] is not from a trusted key`, {
|
|
11459
|
+
...detail,
|
|
11460
|
+
hinted: env.signatures.map((s) => s.keyid ?? null)
|
|
11461
|
+
}));
|
|
11462
|
+
break;
|
|
11463
|
+
default: findings.push(fail("signature.bad", `signatures[${i}] failed verification`, detail));
|
|
11464
|
+
}
|
|
11465
|
+
}
|
|
11466
|
+
return {
|
|
11467
|
+
keyids: [...keyids].sort(),
|
|
11468
|
+
findings
|
|
11469
|
+
};
|
|
11470
|
+
}
|
|
11471
|
+
const manifestRequireSigned = definePredicate({
|
|
11472
|
+
id: PREDICATE_ID,
|
|
11473
|
+
params: RequireSignedParams,
|
|
11474
|
+
requires: ["manifest"],
|
|
11475
|
+
async run(ctx, params) {
|
|
11476
|
+
const loaded = await loadTrustStore$1(ctx, params.trustFile);
|
|
11477
|
+
if ("finding" in loaded) return [loaded.finding];
|
|
11478
|
+
const store = loaded.store;
|
|
11479
|
+
const counter = ctx.manifest.counter;
|
|
11480
|
+
const { keyids, findings } = verifyBundleSignatures(ctx.bundle, store, counter);
|
|
11481
|
+
const out = [];
|
|
11482
|
+
if (keyids.length < params.minSignatures) {
|
|
11483
|
+
out.push(...findings);
|
|
11484
|
+
if (findings.length === 0 || keyids.length > 0) out.push(fail("signature.missing", `need ${params.minSignatures} trusted signature(s), have ${keyids.length}`, {
|
|
11485
|
+
have: keyids,
|
|
11486
|
+
need: params.minSignatures
|
|
11487
|
+
}));
|
|
11488
|
+
return dedupe(out);
|
|
11489
|
+
}
|
|
11490
|
+
if (params.antiRollback) {
|
|
11491
|
+
if (counter === void 0) return [fail("signature.rollback", "antiRollback requires manifest.counter", { reason: "NO_COUNTER" })];
|
|
11492
|
+
const st = await loadTrustState$1(ctx);
|
|
11493
|
+
if ("finding" in st) return [st.finding];
|
|
11494
|
+
const last = st.state?.lastCounter;
|
|
11495
|
+
const floor = store.minCounter;
|
|
11496
|
+
if (last !== void 0 && counter <= last) return [fail("signature.rollback", `counter ${counter} is not greater than last ${last}`, {
|
|
11497
|
+
reason: "ROLLBACK",
|
|
11498
|
+
counter,
|
|
11499
|
+
lastCounter: last
|
|
11500
|
+
})];
|
|
11501
|
+
if (floor !== void 0 && counter < floor) return [fail("signature.rollback", `counter ${counter} is below trust minCounter ${floor}`, {
|
|
11502
|
+
reason: "BELOW_MIN",
|
|
11503
|
+
counter,
|
|
11504
|
+
minCounter: floor
|
|
11505
|
+
})];
|
|
11506
|
+
}
|
|
11507
|
+
return [];
|
|
11508
|
+
}
|
|
11509
|
+
});
|
|
11510
|
+
function dedupe(findings) {
|
|
11511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11512
|
+
return findings.filter((f) => {
|
|
11513
|
+
const k = `${f.id}|${f.message}`;
|
|
11514
|
+
if (seen.has(k)) return false;
|
|
11515
|
+
seen.add(k);
|
|
11516
|
+
return true;
|
|
11517
|
+
});
|
|
11518
|
+
}
|
|
11519
|
+
const BUILTIN_PREDICATES = [
|
|
11520
|
+
pathAllow,
|
|
11521
|
+
pathDeny,
|
|
11522
|
+
pathReservedNames,
|
|
10774
11523
|
contentNoSecrets,
|
|
10775
11524
|
contentMaxBytes,
|
|
10776
11525
|
contentEncodingUtf8,
|
|
@@ -10780,70 +11529,10 @@ const BUILTIN_PREDICATES = [
|
|
|
10780
11529
|
manifestNoDeletes,
|
|
10781
11530
|
depsMax,
|
|
10782
11531
|
depsDeny,
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
async run(ctx, { globs }) {
|
|
10788
|
-
const repo = ctx.facts.repo;
|
|
10789
|
-
if (repo === void 0) return [];
|
|
10790
|
-
const protectedPath = globMatcher(globs, false);
|
|
10791
|
-
const out = [];
|
|
10792
|
-
for (const a of ctx.manifest.artifacts) {
|
|
10793
|
-
if (a.op === "create" || !protectedPath(a.path)) continue;
|
|
10794
|
-
if (!await repo.exists(a.path)) continue;
|
|
10795
|
-
out.push(finding({
|
|
10796
|
-
id: "repo.noOverwriteOf",
|
|
10797
|
-
predicate: "repo.noOverwriteOf",
|
|
10798
|
-
path: a.path,
|
|
10799
|
-
message: `${a.op} of protected existing file`,
|
|
10800
|
-
facts: {
|
|
10801
|
-
op: a.op,
|
|
10802
|
-
globs
|
|
10803
|
-
}
|
|
10804
|
-
}));
|
|
10805
|
-
}
|
|
10806
|
-
return out;
|
|
10807
|
-
}
|
|
10808
|
-
}),
|
|
10809
|
-
definePredicate({
|
|
10810
|
-
id: "repo.requireCompanion",
|
|
10811
|
-
params: object$1({ rules: array(object$1({
|
|
10812
|
-
when: string().min(1),
|
|
10813
|
-
expect: array(object$1({
|
|
10814
|
-
name: string().min(1),
|
|
10815
|
-
match: string().min(1)
|
|
10816
|
-
}).strict()).min(1)
|
|
10817
|
-
}).strict()).min(1) }).strict(),
|
|
10818
|
-
requires: ["manifest", "repo"],
|
|
10819
|
-
async run(ctx, { rules }) {
|
|
10820
|
-
const paths = ctx.facts.manifest.paths;
|
|
10821
|
-
const out = [];
|
|
10822
|
-
for (const rule of rules) {
|
|
10823
|
-
const when = globMatcher([rule.when], false);
|
|
10824
|
-
const triggers = paths.filter(when);
|
|
10825
|
-
if (triggers.length === 0) continue;
|
|
10826
|
-
for (const exp of rule.expect) {
|
|
10827
|
-
const match = globMatcher([exp.match], false);
|
|
10828
|
-
if (paths.some(match)) continue;
|
|
10829
|
-
if ((ctx.facts.repo ? await ctx.facts.repo.glob(exp.match) : []).length > 0) continue;
|
|
10830
|
-
out.push(finding({
|
|
10831
|
-
id: `repo.requireCompanion.${exp.name}`,
|
|
10832
|
-
predicate: "repo.requireCompanion",
|
|
10833
|
-
message: `"${rule.when}" changed but no companion matches "${exp.match}" (${exp.name})`,
|
|
10834
|
-
facts: {
|
|
10835
|
-
when: rule.when,
|
|
10836
|
-
expect: exp.match,
|
|
10837
|
-
name: exp.name,
|
|
10838
|
-
triggers
|
|
10839
|
-
}
|
|
10840
|
-
}));
|
|
10841
|
-
}
|
|
10842
|
-
}
|
|
10843
|
-
return out;
|
|
10844
|
-
}
|
|
10845
|
-
}),
|
|
10846
|
-
guardExternal
|
|
11532
|
+
repoNoOverwriteOf,
|
|
11533
|
+
repoRequireCompanion,
|
|
11534
|
+
guardExternal,
|
|
11535
|
+
exprCel
|
|
10847
11536
|
];
|
|
10848
11537
|
const MiB = 1048576;
|
|
10849
11538
|
const BUILTIN_PROFILE_INPUTS = {
|
|
@@ -11091,7 +11780,7 @@ function errorMessage(e) {
|
|
|
11091
11780
|
return e instanceof Error ? e.message : String(e);
|
|
11092
11781
|
}
|
|
11093
11782
|
function ms(start) {
|
|
11094
|
-
return Math.max(0, Math.round(performance.now() - start));
|
|
11783
|
+
return Math.max(0, Math.round(performance$1.now() - start));
|
|
11095
11784
|
}
|
|
11096
11785
|
const GUARD_POOL_SIZE = Math.max(1, Math.min(4, cpus().length));
|
|
11097
11786
|
async function runPool(items, size, fn) {
|
|
@@ -11101,21 +11790,21 @@ async function runPool(items, size, fn) {
|
|
|
11101
11790
|
}));
|
|
11102
11791
|
}
|
|
11103
11792
|
async function runChecks(opts) {
|
|
11104
|
-
const t0 = performance.now();
|
|
11793
|
+
const t0 = performance$1.now();
|
|
11105
11794
|
const { bundle, profile } = opts;
|
|
11106
11795
|
const registry = opts.registry ?? builtinRegistry();
|
|
11107
11796
|
const checks = mergeChecks(profile.checks, opts.checks ?? []);
|
|
11108
11797
|
const providers = [];
|
|
11109
11798
|
const findings = [];
|
|
11110
11799
|
let providerFailed = false;
|
|
11111
|
-
const tm = performance.now();
|
|
11800
|
+
const tm = performance$1.now();
|
|
11112
11801
|
const manifestFacts = deriveManifestFacts(bundle);
|
|
11113
11802
|
providers.push({
|
|
11114
11803
|
name: "manifest",
|
|
11115
11804
|
status: "ok",
|
|
11116
11805
|
ms: ms(tm)
|
|
11117
11806
|
});
|
|
11118
|
-
const tc = performance.now();
|
|
11807
|
+
const tc = performance$1.now();
|
|
11119
11808
|
const content = contentReader(bundle, opts.casDir);
|
|
11120
11809
|
providers.push({
|
|
11121
11810
|
name: "content",
|
|
@@ -11131,7 +11820,7 @@ async function runChecks(opts) {
|
|
|
11131
11820
|
profile: profile.facts
|
|
11132
11821
|
}
|
|
11133
11822
|
};
|
|
11134
|
-
const tr = performance.now();
|
|
11823
|
+
const tr = performance$1.now();
|
|
11135
11824
|
if (opts.root !== void 0 && profile.facts.allowRepo) try {
|
|
11136
11825
|
ctx.facts.repo = await createRepoFacts(opts.root);
|
|
11137
11826
|
providers.push({
|
|
@@ -11233,7 +11922,7 @@ async function runChecks(opts) {
|
|
|
11233
11922
|
}
|
|
11234
11923
|
await execute(check, predicate, params.params);
|
|
11235
11924
|
}
|
|
11236
|
-
const tg = performance.now();
|
|
11925
|
+
const tg = performance$1.now();
|
|
11237
11926
|
if (guardChecks.length > 0 || guardFailed) {
|
|
11238
11927
|
await runPool(guardChecks, GUARD_POOL_SIZE, (g) => execute(g.check, g.predicate, g.params));
|
|
11239
11928
|
providers.push({
|
|
@@ -11341,11 +12030,222 @@ async function casGet(root, hex) {
|
|
|
11341
12030
|
throw err;
|
|
11342
12031
|
}
|
|
11343
12032
|
}
|
|
12033
|
+
function redactUri(uri) {
|
|
12034
|
+
try {
|
|
12035
|
+
const u = new URL(uri);
|
|
12036
|
+
return u.protocol === "file:" ? `file://${u.pathname}` : `${u.origin}${u.pathname}`;
|
|
12037
|
+
} catch {
|
|
12038
|
+
return "<invalid uri>";
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
12041
|
+
function hostAllowed(host, allowlist) {
|
|
12042
|
+
if (allowlist === void 0) return true;
|
|
12043
|
+
const h = host.toLowerCase();
|
|
12044
|
+
for (const raw of allowlist) {
|
|
12045
|
+
const p = raw.trim().toLowerCase();
|
|
12046
|
+
if (p === "") continue;
|
|
12047
|
+
if (p.startsWith("*.")) {
|
|
12048
|
+
if (h.endsWith(p.slice(1)) && h.length > p.length - 1) return true;
|
|
12049
|
+
} else if (h === p) return true;
|
|
12050
|
+
}
|
|
12051
|
+
return false;
|
|
12052
|
+
}
|
|
12053
|
+
function netError(code, message, src, opts, extra = {}) {
|
|
12054
|
+
let host;
|
|
12055
|
+
try {
|
|
12056
|
+
host = new URL(src.uri).host;
|
|
12057
|
+
} catch {
|
|
12058
|
+
host = void 0;
|
|
12059
|
+
}
|
|
12060
|
+
const details = {
|
|
12061
|
+
uri: redactUri(src.uri),
|
|
12062
|
+
digest: src.digest,
|
|
12063
|
+
...extra
|
|
12064
|
+
};
|
|
12065
|
+
if (host !== void 0) details.host = host;
|
|
12066
|
+
const o = { details };
|
|
12067
|
+
if (opts.path !== void 0) o.path = opts.path;
|
|
12068
|
+
return new AxiomError(code, message, o);
|
|
12069
|
+
}
|
|
12070
|
+
async function openSink(root, hex) {
|
|
12071
|
+
const target = casPath(root, hex);
|
|
12072
|
+
await mkdir(dirname(target), { recursive: true });
|
|
12073
|
+
const tmp = `${target}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
12074
|
+
return {
|
|
12075
|
+
tmp,
|
|
12076
|
+
fh: await open(tmp, "wx"),
|
|
12077
|
+
hash: createHash("sha256"),
|
|
12078
|
+
bytes: 0
|
|
12079
|
+
};
|
|
12080
|
+
}
|
|
12081
|
+
async function readFileStream(uri, sink, maxBytes, src, opts) {
|
|
12082
|
+
const p = fileURLToPath(uri);
|
|
12083
|
+
const st = await stat(p).catch(() => void 0);
|
|
12084
|
+
if (st === void 0 || !st.isFile()) throw netError("ERR_NET_FAILED", "file ref does not exist or is not a file", src, opts);
|
|
12085
|
+
if (st.size > maxBytes) throw new AxiomError("ERR_BLOB_TOO_LARGE", `ref is ${st.size} bytes`, { details: {
|
|
12086
|
+
bytes: st.size,
|
|
12087
|
+
max: maxBytes,
|
|
12088
|
+
uri: redactUri(src.uri)
|
|
12089
|
+
} });
|
|
12090
|
+
for await (const chunk of createReadStream(p)) {
|
|
12091
|
+
const c = chunk;
|
|
12092
|
+
sink.bytes += c.length;
|
|
12093
|
+
if (sink.bytes > maxBytes) throw new AxiomError("ERR_BLOB_TOO_LARGE", "ref exceeds maxBytes", { details: {
|
|
12094
|
+
max: maxBytes,
|
|
12095
|
+
uri: redactUri(src.uri)
|
|
12096
|
+
} });
|
|
12097
|
+
sink.hash.update(c);
|
|
12098
|
+
await sink.fh.write(c);
|
|
12099
|
+
}
|
|
12100
|
+
}
|
|
12101
|
+
async function readHttpsStream(uri, sink, maxBytes, timeoutMs, src, opts) {
|
|
12102
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
12103
|
+
const ac = new AbortController();
|
|
12104
|
+
const timer = setTimeout(() => ac.abort(/* @__PURE__ */ new Error("timeout")), timeoutMs);
|
|
12105
|
+
try {
|
|
12106
|
+
let res;
|
|
12107
|
+
try {
|
|
12108
|
+
res = await fetchImpl(uri, {
|
|
12109
|
+
method: "GET",
|
|
12110
|
+
redirect: "error",
|
|
12111
|
+
signal: ac.signal,
|
|
12112
|
+
headers: { accept: "*/*" }
|
|
12113
|
+
});
|
|
12114
|
+
} catch (err) {
|
|
12115
|
+
const timedOut = ac.signal.aborted;
|
|
12116
|
+
throw netError("ERR_NET_FAILED", timedOut ? `ref fetch timed out after ${timeoutMs} ms` : "ref fetch failed", src, opts, { reason: timedOut ? "timeout" : err.message });
|
|
12117
|
+
}
|
|
12118
|
+
if (!res.ok) throw netError("ERR_NET_FAILED", `ref fetch returned HTTP ${res.status}`, src, opts, { status: res.status });
|
|
12119
|
+
const declared = Number(res.headers.get("content-length") ?? "");
|
|
12120
|
+
if (Number.isFinite(declared) && declared > maxBytes) throw new AxiomError("ERR_BLOB_TOO_LARGE", `ref declares ${declared} bytes`, { details: {
|
|
12121
|
+
bytes: declared,
|
|
12122
|
+
max: maxBytes,
|
|
12123
|
+
uri: redactUri(src.uri)
|
|
12124
|
+
} });
|
|
12125
|
+
if (res.body === null) return;
|
|
12126
|
+
const reader = res.body.getReader();
|
|
12127
|
+
for (;;) {
|
|
12128
|
+
let step;
|
|
12129
|
+
try {
|
|
12130
|
+
step = await reader.read();
|
|
12131
|
+
} catch (err) {
|
|
12132
|
+
throw netError("ERR_NET_FAILED", ac.signal.aborted ? `ref fetch timed out after ${timeoutMs} ms` : "ref body read failed", src, opts, { reason: ac.signal.aborted ? "timeout" : err.message });
|
|
12133
|
+
}
|
|
12134
|
+
if (step.done) break;
|
|
12135
|
+
sink.bytes += step.value.length;
|
|
12136
|
+
if (sink.bytes > maxBytes) {
|
|
12137
|
+
ac.abort(/* @__PURE__ */ new Error("maxBytes"));
|
|
12138
|
+
await reader.cancel().catch(() => void 0);
|
|
12139
|
+
throw new AxiomError("ERR_BLOB_TOO_LARGE", "ref exceeds maxBytes", { details: {
|
|
12140
|
+
max: maxBytes,
|
|
12141
|
+
uri: redactUri(src.uri)
|
|
12142
|
+
} });
|
|
12143
|
+
}
|
|
12144
|
+
sink.hash.update(step.value);
|
|
12145
|
+
await sink.fh.write(step.value);
|
|
12146
|
+
}
|
|
12147
|
+
} finally {
|
|
12148
|
+
clearTimeout(timer);
|
|
12149
|
+
}
|
|
12150
|
+
}
|
|
12151
|
+
async function resolveRef(src, opts) {
|
|
12152
|
+
const hex = parseDigestRef(src.digest);
|
|
12153
|
+
const cached = await casGet(opts.root, hex);
|
|
12154
|
+
if (cached !== void 0) return cached;
|
|
12155
|
+
if (!opts.allowNet) throw netError("ERR_NET_DISABLED", "ref is not in the CAS and network access is disabled (pass --allow-net)", src, opts);
|
|
12156
|
+
let uri;
|
|
12157
|
+
try {
|
|
12158
|
+
uri = new URL(src.uri);
|
|
12159
|
+
} catch {
|
|
12160
|
+
throw netError("ERR_NET_DENIED", "ref uri is not a valid URL", src, opts);
|
|
12161
|
+
}
|
|
12162
|
+
if (uri.protocol === "file:") {
|
|
12163
|
+
if (opts.allowFile !== true) throw netError("ERR_NET_DENIED", "file: refs need allowFile", src, opts, { protocol: "file:" });
|
|
12164
|
+
} else if (uri.protocol !== "https:") throw netError("ERR_NET_DENIED", "only https: refs are fetched", src, opts, { protocol: uri.protocol });
|
|
12165
|
+
else if (uri.username !== "" || uri.password !== "") throw netError("ERR_NET_DENIED", "credentials in a ref uri are not allowed", src, opts);
|
|
12166
|
+
else if (!hostAllowed(uri.hostname, opts.allowlist)) throw netError("ERR_NET_DENIED", "ref host is not in the allowlist", src, opts, { allowlist: opts.allowlist ?? [] });
|
|
12167
|
+
const maxBytes = opts.maxBytes ?? 33554432;
|
|
12168
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
12169
|
+
const sink = await openSink(opts.root, hex);
|
|
12170
|
+
let ok = false;
|
|
12171
|
+
try {
|
|
12172
|
+
if (uri.protocol === "file:") await readFileStream(uri, sink, maxBytes, src, opts);
|
|
12173
|
+
else await readHttpsStream(uri, sink, maxBytes, timeoutMs, src, opts);
|
|
12174
|
+
const actual = sink.hash.digest("hex");
|
|
12175
|
+
if (actual !== hex) throw new AxiomError("ERR_DIGEST_MISMATCH", "fetched ref does not match its pinned digest", { details: {
|
|
12176
|
+
expected: src.digest,
|
|
12177
|
+
actual: `sha256:${actual}`,
|
|
12178
|
+
bytes: sink.bytes,
|
|
12179
|
+
uri: redactUri(src.uri)
|
|
12180
|
+
} });
|
|
12181
|
+
await sink.fh.sync();
|
|
12182
|
+
await sink.fh.close();
|
|
12183
|
+
ok = true;
|
|
12184
|
+
try {
|
|
12185
|
+
await rename(sink.tmp, casPath(opts.root, hex));
|
|
12186
|
+
} catch (err) {
|
|
12187
|
+
await rm(sink.tmp, { force: true });
|
|
12188
|
+
if (!await casHas(opts.root, hex)) throw err;
|
|
12189
|
+
}
|
|
12190
|
+
} finally {
|
|
12191
|
+
if (!ok) {
|
|
12192
|
+
await sink.fh.close().catch(() => void 0);
|
|
12193
|
+
await rm(sink.tmp, { force: true });
|
|
12194
|
+
}
|
|
12195
|
+
}
|
|
12196
|
+
const bytes = await casGet(opts.root, hex);
|
|
12197
|
+
if (bytes === void 0) throw new AxiomError("ERR_BLOB_MISSING", "ref was stored but cannot be read back", { details: { digest: src.digest } });
|
|
12198
|
+
return bytes;
|
|
12199
|
+
}
|
|
11344
12200
|
const INLINE_BASE64_DECODED_MAX = 196608;
|
|
11345
12201
|
const AXIOM_VERSION = "2.0.0";
|
|
11346
12202
|
function invalidPlan(err) {
|
|
11347
12203
|
return new AxiomError("ERR_INVALID_PLAN", "plan does not match PlanSchema", { details: { issues: err.issues } });
|
|
11348
12204
|
}
|
|
12205
|
+
function renderTemplate(a, src, opts) {
|
|
12206
|
+
const emitter = opts.emitters?.get(src.emitter);
|
|
12207
|
+
if (emitter === void 0) throw new AxiomError("ERR_EMITTER_UNKNOWN", `no emitter registered as "${src.emitter}"`, {
|
|
12208
|
+
path: a.path,
|
|
12209
|
+
details: {
|
|
12210
|
+
emitter: src.emitter,
|
|
12211
|
+
available: opts.emitters?.list() ?? []
|
|
12212
|
+
}
|
|
12213
|
+
});
|
|
12214
|
+
const def = Object.hasOwn(emitter.templates, src.template) ? emitter.templates[src.template] : void 0;
|
|
12215
|
+
if (def === void 0) throw new AxiomError("ERR_TEMPLATE_UNKNOWN", `emitter "${src.emitter}" has no template "${src.template}"`, {
|
|
12216
|
+
path: a.path,
|
|
12217
|
+
details: {
|
|
12218
|
+
emitter: src.emitter,
|
|
12219
|
+
template: src.template,
|
|
12220
|
+
available: Object.keys(emitter.templates).sort()
|
|
12221
|
+
}
|
|
12222
|
+
});
|
|
12223
|
+
const parsed = def.params.safeParse(src.params);
|
|
12224
|
+
if (!parsed.success) throw new AxiomError("ERR_TEMPLATE_PARAMS", "template params fail the template schema", {
|
|
12225
|
+
path: a.path,
|
|
12226
|
+
details: {
|
|
12227
|
+
emitter: src.emitter,
|
|
12228
|
+
template: src.template,
|
|
12229
|
+
issues: parsed.error.issues
|
|
12230
|
+
}
|
|
12231
|
+
});
|
|
12232
|
+
const rendered = def.render(parsed.data);
|
|
12233
|
+
const bytes = typeof rendered === "string" ? utf8Bytes(rendered) : rendered;
|
|
12234
|
+
if (bytes.length > 262144) throw new AxiomError("ERR_BLOB_TOO_LARGE", `template rendered ${bytes.length} bytes`, {
|
|
12235
|
+
path: a.path,
|
|
12236
|
+
details: {
|
|
12237
|
+
bytes: bytes.length,
|
|
12238
|
+
max: INLINE_CONTENT_MAX
|
|
12239
|
+
}
|
|
12240
|
+
});
|
|
12241
|
+
return {
|
|
12242
|
+
bytes,
|
|
12243
|
+
emitter: {
|
|
12244
|
+
id: emitter.id,
|
|
12245
|
+
version: emitter.version
|
|
12246
|
+
}
|
|
12247
|
+
};
|
|
12248
|
+
}
|
|
11349
12249
|
async function resolveSource(a, opts) {
|
|
11350
12250
|
const src = a.source;
|
|
11351
12251
|
if (src === void 0) throw new AxiomError("ERR_INVALID_PLAN", "source is required unless op is delete", { path: a.path });
|
|
@@ -11360,7 +12260,7 @@ async function resolveSource(a, opts) {
|
|
|
11360
12260
|
max: INLINE_CONTENT_MAX
|
|
11361
12261
|
}
|
|
11362
12262
|
});
|
|
11363
|
-
return bytes;
|
|
12263
|
+
return { bytes };
|
|
11364
12264
|
}
|
|
11365
12265
|
if (!isBase64(src.content)) throw new AxiomError("ERR_INVALID_PLAN", "inline content is not valid base64", { path: a.path });
|
|
11366
12266
|
const bytes = decodeBlob({
|
|
@@ -11374,7 +12274,7 @@ async function resolveSource(a, opts) {
|
|
|
11374
12274
|
max: INLINE_BASE64_DECODED_MAX
|
|
11375
12275
|
}
|
|
11376
12276
|
});
|
|
11377
|
-
return bytes;
|
|
12277
|
+
return { bytes };
|
|
11378
12278
|
}
|
|
11379
12279
|
case "cas": {
|
|
11380
12280
|
const hex = parseDigestRef(src.digest);
|
|
@@ -11395,22 +12295,19 @@ async function resolveSource(a, opts) {
|
|
|
11395
12295
|
actual: `sha256:${actual}`
|
|
11396
12296
|
}
|
|
11397
12297
|
});
|
|
11398
|
-
return bytes;
|
|
12298
|
+
return { bytes };
|
|
11399
12299
|
}
|
|
11400
|
-
case "ref":
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
template: src.template
|
|
11412
|
-
}
|
|
11413
|
-
});
|
|
12300
|
+
case "ref":
|
|
12301
|
+
if (opts.root === void 0) throw new AxiomError("ERR_REF_OFFLINE", "ref source needs a root (its CAS)", {
|
|
12302
|
+
path: a.path,
|
|
12303
|
+
details: { digest: src.digest }
|
|
12304
|
+
});
|
|
12305
|
+
return { bytes: await resolveRef(src, {
|
|
12306
|
+
...opts.net ?? { allowNet: false },
|
|
12307
|
+
root: opts.root,
|
|
12308
|
+
path: a.path
|
|
12309
|
+
}) };
|
|
12310
|
+
case "template": return renderTemplate(a, src, opts);
|
|
11414
12311
|
}
|
|
11415
12312
|
}
|
|
11416
12313
|
async function resolveArtifact(a, opts) {
|
|
@@ -11422,9 +12319,9 @@ async function resolveArtifact(a, opts) {
|
|
|
11422
12319
|
},
|
|
11423
12320
|
bytes: void 0
|
|
11424
12321
|
};
|
|
11425
|
-
const bytes = await resolveSource(a, opts);
|
|
12322
|
+
const { bytes, emitter } = await resolveSource(a, opts);
|
|
11426
12323
|
const origin = a.source?.type ?? "inline";
|
|
11427
|
-
|
|
12324
|
+
const out = {
|
|
11428
12325
|
artifact: {
|
|
11429
12326
|
path: a.path,
|
|
11430
12327
|
op: a.op,
|
|
@@ -11435,6 +12332,8 @@ async function resolveArtifact(a, opts) {
|
|
|
11435
12332
|
},
|
|
11436
12333
|
bytes
|
|
11437
12334
|
};
|
|
12335
|
+
if (emitter !== void 0) out.emitter = emitter;
|
|
12336
|
+
return out;
|
|
11438
12337
|
}
|
|
11439
12338
|
function digestOnlyPlan(plan, digests) {
|
|
11440
12339
|
const artifacts = [...plan.artifacts].sort((x, y) => compareUtf8(x.path, y.path)).map((a) => {
|
|
@@ -11443,10 +12342,19 @@ function digestOnlyPlan(plan, digests) {
|
|
|
11443
12342
|
mode: a.mode,
|
|
11444
12343
|
op: a.op
|
|
11445
12344
|
};
|
|
11446
|
-
if (a.source !== void 0)
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
12345
|
+
if (a.source !== void 0) {
|
|
12346
|
+
const digest = `sha256:${digests.get(a.path) ?? ""}`;
|
|
12347
|
+
out.source = a.source.type === "template" ? {
|
|
12348
|
+
type: "template",
|
|
12349
|
+
emitter: a.source.emitter,
|
|
12350
|
+
template: a.source.template,
|
|
12351
|
+
params: a.source.params,
|
|
12352
|
+
digest
|
|
12353
|
+
} : {
|
|
12354
|
+
type: a.source.type,
|
|
12355
|
+
digest
|
|
12356
|
+
};
|
|
12357
|
+
}
|
|
11450
12358
|
return out;
|
|
11451
12359
|
});
|
|
11452
12360
|
return {
|
|
@@ -11454,7 +12362,7 @@ function digestOnlyPlan(plan, digests) {
|
|
|
11454
12362
|
artifacts
|
|
11455
12363
|
};
|
|
11456
12364
|
}
|
|
11457
|
-
function splitToolchain(input) {
|
|
12365
|
+
function splitToolchain(input, used) {
|
|
11458
12366
|
const emitters = {};
|
|
11459
12367
|
let axiom = AXIOM_VERSION;
|
|
11460
12368
|
for (const key of Object.keys(input ?? {}).sort()) {
|
|
@@ -11463,6 +12371,7 @@ function splitToolchain(input) {
|
|
|
11463
12371
|
if (key === "axiom") axiom = v;
|
|
11464
12372
|
else emitters[key] = v;
|
|
11465
12373
|
}
|
|
12374
|
+
for (const id of [...used.keys()].sort()) emitters[id] = used.get(id);
|
|
11466
12375
|
return {
|
|
11467
12376
|
axiom,
|
|
11468
12377
|
emitters
|
|
@@ -11492,9 +12401,13 @@ async function compilePlan(planInput, opts = {}) {
|
|
|
11492
12401
|
for (const a of plan.artifacts) resolved.push(await resolveArtifact(a, opts));
|
|
11493
12402
|
resolved.sort((x, y) => compareUtf8(x.artifact.path, y.artifact.path));
|
|
11494
12403
|
const digests = /* @__PURE__ */ new Map();
|
|
11495
|
-
|
|
12404
|
+
const usedEmitters = /* @__PURE__ */ new Map();
|
|
12405
|
+
for (const r of resolved) {
|
|
12406
|
+
if (r.artifact.digest !== void 0) digests.set(r.artifact.path, r.artifact.digest.sha256);
|
|
12407
|
+
if (r.emitter !== void 0) usedEmitters.set(r.emitter.id, r.emitter.version);
|
|
12408
|
+
}
|
|
11496
12409
|
const planDigest = canonicalDigestRef(digestOnlyPlan(plan, digests));
|
|
11497
|
-
const toolchain = splitToolchain(opts.toolchain);
|
|
12410
|
+
const toolchain = splitToolchain(opts.toolchain, usedEmitters);
|
|
11498
12411
|
const body = {
|
|
11499
12412
|
apiVersion: plan.apiVersion,
|
|
11500
12413
|
kind: "Manifest",
|
|
@@ -11505,12 +12418,14 @@ async function compilePlan(planInput, opts = {}) {
|
|
|
11505
12418
|
checks: [...plan.checks].sort((x, y) => compareUtf8(x.id, y.id)),
|
|
11506
12419
|
toolchain
|
|
11507
12420
|
};
|
|
12421
|
+
if (plan.counter !== void 0) body.counter = plan.counter;
|
|
11508
12422
|
const manifestDigest = canonicalDigestRef(body);
|
|
11509
12423
|
const blobs = {};
|
|
11510
12424
|
const useCas = opts.store === "cas" && opts.root !== void 0;
|
|
11511
12425
|
let total = 0;
|
|
11512
12426
|
for (const r of resolved) {
|
|
11513
12427
|
if (r.bytes === void 0 || r.artifact.digest === void 0) continue;
|
|
12428
|
+
if (r.artifact.origin === "ref") continue;
|
|
11514
12429
|
if (useCas) {
|
|
11515
12430
|
await casPut(opts.root, r.bytes);
|
|
11516
12431
|
continue;
|
|
@@ -11587,6 +12502,17 @@ function diffManifests(a, b) {
|
|
|
11587
12502
|
changed
|
|
11588
12503
|
};
|
|
11589
12504
|
}
|
|
12505
|
+
function createEmitterRegistry(emitters) {
|
|
12506
|
+
const byId = /* @__PURE__ */ new Map();
|
|
12507
|
+
for (const e of emitters) {
|
|
12508
|
+
if (byId.has(e.id)) throw new Error(`duplicate emitter id: ${e.id}`);
|
|
12509
|
+
byId.set(e.id, e);
|
|
12510
|
+
}
|
|
12511
|
+
return {
|
|
12512
|
+
get: (id) => byId.get(id),
|
|
12513
|
+
list: () => [...byId.keys()].sort()
|
|
12514
|
+
};
|
|
12515
|
+
}
|
|
11590
12516
|
function issueCode(params) {
|
|
11591
12517
|
const code = params?.code;
|
|
11592
12518
|
return isErrorCode(code) ? code : "ERR_INVALID_MANIFEST";
|
|
@@ -12523,6 +13449,476 @@ var StdioServerTransport = class {
|
|
|
12523
13449
|
});
|
|
12524
13450
|
}
|
|
12525
13451
|
};
|
|
13452
|
+
function defineTemplate(description, params, render) {
|
|
13453
|
+
return {
|
|
13454
|
+
description,
|
|
13455
|
+
params,
|
|
13456
|
+
render
|
|
13457
|
+
};
|
|
13458
|
+
}
|
|
13459
|
+
function lines(...parts) {
|
|
13460
|
+
const flat = [];
|
|
13461
|
+
for (const p of parts) if (typeof p === "string") flat.push(p);
|
|
13462
|
+
else flat.push(...p);
|
|
13463
|
+
return `${flat.join("\n").replace(/\s+$/, "")}\n`;
|
|
13464
|
+
}
|
|
13465
|
+
const Ident = string().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/, "must be a JavaScript identifier").max(64);
|
|
13466
|
+
const HttpMethod = _enum([
|
|
13467
|
+
"GET",
|
|
13468
|
+
"POST",
|
|
13469
|
+
"PUT",
|
|
13470
|
+
"DELETE"
|
|
13471
|
+
]);
|
|
13472
|
+
const Segment = string().regex(/^[a-z0-9][a-z0-9-]*(\/(\[[a-z][a-zA-Z0-9]*\]|[a-z0-9][a-z0-9-]*))*$/, "invalid segment").max(128);
|
|
13473
|
+
const SORT_METHODS = (ms) => {
|
|
13474
|
+
const order = [
|
|
13475
|
+
"GET",
|
|
13476
|
+
"POST",
|
|
13477
|
+
"PUT",
|
|
13478
|
+
"DELETE"
|
|
13479
|
+
];
|
|
13480
|
+
return [...new Set(ms)].sort((a, b) => order.indexOf(a) - order.indexOf(b));
|
|
13481
|
+
};
|
|
13482
|
+
const routeHandler = defineTemplate("Next.js 16 App Router route handler (`app/api/<segment>/route.ts`) — one exported async function per method, `NextRequest` in, `Response.json` out; optional Zod body validation on the non-GET methods.", object$1({
|
|
13483
|
+
segment: Segment,
|
|
13484
|
+
methods: array(HttpMethod).min(1).max(4),
|
|
13485
|
+
zodSchemaName: Ident.optional()
|
|
13486
|
+
}).strict(), ({ segment, methods, zodSchemaName }) => {
|
|
13487
|
+
const schema = zodSchemaName;
|
|
13488
|
+
return lines([
|
|
13489
|
+
"import type { NextRequest } from \"next/server\";",
|
|
13490
|
+
...schema === void 0 ? [] : [`import { ${schema} } from "./schema";`],
|
|
13491
|
+
"",
|
|
13492
|
+
`// ${segment} — generated by @codai/axiom-emitters-web (web@2.0.0).`,
|
|
13493
|
+
""
|
|
13494
|
+
], SORT_METHODS(methods).map((m) => {
|
|
13495
|
+
if (m === "GET") return lines([
|
|
13496
|
+
"export async function GET(request: NextRequest): Promise<Response> {",
|
|
13497
|
+
" const { searchParams } = new URL(request.url);",
|
|
13498
|
+
" return Response.json({ ok: true, query: Object.fromEntries(searchParams) });",
|
|
13499
|
+
"}"
|
|
13500
|
+
]).trimEnd();
|
|
13501
|
+
if (m === "DELETE") return lines([
|
|
13502
|
+
"export async function DELETE(_request: NextRequest): Promise<Response> {",
|
|
13503
|
+
" return new Response(null, { status: 204 });",
|
|
13504
|
+
"}"
|
|
13505
|
+
]).trimEnd();
|
|
13506
|
+
const validate = schema === void 0 ? [" const data = (await request.json()) as unknown;"] : [
|
|
13507
|
+
" const parsed = " + schema + ".safeParse(await request.json());",
|
|
13508
|
+
" if (!parsed.success) {",
|
|
13509
|
+
" return Response.json({ error: parsed.error.issues }, { status: 400 });",
|
|
13510
|
+
" }",
|
|
13511
|
+
" const data = parsed.data;"
|
|
13512
|
+
];
|
|
13513
|
+
return lines([
|
|
13514
|
+
`export async function ${m}(request: NextRequest): Promise<Response> {`,
|
|
13515
|
+
...validate,
|
|
13516
|
+
` return Response.json({ ok: true, data }, { status: ${m === "POST" ? 201 : 200} });`,
|
|
13517
|
+
"}"
|
|
13518
|
+
]).trimEnd();
|
|
13519
|
+
}).join("\n\n"));
|
|
13520
|
+
});
|
|
13521
|
+
const ZOD_FIELD = {
|
|
13522
|
+
string: "z.string().min(1)",
|
|
13523
|
+
number: "z.coerce.number()",
|
|
13524
|
+
boolean: "z.coerce.boolean()",
|
|
13525
|
+
email: "z.email()"
|
|
13526
|
+
};
|
|
13527
|
+
const serverAction = defineTemplate("Next.js 16 Server Action (\"use server\") validating FormData with Zod v4 and returning a `useActionState`-compatible `{ ok, errors, values }` state.", object$1({
|
|
13528
|
+
name: Ident,
|
|
13529
|
+
fields: array(object$1({
|
|
13530
|
+
name: Ident,
|
|
13531
|
+
zod: _enum([
|
|
13532
|
+
"string",
|
|
13533
|
+
"number",
|
|
13534
|
+
"boolean",
|
|
13535
|
+
"email"
|
|
13536
|
+
])
|
|
13537
|
+
}).strict()).min(1).max(50)
|
|
13538
|
+
}).strict(), ({ name, fields }) => {
|
|
13539
|
+
const State = `${name.charAt(0).toUpperCase()}${name.slice(1)}State`;
|
|
13540
|
+
return lines([
|
|
13541
|
+
"\"use server\";",
|
|
13542
|
+
"",
|
|
13543
|
+
"import { z } from \"zod\";",
|
|
13544
|
+
"",
|
|
13545
|
+
`// ${name} — generated by @codai/axiom-emitters-web (web@2.0.0).`,
|
|
13546
|
+
"",
|
|
13547
|
+
`const ${name}Schema = z.object({`,
|
|
13548
|
+
...fields.map((f) => ` ${f.name}: ${ZOD_FIELD[f.zod]},`),
|
|
13549
|
+
"});",
|
|
13550
|
+
"",
|
|
13551
|
+
`export interface ${State} {`,
|
|
13552
|
+
" ok: boolean;",
|
|
13553
|
+
" errors: Record<string, string[]>;",
|
|
13554
|
+
` values: Partial<z.output<typeof ${name}Schema>>;`,
|
|
13555
|
+
"}",
|
|
13556
|
+
"",
|
|
13557
|
+
`export const initial${State}: ${State} = { ok: false, errors: {}, values: {} };`,
|
|
13558
|
+
"",
|
|
13559
|
+
`export async function ${name}(`,
|
|
13560
|
+
` _prev: ${State},`,
|
|
13561
|
+
" formData: FormData,",
|
|
13562
|
+
`): Promise<${State}> {`,
|
|
13563
|
+
` const parsed = ${name}Schema.safeParse({`,
|
|
13564
|
+
...fields.map((f) => ` ${f.name}: formData.get("${f.name}"),`),
|
|
13565
|
+
" });",
|
|
13566
|
+
" if (!parsed.success) {",
|
|
13567
|
+
" const flat = z.flattenError(parsed.error);",
|
|
13568
|
+
" return { ok: false, errors: flat.fieldErrors as Record<string, string[]>, values: {} };",
|
|
13569
|
+
" }",
|
|
13570
|
+
" return { ok: true, errors: {}, values: parsed.data };",
|
|
13571
|
+
"}"
|
|
13572
|
+
]);
|
|
13573
|
+
});
|
|
13574
|
+
const honoRoute = defineTemplate("Hono 4 route module exporting a `Hono` instance with one handler per method; mount it with `app.route(\"/\", <name>)`.", object$1({
|
|
13575
|
+
name: Ident,
|
|
13576
|
+
path: string().regex(/^\/[A-Za-z0-9/:_-]*$/, "path must start with /").max(128),
|
|
13577
|
+
methods: array(HttpMethod).min(1).max(4)
|
|
13578
|
+
}).strict(), ({ name, path, methods }) => lines([
|
|
13579
|
+
"import { Hono } from \"hono\";",
|
|
13580
|
+
"",
|
|
13581
|
+
`// ${name} — generated by @codai/axiom-emitters-web (web@2.0.0).`,
|
|
13582
|
+
"",
|
|
13583
|
+
`export const ${name} = new Hono();`,
|
|
13584
|
+
"",
|
|
13585
|
+
...SORT_METHODS(methods).flatMap((m) => [
|
|
13586
|
+
`${name}.${m.toLowerCase()}("${path}", (c) => {`,
|
|
13587
|
+
m === "DELETE" ? " return c.body(null, 204);" : ` return c.json({ ok: true, method: "${m}" }${m === "POST" ? ", 201" : ""});`,
|
|
13588
|
+
"});",
|
|
13589
|
+
""
|
|
13590
|
+
]),
|
|
13591
|
+
`export default ${name};`
|
|
13592
|
+
]));
|
|
13593
|
+
const DRIZZLE_COLUMN = {
|
|
13594
|
+
text: (n) => `text("${n}")`,
|
|
13595
|
+
integer: (n) => `integer("${n}")`,
|
|
13596
|
+
boolean: (n) => `boolean("${n}")`,
|
|
13597
|
+
timestamp: (n) => `timestamp("${n}", { withTimezone: true })`,
|
|
13598
|
+
uuid: (n) => `uuid("${n}")`
|
|
13599
|
+
};
|
|
13600
|
+
const drizzleTable = defineTemplate("Drizzle `pgTable` with the repo conventions: `id` identity column always generated, `withTimezone: true` on every timestamp, and inferred insert/select types.", object$1({
|
|
13601
|
+
name: string().regex(/^[a-z][a-z0-9_]*$/, "snake_case table name").max(63),
|
|
13602
|
+
columns: array(object$1({
|
|
13603
|
+
name: string().regex(/^[a-z][a-z0-9_]*$/, "snake_case column name").max(63),
|
|
13604
|
+
type: _enum([
|
|
13605
|
+
"text",
|
|
13606
|
+
"integer",
|
|
13607
|
+
"boolean",
|
|
13608
|
+
"timestamp",
|
|
13609
|
+
"uuid"
|
|
13610
|
+
]),
|
|
13611
|
+
primaryKey: boolean().default(false),
|
|
13612
|
+
notNull: boolean().default(false)
|
|
13613
|
+
}).strict()).min(1).max(100)
|
|
13614
|
+
}).strict(), ({ name, columns }) => {
|
|
13615
|
+
const kinds = [...new Set(columns.map((c) => c.type))].sort();
|
|
13616
|
+
const toCamel = (s) => s.replace(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
13617
|
+
const camel = toCamel(name);
|
|
13618
|
+
const Type = `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
13619
|
+
const hasExplicitPk = columns.some((c) => c.primaryKey);
|
|
13620
|
+
return lines([
|
|
13621
|
+
`import { ${["pgTable", ...kinds].sort().join(", ")} } from "drizzle-orm/pg-core";`,
|
|
13622
|
+
"",
|
|
13623
|
+
`// ${name} — generated by @codai/axiom-emitters-web (web@2.0.0).`,
|
|
13624
|
+
"",
|
|
13625
|
+
`export const ${camel} = pgTable("${name}", {`,
|
|
13626
|
+
...hasExplicitPk ? [] : [" id: integer(\"id\").generatedAlwaysAsIdentity().primaryKey(),"],
|
|
13627
|
+
...columns.map((c) => {
|
|
13628
|
+
const suffix = [c.primaryKey ? ".primaryKey()" : "", c.notNull && !c.primaryKey ? ".notNull()" : ""].join("");
|
|
13629
|
+
return ` ${toCamel(c.name)}: ${DRIZZLE_COLUMN[c.type](c.name)}${suffix},`;
|
|
13630
|
+
}),
|
|
13631
|
+
"});",
|
|
13632
|
+
"",
|
|
13633
|
+
`export type ${Type} = typeof ${camel}.$inferSelect;`,
|
|
13634
|
+
`export type New${Type} = typeof ${camel}.$inferInsert;`
|
|
13635
|
+
]);
|
|
13636
|
+
});
|
|
13637
|
+
const biomeConfig = defineTemplate("Biome 2.5 config matching this repo's own style (2-space, width 100, double quotes, LF, `noExplicitAny` / `noConsole` / `noNonNullAssertion` as errors).", object$1({}).strict(), () => lines([
|
|
13638
|
+
"{",
|
|
13639
|
+
" \"$schema\": \"https://biomejs.dev/schemas/2.5.14/schema.json\",",
|
|
13640
|
+
" \"vcs\": { \"enabled\": true, \"clientKind\": \"git\", \"useIgnoreFile\": true },",
|
|
13641
|
+
" \"files\": { \"includes\": [\"**\", \"!**/dist\", \"!**/.next\"] },",
|
|
13642
|
+
" \"formatter\": {",
|
|
13643
|
+
" \"enabled\": true,",
|
|
13644
|
+
" \"indentStyle\": \"space\",",
|
|
13645
|
+
" \"indentWidth\": 2,",
|
|
13646
|
+
" \"lineWidth\": 100,",
|
|
13647
|
+
" \"lineEnding\": \"lf\"",
|
|
13648
|
+
" },",
|
|
13649
|
+
" \"javascript\": {",
|
|
13650
|
+
" \"formatter\": { \"quoteStyle\": \"double\", \"semicolons\": \"always\", \"trailingCommas\": \"all\" }",
|
|
13651
|
+
" },",
|
|
13652
|
+
" \"linter\": {",
|
|
13653
|
+
" \"enabled\": true,",
|
|
13654
|
+
" \"rules\": {",
|
|
13655
|
+
" \"preset\": \"recommended\",",
|
|
13656
|
+
" \"style\": {",
|
|
13657
|
+
" \"noNonNullAssertion\": \"error\",",
|
|
13658
|
+
" \"useImportType\": \"error\",",
|
|
13659
|
+
" \"useNodejsImportProtocol\": \"error\"",
|
|
13660
|
+
" },",
|
|
13661
|
+
" \"suspicious\": {",
|
|
13662
|
+
" \"noExplicitAny\": \"error\",",
|
|
13663
|
+
" \"noConsole\": { \"level\": \"error\", \"options\": { \"allow\": [\"error\", \"warn\"] } }",
|
|
13664
|
+
" },",
|
|
13665
|
+
" \"correctness\": { \"noUnusedImports\": \"error\", \"noUnusedVariables\": \"error\" }",
|
|
13666
|
+
" }",
|
|
13667
|
+
" }",
|
|
13668
|
+
"}"
|
|
13669
|
+
]));
|
|
13670
|
+
const tailwindGlobals = defineTemplate("Tailwind v4 CSS-first entry (`@import \"tailwindcss\"` + an empty `@theme` block and light/dark tokens). No `tailwind.config.js`.", object$1({}).strict(), () => lines([
|
|
13671
|
+
"@import \"tailwindcss\";",
|
|
13672
|
+
"",
|
|
13673
|
+
"@theme {",
|
|
13674
|
+
" --color-background: oklch(1 0 0);",
|
|
13675
|
+
" --color-foreground: oklch(0.145 0 0);",
|
|
13676
|
+
" --radius: 0.625rem;",
|
|
13677
|
+
"}",
|
|
13678
|
+
"",
|
|
13679
|
+
"@layer base {",
|
|
13680
|
+
" :root {",
|
|
13681
|
+
" color-scheme: light dark;",
|
|
13682
|
+
" }",
|
|
13683
|
+
"",
|
|
13684
|
+
" body {",
|
|
13685
|
+
" background-color: var(--color-background);",
|
|
13686
|
+
" color: var(--color-foreground);",
|
|
13687
|
+
" }",
|
|
13688
|
+
"}"
|
|
13689
|
+
]));
|
|
13690
|
+
const EMITTERS = createEmitterRegistry([{
|
|
13691
|
+
id: "web",
|
|
13692
|
+
version: "2.0.0",
|
|
13693
|
+
templates: {
|
|
13694
|
+
"biome.config": biomeConfig,
|
|
13695
|
+
"drizzle.table": drizzleTable,
|
|
13696
|
+
"hono.route": honoRoute,
|
|
13697
|
+
"next.route-handler": routeHandler,
|
|
13698
|
+
"next.server-action": serverAction,
|
|
13699
|
+
"readme.section": defineTemplate("A single `## <title>` markdown section with the given body — the smallest useful template, meant for composing docs from a plan.", object$1({
|
|
13700
|
+
title: string().min(1).max(120),
|
|
13701
|
+
body: string().max(8e3)
|
|
13702
|
+
}).strict(), ({ title, body }) => lines([
|
|
13703
|
+
`## ${title}`,
|
|
13704
|
+
"",
|
|
13705
|
+
body.replace(/\r\n/g, "\n").trimEnd()
|
|
13706
|
+
])),
|
|
13707
|
+
"tailwind.globals": tailwindGlobals
|
|
13708
|
+
}
|
|
13709
|
+
}]);
|
|
13710
|
+
function emitterCatalogue(registry = EMITTERS) {
|
|
13711
|
+
const rows = [];
|
|
13712
|
+
for (const id of registry.list()) {
|
|
13713
|
+
const e = registry.get(id);
|
|
13714
|
+
if (e === void 0) continue;
|
|
13715
|
+
for (const template of Object.keys(e.templates).sort()) rows.push({
|
|
13716
|
+
emitter: e.id,
|
|
13717
|
+
version: e.version,
|
|
13718
|
+
template,
|
|
13719
|
+
description: e.templates[template]?.description ?? ""
|
|
13720
|
+
});
|
|
13721
|
+
}
|
|
13722
|
+
return rows;
|
|
13723
|
+
}
|
|
13724
|
+
function manifestsDir(root) {
|
|
13725
|
+
return path.join(root, ".axiom", "manifests");
|
|
13726
|
+
}
|
|
13727
|
+
function reportsDir(root) {
|
|
13728
|
+
return path.join(root, ".axiom", "reports");
|
|
13729
|
+
}
|
|
13730
|
+
function hexOf(ref) {
|
|
13731
|
+
return ref.slice(7);
|
|
13732
|
+
}
|
|
13733
|
+
function toDigestRef(shaOrRef) {
|
|
13734
|
+
const ref = shaOrRef.startsWith("sha256:") ? shaOrRef : `sha256:${shaOrRef}`;
|
|
13735
|
+
const parsed = DigestRefSchema.safeParse(ref);
|
|
13736
|
+
if (!parsed.success) throw new AxiomError("ERR_NOT_FOUND", `not a sha256 digest: ${shaOrRef}`);
|
|
13737
|
+
return parsed.data;
|
|
13738
|
+
}
|
|
13739
|
+
async function writeJsonAtomic$1(file, value) {
|
|
13740
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
13741
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
13742
|
+
await writeFile(tmp, JSON.stringify(value), "utf8");
|
|
13743
|
+
await rename(tmp, file);
|
|
13744
|
+
}
|
|
13745
|
+
async function readJsonOrUndefined$1(file) {
|
|
13746
|
+
try {
|
|
13747
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
13748
|
+
} catch (err) {
|
|
13749
|
+
if (err.code === "ENOENT") return void 0;
|
|
13750
|
+
throw err;
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
async function saveManifest(root, bundle) {
|
|
13754
|
+
const file = path.join(manifestsDir(root), `${hexOf(bundle.manifestDigest)}.json`);
|
|
13755
|
+
await writeJsonAtomic$1(file, bundle);
|
|
13756
|
+
return file;
|
|
13757
|
+
}
|
|
13758
|
+
async function saveReport(root, report) {
|
|
13759
|
+
const file = path.join(reportsDir(root), `${hexOf(report.manifestDigest)}.json`);
|
|
13760
|
+
await writeJsonAtomic$1(file, report);
|
|
13761
|
+
return file;
|
|
13762
|
+
}
|
|
13763
|
+
async function loadManifest(roots, ref) {
|
|
13764
|
+
for (const root of roots) {
|
|
13765
|
+
const raw = await readJsonOrUndefined$1(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
|
|
13766
|
+
if (raw !== void 0) return ManifestBundleSchema.parse(raw);
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
async function loadReport(roots, ref) {
|
|
13770
|
+
for (const root of roots) {
|
|
13771
|
+
const raw = await readJsonOrUndefined$1(path.join(reportsDir(root), `${hexOf(ref)}.json`));
|
|
13772
|
+
if (raw !== void 0) return CheckReportSchema.parse(raw);
|
|
13773
|
+
}
|
|
13774
|
+
}
|
|
13775
|
+
async function loadApplied(roots, ref) {
|
|
13776
|
+
for (const root of roots) {
|
|
13777
|
+
const raw = await readJsonOrUndefined$1(appliedPath(root, ref));
|
|
13778
|
+
if (raw !== void 0) return ApplyResultSchema.parse(raw);
|
|
13779
|
+
}
|
|
13780
|
+
}
|
|
13781
|
+
async function listStored(roots, sub) {
|
|
13782
|
+
const out = [];
|
|
13783
|
+
for (const root of roots) {
|
|
13784
|
+
let names;
|
|
13785
|
+
try {
|
|
13786
|
+
names = await readdir(path.join(root, ".axiom", sub));
|
|
13787
|
+
} catch {
|
|
13788
|
+
continue;
|
|
13789
|
+
}
|
|
13790
|
+
for (const n of names) {
|
|
13791
|
+
const m = /^([0-9a-f]{64})\.json$/.exec(n);
|
|
13792
|
+
if (m?.[1] !== void 0) out.push({
|
|
13793
|
+
root,
|
|
13794
|
+
sha: m[1]
|
|
13795
|
+
});
|
|
13796
|
+
}
|
|
13797
|
+
}
|
|
13798
|
+
return out;
|
|
13799
|
+
}
|
|
13800
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
13801
|
+
function casRootDir(root) {
|
|
13802
|
+
return path.join(root, ".axiom", "cas", "sha256");
|
|
13803
|
+
}
|
|
13804
|
+
async function readdirOrEmpty(dir) {
|
|
13805
|
+
try {
|
|
13806
|
+
return await readdir(dir);
|
|
13807
|
+
} catch (err) {
|
|
13808
|
+
if (err.code === "ENOENT") return [];
|
|
13809
|
+
throw err;
|
|
13810
|
+
}
|
|
13811
|
+
}
|
|
13812
|
+
async function loadManifestArtifactDigests(root, hex) {
|
|
13813
|
+
const file = path.join(manifestsDir(root), `${hex}.json`);
|
|
13814
|
+
let raw;
|
|
13815
|
+
try {
|
|
13816
|
+
raw = await readFile(file, "utf8");
|
|
13817
|
+
} catch (err) {
|
|
13818
|
+
if (err.code === "ENOENT") throw new AxiomError("ERR_BLOB_MISSING", "a journal names a manifest that is not stored", { details: {
|
|
13819
|
+
manifestDigest: `sha256:${hex}`,
|
|
13820
|
+
file
|
|
13821
|
+
} });
|
|
13822
|
+
throw err;
|
|
13823
|
+
}
|
|
13824
|
+
const parsed = ManifestBundleSchema.safeParse(JSON.parse(raw));
|
|
13825
|
+
if (!parsed.success) throw new AxiomError("ERR_INVALID_MANIFEST", "stored manifest fails ManifestBundleSchema", { details: { file } });
|
|
13826
|
+
const out = /* @__PURE__ */ new Set();
|
|
13827
|
+
for (const a of parsed.data.manifest.artifacts) if (a.digest !== void 0) out.add(a.digest.sha256);
|
|
13828
|
+
return out;
|
|
13829
|
+
}
|
|
13830
|
+
async function liveManifestHexes(root, keep) {
|
|
13831
|
+
const hexes = /* @__PURE__ */ new Set();
|
|
13832
|
+
const { journals } = await listJournals(root);
|
|
13833
|
+
for (const j of journals) hexes.add(j.manifestDigest.slice(7));
|
|
13834
|
+
if (keep === "journal") {
|
|
13835
|
+
for (const n of await readdirOrEmpty(path.join(root, ".axiom", "applied"))) {
|
|
13836
|
+
const m = /^([0-9a-f]{64})\.json$/.exec(n);
|
|
13837
|
+
if (m?.[1] !== void 0) hexes.add(m[1]);
|
|
13838
|
+
}
|
|
13839
|
+
return hexes;
|
|
13840
|
+
}
|
|
13841
|
+
for (const n of await readdirOrEmpty(manifestsDir(root))) {
|
|
13842
|
+
const m = /^([0-9a-f]{64})\.json$/.exec(n);
|
|
13843
|
+
if (m?.[1] !== void 0) hexes.add(m[1]);
|
|
13844
|
+
}
|
|
13845
|
+
return hexes;
|
|
13846
|
+
}
|
|
13847
|
+
async function collectGarbage(root, opts = {}) {
|
|
13848
|
+
const keep = opts.keep ?? "all-manifests";
|
|
13849
|
+
const dryRun = opts.dryRun === true;
|
|
13850
|
+
const now = opts.now ?? Date.now;
|
|
13851
|
+
const lock = await acquireLock(root, "gc", opts.lockTimeoutMs ?? 1e3);
|
|
13852
|
+
try {
|
|
13853
|
+
const live = /* @__PURE__ */ new Set();
|
|
13854
|
+
for (const hex of await liveManifestHexes(root, keep)) for (const d of await loadManifestArtifactDigests(root, hex)) live.add(d);
|
|
13855
|
+
const casDir = casRootDir(root);
|
|
13856
|
+
const removed = [];
|
|
13857
|
+
const present = /* @__PURE__ */ new Set();
|
|
13858
|
+
let scanned = 0;
|
|
13859
|
+
let skippedYoung = 0;
|
|
13860
|
+
let freedBytes = 0;
|
|
13861
|
+
const cutoff = opts.olderThanMs === void 0 ? void 0 : now() - opts.olderThanMs;
|
|
13862
|
+
for (const shard of (await readdirOrEmpty(casDir)).sort()) {
|
|
13863
|
+
const shardDir = path.join(casDir, shard);
|
|
13864
|
+
for (const name of (await readdirOrEmpty(shardDir)).sort()) {
|
|
13865
|
+
const file = path.join(shardDir, name);
|
|
13866
|
+
const st = await stat(file).catch(() => void 0);
|
|
13867
|
+
if (st === void 0 || !st.isFile()) continue;
|
|
13868
|
+
if (!HEX64.test(name)) {
|
|
13869
|
+
if (name.endsWith(".tmp") && (cutoff === void 0 || st.mtimeMs < cutoff)) {
|
|
13870
|
+
if (!dryRun) await rm(file, { force: true });
|
|
13871
|
+
removed.push({
|
|
13872
|
+
sha: name,
|
|
13873
|
+
bytes: st.size
|
|
13874
|
+
});
|
|
13875
|
+
freedBytes += st.size;
|
|
13876
|
+
}
|
|
13877
|
+
continue;
|
|
13878
|
+
}
|
|
13879
|
+
scanned++;
|
|
13880
|
+
present.add(name);
|
|
13881
|
+
if (live.has(name)) continue;
|
|
13882
|
+
if (cutoff !== void 0 && st.mtimeMs >= cutoff) {
|
|
13883
|
+
skippedYoung++;
|
|
13884
|
+
continue;
|
|
13885
|
+
}
|
|
13886
|
+
if (!dryRun) await rm(file, { force: true });
|
|
13887
|
+
removed.push({
|
|
13888
|
+
sha: name,
|
|
13889
|
+
bytes: st.size
|
|
13890
|
+
});
|
|
13891
|
+
freedBytes += st.size;
|
|
13892
|
+
}
|
|
13893
|
+
}
|
|
13894
|
+
let missing = 0;
|
|
13895
|
+
for (const d of live) if (!present.has(d)) missing++;
|
|
13896
|
+
return {
|
|
13897
|
+
root,
|
|
13898
|
+
keep,
|
|
13899
|
+
dryRun,
|
|
13900
|
+
scanned,
|
|
13901
|
+
live: live.size,
|
|
13902
|
+
missing,
|
|
13903
|
+
removed,
|
|
13904
|
+
skippedYoung,
|
|
13905
|
+
freedBytes
|
|
13906
|
+
};
|
|
13907
|
+
} finally {
|
|
13908
|
+
await lock.release();
|
|
13909
|
+
}
|
|
13910
|
+
}
|
|
13911
|
+
function parseDuration(text) {
|
|
13912
|
+
const m = /^(\d+)(ms|s|m|h|d)?$/.exec(text.trim());
|
|
13913
|
+
if (m === null || m[1] === void 0) return void 0;
|
|
13914
|
+
return Number(m[1]) * ({
|
|
13915
|
+
ms: 1,
|
|
13916
|
+
s: 1e3,
|
|
13917
|
+
m: 6e4,
|
|
13918
|
+
h: 36e5,
|
|
13919
|
+
d: 864e5
|
|
13920
|
+
}[m[2] ?? "ms"] ?? 1);
|
|
13921
|
+
}
|
|
12526
13922
|
const SCHEMA_KINDS = [
|
|
12527
13923
|
"Plan",
|
|
12528
13924
|
"Manifest",
|
|
@@ -12530,7 +13926,8 @@ const SCHEMA_KINDS = [
|
|
|
12530
13926
|
"CheckReport",
|
|
12531
13927
|
"ApplyResult",
|
|
12532
13928
|
"Profile",
|
|
12533
|
-
"Journal"
|
|
13929
|
+
"Journal",
|
|
13930
|
+
"RepoSnapshot"
|
|
12534
13931
|
];
|
|
12535
13932
|
const BY_KIND = {
|
|
12536
13933
|
Plan: PlanSchema,
|
|
@@ -12539,22 +13936,180 @@ const BY_KIND = {
|
|
|
12539
13936
|
CheckReport: CheckReportSchema,
|
|
12540
13937
|
ApplyResult: ApplyResultSchema,
|
|
12541
13938
|
Profile: ProfileSchema,
|
|
12542
|
-
Journal: JournalSchema
|
|
13939
|
+
Journal: JournalSchema,
|
|
13940
|
+
RepoSnapshot: RepoSnapshotSchema
|
|
12543
13941
|
};
|
|
12544
13942
|
function isSchemaKind(v) {
|
|
12545
13943
|
return typeof v === "string" && SCHEMA_KINDS.includes(v);
|
|
12546
13944
|
}
|
|
12547
|
-
function jsonSchemaFor(kind) {
|
|
12548
|
-
const json = toJSONSchema(BY_KIND[kind], {
|
|
12549
|
-
target: "draft-2020-12",
|
|
12550
|
-
io: "input",
|
|
12551
|
-
unrepresentable: "any"
|
|
12552
|
-
});
|
|
13945
|
+
function jsonSchemaFor(kind) {
|
|
13946
|
+
const json = toJSONSchema(BY_KIND[kind], {
|
|
13947
|
+
target: "draft-2020-12",
|
|
13948
|
+
io: "input",
|
|
13949
|
+
unrepresentable: "any"
|
|
13950
|
+
});
|
|
13951
|
+
return {
|
|
13952
|
+
$id: `https://axiom.dev/schemas/v2/${kind}.schema.json`,
|
|
13953
|
+
title: kind,
|
|
13954
|
+
...json
|
|
13955
|
+
};
|
|
13956
|
+
}
|
|
13957
|
+
const SIGNING_KEY_ENV = "AXIOM_SIGNING_KEY";
|
|
13958
|
+
function trustFilePath(root, rel = TRUST_FILE_DEFAULT) {
|
|
13959
|
+
return path.join(root, ...rel.split("/"));
|
|
13960
|
+
}
|
|
13961
|
+
function trustStatePath(root) {
|
|
13962
|
+
return path.join(root, ...TRUST_STATE_FILE.split("/"));
|
|
13963
|
+
}
|
|
13964
|
+
async function writeJsonAtomic(file, value, mode) {
|
|
13965
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
13966
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
13967
|
+
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
|
|
13968
|
+
encoding: "utf8",
|
|
13969
|
+
mode
|
|
13970
|
+
});
|
|
13971
|
+
if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
|
|
13972
|
+
await rename(tmp, file);
|
|
13973
|
+
}
|
|
13974
|
+
async function readJsonOrUndefined(file) {
|
|
13975
|
+
try {
|
|
13976
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
13977
|
+
} catch (err) {
|
|
13978
|
+
if (err.code === "ENOENT") return void 0;
|
|
13979
|
+
throw err;
|
|
13980
|
+
}
|
|
13981
|
+
}
|
|
13982
|
+
async function keygen(outDir, name) {
|
|
13983
|
+
const kp = generateKeyPair();
|
|
13984
|
+
const file = path.join(outDir, `axiom-signing-${kp.keyid.slice(0, 16)}.key`);
|
|
13985
|
+
await mkdir(outDir, { recursive: true });
|
|
13986
|
+
try {
|
|
13987
|
+
await writeFile(file, `${kp.privateKeyBase64}\n`, {
|
|
13988
|
+
encoding: "utf8",
|
|
13989
|
+
mode: 384,
|
|
13990
|
+
flag: "wx"
|
|
13991
|
+
});
|
|
13992
|
+
} catch (err) {
|
|
13993
|
+
if (err.code === "EEXIST") throw new AxiomError("ERR_EXISTS", `refusing to overwrite ${file}`, { path: file });
|
|
13994
|
+
throw err;
|
|
13995
|
+
}
|
|
13996
|
+
await chmod(file, 384).catch(() => void 0);
|
|
13997
|
+
const publicEntry = {
|
|
13998
|
+
keyid: kp.keyid,
|
|
13999
|
+
alg: "ed25519",
|
|
14000
|
+
publicKey: kp.publicKeyBase64
|
|
14001
|
+
};
|
|
14002
|
+
if (name !== void 0) publicEntry.name = name;
|
|
14003
|
+
return {
|
|
14004
|
+
publicEntry,
|
|
14005
|
+
privateKeyFile: file
|
|
14006
|
+
};
|
|
14007
|
+
}
|
|
14008
|
+
async function loadSigningKey(src) {
|
|
14009
|
+
let material;
|
|
14010
|
+
if (src.keyFile !== void 0) material = await readFile(path.resolve(src.keyFile), "utf8");
|
|
14011
|
+
else material = (src.env ?? process.env)[SIGNING_KEY_ENV];
|
|
14012
|
+
if (material === void 0 || material.trim().length === 0) throw new AxiomError("ERR_NOT_FOUND", `no signing key: set ${SIGNING_KEY_ENV} (base64 PKCS#8 or raw seed) or pass --key-file`);
|
|
14013
|
+
try {
|
|
14014
|
+
return privateKeyFrom(material);
|
|
14015
|
+
} catch (err) {
|
|
14016
|
+
throw new AxiomError("ERR_SIGNATURE_INVALID", "signing key material is not a usable ed25519 key", { cause: err });
|
|
14017
|
+
}
|
|
14018
|
+
}
|
|
14019
|
+
async function signBundle(bundle, src) {
|
|
14020
|
+
const key = await loadSigningKey(src);
|
|
14021
|
+
const keyid = keyidFor(key);
|
|
14022
|
+
const env = signEnvelope(bundle.manifest, key, keyid);
|
|
14023
|
+
const others = (bundle.signatures ?? []).filter((s) => !s.signatures.some((x) => x.keyid === keyid));
|
|
14024
|
+
return {
|
|
14025
|
+
bundle: {
|
|
14026
|
+
...bundle,
|
|
14027
|
+
signatures: [...others, env]
|
|
14028
|
+
},
|
|
14029
|
+
keyid
|
|
14030
|
+
};
|
|
14031
|
+
}
|
|
14032
|
+
async function verifyBundleAgainstRoot(root, bundle, rel = TRUST_FILE_DEFAULT) {
|
|
14033
|
+
const store = await loadTrustStore(root, rel);
|
|
14034
|
+
if (store === void 0) return void 0;
|
|
14035
|
+
const v = verifyBundleSignatures(bundle, store, bundle.manifest.counter);
|
|
12553
14036
|
return {
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
14037
|
+
trustFile: rel,
|
|
14038
|
+
keyids: v.keyids,
|
|
14039
|
+
findings: v.findings.map((f) => ({
|
|
14040
|
+
id: f.id,
|
|
14041
|
+
message: f.message
|
|
14042
|
+
})),
|
|
14043
|
+
ok: v.keyids.length > 0 && v.findings.length === 0
|
|
14044
|
+
};
|
|
14045
|
+
}
|
|
14046
|
+
async function loadTrustStore(root, rel = TRUST_FILE_DEFAULT) {
|
|
14047
|
+
const raw = await readJsonOrUndefined(trustFilePath(root, rel));
|
|
14048
|
+
if (raw === void 0) return void 0;
|
|
14049
|
+
const parsed = TrustStoreSchema.safeParse(raw);
|
|
14050
|
+
if (!parsed.success) throw new AxiomError("ERR_INVALID_PROFILE", `trust store ${rel} is invalid`, { details: { issues: parsed.error.issues.slice(0, 10).map((i) => i.message) } });
|
|
14051
|
+
return parsed.data;
|
|
14052
|
+
}
|
|
14053
|
+
function parsePublicEntry(raw) {
|
|
14054
|
+
const candidate = typeof raw === "object" && raw !== null && "publicEntry" in raw ? raw.publicEntry : raw;
|
|
14055
|
+
const parsed = TrustedKeySchema.safeParse(candidate);
|
|
14056
|
+
if (!parsed.success) throw new AxiomError("ERR_INVALID_PROFILE", "not a trusted-key entry", { details: { issues: parsed.error.issues.slice(0, 10).map((i) => i.message) } });
|
|
14057
|
+
const derived = keyidFor(publicKeyBase64(publicKeyFrom(parsed.data.publicKey)));
|
|
14058
|
+
if (derived !== parsed.data.keyid) throw new AxiomError("ERR_SIGNATURE_INVALID", "keyid does not match publicKey", { details: {
|
|
14059
|
+
keyid: parsed.data.keyid,
|
|
14060
|
+
derived
|
|
14061
|
+
} });
|
|
14062
|
+
return parsed.data;
|
|
14063
|
+
}
|
|
14064
|
+
async function trustAdd(root, entry, rel = TRUST_FILE_DEFAULT) {
|
|
14065
|
+
const store = await loadTrustStore(root, rel) ?? {
|
|
14066
|
+
version: 1,
|
|
14067
|
+
keys: []
|
|
14068
|
+
};
|
|
14069
|
+
const keys = store.keys.filter((k) => k.keyid !== entry.keyid);
|
|
14070
|
+
keys.push(entry);
|
|
14071
|
+
keys.sort((a, b) => a.keyid < b.keyid ? -1 : a.keyid > b.keyid ? 1 : 0);
|
|
14072
|
+
const next = {
|
|
14073
|
+
...store,
|
|
14074
|
+
keys
|
|
14075
|
+
};
|
|
14076
|
+
await writeJsonAtomic(trustFilePath(root, rel), next);
|
|
14077
|
+
return next;
|
|
14078
|
+
}
|
|
14079
|
+
async function trustRemove(root, keyid, rel = TRUST_FILE_DEFAULT) {
|
|
14080
|
+
const store = await loadTrustStore(root, rel);
|
|
14081
|
+
if (store === void 0) throw new AxiomError("ERR_NOT_FOUND", `trust store ${rel} not found`);
|
|
14082
|
+
const keys = store.keys.filter((k) => k.keyid !== keyid);
|
|
14083
|
+
if (keys.length === store.keys.length) throw new AxiomError("ERR_NOT_FOUND", `keyid ${keyid} not in trust store`);
|
|
14084
|
+
const next = {
|
|
14085
|
+
...store,
|
|
14086
|
+
keys
|
|
12557
14087
|
};
|
|
14088
|
+
await writeJsonAtomic(trustFilePath(root, rel), next);
|
|
14089
|
+
return next;
|
|
14090
|
+
}
|
|
14091
|
+
async function loadTrustState(root) {
|
|
14092
|
+
const raw = await readJsonOrUndefined(trustStatePath(root));
|
|
14093
|
+
if (raw === void 0) return void 0;
|
|
14094
|
+
const parsed = TrustStateSchema.safeParse(raw);
|
|
14095
|
+
if (!parsed.success) throw new AxiomError("ERR_JOURNAL_CORRUPT", `${TRUST_STATE_FILE} is invalid`);
|
|
14096
|
+
return parsed.data;
|
|
14097
|
+
}
|
|
14098
|
+
async function advanceTrustState(root, bundle) {
|
|
14099
|
+
const counter = bundle.manifest.counter;
|
|
14100
|
+
if (counter === void 0) return void 0;
|
|
14101
|
+
const cur = await loadTrustState(root);
|
|
14102
|
+
if (cur !== void 0 && cur.lastCounter >= counter) return cur;
|
|
14103
|
+
const next = {
|
|
14104
|
+
version: 1,
|
|
14105
|
+
lastCounter: counter,
|
|
14106
|
+
manifestDigest: bundle.manifestDigest
|
|
14107
|
+
};
|
|
14108
|
+
await writeJsonAtomic(trustStatePath(root), next);
|
|
14109
|
+
return next;
|
|
14110
|
+
}
|
|
14111
|
+
function profileWantsAntiRollback(checks) {
|
|
14112
|
+
return checks.some((c) => c.predicate === "manifest.requireSigned" && typeof c.params === "object" && c.params !== null && c.params.antiRollback === true);
|
|
12558
14113
|
}
|
|
12559
14114
|
const LOG_LEVELS = [
|
|
12560
14115
|
"error",
|
|
@@ -12593,7 +14148,7 @@ function createLogger(opts = {}) {
|
|
|
12593
14148
|
};
|
|
12594
14149
|
}
|
|
12595
14150
|
const silentLogger = createLogger({ write: () => {} });
|
|
12596
|
-
const realpathNative = promisify(realpath.native);
|
|
14151
|
+
const realpathNative = promisify(realpath$1.native);
|
|
12597
14152
|
const IS_WIN32 = process.platform === "win32";
|
|
12598
14153
|
async function realDir(p, code) {
|
|
12599
14154
|
let real;
|
|
@@ -25387,80 +26942,205 @@ const EMPTY_COMPLETION_RESULT = { completion: {
|
|
|
25387
26942
|
values: [],
|
|
25388
26943
|
hasMore: false
|
|
25389
26944
|
} };
|
|
25390
|
-
|
|
25391
|
-
|
|
25392
|
-
|
|
25393
|
-
|
|
25394
|
-
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
26945
|
+
const SNAPSHOT_MAX_FILES_DEFAULT = 2e4;
|
|
26946
|
+
const SNAPSHOT_MAX_FILES_CAP = 5e4;
|
|
26947
|
+
const SNAPSHOT_MAX_BYTES_DEFAULT = 67108864;
|
|
26948
|
+
const ALWAYS_SKIP = /* @__PURE__ */ new Set([".git", ".axiom"]);
|
|
26949
|
+
function globToRegExp(glob) {
|
|
26950
|
+
let re = "^";
|
|
26951
|
+
for (let i = 0; i < glob.length; i++) {
|
|
26952
|
+
const c = glob[i];
|
|
26953
|
+
if (c === "*") {
|
|
26954
|
+
if (glob[i + 1] === "*") {
|
|
26955
|
+
i++;
|
|
26956
|
+
if (glob[i + 1] === "/") {
|
|
26957
|
+
i++;
|
|
26958
|
+
re += "(?:.*/)?";
|
|
26959
|
+
} else re += ".*";
|
|
26960
|
+
} else re += "[^/]*";
|
|
26961
|
+
} else if (c === "?") re += "[^/]";
|
|
26962
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
26963
|
+
}
|
|
26964
|
+
return new RegExp(`${re}$`);
|
|
25398
26965
|
}
|
|
25399
|
-
function
|
|
25400
|
-
|
|
25401
|
-
const
|
|
25402
|
-
|
|
25403
|
-
return parsed.data;
|
|
26966
|
+
function matcherOf(globs, whenEmpty) {
|
|
26967
|
+
if (globs === void 0 || globs.length === 0) return () => whenEmpty;
|
|
26968
|
+
const res = globs.map(globToRegExp);
|
|
26969
|
+
return (rel) => res.some((r) => r.test(rel));
|
|
25404
26970
|
}
|
|
25405
|
-
|
|
25406
|
-
|
|
25407
|
-
|
|
25408
|
-
await writeFile(tmp, JSON.stringify(value), "utf8");
|
|
25409
|
-
await rename(tmp, file);
|
|
26971
|
+
function validateGlob(g, label) {
|
|
26972
|
+
const probe = g.replace(/\*+/g, "x").replace(/\?/g, "x").replace(/\/+$/, "");
|
|
26973
|
+
if (probe.length === 0 || !isValidRelPath(probe)) throw new AxiomError("ERR_CONTAINMENT", `${label} glob must be a contained relative path: ${g}`, { details: { glob: g } });
|
|
25410
26974
|
}
|
|
25411
|
-
async function
|
|
26975
|
+
async function gitignoreMatcher(root) {
|
|
26976
|
+
let text;
|
|
25412
26977
|
try {
|
|
25413
|
-
|
|
25414
|
-
} catch
|
|
25415
|
-
|
|
25416
|
-
throw err;
|
|
26978
|
+
text = await readFile(path.join(root, ".gitignore"), "utf8");
|
|
26979
|
+
} catch {
|
|
26980
|
+
return () => false;
|
|
25417
26981
|
}
|
|
26982
|
+
const globs = [];
|
|
26983
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
26984
|
+
const l = raw.trim();
|
|
26985
|
+
if (l.length === 0 || l.startsWith("#") || l.startsWith("!")) continue;
|
|
26986
|
+
let pat = l.startsWith("/") ? l.slice(1) : l.includes("/") ? l : `**/${l}`;
|
|
26987
|
+
if (pat.endsWith("/")) pat = pat.slice(0, -1);
|
|
26988
|
+
globs.push(pat, `${pat}/**`);
|
|
26989
|
+
}
|
|
26990
|
+
return matcherOf(globs, false);
|
|
25418
26991
|
}
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
26992
|
+
function sha256File(abs) {
|
|
26993
|
+
return new Promise((resolve, reject) => {
|
|
26994
|
+
const h = createHash("sha256");
|
|
26995
|
+
createReadStream(abs).on("data", (chunk) => h.update(chunk)).on("error", reject).on("end", () => resolve(h.digest("hex")));
|
|
26996
|
+
});
|
|
25423
26997
|
}
|
|
25424
|
-
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
26998
|
+
function modeOf(mode) {
|
|
26999
|
+
return process.platform !== "win32" && (mode & 64) !== 0 ? "0755" : "0644";
|
|
27000
|
+
}
|
|
27001
|
+
async function snapshotRoot(root, opts = {}) {
|
|
27002
|
+
if (opts.followSymlinks === true) throw new AxiomError("ERR_UNSUPPORTED_OP", "followSymlinks is not supported (symlinks are recorded, never followed)");
|
|
27003
|
+
const rootReal = await realpath(root);
|
|
27004
|
+
const maxFiles = Math.min(opts.maxFiles ?? 2e4, SNAPSHOT_MAX_FILES_CAP);
|
|
27005
|
+
const maxBytes = opts.maxBytes ?? 67108864;
|
|
27006
|
+
if (maxFiles < 1 || maxBytes < 0) throw new AxiomError("ERR_INVALID_PLAN", "maxFiles must be ≥ 1 and maxBytes ≥ 0", { details: {
|
|
27007
|
+
maxFiles,
|
|
27008
|
+
maxBytes
|
|
27009
|
+
} });
|
|
27010
|
+
for (const g of opts.include ?? []) validateGlob(g, "include");
|
|
27011
|
+
for (const g of opts.exclude ?? []) validateGlob(g, "exclude");
|
|
27012
|
+
const include = matcherOf(opts.include, true);
|
|
27013
|
+
const exclude = matcherOf(opts.exclude, false);
|
|
27014
|
+
const ignored = opts.respectGitignore === false ? () => false : await gitignoreMatcher(rootReal);
|
|
27015
|
+
const withDigest = opts.withContentDigest !== false;
|
|
27016
|
+
const files = [];
|
|
27017
|
+
let bytes = 0;
|
|
27018
|
+
let truncated = false;
|
|
27019
|
+
const visit = async (dirAbs, dirRel) => {
|
|
27020
|
+
let entries;
|
|
27021
|
+
try {
|
|
27022
|
+
const dir = await opendir(dirAbs);
|
|
27023
|
+
entries = [];
|
|
27024
|
+
for await (const e of dir) entries.push(e);
|
|
27025
|
+
} catch {
|
|
27026
|
+
return;
|
|
27027
|
+
}
|
|
27028
|
+
const sortKey = (d) => d.isDirectory() ? `${d.name}/` : d.name;
|
|
27029
|
+
entries.sort((a, b) => compareUtf8(sortKey(a), sortKey(b)));
|
|
27030
|
+
for (const e of entries) {
|
|
27031
|
+
if (truncated) return;
|
|
27032
|
+
const rel = dirRel === "" ? e.name : `${dirRel}/${e.name}`;
|
|
27033
|
+
if (!isValidRelPath(rel)) continue;
|
|
27034
|
+
if (e.isDirectory()) {
|
|
27035
|
+
if (ALWAYS_SKIP.has(e.name) || ignored(rel) || exclude(rel)) continue;
|
|
27036
|
+
await visit(path.join(dirAbs, e.name), rel);
|
|
27037
|
+
continue;
|
|
27038
|
+
}
|
|
27039
|
+
if (ignored(rel) || !include(rel) || exclude(rel)) continue;
|
|
27040
|
+
const abs = path.join(dirAbs, e.name);
|
|
27041
|
+
let entry;
|
|
27042
|
+
if (e.isSymbolicLink()) entry = await symlinkEntry(rootReal, abs, rel, withDigest);
|
|
27043
|
+
else if (e.isFile()) entry = await fileEntry(abs, rel, withDigest);
|
|
27044
|
+
if (entry === void 0) continue;
|
|
27045
|
+
if (files.length >= maxFiles || bytes + entry.bytes > maxBytes) {
|
|
27046
|
+
truncated = true;
|
|
27047
|
+
return;
|
|
27048
|
+
}
|
|
27049
|
+
files.push(entry);
|
|
27050
|
+
bytes += entry.bytes;
|
|
27051
|
+
}
|
|
27052
|
+
};
|
|
27053
|
+
await visit(rootReal, "");
|
|
27054
|
+
files.sort((a, b) => compareUtf8(a.path, b.path));
|
|
27055
|
+
const body = {
|
|
27056
|
+
files,
|
|
27057
|
+
truncated,
|
|
27058
|
+
counts: {
|
|
27059
|
+
files: files.length,
|
|
27060
|
+
bytes
|
|
27061
|
+
}
|
|
27062
|
+
};
|
|
27063
|
+
return {
|
|
27064
|
+
apiVersion: "axiom.dev/v2",
|
|
27065
|
+
kind: "RepoSnapshot",
|
|
27066
|
+
root: { kind: "relative" },
|
|
27067
|
+
snapshotDigest: canonicalDigestRef(body),
|
|
27068
|
+
body
|
|
27069
|
+
};
|
|
25428
27070
|
}
|
|
25429
|
-
async function
|
|
25430
|
-
|
|
25431
|
-
|
|
25432
|
-
|
|
27071
|
+
async function fileEntry(abs, rel, withDigest) {
|
|
27072
|
+
let st;
|
|
27073
|
+
try {
|
|
27074
|
+
st = await lstat(abs);
|
|
27075
|
+
} catch {
|
|
27076
|
+
return;
|
|
25433
27077
|
}
|
|
25434
|
-
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
|
|
27078
|
+
if (!st.isFile()) return void 0;
|
|
27079
|
+
const entry = {
|
|
27080
|
+
path: rel,
|
|
27081
|
+
bytes: st.size,
|
|
27082
|
+
mode: modeOf(st.mode),
|
|
27083
|
+
kind: "file"
|
|
27084
|
+
};
|
|
27085
|
+
if (withDigest) try {
|
|
27086
|
+
entry.sha256 = await sha256File(abs);
|
|
27087
|
+
} catch {
|
|
27088
|
+
return;
|
|
25439
27089
|
}
|
|
27090
|
+
return entry;
|
|
25440
27091
|
}
|
|
25441
|
-
async function
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
|
|
27092
|
+
async function symlinkEntry(rootReal, abs, rel, withDigest) {
|
|
27093
|
+
const entry = {
|
|
27094
|
+
path: rel,
|
|
27095
|
+
bytes: 0,
|
|
27096
|
+
mode: "0644",
|
|
27097
|
+
kind: "symlink"
|
|
27098
|
+
};
|
|
27099
|
+
let target;
|
|
27100
|
+
try {
|
|
27101
|
+
target = await realpath(abs);
|
|
27102
|
+
} catch {
|
|
27103
|
+
return entry;
|
|
27104
|
+
}
|
|
27105
|
+
if (!isSameOrInside(rootReal, target)) return entry;
|
|
27106
|
+
let st;
|
|
27107
|
+
try {
|
|
27108
|
+
st = await lstat(target);
|
|
27109
|
+
} catch {
|
|
27110
|
+
return entry;
|
|
27111
|
+
}
|
|
27112
|
+
if (!st.isFile()) return entry;
|
|
27113
|
+
entry.bytes = st.size;
|
|
27114
|
+
entry.mode = modeOf(st.mode);
|
|
27115
|
+
if (withDigest) try {
|
|
27116
|
+
entry.sha256 = await sha256File(target);
|
|
27117
|
+
} catch {
|
|
27118
|
+
entry.bytes = 0;
|
|
25445
27119
|
}
|
|
27120
|
+
return entry;
|
|
25446
27121
|
}
|
|
25447
|
-
|
|
25448
|
-
const
|
|
25449
|
-
|
|
25450
|
-
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
27122
|
+
function diffSnapshots(a, b) {
|
|
27123
|
+
const key = (e) => e.sha256 ?? `bytes:${e.bytes}:${e.kind}`;
|
|
27124
|
+
const ma = new Map(a.body.files.map((e) => [e.path, e]));
|
|
27125
|
+
const mb = new Map(b.body.files.map((e) => [e.path, e]));
|
|
27126
|
+
const out = {
|
|
27127
|
+
added: [],
|
|
27128
|
+
removed: [],
|
|
27129
|
+
changed: []
|
|
27130
|
+
};
|
|
27131
|
+
for (const [p, eb] of mb) {
|
|
27132
|
+
const ea = ma.get(p);
|
|
27133
|
+
if (ea === void 0) out.added.push(p);
|
|
27134
|
+
else if (key(ea) !== key(eb) || ea.mode !== eb.mode || ea.kind !== eb.kind) out.changed.push({
|
|
27135
|
+
path: p,
|
|
27136
|
+
from: ea.sha256 ?? null,
|
|
27137
|
+
to: eb.sha256 ?? null
|
|
27138
|
+
});
|
|
25463
27139
|
}
|
|
27140
|
+
for (const p of ma.keys()) if (!mb.has(p)) out.removed.push(p);
|
|
27141
|
+
out.added.sort(compareUtf8);
|
|
27142
|
+
out.removed.sort(compareUtf8);
|
|
27143
|
+
out.changed.sort((x, y) => compareUtf8(x.path, y.path));
|
|
25464
27144
|
return out;
|
|
25465
27145
|
}
|
|
25466
27146
|
const BUNDLE_BYTES_MAX = 4194304;
|
|
@@ -25567,7 +27247,16 @@ const ManifestVerifyOutput = object$1({
|
|
|
25567
27247
|
code: ErrorCodeSchema,
|
|
25568
27248
|
message: string(),
|
|
25569
27249
|
path: string().optional()
|
|
25570
|
-
}))
|
|
27250
|
+
})),
|
|
27251
|
+
signatures: object$1({
|
|
27252
|
+
trustFile: string(),
|
|
27253
|
+
keyids: array(string()),
|
|
27254
|
+
findings: array(object$1({
|
|
27255
|
+
id: string(),
|
|
27256
|
+
message: string()
|
|
27257
|
+
})),
|
|
27258
|
+
ok: boolean()
|
|
27259
|
+
}).optional()
|
|
25571
27260
|
});
|
|
25572
27261
|
const RollbackOutput = object$1({
|
|
25573
27262
|
manifestDigest: DigestRefSchema,
|
|
@@ -25631,7 +27320,10 @@ const TOOL_DEFS = [
|
|
|
25631
27320
|
})
|
|
25632
27321
|
};
|
|
25633
27322
|
try {
|
|
25634
|
-
const { bundle } = await compilePlan(parsed.data, {
|
|
27323
|
+
const { bundle } = await compilePlan(parsed.data, {
|
|
27324
|
+
store: "inline",
|
|
27325
|
+
emitters: EMITTERS
|
|
27326
|
+
});
|
|
25635
27327
|
return {
|
|
25636
27328
|
ok: true,
|
|
25637
27329
|
planDigest: bundle.manifest.planDigest,
|
|
@@ -25654,7 +27346,7 @@ const TOOL_DEFS = [
|
|
|
25654
27346
|
defineTool({
|
|
25655
27347
|
name: "axiom_plan_compile",
|
|
25656
27348
|
title: "Compile a Plan into a ManifestBundle",
|
|
25657
|
-
description: "Compile a Plan into a content-addressed ManifestBundle (sorted artifacts, sha256 digests, in-toto planDigest). `store: cas` writes blobs under <root>/.axiom/cas instead of inlining them. When a root is given the bundle is stored under <root>/.axiom/manifests/<hex>.json so later tools can reference it by digest.",
|
|
27349
|
+
description: "Compile a Plan into a content-addressed ManifestBundle (sorted artifacts, sha256 digests, in-toto planDigest). `store: cas` writes blobs under <root>/.axiom/cas instead of inlining them. When a root is given the bundle is stored under <root>/.axiom/manifests/<hex>.json so later tools can reference it by digest. `template` sources are rendered by the built-in `web` emitter (see `axiom emitters`); its version is recorded in toolchain.emitters.",
|
|
25658
27350
|
inputSchema: {
|
|
25659
27351
|
plan: LooseObject.describe("Plan document"),
|
|
25660
27352
|
store: _enum(["inline", "cas"]).optional().describe("Blob transport; default inline"),
|
|
@@ -25665,7 +27357,10 @@ const TOOL_DEFS = [
|
|
|
25665
27357
|
async handler(ctx, { plan, store, root }) {
|
|
25666
27358
|
guardPayloadSize("plan", plan);
|
|
25667
27359
|
const rootReal = root !== void 0 || store === "cas" ? (await resolveRoot(ctx.policy, root)).rootReal : void 0;
|
|
25668
|
-
const opts = {
|
|
27360
|
+
const opts = {
|
|
27361
|
+
store: store ?? "inline",
|
|
27362
|
+
emitters: EMITTERS
|
|
27363
|
+
};
|
|
25669
27364
|
if (rootReal !== void 0) opts.root = rootReal;
|
|
25670
27365
|
const { bundle } = await compilePlan(plan, opts);
|
|
25671
27366
|
if (rootReal !== void 0) {
|
|
@@ -25690,11 +27385,14 @@ const TOOL_DEFS = [
|
|
|
25690
27385
|
defineTool({
|
|
25691
27386
|
name: "axiom_manifest_verify",
|
|
25692
27387
|
title: "Verify a ManifestBundle",
|
|
25693
|
-
description: "Structural and content-address verification: schema, recomputed manifestDigest, every inline blob hashes to its key, attestation subject matches. Never writes.",
|
|
25694
|
-
inputSchema: {
|
|
27388
|
+
description: "Structural and content-address verification: schema, recomputed manifestDigest, every inline blob hashes to its key, attestation subject matches. When a root with .axiom/trust/keys.json is available, detached DSSE signatures are verified and the trusted keyids are reported under `signatures`. Never writes.",
|
|
27389
|
+
inputSchema: {
|
|
27390
|
+
bundle: LooseObject.describe("ManifestBundle"),
|
|
27391
|
+
root: RootArg
|
|
27392
|
+
},
|
|
25695
27393
|
outputSchema: ManifestVerifyOutput,
|
|
25696
27394
|
annotations: READ,
|
|
25697
|
-
async handler(
|
|
27395
|
+
async handler(ctx, { bundle, root }) {
|
|
25698
27396
|
guardPayloadSize("bundle", bundle);
|
|
25699
27397
|
const r = verifyBundle(bundle);
|
|
25700
27398
|
const out = {
|
|
@@ -25705,12 +27403,25 @@ const TOOL_DEFS = [
|
|
|
25705
27403
|
errors: r.errors
|
|
25706
27404
|
};
|
|
25707
27405
|
if (r.manifestDigest !== void 0) out.manifestDigest = r.manifestDigest;
|
|
27406
|
+
if (r.ok) {
|
|
27407
|
+
const rootReal = await optionalRoot(ctx, root);
|
|
27408
|
+
if (rootReal !== void 0) {
|
|
27409
|
+
const sig = await verifyBundleAgainstRoot(rootReal, parseBundle(bundle));
|
|
27410
|
+
if (sig !== void 0) {
|
|
27411
|
+
out.signatures = sig;
|
|
27412
|
+
out.signed = sig.keyids.length > 0;
|
|
27413
|
+
if (!sig.ok) out.ok = false;
|
|
27414
|
+
}
|
|
27415
|
+
}
|
|
27416
|
+
}
|
|
25708
27417
|
return out;
|
|
25709
27418
|
},
|
|
25710
27419
|
summarize: (o) => ({
|
|
25711
27420
|
ok: o.ok,
|
|
25712
27421
|
manifestDigest: o.manifestDigest,
|
|
25713
27422
|
canonical: o.canonical,
|
|
27423
|
+
signed: o.signed,
|
|
27424
|
+
keyids: o.signatures?.keyids,
|
|
25714
27425
|
missing: o.missing.length,
|
|
25715
27426
|
errors: o.errors.slice(0, 20)
|
|
25716
27427
|
})
|
|
@@ -25776,6 +27487,7 @@ const TOOL_DEFS = [
|
|
|
25776
27487
|
manifestDigest: parsed.manifestDigest
|
|
25777
27488
|
} });
|
|
25778
27489
|
const { rootReal } = await resolveRoot(ctx.policy, root);
|
|
27490
|
+
const profileDoc = await profileFor(ctx, parsed, profile, rootReal);
|
|
25779
27491
|
const result = await apply({
|
|
25780
27492
|
bundle: parsed,
|
|
25781
27493
|
root: rootReal,
|
|
@@ -25789,6 +27501,7 @@ const TOOL_DEFS = [
|
|
|
25789
27501
|
await saveManifest(rootReal, parsed);
|
|
25790
27502
|
ctx.seenRoots.add(rootReal);
|
|
25791
27503
|
}
|
|
27504
|
+
if (result.status === "applied" && profileWantsAntiRollback([...profileDoc.checks, ...parsed.manifest.checks])) await advanceTrustState(rootReal, parsed);
|
|
25792
27505
|
ctx.log.info("apply", {
|
|
25793
27506
|
manifestDigest: parsed.manifestDigest,
|
|
25794
27507
|
status: result.status,
|
|
@@ -25891,6 +27604,46 @@ const TOOL_DEFS = [
|
|
|
25891
27604
|
return { roots };
|
|
25892
27605
|
},
|
|
25893
27606
|
summarize: (o) => o
|
|
27607
|
+
}),
|
|
27608
|
+
defineTool({
|
|
27609
|
+
name: "axiom_repo_snapshot",
|
|
27610
|
+
title: "Snapshot a root",
|
|
27611
|
+
description: "Deterministic, content-addressed inventory of a root: every regular file (and symlink) as { path, bytes, sha256, mode, kind }, sorted by code point, with snapshotDigest = sha256(JCS(body)). No timestamps, no absolute paths — the same tree gives the same digest on every machine. Honours the root .gitignore, always skips .git/ and .axiom/, never follows symlinks, never leaves the root. Use it to build Plans against real pre-image digests, or diff two snapshots with `axiom snapshot-diff`.",
|
|
27612
|
+
inputSchema: {
|
|
27613
|
+
root: RootArg,
|
|
27614
|
+
include: array(string().min(1)).optional().describe("Relative globs (*, **, ?) to keep; default everything"),
|
|
27615
|
+
exclude: array(string().min(1)).optional().describe("Relative globs to drop"),
|
|
27616
|
+
maxFiles: int().min(1).max(SNAPSHOT_MAX_FILES_CAP).default(SNAPSHOT_MAX_FILES_DEFAULT).describe(`Stop after this many files (cap ${SNAPSHOT_MAX_FILES_CAP}); sets truncated`),
|
|
27617
|
+
maxBytes: int().nonnegative().default(SNAPSHOT_MAX_BYTES_DEFAULT).describe("Stop once the summed size would exceed this; sets truncated"),
|
|
27618
|
+
followSymlinks: literal(false).default(false).describe("Always false; symlinks are recorded, never followed"),
|
|
27619
|
+
respectGitignore: boolean().default(true),
|
|
27620
|
+
withContentDigest: boolean().default(true).describe("false → sizes only, no sha256")
|
|
27621
|
+
},
|
|
27622
|
+
outputSchema: RepoSnapshotSchema,
|
|
27623
|
+
annotations: READ,
|
|
27624
|
+
async handler(ctx, input) {
|
|
27625
|
+
const { rootReal } = await resolveRoot(ctx.policy, input.root);
|
|
27626
|
+
const opts = {
|
|
27627
|
+
maxFiles: input.maxFiles,
|
|
27628
|
+
maxBytes: input.maxBytes,
|
|
27629
|
+
respectGitignore: input.respectGitignore,
|
|
27630
|
+
withContentDigest: input.withContentDigest
|
|
27631
|
+
};
|
|
27632
|
+
if (input.include !== void 0) opts.include = input.include;
|
|
27633
|
+
if (input.exclude !== void 0) opts.exclude = input.exclude;
|
|
27634
|
+
const snap = await snapshotRoot(rootReal, opts);
|
|
27635
|
+
ctx.log.debug("snapshot", {
|
|
27636
|
+
root: rootReal,
|
|
27637
|
+
files: snap.body.counts.files
|
|
27638
|
+
});
|
|
27639
|
+
return snap;
|
|
27640
|
+
},
|
|
27641
|
+
summarize: (o) => ({
|
|
27642
|
+
snapshotDigest: o.snapshotDigest,
|
|
27643
|
+
counts: o.body.counts,
|
|
27644
|
+
truncated: o.body.truncated,
|
|
27645
|
+
paths: o.body.files.slice(0, 20).map((f) => f.path)
|
|
27646
|
+
})
|
|
25894
27647
|
})
|
|
25895
27648
|
];
|
|
25896
27649
|
async function resolveBundleOrRef(ctx, v, label) {
|
|
@@ -26083,6 +27836,10 @@ function createServer(policy, opts = {}) {
|
|
|
26083
27836
|
text: JSON.stringify(jsonSchemaFor(k), null, 2)
|
|
26084
27837
|
}] };
|
|
26085
27838
|
});
|
|
27839
|
+
server.registerResource("emitters", "axiom://emitters", {
|
|
27840
|
+
title: "Template emitters available to axiom_plan_compile",
|
|
27841
|
+
mimeType: "application/json"
|
|
27842
|
+
}, async (uri) => json(uri.href, emitterCatalogue()));
|
|
26086
27843
|
log.info("server created", {
|
|
26087
27844
|
name: SERVER_NAME,
|
|
26088
27845
|
version: SERVER_VERSION,
|
|
@@ -26097,20 +27854,34 @@ const help = (version) => `axiom ${version} — transactional write gate for AI
|
|
|
26097
27854
|
|
|
26098
27855
|
Usage:
|
|
26099
27856
|
axiom mcp [--root <abs>]... [--allow-guards] [--guard-allowlist <abs>]... [--log-level ${LOG_LEVELS.join("|")}]
|
|
27857
|
+
[--http <host:port>] [--http-token-env <NAME>]
|
|
26100
27858
|
axiom compile <plan.json|plan.axm> [-o <out.json>] [--store inline|cas] [--root <dir>]
|
|
26101
|
-
|
|
27859
|
+
[--allow-net [--net-allow <host>[,host]]] [--allow-file] (ref sources; offline by default)
|
|
27860
|
+
axiom verify <bundle.json> [--root <dir>] (with --root: also verify signatures against .axiom/trust/keys.json)
|
|
26102
27861
|
axiom check <bundle.json> --root <dir> [--profile <name>] [--json] [--allow-guards] [--guard-allowlist <abs>]...
|
|
26103
27862
|
axiom apply <bundle.json> --root <dir> [--dry-run] [--profile <name>] [--confirm <digest>]
|
|
26104
27863
|
[--pr [--branch <name>] [--message <text>]]
|
|
26105
27864
|
[--allow-guards] [--guard-allowlist <abs>]...
|
|
26106
27865
|
axiom rollback <digest> --root <dir>
|
|
27866
|
+
axiom gc --root <dir> [--dry-run] [--older-than <n>(ms|s|m|h|d)] [--keep all-manifests|journal] (CAS garbage collection; CLI only)
|
|
26107
27867
|
axiom diff <a.json> <b.json>
|
|
26108
27868
|
axiom schema <${SCHEMA_KINDS.join("|")}>
|
|
27869
|
+
axiom emitters [--json] (template emitters available to \`compile\`)
|
|
27870
|
+
axiom keygen [--out <dir>] [--name <label>] (ed25519; private key → file 0600, public entry → stdout)
|
|
27871
|
+
axiom sign <bundle.json> [--key-file <path>] [-o <out.json>] (private key from --key-file or $${SIGNING_KEY_ENV})
|
|
27872
|
+
axiom trust add <pubkey.json> --root <dir> | trust remove <keyid> --root <dir> | trust list --root <dir>
|
|
26109
27873
|
axiom gate --stdin [--root <dir>] [--profile <file>] [--strict] [--log-level ...] (PreToolUse hook; exit 0 allow / 2 deny)
|
|
27874
|
+
axiom migrate v1 <manifest.json> [-o <plan.json>] [--profile <name>] [--cas <root>] [--content <dir>] [--overwrite]
|
|
27875
|
+
(v1 manifest → v2 Plan; exit 1 = migrated with warnings)
|
|
27876
|
+
axiom snapshot --root <dir> [-o <out.json>] [--include <glob>]... [--exclude <glob>]... [--max-files <n>] [--no-gitignore] [--no-digest]
|
|
27877
|
+
axiom snapshot-diff <a.json> <b.json> (RepoSnapshot files → { added, removed, changed })
|
|
26110
27878
|
axiom --version | --help
|
|
26111
27879
|
|
|
26112
27880
|
Exit codes: 0 ok · 1 verdict fail / apply failed · 2 usage or error.
|
|
26113
27881
|
The \`mcp\` verb speaks JSON-RPC on stdout and logs JSON lines on stderr; every other verb prints JSON to stdout.
|
|
27882
|
+
With --http it serves Streamable HTTP at http://<host:port>/mcp instead (port 0 = random; the URL is logged at
|
|
27883
|
+
info level). A non-loopback host requires a bearer token in the env var named by --http-token-env
|
|
27884
|
+
(default AXIOM_HTTP_TOKEN); loopback binds accept an optional token.
|
|
26114
27885
|
A \`.axm\` plan with errors prints its diagnostics as JSON and exits 2.
|
|
26115
27886
|
\`guard.external\` checks run only with --allow-guards AND a profile that sets facts.allowGuards; absolute
|
|
26116
27887
|
commands must additionally appear in --guard-allowlist (relative ones must live under <root>/scripts/).
|
|
@@ -26179,6 +27950,8 @@ async function cmdMcp(argv) {
|
|
|
26179
27950
|
multiple: true
|
|
26180
27951
|
},
|
|
26181
27952
|
"log-level": { type: "string" },
|
|
27953
|
+
http: { type: "string" },
|
|
27954
|
+
"http-token-env": { type: "string" },
|
|
26182
27955
|
...GUARD_FLAGS
|
|
26183
27956
|
});
|
|
26184
27957
|
const level = values["log-level"] ?? "warn";
|
|
@@ -26188,6 +27961,32 @@ async function cmdMcp(argv) {
|
|
|
26188
27961
|
if (policy.roots.size === 0) log.warn("no --root given; every root-taking tool will fail with ERR_ROOT_REQUIRED");
|
|
26189
27962
|
const guards = guardOptions(values);
|
|
26190
27963
|
if (guards.allowGuards) log.warn("external guards ENABLED (--allow-guards)", { allowlist: guards.guardAllowlist });
|
|
27964
|
+
if (values.http !== void 0) {
|
|
27965
|
+
const { parseHostPort, startHttp, HTTP_TOKEN_ENV_DEFAULT } = await import("./http-lazy.js");
|
|
27966
|
+
const { host, port } = parseHostPort(values.http);
|
|
27967
|
+
const tokenEnv = values["http-token-env"] ?? HTTP_TOKEN_ENV_DEFAULT;
|
|
27968
|
+
const token = process.env[tokenEnv];
|
|
27969
|
+
const httpOpts = {
|
|
27970
|
+
host,
|
|
27971
|
+
port,
|
|
27972
|
+
log
|
|
27973
|
+
};
|
|
27974
|
+
if (token !== void 0 && token.length > 0) httpOpts.token = token;
|
|
27975
|
+
const handle = await startHttp(() => createServer(policy, {
|
|
27976
|
+
log,
|
|
27977
|
+
guards
|
|
27978
|
+
}), httpOpts);
|
|
27979
|
+
await new Promise((resolve) => {
|
|
27980
|
+
const stop = () => {
|
|
27981
|
+
handle.close().then(resolve);
|
|
27982
|
+
};
|
|
27983
|
+
process.once("SIGINT", stop);
|
|
27984
|
+
process.once("SIGTERM", stop);
|
|
27985
|
+
process.stdin.on("end", stop);
|
|
27986
|
+
process.stdin.resume();
|
|
27987
|
+
});
|
|
27988
|
+
return EXIT_OK;
|
|
27989
|
+
}
|
|
26191
27990
|
const server = createServer(policy, {
|
|
26192
27991
|
log,
|
|
26193
27992
|
guards
|
|
@@ -26208,15 +28007,31 @@ async function cmdCompile(argv) {
|
|
|
26208
28007
|
short: "o"
|
|
26209
28008
|
},
|
|
26210
28009
|
store: { type: "string" },
|
|
26211
|
-
root: { type: "string" }
|
|
28010
|
+
root: { type: "string" },
|
|
28011
|
+
"allow-net": { type: "boolean" },
|
|
28012
|
+
"net-allow": {
|
|
28013
|
+
type: "string",
|
|
28014
|
+
multiple: true
|
|
28015
|
+
},
|
|
28016
|
+
"allow-file": { type: "boolean" }
|
|
26212
28017
|
});
|
|
26213
28018
|
const file = positionals[0];
|
|
26214
28019
|
if (file === void 0) throw new UsageError("compile: <plan.json|plan.axm> is required");
|
|
26215
28020
|
const store = values.store ?? "inline";
|
|
26216
28021
|
if (store !== "inline" && store !== "cas") throw new UsageError("--store must be inline|cas");
|
|
26217
|
-
const
|
|
28022
|
+
const allowNet = values["allow-net"] === true;
|
|
28023
|
+
const allowlist = (values["net-allow"] ?? []).flatMap((s) => s.split(",")).filter((s) => s !== "");
|
|
28024
|
+
if (!allowNet && allowlist.length > 0) throw new UsageError("--net-allow requires --allow-net");
|
|
28025
|
+
const net = { allowNet };
|
|
28026
|
+
if (allowlist.length > 0) net.allowlist = allowlist;
|
|
28027
|
+
if (values["allow-file"] === true) net.allowFile = true;
|
|
28028
|
+
const compileOpts = {
|
|
28029
|
+
store,
|
|
28030
|
+
emitters: EMITTERS,
|
|
28031
|
+
net
|
|
28032
|
+
};
|
|
26218
28033
|
let rootReal;
|
|
26219
|
-
if (values.root !== void 0 || store === "cas") {
|
|
28034
|
+
if (values.root !== void 0 || store === "cas" || allowNet || net.allowFile === true) {
|
|
26220
28035
|
rootReal = await realRootArg(values.root ?? ".");
|
|
26221
28036
|
compileOpts.root = rootReal;
|
|
26222
28037
|
}
|
|
@@ -26233,12 +28048,30 @@ async function cmdCompile(argv) {
|
|
|
26233
28048
|
return EXIT_OK;
|
|
26234
28049
|
}
|
|
26235
28050
|
async function cmdVerify(argv) {
|
|
26236
|
-
const { positionals } = opts(argv, {});
|
|
28051
|
+
const { values, positionals } = opts(argv, { root: { type: "string" } });
|
|
26237
28052
|
const file = positionals[0];
|
|
26238
28053
|
if (file === void 0) throw new UsageError("verify: <bundle.json> is required");
|
|
26239
|
-
const
|
|
26240
|
-
|
|
26241
|
-
|
|
28054
|
+
const raw = await readJson(file);
|
|
28055
|
+
const r = verifyBundle(raw);
|
|
28056
|
+
if (values.root === void 0 || !r.ok) {
|
|
28057
|
+
out(r);
|
|
28058
|
+
return r.ok ? EXIT_OK : EXIT_FAIL;
|
|
28059
|
+
}
|
|
28060
|
+
const sig = await verifyBundleAgainstRoot(await realRootArg(values.root), parseBundleFile(raw));
|
|
28061
|
+
if (sig === void 0) {
|
|
28062
|
+
out({
|
|
28063
|
+
...r,
|
|
28064
|
+
signatures: null,
|
|
28065
|
+
note: "no .axiom/trust/keys.json under root"
|
|
28066
|
+
});
|
|
28067
|
+
return EXIT_OK;
|
|
28068
|
+
}
|
|
28069
|
+
out({
|
|
28070
|
+
...r,
|
|
28071
|
+
signed: sig.keyids.length > 0,
|
|
28072
|
+
signatures: sig
|
|
28073
|
+
});
|
|
28074
|
+
return sig.ok ? EXIT_OK : EXIT_FAIL;
|
|
26242
28075
|
}
|
|
26243
28076
|
async function loadProfileFor(rootReal, name) {
|
|
26244
28077
|
return loadProfile(name, { searchDirs: [path.join(rootReal, ".axiom", "profiles")] });
|
|
@@ -26314,6 +28147,7 @@ async function cmdApply(argv) {
|
|
|
26314
28147
|
if (values.message !== void 0) applyOpts.commitMessage = values.message;
|
|
26315
28148
|
const result = await apply(applyOpts);
|
|
26316
28149
|
if (result.status === "applied" || result.status === "noop") await saveManifest(rootReal, bundle);
|
|
28150
|
+
if (result.status === "applied" && !dryRun && profileWantsAntiRollback([...profile.checks, ...bundle.manifest.checks])) await advanceTrustState(rootReal, bundle);
|
|
26317
28151
|
out(result);
|
|
26318
28152
|
return result.status === "failed" || result.status === "rolled-back" ? EXIT_FAIL : EXIT_OK;
|
|
26319
28153
|
}
|
|
@@ -26324,6 +28158,28 @@ async function cmdRollback(argv) {
|
|
|
26324
28158
|
out(await rollback(await realRootArg(values.root), toDigestRef(digest)));
|
|
26325
28159
|
return EXIT_OK;
|
|
26326
28160
|
}
|
|
28161
|
+
async function cmdGc(argv) {
|
|
28162
|
+
const { values } = opts(argv, {
|
|
28163
|
+
root: { type: "string" },
|
|
28164
|
+
"dry-run": { type: "boolean" },
|
|
28165
|
+
"older-than": { type: "string" },
|
|
28166
|
+
keep: { type: "string" }
|
|
28167
|
+
});
|
|
28168
|
+
const rootReal = await realRootArg(values.root);
|
|
28169
|
+
const keep = values.keep ?? "all-manifests";
|
|
28170
|
+
if (keep !== "all-manifests" && keep !== "journal") throw new UsageError("--keep must be all-manifests|journal");
|
|
28171
|
+
const gcOpts = {
|
|
28172
|
+
keep,
|
|
28173
|
+
dryRun: values["dry-run"] === true
|
|
28174
|
+
};
|
|
28175
|
+
if (values["older-than"] !== void 0) {
|
|
28176
|
+
const ms = parseDuration(values["older-than"]);
|
|
28177
|
+
if (ms === void 0) throw new UsageError("--older-than must be <n>(ms|s|m|h|d)");
|
|
28178
|
+
gcOpts.olderThanMs = ms;
|
|
28179
|
+
}
|
|
28180
|
+
out(await collectGarbage(rootReal, gcOpts));
|
|
28181
|
+
return EXIT_OK;
|
|
28182
|
+
}
|
|
26327
28183
|
async function cmdDiff(argv) {
|
|
26328
28184
|
const { positionals } = opts(argv, {});
|
|
26329
28185
|
const [a, b] = positionals;
|
|
@@ -26339,6 +28195,86 @@ async function cmdSchema(argv) {
|
|
|
26339
28195
|
out(jsonSchemaFor(kind));
|
|
26340
28196
|
return EXIT_OK;
|
|
26341
28197
|
}
|
|
28198
|
+
async function cmdEmitters(argv) {
|
|
28199
|
+
const { values } = opts(argv, { json: { type: "boolean" } });
|
|
28200
|
+
const rows = emitterCatalogue();
|
|
28201
|
+
if (values.json) {
|
|
28202
|
+
out(rows);
|
|
28203
|
+
return EXIT_OK;
|
|
28204
|
+
}
|
|
28205
|
+
for (const r of rows) console.log(`${r.emitter}@${r.version}: ${r.template} — ${r.description}`);
|
|
28206
|
+
return EXIT_OK;
|
|
28207
|
+
}
|
|
28208
|
+
async function cmdKeygen(argv) {
|
|
28209
|
+
const { values } = opts(argv, {
|
|
28210
|
+
out: { type: "string" },
|
|
28211
|
+
name: { type: "string" }
|
|
28212
|
+
});
|
|
28213
|
+
const r = await keygen(path.resolve(values.out ?? "."), values.name);
|
|
28214
|
+
out({
|
|
28215
|
+
publicEntry: r.publicEntry,
|
|
28216
|
+
privateKeyFile: r.privateKeyFile
|
|
28217
|
+
});
|
|
28218
|
+
return EXIT_OK;
|
|
28219
|
+
}
|
|
28220
|
+
async function cmdSign(argv) {
|
|
28221
|
+
const { values, positionals } = opts(argv, {
|
|
28222
|
+
"key-file": { type: "string" },
|
|
28223
|
+
out: {
|
|
28224
|
+
type: "string",
|
|
28225
|
+
short: "o"
|
|
28226
|
+
}
|
|
28227
|
+
});
|
|
28228
|
+
const file = positionals[0];
|
|
28229
|
+
if (file === void 0) throw new UsageError("sign: <bundle.json> is required");
|
|
28230
|
+
const bundle = parseBundleFile(await readJson(file));
|
|
28231
|
+
const src = {};
|
|
28232
|
+
if (values["key-file"] !== void 0) src.keyFile = values["key-file"];
|
|
28233
|
+
const { bundle: signed, keyid } = await signBundle(bundle, src);
|
|
28234
|
+
const target = values.out ?? file;
|
|
28235
|
+
await writeFile(path.resolve(target), `${JSON.stringify(signed, null, 2)}\n`, "utf8");
|
|
28236
|
+
out({
|
|
28237
|
+
manifestDigest: signed.manifestDigest,
|
|
28238
|
+
keyid,
|
|
28239
|
+
signatures: signed.signatures?.length ?? 0,
|
|
28240
|
+
out: target
|
|
28241
|
+
});
|
|
28242
|
+
return EXIT_OK;
|
|
28243
|
+
}
|
|
28244
|
+
async function cmdTrust(argv) {
|
|
28245
|
+
const [sub, ...rest] = argv;
|
|
28246
|
+
const { values, positionals } = opts(rest, { root: { type: "string" } });
|
|
28247
|
+
const rootReal = await realRootArg(values.root);
|
|
28248
|
+
switch (sub) {
|
|
28249
|
+
case "list":
|
|
28250
|
+
out(await loadTrustStore(rootReal) ?? {
|
|
28251
|
+
version: 1,
|
|
28252
|
+
keys: []
|
|
28253
|
+
});
|
|
28254
|
+
return EXIT_OK;
|
|
28255
|
+
case "add": {
|
|
28256
|
+
const file = positionals[0];
|
|
28257
|
+
if (file === void 0) throw new UsageError("trust add: <pubkey.json> is required");
|
|
28258
|
+
const entry = parsePublicEntry(await readJson(file));
|
|
28259
|
+
const store = await trustAdd(rootReal, entry);
|
|
28260
|
+
out({
|
|
28261
|
+
added: entry.keyid,
|
|
28262
|
+
keys: store.keys.length
|
|
28263
|
+
});
|
|
28264
|
+
return EXIT_OK;
|
|
28265
|
+
}
|
|
28266
|
+
case "remove": {
|
|
28267
|
+
const keyid = positionals[0];
|
|
28268
|
+
if (keyid === void 0) throw new UsageError("trust remove: <keyid> is required");
|
|
28269
|
+
out({
|
|
28270
|
+
removed: keyid,
|
|
28271
|
+
keys: (await trustRemove(rootReal, keyid)).keys.length
|
|
28272
|
+
});
|
|
28273
|
+
return EXIT_OK;
|
|
28274
|
+
}
|
|
28275
|
+
default: throw new UsageError("trust: subcommand must be add|remove|list");
|
|
28276
|
+
}
|
|
28277
|
+
}
|
|
26342
28278
|
async function cmdGate(argv) {
|
|
26343
28279
|
const { gateMain } = await import("./gate-lazy.js");
|
|
26344
28280
|
const r = await gateMain(argv);
|
|
@@ -26346,6 +28282,112 @@ async function cmdGate(argv) {
|
|
|
26346
28282
|
if (r.stdout !== void 0) console.log(r.stdout);
|
|
26347
28283
|
return r.exitCode;
|
|
26348
28284
|
}
|
|
28285
|
+
async function cmdMigrate(argv) {
|
|
28286
|
+
const [from, ...rest] = argv;
|
|
28287
|
+
if (from !== "v1") throw new UsageError("migrate: source format must be v1");
|
|
28288
|
+
const { values, positionals } = opts(rest, {
|
|
28289
|
+
out: {
|
|
28290
|
+
type: "string",
|
|
28291
|
+
short: "o"
|
|
28292
|
+
},
|
|
28293
|
+
profile: { type: "string" },
|
|
28294
|
+
cas: { type: "string" },
|
|
28295
|
+
content: { type: "string" },
|
|
28296
|
+
name: { type: "string" },
|
|
28297
|
+
overwrite: { type: "boolean" }
|
|
28298
|
+
});
|
|
28299
|
+
const file = positionals[0];
|
|
28300
|
+
if (file === void 0) throw new UsageError("migrate v1: <manifest.json> is required");
|
|
28301
|
+
const { migrateV1 } = await import("./migrate-lazy.js");
|
|
28302
|
+
const contentDir = path.resolve(values.content ?? path.dirname(path.resolve(file)));
|
|
28303
|
+
const migrateOpts = {
|
|
28304
|
+
version: MIGRATE_VERSION,
|
|
28305
|
+
resolveContent: async (rel) => {
|
|
28306
|
+
try {
|
|
28307
|
+
return new Uint8Array(await readFile(path.join(contentDir, rel)));
|
|
28308
|
+
} catch (err) {
|
|
28309
|
+
if (err.code === "ENOENT") return void 0;
|
|
28310
|
+
throw err;
|
|
28311
|
+
}
|
|
28312
|
+
}
|
|
28313
|
+
};
|
|
28314
|
+
if (values.profile !== void 0) migrateOpts.profile = values.profile;
|
|
28315
|
+
if (values.name !== void 0) migrateOpts.name = values.name;
|
|
28316
|
+
if (values.overwrite === true) migrateOpts.overwrite = true;
|
|
28317
|
+
if (values.cas !== void 0) migrateOpts.casRoot = await realRootArg(values.cas);
|
|
28318
|
+
const { plan, report } = await migrateV1(await readJson(file), migrateOpts);
|
|
28319
|
+
if (values.out !== void 0) {
|
|
28320
|
+
await writeFile(path.resolve(values.out), `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
|
28321
|
+
out({
|
|
28322
|
+
...report,
|
|
28323
|
+
out: values.out
|
|
28324
|
+
});
|
|
28325
|
+
} else out({
|
|
28326
|
+
plan,
|
|
28327
|
+
report
|
|
28328
|
+
});
|
|
28329
|
+
return report.ok ? EXIT_OK : EXIT_FAIL;
|
|
28330
|
+
}
|
|
28331
|
+
let MIGRATE_VERSION = "0.0.0";
|
|
28332
|
+
async function cmdSnapshot(argv) {
|
|
28333
|
+
const { values } = opts(argv, {
|
|
28334
|
+
root: { type: "string" },
|
|
28335
|
+
out: {
|
|
28336
|
+
type: "string",
|
|
28337
|
+
short: "o"
|
|
28338
|
+
},
|
|
28339
|
+
include: {
|
|
28340
|
+
type: "string",
|
|
28341
|
+
multiple: true
|
|
28342
|
+
},
|
|
28343
|
+
exclude: {
|
|
28344
|
+
type: "string",
|
|
28345
|
+
multiple: true
|
|
28346
|
+
},
|
|
28347
|
+
"max-files": { type: "string" },
|
|
28348
|
+
"max-bytes": { type: "string" },
|
|
28349
|
+
"no-gitignore": { type: "boolean" },
|
|
28350
|
+
"no-digest": { type: "boolean" }
|
|
28351
|
+
});
|
|
28352
|
+
const rootReal = await realRootArg(values.root);
|
|
28353
|
+
const snapOpts = {
|
|
28354
|
+
respectGitignore: values["no-gitignore"] !== true,
|
|
28355
|
+
withContentDigest: values["no-digest"] !== true
|
|
28356
|
+
};
|
|
28357
|
+
if (values.include !== void 0) snapOpts.include = values.include;
|
|
28358
|
+
if (values.exclude !== void 0) snapOpts.exclude = values.exclude;
|
|
28359
|
+
for (const [flag, key] of [["max-files", "maxFiles"], ["max-bytes", "maxBytes"]]) {
|
|
28360
|
+
const raw = values[flag];
|
|
28361
|
+
if (raw === void 0) continue;
|
|
28362
|
+
const n = Number(raw);
|
|
28363
|
+
if (!Number.isInteger(n) || n < 0) throw new UsageError(`--${flag} must be a non-negative integer`);
|
|
28364
|
+
snapOpts[key] = n;
|
|
28365
|
+
}
|
|
28366
|
+
const snap = await snapshotRoot(rootReal, snapOpts);
|
|
28367
|
+
if (values.out !== void 0) {
|
|
28368
|
+
await writeFile(path.resolve(values.out), `${JSON.stringify(snap, null, 2)}\n`, "utf8");
|
|
28369
|
+
out({
|
|
28370
|
+
snapshotDigest: snap.snapshotDigest,
|
|
28371
|
+
...snap.body.counts,
|
|
28372
|
+
truncated: snap.body.truncated,
|
|
28373
|
+
out: values.out
|
|
28374
|
+
});
|
|
28375
|
+
} else out(snap);
|
|
28376
|
+
return EXIT_OK;
|
|
28377
|
+
}
|
|
28378
|
+
function parseSnapshotFile(raw, label) {
|
|
28379
|
+
const parsed = RepoSnapshotSchema.safeParse(raw);
|
|
28380
|
+
if (!parsed.success) throw new AxiomError("ERR_INVALID_MANIFEST", `${label} does not match RepoSnapshotSchema`, { details: { issues: parsed.error.issues.slice(0, 20).map((i) => `${i.path.join(".")}: ${i.message}`) } });
|
|
28381
|
+
return parsed.data;
|
|
28382
|
+
}
|
|
28383
|
+
async function cmdSnapshotDiff(argv) {
|
|
28384
|
+
const { positionals } = opts(argv, {});
|
|
28385
|
+
const [a, b] = positionals;
|
|
28386
|
+
if (a === void 0 || b === void 0) throw new UsageError("snapshot-diff: <a.json> <b.json> are required");
|
|
28387
|
+
const [sa, sb] = await Promise.all([readJson(a), readJson(b)]);
|
|
28388
|
+
out(diffSnapshots(parseSnapshotFile(sa, a), parseSnapshotFile(sb, b)));
|
|
28389
|
+
return EXIT_OK;
|
|
28390
|
+
}
|
|
26349
28391
|
const VERBS = {
|
|
26350
28392
|
mcp: cmdMcp,
|
|
26351
28393
|
compile: cmdCompile,
|
|
@@ -26353,12 +28395,21 @@ const VERBS = {
|
|
|
26353
28395
|
check: cmdCheck,
|
|
26354
28396
|
apply: cmdApply,
|
|
26355
28397
|
rollback: cmdRollback,
|
|
28398
|
+
gc: cmdGc,
|
|
26356
28399
|
diff: cmdDiff,
|
|
26357
28400
|
schema: cmdSchema,
|
|
26358
|
-
|
|
28401
|
+
emitters: cmdEmitters,
|
|
28402
|
+
keygen: cmdKeygen,
|
|
28403
|
+
sign: cmdSign,
|
|
28404
|
+
trust: cmdTrust,
|
|
28405
|
+
gate: cmdGate,
|
|
28406
|
+
migrate: cmdMigrate,
|
|
28407
|
+
snapshot: cmdSnapshot,
|
|
28408
|
+
"snapshot-diff": cmdSnapshotDiff
|
|
26359
28409
|
};
|
|
26360
28410
|
async function main(argv, version) {
|
|
26361
28411
|
const HELP = help(version);
|
|
28412
|
+
MIGRATE_VERSION = version;
|
|
26362
28413
|
const [verb, ...rest] = argv;
|
|
26363
28414
|
if (verb === void 0 || verb === "--help" || verb === "-h" || verb === "help") {
|
|
26364
28415
|
process.stdout.write(HELP);
|
|
@@ -26387,7 +28438,7 @@ async function main(argv, version) {
|
|
|
26387
28438
|
console.error(`error: ${err.message}\n\n${HELP}`);
|
|
26388
28439
|
return EXIT_USAGE;
|
|
26389
28440
|
}
|
|
26390
|
-
if (err
|
|
28441
|
+
if (isAxiomError(err)) {
|
|
26391
28442
|
console.error(JSON.stringify(err.toJSON()));
|
|
26392
28443
|
return EXIT_USAGE;
|
|
26393
28444
|
}
|