qa360 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/commands/history.js +1 -1
  2. package/dist/commands/pack.js +1 -1
  3. package/dist/commands/run.d.ts +1 -1
  4. package/dist/commands/run.d.ts.map +1 -1
  5. package/dist/commands/run.js +1 -1
  6. package/dist/commands/secrets.js +1 -1
  7. package/dist/commands/serve.js +1 -1
  8. package/dist/commands/verify.js +1 -1
  9. package/dist/core/adapters/gitleaks-secrets.d.ts +115 -0
  10. package/dist/core/adapters/gitleaks-secrets.d.ts.map +1 -0
  11. package/dist/core/adapters/gitleaks-secrets.js +410 -0
  12. package/dist/core/adapters/k6-perf.d.ts +86 -0
  13. package/dist/core/adapters/k6-perf.d.ts.map +1 -0
  14. package/dist/core/adapters/k6-perf.js +398 -0
  15. package/dist/core/adapters/osv-deps.d.ts +124 -0
  16. package/dist/core/adapters/osv-deps.d.ts.map +1 -0
  17. package/dist/core/adapters/osv-deps.js +372 -0
  18. package/dist/core/adapters/playwright-api.d.ts +82 -0
  19. package/dist/core/adapters/playwright-api.d.ts.map +1 -0
  20. package/dist/core/adapters/playwright-api.js +252 -0
  21. package/dist/core/adapters/playwright-ui.d.ts +115 -0
  22. package/dist/core/adapters/playwright-ui.d.ts.map +1 -0
  23. package/dist/core/adapters/playwright-ui.js +346 -0
  24. package/dist/core/adapters/semgrep-sast.d.ts +100 -0
  25. package/dist/core/adapters/semgrep-sast.d.ts.map +1 -0
  26. package/dist/core/adapters/semgrep-sast.js +322 -0
  27. package/dist/core/adapters/zap-dast.d.ts +134 -0
  28. package/dist/core/adapters/zap-dast.d.ts.map +1 -0
  29. package/dist/core/adapters/zap-dast.js +424 -0
  30. package/dist/core/hooks/compose.d.ts +62 -0
  31. package/dist/core/hooks/compose.d.ts.map +1 -0
  32. package/dist/core/hooks/compose.js +225 -0
  33. package/dist/core/hooks/runner.d.ts +69 -0
  34. package/dist/core/hooks/runner.d.ts.map +1 -0
  35. package/dist/core/hooks/runner.js +303 -0
  36. package/dist/core/index.d.ts +74 -0
  37. package/dist/core/index.d.ts.map +1 -0
  38. package/dist/core/index.js +39 -0
  39. package/dist/core/pack/migrator.d.ts +52 -0
  40. package/dist/core/pack/migrator.d.ts.map +1 -0
  41. package/dist/core/pack/migrator.js +304 -0
  42. package/dist/core/pack/validator.d.ts +43 -0
  43. package/dist/core/pack/validator.d.ts.map +1 -0
  44. package/dist/core/pack/validator.js +292 -0
  45. package/dist/core/proof/bundle.d.ts +138 -0
  46. package/dist/core/proof/bundle.d.ts.map +1 -0
  47. package/dist/core/proof/bundle.js +160 -0
  48. package/dist/core/proof/canonicalize.d.ts +48 -0
  49. package/dist/core/proof/canonicalize.d.ts.map +1 -0
  50. package/dist/core/proof/canonicalize.js +105 -0
  51. package/dist/core/proof/index.d.ts +14 -0
  52. package/dist/core/proof/index.d.ts.map +1 -0
  53. package/dist/core/proof/index.js +18 -0
  54. package/dist/core/proof/schema.d.ts +218 -0
  55. package/dist/core/proof/schema.d.ts.map +1 -0
  56. package/dist/core/proof/schema.js +263 -0
  57. package/dist/core/proof/signer.d.ts +112 -0
  58. package/dist/core/proof/signer.d.ts.map +1 -0
  59. package/dist/core/proof/signer.js +226 -0
  60. package/dist/core/proof/verifier.d.ts +98 -0
  61. package/dist/core/proof/verifier.d.ts.map +1 -0
  62. package/dist/core/proof/verifier.js +302 -0
  63. package/dist/core/runner/phase3-runner.d.ts +102 -0
  64. package/dist/core/runner/phase3-runner.d.ts.map +1 -0
  65. package/dist/core/runner/phase3-runner.js +471 -0
  66. package/dist/core/secrets/crypto.d.ts +76 -0
  67. package/dist/core/secrets/crypto.d.ts.map +1 -0
  68. package/dist/core/secrets/crypto.js +225 -0
  69. package/dist/core/secrets/manager.d.ts +77 -0
  70. package/dist/core/secrets/manager.d.ts.map +1 -0
  71. package/dist/core/secrets/manager.js +219 -0
  72. package/dist/core/security/redaction-patterns-extended.d.ts +28 -0
  73. package/dist/core/security/redaction-patterns-extended.d.ts.map +1 -0
  74. package/dist/core/security/redaction-patterns-extended.js +247 -0
  75. package/dist/core/security/redactor.d.ts +72 -0
  76. package/dist/core/security/redactor.d.ts.map +1 -0
  77. package/dist/core/security/redactor.js +279 -0
  78. package/dist/core/serve/diagnostics-collector.d.ts +33 -0
  79. package/dist/core/serve/diagnostics-collector.d.ts.map +1 -0
  80. package/dist/core/serve/diagnostics-collector.js +149 -0
  81. package/dist/core/serve/health-checker.d.ts +45 -0
  82. package/dist/core/serve/health-checker.d.ts.map +1 -0
  83. package/dist/core/serve/health-checker.js +219 -0
  84. package/dist/core/serve/index.d.ts +9 -0
  85. package/dist/core/serve/index.d.ts.map +1 -0
  86. package/dist/core/serve/index.js +8 -0
  87. package/dist/core/serve/metrics-collector.d.ts +25 -0
  88. package/dist/core/serve/metrics-collector.d.ts.map +1 -0
  89. package/dist/core/serve/metrics-collector.js +322 -0
  90. package/dist/core/serve/process-manager.d.ts +37 -0
  91. package/dist/core/serve/process-manager.d.ts.map +1 -0
  92. package/dist/core/serve/process-manager.js +213 -0
  93. package/dist/core/serve/server.d.ts +37 -0
  94. package/dist/core/serve/server.d.ts.map +1 -0
  95. package/dist/core/serve/server.js +191 -0
  96. package/dist/core/types/pack-v1.d.ts +162 -0
  97. package/dist/core/types/pack-v1.d.ts.map +1 -0
  98. package/dist/core/types/pack-v1.js +5 -0
  99. package/dist/core/types/trust-score.d.ts +70 -0
  100. package/dist/core/types/trust-score.d.ts.map +1 -0
  101. package/dist/core/types/trust-score.js +191 -0
  102. package/dist/core/vault/cas.d.ts +87 -0
  103. package/dist/core/vault/cas.d.ts.map +1 -0
  104. package/dist/core/vault/cas.js +255 -0
  105. package/dist/core/vault/index.d.ts +205 -0
  106. package/dist/core/vault/index.d.ts.map +1 -0
  107. package/dist/core/vault/index.js +631 -0
  108. package/package.json +13 -6
@@ -0,0 +1,292 @@
1
+ /**
2
+ * QA360 Pack v1 Validator
3
+ * Validates pack.yml files against the official schema
4
+ */
5
+ import Ajv from 'ajv';
6
+ import addFormats from 'ajv-formats';
7
+ import { readFileSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ export class PackValidator {
11
+ ajv;
12
+ schema;
13
+ constructor() {
14
+ this.ajv = new Ajv({
15
+ allErrors: true,
16
+ verbose: true,
17
+ strict: false
18
+ });
19
+ addFormats(this.ajv);
20
+ // Load schema (ES modules compatible)
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const schemaPath = join(__dirname, '../../schemas/pack.schema.json');
24
+ this.schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
25
+ this.ajv.addSchema(this.schema, 'pack-v1');
26
+ }
27
+ /**
28
+ * Validate a pack configuration
29
+ */
30
+ validate(pack) {
31
+ const errors = [];
32
+ const warnings = [];
33
+ // Schema validation
34
+ const valid = this.ajv.validate('pack-v1', pack);
35
+ if (!valid && this.ajv.errors) {
36
+ for (const error of this.ajv.errors) {
37
+ errors.push({
38
+ code: this.getErrorCode(error),
39
+ path: error.instancePath || error.schemaPath || 'root',
40
+ message: this.formatErrorMessage(error),
41
+ suggestion: this.getSuggestion(error)
42
+ });
43
+ }
44
+ }
45
+ // Custom business logic validation
46
+ if (valid) {
47
+ const businessRules = this.validateBusinessRules(pack);
48
+ errors.push(...businessRules.errors);
49
+ warnings.push(...businessRules.warnings);
50
+ }
51
+ return {
52
+ valid: errors.length === 0,
53
+ errors,
54
+ warnings
55
+ };
56
+ }
57
+ /**
58
+ * Validate business rules beyond schema
59
+ */
60
+ validateBusinessRules(pack) {
61
+ const errors = [];
62
+ const warnings = [];
63
+ // Check gate-target consistency (only for non-minimal packs)
64
+ if (pack.gates.includes('api_smoke') && !pack.targets?.api) {
65
+ // For minimal smoke packs, this is a warning, not an error
66
+ if (pack.gates.length === 1 && pack.gates[0] === 'api_smoke') {
67
+ warnings.push({
68
+ code: 'QP001',
69
+ path: 'targets.api',
70
+ message: 'API smoke gate without api target - using default configuration',
71
+ suggestion: 'Add targets.api with baseUrl for better control'
72
+ });
73
+ }
74
+ else {
75
+ errors.push({
76
+ code: 'QP001',
77
+ path: 'targets.api',
78
+ message: 'API smoke gate requires api target configuration',
79
+ suggestion: 'Add targets.api with baseUrl'
80
+ });
81
+ }
82
+ }
83
+ if (pack.gates.includes('ui') && !pack.targets?.web) {
84
+ errors.push({
85
+ code: 'QP002',
86
+ path: 'targets.web',
87
+ message: 'UI gate requires web target configuration',
88
+ suggestion: 'Add targets.web with baseUrl'
89
+ });
90
+ }
91
+ // Check budget recommendations
92
+ if (pack.gates.includes('perf') && !pack.budgets?.perf_p95_ms) {
93
+ warnings.push({
94
+ code: 'QP003',
95
+ path: 'budgets.perf_p95_ms',
96
+ message: 'Performance gate without budget may not fail appropriately',
97
+ suggestion: 'Add budgets.perf_p95_ms (recommended: 800-2000ms)'
98
+ });
99
+ }
100
+ if (pack.gates.includes('a11y') && !pack.budgets?.a11y_min) {
101
+ warnings.push({
102
+ code: 'QP004',
103
+ path: 'budgets.a11y_min',
104
+ message: 'Accessibility gate without budget may not fail appropriately',
105
+ suggestion: 'Add budgets.a11y_min (recommended: 90-95%)'
106
+ });
107
+ }
108
+ // Check security configuration
109
+ if ((pack.gates.includes('sast') || pack.gates.includes('dast')) && !pack.security) {
110
+ warnings.push({
111
+ code: 'QP005',
112
+ path: 'security',
113
+ message: 'Security gates without security configuration',
114
+ suggestion: 'Add security section with sast_max_high and secrets_leak settings'
115
+ });
116
+ }
117
+ // Check for potential secrets in plain text (ignore secret references)
118
+ if (pack.environment) {
119
+ const SECRET_REF = /\$\{\{\s*secrets\.[A-Z0-9_]+\s*\}\}/g;
120
+ for (const [key, value] of Object.entries(pack.environment)) {
121
+ if (typeof value === 'string' && !SECRET_REF.test(value) && this.looksLikeSecret(value)) {
122
+ errors.push({
123
+ code: 'QP006',
124
+ path: `environment.${key}`,
125
+ message: 'Potential secret detected in plain text',
126
+ suggestion: `Use secret reference: \${{ secrets.${key.toUpperCase()} }}`
127
+ });
128
+ }
129
+ }
130
+ }
131
+ // Check for dangerous hook commands
132
+ if (pack.hooks) {
133
+ const dangerousCommands = ['rm -rf', 'sudo rm', 'del /f', 'format', 'mkfs', 'dd if='];
134
+ ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'].forEach(hookType => {
135
+ const hooks = pack.hooks[hookType];
136
+ if (hooks) {
137
+ hooks.forEach((hook, index) => {
138
+ const command = typeof hook === 'string' ? hook :
139
+ ('run' in hook ? hook.run :
140
+ 'compose' in hook ? `compose ${hook.compose}` :
141
+ 'wait_on' in hook ? `wait_on ${hook.wait_on}` : '');
142
+ if (command && dangerousCommands.some(dangerous => command.toLowerCase().includes(dangerous))) {
143
+ warnings.push({
144
+ code: 'QP007',
145
+ path: `hooks.${hookType}[${index}]`,
146
+ message: 'Potentially dangerous command detected',
147
+ suggestion: 'Review command for safety'
148
+ });
149
+ }
150
+ });
151
+ }
152
+ });
153
+ }
154
+ return { errors, warnings };
155
+ }
156
+ /**
157
+ * Check if a value looks like a secret
158
+ */
159
+ looksLikeSecret(value) {
160
+ const secretPatterns = [
161
+ /^[A-Za-z0-9+/]{20,}={0,2}$/, // Base64-like
162
+ /^[a-f0-9]{32,}$/i, // Hex tokens
163
+ /^sk-[a-zA-Z0-9]{32,}$/, // API keys
164
+ /^ghp_[a-zA-Z0-9]{36}$/, // GitHub tokens
165
+ /^xoxb-[a-zA-Z0-9-]+$/, // Slack tokens
166
+ ];
167
+ return secretPatterns.some(pattern => pattern.test(value));
168
+ }
169
+ /**
170
+ * Check if a value is a secret reference
171
+ */
172
+ isSecretReference(value) {
173
+ return /^\$\{\{\s*secrets\.[A-Z_][A-Z0-9_]*\s*\}\}$/.test(value);
174
+ }
175
+ /**
176
+ * Validate hook commands for security issues
177
+ */
178
+ validateHookSecurity(hooks, basePath, warnings) {
179
+ for (let i = 0; i < hooks.length; i++) {
180
+ const hook = hooks[i];
181
+ if (typeof hook.run === 'string') {
182
+ // Check for dangerous commands
183
+ const dangerousPatterns = [
184
+ /rm\s+-rf\s+\//, // rm -rf /
185
+ /sudo\s+/, // sudo commands
186
+ /curl.*\|\s*sh/, // curl | sh
187
+ /wget.*\|\s*sh/, // wget | sh
188
+ ];
189
+ for (const pattern of dangerousPatterns) {
190
+ if (pattern.test(hook.run)) {
191
+ warnings.push({
192
+ code: 'QP007',
193
+ path: `${basePath}[${i}].run`,
194
+ message: 'Potentially dangerous command detected',
195
+ suggestion: 'Review command for security implications'
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Get error code from AJV error
205
+ */
206
+ getErrorCode(error) {
207
+ const codeMap = {
208
+ 'required': 'QP100',
209
+ 'type': 'QP101',
210
+ 'format': 'QP102',
211
+ 'pattern': 'QP103',
212
+ 'enum': 'QP104',
213
+ 'const': 'QP105',
214
+ 'minimum': 'QP106',
215
+ 'maximum': 'QP107',
216
+ 'minLength': 'QP108',
217
+ 'maxLength': 'QP109',
218
+ 'minItems': 'QP110',
219
+ 'uniqueItems': 'QP111',
220
+ 'additionalProperties': 'QP112'
221
+ };
222
+ return codeMap[error.keyword] || 'QP999';
223
+ }
224
+ /**
225
+ * Format error message for better UX
226
+ */
227
+ formatErrorMessage(error) {
228
+ const path = error.instancePath || 'root';
229
+ switch (error.keyword) {
230
+ case 'required':
231
+ return `Missing required field: ${error.params.missingProperty}`;
232
+ case 'type':
233
+ return `Expected ${error.params.type}, got ${typeof error.data}`;
234
+ case 'format':
235
+ return `Invalid ${error.params.format} format`;
236
+ case 'pattern':
237
+ return `Value does not match required pattern`;
238
+ case 'enum':
239
+ return `Value must be one of: ${error.params.allowedValues.join(', ')}`;
240
+ case 'const':
241
+ return `Value must be exactly: ${error.params.allowedValue}`;
242
+ case 'minimum':
243
+ return `Value must be >= ${error.params.limit}`;
244
+ case 'maximum':
245
+ return `Value must be <= ${error.params.limit}`;
246
+ case 'minLength':
247
+ return `Value must be at least ${error.params.limit} characters`;
248
+ case 'maxLength':
249
+ return `Value must be at most ${error.params.limit} characters`;
250
+ case 'minItems':
251
+ return `Array must have at least ${error.params.limit} items`;
252
+ case 'uniqueItems':
253
+ return `Array items must be unique`;
254
+ case 'additionalProperties':
255
+ return `Unknown property: ${error.params.additionalProperty}`;
256
+ default:
257
+ return error.message || 'Validation error';
258
+ }
259
+ }
260
+ /**
261
+ * Get suggestion for fixing error
262
+ */
263
+ getSuggestion(error) {
264
+ switch (error.keyword) {
265
+ case 'required':
266
+ const field = error.params.missingProperty;
267
+ const suggestions = {
268
+ 'version': 'Add: version: 1',
269
+ 'name': 'Add: name: "my-pack"',
270
+ 'gates': 'Add: gates: ["api_smoke"]',
271
+ 'baseUrl': 'Add: baseUrl: "https://api.example.com"'
272
+ };
273
+ return suggestions[field];
274
+ case 'enum':
275
+ return `Use one of: ${error.params.allowedValues.join(', ')}`;
276
+ case 'format':
277
+ if (error.params.format === 'uri') {
278
+ return 'Use full URL with protocol: https://example.com';
279
+ }
280
+ break;
281
+ case 'pattern':
282
+ if (error.instancePath.includes('name')) {
283
+ return 'Use only letters, numbers, underscore, and hyphen';
284
+ }
285
+ if (error.instancePath.includes('smoke')) {
286
+ return 'Format: "GET /path -> 200"';
287
+ }
288
+ break;
289
+ }
290
+ return undefined;
291
+ }
292
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Proof Bundle Creation
3
+ *
4
+ * Creates cryptographically signed proof bundles from test run data.
5
+ *
6
+ * @see docs/rfc/proof-bundle-v1.md#3-data-model
7
+ */
8
+ import { type KeyPair } from './signer.js';
9
+ /**
10
+ * Proof Bundle structure (matches RFC spec)
11
+ */
12
+ export interface ProofBundle {
13
+ spec: 'qa360.proof.v1';
14
+ run: RunMetadata;
15
+ artifacts: Artifact[];
16
+ results: TestResults;
17
+ signing: SigningMetadata;
18
+ signature: string;
19
+ }
20
+ /**
21
+ * Run metadata
22
+ */
23
+ export interface RunMetadata {
24
+ id: string;
25
+ startedAt: string;
26
+ finishedAt: string;
27
+ environment: Environment;
28
+ packHash: string;
29
+ ciContext: CIContext;
30
+ }
31
+ /**
32
+ * Environment information
33
+ */
34
+ export interface Environment {
35
+ os: 'windows' | 'linux' | 'darwin';
36
+ node: string;
37
+ arch: 'x64' | 'arm64';
38
+ ci: boolean;
39
+ }
40
+ /**
41
+ * CI context
42
+ */
43
+ export interface CIContext {
44
+ provider: string | null;
45
+ }
46
+ /**
47
+ * Artifact metadata
48
+ */
49
+ export interface Artifact {
50
+ name: string;
51
+ sha256: string;
52
+ size: number;
53
+ path?: string;
54
+ }
55
+ /**
56
+ * Test results
57
+ */
58
+ export interface TestResults {
59
+ trustScore: number;
60
+ gates: Gate[];
61
+ }
62
+ /**
63
+ * Gate result
64
+ */
65
+ export interface Gate {
66
+ name: string;
67
+ status: 'pass' | 'fail' | 'skip';
68
+ metrics?: Record<string, any>;
69
+ }
70
+ /**
71
+ * Signing metadata
72
+ */
73
+ export interface SigningMetadata {
74
+ algo: 'ed25519';
75
+ signerId: string;
76
+ timestamp: TimestampInfo;
77
+ identity: IdentityInfo;
78
+ }
79
+ /**
80
+ * Timestamp information (Phase 1: none, Phase 2: rfc3161)
81
+ */
82
+ export interface TimestampInfo {
83
+ type: 'none' | 'rfc3161';
84
+ token: string | null;
85
+ }
86
+ /**
87
+ * Identity information (Phase 1: none, Phase 2: did/sigstore)
88
+ */
89
+ export interface IdentityInfo {
90
+ type: 'none' | 'did' | 'sigstore';
91
+ evidence: string | object | null;
92
+ }
93
+ /**
94
+ * Parameters for creating a proof bundle
95
+ */
96
+ export interface ProofBundleParams {
97
+ runId?: string;
98
+ startedAt: Date;
99
+ finishedAt: Date;
100
+ packHash: string;
101
+ artifacts: Artifact[];
102
+ trustScore: number;
103
+ gates: Gate[];
104
+ signerId?: string;
105
+ keyPair?: KeyPair;
106
+ }
107
+ /**
108
+ * Compute SHA-256 hash of string
109
+ */
110
+ export declare function computeSHA256(data: string): string;
111
+ /**
112
+ * Create and sign a proof bundle
113
+ *
114
+ * @param params - Bundle parameters
115
+ * @returns Signed proof bundle
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const bundle = await createProofBundle({
120
+ * startedAt: new Date('2025-01-01T00:00:00Z'),
121
+ * finishedAt: new Date('2025-01-01T00:01:00Z'),
122
+ * packHash: 'sha256-abc123...',
123
+ * artifacts: [],
124
+ * trustScore: 87,
125
+ * gates: [{ name: 'api_smoke', status: 'pass' }],
126
+ * });
127
+ * ```
128
+ */
129
+ export declare function createProofBundle(params: ProofBundleParams): Promise<ProofBundle>;
130
+ /**
131
+ * Create proof bundle from pack file content
132
+ *
133
+ * @param packContent - Pack YAML/JSON content
134
+ * @param params - Other bundle parameters
135
+ * @returns Signed proof bundle
136
+ */
137
+ export declare function createProofBundleFromPack(packContent: string, params: Omit<ProofBundleParams, 'packHash'>): Promise<ProofBundle>;
138
+ //# sourceMappingURL=bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/core/proof/bundle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Proof Bundle Creation
3
+ *
4
+ * Creates cryptographically signed proof bundles from test run data.
5
+ *
6
+ * @see docs/rfc/proof-bundle-v1.md#3-data-model
7
+ */
8
+ import { randomUUID } from 'crypto';
9
+ import { platform, arch } from 'os';
10
+ import { createHash } from 'crypto';
11
+ import { canonicalizeForSigning } from './canonicalize.js';
12
+ import { sign, initializeKeys } from './signer.js';
13
+ import { validateProofBundle } from './schema.js';
14
+ /**
15
+ * Get current OS in RFC format
16
+ */
17
+ function getCurrentOS() {
18
+ const p = platform();
19
+ if (p === 'win32')
20
+ return 'windows';
21
+ if (p === 'darwin')
22
+ return 'darwin';
23
+ return 'linux';
24
+ }
25
+ /**
26
+ * Get current architecture in RFC format
27
+ */
28
+ function getCurrentArch() {
29
+ const a = arch();
30
+ if (a === 'arm64')
31
+ return 'arm64';
32
+ return 'x64';
33
+ }
34
+ /**
35
+ * Get Node.js version (semver format)
36
+ */
37
+ function getNodeVersion() {
38
+ return process.version.replace(/^v/, '');
39
+ }
40
+ /**
41
+ * Detect if running in CI
42
+ */
43
+ function isCI() {
44
+ return !!(process.env.CI ||
45
+ process.env.GITHUB_ACTIONS ||
46
+ process.env.GITLAB_CI ||
47
+ process.env.CIRCLECI ||
48
+ process.env.JENKINS_URL);
49
+ }
50
+ /**
51
+ * Detect CI provider
52
+ */
53
+ function getCIProvider() {
54
+ if (process.env.GITHUB_ACTIONS)
55
+ return 'github-actions';
56
+ if (process.env.GITLAB_CI)
57
+ return 'gitlab-ci';
58
+ if (process.env.CIRCLECI)
59
+ return 'circleci';
60
+ if (process.env.JENKINS_URL)
61
+ return 'jenkins';
62
+ return null;
63
+ }
64
+ /**
65
+ * Compute SHA-256 hash of string
66
+ */
67
+ export function computeSHA256(data) {
68
+ const hash = createHash('sha256').update(data, 'utf-8').digest('hex');
69
+ return `sha256-${hash}`;
70
+ }
71
+ /**
72
+ * Create unsigned proof bundle (without signature)
73
+ */
74
+ function createUnsignedBundle(params) {
75
+ return {
76
+ spec: 'qa360.proof.v1',
77
+ run: {
78
+ id: params.runId || randomUUID(),
79
+ startedAt: params.startedAt.toISOString(),
80
+ finishedAt: params.finishedAt.toISOString(),
81
+ environment: {
82
+ os: getCurrentOS(),
83
+ node: getNodeVersion(),
84
+ arch: getCurrentArch(),
85
+ ci: isCI(),
86
+ },
87
+ packHash: params.packHash,
88
+ ciContext: {
89
+ provider: getCIProvider(),
90
+ },
91
+ },
92
+ artifacts: params.artifacts,
93
+ results: {
94
+ trustScore: params.trustScore,
95
+ gates: params.gates,
96
+ },
97
+ signing: {
98
+ algo: 'ed25519',
99
+ signerId: params.signerId || 'local@qa360',
100
+ timestamp: {
101
+ type: 'none',
102
+ token: null,
103
+ },
104
+ identity: {
105
+ type: 'none',
106
+ evidence: null,
107
+ },
108
+ },
109
+ };
110
+ }
111
+ /**
112
+ * Create and sign a proof bundle
113
+ *
114
+ * @param params - Bundle parameters
115
+ * @returns Signed proof bundle
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const bundle = await createProofBundle({
120
+ * startedAt: new Date('2025-01-01T00:00:00Z'),
121
+ * finishedAt: new Date('2025-01-01T00:01:00Z'),
122
+ * packHash: 'sha256-abc123...',
123
+ * artifacts: [],
124
+ * trustScore: 87,
125
+ * gates: [{ name: 'api_smoke', status: 'pass' }],
126
+ * });
127
+ * ```
128
+ */
129
+ export async function createProofBundle(params) {
130
+ // Load or initialize keys
131
+ const keyPair = params.keyPair || await initializeKeys();
132
+ // Create unsigned bundle
133
+ const unsigned = createUnsignedBundle(params);
134
+ // Canonicalize for signing (removes signature field, adds newline)
135
+ const canonical = canonicalizeForSigning(unsigned);
136
+ // Sign the canonical form
137
+ const signature = sign(canonical, keyPair.secretKey);
138
+ // Create final bundle
139
+ const bundle = {
140
+ ...unsigned,
141
+ signature,
142
+ };
143
+ // Validate against schema
144
+ const validation = validateProofBundle(bundle);
145
+ if (!validation.valid) {
146
+ throw new Error(`Invalid proof bundle: ${validation.errors?.join(', ')}`);
147
+ }
148
+ return bundle;
149
+ }
150
+ /**
151
+ * Create proof bundle from pack file content
152
+ *
153
+ * @param packContent - Pack YAML/JSON content
154
+ * @param params - Other bundle parameters
155
+ * @returns Signed proof bundle
156
+ */
157
+ export async function createProofBundleFromPack(packContent, params) {
158
+ const packHash = computeSHA256(packContent);
159
+ return createProofBundle({ ...params, packHash });
160
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * JSON Canonicalization for deterministic hashing
3
+ *
4
+ * Implements RFC 8785-like canonicalization:
5
+ * - Alphabetically sorted keys (recursive)
6
+ * - UTF-8 NFC normalization
7
+ * - No whitespace (compact)
8
+ * - Deterministic across platforms
9
+ *
10
+ * @see docs/rfc/proof-bundle-v1.md#4-canonicalization
11
+ */
12
+ /**
13
+ * Canonicalize a JavaScript object into deterministic JSON string
14
+ *
15
+ * @param obj - Object to canonicalize
16
+ * @returns Canonical JSON string (compact, sorted keys, UTF-8 NFC)
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const obj = { b: 2, a: 1 };
21
+ * const canonical = canonicalize(obj);
22
+ * // Returns: '{"a":1,"b":2}\n'
23
+ * ```
24
+ */
25
+ export declare function canonicalize(obj: any): string;
26
+ /**
27
+ * Canonicalize and append newline (standard format)
28
+ *
29
+ * @param obj - Object to canonicalize
30
+ * @returns Canonical JSON string with trailing newline
31
+ */
32
+ export declare function canonicalizeWithNewline(obj: any): string;
33
+ /**
34
+ * Remove signature field and canonicalize
35
+ * Used for signature verification
36
+ *
37
+ * @param bundle - Proof bundle (may contain signature field)
38
+ * @returns Canonical JSON without signature field
39
+ */
40
+ export declare function canonicalizeForSigning(bundle: any): string;
41
+ /**
42
+ * Verify canonicalization is deterministic
43
+ *
44
+ * @param obj - Object to test
45
+ * @returns true if canonicalization is stable
46
+ */
47
+ export declare function isCanonicalStable(obj: any): boolean;
48
+ //# sourceMappingURL=canonicalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../../src/core/proof/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CA+C7C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAExD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAQ1D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAQnD"}