@typedstandards/verify-core 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,11 +1,92 @@
1
1
  # Changelog — @typedstandards/verify-core
2
2
 
3
- Factual record of what changed per published version. Check numbers (#1–#16)
3
+ Factual record of what changed per published version. Check numbers (#1–#17)
4
4
  refer to the Typed Standards specification §9.2 verification sequence. Issue
5
5
  references are to `npstorey/civic-ai-tools-website` (#119 is the offline-crypto
6
6
  hardening arc; #116 is the standalone-verifier arc this package was extracted
7
7
  in).
8
8
 
9
+ ## 0.15.1 — 2026-11-02
10
+
11
+ The R11 patch (npstorey/civic-ai-tools#293; anchor npstorey/civic-ai-tools#294). **A patch**: no
12
+ export, signature or type changes.
13
+
14
+ - **Check #8's inclusion verdict requires the log entry to record this record's hash.**
15
+ `rekorInclusion.inclusionVerified` and `checkpointVerified` are true only when the entry body's
16
+ `spec.data.hash` is `sha512` with the value `rekorHashForPackage(packageHash)`. That holds for an
17
+ entry body and proof the input carries (`verifyRecord`) and for an entry `verifyRekorEntry`
18
+ fetches (its `inclusion`). An entry that does not record the hash, or an empty `packageHash`,
19
+ reads both false, with the proof's `treeSize` and `leafIndex` and no `reason`.
20
+ `verifyRekorInclusion` is unchanged: it checks inclusion alone.
21
+ - **Every genuine pairing reads as before.** Over the package objects and the carried log
22
+ inclusions in this repository's fixtures, and two host-served commitment views, `verifyRecord`
23
+ gives output deep-equal to 0.15.0's, with the entry carried, fetched, or both.
24
+ - **The `ai-assisted-analysis/civicaitools-v1` row pins revision 2 of its guidance bundle**
25
+ (2026-11-02), as hub ADR-0038's amendment of 2026-11-02 records:
26
+ `https://raw.githubusercontent.com/npstorey/civic-ai-tools/v0.1.14-typed-standards-spec/docs/profiles/ai-assisted-analysis/civicaitools-v1/bundle.json`,
27
+ SHA-256 `45bf3ab479325a791829ed9987f5f291fea44238b54bfb598f32f306f574eb9b`. The row's key and
28
+ `admits` are unchanged. The local copy (not published) is byte-identical to the hub file at
29
+ tree `bcd06c5f`; its provenance is in `src/__fixtures__/README.md`.
30
+
31
+ ## 0.15.0 — 2026-10-30
32
+
33
+ The R10 release (civic-ai-tools#289; hub ADR-0038 §3, §5 and §6, specification v0.1.13 §8.1.1,
34
+ §8.6 and §9.2). **A minor bump**: new exports, a new `VerifyResult` field, and one combination
35
+ check #16 newly reads `ok`.
36
+
37
+ - **The subtype table (new exports `PRODUCER_PROFILE_SUBTYPES`, `isContentProfileConsistent`, and
38
+ the types `ProducerProfileSubtypeRow` and `ProducerProfileBundleRef`).** Beside the profile-type
39
+ table, one frozen row per built subtype of `ai-assisted-analysis`, keyed by the full
40
+ `producerProfile` value:
41
+ - `ai-assisted-analysis/datHere` admits `datHere`, with no bundle pinned;
42
+ - `ai-assisted-analysis/civicaitools-v1` admits `datHere` and `default`, with its guidance
43
+ bundle pinned by URL
44
+ (`https://raw.githubusercontent.com/npstorey/civic-ai-tools/v0.1.13-typed-standards-spec/docs/profiles/ai-assisted-analysis/civicaitools-v1/bundle.json`)
45
+ and SHA-256 (`724e399e02db8993041b64982eec731505084bec2ec5e6ca58fda2de8087a281`), revision 1.
46
+
47
+ `isContentProfileConsistent(producerProfile, contentProfile)` is the §8.1.1 invariant over the
48
+ table: the content profile is one the subtype's row admits; a subtype with no row admits
49
+ `"datHere"` exactly when it begins with `ai-assisted-analysis/datHere`, and `"default"` exactly
50
+ when it does not. Only the table's own keys are rows. The row does not change the capture
51
+ vocabulary: `PROFILE_CAPTURE_VOCAB`, `resolveProfileType` and `captureVocabForProfile` (check #15)
52
+ are unchanged.
53
+ - **Check #16 reads the predicate.** `checkContentProfile` reports `contentProfile_inconsistent`
54
+ when `isContentProfileConsistent` is false, in place of the `startsWith` comparison. The no-row
55
+ clause is that comparison, so every status is unchanged except one: `datHere` under
56
+ `ai-assisted-analysis/civicaitools-v1` reads `ok` (it read `contentProfile_inconsistent`). No
57
+ published record carries that combination.
58
+ - **Check #3's implicit rule reads the predicate.** For a package with no
59
+ `contentCanonicalization`, a `producerProfile` reads as datHere when its subtype admits
60
+ `"datHere"` and not `"default"`, which is the `startsWith` reading for the `datHere` row and every
61
+ subtype with no row; under `ai-assisted-analysis/civicaitools-v1` the choice is
62
+ `metadata.contentProfile`'s. Every result is unchanged.
63
+ - **Check #17, the content-profile system prompt (new exports `checkSystemPrompt`,
64
+ `SYSTEM_PROMPT_STATUSES`, and the types `SystemPromptStatus` and `SystemPromptCheck`).** When
65
+ `metadata.contentProfile` is `"datHere"`, `skillMetadata.skillText` as a non-empty string reads
66
+ `ok` (`form: 'string'`) and as a BlobRef `ok` (`form: 'blobRef'`); absent, empty, or neither
67
+ reads `skillText_absent`. Any other content profile (absent, `"default"` or unknown) reads
68
+ `not_applicable`. `skillText_absent` is an attention status and never a failure. The check does
69
+ not resolve a BlobRef (check #9 does).
70
+ - **`VerifyResult` gains `systemPrompt`** (`SystemPromptCheck | null`, null when the package is
71
+ absent), set by `verifyRecord` beside `contentProfile`. Every other field of the result is
72
+ unchanged. A consumer that builds a `VerifyResult` literal needs the new field.
73
+ - **Every earlier fixture reads as before.** Over the signed bundles, signed documents and package
74
+ objects in this repository's fixtures and the hub harness's `reference-golden.json` and
75
+ `in-loop-golden.json`, `verifyRecord`, `checkContentProfile`, `resolveContentCanonicalization`
76
+ and `captureVocabForProfile` give byte-identical output to 0.14.0 once `systemPrompt` is removed.
77
+ Check #17 reads `skillText_absent` on the five fixture packages that declare `datHere` without
78
+ `skillText` (the hub reference golden's `v01-dathere-empty-notebook` and
79
+ `v01-dathere-executed-notebook`, their copies in produce-core's `reference-golden.json`, and the
80
+ hub's `in-loop-golden.json`), `ok` on the site's `q15-d67b8e` record, and `not_applicable` on
81
+ every other.
82
+ - **Every lookup of a verify-core table by a value a record or bundle supplies reads only the
83
+ table's own entries;** a malformed value reads as an unknown one. Every well-formed value reads as
84
+ before.
85
+ - **A local copy of the bundle**, `src/__fixtures__/civicaitools-v1-guidance-bundle.json` (not
86
+ published), byte-identical to the hub file at tree `7060f5e3`. A test asserts its SHA-256,
87
+ `subtype`, `url` and admitted set against the row, without the network. Its provenance is in
88
+ `src/__fixtures__/README.md`.
89
+
9
90
  ## 0.14.0 — 2026-10-16
10
91
 
11
92
  The W5 release (civic-ai-tools#260, shipped in #146 and #147). **A minor bump**: new exports
package/dist/checks.d.ts CHANGED
@@ -22,7 +22,12 @@ export interface ContentCanonicalizationResolution {
22
22
  * `unknown_canonicalization_rule` (renders; check #4 then cannot recompute). An
23
23
  * absent field on a pre-v0.1 package → `implicit`, inferring the rule from
24
24
  * contentProfile / producerProfile (datHere → dathere-ag-jupyter/v1; otherwise
25
- * legacy-json/v1).
25
+ * legacy-json/v1). A `producerProfile` reads as datHere when the subtype admits
26
+ * `"datHere"` and not `"default"` under the §8.1.1 invariant
27
+ * (`isContentProfileConsistent`): with no row, exactly when it begins with
28
+ * `ai-assisted-analysis/datHere`, as before; a subtype whose row admits both, as
29
+ * `ai-assisted-analysis/civicaitools-v1` does, leaves the choice to
30
+ * `contentProfile`.
26
31
  */
27
32
  export declare function resolveContentCanonicalization(pkg: Record<string, unknown>): ContentCanonicalizationResolution;
28
33
  export declare const CONTENT_HASH_STATUSES: readonly ["ok", "content_hash_mismatch", "contentHash_no_supported_algorithm", "unresolved_rule", "legacy_relabeled", "content_bytes_unavailable"];
@@ -94,14 +99,38 @@ export interface ContentProfileCheck {
94
99
  * - key absent → `contentProfile_absent` (read as `"default"`, spec §8.1.2).
95
100
  * - present and neither `"default"` nor `"datHere"` → `contentProfile_unknown`
96
101
  * (reported, not passed; consistency is not judged).
97
- * - present, known, `producerProfile` present, and the ADR-0006 §2 invariant
98
- * fails (`contentProfile === "datHere"` iff `producerProfile` starts with
102
+ * - present, known, `producerProfile` present, and the §8.1.1 invariant fails
103
+ * (ADR-0006 §2 as amended by hub ADR-0038 §3: the content profile is not one
104
+ * the subtype's row admits, `isContentProfileConsistent`; a subtype with no
105
+ * row admits `"datHere"` exactly when it begins with
99
106
  * `ai-assisted-analysis/datHere`) → `contentProfile_inconsistent`.
100
107
  * - otherwise → `ok`.
101
108
  * The invariant is compared only when both fields are present. No status is an
102
109
  * integrity failure: both labels are signature-covered.
103
110
  */
104
111
  export declare function checkContentProfile(pkg: Record<string, unknown>): ContentProfileCheck;
112
+ export declare const SYSTEM_PROMPT_STATUSES: readonly ["ok", "skillText_absent", "not_applicable"];
113
+ export type SystemPromptStatus = (typeof SYSTEM_PROMPT_STATUSES)[number];
114
+ export interface SystemPromptCheck {
115
+ status: SystemPromptStatus;
116
+ /** On `ok`, how the text is carried: inline, or a BlobRef (which check #9 resolves). */
117
+ form?: 'string' | 'blobRef';
118
+ }
119
+ /**
120
+ * Check #17 — the system prompt §8.7.1 requirement 2 requires of a
121
+ * `datHere`-content-profile package (hub ADR-0038 §6).
122
+ * - `metadata.contentProfile` not `"datHere"` (absent, `"default"` or unknown)
123
+ * → `not_applicable`, and nothing else is read.
124
+ * - `"datHere"` and `skillMetadata.skillText` a non-empty string → `ok`
125
+ * (`form: 'string'`), or a BlobRef → `ok` (`form: 'blobRef'`).
126
+ * - `"datHere"` and the key absent, empty, or neither a string nor a BlobRef
127
+ * → `skillText_absent`.
128
+ * `skillText_absent` is an attention status, never a failure: it says nothing
129
+ * about the envelope, the signature or the notebook. The check does not resolve
130
+ * a BlobRef (check #9 does), and cannot check that the text is the prompt the
131
+ * model ran under.
132
+ */
133
+ export declare function checkSystemPrompt(pkg: Record<string, unknown>): SystemPromptCheck;
105
134
  export declare const CONTENT_LAYOUT = "content/layout/v1";
106
135
  export declare const KNOWN_TYPE_URIS: readonly string[];
107
136
  export declare const TYPE_RESOLUTION_STATUSES: readonly ["ok", "implicit", "unknown_type"];
package/dist/checks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Content-canonicalization, content-hash, and typed-standards envelope checks
2
- // (spec §9.2 checks #3, #4, #6, #12, #13, #14, #15, #16) + the package-level
2
+ // (spec §9.2 checks #3, #4, #6, #12, #13, #14, #15, #16, #17) + the package-level
3
3
  // blob-ref walker (#9) — browser-safe.
4
4
  //
5
5
  // Factored verbatim from the server `verify.ts` (WS2); these functions were
@@ -8,7 +8,7 @@
8
8
  // for pre-v0.1 packages that omit the corresponding field.
9
9
  import { computeEnvelopeHash, computeContentHashSha256, isMultihashContentHash, KNOWN_CANONICALIZATION_RULES, LEGACY_JSON_CANONICALIZATION, DATHERE_AG_JUPYTER_CANONICALIZATION, RAW_BYTES_CANONICALIZATION, } from "./canonicalization.js";
10
10
  import { sha256Hex } from "./primitives.js";
11
- import { captureVocabForProfile, resolveProfileType } from "./profiles.js";
11
+ import { captureVocabForProfile, isContentProfileConsistent, resolveProfileType } from "./profiles.js";
12
12
  import { isBlobRef, parseBlobRef, verifyBlobRef, } from "./blob-ref.js";
13
13
  import { isKeyDerivedIdentifier } from "./did-key.js";
14
14
  import { compareKeyDerivedIdentifier } from "./self-certifying.js";
@@ -34,7 +34,12 @@ export const CONTENT_CANONICALIZATION_STATUSES = [
34
34
  * `unknown_canonicalization_rule` (renders; check #4 then cannot recompute). An
35
35
  * absent field on a pre-v0.1 package → `implicit`, inferring the rule from
36
36
  * contentProfile / producerProfile (datHere → dathere-ag-jupyter/v1; otherwise
37
- * legacy-json/v1).
37
+ * legacy-json/v1). A `producerProfile` reads as datHere when the subtype admits
38
+ * `"datHere"` and not `"default"` under the §8.1.1 invariant
39
+ * (`isContentProfileConsistent`): with no row, exactly when it begins with
40
+ * `ai-assisted-analysis/datHere`, as before; a subtype whose row admits both, as
41
+ * `ai-assisted-analysis/civicaitools-v1` does, leaves the choice to
42
+ * `contentProfile`.
38
43
  */
39
44
  export function resolveContentCanonicalization(pkg) {
40
45
  const raw = pkg['contentCanonicalization'];
@@ -54,7 +59,9 @@ export function resolveContentCanonicalization(pkg) {
54
59
  ? pkg['producerProfile']
55
60
  : undefined;
56
61
  const isDatHere = contentProfile === 'datHere' ||
57
- (producerProfile?.startsWith('ai-assisted-analysis/datHere') ?? false);
62
+ (producerProfile !== undefined &&
63
+ isContentProfileConsistent(producerProfile, 'datHere') &&
64
+ !isContentProfileConsistent(producerProfile, 'default'));
58
65
  return {
59
66
  status: 'implicit',
60
67
  rule: isDatHere
@@ -197,7 +204,7 @@ export async function verifyContentHashWithFetch(pkg, resolution, legacyExternal
197
204
  return first;
198
205
  return verifyContentHash(pkg, resolution, legacyExternalHash, bytes);
199
206
  }
200
- // --- Content-profile check (spec §9.2 check #16; hub ADR-0029 §5) ---
207
+ // --- Content-profile check (spec §9.2 check #16; hub ADR-0029 §5, ADR-0038 §3) ---
201
208
  /** The known `metadata.contentProfile` values (spec §8.1.2). */
202
209
  export const KNOWN_CONTENT_PROFILES = ['default', 'datHere'];
203
210
  export const CONTENT_PROFILE_STATUSES = [
@@ -211,8 +218,10 @@ export const CONTENT_PROFILE_STATUSES = [
211
218
  * - key absent → `contentProfile_absent` (read as `"default"`, spec §8.1.2).
212
219
  * - present and neither `"default"` nor `"datHere"` → `contentProfile_unknown`
213
220
  * (reported, not passed; consistency is not judged).
214
- * - present, known, `producerProfile` present, and the ADR-0006 §2 invariant
215
- * fails (`contentProfile === "datHere"` iff `producerProfile` starts with
221
+ * - present, known, `producerProfile` present, and the §8.1.1 invariant fails
222
+ * (ADR-0006 §2 as amended by hub ADR-0038 §3: the content profile is not one
223
+ * the subtype's row admits, `isContentProfileConsistent`; a subtype with no
224
+ * row admits `"datHere"` exactly when it begins with
216
225
  * `ai-assisted-analysis/datHere`) → `contentProfile_inconsistent`.
217
226
  * - otherwise → `ok`.
218
227
  * The invariant is compared only when both fields are present. No status is an
@@ -234,15 +243,44 @@ export function checkContentProfile(pkg) {
234
243
  if (typeof raw !== 'string' || !KNOWN_CONTENT_PROFILES.includes(raw)) {
235
244
  return { status: 'contentProfile_unknown', ...base };
236
245
  }
237
- if (producerProfile !== undefined) {
238
- const isDatHere = raw === 'datHere';
239
- const profileIsDatHere = producerProfile.startsWith('ai-assisted-analysis/datHere');
240
- if (isDatHere !== profileIsDatHere) {
241
- return { status: 'contentProfile_inconsistent', ...base };
242
- }
246
+ if (producerProfile !== undefined && !isContentProfileConsistent(producerProfile, raw)) {
247
+ return { status: 'contentProfile_inconsistent', ...base };
243
248
  }
244
249
  return { status: 'ok', ...base };
245
250
  }
251
+ // --- Content-profile system prompt (spec §9.2 check #17; hub ADR-0038 §6) ---
252
+ export const SYSTEM_PROMPT_STATUSES = ['ok', 'skillText_absent', 'not_applicable'];
253
+ /**
254
+ * Check #17 — the system prompt §8.7.1 requirement 2 requires of a
255
+ * `datHere`-content-profile package (hub ADR-0038 §6).
256
+ * - `metadata.contentProfile` not `"datHere"` (absent, `"default"` or unknown)
257
+ * → `not_applicable`, and nothing else is read.
258
+ * - `"datHere"` and `skillMetadata.skillText` a non-empty string → `ok`
259
+ * (`form: 'string'`), or a BlobRef → `ok` (`form: 'blobRef'`).
260
+ * - `"datHere"` and the key absent, empty, or neither a string nor a BlobRef
261
+ * → `skillText_absent`.
262
+ * `skillText_absent` is an attention status, never a failure: it says nothing
263
+ * about the envelope, the signature or the notebook. The check does not resolve
264
+ * a BlobRef (check #9 does), and cannot check that the text is the prompt the
265
+ * model ran under.
266
+ */
267
+ export function checkSystemPrompt(pkg) {
268
+ const metadata = pkg['metadata'];
269
+ const contentProfile = typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)
270
+ ? metadata['contentProfile']
271
+ : undefined;
272
+ if (contentProfile !== 'datHere')
273
+ return { status: 'not_applicable' };
274
+ const skillMetadata = pkg['skillMetadata'];
275
+ const skillText = typeof skillMetadata === 'object' && skillMetadata !== null && !Array.isArray(skillMetadata)
276
+ ? skillMetadata['skillText']
277
+ : undefined;
278
+ if (typeof skillText === 'string' && skillText.length > 0)
279
+ return { status: 'ok', form: 'string' };
280
+ if (isBlobRef(skillText))
281
+ return { status: 'ok', form: 'blobRef' };
282
+ return { status: 'skillText_absent' };
283
+ }
246
284
  // --- Typed-standards envelope checks (spec §9.2 checks #12, #14, #15) ---
247
285
  // The `content/layout/v1` sub-type (spec §8.14, hub ADR-0034): a standalone
248
286
  // assertion of how a host's page arranges a node's parts. Registered for check
package/dist/lifecycle.js CHANGED
@@ -319,7 +319,8 @@ function isAbsent(v) {
319
319
  }
320
320
  /** Each payload field §8.12.1 requires of `type` that `node` lacks, by name. */
321
321
  function missingRequiredFields(node, type) {
322
- return (ATTESTATION_REQUIRED_FIELDS[type] ?? []).filter((f) => isAbsent(node[f]));
322
+ const required = Object.hasOwn(ATTESTATION_REQUIRED_FIELDS, type) ? ATTESTATION_REQUIRED_FIELDS[type] : [];
323
+ return required.filter((f) => isAbsent(node[f]));
323
324
  }
324
325
  /**
325
326
  * Check one `attestation/*` node on its own (spec §8.12.3), for `withdraws`,
@@ -343,7 +344,7 @@ function missingRequiredFields(node, type) {
343
344
  export function checkAttestationNode(carried, context = {}) {
344
345
  const node = carried.node;
345
346
  const type = pickString(node['type']) ?? '';
346
- const rule = ATTESTATION_AUTHORIZATION_RULES[type] ?? null;
347
+ const rule = Object.hasOwn(ATTESTATION_AUTHORIZATION_RULES, type) ? ATTESTATION_AUTHORIZATION_RULES[type] : null;
347
348
  const verdict = verifyAttestationNode(node, carried.nodeId, carried.signature ?? null);
348
349
  const missingFields = missingRequiredFields(node, type);
349
350
  const publicKey = carried.signature?.publicKey ?? '';
@@ -18,3 +18,32 @@ export declare function resolveProfileType(producerProfile?: string | null, cont
18
18
  * `producerProfile_bundle_unresolved` degraded case).
19
19
  */
20
20
  export declare function captureVocabForProfile(producerProfile?: string | null, contentProfile?: string | null): readonly CaptureMethod[] | undefined;
21
+ /** A guidance bundle pinned by its address and the SHA-256 of its bytes. */
22
+ export interface ProducerProfileBundleRef {
23
+ readonly url: string;
24
+ /** Lowercase hex SHA-256 of the bundle file's bytes. */
25
+ readonly sha256: string;
26
+ }
27
+ /** One built subtype's row. */
28
+ export interface ProducerProfileSubtypeRow {
29
+ /** The `metadata.contentProfile` values the subtype admits. */
30
+ readonly admits: readonly string[];
31
+ /** The subtype's guidance bundle, when one is pinned. */
32
+ readonly bundle?: ProducerProfileBundleRef;
33
+ }
34
+ /**
35
+ * The subtype table, keyed by the full `producerProfile` value. The
36
+ * `ai-assisted-analysis/civicaitools-v1` bundle is revision 2 (2026-11-02), as
37
+ * hub ADR-0038's amendment of 2026-11-02 records its address and digest; a later
38
+ * revision moves the digest and this pin, and nothing else.
39
+ */
40
+ export declare const PRODUCER_PROFILE_SUBTYPES: Readonly<Record<string, ProducerProfileSubtypeRow>>;
41
+ /**
42
+ * The §8.1.1 consistency invariant, for a package that carries both
43
+ * `producerProfile` and `metadata.contentProfile`: whether the content profile is
44
+ * one the subtype's row admits. A subtype with no row admits `"datHere"` exactly
45
+ * when it begins with `ai-assisted-analysis/datHere`, and `"default"` exactly when
46
+ * it does not: the reading every earlier release applied, so a record whose
47
+ * subtype has no row reads as before. Check #16 reads it.
48
+ */
49
+ export declare function isContentProfileConsistent(producerProfile: string, contentProfile: string): boolean;
package/dist/profiles.js CHANGED
@@ -40,5 +40,38 @@ export function resolveProfileType(producerProfile, contentProfile) {
40
40
  * `producerProfile_bundle_unresolved` degraded case).
41
41
  */
42
42
  export function captureVocabForProfile(producerProfile, contentProfile) {
43
- return PROFILE_CAPTURE_VOCAB[resolveProfileType(producerProfile, contentProfile)];
43
+ const profileType = resolveProfileType(producerProfile, contentProfile);
44
+ return Object.hasOwn(PROFILE_CAPTURE_VOCAB, profileType) ? PROFILE_CAPTURE_VOCAB[profileType] : undefined;
45
+ }
46
+ /**
47
+ * The subtype table, keyed by the full `producerProfile` value. The
48
+ * `ai-assisted-analysis/civicaitools-v1` bundle is revision 2 (2026-11-02), as
49
+ * hub ADR-0038's amendment of 2026-11-02 records its address and digest; a later
50
+ * revision moves the digest and this pin, and nothing else.
51
+ */
52
+ export const PRODUCER_PROFILE_SUBTYPES = Object.freeze({
53
+ // No bundle pinned: the integration partner's, not this table's to name.
54
+ 'ai-assisted-analysis/datHere': Object.freeze({ admits: Object.freeze(['datHere']) }),
55
+ 'ai-assisted-analysis/civicaitools-v1': Object.freeze({
56
+ admits: Object.freeze(['datHere', 'default']),
57
+ bundle: Object.freeze({
58
+ url: 'https://raw.githubusercontent.com/npstorey/civic-ai-tools/v0.1.14-typed-standards-spec/docs/profiles/ai-assisted-analysis/civicaitools-v1/bundle.json',
59
+ sha256: '45bf3ab479325a791829ed9987f5f291fea44238b54bfb598f32f306f574eb9b',
60
+ }),
61
+ }),
62
+ });
63
+ const DATHERE_PREFIX = 'ai-assisted-analysis/datHere';
64
+ /**
65
+ * The §8.1.1 consistency invariant, for a package that carries both
66
+ * `producerProfile` and `metadata.contentProfile`: whether the content profile is
67
+ * one the subtype's row admits. A subtype with no row admits `"datHere"` exactly
68
+ * when it begins with `ai-assisted-analysis/datHere`, and `"default"` exactly when
69
+ * it does not: the reading every earlier release applied, so a record whose
70
+ * subtype has no row reads as before. Check #16 reads it.
71
+ */
72
+ export function isContentProfileConsistent(producerProfile, contentProfile) {
73
+ if (Object.hasOwn(PRODUCER_PROFILE_SUBTYPES, producerProfile)) {
74
+ return PRODUCER_PROFILE_SUBTYPES[producerProfile].admits.includes(contentProfile);
75
+ }
76
+ return producerProfile.startsWith(DATHERE_PREFIX) ? contentProfile === 'datHere' : contentProfile === 'default';
44
77
  }
@@ -0,0 +1,12 @@
1
+ import { type RekorInclusionProof, type RekorInclusionResult } from './rekor-inclusion.ts';
2
+ /** Whether a log entry body records `packageHash`: `spec.data.hash` is `sha512`
3
+ * with the value `rekorHashForPackage(packageHash)`. An empty hash, or a body that
4
+ * does not decode, records nothing. */
5
+ export declare function entryRecordsPackage(entryBody: string | Uint8Array, packageHash: string): boolean;
6
+ /**
7
+ * Check 8's inclusion verdict for a record. An entry that does not record
8
+ * `packageHash` reads `inclusionVerified: false` and `checkpointVerified: false`,
9
+ * with the proof's `treeSize` and `leafIndex` and no `reason` (the proof is not
10
+ * evaluated). Otherwise the verdict is `verifyRekorInclusion`'s.
11
+ */
12
+ export declare function verifyRekorInclusionForRecord(entryBody: string | Uint8Array, inclusionProof: RekorInclusionProof, packageHash: string): RekorInclusionResult;
@@ -0,0 +1,48 @@
1
+ // Check 8's inclusion verdict, bound to the record (spec §9.2 check #8) —
2
+ // browser-safe, offline.
3
+ //
4
+ // `verifyRekorInclusion` says whether a log entry is in a log the pinned key
5
+ // vouches for. A verdict about a record also needs the entry to be that record's:
6
+ // its body's `spec.data.hash` is `sha512` with the value
7
+ // `rekorHashForPackage(packageHash)`, the comparison `verifyRekorEntry` makes for
8
+ // hash parity. Both sources of a verdict read it here: the entry body and proof an
9
+ // input carries (`verifyRecord`) and an entry `verifyRekorEntry` fetches.
10
+ //
11
+ // Internal: `index.ts` does not re-export this module.
12
+ import { base64ToBytes } from "./primitives.js";
13
+ import { rekorHashForPackage } from "./signature.js";
14
+ import { verifyRekorInclusion, } from "./rekor-inclusion.js";
15
+ /** Whether a log entry body records `packageHash`: `spec.data.hash` is `sha512`
16
+ * with the value `rekorHashForPackage(packageHash)`. An empty hash, or a body that
17
+ * does not decode, records nothing. */
18
+ export function entryRecordsPackage(entryBody, packageHash) {
19
+ if (!packageHash)
20
+ return false;
21
+ try {
22
+ const bytes = typeof entryBody === 'string' ? base64ToBytes(entryBody) : entryBody;
23
+ const body = JSON.parse(new TextDecoder().decode(bytes));
24
+ const hash = body
25
+ ?.spec?.data?.hash;
26
+ return hash?.algorithm === 'sha512' && hash.value === rekorHashForPackage(packageHash);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Check 8's inclusion verdict for a record. An entry that does not record
34
+ * `packageHash` reads `inclusionVerified: false` and `checkpointVerified: false`,
35
+ * with the proof's `treeSize` and `leafIndex` and no `reason` (the proof is not
36
+ * evaluated). Otherwise the verdict is `verifyRekorInclusion`'s.
37
+ */
38
+ export function verifyRekorInclusionForRecord(entryBody, inclusionProof, packageHash) {
39
+ if (!entryRecordsPackage(entryBody, packageHash)) {
40
+ return {
41
+ inclusionVerified: false,
42
+ checkpointVerified: false,
43
+ treeSize: inclusionProof.treeSize,
44
+ leafIndex: inclusionProof.logIndex,
45
+ };
46
+ }
47
+ return verifyRekorInclusion(entryBody, inclusionProof);
48
+ }
@@ -53,7 +53,9 @@ export interface RekorInclusionResult {
53
53
  origin?: string;
54
54
  /** The matched anchor's key hint (hex), for display / provenance. */
55
55
  keyHint?: string;
56
- /** The first thing that failed, when not fully verified. */
56
+ /** The first thing that failed, when not fully verified. Absent when
57
+ * `verifyRecord` or `verifyRekorEntry` found that the entry does not record the
58
+ * package's hash: the proof is then not evaluated. */
57
59
  reason?: RekorInclusionFailReason;
58
60
  }
59
61
  /**
@@ -103,7 +105,9 @@ export declare function verifyCheckpointSignature(checkpoint: ParsedCheckpoint,
103
105
  * is the carried `verification.inclusionProof`. Returns a graded verdict:
104
106
  * `inclusionVerified` (the proof folds to its root) and `checkpointVerified` (that
105
107
  * root is in a checkpoint a pinned log key signed). The strong property — "this
106
- * entry is in a log the sigstore key vouches for" — is both being true.
108
+ * entry is in a log the sigstore key vouches for" — is both being true. This
109
+ * export checks inclusion alone; `verifyRecord` (`verifyEvidence`) applies the
110
+ * record binding.
107
111
  */
108
112
  export declare function verifyRekorInclusion(entryBody: string | Uint8Array, inclusionProof: RekorInclusionProof, anchors?: readonly RekorLogAnchor[]): RekorInclusionResult;
109
113
  export {};
@@ -239,7 +239,9 @@ export function verifyCheckpointSignature(checkpoint, anchors) {
239
239
  * is the carried `verification.inclusionProof`. Returns a graded verdict:
240
240
  * `inclusionVerified` (the proof folds to its root) and `checkpointVerified` (that
241
241
  * root is in a checkpoint a pinned log key signed). The strong property — "this
242
- * entry is in a log the sigstore key vouches for" — is both being true.
242
+ * entry is in a log the sigstore key vouches for" — is both being true. This
243
+ * export checks inclusion alone; `verifyRecord` (`verifyEvidence`) applies the
244
+ * record binding.
243
245
  */
244
246
  export function verifyRekorInclusion(entryBody, inclusionProof, anchors = REKOR_LOG_ANCHORS) {
245
247
  const base = {
package/dist/rekor.d.ts CHANGED
@@ -7,7 +7,8 @@ export interface RekorVerifyResult {
7
7
  integratedTime?: number;
8
8
  logEntryUrl?: string;
9
9
  /** Cryptographic Merkle-inclusion verdict (#119), when the fetched entry carries
10
- * an inclusion proof. Additive — `verified` (hash-parity) is unchanged. */
10
+ * an inclusion proof; not verified when the entry does not record this package's
11
+ * hash. Additive — `verified` (hash-parity) is unchanged. */
11
12
  inclusion?: RekorInclusionResult;
12
13
  }
13
14
  /**
package/dist/rekor.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // instead of `Buffer`, and the network fetch is injected.
10
10
  import { rekorHashForPackage } from "./signature.js";
11
11
  import { base64ToBytes } from "./primitives.js";
12
- import { verifyRekorInclusion, } from "./rekor-inclusion.js";
12
+ import { verifyRekorInclusionForRecord } from "./rekor-binding.js";
13
13
  /**
14
14
  * Verify that a Rekor transparency log entry is consistent with a published
15
15
  * package.
@@ -31,7 +31,7 @@ export async function verifyRekorEntry(entryId, packageHash, options = {}) {
31
31
  return { verified: false };
32
32
  }
33
33
  const result = (await response.json());
34
- const entry = result[entryId] || Object.values(result)[0];
34
+ const entry = (Object.hasOwn(result, entryId) ? result[entryId] : undefined) || Object.values(result)[0];
35
35
  if (!entry)
36
36
  return { verified: false };
37
37
  // Decode the base64 body and cross-check both hash algorithm and value.
@@ -43,9 +43,11 @@ export async function verifyRekorEntry(entryId, packageHash, options = {}) {
43
43
  const verified = recordedAlgo === 'sha512' && recordedHash === expectedHash;
44
44
  // Deepen #8 (civic-ai-tools-website#119): when the entry carries an inclusion
45
45
  // proof, cryptographically verify Merkle inclusion against the pinned log key —
46
- // the same offline check a carried bundle runs, here over the fetched entry.
46
+ // the same offline check a carried bundle runs, here over the fetched entry, and
47
+ // bound to this record: an entry that does not record `packageHash` is not
48
+ // verified (`rekor-binding.ts`).
47
49
  const proof = entry.verification?.inclusionProof;
48
- const inclusion = proof && entry.body ? verifyRekorInclusion(entry.body, proof) : undefined;
50
+ const inclusion = proof && entry.body ? verifyRekorInclusionForRecord(entry.body, proof, packageHash) : undefined;
49
51
  return {
50
52
  verified,
51
53
  logIndex: entry.logIndex,
package/dist/verify.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type RekorInclusionProof, type RekorInclusionResult } from './rekor-inclusion.ts';
2
2
  import { type Rfc3161VerifyResult } from './rfc3161.ts';
3
3
  import { type KeyTrustResult, type TrustRegistry, type TrustRegistryProvenance } from './trust-registry.ts';
4
- import { type ContentCanonicalizationResolution, type ContentHashCheck, type TypeResolution, type SignerIdentityCheck, type SigningKeyIdConsistencyCheck, type CaptureMethodVocabCheck, type ContentProfileCheck, type BlobRefVerification } from './checks.ts';
4
+ import { type ContentCanonicalizationResolution, type ContentHashCheck, type TypeResolution, type SignerIdentityCheck, type SigningKeyIdConsistencyCheck, type CaptureMethodVocabCheck, type ContentProfileCheck, type SystemPromptCheck, type BlobRefVerification } from './checks.ts';
5
5
  import { type LifecycleResolution } from './lifecycle.ts';
6
6
  import type { FetchLike } from './types.ts';
7
7
  /** The parsed signature envelope (`buildCommitmentView`'s `signature`). */
@@ -140,7 +140,8 @@ export interface VerifyResult {
140
140
  } | null;
141
141
  rekorIntegratedTime?: number;
142
142
  /** Cryptographic Merkle-inclusion verdict (#119): null when no proof was
143
- * available, else the graded result (`inclusionVerified` / `checkpointVerified`).
143
+ * available, else the graded result (`inclusionVerified` / `checkpointVerified`),
144
+ * both false when the log entry does not record `packageHash`.
144
145
  * Additive to `rekorVerified` (hash-parity). */
145
146
  rekorInclusion: RekorInclusionResult | null;
146
147
  hasRekor: boolean;
@@ -163,6 +164,9 @@ export interface VerifyResult {
163
164
  captureMethodVocab: CaptureMethodVocabCheck | null;
164
165
  /** Check #16 — `metadata.contentProfile` (hub ADR-0029 §5). */
165
166
  contentProfile: ContentProfileCheck | null;
167
+ /** Check #17 — whether a `datHere`-content-profile package carries
168
+ * `skillMetadata.skillText` (hub ADR-0038 §6). Null when the package is absent. */
169
+ systemPrompt: SystemPromptCheck | null;
166
170
  lifecycle: LifecycleResolution;
167
171
  }
168
172
  /**
package/dist/verify.js CHANGED
@@ -31,11 +31,11 @@
31
31
  import { recomputePackageHash } from "./checks.js";
32
32
  import { verifySignature } from "./signature.js";
33
33
  import { verifyRekorEntry } from "./rekor.js";
34
- import { verifyRekorInclusion, } from "./rekor-inclusion.js";
34
+ import { verifyRekorInclusionForRecord } from "./rekor-binding.js";
35
35
  import { verifyRfc3161Timestamp } from "./rfc3161.js";
36
36
  import { verifyKeyTrust, legacyEmbeddedKeyTrust, } from "./trust-registry.js";
37
37
  import { applyKeyDerivedRegistryRule, applySelfCertifiedKeyTrust, compareKeyDerivedIdentifier, } from "./self-certifying.js";
38
- import { resolveContentCanonicalization, verifyContentHashWithFetch, resolvePackageType, checkSignerIdentity, checkSigningKeyIdConsistency, checkCaptureMethodVocab, checkContentProfile, verifyPackageBlobRefs, } from "./checks.js";
38
+ import { resolveContentCanonicalization, verifyContentHashWithFetch, resolvePackageType, checkSignerIdentity, checkSigningKeyIdConsistency, checkCaptureMethodVocab, checkContentProfile, checkSystemPrompt, verifyPackageBlobRefs, } from "./checks.js";
39
39
  import { resolveLifecycleFromLegacyColumns, } from "./lifecycle.js";
40
40
  /**
41
41
  * A fetcher that sends at most one request per URL and replays that request's
@@ -128,6 +128,7 @@ export async function verifyRecord(input, deps) {
128
128
  // Merkle inclusion (#119) is verified cryptographically. The carried proof + body
129
129
  // verify inclusion OFFLINE (no fetch); otherwise the online fetch's entry carries
130
130
  // the proof. Carried-offline takes precedence — it is the zero-dependency property.
131
+ // Either way the entry must record this record's hash (`rekor-binding.ts`).
131
132
  let rekorVerified = null;
132
133
  let rekorDetails = null;
133
134
  let rekorIntegratedTime;
@@ -146,7 +147,7 @@ export async function verifyRecord(input, deps) {
146
147
  rekorInclusion = rekorResult.inclusion;
147
148
  }
148
149
  if (input.rekorInclusionProof && input.rekorEntryBody) {
149
- rekorInclusion = verifyRekorInclusion(input.rekorEntryBody, input.rekorInclusionProof);
150
+ rekorInclusion = verifyRekorInclusionForRecord(input.rekorEntryBody, input.rekorInclusionProof, packageHash);
150
151
  }
151
152
  // Step 3c — RFC 3161 timestamp (check #7, deep). Presence is `hasTimestamp`; when a
152
153
  // token + package hash are present, cryptographically verify the TSA signature over
@@ -204,7 +205,7 @@ export async function verifyRecord(input, deps) {
204
205
  : applyKeyDerivedRegistryRule(keyTrust, deps.registryProvenance);
205
206
  }
206
207
  // Step 5 — canonicalization, content-hash, and envelope checks
207
- // (#3/#4/#6/#12/#14/#15/#16). #4 fetches only for a raw-bytes/v1 BlobRef output.
208
+ // (#3/#4/#6/#12/#14/#15/#16/#17). #4 fetches only for a raw-bytes/v1 BlobRef output.
208
209
  let contentCanonicalization = null;
209
210
  let contentHashCheck = null;
210
211
  let typeResolution = null;
@@ -212,6 +213,7 @@ export async function verifyRecord(input, deps) {
212
213
  let signingKeyIdConsistency = null;
213
214
  let captureMethodVocab = null;
214
215
  let contentProfile = null;
216
+ let systemPrompt = null;
215
217
  if (pkg) {
216
218
  contentCanonicalization = resolveContentCanonicalization(pkg);
217
219
  contentHashCheck = await verifyContentHashWithFetch(pkg, contentCanonicalization, input.legacyExternalHash ?? packageHash, { fetch: blobFetch });
@@ -222,6 +224,7 @@ export async function verifyRecord(input, deps) {
222
224
  }
223
225
  captureMethodVocab = checkCaptureMethodVocab(pkg);
224
226
  contentProfile = checkContentProfile(pkg);
227
+ systemPrompt = checkSystemPrompt(pkg);
225
228
  }
226
229
  // Step 6 — lifecycle (check #10). Server-deeper resolution wins when supplied;
227
230
  // otherwise derive STATE from the sidecar's lifecycle (the portable depth).
@@ -257,6 +260,7 @@ export async function verifyRecord(input, deps) {
257
260
  signingKeyIdConsistency,
258
261
  captureMethodVocab,
259
262
  contentProfile,
263
+ systemPrompt,
260
264
  lifecycle,
261
265
  };
262
266
  }
package/dist/x509.js CHANGED
@@ -162,7 +162,7 @@ function isImplementedCertSignatureAlgorithm(oid) {
162
162
  * apart before it calls this.
163
163
  */
164
164
  export async function verifyCertSignatureRsa(cert, issuerSpkiDer) {
165
- const hash = RSA_SIG_HASH[cert.sigAlgOid];
165
+ const hash = isImplementedCertSignatureAlgorithm(cert.sigAlgOid) ? RSA_SIG_HASH[cert.sigAlgOid] : undefined;
166
166
  if (!hash)
167
167
  return false;
168
168
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typedstandards/verify-core",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "Portable, browser-safe verification core for Typed Standards record packages — the §9.2 check suite, single-sourced so it verifies identically on a server and in the browser.",
5
5
  "license": "MIT",
6
6
  "author": "Nathan Storey",
@@ -31,12 +31,12 @@
31
31
  "prepublishOnly": "npm run clean && npm run build"
32
32
  },
33
33
  "dependencies": {
34
- "@noble/curves": "^2.2.0",
34
+ "@noble/curves": "^2.4.0",
35
35
  "@noble/hashes": "^2.2.0",
36
- "canonicalize": "^3.0.0"
36
+ "canonicalize": "^5.1.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^20",
39
+ "@types/node": "^22",
40
40
  "typescript": "^5.9.3"
41
41
  },
42
42
  "engines": {