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 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.18-pre-alpha` is the current release candidate: `0.2.18-pre-alpha` for npm and `0.2.18a0` 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`.
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.18-pre-alpha
129
- pip install twin3-sdk==0.2.18a0
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.18-pre-alpha.tgz
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"];
@@ -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;
@@ -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 visitFields(row, ownerHost) {
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: ["/.well-known/agent-card.json", "/.well-known/agent.json"].includes(path) ? "agent_card" : "website",
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 fields = visitFields(row, ownerHost);
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 bot = BOTS.find(([name]) => new RegExp("(^|[^A-Za-z0-9_-])" + name + "($|[^A-Za-z0-9_-])", "i").test(userAgent));
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: bot ? "ua_claim" : "unclassified",
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
- requireValue(hasFields(row, VISIT_FIELDS), "visit_invalid");
178
- const fields = visitFields(row, ownerHost);
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 bot = BOTS.find(([name]) => name === row.bot);
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) && row.evidence_level === (bot ? "ua_claim" : "unclassified"), "visit_invalid");
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(BOTS.some(([name]) => name === key), "summary_invalid");
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
@@ -89,6 +89,7 @@ export {
89
89
  resolveDocumentPath,
90
90
  signA2AAgentCard,
91
91
  signDescriptor,
92
+ summarizeCohortVisibility,
92
93
  summarizeDiscoveryEvidence,
93
94
  summarizeEngineObservations,
94
95
  unsignedDescriptor,
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;
@@ -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
- return { normalizeEngineObservation, summarizeEngineObservations };
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;
@@ -10,11 +10,32 @@ function envelopeBuilderNumber(value, code) {
10
10
  core.fail(code);
11
11
  return Math.floor(value);
12
12
  }
13
- function buildEnvelope({ sender, kind = "message", payload = {}, ttlSeconds = 900, now: at }) {
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);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twin3-sdk",
3
- "version": "0.2.18-pre-alpha",
3
+ "version": "0.2.19-pre-alpha",
4
4
  "description": "Universal Capability Motherboard for Autonomous AI Agents and Self-Sovereign Humans (JS/Node runtime)",
5
5
  "type": "commonjs",
6
6
  "main": "./js/index.js",