@ggui-ai/gadget-signing 0.6.3 → 0.8.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/dist/index.d.ts CHANGED
@@ -110,8 +110,11 @@ export declare function isGadgetSignature(value: unknown): value is GadgetSignat
110
110
  export declare function canonicalJson(value: unknown): string;
111
111
  /**
112
112
  * Derive a stable public-key identifier from a 32-byte Ed25519 public key.
113
- * `base64(sha256(publicKey))[:16]`. Used as the registry's stable handle
114
- * for a stored author public key.
113
+ * `base64url(sha256(publicKey))[:16]` — RFC 4648 §5 URL-safe alphabet
114
+ * (`-`/`_`, no padding), because keyIds travel as URL path segments
115
+ * (`DELETE /author-keys/{keyId}`) and as storage row-key components;
116
+ * standard base64 would put `/` in ~22% of ids. Used as the registry's
117
+ * stable handle for a stored author public key.
115
118
  */
116
119
  export declare function derivePublicKeyId(publicKey: Uint8Array): string;
117
120
  /**
@@ -248,14 +251,41 @@ export interface VerifyBundleSigstoreInput {
248
251
  readonly issuer?: string;
249
252
  };
250
253
  /**
251
- * Optional endpoint overrides — only meaningful when paired with a
252
- * non-prod TUF mirror. Production gadgets verify against the prod
253
- * Sigstore TUF root.
254
+ * TUF mirror serving the trust-root metadata + targets. When unset,
255
+ * the upstream verifier resolves against the Sigstore public-good
256
+ * mirror. Point this (together with {@link tufRootPath}) at an
257
+ * alternative TUF repository for private sigstore deployments and
258
+ * hermetic tests — verification never contacts Fulcio/Rekor
259
+ * directly, so the TUF trust root is THE knob that selects which
260
+ * signing infrastructure a verifier trusts.
254
261
  */
255
- readonly endpoints?: {
256
- readonly fulcioURL?: string;
257
- readonly rekorURL?: string;
258
- };
262
+ readonly tufMirrorURL?: string;
263
+ /**
264
+ * Path to the initial TUF `root.json` that anchors trust in
265
+ * {@link tufMirrorURL}. Required for any non-public-good mirror
266
+ * (the upstream client only ships seeds for the public-good
267
+ * repository); ignored once a cached root exists under
268
+ * {@link tufCachePath}.
269
+ */
270
+ readonly tufRootPath?: string;
271
+ /**
272
+ * Writable cache directory for the sigstore TUF trust root. The
273
+ * upstream verifier refreshes TUF metadata into this directory
274
+ * before verifying; when unset it falls back to the platform
275
+ * app-data directory (derived from `$XDG_DATA_HOME` / `$HOME`),
276
+ * which read-only filesystems reject at `mkdir` time — serverless
277
+ * runtimes typically only allow writes under `/tmp`, so point this
278
+ * at e.g. `/tmp/sigstore-js` there.
279
+ */
280
+ readonly tufCachePath?: string;
281
+ /**
282
+ * Reuse cached TUF metadata without a remote refresh while the
283
+ * cached copy is still valid. Keeps the trust-root network
284
+ * round-trip off warm verification paths; a missing, expired, or
285
+ * corrupt cache falls back to a normal remote refresh, so this is
286
+ * a latency knob — never a correctness one.
287
+ */
288
+ readonly tufForceCache?: boolean;
259
289
  }
260
290
  /**
261
291
  * Verify a sigstore-signed gadget bundle.
@@ -282,10 +312,19 @@ export interface VerifyBundleSigstoreInput {
282
312
  export declare function verifyBundleSigstore(input: VerifyBundleSigstoreInput): Promise<VerifyResult>;
283
313
  /**
284
314
  * Extract the Fulcio leaf cert's base64 raw bytes from a
285
- * {@link SigstoreSignature}'s serialized cosign bundle (per
286
- * `@sigstore/bundle` v0.3, `verificationMaterial.x509CertificateChain
287
- * .certificates[0].rawBytes`). Returns `undefined` if the bundle is
288
- * malformed or missing the cert chain.
315
+ * {@link SigstoreSignature}'s serialized cosign bundle. Two
316
+ * `verificationMaterial` shapes exist on the wire:
317
+ *
318
+ * - `certificate.rawBytes` — the single-leaf shape bundle v0.3
319
+ * emits. What real `sigstore.sign()` produces today (surfaced by
320
+ * this path's first real signing run, 2026-08-10 — the extractor
321
+ * previously only knew the chain shape and returned `undefined`
322
+ * for every genuinely-signed bundle).
323
+ * - `x509CertificateChain.certificates[0].rawBytes` — the v0.1/v0.2
324
+ * chain shape, still valid on the wire for externally-produced
325
+ * bundles.
326
+ *
327
+ * Returns `undefined` if the bundle is malformed or carries neither.
289
328
  *
290
329
  * Lives here — next to {@link verifyBundleSigstore} — so cosign
291
330
  * bundle-format knowledge has ONE home. Registries persist the
@@ -299,4 +338,29 @@ export declare function verifyBundleSigstore(input: VerifyBundleSigstoreInput):
299
338
  * a verification.
300
339
  */
301
340
  export declare function extractSigstoreLeafCertPem(signature: SigstoreSignature): string | undefined;
341
+ /**
342
+ * Extract EVERY signer identity — the embedded Fulcio cert's URI and
343
+ * rfc822 (email) `subjectAlternativeName` values — from a
344
+ * {@link SigstoreSignature}'s serialized cosign bundle. Returns an
345
+ * EMPTY array when the bundle is malformed, carries no certificate,
346
+ * or the cert has no URI/email SAN.
347
+ *
348
+ * ALL SANs, deliberately: a certificate can carry BOTH a URI and an
349
+ * email SAN, and a policy that inspected only the first would
350
+ * dead-end on such certs. Callers match their policy against every
351
+ * returned identity and accept on ANY hit.
352
+ *
353
+ * This is a PROJECTION, not a verification: callers that make an
354
+ * authorization decision on the returned identities (e.g. a
355
+ * registry's publish-time identity binding) MUST pair it with
356
+ * {@link verifyBundleSigstore} over the same signature — verification
357
+ * is what proves the SAN-bearing cert is genuinely CA-issued and tied
358
+ * to the signed bytes. Parsing delegates to the same X.509 parser the
359
+ * verifier uses (see {@link extractSANsFromBundle}), so the identities
360
+ * a policy check sees can never drift from what verification enforces.
361
+ *
362
+ * Lives here — next to {@link extractSigstoreLeafCertPem} — so cosign
363
+ * bundle-format knowledge has ONE home.
364
+ */
365
+ export declare function extractSigstoreSANs(signature: SigstoreSignature): readonly string[];
302
366
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAgBH,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,iDAAiD;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,mDAAmD;AACnD,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAWnE;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAc5E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAS9E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAE1E;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAqBD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAQ/D;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,UAAU,CAAC,CAOrB;AAMD;;;;;GAKG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC;IACtD,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAQD;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE;IAC7C,WAAW,EAAE,UAAU,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB5B;AAED,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GACpB;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,WAAW,EAAE,UAAU,CAAC;IACxB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,SAAS,EAAE,UAAU,CAAC;CACvB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmDxB;AAiBD,sEAAsE;AACtE,MAAM,MAAM,wBAAwB,GAChC,cAAc,GACd,cAAc,GACd,aAAa,GACb,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,SAAkB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEtB,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM7E;AAED,2CAA2C;AAC3C,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,iBAAiB,CAAC,CAwB5B;AAkDD,6CAA6C;AAC7C,MAAM,WAAW,yBAAyB;IACxC,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE;QAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;QAClC,4EAA4E;QAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,YAAY,CAAC,CAyGvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,iBAAiB,GAC3B,MAAM,GAAG,SAAS,CA8BpB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,iBAAiB,GAC3B,SAAS,MAAM,EAAE,CASnB"}
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@
31
31
  import { signAsync, verifyAsync, getPublicKeyAsync, utils } from "@noble/ed25519";
32
32
  import { sha384, sha256 } from "@noble/hashes/sha2";
33
33
  import { bundleFromJSON, bundleToJSON, } from "@sigstore/bundle";
34
+ import { X509Certificate } from "@sigstore/core";
34
35
  import * as sigstoreClient from "sigstore";
35
36
  // ---------------------------------------------------------------------------
36
37
  // Canonical type guards.
@@ -91,6 +92,18 @@ function bytesToBase64(bytes) {
91
92
  // `btoa` exists in browsers and modern Node (>=16) globals.
92
93
  return btoa(binary);
93
94
  }
95
+ /**
96
+ * base64url (RFC 4648 §5, unpadded) — the URL- and filename-safe
97
+ * alphabet. Used for identifiers that travel as URL path segments or
98
+ * become row-key/filename components ({@link derivePublicKeyId});
99
+ * signatures and digests stay standard base64 (JSON-body-only).
100
+ */
101
+ function bytesToBase64Url(bytes) {
102
+ return bytesToBase64(bytes)
103
+ .replaceAll("+", "-")
104
+ .replaceAll("/", "_")
105
+ .replace(/=+$/, "");
106
+ }
94
107
  function base64ToBytes(b64) {
95
108
  const binary = atob(b64);
96
109
  const out = new Uint8Array(binary.length);
@@ -141,15 +154,18 @@ function canonicalSort(value) {
141
154
  // ---------------------------------------------------------------------------
142
155
  /**
143
156
  * Derive a stable public-key identifier from a 32-byte Ed25519 public key.
144
- * `base64(sha256(publicKey))[:16]`. Used as the registry's stable handle
145
- * for a stored author public key.
157
+ * `base64url(sha256(publicKey))[:16]` — RFC 4648 §5 URL-safe alphabet
158
+ * (`-`/`_`, no padding), because keyIds travel as URL path segments
159
+ * (`DELETE /author-keys/{keyId}`) and as storage row-key components;
160
+ * standard base64 would put `/` in ~22% of ids. Used as the registry's
161
+ * stable handle for a stored author public key.
146
162
  */
147
163
  export function derivePublicKeyId(publicKey) {
148
164
  if (publicKey.length !== 32) {
149
165
  throw new Error(`derivePublicKeyId: expected 32-byte Ed25519 public key, got ${publicKey.length}`);
150
166
  }
151
167
  const digest = sha256(publicKey);
152
- return bytesToBase64(digest).slice(0, 16);
168
+ return bytesToBase64Url(digest).slice(0, 16);
153
169
  }
154
170
  /**
155
171
  * Derive the Ed25519 public key from a 32-byte private key. Deterministic
@@ -372,7 +388,7 @@ function classifySigstoreSigningError(err) {
372
388
  * `signature.bundle` JSON).
373
389
  */
374
390
  export async function verifyBundleSigstore(input) {
375
- const { bundleBytes, signature, expectedIdentity } = input;
391
+ const { bundleBytes, signature, expectedIdentity, tufMirrorURL, tufRootPath, tufCachePath, tufForceCache, } = input;
376
392
  // 1. Fast tamper check.
377
393
  const recomputed = sha384(bundleBytes);
378
394
  const recomputedB64 = bytesToBase64(recomputed);
@@ -406,25 +422,40 @@ export async function verifyBundleSigstore(input) {
406
422
  }
407
423
  // 3. RegExp identity pre-check. Upstream `sigstore.verify` accepts
408
424
  // only literal-equality identity strings, so a RegExp expectation
409
- // must be enforced here against the bundle's embedded SAN.
425
+ // must be enforced here against the bundle's embedded SANs — ANY
426
+ // matching SAN satisfies the pattern (a cert can carry both a URI
427
+ // and an email SAN).
410
428
  if (expectedIdentity && expectedIdentity.subject instanceof RegExp) {
411
- const san = extractSANFromBundle(parsedBundle);
412
- if (san === undefined) {
429
+ const sans = extractSANsFromBundle(parsedBundle);
430
+ if (sans.length === 0) {
413
431
  return {
414
432
  valid: false,
415
433
  reason: "expectedIdentity.subject is RegExp but bundle has no subjectAlternativeName to match against",
416
434
  };
417
435
  }
418
- if (!expectedIdentity.subject.test(san)) {
436
+ const pattern = expectedIdentity.subject;
437
+ if (!sans.some((san) => pattern.test(san))) {
419
438
  return {
420
439
  valid: false,
421
- reason: `identity mismatch: bundle SAN '${san}' does not match expected pattern ${expectedIdentity.subject}`,
440
+ reason: `identity mismatch: no bundle SAN (${sans.map((s) => `'${s}'`).join(', ')}) matches expected pattern ${pattern}`,
422
441
  };
423
442
  }
424
443
  }
425
444
  // 4. Run the full upstream verify.
426
445
  try {
427
446
  const verifyOpts = {};
447
+ if (tufMirrorURL !== undefined) {
448
+ verifyOpts.tufMirrorURL = tufMirrorURL;
449
+ }
450
+ if (tufRootPath !== undefined) {
451
+ verifyOpts.tufRootPath = tufRootPath;
452
+ }
453
+ if (tufCachePath !== undefined) {
454
+ verifyOpts.tufCachePath = tufCachePath;
455
+ }
456
+ if (tufForceCache !== undefined) {
457
+ verifyOpts.tufForceCache = tufForceCache;
458
+ }
428
459
  if (expectedIdentity) {
429
460
  if (typeof expectedIdentity.subject === "string") {
430
461
  // Use email-shaped vs URI-shaped routing per upstream's two
@@ -455,10 +486,19 @@ export async function verifyBundleSigstore(input) {
455
486
  }
456
487
  /**
457
488
  * Extract the Fulcio leaf cert's base64 raw bytes from a
458
- * {@link SigstoreSignature}'s serialized cosign bundle (per
459
- * `@sigstore/bundle` v0.3, `verificationMaterial.x509CertificateChain
460
- * .certificates[0].rawBytes`). Returns `undefined` if the bundle is
461
- * malformed or missing the cert chain.
489
+ * {@link SigstoreSignature}'s serialized cosign bundle. Two
490
+ * `verificationMaterial` shapes exist on the wire:
491
+ *
492
+ * - `certificate.rawBytes` — the single-leaf shape bundle v0.3
493
+ * emits. What real `sigstore.sign()` produces today (surfaced by
494
+ * this path's first real signing run, 2026-08-10 — the extractor
495
+ * previously only knew the chain shape and returned `undefined`
496
+ * for every genuinely-signed bundle).
497
+ * - `x509CertificateChain.certificates[0].rawBytes` — the v0.1/v0.2
498
+ * chain shape, still valid on the wire for externally-produced
499
+ * bundles.
500
+ *
501
+ * Returns `undefined` if the bundle is malformed or carries neither.
462
502
  *
463
503
  * Lives here — next to {@link verifyBundleSigstore} — so cosign
464
504
  * bundle-format knowledge has ONE home. Registries persist the
@@ -486,6 +526,14 @@ export function extractSigstoreLeafCertPem(signature) {
486
526
  if (verificationMaterial === null || typeof verificationMaterial !== "object") {
487
527
  return undefined;
488
528
  }
529
+ // Bundle v0.3 single-leaf shape first — what real signing emits.
530
+ const certificate = verificationMaterial.certificate;
531
+ if (certificate !== null && typeof certificate === "object") {
532
+ const rawBytes = certificate.rawBytes;
533
+ if (typeof rawBytes === "string" && rawBytes.length > 0)
534
+ return rawBytes;
535
+ }
536
+ // v0.1/v0.2 chain shape — leaf first.
489
537
  const chain = verificationMaterial
490
538
  .x509CertificateChain;
491
539
  if (chain === null || typeof chain !== "object")
@@ -502,19 +550,62 @@ export function extractSigstoreLeafCertPem(signature) {
502
550
  return rawBytes;
503
551
  }
504
552
  /**
505
- * Reach into a serialized sigstore Bundle and pull the
506
- * `subjectAlternativeName` from the embedded X.509 cert (if any). Used
507
- * by the RegExp-identity pre-check; production verification still goes
553
+ * Extract EVERY signer identity — the embedded Fulcio cert's URI and
554
+ * rfc822 (email) `subjectAlternativeName` values — from a
555
+ * {@link SigstoreSignature}'s serialized cosign bundle. Returns an
556
+ * EMPTY array when the bundle is malformed, carries no certificate,
557
+ * or the cert has no URI/email SAN.
558
+ *
559
+ * ALL SANs, deliberately: a certificate can carry BOTH a URI and an
560
+ * email SAN, and a policy that inspected only the first would
561
+ * dead-end on such certs. Callers match their policy against every
562
+ * returned identity and accept on ANY hit.
563
+ *
564
+ * This is a PROJECTION, not a verification: callers that make an
565
+ * authorization decision on the returned identities (e.g. a
566
+ * registry's publish-time identity binding) MUST pair it with
567
+ * {@link verifyBundleSigstore} over the same signature — verification
568
+ * is what proves the SAN-bearing cert is genuinely CA-issued and tied
569
+ * to the signed bytes. Parsing delegates to the same X.509 parser the
570
+ * verifier uses (see {@link extractSANsFromBundle}), so the identities
571
+ * a policy check sees can never drift from what verification enforces.
572
+ *
573
+ * Lives here — next to {@link extractSigstoreLeafCertPem} — so cosign
574
+ * bundle-format knowledge has ONE home.
575
+ */
576
+ export function extractSigstoreSANs(signature) {
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(signature.bundle);
580
+ }
581
+ catch {
582
+ return [];
583
+ }
584
+ if (parsed === null || typeof parsed !== "object")
585
+ return [];
586
+ return extractSANsFromBundle(parsed);
587
+ }
588
+ /**
589
+ * Reach into a serialized sigstore Bundle and pull every URI/email
590
+ * `subjectAlternativeName` from the embedded X.509 cert (if any).
591
+ * Used by the RegExp-identity pre-check and
592
+ * {@link extractSigstoreSANs}; production verification still goes
508
593
  * through the upstream verifier for the actual cryptographic check.
509
594
  *
510
- * Returns `undefined` if the bundle has no cert (e.g. publicKey-only
511
- * bundle) or no extractable SAN. Uses lightweight base64-DER scanning —
512
- * defers to the upstream verifier for the trust-chain semantics.
595
+ * Parsing delegates to `@sigstore/core`'s `X509Certificate` — the
596
+ * SAME parser the upstream verifier uses — so the SANs this pre-check
597
+ * sees can never drift from the SANs verification enforces. (A prior
598
+ * hand-rolled DER scan over-captured on real certificates —
599
+ * discovered on the first genuinely Fulcio-issued cert it ever saw,
600
+ * 2026-08-10 — and was replaced wholesale rather than patched.)
601
+ *
602
+ * Returns `[]` if the bundle has no cert (e.g. publicKey-only
603
+ * bundle), the cert fails to parse, or it carries no URI/email SAN.
513
604
  */
514
- function extractSANFromBundle(bundle) {
605
+ function extractSANsFromBundle(bundle) {
515
606
  const material = bundle.verificationMaterial;
516
607
  if (!material)
517
- return undefined;
608
+ return [];
518
609
  let certB64;
519
610
  if ("certificate" in material && material.certificate) {
520
611
  certB64 = material.certificate.rawBytes;
@@ -525,60 +616,22 @@ function extractSANFromBundle(bundle) {
525
616
  certB64 = material.x509CertificateChain.certificates[0]?.rawBytes;
526
617
  }
527
618
  if (!certB64)
528
- return undefined;
529
- // Very lightweight SAN extraction: decode DER, locate the SAN
530
- // extension OID (2.5.29.17), and pull the first URI/email-shaped
531
- // ASCII run. This is intentionally tolerant — exact parsing happens
532
- // in `@sigstore/verify` downstream.
533
- let der;
619
+ return [];
534
620
  try {
535
- der = base64ToBytes(certB64);
621
+ const cert = X509Certificate.parse(Buffer.from(certB64, "base64"));
622
+ // Collect BOTH GeneralName kinds the identity policies use — URI
623
+ // first (matching upstream's preference order), then rfc822.
624
+ const ext = cert.extSubjectAltName;
625
+ if (ext === undefined)
626
+ return [];
627
+ const sans = [];
628
+ if (ext.uri !== undefined)
629
+ sans.push(ext.uri);
630
+ if (ext.rfc822Name !== undefined)
631
+ sans.push(ext.rfc822Name);
632
+ return sans;
536
633
  }
537
634
  catch {
538
- return undefined;
539
- }
540
- // OID 2.5.29.17 (subjectAltName) DER prefix: 06 03 55 1d 11.
541
- const sanOid = [0x06, 0x03, 0x55, 0x1d, 0x11];
542
- let idx = -1;
543
- for (let i = 0; i < der.length - sanOid.length; i++) {
544
- let match = true;
545
- for (let j = 0; j < sanOid.length; j++) {
546
- if (der[i + j] !== sanOid[j]) {
547
- match = false;
548
- break;
549
- }
550
- }
551
- if (match) {
552
- idx = i;
553
- break;
554
- }
555
- }
556
- if (idx === -1)
557
- return undefined;
558
- // After the OID DER there's a BOOLEAN (critical, optional) then an
559
- // OCTET STRING wrapping a SEQUENCE of GeneralName. Scan forward for
560
- // the first printable ASCII run containing a URI or email shape.
561
- const tail = der.subarray(idx + sanOid.length);
562
- let buf = "";
563
- const uriPattern = /[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]+|[\w.+-]+@[\w.-]+/;
564
- for (let i = 0; i < tail.length; i++) {
565
- const b = tail[i];
566
- if (b >= 0x20 && b <= 0x7e) {
567
- buf += String.fromCharCode(b);
568
- }
569
- else {
570
- if (buf.length >= 3) {
571
- const m = buf.match(uriPattern);
572
- if (m)
573
- return m[0];
574
- }
575
- buf = "";
576
- }
577
- }
578
- if (buf.length >= 3) {
579
- const m = buf.match(uriPattern);
580
- if (m)
581
- return m[0];
635
+ return [];
582
636
  }
583
- return undefined;
584
637
  }
@@ -0,0 +1,92 @@
1
+ import { initializeCA, initializeCTLog, initializeTLog } from '@sigstore/mock';
2
+ type CA = Awaited<ReturnType<typeof initializeCA>>;
3
+ type CTLog = Awaited<ReturnType<typeof initializeCTLog>>;
4
+ type TLog = Awaited<ReturnType<typeof initializeTLog>>;
5
+ /** Options for {@link startSigstoreMockStack}. */
6
+ export interface SigstoreMockStackOptions {
7
+ /**
8
+ * Mount the Fulcio signing-cert endpoint. Default `true`. Pass
9
+ * `false` to exercise CA-unreachable signing failure paths.
10
+ */
11
+ readonly fulcio?: boolean;
12
+ /**
13
+ * Mount the Rekor create-entry endpoint. Default `true`. Pass
14
+ * `false` to exercise transparency-log failure paths.
15
+ */
16
+ readonly rekor?: boolean;
17
+ }
18
+ /** Claims for {@link SigstoreMockStack.identityToken}. */
19
+ export interface MockIdentityClaims {
20
+ /**
21
+ * OIDC subject — becomes the issued certificate's SAN (as a URI
22
+ * GeneralName; see the module docstring's mock-fidelity note).
23
+ */
24
+ readonly sub?: string;
25
+ /** OIDC issuer claim — lands in the cert's issuer extension. */
26
+ readonly iss?: string;
27
+ /** Additional claims (email, GitHub Actions workflow claims, …). */
28
+ readonly [claim: string]: unknown;
29
+ }
30
+ /** Handle returned by {@link startSigstoreMockStack}. */
31
+ export interface SigstoreMockStack {
32
+ /** Base URL of the mock Fulcio instance. */
33
+ readonly fulcioURL: string;
34
+ /** Base URL of the mock Rekor instance. */
35
+ readonly rekorURL: string;
36
+ /**
37
+ * Endpoint overrides for `signBundleSigstore` — spread into its
38
+ * `endpoints` input.
39
+ */
40
+ readonly signEndpoints: {
41
+ readonly fulcioURL: string;
42
+ readonly rekorURL: string;
43
+ };
44
+ /**
45
+ * TUF trust-root overrides for `verifyBundleSigstore` — spread into
46
+ * its input (or a registry publish op's `sigstoreTuf` deps slot) so
47
+ * verification resolves the MOCK trust root instead of the
48
+ * public-good one.
49
+ *
50
+ * Includes `tufForceCache: true`, and load-bearingly so: the mock
51
+ * TUF mirror's nock interceptors are single-use, so exactly ONE
52
+ * remote metadata refresh succeeds per stack (discovered on this
53
+ * fixture's first multi-verify run — the second refresh died with
54
+ * `error refreshing TUF metadata`). The first verify populates the
55
+ * cache over the mock network; every later verify reuses it — which
56
+ * is also precisely the warm-start path serverless verifiers run in
57
+ * production, so the cache-reuse semantics get real coverage for
58
+ * free.
59
+ */
60
+ readonly tuf: {
61
+ readonly tufMirrorURL: string;
62
+ readonly tufRootPath: string;
63
+ readonly tufCachePath: string;
64
+ readonly tufForceCache: true;
65
+ };
66
+ /**
67
+ * Subject the zero-argument {@link identityToken} carries — handy
68
+ * for identity-policy assertions.
69
+ */
70
+ readonly defaultSubject: string;
71
+ /** Issuer the zero-argument {@link identityToken} carries. */
72
+ readonly defaultIssuer: string;
73
+ /**
74
+ * Mint an unsigned JWT-shaped OIDC token the mock Fulcio accepts.
75
+ * The mock decodes claims without verifying a signature.
76
+ */
77
+ identityToken(claims?: MockIdentityClaims): string;
78
+ /**
79
+ * Remove ALL nock interceptors registered in this process (including
80
+ * other live stacks — one stack per suite), restore real network
81
+ * access, and delete the TUF repo's cache directory.
82
+ */
83
+ teardown(): void;
84
+ }
85
+ /**
86
+ * Boot a full in-process sigstore mock stack: Fulcio + Rekor
87
+ * interceptors plus a TUF repository serving a trusted root built from
88
+ * the same keys. See the module docstring for the trust wiring.
89
+ */
90
+ export declare function startSigstoreMockStack(options?: SigstoreMockStackOptions): Promise<SigstoreMockStack>;
91
+ export type { CA as MockCA, CTLog as MockCTLog, TLog as MockTLog };
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AA2DA,OAAO,EAEL,YAAY,EACZ,eAAe,EACf,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAkBxB,KAAK,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;AACnD,KAAK,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC;AACzD,KAAK,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC;AAGvD,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE;QACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,GAAG,EAAE;QACZ,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC;KAC9B,CAAC;IACF;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,aAAa,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC;IACnD;;;;OAIG;IACH,QAAQ,IAAI,IAAI,CAAC;CAClB;AAaD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA6H5B;AAgDD,YAAY,EAAE,EAAE,IAAI,MAAM,EAAE,KAAK,IAAI,SAAS,EAAE,IAAI,IAAI,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Hermetic sigstore test infrastructure for `@ggui-ai/gadget-signing`
3
+ * consumers.
4
+ *
5
+ * `startSigstoreMockStack()` boots in-process mock Fulcio + Rekor HTTP
6
+ * endpoints (nock interceptors over `@sigstore/mock`'s CA / CTLog /
7
+ * TLog primitives) **plus** a mock TUF repository (via
8
+ * `@tufjs/repo-mock`) that serves a `trusted_root.json` built from the
9
+ * SAME key material the mock services sign with. The result: the real
10
+ * `signBundleSigstore` and `verifyBundleSigstore` code paths execute
11
+ * end-to-end — real ephemeral key generation, real cert issuance, real
12
+ * transparency-log entries, real bundle serialization, real TUF
13
+ * trust-root resolution, real cryptographic verification — with only
14
+ * the network endpoints and the root of trust substituted, all through
15
+ * the upstream `sigstore` package's public option surface
16
+ * (`fulcioURL` / `rekorURL` on sign; `tufMirrorURL` / `tufRootPath` /
17
+ * `tufCachePath` on verify). Nothing in the sign/verify pipeline is
18
+ * stubbed.
19
+ *
20
+ * Subpath export — consumers
21
+ * `import { startSigstoreMockStack } from '@ggui-ai/gadget-signing/testing'`.
22
+ * Requires `@sigstore/mock`, `@tufjs/repo-mock`, `nock`, and
23
+ * `@sigstore/protobuf-specs` at test-runtime (declared as optional
24
+ * peerDeps; add them to devDependencies alongside this package).
25
+ * `@tufjs/repo-mock` is pinned to exactly 4.0.1 on purpose: 4.0.2
26
+ * moves its nock dependency to v14, which would load a second,
27
+ * differently-patched `http` interceptor next to `@sigstore/mock`'s
28
+ * nock v13 in the same process. At 4.0.1 both resolve one shared nock.
29
+ *
30
+ * Known mock-fidelity limits (upstream `@sigstore/mock` behavior —
31
+ * document divergence from the public-good instance, don't paper over
32
+ * it):
33
+ *
34
+ * - The mock CA writes the certificate SAN as a **URI** GeneralName
35
+ * regardless of the subject's shape, so email-based identity
36
+ * policies (`certificateIdentityEmail`) can never match a
37
+ * mock-issued cert. Use URI-shaped subjects in identity-policy
38
+ * tests; the email routing itself is covered by the
39
+ * option-threading seam tests.
40
+ * - The mock Fulcio decodes (but does not cryptographically verify)
41
+ * the OIDC token, so {@link SigstoreMockStack.identityToken}
42
+ * mints unsigned JWT-shaped tokens.
43
+ *
44
+ * Interceptors are process-global (nock patches `http`), so run ONE
45
+ * stack per suite and call {@link SigstoreMockStack.teardown} in
46
+ * `afterAll`. While a stack is live the stack is HERMETIC: real
47
+ * network egress over `http.ClientRequest` is disabled (loopback
48
+ * excepted), so a request to any unmocked host fails fast and
49
+ * deterministically instead of silently escaping to live DNS — a
50
+ * `rekor: false` stack answers in milliseconds with nock's
51
+ * disallowed-net-connect error rather than waiting on a real
52
+ * NXDOMAIN. Undici-based `fetch` traffic is NOT intercepted (nor
53
+ * blocked) — only the `http.ClientRequest` path the sigstore/TUF
54
+ * clients use — so an in-process HTTP server under test keeps
55
+ * working alongside a live stack. `teardown()` restores real network
56
+ * access.
57
+ */
58
+ import { createHash, generateKeyPairSync } from 'node:crypto';
59
+ import { join } from 'node:path';
60
+ import { fulcioHandler, initializeCA, initializeCTLog, initializeTLog, rekorHandler, } from '@sigstore/mock';
61
+ import { TrustedRoot } from '@sigstore/protobuf-specs';
62
+ import { createRequire } from 'node:module';
63
+ import nock from 'nock';
64
+ // `@tufjs/repo-mock` ships CJS with a transpiled `exports.default`.
65
+ // Under native Node ESM a default-import binds the whole `exports`
66
+ // object (Node does not unwrap transpiled `.default`), so
67
+ // `mocktuf(...)` throws `TypeError: mocktuf is not a function` in the
68
+ // shipped artifact even though vitest's interop keeps in-repo tests
69
+ // green. `createRequire` reads the CJS shape identically in every
70
+ // runtime — pinned by `dist-esm-interop.integration.test.ts`, which
71
+ // imports the BUILT artifact under plain `node`.
72
+ const cjsRequire = createRequire(import.meta.url);
73
+ const repoMock = cjsRequire('@tufjs/repo-mock');
74
+ const mocktuf = repoMock.default;
75
+ /**
76
+ * Default SAN subject for minted identity tokens. URI-shaped on
77
+ * purpose — the mock CA writes URI SANs (module docstring).
78
+ */
79
+ const DEFAULT_SUBJECT = 'https://gadgets.ggui.test/e2e-signer';
80
+ /** Mirrors the mock Fulcio's default issuer claim. */
81
+ const DEFAULT_ISSUER = 'https://fake.oidcissuer.com';
82
+ /** Per-process counter so parallel suites get distinct mock hosts. */
83
+ let stackCounter = 0;
84
+ /**
85
+ * Boot a full in-process sigstore mock stack: Fulcio + Rekor
86
+ * interceptors plus a TUF repository serving a trusted root built from
87
+ * the same keys. See the module docstring for the trust wiring.
88
+ */
89
+ export async function startSigstoreMockStack(options = {}) {
90
+ const id = (stackCounter += 1);
91
+ const fulcioURL = `https://fulcio.mock-${id}.ggui.test`;
92
+ const rekorURL = `https://rekor.mock-${id}.ggui.test`;
93
+ const tufMirrorURL = `https://tuf.mock-${id}.ggui.test`;
94
+ // One P-256 keypair roots the whole stack (CA + CTLog + TLog) —
95
+ // mirrors `@sigstore/mock`'s own top-level `mockFulcio` /
96
+ // `mockRekor` composition, which shares a keypair between the CA
97
+ // and its CTLog.
98
+ const keyPair = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
99
+ const ctlog = await initializeCTLog(keyPair);
100
+ const ca = await initializeCA(keyPair, ctlog);
101
+ const tlog = await initializeTLog(rekorURL, keyPair);
102
+ // Hermetic: while the stack is live, requests to any host without a
103
+ // registered interceptor fail fast with nock's NetConnectNotAllowed
104
+ // error instead of escaping to real DNS/network (empirically: an
105
+ // unmocked `.test` host cost ~3s of live NXDOMAIN lookups per
106
+ // attempt before this). Loopback stays open for in-process HTTP
107
+ // servers under test.
108
+ nock.disableNetConnect();
109
+ nock.enableNetConnect((host) => host.startsWith('127.0.0.1') || host.startsWith('localhost'));
110
+ if (options.fulcio !== false) {
111
+ mount(fulcioURL, fulcioHandler(ca, { strict: true }));
112
+ }
113
+ if (options.rekor !== false) {
114
+ mount(rekorURL, rekorHandler(tlog, { strict: true }));
115
+ }
116
+ // Trust material for verification — the SAME roots the mock services
117
+ // sign with, projected onto the `trusted_root.json` shape the TUF
118
+ // client resolves. Built in the protobuf-specs JSON encoding (base64
119
+ // bytes, enum names, ISO timestamps) and round-tripped through the
120
+ // `TrustedRoot` codec so a field drift fails loudly here rather than
121
+ // deep inside the verifier. (The installed protobuf-specs build
122
+ // exposes only `fromJSON`/`toJSON` — no `fromPartial` — discovered
123
+ // on this fixture's first run.)
124
+ //
125
+ // Validity windows are backdated one minute so cert-chain checks sit
126
+ // comfortably inside the window even under clock skew between this
127
+ // init and the moment a test signs.
128
+ const validityStart = new Date(Date.now() - 60_000).toISOString();
129
+ const trustedRootJSON = JSON.stringify(TrustedRoot.toJSON(TrustedRoot.fromJSON({
130
+ mediaType: 'application/vnd.dev.sigstore.trustedroot+json;version=0.1',
131
+ certificateAuthorities: [
132
+ {
133
+ subject: { commonName: 'sigstore', organization: 'sigstore.mock' },
134
+ uri: fulcioURL,
135
+ certChain: {
136
+ certificates: [{ rawBytes: toBase64(ca.rootCertificate) }],
137
+ },
138
+ validFor: { start: validityStart },
139
+ },
140
+ ],
141
+ tlogs: [
142
+ {
143
+ baseUrl: rekorURL,
144
+ logId: {
145
+ keyId: createHash('sha256').update(tlog.publicKey).digest('base64'),
146
+ },
147
+ hashAlgorithm: 'SHA2_256',
148
+ publicKey: {
149
+ rawBytes: tlog.publicKey.toString('base64'),
150
+ keyDetails: 'PKIX_ECDSA_P256_SHA_256',
151
+ validFor: { start: validityStart },
152
+ },
153
+ },
154
+ ],
155
+ ctlogs: [
156
+ {
157
+ baseUrl: `${fulcioURL}/ctlog`,
158
+ logId: { keyId: toBase64(ctlog.logID) },
159
+ hashAlgorithm: 'SHA2_256',
160
+ publicKey: {
161
+ rawBytes: ctlog.publicKey.toString('base64'),
162
+ keyDetails: 'PKIX_ECDSA_P256_SHA_256',
163
+ validFor: { start: validityStart },
164
+ },
165
+ },
166
+ ],
167
+ timestampAuthorities: [],
168
+ })));
169
+ const target = {
170
+ name: 'trusted_root.json',
171
+ content: Buffer.from(trustedRootJSON),
172
+ };
173
+ // `metadataPathPrefix: ''` matches `@sigstore/tuf`'s mirror layout
174
+ // (metadata at the mirror root, targets under /targets).
175
+ const tufRepo = mocktuf(target, { baseURL: tufMirrorURL, metadataPathPrefix: '' });
176
+ return {
177
+ fulcioURL,
178
+ rekorURL,
179
+ signEndpoints: { fulcioURL, rekorURL },
180
+ tuf: {
181
+ tufMirrorURL: tufRepo.baseURL,
182
+ tufCachePath: tufRepo.cachePath,
183
+ tufRootPath: join(tufRepo.cachePath, 'root.json'),
184
+ tufForceCache: true,
185
+ },
186
+ defaultSubject: DEFAULT_SUBJECT,
187
+ defaultIssuer: DEFAULT_ISSUER,
188
+ identityToken(claims = {}) {
189
+ return mintUnsignedJwt({
190
+ sub: DEFAULT_SUBJECT,
191
+ iss: DEFAULT_ISSUER,
192
+ ...claims,
193
+ });
194
+ },
195
+ teardown() {
196
+ nock.cleanAll();
197
+ nock.enableNetConnect();
198
+ tufRepo.teardown();
199
+ },
200
+ };
201
+ }
202
+ /**
203
+ * Register a persistent nock interceptor for a `@sigstore/mock`
204
+ * service handler. Persistence matters: a suite signs more than once
205
+ * per stack, and nock interceptors are single-use by default.
206
+ */
207
+ function mount(baseURL, handler) {
208
+ nock(baseURL)
209
+ .persist()
210
+ .post(handler.path)
211
+ .reply(async (_uri, requestBody) => {
212
+ const raw = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
213
+ const { statusCode, response, contentType } = await handler.fn(raw);
214
+ return [statusCode, response, { 'Content-Type': contentType ?? 'text/plain' }];
215
+ });
216
+ }
217
+ /**
218
+ * Mint an unsigned JWT-shaped token (`<b64url-header>.<b64url-payload>.`)
219
+ * that JWT decoders parse without signature verification — all the
220
+ * mock Fulcio does with it.
221
+ */
222
+ function mintUnsignedJwt(payload) {
223
+ const header = base64Url(JSON.stringify({ alg: 'none', typ: 'JWT' }));
224
+ const body = base64Url(JSON.stringify(payload));
225
+ return `${header}.${body}.`;
226
+ }
227
+ function base64Url(value) {
228
+ return Buffer.from(value)
229
+ .toString('base64')
230
+ .replace(/=+$/u, '')
231
+ .replace(/\+/gu, '-')
232
+ .replace(/\//gu, '_');
233
+ }
234
+ /** Base64-encode an ArrayBufferView's exact byte range. */
235
+ function toBase64(view) {
236
+ return Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString('base64');
237
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/gadget-signing",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "description": "Gadget bundle signing + verification for the ggui gadget marketplace. Ed25519 author-key path + sigstore/cosign keyless path. Pure-TS @noble crypto for Ed25519 — browser-safe.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -25,16 +25,47 @@
25
25
  "types": "./dist/index.d.ts",
26
26
  "import": "./dist/index.js",
27
27
  "default": "./dist/index.js"
28
+ },
29
+ "./testing": {
30
+ "types": "./dist/testing/index.d.ts",
31
+ "import": "./dist/testing/index.js",
32
+ "default": "./dist/testing/index.js"
28
33
  }
29
34
  },
30
35
  "dependencies": {
31
36
  "@noble/ed25519": "^2.2.0",
32
37
  "@noble/hashes": "^1.6.0",
33
38
  "@sigstore/bundle": "^4.0.0",
39
+ "@sigstore/core": "^3.2.1",
34
40
  "sigstore": "^4.0.0"
35
41
  },
42
+ "peerDependencies": {
43
+ "@sigstore/mock": "~0.12.1",
44
+ "@sigstore/protobuf-specs": "^0.5.1",
45
+ "@tufjs/repo-mock": "4.0.1",
46
+ "nock": "^13.5.6"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@sigstore/mock": {
50
+ "optional": true
51
+ },
52
+ "@sigstore/protobuf-specs": {
53
+ "optional": true
54
+ },
55
+ "@tufjs/repo-mock": {
56
+ "optional": true
57
+ },
58
+ "nock": {
59
+ "optional": true
60
+ }
61
+ },
36
62
  "devDependencies": {
63
+ "@sigstore/mock": "~0.12.1",
64
+ "@sigstore/protobuf-specs": "^0.5.1",
65
+ "@sigstore/verify": "^3.1.1",
66
+ "@tufjs/repo-mock": "4.0.1",
37
67
  "@types/node": "^24.0.0",
68
+ "nock": "^13.5.6",
38
69
  "typescript": "^5.0.0",
39
70
  "vitest": "^3.2.6"
40
71
  },