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 +5 -5
- package/index.d.ts +160 -0
- package/js/comm/counterparty.js +2 -1
- package/js/discovery.d.ts +9 -0
- package/js/discovery.js +5 -0
- package/js/discovery.mjs +5 -0
- package/js/engine-observation.js +261 -0
- package/js/human-score.mjs +13 -0
- package/js/index.js +15 -0
- package/js/index.mjs +15 -0
- package/js/internal/comm.js +30 -20
- package/js/internal/discovery.js +9 -0
- package/js/internal/matrix.js +1 -1
- package/js/internal/shield.js +55 -17
- package/js/shield.d.ts +1 -0
- package/js/trust/consumer-registration.js +259 -0
- package/js/trust/didweb.js +145 -0
- package/js/trust.d.ts +21 -0
- package/js/trust.js +24 -0
- package/js/trust.mjs +10 -0
- package/package.json +6 -2
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.
|
|
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`
|
|
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.
|
|
127
|
-
pip install twin3-sdk==0.2.
|
|
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.
|
|
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;
|
package/js/comm/counterparty.js
CHANGED
|
@@ -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 =
|
|
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,
|