twin3-sdk 0.2.18-pre-alpha → 0.2.19-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 +4 -4
- package/index.d.ts +3 -0
- package/js/comm/counterparty.js +14 -1
- package/js/discovery-evidence.js +81 -11
- package/js/discovery.d.ts +1 -0
- package/js/discovery.js +1 -0
- package/js/discovery.mjs +1 -0
- package/js/engine-observation.js +122 -1
- package/js/index.js +3 -0
- package/js/index.mjs +3 -0
- package/js/internal/comm.js +35 -1
- package/js/internal/shield.js +197 -1
- package/js/shield.d.ts +2 -0
- package/js/shield.js +2 -0
- package/js/shield.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ 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.19-pre-alpha` is the current release candidate: `0.2.19-pre-alpha` for npm and `0.2.19a0` for PyPI. It is published from reviewed main through the existing Cloud Build tag trigger. 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
121
|
The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime candidate. Products retain authenticated authority, context egress consent, durable storage and trusted tool brokers. Python `twin3.runtime` provides compatible contracts and digests only. OpenShell, ADK and Hermes adapters are not included. See [runtime scope and limits](docs/architecture/PERSONAL-AGENT-RUNTIME-V1.md).
|
|
122
122
|
|
|
@@ -125,15 +125,15 @@ The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime
|
|
|
125
125
|
After publication, install the exact released version from the registry:
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
|
-
npm install twin3-sdk@0.2.
|
|
129
|
-
pip install twin3-sdk==0.2.
|
|
128
|
+
npm install twin3-sdk@0.2.19-pre-alpha
|
|
129
|
+
pip install twin3-sdk==0.2.19a0
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
For local evaluation, you can also use the packed npm tarball. Do not install from a git URL or a version range:
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
135
|
npm pack --ignore-scripts
|
|
136
|
-
npm install ./twin3-sdk-0.2.
|
|
136
|
+
npm install ./twin3-sdk-0.2.19-pre-alpha.tgz
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
---
|
package/index.d.ts
CHANGED
|
@@ -223,6 +223,9 @@ export interface EngineObservationReading {
|
|
|
223
223
|
}
|
|
224
224
|
export declare function normalizeEngineObservation(row: EngineObservationInput, options: { host: string }): EngineObservation;
|
|
225
225
|
export declare function summarizeEngineObservations(observations: EngineObservation[], options: { host: string; sourceLevel?: boolean }): EngineObservationReading;
|
|
226
|
+
export declare function summarizeCohortVisibility(readingsBySubject: Record<string, EngineObservationReading>, subjectId: string, options?: { minCohortSize?: number }): Record<string, any>;
|
|
227
|
+
export declare function buildSignedReferral(options: Record<string, any>): Record<string, any>;
|
|
228
|
+
export declare function verifySignedReferral(token: Record<string, any>, options: Record<string, any>): Record<string, any>;
|
|
226
229
|
|
|
227
230
|
export declare const DESCRIPTOR_CANONICALIZATION: "rfc8785";
|
|
228
231
|
export declare const DESCRIPTOR_SIGNATURE_FIELDS: readonly ["signatures", "signatureStatus"];
|
package/js/comm/counterparty.js
CHANGED
|
@@ -250,10 +250,20 @@ function envelopeDigest(envelope) {
|
|
|
250
250
|
return crypto.createHash("sha256").update(canonicalJsonBuffer(body)).digest("hex");
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.floor(Date.now() / 1000), legacySchemas = false } = {}) {
|
|
253
|
+
function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.floor(Date.now() / 1000), legacySchemas = false, attribution = undefined } = {}) {
|
|
254
254
|
const sender = identityOf(identity);
|
|
255
255
|
if (!ENVELOPE_KINDS.has(kind)) throw sdkError("envelope_kind_unknown");
|
|
256
256
|
if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length === 0) throw sdkError("envelope_payload_empty");
|
|
257
|
+
if (attribution !== undefined && attribution !== null) {
|
|
258
|
+
if (!attribution || typeof attribution !== "object" || Array.isArray(attribution)
|
|
259
|
+
|| Object.keys(attribution).length !== 2
|
|
260
|
+
|| !Object.prototype.hasOwnProperty.call(attribution, "attributed_card_digest")
|
|
261
|
+
|| !Object.prototype.hasOwnProperty.call(attribution, "attribution_source")
|
|
262
|
+
|| (attribution.attributed_card_digest !== null && (typeof attribution.attributed_card_digest !== "string" || !/^[0-9a-f]{64}$/.test(attribution.attributed_card_digest)))
|
|
263
|
+
|| !["card_endpoint", "referral_token", "unknown"].includes(attribution.attribution_source)) {
|
|
264
|
+
throw sdkError("envelope_attribution_invalid");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
257
267
|
const at = Math.floor(now);
|
|
258
268
|
const envelope = {
|
|
259
269
|
schema: legacySchemas ? LEGACY.envelope : ENVELOPE_SCHEMA,
|
|
@@ -267,6 +277,9 @@ function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.f
|
|
|
267
277
|
issued_at: at,
|
|
268
278
|
deadline: at + Math.floor(ttlSeconds),
|
|
269
279
|
};
|
|
280
|
+
if (attribution !== undefined && attribution !== null) {
|
|
281
|
+
envelope.attribution = JSON.parse(JSON.stringify(attribution));
|
|
282
|
+
}
|
|
270
283
|
void sender;
|
|
271
284
|
envelope.digest = envelopeDigest(envelope);
|
|
272
285
|
return envelope;
|
package/js/discovery-evidence.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
4
|
const zhTw = require("../twin3/discovery/evidence-i18n.json")["zh-TW"];
|
|
5
|
+
const { validId } = require("./id-format.js");
|
|
5
6
|
|
|
6
7
|
const VISIT_EVIDENCE_SCHEMA = "twin3-discovery-visit-evidence-v1";
|
|
7
8
|
const AI_EXPOSURE_SCHEMA = "twin3-discovery-ai-exposure-v1";
|
|
8
9
|
const DISCOVERY_SUMMARY_SCHEMA = "twin3-discovery-evidence-summary-v1";
|
|
9
10
|
const DISCOVERY_VIEW_SCHEMA = "twin3-discovery-evidence-view-v1";
|
|
10
11
|
const VISIT_FIELDS = ["schema", "host", "path", "surface", "source", "record_id", "source_digest", "observed_at", "request_count", "method", "status", "provider", "bot", "evidence_level", "user_agent_digest", "digest"];
|
|
12
|
+
const CARD_ATTRIBUTION_FIELDS = ["subject_namespace", "subject_id", "card_digest", "claim_axis", "network_axis", "identity_axis"];
|
|
11
13
|
const EXPOSURE_FIELDS = ["schema", "host", "source", "report_digest", "date", "page", "page_digest", "impressions", "evidence_level", "digest"];
|
|
12
14
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
13
15
|
const URL_COMPONENT = /^(?:[A-Za-z0-9._~!$&'()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*$/;
|
|
@@ -28,6 +30,14 @@ const BOTS = [
|
|
|
28
30
|
["Bingbot", "Microsoft"],
|
|
29
31
|
["Applebot", "Apple"],
|
|
30
32
|
];
|
|
33
|
+
const PROTOCOL_BOTS = [
|
|
34
|
+
["A2A-Client", "A2A"],
|
|
35
|
+
["WebMCP-Client", "WebMCP"],
|
|
36
|
+
];
|
|
37
|
+
const ALL_BOTS = [...BOTS, ...PROTOCOL_BOTS];
|
|
38
|
+
const CLAIM_AXES = ["unclassified", "ua_claim", "protocol_claim"];
|
|
39
|
+
const NETWORK_AXES = ["unverified", "published_ip_range_matched", "rdns_verified"];
|
|
40
|
+
const IDENTITY_AXES = ["unsigned", "request_signature_valid", "kya_delegation_bound"];
|
|
31
41
|
|
|
32
42
|
class DiscoveryEvidenceError extends Error {
|
|
33
43
|
constructor(code) {
|
|
@@ -97,7 +107,35 @@ function pathValue(value) {
|
|
|
97
107
|
return path;
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
function
|
|
110
|
+
function cardAttributionInput(row) {
|
|
111
|
+
const hasAttribution = CARD_ATTRIBUTION_FIELDS.some(key => Object.hasOwn(row, key)) || row.surface === "hosted_card";
|
|
112
|
+
if (!hasAttribution) return null;
|
|
113
|
+
const namespace = Object.hasOwn(row, "subject_namespace") ? row.subject_namespace : null;
|
|
114
|
+
const subjectId = Object.hasOwn(row, "subject_id") ? row.subject_id : null;
|
|
115
|
+
requireValue((namespace === null) === (subjectId === null), "visit_subject_invalid");
|
|
116
|
+
if (namespace !== null) {
|
|
117
|
+
requireValue(namespace === "agt" || namespace === "hum", "visit_subject_invalid");
|
|
118
|
+
requireValue(typeof subjectId === "string" && validId(subjectId) && subjectId.startsWith(namespace + "_") && subjectId.length > 4, "visit_subject_invalid");
|
|
119
|
+
}
|
|
120
|
+
const cardDigest = Object.hasOwn(row, "card_digest") ? row.card_digest : null;
|
|
121
|
+
requireValue(cardDigest === null || isSha256(cardDigest), "visit_card_digest_invalid");
|
|
122
|
+
const networkAxis = row.network_axis === undefined ? "unverified" : row.network_axis;
|
|
123
|
+
requireValue(NETWORK_AXES.includes(networkAxis), "visit_network_axis_invalid");
|
|
124
|
+
const identityAxis = row.identity_axis === undefined ? "unsigned" : row.identity_axis;
|
|
125
|
+
requireValue(IDENTITY_AXES.includes(identityAxis), "visit_identity_axis_invalid");
|
|
126
|
+
const claimAxis = Object.hasOwn(row, "claim_axis") ? row.claim_axis : null;
|
|
127
|
+
requireValue(claimAxis === null || CLAIM_AXES.includes(claimAxis), "visit_claim_axis_invalid");
|
|
128
|
+
return {
|
|
129
|
+
subject_namespace: namespace,
|
|
130
|
+
subject_id: subjectId,
|
|
131
|
+
card_digest: cardDigest,
|
|
132
|
+
claim_axis: claimAxis,
|
|
133
|
+
network_axis: networkAxis,
|
|
134
|
+
identity_axis: identityAxis,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function visitFields(row, ownerHost, { allowHostedCard = false } = {}) {
|
|
101
139
|
requireValue(["cdn_log", "server_log"].includes(row.source), "visit_source_invalid");
|
|
102
140
|
requireValue(typeof row.record_id === "string" && row.record_id.length > 0 && row.record_id.length <= 160 && !/[^A-Za-z0-9._:-]/.test(row.record_id), "visit_record_id_required");
|
|
103
141
|
requireValue(isSha256(row.source_digest), "visit_source_digest_invalid");
|
|
@@ -109,8 +147,10 @@ function visitFields(row, ownerHost) {
|
|
|
109
147
|
requireValue(method === "GET" || method === "HEAD", "visit_method_invalid");
|
|
110
148
|
const path = pathValue(row.path);
|
|
111
149
|
if (Object.hasOwn(row, "host")) requireValue(hostValue(row.host) === ownerHost, "visit_host_mismatch");
|
|
150
|
+
const defaultSurface = ["/.well-known/agent-card.json", "/.well-known/agent.json"].includes(path) ? "agent_card" : "website";
|
|
151
|
+
const surface = allowHostedCard && row.surface === "hosted_card" ? "hosted_card" : defaultSurface;
|
|
112
152
|
return { schema: VISIT_EVIDENCE_SCHEMA, host: ownerHost, path,
|
|
113
|
-
surface
|
|
153
|
+
surface,
|
|
114
154
|
source: row.source, record_id: row.record_id, source_digest: row.source_digest,
|
|
115
155
|
observed_at: row.observed_at, request_count: requestCount, method, status };
|
|
116
156
|
}
|
|
@@ -137,17 +177,35 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
|
|
|
137
177
|
function normalizeDiscoveryVisit(row, { host } = {}) {
|
|
138
178
|
requireValue(isRecord(row), "visit_invalid");
|
|
139
179
|
const ownerHost = hostValue(host);
|
|
140
|
-
const
|
|
180
|
+
const attribution = cardAttributionInput(row);
|
|
181
|
+
const fields = visitFields(row, ownerHost, { allowHostedCard: attribution !== null });
|
|
141
182
|
const userAgent = row.user_agent === undefined ? "" : row.user_agent;
|
|
142
183
|
requireValue(typeof userAgent === "string" && [...userAgent].length <= 512 && !/[\ud800-\udfff]/u.test(userAgent), "visit_user_agent_invalid");
|
|
143
|
-
const
|
|
184
|
+
const botCandidates = attribution !== null ? ALL_BOTS : BOTS;
|
|
185
|
+
const bot = botCandidates.find(([name]) => new RegExp("(^|[^A-Za-z0-9_-])" + name + "($|[^A-Za-z0-9_-])", "i").test(userAgent));
|
|
186
|
+
const isProtocol = Boolean(bot && PROTOCOL_BOTS.some(([name]) => name === bot[0]));
|
|
187
|
+
const evidenceLevel = bot ? (isProtocol ? "protocol_claim" : "ua_claim") : (
|
|
188
|
+
attribution && attribution.claim_axis === "protocol_claim" ? "protocol_claim" : "unclassified"
|
|
189
|
+
);
|
|
144
190
|
const body = {
|
|
145
191
|
...fields,
|
|
146
192
|
provider: bot ? bot[1] : null,
|
|
147
193
|
bot: bot ? bot[0] : null,
|
|
148
|
-
evidence_level:
|
|
194
|
+
evidence_level: evidenceLevel,
|
|
149
195
|
user_agent_digest: crypto.createHash("sha256").update(userAgent, "utf8").digest("hex"),
|
|
150
196
|
};
|
|
197
|
+
if (attribution !== null) {
|
|
198
|
+
const claimAxis = attribution.claim_axis || evidenceLevel;
|
|
199
|
+
requireValue(claimAxis === evidenceLevel, "visit_claim_axis_invalid");
|
|
200
|
+
Object.assign(body, {
|
|
201
|
+
subject_namespace: attribution.subject_namespace,
|
|
202
|
+
subject_id: attribution.subject_id,
|
|
203
|
+
card_digest: attribution.card_digest,
|
|
204
|
+
claim_axis: claimAxis,
|
|
205
|
+
network_axis: attribution.network_axis,
|
|
206
|
+
identity_axis: attribution.identity_axis,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
151
209
|
return { ...body, digest: digest(body, canonicalJsonBuffer) };
|
|
152
210
|
}
|
|
153
211
|
|
|
@@ -174,13 +232,25 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
|
|
|
174
232
|
function validateVisit(row, ownerHost) {
|
|
175
233
|
requireValue(isRecord(row) && row.schema === VISIT_EVIDENCE_SCHEMA && row.host === ownerHost, "visit_invalid");
|
|
176
234
|
verifyDigest(row, "visit_digest_mismatch");
|
|
177
|
-
|
|
178
|
-
|
|
235
|
+
const hasAttribution = CARD_ATTRIBUTION_FIELDS.some(key => Object.hasOwn(row, key));
|
|
236
|
+
requireValue(hasFields(row, hasAttribution ? [...VISIT_FIELDS, ...CARD_ATTRIBUTION_FIELDS] : VISIT_FIELDS), "visit_invalid");
|
|
237
|
+
const fields = visitFields(row, ownerHost, { allowHostedCard: hasAttribution });
|
|
179
238
|
requireValue(row.path === fields.path && row.surface === fields.surface, "visit_invalid");
|
|
180
239
|
requireValue(isSha256(row.user_agent_digest), "visit_invalid");
|
|
181
|
-
const
|
|
240
|
+
const botTable = hasAttribution ? ALL_BOTS : BOTS;
|
|
241
|
+
const bot = botTable.find(([name]) => name === row.bot);
|
|
182
242
|
requireValue(row.bot === null || bot !== undefined, "visit_invalid");
|
|
183
|
-
requireValue(row.provider === (bot ? bot[1] : null)
|
|
243
|
+
requireValue(row.provider === (bot ? bot[1] : null), "visit_invalid");
|
|
244
|
+
if (hasAttribution) {
|
|
245
|
+
const attribution = cardAttributionInput(row);
|
|
246
|
+
const isProtocol = Boolean(bot && PROTOCOL_BOTS.some(([name]) => name === bot[0]));
|
|
247
|
+
const expectedLevel = bot ? (isProtocol ? "protocol_claim" : "ua_claim") : (
|
|
248
|
+
attribution.claim_axis === "protocol_claim" ? "protocol_claim" : "unclassified"
|
|
249
|
+
);
|
|
250
|
+
requireValue(row.evidence_level === expectedLevel && row.claim_axis === expectedLevel, "visit_invalid");
|
|
251
|
+
} else {
|
|
252
|
+
requireValue(row.evidence_level === (bot ? "ua_claim" : "unclassified"), "visit_invalid");
|
|
253
|
+
}
|
|
184
254
|
}
|
|
185
255
|
|
|
186
256
|
function validateExposure(row, ownerHost) {
|
|
@@ -226,7 +296,7 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
|
|
|
226
296
|
for (const row of visitRows) {
|
|
227
297
|
const count = row.request_count;
|
|
228
298
|
const success = row.status >= 200 && row.status < 400;
|
|
229
|
-
const cardSuccess = row.status >= 200 && row.status < 300 && row.surface === "agent_card";
|
|
299
|
+
const cardSuccess = row.status >= 200 && row.status < 300 && (row.surface === "agent_card" || row.surface === "hosted_card");
|
|
230
300
|
successful += success ? count : 0;
|
|
231
301
|
failed += success ? 0 : count;
|
|
232
302
|
cardReads += cardSuccess ? count : 0;
|
|
@@ -276,7 +346,7 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
|
|
|
276
346
|
summaryCount(count);
|
|
277
347
|
requireValue(field === "by_date" || count > 0, "summary_invalid");
|
|
278
348
|
if (field === "by_path") requireValue(pathValue(key) === key, "summary_invalid");
|
|
279
|
-
else if (field === "by_bot_claim") requireValue(
|
|
349
|
+
else if (field === "by_bot_claim") requireValue(ALL_BOTS.some(([name]) => name === key), "summary_invalid");
|
|
280
350
|
else dateValue(key);
|
|
281
351
|
}
|
|
282
352
|
if (total !== null) requireValue(Object.values(values).reduce((sum, value) => sum + value, 0) === total, "summary_invalid");
|
package/js/discovery.d.ts
CHANGED
package/js/discovery.js
CHANGED
|
@@ -16,6 +16,7 @@ module.exports = {
|
|
|
16
16
|
EngineObservationError: impl.engineObservation.EngineObservationError,
|
|
17
17
|
normalizeEngineObservation: impl.engineObservationApi.normalizeEngineObservation,
|
|
18
18
|
summarizeEngineObservations: impl.engineObservationApi.summarizeEngineObservations,
|
|
19
|
+
summarizeCohortVisibility: impl.engineObservationApi.summarizeCohortVisibility,
|
|
19
20
|
createSovereignCard: impl.createSovereignCard,
|
|
20
21
|
createTwin3AgentCard: impl.createTwin3AgentCard,
|
|
21
22
|
createAgentCard: impl.createAgentCard,
|
package/js/discovery.mjs
CHANGED
|
@@ -13,6 +13,7 @@ export const ENGINE_READING_SCHEMA = api.ENGINE_READING_SCHEMA;
|
|
|
13
13
|
export const EngineObservationError = api.EngineObservationError;
|
|
14
14
|
export const normalizeEngineObservation = api.normalizeEngineObservation;
|
|
15
15
|
export const summarizeEngineObservations = api.summarizeEngineObservations;
|
|
16
|
+
export const summarizeCohortVisibility = api.summarizeCohortVisibility;
|
|
16
17
|
export const createSovereignCard = api.createSovereignCard;
|
|
17
18
|
export const createTwin3AgentCard = api.createTwin3AgentCard;
|
|
18
19
|
export const createAgentCard = api.createAgentCard;
|
package/js/engine-observation.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
|
+
const { validId } = require("./id-format.js");
|
|
4
5
|
|
|
5
6
|
const ENGINE_OBSERVATION_SCHEMA = "twin3-discovery-engine-observation-v1";
|
|
6
7
|
const ENGINE_READING_SCHEMA = "twin3-discovery-engine-reading-v1";
|
|
8
|
+
const COHORT_VISIBILITY_SCHEMA = "twin3-discovery-cohort-visibility-v1";
|
|
7
9
|
const MIN_RUNS_PER_PROMPT = 7;
|
|
8
10
|
const MIN_RUNS_FOR_SOURCE_COVERAGE = 8;
|
|
9
11
|
const MIN_PARAPHRASES_PER_NEED = 3;
|
|
@@ -250,12 +252,131 @@ function createEngineObservationApi(canonicalJsonBuffer) {
|
|
|
250
252
|
return { ...body, digest: digest(body) };
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
|
|
255
|
+
function validateReadingForCohort(reading) {
|
|
256
|
+
requireValue(isRecord(reading) && reading.schema === ENGINE_READING_SCHEMA, "cohort_reading_invalid");
|
|
257
|
+
const { digest: supplied, ...body } = reading;
|
|
258
|
+
requireValue(typeof supplied === "string" && SHA256.test(supplied), "cohort_reading_digest_mismatch");
|
|
259
|
+
let expected;
|
|
260
|
+
try {
|
|
261
|
+
expected = digest(body);
|
|
262
|
+
} catch {
|
|
263
|
+
throw new EngineObservationError("cohort_reading_digest_mismatch");
|
|
264
|
+
}
|
|
265
|
+
requireValue(crypto.timingSafeEqual(Buffer.from(supplied, "ascii"), Buffer.from(expected, "ascii")), "cohort_reading_digest_mismatch");
|
|
266
|
+
requireValue(isRecord(reading.by_engine), "cohort_reading_invalid");
|
|
267
|
+
for (const [engine, engineEntry] of Object.entries(reading.by_engine)) {
|
|
268
|
+
requireValue(typeof engine === "string" && ENGINE.test(engine), "cohort_reading_invalid");
|
|
269
|
+
requireValue(isRecord(engineEntry), "cohort_reading_invalid");
|
|
270
|
+
const signals = engineEntry.signals;
|
|
271
|
+
requireValue(
|
|
272
|
+
isRecord(signals) && Object.keys(signals).length === SIGNALS.length && SIGNALS.every(name => Object.hasOwn(signals, name)),
|
|
273
|
+
"cohort_reading_invalid",
|
|
274
|
+
);
|
|
275
|
+
for (const signalName of SIGNALS) {
|
|
276
|
+
const sig = signals[signalName];
|
|
277
|
+
requireValue(isRecord(sig), "cohort_reading_invalid");
|
|
278
|
+
const state = sig.state;
|
|
279
|
+
const rate = sig.rate_bp;
|
|
280
|
+
if (state === "measured") {
|
|
281
|
+
requireValue(typeof rate === "number" && Number.isInteger(rate) && rate >= 0 && rate <= 10000, "cohort_reading_invalid");
|
|
282
|
+
} else if (state === "withheld" || state === "unknown") {
|
|
283
|
+
requireValue(rate === null || rate === undefined, "cohort_reading_invalid");
|
|
284
|
+
} else {
|
|
285
|
+
throw new EngineObservationError("cohort_reading_invalid");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function summarizeCohortVisibility(readingsBySubject, subjectId, { minCohortSize = 3 } = {}) {
|
|
292
|
+
requireValue(isRecord(readingsBySubject) && Object.keys(readingsBySubject).length > 0, "cohort_readings_invalid");
|
|
293
|
+
requireValue(typeof subjectId === "string" && validId(subjectId), "cohort_subject_id_invalid");
|
|
294
|
+
requireValue(typeof minCohortSize === "number" && Number.isInteger(minCohortSize) && minCohortSize >= 1 && minCohortSize <= 10000, "cohort_min_size_invalid");
|
|
295
|
+
requireValue(Object.hasOwn(readingsBySubject, subjectId), "cohort_subject_not_found");
|
|
296
|
+
const subjectIds = Object.keys(readingsBySubject).sort(compare);
|
|
297
|
+
const engineSet = new Set();
|
|
298
|
+
for (const sid of subjectIds) {
|
|
299
|
+
requireValue(typeof sid === "string" && validId(sid), "cohort_subject_id_invalid");
|
|
300
|
+
const reading = readingsBySubject[sid];
|
|
301
|
+
validateReadingForCohort(reading);
|
|
302
|
+
for (const engine of Object.keys(reading.by_engine)) engineSet.add(engine);
|
|
303
|
+
}
|
|
304
|
+
const sortedEngines = [...engineSet].sort(compare);
|
|
305
|
+
const byEngine = {};
|
|
306
|
+
for (const engine of sortedEngines) {
|
|
307
|
+
const engineSignals = {};
|
|
308
|
+
for (const signalName of SIGNALS) {
|
|
309
|
+
const subjectReading = readingsBySubject[subjectId].by_engine[engine]?.signals?.[signalName] ?? null;
|
|
310
|
+
const measuredRates = new Map();
|
|
311
|
+
let anyWithheld = false;
|
|
312
|
+
for (const sid of subjectIds) {
|
|
313
|
+
const sig = readingsBySubject[sid].by_engine[engine]?.signals?.[signalName];
|
|
314
|
+
if (!isRecord(sig)) continue;
|
|
315
|
+
const state = sig.state;
|
|
316
|
+
const rate = sig.rate_bp;
|
|
317
|
+
if (state === "measured" && typeof rate === "number" && Number.isInteger(rate) && rate >= 0 && rate <= 10000) {
|
|
318
|
+
measuredRates.set(sid, rate);
|
|
319
|
+
} else if (state === "withheld") {
|
|
320
|
+
anyWithheld = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const cohortSize = measuredRates.size;
|
|
324
|
+
if (subjectReading === null || subjectReading.state === "unknown") {
|
|
325
|
+
engineSignals[signalName] = {
|
|
326
|
+
state: "unknown",
|
|
327
|
+
cohort_size: cohortSize,
|
|
328
|
+
subject_rate_bp: null,
|
|
329
|
+
percentile_bp: null,
|
|
330
|
+
rank: null,
|
|
331
|
+
};
|
|
332
|
+
} else if (!measuredRates.has(subjectId) || cohortSize < minCohortSize) {
|
|
333
|
+
engineSignals[signalName] = {
|
|
334
|
+
state: (subjectReading.state === "withheld" || subjectReading.state === "measured" || anyWithheld) ? "withheld" : "unknown",
|
|
335
|
+
cohort_size: cohortSize,
|
|
336
|
+
subject_rate_bp: measuredRates.has(subjectId) ? measuredRates.get(subjectId) : null,
|
|
337
|
+
percentile_bp: null,
|
|
338
|
+
rank: null,
|
|
339
|
+
};
|
|
340
|
+
} else {
|
|
341
|
+
const subjRate = measuredRates.get(subjectId);
|
|
342
|
+
let better = 0;
|
|
343
|
+
let belowOrEqual = 0;
|
|
344
|
+
for (const r of measuredRates.values()) {
|
|
345
|
+
if (r > subjRate) better += 1;
|
|
346
|
+
if (r <= subjRate) belowOrEqual += 1;
|
|
347
|
+
}
|
|
348
|
+
engineSignals[signalName] = {
|
|
349
|
+
state: "measured",
|
|
350
|
+
cohort_size: cohortSize,
|
|
351
|
+
subject_rate_bp: subjRate,
|
|
352
|
+
percentile_bp: Math.floor(belowOrEqual * 10000 / cohortSize),
|
|
353
|
+
rank: 1 + better,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
byEngine[engine] = { signals: engineSignals };
|
|
358
|
+
}
|
|
359
|
+
const readingDigests = Object.fromEntries(subjectIds.map(sid => [sid, readingsBySubject[sid].digest]));
|
|
360
|
+
const body = {
|
|
361
|
+
schema: COHORT_VISIBILITY_SCHEMA,
|
|
362
|
+
subject_id: subjectId,
|
|
363
|
+
min_cohort_size: minCohortSize,
|
|
364
|
+
subjects_total: subjectIds.length,
|
|
365
|
+
engines: sortedEngines,
|
|
366
|
+
by_engine: byEngine,
|
|
367
|
+
reading_digests: readingDigests,
|
|
368
|
+
non_claims: [...NON_CLAIMS],
|
|
369
|
+
};
|
|
370
|
+
return { ...body, digest: digest(body) };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { normalizeEngineObservation, summarizeEngineObservations, summarizeCohortVisibility };
|
|
254
374
|
}
|
|
255
375
|
|
|
256
376
|
module.exports = {
|
|
257
377
|
ENGINE_OBSERVATION_SCHEMA,
|
|
258
378
|
ENGINE_READING_SCHEMA,
|
|
379
|
+
COHORT_VISIBILITY_SCHEMA,
|
|
259
380
|
EngineObservationError,
|
|
260
381
|
createEngineObservationApi,
|
|
261
382
|
};
|
package/js/index.js
CHANGED
|
@@ -44,6 +44,7 @@ module.exports = {
|
|
|
44
44
|
EngineObservationError: discovery.EngineObservationError,
|
|
45
45
|
normalizeEngineObservation: discovery.normalizeEngineObservation,
|
|
46
46
|
summarizeEngineObservations: discovery.summarizeEngineObservations,
|
|
47
|
+
summarizeCohortVisibility: discovery.summarizeCohortVisibility,
|
|
47
48
|
AuthorizationLeaseError: shield.AuthorizationLeaseError,
|
|
48
49
|
verifyAuthorizationLease: shield.verifyAuthorizationLease,
|
|
49
50
|
leaseClearanceContext: shield.leaseClearanceContext,
|
|
@@ -57,6 +58,8 @@ module.exports = {
|
|
|
57
58
|
claimExecution: shield.claimExecution,
|
|
58
59
|
createInMemoryExecutionClaimStore: shield.createInMemoryExecutionClaimStore,
|
|
59
60
|
recordExecutionOutcome: shield.recordExecutionOutcome,
|
|
61
|
+
buildSignedReferral: shield.buildSignedReferral,
|
|
62
|
+
verifySignedReferral: shield.verifySignedReferral,
|
|
60
63
|
createSovereignCard: discovery.createSovereignCard,
|
|
61
64
|
createTwin3AgentCard: discovery.createTwin3AgentCard,
|
|
62
65
|
createAgentCard: discovery.createAgentCard,
|
package/js/index.mjs
CHANGED
|
@@ -132,6 +132,7 @@ export const authorizeExecution = api.authorizeExecution;
|
|
|
132
132
|
export const buildDeclaration = api.buildDeclaration;
|
|
133
133
|
export const buildDiscoveryEvidenceView = api.buildDiscoveryEvidenceView;
|
|
134
134
|
export const buildEnvelope = api.buildEnvelope;
|
|
135
|
+
export const buildSignedReferral = api.buildSignedReferral;
|
|
135
136
|
export const buildWellKnownDocument = api.buildWellKnownDocument;
|
|
136
137
|
export const canStrangerDecide = api.canStrangerDecide;
|
|
137
138
|
export const canonicalJson = api.canonicalJson;
|
|
@@ -242,6 +243,7 @@ export const signDelegationWithSigner = api.signDelegationWithSigner;
|
|
|
242
243
|
export const signDescriptor = api.signDescriptor;
|
|
243
244
|
export const signEnvelope = api.signEnvelope;
|
|
244
245
|
export const signEnvelopeWithSigner = api.signEnvelopeWithSigner;
|
|
246
|
+
export const summarizeCohortVisibility = api.summarizeCohortVisibility;
|
|
245
247
|
export const summarizeDiscoveryEvidence = api.summarizeDiscoveryEvidence;
|
|
246
248
|
export const summarizeEngineObservations = api.summarizeEngineObservations;
|
|
247
249
|
export const taskTrustLine = api.taskTrustLine;
|
|
@@ -275,6 +277,7 @@ export const verifyPasskeyAssertionWithStores = api.verifyPasskeyAssertionWithSt
|
|
|
275
277
|
export const verifyPasskeyRegistration = api.verifyPasskeyRegistration;
|
|
276
278
|
export const verifyReceipt = api.verifyReceipt;
|
|
277
279
|
export const verifySignedPaymentReceipt = api.verifySignedPaymentReceipt;
|
|
280
|
+
export const verifySignedReferral = api.verifySignedReferral;
|
|
278
281
|
export const verifyX402Payment = api.verifyX402Payment;
|
|
279
282
|
export const x402DnsTxtRecord = api.x402DnsTxtRecord;
|
|
280
283
|
export default api;
|
package/js/internal/comm.js
CHANGED
|
@@ -10,11 +10,32 @@ function envelopeBuilderNumber(value, code) {
|
|
|
10
10
|
core.fail(code);
|
|
11
11
|
return Math.floor(value);
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function validateEnvelopeAttribution(attr, failFn) {
|
|
14
|
+
if (attr === undefined || attr === null)
|
|
15
|
+
return;
|
|
16
|
+
if (!attr || typeof attr !== "object" || Array.isArray(attr))
|
|
17
|
+
failFn("envelope_attribution_invalid");
|
|
18
|
+
const keys = Object.keys(attr);
|
|
19
|
+
if (keys.length !== 2 || !Object.prototype.hasOwnProperty.call(attr, "attributed_card_digest")
|
|
20
|
+
|| !Object.prototype.hasOwnProperty.call(attr, "attribution_source")) {
|
|
21
|
+
failFn("envelope_attribution_invalid");
|
|
22
|
+
}
|
|
23
|
+
const cardDigest = attr.attributed_card_digest;
|
|
24
|
+
if (cardDigest !== null && (typeof cardDigest !== "string" || !/^[0-9a-f]{64}$/.test(cardDigest))) {
|
|
25
|
+
failFn("envelope_attribution_invalid");
|
|
26
|
+
}
|
|
27
|
+
if (!["card_endpoint", "referral_token", "unknown"].includes(attr.attribution_source)) {
|
|
28
|
+
failFn("envelope_attribution_invalid");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function buildEnvelope({ sender, kind = "message", payload = {}, ttlSeconds = 900, now: at, attribution = undefined }) {
|
|
14
32
|
const now = at === undefined
|
|
15
33
|
? Math.floor(Date.now() / 1000)
|
|
16
34
|
: envelopeBuilderNumber(at, "envelope_timestamps_invalid");
|
|
17
35
|
const ttl = envelopeBuilderNumber(ttlSeconds, "envelope_ttl_invalid");
|
|
36
|
+
if (attribution !== undefined && attribution !== null) {
|
|
37
|
+
validateEnvelopeAttribution(attribution, core.fail);
|
|
38
|
+
}
|
|
18
39
|
const envelope = {
|
|
19
40
|
schema: trust.ENVELOPE_SCHEMA,
|
|
20
41
|
envelope_id: core.crypto.randomUUID(),
|
|
@@ -27,6 +48,9 @@ function buildEnvelope({ sender, kind = "message", payload = {}, ttlSeconds = 90
|
|
|
27
48
|
issued_at: now,
|
|
28
49
|
deadline: now + ttl,
|
|
29
50
|
};
|
|
51
|
+
if (attribution !== undefined && attribution !== null) {
|
|
52
|
+
envelope.attribution = JSON.parse(JSON.stringify(attribution));
|
|
53
|
+
}
|
|
30
54
|
const body = { ...envelope };
|
|
31
55
|
delete body.digest;
|
|
32
56
|
delete body.sender_signature;
|
|
@@ -154,6 +178,9 @@ const experimentalTaskInbox = (() => {
|
|
|
154
178
|
if (tag.treat_as !== "data")
|
|
155
179
|
fail("envelope_field_not_marked_data");
|
|
156
180
|
}
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(envelope, "attribution") && envelope.attribution !== null) {
|
|
182
|
+
validateEnvelopeAttribution(envelope.attribution, fail);
|
|
183
|
+
}
|
|
157
184
|
if (![envelope.issued_at, envelope.deadline].every(Number.isFinite))
|
|
158
185
|
fail("envelope_timestamps_invalid");
|
|
159
186
|
if (envelope.deadline <= envelope.issued_at)
|
|
@@ -302,6 +329,8 @@ const experimentalTaskInbox = (() => {
|
|
|
302
329
|
const task = { schema: TASK_SCHEMA, task_id: taskId, tenant_id: this.#tenant, sender, kind: envelope.kind,
|
|
303
330
|
payload: envelope.payload, provenance: envelope.provenance, authority: "none", state: "submitted", version: 1,
|
|
304
331
|
review: { required: true }, created_at: at, updated_at: at, history: [], audit_head: "" };
|
|
332
|
+
if (envelope.attribution !== undefined && envelope.attribution !== null)
|
|
333
|
+
task.attribution = clone(envelope.attribution);
|
|
305
334
|
audit(task, permission.principalId, "intake", at, digest([this.#tenant, operationKey]));
|
|
306
335
|
state.tasks[taskId] = task;
|
|
307
336
|
state.operations[operationKey] = { command_digest: commandDigest, result: clone(task) };
|
|
@@ -647,6 +676,9 @@ function taskEnvelopeInput(input, now) {
|
|
|
647
676
|
if (tag.treat_as !== "data")
|
|
648
677
|
throw new IntakeRejected("envelope_field_not_marked_data");
|
|
649
678
|
}
|
|
679
|
+
if (Object.prototype.hasOwnProperty.call(envelope, "attribution") && envelope.attribution !== null) {
|
|
680
|
+
validateEnvelopeAttribution(envelope.attribution, (code) => { throw new IntakeRejected(code); });
|
|
681
|
+
}
|
|
650
682
|
if (typeof envelope.issued_at !== "number" || !Number.isFinite(envelope.issued_at)
|
|
651
683
|
|| typeof envelope.deadline !== "number" || !Number.isFinite(envelope.deadline)) {
|
|
652
684
|
throw new IntakeRejected("envelope_timestamps_invalid");
|
|
@@ -1352,6 +1384,8 @@ class TaskInbox {
|
|
|
1352
1384
|
history: [],
|
|
1353
1385
|
audit_head: "",
|
|
1354
1386
|
};
|
|
1387
|
+
if (validated.attribution !== undefined && validated.attribution !== null)
|
|
1388
|
+
task.attribution = taskClone(validated.attribution);
|
|
1355
1389
|
if (usesFreeAllowance)
|
|
1356
1390
|
this._parties.noteFreeAllowanceUse(sender, { now: at });
|
|
1357
1391
|
taskAudit(task, "submitted", senderKey, "intake", at);
|
package/js/internal/shield.js
CHANGED
|
@@ -924,9 +924,205 @@ function createLeaseApi({ ProtocolError, canonicalJsonBuffer, normalizeIdentity,
|
|
|
924
924
|
return { ...denied("claim", ["occurrence_" + claim.state]), claim };
|
|
925
925
|
return { allowed: true, stage: "allowed", reasons: [], claim, context, authorization: verified };
|
|
926
926
|
}
|
|
927
|
+
const REFERRAL_SCHEMA = "twin3-shield-signed-referral-v1";
|
|
928
|
+
const REFERRAL_PURPOSE = "card_outbound_referral";
|
|
929
|
+
const MAX_REFERRAL_TTL_SECONDS = 300;
|
|
930
|
+
const REFERRAL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
931
|
+
const REFERRAL_SHA256_RE = /^[0-9a-f]{64}$/;
|
|
932
|
+
const REFERRAL_URL_COMPONENT = /^(?:[A-Za-z0-9._~!$&'()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*$/;
|
|
933
|
+
const REFERRAL_FIELDS = [
|
|
934
|
+
"schema", "issuer", "audience", "purpose", "subject_id", "card_digest",
|
|
935
|
+
"destination_id", "target_url", "epoch_id", "policy_revision", "issued_at",
|
|
936
|
+
"expires_at", "ttl_seconds", "nonce", "key_id", "signature", "digest",
|
|
937
|
+
];
|
|
938
|
+
function referralRequire(condition, code) {
|
|
939
|
+
if (!condition)
|
|
940
|
+
fail(code);
|
|
941
|
+
}
|
|
942
|
+
function referralHost(value, code) {
|
|
943
|
+
referralRequire(typeof value === "string" && /^[\x00-\x7f]*$/.test(value), code);
|
|
944
|
+
const trimmed = value.replace(/^[ \t\r\n\v\f]+|[ \t\r\n\v\f]+$/g, "").toLowerCase();
|
|
945
|
+
const host = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
|
|
946
|
+
referralRequire(/^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/.test(host) && !host.includes(".."), code);
|
|
947
|
+
return host;
|
|
948
|
+
}
|
|
949
|
+
function referralTargetUrl(value) {
|
|
950
|
+
referralRequire(typeof value === "string" && value.length > 0 && value.length <= 2048, "referral_target_url_invalid");
|
|
951
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
952
|
+
const code = value.charCodeAt(i);
|
|
953
|
+
referralRequire(code >= 33 && code <= 126, "referral_target_url_invalid");
|
|
954
|
+
}
|
|
955
|
+
const parsed = /^https:\/\/([^/?#]+)(\/[^?#]*)?(\?[^#]*)?(#[^#]*)?$/i.exec(value);
|
|
956
|
+
referralRequire(parsed !== null, "referral_target_url_invalid");
|
|
957
|
+
const [, authority, rawPath, query, fragment] = parsed;
|
|
958
|
+
referralRequire(!/[@[\]\\]/.test(authority), "referral_target_url_invalid");
|
|
959
|
+
const parts = authority.split(":");
|
|
960
|
+
referralRequire(parts.length <= 2 && (parts.length === 1 || /^0*443$/.test(parts[1])), "referral_target_url_invalid");
|
|
961
|
+
const host = referralHost(parts[0], "referral_target_url_invalid");
|
|
962
|
+
const path = rawPath || "/";
|
|
963
|
+
referralRequire(
|
|
964
|
+
REFERRAL_URL_COMPONENT.test(path)
|
|
965
|
+
&& REFERRAL_URL_COMPONENT.test((query || "").slice(1))
|
|
966
|
+
&& REFERRAL_URL_COMPONENT.test((fragment || "").slice(1)),
|
|
967
|
+
"referral_target_url_invalid",
|
|
968
|
+
);
|
|
969
|
+
return ["https://" + host + path + (query || "") + (fragment || ""), host];
|
|
970
|
+
}
|
|
971
|
+
function referralSubjectId(value) {
|
|
972
|
+
referralRequire(typeof value === "string" && REFERRAL_ID_RE.test(value), "referral_subject_id_invalid");
|
|
973
|
+
referralRequire((value.startsWith("agt_") || value.startsWith("hum_")) && value.length > 4, "referral_subject_id_invalid");
|
|
974
|
+
return value;
|
|
975
|
+
}
|
|
976
|
+
function referralIdentifier(value, code) {
|
|
977
|
+
referralRequire(typeof value === "string" && REFERRAL_ID_RE.test(value), code);
|
|
978
|
+
return value;
|
|
979
|
+
}
|
|
980
|
+
function referralTimestamp(value, code) {
|
|
981
|
+
referralRequire(typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 253402300799, code);
|
|
982
|
+
return value;
|
|
983
|
+
}
|
|
984
|
+
function referralTtl(value) {
|
|
985
|
+
referralRequire(typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= MAX_REFERRAL_TTL_SECONDS, "referral_ttl_invalid");
|
|
986
|
+
return value;
|
|
987
|
+
}
|
|
988
|
+
function buildSignedReferral(options = {}) {
|
|
989
|
+
referralRequire(object(options), "referral_invalid");
|
|
990
|
+
const issuerVal = referralIdentifier(options.issuer, "referral_issuer_invalid");
|
|
991
|
+
const audienceVal = referralIdentifier(options.audience, "referral_audience_invalid");
|
|
992
|
+
const subjectVal = referralSubjectId(options.subjectId !== undefined ? options.subjectId : options.subject_id);
|
|
993
|
+
const rawCardDigest = options.cardDigest !== undefined ? options.cardDigest : options.card_digest;
|
|
994
|
+
referralRequire(typeof rawCardDigest === "string" && REFERRAL_SHA256_RE.test(rawCardDigest), "referral_card_digest_invalid");
|
|
995
|
+
const destVal = referralIdentifier(options.destinationId !== undefined ? options.destinationId : options.destination_id, "referral_destination_invalid");
|
|
996
|
+
const [normalizedUrl] = referralTargetUrl(options.targetUrl !== undefined ? options.targetUrl : options.target_url);
|
|
997
|
+
const epochVal = referralIdentifier(options.epochId !== undefined ? options.epochId : options.epoch_id, "referral_epoch_invalid");
|
|
998
|
+
const policyVal = referralIdentifier(options.policyRevision !== undefined ? options.policyRevision : options.policy_revision, "referral_policy_revision_invalid");
|
|
999
|
+
const rawTtl = options.ttlSeconds !== undefined ? options.ttlSeconds : (options.ttl_seconds !== undefined ? options.ttl_seconds : MAX_REFERRAL_TTL_SECONDS);
|
|
1000
|
+
const ttlVal = referralTtl(rawTtl);
|
|
1001
|
+
const rawIssuedAt = options.issuedAt !== undefined ? options.issuedAt : options.issued_at;
|
|
1002
|
+
const at = referralTimestamp(rawIssuedAt === undefined || rawIssuedAt === null ? Math.floor(Date.now() / 1000) : rawIssuedAt, "referral_time_invalid");
|
|
1003
|
+
const nonceVal = options.nonce === undefined || options.nonce === null
|
|
1004
|
+
? core.crypto.randomBytes(16).toString("hex")
|
|
1005
|
+
: referralIdentifier(options.nonce, "referral_nonce_invalid");
|
|
1006
|
+
const rawPrivateKey = options.privateKey !== undefined ? options.privateKey : options.private_key;
|
|
1007
|
+
let signingKey, signerKid;
|
|
1008
|
+
try {
|
|
1009
|
+
signingKey = core.loadEd25519PrivateKey(rawPrivateKey);
|
|
1010
|
+
signerKid = keyId(core.crypto.createPublicKey(signingKey));
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
fail("referral_private_key_invalid");
|
|
1014
|
+
}
|
|
1015
|
+
const unsigned = {
|
|
1016
|
+
schema: REFERRAL_SCHEMA,
|
|
1017
|
+
issuer: issuerVal,
|
|
1018
|
+
audience: audienceVal,
|
|
1019
|
+
purpose: REFERRAL_PURPOSE,
|
|
1020
|
+
subject_id: subjectVal,
|
|
1021
|
+
card_digest: rawCardDigest,
|
|
1022
|
+
destination_id: destVal,
|
|
1023
|
+
target_url: normalizedUrl,
|
|
1024
|
+
epoch_id: epochVal,
|
|
1025
|
+
policy_revision: policyVal,
|
|
1026
|
+
issued_at: at,
|
|
1027
|
+
expires_at: at + ttlVal,
|
|
1028
|
+
ttl_seconds: ttlVal,
|
|
1029
|
+
nonce: nonceVal,
|
|
1030
|
+
key_id: signerKid,
|
|
1031
|
+
};
|
|
1032
|
+
const signature = core.b64url(core.crypto.sign(null, canonicalJsonBuffer(unsigned), signingKey));
|
|
1033
|
+
const signed = { ...unsigned, signature };
|
|
1034
|
+
return { ...signed, digest: digest(signed) };
|
|
1035
|
+
}
|
|
1036
|
+
function verifySignedReferral(token, options = {}) {
|
|
1037
|
+
referralRequire(object(token) && token.schema === REFERRAL_SCHEMA, "referral_invalid");
|
|
1038
|
+
referralRequire(object(options), "referral_invalid");
|
|
1039
|
+
const keys = Object.keys(token);
|
|
1040
|
+
referralRequire(keys.length === REFERRAL_FIELDS.length && REFERRAL_FIELDS.every((k) => own(token, k)), "referral_invalid");
|
|
1041
|
+
referralRequire(token.purpose === REFERRAL_PURPOSE, "referral_purpose_invalid");
|
|
1042
|
+
const suppliedDigest = token.digest;
|
|
1043
|
+
referralRequire(typeof suppliedDigest === "string" && REFERRAL_SHA256_RE.test(suppliedDigest), "referral_digest_mismatch");
|
|
1044
|
+
const signedBody = without(token, ["digest"]);
|
|
1045
|
+
let expectedDigest;
|
|
1046
|
+
try {
|
|
1047
|
+
expectedDigest = digest(signedBody);
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
fail("referral_digest_mismatch");
|
|
1051
|
+
}
|
|
1052
|
+
referralRequire(core.secureEquals(suppliedDigest, expectedDigest), "referral_digest_mismatch");
|
|
1053
|
+
const issuerVal = referralIdentifier(token.issuer, "referral_issuer_invalid");
|
|
1054
|
+
const audienceVal = referralIdentifier(token.audience, "referral_audience_invalid");
|
|
1055
|
+
const subjectVal = referralSubjectId(token.subject_id);
|
|
1056
|
+
referralRequire(typeof token.card_digest === "string" && REFERRAL_SHA256_RE.test(token.card_digest), "referral_card_digest_invalid");
|
|
1057
|
+
const destVal = referralIdentifier(token.destination_id, "referral_destination_invalid");
|
|
1058
|
+
const [normalizedUrl, targetHost] = referralTargetUrl(token.target_url);
|
|
1059
|
+
referralRequire(normalizedUrl === token.target_url, "referral_target_url_invalid");
|
|
1060
|
+
const epochVal = referralIdentifier(token.epoch_id, "referral_epoch_invalid");
|
|
1061
|
+
const policyVal = referralIdentifier(token.policy_revision, "referral_policy_revision_invalid");
|
|
1062
|
+
const issuedAt = referralTimestamp(token.issued_at, "referral_time_invalid");
|
|
1063
|
+
const expiresAt = referralTimestamp(token.expires_at, "referral_time_invalid");
|
|
1064
|
+
const ttlVal = referralTtl(token.ttl_seconds);
|
|
1065
|
+
referralRequire(expiresAt === issuedAt + ttlVal, "referral_window_invalid");
|
|
1066
|
+
referralIdentifier(token.nonce, "referral_nonce_invalid");
|
|
1067
|
+
const at = referralTimestamp(options.now, "referral_time_invalid");
|
|
1068
|
+
referralRequire(at >= issuedAt, "referral_not_yet_valid");
|
|
1069
|
+
referralRequire(at <= expiresAt, "referral_expired");
|
|
1070
|
+
const rawPublicKey = options.publicKey !== undefined ? options.publicKey : options.public_key;
|
|
1071
|
+
let pub, expectedKid;
|
|
1072
|
+
try {
|
|
1073
|
+
pub = loadEd25519PublicKey(rawPublicKey);
|
|
1074
|
+
expectedKid = keyId(pub);
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
fail("referral_public_key_invalid");
|
|
1078
|
+
}
|
|
1079
|
+
referralRequire(token.key_id === expectedKid, "referral_key_mismatch");
|
|
1080
|
+
const sigBytes = core.strictB64urlDecode(token.signature, "referral_signature_invalid");
|
|
1081
|
+
const unsignedBody = without(signedBody, ["signature"]);
|
|
1082
|
+
referralRequire(core.crypto.verify(null, canonicalJsonBuffer(unsignedBody), pub, sigBytes), "referral_signature_invalid");
|
|
1083
|
+
const expectedIssuer = options.expectedIssuer !== undefined ? options.expectedIssuer : options.expected_issuer;
|
|
1084
|
+
if (expectedIssuer !== undefined && expectedIssuer !== null)
|
|
1085
|
+
referralRequire(issuerVal === expectedIssuer, "referral_issuer_mismatch");
|
|
1086
|
+
const expectedAudience = options.expectedAudience !== undefined ? options.expectedAudience : options.expected_audience;
|
|
1087
|
+
if (expectedAudience !== undefined && expectedAudience !== null)
|
|
1088
|
+
referralRequire(audienceVal === expectedAudience, "referral_audience_mismatch");
|
|
1089
|
+
const expectedSubjectId = options.expectedSubjectId !== undefined ? options.expectedSubjectId : options.expected_subject_id;
|
|
1090
|
+
if (expectedSubjectId !== undefined && expectedSubjectId !== null)
|
|
1091
|
+
referralRequire(subjectVal === expectedSubjectId, "referral_subject_mismatch");
|
|
1092
|
+
const expectedCardDigest = options.expectedCardDigest !== undefined ? options.expectedCardDigest : options.expected_card_digest;
|
|
1093
|
+
if (expectedCardDigest !== undefined && expectedCardDigest !== null)
|
|
1094
|
+
referralRequire(token.card_digest === expectedCardDigest, "referral_card_digest_mismatch");
|
|
1095
|
+
const expectedDestinationId = options.expectedDestinationId !== undefined ? options.expectedDestinationId : options.expected_destination_id;
|
|
1096
|
+
if (expectedDestinationId !== undefined && expectedDestinationId !== null)
|
|
1097
|
+
referralRequire(destVal === expectedDestinationId, "referral_destination_mismatch");
|
|
1098
|
+
const expectedEpochId = options.expectedEpochId !== undefined ? options.expectedEpochId : options.expected_epoch_id;
|
|
1099
|
+
if (expectedEpochId !== undefined && expectedEpochId !== null)
|
|
1100
|
+
referralRequire(epochVal === expectedEpochId, "referral_epoch_mismatch");
|
|
1101
|
+
const expectedPolicyRevision = options.expectedPolicyRevision !== undefined ? options.expectedPolicyRevision : options.expected_policy_revision;
|
|
1102
|
+
if (expectedPolicyRevision !== undefined && expectedPolicyRevision !== null)
|
|
1103
|
+
referralRequire(policyVal === expectedPolicyRevision, "referral_policy_revision_mismatch");
|
|
1104
|
+
const allowedDestinations = options.allowedDestinations !== undefined ? options.allowedDestinations : options.allowed_destinations;
|
|
1105
|
+
if (allowedDestinations !== undefined && allowedDestinations !== null) {
|
|
1106
|
+
referralRequire(object(allowedDestinations) && own(allowedDestinations, destVal), "referral_destination_unregistered");
|
|
1107
|
+
const [registeredUrl] = referralTargetUrl(allowedDestinations[destVal]);
|
|
1108
|
+
referralRequire(registeredUrl === normalizedUrl, "referral_target_url_mismatch");
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
verified: true,
|
|
1112
|
+
subject_id: subjectVal,
|
|
1113
|
+
card_digest: token.card_digest,
|
|
1114
|
+
destination_id: destVal,
|
|
1115
|
+
target_url: normalizedUrl,
|
|
1116
|
+
target_host: targetHost,
|
|
1117
|
+
epoch_id: epochVal,
|
|
1118
|
+
policy_revision: policyVal,
|
|
1119
|
+
receipt_digest: suppliedDigest,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
927
1122
|
return { AuthorizationLeaseError, verifyAuthorizationLease, leaseClearanceContext, guardLeasedAction,
|
|
928
1123
|
planDigest, occurrenceContext, occurrenceId, contextDigest, issueClearance, checkExecutionGate,
|
|
929
|
-
claimExecution, createInMemoryExecutionClaimStore, recordExecutionOutcome
|
|
1124
|
+
claimExecution, createInMemoryExecutionClaimStore, recordExecutionOutcome,
|
|
1125
|
+
buildSignedReferral, verifySignedReferral };
|
|
930
1126
|
}
|
|
931
1127
|
// The consumer-registration verifier lives in the public trust layer, which itself loads
|
|
932
1128
|
// this module through internal/comm.js; resolving it lazily at call time avoids the cycle.
|
package/js/shield.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export {
|
|
|
35
35
|
VerifiedLeaseRequest,
|
|
36
36
|
actionKey,
|
|
37
37
|
authorizeExecution,
|
|
38
|
+
buildSignedReferral,
|
|
38
39
|
checkExecutionGate,
|
|
39
40
|
claimExecution,
|
|
40
41
|
contextDigest,
|
|
@@ -59,4 +60,5 @@ export {
|
|
|
59
60
|
toolAuthorityMatches,
|
|
60
61
|
validateToolCatalog,
|
|
61
62
|
verifyAuthorizationLease,
|
|
63
|
+
verifySignedReferral,
|
|
62
64
|
} from "../index";
|
package/js/shield.js
CHANGED
|
@@ -16,6 +16,8 @@ module.exports = {
|
|
|
16
16
|
claimExecution: impl.authorizationLeaseApi.claimExecution,
|
|
17
17
|
createInMemoryExecutionClaimStore: impl.authorizationLeaseApi.createInMemoryExecutionClaimStore,
|
|
18
18
|
recordExecutionOutcome: impl.authorizationLeaseApi.recordExecutionOutcome,
|
|
19
|
+
buildSignedReferral: impl.authorizationLeaseApi.buildSignedReferral,
|
|
20
|
+
verifySignedReferral: impl.authorizationLeaseApi.verifySignedReferral,
|
|
19
21
|
scanText: impl.scanText,
|
|
20
22
|
MEMORY_SCHEMA: memory.MEMORY_SCHEMA,
|
|
21
23
|
REVOCATION_SCHEMA: memory.REVOCATION_SCHEMA,
|
package/js/shield.mjs
CHANGED
|
@@ -12,6 +12,8 @@ export const checkExecutionGate = api.checkExecutionGate;
|
|
|
12
12
|
export const claimExecution = api.claimExecution;
|
|
13
13
|
export const createInMemoryExecutionClaimStore = api.createInMemoryExecutionClaimStore;
|
|
14
14
|
export const recordExecutionOutcome = api.recordExecutionOutcome;
|
|
15
|
+
export const buildSignedReferral = api.buildSignedReferral;
|
|
16
|
+
export const verifySignedReferral = api.verifySignedReferral;
|
|
15
17
|
export const scanText = api.scanText;
|
|
16
18
|
export const MEMORY_SCHEMA = api.MEMORY_SCHEMA;
|
|
17
19
|
export const REVOCATION_SCHEMA = api.REVOCATION_SCHEMA;
|
package/package.json
CHANGED