twin3-sdk 0.2.15-pre-alpha → 0.2.17-pre-alpha

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/README.md CHANGED
@@ -116,22 +116,22 @@ pip install -e /path/to/twin3-sdk
116
116
  * Any protocol or algorithm improvement in `twin3-sdk` is reflected in both applications in **0 milliseconds**.
117
117
 
118
118
  #### 2. Production Distribution
119
- `v0.2.15-pre-alpha` is the current release candidate: `0.2.15-pre-alpha` for npm and `0.2.15a0` for PyPI. Publication is pending. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This release stays pre-alpha. Marketplace or sales copy may offer it for evaluation; it is not a stable release, and the npm dist-tag is `next`, not `latest`.
119
+ `v0.2.17-pre-alpha` is the current release candidate: `0.2.17-pre-alpha` for npm and `0.2.17a0` for PyPI. It is published from main through the Cloud Build tag trigger; the previous version number was never published. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This release stays pre-alpha. Marketplace or sales copy may offer it for evaluation; it is not a stable release, and the npm dist-tag is `next`, not `latest`.
120
120
 
121
- `package.json` `engines` sets Node.js to `>=18` and does not set an npm engine. Any npm that can install a tarball can consume the package. Packing this repository was checked with npm 10 or newer; the stricter npm version in the release runbook applies only to Trusted Publishing.
121
+ `package.json` `engines` requires Node.js `>=22.23.4` and does not set an npm engine. CI covers patched Node 22, 24 and 26; Node 18 and 20 are outside the supported baseline. Packing this repository was checked with npm 10 or newer; the stricter npm version in the release runbook applies only to Trusted Publishing.
122
122
 
123
123
  After publication, install the exact released version from the registry:
124
124
 
125
125
  ```bash
126
- npm install twin3-sdk@0.2.15-pre-alpha
127
- pip install twin3-sdk==0.2.15a0
126
+ npm install twin3-sdk@0.2.17-pre-alpha
127
+ pip install twin3-sdk==0.2.17a0
128
128
  ```
129
129
 
130
130
  For local evaluation, you can also use the packed npm tarball. Do not install from a git URL or a version range:
131
131
 
132
132
  ```bash
133
133
  npm pack --ignore-scripts
134
- npm install ./twin3-sdk-0.2.15-pre-alpha.tgz
134
+ npm install ./twin3-sdk-0.2.17-pre-alpha.tgz
135
135
  ```
136
136
 
137
137
  ---
package/index.d.ts CHANGED
@@ -158,6 +158,71 @@ export interface DiscoveryEvidenceView {
158
158
  digest: string;
159
159
  }
160
160
  export declare function buildDiscoveryEvidenceView(summary: DiscoveryEvidenceSummary, options: { visitSourceStatus: "connected" | "not_connected"; googleAiSearchSourceStatus: "connected" | "not_connected" }): DiscoveryEvidenceView;
161
+ export declare const ENGINE_OBSERVATION_SCHEMA: "twin3-discovery-engine-observation-v1";
162
+ export declare const ENGINE_READING_SCHEMA: "twin3-discovery-engine-reading-v1";
163
+ export declare class EngineObservationError extends Error { code: string; }
164
+ export type EngineObservationEvidenceLevel = "server_observed_engine_answer" | "replay_api" | "owner_supplied_platform_export";
165
+ export interface EngineObservationInput {
166
+ engine: string;
167
+ prompt_id: string;
168
+ observed_at: number;
169
+ status: "answered" | "error";
170
+ model?: string | null;
171
+ need_id?: string | null;
172
+ paraphrase_of?: string | null;
173
+ run_id?: string | null;
174
+ host?: string | null;
175
+ retrieved?: boolean | null;
176
+ mentioned?: boolean | null;
177
+ cited?: boolean | null;
178
+ evidence_level?: EngineObservationEvidenceLevel;
179
+ evidence_strength?: EngineObservationEvidenceLevel;
180
+ answer?: string | null;
181
+ answer_digest?: string | null;
182
+ [extra: string]: unknown;
183
+ }
184
+ export interface EngineObservation {
185
+ schema: "twin3-discovery-engine-observation-v1";
186
+ host: string;
187
+ engine: string;
188
+ model: string | null;
189
+ prompt_id: string;
190
+ need_id: string | null;
191
+ run_id: string | null;
192
+ observed_at: number;
193
+ status: "answered" | "error";
194
+ retrieved: boolean | null;
195
+ mentioned: boolean | null;
196
+ cited: boolean | null;
197
+ evidence_level: EngineObservationEvidenceLevel;
198
+ answer_digest: string | null;
199
+ digest: string;
200
+ }
201
+ export interface EngineObservationSignalReading {
202
+ state: "measured" | "withheld" | "unknown";
203
+ observed: number;
204
+ positive: number;
205
+ rate_bp: number | null;
206
+ interval_bp: [number, number] | null;
207
+ }
208
+ export interface EngineObservationReading {
209
+ schema: "twin3-discovery-engine-reading-v1";
210
+ host: string;
211
+ valid: boolean;
212
+ source_level: boolean;
213
+ blockers: Array<"no_answered_observations" | "insufficient_repetitions" | "insufficient_paraphrases">;
214
+ caveats: Array<"window_shorter_than_21_days">;
215
+ window_utc: { first: string; last: string; span_days: number; observed_days: number } | null;
216
+ engines: string[];
217
+ by_engine: Record<string, { answered: number; errors: number; prompts: number; signals: Record<"retrieved" | "mentioned" | "cited", EngineObservationSignalReading> }>;
218
+ evidence_levels: EngineObservationEvidenceLevel[];
219
+ protocol: { min_runs_per_prompt: number; min_runs_for_source_coverage: number; min_paraphrases_per_need: number; window_days: number; interval: string; source: string };
220
+ non_claims: string[];
221
+ observation_digests: string[];
222
+ digest: string;
223
+ }
224
+ export declare function normalizeEngineObservation(row: EngineObservationInput, options: { host: string }): EngineObservation;
225
+ export declare function summarizeEngineObservations(observations: EngineObservation[], options: { host: string; sourceLevel?: boolean }): EngineObservationReading;
161
226
 
162
227
  export declare const DESCRIPTOR_CANONICALIZATION: "rfc8785";
163
228
  export declare const DESCRIPTOR_SIGNATURE_FIELDS: readonly ["signatures", "signatureStatus"];
@@ -853,6 +918,15 @@ export declare function didWebForHost(host: string): string;
853
918
  export declare function didWebUrl(did: string): string;
854
919
  export declare function resolveDidWeb(did: string, fetch: (method: string, url: string, headers: Record<string, string>, body: null) => [number, string | Uint8Array | null], maxBytes?: number): { did: string; url: string; method_id: string; key_id: string; public_key: string; };
855
920
  export declare function multibaseEd25519(raw: Uint8Array, multicodec?: boolean): string;
921
+ export type DidFetch = (method: string, url: string, headers: Record<string, string>, body: null) => [number, string | Uint8Array | null];
922
+ export type DidKeyPurpose = "assertionMethod" | "authentication" | "keyAgreement" | "capabilityInvocation" | "capabilityDelegation";
923
+ export interface DidDocumentKey { did: string; method_id: string; key_id: string; public_key: string; purpose: DidKeyPurpose | null; }
924
+ /** Ed25519 verification methods of a did:web document, restricted to the requested verification relationship (default `assertionMethod`). */
925
+ export declare function didWebDocumentKeys(doc: Record<string, unknown>, did: string, options?: { purpose?: DidKeyPurpose | null }): DidDocumentKey[];
926
+ /** Resolve one did:web key through the injected fetch; `kid` must select exactly one method (`did_kid_not_found` / `did_kid_ambiguous`). */
927
+ export declare function resolveDidWebKey(did: string, fetch: DidFetch, options?: { kid?: string | null; purpose?: DidKeyPurpose | null; maxBytes?: number }): DidDocumentKey & { url: string };
928
+ /** Select one Ed25519 (`OKP`/`Ed25519`, `alg` EdDSA if present) key from a JWKS by `kid`. */
929
+ export declare function jwksKey(jwks: Record<string, unknown>, kid: string): { kid: string; key_id: string; public_key: string };
856
930
  export declare const AuthorityAnchor: { readonly SUPPORTED_KINDS: readonly string[]; isValidKind(kind: string): boolean; };
857
931
  export declare function createKyaChallenge(params: {
858
932
  owner: string;
@@ -906,6 +980,9 @@ export interface TrustDelegation {
906
980
  export interface AuthorizationLease {
907
981
  bindings: KyaBinding[];
908
982
  delegations: TrustDelegation[];
983
+ /** Optional copy of the consumer registration the bindings were issued under; it is compared
984
+ * against `LeaseAuthority.consumer_registration` and is never authority on its own. */
985
+ registration?: ConsumerRegistrationStatement | null;
909
986
  }
910
987
  export interface BindingWindow { not_before: number; not_after: number; epoch: number; }
911
988
  export interface LeaseAuthority {
@@ -916,6 +993,10 @@ export interface LeaseAuthority {
916
993
  policy_revision: string;
917
994
  binding_windows: Record<string, BindingWindow>;
918
995
  reanchored_at: number;
996
+ /** A `twin3-trust-consumer-registration-v1` statement signed by `owner_public_key` (the vertical's
997
+ * root R). When present, a binding whose challenge was signed by the statement's `kya_owner` key
998
+ * verifies; bindings signed by R itself verify unchanged. `null` means no registration. */
999
+ consumer_registration?: ConsumerRegistrationStatement | null;
919
1000
  }
920
1001
  export interface LeaseAuthorityPort {
921
1002
  currentAuthority(tenant: string, owner: string): LeaseAuthority | Promise<LeaseAuthority>;
@@ -935,6 +1016,8 @@ export interface VerifiedLeaseRequest {
935
1016
  policy_revision: string;
936
1017
  binding_receipts: string[];
937
1018
  delegation_receipts: string[];
1019
+ /** Receipt of the verified consumer registration, or `null` when the authority carries none. */
1020
+ registration_receipt: string | null;
938
1021
  }
939
1022
  export declare class AuthorizationLeaseError extends ProtocolError {
940
1023
  constructor(code: string);
@@ -964,6 +1047,8 @@ export interface LeaseClearanceContext {
964
1047
  actor_key_id: string; purpose: string; occurrence_index: number;
965
1048
  binding_receipts: string[]; delegation_receipts: string[];
966
1049
  binding_windows: Array<BindingWindow | null>; usage: Record<string, number>;
1050
+ /** Present only when the authority carries a consumer registration. */
1051
+ registration_receipt?: string;
967
1052
  };
968
1053
  }
969
1054
  export type DetectorState = "allow" | "flag" | "block" | "unavailable" | "not_configured";
@@ -1790,3 +1875,78 @@ export declare function signDelegationWithSigner(delegation: Record<string, unkn
1790
1875
  options?: ExternalSignerOptions): Promise<Readonly<Record<string, unknown>>>;
1791
1876
  export declare function signEnvelopeWithSigner(envelope: Envelope, signer: ExternalEd25519Signer,
1792
1877
  options?: ExternalSignerOptions): Promise<Readonly<Envelope>>;
1878
+
1879
+ export declare const CONSUMER_REGISTRATION_SCHEMA: "twin3-trust-consumer-registration-v1";
1880
+ export type ConsumerRegistrationEnvironment = "sandbox" | "staging" | "production";
1881
+ export type ConsumerRegistrationKeyRole = "kya_owner" | "evidence";
1882
+ export interface ConsumerRegistrationKey {
1883
+ role: ConsumerRegistrationKeyRole;
1884
+ key_id: string;
1885
+ public_key: string;
1886
+ label?: string;
1887
+ }
1888
+ export interface ConsumerRegistrationSubject {
1889
+ consumer_id: string;
1890
+ namespace: string;
1891
+ environment: ConsumerRegistrationEnvironment;
1892
+ identity_kinds: Array<"lei" | "duns" | "human_id">;
1893
+ epoch: number;
1894
+ policy_revision: string;
1895
+ endpoints?: Record<string, string>;
1896
+ sdk_contract_digest?: string;
1897
+ }
1898
+ /** `twin3-trust-consumer-registration-v1`: the vertical's root R signs which consumer keys it authorizes.
1899
+ * Exactly one `kya_owner` key; `issuer.key_id` and the JWS `kid` are descriptive and are compared against
1900
+ * the injected root, never fetched. */
1901
+ export interface ConsumerRegistrationStatement {
1902
+ schema: "twin3-trust-consumer-registration-v1";
1903
+ issuer: { key_id: string; did?: string };
1904
+ subject: ConsumerRegistrationSubject;
1905
+ authorized_keys: ConsumerRegistrationKey[];
1906
+ not_before: number;
1907
+ not_after: number;
1908
+ signatures?: DescriptorSignature[];
1909
+ signatureStatus?: string;
1910
+ }
1911
+ export interface ConsumerRegistrationExpectation {
1912
+ consumer_id?: string;
1913
+ namespace?: string;
1914
+ environment?: ConsumerRegistrationEnvironment;
1915
+ epoch?: number;
1916
+ policy_revision?: string;
1917
+ identity_kind?: "lei" | "duns" | "human_id";
1918
+ }
1919
+ export interface VerifiedConsumerRegistration {
1920
+ receipt: string;
1921
+ issuer_key_id: string;
1922
+ subject: ConsumerRegistrationSubject;
1923
+ kya_owner_key: ConsumerRegistrationKey;
1924
+ evidence_keys: ConsumerRegistrationKey[];
1925
+ not_before: number;
1926
+ not_after: number;
1927
+ kid: string;
1928
+ jku: string | null;
1929
+ }
1930
+ export type ConsumerRegistrationErrorCode = "registration_schema_mismatch" | "registration_unknown_field"
1931
+ | "registration_field_required" | "registration_signature_invalid" | "registration_root_required"
1932
+ | "registration_root_mismatch" | "registration_kid_mismatch" | "registration_time_invalid"
1933
+ | "registration_not_yet_valid" | "registration_expired" | "registration_subject_mismatch"
1934
+ | "registration_key_unauthorized" | "registration_key_role_invalid" | "registration_required"
1935
+ | "registration_mismatch";
1936
+ export declare class ConsumerRegistrationError extends ProtocolError {
1937
+ readonly code: ConsumerRegistrationErrorCode;
1938
+ constructor(code: ConsumerRegistrationErrorCode);
1939
+ }
1940
+ /** Shape rules only (no signature); returns the canonical body. With `now`, also checks the validity window. */
1941
+ export declare function validateConsumerRegistration(statement: ConsumerRegistrationStatement, options?: { now?: number | null }): Omit<ConsumerRegistrationStatement, "signatures" | "signatureStatus">;
1942
+ /** SHA-256 hex of the RFC 8785 body without signature fields; the revocation reference for the statement. */
1943
+ export declare function registrationReceipt(statement: ConsumerRegistrationStatement): string;
1944
+ export declare function signConsumerRegistration(statement: ConsumerRegistrationStatement, privateKey: string | Buffer | import("node:crypto").KeyObject,
1945
+ options?: { kid?: string; jku?: string | null }): ConsumerRegistrationStatement & SignedDescriptor;
1946
+ export declare function signConsumerRegistrationWithSigner(statement: ConsumerRegistrationStatement, signer: ExternalEd25519Signer,
1947
+ options?: { kid?: string; jku?: string | null; timeoutMs?: number; signal?: AbortSignal }): Promise<ConsumerRegistrationStatement & SignedDescriptor>;
1948
+ /** Verify shape, root signature (`rootPublicKey` is injected, never fetched), optional `kid`, window at `now` and the expected subject. No network. */
1949
+ export declare function verifyConsumerRegistration(statement: ConsumerRegistrationStatement, options: {
1950
+ rootPublicKey: string | Buffer | import("node:crypto").KeyObject; expectedKid?: string | null; now: number;
1951
+ expected?: ConsumerRegistrationExpectation | null;
1952
+ }): VerifiedConsumerRegistration;
@@ -316,7 +316,8 @@ function createCounterpartyAgent({ identity, keyPath, privateKeyPem, privateKey,
316
316
  if (id.kind !== self.identity.kind || id.value !== self.identity.value) throw sdkError("challenge_identity_mismatch");
317
317
  if (!(now() <= Number(challenge.expires_at))) throw sdkError("challenge_expired");
318
318
  verifyOwnerSignature(challenge, ownerPublicKey || challenge.owner_public_key);
319
- const payload = signPayload ? b64urlDecode(signPayload) : challengeSignaturePayload(challenge);
319
+ const payload = challengeSignaturePayload(challenge);
320
+ if (signPayload && !b64urlDecode(signPayload).equals(payload)) throw sdkError("challenge_payload_mismatch");
320
321
  return { handshake_id: challenge.handshake_id, signature: b64url(crypto.sign(null, payload, key)) };
321
322
  };
322
323
 
package/js/discovery.d.ts CHANGED
@@ -24,6 +24,8 @@ export {
24
24
  DESCRIPTOR_CANONICALIZATION,
25
25
  DESCRIPTOR_SIGNATURE_FIELDS,
26
26
  DISCOVERY_SUMMARY_SCHEMA,
27
+ ENGINE_OBSERVATION_SCHEMA,
28
+ ENGINE_READING_SCHEMA,
27
29
  DISCOVERY_VIEW_SCHEMA,
28
30
  DeclarationEndpoints,
29
31
  DescriptorSignature,
@@ -31,6 +33,11 @@ export {
31
33
  DiscoveryEvidenceError,
32
34
  DiscoveryEvidenceSummary,
33
35
  DiscoveryEvidenceView,
36
+ EngineObservation,
37
+ EngineObservationError,
38
+ EngineObservationInput,
39
+ EngineObservationReading,
40
+ EngineObservationSignalReading,
34
41
  DiscoveryVisitEvidence,
35
42
  DiscoveryVisitInput,
36
43
  InboxDeclaration,
@@ -73,6 +80,7 @@ export {
73
80
  inspectCardWire,
74
81
  migrateCardToEmit,
75
82
  normalizeAiExposure,
83
+ normalizeEngineObservation,
76
84
  normalizeDiscoveryVisit,
77
85
  parseSovereignCard,
78
86
  parseTwin3AgentCard,
@@ -82,6 +90,7 @@ export {
82
90
  signA2AAgentCard,
83
91
  signDescriptor,
84
92
  summarizeDiscoveryEvidence,
93
+ summarizeEngineObservations,
85
94
  unsignedDescriptor,
86
95
  validateAgentCardV1,
87
96
  validateWellKnownDocument,
package/js/discovery.js CHANGED
@@ -11,6 +11,11 @@ module.exports = {
11
11
  normalizeAiExposure: impl.discoveryEvidenceApi.normalizeAiExposure,
12
12
  summarizeDiscoveryEvidence: impl.discoveryEvidenceApi.summarizeDiscoveryEvidence,
13
13
  buildDiscoveryEvidenceView: impl.discoveryEvidenceApi.buildDiscoveryEvidenceView,
14
+ ENGINE_OBSERVATION_SCHEMA: impl.engineObservation.ENGINE_OBSERVATION_SCHEMA,
15
+ ENGINE_READING_SCHEMA: impl.engineObservation.ENGINE_READING_SCHEMA,
16
+ EngineObservationError: impl.engineObservation.EngineObservationError,
17
+ normalizeEngineObservation: impl.engineObservationApi.normalizeEngineObservation,
18
+ summarizeEngineObservations: impl.engineObservationApi.summarizeEngineObservations,
14
19
  createSovereignCard: impl.createSovereignCard,
15
20
  createTwin3AgentCard: impl.createTwin3AgentCard,
16
21
  createAgentCard: impl.createAgentCard,
package/js/discovery.mjs CHANGED
@@ -8,6 +8,11 @@ export const normalizeDiscoveryVisit = api.normalizeDiscoveryVisit;
8
8
  export const normalizeAiExposure = api.normalizeAiExposure;
9
9
  export const summarizeDiscoveryEvidence = api.summarizeDiscoveryEvidence;
10
10
  export const buildDiscoveryEvidenceView = api.buildDiscoveryEvidenceView;
11
+ export const ENGINE_OBSERVATION_SCHEMA = api.ENGINE_OBSERVATION_SCHEMA;
12
+ export const ENGINE_READING_SCHEMA = api.ENGINE_READING_SCHEMA;
13
+ export const EngineObservationError = api.EngineObservationError;
14
+ export const normalizeEngineObservation = api.normalizeEngineObservation;
15
+ export const summarizeEngineObservations = api.summarizeEngineObservations;
11
16
  export const createSovereignCard = api.createSovereignCard;
12
17
  export const createTwin3AgentCard = api.createTwin3AgentCard;
13
18
  export const createAgentCard = api.createAgentCard;
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+
5
+ const ENGINE_OBSERVATION_SCHEMA = "twin3-discovery-engine-observation-v1";
6
+ const ENGINE_READING_SCHEMA = "twin3-discovery-engine-reading-v1";
7
+ const MIN_RUNS_PER_PROMPT = 7;
8
+ const MIN_RUNS_FOR_SOURCE_COVERAGE = 8;
9
+ const MIN_PARAPHRASES_PER_NEED = 3;
10
+ const PROTOCOL_WINDOW_DAYS = 21;
11
+ const SIGNALS = ["retrieved", "mentioned", "cited"];
12
+ const EVIDENCE_LEVELS = ["server_observed_engine_answer", "replay_api", "owner_supplied_platform_export"];
13
+ const NON_CLAIMS = [
14
+ "observation_is_not_trust_or_authorization",
15
+ "rates_do_not_pool_across_engines",
16
+ "does_not_generalise_to_engines_that_were_not_queried",
17
+ "mention_rate_is_not_traffic_clicks_or_revenue",
18
+ "does_not_prove_a_buyer_saw_this_answer",
19
+ ];
20
+ const PROTOCOL = {
21
+ min_runs_per_prompt: MIN_RUNS_PER_PROMPT,
22
+ min_runs_for_source_coverage: MIN_RUNS_FOR_SOURCE_COVERAGE,
23
+ min_paraphrases_per_need: MIN_PARAPHRASES_PER_NEED,
24
+ window_days: PROTOCOL_WINDOW_DAYS,
25
+ interval: "wilson_95_basis_points_floor_ceil",
26
+ source: "arXiv:2604.07585",
27
+ };
28
+ const IDENTIFIER = /^[A-Za-z0-9._:-]{1,160}$/;
29
+ const ENGINE = /^[A-Za-z0-9._:/-](?:[A-Za-z0-9 ._:/-]{0,78}[A-Za-z0-9._:/-])?$/;
30
+ const SHA256 = /^[0-9a-f]{64}$/;
31
+ const OBSERVATION_FIELDS = [
32
+ "schema", "host", "engine", "model", "prompt_id", "need_id", "run_id", "observed_at", "status",
33
+ "retrieved", "mentioned", "cited", "evidence_level", "answer_digest", "digest",
34
+ ];
35
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
36
+
37
+ class EngineObservationError extends Error {
38
+ constructor(code) {
39
+ super(code);
40
+ this.name = "EngineObservationError";
41
+ this.code = code;
42
+ }
43
+ }
44
+
45
+ function requireValue(condition, code) {
46
+ if (!condition) throw new EngineObservationError(code);
47
+ }
48
+
49
+ function isRecord(value) {
50
+ return value !== null && typeof value === "object" && !Array.isArray(value)
51
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value));
52
+ }
53
+
54
+ function field(row, key) {
55
+ return Object.hasOwn(row, key) && row[key] !== undefined ? row[key] : null;
56
+ }
57
+
58
+ function hostValue(value) {
59
+ requireValue(typeof value === "string" && !/[^\x00-\x7f]/.test(value), "observation_host_invalid");
60
+ const host = value.replace(/^[ \t\r\n\v\f]+|[ \t\r\n\v\f]+$/g, "").toLowerCase().replace(/\.$/, "");
61
+ requireValue(/^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/.test(host) && !host.includes(".."), "observation_host_invalid");
62
+ return host;
63
+ }
64
+
65
+ function identifier(value, pattern, code) {
66
+ requireValue(typeof value === "string" && pattern.test(value), code);
67
+ return value;
68
+ }
69
+
70
+ function optionalIdentifier(value, code) {
71
+ return value === null ? null : identifier(value, IDENTIFIER, code);
72
+ }
73
+
74
+ function modelValue(value) {
75
+ if (value === null) return null;
76
+ requireValue(typeof value === "string" && value.length > 0 && value.length <= 120 && !/[^\x20-\x7e]/.test(value), "observation_model_invalid");
77
+ return value;
78
+ }
79
+
80
+ function timestamp(value) {
81
+ requireValue(typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 253402300799, "observation_time_invalid");
82
+ return value;
83
+ }
84
+
85
+ function signal(value) {
86
+ requireValue(value === null || typeof value === "boolean", "observation_signal_invalid");
87
+ return value;
88
+ }
89
+
90
+ function aliased(row, primary, alias, code) {
91
+ const values = [primary, alias].map(key => field(row, key)).filter(value => value !== null);
92
+ requireValue(values.length < 2 || values[0] === values[1], code);
93
+ return values.length ? values[0] : null;
94
+ }
95
+
96
+ function sha256(text) {
97
+ return crypto.createHash("sha256").update(text, "utf8").digest("hex");
98
+ }
99
+
100
+ function observationFields(row, ownerHost) {
101
+ const status = field(row, "status");
102
+ requireValue(status === "answered" || status === "error", "observation_status_invalid");
103
+ const signals = Object.fromEntries(SIGNALS.map(name => [name, signal(field(row, name))]));
104
+ requireValue(status === "answered" || SIGNALS.every(name => signals[name] === null), "observation_error_has_signals");
105
+ const evidenceLevel = aliased(row, "evidence_level", "evidence_strength", "observation_evidence_level_invalid");
106
+ requireValue(EVIDENCE_LEVELS.includes(evidenceLevel), "observation_evidence_level_invalid");
107
+ if (field(row, "host") !== null) requireValue(hostValue(row.host) === ownerHost, "observation_host_mismatch");
108
+ return {
109
+ schema: ENGINE_OBSERVATION_SCHEMA,
110
+ host: ownerHost,
111
+ engine: identifier(field(row, "engine"), ENGINE, "observation_engine_invalid"),
112
+ model: modelValue(field(row, "model")),
113
+ prompt_id: identifier(field(row, "prompt_id"), IDENTIFIER, "observation_prompt_id_required"),
114
+ need_id: optionalIdentifier(aliased(row, "need_id", "paraphrase_of", "observation_need_id_invalid"), "observation_need_id_invalid"),
115
+ run_id: optionalIdentifier(field(row, "run_id"), "observation_run_id_invalid"),
116
+ observed_at: timestamp(field(row, "observed_at")),
117
+ status,
118
+ ...signals,
119
+ evidence_level: evidenceLevel,
120
+ };
121
+ }
122
+
123
+ function answerDigest(row) {
124
+ const supplied = field(row, "answer_digest");
125
+ requireValue(supplied === null || typeof supplied === "string" && SHA256.test(supplied), "observation_answer_digest_invalid");
126
+ const answer = field(row, "answer");
127
+ if (answer === null) return supplied;
128
+ requireValue(typeof answer === "string" && !/[\ud800-\udfff]/u.test(answer), "observation_answer_invalid");
129
+ const computed = sha256(answer);
130
+ requireValue(supplied === null || supplied === computed, "observation_answer_digest_invalid");
131
+ return computed;
132
+ }
133
+
134
+ function wilsonBasisPoints(positive, trials) {
135
+ const z = 1.96;
136
+ const phat = positive / trials;
137
+ const denominator = 1 + z * z / trials;
138
+ const centre = phat + z * z / (2 * trials);
139
+ const margin = z * Math.sqrt(phat * (1 - phat) / trials + z * z / (4 * trials * trials));
140
+ const low = Math.max(0, (centre - margin) / denominator);
141
+ const high = Math.min(1, (centre + margin) / denominator);
142
+ return [Math.max(0, Math.floor(low * 10000)), Math.min(10000, Math.ceil(high * 10000))];
143
+ }
144
+
145
+ function signalReading(rows, name, reportable) {
146
+ const known = rows.map(row => row[name]).filter(value => value !== null);
147
+ const positive = known.filter(Boolean).length;
148
+ if (!known.length) return { state: "unknown", observed: 0, positive: 0, rate_bp: null, interval_bp: null };
149
+ return {
150
+ state: reportable ? "measured" : "withheld",
151
+ observed: known.length,
152
+ positive,
153
+ rate_bp: reportable ? Math.floor(positive * 10000 / known.length) : null,
154
+ interval_bp: reportable ? wilsonBasisPoints(positive, known.length) : null,
155
+ };
156
+ }
157
+
158
+ function utcDay(seconds) {
159
+ return new Date(seconds * 1000).toISOString().slice(0, 10);
160
+ }
161
+
162
+ function createEngineObservationApi(canonicalJsonBuffer) {
163
+ const digest = value => crypto.createHash("sha256").update(canonicalJsonBuffer(value)).digest("hex");
164
+
165
+ function normalizeEngineObservation(row, { host } = {}) {
166
+ requireValue(isRecord(row), "observation_invalid");
167
+ const ownerHost = hostValue(host);
168
+ const body = { ...observationFields(row, ownerHost), answer_digest: answerDigest(row) };
169
+ return { ...body, digest: digest(body) };
170
+ }
171
+
172
+ function validateObservation(row, ownerHost) {
173
+ requireValue(isRecord(row) && row.schema === ENGINE_OBSERVATION_SCHEMA && row.host === ownerHost, "observation_invalid");
174
+ requireValue(Reflect.ownKeys(row).length === OBSERVATION_FIELDS.length && OBSERVATION_FIELDS.every(key => Object.hasOwn(row, key)), "observation_invalid");
175
+ const { digest: supplied, ...body } = row;
176
+ let expected;
177
+ try {
178
+ expected = digest(body);
179
+ } catch {
180
+ throw new EngineObservationError("observation_digest_mismatch");
181
+ }
182
+ requireValue(typeof supplied === "string" && !/[^\x00-\x7f]/.test(supplied) && supplied.length === expected.length
183
+ && crypto.timingSafeEqual(Buffer.from(supplied, "ascii"), Buffer.from(expected, "ascii")), "observation_digest_mismatch");
184
+ const fields = observationFields(row, ownerHost);
185
+ requireValue(Object.entries(fields).every(([key, value]) => row[key] === value), "observation_invalid");
186
+ requireValue(row.answer_digest === null || typeof row.answer_digest === "string" && SHA256.test(row.answer_digest), "observation_invalid");
187
+ }
188
+
189
+ function summarizeEngineObservations(observations = [], { host, sourceLevel = false } = {}) {
190
+ const ownerHost = hostValue(host);
191
+ requireValue(Array.isArray(observations), "observation_invalid");
192
+ requireValue(typeof sourceLevel === "boolean", "observation_source_level_invalid");
193
+ const seen = new Set();
194
+ for (const row of observations) {
195
+ validateObservation(row, ownerHost);
196
+ requireValue(!seen.has(row.digest), "observation_duplicate");
197
+ seen.add(row.digest);
198
+ }
199
+ const rows = [...observations].sort((a, b) =>
200
+ compare(a.engine, b.engine) || a.observed_at - b.observed_at || compare(a.prompt_id, b.prompt_id) || compare(a.digest, b.digest));
201
+ const minimum = sourceLevel ? MIN_RUNS_FOR_SOURCE_COVERAGE : MIN_RUNS_PER_PROMPT;
202
+ const answered = rows.filter(row => row.status === "answered");
203
+ const blockers = new Set();
204
+ if (!answered.length) blockers.add("no_answered_observations");
205
+ if (answered.some(row => !row.need_id)) blockers.add("insufficient_paraphrases");
206
+ const engines = [...new Set(rows.map(row => row.engine))].sort(compare);
207
+ for (const engine of engines) {
208
+ const engineAnswered = answered.filter(row => row.engine === engine);
209
+ const runsPerPrompt = new Map();
210
+ const promptsPerNeed = new Map();
211
+ for (const row of engineAnswered) {
212
+ runsPerPrompt.set(row.prompt_id, (runsPerPrompt.get(row.prompt_id) || 0) + 1);
213
+ if (row.need_id) {
214
+ if (!promptsPerNeed.has(row.need_id)) promptsPerNeed.set(row.need_id, new Set());
215
+ promptsPerNeed.get(row.need_id).add(row.prompt_id);
216
+ }
217
+ }
218
+ if (!engineAnswered.length || [...runsPerPrompt.values()].some(count => count < minimum)) blockers.add("insufficient_repetitions");
219
+ if ([...promptsPerNeed.values()].some(prompts => prompts.size < MIN_PARAPHRASES_PER_NEED)) blockers.add("insufficient_paraphrases");
220
+ }
221
+ const reportable = blockers.size === 0;
222
+ const byEngine = {};
223
+ for (const engine of engines) {
224
+ const engineRows = rows.filter(row => row.engine === engine);
225
+ const engineAnswered = engineRows.filter(row => row.status === "answered");
226
+ byEngine[engine] = {
227
+ answered: engineAnswered.length,
228
+ errors: engineRows.length - engineAnswered.length,
229
+ prompts: new Set(engineAnswered.map(row => row.prompt_id)).size,
230
+ signals: Object.fromEntries(SIGNALS.map(name => [name, signalReading(engineAnswered, name, reportable)])),
231
+ };
232
+ }
233
+ const days = [...new Set(answered.map(row => utcDay(row.observed_at)))].sort(compare);
234
+ const spanDays = days.length ? (Date.parse(days[days.length - 1]) - Date.parse(days[0])) / 86400000 + 1 : 0;
235
+ const body = {
236
+ schema: ENGINE_READING_SCHEMA,
237
+ host: ownerHost,
238
+ valid: reportable,
239
+ source_level: sourceLevel,
240
+ blockers: [...blockers].sort(compare),
241
+ caveats: answered.length && spanDays < PROTOCOL_WINDOW_DAYS ? ["window_shorter_than_21_days"] : [],
242
+ window_utc: days.length ? { first: days[0], last: days[days.length - 1], span_days: spanDays, observed_days: days.length } : null,
243
+ engines,
244
+ by_engine: byEngine,
245
+ evidence_levels: [...new Set(rows.map(row => row.evidence_level))].sort(compare),
246
+ protocol: { ...PROTOCOL },
247
+ non_claims: [...NON_CLAIMS],
248
+ observation_digests: rows.map(row => row.digest),
249
+ };
250
+ return { ...body, digest: digest(body) };
251
+ }
252
+
253
+ return { normalizeEngineObservation, summarizeEngineObservations };
254
+ }
255
+
256
+ module.exports = {
257
+ ENGINE_OBSERVATION_SCHEMA,
258
+ ENGINE_READING_SCHEMA,
259
+ EngineObservationError,
260
+ createEngineObservationApi,
261
+ };
@@ -0,0 +1,13 @@
1
+ import api from "./human-score.js";
2
+ export const HumanScoreError = api.HumanScoreError;
3
+ export const getHumanScorePolicy = api.getHumanScorePolicy;
4
+ export const scoreUnits = api.scoreUnits;
5
+ export const rawToHumanScore = api.rawToHumanScore;
6
+ export const freshnessFactor = api.freshnessFactor;
7
+ export const calculateTechnicalScore = api.calculateTechnicalScore;
8
+ export const socialRaw = api.socialRaw;
9
+ export const socialEdgeWeight = api.socialEdgeWeight;
10
+ export const previewTechnicalDelta = api.previewTechnicalDelta;
11
+ export const calculateHumanScoreEpoch = api.calculateHumanScoreEpoch;
12
+ export const evaluateHumanScoreThresholds = api.evaluateHumanScoreThresholds;
13
+ export default api;
package/js/index.js CHANGED
@@ -15,6 +15,16 @@ module.exports = {
15
15
  createKyaChallengeWithSigner: trust.createKyaChallengeWithSigner,
16
16
  signDelegationWithSigner: trust.signDelegationWithSigner,
17
17
  signEnvelopeWithSigner: trust.signEnvelopeWithSigner,
18
+ CONSUMER_REGISTRATION_SCHEMA: trust.CONSUMER_REGISTRATION_SCHEMA,
19
+ ConsumerRegistrationError: trust.ConsumerRegistrationError,
20
+ validateConsumerRegistration: trust.validateConsumerRegistration,
21
+ registrationReceipt: trust.registrationReceipt,
22
+ signConsumerRegistration: trust.signConsumerRegistration,
23
+ signConsumerRegistrationWithSigner: trust.signConsumerRegistrationWithSigner,
24
+ verifyConsumerRegistration: trust.verifyConsumerRegistration,
25
+ didWebDocumentKeys: trust.didWebDocumentKeys,
26
+ resolveDidWebKey: trust.resolveDidWebKey,
27
+ jwksKey: trust.jwksKey,
18
28
  VISIT_EVIDENCE_SCHEMA: discovery.VISIT_EVIDENCE_SCHEMA,
19
29
  AI_EXPOSURE_SCHEMA: discovery.AI_EXPOSURE_SCHEMA,
20
30
  DISCOVERY_SUMMARY_SCHEMA: discovery.DISCOVERY_SUMMARY_SCHEMA,
@@ -24,6 +34,11 @@ module.exports = {
24
34
  normalizeAiExposure: discovery.normalizeAiExposure,
25
35
  summarizeDiscoveryEvidence: discovery.summarizeDiscoveryEvidence,
26
36
  buildDiscoveryEvidenceView: discovery.buildDiscoveryEvidenceView,
37
+ ENGINE_OBSERVATION_SCHEMA: discovery.ENGINE_OBSERVATION_SCHEMA,
38
+ ENGINE_READING_SCHEMA: discovery.ENGINE_READING_SCHEMA,
39
+ EngineObservationError: discovery.EngineObservationError,
40
+ normalizeEngineObservation: discovery.normalizeEngineObservation,
41
+ summarizeEngineObservations: discovery.summarizeEngineObservations,
27
42
  AuthorizationLeaseError: shield.AuthorizationLeaseError,
28
43
  verifyAuthorizationLease: shield.verifyAuthorizationLease,
29
44
  leaseClearanceContext: shield.leaseClearanceContext,