@indigoai-us/hq-cli 5.33.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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Deterministic, platform-independent malicious-tar fixture builder.
3
+ *
4
+ * WHY THIS EXISTS (cross-platform zip-slip coverage). The adversarial
5
+ * safe-extract / marketplace-security suites need archives that GENUINELY carry
6
+ * hostile entries — a `../` traversal name, an absolute path, an escaping
7
+ * symlink/hardlink. The obvious approach (shell out to the system `tar` to
8
+ * CREATE the archive) is NOT portable: GNU tar (Linux / CI / prod Lambdas)
9
+ * STRIPS the leading `../` and absolute `/` when it WRITES an archive, so the
10
+ * resulting "malicious" fixture contains a benign `evil-escape` entry and the
11
+ * guard has nothing to reject — the test passes on macOS bsdtar (which
12
+ * preserves them) but fails on Linux. The guard itself is correct; only the
13
+ * fixture construction was platform-dependent.
14
+ *
15
+ * Fix: WRITE THE TAR BYTES DIRECTLY here. A tar archive is just a sequence of
16
+ * 512-byte ustar headers, each optionally followed by the file's content padded
17
+ * to a 512-byte boundary, terminated by two zero blocks. By emitting the bytes
18
+ * ourselves we control the recorded entry names verbatim — `../evil-escape`,
19
+ * `/tmp/evil`, a symlink with linkname `../../etc/passwd` — identically on every
20
+ * platform, with no create-time stripping. The reading side (`tar -tvf`, which
21
+ * safeExtractTarball's pre-flight uses) does NOT strip anything; it faithfully
22
+ * lists whatever bytes we wrote, so the guard sees the real attack everywhere.
23
+ */
24
+ /** ustar typeflag values we use. */
25
+ export type TarType = 'file' | 'symlink' | 'hardlink';
26
+ export interface TarEntrySpec {
27
+ /** Recorded entry name — written VERBATIM (may contain `..`, be absolute…). */
28
+ name: string;
29
+ type?: TarType;
30
+ /** For sym/hardlinks: the recorded link target (also written verbatim). */
31
+ linkname?: string;
32
+ /** File content (regular files only). */
33
+ content?: string | Buffer;
34
+ }
35
+ /**
36
+ * Build one 512-byte ustar header (plus content blocks for regular files) for a
37
+ * single entry. The header checksum is computed exactly per spec: sum every
38
+ * header byte treating the 8 checksum bytes themselves as ASCII spaces, then
39
+ * write that sum as 6 octal digits + NUL + space.
40
+ */
41
+ export declare function makeTarEntry(spec: TarEntrySpec): Buffer;
42
+ /** Concatenate entries and append the two zero blocks that end every tar. */
43
+ export declare function makeTar(entries: TarEntrySpec[]): Buffer;
44
+ /** Build the tar, gzip it, and write it to `outPath`. Returns `outPath`. */
45
+ export declare function writeMaliciousTarGz(outPath: string, entries: TarEntrySpec[]): string;
46
+ //# sourceMappingURL=make-tar.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Deterministic, platform-independent malicious-tar fixture builder.
3
+ *
4
+ * WHY THIS EXISTS (cross-platform zip-slip coverage). The adversarial
5
+ * safe-extract / marketplace-security suites need archives that GENUINELY carry
6
+ * hostile entries — a `../` traversal name, an absolute path, an escaping
7
+ * symlink/hardlink. The obvious approach (shell out to the system `tar` to
8
+ * CREATE the archive) is NOT portable: GNU tar (Linux / CI / prod Lambdas)
9
+ * STRIPS the leading `../` and absolute `/` when it WRITES an archive, so the
10
+ * resulting "malicious" fixture contains a benign `evil-escape` entry and the
11
+ * guard has nothing to reject — the test passes on macOS bsdtar (which
12
+ * preserves them) but fails on Linux. The guard itself is correct; only the
13
+ * fixture construction was platform-dependent.
14
+ *
15
+ * Fix: WRITE THE TAR BYTES DIRECTLY here. A tar archive is just a sequence of
16
+ * 512-byte ustar headers, each optionally followed by the file's content padded
17
+ * to a 512-byte boundary, terminated by two zero blocks. By emitting the bytes
18
+ * ourselves we control the recorded entry names verbatim — `../evil-escape`,
19
+ * `/tmp/evil`, a symlink with linkname `../../etc/passwd` — identically on every
20
+ * platform, with no create-time stripping. The reading side (`tar -tvf`, which
21
+ * safeExtractTarball's pre-flight uses) does NOT strip anything; it faithfully
22
+ * lists whatever bytes we wrote, so the guard sees the real attack everywhere.
23
+ */
24
+
25
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3b6b9745-1cda-547f-a99d-34b70ea052c8")}catch(e){}}();
26
+ import { gzipSync } from 'node:zlib';
27
+ import * as fs from 'node:fs';
28
+ const BLOCK = 512;
29
+ const TYPEFLAG = {
30
+ file: '0', // regular file
31
+ symlink: '2', // symbolic link
32
+ hardlink: '1', // hard link
33
+ };
34
+ /** Write an ASCII string into a fixed-width field at `offset`, NUL-padded. */
35
+ function writeField(buf, offset, width, value) {
36
+ // Truncate defensively; ustar fields are fixed-width.
37
+ const s = value.slice(0, width);
38
+ buf.write(s, offset, 'ascii');
39
+ // Remaining bytes are already 0 from Buffer.alloc.
40
+ }
41
+ /** Write an octal numeric field: `width-1` octal digits, space-or-NUL terminated. */
42
+ function writeOctal(buf, offset, width, value) {
43
+ // Classic ustar numeric field: zero-padded octal in (width-1) chars + NUL.
44
+ const digits = width - 1;
45
+ const oct = value.toString(8).padStart(digits, '0').slice(-digits);
46
+ buf.write(oct, offset, 'ascii');
47
+ buf[offset + digits] = 0; // NUL terminator
48
+ }
49
+ /**
50
+ * Build one 512-byte ustar header (plus content blocks for regular files) for a
51
+ * single entry. The header checksum is computed exactly per spec: sum every
52
+ * header byte treating the 8 checksum bytes themselves as ASCII spaces, then
53
+ * write that sum as 6 octal digits + NUL + space.
54
+ */
55
+ export function makeTarEntry(spec) {
56
+ const type = spec.type ?? 'file';
57
+ const content = type === 'file'
58
+ ? Buffer.isBuffer(spec.content)
59
+ ? spec.content
60
+ : Buffer.from(spec.content ?? '', 'utf-8')
61
+ : Buffer.alloc(0); // links carry no content payload
62
+ const header = Buffer.alloc(BLOCK); // zero-filled
63
+ writeField(header, 0, 100, spec.name); // name
64
+ writeOctal(header, 100, 8, 0o644); // mode
65
+ writeOctal(header, 108, 8, 0); // uid
66
+ writeOctal(header, 116, 8, 0); // gid
67
+ writeOctal(header, 124, 12, content.length); // size (0 for links)
68
+ writeOctal(header, 136, 12, 0); // mtime (deterministic: epoch)
69
+ // checksum field (148, 8) — filled below; start as spaces for the sum.
70
+ header.fill(' '.charCodeAt(0), 148, 156);
71
+ writeField(header, 156, 1, TYPEFLAG[type]); // typeflag
72
+ if (spec.linkname !== undefined) {
73
+ writeField(header, 157, 100, spec.linkname); // linkname (verbatim)
74
+ }
75
+ writeField(header, 257, 6, 'ustar'); // magic "ustar\0"
76
+ writeField(header, 263, 2, '00'); // version "00"
77
+ // uname/gname left empty; devmajor/devminor zero (already NUL).
78
+ // Header checksum: unsigned sum of all 512 bytes (with the checksum field as
79
+ // spaces, which we set above). Written as 6 octal digits, NUL, space.
80
+ let sum = 0;
81
+ for (let i = 0; i < BLOCK; i++)
82
+ sum += header[i];
83
+ const cksum = sum.toString(8).padStart(6, '0').slice(-6);
84
+ header.write(cksum, 148, 'ascii');
85
+ header[154] = 0; // NUL
86
+ header[155] = ' '.charCodeAt(0); // space
87
+ if (type !== 'file' || content.length === 0)
88
+ return header;
89
+ // Content padded up to a 512-byte boundary.
90
+ const pad = (BLOCK - (content.length % BLOCK)) % BLOCK;
91
+ return Buffer.concat([header, content, Buffer.alloc(pad)]);
92
+ }
93
+ /** Concatenate entries and append the two zero blocks that end every tar. */
94
+ export function makeTar(entries) {
95
+ const blocks = entries.map(makeTarEntry);
96
+ blocks.push(Buffer.alloc(BLOCK * 2)); // end-of-archive marker
97
+ return Buffer.concat(blocks);
98
+ }
99
+ /** Build the tar, gzip it, and write it to `outPath`. Returns `outPath`. */
100
+ export function writeMaliciousTarGz(outPath, entries) {
101
+ fs.writeFileSync(outPath, gzipSync(makeTar(entries)));
102
+ return outPath;
103
+ }
104
+ //# sourceMappingURL=make-tar.js.map
105
+ //# debugId=3b6b9745-1cda-547f-a99d-34b70ea052c8
@@ -33,8 +33,11 @@
33
33
  * registry file under the v12 layout. (`hq update <pack>` re-resolves source
34
34
  * from each pack's package.yaml; rationale lives in the layout-fix PR.)
35
35
  */
36
+ import { type KeyObject } from 'node:crypto';
36
37
  import type { PackManifest } from '../types.js';
37
- export type Transport = 'npm' | 'git' | 'local';
38
+ export type Transport = 'npm' | 'git' | 'local' | 'marketplace';
39
+ /** Prefix that routes a source through the HQ marketplace transport (US-006). */
40
+ export declare const MARKETPLACE_PREFIX = "marketplace:";
38
41
  export declare function classify(source: string): Transport;
39
42
  /**
40
43
  * Parse a git source's '#<...>' fragment into { url, subpath, ref }.
@@ -52,6 +55,112 @@ export declare function parseGitFragment(source: string): {
52
55
  * back to the legacy registry flow.
53
56
  */
54
57
  export declare function sourceMatchesPackPattern(source: string): boolean;
58
+ interface FetchResult {
59
+ payloadDir: string;
60
+ resolvedSource: string;
61
+ resolvedSha?: string;
62
+ }
63
+ /**
64
+ * Optional marketplace integrity metadata for a tarball-based install (US-021).
65
+ * When present, `verifyArtifact` runs over the downloaded tarball bytes BEFORE
66
+ * `safeExtractTarball` — a hash/signature mismatch aborts the install before
67
+ * any byte is unpacked. US-006 populates this from the listing detail; the npm
68
+ * fetch path also honors it so a registry-fetched tarball can be verified.
69
+ */
70
+ export interface ArtifactIntegrity {
71
+ expectedHash: string;
72
+ signature?: string;
73
+ publicKey?: string;
74
+ requireSignature?: boolean;
75
+ }
76
+ /** Parsed `marketplace:<slug>[@version]` source. */
77
+ export declare function parseMarketplaceSource(source: string): {
78
+ slug: string;
79
+ version?: string;
80
+ };
81
+ /**
82
+ * A resolved marketplace listing detail — the public `GET /v1/listings/{id}`
83
+ * shape (US-005) reduced to the fields the installer needs. The presigned
84
+ * `downloadUrl` is short-lived; `listingId` lets us re-resolve a fresh one if
85
+ * it expires mid-download.
86
+ */
87
+ export interface MarketplaceListing {
88
+ listingId: string;
89
+ slug: string;
90
+ version?: string;
91
+ /** Short-lived presigned S3 URL for the tarball. */
92
+ downloadUrl: string;
93
+ /** Lowercase-hex sha256 the listing pinned + approval is bound to (US-021). */
94
+ contentHash: string;
95
+ /** Hash algorithm the server reported (e.g. `sha256`), when present. */
96
+ contentHashAlg?: string;
97
+ /** Base64 Ed25519 signature over the contentHash (optional during key-defer). */
98
+ signature?: string;
99
+ /** Identifier of the signing key the server used, when present. */
100
+ signingKeyId?: string;
101
+ /** Platform public key (PEM/SPKI) to verify the signature against. */
102
+ publicKey?: string;
103
+ }
104
+ /**
105
+ * Network seams for the marketplace transport, dependency-injected so unit
106
+ * tests can mock resolve + download without real HTTP/S3. Production wiring
107
+ * lives in `defaultMarketplaceDeps`.
108
+ */
109
+ export interface MarketplaceDeps {
110
+ /**
111
+ * Resolve `slug[@version]` to a listing detail (id + presigned URL + the
112
+ * approved hash/signature/publicKey). Throws if no approved listing matches.
113
+ */
114
+ resolveListing: (slug: string, version?: string) => Promise<MarketplaceListing>;
115
+ /** Re-fetch the listing detail by id to mint a FRESH presigned URL. */
116
+ refreshListing: (listingId: string) => Promise<MarketplaceListing>;
117
+ /** Download the tarball bytes from a presigned URL. Returns null on 403/expired. */
118
+ download: (url: string) => Promise<Uint8Array | {
119
+ expired: true;
120
+ }>;
121
+ }
122
+ interface RawListingSummary {
123
+ listingId?: string;
124
+ id?: string;
125
+ slug?: string;
126
+ version?: string;
127
+ latestVersion?: string;
128
+ status?: string;
129
+ }
130
+ interface RawListingDetailFields extends RawListingSummary {
131
+ downloadUrl?: string;
132
+ url?: string;
133
+ contentHash?: string;
134
+ contentHashAlg?: string;
135
+ sha256?: string;
136
+ signature?: string;
137
+ signingKeyId?: string;
138
+ publicKey?: string;
139
+ }
140
+ /**
141
+ * `GET /v1/listings/{id}` returns the listing WRAPPED in an envelope
142
+ * (`{ listing: { id, downloadUrl, contentHash, ... } }`). Older/back-compat
143
+ * responses returned the listing fields at the top level, so we accept both.
144
+ */
145
+ interface RawListingDetail extends RawListingDetailFields {
146
+ listing?: RawListingDetailFields;
147
+ }
148
+ /** Map a raw `GET /v1/listings/{id}` body into a MarketplaceListing. */
149
+ export declare function toMarketplaceListing(raw: RawListingDetail): MarketplaceListing;
150
+ /** Default production marketplace deps — public listings API + presigned S3. */
151
+ export declare function defaultMarketplaceDeps(): MarketplaceDeps;
152
+ /**
153
+ * Fetch + verify + safe-extract a marketplace pack. Mirrors `fetchNpm`'s
154
+ * contract (returns a payloadDir the caller mvs into core/packages/), but
155
+ * resolves the tarball from the marketplace and ALWAYS verifies it against the
156
+ * listing's approved hash/signature BEFORE extraction (US-021 → US-020).
157
+ *
158
+ * @param requireSignature production posture once signing keys exist; during
159
+ * the key-deferral window callers pass false (hash still always checked).
160
+ */
161
+ export declare function fetchMarketplace(source: string, tmpDir: string, deps: MarketplaceDeps, opts?: {
162
+ requireSignature?: boolean;
163
+ }): Promise<FetchResult>;
55
164
  export interface LatestResult {
56
165
  transport: Transport;
57
166
  /** Identifier of the currently-installed pack (sha for git, version for npm). */
@@ -72,6 +181,63 @@ export interface LatestResult {
72
181
  * @param installedVersion the installed pack's manifest `version` (npm compare)
73
182
  */
74
183
  export declare function resolveLatest(source: string, installedVersion?: string): LatestResult;
184
+ /**
185
+ * Async marketplace update probe (US-006): resolve the slug's latest approved
186
+ * listing version and compare it to the installed version. Reuses the same
187
+ * `MarketplaceDeps.resolveListing` seam as the install path (resolving with NO
188
+ * version pin returns the latest listing). Never throws — network/parse
189
+ * failures return `{ updateAvailable: null, error }` so `hq packs update`
190
+ * stays resilient.
191
+ *
192
+ * @param source the stamped `marketplace:<slug>[@version]` source
193
+ * @param installedVersion the installed pack's manifest version (semver compare)
194
+ * @param deps injected for tests; defaults to the public API
195
+ */
196
+ export declare function resolveLatestMarketplace(source: string, installedVersion?: string, deps?: MarketplaceDeps): Promise<LatestResult>;
197
+ /** Thrown when a downloaded artifact fails hash or signature verification. */
198
+ export declare class ArtifactVerificationError extends Error {
199
+ constructor(message: string);
200
+ }
201
+ /** Hash algorithm for the content hash — pinned to sha256 (matches publish). */
202
+ export declare const ARTIFACT_HASH_ALG: "sha256";
203
+ export interface VerifyArtifactInput {
204
+ /** The downloaded gzipped tarball bytes (canonical bytes). */
205
+ tarballBytes: Uint8Array;
206
+ /** Lowercase-hex sha256 the listing pinned at publish (approval bound to it). */
207
+ expectedHash: string;
208
+ /**
209
+ * Base64 Ed25519 signature over the lowercase-hex `expectedHash`. Optional:
210
+ * when the listing carries no signature (e.g. published before signing-key
211
+ * provisioning), hash verification still runs but signature verification is
212
+ * skipped UNLESS `requireSignature` is set.
213
+ */
214
+ signature?: string;
215
+ /**
216
+ * The platform PUBLIC key (Ed25519) as a PEM/SPKI string or a pre-parsed
217
+ * KeyObject. Required to verify a signature. The public key is distributable
218
+ * (embedded in / fetched by the installer).
219
+ */
220
+ publicKey?: string | KeyObject;
221
+ /**
222
+ * When true, a missing signature OR missing public key is a hard failure
223
+ * (production posture once signing keys are provisioned). Default false so an
224
+ * unsigned listing still hash-verifies during the deferred-key window.
225
+ */
226
+ requireSignature?: boolean;
227
+ }
228
+ /** Lowercase-hex sha256 of the given bytes — the install-side content hash. */
229
+ export declare function computeArtifactHash(tarballBytes: Uint8Array): string;
230
+ /**
231
+ * Verify a downloaded marketplace artifact's integrity (hash) and authenticity
232
+ * (signature). Throws `ArtifactVerificationError` on ANY mismatch — the caller
233
+ * MUST let it propagate so the install aborts before extraction.
234
+ *
235
+ * Constant-time-ish hash comparison: hashes are fixed-length lowercase hex, and
236
+ * the early-exit risk is negligible (the hash is public), but we still compare
237
+ * full strings rather than prefixes.
238
+ */
239
+ export declare function verifyArtifact(input: VerifyArtifactInput): void;
240
+ export declare function validateManifest(payloadDir: string, hqVersion: string | null): PackManifest;
75
241
  /**
76
242
  * Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
77
243
  * v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
@@ -136,6 +302,26 @@ export interface InstallPackOptions {
136
302
  * `rsync -a`, `git clone` progress -> stderr), so this is sufficient.
137
303
  */
138
304
  quiet?: boolean;
305
+ /**
306
+ * US-021 marketplace artifact integrity. When provided (a marketplace listing
307
+ * carries a pinned content hash + signature + public key), the downloaded
308
+ * tarball is verified BEFORE extraction; a mismatch aborts the install. US-006
309
+ * populates this from the listing detail response.
310
+ */
311
+ integrity?: ArtifactIntegrity;
312
+ /**
313
+ * US-006 marketplace transport seams (resolve/refresh/download), injected for
314
+ * tests. When omitted, `defaultMarketplaceDeps()` (public listings API +
315
+ * presigned S3) is used. Only consulted when `source` is `marketplace:...`.
316
+ */
317
+ marketplaceDeps?: MarketplaceDeps;
318
+ /**
319
+ * US-006: require a valid signature for marketplace artifacts. Default false
320
+ * during the signing-key-deferral window (the content hash is still ALWAYS
321
+ * verified); set true once platform signing keys are provisioned.
322
+ */
323
+ requireSignature?: boolean;
139
324
  }
140
325
  export declare function installPack(source: string, opts?: InstallPackOptions): Promise<void>;
326
+ export {};
141
327
  //# sourceMappingURL=pack-install.d.ts.map