@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 +60 -1
- package/dist/checks.d.ts +32 -3
- package/dist/checks.js +51 -13
- package/dist/lifecycle.js +3 -2
- package/dist/profiles.d.ts +29 -0
- package/dist/profiles.js +34 -1
- package/dist/rekor.js +1 -1
- package/dist/verify.d.ts +4 -1
- package/dist/verify.js +5 -2
- package/dist/x509.js +1 -1
- package/package.json +4 -4
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–#
|
|
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
|
|
98
|
-
*
|
|
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
|
|
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
|
|
215
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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 ?? '';
|
package/dist/profiles.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
34
|
+
"@noble/curves": "^2.4.0",
|
|
35
35
|
"@noble/hashes": "^2.2.0",
|
|
36
|
-
"canonicalize": "^
|
|
36
|
+
"canonicalize": "^5.1.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^
|
|
39
|
+
"@types/node": "^22",
|
|
40
40
|
"typescript": "^5.9.3"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|