twin3-sdk 0.2.18-pre-alpha → 0.2.20-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.20-pre-alpha` is the current release candidate: `0.2.20-pre-alpha` for npm and `0.2.20a0` 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.20-pre-alpha
129
+ pip install twin3-sdk==0.2.20a0
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.20-pre-alpha.tgz
137
137
  ```
138
138
 
139
139
  ---
@@ -146,6 +146,9 @@ Agent ID consumes Twin3 SDK to execute the **5-Stage A2A Autonomous Commercial L
146
146
  2. **Understand**: Uses `twin3.matrix` to compute tolerance matching against industrial RFQs.
147
147
  3. **Negotiate**: Uses `twin3.comm` and `twin3.shield` to exchange signed quotes without prompt-tampering risks.
148
148
  4. **Settle**: Uses the Twin3 signed-receipt adapter at an HTTP 402 gate. Acceptance additionally requires a trusted signer key, a chain-specific finality verifier, and a shared replay store.
149
+
150
+ Settlement integrations must follow the [x402 security boundaries](docs/x402-security-boundaries.md),
151
+ including case-sensitive wallet binding, canonical network names and durable payment recovery.
149
152
  5. **Dispute**: Invokes `twin3.governance` to resolve contractual non-performance.
150
153
 
151
154
  ### 👤 Human ID (Personal Sovereignty Projection)
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");
@@ -790,7 +822,7 @@ function x402ChainAdapter(chainVerifier, demand, receipt) {
790
822
  : (chainVerifier && (chainVerifier.verifyPayment || chainVerifier.verify_payment));
791
823
  if (typeof method !== "function")
792
824
  core.fail("chain_verifier_invalid");
793
- const evidence = await method.call(chainVerifier, demand, receipt);
825
+ const evidence = await method.call(chainVerifier, structuredClone(demand), structuredClone(receipt));
794
826
  if (!evidence || typeof evidence !== "object")
795
827
  return evidence;
796
828
  return {
@@ -928,21 +960,24 @@ class TaskInbox {
928
960
  const payout = String(policy.payout_address || "").trim();
929
961
  if (!payout)
930
962
  core.fail("payment_payout_address_required");
931
- if (demand.recipient.trim().toLowerCase() !== payout.toLowerCase())
963
+ if (settlement.chainIdentifier(demand.recipient, demand.network) !== settlement.chainIdentifier(payout, demand.network))
932
964
  core.fail("payment_demand_recipient_mismatch");
933
965
  const price = core.isPlainObject(policy.price) ? policy.price : {};
934
- if (Number(demand.amount) < Number(price.amount || 0))
966
+ settlement.positiveProtocolAmount(demand.amount, "invalid_demand_amount");
967
+ if (settlement.money(demand.amount).compare(settlement.money(price.amount || "0")) < 0)
935
968
  core.fail("payment_demand_amount_insufficient");
936
969
  const policyCurrency = String(price.currency || "").trim().toUpperCase();
937
970
  if (!policyCurrency || demand.currency.toUpperCase() !== policyCurrency)
938
971
  core.fail("payment_demand_currency_mismatch");
939
972
  const partyRow = this._parties.get(identity, { now: at }) || {};
940
- const boundWallet = String(partyRow.bound_wallet || "").trim().toLowerCase();
941
- const payer = receipt.payer.trim().toLowerCase();
942
- const senderValue = String(identity.value || "").trim().toLowerCase();
943
- const senderKey = counterpartyKey(identity).toLowerCase();
973
+ const boundWallet = String(partyRow.bound_wallet || "").trim();
974
+ const payer = settlement.chainIdentifier(receipt.payer, demand.network);
975
+ const senderValue = settlement.chainIdentifier(String(identity.value || ""), demand.network);
976
+ const senderKey = settlement.chainIdentifier(counterpartyKey(identity), demand.network);
944
977
  if (boundWallet) {
945
- if (payer !== boundWallet)
978
+ if (settlement.chainIdentifier("A", demand.network) === "A" && partyRow.bound_wallet_case_preserved !== true)
979
+ core.fail("payment_wallet_rebinding_required");
980
+ if (payer !== settlement.chainIdentifier(boundWallet, demand.network))
946
981
  core.fail("payment_payer_mismatch");
947
982
  }
948
983
  else if (payer !== senderValue && payer !== senderKey) {
@@ -1352,6 +1387,8 @@ class TaskInbox {
1352
1387
  history: [],
1353
1388
  audit_head: "",
1354
1389
  };
1390
+ if (validated.attribution !== undefined && validated.attribution !== null)
1391
+ task.attribution = taskClone(validated.attribution);
1355
1392
  if (usesFreeAllowance)
1356
1393
  this._parties.noteFreeAllowanceUse(sender, { now: at });
1357
1394
  taskAudit(task, "submitted", senderKey, "intake", at);
@@ -2544,11 +2581,14 @@ function createCounterpartyTable({ store, owner, now, ownerPublicKey, legacyTier
2544
2581
  },
2545
2582
  bindWallet(identity, walletAddress, { actor = "owner", reason = "wallet_binding", now: when } = {}) {
2546
2583
  const [row, when_] = mutate(identity, when);
2547
- const wallet = (core.pythonTruthy(walletAddress) ? core.pythonText(walletAddress, "wallet_address_required") : "")
2548
- .trim().toLowerCase();
2584
+ let wallet = (core.pythonTruthy(walletAddress) ? core.pythonText(walletAddress, "wallet_address_required") : "")
2585
+ .trim();
2586
+ if (/^0x/i.test(wallet))
2587
+ wallet = wallet.toLowerCase();
2549
2588
  if (!wallet)
2550
2589
  core.fail("wallet_address_required");
2551
2590
  row.bound_wallet = wallet;
2591
+ row.bound_wallet_case_preserved = true;
2552
2592
  audit(row, "bind_wallet", row.tier, row.tier, actor, `${reason}:${wallet}`, when_);
2553
2593
  return structuredClone(row);
2554
2594
  },
@@ -903,7 +903,7 @@ function x402Facilitator(value, host) {
903
903
  if (!endpoints || typeof endpoints !== "object" || Array.isArray(endpoints))
904
904
  core.fail("x402_facilitator_endpoints_invalid");
905
905
  for (const name of ["supported", "verify", "settle"]) {
906
- if (typeof endpoints[name] !== "string" || !endpoints[name].startsWith("/"))
906
+ if (typeof endpoints[name] !== "string" || !endpoints[name].startsWith("/") || endpoints[name].startsWith("//") || /[\\\s\x00-\x1f\x7f#]/.test(endpoints[name]))
907
907
  core.fail("x402_facilitator_endpoints_invalid");
908
908
  }
909
909
  const kinds = value.kinds;
@@ -1041,6 +1041,8 @@ function parseX402DnsTxt(records, host) {
1041
1041
  }
1042
1042
  function x402AddressIsUnsafe(address) {
1043
1043
  const text = String(address).trim();
1044
+ if (require("node:net").isIP(text) === 0 || text.includes("%"))
1045
+ core.fail("x402_fetch_address_invalid");
1044
1046
  const v4 = text.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
1045
1047
  if (v4) {
1046
1048
  const [a, b] = [Number(v4[1]), Number(v4[2])];
@@ -1052,15 +1054,11 @@ function x402AddressIsUnsafe(address) {
1052
1054
  || a >= 224;
1053
1055
  }
1054
1056
  if (text.includes(":")) {
1055
- const lower = text.toLowerCase();
1056
- if (!/^[0-9a-f:.]+$/.test(lower))
1057
- core.fail("x402_fetch_address_invalid");
1058
- if (lower === "::" || lower === "::1")
1059
- return true;
1060
- if (lower.startsWith("::ffff:"))
1061
- return x402AddressIsUnsafe(lower.slice("::ffff:".length));
1062
- return lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb")
1063
- || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("ff") || lower.startsWith("2001:db8") || lower.startsWith("2001:0db8");
1057
+ const lower = new URL(`https://[${text}]/`).hostname.slice(1, -1);
1058
+ const [first, second] = lower.split(":").map((part) => parseInt(part || "0", 16));
1059
+ return first < 0x2000 || first > 0x3fff || first === 0x2002
1060
+ || (first === 0x2001 && (second < 0x200 || second === 0xdb8))
1061
+ || (first === 0x3fff && second < 0x1000);
1064
1062
  }
1065
1063
  core.fail("x402_fetch_address_invalid");
1066
1064
  }
@@ -1079,6 +1077,8 @@ function checkX402FetchTarget(url, host, { resolvedAddresses = [], hop = 0, maxR
1079
1077
  if (targetHost === "localhost" || targetHost.endsWith(".localhost"))
1080
1078
  core.fail("x402_fetch_private_address");
1081
1079
  const unbracketed = targetHost.replace(/^\[|\]$/g, "");
1080
+ if (/^(?:\d+|0x[0-9a-f]*)$/.test(unbracketed.split(".").at(-1)) && !/^(\d{1,3}\.){3}\d{1,3}$/.test(unbracketed))
1081
+ core.fail("x402_fetch_private_address");
1082
1082
  if ((/^(\d{1,3}\.){3}\d{1,3}$/.test(unbracketed) || unbracketed.includes(":")) && x402AddressIsUnsafe(unbracketed)) {
1083
1083
  core.fail("x402_fetch_private_address");
1084
1084
  }
@@ -216,6 +216,16 @@ function positiveProtocolInteger(value, code) {
216
216
  core.fail(code);
217
217
  return value;
218
218
  }
219
+ function paymentNetworkKey(value) {
220
+ value = requiredProtocolText(value, "network_required");
221
+ return value.includes(":") ? value : value.toLowerCase();
222
+ }
223
+ function chainIdentifier(value, network) {
224
+ value = requiredProtocolText(value, "chain_evidence_invalid");
225
+ network = paymentNetworkKey(network);
226
+ return network.startsWith("eip155:") || ["base", "base-sepolia", "ethereum", "ethereum-sepolia", "polygon", "arbitrum", "optimism"].includes(network)
227
+ ? value.toLowerCase() : value;
228
+ }
219
229
  function computeReceiptDigest({ demandId, txHash, payer, amount, currency, settledAt, recipient, network }) {
220
230
  demandId = requiredProtocolText(demandId, "demand_id_required");
221
231
  txHash = requiredProtocolText(txHash, "tx_hash_required");
@@ -259,7 +269,7 @@ function verifySignedPaymentReceipt({ demandAmount, receiptAmount, demandRecipie
259
269
  core.fail("payment_currency_mismatch");
260
270
  demandNetwork = requiredProtocolText(demandNetwork, "network_required");
261
271
  receiptNetwork = requiredProtocolText(receiptNetwork, "network_required");
262
- if (receiptNetwork.toLowerCase() !== demandNetwork.toLowerCase())
272
+ if (paymentNetworkKey(receiptNetwork) !== paymentNetworkKey(demandNetwork))
263
273
  core.fail("payment_network_mismatch");
264
274
  demandExpiresAt = positiveProtocolInteger(demandExpiresAt, "demand_expiry_required");
265
275
  if (typeof now !== "number" || !Number.isFinite(now))
@@ -271,7 +281,7 @@ function verifySignedPaymentReceipt({ demandAmount, receiptAmount, demandRecipie
271
281
  core.fail("settled_at_in_future");
272
282
  receiptRecipient = requiredProtocolText(receiptRecipient, "recipient_required");
273
283
  demandRecipient = requiredProtocolText(demandRecipient, "expected_recipient_required");
274
- if (receiptRecipient.toLowerCase() !== demandRecipient.toLowerCase())
284
+ if (chainIdentifier(receiptRecipient, demandNetwork) !== chainIdentifier(demandRecipient, demandNetwork))
275
285
  core.fail("payment_recipient_mismatch");
276
286
  signature = requiredProtocolText(signature, "receipt_signature_required");
277
287
  txHash = requiredProtocolText(txHash, "tx_hash_required");
@@ -314,6 +324,12 @@ function isReservationStore(store) {
314
324
  typeof store.release === "function");
315
325
  }
316
326
  async function verifyX402Payment(params) {
327
+ params = { ...params };
328
+ for (const name of ["demandRecipient", "receiptRecipient", "demandCurrency", "receiptCurrency", "demandNetwork", "receiptNetwork", "txHash", "payer", "signature", "demandId", "receiptDemandId"])
329
+ if (typeof params[name] === "string") params[name] = params[name].trim();
330
+ if (params.tokenAddress !== undefined && params.tokenAddress !== null)
331
+ params.tokenAddress = requiredProtocolText(params.tokenAddress, "token_address_invalid");
332
+ Object.freeze(params);
317
333
  verifySignedPaymentReceipt(params);
318
334
  const { chainVerifier, replayStore, minimumConfirmations = 1 } = params;
319
335
  if (!chainVerifier)
@@ -327,7 +343,7 @@ async function verifyX402Payment(params) {
327
343
  if (!reservationMode) {
328
344
  core.fail("reservation_capable_store_required");
329
345
  }
330
- if (typeof minimumConfirmations !== "number" || !Number.isInteger(minimumConfirmations) || minimumConfirmations < 1) {
346
+ if (typeof minimumConfirmations !== "number" || !Number.isSafeInteger(minimumConfirmations) || minimumConfirmations < 1) {
331
347
  core.fail("minimum_confirmations_invalid");
332
348
  }
333
349
  const requiredConfirmations = minimumConfirmations;
@@ -372,20 +388,20 @@ async function verifyX402Payment(params) {
372
388
  }
373
389
  if (!evidence || typeof evidence !== "object")
374
390
  core.fail("chain_evidence_invalid");
375
- if (typeof evidence.confirmations !== "number" || !Number.isInteger(evidence.confirmations) || evidence.confirmations < 0) {
391
+ if (typeof evidence.confirmations !== "number" || !Number.isSafeInteger(evidence.confirmations) || evidence.confirmations < 0) {
376
392
  core.fail("chain_evidence_invalid");
377
393
  }
378
394
  if (evidence.finalized !== true || evidence.confirmations < requiredConfirmations) {
379
395
  core.fail("payment_not_finalized");
380
396
  }
381
397
  const exact = [
382
- [evidence.txHash, params.txHash],
383
- [evidence.network, params.demandNetwork],
384
- [evidence.payer, params.payer],
385
- [evidence.recipient, params.demandRecipient],
386
- [String(evidence.currency || "").toUpperCase(), String(params.demandCurrency).toUpperCase()],
398
+ [chainIdentifier(evidence.txHash, params.demandNetwork), chainIdentifier(params.txHash, params.demandNetwork)],
399
+ [paymentNetworkKey(evidence.network), paymentNetworkKey(params.demandNetwork)],
400
+ [chainIdentifier(evidence.payer, params.demandNetwork), chainIdentifier(params.payer, params.demandNetwork)],
401
+ [chainIdentifier(evidence.recipient, params.demandNetwork), chainIdentifier(params.demandRecipient, params.demandNetwork)],
402
+ [requiredProtocolText(evidence.currency, "chain_evidence_invalid").toUpperCase(), params.demandCurrency.toUpperCase()],
387
403
  ];
388
- if (exact.some(([actual, expected]) => String(actual || "").toLowerCase() !== String(expected || "").toLowerCase())) {
404
+ if (exact.some(([actual, expected]) => actual !== expected)) {
389
405
  core.fail("chain_evidence_mismatch");
390
406
  }
391
407
  let evidenceAmount;
@@ -397,7 +413,7 @@ async function verifyX402Payment(params) {
397
413
  }
398
414
  if (evidenceAmount !== params.receiptAmount)
399
415
  core.fail("chain_evidence_mismatch");
400
- if (params.tokenAddress && String(evidence.tokenAddress || "").toLowerCase() !== String(params.tokenAddress).toLowerCase()) {
416
+ if (params.tokenAddress && (!evidence.tokenAddress || chainIdentifier(evidence.tokenAddress, params.demandNetwork) !== chainIdentifier(params.tokenAddress, params.demandNetwork))) {
401
417
  core.fail("chain_token_mismatch");
402
418
  }
403
419
  let committed;
@@ -461,6 +477,7 @@ module.exports = {
461
477
  requiredProtocolText,
462
478
  positiveProtocolAmount,
463
479
  positiveProtocolInteger,
480
+ chainIdentifier,
464
481
  computeReceiptDigest,
465
482
  verifySignedPaymentReceipt,
466
483
  REPLAY_RESERVATION_SECONDS,
@@ -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.20-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",