@adastracomputing/ink 0.2.0 → 0.4.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.
@@ -24,13 +24,15 @@ export interface FetchAgentCardOptions {
24
24
  * connect targets (e.g. undici with a custom dispatcher on Node, or
25
25
  * `cf: { resolveOverride: validatedIp }` on Cloudflare Workers). */
26
26
  fetch?: typeof fetch;
27
- /** Strict mode: require that the caller supply `options.fetch`. When
28
- * true, the default global `fetch` is refused for non-literal hostnames
29
- * because the default cannot perform connect-time IP filtering and is
30
- * therefore vulnerable to DNS rebinding. Off by default for backwards
31
- * compatibility; on by default for any production integration where
32
- * `baseUrl` is taken from untrusted input. Returns null without
33
- * fetching when the condition fails. */
27
+ /** Strict mode: require that the caller supply `options.fetch`, returning
28
+ * null (without fetching) if it is absent. This only guarantees that *some*
29
+ * fetch override was provided; it does NOT and cannot verify that the
30
+ * override pins connect-time IPs, so passing `requireSafeFetch: true` with
31
+ * the plain global `fetch` does not close the DNS-rebinding window. The
32
+ * literal-private-IP allowlist this module applies to `baseUrl` does not stop
33
+ * a public hostname that resolves to a private address at fetch time; only a
34
+ * connect-time-IP-pinning `options.fetch` (for example a custom undici
35
+ * dispatcher) does. Off by default for backwards compatibility. */
34
36
  requireSafeFetch?: boolean;
35
37
  }
36
38
  /**
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  export { signInkMessage, verifyInkSignature, buildSignatureBase, buildAuthHeader, computeMessageHash, computeEventHash, computeAuditMerkleLeafHash, signAuditEvent, verifyAuditEventSignature, signAuditResponse, verifyAuditResponseSignature, verifyAuditEventChain, signAuditQueryResponse, verifyAuditQueryResponseSignature, encryptInkPayload, decryptInkPayload, checkReplay, base64urlEncode, base64urlDecode, hexToBytes, bytesToHex, jcsCanonicalize, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS, } from "./crypto/ink.js";
2
2
  export { signMessage, verifyMessage } from "./crypto/sign.js";
3
3
  export { verifyInkSignatureWithKeys } from "./crypto/multi-key-verify.js";
4
- export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, } from "./crypto/keys.js";
4
+ export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, canonicalAgentPrincipal, AGENT_ID_KEY_PREFIXES, } from "./crypto/keys.js";
5
5
  export { fetchAgentCard, extractCandidateKeys, resolveBaseUrl, } from "./discovery/agent-card.js";
6
6
  export { verifyInkAuth, type NonceStore } from "./middleware/ink-auth.js";
7
7
  export { verifyInclusionReceipt, verifyAuditQueryResponse, type InclusionReceipt, type InclusionReceiptVerifyResult, type AuditQueryResponse, type AuditQueryResponseVerifyResult, type VerifyStep, } from "./audit/inclusion-receipt.js";
8
8
  export { HandshakeBudgetTracker } from "./ink/handshake-budget.js";
9
- export { buildReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
9
+ export { buildReceipt, verifyReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
10
10
  export { resolveEffectiveTransports, checkTransportAllowed, } from "./ink/transport-auth.js";
11
11
  export { buildRedactedCard, shouldRedactOnGet, AgentCardQuerySchema, } from "./ink/discovery-gating.js";
12
- export { parseCheckpoint, formatCheckpoint, } from "./ink/checkpoint.js";
12
+ export { parseCheckpoint, formatCheckpoint, verifyCheckpoint, } from "./ink/checkpoint.js";
13
13
  export type { CheckpointData } from "./ink/checkpoint.js";
14
14
  export { InkAuditEventTypeSchema, InkAuditEventSchema, InkAuditInclusionSchema, InkReceiptSchema, InkAuditQuerySchema, InkIntroductionReceiptSchema, } from "./models/ink-audit.js";
15
15
  export type { InkAuditEventType, InkAuditEvent, InkAuditInclusion, InkReceipt, InkAuditQuery, InkAuditResponse, InkIntroductionReceiptStatus, } from "./models/ink-audit.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  export { signInkMessage, verifyInkSignature, buildSignatureBase, buildAuthHeader, computeMessageHash, computeEventHash, computeAuditMerkleLeafHash, signAuditEvent, verifyAuditEventSignature, signAuditResponse, verifyAuditResponseSignature, verifyAuditEventChain, signAuditQueryResponse, verifyAuditQueryResponseSignature, encryptInkPayload, decryptInkPayload, checkReplay, base64urlEncode, base64urlDecode, hexToBytes, bytesToHex, jcsCanonicalize, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS, } from "./crypto/ink.js";
5
5
  export { signMessage, verifyMessage } from "./crypto/sign.js";
6
6
  export { verifyInkSignatureWithKeys } from "./crypto/multi-key-verify.js";
7
- export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, } from "./crypto/keys.js";
7
+ export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, canonicalAgentPrincipal, AGENT_ID_KEY_PREFIXES, } from "./crypto/keys.js";
8
8
  // Discovery: Agent Card fetch + candidate-key extraction
9
9
  export { fetchAgentCard, extractCandidateKeys, resolveBaseUrl, } from "./discovery/agent-card.js";
10
10
  // Middleware: transport-level INK auth
@@ -13,14 +13,14 @@ export { verifyInkAuth } from "./middleware/ink-auth.js";
13
13
  export { verifyInclusionReceipt, verifyAuditQueryResponse, } from "./audit/inclusion-receipt.js";
14
14
  // Optional containment / governance primitives
15
15
  export { HandshakeBudgetTracker } from "./ink/handshake-budget.js";
16
- // Receipts: build and send INK delivery receipts
17
- export { buildReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
16
+ // Receipts: build, verify, and send INK delivery receipts
17
+ export { buildReceipt, verifyReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
18
18
  // Transport-auth: token-level transport allowlist for extension tokens
19
19
  export { resolveEffectiveTransports, checkTransportAllowed, } from "./ink/transport-auth.js";
20
20
  // Discovery-gating: visibility-aware Agent Card redaction
21
21
  export { buildRedactedCard, shouldRedactOnGet, AgentCardQuerySchema, } from "./ink/discovery-gating.js";
22
- // Checkpoint parsing for transparency-log signed checkpoints
23
- export { parseCheckpoint, formatCheckpoint, } from "./ink/checkpoint.js";
22
+ // Checkpoint parsing and signature verification for transparency-log checkpoints
23
+ export { parseCheckpoint, formatCheckpoint, verifyCheckpoint, } from "./ink/checkpoint.js";
24
24
  // Audit event schemas + types for receipts, query, inclusion proofs
25
25
  export { InkAuditEventTypeSchema, InkAuditEventSchema, InkAuditInclusionSchema, InkReceiptSchema, InkAuditQuerySchema, InkIntroductionReceiptSchema, } from "./models/ink-audit.js";
26
26
  // Handshake message schemas
@@ -17,3 +17,24 @@ export interface CheckpointData {
17
17
  export declare function formatCheckpoint(data: CheckpointData): string;
18
18
  /** Parse a checkpoint body. Returns null if invalid. */
19
19
  export declare function parseCheckpoint(body: string): CheckpointData | null;
20
+ /**
21
+ * Verify a signed checkpoint and return its parsed body, or `null` if the
22
+ * signature, origin, or format is invalid.
23
+ *
24
+ * The signed form is the C2SP-style note used by the INK witness:
25
+ *
26
+ * <origin>\n<treeSize>\n<rootHash>\n\n-- <origin> <base64url(sig)>\n
27
+ *
28
+ * The Ed25519 signature covers the body bytes `<origin>\n<treeSize>\n<rootHash>`
29
+ * exactly (no trailing newline), so the `origin` first line is the domain
30
+ * separator binding the signed bytes to this log. Verification REQUIRES the
31
+ * caller's `expectedOrigin`: a checkpoint whose body origin, or whose matching
32
+ * signature-line origin, is not `expectedOrigin` is rejected, so a witness that
33
+ * operates several logs (or an attacker replaying another log's signed
34
+ * checkpoint) cannot substitute a different tree. This is the authenticated
35
+ * input that anti-rollback / freshness checks MUST consume; an unverified
36
+ * checkpoint body provides no security.
37
+ *
38
+ * Verification uses RFC 8032 strict mode (small-order keys rejected).
39
+ */
40
+ export declare function verifyCheckpoint(signed: string, witnessPublicKey: Uint8Array, expectedOrigin: string): Promise<CheckpointData | null>;
@@ -2,6 +2,8 @@
2
2
  * INK Checkpoint formatting (C2SP tlog-checkpoint compatible).
3
3
  * Used for the public checkpoint endpoint (INK Auditability §7.7).
4
4
  */
5
+ import * as ed from "@noble/ed25519";
6
+ import { base64urlDecode } from "../crypto/ink.js";
5
7
  /**
6
8
  * Format a checkpoint body per C2SP tlog-checkpoint spec:
7
9
  * line 1: origin (log identity)
@@ -67,3 +69,80 @@ export function parseCheckpoint(body) {
67
69
  return null;
68
70
  return { origin, treeSize, rootHash };
69
71
  }
72
+ /** A signed checkpoint is the 3-line body, a blank line, then one or more
73
+ * signature lines, plus a trailing newline. Bound the whole thing so an
74
+ * attacker-supplied blob cannot drive large scans before rejection. */
75
+ const MAX_SIGNED_CHECKPOINT_BODY = 4096;
76
+ /** Cap the number of cosignature lines a verifier will scan. */
77
+ const MAX_CHECKPOINT_SIGNATURES = 8;
78
+ /**
79
+ * Verify a signed checkpoint and return its parsed body, or `null` if the
80
+ * signature, origin, or format is invalid.
81
+ *
82
+ * The signed form is the C2SP-style note used by the INK witness:
83
+ *
84
+ * <origin>\n<treeSize>\n<rootHash>\n\n-- <origin> <base64url(sig)>\n
85
+ *
86
+ * The Ed25519 signature covers the body bytes `<origin>\n<treeSize>\n<rootHash>`
87
+ * exactly (no trailing newline), so the `origin` first line is the domain
88
+ * separator binding the signed bytes to this log. Verification REQUIRES the
89
+ * caller's `expectedOrigin`: a checkpoint whose body origin, or whose matching
90
+ * signature-line origin, is not `expectedOrigin` is rejected, so a witness that
91
+ * operates several logs (or an attacker replaying another log's signed
92
+ * checkpoint) cannot substitute a different tree. This is the authenticated
93
+ * input that anti-rollback / freshness checks MUST consume; an unverified
94
+ * checkpoint body provides no security.
95
+ *
96
+ * Verification uses RFC 8032 strict mode (small-order keys rejected).
97
+ */
98
+ export async function verifyCheckpoint(signed, witnessPublicKey, expectedOrigin) {
99
+ if (typeof signed !== "string" || signed.length === 0 || signed.length > MAX_SIGNED_CHECKPOINT_BODY) {
100
+ return null;
101
+ }
102
+ if (!(witnessPublicKey instanceof Uint8Array) || witnessPublicKey.length !== 32) {
103
+ return null;
104
+ }
105
+ if (typeof expectedOrigin !== "string" || expectedOrigin.length === 0 || expectedOrigin.length > MAX_CHECKPOINT_LINE) {
106
+ return null;
107
+ }
108
+ const SEP = "\n\n-- ";
109
+ const idx = signed.indexOf(SEP);
110
+ if (idx === -1)
111
+ return null;
112
+ const body = signed.slice(0, idx); // <origin>\n<treeSize>\n<rootHash>
113
+ const data = parseCheckpoint(body + "\n");
114
+ if (!data)
115
+ return null;
116
+ // Bind the body's own origin to the caller's expectation before any crypto.
117
+ if (data.origin !== expectedOrigin)
118
+ return null;
119
+ // Signature block starts at the "-- " that began the separator.
120
+ const sigBlock = signed.slice(idx + 2);
121
+ const sigLines = sigBlock.split("\n").filter((l) => l.length > 0);
122
+ if (sigLines.length === 0 || sigLines.length > MAX_CHECKPOINT_SIGNATURES)
123
+ return null;
124
+ const bodyBytes = new TextEncoder().encode(body);
125
+ for (const line of sigLines) {
126
+ if (!line.startsWith("-- "))
127
+ return null; // any malformed signature line is fatal
128
+ const rest = line.slice(3);
129
+ const sp = rest.indexOf(" ");
130
+ if (sp === -1)
131
+ return null;
132
+ const lineOrigin = rest.slice(0, sp);
133
+ const sigB64 = rest.slice(sp + 1);
134
+ if (lineOrigin !== expectedOrigin)
135
+ continue; // a cosigner whose key we were not given
136
+ try {
137
+ const sig = base64urlDecode(sigB64);
138
+ if (sig.length !== 64)
139
+ return null;
140
+ const ok = await ed.verifyAsync(sig, bodyBytes, witnessPublicKey, { zip215: false });
141
+ return ok ? data : null; // a matching-origin signature that fails verification is fatal
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ return null; // no signature line for the expected origin
148
+ }
@@ -66,16 +66,16 @@ export function buildRedactedCard(card) {
66
66
  export const AgentCardQuerySchema = z.object({
67
67
  protocol: z.literal("ink/0.1"),
68
68
  type: z.literal("network.tulpa.agent_card_query"),
69
- from: z.string(),
70
- nonce: z.string(),
69
+ from: z.string().max(512),
70
+ nonce: z.string().max(256),
71
71
  timestamp: z.string().datetime(),
72
- requestedFields: z.array(z.string()).optional(),
72
+ requestedFields: z.array(z.string().max(64)).max(32).optional(),
73
73
  });
74
74
  export const AgentCardResponseSchema = z.object({
75
75
  protocol: z.literal("ink/0.1"),
76
76
  type: z.literal("network.tulpa.agent_card_response"),
77
77
  card: AgentCardSchema,
78
- grantedFields: z.array(z.string()),
78
+ grantedFields: z.array(z.string().max(64)).max(32),
79
79
  timestamp: z.string().datetime(),
80
80
  });
81
81
  export const AgentCardDeniedSchema = z.object({
@@ -1,4 +1,4 @@
1
- import type { InkReceipt } from "../models/ink-audit.js";
1
+ import { type InkReceipt } from "../models/ink-audit.js";
2
2
  export interface BuildReceiptInput {
3
3
  from: string;
4
4
  to: string;
@@ -10,6 +10,38 @@ export interface BuildReceiptInput {
10
10
  }
11
11
  /** Build a signed INK receipt envelope. */
12
12
  export declare function buildReceipt(input: BuildReceiptInput): Promise<InkReceipt>;
13
+ export interface VerifyReceiptResult {
14
+ valid: boolean;
15
+ /** Machine-readable reason when `valid` is false. */
16
+ reason?: string;
17
+ }
18
+ /**
19
+ * Verify an INK receipt against the message it claims to acknowledge.
20
+ *
21
+ * Checks all the bindings a hand-rolled verifier commonly forgets: the
22
+ * receipt's Ed25519 signature against the issuer's (`from`) key, that `from`,
23
+ * `to` and `messageId` equal the expected values, and that `messageHash`
24
+ * equals the hash of the exact message body that was sent (recomputed here,
25
+ * not trusted from the receipt). A receipt that passes proves the named
26
+ * counterparty acknowledged that specific message; nothing weaker should be
27
+ * treated as proof of delivery.
28
+ */
29
+ export declare function verifyReceipt(opts: {
30
+ receipt: unknown;
31
+ /** Raw 32-byte Ed25519 public key of the issuer (the receipt's `from`). */
32
+ senderPublicKey: Uint8Array;
33
+ expected: {
34
+ from: string;
35
+ to: string;
36
+ messageId: string;
37
+ messageBody: Record<string, unknown>;
38
+ /** When set, require the receipt to acknowledge this specific disposition
39
+ * (e.g. "delivered"). The disposition is covered by the signature, but
40
+ * without this a signed receipt for a different state would still pass, so
41
+ * callers proving a specific delivery state MUST pin it. */
42
+ disposition?: InkReceipt["disposition"];
43
+ };
44
+ }): Promise<VerifyReceiptResult>;
13
45
  export declare function shouldSendReceipt(intentOrType: string): boolean;
14
46
  export interface SendReceiptOptions {
15
47
  /** Allow endpoints whose hostname is loopback / private / link-local /
@@ -1,6 +1,7 @@
1
1
  import { computeMessageHash, signInkMessage, buildAuthHeader } from "../crypto/ink.js";
2
- import { signMessage } from "../crypto/sign.js";
2
+ import { signMessage, verifyMessage } from "../crypto/sign.js";
3
3
  import { isPrivateHostname } from "../discovery/agent-card.js";
4
+ import { InkReceiptSchema } from "../models/ink-audit.js";
4
5
  /** Build a signed INK receipt envelope. */
5
6
  export async function buildReceipt(input) {
6
7
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
@@ -28,6 +29,49 @@ export async function buildReceipt(input) {
28
29
  const signature = await signMessage(unsigned, input.privateKey);
29
30
  return { ...unsigned, signature };
30
31
  }
32
+ /**
33
+ * Verify an INK receipt against the message it claims to acknowledge.
34
+ *
35
+ * Checks all the bindings a hand-rolled verifier commonly forgets: the
36
+ * receipt's Ed25519 signature against the issuer's (`from`) key, that `from`,
37
+ * `to` and `messageId` equal the expected values, and that `messageHash`
38
+ * equals the hash of the exact message body that was sent (recomputed here,
39
+ * not trusted from the receipt). A receipt that passes proves the named
40
+ * counterparty acknowledged that specific message; nothing weaker should be
41
+ * treated as proof of delivery.
42
+ */
43
+ export async function verifyReceipt(opts) {
44
+ const parsed = InkReceiptSchema.safeParse(opts.receipt);
45
+ if (!parsed.success)
46
+ return { valid: false, reason: "malformed_receipt" };
47
+ const receipt = parsed.data;
48
+ const { senderPublicKey, expected } = opts;
49
+ if (!(senderPublicKey instanceof Uint8Array) || senderPublicKey.length !== 32) {
50
+ return { valid: false, reason: "invalid_public_key" };
51
+ }
52
+ if (receipt.from !== expected.from)
53
+ return { valid: false, reason: "from_mismatch" };
54
+ if (receipt.to !== expected.to)
55
+ return { valid: false, reason: "to_mismatch" };
56
+ if (receipt.messageId !== expected.messageId)
57
+ return { valid: false, reason: "message_id_mismatch" };
58
+ if (expected.disposition !== undefined && receipt.disposition !== expected.disposition) {
59
+ return { valid: false, reason: "disposition_mismatch" };
60
+ }
61
+ let expectedHash;
62
+ try {
63
+ expectedHash = await computeMessageHash(expected.messageBody);
64
+ }
65
+ catch {
66
+ return { valid: false, reason: "message_hash_error" };
67
+ }
68
+ if (receipt.messageHash !== expectedHash)
69
+ return { valid: false, reason: "message_hash_mismatch" };
70
+ const sigOk = await verifyMessage(receipt, senderPublicKey);
71
+ if (!sigOk)
72
+ return { valid: false, reason: "invalid_signature" };
73
+ return { valid: true };
74
+ }
31
75
  /** Loop prevention: don't send receipts for receipts or audit messages. */
32
76
  const NO_RECEIPT_TYPES = new Set([
33
77
  "network.tulpa.receipt",
@@ -60,6 +60,7 @@ export declare function verifyInkAuth(opts: {
60
60
  }): Promise<{
61
61
  valid: true;
62
62
  senderAgentId: string;
63
+ principal: string;
63
64
  keyId?: string;
64
65
  keyStatus?: KeyStatus;
65
66
  } | {
@@ -1,5 +1,5 @@
1
1
  import { verifyInkSignature, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS } from "../crypto/ink.js";
2
- import { extractPublicKeyFromAgentId } from "../crypto/keys.js";
2
+ import { extractPublicKeyFromAgentId, canonicalAgentPrincipal } from "../crypto/keys.js";
3
3
  import { verifyInkSignatureWithKeys } from "../crypto/multi-key-verify.js";
4
4
  /**
5
5
  * Parse and verify an INK-Ed25519 Authorization header.
@@ -33,8 +33,10 @@ export async function verifyInkAuth(opts) {
33
33
  }
34
34
  // Ed25519 signatures are exactly 86 base64url chars — tighten the regex to
35
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}))?$/);
36
+ // CPU on verifyInkSignature for a malformed value. Single literal spaces
37
+ // match the spec grammar and buildAuthHeader's output; `\s` would let
38
+ // CR/LF/TAB into a parsed header value.
39
+ const match = opts.authHeader.match(/^INK-Ed25519 ([A-Za-z0-9_-]{86})(?: keyId=([A-Za-z0-9_:.-]{1,128}))?$/);
38
40
  if (!match) {
39
41
  return { valid: false, error: "invalid_auth_scheme" };
40
42
  }
@@ -172,6 +174,7 @@ export async function verifyInkAuth(opts) {
172
174
  return {
173
175
  valid: true,
174
176
  senderAgentId: senderDid,
177
+ principal: canonicalAgentPrincipal(senderDid),
175
178
  keyId: result.keyId,
176
179
  keyStatus: result.keyStatus,
177
180
  };
@@ -206,7 +209,7 @@ export async function verifyInkAuth(opts) {
206
209
  const noncePass = await recordNonce();
207
210
  if (!noncePass.ok)
208
211
  return { valid: false, error: noncePass.error };
209
- return { valid: true, senderAgentId: senderDid };
212
+ return { valid: true, senderAgentId: senderDid, principal: canonicalAgentPrincipal(senderDid) };
210
213
  }
211
214
  catch {
212
215
  return { valid: false, error: "signature_verification_failed" };
@@ -5,17 +5,17 @@ import { ProfileSnapshotSchema } from "./profile.js";
5
5
  import { KeyEntrySchema } from "./key-entry.js";
6
6
  import { InkTransportSchema, AgentCardVisibilitySchema } from "./ink-handshake.js";
7
7
  export const ThirdPartyAuditServiceSchema = z.object({
8
- endpoint: z.string().url(),
9
- did: z.string(),
10
- publicKey: z.string(),
8
+ endpoint: z.string().max(2048).url(),
9
+ did: z.string().max(512),
10
+ publicKey: z.string().max(256),
11
11
  });
12
12
  export const AgentCardSchema = z.object({
13
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(),
14
+ agentId: z.string().max(512),
15
+ ownerDid: z.string().max(512).optional(),
16
+ ownerHandle: z.string().max(256).optional(),
17
+ atprotoRecordUri: z.string().max(2048).optional(),
18
+ handle: z.string().max(256),
19
19
  displayName: z.string().max(200),
20
20
  /**
21
21
  * Inbound message endpoint URL. Required.
@@ -27,36 +27,36 @@ export const AgentCardSchema = z.object({
27
27
  * MAY emit `inboxEndpoint` alongside it. The runtime helper
28
28
  * `resolveAgentInbox(card)` returns whichever value is present.
29
29
  */
30
- endpoint: z.string().url(),
31
- inboxEndpoint: z.string().url().optional(),
32
- publicKeyMultibase: z.string().startsWith("z"),
30
+ endpoint: z.string().max(2048).url(),
31
+ inboxEndpoint: z.string().max(2048).url().optional(),
32
+ publicKeyMultibase: z.string().startsWith("z").max(128),
33
33
  // (other fields below; the `inboxEndpoint === endpoint` invariant
34
34
  // is enforced by the .superRefine() at the bottom of this schema.)
35
35
  profileSnapshot: ProfileSnapshotSchema.optional(),
36
36
  capabilities: z.object({
37
- intentsAccepted: z.array(IntentTypeSchema),
38
- intentsSent: z.array(IntentTypeSchema),
37
+ intentsAccepted: z.array(IntentTypeSchema).max(32),
38
+ intentsSent: z.array(IntentTypeSchema).max(32),
39
39
  receipts: z.object({
40
40
  send: z.boolean(),
41
- dispositions: z.array(InkReceiptDispositionSchema),
41
+ dispositions: z.array(InkReceiptDispositionSchema).max(16),
42
42
  }).optional(),
43
43
  auditExchange: z.boolean().optional(),
44
44
  thirdPartyAudit: z.object({
45
- services: z.array(ThirdPartyAuditServiceSchema),
45
+ services: z.array(ThirdPartyAuditServiceSchema).max(16),
46
46
  submitPolicy: z.enum(["all", "high_value", "none"]),
47
47
  }).optional(),
48
48
  }),
49
49
  availability: z.object({
50
- timezone: z.string(),
51
- meetingHours: z.string().optional(),
52
- responseSla: z.string().optional(),
50
+ timezone: z.string().max(64),
51
+ meetingHours: z.string().max(200).optional(),
52
+ responseSla: z.string().max(200).optional(),
53
53
  }),
54
54
  keys: z.object({
55
- signing: z.array(KeyEntrySchema),
56
- encryption: z.array(KeyEntrySchema),
55
+ signing: z.array(KeyEntrySchema).max(32),
56
+ encryption: z.array(KeyEntrySchema).max(32),
57
57
  }).optional(),
58
- currentSigningKeyId: z.string().optional(),
59
- currentEncryptionKeyId: z.string().optional(),
58
+ currentSigningKeyId: z.string().max(128).optional(),
59
+ currentEncryptionKeyId: z.string().max(128).optional(),
60
60
  keySetVersion: z.number().int().positive().optional(),
61
61
  // Message protocol versions this agent's receiver can verify on the
62
62
  // body signature. When absent, assume ink/0.1 only. A sender MUST NOT
@@ -60,18 +60,18 @@ export const InkAuditEventTypeSchema = z.enum([
60
60
  ]);
61
61
  // ── INK Audit Event (hash-chained, signed) ──
62
62
  export const InkAuditEventSchema = z.object({
63
- id: z.string().min(1),
63
+ id: z.string().min(1).max(256),
64
64
  version: z.literal("ink-audit/1"),
65
- agentId: z.string().min(1),
66
- agentSignature: z.string().min(1),
65
+ agentId: z.string().min(1).max(512),
66
+ agentSignature: z.string().min(1).max(256),
67
67
  sequence: z.number().int().positive(),
68
68
  previousEventHash: z.string().regex(/^[0-9a-f]{64}$/).nullable(),
69
69
  eventType: InkAuditEventTypeSchema,
70
70
  timestamp: z.string().datetime(),
71
- messageId: z.string().min(1).optional(),
72
- correlationId: z.string().min(1).optional(),
73
- counterpartyId: z.string().min(1).optional(),
74
- signingKeyId: z.string().min(1).optional(),
71
+ messageId: z.string().min(1).max(256).optional(),
72
+ correlationId: z.string().min(1).max(256).optional(),
73
+ counterpartyId: z.string().min(1).max(512).optional(),
74
+ signingKeyId: z.string().min(1).max(128).optional(),
75
75
  data: z.record(z.string(), z.unknown()).optional(),
76
76
  });
77
77
  // ── Receipt (INK Auditability §1) ──
@@ -85,34 +85,38 @@ export const InkReceiptDispositionSchema = z.enum([
85
85
  export const InkReceiptSchema = z.object({
86
86
  protocol: z.literal("ink/0.1"),
87
87
  type: z.literal("network.tulpa.receipt"),
88
- from: z.string(),
89
- to: z.string(),
90
- messageId: z.string(),
88
+ from: z.string().max(512),
89
+ to: z.string().max(512),
90
+ messageId: z.string().max(256),
91
91
  disposition: InkReceiptDispositionSchema,
92
92
  dispositionAt: z.string().datetime(),
93
93
  note: z.string().max(500).optional(),
94
- messageHash: z.string(),
95
- nonce: z.string(),
94
+ messageHash: z.string().max(256),
95
+ nonce: z.string().max(256),
96
96
  timestamp: z.string().datetime(),
97
- signature: z.string(),
97
+ signature: z.string().max(256),
98
98
  });
99
99
  // ── Audit Query (INK Auditability §3) ──
100
100
  export const InkAuditQuerySchema = z.object({
101
101
  protocol: z.literal("ink/0.1"),
102
102
  type: z.literal("network.tulpa.audit_query"),
103
- from: z.string(),
104
- to: z.string(),
105
- messageId: z.string(),
106
- nonce: z.string(),
103
+ from: z.string().max(512),
104
+ to: z.string().max(512),
105
+ messageId: z.string().max(256),
106
+ nonce: z.string().max(256),
107
107
  timestamp: z.string().datetime(),
108
108
  });
109
109
  // ── Audit Response (INK Auditability §3) ──
110
110
  export const InkAuditResponseSchema = z.object({
111
111
  protocol: z.literal("ink/0.1"),
112
112
  type: z.literal("network.tulpa.audit_response"),
113
- messageId: z.string(),
114
- events: z.array(InkAuditEventSchema),
115
- responseSignature: z.string(),
113
+ messageId: z.string().max(256),
114
+ // Bound both the event count and (via InkAuditEventSchema's per-field caps)
115
+ // each event, so an audit response from an untrusted witness cannot force
116
+ // unbounded buffering. A single response that needs more than this should
117
+ // page rather than return one giant array.
118
+ events: z.array(InkAuditEventSchema).max(1000),
119
+ responseSignature: z.string().max(256),
116
120
  });
117
121
  // ── Third-Party Audit Submit (INK Auditability §7.2) ──
118
122
  export const InkAuditSubmitSchema = z.object({
@@ -128,10 +132,10 @@ export const InkAuditSubmitSchema = z.object({
128
132
  export const InkAuditInclusionSchema = z.object({
129
133
  protocol: z.literal("ink/0.1"),
130
134
  type: z.literal("network.tulpa.audit_inclusion"),
131
- eventId: z.string(),
135
+ eventId: z.string().max(256),
132
136
  treeSize: z.number().int().positive(),
133
137
  leafIndex: z.number().int().min(0),
134
- rootHash: z.string(),
138
+ rootHash: z.string().max(128),
135
139
  /** Optional Merkle inclusion proof — array of 64-character lowercase hex
136
140
  * hash siblings on the path from the leaf to the root. Consumers that
137
141
  * verify proofs (third-party auditor clients) read this field; consumers
@@ -141,7 +145,7 @@ export const InkAuditInclusionSchema = z.object({
141
145
  * force the parser to allocate megabytes of garbage proof data. */
142
146
  inclusionProof: z.array(z.string().regex(/^[0-9a-f]{64}$/)).max(64).optional(),
143
147
  timestamp: z.string().datetime(),
144
- serviceSignature: z.string(),
148
+ serviceSignature: z.string().max(256),
145
149
  });
146
150
  // ── Introduction Receipt (INK Introduction Receipts Extension §4) ──
147
151
  export const InkIntroductionReceiptStatusSchema = z.enum([
@@ -154,22 +158,22 @@ export const InkIntroductionReceiptStatusSchema = z.enum([
154
158
  export const InkIntroductionReceiptSchema = z.object({
155
159
  protocol: z.literal("ink/0.1"),
156
160
  type: z.literal("network.tulpa.introduction_receipt"),
157
- id: z.string(),
158
- correlationId: z.string(),
159
- from: z.string(),
160
- to: z.string(),
161
- requesterDid: z.string(),
162
- introducerDid: z.string(),
163
- beneficiaryDid: z.string(),
164
- targetDid: z.string(),
161
+ id: z.string().max(256),
162
+ correlationId: z.string().max(256),
163
+ from: z.string().max(512),
164
+ to: z.string().max(512),
165
+ requesterDid: z.string().max(512),
166
+ introducerDid: z.string().max(512),
167
+ beneficiaryDid: z.string().max(512),
168
+ targetDid: z.string().max(512),
165
169
  status: InkIntroductionReceiptStatusSchema,
166
170
  purpose: z.string().min(1).max(500),
167
- nonce: z.string(),
171
+ nonce: z.string().max(256),
168
172
  timestamp: z.string().datetime(),
169
- relatedIntentId: z.string().optional(),
170
- relatedResolutionId: z.string().optional(),
173
+ relatedIntentId: z.string().max(256).optional(),
174
+ relatedResolutionId: z.string().max(256).optional(),
171
175
  note: z.string().max(500).optional(),
172
- contextHash: z.string().optional(),
173
- authorizationChainRef: z.string().optional(),
176
+ contextHash: z.string().max(256).optional(),
177
+ authorizationChainRef: z.string().max(512).optional(),
174
178
  expiresAt: z.string().datetime().optional(),
175
179
  });
@@ -32,12 +32,12 @@ export const ChallengeTypeSchema = z.enum([
32
32
  export const InkChallengeSchema = z.object({
33
33
  protocol: z.literal("ink/0.1"),
34
34
  type: z.literal("network.tulpa.challenge"),
35
- intentRef: z.string(),
35
+ intentRef: z.string().max(256),
36
36
  challengeType: ChallengeTypeSchema,
37
- fields: z.array(z.string()).optional(),
38
- availableWindows: z.array(z.string()).optional(),
39
- contextFields: z.array(z.string()).optional(),
40
- nonce: z.string(),
37
+ fields: z.array(z.string().max(256)).max(32).optional(),
38
+ availableWindows: z.array(z.string().max(64)).max(32).optional(),
39
+ contextFields: z.array(z.string().max(256)).max(32).optional(),
40
+ nonce: z.string().max(256),
41
41
  timestamp: z.string().datetime(),
42
42
  });
43
43
  // ── Rejection (network.tulpa.rejection) — Stage 2b ──
@@ -58,12 +58,12 @@ export const RejectionReasonSchema = z.enum([
58
58
  export const InkRejectionSchema = z.object({
59
59
  protocol: z.literal("ink/0.1"),
60
60
  type: z.literal("network.tulpa.rejection"),
61
- intentRef: z.string(),
61
+ intentRef: z.string().max(256),
62
62
  reason: RejectionReasonSchema,
63
63
  detail: z.string().max(500).optional(),
64
- retryAfter: z.string().optional(),
64
+ retryAfter: z.string().max(64).optional(),
65
65
  backoffHint: InkBackoffHintSchema.optional(),
66
- nonce: z.string(),
66
+ nonce: z.string().max(256),
67
67
  timestamp: z.string().datetime(),
68
68
  });
69
69
  // ── Resolution (network.tulpa.resolution) — Stage 3 ──
@@ -74,16 +74,16 @@ export const ResolutionOutcomeSchema = z.enum([
74
74
  "expired",
75
75
  ]);
76
76
  export const ResolutionDetailsSchema = z.object({
77
- scheduledAt: z.string().optional(),
78
- duration: z.string().optional(),
77
+ scheduledAt: z.string().max(64).optional(),
78
+ duration: z.string().max(64).optional(),
79
79
  }).passthrough();
80
80
  export const InkResolutionSchema = z.object({
81
81
  protocol: z.literal("ink/0.1"),
82
82
  type: z.literal("network.tulpa.resolution"),
83
- intentRef: z.string(),
83
+ intentRef: z.string().max(256),
84
84
  outcome: ResolutionOutcomeSchema,
85
85
  details: ResolutionDetailsSchema.optional(),
86
- counterpartyDid: z.string().optional(),
87
- nonce: z.string(),
86
+ counterpartyDid: z.string().max(512).optional(),
87
+ nonce: z.string().max(256),
88
88
  timestamp: z.string().datetime(),
89
89
  });
@@ -223,14 +223,14 @@ export declare const MessageProvenanceSchema: z.ZodOptional<z.ZodObject<{
223
223
  */
224
224
  export declare const INK_PROTOCOL_VERSIONS: readonly ["ink/0.1", "ink/0.2"];
225
225
  export declare const ProtocolVersionSchema: z.ZodEnum<{
226
- "ink/0.1": "ink/0.1";
227
226
  "ink/0.2": "ink/0.2";
227
+ "ink/0.1": "ink/0.1";
228
228
  }>;
229
229
  export type ProtocolVersion = z.infer<typeof ProtocolVersionSchema>;
230
230
  export declare const MessageEnvelopeSchema: z.ZodObject<{
231
231
  protocol: z.ZodEnum<{
232
- "ink/0.1": "ink/0.1";
233
232
  "ink/0.2": "ink/0.2";
233
+ "ink/0.1": "ink/0.1";
234
234
  }>;
235
235
  id: z.ZodString;
236
236
  correlationId: z.ZodString;