@indigoai-us/hq-cli 5.32.0 → 5.33.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.
- package/dist/commands/__fixtures__/make-tar.d.ts +46 -0
- package/dist/commands/__fixtures__/make-tar.js +105 -0
- package/dist/commands/pack-install.d.ts +187 -1
- package/dist/commands/pack-install.js +405 -16
- package/dist/commands/packs.js +9 -4
- package/dist/commands/publish.d.ts +186 -0
- package/dist/commands/publish.js +375 -0
- package/dist/commands/safe-extract.d.ts +154 -0
- package/dist/commands/safe-extract.js +347 -0
- package/dist/index.js +7 -2
- package/dist/lib/local-tree-diff.d.ts +21 -0
- package/dist/lib/local-tree-diff.js +18 -3
- package/dist/types.d.ts +22 -0
- package/dist/utils/vault-api.d.ts +11 -0
- package/dist/utils/vault-api.js +39 -2
- package/package.json +2 -2
- package/src/commands/__fixtures__/make-tar.ts +126 -0
- package/src/commands/artifact-verify.test.ts +177 -0
- package/src/commands/marketplace-install.test.ts +414 -0
- package/src/commands/marketplace-security.test.ts +646 -0
- package/src/commands/pack-install.test.ts +209 -1
- package/src/commands/pack-install.ts +617 -15
- package/src/commands/packs.ts +8 -1
- package/src/commands/publish.test.ts +538 -0
- package/src/commands/publish.ts +517 -0
- package/src/commands/safe-extract.test.ts +459 -0
- package/src/commands/safe-extract.ts +444 -0
- package/src/index.ts +6 -0
- package/src/lib/local-tree-diff.test.ts +19 -0
- package/src/lib/local-tree-diff.ts +17 -1
- package/src/types.ts +23 -0
- package/src/utils/vault-api.ts +41 -0
|
@@ -21,7 +21,17 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
21
21
|
import * as fs from 'fs';
|
|
22
22
|
import * as os from 'os';
|
|
23
23
|
import * as path from 'path';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
installToPackages,
|
|
26
|
+
runScanPackages,
|
|
27
|
+
stampInstallSource,
|
|
28
|
+
validateManifest,
|
|
29
|
+
toMarketplaceListing,
|
|
30
|
+
fetchMarketplace,
|
|
31
|
+
computeArtifactHash,
|
|
32
|
+
ArtifactVerificationError,
|
|
33
|
+
} from './pack-install.js';
|
|
34
|
+
import type { MarketplaceDeps } from './pack-install.js';
|
|
25
35
|
import type { PackManifest } from '../types.js';
|
|
26
36
|
|
|
27
37
|
// ---------------------------------------------------------------------------
|
|
@@ -222,6 +232,81 @@ describe('pack-install: install path layout', () => {
|
|
|
222
232
|
expect(() => stampInstallSource(dest, '@scope/x@1.0.0')).toThrow(/package\.yaml.*not found/);
|
|
223
233
|
});
|
|
224
234
|
|
|
235
|
+
// ---- 6. author + capabilities attribution (US-001) ----------------------
|
|
236
|
+
// Records pack authorship so installs can be attributed. Both fields are
|
|
237
|
+
// OPTIONAL and backwards-compatible: legacy packs without an author still
|
|
238
|
+
// validate; a pack that declares an author surfaces uid/handle/displayName.
|
|
239
|
+
describe('author + capabilities (US-001)', () => {
|
|
240
|
+
// A minimal, otherwise-valid pack payload that validateManifest accepts.
|
|
241
|
+
// `contributes` declares one knowledge entry whose payload file exists, so
|
|
242
|
+
// the only variable across these tests is the author/capabilities block.
|
|
243
|
+
function writeValidPack(extraYaml: string): string {
|
|
244
|
+
const dir = mkFakePackPayload({
|
|
245
|
+
'knowledge/demo/README.md': '# demo knowledge',
|
|
246
|
+
});
|
|
247
|
+
fs.writeFileSync(
|
|
248
|
+
path.join(dir, 'package.yaml'),
|
|
249
|
+
[
|
|
250
|
+
'name: hq-pack-test',
|
|
251
|
+
'version: 1.0.0',
|
|
252
|
+
"publisher: '@indigoai-us'",
|
|
253
|
+
'access: public',
|
|
254
|
+
'requires:',
|
|
255
|
+
" hqCore: '>=12.0.0'",
|
|
256
|
+
'contributes:',
|
|
257
|
+
' knowledge:',
|
|
258
|
+
' - demo',
|
|
259
|
+
extraYaml,
|
|
260
|
+
'',
|
|
261
|
+
].join('\n'),
|
|
262
|
+
);
|
|
263
|
+
return dir;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
it('a manifest WITHOUT author validates (backwards-compatible)', () => {
|
|
267
|
+
const dir = writeValidPack('');
|
|
268
|
+
const m = validateManifest(dir, '12.0.0');
|
|
269
|
+
expect(m.name).toBe('hq-pack-test');
|
|
270
|
+
expect(m.author).toBeUndefined();
|
|
271
|
+
expect(m.capabilities).toBeUndefined();
|
|
272
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('a manifest WITH an author block surfaces uid/handle/displayName', () => {
|
|
276
|
+
const dir = writeValidPack(
|
|
277
|
+
[
|
|
278
|
+
'author:',
|
|
279
|
+
' uid: prs_abc123',
|
|
280
|
+
' handle: corey',
|
|
281
|
+
" displayName: 'Corey Epstein'",
|
|
282
|
+
'capabilities:',
|
|
283
|
+
' - hooks',
|
|
284
|
+
' - network',
|
|
285
|
+
].join('\n'),
|
|
286
|
+
);
|
|
287
|
+
const m = validateManifest(dir, '12.0.0');
|
|
288
|
+
expect(m.author).toEqual({
|
|
289
|
+
uid: 'prs_abc123',
|
|
290
|
+
handle: 'corey',
|
|
291
|
+
displayName: 'Corey Epstein',
|
|
292
|
+
});
|
|
293
|
+
expect(m.capabilities).toEqual(['hooks', 'network']);
|
|
294
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('a malformed author (missing displayName) is rejected — not silently accepted', () => {
|
|
298
|
+
const dir = writeValidPack(['author:', ' uid: prs_abc123', ' handle: corey'].join('\n'));
|
|
299
|
+
expect(() => validateManifest(dir, '12.0.0')).toThrow(/author\.displayName/);
|
|
300
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('capabilities must be a list of strings', () => {
|
|
304
|
+
const dir = writeValidPack(['capabilities:', ' hooks: true'].join('\n'));
|
|
305
|
+
expect(() => validateManifest(dir, '12.0.0')).toThrow(/capabilities must be a list/);
|
|
306
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
225
310
|
it('stampInstallSource preserves a leading `---` document marker — no multi-doc YAML stream', () => {
|
|
226
311
|
// Regression: prepending `source:` before a `---` would split the file
|
|
227
312
|
// into two YAML documents, and single-doc `yaml.load` callers downstream
|
|
@@ -260,3 +345,126 @@ describe('pack-install: install path layout', () => {
|
|
|
260
345
|
expect(rewritten.split('\n')[0]).toBe('---');
|
|
261
346
|
});
|
|
262
347
|
});
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// GET /v1/listings/{id} detail-envelope mapping (Bug 1: contract/envelope)
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
describe('pack-install: listing-detail envelope mapping (toMarketplaceListing)', () => {
|
|
354
|
+
it('maps from the `{ listing: {...} }` envelope (id + downloadUrl + contentHash)', () => {
|
|
355
|
+
// Prod shape: GET /v1/listings/{id} wraps the listing in an envelope and
|
|
356
|
+
// uses `id` (not `listingId`). Previously this threw "Listing detail
|
|
357
|
+
// missing an id." because the mapper read from the top level.
|
|
358
|
+
const listing = toMarketplaceListing({
|
|
359
|
+
listing: {
|
|
360
|
+
id: 'lst_abc123',
|
|
361
|
+
slug: 'hq-pack-test',
|
|
362
|
+
version: '1.2.3',
|
|
363
|
+
downloadUrl: 'https://s3.example/presigned',
|
|
364
|
+
contentHash: 'a'.repeat(64),
|
|
365
|
+
contentHashAlg: 'sha256',
|
|
366
|
+
signature: 'sig-base64',
|
|
367
|
+
signingKeyId: 'key-1',
|
|
368
|
+
publicKey: 'pub-pem',
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
expect(listing.listingId).toBe('lst_abc123');
|
|
372
|
+
expect(listing.downloadUrl).toBe('https://s3.example/presigned');
|
|
373
|
+
expect(listing.contentHash).toBe('a'.repeat(64));
|
|
374
|
+
expect(listing.contentHashAlg).toBe('sha256');
|
|
375
|
+
expect(listing.slug).toBe('hq-pack-test');
|
|
376
|
+
expect(listing.version).toBe('1.2.3');
|
|
377
|
+
expect(listing.signature).toBe('sig-base64');
|
|
378
|
+
expect(listing.signingKeyId).toBe('key-1');
|
|
379
|
+
expect(listing.publicKey).toBe('pub-pem');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('still maps the legacy FLAT shape at the top level (backwards-compat)', () => {
|
|
383
|
+
const listing = toMarketplaceListing({
|
|
384
|
+
listingId: 'lst_legacy',
|
|
385
|
+
slug: 'hq-pack-old',
|
|
386
|
+
version: '0.9.0',
|
|
387
|
+
downloadUrl: 'https://s3.example/legacy',
|
|
388
|
+
contentHash: 'b'.repeat(64),
|
|
389
|
+
});
|
|
390
|
+
expect(listing.listingId).toBe('lst_legacy');
|
|
391
|
+
expect(listing.downloadUrl).toBe('https://s3.example/legacy');
|
|
392
|
+
expect(listing.contentHash).toBe('b'.repeat(64));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('throws the missing-id error only when neither envelope nor flat carry an id', () => {
|
|
396
|
+
expect(() =>
|
|
397
|
+
toMarketplaceListing({ listing: { downloadUrl: 'x', contentHash: 'c'.repeat(64) } }),
|
|
398
|
+
).toThrow(/missing an id/);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('refuses (no content hash) when the unwrapped detail lacks contentHash', () => {
|
|
402
|
+
expect(() =>
|
|
403
|
+
toMarketplaceListing({
|
|
404
|
+
listing: { id: 'lst_nohash', downloadUrl: 'https://s3.example/x' },
|
|
405
|
+
}),
|
|
406
|
+
).toThrow(/no content hash/);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// fetchMarketplace integrity: envelope-mapped hash verifies / refuses
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
describe('pack-install: fetchMarketplace honors the envelope-mapped contentHash', () => {
|
|
415
|
+
function depsFor(
|
|
416
|
+
envelopeContentHash: string,
|
|
417
|
+
tarballBytes: Uint8Array,
|
|
418
|
+
): MarketplaceDeps {
|
|
419
|
+
// resolveListing returns the envelope-mapped detail; download returns the
|
|
420
|
+
// bytes. fetchMarketplace verifies the bytes against the pinned hash.
|
|
421
|
+
const listing = toMarketplaceListing({
|
|
422
|
+
listing: {
|
|
423
|
+
id: 'lst_int',
|
|
424
|
+
slug: 'hq-pack-int',
|
|
425
|
+
version: '1.0.0',
|
|
426
|
+
downloadUrl: 'https://s3.example/int',
|
|
427
|
+
contentHash: envelopeContentHash,
|
|
428
|
+
contentHashAlg: 'sha256',
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
resolveListing: async () => listing,
|
|
433
|
+
refreshListing: async () => listing,
|
|
434
|
+
download: async () => tarballBytes,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
it('a tampered/mismatched hash still refuses (ArtifactVerificationError)', async () => {
|
|
439
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
440
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-cli-int-'));
|
|
441
|
+
try {
|
|
442
|
+
await expect(
|
|
443
|
+
fetchMarketplace(
|
|
444
|
+
'marketplace:hq-pack-int@1.0.0',
|
|
445
|
+
tmpDir,
|
|
446
|
+
depsFor('f'.repeat(64), bytes), // wrong hash
|
|
447
|
+
),
|
|
448
|
+
).rejects.toBeInstanceOf(ArtifactVerificationError);
|
|
449
|
+
} finally {
|
|
450
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('the correct envelope-mapped hash passes verification (reaches extraction)', async () => {
|
|
455
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
456
|
+
const realHash = computeArtifactHash(bytes);
|
|
457
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-cli-int-'));
|
|
458
|
+
try {
|
|
459
|
+
// The 4 junk bytes are not a real gzip tarball, so extraction fails AFTER
|
|
460
|
+
// verification — proving the hash check passed (it did NOT throw the
|
|
461
|
+
// verification error first). A verification failure would reject with
|
|
462
|
+
// ArtifactVerificationError instead.
|
|
463
|
+
await expect(
|
|
464
|
+
fetchMarketplace('marketplace:hq-pack-int@1.0.0', tmpDir, depsFor(realHash, bytes)),
|
|
465
|
+
).rejects.not.toBeInstanceOf(ArtifactVerificationError);
|
|
466
|
+
} finally {
|
|
467
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
});
|