@tacitprotocol/sdk 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,960 @@
1
+ // src/core/agent.ts
2
+ import { v4 as uuidv4 } from "uuid";
3
+
4
+ // src/identity/did.ts
5
+ async function generateKeypair() {
6
+ const keyPair = await crypto.subtle.generateKey(
7
+ { name: "Ed25519" },
8
+ true,
9
+ ["sign", "verify"]
10
+ );
11
+ const pair = keyPair;
12
+ const publicKeyBuffer = await crypto.subtle.exportKey("raw", pair.publicKey);
13
+ const privateKeyBuffer = await crypto.subtle.exportKey("pkcs8", pair.privateKey);
14
+ return {
15
+ publicKey: new Uint8Array(publicKeyBuffer),
16
+ privateKey: new Uint8Array(privateKeyBuffer)
17
+ };
18
+ }
19
+ function publicKeyToDid(publicKey) {
20
+ const ED25519_MULTICODEC = new Uint8Array([237, 1]);
21
+ const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKey.length);
22
+ prefixed.set(ED25519_MULTICODEC);
23
+ prefixed.set(publicKey, ED25519_MULTICODEC.length);
24
+ const encoded = base58btcEncode(prefixed);
25
+ return `did:key:z${encoded}`;
26
+ }
27
+ async function createIdentity() {
28
+ const { publicKey, privateKey } = await generateKeypair();
29
+ const did = publicKeyToDid(publicKey);
30
+ return {
31
+ did,
32
+ publicKey,
33
+ privateKey,
34
+ created: /* @__PURE__ */ new Date()
35
+ };
36
+ }
37
+ function resolveDid(did) {
38
+ if (!did.startsWith("did:key:z")) {
39
+ return null;
40
+ }
41
+ const encoded = did.slice("did:key:z".length);
42
+ const decoded = base58btcDecode(encoded);
43
+ const publicKey = decoded.slice(2);
44
+ return { publicKey };
45
+ }
46
+ async function sign(data, privateKey) {
47
+ const key = await crypto.subtle.importKey(
48
+ "pkcs8",
49
+ privateKey.buffer,
50
+ { name: "Ed25519" },
51
+ false,
52
+ ["sign"]
53
+ );
54
+ const signature = await crypto.subtle.sign("Ed25519", key, data.buffer);
55
+ return new Uint8Array(signature);
56
+ }
57
+ async function verify(data, signature, publicKey) {
58
+ const key = await crypto.subtle.importKey(
59
+ "raw",
60
+ publicKey.buffer,
61
+ { name: "Ed25519" },
62
+ false,
63
+ ["verify"]
64
+ );
65
+ return crypto.subtle.verify("Ed25519", key, signature.buffer, data.buffer);
66
+ }
67
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
68
+ function base58btcEncode(bytes) {
69
+ if (bytes.length === 0) return "";
70
+ let zeros = 0;
71
+ for (const byte of bytes) {
72
+ if (byte !== 0) break;
73
+ zeros++;
74
+ }
75
+ const digits = [0];
76
+ for (const byte of bytes) {
77
+ let carry = byte;
78
+ for (let j = 0; j < digits.length; j++) {
79
+ carry += digits[j] << 8;
80
+ digits[j] = carry % 58;
81
+ carry = carry / 58 | 0;
82
+ }
83
+ while (carry > 0) {
84
+ digits.push(carry % 58);
85
+ carry = carry / 58 | 0;
86
+ }
87
+ }
88
+ let result = "";
89
+ for (let i = 0; i < zeros; i++) result += "1";
90
+ for (let i = digits.length - 1; i >= 0; i--) result += BASE58_ALPHABET[digits[i]];
91
+ return result;
92
+ }
93
+ function base58btcDecode(str) {
94
+ if (str.length === 0) return new Uint8Array(0);
95
+ const bytes = [0];
96
+ for (const char of str) {
97
+ const value = BASE58_ALPHABET.indexOf(char);
98
+ if (value === -1) throw new Error(`Invalid base58 character: ${char}`);
99
+ let carry = value;
100
+ for (let j = 0; j < bytes.length; j++) {
101
+ carry += bytes[j] * 58;
102
+ bytes[j] = carry & 255;
103
+ carry >>= 8;
104
+ }
105
+ while (carry > 0) {
106
+ bytes.push(carry & 255);
107
+ carry >>= 8;
108
+ }
109
+ }
110
+ let zeros = 0;
111
+ for (const char of str) {
112
+ if (char !== "1") break;
113
+ zeros++;
114
+ }
115
+ const result = new Uint8Array(zeros + bytes.length);
116
+ for (let i = 0; i < zeros; i++) result[i] = 0;
117
+ for (let i = 0; i < bytes.length; i++) result[zeros + i] = bytes[bytes.length - 1 - i];
118
+ return result;
119
+ }
120
+
121
+ // src/identity/authenticity.ts
122
+ var DIMENSION_WEIGHTS = {
123
+ tenure: 0.2,
124
+ consistency: 0.3,
125
+ attestations: 0.25,
126
+ networkTrust: 0.25
127
+ };
128
+ var TRUST_LEVEL_THRESHOLDS = [
129
+ ["exemplary", 90],
130
+ ["trusted", 70],
131
+ ["established", 40],
132
+ ["emerging", 20],
133
+ ["new", 0]
134
+ ];
135
+ var CONSISTENCY_WEIGHTS = {
136
+ intentStability: 0.3,
137
+ profileConsistency: 0.25,
138
+ responseReliability: 0.25,
139
+ interactionQuality: 0.2
140
+ };
141
+ var ATTESTATION_WEIGHTS = {
142
+ institutional: 0.4,
143
+ peer: 0.3,
144
+ transaction: 0.3
145
+ };
146
+ var DECAY_RATE_PER_DAY = 1e-3;
147
+ var DECAY_GRACE_PERIOD_DAYS = 30;
148
+ var AuthenticityEngine = class {
149
+ /**
150
+ * Compute the tenure dimension score.
151
+ * Linear growth over 365 days, capped at 1.0.
152
+ */
153
+ computeTenure(agentCreatedDate, now = /* @__PURE__ */ new Date()) {
154
+ const daysActive = (now.getTime() - agentCreatedDate.getTime()) / (1e3 * 60 * 60 * 24);
155
+ return Math.min(1, Math.max(0, daysActive / 365));
156
+ }
157
+ /**
158
+ * Compute the consistency dimension score.
159
+ * Based on behavioral stability signals over time.
160
+ */
161
+ computeConsistency(signals) {
162
+ return clamp(
163
+ signals.intentStability * CONSISTENCY_WEIGHTS.intentStability + signals.profileConsistency * CONSISTENCY_WEIGHTS.profileConsistency + signals.responseReliability * CONSISTENCY_WEIGHTS.responseReliability + signals.interactionQuality * CONSISTENCY_WEIGHTS.interactionQuality
164
+ );
165
+ }
166
+ /**
167
+ * Compute the attestation dimension score.
168
+ * Based on third-party verifiable credentials.
169
+ */
170
+ computeAttestations(credentials) {
171
+ if (credentials.length === 0) return 0;
172
+ let institutional = 0;
173
+ let peer = 0;
174
+ let transaction = 0;
175
+ for (const cred of credentials) {
176
+ if (isInstitutionalCredential(cred)) {
177
+ institutional += credentialWeight(cred);
178
+ } else if (isPeerAttestation(cred)) {
179
+ peer += credentialWeight(cred);
180
+ } else {
181
+ transaction += credentialWeight(cred);
182
+ }
183
+ }
184
+ institutional = Math.min(1, institutional);
185
+ peer = Math.min(1, peer);
186
+ transaction = Math.min(1, transaction);
187
+ return clamp(
188
+ institutional * ATTESTATION_WEIGHTS.institutional + peer * ATTESTATION_WEIGHTS.peer + transaction * ATTESTATION_WEIGHTS.transaction
189
+ );
190
+ }
191
+ /**
192
+ * Compute the network trust dimension score.
193
+ * Based on the quality of the agent's network relationships.
194
+ *
195
+ * This is a simplified version — production implementations
196
+ * should use a full PageRank-style algorithm.
197
+ */
198
+ computeNetworkTrust(signals) {
199
+ if (signals.totalInteractions === 0) return 0;
200
+ const successRate = signals.successfulIntros / Math.max(1, signals.totalIntros);
201
+ const positiveRate = signals.positiveInteractions / signals.totalInteractions;
202
+ const bidirectionalBonus = Math.min(1, signals.bidirectionalTrustEdges * 0.1);
203
+ return clamp(
204
+ successRate * 0.35 + positiveRate * 0.35 + bidirectionalBonus * 0.3
205
+ );
206
+ }
207
+ /**
208
+ * Compute the full authenticity vector from all dimensions.
209
+ */
210
+ computeVector(params) {
211
+ const now = /* @__PURE__ */ new Date();
212
+ const dimensions = {
213
+ tenure: this.computeTenure(params.agentCreatedDate, now),
214
+ consistency: this.computeConsistency(params.consistencySignals),
215
+ attestations: this.computeAttestations(params.credentials),
216
+ networkTrust: this.computeNetworkTrust(params.networkSignals)
217
+ };
218
+ let score = dimensions.tenure * DIMENSION_WEIGHTS.tenure + dimensions.consistency * DIMENSION_WEIGHTS.consistency + dimensions.attestations * DIMENSION_WEIGHTS.attestations + dimensions.networkTrust * DIMENSION_WEIGHTS.networkTrust;
219
+ if (params.lastActiveDate) {
220
+ const inactiveDays = (now.getTime() - params.lastActiveDate.getTime()) / (1e3 * 60 * 60 * 24);
221
+ if (inactiveDays > DECAY_GRACE_PERIOD_DAYS) {
222
+ const decayDays = inactiveDays - DECAY_GRACE_PERIOD_DAYS;
223
+ const decayFactor = Math.max(0, 1 - decayDays * DECAY_RATE_PER_DAY);
224
+ score *= decayFactor;
225
+ }
226
+ }
227
+ score = Math.round(score * 100);
228
+ return {
229
+ level: this.scoreToLevel(score),
230
+ score,
231
+ dimensions,
232
+ verifiableCredentials: params.credentials
233
+ };
234
+ }
235
+ /**
236
+ * Convert a numeric score to a trust level.
237
+ */
238
+ scoreToLevel(score) {
239
+ for (const [level, threshold] of TRUST_LEVEL_THRESHOLDS) {
240
+ if (score >= threshold) return level;
241
+ }
242
+ return "new";
243
+ }
244
+ /**
245
+ * Check if an agent meets minimum authenticity requirements.
246
+ */
247
+ meetsMinimum(vector, minScore) {
248
+ return vector.score >= minScore;
249
+ }
250
+ };
251
+ function clamp(value, min = 0, max = 1) {
252
+ return Math.min(max, Math.max(min, value));
253
+ }
254
+ var INSTITUTIONAL_TYPES = /* @__PURE__ */ new Set([
255
+ "EmploymentCredential",
256
+ "EducationCredential",
257
+ "ProfessionalCertification",
258
+ "GovernmentId",
259
+ "LicenseCredential"
260
+ ]);
261
+ function isInstitutionalCredential(cred) {
262
+ return INSTITUTIONAL_TYPES.has(cred.type);
263
+ }
264
+ var PEER_TYPES = /* @__PURE__ */ new Set([
265
+ "PeerEndorsement",
266
+ "PeerAttestation",
267
+ "RecommendationCredential"
268
+ ]);
269
+ function isPeerAttestation(cred) {
270
+ return PEER_TYPES.has(cred.type);
271
+ }
272
+ function credentialWeight(cred) {
273
+ const issued = new Date(cred.issued);
274
+ const ageYears = (Date.now() - issued.getTime()) / (1e3 * 60 * 60 * 24 * 365);
275
+ const ageFactor = Math.max(0.1, 1 - (ageYears - 1) * 0.1);
276
+ const baseWeight = INSTITUTIONAL_TYPES.has(cred.type) ? 0.5 : 0.3;
277
+ return baseWeight * ageFactor;
278
+ }
279
+
280
+ // src/core/agent.ts
281
+ var EventBus = class {
282
+ handlers = /* @__PURE__ */ new Map();
283
+ globalHandlers = /* @__PURE__ */ new Set();
284
+ on(type, handler) {
285
+ if (!this.handlers.has(type)) {
286
+ this.handlers.set(type, /* @__PURE__ */ new Set());
287
+ }
288
+ this.handlers.get(type).add(handler);
289
+ }
290
+ onAny(handler) {
291
+ this.globalHandlers.add(handler);
292
+ }
293
+ off(type, handler) {
294
+ this.handlers.get(type)?.delete(handler);
295
+ }
296
+ async emit(event) {
297
+ const handlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
298
+ const all = [...handlers, ...this.globalHandlers];
299
+ for (const handler of all) {
300
+ try {
301
+ await handler(event);
302
+ } catch (error) {
303
+ console.error(`[Tacit] Event handler error for ${event.type}:`, error);
304
+ }
305
+ }
306
+ }
307
+ };
308
+ var TacitAgent = class {
309
+ identity = null;
310
+ config;
311
+ events = new EventBus();
312
+ authenticityEngine = new AuthenticityEngine();
313
+ intents = /* @__PURE__ */ new Map();
314
+ proposals = /* @__PURE__ */ new Map();
315
+ connected = false;
316
+ constructor(config) {
317
+ this.config = {
318
+ relayUrl: "wss://relay.tacitprotocol.dev",
319
+ matchThresholds: {
320
+ autoPropose: 80,
321
+ suggest: 60
322
+ },
323
+ preferences: {
324
+ introductionStyle: "progressive",
325
+ initialAnonymity: true,
326
+ responseTime: "24h",
327
+ languages: ["en"]
328
+ },
329
+ ...config
330
+ };
331
+ if (config.identity) {
332
+ this.identity = config.identity;
333
+ }
334
+ }
335
+ // ─── Static Factory ───────────────────────────────────────────
336
+ /**
337
+ * Create a new agent identity.
338
+ * Returns an AgentIdentity that can be passed to the constructor.
339
+ */
340
+ static async createIdentity() {
341
+ return createIdentity();
342
+ }
343
+ // ─── Lifecycle ────────────────────────────────────────────────
344
+ /**
345
+ * Connect the agent to the Tacit network via a relay node.
346
+ */
347
+ async connect() {
348
+ if (!this.identity) {
349
+ this.identity = await createIdentity();
350
+ }
351
+ this.connected = true;
352
+ await this.events.emit({
353
+ type: "connection:established",
354
+ endpoint: this.config.relayUrl
355
+ });
356
+ }
357
+ /**
358
+ * Disconnect the agent from the network.
359
+ * Active intents remain on the network until their TTL expires.
360
+ */
361
+ async disconnect() {
362
+ this.connected = false;
363
+ }
364
+ /**
365
+ * Check if the agent is connected to the network.
366
+ */
367
+ isConnected() {
368
+ return this.connected;
369
+ }
370
+ // ─── Identity ─────────────────────────────────────────────────
371
+ /**
372
+ * Get the agent's DID.
373
+ */
374
+ getDid() {
375
+ if (!this.identity) throw new Error("Agent has no identity. Call connect() first.");
376
+ return this.identity.did;
377
+ }
378
+ /**
379
+ * Get the agent's Agent Card.
380
+ */
381
+ getAgentCard() {
382
+ if (!this.identity) throw new Error("Agent has no identity. Call connect() first.");
383
+ return {
384
+ version: "0.1.0",
385
+ agent: {
386
+ did: this.identity.did,
387
+ name: this.config.profile?.name ?? "Unnamed Tacit",
388
+ description: this.config.profile?.description ?? "",
389
+ created: this.identity.created.toISOString(),
390
+ protocols: ["tacit/discovery/v0.1", "tacit/intro/v0.1"],
391
+ transport: {
392
+ type: "didcomm/v2",
393
+ endpoint: this.config.relayUrl
394
+ }
395
+ },
396
+ domains: this.config.profile ? [{
397
+ type: this.config.profile.domain,
398
+ seeking: [this.config.profile.seeking],
399
+ offering: [this.config.profile.offering],
400
+ context: {}
401
+ }] : [],
402
+ authenticity: this.getAuthenticity(),
403
+ preferences: {
404
+ introductionStyle: this.config.preferences?.introductionStyle ?? "progressive",
405
+ initialAnonymity: this.config.preferences?.initialAnonymity ?? true,
406
+ responseTime: this.config.preferences?.responseTime ?? "24h",
407
+ languages: this.config.preferences?.languages ?? ["en"]
408
+ }
409
+ };
410
+ }
411
+ /**
412
+ * Get the agent's current authenticity vector.
413
+ */
414
+ getAuthenticity() {
415
+ if (!this.identity) {
416
+ return {
417
+ level: "new",
418
+ score: 0,
419
+ dimensions: { tenure: 0, consistency: 0, attestations: 0, networkTrust: 0 },
420
+ verifiableCredentials: []
421
+ };
422
+ }
423
+ return this.authenticityEngine.computeVector({
424
+ agentCreatedDate: this.identity.created,
425
+ consistencySignals: {
426
+ intentStability: 1,
427
+ // New agent, no changes yet
428
+ profileConsistency: 1,
429
+ responseReliability: 0.5,
430
+ // No track record
431
+ interactionQuality: 0.5
432
+ },
433
+ credentials: [],
434
+ networkSignals: {
435
+ totalInteractions: 0,
436
+ positiveInteractions: 0,
437
+ totalIntros: 0,
438
+ successfulIntros: 0,
439
+ bidirectionalTrustEdges: 0
440
+ },
441
+ lastActiveDate: /* @__PURE__ */ new Date()
442
+ });
443
+ }
444
+ // ─── Intents ──────────────────────────────────────────────────
445
+ /**
446
+ * Publish an intent to the network.
447
+ */
448
+ async publishIntent(params) {
449
+ if (!this.identity) throw new Error("Agent has no identity. Call connect() first.");
450
+ const intent = {
451
+ id: `intent:${this.identity.did.split(":").pop()}:${Date.now()}`,
452
+ agentDid: this.identity.did,
453
+ type: params.type,
454
+ domain: params.domain,
455
+ intent: {
456
+ seeking: params.seeking,
457
+ context: params.context ?? {}
458
+ },
459
+ filters: {
460
+ minAuthenticityScore: params.filters?.minAuthenticityScore ?? 50,
461
+ requiredCredentials: params.filters?.requiredCredentials ?? [],
462
+ excludedDomains: []
463
+ },
464
+ privacyLevel: params.privacyLevel ?? "filtered",
465
+ ttl: params.ttlSeconds ?? 604800,
466
+ // 7 days default
467
+ created: (/* @__PURE__ */ new Date()).toISOString(),
468
+ signature: ""
469
+ // TODO: Sign with agent's private key
470
+ };
471
+ this.intents.set(intent.id, intent);
472
+ await this.events.emit({ type: "intent:published", intent });
473
+ return intent;
474
+ }
475
+ /**
476
+ * Withdraw an active intent.
477
+ */
478
+ async withdrawIntent(intentId) {
479
+ const intent = this.intents.get(intentId);
480
+ if (!intent) throw new Error(`Intent ${intentId} not found`);
481
+ this.intents.delete(intentId);
482
+ }
483
+ /**
484
+ * Get all active intents.
485
+ */
486
+ getActiveIntents() {
487
+ return Array.from(this.intents.values());
488
+ }
489
+ // ─── Proposals ────────────────────────────────────────────────
490
+ /**
491
+ * Handle an incoming match from the network.
492
+ * Called when another agent's intent is compatible with ours.
493
+ */
494
+ async handleMatch(match) {
495
+ const thresholds = this.config.matchThresholds;
496
+ if (match.score.overall >= thresholds.autoPropose) {
497
+ await this.proposeIntro(match);
498
+ } else if (match.score.overall >= thresholds.suggest) {
499
+ await this.events.emit({ type: "intent:matched", match });
500
+ }
501
+ }
502
+ /**
503
+ * Create and send an introduction proposal.
504
+ */
505
+ async proposeIntro(match) {
506
+ if (!this.identity) throw new Error("Agent has no identity.");
507
+ const proposal = {
508
+ id: `proposal:${uuidv4()}`,
509
+ type: "introduction",
510
+ initiator: {
511
+ agentDid: this.identity.did,
512
+ persona: {
513
+ displayName: this.config.profile?.name ?? "Anonymous",
514
+ context: this.config.profile?.seeking ?? "",
515
+ anonymityLevel: this.config.preferences?.initialAnonymity ? "pseudonymous" : "identified",
516
+ sessionId: uuidv4()
517
+ }
518
+ },
519
+ responder: {
520
+ agentDid: match.agents.responder
521
+ },
522
+ match: {
523
+ score: match.score.overall,
524
+ rationale: this.generateRationale(match),
525
+ domain: "professional"
526
+ // TODO: derive from intent
527
+ },
528
+ terms: {
529
+ initialReveal: "pseudonymous",
530
+ revealStages: ["domain_context", "professional_background", "identity"],
531
+ communicationChannel: "tacit_direct",
532
+ expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
533
+ },
534
+ status: "pending",
535
+ created: (/* @__PURE__ */ new Date()).toISOString(),
536
+ signature: ""
537
+ // TODO: Sign
538
+ };
539
+ this.proposals.set(proposal.id, proposal);
540
+ return proposal;
541
+ }
542
+ /**
543
+ * Accept an introduction proposal.
544
+ * This is the human's explicit consent (one half of double opt-in).
545
+ */
546
+ async acceptProposal(proposalId) {
547
+ const proposal = this.proposals.get(proposalId);
548
+ if (!proposal) throw new Error(`Proposal ${proposalId} not found`);
549
+ if (!this.identity) throw new Error("Agent has no identity.");
550
+ const isInitiator = proposal.initiator.agentDid === this.identity.did;
551
+ if (isInitiator) {
552
+ proposal.status = "accepted_by_initiator";
553
+ } else {
554
+ proposal.status = "accepted_by_responder";
555
+ }
556
+ await this.events.emit({ type: "proposal:accepted", proposal });
557
+ }
558
+ /**
559
+ * Decline an introduction proposal.
560
+ * The other party receives a generic "not a match" response.
561
+ */
562
+ async declineProposal(proposalId) {
563
+ const proposal = this.proposals.get(proposalId);
564
+ if (!proposal) throw new Error(`Proposal ${proposalId} not found`);
565
+ proposal.status = "declined";
566
+ this.proposals.delete(proposalId);
567
+ await this.events.emit({ type: "proposal:declined", proposalId });
568
+ }
569
+ /**
570
+ * Get all pending proposals.
571
+ */
572
+ getPendingProposals() {
573
+ return Array.from(this.proposals.values()).filter((p) => p.status === "pending");
574
+ }
575
+ // ─── Events ───────────────────────────────────────────────────
576
+ /**
577
+ * Subscribe to specific event types.
578
+ */
579
+ on(type, handler) {
580
+ this.events.on(type, handler);
581
+ }
582
+ /**
583
+ * Subscribe to all events.
584
+ */
585
+ onAny(handler) {
586
+ this.events.onAny(handler);
587
+ }
588
+ /**
589
+ * Unsubscribe from an event type.
590
+ */
591
+ off(type, handler) {
592
+ this.events.off(type, handler);
593
+ }
594
+ // ─── Helpers ──────────────────────────────────────────────────
595
+ generateRationale(match) {
596
+ const parts = [];
597
+ const b = match.score.breakdown;
598
+ if (b.intentAlignment > 0.8) parts.push("Strong intent alignment");
599
+ if (b.domainFit > 0.8) parts.push("Excellent domain fit");
600
+ if (b.authenticityCompatibility > 0.7) parts.push("High authenticity compatibility");
601
+ if (b.preferenceMatch > 0.7) parts.push("Good preference match");
602
+ return parts.length > 0 ? parts.join(". ") + "." : `Match score: ${match.score.overall}/100`;
603
+ }
604
+ };
605
+
606
+ // src/discovery/intent.ts
607
+ var IntentBuilder = class {
608
+ partial = {};
609
+ constructor(agentDid) {
610
+ this.partial.agentDid = agentDid;
611
+ this.partial.id = `intent:${agentDid.split(":").pop()}:${Date.now()}`;
612
+ this.partial.created = (/* @__PURE__ */ new Date()).toISOString();
613
+ this.partial.privacyLevel = "filtered";
614
+ this.partial.ttl = 604800;
615
+ this.partial.filters = {
616
+ minAuthenticityScore: 50,
617
+ requiredCredentials: [],
618
+ excludedDomains: []
619
+ };
620
+ }
621
+ type(type) {
622
+ this.partial.type = type;
623
+ return this;
624
+ }
625
+ domain(domain) {
626
+ this.partial.domain = domain;
627
+ return this;
628
+ }
629
+ seeking(seeking) {
630
+ if (!this.partial.intent) {
631
+ this.partial.intent = { seeking: {}, context: {} };
632
+ }
633
+ this.partial.intent.seeking = seeking;
634
+ return this;
635
+ }
636
+ context(context) {
637
+ if (!this.partial.intent) {
638
+ this.partial.intent = { seeking: {}, context: {} };
639
+ }
640
+ this.partial.intent.context = context;
641
+ return this;
642
+ }
643
+ privacy(level) {
644
+ this.partial.privacyLevel = level;
645
+ return this;
646
+ }
647
+ ttl(seconds) {
648
+ this.partial.ttl = seconds;
649
+ return this;
650
+ }
651
+ minAuthenticity(score) {
652
+ this.partial.filters.minAuthenticityScore = score;
653
+ return this;
654
+ }
655
+ requireCredentials(...types) {
656
+ this.partial.filters.requiredCredentials = types;
657
+ return this;
658
+ }
659
+ build() {
660
+ if (!this.partial.type) throw new Error("Intent type is required");
661
+ if (!this.partial.domain) throw new Error("Intent domain is required");
662
+ if (!this.partial.intent?.seeking) throw new Error("Intent seeking is required");
663
+ return {
664
+ id: this.partial.id,
665
+ agentDid: this.partial.agentDid,
666
+ type: this.partial.type,
667
+ domain: this.partial.domain,
668
+ intent: this.partial.intent,
669
+ filters: this.partial.filters,
670
+ privacyLevel: this.partial.privacyLevel,
671
+ ttl: this.partial.ttl,
672
+ created: this.partial.created,
673
+ signature: ""
674
+ // Signed by agent before publishing
675
+ };
676
+ }
677
+ };
678
+ var IntentStore = class {
679
+ intents = /* @__PURE__ */ new Map();
680
+ add(intent) {
681
+ this.intents.set(intent.id, { intent, status: "active" });
682
+ }
683
+ get(id) {
684
+ return this.intents.get(id)?.intent;
685
+ }
686
+ getStatus(id) {
687
+ return this.intents.get(id)?.status;
688
+ }
689
+ setStatus(id, status) {
690
+ const entry = this.intents.get(id);
691
+ if (entry) entry.status = status;
692
+ }
693
+ withdraw(id) {
694
+ const entry = this.intents.get(id);
695
+ if (!entry) return false;
696
+ entry.status = "withdrawn";
697
+ return true;
698
+ }
699
+ /**
700
+ * Get all active intents (not expired, not withdrawn, not fulfilled).
701
+ */
702
+ getActive() {
703
+ const now = Date.now();
704
+ const active = [];
705
+ for (const [, entry] of this.intents) {
706
+ if (entry.status !== "active") continue;
707
+ const created = new Date(entry.intent.created).getTime();
708
+ if (now - created > entry.intent.ttl * 1e3) {
709
+ entry.status = "expired";
710
+ continue;
711
+ }
712
+ active.push(entry.intent);
713
+ }
714
+ return active;
715
+ }
716
+ /**
717
+ * Find intents that match a given query.
718
+ * This is the local equivalent of a relay query.
719
+ */
720
+ query(params) {
721
+ return this.getActive().filter((intent) => {
722
+ if (params.type && intent.type !== params.type) return false;
723
+ if (params.domain && intent.domain !== params.domain) return false;
724
+ if (params.keywords && params.keywords.length > 0) {
725
+ const intentText = JSON.stringify(intent.intent).toLowerCase();
726
+ const hasKeyword = params.keywords.some(
727
+ (kw) => intentText.includes(kw.toLowerCase())
728
+ );
729
+ if (!hasKeyword) return false;
730
+ }
731
+ return true;
732
+ });
733
+ }
734
+ /**
735
+ * Remove expired intents from the store.
736
+ */
737
+ cleanup() {
738
+ const now = Date.now();
739
+ let removed = 0;
740
+ for (const [id, entry] of this.intents) {
741
+ if (entry.status === "expired" || entry.status === "withdrawn" || entry.status === "fulfilled") {
742
+ this.intents.delete(id);
743
+ removed++;
744
+ }
745
+ if (entry.status === "active") {
746
+ const created = new Date(entry.intent.created).getTime();
747
+ if (now - created > entry.intent.ttl * 1e3) {
748
+ entry.status = "expired";
749
+ this.intents.delete(id);
750
+ removed++;
751
+ }
752
+ }
753
+ }
754
+ return removed;
755
+ }
756
+ };
757
+
758
+ // src/matching/scorer.ts
759
+ import { v4 as uuidv42 } from "uuid";
760
+ var SCORE_WEIGHTS = {
761
+ intentAlignment: 0.3,
762
+ domainFit: 0.25,
763
+ authenticityCompatibility: 0.2,
764
+ preferenceMatch: 0.15,
765
+ timingFit: 0.1
766
+ };
767
+ var DEFAULT_THRESHOLDS = {
768
+ autoPropose: 80,
769
+ suggest: 60
770
+ };
771
+ var MatchScorer = class {
772
+ thresholds;
773
+ constructor(thresholds) {
774
+ this.thresholds = {
775
+ autoPropose: thresholds?.autoPropose ?? DEFAULT_THRESHOLDS.autoPropose,
776
+ suggest: thresholds?.suggest ?? DEFAULT_THRESHOLDS.suggest
777
+ };
778
+ }
779
+ /**
780
+ * Score the compatibility between two agents based on their intents and cards.
781
+ */
782
+ score(params) {
783
+ const { initiator, responder } = params;
784
+ const breakdown = {
785
+ intentAlignment: this.scoreIntentAlignment(initiator.intent, responder.intent),
786
+ domainFit: this.scoreDomainFit(initiator.intent, responder.intent),
787
+ authenticityCompatibility: this.scoreAuthenticityCompatibility(
788
+ initiator.card.authenticity,
789
+ responder.card.authenticity,
790
+ initiator.intent,
791
+ responder.intent
792
+ ),
793
+ preferenceMatch: this.scorePreferenceMatch(initiator.card, responder.card),
794
+ timingFit: this.scoreTimingFit(initiator.intent, responder.intent)
795
+ };
796
+ const overall = Math.round(
797
+ breakdown.intentAlignment * SCORE_WEIGHTS.intentAlignment + breakdown.domainFit * SCORE_WEIGHTS.domainFit + breakdown.authenticityCompatibility * SCORE_WEIGHTS.authenticityCompatibility + breakdown.preferenceMatch * SCORE_WEIGHTS.preferenceMatch + breakdown.timingFit * SCORE_WEIGHTS.timingFit
798
+ ) * 100;
799
+ return {
800
+ matchId: `match:${uuidv42()}`,
801
+ agents: {
802
+ initiator: initiator.card.agent.did,
803
+ responder: responder.card.agent.did
804
+ },
805
+ score: {
806
+ overall: Math.min(100, Math.max(0, overall)),
807
+ breakdown
808
+ },
809
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
810
+ };
811
+ }
812
+ /**
813
+ * Determine what action to take based on a match score.
814
+ */
815
+ determineAction(score) {
816
+ if (score >= this.thresholds.autoPropose) return "auto-propose";
817
+ if (score >= this.thresholds.suggest) return "suggest";
818
+ return "ignore";
819
+ }
820
+ // ─── Dimension Scorers ──────────────────────────────────────────
821
+ /**
822
+ * Score how well the two intents complement each other.
823
+ * A seeking-offering match scores highest.
824
+ */
825
+ scoreIntentAlignment(a, b) {
826
+ const aSeeking = flattenValues(a.intent.seeking);
827
+ const bSeeking = flattenValues(b.intent.seeking);
828
+ const aContext = flattenValues(a.intent.context);
829
+ const bContext = flattenValues(b.intent.context);
830
+ const aToB = computeOverlap(aSeeking, [...bSeeking, ...bContext]);
831
+ const bToA = computeOverlap(bSeeking, [...aSeeking, ...aContext]);
832
+ return (aToB + bToA) / 2;
833
+ }
834
+ /**
835
+ * Score the domain overlap between two intents.
836
+ */
837
+ scoreDomainFit(a, b) {
838
+ if (a.domain === b.domain) return 0.9;
839
+ const related = RELATED_DOMAINS.get(a.domain);
840
+ if (related?.includes(b.domain)) return 0.5;
841
+ return 0.1;
842
+ }
843
+ /**
844
+ * Score whether both agents meet each other's authenticity requirements.
845
+ */
846
+ scoreAuthenticityCompatibility(aAuth, bAuth, aIntent, bIntent) {
847
+ const aMinScore = aIntent.filters.minAuthenticityScore;
848
+ const bMinScore = bIntent.filters.minAuthenticityScore;
849
+ const aMeetsBMin = bAuth.score >= aMinScore;
850
+ const bMeetsAMin = aAuth.score >= bMinScore;
851
+ if (aMeetsBMin && bMeetsAMin) {
852
+ const aExcess = (bAuth.score - aMinScore) / (100 - aMinScore);
853
+ const bExcess = (aAuth.score - bMinScore) / (100 - bMinScore);
854
+ return 0.6 + (aExcess + bExcess) / 2 * 0.4;
855
+ }
856
+ if (aMeetsBMin || bMeetsAMin) {
857
+ return 0.3;
858
+ }
859
+ return 0;
860
+ }
861
+ /**
862
+ * Score preference compatibility (communication style, timing, language).
863
+ */
864
+ scorePreferenceMatch(a, b) {
865
+ let score = 0;
866
+ let factors = 0;
867
+ const langOverlap = a.preferences.languages.filter(
868
+ (l) => b.preferences.languages.includes(l)
869
+ );
870
+ if (langOverlap.length > 0) {
871
+ score += 1;
872
+ }
873
+ factors++;
874
+ if (a.preferences.introductionStyle === b.preferences.introductionStyle) {
875
+ score += 1;
876
+ } else {
877
+ score += 0.5;
878
+ }
879
+ factors++;
880
+ return factors > 0 ? score / factors : 0.5;
881
+ }
882
+ /**
883
+ * Score timing compatibility — are both intents active and aligned in urgency?
884
+ */
885
+ scoreTimingFit(a, b) {
886
+ const now = Date.now();
887
+ const aExpiry = new Date(a.created).getTime() + a.ttl * 1e3;
888
+ const bExpiry = new Date(b.created).getTime() + b.ttl * 1e3;
889
+ if (now > aExpiry || now > bExpiry) return 0;
890
+ const aUrgency = extractUrgency(a.intent.context);
891
+ const bUrgency = extractUrgency(b.intent.context);
892
+ if (aUrgency === bUrgency) return 1;
893
+ if (Math.abs(aUrgency - bUrgency) <= 1) return 0.7;
894
+ return 0.3;
895
+ }
896
+ };
897
+ function flattenValues(obj) {
898
+ const values = [];
899
+ for (const value of Object.values(obj)) {
900
+ if (typeof value === "string") {
901
+ values.push(value.toLowerCase());
902
+ } else if (Array.isArray(value)) {
903
+ values.push(...value.filter((v) => typeof v === "string").map((v) => v.toLowerCase()));
904
+ }
905
+ }
906
+ return values;
907
+ }
908
+ function computeOverlap(a, b) {
909
+ if (a.length === 0 || b.length === 0) return 0;
910
+ let matches = 0;
911
+ for (const term of a) {
912
+ for (const bTerm of b) {
913
+ if (term === bTerm || bTerm.includes(term) || term.includes(bTerm)) {
914
+ matches++;
915
+ break;
916
+ }
917
+ }
918
+ }
919
+ return matches / Math.max(a.length, 1);
920
+ }
921
+ function extractUrgency(context) {
922
+ const urgency = context["urgency"];
923
+ if (typeof urgency !== "string") return 1;
924
+ switch (urgency.toLowerCase()) {
925
+ case "immediate":
926
+ case "asap":
927
+ return 3;
928
+ case "active":
929
+ case "urgent":
930
+ return 2;
931
+ case "moderate":
932
+ case "normal":
933
+ return 1;
934
+ case "passive":
935
+ case "low":
936
+ case "whenever":
937
+ return 0;
938
+ default:
939
+ return 1;
940
+ }
941
+ }
942
+ var RELATED_DOMAINS = /* @__PURE__ */ new Map([
943
+ ["professional", ["commerce", "learning"]],
944
+ ["dating", []],
945
+ ["local-services", ["commerce"]],
946
+ ["learning", ["professional"]],
947
+ ["commerce", ["professional", "local-services"]]
948
+ ]);
949
+ export {
950
+ AuthenticityEngine,
951
+ IntentBuilder,
952
+ IntentStore,
953
+ MatchScorer,
954
+ TacitAgent,
955
+ createIdentity,
956
+ publicKeyToDid,
957
+ resolveDid,
958
+ sign,
959
+ verify
960
+ };