@typedstandards/verify-core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nathan Storey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @typedstandards/verify-core
2
+
3
+ The portable, **browser-safe** verification core for [Typed Standards][ts] evidence
4
+ packages — the spec §9.2 check suite, factored so it runs **identically** on a
5
+ server (e.g. civicaitools.org's verify route) and in the browser (the
6
+ typedstandards.org client-side verifier). One implementation, so a tampered
7
+ package fails the same way wherever it is checked.
8
+
9
+ > Originally extracted from `civic-ai-tools-website` (#116 WS2) and published here
10
+ > (#116 WS3) so every consumer depends on **one versioned source** that cannot
11
+ > drift.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install @typedstandards/verify-core
17
+ ```
18
+
19
+ ## Browser-safety contract
20
+
21
+ This package depends on **no Node built-ins**. There is no `node:crypto` / `fs` /
22
+ `path` / `process` / `Buffer` anywhere in the source (an upstream ESLint
23
+ `no-restricted-imports` rule enforces it). It runs unchanged in a browser, on the
24
+ edge, and in Node.
25
+
26
+ - **Hashing** — [`@noble/hashes`][nh] (SHA-256/512). Digests are byte-identical to
27
+ Node's `crypto.createHash`.
28
+ - **Signatures** — [`@noble/curves`][nc] Ed25519 / Ed25519ph, with algorithm
29
+ dispatch (a signature verifies only under the scheme it was made with). Raw key
30
+ bytes come from a fixed-prefix SPKI slice, not `crypto.createPublicKey`.
31
+ - **Canonicalization** — RFC 8785 JCS via [`canonicalize`][cz].
32
+ - **Network is injected** — every check that touches the network takes a
33
+ `FetchLike` (defaulting to `globalThis.fetch`, read at call time). The server
34
+ injects its `fetch`, the browser injects `window.fetch`, tests inject a stub.
35
+
36
+ > **Fetch note.** A `FetchLike` should issue **plain GETs with no custom request
37
+ > headers**. A custom header triggers a CORS preflight, and a preflight `OPTIONS`
38
+ > hitting civicaitools.org's site-wide 307 to its canonical host can be rejected.
39
+ > A simple GET follows the 307 transparently.
40
+
41
+ ## Usage
42
+
43
+ ```ts
44
+ import { verifyEvidence, type VerifyInput, type TrustRegistry } from '@typedstandards/verify-core';
45
+
46
+ const input: VerifyInput = {
47
+ package: pkg, // the parsed package JSON
48
+ packageHash, // the claimed envelope hash
49
+ signature, // { signature, publicKey, algorithm?, kid? }
50
+ rfc3161Timestamp, // optional
51
+ rekorEntryId, // optional
52
+ lifecycle, // optional sidecar lifecycle state
53
+ };
54
+
55
+ const result = await verifyEvidence(input, {
56
+ registry, // a TrustRegistry (fetched from the package's trustRegistryUrl)
57
+ fetch: globalThis.fetch,
58
+ });
59
+ // result carries the per-check verdicts: hashMatch, signatureValid, keyTrust,
60
+ // contentHash, typeResolution, signerIdentity, lifecycle, ...
61
+ ```
62
+
63
+ The barrel (`.`) also exports the individual check functions
64
+ (`recomputePackageHash`, `verifySignature`, `verifyKeyTrust`, `verifyContentHash`,
65
+ …), the canonicalization primitives (`computeEnvelopeHash`,
66
+ `computeContentHashSha256`), and every status vocabulary type — so a consumer can
67
+ drive the checks one-at-a-time and render the math as it resolves.
68
+
69
+ ## Check depth (v1)
70
+
71
+ Fully client-side: #1 envelope hash, #2 signature, #3 canonicalization, #4 content
72
+ hash, #5 key trust, #6 kid consistency, #9 blob refs, #12 type, #13 nodeId, #14
73
+ signer identity, #15 captureMethod vocab. **Presence** only for #7 (RFC 3161).
74
+ **Hash-parity** only for #8 (Rekor). **State** depth for #10 (lifecycle).
75
+
76
+ The deeper offline crypto — TSA-chain verification, Rekor Merkle-inclusion,
77
+ independent lifecycle-chain verification, and the authoritative bundle-mode test —
78
+ is a fast-follow (#119), not in this release. The UI built on this should be
79
+ honest about what is presence vs. full-crypto.
80
+
81
+ ## License
82
+
83
+ MIT © Nathan Storey
84
+
85
+ [ts]: https://typedstandards.org
86
+ [nh]: https://github.com/paulmillr/noble-hashes
87
+ [nc]: https://github.com/paulmillr/noble-curves
88
+ [cz]: https://github.com/erdtman/canonicalize
@@ -0,0 +1,5 @@
1
+ export declare const ATTESTATION_WITHDRAWS = "attestation/withdraws/v1";
2
+ export declare const ATTESTATION_REINSTATES = "attestation/reinstates/v1";
3
+ export type LifecycleAttestationType = typeof ATTESTATION_WITHDRAWS | typeof ATTESTATION_REINSTATES;
4
+ /** The lifecycle sub-type URIs the verify side resolves (spec §8.12.1). */
5
+ export declare const LIFECYCLE_ATTESTATION_TYPES: readonly string[];
@@ -0,0 +1,12 @@
1
+ // Lifecycle attestation sub-type URIs (spec §8.10, §8.12.1) — browser-safe
2
+ // constants. The server `attestation.ts` builder (which mints nodes and so needs
3
+ // Node's `crypto.randomUUID`) re-exports these from verify-core, keeping a single
4
+ // definition of the URIs the verify side dispatches on. verify-core never builds
5
+ // nodes — it only verifies and orders them — so it carries the constants alone.
6
+ export const ATTESTATION_WITHDRAWS = 'attestation/withdraws/v1';
7
+ export const ATTESTATION_REINSTATES = 'attestation/reinstates/v1';
8
+ /** The lifecycle sub-type URIs the verify side resolves (spec §8.12.1). */
9
+ export const LIFECYCLE_ATTESTATION_TYPES = [
10
+ ATTESTATION_WITHDRAWS,
11
+ ATTESTATION_REINSTATES,
12
+ ];
@@ -0,0 +1,61 @@
1
+ import type { FetchLike } from './types.ts';
2
+ /** Format of `ref`: `blob:sha256:<64 hex chars>`. */
3
+ export interface BlobRef {
4
+ ref: string;
5
+ url: string;
6
+ contentType: string;
7
+ size: number;
8
+ }
9
+ /**
10
+ * Detect whether a field value is a BlobRef object. Used by the verifier and the
11
+ * detail-page renderer to branch between inline content and blob-referenced
12
+ * content.
13
+ */
14
+ export declare function isBlobRef(value: unknown): value is BlobRef;
15
+ export interface ParsedBlobRef {
16
+ algo: 'sha256';
17
+ hash: string;
18
+ }
19
+ /**
20
+ * Parse a `blob:sha256:<hash>` reference string into its components.
21
+ * Throws if the format is invalid.
22
+ */
23
+ export declare function parseBlobRef(ref: string): ParsedBlobRef;
24
+ export declare const BLOB_REF_VERIFY_REASONS: readonly ["invalid_ref", "fetch_failed", "size_mismatch", "hash_mismatch"];
25
+ export type BlobRefVerifyReason = (typeof BLOB_REF_VERIFY_REASONS)[number];
26
+ export interface BlobRefVerifyResult {
27
+ ok: boolean;
28
+ reason?: BlobRefVerifyReason;
29
+ /** SHA-256 of the fetched blob content, if fetched. Lets callers surface both
30
+ * the expected (ref) and actual hash when they mismatch. */
31
+ computedHash?: string;
32
+ /** Byte length of the fetched blob. */
33
+ computedSize?: number;
34
+ }
35
+ /** Options accepted by the network-touching blob helpers: an injected fetcher
36
+ * (defaults to `globalThis.fetch`, read at call time so a test stub on the
37
+ * global is honored) + an optional abort signal. */
38
+ export interface BlobFetchOptions {
39
+ fetch?: FetchLike;
40
+ signal?: AbortSignal;
41
+ }
42
+ /**
43
+ * Fetch a blob and verify that its SHA-256 matches the reference hash and that
44
+ * its byte size matches the metadata. Returns a structured result; never throws.
45
+ *
46
+ * Fetches over HTTPS without auth (Vercel Blob public access is the storage
47
+ * default for evidence content). A plain GET with no custom headers, so it
48
+ * follows civicaitools.org's canonical-host 307 without a CORS preflight.
49
+ */
50
+ export declare function verifyBlobRef(ref: BlobRef, options?: BlobFetchOptions): Promise<BlobRefVerifyResult>;
51
+ /**
52
+ * Compute the canonical `blob:sha256:<hash>` reference hash for content bytes.
53
+ * Used by uploaders constructing a BlobRef object.
54
+ */
55
+ export declare function computeBlobRefHash(content: string | Uint8Array): string;
56
+ /**
57
+ * Fetch a BlobRef's content as a UTF-8 string. Used by the detail-page renderer
58
+ * to resolve output-level BlobRefs. Returns null on any failure; callers fall
59
+ * back to showing only the reference metadata rather than blocking the render.
60
+ */
61
+ export declare function fetchBlobRefText(ref: BlobRef, options?: BlobFetchOptions): Promise<string | null>;
@@ -0,0 +1,127 @@
1
+ // Content-addressable blob references for evidence package fields (spec §9.2
2
+ // check #9) — browser-safe.
3
+ //
4
+ // Large fields (full output text, trace JSON, composed skill guidance) can be
5
+ // stored as a separate blob and referenced from the package JSON rather than
6
+ // inlined. The package hash still binds the reference (the BlobRef object is part
7
+ // of the canonical JSON), and the referenced content has its own SHA-256 so
8
+ // consumers verify each piece independently.
9
+ //
10
+ // Factored from the server `blob-ref.ts` (WS2): the SHA-256 backend is
11
+ // `@noble/hashes` and the network fetch is INJECTED (defaulting to the universal
12
+ // `globalThis.fetch`, which the server provides and the browser provides as
13
+ // `window.fetch`). No `node:crypto`, no hardcoded transport.
14
+ import { sha256Hex } from "./primitives.js";
15
+ const REF_PATTERN = /^blob:sha256:([0-9a-f]{64})$/;
16
+ /**
17
+ * Detect whether a field value is a BlobRef object. Used by the verifier and the
18
+ * detail-page renderer to branch between inline content and blob-referenced
19
+ * content.
20
+ */
21
+ export function isBlobRef(value) {
22
+ if (typeof value !== 'object' || value === null)
23
+ return false;
24
+ const v = value;
25
+ return (typeof v.ref === 'string' &&
26
+ REF_PATTERN.test(v.ref) &&
27
+ typeof v.url === 'string' &&
28
+ typeof v.contentType === 'string' &&
29
+ typeof v.size === 'number');
30
+ }
31
+ /**
32
+ * Parse a `blob:sha256:<hash>` reference string into its components.
33
+ * Throws if the format is invalid.
34
+ */
35
+ export function parseBlobRef(ref) {
36
+ const match = REF_PATTERN.exec(ref);
37
+ if (!match) {
38
+ throw new Error(`Invalid blob reference: expected "blob:sha256:<64 hex>", got "${ref}"`);
39
+ }
40
+ return { algo: 'sha256', hash: match[1] };
41
+ }
42
+ // Source-of-truth array (not just a type) so the trust-signal vocabulary
43
+ // (civic-ai-tools-website#110) can enumerate every BlobRef failure reason at
44
+ // runtime. Each reason is a sub-explanation of a failed (Alarm-tier) BlobRef
45
+ // integrity check (#9).
46
+ export const BLOB_REF_VERIFY_REASONS = [
47
+ 'invalid_ref',
48
+ 'fetch_failed',
49
+ 'size_mismatch',
50
+ 'hash_mismatch',
51
+ ];
52
+ function resolveFetch(options) {
53
+ return options.fetch ?? globalThis.fetch;
54
+ }
55
+ /**
56
+ * Fetch a blob and verify that its SHA-256 matches the reference hash and that
57
+ * its byte size matches the metadata. Returns a structured result; never throws.
58
+ *
59
+ * Fetches over HTTPS without auth (Vercel Blob public access is the storage
60
+ * default for evidence content). A plain GET with no custom headers, so it
61
+ * follows civicaitools.org's canonical-host 307 without a CORS preflight.
62
+ */
63
+ export async function verifyBlobRef(ref, options = {}) {
64
+ let parsed;
65
+ try {
66
+ parsed = parseBlobRef(ref.ref);
67
+ }
68
+ catch {
69
+ return { ok: false, reason: 'invalid_ref' };
70
+ }
71
+ const fetcher = resolveFetch(options);
72
+ let response;
73
+ try {
74
+ response = await fetcher(ref.url, {
75
+ signal: options.signal ?? AbortSignal.timeout(15_000),
76
+ });
77
+ }
78
+ catch {
79
+ return { ok: false, reason: 'fetch_failed' };
80
+ }
81
+ if (!response.ok) {
82
+ return { ok: false, reason: 'fetch_failed' };
83
+ }
84
+ let bytes;
85
+ try {
86
+ const buffer = await response.arrayBuffer();
87
+ bytes = new Uint8Array(buffer);
88
+ }
89
+ catch {
90
+ return { ok: false, reason: 'fetch_failed' };
91
+ }
92
+ const computedSize = bytes.byteLength;
93
+ if (computedSize !== ref.size) {
94
+ return { ok: false, reason: 'size_mismatch', computedSize };
95
+ }
96
+ const computedHash = sha256Hex(bytes);
97
+ if (computedHash !== parsed.hash) {
98
+ return { ok: false, reason: 'hash_mismatch', computedHash, computedSize };
99
+ }
100
+ return { ok: true, computedHash, computedSize };
101
+ }
102
+ /**
103
+ * Compute the canonical `blob:sha256:<hash>` reference hash for content bytes.
104
+ * Used by uploaders constructing a BlobRef object.
105
+ */
106
+ export function computeBlobRefHash(content) {
107
+ return sha256Hex(content);
108
+ }
109
+ /**
110
+ * Fetch a BlobRef's content as a UTF-8 string. Used by the detail-page renderer
111
+ * to resolve output-level BlobRefs. Returns null on any failure; callers fall
112
+ * back to showing only the reference metadata rather than blocking the render.
113
+ */
114
+ export async function fetchBlobRefText(ref, options = {}) {
115
+ try {
116
+ const fetcher = resolveFetch(options);
117
+ const response = await fetcher(ref.url, {
118
+ signal: options.signal ?? AbortSignal.timeout(15_000),
119
+ });
120
+ if (!response.ok)
121
+ return null;
122
+ return await response.text();
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
@@ -0,0 +1,47 @@
1
+ export declare const LEGACY_JSON_CANONICALIZATION = "https://typedstandards.org/canonicalization/legacy-json/v1";
2
+ export declare const DATHERE_AG_JUPYTER_CANONICALIZATION = "https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1";
3
+ /** Canonicalization-rule URIs this implementation knows how to apply. An
4
+ * unknown URI fails verify check #3 (`unknown_canonicalization_rule`). */
5
+ export declare const KNOWN_CANONICALIZATION_RULES: readonly string[];
6
+ /**
7
+ * RFC 8785 JSON Canonicalization Scheme. Wraps the `canonicalize` reference
8
+ * implementation (by the RFC 8785 author); its return type is
9
+ * `string | undefined`, undefined when the input isn't JSON-canonicalizable
10
+ * (e.g. `undefined`). We throw in that case so a malformed package fails loudly
11
+ * rather than silently producing a wrong hash.
12
+ */
13
+ export declare function jcs(value: unknown): string;
14
+ /**
15
+ * §8.2 detection rule: a package whose `contentHash` is embedded as a multihash
16
+ * OBJECT (keyed by lowercase algorithm name, hex-string values) is a v0.1
17
+ * package on the JCS chain. Absence — or an external single-SHA-256 hex string
18
+ * carried out of band on the DB row — marks a pre-v0.1 package on the legacy
19
+ * `JSON.stringify` chain.
20
+ */
21
+ export declare function isMultihashContentHash(value: unknown): value is Record<string, string>;
22
+ /**
23
+ * The envelope hash for a package, routed by the §8.2 detection rule:
24
+ * - v0.1 (multihash `contentHash` present) → SHA-256 of the RFC 8785 JCS
25
+ * canonicalization of the unsigned envelope.
26
+ * - pre-v0.1 (no multihash `contentHash`) → SHA-256 of the legacy
27
+ * insertion-order `JSON.stringify`, preserving byte-identical verification
28
+ * of every package published before the JCS switch.
29
+ *
30
+ * The signature envelope is persisted on the database row, not inside the
31
+ * package object, so the package object already IS the unsigned envelope — there
32
+ * is no `sig` field to strip here. Used by BOTH the packager and the verifier so
33
+ * producer and verifier agree.
34
+ */
35
+ export declare function computeEnvelopeHash(pkg: Record<string, unknown>): string;
36
+ /**
37
+ * SHA-256 of a package's off-log content, canonicalized per the named rule
38
+ * (spec §8.2). Symmetric across produce/verify: `contentHash` is stripped before
39
+ * hashing, so the result is identical whether the package already carries
40
+ * `contentHash` (verify) or does not yet (packager).
41
+ *
42
+ * - legacy-json/v1 → the canonical-JSON package with `contentHash`
43
+ * (and the sig envelope, which lives off-package here) omitted.
44
+ * - dathere-ag-jupyter/v1 → the executed notebook object
45
+ * (`extensions["org.civicaitools.notebook"]` and its rendered outputs).
46
+ */
47
+ export declare function computeContentHashSha256(pkg: Record<string, unknown>, rule: string): string;
@@ -0,0 +1,100 @@
1
+ // Canonicalization + hashing core (spec §8.2, §8.3.1, §12.3) — browser-safe.
2
+ //
3
+ // Factored from the server `src/lib/evidence/canonicalization.ts` (WS2) with one
4
+ // change: the SHA-256 backend is `@noble/hashes` instead of `node:crypto`, so the
5
+ // same code hashes in the browser. The `canonicalize` (RFC 8785 JCS) reference
6
+ // implementation was already pure and is reused verbatim. The server module now
7
+ // re-exports everything here, so producer and verifier — across both runtimes —
8
+ // remain on a single implementation; a package re-verifies to the exact hash it
9
+ // was published under regardless of where the check runs.
10
+ //
11
+ // The §8.2 detection rule routes each package to its chain: a package whose
12
+ // `contentHash` is a multihash object is a v0.1 package on the JCS chain;
13
+ // absence marks a pre-v0.1 package on the legacy `JSON.stringify` chain.
14
+ import canonicalize from 'canonicalize';
15
+ import { sha256Hex } from "./primitives.js";
16
+ // Content-canonicalization rule URIs (spec §8.2, §12.3). Identifiers, not fetch
17
+ // targets — verifiers resolve them via the local registry below.
18
+ export const LEGACY_JSON_CANONICALIZATION = 'https://typedstandards.org/canonicalization/legacy-json/v1';
19
+ export const DATHERE_AG_JUPYTER_CANONICALIZATION = 'https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1';
20
+ /** Canonicalization-rule URIs this implementation knows how to apply. An
21
+ * unknown URI fails verify check #3 (`unknown_canonicalization_rule`). */
22
+ export const KNOWN_CANONICALIZATION_RULES = [
23
+ LEGACY_JSON_CANONICALIZATION,
24
+ DATHERE_AG_JUPYTER_CANONICALIZATION,
25
+ ];
26
+ /** Notebook extension whose object the dathere-ag-jupyter/v1 rule fingerprints
27
+ * (spec §8.2, §8.7.2). */
28
+ const NOTEBOOK_EXTENSION_KEY = 'org.civicaitools.notebook';
29
+ /**
30
+ * RFC 8785 JSON Canonicalization Scheme. Wraps the `canonicalize` reference
31
+ * implementation (by the RFC 8785 author); its return type is
32
+ * `string | undefined`, undefined when the input isn't JSON-canonicalizable
33
+ * (e.g. `undefined`). We throw in that case so a malformed package fails loudly
34
+ * rather than silently producing a wrong hash.
35
+ */
36
+ export function jcs(value) {
37
+ const out = canonicalize(value);
38
+ if (typeof out !== 'string') {
39
+ throw new Error('JCS canonicalization produced no output (non-JSON value)');
40
+ }
41
+ return out;
42
+ }
43
+ /**
44
+ * §8.2 detection rule: a package whose `contentHash` is embedded as a multihash
45
+ * OBJECT (keyed by lowercase algorithm name, hex-string values) is a v0.1
46
+ * package on the JCS chain. Absence — or an external single-SHA-256 hex string
47
+ * carried out of band on the DB row — marks a pre-v0.1 package on the legacy
48
+ * `JSON.stringify` chain.
49
+ */
50
+ export function isMultihashContentHash(value) {
51
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
52
+ return false;
53
+ }
54
+ const entries = Object.entries(value);
55
+ return entries.length > 0 && entries.every(([, v]) => typeof v === 'string');
56
+ }
57
+ /**
58
+ * The envelope hash for a package, routed by the §8.2 detection rule:
59
+ * - v0.1 (multihash `contentHash` present) → SHA-256 of the RFC 8785 JCS
60
+ * canonicalization of the unsigned envelope.
61
+ * - pre-v0.1 (no multihash `contentHash`) → SHA-256 of the legacy
62
+ * insertion-order `JSON.stringify`, preserving byte-identical verification
63
+ * of every package published before the JCS switch.
64
+ *
65
+ * The signature envelope is persisted on the database row, not inside the
66
+ * package object, so the package object already IS the unsigned envelope — there
67
+ * is no `sig` field to strip here. Used by BOTH the packager and the verifier so
68
+ * producer and verifier agree.
69
+ */
70
+ export function computeEnvelopeHash(pkg) {
71
+ if (isMultihashContentHash(pkg['contentHash'])) {
72
+ return sha256Hex(jcs(pkg));
73
+ }
74
+ return sha256Hex(JSON.stringify(pkg));
75
+ }
76
+ /**
77
+ * SHA-256 of a package's off-log content, canonicalized per the named rule
78
+ * (spec §8.2). Symmetric across produce/verify: `contentHash` is stripped before
79
+ * hashing, so the result is identical whether the package already carries
80
+ * `contentHash` (verify) or does not yet (packager).
81
+ *
82
+ * - legacy-json/v1 → the canonical-JSON package with `contentHash`
83
+ * (and the sig envelope, which lives off-package here) omitted.
84
+ * - dathere-ag-jupyter/v1 → the executed notebook object
85
+ * (`extensions["org.civicaitools.notebook"]` and its rendered outputs).
86
+ */
87
+ export function computeContentHashSha256(pkg, rule) {
88
+ if (rule === DATHERE_AG_JUPYTER_CANONICALIZATION) {
89
+ const extensions = pkg['extensions'];
90
+ const notebook = extensions?.[NOTEBOOK_EXTENSION_KEY];
91
+ if (notebook === undefined) {
92
+ throw new Error(`dathere-ag-jupyter/v1 content hash requires the ${NOTEBOOK_EXTENSION_KEY} extension`);
93
+ }
94
+ return sha256Hex(jcs(notebook));
95
+ }
96
+ // legacy-json/v1 (default): fingerprint the package minus contentHash.
97
+ const rest = { ...pkg };
98
+ delete rest['contentHash'];
99
+ return sha256Hex(jcs(rest));
100
+ }
@@ -0,0 +1,128 @@
1
+ import { type BlobRefVerifyReason, type BlobFetchOptions } from './blob-ref.ts';
2
+ import type { TrustRegistry } from './trust-registry.ts';
3
+ /**
4
+ * Recompute a package's envelope hash, routed by the §8.2 detection rule: v0.1
5
+ * packages (multihash `contentHash` present) hash via RFC 8785 JCS; pre-v0.1
6
+ * packages hash via the legacy insertion-order `JSON.stringify`. Drives both
7
+ * check #1 (envelope integrity / `hashMatch`) and check #13 (`nodeId`).
8
+ */
9
+ export declare function recomputePackageHash(pkg: Record<string, unknown>): string;
10
+ export declare const CONTENT_CANONICALIZATION_STATUSES: readonly ["ok", "implicit", "unknown_canonicalization_rule"];
11
+ export type ContentCanonicalizationStatus = (typeof CONTENT_CANONICALIZATION_STATUSES)[number];
12
+ export interface ContentCanonicalizationResolution {
13
+ status: ContentCanonicalizationStatus;
14
+ /** The resolved canonicalization-rule URI — the explicit field value, or the
15
+ * rule inferred from the content profile for pre-v0.1 packages. */
16
+ rule: string;
17
+ }
18
+ /**
19
+ * Check #3 — content-canonicalization rule resolution (spec §9.2). Reads the
20
+ * package's `contentCanonicalization` URI and resolves it against the local rule
21
+ * registry. A known URI → `ok`. An unrecognized URI →
22
+ * `unknown_canonicalization_rule` (renders; check #4 then cannot recompute). An
23
+ * absent field on a pre-v0.1 package → `implicit`, inferring the rule from
24
+ * contentProfile / producerProfile (datHere → dathere-ag-jupyter/v1; otherwise
25
+ * legacy-json/v1).
26
+ */
27
+ export declare function resolveContentCanonicalization(pkg: Record<string, unknown>): ContentCanonicalizationResolution;
28
+ export declare const CONTENT_HASH_STATUSES: readonly ["ok", "content_hash_mismatch", "contentHash_no_supported_algorithm", "unresolved_rule", "legacy_relabeled"];
29
+ export type ContentHashStatus = (typeof CONTENT_HASH_STATUSES)[number];
30
+ export interface ContentHashCheck {
31
+ status: ContentHashStatus;
32
+ /** Algorithm names listed in the package's multihash `contentHash`. */
33
+ algorithms?: string[];
34
+ /** The algorithm whose recomputed digest matched (status === 'ok'). */
35
+ matched?: string;
36
+ /** The multihash digest set the verifier reports for the package. For pre-v0.1
37
+ * packages this is the legacy external single-SHA-256 relabeled as
38
+ * `{ sha256: <hex> }` per §8.2. */
39
+ contentHash?: Record<string, string>;
40
+ }
41
+ /**
42
+ * Check #4 — content-hash verification (spec §9.2, §8.2).
43
+ *
44
+ * v0.1 packages (multihash `contentHash` present): recompute the off-log
45
+ * content's digest under the resolved canonicalization rule for every listed
46
+ * algorithm this verifier supports, and confirm at least one matches.
47
+ * - at least one supported algorithm matches → `ok`.
48
+ * - a supported algorithm is present but none match → `content_hash_mismatch`.
49
+ * - none of the listed algorithms are ones this verifier can compute →
50
+ * `contentHash_no_supported_algorithm` (degrades; the value is preserved).
51
+ * - the canonicalization rule could not be resolved (check #3 unknown) →
52
+ * `unresolved_rule` (cannot recompute).
53
+ *
54
+ * pre-v0.1 packages (no multihash `contentHash`): the historical external
55
+ * single-SHA-256 is RELABELED as `{ sha256: <hex> }` per §8.2 rather than
56
+ * recomputed; its integrity is established by check #1, so check #4 reports
57
+ * `legacy_relabeled`.
58
+ */
59
+ export declare function verifyContentHash(pkg: Record<string, unknown>, resolution: ContentCanonicalizationResolution, legacyExternalHash?: string): ContentHashCheck;
60
+ export declare const TYPE_RESOLUTION_STATUSES: readonly ["ok", "implicit", "unknown_type"];
61
+ export type TypeResolutionStatus = (typeof TYPE_RESOLUTION_STATUSES)[number];
62
+ export interface TypeResolution {
63
+ status: TypeResolutionStatus;
64
+ /** The resolved type URI (the implicit `content/analysis/v1` when omitted). */
65
+ type: string;
66
+ }
67
+ /**
68
+ * Check #12 — `type` resolution. An absent field resolves to the implicit
69
+ * `content/analysis/v1` (pre-v0.1). An unrecognized URI reports `unknown_type`
70
+ * and renders as such — it does NOT fail verification.
71
+ */
72
+ export declare function resolvePackageType(pkg: Record<string, unknown>): TypeResolution;
73
+ export declare const SIGNER_IDENTITY_CHECK_STATUSES: readonly ["ok", "signer_identity_mismatch", "no_signer", "no_registry_identity"];
74
+ export type SignerIdentityCheckStatus = (typeof SIGNER_IDENTITY_CHECK_STATUSES)[number];
75
+ export interface SignerIdentityCheck {
76
+ status: SignerIdentityCheckStatus;
77
+ /** The `signer.identifier` claimed in the envelope, when present. */
78
+ claimed?: string;
79
+ /** The identifier the registry records for the signing `kid`, when present. */
80
+ registered?: string;
81
+ }
82
+ /**
83
+ * Check #14 — `signer.identifier` ↔ trust-registry `signerIdentity` cross-check
84
+ * (rules out a kid-swap-with-mismatched-identity attack). `signer_identity_mismatch`
85
+ * is fatal. Pre-v0.1 packages carry no envelope-side `signer` (`no_signer`) — the
86
+ * verifier derives the signer from the registry and skips the cross-check. A
87
+ * registry entry without a `signerIdentity` (legacy registry) yields
88
+ * `no_registry_identity`.
89
+ */
90
+ export declare function checkSignerIdentity(pkg: Record<string, unknown>, kid: string | undefined, registry: TrustRegistry | undefined): SignerIdentityCheck;
91
+ export declare const CAPTURE_METHOD_VOCAB_STATUSES: readonly ["ok", "captureMethod_unknown", "producerProfile_bundle_unresolved", "no_capture_method"];
92
+ export type CaptureMethodVocabStatus = (typeof CAPTURE_METHOD_VOCAB_STATUSES)[number];
93
+ export interface CaptureMethodVocabCheck {
94
+ status: CaptureMethodVocabStatus;
95
+ captureMethod?: string;
96
+ /** The resolved Producer Profile type whose vocabulary was consulted. */
97
+ profileType: string;
98
+ }
99
+ /**
100
+ * Check #15 — `captureMethod` per-profile vocabulary conformance. Resolves the
101
+ * package's Producer Profile (or its legacy-alias / pre-v0.1 fallback) and
102
+ * confirms `metadata.captureMethod` is in the declared vocabulary. A value not in
103
+ * the vocabulary reports `captureMethod_unknown` (rejects). An unresolvable
104
+ * profile bundle reports `producerProfile_bundle_unresolved` and degrades
105
+ * gracefully. A null captureMethod (pre-v0.1) is neutral (`no_capture_method`).
106
+ */
107
+ export declare function checkCaptureMethodVocab(pkg: Record<string, unknown>): CaptureMethodVocabCheck;
108
+ /** Field paths that the core verifier scans for BlobRef objects. */
109
+ declare const BLOB_REF_FIELDS: readonly ["output", "trace", "skillMetadata.skillText"];
110
+ export type BlobRefField = (typeof BLOB_REF_FIELDS)[number];
111
+ export interface BlobRefVerification {
112
+ field: BlobRefField;
113
+ ref: string;
114
+ url: string;
115
+ size: number;
116
+ contentType: string;
117
+ ok: boolean;
118
+ reason?: BlobRefVerifyReason;
119
+ }
120
+ /**
121
+ * Walk the package JSON for BlobRef fields, fetch each referenced blob, and
122
+ * confirm the bytes hash to the advertised ref. Returns per-field verification
123
+ * results. Runs fetches in parallel with a per-blob 15 s timeout; failures are
124
+ * reported in the result rather than thrown. The fetcher is injected (defaults to
125
+ * `globalThis.fetch`).
126
+ */
127
+ export declare function verifyPackageBlobRefs(pkg: Record<string, unknown>, options?: BlobFetchOptions): Promise<BlobRefVerification[]>;
128
+ export {};