@awak-app/simy-cli 0.2.3 → 0.4.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.
@@ -13,7 +13,7 @@ import {
13
13
  normalizeVisualReview,
14
14
  } from "./completion-contract.js";
15
15
 
16
- export async function auditAttempt(charter, attempt, { previousAttempt = null } = {}) {
16
+ export async function auditAttempt(charter, attempt, { previousAttempt = null, sqm = null } = {}) {
17
17
  const checks = [];
18
18
  const addCheck = (
19
19
  id,
@@ -219,6 +219,7 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null }
219
219
  ? "Implementation evidence passed local verification."
220
220
  : "Implementation evidence failed local verification.",
221
221
  checks,
222
+ sqm: normalizeSqmAudit(sqm),
222
223
  };
223
224
 
224
225
  return {
@@ -257,6 +258,46 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null }
257
258
  release_gate: null,
258
259
  work_log_signals: [],
259
260
  work_log_interventions: [],
261
+ sqm: normalizeSqmAudit(sqm),
262
+ };
263
+ }
264
+
265
+ function normalizeSqmAudit(sqm) {
266
+ if (!sqm) return null;
267
+ return {
268
+ status: sqm.status || "unavailable",
269
+ mode: "shadow",
270
+ bundle: sqm.bundle || null,
271
+ warnings: Array.isArray(sqm.warnings) ? sqm.warnings : [],
272
+ findings: (Array.isArray(sqm.findings) ? sqm.findings : []).map((finding) => ({
273
+ finding_id: finding.finding_id,
274
+ module_id: finding.module_id,
275
+ module_version: finding.module_version,
276
+ rule_id: finding.rule_id,
277
+ severity: finding.severity,
278
+ file: finding.file || null,
279
+ line: finding.line || null,
280
+ side: finding.side || null,
281
+ evidence_hash: finding.evidence_hash,
282
+ bundle_id: finding.bundle_id,
283
+ bundle_version: finding.bundle_version,
284
+ bundle_digest: finding.bundle_digest,
285
+ })),
286
+ min_check: sqm.min_check ? {
287
+ result_digest: sqm.min_check.result_digest || null,
288
+ status: sqm.min_check.status || null,
289
+ executed_at: sqm.min_check.executed_at || null,
290
+ } : null,
291
+ proof: sqm.proof ? {
292
+ proof_id: sqm.proof.proof_id || null,
293
+ digest: sqm.proof.integrity?.digest || null,
294
+ outcome: sqm.proof.outcome?.status || null,
295
+ } : null,
296
+ signed_evidence: sqm.signed_evidence ? {
297
+ evidence_id: sqm.signed_evidence.evidence_id || null,
298
+ key_id: sqm.signed_evidence.signer?.key_id || null,
299
+ algorithm: sqm.signed_evidence.integrity?.algorithm || null,
300
+ } : null,
260
301
  };
261
302
  }
262
303
 
@@ -14,6 +14,7 @@ export async function runCodingLoop({
14
14
  executeAttempt,
15
15
  executeIndependentAudit,
16
16
  collectEvidence,
17
+ checkSqm = null,
17
18
  onUpdate,
18
19
  humanGuidance = "",
19
20
  resume = false,
@@ -127,6 +128,7 @@ export async function runCodingLoop({
127
128
  });
128
129
  await publish(snapshot, "collecting_evidence", onUpdate);
129
130
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
131
+ attempt.sqm = await safeSqm(checkSqm, attempt);
130
132
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
131
133
 
132
134
  appendEvent(snapshot, "auditing", `Verifying local evidence for attempt ${attemptNumber}.`, {
@@ -140,7 +142,10 @@ export async function runCodingLoop({
140
142
  attempt.observed_evidence?.github?.available === true,
141
143
  });
142
144
  await publish(snapshot, "auditing", onUpdate);
143
- attempt.audit = await auditAttempt(snapshot.charter, attempt, { previousAttempt });
145
+ attempt.audit = await auditAttempt(snapshot.charter, attempt, {
146
+ previousAttempt,
147
+ sqm: attempt.sqm,
148
+ });
144
149
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
145
150
  attempt.implementation_gate = attempt.audit.implementation_gate;
146
151
  snapshot.attempts.push(attempt);
@@ -212,8 +217,12 @@ export async function runCodingLoop({
212
217
 
213
218
  // Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
214
219
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
220
+ attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm);
215
221
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
216
- attempt.audit = await auditAttempt(snapshot.charter, attempt, { previousAttempt });
222
+ attempt.audit = await auditAttempt(snapshot.charter, attempt, {
223
+ previousAttempt,
224
+ sqm: attempt.sqm,
225
+ });
217
226
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
218
227
  attempt.implementation_gate = attempt.audit.implementation_gate;
219
228
 
@@ -267,7 +276,7 @@ async function stopCodingLoop(snapshot, onUpdate) {
267
276
  return snapshot;
268
277
  }
269
278
 
270
- export async function recheckPrReadiness({ snapshot, collectEvidence, onUpdate }) {
279
+ export async function recheckPrReadiness({ snapshot, collectEvidence, checkSqm = null, onUpdate }) {
271
280
  const attempt = snapshot.attempts.at(-1);
272
281
  if (!attempt || !attempt.audit?.passed || !attempt.independent_audit?.passed) return snapshot;
273
282
  appendEvent(snapshot, "checking_pr", "Refreshing GitHub review and CI evidence.", {
@@ -275,8 +284,10 @@ export async function recheckPrReadiness({ snapshot, collectEvidence, onUpdate }
275
284
  });
276
285
  await publish(snapshot, "checking_pr", onUpdate);
277
286
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
287
+ attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm, "full");
278
288
  attempt.audit = await auditAttempt(snapshot.charter, attempt, {
279
289
  previousAttempt: snapshot.attempts.at(-2) ?? null,
290
+ sqm: attempt.sqm,
280
291
  });
281
292
  snapshot.implementation_gate_passed = attempt.audit.passed;
282
293
  attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
@@ -352,6 +363,21 @@ async function safeEvidence(collector, attempt) {
352
363
  }
353
364
  }
354
365
 
366
+ async function safeSqm(checker, attempt, previous = null, phase = "min") {
367
+ if (!checker) return previous;
368
+ try {
369
+ return await checker(attempt, { phase });
370
+ } catch (error) {
371
+ return {
372
+ status: "unavailable",
373
+ mode: "shadow",
374
+ bundle: null,
375
+ findings: [],
376
+ warnings: [error instanceof Error ? error.message : "SQM shadow check failed."],
377
+ };
378
+ }
379
+ }
380
+
355
381
  function unavailableEvidence(error) {
356
382
  return {
357
383
  collected_at: new Date().toISOString(),
@@ -1,5 +1,12 @@
1
1
  import { execFile } from "node:child_process";
2
- import { mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises";
2
+ import {
3
+ lstat,
4
+ mkdir,
5
+ readFile,
6
+ readdir,
7
+ realpath,
8
+ writeFile,
9
+ } from "node:fs/promises";
3
10
  import { homedir } from "node:os";
4
11
  import { dirname, join, resolve } from "node:path";
5
12
  import { promisify } from "node:util";
@@ -152,6 +159,35 @@ export function findRepository(inventory, repository) {
152
159
  );
153
160
  }
154
161
 
162
+ export async function verifyRepositoryIdentity(entry, expectedRepository) {
163
+ const expected = normalizeGitHubRemote(expectedRepository)?.toLowerCase();
164
+ const localPath = resolve(String(entry?.local_path || ""));
165
+ if (!expected || !localPath) return null;
166
+ try {
167
+ const details = await lstat(localPath);
168
+ if (!details.isDirectory() || details.isSymbolicLink()) return null;
169
+ const canonicalPath = await realpath(localPath);
170
+ const [{ stdout: root }, { stdout: remote }] = await Promise.all([
171
+ execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd: canonicalPath }),
172
+ execFileAsync("git", ["remote", "get-url", "origin"], { cwd: canonicalPath }),
173
+ ]);
174
+ const canonicalRoot = await realpath(resolve(String(root || "").trim()));
175
+ if (
176
+ canonicalRoot !== canonicalPath ||
177
+ normalizeGitHubRemote(remote)?.toLowerCase() !== expected
178
+ ) {
179
+ return null;
180
+ }
181
+ return {
182
+ ...entry,
183
+ repository: normalizeGitHubRemote(remote),
184
+ local_path: canonicalPath,
185
+ };
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
155
191
  async function inspectGitRepository(directory) {
156
192
  try {
157
193
  const [{ stdout: root }, { stdout: remote }, { stdout: branch }] = await Promise.all([
package/src/runner.js CHANGED
@@ -40,6 +40,8 @@ import {
40
40
  import { LocalRunRegistry } from "./run-registry.js";
41
41
  import { webApiHeaders, webApiUrl } from "./web-api.js";
42
42
  import { normalizeGitHubRemote } from "./workspace-context.js";
43
+ import { CLI_VERSION } from "./cli-contract.js";
44
+ import { createSqmChecker } from "./sqm/index.js";
43
45
 
44
46
  const execFileAsync = promisify(execFile);
45
47
  const MAX_LOCAL_LOG_LINES = 1_000;
@@ -543,6 +545,20 @@ async function runLocalCodingRun(
543
545
  attempt,
544
546
  repositoryPath,
545
547
  }));
548
+ const sqmChecker =
549
+ run.sqmChecker ||
550
+ createSqmChecker({
551
+ cwd: repositoryPath,
552
+ baseBranch: run.snapshot.charter.base_branch,
553
+ cliVersion: CLI_VERSION,
554
+ token: run.session?.token,
555
+ endpoint: sqmBundleEndpoint(run.session),
556
+ evidenceEndpoint: sqmEvidenceEndpoint(run.session),
557
+ accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
558
+ organizationId: run.session?.organization_id || run.session?.org_id,
559
+ deviceId: run.session?.device_id,
560
+ });
561
+ run.sqmChecker = sqmChecker;
546
562
 
547
563
  try {
548
564
  await runCodingLoop({
@@ -550,6 +566,7 @@ async function runLocalCodingRun(
550
566
  executeAttempt: executor,
551
567
  executeIndependentAudit: independentAuditor,
552
568
  collectEvidence: evidenceCollector,
569
+ checkSqm: sqmChecker,
553
570
  humanGuidance,
554
571
  resume,
555
572
  shouldStop: () => run.stopRequested,
@@ -601,9 +618,24 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
601
618
  attempt,
602
619
  repositoryPath,
603
620
  }));
621
+ const sqmChecker =
622
+ run.sqmChecker ||
623
+ createSqmChecker({
624
+ cwd: repositoryPath,
625
+ baseBranch: run.snapshot.charter.base_branch,
626
+ cliVersion: CLI_VERSION,
627
+ token: run.session?.token,
628
+ endpoint: sqmBundleEndpoint(run.session),
629
+ evidenceEndpoint: sqmEvidenceEndpoint(run.session),
630
+ accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
631
+ organizationId: run.session?.organization_id || run.session?.org_id,
632
+ deviceId: run.session?.device_id,
633
+ });
634
+ run.sqmChecker = sqmChecker;
604
635
  await recheckPrReadiness({
605
636
  snapshot: run.snapshot,
606
637
  collectEvidence: evidenceCollector,
638
+ checkSqm: sqmChecker,
607
639
  onUpdate: async (snapshot) => {
608
640
  run.snapshot = snapshot;
609
641
  await updateRun(run, snapshot.state);
@@ -612,6 +644,18 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
612
644
  return run.snapshot;
613
645
  }
614
646
 
647
+ function sqmBundleEndpoint(session) {
648
+ if (process.env.SIMY_SQM_BUNDLE_URL?.trim()) return process.env.SIMY_SQM_BUNDLE_URL.trim();
649
+ if (!session?.api_base_url) return undefined;
650
+ return new URL("sqm/knowledge-bundle", session.api_base_url).href;
651
+ }
652
+
653
+ function sqmEvidenceEndpoint(session) {
654
+ if (process.env.SIMY_SQM_EVIDENCE_URL?.trim()) return process.env.SIMY_SQM_EVIDENCE_URL.trim();
655
+ if (!session?.api_base_url) return undefined;
656
+ return new URL("sqm/run-evidence", session.api_base_url).href;
657
+ }
658
+
615
659
  async function executeProcessAttempt({
616
660
  backend,
617
661
  instruction,
@@ -0,0 +1,63 @@
1
+ const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
2
+
3
+ export function createAgentShutdown(agent) {
4
+ let shutdownPromise = null;
5
+ return () => {
6
+ if (!shutdownPromise) shutdownPromise = closeAgent(agent);
7
+ return shutdownPromise;
8
+ };
9
+ }
10
+
11
+ export async function closeAgent(agent) {
12
+ const serverClose = beginServerClose(agent?.server);
13
+ let shutdownError = null;
14
+ try {
15
+ await agent?.shutdown?.();
16
+ } catch (error) {
17
+ shutdownError = error;
18
+ } finally {
19
+ // server.close() stops new connections immediately, but its callback waits
20
+ // for active SSE and other long-lived requests. Drain Provider work and its
21
+ // final durable status first, then release those remaining connections.
22
+ agent?.server?.closeAllConnections?.();
23
+ }
24
+ await serverClose;
25
+ if (shutdownError) throw shutdownError;
26
+ }
27
+
28
+ export function installAgentShutdownSignalHandlers({
29
+ signalSource = process,
30
+ shutdown,
31
+ exit = (code) => process.exit(code),
32
+ reportError = (error) => console.error(error instanceof Error ? error.message : String(error)),
33
+ } = {}) {
34
+ if (typeof shutdown !== "function") throw new TypeError("shutdown is required");
35
+ const handlers = new Map();
36
+ for (const signal of SHUTDOWN_SIGNALS) {
37
+ const handler = () => {
38
+ void shutdown()
39
+ .then(() => exit(0))
40
+ .catch((error) => {
41
+ reportError(error);
42
+ exit(1);
43
+ });
44
+ };
45
+ handlers.set(signal, handler);
46
+ signalSource.once(signal, handler);
47
+ }
48
+ return () => {
49
+ for (const [signal, handler] of handlers) {
50
+ signalSource.off(signal, handler);
51
+ }
52
+ };
53
+ }
54
+
55
+ function beginServerClose(server) {
56
+ if (!server?.listening) return Promise.resolve();
57
+ return new Promise((resolve, reject) => {
58
+ server.close((error) => {
59
+ if (error) reject(error);
60
+ else resolve();
61
+ });
62
+ });
63
+ }
@@ -0,0 +1,249 @@
1
+ import { createHash, createPublicKey } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { verifyBundleIntegrity } from "./canonical.js";
7
+ import { validateKnowledgeBundle } from "./validation.js";
8
+
9
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
10
+ export const SQM_BUNDLE_FETCH_TIMEOUT_MS = 15_000;
11
+
12
+ export async function loadKnowledgeBundle({
13
+ repository,
14
+ cliVersion,
15
+ endpoint = process.env.SIMY_SQM_BUNDLE_URL,
16
+ token = process.env.SIMY_SQM_TOKEN || process.env.SIMY_ACCESS_TOKEN,
17
+ accountId = process.env.SIMY_SQM_ACCOUNT_ID || (token ? `token:${createHash("sha256").update(token).digest("hex")}` : "anonymous"),
18
+ organizationId = process.env.SIMY_SQM_ORGANIZATION_ID,
19
+ cacheRoot = process.env.SIMY_SQM_CACHE_DIR || path.join(process.env.SIMY_HOME?.trim() || path.join(homedir(), ".simy"), "sqm"),
20
+ ttlMs = numberEnv("SIMY_SQM_CACHE_TTL_MS", DEFAULT_TTL_MS),
21
+ offline = false,
22
+ refresh = false,
23
+ fetchImpl = globalThis.fetch,
24
+ now = Date.now(),
25
+ publicKeys = publicKeysFromEnvironment(),
26
+ }) {
27
+ if (!organizationId) {
28
+ return result(null, ["SQM organization identity is unavailable; shadow checks were skipped."], "unavailable");
29
+ }
30
+ const cacheIdentity = normalizedCacheIdentity({ endpoint, accountId, organizationId, repository });
31
+ const location = cacheLocation(cacheRoot, cacheIdentity);
32
+ const validation = { cliVersion, publicKeys, organizationId };
33
+ const cached = await readVerifiedCache(location, validation, cacheIdentity);
34
+ const fresh =
35
+ cached &&
36
+ now - cached.fetchedAt < ttlMs &&
37
+ Date.parse(cached.bundle.expires_at) > now;
38
+ if (!offline && endpoint && (!fresh || refresh)) {
39
+ try {
40
+ const fetched = await fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag: cached?.etag, fetchImpl });
41
+ if (fetched.notModified) {
42
+ if (!cached) throw new Error("SQM Cloud returned 304 without a verified local bundle.");
43
+ if (Date.parse(cached.bundle.expires_at) <= now) throw nonFallbackError("SQM Cloud returned 304 for an expired knowledge bundle.");
44
+ await writeMetadata(location, { ...cached.metadata, fetched_at: new Date(now).toISOString(), etag: fetched.etag || cached.etag });
45
+ return result(cached.bundle, [], "cache_revalidated");
46
+ }
47
+ const discoveredTrust = signingTrustForFetch({
48
+ bundle: fetched.bundle,
49
+ endpoint,
50
+ publicKeys,
51
+ signingKeyId: fetched.signingKeyId,
52
+ signingPublicKey: fetched.signingPublicKey,
53
+ });
54
+ const bundle = verifyAndValidate(fetched.bundle, {
55
+ ...validation,
56
+ publicKeys: discoveredTrust.publicKeys,
57
+ });
58
+ if (Date.parse(bundle.expires_at) <= now) {
59
+ throw nonFallbackError("SQM Cloud returned an expired knowledge bundle.");
60
+ }
61
+ if (fetched.etag && unquote(fetched.etag) !== bundle.integrity.digest) throw nonFallbackError("SQM response ETag does not match the bundle digest.");
62
+ await writeCache(location, bundle, {
63
+ ...cacheIdentity,
64
+ fetched_at: new Date(now).toISOString(),
65
+ etag: fetched.etag || `\"${bundle.integrity.digest}\"`,
66
+ signing_key_id: discoveredTrust.signingKeyId,
67
+ signing_public_key: discoveredTrust.signingPublicKey,
68
+ });
69
+ return result(bundle, [], "cloud");
70
+ } catch (error) {
71
+ if (error?.fallbackAllowed !== false && cached && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
72
+ return result(null, [`SQM knowledge could not be used; shadow checks were skipped: ${message(error)}`], "unavailable");
73
+ }
74
+ }
75
+ if (cached) {
76
+ if (Date.parse(cached.bundle.expires_at) <= now) return result(null, ["The cached SQM knowledge bundle is expired; shadow checks were skipped."], "unavailable");
77
+ const warnings = [];
78
+ if (offline) warnings.push("SQM is offline; using the last verified knowledge bundle.");
79
+ else if (!endpoint) warnings.push("SQM Cloud endpoint is not configured; using the last verified knowledge bundle.");
80
+ return result(cached.bundle, warnings, fresh ? "cache" : "stale_cache");
81
+ }
82
+ const reason = offline ? "offline mode" : "SQM Cloud endpoint is not configured";
83
+ return result(null, [`SQM has no verified cache (${reason}); shadow checks were skipped.`], "unavailable");
84
+ }
85
+
86
+ export function verifyAndValidate(bundle, { cliVersion, publicKeys, organizationId } = {}) {
87
+ try { validateKnowledgeBundle(bundle, { cliVersion }); } catch (error) { throw nonFallbackError(message(error), error); }
88
+ if (organizationId && bundle.organization_id !== organizationId) throw nonFallbackError(`SQM bundle organization mismatch: expected ${organizationId}.`);
89
+ for (const module of bundle.modules) {
90
+ if (module.scope.visibility === "organization" && module.scope.organization_id !== bundle.organization_id) throw nonFallbackError(`SQM module ${module.id} is scoped to a different organization.`);
91
+ }
92
+ const keyId = bundle.integrity.key_id;
93
+ const key = publicKeys.get(keyId);
94
+ if (!key) throw nonFallbackError(`No trusted SQM public key is configured for ${keyId}.`);
95
+ try { verifyBundleIntegrity(bundle, key); } catch (error) { throw nonFallbackError(message(error), error); }
96
+ return bundle;
97
+ }
98
+
99
+ export function publicKeysFromEnvironment(env = process.env) {
100
+ const keys = new Map();
101
+ if (env.SIMY_SQM_PUBLIC_KEYS_JSON) {
102
+ let parsed;
103
+ try { parsed = JSON.parse(env.SIMY_SQM_PUBLIC_KEYS_JSON); } catch { throw new Error("SIMY_SQM_PUBLIC_KEYS_JSON is not valid JSON."); }
104
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("SIMY_SQM_PUBLIC_KEYS_JSON must be an object keyed by key ID.");
105
+ for (const [id, value] of Object.entries(parsed)) keys.set(id, parsePublicKey(value));
106
+ }
107
+ if (env.SIMY_SQM_PUBLIC_KEY && env.SIMY_SQM_KEY_ID) keys.set(env.SIMY_SQM_KEY_ID, parsePublicKey(env.SIMY_SQM_PUBLIC_KEY));
108
+ return keys;
109
+ }
110
+
111
+ function parsePublicKey(value) {
112
+ const text = String(value || "").replace(/\\n/g, "\n").trim();
113
+ if (text.startsWith("-----BEGIN")) return createPublicKey(text);
114
+ const raw = Buffer.from(text, "base64");
115
+ if (raw.length === 32) {
116
+ const prefix = Buffer.from("302a300506032b6570032100", "hex");
117
+ return createPublicKey({ key: Buffer.concat([prefix, raw]), format: "der", type: "spki" });
118
+ }
119
+ return createPublicKey({ key: raw, format: "der", type: "spki" });
120
+ }
121
+
122
+ function signingTrustForFetch({ bundle, endpoint, publicKeys, signingKeyId, signingPublicKey }) {
123
+ const keyId = bundle?.integrity?.key_id;
124
+ if (publicKeys.has(keyId)) {
125
+ return { publicKeys, signingKeyId: null, signingPublicKey: null };
126
+ }
127
+ if (!trustedDiscoveryEndpoint(endpoint)) {
128
+ throw nonFallbackError("SQM signing-key discovery requires authenticated HTTPS or loopback.");
129
+ }
130
+ if (!signingKeyId || signingKeyId !== keyId || !signingPublicKey) {
131
+ throw nonFallbackError(`SQM Cloud did not provide the trusted public key for ${keyId}.`);
132
+ }
133
+ const discovered = new Map(publicKeys);
134
+ try {
135
+ discovered.set(keyId, parsePublicKey(signingPublicKey));
136
+ } catch (error) {
137
+ throw nonFallbackError(`SQM Cloud returned an invalid public key for ${keyId}.`, error);
138
+ }
139
+ return {
140
+ publicKeys: discovered,
141
+ signingKeyId,
142
+ signingPublicKey: String(signingPublicKey).trim(),
143
+ };
144
+ }
145
+
146
+ function trustedDiscoveryEndpoint(endpoint) {
147
+ const url = new URL(endpoint);
148
+ return url.protocol === "https:" ||
149
+ (url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname));
150
+ }
151
+
152
+ async function fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag, fetchImpl }) {
153
+ const url = new URL(endpoint);
154
+ url.searchParams.set("repository", repository);
155
+ url.searchParams.set("cli_version", cliVersion);
156
+ const headers = { Accept: "application/json" };
157
+ if (token) headers.Authorization = `Bearer ${token}`;
158
+ if (organizationId) headers["x-simy-org-id"] = organizationId;
159
+ if (etag) headers["If-None-Match"] = etag;
160
+ const response = await fetchImpl(url, {
161
+ headers,
162
+ signal: AbortSignal.timeout(SQM_BUNDLE_FETCH_TIMEOUT_MS),
163
+ });
164
+ const responseEtag = response.headers?.get?.("etag") || null;
165
+ const signingKeyId = response.headers?.get?.("x-simy-sqm-key-id") || null;
166
+ const signingPublicKey = response.headers?.get?.("x-simy-sqm-public-key") || null;
167
+ if (response.status === 304) {
168
+ return { notModified: true, etag: responseEtag, signingKeyId, signingPublicKey };
169
+ }
170
+ if (!response.ok) {
171
+ const body = await response.text().catch(() => "");
172
+ const error = new Error(`SQM Cloud returned HTTP ${response.status}${body ? `: ${body.slice(0, 300)}` : ""}.`);
173
+ error.fallbackAllowed = response.status >= 500 && response.status <= 599;
174
+ throw error;
175
+ }
176
+ try {
177
+ return {
178
+ notModified: false,
179
+ etag: responseEtag,
180
+ signingKeyId,
181
+ signingPublicKey,
182
+ bundle: await response.json(),
183
+ };
184
+ } catch (error) {
185
+ throw nonFallbackError("SQM Cloud returned an invalid JSON knowledge bundle.", error);
186
+ }
187
+ }
188
+
189
+ async function readVerifiedCache(location, options, identity) {
190
+ try {
191
+ const [bundleRaw, metadataRaw] = await Promise.all([readFile(location.bundle, "utf8"), readFile(location.metadata, "utf8")]);
192
+ const metadata = JSON.parse(metadataRaw);
193
+ for (const key of ["endpoint", "account_id", "organization_id", "repository"]) if (metadata[key] !== identity[key]) throw new Error("SQM cache identity does not match the current tenant context.");
194
+ const publicKeys = new Map(options.publicKeys);
195
+ if (metadata.signing_key_id && metadata.signing_public_key && !publicKeys.has(metadata.signing_key_id)) {
196
+ publicKeys.set(metadata.signing_key_id, parsePublicKey(metadata.signing_public_key));
197
+ }
198
+ const bundle = verifyAndValidate(JSON.parse(bundleRaw), { ...options, publicKeys });
199
+ if (metadata.digest !== bundle.integrity.digest || metadata.bundle_id !== bundle.bundle_id || metadata.version !== bundle.version) throw new Error("SQM cache metadata does not match its bundle.");
200
+ if (metadata.signing_key_id && metadata.signing_key_id !== bundle.integrity.key_id) throw new Error("SQM cache signing key does not match its bundle.");
201
+ return { bundle, metadata, etag: metadata.etag || null, fetchedAt: Date.parse(metadata.fetched_at) || 0 };
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ async function writeCache(location, bundle, metadata) {
208
+ await mkdir(location.directory, { recursive: true, mode: 0o700 });
209
+ await atomicWrite(location.bundle, `${JSON.stringify(bundle)}\n`);
210
+ await writeMetadata(location, { ...metadata, bundle_id: bundle.bundle_id, version: bundle.version, digest: bundle.integrity.digest, verified_at: new Date().toISOString() });
211
+ }
212
+
213
+ async function writeMetadata(location, metadata) {
214
+ await mkdir(location.directory, { recursive: true, mode: 0o700 });
215
+ await atomicWrite(location.metadata, `${JSON.stringify(metadata)}\n`);
216
+ }
217
+
218
+ async function atomicWrite(target, content) {
219
+ const temporary = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
220
+ await writeFile(temporary, content, { encoding: "utf8", mode: 0o600 });
221
+ await rename(temporary, target);
222
+ }
223
+
224
+ function cacheLocation(root, identity) {
225
+ const id = createHash("sha256").update(JSON.stringify(identity)).digest("hex");
226
+ const directory = path.join(root, id);
227
+ return { directory, bundle: path.join(directory, "bundle.json"), metadata: path.join(directory, "metadata.json") };
228
+ }
229
+
230
+ function normalizedCacheIdentity({ endpoint, accountId, organizationId, repository }) {
231
+ let normalizedEndpoint = "unconfigured";
232
+ if (endpoint) {
233
+ const url = new URL(endpoint);
234
+ url.hash = "";
235
+ normalizedEndpoint = url.href;
236
+ }
237
+ return { endpoint: normalizedEndpoint, account_id: String(accountId || "anonymous"), organization_id: organizationId ? String(organizationId) : null, repository: String(repository).toLowerCase() };
238
+ }
239
+
240
+ function nonFallbackError(message, cause) {
241
+ const error = new Error(message, { cause });
242
+ error.fallbackAllowed = false;
243
+ return error;
244
+ }
245
+
246
+ function result(bundle, warnings, source) { return { bundle, warnings, source }; }
247
+ function message(error) { return error instanceof Error ? error.message : String(error); }
248
+ function numberEnv(name, fallback) { const parsed = Number(process.env[name]); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; }
249
+ function unquote(value) { return String(value).replace(/^W\//, "").replace(/^\"|\"$/g, ""); }
@@ -0,0 +1,45 @@
1
+ import { createHash, verify } from "node:crypto";
2
+
3
+ export function canonicalize(value) {
4
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
5
+ return JSON.stringify(value);
6
+ }
7
+ if (typeof value === "number") {
8
+ if (!Number.isFinite(value)) throw new TypeError("JCS does not allow non-finite numbers.");
9
+ return JSON.stringify(value);
10
+ }
11
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
12
+ if (value && typeof value === "object") {
13
+ return `{${Object.keys(value)
14
+ .sort()
15
+ .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`)
16
+ .join(",")}}`;
17
+ }
18
+ throw new TypeError(`JCS does not allow ${typeof value} values.`);
19
+ }
20
+
21
+ export function bundlePayloadBytes(bundle) {
22
+ const { integrity: _integrity, ...payload } = bundle;
23
+ return Buffer.from(canonicalize(payload), "utf8");
24
+ }
25
+
26
+ export function bundleDigest(bundle) {
27
+ return createHash("sha256").update(bundlePayloadBytes(bundle)).digest();
28
+ }
29
+
30
+ export function verifyBundleIntegrity(bundle, publicKey) {
31
+ const digest = bundleDigest(bundle);
32
+ const expected = `sha256:${digest.toString("hex")}`;
33
+ if (bundle?.integrity?.digest !== expected) {
34
+ throw new Error(`SQM bundle digest mismatch: expected ${expected}.`);
35
+ }
36
+ const encoded = String(bundle?.integrity?.signature || "");
37
+ if (!/^[A-Za-z0-9+/]+$/.test(encoded)) {
38
+ throw new Error("SQM bundle signature is not valid base64.");
39
+ }
40
+ const signature = Buffer.from(encoded, "base64");
41
+ if (signature.length !== 64 || !verify(null, digest, publicKey, signature)) {
42
+ throw new Error("SQM bundle Ed25519 signature verification failed.");
43
+ }
44
+ return expected;
45
+ }