@graphorin/cli 0.6.0 → 0.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.
- package/CHANGELOG.md +80 -0
- package/README.md +4 -4
- package/dist/bin/graphorin.js +51 -13
- package/dist/bin/graphorin.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +2 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/consolidator.d.ts +56 -1
- package/dist/commands/consolidator.d.ts.map +1 -1
- package/dist/commands/consolidator.js +88 -2
- package/dist/commands/consolidator.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/index.d.ts +4 -4
- package/dist/commands/index.js +4 -4
- package/dist/commands/init.d.ts +11 -4
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -11
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/memory.d.ts +26 -1
- package/dist/commands/memory.d.ts.map +1 -1
- package/dist/commands/memory.js +56 -3
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/migrate-export.d.ts +1 -2
- package/dist/commands/migrate-export.d.ts.map +1 -1
- package/dist/commands/migrate-export.js +2 -1
- package/dist/commands/migrate-export.js.map +1 -1
- package/dist/commands/pricing.d.ts +6 -0
- package/dist/commands/pricing.d.ts.map +1 -1
- package/dist/commands/pricing.js +5 -2
- package/dist/commands/pricing.js.map +1 -1
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +2 -2
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/storage.d.ts +41 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +75 -1
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +2 -2
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/tools-lint.js +1 -1
- package/dist/commands/tools-lint.js.map +1 -1
- package/dist/commands/traces.d.ts +14 -2
- package/dist/commands/traces.d.ts.map +1 -1
- package/dist/commands/traces.js +39 -22
- package/dist/commands/traces.js.map +1 -1
- package/dist/commands/triggers.d.ts.map +1 -1
- package/dist/commands/triggers.js +5 -2
- package/dist/commands/triggers.js.map +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/output.js +6 -0
- package/dist/internal/output.js.map +1 -1
- package/dist/internal/store-context.js +13 -2
- package/dist/internal/store-context.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +18 -14
- package/src/bin/graphorin.ts +1387 -0
- package/src/commands/audit.ts +256 -0
- package/src/commands/auth.ts +238 -0
- package/src/commands/consolidator.ts +382 -0
- package/src/commands/doctor.ts +253 -0
- package/src/commands/guard.ts +144 -0
- package/src/commands/index.ts +223 -0
- package/src/commands/init.ts +194 -0
- package/src/commands/memory.ts +1052 -0
- package/src/commands/migrate-config.ts +77 -0
- package/src/commands/migrate-export.ts +117 -0
- package/src/commands/migrate.ts +83 -0
- package/src/commands/pricing.ts +244 -0
- package/src/commands/secrets.ts +309 -0
- package/src/commands/skills.ts +272 -0
- package/src/commands/start.ts +180 -0
- package/src/commands/storage.ts +659 -0
- package/src/commands/telemetry.ts +91 -0
- package/src/commands/token.ts +361 -0
- package/src/commands/tools-lint.ts +430 -0
- package/src/commands/traces.ts +188 -0
- package/src/commands/triggers.ts +237 -0
- package/src/index.ts +30 -0
- package/src/internal/exit.ts +62 -0
- package/src/internal/load-config.ts +107 -0
- package/src/internal/offline.ts +81 -0
- package/src/internal/output.ts +146 -0
- package/src/internal/prompts.ts +58 -0
- package/src/internal/store-context.ts +165 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.js","names":["result: SkillInstallationStatus","migrated: Array<{ file: string; result: MigrationResult }>","result: MigrationResult","out: SkillsMigrateFrontmatterResult","out: string[]","entries: string[]","s: import('node:fs').Stats"],"sources":["../../src/commands/skills.ts"],"sourcesContent":["/**\n * `graphorin skills` - install + audit + migrate operator-managed\n * skill packages.\n *\n * Surface (per Phase 15 § Skills):\n *\n * - `graphorin skills install <source>` - `npm:` or `git:` source.\n * Honours allowlist / denylist + signature verification (DEC-140 /\n * ADR-034). Refuses when `GRAPHORIN_OFFLINE=1` is set.\n * - `graphorin skills inspect <name>` - frontmatter + signature +\n * supply-chain status of an installed skill.\n * - `graphorin skills audit` - full audit of every recorded\n * installation.\n * - `graphorin skills migrate-frontmatter [--apply] [--recursive]`\n * - DEC-156: rewrites legacy `graphorin-*` frontmatter fields onto\n * their upstream equivalents.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport process from 'node:process';\n\nimport {\n auditInstalledSkills,\n type InstallSkillFromGitOptions,\n type InstallSkillFromNpmOptions,\n installSkillFromGit,\n installSkillFromNpm,\n type SkillInstallationStatus,\n} from '@graphorin/security';\nimport { loadSkills, type MigrationResult, migrateFrontmatter } from '@graphorin/skills';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport { checkOfflineModeBlocked } from '../internal/offline.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\n\n/** @stable */\nexport interface SkillsCommonOptions extends CommonOutputOptions {}\n\n/** @stable */\nexport type SkillTrustLevelInput = 'trusted' | 'trusted-with-scripts' | 'untrusted';\n\n/** @stable */\nexport interface SkillsInstallOptions extends SkillsCommonOptions {\n /** `npm:<name>[@version]` or `git:<url>` source. */\n readonly source: string;\n /** Optional explicit version pin (npm sources only). */\n readonly version?: string;\n /** Optional git ref (git sources only). */\n readonly ref?: string;\n /** Trust level for the operator's project. Defaults to the helper's own default. */\n readonly trustLevel?: SkillTrustLevelInput;\n /** Working directory for npm installs. */\n readonly cwd?: string;\n readonly dryRun?: boolean;\n}\n\n/** @stable */\nexport async function runSkillsInstall(\n options: SkillsInstallOptions,\n): Promise<SkillInstallationStatus> {\n if (\n !checkOfflineModeBlocked('skills install', {\n ...(options.print !== undefined ? { print: options.print } : {}),\n })\n ) {\n process.exit(EXIT_CODES.RECOVERABLE_FAILURE);\n }\n const print = options.print ?? defaultPrintSink;\n const parsed = parseSource(options.source);\n let result: SkillInstallationStatus;\n if (parsed.kind === 'npm') {\n const installOpts: InstallSkillFromNpmOptions = {\n packageName: parsed.value,\n ...(options.version !== undefined ? { version: options.version } : {}),\n ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),\n };\n result = await installSkillFromNpm(installOpts);\n } else {\n const installOpts: InstallSkillFromGitOptions = {\n repoUrl: parsed.value,\n ...(options.ref !== undefined ? { ref: options.ref } : {}),\n ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),\n ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),\n };\n result = await installSkillFromGit(installOpts);\n }\n emitReport(options, result, () => {\n print(\n brand(\n `installed skill '${result.id}' (${result.source.kind}, signatureVerified=${result.signatureVerified})`,\n ),\n );\n if (result.signature !== undefined) {\n print(` signature valid: ${result.signature.valid}`);\n }\n });\n return result;\n}\n\n/** @stable */\nexport interface SkillsInspectOptions extends SkillsCommonOptions {\n readonly name: string;\n}\n\n/** @stable */\nexport async function runSkillsInspect(\n options: SkillsInspectOptions,\n): Promise<SkillInstallationStatus | null> {\n const all = auditInstalledSkills();\n const match = all.find((s) => s.id === options.name) ?? null;\n emitReport(options, match, () => {\n const print = options.print ?? defaultPrintSink;\n if (match === null) {\n print(brand(`skill '${options.name}' not found in this process registry.`));\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return;\n }\n print(brand(`skill ${match.id}`));\n print(` source: ${match.source.kind}`);\n print(` trust level: ${match.trustLevel}`);\n print(` ignoreScripts: ${match.ignoreScripts}`);\n print(` signatureVerified: ${match.signatureVerified}`);\n print(` installedAt: ${new Date(match.installedAt).toISOString()}`);\n if (match.installPath !== undefined) print(` installPath: ${match.installPath}`);\n if (match.publisher !== undefined) print(` publisher: ${match.publisher}`);\n });\n return match;\n}\n\n/** @stable */\nexport interface SkillsAuditOptions extends SkillsCommonOptions {}\n\n/** @stable */\nexport function runSkillsAudit(options: SkillsAuditOptions = {}) {\n const all = auditInstalledSkills();\n emitReport(options, all, () => {\n const print = options.print ?? defaultPrintSink;\n if (all.length === 0) {\n print(brand('no skills installed in this process registry.'));\n return;\n }\n print(brand(`${all.length} installed skill(s):`));\n for (const s of all) {\n const mark = s.signatureVerified ? statusMarker('ok') : statusMarker('warn');\n print(\n ` ${mark} ${s.id} (${s.source.kind}, trust=${s.trustLevel}, signatureVerified=${s.signatureVerified})`,\n );\n }\n });\n return all;\n}\n\n/** @stable */\nexport interface SkillsMigrateFrontmatterOptions extends SkillsCommonOptions {\n /** Directory to walk. Defaults to `process.cwd()`. */\n readonly path?: string;\n readonly recursive?: boolean;\n readonly apply?: boolean;\n}\n\n/** @stable */\nexport interface SkillsMigrateFrontmatterResult {\n readonly directory: string;\n readonly visited: number;\n readonly migrated: ReadonlyArray<{\n readonly file: string;\n readonly result: MigrationResult;\n }>;\n readonly applied: boolean;\n}\n\n/** @stable */\nexport async function runSkillsMigrateFrontmatter(\n options: SkillsMigrateFrontmatterOptions = {},\n): Promise<SkillsMigrateFrontmatterResult> {\n const root = resolve(options.path ?? process.cwd());\n const recursive = options.recursive === true;\n const apply = options.apply === true;\n const files = await collectSkillMd(root, recursive);\n const migrated: Array<{ file: string; result: MigrationResult }> = [];\n for (const file of files) {\n const raw = await readFile(file, 'utf8');\n let result: MigrationResult;\n try {\n result = migrateFrontmatter(raw, { apply, skillId: file });\n } catch {\n continue;\n }\n if (result.changed) {\n if (apply) {\n await writeFile(file, result.migratedSkillMd, { mode: 0o600 });\n }\n migrated.push({ file, result });\n }\n }\n const out: SkillsMigrateFrontmatterResult = Object.freeze({\n directory: root,\n visited: files.length,\n migrated: Object.freeze(migrated),\n applied: apply,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(\n brand(`migrate-frontmatter visited ${out.visited} SKILL.md file(s) under ${out.directory}.`),\n );\n if (out.migrated.length === 0) {\n print(brand('no rewrites required.'));\n return;\n }\n print(\n brand(\n `${out.migrated.length} file(s) ${apply ? 'rewritten' : 'would be rewritten (dry-run)'}:`,\n ),\n );\n for (const m of out.migrated) print(` - ${m.file}`);\n });\n return out;\n}\n\nfunction parseSource(input: string): { readonly kind: 'npm' | 'git'; readonly value: string } {\n if (input.startsWith('npm:')) return { kind: 'npm', value: input.slice(4) };\n if (input.startsWith('git:')) return { kind: 'git', value: input.slice(4) };\n if (input.includes('://')) return { kind: 'git', value: input };\n return { kind: 'npm', value: input };\n}\n\nasync function collectSkillMd(root: string, recursive: boolean): Promise<string[]> {\n const out: string[] = [];\n await walk(root, recursive, out);\n return out;\n}\n\nasync function walk(dir: string, recursive: boolean, acc: string[]): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch {\n return;\n }\n for (const name of entries) {\n if (name === 'node_modules' || name === '.git' || name === 'dist') continue;\n const full = join(dir, name);\n let s: import('node:fs').Stats;\n try {\n s = await stat(full);\n } catch {\n continue;\n }\n if (s.isDirectory()) {\n if (recursive) await walk(full, recursive, acc);\n } else if (name === 'SKILL.md') {\n acc.push(full);\n }\n }\n}\n\n// Touch the loader once so the import is preserved (downstream callers\n// that want to re-load the migrated skill consume `loadSkills(...)`).\nvoid loadSkills;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"skills.js","names":["result: SkillInstallationStatus","migrated: Array<{ file: string; result: MigrationResult }>","result: MigrationResult","out: SkillsMigrateFrontmatterResult","out: string[]","entries: string[]","s: import('node:fs').Stats"],"sources":["../../src/commands/skills.ts"],"sourcesContent":["/**\n * `graphorin skills` - install + audit + migrate operator-managed\n * skill packages.\n *\n * Surface (per Phase 15 § Skills):\n *\n * - `graphorin skills install <source>` - `npm:` or `git:` source.\n * Honours allowlist / denylist + signature verification (DEC-140 /\n * ADR-034). Refuses when `GRAPHORIN_OFFLINE=1` is set.\n * - `graphorin skills inspect <name>` - frontmatter + signature +\n * supply-chain status of an installed skill.\n * - `graphorin skills audit` - full audit of every recorded\n * installation.\n * - `graphorin skills migrate-frontmatter [--apply] [--recursive]`\n * - DEC-156: rewrites legacy `graphorin-*` frontmatter fields onto\n * their upstream equivalents.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport process from 'node:process';\n\nimport {\n auditInstalledSkills,\n type InstallSkillFromGitOptions,\n type InstallSkillFromNpmOptions,\n installSkillFromGit,\n installSkillFromNpm,\n type SkillInstallationStatus,\n} from '@graphorin/security';\nimport { loadSkills, type MigrationResult, migrateFrontmatter } from '@graphorin/skills';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport { checkOfflineModeBlocked } from '../internal/offline.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\n\n/** @stable */\nexport interface SkillsCommonOptions extends CommonOutputOptions {}\n\n/** @stable */\nexport type SkillTrustLevelInput = 'trusted' | 'trusted-with-scripts' | 'untrusted';\n\n/** @stable */\nexport interface SkillsInstallOptions extends SkillsCommonOptions {\n /** `npm:<name>[@version]` or `git:<url>` source. */\n readonly source: string;\n /** Optional explicit version pin (npm sources only). */\n readonly version?: string;\n /** Optional git ref (git sources only). */\n readonly ref?: string;\n /** Trust level for the operator's project. Defaults to the helper's own default. */\n readonly trustLevel?: SkillTrustLevelInput;\n /** Working directory for npm installs. */\n readonly cwd?: string;\n readonly dryRun?: boolean;\n}\n\n/** @stable */\nexport async function runSkillsInstall(\n options: SkillsInstallOptions,\n): Promise<SkillInstallationStatus> {\n if (\n !checkOfflineModeBlocked('skills install', {\n ...(options.print !== undefined ? { print: options.print } : {}),\n })\n ) {\n process.exit(EXIT_CODES.RECOVERABLE_FAILURE);\n }\n const print = options.print ?? defaultPrintSink;\n const parsed = parseSource(options.source);\n let result: SkillInstallationStatus;\n if (parsed.kind === 'npm') {\n const installOpts: InstallSkillFromNpmOptions = {\n packageName: parsed.value,\n ...(options.version !== undefined ? { version: options.version } : {}),\n ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),\n };\n result = await installSkillFromNpm(installOpts);\n } else {\n const installOpts: InstallSkillFromGitOptions = {\n repoUrl: parsed.value,\n ...(options.ref !== undefined ? { ref: options.ref } : {}),\n ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),\n ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),\n };\n result = await installSkillFromGit(installOpts);\n }\n emitReport(options, result, () => {\n print(\n brand(\n `installed skill '${result.id}' (${result.source.kind}, signatureVerified=${result.signatureVerified})`,\n ),\n );\n if (result.signature !== undefined) {\n print(` signature valid: ${result.signature.valid}`);\n }\n });\n return result;\n}\n\n/** @stable */\nexport interface SkillsInspectOptions extends SkillsCommonOptions {\n readonly name: string;\n}\n\n/** @stable */\nexport async function runSkillsInspect(\n options: SkillsInspectOptions,\n): Promise<SkillInstallationStatus | null> {\n const all = auditInstalledSkills();\n const match = all.find((s) => s.id === options.name) ?? null;\n emitReport(options, match, () => {\n const print = options.print ?? defaultPrintSink;\n if (match === null) {\n print(brand(`skill '${options.name}' not found in this process registry.`));\n return;\n }\n print(brand(`skill ${match.id}`));\n print(` source: ${match.source.kind}`);\n print(` trust level: ${match.trustLevel}`);\n print(` ignoreScripts: ${match.ignoreScripts}`);\n print(` signatureVerified: ${match.signatureVerified}`);\n print(` installedAt: ${new Date(match.installedAt).toISOString()}`);\n if (match.installPath !== undefined) print(` installPath: ${match.installPath}`);\n if (match.publisher !== undefined) print(` publisher: ${match.publisher}`);\n });\n // W-002: exit code independent of --json (see runAuditVerify).\n if (match === null) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return match;\n}\n\n/** @stable */\nexport interface SkillsAuditOptions extends SkillsCommonOptions {}\n\n/** @stable */\nexport function runSkillsAudit(options: SkillsAuditOptions = {}) {\n const all = auditInstalledSkills();\n emitReport(options, all, () => {\n const print = options.print ?? defaultPrintSink;\n if (all.length === 0) {\n print(brand('no skills installed in this process registry.'));\n return;\n }\n print(brand(`${all.length} installed skill(s):`));\n for (const s of all) {\n const mark = s.signatureVerified ? statusMarker('ok') : statusMarker('warn');\n print(\n ` ${mark} ${s.id} (${s.source.kind}, trust=${s.trustLevel}, signatureVerified=${s.signatureVerified})`,\n );\n }\n });\n return all;\n}\n\n/** @stable */\nexport interface SkillsMigrateFrontmatterOptions extends SkillsCommonOptions {\n /** Directory to walk. Defaults to `process.cwd()`. */\n readonly path?: string;\n readonly recursive?: boolean;\n readonly apply?: boolean;\n}\n\n/** @stable */\nexport interface SkillsMigrateFrontmatterResult {\n readonly directory: string;\n readonly visited: number;\n readonly migrated: ReadonlyArray<{\n readonly file: string;\n readonly result: MigrationResult;\n }>;\n readonly applied: boolean;\n}\n\n/** @stable */\nexport async function runSkillsMigrateFrontmatter(\n options: SkillsMigrateFrontmatterOptions = {},\n): Promise<SkillsMigrateFrontmatterResult> {\n const root = resolve(options.path ?? process.cwd());\n const recursive = options.recursive === true;\n const apply = options.apply === true;\n const files = await collectSkillMd(root, recursive);\n const migrated: Array<{ file: string; result: MigrationResult }> = [];\n for (const file of files) {\n const raw = await readFile(file, 'utf8');\n let result: MigrationResult;\n try {\n result = migrateFrontmatter(raw, { apply, skillId: file });\n } catch {\n continue;\n }\n if (result.changed) {\n if (apply) {\n await writeFile(file, result.migratedSkillMd, { mode: 0o600 });\n }\n migrated.push({ file, result });\n }\n }\n const out: SkillsMigrateFrontmatterResult = Object.freeze({\n directory: root,\n visited: files.length,\n migrated: Object.freeze(migrated),\n applied: apply,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(\n brand(`migrate-frontmatter visited ${out.visited} SKILL.md file(s) under ${out.directory}.`),\n );\n if (out.migrated.length === 0) {\n print(brand('no rewrites required.'));\n return;\n }\n print(\n brand(\n `${out.migrated.length} file(s) ${apply ? 'rewritten' : 'would be rewritten (dry-run)'}:`,\n ),\n );\n for (const m of out.migrated) print(` - ${m.file}`);\n });\n return out;\n}\n\nfunction parseSource(input: string): { readonly kind: 'npm' | 'git'; readonly value: string } {\n if (input.startsWith('npm:')) return { kind: 'npm', value: input.slice(4) };\n if (input.startsWith('git:')) return { kind: 'git', value: input.slice(4) };\n if (input.includes('://')) return { kind: 'git', value: input };\n return { kind: 'npm', value: input };\n}\n\nasync function collectSkillMd(root: string, recursive: boolean): Promise<string[]> {\n const out: string[] = [];\n await walk(root, recursive, out);\n return out;\n}\n\nasync function walk(dir: string, recursive: boolean, acc: string[]): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch {\n return;\n }\n for (const name of entries) {\n if (name === 'node_modules' || name === '.git' || name === 'dist') continue;\n const full = join(dir, name);\n let s: import('node:fs').Stats;\n try {\n s = await stat(full);\n } catch {\n continue;\n }\n if (s.isDirectory()) {\n if (recursive) await walk(full, recursive, acc);\n } else if (name === 'SKILL.md') {\n acc.push(full);\n }\n }\n}\n\n// Touch the loader once so the import is preserved (downstream callers\n// that want to re-load the migrated skill consume `loadSkills(...)`).\nvoid loadSkills;\n"],"mappings":"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"}
|
|
@@ -59,6 +59,46 @@ interface StorageBackupResult {
|
|
|
59
59
|
*/
|
|
60
60
|
declare function runStorageBackup(options: StorageBackupOptions): Promise<StorageBackupResult>;
|
|
61
61
|
/** @stable */
|
|
62
|
+
interface StorageCompactOptions extends StorageCommonOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Free pages released per `PRAGMA incremental_vacuum(N)` batch. Small
|
|
65
|
+
* batches keep the writer lock short on a huge freelist.
|
|
66
|
+
* @default 1000
|
|
67
|
+
*/
|
|
68
|
+
readonly batchPages?: number;
|
|
69
|
+
}
|
|
70
|
+
/** @stable */
|
|
71
|
+
interface StorageCompactResult {
|
|
72
|
+
readonly path: string;
|
|
73
|
+
/** Raw `PRAGMA auto_vacuum` value: 0 none, 1 full, 2 incremental. */
|
|
74
|
+
readonly autoVacuum: number;
|
|
75
|
+
/** `true` when the database supports incremental compaction. */
|
|
76
|
+
readonly supported: boolean;
|
|
77
|
+
readonly freelistBefore?: number;
|
|
78
|
+
readonly freelistAfter?: number;
|
|
79
|
+
readonly pageSize?: number;
|
|
80
|
+
/** `page_size * (freelistBefore - freelistAfter)`. */
|
|
81
|
+
readonly reclaimedBytes?: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* W-064: `graphorin storage compact` - return pruned pages to the OS.
|
|
85
|
+
* `VACUUM` stays forbidden (it renumbers implicit rowids and corrupts
|
|
86
|
+
* the FTS5 external-content mappings), but `PRAGMA incremental_vacuum`
|
|
87
|
+
* relocates free pages via the ptrmap WITHOUT rebuilding tables, so it
|
|
88
|
+
* is rowid-safe. Requires `auto_vacuum=2`, which `openConnection` sets
|
|
89
|
+
* on every database it CREATES from this version on; on an older
|
|
90
|
+
* database the command reports the limitation honestly (exit 0, file
|
|
91
|
+
* untouched) - the only way out is recreating the store (fresh init +
|
|
92
|
+
* `migrate-export` / import, or re-remember), because switching
|
|
93
|
+
* auto_vacuum on retroactively needs the very VACUUM that is banned.
|
|
94
|
+
* The vacuum runs in batches so a huge freelist never holds the writer
|
|
95
|
+
* lock in one long bite; the WAL is checkpoint-TRUNCATEd first so
|
|
96
|
+
* freed pages do not linger in the -wal file.
|
|
97
|
+
*
|
|
98
|
+
* @stable
|
|
99
|
+
*/
|
|
100
|
+
declare function runStorageCompact(options?: StorageCompactOptions): Promise<StorageCompactResult>;
|
|
101
|
+
/** @stable */
|
|
62
102
|
interface StorageEncryptOptions extends StorageCommonOptions {
|
|
63
103
|
/** SecretRef URI for the new passphrase. */
|
|
64
104
|
readonly passphraseFrom: string;
|
|
@@ -130,5 +170,5 @@ interface StorageCleanupBackupsResult {
|
|
|
130
170
|
*/
|
|
131
171
|
declare function runStorageCleanupBackups(options?: StorageCleanupBackupsOptions): Promise<StorageCleanupBackupsResult>;
|
|
132
172
|
//#endregion
|
|
133
|
-
export { StorageBackupOptions, StorageBackupResult, StorageCleanupBackupsOptions, StorageCleanupBackupsResult, StorageCommonOptions, StorageEncryptOptions, StorageRekeyOptions, StorageStatusResult, runStorageBackup, runStorageCleanupBackups, runStorageEncrypt, runStorageRekey, runStorageStatus };
|
|
173
|
+
export { StorageBackupOptions, StorageBackupResult, StorageCleanupBackupsOptions, StorageCleanupBackupsResult, StorageCommonOptions, StorageCompactOptions, StorageCompactResult, StorageEncryptOptions, StorageRekeyOptions, StorageStatusResult, runStorageBackup, runStorageCleanupBackups, runStorageCompact, runStorageEncrypt, runStorageRekey, runStorageStatus };
|
|
134
174
|
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","names":[],"sources":["../../src/commands/storage.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","names":[],"sources":["../../src/commands/storage.ts"],"sourcesContent":[],"mappings":";;;;;AA8VU,UA/SO,oBAAA,SAA6B,mBA+SpC,CAAA;EA8CO,SAAA,MAAA,CAAA,EAAA,MAAoB;AAMrC;AAOA;AAA+C,UArW9B,mBAAA,CAqW8B;EAA8B,SAAA,IAAA,EAAA,MAAA;EAAR,SAAA,IAAA,EAAA,KAAA,GAAA,QAAA;EAAO,SAAA,UAAA,EAAA;IA0C3D,SAAA,OAAA,EAAA,OAAA;IASA,SAAA,MAAA,CAAA,EAAA,MAAA;EAaK,CAAA;EACX,SAAA,UAAA,EAAA;IACA,SAAA,SAAA,EAAA,OAAA;IAAR,SAAA,IAAA,CAAA,EAAA,MAAA;EAAO,CAAA;;;;;;;;;;;;;;;;iBAxZY,gBAAA,WACX,uBACR,QAAQ;;UA+DM,oBAAA,SAA6B;;;;;;;UAQ7B,mBAAA;;;;;;;;;;;;;;;iBAgBK,gBAAA,UACX,uBACR,QAAQ;;UAqCM,qBAAA,SAA8B;;;;;;;;;UAU9B,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BK,iBAAA,WACX,wBACR,QAAQ;;UA6EM,qBAAA,SAA8B;;;;;;;;;;;;;;;;UAiB9B,oBAAA;;;;;;;;;;;;;;;;;;iBAiBK,iBAAA,UACX,wBACR,QAAQ;;UA8CM,mBAAA,SAA4B;;;;;UAM5B,kBAAA;;;;;;iBAOK,eAAA,UAAyB,sBAAsB,QAAQ;;UA0C5D,4BAAA,SAAqC;;;;;;;;UASrC,2BAAA;;oBAEG;;;;;;;;;;iBAWE,wBAAA,WACX,+BACR,QAAQ"}
|
package/dist/commands/storage.js
CHANGED
|
@@ -123,6 +123,80 @@ async function runStorageBackup(options) {
|
|
|
123
123
|
return out;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
+
* W-064: `graphorin storage compact` - return pruned pages to the OS.
|
|
127
|
+
* `VACUUM` stays forbidden (it renumbers implicit rowids and corrupts
|
|
128
|
+
* the FTS5 external-content mappings), but `PRAGMA incremental_vacuum`
|
|
129
|
+
* relocates free pages via the ptrmap WITHOUT rebuilding tables, so it
|
|
130
|
+
* is rowid-safe. Requires `auto_vacuum=2`, which `openConnection` sets
|
|
131
|
+
* on every database it CREATES from this version on; on an older
|
|
132
|
+
* database the command reports the limitation honestly (exit 0, file
|
|
133
|
+
* untouched) - the only way out is recreating the store (fresh init +
|
|
134
|
+
* `migrate-export` / import, or re-remember), because switching
|
|
135
|
+
* auto_vacuum on retroactively needs the very VACUUM that is banned.
|
|
136
|
+
* The vacuum runs in batches so a huge freelist never holds the writer
|
|
137
|
+
* lock in one long bite; the WAL is checkpoint-TRUNCATEd first so
|
|
138
|
+
* freed pages do not linger in the -wal file.
|
|
139
|
+
*
|
|
140
|
+
* @stable
|
|
141
|
+
*/
|
|
142
|
+
async function runStorageCompact(options = {}) {
|
|
143
|
+
const { openStoreContext } = await import("../internal/store-context.js");
|
|
144
|
+
const batchPages = options.batchPages ?? 1e3;
|
|
145
|
+
const ctx = await openStoreContext({
|
|
146
|
+
...options.config !== void 0 ? { config: options.config } : {},
|
|
147
|
+
skipInit: true
|
|
148
|
+
});
|
|
149
|
+
let out;
|
|
150
|
+
try {
|
|
151
|
+
const conn = ctx.store.connection;
|
|
152
|
+
const autoVacuum = Number(conn.pragma("auto_vacuum", { simple: true }));
|
|
153
|
+
if (autoVacuum !== 2) out = Object.freeze({
|
|
154
|
+
path: conn.path,
|
|
155
|
+
autoVacuum,
|
|
156
|
+
supported: false
|
|
157
|
+
});
|
|
158
|
+
else {
|
|
159
|
+
const pageSize = Number(conn.pragma("page_size", { simple: true }));
|
|
160
|
+
conn.pragma("wal_checkpoint(TRUNCATE)");
|
|
161
|
+
const freelistBefore = Number(conn.pragma("freelist_count", { simple: true }));
|
|
162
|
+
let freelist = freelistBefore;
|
|
163
|
+
while (freelist > 0) {
|
|
164
|
+
conn.pragma(`incremental_vacuum(${batchPages})`);
|
|
165
|
+
const next = Number(conn.pragma("freelist_count", { simple: true }));
|
|
166
|
+
if (next >= freelist) break;
|
|
167
|
+
freelist = next;
|
|
168
|
+
}
|
|
169
|
+
const freelistAfter = freelist;
|
|
170
|
+
conn.pragma("wal_checkpoint(TRUNCATE)");
|
|
171
|
+
out = Object.freeze({
|
|
172
|
+
path: conn.path,
|
|
173
|
+
autoVacuum,
|
|
174
|
+
supported: true,
|
|
175
|
+
freelistBefore,
|
|
176
|
+
freelistAfter,
|
|
177
|
+
pageSize,
|
|
178
|
+
reclaimedBytes: Math.max(0, (freelistBefore - freelistAfter) * pageSize)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
await ctx.close();
|
|
183
|
+
}
|
|
184
|
+
emitReport(options, out, () => {
|
|
185
|
+
const print = options.print ?? defaultPrintSink;
|
|
186
|
+
print(brand("storage compact"));
|
|
187
|
+
if (!out.supported) {
|
|
188
|
+
print(` ${statusMarker("warn")} auto_vacuum=${out.autoVacuum} - incremental compaction unavailable.`);
|
|
189
|
+
print(" This database was created before incremental auto-vacuum was enabled;");
|
|
190
|
+
print(" its file keeps the high-water-mark size (freed pages are reused, not returned to the OS).");
|
|
191
|
+
print(" To reclaim disk: initialise a fresh store (new databases get auto_vacuum=2) and move the data");
|
|
192
|
+
print(" across (graphorin migrate-export / import); VACUUM stays forbidden - it corrupts FTS5 rowid mappings.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
print(` ${statusMarker("ok")} ${out.path}: freelist ${out.freelistBefore} -> ${out.freelistAfter} pages, reclaimed ${formatSize(out.reclaimedBytes)}`);
|
|
196
|
+
});
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
126
200
|
* `graphorin storage encrypt --passphrase-from <ref>` - encrypt a
|
|
127
201
|
* previously unencrypted SQLite store. Delegates to the optional Phase
|
|
128
202
|
* 16 sub-pack `@graphorin/store-sqlite-encrypted` once installed; when
|
|
@@ -310,5 +384,5 @@ function failUnsupported(options, message, hint) {
|
|
|
310
384
|
}
|
|
311
385
|
|
|
312
386
|
//#endregion
|
|
313
|
-
export { runStorageBackup, runStorageCleanupBackups, runStorageEncrypt, runStorageRekey, runStorageStatus };
|
|
387
|
+
export { runStorageBackup, runStorageCleanupBackups, runStorageCompact, runStorageEncrypt, runStorageRekey, runStorageStatus };
|
|
314
388
|
//# sourceMappingURL=storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","names":["auditPath: string | undefined","auditExists: boolean | undefined","out: StorageStatusResult","source: string","out: StorageBackupResult","out: StorageEncryptResult","out: StorageRekeyResult","basename","pathBasename","entries: string[]","removed: string[]","out: StorageCleanupBackupsResult"],"sources":["../../src/commands/storage.ts"],"sourcesContent":["/**\n * `graphorin storage` - manage the SQLite store and its encryption-\n * at-rest opt-in.\n *\n * Surface (per Phase 15 § Storage):\n *\n * - `graphorin storage status` - reports cipher peer + WAL + size +\n * encryption mode.\n * - `graphorin storage encrypt --passphrase-from <ref>` - opt-in\n * encryption migration. Requires the `@graphorin/store-sqlite-\n * encrypted` sub-pack from Phase 16.\n * - `graphorin storage rekey --new-passphrase-from <ref>` - re-key\n * an already-encrypted DB.\n * - `graphorin storage cleanup-backups` - drop stale `.bak` /\n * `.bak.<ts>` files left by previous encrypt / rekey runs.\n * - `graphorin storage backup <dest>` - online, consistent copy via\n * the driver's page-level backup API (store-02). Safe under a live\n * writer, preserves rowids (FTS5 mappings survive). Never use\n * `VACUUM INTO` - rowid renumbering corrupts FTS mappings.\n *\n * `encrypt`, `rekey`, and `cleanup-backups` need the cipher peer\n * (`better-sqlite3-multiple-ciphers`) which ships in the optional\n * Phase 16 sub-pack. When the peer is missing the CLI exits `2`\n * (`UNSUPPORTED`) with an actionable hint instead of silently\n * doing nothing.\n *\n * @packageDocumentation\n */\n\nimport { readdir, stat, unlink } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, basename as pathBasename, resolve } from 'node:path';\nimport process from 'node:process';\n\nimport { resolveSecret, type SecretValue } from '@graphorin/security';\nimport { parseServerConfig } from '@graphorin/server';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport { loadConfig } from '../internal/load-config.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\n\n/** @stable */\nexport interface StorageCommonOptions extends CommonOutputOptions {\n readonly config?: string;\n}\n\n/** @stable */\nexport interface StorageStatusResult {\n readonly path: string;\n readonly mode: 'lib' | 'server';\n readonly encryption: { readonly enabled: boolean; readonly cipher?: string };\n readonly cipherPeer: { readonly installed: boolean; readonly hint?: string };\n readonly mainDb: { readonly exists: boolean; readonly sizeBytes?: number };\n readonly walFile: { readonly exists: boolean; readonly sizeBytes?: number };\n readonly auditDb: {\n readonly enabled: boolean;\n readonly path?: string;\n readonly exists?: boolean;\n };\n}\n\n/** @stable */\nexport async function runStorageStatus(\n options: StorageCommonOptions = {},\n): Promise<StorageStatusResult> {\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const cipherPeer = await probeCipherPeer();\n const mainDb = await statSafely(resolveStoragePath(config.storage.path));\n const walFile = await statSafely(`${resolveStoragePath(config.storage.path)}-wal`);\n let auditPath: string | undefined;\n let auditExists: boolean | undefined;\n if (config.audit.enabled) {\n auditPath = resolveStoragePath(config.audit.path ?? deriveAuditPath(config.storage.path));\n auditExists = (await statSafely(auditPath)).exists;\n }\n const out: StorageStatusResult = Object.freeze({\n path: resolveStoragePath(config.storage.path),\n mode: config.storage.mode,\n encryption: Object.freeze({\n enabled: config.storage.encryption.enabled,\n ...(config.storage.encryption.cipher !== undefined\n ? { cipher: config.storage.encryption.cipher }\n : {}),\n }),\n cipherPeer: Object.freeze({\n installed: cipherPeer.installed,\n ...(cipherPeer.hint !== undefined ? { hint: cipherPeer.hint } : {}),\n }),\n mainDb: Object.freeze({\n exists: mainDb.exists,\n ...(mainDb.size !== undefined ? { sizeBytes: mainDb.size } : {}),\n }),\n walFile: Object.freeze({\n exists: walFile.exists,\n ...(walFile.size !== undefined ? { sizeBytes: walFile.size } : {}),\n }),\n auditDb: Object.freeze({\n enabled: config.audit.enabled,\n ...(auditPath !== undefined ? { path: auditPath } : {}),\n ...(auditExists !== undefined ? { exists: auditExists } : {}),\n }),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`storage status (${loaded.path})`));\n print(\n ` ${marker(out.mainDb.exists)} main: ${out.path} (${formatSize(out.mainDb.sizeBytes)})`,\n );\n print(\n ` ${marker(out.walFile.exists)} wal: ${out.path}-wal (${formatSize(out.walFile.sizeBytes)})`,\n );\n print(\n ` ${marker(out.encryption.enabled)} encryption: ${out.encryption.enabled ? `enabled (cipher=${out.encryption.cipher ?? 'sqlcipher'})` : 'disabled'}`,\n );\n print(\n ` ${marker(out.cipherPeer.installed)} cipher peer (better-sqlite3-multiple-ciphers): ${out.cipherPeer.installed ? 'installed' : 'missing'}`,\n );\n if (out.cipherPeer.hint !== undefined) print(` -> ${out.cipherPeer.hint}`);\n if (out.auditDb.enabled) {\n print(` ${marker(out.auditDb.exists === true)} audit.db: ${out.auditDb.path}`);\n }\n });\n return out;\n}\n\n/** @stable */\nexport interface StorageBackupOptions extends StorageCommonOptions {\n /** Destination file path for the backup copy. */\n readonly dest: string;\n /** Replace an existing destination file. Default: refuse. */\n readonly overwrite?: boolean;\n}\n\n/** @stable */\nexport interface StorageBackupResult {\n readonly source: string;\n readonly dest: string;\n readonly sizeBytes?: number;\n}\n\n/**\n * store-02: online backup via the driver's page-level `backup()` API -\n * consistent under a live writer (the daemon can keep running),\n * preserves rowids so FTS5 external-content mappings survive, and for\n * an encrypted store produces an equally-encrypted copy (same key).\n * This is the ONLY supported SQL-level backup: `VACUUM INTO`\n * renumbers rowids and corrupts the FTS mapping on restore.\n *\n * @stable\n */\nexport async function runStorageBackup(\n options: StorageBackupOptions,\n): Promise<StorageBackupResult> {\n const { openStoreContext } = await import('../internal/store-context.js');\n const dest = isAbsolute(options.dest) ? options.dest : resolve(process.cwd(), options.dest);\n const destStat = await statSafely(dest);\n if (destStat.exists && options.overwrite !== true) {\n throw new Error(\n `[graphorin/cli] backup destination already exists: ${dest}. Pass --overwrite to replace it.`,\n );\n }\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n let source: string;\n try {\n source = ctx.store.connection.path;\n if (source === dest) {\n throw new Error('[graphorin/cli] backup destination must differ from the store path.');\n }\n await ctx.store.connection.raw().backup(dest);\n } finally {\n await ctx.close();\n }\n const written = await statSafely(dest);\n const out: StorageBackupResult = Object.freeze({\n source,\n dest,\n ...(written.size !== undefined ? { sizeBytes: written.size } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand('storage backup'));\n print(` ${statusMarker('ok')} ${out.source} -> ${out.dest} (${formatSize(out.sizeBytes)})`);\n });\n return out;\n}\n\n/** @stable */\nexport interface StorageEncryptOptions extends StorageCommonOptions {\n /** SecretRef URI for the new passphrase. */\n readonly passphraseFrom: string;\n /**\n * Optional explicit target path for the encrypted output. Default:\n * `<storage.path>.encrypted`.\n */\n readonly targetPath?: string;\n /**\n * If `true`, atomically swap the encrypted target into the\n * `storage.path` location after the integrity check, leaving the\n * original under `<storage.path>.bak.<timestamp>`. Default `false`.\n */\n readonly swap?: boolean;\n}\n\n/** @stable */\nexport interface StorageEncryptResult {\n readonly sourcePath: string;\n readonly targetPath: string;\n readonly cipher: string;\n readonly integrityOk: boolean;\n readonly swap?: { readonly originalRenamedTo: string };\n}\n\n/**\n * `graphorin storage encrypt --passphrase-from <ref>` - encrypt a\n * previously unencrypted SQLite store. Delegates to the optional Phase\n * 16 sub-pack `@graphorin/store-sqlite-encrypted` once installed; when\n * the sub-pack is missing the CLI exits `2` (`UNSUPPORTED`) with an\n * actionable hint.\n *\n * @stable\n */\nexport async function runStorageEncrypt(\n options: StorageEncryptOptions,\n): Promise<StorageEncryptResult> {\n const subpack = await loadEncryptedSubpack();\n if (subpack === null) {\n return failUnsupported(\n options,\n `'graphorin storage encrypt' requires the optional sub-pack '@graphorin/store-sqlite-encrypted' (Phase 16) and the cipher peer 'better-sqlite3-multiple-ciphers'.`,\n 'Install @graphorin/store-sqlite-encrypted (which pulls the cipher peer transitively) before running this command.',\n );\n }\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const sourcePath = resolveStoragePath(config.storage.path);\n const targetPath = options.targetPath ?? `${sourcePath}.encrypted`;\n const passphrase = await resolvePassphraseRef(options.passphraseFrom);\n try {\n const result = await passphrase.use((raw) =>\n subpack.encryptDatabase({\n sourcePath,\n targetPath,\n passphrase: raw,\n ...(options.swap === true ? { swap: true } : {}),\n overwriteTarget: false,\n }),\n );\n const out: StorageEncryptResult = Object.freeze({\n sourcePath,\n targetPath: result.targetPath,\n cipher: result.cipher,\n integrityOk: result.integrityCheck.ok,\n ...(result.swap !== undefined ? { swap: result.swap } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`encrypt: ${out.sourcePath} -> ${out.targetPath} (cipher=${out.cipher})`));\n print(` ${marker(out.integrityOk)} cipher_integrity_check`);\n if (out.swap !== undefined) {\n print(` ${statusMarker('ok')} swapped; original kept at ${out.swap.originalRenamedTo}`);\n }\n });\n return out;\n } finally {\n passphrase.dispose();\n }\n}\n\n/** @stable */\nexport interface StorageRekeyOptions extends StorageCommonOptions {\n readonly oldPassphraseFrom: string;\n readonly newPassphraseFrom: string;\n}\n\n/** @stable */\nexport interface StorageRekeyResult {\n readonly path: string;\n readonly cipher: string;\n readonly integrityOk: boolean;\n}\n\n/** @stable */\nexport async function runStorageRekey(options: StorageRekeyOptions): Promise<StorageRekeyResult> {\n const subpack = await loadEncryptedSubpack();\n if (subpack === null) {\n return failUnsupported(\n options,\n `'graphorin storage rekey' requires the optional sub-pack '@graphorin/store-sqlite-encrypted' (Phase 16).`,\n 'Install @graphorin/store-sqlite-encrypted before running this command.',\n );\n }\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const path = resolveStoragePath(config.storage.path);\n const oldPassphrase = await resolvePassphraseRef(options.oldPassphraseFrom);\n const newPassphrase = await resolvePassphraseRef(options.newPassphraseFrom);\n try {\n const result = await oldPassphrase.use((oldRaw) =>\n newPassphrase.use((newRaw) =>\n subpack.rekeyDatabase({\n path,\n oldPassphrase: oldRaw,\n newPassphrase: newRaw,\n }),\n ),\n );\n const out: StorageRekeyResult = Object.freeze({\n path: result.path,\n cipher: result.cipher,\n integrityOk: result.integrityCheck.ok,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`rekey: ${out.path} (cipher=${out.cipher})`));\n print(` ${marker(out.integrityOk)} cipher_integrity_check`);\n });\n return out;\n } finally {\n oldPassphrase.dispose();\n newPassphrase.dispose();\n }\n}\n\n/** @stable */\nexport interface StorageCleanupBackupsOptions extends StorageCommonOptions {\n /**\n * Skip the actual delete; print what would be removed. Default `false`.\n * Tests pass `true` to assert the discovery without touching files.\n */\n readonly dryRun?: boolean;\n}\n\n/** @stable */\nexport interface StorageCleanupBackupsResult {\n readonly directory: string;\n readonly removed: ReadonlyArray<string>;\n readonly dryRun: boolean;\n}\n\n/**\n * Drop stale `.bak`, `.bak.<ts>`, and `.tmp.<ts>` siblings of the\n * configured storage path. Useful after `encrypt` / `rekey` runs that\n * leave intermediate copies around.\n *\n * @stable\n */\nexport async function runStorageCleanupBackups(\n options: StorageCleanupBackupsOptions = {},\n): Promise<StorageCleanupBackupsResult> {\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const dbPath = resolveStoragePath(config.storage.path);\n const dir = dirname(dbPath);\n // E6: node:path basename, NOT dbPath.split('/') - on Windows the path is\n // backslash-separated, so the split returned the whole path, no readdir\n // entry ever matched, and cleanup-backups was a silent no-op.\n const basename = pathBasename(dbPath);\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch (err) {\n throw new Error(\n `[graphorin/cli] cannot read storage directory '${dir}': ${(err as Error).message}`,\n );\n }\n const candidates = entries.filter((name) => isStaleBackup(basename, name));\n const removed: string[] = [];\n for (const name of candidates) {\n const full = join(dir, name);\n if (options.dryRun !== true) {\n try {\n await unlink(full);\n removed.push(full);\n } catch {\n // best-effort cleanup; surface in summary\n }\n } else {\n removed.push(full);\n }\n }\n const out: StorageCleanupBackupsResult = Object.freeze({\n directory: dir,\n removed: Object.freeze(removed),\n dryRun: options.dryRun === true,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n if (out.removed.length === 0) {\n print(brand(`no stale backups found in ${dir}.`));\n return;\n }\n print(\n brand(\n `${out.dryRun ? 'would remove' : 'removed'} ${out.removed.length} stale backup file(s) in ${dir}:`,\n ),\n );\n for (const name of out.removed) print(` - ${name}`);\n });\n return out;\n}\n\nfunction isStaleBackup(baseName: string, candidate: string): boolean {\n if (candidate === baseName) return false;\n if (!candidate.startsWith(baseName)) return false;\n const suffix = candidate.slice(baseName.length);\n if (suffix === '.bak') return true;\n if (/^\\.bak\\.\\d+$/.test(suffix)) return true;\n if (/^\\.tmp\\.\\d+$/.test(suffix)) return true;\n return false;\n}\n\nasync function probeCipherPeer(): Promise<{\n readonly installed: boolean;\n readonly hint?: string;\n}> {\n try {\n const moduleName = 'better-sqlite3-multiple-ciphers';\n await import(/* @vite-ignore */ moduleName);\n return Object.freeze({ installed: true });\n } catch {\n return Object.freeze({\n installed: false,\n hint: \"install '@graphorin/store-sqlite-encrypted' (Phase 16) which transitively installs the cipher peer.\",\n });\n }\n}\n\n/**\n * Encryption sub-pack surface consumed by the CLI runners. Declared\n * here so the dynamic-import path stays typed without forcing the CLI\n * to take a hard dependency on the optional package.\n *\n * @internal\n */\ninterface EncryptedSubpack {\n encryptDatabase(args: {\n sourcePath: string;\n targetPath: string;\n passphrase: string;\n swap?: boolean;\n overwriteTarget?: boolean;\n }): Promise<{\n sourcePath: string;\n targetPath: string;\n cipher: string;\n integrityCheck: { ok: boolean; rows: ReadonlyArray<string> };\n swap?: { originalRenamedTo: string };\n }>;\n rekeyDatabase(args: { path: string; oldPassphrase: string; newPassphrase: string }): Promise<{\n path: string;\n cipher: string;\n integrityCheck: { ok: boolean; rows: ReadonlyArray<string> };\n }>;\n}\n\n/** @internal */\nasync function loadEncryptedSubpack(): Promise<EncryptedSubpack | null> {\n try {\n // Computed module name keeps TypeScript's resolution off the\n // build-time graph so the CLI typechecks without the optional\n // package installed.\n const moduleName = '@graphorin/store-sqlite-encrypted';\n const mod = (await import(/* @vite-ignore */ moduleName)) as Partial<EncryptedSubpack>;\n if (typeof mod?.encryptDatabase === 'function' && typeof mod?.rekeyDatabase === 'function') {\n return mod as EncryptedSubpack;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** @internal */\nasync function resolvePassphraseRef(ref: string): Promise<SecretValue> {\n try {\n return await resolveSecret(ref);\n } catch (err) {\n throw new Error(\n `[graphorin/cli] failed to resolve storage passphrase '${ref}': ${(err as Error).message}`,\n { cause: err },\n );\n }\n}\n\nasync function statSafely(\n path: string,\n): Promise<{ readonly exists: boolean; readonly size?: number }> {\n try {\n const s = await stat(path);\n return { exists: true, size: s.size };\n } catch {\n return { exists: false };\n }\n}\n\n// IP-20: resolve a relative storage path against the CWD - the SAME rule the\n// server (`createServer` → `createSqliteStore`) and `openStoreContext` use - so\n// `graphorin storage status / encrypt` from any directory reports the same\n// database the server and the other CLI commands (`memory`, …) open. Resolving\n// against the config-file dir made `storage status` the lone outlier: from a\n// foreign CWD it inspected a different `data.db` than everything else.\nfunction resolveStoragePath(target: string): string {\n return isAbsolute(target) ? target : resolve(target);\n}\n\nfunction deriveAuditPath(storagePath: string): string {\n const idx = storagePath.lastIndexOf('/');\n if (idx < 0) return 'audit.db';\n return `${storagePath.slice(0, idx)}/audit.db`;\n}\n\nfunction formatSize(bytes: number | undefined): string {\n if (bytes === undefined) return 'missing';\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;\n return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;\n}\n\nfunction marker(ok: boolean): string {\n return ok ? statusMarker('ok') : statusMarker('warn');\n}\n\nfunction failUnsupported(options: CommonOutputOptions, message: string, hint: string): never {\n const print = options.print ?? defaultPrintSink;\n print(brand(message));\n print(brand(`hint: ${hint}`));\n process.exit(EXIT_CODES.UNSUPPORTED);\n // `process.exit` returns `never` at runtime, but the type system\n // narrows the return so explicit callers do not need a cast.\n /* c8 ignore next */\n throw new Error('unreachable');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage.js","names":["auditPath: string | undefined","auditExists: boolean | undefined","out: StorageStatusResult","source: string","out: StorageBackupResult","out: StorageCompactResult","out: StorageEncryptResult","out: StorageRekeyResult","basename","pathBasename","entries: string[]","removed: string[]","out: StorageCleanupBackupsResult"],"sources":["../../src/commands/storage.ts"],"sourcesContent":["/**\n * `graphorin storage` - manage the SQLite store and its encryption-\n * at-rest opt-in.\n *\n * Surface (per Phase 15 § Storage):\n *\n * - `graphorin storage status` - reports cipher peer + WAL + size +\n * encryption mode.\n * - `graphorin storage encrypt --passphrase-from <ref>` - opt-in\n * encryption migration. Requires the `@graphorin/store-sqlite-\n * encrypted` sub-pack from Phase 16.\n * - `graphorin storage rekey --new-passphrase-from <ref>` - re-key\n * an already-encrypted DB.\n * - `graphorin storage cleanup-backups` - drop stale `.bak` /\n * `.bak.<ts>` files left by previous encrypt / rekey runs.\n * - `graphorin storage backup <dest>` - online, consistent copy via\n * the driver's page-level backup API (store-02). Safe under a live\n * writer, preserves rowids (FTS5 mappings survive). Never use\n * `VACUUM INTO` - rowid renumbering corrupts FTS mappings.\n *\n * `encrypt`, `rekey`, and `cleanup-backups` need the cipher peer\n * (`better-sqlite3-multiple-ciphers`) which ships in the optional\n * Phase 16 sub-pack. When the peer is missing the CLI exits `2`\n * (`UNSUPPORTED`) with an actionable hint instead of silently\n * doing nothing.\n *\n * @packageDocumentation\n */\n\nimport { readdir, stat, unlink } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, basename as pathBasename, resolve } from 'node:path';\nimport process from 'node:process';\n\nimport { resolveSecret, type SecretValue } from '@graphorin/security';\nimport { parseServerConfig } from '@graphorin/server';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport { loadConfig } from '../internal/load-config.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\n\n/** @stable */\nexport interface StorageCommonOptions extends CommonOutputOptions {\n readonly config?: string;\n}\n\n/** @stable */\nexport interface StorageStatusResult {\n readonly path: string;\n readonly mode: 'lib' | 'server';\n readonly encryption: { readonly enabled: boolean; readonly cipher?: string };\n readonly cipherPeer: { readonly installed: boolean; readonly hint?: string };\n readonly mainDb: { readonly exists: boolean; readonly sizeBytes?: number };\n readonly walFile: { readonly exists: boolean; readonly sizeBytes?: number };\n readonly auditDb: {\n readonly enabled: boolean;\n readonly path?: string;\n readonly exists?: boolean;\n };\n}\n\n/** @stable */\nexport async function runStorageStatus(\n options: StorageCommonOptions = {},\n): Promise<StorageStatusResult> {\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const cipherPeer = await probeCipherPeer();\n const mainDb = await statSafely(resolveStoragePath(config.storage.path));\n const walFile = await statSafely(`${resolveStoragePath(config.storage.path)}-wal`);\n let auditPath: string | undefined;\n let auditExists: boolean | undefined;\n if (config.audit.enabled) {\n auditPath = resolveStoragePath(config.audit.path ?? deriveAuditPath(config.storage.path));\n auditExists = (await statSafely(auditPath)).exists;\n }\n const out: StorageStatusResult = Object.freeze({\n path: resolveStoragePath(config.storage.path),\n mode: config.storage.mode,\n encryption: Object.freeze({\n enabled: config.storage.encryption.enabled,\n ...(config.storage.encryption.cipher !== undefined\n ? { cipher: config.storage.encryption.cipher }\n : {}),\n }),\n cipherPeer: Object.freeze({\n installed: cipherPeer.installed,\n ...(cipherPeer.hint !== undefined ? { hint: cipherPeer.hint } : {}),\n }),\n mainDb: Object.freeze({\n exists: mainDb.exists,\n ...(mainDb.size !== undefined ? { sizeBytes: mainDb.size } : {}),\n }),\n walFile: Object.freeze({\n exists: walFile.exists,\n ...(walFile.size !== undefined ? { sizeBytes: walFile.size } : {}),\n }),\n auditDb: Object.freeze({\n enabled: config.audit.enabled,\n ...(auditPath !== undefined ? { path: auditPath } : {}),\n ...(auditExists !== undefined ? { exists: auditExists } : {}),\n }),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`storage status (${loaded.path})`));\n print(\n ` ${marker(out.mainDb.exists)} main: ${out.path} (${formatSize(out.mainDb.sizeBytes)})`,\n );\n print(\n ` ${marker(out.walFile.exists)} wal: ${out.path}-wal (${formatSize(out.walFile.sizeBytes)})`,\n );\n print(\n ` ${marker(out.encryption.enabled)} encryption: ${out.encryption.enabled ? `enabled (cipher=${out.encryption.cipher ?? 'sqlcipher'})` : 'disabled'}`,\n );\n print(\n ` ${marker(out.cipherPeer.installed)} cipher peer (better-sqlite3-multiple-ciphers): ${out.cipherPeer.installed ? 'installed' : 'missing'}`,\n );\n if (out.cipherPeer.hint !== undefined) print(` -> ${out.cipherPeer.hint}`);\n if (out.auditDb.enabled) {\n print(` ${marker(out.auditDb.exists === true)} audit.db: ${out.auditDb.path}`);\n }\n });\n return out;\n}\n\n/** @stable */\nexport interface StorageBackupOptions extends StorageCommonOptions {\n /** Destination file path for the backup copy. */\n readonly dest: string;\n /** Replace an existing destination file. Default: refuse. */\n readonly overwrite?: boolean;\n}\n\n/** @stable */\nexport interface StorageBackupResult {\n readonly source: string;\n readonly dest: string;\n readonly sizeBytes?: number;\n}\n\n/**\n * store-02: online backup via the driver's page-level `backup()` API -\n * consistent under a live writer (the daemon can keep running),\n * preserves rowids so FTS5 external-content mappings survive, and for\n * an encrypted store produces an equally-encrypted copy (same key).\n * This is the ONLY supported SQL-level backup: `VACUUM INTO`\n * renumbers rowids and corrupts the FTS mapping on restore.\n *\n * @stable\n */\nexport async function runStorageBackup(\n options: StorageBackupOptions,\n): Promise<StorageBackupResult> {\n const { openStoreContext } = await import('../internal/store-context.js');\n const dest = isAbsolute(options.dest) ? options.dest : resolve(process.cwd(), options.dest);\n const destStat = await statSafely(dest);\n if (destStat.exists && options.overwrite !== true) {\n throw new Error(\n `[graphorin/cli] backup destination already exists: ${dest}. Pass --overwrite to replace it.`,\n );\n }\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n let source: string;\n try {\n source = ctx.store.connection.path;\n if (source === dest) {\n throw new Error('[graphorin/cli] backup destination must differ from the store path.');\n }\n await ctx.store.connection.raw().backup(dest);\n } finally {\n await ctx.close();\n }\n const written = await statSafely(dest);\n const out: StorageBackupResult = Object.freeze({\n source,\n dest,\n ...(written.size !== undefined ? { sizeBytes: written.size } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand('storage backup'));\n print(` ${statusMarker('ok')} ${out.source} -> ${out.dest} (${formatSize(out.sizeBytes)})`);\n });\n return out;\n}\n\n/** @stable */\nexport interface StorageCompactOptions extends StorageCommonOptions {\n /**\n * Free pages released per `PRAGMA incremental_vacuum(N)` batch. Small\n * batches keep the writer lock short on a huge freelist.\n * @default 1000\n */\n readonly batchPages?: number;\n}\n\n/** @stable */\nexport interface StorageCompactResult {\n readonly path: string;\n /** Raw `PRAGMA auto_vacuum` value: 0 none, 1 full, 2 incremental. */\n readonly autoVacuum: number;\n /** `true` when the database supports incremental compaction. */\n readonly supported: boolean;\n readonly freelistBefore?: number;\n readonly freelistAfter?: number;\n readonly pageSize?: number;\n /** `page_size * (freelistBefore - freelistAfter)`. */\n readonly reclaimedBytes?: number;\n}\n\n/**\n * W-064: `graphorin storage compact` - return pruned pages to the OS.\n * `VACUUM` stays forbidden (it renumbers implicit rowids and corrupts\n * the FTS5 external-content mappings), but `PRAGMA incremental_vacuum`\n * relocates free pages via the ptrmap WITHOUT rebuilding tables, so it\n * is rowid-safe. Requires `auto_vacuum=2`, which `openConnection` sets\n * on every database it CREATES from this version on; on an older\n * database the command reports the limitation honestly (exit 0, file\n * untouched) - the only way out is recreating the store (fresh init +\n * `migrate-export` / import, or re-remember), because switching\n * auto_vacuum on retroactively needs the very VACUUM that is banned.\n * The vacuum runs in batches so a huge freelist never holds the writer\n * lock in one long bite; the WAL is checkpoint-TRUNCATEd first so\n * freed pages do not linger in the -wal file.\n *\n * @stable\n */\nexport async function runStorageCompact(\n options: StorageCompactOptions = {},\n): Promise<StorageCompactResult> {\n const { openStoreContext } = await import('../internal/store-context.js');\n const batchPages = options.batchPages ?? 1000;\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n // Compaction is a page-level operation: never migrate the schema\n // (an older server may own this database), never refuse on a\n // pending migration either.\n skipInit: true,\n });\n let out: StorageCompactResult;\n try {\n const conn = ctx.store.connection;\n const autoVacuum = Number(conn.pragma('auto_vacuum', { simple: true }));\n if (autoVacuum !== 2) {\n out = Object.freeze({\n path: conn.path,\n autoVacuum,\n supported: false,\n });\n } else {\n const pageSize = Number(conn.pragma('page_size', { simple: true }));\n // Flush the WAL first so already-checkpointed free pages are in\n // the main file and the -wal file shrinks too.\n conn.pragma('wal_checkpoint(TRUNCATE)');\n const freelistBefore = Number(conn.pragma('freelist_count', { simple: true }));\n let freelist = freelistBefore;\n while (freelist > 0) {\n conn.pragma(`incremental_vacuum(${batchPages})`);\n const next = Number(conn.pragma('freelist_count', { simple: true }));\n if (next >= freelist) break; // no progress - stop rather than spin\n freelist = next;\n }\n const freelistAfter = freelist;\n // In WAL mode the main file only shrinks at a checkpoint - flush\n // again so the reclaimed bytes leave the disk immediately.\n conn.pragma('wal_checkpoint(TRUNCATE)');\n out = Object.freeze({\n path: conn.path,\n autoVacuum,\n supported: true,\n freelistBefore,\n freelistAfter,\n pageSize,\n reclaimedBytes: Math.max(0, (freelistBefore - freelistAfter) * pageSize),\n });\n }\n } finally {\n await ctx.close();\n }\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand('storage compact'));\n if (!out.supported) {\n print(\n ` ${statusMarker('warn')} auto_vacuum=${out.autoVacuum} - incremental compaction unavailable.`,\n );\n print(' This database was created before incremental auto-vacuum was enabled;');\n print(\n ' its file keeps the high-water-mark size (freed pages are reused, not returned to the OS).',\n );\n print(\n ' To reclaim disk: initialise a fresh store (new databases get auto_vacuum=2) and move the data',\n );\n print(\n ' across (graphorin migrate-export / import); VACUUM stays forbidden - it corrupts FTS5 rowid mappings.',\n );\n return;\n }\n print(\n ` ${statusMarker('ok')} ${out.path}: freelist ${out.freelistBefore} -> ${out.freelistAfter} pages, reclaimed ${formatSize(out.reclaimedBytes)}`,\n );\n });\n return out;\n}\n\n/** @stable */\nexport interface StorageEncryptOptions extends StorageCommonOptions {\n /** SecretRef URI for the new passphrase. */\n readonly passphraseFrom: string;\n /**\n * Optional explicit target path for the encrypted output. Default:\n * `<storage.path>.encrypted`.\n */\n readonly targetPath?: string;\n /**\n * If `true`, atomically swap the encrypted target into the\n * `storage.path` location after the integrity check, leaving the\n * original under `<storage.path>.bak.<timestamp>`. Default `false`.\n */\n readonly swap?: boolean;\n}\n\n/** @stable */\nexport interface StorageEncryptResult {\n readonly sourcePath: string;\n readonly targetPath: string;\n readonly cipher: string;\n readonly integrityOk: boolean;\n readonly swap?: { readonly originalRenamedTo: string };\n}\n\n/**\n * `graphorin storage encrypt --passphrase-from <ref>` - encrypt a\n * previously unencrypted SQLite store. Delegates to the optional Phase\n * 16 sub-pack `@graphorin/store-sqlite-encrypted` once installed; when\n * the sub-pack is missing the CLI exits `2` (`UNSUPPORTED`) with an\n * actionable hint.\n *\n * @stable\n */\nexport async function runStorageEncrypt(\n options: StorageEncryptOptions,\n): Promise<StorageEncryptResult> {\n const subpack = await loadEncryptedSubpack();\n if (subpack === null) {\n return failUnsupported(\n options,\n `'graphorin storage encrypt' requires the optional sub-pack '@graphorin/store-sqlite-encrypted' (Phase 16) and the cipher peer 'better-sqlite3-multiple-ciphers'.`,\n 'Install @graphorin/store-sqlite-encrypted (which pulls the cipher peer transitively) before running this command.',\n );\n }\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const sourcePath = resolveStoragePath(config.storage.path);\n const targetPath = options.targetPath ?? `${sourcePath}.encrypted`;\n const passphrase = await resolvePassphraseRef(options.passphraseFrom);\n try {\n const result = await passphrase.use((raw) =>\n subpack.encryptDatabase({\n sourcePath,\n targetPath,\n passphrase: raw,\n ...(options.swap === true ? { swap: true } : {}),\n overwriteTarget: false,\n }),\n );\n const out: StorageEncryptResult = Object.freeze({\n sourcePath,\n targetPath: result.targetPath,\n cipher: result.cipher,\n integrityOk: result.integrityCheck.ok,\n ...(result.swap !== undefined ? { swap: result.swap } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`encrypt: ${out.sourcePath} -> ${out.targetPath} (cipher=${out.cipher})`));\n print(` ${marker(out.integrityOk)} cipher_integrity_check`);\n if (out.swap !== undefined) {\n print(` ${statusMarker('ok')} swapped; original kept at ${out.swap.originalRenamedTo}`);\n }\n });\n return out;\n } finally {\n passphrase.dispose();\n }\n}\n\n/** @stable */\nexport interface StorageRekeyOptions extends StorageCommonOptions {\n readonly oldPassphraseFrom: string;\n readonly newPassphraseFrom: string;\n}\n\n/** @stable */\nexport interface StorageRekeyResult {\n readonly path: string;\n readonly cipher: string;\n readonly integrityOk: boolean;\n}\n\n/** @stable */\nexport async function runStorageRekey(options: StorageRekeyOptions): Promise<StorageRekeyResult> {\n const subpack = await loadEncryptedSubpack();\n if (subpack === null) {\n return failUnsupported(\n options,\n `'graphorin storage rekey' requires the optional sub-pack '@graphorin/store-sqlite-encrypted' (Phase 16).`,\n 'Install @graphorin/store-sqlite-encrypted before running this command.',\n );\n }\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const path = resolveStoragePath(config.storage.path);\n const oldPassphrase = await resolvePassphraseRef(options.oldPassphraseFrom);\n const newPassphrase = await resolvePassphraseRef(options.newPassphraseFrom);\n try {\n const result = await oldPassphrase.use((oldRaw) =>\n newPassphrase.use((newRaw) =>\n subpack.rekeyDatabase({\n path,\n oldPassphrase: oldRaw,\n newPassphrase: newRaw,\n }),\n ),\n );\n const out: StorageRekeyResult = Object.freeze({\n path: result.path,\n cipher: result.cipher,\n integrityOk: result.integrityCheck.ok,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`rekey: ${out.path} (cipher=${out.cipher})`));\n print(` ${marker(out.integrityOk)} cipher_integrity_check`);\n });\n return out;\n } finally {\n oldPassphrase.dispose();\n newPassphrase.dispose();\n }\n}\n\n/** @stable */\nexport interface StorageCleanupBackupsOptions extends StorageCommonOptions {\n /**\n * Skip the actual delete; print what would be removed. Default `false`.\n * Tests pass `true` to assert the discovery without touching files.\n */\n readonly dryRun?: boolean;\n}\n\n/** @stable */\nexport interface StorageCleanupBackupsResult {\n readonly directory: string;\n readonly removed: ReadonlyArray<string>;\n readonly dryRun: boolean;\n}\n\n/**\n * Drop stale `.bak`, `.bak.<ts>`, and `.tmp.<ts>` siblings of the\n * configured storage path. Useful after `encrypt` / `rekey` runs that\n * leave intermediate copies around.\n *\n * @stable\n */\nexport async function runStorageCleanupBackups(\n options: StorageCleanupBackupsOptions = {},\n): Promise<StorageCleanupBackupsResult> {\n const loaded = await loadConfig(options.config);\n const config = parseServerConfig(loaded.config);\n const dbPath = resolveStoragePath(config.storage.path);\n const dir = dirname(dbPath);\n // E6: node:path basename, NOT dbPath.split('/') - on Windows the path is\n // backslash-separated, so the split returned the whole path, no readdir\n // entry ever matched, and cleanup-backups was a silent no-op.\n const basename = pathBasename(dbPath);\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch (err) {\n throw new Error(\n `[graphorin/cli] cannot read storage directory '${dir}': ${(err as Error).message}`,\n );\n }\n const candidates = entries.filter((name) => isStaleBackup(basename, name));\n const removed: string[] = [];\n for (const name of candidates) {\n const full = join(dir, name);\n if (options.dryRun !== true) {\n try {\n await unlink(full);\n removed.push(full);\n } catch {\n // best-effort cleanup; surface in summary\n }\n } else {\n removed.push(full);\n }\n }\n const out: StorageCleanupBackupsResult = Object.freeze({\n directory: dir,\n removed: Object.freeze(removed),\n dryRun: options.dryRun === true,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n if (out.removed.length === 0) {\n print(brand(`no stale backups found in ${dir}.`));\n return;\n }\n print(\n brand(\n `${out.dryRun ? 'would remove' : 'removed'} ${out.removed.length} stale backup file(s) in ${dir}:`,\n ),\n );\n for (const name of out.removed) print(` - ${name}`);\n });\n return out;\n}\n\nfunction isStaleBackup(baseName: string, candidate: string): boolean {\n if (candidate === baseName) return false;\n if (!candidate.startsWith(baseName)) return false;\n const suffix = candidate.slice(baseName.length);\n if (suffix === '.bak') return true;\n if (/^\\.bak\\.\\d+$/.test(suffix)) return true;\n if (/^\\.tmp\\.\\d+$/.test(suffix)) return true;\n return false;\n}\n\nasync function probeCipherPeer(): Promise<{\n readonly installed: boolean;\n readonly hint?: string;\n}> {\n try {\n const moduleName = 'better-sqlite3-multiple-ciphers';\n await import(/* @vite-ignore */ moduleName);\n return Object.freeze({ installed: true });\n } catch {\n return Object.freeze({\n installed: false,\n hint: \"install '@graphorin/store-sqlite-encrypted' (Phase 16) which transitively installs the cipher peer.\",\n });\n }\n}\n\n/**\n * Encryption sub-pack surface consumed by the CLI runners. Declared\n * here so the dynamic-import path stays typed without forcing the CLI\n * to take a hard dependency on the optional package.\n *\n * @internal\n */\ninterface EncryptedSubpack {\n encryptDatabase(args: {\n sourcePath: string;\n targetPath: string;\n passphrase: string;\n swap?: boolean;\n overwriteTarget?: boolean;\n }): Promise<{\n sourcePath: string;\n targetPath: string;\n cipher: string;\n integrityCheck: { ok: boolean; rows: ReadonlyArray<string> };\n swap?: { originalRenamedTo: string };\n }>;\n rekeyDatabase(args: { path: string; oldPassphrase: string; newPassphrase: string }): Promise<{\n path: string;\n cipher: string;\n integrityCheck: { ok: boolean; rows: ReadonlyArray<string> };\n }>;\n}\n\n/** @internal */\nasync function loadEncryptedSubpack(): Promise<EncryptedSubpack | null> {\n try {\n // Computed module name keeps TypeScript's resolution off the\n // build-time graph so the CLI typechecks without the optional\n // package installed.\n const moduleName = '@graphorin/store-sqlite-encrypted';\n const mod = (await import(/* @vite-ignore */ moduleName)) as Partial<EncryptedSubpack>;\n if (typeof mod?.encryptDatabase === 'function' && typeof mod?.rekeyDatabase === 'function') {\n return mod as EncryptedSubpack;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** @internal */\nasync function resolvePassphraseRef(ref: string): Promise<SecretValue> {\n try {\n return await resolveSecret(ref);\n } catch (err) {\n throw new Error(\n `[graphorin/cli] failed to resolve storage passphrase '${ref}': ${(err as Error).message}`,\n { cause: err },\n );\n }\n}\n\nasync function statSafely(\n path: string,\n): Promise<{ readonly exists: boolean; readonly size?: number }> {\n try {\n const s = await stat(path);\n return { exists: true, size: s.size };\n } catch {\n return { exists: false };\n }\n}\n\n// IP-20: resolve a relative storage path against the CWD - the SAME rule the\n// server (`createServer` → `createSqliteStore`) and `openStoreContext` use - so\n// `graphorin storage status / encrypt` from any directory reports the same\n// database the server and the other CLI commands (`memory`, …) open. Resolving\n// against the config-file dir made `storage status` the lone outlier: from a\n// foreign CWD it inspected a different `data.db` than everything else.\nfunction resolveStoragePath(target: string): string {\n return isAbsolute(target) ? target : resolve(target);\n}\n\nfunction deriveAuditPath(storagePath: string): string {\n const idx = storagePath.lastIndexOf('/');\n if (idx < 0) return 'audit.db';\n return `${storagePath.slice(0, idx)}/audit.db`;\n}\n\nfunction formatSize(bytes: number | undefined): string {\n if (bytes === undefined) return 'missing';\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;\n return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;\n}\n\nfunction marker(ok: boolean): string {\n return ok ? statusMarker('ok') : statusMarker('warn');\n}\n\nfunction failUnsupported(options: CommonOutputOptions, message: string, hint: string): never {\n const print = options.print ?? defaultPrintSink;\n print(brand(message));\n print(brand(`hint: ${hint}`));\n process.exit(EXIT_CODES.UNSUPPORTED);\n // `process.exit` returns `never` at runtime, but the type system\n // narrows the return so explicit callers do not need a cast.\n /* c8 ignore next */\n throw new Error('unreachable');\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.d.ts","names":[],"sources":["../../src/commands/token.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"token.d.ts","names":[],"sources":["../../src/commands/token.ts"],"sourcesContent":[],"mappings":";;;;;AAiMA;AAWsB,UA7JL,kBAAA,SAA2B,mBA6JR,CAAA;EAAU,SAAA,MAAA,CAAA,EAAA,MAAA;;;AAA4B,UAxJzD,kBAAA,SAA2B,kBAwJ8B,CAAA;EAuCzD,SAAA,KAAA,CAAA,EAAA,MAAkB;EASb;EACX,SAAA,MAAA,EAtMQ,aAsMR,CAAA,MAAA,CAAA;EAET;EADC,SAAA,SAAA,CAAA,EAAA,MAAA;EAAO,SAAA,GAAA,CAAA,EAAA,MAAA,GAAA,MAAA;AAqCV;AAOA;AAagB,UAzPC,iBAAA,CAyPuB;EA4BxB,SAAA,EAAA,EAAA,MAAa;;mBAlRV;;;;;;;;;iBAUG,cAAA,UAAwB,qBAAqB,QAAQ;;UA8C1D,gBAAA,SAAyB;;;;;;;;iBASpB,YAAA,WACX,mBACR,QAAQ,cAAc;;UA2BR,kBAAA,SAA2B;;;;;;;;iBAStB,cAAA,UACX,qBACR,QAAQ;;UAuBM,kBAAA,SAA2B;;;;;;;;;;iBAWtB,cAAA,UAAwB,qBAAqB,QAAQ;;UAuC1D,iBAAA,SAA0B;;;;;;;;iBASrB,aAAA,WACX,oBACR,QACD;;;;;;UAoCe,kBAAA,SAA2B;;;;;;UAO3B,iBAAA;;;;;;;;;;;;iBAaD,cAAA,UAAwB,qBAAqB;;;;;;;;iBA4B7C,aAAA"}
|
package/dist/commands/token.js
CHANGED
|
@@ -106,11 +106,11 @@ async function runTokenRevoke(options) {
|
|
|
106
106
|
const print = options.print ?? defaultPrintSink;
|
|
107
107
|
if (result === void 0) {
|
|
108
108
|
print(brand(`token '${options.id}' not found.`));
|
|
109
|
-
process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
112
111
|
print(brand(`token '${result.id}' revoked at ${result.revokedAt ?? "<now>"}`));
|
|
113
112
|
});
|
|
113
|
+
if (result === void 0) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
114
114
|
return result;
|
|
115
115
|
} finally {
|
|
116
116
|
await ctx.close();
|
|
@@ -216,8 +216,8 @@ function runTokenVerify(options) {
|
|
|
216
216
|
return;
|
|
217
217
|
}
|
|
218
218
|
print(brand(`token format ${statusMarker("fail")} (${out.reason ?? "unknown reason"})`));
|
|
219
|
-
process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
220
219
|
});
|
|
220
|
+
if (!out.ok) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
221
221
|
return out;
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.js","names":["out: TokenCreateResult","out: Array<{ readonly oldId: string; readonly newId: string; readonly raw: string }>","out: TokenVerifyResult"],"sources":["../../src/commands/token.ts"],"sourcesContent":["/**\n * `graphorin token` - manage server auth tokens.\n *\n * The subcommand thin-wraps the library functions in\n * `@graphorin/security/auth` (`createToken`, `listTokens`,\n * `revokeToken`, `rotateToken`, `rekeyTokens`, `verifyOffline`). Per\n * Phase 15 § Tokens the surface ships:\n *\n * - `graphorin token create --label <name> --scopes <list> [--expires-in <duration>]`\n * - `graphorin token list`\n * - `graphorin token revoke <id>`\n * - `graphorin token rotate <id>`\n * - `graphorin token rekey`\n * - `graphorin token verify <token>`\n *\n * The raw token bytes are shown to the operator at most once - at the\n * call site of `create` / `rotate` / `rekey`. They are wrapped in a\n * {@link SecretValue} on the way back to keep accidental logging out\n * of the codepath; the CLI prints them via `value.use((s) => ...)` so\n * the bytes never live as a plain string variable longer than needed.\n *\n * @packageDocumentation\n */\n\nimport {\n createToken,\n listTokens,\n rekeyTokens,\n revokeToken,\n rotateToken,\n type TokenMetadata,\n verifyOffline,\n} from '@graphorin/security';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\nimport { openStoreContext } from '../internal/store-context.js';\n\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\n\n/** @stable */\nexport interface TokenCommonOptions extends CommonOutputOptions {\n readonly config?: string;\n}\n\n/** @stable */\nexport interface TokenCreateOptions extends TokenCommonOptions {\n readonly label?: string;\n /** Comma-separated scope list. */\n readonly scopes: ReadonlyArray<string>;\n /** Duration string: `30d`, `12h`, `90m`, `45s`. */\n readonly expiresIn?: string;\n readonly env?: 'live' | 'test';\n}\n\n/** @stable */\nexport interface TokenCreateResult {\n readonly id: string;\n readonly label?: string;\n readonly scopes: ReadonlyArray<string>;\n readonly raw: string;\n readonly expiresAt?: string;\n}\n\n/**\n * Create a token. Returns the raw value once + the persisted record.\n *\n * @stable\n */\nexport async function runTokenCreate(options: TokenCreateOptions): Promise<TokenCreateResult> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n requirePepper: true,\n });\n try {\n if (ctx.pepper === undefined) {\n throw new Error(\n '[graphorin/cli] internal: pepper resolution should have populated ctx.pepper',\n );\n }\n const env = options.env ?? 'live';\n const expiresInMs =\n options.expiresIn !== undefined ? parseDuration(options.expiresIn) : undefined;\n const created = await createToken({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n env,\n scopes: options.scopes,\n ...(options.label !== undefined ? { label: options.label } : {}),\n ...(expiresInMs !== undefined ? { expiresInMs } : {}),\n });\n const raw = await created.raw.use((s) => String(s));\n const out: TokenCreateResult = Object.freeze({\n id: created.record.id,\n ...(created.record.label !== undefined ? { label: created.record.label } : {}),\n scopes: Object.freeze([...created.record.scopes]),\n raw,\n ...(created.record.expiresAt !== undefined ? { expiresAt: created.record.expiresAt } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`token created (id=${out.id}, scopes=${out.scopes.join(',')})`));\n print(brand(`raw token (shown ONCE):`));\n print(` ${out.raw}`);\n if (out.expiresAt !== undefined) {\n print(brand(`expires at: ${out.expiresAt}`));\n }\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenListOptions extends TokenCommonOptions {\n readonly includeRevoked?: boolean;\n}\n\n/**\n * List token metadata.\n *\n * @stable\n */\nexport async function runTokenList(\n options: TokenListOptions = {},\n): Promise<ReadonlyArray<TokenMetadata>> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const list = await listTokens(ctx.store.authTokens, {\n includeRevoked: options.includeRevoked === true,\n });\n emitReport(options, list, () => {\n const print = options.print ?? defaultPrintSink;\n if (list.length === 0) {\n print(brand('no tokens registered.'));\n return;\n }\n print(brand(`${list.length} token(s):`));\n for (const t of list) {\n const status = t.revokedAt !== undefined ? statusMarker('warn') : statusMarker('ok');\n print(` ${status} ${t.id} (label=${t.label ?? '-'}, scopes=${t.scopes.join(',')})`);\n }\n });\n return list;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenRevokeOptions extends TokenCommonOptions {\n readonly id: string;\n}\n\n/**\n * Revoke a single token.\n *\n * @stable\n */\nexport async function runTokenRevoke(\n options: TokenRevokeOptions,\n): Promise<TokenMetadata | undefined> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const result = await revokeToken(ctx.store.authTokens, options.id);\n emitReport(options, result ?? null, () => {\n const print = options.print ?? defaultPrintSink;\n if (result === undefined) {\n print(brand(`token '${options.id}' not found.`));\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return;\n }\n print(brand(`token '${result.id}' revoked at ${result.revokedAt ?? '<now>'}`));\n });\n return result;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenRotateOptions extends TokenCommonOptions {\n readonly id: string;\n readonly env?: 'live' | 'test';\n}\n\n/**\n * Revoke an existing token and immediately mint a fresh one with the\n * same scopes. Returns the new raw token bytes once.\n *\n * @stable\n */\nexport async function runTokenRotate(options: TokenRotateOptions): Promise<TokenCreateResult> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n requirePepper: true,\n });\n try {\n if (ctx.pepper === undefined) {\n throw new Error('[graphorin/cli] internal: pepper missing.');\n }\n const env = options.env ?? 'live';\n const result = await rotateToken({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n id: options.id,\n env,\n });\n const raw = await result.next.raw.use((s) => String(s));\n const out: TokenCreateResult = Object.freeze({\n id: result.next.record.id,\n ...(result.next.record.label !== undefined ? { label: result.next.record.label } : {}),\n scopes: Object.freeze([...result.next.record.scopes]),\n raw,\n ...(result.next.record.expiresAt !== undefined\n ? { expiresAt: result.next.record.expiresAt }\n : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`token '${result.old.id}' revoked + replaced by '${out.id}'`));\n print(brand('raw token (shown ONCE):'));\n print(` ${raw}`);\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenRekeyOptions extends TokenCommonOptions {\n readonly env?: 'live' | 'test';\n}\n\n/**\n * Re-issue every active token. Used after a known compromise.\n *\n * @stable\n */\nexport async function runTokenRekey(\n options: TokenRekeyOptions = {},\n): Promise<\n ReadonlyArray<{ readonly oldId: string; readonly newId: string; readonly raw: string }>\n> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n requirePepper: true,\n });\n try {\n if (ctx.pepper === undefined) {\n throw new Error('[graphorin/cli] internal: pepper missing.');\n }\n const result = await rekeyTokens({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n env: options.env ?? 'live',\n });\n const out: Array<{ readonly oldId: string; readonly newId: string; readonly raw: string }> = [];\n for (const [oldId, created] of result) {\n const raw = await created.raw.use((s) => String(s));\n out.push(Object.freeze({ oldId, newId: created.record.id, raw }));\n }\n const frozen = Object.freeze(out);\n emitReport(options, frozen, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`rekeyed ${frozen.length} token(s):`));\n for (const row of frozen) {\n print(` - old=${row.oldId} new=${row.newId}`);\n print(` raw: ${row.raw}`);\n }\n });\n return frozen;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenVerifyOptions extends CommonOutputOptions {\n readonly token: string;\n /** Optional override of the expected token prefix. */\n readonly prefix?: string;\n}\n\n/** @stable */\nexport interface TokenVerifyResult {\n readonly ok: boolean;\n readonly reason?: string;\n}\n\n/**\n * Offline checksum verification. Confirms the structural shape, the\n * environment marker, and the CRC checksum but does NOT consult the\n * token store - it only proves the token was minted by a Graphorin\n * helper.\n *\n * @stable\n */\nexport function runTokenVerify(options: TokenVerifyOptions): TokenVerifyResult {\n const result = verifyOffline(options.token, {\n ...(options.prefix !== undefined ? { acceptPrefix: options.prefix } : {}),\n });\n const out: TokenVerifyResult = Object.freeze({\n ok: result.ok,\n ...(result.ok ? {} : { reason: result.reason }),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n if (out.ok) {\n print(brand(`token format ${statusMarker('ok')} (offline checksum verified)`));\n return;\n }\n print(brand(`token format ${statusMarker('fail')} (${out.reason ?? 'unknown reason'})`));\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n });\n return out;\n}\n\n/**\n * Tiny duration parser. Accepts `Ns`, `Nm`, `Nh`, `Nd`. Returns\n * milliseconds. Throws on invalid input - surfaced as a fail-fast at\n * the CLI boundary.\n *\n * @internal\n */\nexport function parseDuration(input: string): number {\n const match = /^(\\d+)\\s*([smhd])$/i.exec(input.trim());\n if (!match) {\n throw new Error(\n `[graphorin/cli] invalid --expires-in '${input}'. Use the form '<number><s|m|h|d>' (e.g. 30d).`,\n );\n }\n const value = Number.parseInt(match[1] as string, 10);\n const unit = (match[2] as string).toLowerCase();\n switch (unit) {\n case 's':\n return value * 1000;\n case 'm':\n return value * 60_000;\n case 'h':\n return value * 60 * 60_000;\n case 'd':\n return value * ONE_DAY_MS;\n default:\n throw new Error(`[graphorin/cli] invalid --expires-in unit '${unit}'.`);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"token.js","names":["out: TokenCreateResult","out: Array<{ readonly oldId: string; readonly newId: string; readonly raw: string }>","out: TokenVerifyResult"],"sources":["../../src/commands/token.ts"],"sourcesContent":["/**\n * `graphorin token` - manage server auth tokens.\n *\n * The subcommand thin-wraps the library functions in\n * `@graphorin/security/auth` (`createToken`, `listTokens`,\n * `revokeToken`, `rotateToken`, `rekeyTokens`, `verifyOffline`). Per\n * Phase 15 § Tokens the surface ships:\n *\n * - `graphorin token create --label <name> --scopes <list> [--expires-in <duration>]`\n * - `graphorin token list`\n * - `graphorin token revoke <id>`\n * - `graphorin token rotate <id>`\n * - `graphorin token rekey`\n * - `graphorin token verify <token>`\n *\n * The raw token bytes are shown to the operator at most once - at the\n * call site of `create` / `rotate` / `rekey`. They are wrapped in a\n * {@link SecretValue} on the way back to keep accidental logging out\n * of the codepath; the CLI prints them via `value.use((s) => ...)` so\n * the bytes never live as a plain string variable longer than needed.\n *\n * @packageDocumentation\n */\n\nimport {\n createToken,\n listTokens,\n rekeyTokens,\n revokeToken,\n rotateToken,\n type TokenMetadata,\n verifyOffline,\n} from '@graphorin/security';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\nimport { openStoreContext } from '../internal/store-context.js';\n\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\n\n/** @stable */\nexport interface TokenCommonOptions extends CommonOutputOptions {\n readonly config?: string;\n}\n\n/** @stable */\nexport interface TokenCreateOptions extends TokenCommonOptions {\n readonly label?: string;\n /** Comma-separated scope list. */\n readonly scopes: ReadonlyArray<string>;\n /** Duration string: `30d`, `12h`, `90m`, `45s`. */\n readonly expiresIn?: string;\n readonly env?: 'live' | 'test';\n}\n\n/** @stable */\nexport interface TokenCreateResult {\n readonly id: string;\n readonly label?: string;\n readonly scopes: ReadonlyArray<string>;\n readonly raw: string;\n readonly expiresAt?: string;\n}\n\n/**\n * Create a token. Returns the raw value once + the persisted record.\n *\n * @stable\n */\nexport async function runTokenCreate(options: TokenCreateOptions): Promise<TokenCreateResult> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n requirePepper: true,\n });\n try {\n if (ctx.pepper === undefined) {\n throw new Error(\n '[graphorin/cli] internal: pepper resolution should have populated ctx.pepper',\n );\n }\n const env = options.env ?? 'live';\n const expiresInMs =\n options.expiresIn !== undefined ? parseDuration(options.expiresIn) : undefined;\n const created = await createToken({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n env,\n scopes: options.scopes,\n ...(options.label !== undefined ? { label: options.label } : {}),\n ...(expiresInMs !== undefined ? { expiresInMs } : {}),\n });\n const raw = await created.raw.use((s) => String(s));\n const out: TokenCreateResult = Object.freeze({\n id: created.record.id,\n ...(created.record.label !== undefined ? { label: created.record.label } : {}),\n scopes: Object.freeze([...created.record.scopes]),\n raw,\n ...(created.record.expiresAt !== undefined ? { expiresAt: created.record.expiresAt } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`token created (id=${out.id}, scopes=${out.scopes.join(',')})`));\n print(brand(`raw token (shown ONCE):`));\n print(` ${out.raw}`);\n if (out.expiresAt !== undefined) {\n print(brand(`expires at: ${out.expiresAt}`));\n }\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenListOptions extends TokenCommonOptions {\n readonly includeRevoked?: boolean;\n}\n\n/**\n * List token metadata.\n *\n * @stable\n */\nexport async function runTokenList(\n options: TokenListOptions = {},\n): Promise<ReadonlyArray<TokenMetadata>> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const list = await listTokens(ctx.store.authTokens, {\n includeRevoked: options.includeRevoked === true,\n });\n emitReport(options, list, () => {\n const print = options.print ?? defaultPrintSink;\n if (list.length === 0) {\n print(brand('no tokens registered.'));\n return;\n }\n print(brand(`${list.length} token(s):`));\n for (const t of list) {\n const status = t.revokedAt !== undefined ? statusMarker('warn') : statusMarker('ok');\n print(` ${status} ${t.id} (label=${t.label ?? '-'}, scopes=${t.scopes.join(',')})`);\n }\n });\n return list;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenRevokeOptions extends TokenCommonOptions {\n readonly id: string;\n}\n\n/**\n * Revoke a single token.\n *\n * @stable\n */\nexport async function runTokenRevoke(\n options: TokenRevokeOptions,\n): Promise<TokenMetadata | undefined> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const result = await revokeToken(ctx.store.authTokens, options.id);\n emitReport(options, result ?? null, () => {\n const print = options.print ?? defaultPrintSink;\n if (result === undefined) {\n print(brand(`token '${options.id}' not found.`));\n return;\n }\n print(brand(`token '${result.id}' revoked at ${result.revokedAt ?? '<now>'}`));\n });\n // W-002: exit code independent of --json (see runAuditVerify).\n if (result === undefined) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return result;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenRotateOptions extends TokenCommonOptions {\n readonly id: string;\n readonly env?: 'live' | 'test';\n}\n\n/**\n * Revoke an existing token and immediately mint a fresh one with the\n * same scopes. Returns the new raw token bytes once.\n *\n * @stable\n */\nexport async function runTokenRotate(options: TokenRotateOptions): Promise<TokenCreateResult> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n requirePepper: true,\n });\n try {\n if (ctx.pepper === undefined) {\n throw new Error('[graphorin/cli] internal: pepper missing.');\n }\n const env = options.env ?? 'live';\n const result = await rotateToken({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n id: options.id,\n env,\n });\n const raw = await result.next.raw.use((s) => String(s));\n const out: TokenCreateResult = Object.freeze({\n id: result.next.record.id,\n ...(result.next.record.label !== undefined ? { label: result.next.record.label } : {}),\n scopes: Object.freeze([...result.next.record.scopes]),\n raw,\n ...(result.next.record.expiresAt !== undefined\n ? { expiresAt: result.next.record.expiresAt }\n : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`token '${result.old.id}' revoked + replaced by '${out.id}'`));\n print(brand('raw token (shown ONCE):'));\n print(` ${raw}`);\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenRekeyOptions extends TokenCommonOptions {\n readonly env?: 'live' | 'test';\n}\n\n/**\n * Re-issue every active token. Used after a known compromise.\n *\n * @stable\n */\nexport async function runTokenRekey(\n options: TokenRekeyOptions = {},\n): Promise<\n ReadonlyArray<{ readonly oldId: string; readonly newId: string; readonly raw: string }>\n> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n requirePepper: true,\n });\n try {\n if (ctx.pepper === undefined) {\n throw new Error('[graphorin/cli] internal: pepper missing.');\n }\n const result = await rekeyTokens({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n env: options.env ?? 'live',\n });\n const out: Array<{ readonly oldId: string; readonly newId: string; readonly raw: string }> = [];\n for (const [oldId, created] of result) {\n const raw = await created.raw.use((s) => String(s));\n out.push(Object.freeze({ oldId, newId: created.record.id, raw }));\n }\n const frozen = Object.freeze(out);\n emitReport(options, frozen, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`rekeyed ${frozen.length} token(s):`));\n for (const row of frozen) {\n print(` - old=${row.oldId} new=${row.newId}`);\n print(` raw: ${row.raw}`);\n }\n });\n return frozen;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TokenVerifyOptions extends CommonOutputOptions {\n readonly token: string;\n /** Optional override of the expected token prefix. */\n readonly prefix?: string;\n}\n\n/** @stable */\nexport interface TokenVerifyResult {\n readonly ok: boolean;\n readonly reason?: string;\n}\n\n/**\n * Offline checksum verification. Confirms the structural shape, the\n * environment marker, and the CRC checksum but does NOT consult the\n * token store - it only proves the token was minted by a Graphorin\n * helper.\n *\n * @stable\n */\nexport function runTokenVerify(options: TokenVerifyOptions): TokenVerifyResult {\n const result = verifyOffline(options.token, {\n ...(options.prefix !== undefined ? { acceptPrefix: options.prefix } : {}),\n });\n const out: TokenVerifyResult = Object.freeze({\n ok: result.ok,\n ...(result.ok ? {} : { reason: result.reason }),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n if (out.ok) {\n print(brand(`token format ${statusMarker('ok')} (offline checksum verified)`));\n return;\n }\n print(brand(`token format ${statusMarker('fail')} (${out.reason ?? 'unknown reason'})`));\n });\n // W-002: exit code independent of --json (see runAuditVerify).\n if (!out.ok) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return out;\n}\n\n/**\n * Tiny duration parser. Accepts `Ns`, `Nm`, `Nh`, `Nd`. Returns\n * milliseconds. Throws on invalid input - surfaced as a fail-fast at\n * the CLI boundary.\n *\n * @internal\n */\nexport function parseDuration(input: string): number {\n const match = /^(\\d+)\\s*([smhd])$/i.exec(input.trim());\n if (!match) {\n throw new Error(\n `[graphorin/cli] invalid --expires-in '${input}'. Use the form '<number><s|m|h|d>' (e.g. 30d).`,\n );\n }\n const value = Number.parseInt(match[1] as string, 10);\n const unit = (match[2] as string).toLowerCase();\n switch (unit) {\n case 's':\n return value * 1000;\n case 'm':\n return value * 60_000;\n case 'h':\n return value * 60 * 60_000;\n case 'd':\n return value * ONE_DAY_MS;\n default:\n throw new Error(`[graphorin/cli] invalid --expires-in unit '${unit}'.`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@ import { discoverToolCallsInSource, gradeTool, runToolRules } from "@graphorin/e
|
|
|
10
10
|
* `graphorin tools lint` - RB-49 / suggested DEC-165.
|
|
11
11
|
*
|
|
12
12
|
* Discovers every `tool({...})` registration in the operator's project
|
|
13
|
-
* via static
|
|
13
|
+
* via a text-based static scan (no runtime probe; no `tsc` invocation), runs
|
|
14
14
|
* the three RB-49 rules from `@graphorin/eslint-plugin`, computes the
|
|
15
15
|
* per-tool grader score (40 + 30 + 30 = 100 points), aggregates a
|
|
16
16
|
* structured report, and exits `1` when any tool falls below the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools-lint.js","names":["tools: DiscoveredTool[]","scored: ToolGraderScore[]","violations: ToolsLintThresholdViolation[]","report: ToolsLintReport","out: Array<{ file: string; source: string }>","raw: string","parsed: { readonly include?: ReadonlyArray<string> }","out: string[]","s: import('node:fs').Stats","queue: string[]","entries: import('node:fs').Dirent[]"],"sources":["../../src/commands/tools-lint.ts"],"sourcesContent":["/**\n * `graphorin tools lint` - RB-49 / suggested DEC-165.\n *\n * Discovers every `tool({...})` registration in the operator's project\n * via static AST analysis (no runtime probe; no `tsc` invocation), runs\n * the three RB-49 rules from `@graphorin/eslint-plugin`, computes the\n * per-tool grader score (40 + 30 + 30 = 100 points), aggregates a\n * structured report, and exits `1` when any tool falls below the\n * `--threshold` (default `60`).\n *\n * ## Surface\n *\n * graphorin tools lint [--config <path>] [--threshold <n>]\n * [--format <text|json>] [--source <pattern>]\n *\n * ## Grader rubric (RB-49 calibration - 40 + 30 + 30 = 100 points)\n *\n * - **description axis (0..40):** 0 if missing / placeholder / shorter\n * than 20 chars; 16 if length >= 20; 24 if length >= 30; 32 if\n * length >= 50; 40 if length >= 80.\n * - **examples axis (0..30):** 0 if no examples or > 5; 12 base for\n * the first example, +6 per additional, cap at 30. Subtract 6 per\n * PII finding (cap at 0).\n * - **parameter naming axis (0..30):** 30 base, deducted per finding.\n * -30/N for ambiguous names; -10/N for numeric suffixes.\n *\n * ## Exit codes\n *\n * - `0` - every tool meets or exceeds the threshold.\n * - `1` - at least one tool falls below the threshold.\n * - `2` - invocation could not start (config missing, walker failed).\n *\n * The CLI re-uses the rule modules from `@graphorin/eslint-plugin` so\n * the rule logic has a single source of truth - the per-tool grader,\n * threshold gate, and report formatting are the only logic that lives\n * in the CLI.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve, sep } from 'node:path';\nimport process from 'node:process';\n\nimport {\n type DiscoveredTool,\n discoverToolCallsInSource,\n gradeTool,\n type LintFinding,\n type LintFindingKind,\n runToolRules,\n type ToolGraderScore,\n} from '@graphorin/eslint-plugin';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultJsonSink,\n defaultPrintSink,\n statusMarker,\n} from '../internal/output.js';\n\n/**\n * Counter event emitted per below-threshold tool per invocation. The\n * CLI exposes a configurable sink so observability pipelines (Phase\n * 04) can wire the counter into Prometheus / OTLP without touching\n * the CLI runtime. Default: no-op.\n *\n * Mirrors the `tool.lint.threshold.violations.total{toolName,score,\n * threshold}` contract documented in RB-49 § Counter emission.\n *\n * @stable\n */\nexport interface ToolsLintThresholdViolation {\n readonly toolName: string;\n readonly score: number;\n readonly threshold: number;\n}\n\n/** @stable */\nexport type ToolsLintCounterSink = (event: ToolsLintThresholdViolation) => void;\n\n/** @stable */\nexport interface ToolsLintOptions extends CommonOutputOptions {\n /** Optional path to a `tsconfig.json` whose `include` overrides the file glob. */\n readonly config?: string;\n /** Minimum acceptable per-tool score. Default `60`. */\n readonly threshold?: number;\n /** Output format. Default `'text'`. */\n readonly format?: 'text' | 'json';\n /** Optional override of the file glob pattern. */\n readonly source?: string;\n /** Override `cwd`. Default `process.cwd()`. */\n readonly cwd?: string;\n /**\n * Test seam - supply a list of `(file, source)` pairs directly so\n * the test does not need to fish around the filesystem.\n */\n readonly inlineSources?: ReadonlyArray<{ readonly file: string; readonly source: string }>;\n /**\n * Optional sink for the `tool.lint.threshold.violations.total`\n * counter (RB-49). The CLI calls this once per below-threshold tool\n * per invocation. Default: no-op.\n */\n readonly counterSink?: ToolsLintCounterSink;\n}\n\n/**\n * Documented JSON shape emitted on `--format json`. The shape is\n * stable + forward-compatible with the `@eslint/mcp` industry\n * direction (per-finding `rule` / `severity` / `message` / `location`\n * mirror the ESLint LSP convention).\n *\n * @stable\n */\nexport interface ToolsLintReport {\n readonly summary: {\n readonly totalTools: number;\n readonly totalFindings: number;\n readonly threshold: number;\n readonly passed: number;\n readonly failed: number;\n };\n readonly tools: ReadonlyArray<ToolsLintReportTool>;\n}\n\n/** @stable */\nexport interface ToolsLintReportTool {\n readonly name: string;\n readonly source: string;\n readonly score: number;\n readonly axes: {\n readonly description: number;\n readonly examples: number;\n readonly parameterNaming: number;\n };\n readonly findings: ReadonlyArray<ToolsLintReportFinding>;\n}\n\n/** @stable */\nexport interface ToolsLintReportFinding {\n readonly rule: LintFinding['rule'];\n readonly kind: LintFindingKind;\n readonly severity: LintFinding['severity'];\n readonly message: string;\n readonly location: { readonly file: string; readonly line: number };\n readonly hint?: string;\n readonly matchedPattern?: string;\n}\n\n/** Default file glob honoured when no `--source` / `--config` is supplied. */\nconst DEFAULT_GLOB = 'src/**/*.{ts,tsx}';\n\n/**\n * Run the discovery + grader pipeline. Returns the structured report\n * the CLI emits to stdout.\n *\n * @stable\n */\nexport async function runToolsLint(options: ToolsLintOptions = {}): Promise<ToolsLintReport> {\n const cwd = options.cwd ?? process.cwd();\n const threshold = options.threshold ?? 60;\n const format = options.format ?? 'text';\n\n const sources = await collectSources(cwd, options);\n const tools: DiscoveredTool[] = [];\n for (const { file, source } of sources) {\n tools.push(...discoverToolCallsInSource(file, source));\n }\n\n const scored: ToolGraderScore[] = [];\n let failed = 0;\n let passed = 0;\n let totalFindings = 0;\n const violations: ToolsLintThresholdViolation[] = [];\n for (const tool of tools) {\n const findings = runToolRules(tool);\n const score = gradeTool(tool, findings);\n scored.push(score);\n totalFindings += findings.length;\n if (score.score < threshold) {\n failed += 1;\n violations.push(\n Object.freeze({\n toolName: score.toolName,\n score: score.score,\n threshold,\n }),\n );\n } else {\n passed += 1;\n }\n }\n\n // Emit the documented counter exactly once per below-threshold tool\n // per invocation. The default sink is a no-op so non-observability\n // hosts pay nothing; the standalone server's tracer wires its own\n // counter pipeline through `--counter-sink` in v0.2.\n const counterSink = options.counterSink ?? noopCounterSink;\n for (const v of violations) counterSink(v);\n\n const report: ToolsLintReport = Object.freeze({\n summary: Object.freeze({\n totalTools: scored.length,\n totalFindings,\n threshold,\n passed,\n failed,\n }),\n tools: Object.freeze(scored.map(toReportTool)),\n });\n\n if (format === 'json') {\n const sink = options.jsonPrint ?? defaultJsonSink;\n sink(report);\n } else {\n emitTextReport(report, options);\n }\n\n if (failed > 0) {\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n }\n\n return report;\n}\n\nfunction noopCounterSink(_event: ToolsLintThresholdViolation): void {\n /* no-op */\n}\n\nfunction toReportTool(score: ToolGraderScore): ToolsLintReportTool {\n return Object.freeze({\n name: score.toolName,\n source: `${score.file}:${score.line}`,\n score: score.score,\n axes: score.axes,\n findings: Object.freeze(score.findings.map(toReportFinding)),\n });\n}\n\nfunction toReportFinding(finding: LintFinding): ToolsLintReportFinding {\n return Object.freeze({\n rule: finding.rule,\n kind: finding.kind,\n severity: finding.severity,\n message: finding.message,\n location: Object.freeze({ file: finding.file, line: finding.line }),\n ...(finding.hint !== undefined ? { hint: finding.hint } : {}),\n ...(finding.matchedPattern !== undefined ? { matchedPattern: finding.matchedPattern } : {}),\n });\n}\n\nfunction emitTextReport(report: ToolsLintReport, options: ToolsLintOptions): void {\n const print = options.print ?? defaultPrintSink;\n if (report.tools.length === 0) {\n print(brand('no tool({...}) registrations were discovered.'));\n return;\n }\n for (const tool of report.tools) {\n const ok = tool.score >= report.summary.threshold;\n const mark = ok ? statusMarker('ok') : statusMarker('fail');\n const flag = ok ? '' : ' (BELOW THRESHOLD)';\n print(brand(`${mark} ${tool.name} (score=${tool.score}/100)${flag}`));\n print(` source: ${tool.source}`);\n print(\n ` axes: description=${tool.axes.description}, examples=${tool.axes.examples}, parameterNaming=${tool.axes.parameterNaming}`,\n );\n if (tool.findings.length === 0) {\n print(` findings: none`);\n } else {\n print(` findings:`);\n for (const f of tool.findings) {\n print(\n ` [${f.severity}] ${f.rule}: ${f.message} (${f.location.file}:${f.location.line})`,\n );\n }\n }\n }\n const tag = report.summary.failed === 0 ? statusMarker('ok') : statusMarker('fail');\n print(\n brand(\n `${tag} summary: ${report.summary.totalTools} tool(s), ${report.summary.totalFindings} finding(s), threshold=${report.summary.threshold}, passed=${report.summary.passed}, failed=${report.summary.failed}`,\n ),\n );\n}\n\nasync function collectSources(\n cwd: string,\n options: ToolsLintOptions,\n): Promise<ReadonlyArray<{ readonly file: string; readonly source: string }>> {\n if (options.inlineSources !== undefined) return options.inlineSources;\n\n const glob = options.source ?? (await loadIncludeGlob(cwd, options.config)) ?? DEFAULT_GLOB;\n const files = await walkGlob(cwd, glob);\n const out: Array<{ file: string; source: string }> = [];\n for (const file of files) {\n try {\n const source = await readFile(file, 'utf8');\n out.push({ file, source });\n } catch {\n // Best-effort - skip unreadable files.\n }\n }\n return out;\n}\n\nasync function loadIncludeGlob(\n cwd: string,\n configPath: string | undefined,\n): Promise<string | undefined> {\n if (configPath === undefined) return undefined;\n const abs = isAbsolute(configPath) ? configPath : resolve(cwd, configPath);\n let raw: string;\n try {\n raw = await readFile(abs, 'utf8');\n } catch {\n return undefined;\n }\n let parsed: { readonly include?: ReadonlyArray<string> };\n try {\n parsed = JSON.parse(stripJsonComments(raw)) as { readonly include?: ReadonlyArray<string> };\n } catch {\n return undefined;\n }\n return parsed.include?.[0];\n}\n\nfunction stripJsonComments(raw: string): string {\n // Lightweight block-comment + line-comment stripper. tsconfig.json\n // commonly carries `//` comments which JSON.parse rejects.\n return raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/(^|[^:])\\/\\/.*$/gm, '$1');\n}\n\nasync function walkGlob(cwd: string, pattern: string): Promise<string[]> {\n const root = resolveGlobRoot(cwd, pattern);\n const matcher = compileGlob(pattern);\n const out: string[] = [];\n await walkDir(root, async (file) => {\n const rel = relative(cwd, file);\n if (!matcher(rel)) return;\n out.push(file);\n });\n return out.sort();\n}\n\nfunction resolveGlobRoot(cwd: string, pattern: string): string {\n const idx = pattern.indexOf('*');\n if (idx < 0) return resolve(cwd, pattern);\n const prefix = pattern.slice(0, idx);\n return resolve(cwd, prefix);\n}\n\nfunction compileGlob(pattern: string): (rel: string) => boolean {\n // The translation is intentionally done in three phases so the\n // brace-expansion output (which includes literal `(?:...)`) does not\n // collide with the `?` glob translation:\n //\n // 1. Tokenize the four glob constructs into ASCII placeholders\n // that are guaranteed not to appear in any host project's\n // filename.\n // 2. Escape every regex meta-character on the remaining literal\n // text.\n // 3. Substitute the placeholders for the equivalent regex\n // fragments (with `(?:...)` for brace expansion).\n if (pattern.length > 4096) {\n throw new Error(`compileGlob: pattern is too long (${pattern.length} > 4096 chars).`);\n }\n const BRACE_OPEN = '\\u0001';\n const BRACE_CLOSE = '\\u0002';\n const BRACE_SEP = '\\u0003';\n const GLOBSTAR = '\\u0004';\n const STAR = '\\u0005';\n const QUESTION = '\\u0006';\n\n // Phase 1: tokenize.\n let tokenized = pattern.replace(\n /\\{([^}]+)\\}/g,\n (_m, group: string) => `${BRACE_OPEN}${group.split(',').join(BRACE_SEP)}${BRACE_CLOSE}`,\n );\n tokenized = tokenized.replace(/\\*\\*/g, GLOBSTAR).replace(/\\*/g, STAR).replace(/\\?/g, QUESTION);\n\n // Phase 2: escape every remaining regex meta-char on the literal\n // text. The placeholders are non-printable and never collide.\n const escaped = tokenized.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // Phase 3: substitute the placeholders for the regex fragments.\n const translated = escaped\n .replace(new RegExp(BRACE_OPEN, 'g'), '(?:')\n .replace(new RegExp(BRACE_CLOSE, 'g'), ')')\n .replace(new RegExp(BRACE_SEP, 'g'), '|')\n .replace(new RegExp(GLOBSTAR, 'g'), '.*')\n .replace(new RegExp(STAR, 'g'), '[^/]*')\n .replace(new RegExp(QUESTION, 'g'), '[^/]');\n\n const re = new RegExp(`^${translated}$`);\n return (rel: string) => {\n const normalized = rel.split(sep).join('/');\n return re.test(normalized);\n };\n}\n\nasync function walkDir(root: string, visit: (file: string) => Promise<void>): Promise<void> {\n let s: import('node:fs').Stats;\n try {\n s = await stat(root);\n } catch {\n return;\n }\n if (!s.isDirectory()) {\n if (s.isFile()) await visit(root);\n return;\n }\n const queue: string[] = [root];\n while (queue.length > 0) {\n const dir = queue.shift() as string;\n let entries: import('node:fs').Dirent[];\n try {\n entries = await (await import('node:fs/promises')).readdir(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') continue;\n const next = `${dir}${sep}${entry.name}`;\n if (entry.isDirectory()) queue.push(next);\n else if (entry.isFile()) await visit(next);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tools-lint.js","names":["tools: DiscoveredTool[]","scored: ToolGraderScore[]","violations: ToolsLintThresholdViolation[]","report: ToolsLintReport","out: Array<{ file: string; source: string }>","raw: string","parsed: { readonly include?: ReadonlyArray<string> }","out: string[]","s: import('node:fs').Stats","queue: string[]","entries: import('node:fs').Dirent[]"],"sources":["../../src/commands/tools-lint.ts"],"sourcesContent":["/**\n * `graphorin tools lint` - RB-49 / suggested DEC-165.\n *\n * Discovers every `tool({...})` registration in the operator's project\n * via a text-based static scan (no runtime probe; no `tsc` invocation), runs\n * the three RB-49 rules from `@graphorin/eslint-plugin`, computes the\n * per-tool grader score (40 + 30 + 30 = 100 points), aggregates a\n * structured report, and exits `1` when any tool falls below the\n * `--threshold` (default `60`).\n *\n * ## Surface\n *\n * graphorin tools lint [--config <path>] [--threshold <n>]\n * [--format <text|json>] [--source <pattern>]\n *\n * ## Grader rubric (RB-49 calibration - 40 + 30 + 30 = 100 points)\n *\n * - **description axis (0..40):** 0 if missing / placeholder / shorter\n * than 20 chars; 16 if length >= 20; 24 if length >= 30; 32 if\n * length >= 50; 40 if length >= 80.\n * - **examples axis (0..30):** 0 if no examples or > 5; 12 base for\n * the first example, +6 per additional, cap at 30. Subtract 6 per\n * PII finding (cap at 0).\n * - **parameter naming axis (0..30):** 30 base, deducted per finding.\n * -30/N for ambiguous names; -10/N for numeric suffixes.\n *\n * ## Exit codes\n *\n * - `0` - every tool meets or exceeds the threshold.\n * - `1` - at least one tool falls below the threshold.\n * - `2` - invocation could not start (config missing, walker failed).\n *\n * The CLI re-uses the rule modules from `@graphorin/eslint-plugin` so\n * the rule logic has a single source of truth - the per-tool grader,\n * threshold gate, and report formatting are the only logic that lives\n * in the CLI.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve, sep } from 'node:path';\nimport process from 'node:process';\n\nimport {\n type DiscoveredTool,\n discoverToolCallsInSource,\n gradeTool,\n type LintFinding,\n type LintFindingKind,\n runToolRules,\n type ToolGraderScore,\n} from '@graphorin/eslint-plugin';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultJsonSink,\n defaultPrintSink,\n statusMarker,\n} from '../internal/output.js';\n\n/**\n * Counter event emitted per below-threshold tool per invocation. The\n * CLI exposes a configurable sink so observability pipelines (Phase\n * 04) can wire the counter into Prometheus / OTLP without touching\n * the CLI runtime. Default: no-op.\n *\n * Mirrors the `tool.lint.threshold.violations.total{toolName,score,\n * threshold}` contract documented in RB-49 § Counter emission.\n *\n * @stable\n */\nexport interface ToolsLintThresholdViolation {\n readonly toolName: string;\n readonly score: number;\n readonly threshold: number;\n}\n\n/** @stable */\nexport type ToolsLintCounterSink = (event: ToolsLintThresholdViolation) => void;\n\n/** @stable */\nexport interface ToolsLintOptions extends CommonOutputOptions {\n /** Optional path to a `tsconfig.json` whose `include` overrides the file glob. */\n readonly config?: string;\n /** Minimum acceptable per-tool score. Default `60`. */\n readonly threshold?: number;\n /** Output format. Default `'text'`. */\n readonly format?: 'text' | 'json';\n /** Optional override of the file glob pattern. */\n readonly source?: string;\n /** Override `cwd`. Default `process.cwd()`. */\n readonly cwd?: string;\n /**\n * Test seam - supply a list of `(file, source)` pairs directly so\n * the test does not need to fish around the filesystem.\n */\n readonly inlineSources?: ReadonlyArray<{ readonly file: string; readonly source: string }>;\n /**\n * Optional sink for the `tool.lint.threshold.violations.total`\n * counter (RB-49). The CLI calls this once per below-threshold tool\n * per invocation. Default: no-op.\n */\n readonly counterSink?: ToolsLintCounterSink;\n}\n\n/**\n * Documented JSON shape emitted on `--format json`. The shape is\n * stable + forward-compatible with the `@eslint/mcp` industry\n * direction (per-finding `rule` / `severity` / `message` / `location`\n * mirror the ESLint LSP convention).\n *\n * @stable\n */\nexport interface ToolsLintReport {\n readonly summary: {\n readonly totalTools: number;\n readonly totalFindings: number;\n readonly threshold: number;\n readonly passed: number;\n readonly failed: number;\n };\n readonly tools: ReadonlyArray<ToolsLintReportTool>;\n}\n\n/** @stable */\nexport interface ToolsLintReportTool {\n readonly name: string;\n readonly source: string;\n readonly score: number;\n readonly axes: {\n readonly description: number;\n readonly examples: number;\n readonly parameterNaming: number;\n };\n readonly findings: ReadonlyArray<ToolsLintReportFinding>;\n}\n\n/** @stable */\nexport interface ToolsLintReportFinding {\n readonly rule: LintFinding['rule'];\n readonly kind: LintFindingKind;\n readonly severity: LintFinding['severity'];\n readonly message: string;\n readonly location: { readonly file: string; readonly line: number };\n readonly hint?: string;\n readonly matchedPattern?: string;\n}\n\n/** Default file glob honoured when no `--source` / `--config` is supplied. */\nconst DEFAULT_GLOB = 'src/**/*.{ts,tsx}';\n\n/**\n * Run the discovery + grader pipeline. Returns the structured report\n * the CLI emits to stdout.\n *\n * @stable\n */\nexport async function runToolsLint(options: ToolsLintOptions = {}): Promise<ToolsLintReport> {\n const cwd = options.cwd ?? process.cwd();\n const threshold = options.threshold ?? 60;\n const format = options.format ?? 'text';\n\n const sources = await collectSources(cwd, options);\n const tools: DiscoveredTool[] = [];\n for (const { file, source } of sources) {\n tools.push(...discoverToolCallsInSource(file, source));\n }\n\n const scored: ToolGraderScore[] = [];\n let failed = 0;\n let passed = 0;\n let totalFindings = 0;\n const violations: ToolsLintThresholdViolation[] = [];\n for (const tool of tools) {\n const findings = runToolRules(tool);\n const score = gradeTool(tool, findings);\n scored.push(score);\n totalFindings += findings.length;\n if (score.score < threshold) {\n failed += 1;\n violations.push(\n Object.freeze({\n toolName: score.toolName,\n score: score.score,\n threshold,\n }),\n );\n } else {\n passed += 1;\n }\n }\n\n // Emit the documented counter exactly once per below-threshold tool\n // per invocation. The default sink is a no-op so non-observability\n // hosts pay nothing; the standalone server's tracer wires its own\n // counter pipeline through `--counter-sink` in v0.2.\n const counterSink = options.counterSink ?? noopCounterSink;\n for (const v of violations) counterSink(v);\n\n const report: ToolsLintReport = Object.freeze({\n summary: Object.freeze({\n totalTools: scored.length,\n totalFindings,\n threshold,\n passed,\n failed,\n }),\n tools: Object.freeze(scored.map(toReportTool)),\n });\n\n if (format === 'json') {\n const sink = options.jsonPrint ?? defaultJsonSink;\n sink(report);\n } else {\n emitTextReport(report, options);\n }\n\n if (failed > 0) {\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n }\n\n return report;\n}\n\nfunction noopCounterSink(_event: ToolsLintThresholdViolation): void {\n /* no-op */\n}\n\nfunction toReportTool(score: ToolGraderScore): ToolsLintReportTool {\n return Object.freeze({\n name: score.toolName,\n source: `${score.file}:${score.line}`,\n score: score.score,\n axes: score.axes,\n findings: Object.freeze(score.findings.map(toReportFinding)),\n });\n}\n\nfunction toReportFinding(finding: LintFinding): ToolsLintReportFinding {\n return Object.freeze({\n rule: finding.rule,\n kind: finding.kind,\n severity: finding.severity,\n message: finding.message,\n location: Object.freeze({ file: finding.file, line: finding.line }),\n ...(finding.hint !== undefined ? { hint: finding.hint } : {}),\n ...(finding.matchedPattern !== undefined ? { matchedPattern: finding.matchedPattern } : {}),\n });\n}\n\nfunction emitTextReport(report: ToolsLintReport, options: ToolsLintOptions): void {\n const print = options.print ?? defaultPrintSink;\n if (report.tools.length === 0) {\n print(brand('no tool({...}) registrations were discovered.'));\n return;\n }\n for (const tool of report.tools) {\n const ok = tool.score >= report.summary.threshold;\n const mark = ok ? statusMarker('ok') : statusMarker('fail');\n const flag = ok ? '' : ' (BELOW THRESHOLD)';\n print(brand(`${mark} ${tool.name} (score=${tool.score}/100)${flag}`));\n print(` source: ${tool.source}`);\n print(\n ` axes: description=${tool.axes.description}, examples=${tool.axes.examples}, parameterNaming=${tool.axes.parameterNaming}`,\n );\n if (tool.findings.length === 0) {\n print(` findings: none`);\n } else {\n print(` findings:`);\n for (const f of tool.findings) {\n print(\n ` [${f.severity}] ${f.rule}: ${f.message} (${f.location.file}:${f.location.line})`,\n );\n }\n }\n }\n const tag = report.summary.failed === 0 ? statusMarker('ok') : statusMarker('fail');\n print(\n brand(\n `${tag} summary: ${report.summary.totalTools} tool(s), ${report.summary.totalFindings} finding(s), threshold=${report.summary.threshold}, passed=${report.summary.passed}, failed=${report.summary.failed}`,\n ),\n );\n}\n\nasync function collectSources(\n cwd: string,\n options: ToolsLintOptions,\n): Promise<ReadonlyArray<{ readonly file: string; readonly source: string }>> {\n if (options.inlineSources !== undefined) return options.inlineSources;\n\n const glob = options.source ?? (await loadIncludeGlob(cwd, options.config)) ?? DEFAULT_GLOB;\n const files = await walkGlob(cwd, glob);\n const out: Array<{ file: string; source: string }> = [];\n for (const file of files) {\n try {\n const source = await readFile(file, 'utf8');\n out.push({ file, source });\n } catch {\n // Best-effort - skip unreadable files.\n }\n }\n return out;\n}\n\nasync function loadIncludeGlob(\n cwd: string,\n configPath: string | undefined,\n): Promise<string | undefined> {\n if (configPath === undefined) return undefined;\n const abs = isAbsolute(configPath) ? configPath : resolve(cwd, configPath);\n let raw: string;\n try {\n raw = await readFile(abs, 'utf8');\n } catch {\n return undefined;\n }\n let parsed: { readonly include?: ReadonlyArray<string> };\n try {\n parsed = JSON.parse(stripJsonComments(raw)) as { readonly include?: ReadonlyArray<string> };\n } catch {\n return undefined;\n }\n return parsed.include?.[0];\n}\n\nfunction stripJsonComments(raw: string): string {\n // Lightweight block-comment + line-comment stripper. tsconfig.json\n // commonly carries `//` comments which JSON.parse rejects.\n return raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/(^|[^:])\\/\\/.*$/gm, '$1');\n}\n\nasync function walkGlob(cwd: string, pattern: string): Promise<string[]> {\n const root = resolveGlobRoot(cwd, pattern);\n const matcher = compileGlob(pattern);\n const out: string[] = [];\n await walkDir(root, async (file) => {\n const rel = relative(cwd, file);\n if (!matcher(rel)) return;\n out.push(file);\n });\n return out.sort();\n}\n\nfunction resolveGlobRoot(cwd: string, pattern: string): string {\n const idx = pattern.indexOf('*');\n if (idx < 0) return resolve(cwd, pattern);\n const prefix = pattern.slice(0, idx);\n return resolve(cwd, prefix);\n}\n\nfunction compileGlob(pattern: string): (rel: string) => boolean {\n // The translation is intentionally done in three phases so the\n // brace-expansion output (which includes literal `(?:...)`) does not\n // collide with the `?` glob translation:\n //\n // 1. Tokenize the four glob constructs into ASCII placeholders\n // that are guaranteed not to appear in any host project's\n // filename.\n // 2. Escape every regex meta-character on the remaining literal\n // text.\n // 3. Substitute the placeholders for the equivalent regex\n // fragments (with `(?:...)` for brace expansion).\n if (pattern.length > 4096) {\n throw new Error(`compileGlob: pattern is too long (${pattern.length} > 4096 chars).`);\n }\n const BRACE_OPEN = '\\u0001';\n const BRACE_CLOSE = '\\u0002';\n const BRACE_SEP = '\\u0003';\n const GLOBSTAR = '\\u0004';\n const STAR = '\\u0005';\n const QUESTION = '\\u0006';\n\n // Phase 1: tokenize.\n let tokenized = pattern.replace(\n /\\{([^}]+)\\}/g,\n (_m, group: string) => `${BRACE_OPEN}${group.split(',').join(BRACE_SEP)}${BRACE_CLOSE}`,\n );\n tokenized = tokenized.replace(/\\*\\*/g, GLOBSTAR).replace(/\\*/g, STAR).replace(/\\?/g, QUESTION);\n\n // Phase 2: escape every remaining regex meta-char on the literal\n // text. The placeholders are non-printable and never collide.\n const escaped = tokenized.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // Phase 3: substitute the placeholders for the regex fragments.\n const translated = escaped\n .replace(new RegExp(BRACE_OPEN, 'g'), '(?:')\n .replace(new RegExp(BRACE_CLOSE, 'g'), ')')\n .replace(new RegExp(BRACE_SEP, 'g'), '|')\n .replace(new RegExp(GLOBSTAR, 'g'), '.*')\n .replace(new RegExp(STAR, 'g'), '[^/]*')\n .replace(new RegExp(QUESTION, 'g'), '[^/]');\n\n const re = new RegExp(`^${translated}$`);\n return (rel: string) => {\n const normalized = rel.split(sep).join('/');\n return re.test(normalized);\n };\n}\n\nasync function walkDir(root: string, visit: (file: string) => Promise<void>): Promise<void> {\n let s: import('node:fs').Stats;\n try {\n s = await stat(root);\n } catch {\n return;\n }\n if (!s.isDirectory()) {\n if (s.isFile()) await visit(root);\n return;\n }\n const queue: string[] = [root];\n while (queue.length > 0) {\n const dir = queue.shift() as string;\n let entries: import('node:fs').Dirent[];\n try {\n entries = await (await import('node:fs/promises')).readdir(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') continue;\n const next = `${dir}${sep}${entry.name}`;\n if (entry.isDirectory()) queue.push(next);\n else if (entry.isFile()) await visit(next);\n }\n }\n}\n"],"mappings":"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"}
|