@f5-sales-demo/xcsh 22.5.2 → 22.7.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.
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "22.5.2",
21
- "commit": "ddbc149ff3ab8184b952df625a2590e27fcf86e9",
22
- "shortCommit": "ddbc149",
20
+ "version": "22.7.0",
21
+ "commit": "72fca0c9c820382ced59f8437bf56100f2671760",
22
+ "shortCommit": "72fca0c",
23
23
  "branch": "main",
24
- "tag": "v22.5.2",
25
- "commitDate": "2026-09-30T18:41:58Z",
26
- "buildDate": "2026-09-30T19:16:02.766Z",
24
+ "tag": "v22.7.0",
25
+ "commitDate": "2026-10-01T14:28:59Z",
26
+ "buildDate": "2026-10-01T15:21:46.592Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/ddbc149ff3ab8184b952df625a2590e27fcf86e9",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.5.2"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/72fca0c9c820382ced59f8437bf56100f2671760",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.7.0"
33
33
  };
@@ -0,0 +1,2 @@
1
+ import type { TerraformEmbeddedAssets } from "./terraform-documentation";
2
+ export const EMBEDDED_TERRAFORM_DOCUMENTATION: TerraformEmbeddedAssets | null = null;
@@ -0,0 +1,660 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { gunzipSync } from "node:zlib";
7
+ import { createStore } from "@tobilu/qmd";
8
+ import tar from "tar-stream";
9
+ import { parse as parseYaml } from "yaml";
10
+ import { type DocumentationPassage, githubHeadingAnchor } from "./documentation-metadata";
11
+ import type { InternalResource, InternalUrl } from "./types";
12
+
13
+ export const TERRAFORM_ASSETS = ["terraform-docs.tar.gz", "manifest.json", "publication.json", "SHA256SUMS"] as const;
14
+ export interface TerraformMetadata {
15
+ id: string;
16
+ canonical_id: string;
17
+ path: string;
18
+ provider_type: string;
19
+ provider_name: string;
20
+ role: string;
21
+ schema_path: string[];
22
+ summary: string;
23
+ aliases: string[];
24
+ parent_id: string | null;
25
+ child_ids: string[];
26
+ projection_part?: number;
27
+ [key: string]: unknown;
28
+ }
29
+ export interface TerraformDocument {
30
+ path: string;
31
+ size_bytes: number;
32
+ sha256: string;
33
+ body_sha256: string;
34
+ metadata: TerraformMetadata;
35
+ markdown: string;
36
+ body: string;
37
+ }
38
+ export interface TerraformPin {
39
+ schema_version: 2;
40
+ source_root: "documentation";
41
+ source_repository: "f5-sales-demo/terraform-provider-xcsh";
42
+ provider_version: string;
43
+ release_tag: string;
44
+ source_commit: string;
45
+ receipt_sha256: string;
46
+ document_count: number;
47
+ provider_schema_digest: string;
48
+ spec_pin_digest: string;
49
+ assets: Record<string, { sha256: string; size_bytes: number }>;
50
+ index?: { sha256: string; size_bytes: number; gzip_sha256: string; gzip_size_bytes: number };
51
+ }
52
+ export function terraformHash(data: Uint8Array | string): string {
53
+ return createHash("sha256").update(data).digest("hex");
54
+ }
55
+ function safePath(value: string): void {
56
+ if (
57
+ !/^documentation\/(?:[A-Za-z0-9_~-]+\/)*[A-Za-z0-9_.~-]+\.md$/.test(value) ||
58
+ value.split("/").some(v => v === "." || v === "..")
59
+ )
60
+ throw new Error(`Unsafe Terraform document path: ${value}`);
61
+ }
62
+ export function parseTerraformPin(value: unknown): TerraformPin {
63
+ const pin = value as TerraformPin;
64
+ if (
65
+ pin?.schema_version !== 2 ||
66
+ pin.source_root !== "documentation" ||
67
+ pin.source_repository !== "f5-sales-demo/terraform-provider-xcsh" ||
68
+ !/^v\d+\.\d+\.\d+$/.test(pin.provider_version) ||
69
+ pin.release_tag !== `documentation-${pin.provider_version}` ||
70
+ !/^[a-f0-9]{40}$/.test(pin.source_commit) ||
71
+ !/^[a-f0-9]{64}$/.test(pin.receipt_sha256) ||
72
+ !Number.isSafeInteger(pin.document_count) ||
73
+ pin.document_count < 1 ||
74
+ pin.document_count > 100000 ||
75
+ !/^sha256:[a-f0-9]{64}$/.test(pin.provider_schema_digest) ||
76
+ !/^sha256:[a-f0-9]{64}$/.test(pin.spec_pin_digest)
77
+ )
78
+ throw new Error("Invalid Terraform snapshot identity");
79
+ if (
80
+ Object.keys(pin.assets ?? {})
81
+ .sort()
82
+ .join() !== [...TERRAFORM_ASSETS].sort().join()
83
+ )
84
+ throw new Error("Invalid Terraform snapshot assets");
85
+ for (const asset of Object.values(pin.assets)) {
86
+ if (
87
+ !/^[a-f0-9]{64}$/.test(asset.sha256) ||
88
+ !Number.isSafeInteger(asset.size_bytes) ||
89
+ asset.size_bytes <= 0 ||
90
+ asset.size_bytes > 512 * 1024 * 1024
91
+ )
92
+ throw new Error("Invalid Terraform asset hash or size");
93
+ }
94
+ if (pin.assets["publication.json"]!.sha256 !== pin.receipt_sha256) throw new Error("Terraform receipt pin mismatch");
95
+ if (
96
+ pin.index &&
97
+ (!/^[a-f0-9]{64}$/.test(pin.index.sha256) ||
98
+ !/^[a-f0-9]{64}$/.test(pin.index.gzip_sha256) ||
99
+ !Number.isSafeInteger(pin.index.size_bytes) ||
100
+ pin.index.size_bytes < 1 ||
101
+ pin.index.size_bytes > 2 * 1024 * 1024 * 1024 ||
102
+ !Number.isSafeInteger(pin.index.gzip_size_bytes) ||
103
+ pin.index.gzip_size_bytes < 1 ||
104
+ pin.index.gzip_size_bytes > 512 * 1024 * 1024)
105
+ )
106
+ throw new Error("Invalid Terraform index pin");
107
+ return pin;
108
+ }
109
+ function splitMarkdown(markdown: string): { body: string; enriched: unknown } {
110
+ if (!markdown.startsWith("---\n")) return { body: markdown, enriched: undefined };
111
+ const end = markdown.indexOf("\n---\n", 4);
112
+ if (end < 0) throw new Error("Unterminated Terraform frontmatter");
113
+ const header = parseYaml(markdown.slice(4, end));
114
+ return { body: markdown.slice(end + 5).replace(/^\n/, ""), enriched: header?.xcsh_docs };
115
+ }
116
+
117
+ export async function verifyTerraformSnapshot(root: string, inputPin: TerraformPin): Promise<TerraformDocument[]> {
118
+ const pin = parseTerraformPin(inputPin);
119
+ const files = new Map<string, Buffer>();
120
+ for (const name of TERRAFORM_ASSETS) {
121
+ const data = await readFile(path.join(root, name));
122
+ const expected = pin.assets[name]!;
123
+ if (data.length !== expected.size_bytes || terraformHash(data) !== expected.sha256)
124
+ throw new Error(`Terraform asset verification failed: ${name}`);
125
+ files.set(name, data);
126
+ }
127
+ const receipt = JSON.parse(files.get("publication.json")!.toString());
128
+ for (const key of [
129
+ "schema_version",
130
+ "source_root",
131
+ "source_repository",
132
+ "provider_version",
133
+ "release_tag",
134
+ "source_commit",
135
+ "document_count",
136
+ "provider_schema_digest",
137
+ "spec_pin_digest",
138
+ ] as const)
139
+ if (receipt[key] !== pin[key]) throw new Error(`Terraform receipt identity mismatch: ${key}`);
140
+ for (const name of ["manifest.json", "terraform-docs.tar.gz"]) {
141
+ if (JSON.stringify(receipt.assets[name]) !== JSON.stringify(pin.assets[name])) {
142
+ if (
143
+ receipt.assets[name]?.sha256 !== pin.assets[name]!.sha256 ||
144
+ receipt.assets[name]?.size_bytes !== pin.assets[name]!.size_bytes
145
+ )
146
+ throw new Error(`Terraform receipt asset mismatch: ${name}`);
147
+ }
148
+ }
149
+ const sums = files.get("SHA256SUMS")!.toString().trim().split("\n");
150
+ if (sums.length !== 3) throw new Error("Invalid Terraform checksum list");
151
+ const seenSums = new Set<string>();
152
+ for (const line of sums) {
153
+ const match = /^([a-f0-9]{64}) {2}(manifest.json|publication.json|terraform-docs.tar.gz)$/.exec(line);
154
+ if (!match || seenSums.has(match[2]!) || pin.assets[match[2]!]!.sha256 !== match[1])
155
+ throw new Error("Terraform checksum mismatch");
156
+ seenSums.add(match[2]!);
157
+ }
158
+ const manifest = JSON.parse(files.get("manifest.json")!.toString());
159
+ for (const key of [
160
+ "schema_version",
161
+ "source_root",
162
+ "source_repository",
163
+ "provider_version",
164
+ "source_commit",
165
+ "document_count",
166
+ "provider_schema_digest",
167
+ "spec_pin_digest",
168
+ ] as const)
169
+ if (manifest[key] !== pin[key]) throw new Error(`Terraform manifest identity mismatch: ${key}`);
170
+ if (!Array.isArray(manifest.documents) || manifest.documents.length !== pin.document_count)
171
+ throw new Error("Terraform document count mismatch");
172
+ const expected = new Map<string, Omit<TerraformDocument, "markdown" | "body">>();
173
+ for (const entry of manifest.documents) {
174
+ safePath(entry.path);
175
+ if (expected.has(entry.path)) throw new Error("Duplicate Terraform document path");
176
+ if (
177
+ !Number.isSafeInteger(entry.size_bytes) ||
178
+ entry.size_bytes < 1 ||
179
+ entry.size_bytes > 4 * 1024 * 1024 ||
180
+ !/^[a-f0-9]{64}$/.test(entry.sha256) ||
181
+ !/^[a-f0-9]{64}$/.test(entry.body_sha256)
182
+ )
183
+ throw new Error("Invalid Terraform document hash or size");
184
+ const m = entry.metadata as TerraformMetadata;
185
+ if (
186
+ !m ||
187
+ m.path !== entry.path ||
188
+ typeof m.id !== "string" ||
189
+ typeof m.canonical_id !== "string" ||
190
+ ![m.provider_type, m.provider_name, m.role].every(v => typeof v === "string" && /^[A-Za-z0-9_-]+$/.test(v)) ||
191
+ typeof m.summary !== "string" ||
192
+ !Array.isArray(m.schema_path) ||
193
+ !Array.isArray(m.aliases) ||
194
+ !Array.isArray(m.child_ids) ||
195
+ ![...m.schema_path, ...m.aliases, ...m.child_ids].every(v => typeof v === "string") ||
196
+ (m.parent_id !== null && typeof m.parent_id !== "string") ||
197
+ (m.projection_part !== undefined && (!Number.isSafeInteger(m.projection_part) || m.projection_part < 1))
198
+ )
199
+ throw new Error("Invalid Terraform metadata");
200
+ expected.set(entry.path, entry);
201
+ }
202
+ const documents: TerraformDocument[] = [];
203
+ const extract = tar.extract();
204
+ let failure: Error | undefined;
205
+ let total = 0;
206
+ extract.on("entry", (header, stream, next) => {
207
+ const chunks: Buffer[] = [];
208
+ try {
209
+ safePath(header.name);
210
+ const entry = expected.get(header.name);
211
+ if (header.type !== "file" || !entry || header.size !== entry.size_bytes)
212
+ throw new Error("Unexpected Terraform archive member");
213
+ expected.delete(header.name);
214
+ } catch (error) {
215
+ failure = error as Error;
216
+ }
217
+ let bytes = 0;
218
+ stream.on("data", chunk => {
219
+ bytes += chunk.length;
220
+ total += chunk.length;
221
+ if (bytes > 4 * 1024 * 1024 || total > 1024 * 1024 * 1024)
222
+ failure = new Error("Terraform archive exceeds size limit");
223
+ if (!failure) chunks.push(Buffer.from(chunk));
224
+ });
225
+ stream.on("end", () => {
226
+ if (!failure) {
227
+ try {
228
+ const data = Buffer.concat(chunks);
229
+ const entry = manifest.documents.find((d: TerraformDocument) => d.path === header.name);
230
+ if (terraformHash(data) !== entry.sha256) throw new Error("Terraform document hash mismatch");
231
+ const markdown = new TextDecoder("utf-8", { fatal: true }).decode(data);
232
+ const parsed = splitMarkdown(markdown);
233
+ if (terraformHash(parsed.body) !== entry.body_sha256) throw new Error("Terraform body hash mismatch");
234
+ if (parsed.enriched) {
235
+ const m = parsed.enriched as TerraformMetadata;
236
+ if (
237
+ m.body_sha256 !== `sha256:${entry.body_sha256}` ||
238
+ m.body_bytes !== Buffer.byteLength(parsed.body) ||
239
+ JSON.stringify(Object.entries({ ...m, canonical_id: m.canonical_id ?? m.id }).sort()) !==
240
+ JSON.stringify(Object.entries(entry.metadata).sort())
241
+ )
242
+ throw new Error("Terraform enriched metadata mismatch");
243
+ }
244
+ documents.push({ ...entry, markdown, body: parsed.body });
245
+ } catch (error) {
246
+ failure = error as Error;
247
+ }
248
+ }
249
+ next();
250
+ });
251
+ });
252
+ const archive = gunzipSync(files.get("terraform-docs.tar.gz")!, { maxOutputLength: 1024 * 1024 * 1024 });
253
+ await new Promise<void>((resolve, reject) => {
254
+ extract.on("finish", resolve);
255
+ extract.on("error", reject);
256
+ Readable.from(archive).pipe(extract);
257
+ });
258
+ if (failure) throw failure;
259
+ if (expected.size || documents.length !== pin.document_count) throw new Error("Missing Terraform archive documents");
260
+ const ids = new Set(documents.flatMap(d => [d.metadata.id, d.metadata.canonical_id]));
261
+ for (const d of documents)
262
+ for (const id of [d.metadata.parent_id, ...d.metadata.child_ids])
263
+ if (id && !ids.has(id)) throw new Error(`Missing Terraform relationship target: ${id}`);
264
+ const paths = new Map(documents.map(d => [d.path, d]));
265
+ for (const document of documents) {
266
+ const prose = document.body.replace(/```[^\n]*\n[\s\S]*?```/g, "");
267
+ for (const match of prose.matchAll(/\[[^\]\n]+\]\(([^)\n]+)\)/g)) {
268
+ const destination = terraformLinkPath(match[1]!, document.path);
269
+ if (!destination) continue;
270
+ const target = paths.get(destination.path);
271
+ if (!target) throw new Error(`Missing Terraform internal link: ${destination.path}`);
272
+ if (destination.anchor && !terraformPassages(target.body).some(p => p.anchor === destination.anchor))
273
+ throw new Error(`Missing Terraform internal anchor: ${destination.path}#${destination.anchor}`);
274
+ }
275
+ }
276
+ return documents.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
277
+ }
278
+
279
+ export function terraformPassages(body: string, complete = true): DocumentationPassage[] {
280
+ const lines = body.split("\n");
281
+ const starts: Array<{ line: number; level: number; anchor: string; heading: string }> = [];
282
+ const occurrences = new Map<string, number>();
283
+ let fence: string | undefined;
284
+ for (const [line, value] of lines.entries()) {
285
+ const marker = /^\s*(`{3,}|~{3,})/.exec(value)?.[1];
286
+ if (marker) {
287
+ if (!fence) fence = marker[0];
288
+ else if (fence === marker[0]) fence = undefined;
289
+ continue;
290
+ }
291
+ if (fence) continue;
292
+ const explicit = /^\s*<a\s+(?:id|name)=["']([^"']+)["']\s*><\/a>\s*$/.exec(value);
293
+ if (explicit) {
294
+ let nextLine = line + 1;
295
+ while (nextLine < lines.length && !lines[nextLine]!.trim()) nextLine++;
296
+ const next = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[nextLine] ?? "");
297
+ starts.push({ line, level: next?.[1]?.length ?? 3, anchor: explicit[1]!, heading: next?.[2] ?? explicit[1]! });
298
+ }
299
+ const heading = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(value);
300
+ if (heading)
301
+ starts.push({
302
+ line,
303
+ level: heading[1]!.length,
304
+ anchor: githubHeadingAnchor(heading[2]!, occurrences),
305
+ heading: heading[2]!,
306
+ });
307
+ }
308
+ const anchors = new Set<string>();
309
+ return starts
310
+ .filter(s => {
311
+ if (anchors.has(s.anchor)) return false;
312
+ anchors.add(s.anchor);
313
+ return true;
314
+ })
315
+ .map((start, ordinal) => {
316
+ const end =
317
+ starts.find(
318
+ s =>
319
+ s.line > start.line &&
320
+ !(
321
+ /^\s*<a\s/.test(lines[start.line]!) &&
322
+ s.heading === start.heading &&
323
+ lines.slice(start.line + 1, s.line).every(value => !value.trim())
324
+ ) &&
325
+ (!complete || s.level <= start.level),
326
+ )?.line ?? lines.length;
327
+ return {
328
+ anchor: start.anchor,
329
+ heading: start.heading,
330
+ markdown: lines.slice(start.line, end).join("\n"),
331
+ ordinal,
332
+ };
333
+ })
334
+ .concat(starts.length ? [] : [{ anchor: "document", heading: "Document", markdown: body, ordinal: 0 }]);
335
+ }
336
+
337
+ export async function buildTerraformIndex(
338
+ documents: TerraformDocument[],
339
+ pin: TerraformPin,
340
+ output: string,
341
+ ): Promise<void> {
342
+ await rm(output, { force: true });
343
+ const store = await createStore({
344
+ dbPath: output,
345
+ config: { collections: { terraform: { path: "/terraform", pattern: "**/*.md" } } },
346
+ });
347
+ try {
348
+ const db = store.internal.db;
349
+ db.exec(
350
+ "CREATE TABLE terraform_documents(path TEXT PRIMARY KEY, metadata TEXT NOT NULL, markdown TEXT NOT NULL); CREATE TABLE terraform_passages(qmd_path TEXT PRIMARY KEY,path TEXT NOT NULL,anchor TEXT NOT NULL,heading TEXT NOT NULL,ordinal INTEGER NOT NULL); CREATE TABLE terraform_provenance(pin TEXT NOT NULL)",
351
+ );
352
+ const { index: _index, ...snapshotPin } = pin;
353
+ db.prepare("INSERT INTO terraform_provenance VALUES (?)").run(JSON.stringify(snapshotPin));
354
+ const insertDoc = db.prepare("INSERT INTO terraform_documents VALUES (?,?,?)");
355
+ const insertPassage = db.prepare("INSERT INTO terraform_passages VALUES (?,?,?,?,?)");
356
+ db.transaction(() => {
357
+ for (const d of documents) {
358
+ insertDoc.run(d.path, JSON.stringify(d.metadata), d.markdown);
359
+ const m = d.metadata;
360
+ const title = [
361
+ m.provider_name,
362
+ `xcsh_${m.provider_name}`,
363
+ m.provider_type,
364
+ m.role,
365
+ m.schema_path.join(" "),
366
+ m.summary,
367
+ ...m.aliases,
368
+ ].join(" ");
369
+ for (const p of terraformPassages(d.body, false)) {
370
+ const qmdPath = `${d.path}#${p.anchor}`;
371
+ const hash = terraformHash(`${d.sha256}\0${p.anchor}\0${p.markdown}`);
372
+ store.internal.insertContent(hash, p.markdown, "2000-01-01T00:00:00Z");
373
+ store.internal.insertDocument(
374
+ "terraform",
375
+ qmdPath,
376
+ `${title} ${p.heading}`,
377
+ hash,
378
+ "2000-01-01T00:00:00Z",
379
+ "2000-01-01T00:00:00Z",
380
+ );
381
+ insertPassage.run(qmdPath, d.path, p.anchor, p.heading, p.ordinal);
382
+ }
383
+ }
384
+ })();
385
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE); PRAGMA journal_mode=DELETE; VACUUM");
386
+ } finally {
387
+ await store.close();
388
+ }
389
+ }
390
+
391
+ export interface TerraformEmbeddedAssets {
392
+ indexGzipPath: string;
393
+ pin: TerraformPin & { index: NonNullable<TerraformPin["index"]> };
394
+ }
395
+ const TERRAFORM_QUERY_STOPWORDS = new Set(
396
+ "a an the and how where what do does i we you my me our your can could should would please help tell know put already have that to for of in on with using use configure configuration setup set up terraform provider documentation document".split(
397
+ " ",
398
+ ),
399
+ );
400
+ const TERRAFORM_QUERY_VARIANTS: Record<string, string> = {
401
+ succeeded: "success",
402
+ successful: "success",
403
+ succeeds: "success",
404
+ failed: "failure",
405
+ fails: "failure",
406
+ cert: "certificate",
407
+ certs: "certificate",
408
+ };
409
+
410
+ export function terraformSearchQuery(query: string): string {
411
+ const terms =
412
+ query
413
+ .normalize("NFKC")
414
+ .toLowerCase()
415
+ .replace(/http load balancer/g, "http_loadbalancer")
416
+ .replace(/cdn load balancer/g, "cdn_loadbalancer")
417
+ .match(/[\p{L}\p{N}_]+/gu) ?? [];
418
+ return [...new Set(terms.filter(term => !TERRAFORM_QUERY_STOPWORDS.has(term)))]
419
+ .map(term => {
420
+ const variant = TERRAFORM_QUERY_VARIANTS[term];
421
+ return variant ? `("${term}"* OR "${variant}"*)` : `"${term}"*`;
422
+ })
423
+ .join(" AND ");
424
+ }
425
+
426
+ interface TerraformNavigationNode {
427
+ path: string;
428
+ id: string;
429
+ parent: string | null;
430
+ children: string;
431
+ label: string;
432
+ part: number | null;
433
+ }
434
+
435
+ export class TerraformDocumentationRepository {
436
+ #ready: Promise<Database> | undefined;
437
+ #navigation: Map<string, TerraformNavigationNode> | undefined;
438
+ constructor(
439
+ readonly assets: TerraformEmbeddedAssets,
440
+ readonly cacheRoot: string,
441
+ ) {}
442
+ async database(): Promise<Database> {
443
+ if (!this.#ready)
444
+ this.#ready = this.#materialize().catch(error => {
445
+ this.#ready = undefined;
446
+ throw error;
447
+ });
448
+ return this.#ready;
449
+ }
450
+ async #materialize(): Promise<Database> {
451
+ const pin = parseTerraformPin(this.assets.pin);
452
+ const expected = this.assets.pin.index;
453
+ await mkdir(this.cacheRoot, { recursive: true });
454
+ const file = path.join(this.cacheRoot, `${expected.sha256}.sqlite`);
455
+ let cached: Buffer | undefined;
456
+ try {
457
+ cached = await readFile(file);
458
+ } catch {}
459
+ if (!cached || cached.length !== expected.size_bytes || terraformHash(cached) !== expected.sha256) {
460
+ const compressed = await Bun.file(this.assets.indexGzipPath).bytes();
461
+ if (compressed.length !== expected.gzip_size_bytes || terraformHash(compressed) !== expected.gzip_sha256)
462
+ throw new Error("Terraform compressed index verification failed");
463
+ const bytes = gunzipSync(compressed, { maxOutputLength: expected.size_bytes });
464
+ if (bytes.length !== expected.size_bytes || terraformHash(bytes) !== expected.sha256)
465
+ throw new Error("Terraform index verification failed");
466
+ const temporary = `${file}.${randomUUID()}.tmp`;
467
+ try {
468
+ await writeFile(temporary, bytes, { mode: 0o600 });
469
+ await rename(temporary, file);
470
+ } finally {
471
+ await rm(temporary, { force: true });
472
+ }
473
+ }
474
+ const db = new Database(file, { readonly: true });
475
+ const row = db.query("SELECT pin FROM terraform_provenance").get() as { pin: string };
476
+ const indexed = JSON.parse(row.pin);
477
+ if (indexed.source_commit !== pin.source_commit || indexed.receipt_sha256 !== pin.receipt_sha256) {
478
+ db.close();
479
+ throw new Error("Terraform index provenance mismatch");
480
+ }
481
+ return db;
482
+ }
483
+ #trace(db: Database, documentPath: string): string {
484
+ if (!this.#navigation) {
485
+ const nodes = db
486
+ .query(
487
+ "SELECT path,json_extract(metadata,'$.id') id,json_extract(metadata,'$.parent_id') parent,json_extract(metadata,'$.child_ids') children,json_extract(metadata,'$.summary') label,json_extract(metadata,'$.projection_part') part FROM terraform_documents ORDER BY path COLLATE BINARY",
488
+ )
489
+ .all() as TerraformNavigationNode[];
490
+ this.#navigation = new Map(nodes.map(node => [node.path, node]));
491
+ for (const node of nodes) if (!node.part || node.part === 1) this.#navigation.set(node.id, node);
492
+ }
493
+ const selected = this.#navigation.get(documentPath);
494
+ if (!selected) return "";
495
+ const trail: TerraformNavigationNode[] = [];
496
+ const seen = new Set<string>();
497
+ let current: TerraformNavigationNode | undefined = selected;
498
+ while (current && !seen.has(current.path)) {
499
+ seen.add(current.path);
500
+ trail.unshift(current);
501
+ current = current.parent ? this.#navigation.get(current.parent) : undefined;
502
+ }
503
+ const link = (node: TerraformNavigationNode) => `- [${node.label}](xcsh://terraform-documentation/${node.path})`;
504
+ const children = (JSON.parse(selected.children) as string[])
505
+ .map(id => this.#navigation!.get(id))
506
+ .filter((node): node is TerraformNavigationNode => Boolean(node));
507
+ return [
508
+ "Documentation trail:",
509
+ ...trail.map(link),
510
+ ...(children.length ? ["Child sections:", ...children.map(link)] : []),
511
+ ].join("\n");
512
+ }
513
+
514
+ async resolve(url: InternalUrl): Promise<InternalResource> {
515
+ const allowed = new Set(["search", "provider_type", "provider_name", "role", "limit"]);
516
+ for (const key of url.searchParams.keys())
517
+ if (!allowed.has(key) || url.searchParams.getAll(key).length !== 1)
518
+ throw new Error(`Invalid Terraform parameter: ${key}`);
519
+ const rawPath = url.rawPathname ?? url.pathname;
520
+ if (
521
+ /%(?:2f|5c|2e)/i.test(rawPath) ||
522
+ rawPath.includes("\\") ||
523
+ rawPath.split("/").some(v => v === "." || v === "..")
524
+ )
525
+ throw new Error("Unsafe Terraform request path");
526
+ const documentPath = rawPath.replace(/^\//, "");
527
+ const search = url.searchParams.get("search")?.trim();
528
+ if (search !== undefined && (!search || Buffer.byteLength(search) > 512))
529
+ throw new Error("Terraform search must contain 1 to 512 UTF-8 bytes");
530
+ const limitValue = url.searchParams.get("limit");
531
+ if (limitValue !== null && !/^(?:[1-9]|10)$/.test(limitValue))
532
+ throw new Error("Terraform search limit must be 1 to 10");
533
+ if (!search && url.searchParams.size) throw new Error("Terraform filters and limit require search");
534
+ if (documentPath && search) throw new Error("Terraform search requires the inventory path");
535
+ if (documentPath) safePath(documentPath);
536
+ const db = await this.database();
537
+ const provenance = `Provider: ${this.assets.pin.provider_version}\nSnapshot: ${this.assets.pin.release_tag}\nCommit: ${this.assets.pin.source_commit}\nReceipt SHA-256: ${this.assets.pin.receipt_sha256}`;
538
+ let content: string;
539
+ if (documentPath) {
540
+ const row = db.query("SELECT * FROM terraform_documents WHERE path=?").get(documentPath) as {
541
+ metadata: string;
542
+ markdown: string;
543
+ body: string;
544
+ } | null;
545
+ if (!row) throw new Error(`Terraform document not found: ${documentPath}`);
546
+ const sections = terraformPassages(splitMarkdown(row.markdown).body);
547
+ const sectionLinks = sections
548
+ .filter(p => p.anchor.startsWith("schema-"))
549
+ .map(p => `- [${p.heading}](xcsh://terraform-documentation/${documentPath}#${p.anchor})`)
550
+ .join("\n");
551
+ const navigation = `${this.#trace(db, documentPath)}${sectionLinks ? `\nProperty sections:\n${sectionLinks}` : ""}`;
552
+ const anchor = decodeURIComponent(url.hash.slice(1));
553
+ const passage = anchor ? sections.find(p => p.anchor === anchor) : undefined;
554
+ if (anchor && !passage) throw new Error(`Terraform anchor not found: ${anchor}`);
555
+ content = `${provenance}\nDocument: ${documentPath}\nMetadata: ${row.metadata}\n${navigation}\nPinned source: https://github.com/${this.assets.pin.source_repository}/blob/${this.assets.pin.source_commit}/${documentPath}\n\n${rewriteTerraformLinks(passage?.markdown ?? row.markdown, documentPath)}`;
556
+ } else if (search) {
557
+ const clauses = ["documents_fts MATCH ?", "d.active=1"];
558
+ const query = terraformSearchQuery(search);
559
+ const args: Array<string | number> = [query];
560
+ for (const key of ["provider_type", "provider_name", "role"]) {
561
+ const value = url.searchParams.get(key);
562
+ if (value === null) continue;
563
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error(`Invalid Terraform filter: ${key}`);
564
+ clauses.push(`json_extract(td.metadata,'$.${key}')=?`);
565
+ args.push(value);
566
+ }
567
+ args.push(limitValue === null ? 5 : Number(limitValue));
568
+ const statement = db.query(`WITH scored AS (
569
+ SELECT td.path,td.metadata,p.anchor,p.heading,c.doc AS markdown,p.ordinal,
570
+ ABS(bm25(documents_fts,1.5,4.0,1.0))/(1+ABS(bm25(documents_fts,1.5,4.0,1.0))) score
571
+ FROM documents_fts JOIN documents d ON d.id=documents_fts.rowid
572
+ JOIN content c ON c.hash=d.hash JOIN terraform_passages p ON p.qmd_path=d.path JOIN terraform_documents td ON td.path=p.path
573
+ WHERE ${clauses.join(" AND ")}), ranked AS (
574
+ SELECT *,ROW_NUMBER() OVER(PARTITION BY path ORDER BY score DESC,ordinal ASC) rank FROM scored)
575
+ SELECT * FROM ranked WHERE rank=1 ORDER BY score DESC,path COLLATE BINARY,ordinal LIMIT ?`);
576
+ type SearchRow = {
577
+ path: string;
578
+ metadata: string;
579
+ anchor: string;
580
+ heading: string;
581
+ markdown: string;
582
+ score: number;
583
+ };
584
+ let rows = query ? (statement.all(...args) as SearchRow[]) : [];
585
+ let broadened = false;
586
+ if (!rows.length && query.includes(" AND ")) {
587
+ const knownTerms = query
588
+ .split(" AND ")
589
+ .every(term =>
590
+ Boolean(db.query("SELECT 1 FROM documents_fts WHERE documents_fts MATCH ? LIMIT 1").get(term)),
591
+ );
592
+ if (knownTerms) {
593
+ args[0] = query.replaceAll(" AND ", " OR ");
594
+ rows = statement.all(...args) as SearchRow[];
595
+ broadened = rows.length > 0;
596
+ }
597
+ }
598
+ content = `${provenance}\n\n# Terraform search: ${search}\n${broadened ? "Broader word matching was needed; verify the candidate and clarify ambiguous configuration choices.\n" : ""}\n${
599
+ rows.length
600
+ ? rows
601
+ .map(r => {
602
+ const m = JSON.parse(r.metadata) as TerraformMetadata;
603
+ return `## ${m.provider_type}: xcsh_${m.provider_name} — ${m.role}\nDocument: ${r.path}\nIdentity: ${m.id}\nCanonical: ${m.canonical_id}\nSchema path: ${m.schema_path.join(".") || "root"}\nScore: ${Number(r.score.toFixed(12))}\nRead: xcsh://terraform-documentation/${r.path}#${r.anchor}\n${this.#trace(db, r.path)}\n\n${rewriteTerraformLinks(r.markdown, r.path).replace(/\s+/g, " ").slice(0, 600)}`;
604
+ })
605
+ .join("\n\n")
606
+ : "No results."
607
+ }`;
608
+ } else {
609
+ const values = (key: string) =>
610
+ (
611
+ db
612
+ .query(
613
+ `SELECT DISTINCT json_extract(metadata,'$.${key}') value FROM terraform_documents ORDER BY value COLLATE BINARY`,
614
+ )
615
+ .all() as Array<{ value: string }>
616
+ )
617
+ .map(r => r.value)
618
+ .join(", ");
619
+ content = `${provenance}\n\n# Offline Terraform documentation\n${this.assets.pin.document_count} Markdown documents from documentation/.\n\nSearch: xcsh://terraform-documentation/?search=<query>&provider_type=<type>&provider_name=<name>&role=<role>&limit=<1-10>\nDefault limit: five. Filters combine with AND.\nprovider_type: ${values("provider_type")}\nprovider_name: ${values("provider_name")}\nrole: ${values("role")}\n\nExact reads: xcsh://terraform-documentation/documentation/<path>/index.md#<heading-or-explicit-anchor>\nGuidance reflects documented schema validation; it is not live-apply evidence.`;
620
+ }
621
+ return {
622
+ url: url.href,
623
+ content,
624
+ contentType: "text/markdown",
625
+ size: Buffer.byteLength(content),
626
+ sourcePath: url.href,
627
+ };
628
+ }
629
+ }
630
+
631
+ export function terraformLinkPath(href: string, source: string): { path: string; anchor: string } | null {
632
+ let target = href;
633
+ if (/^https?:\/\//.test(href)) {
634
+ const url = new URL(href);
635
+ const base = "/terraform-provider-xcsh/";
636
+ if (url.hostname !== "f5-sales-demo.github.io" || !url.pathname.startsWith(base)) return null;
637
+ let relative = url.pathname.slice(base.length);
638
+ if (relative.startsWith("_data/") || relative.endsWith(".json") || relative.endsWith(".txt")) return null;
639
+ if (relative.endsWith("/") || !relative) relative += "index.md";
640
+ target = `documentation/${relative}${url.hash}`;
641
+ } else if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(href) || href.startsWith("//")) return null;
642
+ const [relative, anchor = ""] = target.split("#");
643
+ const resolved = relative?.startsWith("documentation/")
644
+ ? relative
645
+ : relative
646
+ ? path.posix.normalize(path.posix.join(path.posix.dirname(source), relative))
647
+ : source;
648
+ if (!resolved.endsWith(".md")) return null;
649
+ safePath(resolved);
650
+ return { path: resolved, anchor };
651
+ }
652
+
653
+ export function rewriteTerraformLinks(markdown: string, source: string): string {
654
+ return markdown.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, (whole, title: string, href: string) => {
655
+ const destination = terraformLinkPath(href, source);
656
+ return destination
657
+ ? `[${title}](xcsh://terraform-documentation/${destination.path}${destination.anchor ? `#${destination.anchor}` : ""})`
658
+ : whole;
659
+ });
660
+ }