@adastracomputing/ink 0.1.0-alpha.3 → 0.1.1

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +15 -3
  3. package/dist/audit/inclusion-receipt.d.ts +142 -0
  4. package/dist/audit/inclusion-receipt.js +496 -0
  5. package/dist/crypto/ink.d.ts +178 -0
  6. package/dist/crypto/ink.js +915 -0
  7. package/dist/crypto/keys.d.ts +42 -0
  8. package/dist/crypto/keys.js +179 -0
  9. package/dist/crypto/multi-key-verify.d.ts +29 -0
  10. package/dist/crypto/multi-key-verify.js +153 -0
  11. package/dist/crypto/sign.d.ts +17 -0
  12. package/dist/crypto/sign.js +152 -0
  13. package/dist/crypto/verify.js +1 -0
  14. package/dist/discovery/agent-card.d.ts +83 -0
  15. package/dist/discovery/agent-card.js +545 -0
  16. package/dist/index.d.ts +13 -0
  17. package/dist/index.js +16 -0
  18. package/dist/ink/checkpoint.d.ts +19 -0
  19. package/dist/ink/checkpoint.js +69 -0
  20. package/dist/ink/discovery-gating.d.ts +247 -0
  21. package/dist/ink/discovery-gating.js +94 -0
  22. package/dist/ink/handshake-budget.d.ts +90 -0
  23. package/dist/ink/handshake-budget.js +397 -0
  24. package/dist/ink/receipts.d.ts +31 -0
  25. package/dist/ink/receipts.js +89 -0
  26. package/dist/ink/transport-auth.d.ts +47 -0
  27. package/dist/ink/transport-auth.js +77 -0
  28. package/dist/middleware/ink-auth.d.ts +68 -0
  29. package/dist/middleware/ink-auth.js +214 -0
  30. package/dist/models/agent-card.d.ts +170 -0
  31. package/dist/models/agent-card.js +107 -0
  32. package/dist/models/ink-audit.d.ts +344 -0
  33. package/dist/models/ink-audit.js +167 -0
  34. package/dist/models/ink-handshake.d.ts +129 -0
  35. package/dist/models/ink-handshake.js +89 -0
  36. package/dist/models/intent.d.ts +437 -0
  37. package/dist/models/intent.js +172 -0
  38. package/dist/models/key-entry.d.ts +60 -0
  39. package/dist/models/key-entry.js +13 -0
  40. package/dist/models/profile.d.ts +61 -0
  41. package/dist/models/profile.js +24 -0
  42. package/package.json +15 -11
  43. package/specs/ink-auditability.md +2 -2
  44. package/specs/ink-containment-phase1-implementation-spec.md +1 -1
  45. package/src/audit/inclusion-receipt.ts +0 -604
  46. package/src/crypto/ink.ts +0 -1046
  47. package/src/crypto/keys.ts +0 -210
  48. package/src/crypto/multi-key-verify.ts +0 -170
  49. package/src/crypto/sign.ts +0 -155
  50. package/src/discovery/agent-card.ts +0 -508
  51. package/src/index.ts +0 -73
  52. package/src/ink/checkpoint.ts +0 -75
  53. package/src/ink/discovery-gating.ts +0 -147
  54. package/src/ink/handshake-budget.ts +0 -413
  55. package/src/ink/receipts.ts +0 -114
  56. package/src/ink/transport-auth.ts +0 -96
  57. package/src/middleware/ink-auth.ts +0 -263
  58. package/src/models/agent-card.ts +0 -63
  59. package/src/models/ink-audit.ts +0 -205
  60. package/src/models/ink-handshake.ts +0 -123
  61. package/src/models/intent.ts +0 -201
  62. package/src/models/key-entry.ts +0 -52
  63. package/src/models/profile.ts +0 -31
  64. /package/{src/crypto/verify.ts → dist/crypto/verify.d.ts} +0 -0
@@ -0,0 +1,68 @@
1
+ import type { CandidateKey, KeyStatus } from "../models/key-entry.js";
2
+ /**
3
+ * Pluggable nonce-record interface. The middleware uses this to enforce
4
+ * single-use semantics on body.nonce so a captured-and-replayed request
5
+ * is rejected even within the timestamp freshness window.
6
+ */
7
+ export interface NonceStore {
8
+ has(nonce: string): boolean | Promise<boolean>;
9
+ add(nonce: string): void | Promise<void>;
10
+ }
11
+ /**
12
+ * Parse and verify an INK-Ed25519 Authorization header.
13
+ *
14
+ * The spec (§3.3) defines request signing as:
15
+ * Authorization: INK-Ed25519 <base64url(sig)>
16
+ *
17
+ * Signature base: ink/0.1\nMETHOD\nPATH\nrecipientDid\nJCS(body)\ntimestamp
18
+ *
19
+ * The body must contain `from` (sender DID/agentId — used to resolve the public key)
20
+ * and `timestamp` (used in the signature base).
21
+ *
22
+ * Also enforces timestamp freshness per §3.5:
23
+ * - Rejects timestamps older than 5 minutes
24
+ * - Rejects timestamps more than 30 seconds in the future
25
+ *
26
+ * Key resolution order:
27
+ * 1. resolveKeySet (multi-key, if provided and returns candidates)
28
+ * 2. resolvePublicKey (single-key from connection store)
29
+ * 3. extractPublicKeyFromAgentId (bootstrap fallback — only when no key set exists)
30
+ */
31
+ export declare function verifyInkAuth(opts: {
32
+ authHeader: string | undefined;
33
+ method: string;
34
+ path: string;
35
+ recipientAgentId: string;
36
+ body: Record<string, unknown>;
37
+ resolvePublicKey?: (agentId: string) => Uint8Array | null;
38
+ resolveKeySet?: (agentId: string) => CandidateKey[] | null;
39
+ /**
40
+ * When true, signatures that only verify against a retired key are
41
+ * rejected with `retired_key_for_live_auth`. Defaults to false so the
42
+ * middleware stays spec-conformant (active OR retired during rotation
43
+ * grace per the authority rule) but lets callers opt into the stricter
44
+ * policy for endpoints that should never accept a possibly-stolen
45
+ * retired key. Bootstrap and single-key (resolvePublicKey) verification
46
+ * paths are unaffected because they do not have status metadata.
47
+ */
48
+ requireActiveKey?: boolean;
49
+ /**
50
+ * Single-use nonce enforcement. Required (fail-closed) because the
51
+ * 5-minute freshness window otherwise allows a captured signed request
52
+ * to replay. Pass a NonceStore to have the middleware check+record
53
+ * body.nonce, or pass the literal "deferred" to explicitly take
54
+ * responsibility for calling `checkReplay` (or equivalent) in the
55
+ * caller's own request pipeline. Omitting this option returns
56
+ * `nonce_handling_required` so misconfigured production deployments
57
+ * fail loudly.
58
+ */
59
+ nonceStore: NonceStore | "deferred";
60
+ }): Promise<{
61
+ valid: true;
62
+ senderAgentId: string;
63
+ keyId?: string;
64
+ keyStatus?: KeyStatus;
65
+ } | {
66
+ valid: false;
67
+ error: string;
68
+ }>;
@@ -0,0 +1,214 @@
1
+ import { verifyInkSignature, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS } from "../crypto/ink.js";
2
+ import { extractPublicKeyFromAgentId } from "../crypto/keys.js";
3
+ import { verifyInkSignatureWithKeys } from "../crypto/multi-key-verify.js";
4
+ /**
5
+ * Parse and verify an INK-Ed25519 Authorization header.
6
+ *
7
+ * The spec (§3.3) defines request signing as:
8
+ * Authorization: INK-Ed25519 <base64url(sig)>
9
+ *
10
+ * Signature base: ink/0.1\nMETHOD\nPATH\nrecipientDid\nJCS(body)\ntimestamp
11
+ *
12
+ * The body must contain `from` (sender DID/agentId — used to resolve the public key)
13
+ * and `timestamp` (used in the signature base).
14
+ *
15
+ * Also enforces timestamp freshness per §3.5:
16
+ * - Rejects timestamps older than 5 minutes
17
+ * - Rejects timestamps more than 30 seconds in the future
18
+ *
19
+ * Key resolution order:
20
+ * 1. resolveKeySet (multi-key, if provided and returns candidates)
21
+ * 2. resolvePublicKey (single-key from connection store)
22
+ * 3. extractPublicKeyFromAgentId (bootstrap fallback — only when no key set exists)
23
+ */
24
+ export async function verifyInkAuth(opts) {
25
+ if (typeof opts.authHeader !== "string" || opts.authHeader.length === 0) {
26
+ return { valid: false, error: "missing_authorization" };
27
+ }
28
+ if (opts.body === null || typeof opts.body !== "object" || Array.isArray(opts.body)) {
29
+ return { valid: false, error: "missing_sender" };
30
+ }
31
+ if (opts.authHeader.length > 512) {
32
+ return { valid: false, error: "invalid_auth_scheme" };
33
+ }
34
+ // Ed25519 signatures are exactly 86 base64url chars — tighten the regex to
35
+ // {86} so clearly-wrong lengths get rejected up front, rather than burning
36
+ // CPU on verifyInkSignature for a malformed value.
37
+ const match = opts.authHeader.match(/^INK-Ed25519\s+([A-Za-z0-9_-]{86})(?:\s+keyId=([A-Za-z0-9_:.-]{1,128}))?$/);
38
+ if (!match) {
39
+ return { valid: false, error: "invalid_auth_scheme" };
40
+ }
41
+ const signature = match[1];
42
+ const hintKeyId = match[2] ?? undefined;
43
+ const senderDid = opts.body.from;
44
+ if (senderDid !== undefined && typeof senderDid !== "string") {
45
+ return { valid: false, error: "invalid_from_field" };
46
+ }
47
+ if (!senderDid) {
48
+ return { valid: false, error: "missing_sender" };
49
+ }
50
+ // Cap sender DID length before passing to key resolvers and base58 decoding.
51
+ // Real agent IDs are ~50-100 chars; 256 leaves generous headroom while
52
+ // preventing CPU/memory waste on huge attacker-supplied values.
53
+ if (senderDid.length > 256) {
54
+ return { valid: false, error: "invalid_from_field" };
55
+ }
56
+ const timestamp = opts.body.timestamp;
57
+ if (typeof timestamp !== "string" || timestamp.length === 0) {
58
+ return { valid: false, error: "missing_timestamp" };
59
+ }
60
+ // Cap length BEFORE handing to Date.parse. Real ISO 8601 timestamps
61
+ // are ≤ ~30 chars; we cap at 64 (matches buildSignatureBase). Without
62
+ // this, an unauthenticated request with a multi-megabyte timestamp
63
+ // string burns CPU inside the engine's Date parser before the
64
+ // signature ever runs.
65
+ if (timestamp.length > 64) {
66
+ return { valid: false, error: "invalid_timestamp" };
67
+ }
68
+ // Timestamp freshness check (§3.5)
69
+ const msgTime = new Date(timestamp).getTime();
70
+ if (isNaN(msgTime)) {
71
+ return { valid: false, error: "invalid_timestamp" };
72
+ }
73
+ const now = Date.now();
74
+ const drift = msgTime - now;
75
+ if (drift > MAX_FUTURE_TIMESTAMP_MS) {
76
+ return { valid: false, error: "timestamp_too_far_future" };
77
+ }
78
+ if (-drift > MAX_TIMESTAMP_AGE_MS) {
79
+ return { valid: false, error: "timestamp_expired" };
80
+ }
81
+ // Fail-closed nonce policy. Callers must either pass a NonceStore
82
+ // (middleware enforces single-use within the freshness window) or
83
+ // explicitly pass "deferred" (caller commits to calling checkReplay
84
+ // or equivalent in their request pipeline). An omitted/malformed
85
+ // nonceStore returns nonce_handling_required so a production
86
+ // deployment without nonce handling fails loudly rather than
87
+ // silently accepting replays.
88
+ const storeIsObject = opts.nonceStore !== "deferred" &&
89
+ opts.nonceStore !== undefined &&
90
+ opts.nonceStore !== null &&
91
+ typeof opts.nonceStore.has === "function" &&
92
+ typeof opts.nonceStore.add === "function";
93
+ if (opts.nonceStore !== "deferred" && !storeIsObject) {
94
+ return { valid: false, error: "nonce_handling_required" };
95
+ }
96
+ const usingNonceStore = storeIsObject;
97
+ let bodyNonce;
98
+ if (usingNonceStore) {
99
+ const candidate = opts.body.nonce;
100
+ if (typeof candidate !== "string" ||
101
+ candidate.length < 16 ||
102
+ candidate.length > 256 ||
103
+ !/^[A-Za-z0-9_-]+$/.test(candidate)) {
104
+ return { valid: false, error: "missing_nonce" };
105
+ }
106
+ bodyNonce = candidate;
107
+ }
108
+ const input = {
109
+ method: opts.method,
110
+ path: opts.path,
111
+ recipientDid: opts.recipientAgentId,
112
+ body: opts.body,
113
+ timestamp,
114
+ };
115
+ // Post-verify nonce check+record. Runs only when the caller provided
116
+ // a NonceStore object. Checking after signature verification means a
117
+ // forged request never pollutes the nonce store, but a replay of an
118
+ // authentic signed request is still rejected within the freshness
119
+ // window. Backend errors fail closed.
120
+ async function recordNonce() {
121
+ if (!usingNonceStore)
122
+ return { ok: true };
123
+ const store = opts.nonceStore;
124
+ const nonce = bodyNonce;
125
+ let alreadySeen;
126
+ try {
127
+ alreadySeen = await Promise.resolve(store.has(nonce));
128
+ }
129
+ catch {
130
+ return { ok: false, error: "nonce_store_error" };
131
+ }
132
+ if (alreadySeen)
133
+ return { ok: false, error: "nonce_replay" };
134
+ try {
135
+ await Promise.resolve(store.add(nonce));
136
+ }
137
+ catch {
138
+ return { ok: false, error: "nonce_store_error" };
139
+ }
140
+ return { ok: true };
141
+ }
142
+ // Try multi-key verification first (Phase 1 key rotation support).
143
+ // If the agent has published a key set, it is authoritative: we must NOT
144
+ // fall through to resolvePublicKey or the bootstrap derivation, because
145
+ // either could surface a key (retired/revoked or stale conn-stored) that
146
+ // was already rejected — or deliberately excluded — from the key set.
147
+ if (opts.resolveKeySet) {
148
+ const candidates = opts.resolveKeySet(senderDid);
149
+ // null/undefined = no key set published for this agent → fall through to bootstrap.
150
+ // Empty array = key set exists but no usable signing keys (e.g. all revoked) →
151
+ // authoritative reject. Falling through here would let an attacker with the
152
+ // bootstrap-derived key authenticate even after the agent has revoked it.
153
+ if (candidates !== null && candidates !== undefined) {
154
+ if (candidates.length === 0) {
155
+ return { valid: false, error: "signature_verification_failed" };
156
+ }
157
+ try {
158
+ const result = await verifyInkSignatureWithKeys(input, signature, candidates, hintKeyId);
159
+ if (result.verified) {
160
+ // Local-policy gate: a retired key still verifies per the spec's
161
+ // authority rule, but a caller that runs sensitive endpoints
162
+ // (writes, capability grants, etc.) can require an active key.
163
+ // This closes the "stolen retired key signs a fresh message"
164
+ // window: even though the spec allows retired keys for grace,
165
+ // callers don't have to.
166
+ if (opts.requireActiveKey && result.keyStatus === "retired") {
167
+ return { valid: false, error: "retired_key_for_live_auth" };
168
+ }
169
+ const noncePass = await recordNonce();
170
+ if (!noncePass.ok)
171
+ return { valid: false, error: noncePass.error };
172
+ return {
173
+ valid: true,
174
+ senderAgentId: senderDid,
175
+ keyId: result.keyId,
176
+ keyStatus: result.keyStatus,
177
+ };
178
+ }
179
+ }
180
+ catch { /* treated as verification failure below */ }
181
+ // Authoritative key set rejected the signature — do not fall back.
182
+ return { valid: false, error: "signature_verification_failed" };
183
+ }
184
+ }
185
+ // No key set published yet — first-contact / bootstrap path.
186
+ let publicKey = null;
187
+ if (opts.resolvePublicKey) {
188
+ publicKey = opts.resolvePublicKey(senderDid);
189
+ }
190
+ if (!publicKey) {
191
+ try {
192
+ publicKey = extractPublicKeyFromAgentId(senderDid);
193
+ }
194
+ catch {
195
+ return { valid: false, error: "unresolvable_sender_key" };
196
+ }
197
+ }
198
+ if (!publicKey) {
199
+ return { valid: false, error: "unresolvable_sender_key" };
200
+ }
201
+ try {
202
+ const valid = await verifyInkSignature(input, signature, publicKey);
203
+ if (!valid) {
204
+ return { valid: false, error: "invalid_signature" };
205
+ }
206
+ const noncePass = await recordNonce();
207
+ if (!noncePass.ok)
208
+ return { valid: false, error: noncePass.error };
209
+ return { valid: true, senderAgentId: senderDid };
210
+ }
211
+ catch {
212
+ return { valid: false, error: "signature_verification_failed" };
213
+ }
214
+ }
@@ -0,0 +1,170 @@
1
+ import { z } from "zod";
2
+ export declare const ThirdPartyAuditServiceSchema: z.ZodObject<{
3
+ endpoint: z.ZodString;
4
+ did: z.ZodString;
5
+ publicKey: z.ZodString;
6
+ }, z.core.$strip>;
7
+ export declare const AgentCardSchema: z.ZodObject<{
8
+ protocol: z.ZodLiteral<"ink/0.1">;
9
+ agentId: z.ZodString;
10
+ ownerDid: z.ZodOptional<z.ZodString>;
11
+ ownerHandle: z.ZodOptional<z.ZodString>;
12
+ atprotoRecordUri: z.ZodOptional<z.ZodString>;
13
+ handle: z.ZodString;
14
+ displayName: z.ZodString;
15
+ endpoint: z.ZodString;
16
+ inboxEndpoint: z.ZodOptional<z.ZodString>;
17
+ publicKeyMultibase: z.ZodString;
18
+ profileSnapshot: z.ZodOptional<z.ZodObject<{
19
+ headline: z.ZodString;
20
+ skills: z.ZodArray<z.ZodString>;
21
+ interests: z.ZodArray<z.ZodString>;
22
+ availability: z.ZodOptional<z.ZodObject<{
23
+ timezone: z.ZodString;
24
+ meetingHours: z.ZodOptional<z.ZodString>;
25
+ responseSla: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>;
27
+ openTo: z.ZodArray<z.ZodString>;
28
+ }, z.core.$strip>>;
29
+ capabilities: z.ZodObject<{
30
+ intentsAccepted: z.ZodArray<z.ZodEnum<{
31
+ schedule_meeting: "schedule_meeting";
32
+ schedule_meeting_response: "schedule_meeting_response";
33
+ intro_request: "intro_request";
34
+ intro_response: "intro_response";
35
+ opportunity: "opportunity";
36
+ opportunity_response: "opportunity_response";
37
+ follow_up: "follow_up";
38
+ ask: "ask";
39
+ ask_response: "ask_response";
40
+ connection_request: "connection_request";
41
+ connection_response: "connection_response";
42
+ context_share: "context_share";
43
+ ping: "ping";
44
+ retract: "retract";
45
+ multi_party_sync: "multi_party_sync";
46
+ }>>;
47
+ intentsSent: z.ZodArray<z.ZodEnum<{
48
+ schedule_meeting: "schedule_meeting";
49
+ schedule_meeting_response: "schedule_meeting_response";
50
+ intro_request: "intro_request";
51
+ intro_response: "intro_response";
52
+ opportunity: "opportunity";
53
+ opportunity_response: "opportunity_response";
54
+ follow_up: "follow_up";
55
+ ask: "ask";
56
+ ask_response: "ask_response";
57
+ connection_request: "connection_request";
58
+ connection_response: "connection_response";
59
+ context_share: "context_share";
60
+ ping: "ping";
61
+ retract: "retract";
62
+ multi_party_sync: "multi_party_sync";
63
+ }>>;
64
+ receipts: z.ZodOptional<z.ZodObject<{
65
+ send: z.ZodBoolean;
66
+ dispositions: z.ZodArray<z.ZodEnum<{
67
+ received: "received";
68
+ delivered: "delivered";
69
+ acted: "acted";
70
+ rejected: "rejected";
71
+ expired: "expired";
72
+ }>>;
73
+ }, z.core.$strip>>;
74
+ auditExchange: z.ZodOptional<z.ZodBoolean>;
75
+ thirdPartyAudit: z.ZodOptional<z.ZodObject<{
76
+ services: z.ZodArray<z.ZodObject<{
77
+ endpoint: z.ZodString;
78
+ did: z.ZodString;
79
+ publicKey: z.ZodString;
80
+ }, z.core.$strip>>;
81
+ submitPolicy: z.ZodEnum<{
82
+ none: "none";
83
+ all: "all";
84
+ high_value: "high_value";
85
+ }>;
86
+ }, z.core.$strip>>;
87
+ }, z.core.$strip>;
88
+ availability: z.ZodObject<{
89
+ timezone: z.ZodString;
90
+ meetingHours: z.ZodOptional<z.ZodString>;
91
+ responseSla: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>;
93
+ keys: z.ZodOptional<z.ZodObject<{
94
+ signing: z.ZodArray<z.ZodObject<{
95
+ keyId: z.ZodString;
96
+ algorithm: z.ZodEnum<{
97
+ Ed25519: "Ed25519";
98
+ X25519: "X25519";
99
+ }>;
100
+ publicKeyMultibase: z.ZodString;
101
+ status: z.ZodEnum<{
102
+ active: "active";
103
+ retired: "retired";
104
+ revoked: "revoked";
105
+ }>;
106
+ validFrom: z.ZodString;
107
+ validUntil: z.ZodOptional<z.ZodString>;
108
+ revokedAt: z.ZodOptional<z.ZodString>;
109
+ revokeReason: z.ZodOptional<z.ZodString>;
110
+ }, z.core.$strip>>;
111
+ encryption: z.ZodArray<z.ZodObject<{
112
+ keyId: z.ZodString;
113
+ algorithm: z.ZodEnum<{
114
+ Ed25519: "Ed25519";
115
+ X25519: "X25519";
116
+ }>;
117
+ publicKeyMultibase: z.ZodString;
118
+ status: z.ZodEnum<{
119
+ active: "active";
120
+ retired: "retired";
121
+ revoked: "revoked";
122
+ }>;
123
+ validFrom: z.ZodString;
124
+ validUntil: z.ZodOptional<z.ZodString>;
125
+ revokedAt: z.ZodOptional<z.ZodString>;
126
+ revokeReason: z.ZodOptional<z.ZodString>;
127
+ }, z.core.$strip>>;
128
+ }, z.core.$strip>>;
129
+ currentSigningKeyId: z.ZodOptional<z.ZodString>;
130
+ currentEncryptionKeyId: z.ZodOptional<z.ZodString>;
131
+ keySetVersion: z.ZodOptional<z.ZodNumber>;
132
+ visibility: z.ZodOptional<z.ZodEnum<{
133
+ public: "public";
134
+ network_only: "network_only";
135
+ capability_gated: "capability_gated";
136
+ private: "private";
137
+ }>>;
138
+ governance: z.ZodOptional<z.ZodObject<{
139
+ maxAcceptedDelegationDepth: z.ZodOptional<z.ZodNumber>;
140
+ supportedTransports: z.ZodOptional<z.ZodArray<z.ZodEnum<{
141
+ ink_http: "ink_http";
142
+ ink_ws: "ink_ws";
143
+ extension_api: "extension_api";
144
+ voice: "voice";
145
+ line_phone: "line_phone";
146
+ human_review_queue: "human_review_queue";
147
+ }>>>;
148
+ supportsCapabilityGatedDiscovery: z.ZodOptional<z.ZodBoolean>;
149
+ handshakeBudget: z.ZodOptional<z.ZodObject<{
150
+ maxChallengesPerCorrelation: z.ZodOptional<z.ZodNumber>;
151
+ maxIntentsPerMinute: z.ZodOptional<z.ZodNumber>;
152
+ }, z.core.$strip>>;
153
+ }, z.core.$strip>>;
154
+ }, z.core.$strip>;
155
+ export type AgentCard = z.infer<typeof AgentCardSchema>;
156
+ /**
157
+ * Return the inbound message URL for an Agent Card.
158
+ *
159
+ * Under v0.1.1, `endpoint` is still required on every parsed
160
+ * `AgentCard`, so this helper effectively returns `card.endpoint`
161
+ * today. The `?? card.inboxEndpoint` fallback is in place for the
162
+ * future v0.x revision where `inboxEndpoint` will become primary
163
+ * (with `endpoint` accepted as the legacy alias). Consumers SHOULD
164
+ * use this helper rather than reading `.endpoint` directly so the
165
+ * eventual swap is a one-import change.
166
+ *
167
+ * `inboxEndpoint` (when present alongside `endpoint`) MUST equal
168
+ * `endpoint`; that invariant is enforced by `AgentCardSchema`.
169
+ */
170
+ export declare function resolveAgentInbox(card: AgentCard): string;
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { IntentTypeSchema } from "./intent.js";
3
+ import { InkReceiptDispositionSchema } from "./ink-audit.js";
4
+ import { ProfileSnapshotSchema } from "./profile.js";
5
+ import { KeyEntrySchema } from "./key-entry.js";
6
+ import { InkTransportSchema, AgentCardVisibilitySchema } from "./ink-handshake.js";
7
+ export const ThirdPartyAuditServiceSchema = z.object({
8
+ endpoint: z.string().url(),
9
+ did: z.string(),
10
+ publicKey: z.string(),
11
+ });
12
+ export const AgentCardSchema = z.object({
13
+ protocol: z.literal("ink/0.1"),
14
+ agentId: z.string(),
15
+ ownerDid: z.string().optional(),
16
+ ownerHandle: z.string().optional(),
17
+ atprotoRecordUri: z.string().optional(),
18
+ handle: z.string(),
19
+ displayName: z.string().max(200),
20
+ /**
21
+ * Inbound message endpoint URL. Required.
22
+ *
23
+ * `inboxEndpoint` is accepted as an optional forward-compat hint
24
+ * from v0.1.1 onward; when present it MUST equal `endpoint`. The
25
+ * long-term name is settled at the next wire-version bump, so
26
+ * publishers SHOULD continue to emit `endpoint` for v0.1.x and
27
+ * MAY emit `inboxEndpoint` alongside it. The runtime helper
28
+ * `resolveAgentInbox(card)` returns whichever value is present.
29
+ */
30
+ endpoint: z.string().url(),
31
+ inboxEndpoint: z.string().url().optional(),
32
+ publicKeyMultibase: z.string().startsWith("z"),
33
+ // (other fields below; the `inboxEndpoint === endpoint` invariant
34
+ // is enforced by the .superRefine() at the bottom of this schema.)
35
+ profileSnapshot: ProfileSnapshotSchema.optional(),
36
+ capabilities: z.object({
37
+ intentsAccepted: z.array(IntentTypeSchema),
38
+ intentsSent: z.array(IntentTypeSchema),
39
+ receipts: z.object({
40
+ send: z.boolean(),
41
+ dispositions: z.array(InkReceiptDispositionSchema),
42
+ }).optional(),
43
+ auditExchange: z.boolean().optional(),
44
+ thirdPartyAudit: z.object({
45
+ services: z.array(ThirdPartyAuditServiceSchema),
46
+ submitPolicy: z.enum(["all", "high_value", "none"]),
47
+ }).optional(),
48
+ }),
49
+ availability: z.object({
50
+ timezone: z.string(),
51
+ meetingHours: z.string().optional(),
52
+ responseSla: z.string().optional(),
53
+ }),
54
+ keys: z.object({
55
+ signing: z.array(KeyEntrySchema),
56
+ encryption: z.array(KeyEntrySchema),
57
+ }).optional(),
58
+ currentSigningKeyId: z.string().optional(),
59
+ currentEncryptionKeyId: z.string().optional(),
60
+ keySetVersion: z.number().int().positive().optional(),
61
+ // Containment extension (Phase 1)
62
+ visibility: AgentCardVisibilitySchema.optional(),
63
+ governance: z.object({
64
+ maxAcceptedDelegationDepth: z.number().int().positive().optional(),
65
+ supportedTransports: z.array(InkTransportSchema).optional(),
66
+ supportsCapabilityGatedDiscovery: z.boolean().optional(),
67
+ handshakeBudget: z.object({
68
+ maxChallengesPerCorrelation: z.number().int().positive().optional(),
69
+ maxIntentsPerMinute: z.number().int().positive().optional(),
70
+ }).optional(),
71
+ }).optional(),
72
+ }).superRefine((card, ctx) => {
73
+ // v0.1.1: when both endpoint and inboxEndpoint are present they
74
+ // MUST refer to the same URL. The spec rationale is that the alias
75
+ // exists for forward compat, not as a way to publish two distinct
76
+ // inbound URLs under one card — a card with mismatched endpoints
77
+ // is ambiguous about which one to deliver to.
78
+ if (card.inboxEndpoint && card.endpoint && card.inboxEndpoint !== card.endpoint) {
79
+ ctx.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ path: ["inboxEndpoint"],
82
+ message: "inboxEndpoint MUST equal endpoint when both are present (v0.1.1 spec).",
83
+ });
84
+ }
85
+ });
86
+ /**
87
+ * Return the inbound message URL for an Agent Card.
88
+ *
89
+ * Under v0.1.1, `endpoint` is still required on every parsed
90
+ * `AgentCard`, so this helper effectively returns `card.endpoint`
91
+ * today. The `?? card.inboxEndpoint` fallback is in place for the
92
+ * future v0.x revision where `inboxEndpoint` will become primary
93
+ * (with `endpoint` accepted as the legacy alias). Consumers SHOULD
94
+ * use this helper rather than reading `.endpoint` directly so the
95
+ * eventual swap is a one-import change.
96
+ *
97
+ * `inboxEndpoint` (when present alongside `endpoint`) MUST equal
98
+ * `endpoint`; that invariant is enforced by `AgentCardSchema`.
99
+ */
100
+ export function resolveAgentInbox(card) {
101
+ // `card.endpoint` is required by the v0.1.x schema so the first
102
+ // branch always succeeds today. The `??` chain is in place for the
103
+ // future revision where `endpoint` becomes optional and
104
+ // `inboxEndpoint` becomes the primary field; consumers using this
105
+ // helper get the swap for free.
106
+ return card.endpoint ?? card.inboxEndpoint;
107
+ }