@typedstandards/verify-core 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,11 +1,70 @@
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.0 — 2026-10-30
10
+
11
+ The R10 release (civic-ai-tools#289; hub ADR-0038 §3, §5 and §6, specification v0.1.13 §8.1.1,
12
+ §8.6 and §9.2). **A minor bump**: new exports, a new `VerifyResult` field, and one combination
13
+ check #16 newly reads `ok`.
14
+
15
+ - **The subtype table (new exports `PRODUCER_PROFILE_SUBTYPES`, `isContentProfileConsistent`, and
16
+ the types `ProducerProfileSubtypeRow` and `ProducerProfileBundleRef`).** Beside the profile-type
17
+ table, one frozen row per built subtype of `ai-assisted-analysis`, keyed by the full
18
+ `producerProfile` value:
19
+ - `ai-assisted-analysis/datHere` admits `datHere`, with no bundle pinned;
20
+ - `ai-assisted-analysis/civicaitools-v1` admits `datHere` and `default`, with its guidance
21
+ bundle pinned by URL
22
+ (`https://raw.githubusercontent.com/npstorey/civic-ai-tools/v0.1.13-typed-standards-spec/docs/profiles/ai-assisted-analysis/civicaitools-v1/bundle.json`)
23
+ and SHA-256 (`724e399e02db8993041b64982eec731505084bec2ec5e6ca58fda2de8087a281`), revision 1.
24
+
25
+ `isContentProfileConsistent(producerProfile, contentProfile)` is the §8.1.1 invariant over the
26
+ table: the content profile is one the subtype's row admits; a subtype with no row admits
27
+ `"datHere"` exactly when it begins with `ai-assisted-analysis/datHere`, and `"default"` exactly
28
+ when it does not. Only the table's own keys are rows. The row does not change the capture
29
+ vocabulary: `PROFILE_CAPTURE_VOCAB`, `resolveProfileType` and `captureVocabForProfile` (check #15)
30
+ are unchanged.
31
+ - **Check #16 reads the predicate.** `checkContentProfile` reports `contentProfile_inconsistent`
32
+ when `isContentProfileConsistent` is false, in place of the `startsWith` comparison. The no-row
33
+ clause is that comparison, so every status is unchanged except one: `datHere` under
34
+ `ai-assisted-analysis/civicaitools-v1` reads `ok` (it read `contentProfile_inconsistent`). No
35
+ published record carries that combination.
36
+ - **Check #3's implicit rule reads the predicate.** For a package with no
37
+ `contentCanonicalization`, a `producerProfile` reads as datHere when its subtype admits
38
+ `"datHere"` and not `"default"`, which is the `startsWith` reading for the `datHere` row and every
39
+ subtype with no row; under `ai-assisted-analysis/civicaitools-v1` the choice is
40
+ `metadata.contentProfile`'s. Every result is unchanged.
41
+ - **Check #17, the content-profile system prompt (new exports `checkSystemPrompt`,
42
+ `SYSTEM_PROMPT_STATUSES`, and the types `SystemPromptStatus` and `SystemPromptCheck`).** When
43
+ `metadata.contentProfile` is `"datHere"`, `skillMetadata.skillText` as a non-empty string reads
44
+ `ok` (`form: 'string'`) and as a BlobRef `ok` (`form: 'blobRef'`); absent, empty, or neither
45
+ reads `skillText_absent`. Any other content profile (absent, `"default"` or unknown) reads
46
+ `not_applicable`. `skillText_absent` is an attention status and never a failure. The check does
47
+ not resolve a BlobRef (check #9 does).
48
+ - **`VerifyResult` gains `systemPrompt`** (`SystemPromptCheck | null`, null when the package is
49
+ absent), set by `verifyRecord` beside `contentProfile`. Every other field of the result is
50
+ unchanged. A consumer that builds a `VerifyResult` literal needs the new field.
51
+ - **Every earlier fixture reads as before.** Over the signed bundles, signed documents and package
52
+ objects in this repository's fixtures and the hub harness's `reference-golden.json` and
53
+ `in-loop-golden.json`, `verifyRecord`, `checkContentProfile`, `resolveContentCanonicalization`
54
+ and `captureVocabForProfile` give byte-identical output to 0.14.0 once `systemPrompt` is removed.
55
+ Check #17 reads `skillText_absent` on the five fixture packages that declare `datHere` without
56
+ `skillText` (the hub reference golden's `v01-dathere-empty-notebook` and
57
+ `v01-dathere-executed-notebook`, their copies in produce-core's `reference-golden.json`, and the
58
+ hub's `in-loop-golden.json`), `ok` on the site's `q15-d67b8e` record, and `not_applicable` on
59
+ every other.
60
+ - **Every lookup of a verify-core table by a value a record or bundle supplies reads only the
61
+ table's own entries;** a malformed value reads as an unknown one. Every well-formed value reads as
62
+ before.
63
+ - **A local copy of the bundle**, `src/__fixtures__/civicaitools-v1-guidance-bundle.json` (not
64
+ published), byte-identical to the hub file at tree `7060f5e3`. A test asserts its SHA-256,
65
+ `subtype`, `url` and admitted set against the row, without the network. Its provenance is in
66
+ `src/__fixtures__/README.md`.
67
+
9
68
  ## 0.14.0 — 2026-10-16
10
69
 
11
70
  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 1 (2026-10-30), as
37
+ * hub ADR-0038 §5 records its address and digest; a later revision moves the
38
+ * 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 1 (2026-10-30), as
49
+ * hub ADR-0038 §5 records its address and digest; a later revision moves the
50
+ * 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.13-typed-standards-spec/docs/profiles/ai-assisted-analysis/civicaitools-v1/bundle.json',
59
+ sha256: '724e399e02db8993041b64982eec731505084bec2ec5e6ca58fda2de8087a281',
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
  }
package/dist/rekor.js CHANGED
@@ -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.
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`). */
@@ -163,6 +163,9 @@ export interface VerifyResult {
163
163
  captureMethodVocab: CaptureMethodVocabCheck | null;
164
164
  /** Check #16 — `metadata.contentProfile` (hub ADR-0029 §5). */
165
165
  contentProfile: ContentProfileCheck | null;
166
+ /** Check #17 — whether a `datHere`-content-profile package carries
167
+ * `skillMetadata.skillText` (hub ADR-0038 §6). Null when the package is absent. */
168
+ systemPrompt: SystemPromptCheck | null;
166
169
  lifecycle: LifecycleResolution;
167
170
  }
168
171
  /**
package/dist/verify.js CHANGED
@@ -35,7 +35,7 @@ import { verifyRekorInclusion, } from "./rekor-inclusion.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
@@ -204,7 +204,7 @@ export async function verifyRecord(input, deps) {
204
204
  : applyKeyDerivedRegistryRule(keyTrust, deps.registryProvenance);
205
205
  }
206
206
  // 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.
207
+ // (#3/#4/#6/#12/#14/#15/#16/#17). #4 fetches only for a raw-bytes/v1 BlobRef output.
208
208
  let contentCanonicalization = null;
209
209
  let contentHashCheck = null;
210
210
  let typeResolution = null;
@@ -212,6 +212,7 @@ export async function verifyRecord(input, deps) {
212
212
  let signingKeyIdConsistency = null;
213
213
  let captureMethodVocab = null;
214
214
  let contentProfile = null;
215
+ let systemPrompt = null;
215
216
  if (pkg) {
216
217
  contentCanonicalization = resolveContentCanonicalization(pkg);
217
218
  contentHashCheck = await verifyContentHashWithFetch(pkg, contentCanonicalization, input.legacyExternalHash ?? packageHash, { fetch: blobFetch });
@@ -222,6 +223,7 @@ export async function verifyRecord(input, deps) {
222
223
  }
223
224
  captureMethodVocab = checkCaptureMethodVocab(pkg);
224
225
  contentProfile = checkContentProfile(pkg);
226
+ systemPrompt = checkSystemPrompt(pkg);
225
227
  }
226
228
  // Step 6 — lifecycle (check #10). Server-deeper resolution wins when supplied;
227
229
  // otherwise derive STATE from the sidecar's lifecycle (the portable depth).
@@ -257,6 +259,7 @@ export async function verifyRecord(input, deps) {
257
259
  signingKeyIdConsistency,
258
260
  captureMethodVocab,
259
261
  contentProfile,
262
+ systemPrompt,
260
263
  lifecycle,
261
264
  };
262
265
  }
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.0",
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": {