@adastracomputing/ink 0.1.6 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,9 +4,23 @@ All notable changes to INK are recorded
4
4
  here. Pre-1.0 releases follow `0.Y.Z` semantics, see
5
5
  [`docs/maturity.md`](docs/maturity.md) for the versioning policy.
6
6
 
7
- ## Unreleased
7
+ ## 0.2.0, version-keyed body-signature domain
8
8
 
9
- No unreleased changes.
9
+ Version-keyed body-signature domain. The body message signature is now domain-separated by protocol version. ink/0.1 messages, and any object with no explicit ink/0.2 protocol, keep the legacy `tulpa/sign` domain so every signature produced to date still verifies. ink/0.2 messages are signed and verified under the neutral `ink/sign` domain. The verifier selects exactly one domain from the signed `protocol` field and never tries an alternate, so a signature made under one version's domain cannot be replayed under another.
10
+
11
+ This change is receiver-first and backward compatible. Verifiers accept both versions and `MessageEnvelopeSchema` now accepts ink/0.1 and ink/0.2 as a strict enum, rejecting any unknown version. Senders still emit ink/0.1 by default. The HTTP transport-auth signature is unchanged.
12
+
13
+ New vectors in `test-vectors/body-signature.json` pin the version-keyed domain including the cross-version and tamper cases. The standalone Python interop client verifies them identically.
14
+
15
+ Per the pre-1.0 policy this release publishes under the `next` dist-tag.
16
+
17
+ ## 0.1.7, expose per-intent payload schemas and getPayloadSchema from the package root
18
+
19
+ Pure additive release. Re-exports every per-intent Zod payload schema (`ScheduleMeetingPayloadSchema`, `IntroRequestPayloadSchema`, `OpportunityPayloadSchema`, `ConnectionRequestPayloadSchema`, `FollowUpPayloadSchema`, `AskPayloadSchema`, `PingPayloadSchema`, `RetractPayloadSchema`, `ContextSharePayloadSchema`, `MultiPartySyncPayloadSchema`, plus the matching `*ResponsePayloadSchema` variants) and the `getPayloadSchema(intent)` resolver from the package root. Adopters writing intent-aware receivers / handlers can now type their dispatch surface directly against the canonical payload shapes.
20
+
21
+ No wire-level changes. No behavior changes inside the existing functions. Receivers on 0.1.6 work unchanged on 0.1.7.
22
+
23
+ Per the pre-1.0 policy this release publishes under the `next` dist-tag.
10
24
 
11
25
  ## 0.1.6, expose intent + key-entry types and add optional inclusionProof to InkAuditInclusionSchema
12
26
 
@@ -71,7 +85,7 @@ Fixes the v0.1.1 erratum: the Python `examples/interop-cli/` shipped in v0.1.1 e
71
85
 
72
86
  **CLI now builds `connection_request` envelopes.** `ink-interop send/build --intent-type connection_request` (or the alias `connection`) constructs a `ConnectionRequestPayloadSchema`-conformant payload (`method`, `context`, `profileSnapshot`). This is the bootstrap intent for first contact between strangers: receivers that opt in to foreign senders verify the body signature against the inline key extracted from the sender's `did:key` (trust-on-first-use). Other intent types (`intro_request`, `ask`, `follow_up`) presume the sender is already a known contact and remain reserved for established relationships.
73
87
 
74
- Verified end-to-end against `https://api.tulpa.network/ink/v1/<agentId>/intent`: a `did:key:` `connection_request` from `ink-interop send` lands as a pending action in the recipient's inbox (`status: 200`, `accepted: true`, `pendingActionId: 01KT…`). Coverage spans schema validation, body + transport signature verification, replay/freshness, identity resolution, routing, the foreign-DID policy gate, and shield risk-scoring. Tests pinned to the canonical shape (`tests/test_envelope.py`) prevent regression. The npm library itself is unchanged from v0.1.1.
88
+ Verified end-to-end against `https://api.tulpa.network/ink/v1/<agentId>/intent`: a `did:key:` `connection_request` from `ink-interop send` lands as a pending action in the recipient's inbox (`status: 200`, `accepted: true`, `pendingActionId: 01KT…`). Coverage spans schema validation, body + transport signature verification, replay/freshness, identity resolution, routing, and the foreign-DID policy gate. Tests pinned to the canonical shape (`tests/test_envelope.py`) prevent regression. The npm library itself is unchanged from v0.1.1.
75
89
 
76
90
  **Example-helper API break.** `examples/interop-cli/`'s Python helper `build_intent_envelope()` now requires `keypair`, replaces `intent_type`/`purpose`/`timestamp` with canonical args (`target`, `reason`, `created_at`, etc.), and removes the `extra=` kwarg. Adopters who imported the old helper directly will need to update their calls — the previous signature emitted invalid wire data so no callable interop existed there to preserve. This is an example-only change; the npm library (`@adastracomputing/ink`) exports are unchanged.
77
91
 
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 1. Remove `signature` field if present
5
5
  * 2. JCS canonicalize (RFC 8785) via `canonicalize` library
6
- * 3. Sign canonical bytes directly with Ed25519
6
+ * 3. Sign domain-prefixed canonical bytes directly with Ed25519
7
7
  * 4. Return base64url-encoded signature (no padding)
8
8
  */
9
9
  export declare function signMessage(message: Record<string, unknown>, privateKey: Uint8Array): Promise<string>;
@@ -54,12 +54,39 @@ function isWithinBounds(value) {
54
54
  }
55
55
  return walk(value, 0);
56
56
  }
57
+ /**
58
+ * Body-signature domain-separation prefix, keyed off the message's
59
+ * declared `protocol` version.
60
+ *
61
+ * - `ink/0.2` -> `ink/sign\n` (neutral, current).
62
+ * - everything else (`ink/0.1`, or any object with no explicit
63
+ * `ink/0.2` protocol) -> `tulpa/sign\n`, the legacy domain, kept
64
+ * forever so every signature ever produced still verifies.
65
+ *
66
+ * The prefix is derived from the `protocol` field that is part of the
67
+ * signed body, so a verifier selects exactly one domain and tampering
68
+ * with `protocol` after signing breaks the signature (an `ink/0.2` body
69
+ * re-labelled `ink/0.1` is verified under `tulpa/sign\n` against a
70
+ * signature made over `ink/sign\n`, and fails). Only the exact string
71
+ * `"ink/0.2"` switches domains, so no other value can smuggle one in.
72
+ *
73
+ * This raw signer stays permissive on purpose: it is a general-purpose
74
+ * Ed25519 message signer (receipts, arbitrary objects), not envelope-
75
+ * specific. Strict "reject unknown protocol version" lives at the
76
+ * envelope schema layer, which validates `protocol` against the allowed
77
+ * set before this function is reached.
78
+ */
79
+ const LEGACY_SIGN_DOMAIN = "tulpa/sign\n";
80
+ const V02_SIGN_DOMAIN = "ink/sign\n";
81
+ function bodySignatureDomain(unsigned) {
82
+ return unsigned.protocol === "ink/0.2" ? V02_SIGN_DOMAIN : LEGACY_SIGN_DOMAIN;
83
+ }
57
84
  /**
58
85
  * Sign a message object using Ed25519.
59
86
  *
60
87
  * 1. Remove `signature` field if present
61
88
  * 2. JCS canonicalize (RFC 8785) via `canonicalize` library
62
- * 3. Sign canonical bytes directly with Ed25519
89
+ * 3. Sign domain-prefixed canonical bytes directly with Ed25519
63
90
  * 4. Return base64url-encoded signature (no padding)
64
91
  */
65
92
  export async function signMessage(message, privateKey) {
@@ -83,8 +110,10 @@ export async function signMessage(message, privateKey) {
83
110
  if (canonical.length > MAX_MESSAGE_CANONICAL_BYTES) {
84
111
  throw new Error("Canonicalized message exceeds maximum allowed size");
85
112
  }
86
- // Domain-separated signing to prevent cross-protocol signature replay
87
- const prefixed = `tulpa/sign\n${canonical}`;
113
+ // Domain-separated signing to prevent cross-protocol signature replay.
114
+ // Domain is keyed off the (signed) protocol version; see
115
+ // bodySignatureDomain. Legacy ink/0.1 keeps the tulpa/sign domain.
116
+ const prefixed = `${bodySignatureDomain(unsigned)}${canonical}`;
88
117
  const bytes = new TextEncoder().encode(prefixed);
89
118
  const sig = await ed.signAsync(bytes, privateKey);
90
119
  return base64urlEncode(sig);
@@ -124,9 +153,12 @@ export async function verifyMessage(message, publicKey) {
124
153
  if (canonical.length > MAX_MESSAGE_CANONICAL_BYTES) {
125
154
  return false;
126
155
  }
127
- // Domain-prefixed verification only legacy unprefixed signatures are no longer accepted.
128
- // signMessage() has always used `tulpa/sign\n` prefix; no callers produce unprefixed signatures.
129
- const prefixed = `tulpa/sign\n${canonical}`;
156
+ // Domain-prefixed verification only. The domain is selected from the
157
+ // signed `protocol` field (see bodySignatureDomain); a verifier never
158
+ // tries an alternate prefix, so a signature made under one version's
159
+ // domain cannot be replayed under another. Legacy unprefixed
160
+ // signatures are not accepted.
161
+ const prefixed = `${bodySignatureDomain(unsigned)}${canonical}`;
130
162
  const prefixedBytes = new TextEncoder().encode(prefixed);
131
163
  try {
132
164
  const sig = base64urlDecode(signature);
package/dist/index.d.ts CHANGED
@@ -16,12 +16,12 @@ export type { InkAuditEventType, InkAuditEvent, InkAuditInclusion, InkReceipt, I
16
16
  export { InkChallengeSchema, InkRejectionSchema, InkResolutionSchema, InkTransportSchema, } from "./models/ink-handshake.js";
17
17
  export type { AgentCardVisibility, InkChallenge, InkRejection, InkResolution, InkTransport, } from "./models/ink-handshake.js";
18
18
  export { AgentCardSchema } from "./models/agent-card.js";
19
- export { validateMessage, MessageEnvelopeSchema, IntentTypeSchema, } from "./models/intent.js";
20
- export type { MessageEnvelope, IntentType, } from "./models/intent.js";
19
+ export { validateMessage, getPayloadSchema, MessageEnvelopeSchema, ProtocolVersionSchema, INK_PROTOCOL_VERSIONS, IntentTypeSchema, ScheduleMeetingPayloadSchema, ScheduleMeetingResponsePayloadSchema, IntroRequestPayloadSchema, IntroResponsePayloadSchema, OpportunityPayloadSchema, OpportunityResponsePayloadSchema, ConnectionRequestPayloadSchema, ConnectionResponsePayloadSchema, FollowUpPayloadSchema, AskPayloadSchema, AskResponsePayloadSchema, PingPayloadSchema, RetractPayloadSchema, ContextSharePayloadSchema, MultiPartySyncPayloadSchema, } from "./models/intent.js";
20
+ export type { MessageEnvelope, ProtocolVersion, IntentType, } from "./models/intent.js";
21
21
  export { KeyStatusSchema, KeyRoleSchema, KeyEntrySchema, } from "./models/key-entry.js";
22
22
  export type { KeyStatus, KeyRole, KeyEntry, StoredKey, } from "./models/key-entry.js";
23
23
  export type { InkSignInput } from "./crypto/ink.js";
24
24
  export type { CandidateKey } from "./models/key-entry.js";
25
- export { resolveAgentInbox } from "./models/agent-card.js";
25
+ export { resolveAgentInbox, agentSupportedProtocolVersions } from "./models/agent-card.js";
26
26
  export type { AgentCard } from "./models/agent-card.js";
27
27
  export type { BudgetCheckResult, HandshakeBudgetConfig, } from "./ink/handshake-budget.js";
package/dist/index.js CHANGED
@@ -32,9 +32,9 @@ export { AgentCardSchema } from "./models/agent-card.js";
32
32
  // reject malformed envelopes before signature verification; without
33
33
  // it they have to re-implement the schema check or import from a
34
34
  // non-public path.
35
- export { validateMessage, MessageEnvelopeSchema, IntentTypeSchema, } from "./models/intent.js";
35
+ export { validateMessage, getPayloadSchema, MessageEnvelopeSchema, ProtocolVersionSchema, INK_PROTOCOL_VERSIONS, IntentTypeSchema, ScheduleMeetingPayloadSchema, ScheduleMeetingResponsePayloadSchema, IntroRequestPayloadSchema, IntroResponsePayloadSchema, OpportunityPayloadSchema, OpportunityResponsePayloadSchema, ConnectionRequestPayloadSchema, ConnectionResponsePayloadSchema, FollowUpPayloadSchema, AskPayloadSchema, AskResponsePayloadSchema, PingPayloadSchema, RetractPayloadSchema, ContextSharePayloadSchema, MultiPartySyncPayloadSchema, } from "./models/intent.js";
36
36
  // Key-entry types and schemas for adopters wiring their own key-set
37
37
  // storage and rotation. `CandidateKey` was already root-exported via
38
38
  // the verifier surface; this batch adds the persistence shapes.
39
39
  export { KeyStatusSchema, KeyRoleSchema, KeyEntrySchema, } from "./models/key-entry.js";
40
- export { resolveAgentInbox } from "./models/agent-card.js";
40
+ export { resolveAgentInbox, agentSupportedProtocolVersions } from "./models/agent-card.js";
@@ -96,9 +96,9 @@ export declare const AgentCardResponseSchema: z.ZodObject<{
96
96
  timezone: z.ZodString;
97
97
  meetingHours: z.ZodOptional<z.ZodString>;
98
98
  responseSla: z.ZodOptional<z.ZodString>;
99
- }, z.core.$strip>>;
99
+ }, z.core.$strict>>;
100
100
  openTo: z.ZodArray<z.ZodString>;
101
- }, z.core.$strip>>;
101
+ }, z.core.$strict>>;
102
102
  capabilities: z.ZodObject<{
103
103
  intentsAccepted: z.ZodArray<z.ZodEnum<{
104
104
  schedule_meeting: "schedule_meeting";
@@ -202,6 +202,7 @@ export declare const AgentCardResponseSchema: z.ZodObject<{
202
202
  currentSigningKeyId: z.ZodOptional<z.ZodString>;
203
203
  currentEncryptionKeyId: z.ZodOptional<z.ZodString>;
204
204
  keySetVersion: z.ZodOptional<z.ZodNumber>;
205
+ supportedProtocolVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
205
206
  visibility: z.ZodOptional<z.ZodEnum<{
206
207
  public: "public";
207
208
  network_only: "network_only";
@@ -23,9 +23,9 @@ export declare const AgentCardSchema: z.ZodObject<{
23
23
  timezone: z.ZodString;
24
24
  meetingHours: z.ZodOptional<z.ZodString>;
25
25
  responseSla: z.ZodOptional<z.ZodString>;
26
- }, z.core.$strip>>;
26
+ }, z.core.$strict>>;
27
27
  openTo: z.ZodArray<z.ZodString>;
28
- }, z.core.$strip>>;
28
+ }, z.core.$strict>>;
29
29
  capabilities: z.ZodObject<{
30
30
  intentsAccepted: z.ZodArray<z.ZodEnum<{
31
31
  schedule_meeting: "schedule_meeting";
@@ -129,6 +129,7 @@ export declare const AgentCardSchema: z.ZodObject<{
129
129
  currentSigningKeyId: z.ZodOptional<z.ZodString>;
130
130
  currentEncryptionKeyId: z.ZodOptional<z.ZodString>;
131
131
  keySetVersion: z.ZodOptional<z.ZodNumber>;
132
+ supportedProtocolVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
132
133
  visibility: z.ZodOptional<z.ZodEnum<{
133
134
  public: "public";
134
135
  network_only: "network_only";
@@ -153,6 +154,12 @@ export declare const AgentCardSchema: z.ZodObject<{
153
154
  }, z.core.$strip>>;
154
155
  }, z.core.$strip>;
155
156
  export type AgentCard = z.infer<typeof AgentCardSchema>;
157
+ /**
158
+ * The message protocol versions a card's receiver can verify. Falls back
159
+ * to ink/0.1 when the card does not advertise the field, so a sender
160
+ * defaults to the original version for any card that predates it.
161
+ */
162
+ export declare function agentSupportedProtocolVersions(card: Pick<AgentCard, "supportedProtocolVersions">): string[];
156
163
  /**
157
164
  * Return the inbound message URL for an Agent Card.
158
165
  *
@@ -58,6 +58,18 @@ export const AgentCardSchema = z.object({
58
58
  currentSigningKeyId: z.string().optional(),
59
59
  currentEncryptionKeyId: z.string().optional(),
60
60
  keySetVersion: z.number().int().positive().optional(),
61
+ // Message protocol versions this agent's receiver can verify on the
62
+ // body signature. When absent, assume ink/0.1 only. A sender MUST NOT
63
+ // emit a newer version to a card that does not advertise it; advertising
64
+ // a version here is necessary but not sufficient for a sender to use it.
65
+ //
66
+ // The entries are advisory hints, so they are accepted as bounded
67
+ // strings rather than the strict version enum: a newer peer may
68
+ // advertise a version this build does not know yet, and that must not
69
+ // make its whole card unparseable. A sender intersects this list with
70
+ // the versions it can actually emit. The strict enum lives on the
71
+ // envelope (MessageEnvelopeSchema), where an unknown version is rejected.
72
+ supportedProtocolVersions: z.array(z.string().max(16)).max(8).optional(),
61
73
  // Containment extension (Phase 1)
62
74
  visibility: AgentCardVisibilitySchema.optional(),
63
75
  governance: z.object({
@@ -83,6 +95,15 @@ export const AgentCardSchema = z.object({
83
95
  });
84
96
  }
85
97
  });
98
+ /**
99
+ * The message protocol versions a card's receiver can verify. Falls back
100
+ * to ink/0.1 when the card does not advertise the field, so a sender
101
+ * defaults to the original version for any card that predates it.
102
+ */
103
+ export function agentSupportedProtocolVersions(card) {
104
+ const advertised = card.supportedProtocolVersions;
105
+ return advertised && advertised.length > 0 ? advertised : ["ink/0.1"];
106
+ }
86
107
  /**
87
108
  * Return the inbound message URL for an Agent Card.
88
109
  *
@@ -33,7 +33,7 @@ export declare const ScheduleMeetingPayloadSchema: z.ZodObject<{
33
33
  }>;
34
34
  context: z.ZodOptional<z.ZodString>;
35
35
  location: z.ZodOptional<z.ZodString>;
36
- }, z.core.$strip>;
36
+ }, z.core.$strict>;
37
37
  export declare const ScheduleMeetingResponsePayloadSchema: z.ZodObject<{
38
38
  status: z.ZodEnum<{
39
39
  accepted: "accepted";
@@ -50,7 +50,7 @@ export declare const ScheduleMeetingResponsePayloadSchema: z.ZodObject<{
50
50
  too_busy: "too_busy";
51
51
  deferred: "deferred";
52
52
  }>>;
53
- }, z.core.$strip>;
53
+ }, z.core.$strict>;
54
54
  export declare const IntroRequestPayloadSchema: z.ZodObject<{
55
55
  target: z.ZodString;
56
56
  reason: z.ZodString;
@@ -59,7 +59,7 @@ export declare const IntroRequestPayloadSchema: z.ZodObject<{
59
59
  low: "low";
60
60
  normal: "normal";
61
61
  }>;
62
- }, z.core.$strip>;
62
+ }, z.core.$strict>;
63
63
  export declare const IntroResponsePayloadSchema: z.ZodObject<{
64
64
  status: z.ZodEnum<{
65
65
  declined: "declined";
@@ -72,7 +72,7 @@ export declare const IntroResponsePayloadSchema: z.ZodObject<{
72
72
  declined: "declined";
73
73
  pending: "pending";
74
74
  }>>;
75
- }, z.core.$strip>;
75
+ }, z.core.$strict>;
76
76
  export declare const OpportunityPayloadSchema: z.ZodObject<{
77
77
  type: z.ZodEnum<{
78
78
  role: "role";
@@ -88,7 +88,7 @@ export declare const OpportunityPayloadSchema: z.ZodObject<{
88
88
  matchReason: z.ZodString;
89
89
  expiresAt: z.ZodOptional<z.ZodString>;
90
90
  url: z.ZodOptional<z.ZodString>;
91
- }, z.core.$strip>;
91
+ }, z.core.$strict>;
92
92
  export declare const OpportunityResponsePayloadSchema: z.ZodObject<{
93
93
  status: z.ZodEnum<{
94
94
  not_interested: "not_interested";
@@ -113,7 +113,7 @@ export declare const OpportunityResponsePayloadSchema: z.ZodObject<{
113
113
  retract: "retract";
114
114
  multi_party_sync: "multi_party_sync";
115
115
  }>>;
116
- }, z.core.$strip>;
116
+ }, z.core.$strict>;
117
117
  export declare const ConnectionRequestPayloadSchema: z.ZodObject<{
118
118
  method: z.ZodEnum<{
119
119
  qr: "qr";
@@ -131,10 +131,10 @@ export declare const ConnectionRequestPayloadSchema: z.ZodObject<{
131
131
  timezone: z.ZodString;
132
132
  meetingHours: z.ZodOptional<z.ZodString>;
133
133
  responseSla: z.ZodOptional<z.ZodString>;
134
- }, z.core.$strip>>;
134
+ }, z.core.$strict>>;
135
135
  openTo: z.ZodArray<z.ZodString>;
136
- }, z.core.$strip>;
137
- }, z.core.$strip>;
136
+ }, z.core.$strict>;
137
+ }, z.core.$strict>;
138
138
  export declare const ConnectionResponsePayloadSchema: z.ZodObject<{
139
139
  status: z.ZodEnum<{
140
140
  accepted: "accepted";
@@ -149,11 +149,11 @@ export declare const ConnectionResponsePayloadSchema: z.ZodObject<{
149
149
  timezone: z.ZodString;
150
150
  meetingHours: z.ZodOptional<z.ZodString>;
151
151
  responseSla: z.ZodOptional<z.ZodString>;
152
- }, z.core.$strip>>;
152
+ }, z.core.$strict>>;
153
153
  openTo: z.ZodArray<z.ZodString>;
154
- }, z.core.$strip>>;
154
+ }, z.core.$strict>>;
155
155
  note: z.ZodOptional<z.ZodString>;
156
- }, z.core.$strip>;
156
+ }, z.core.$strict>;
157
157
  export declare const FollowUpPayloadSchema: z.ZodObject<{
158
158
  referenceId: z.ZodString;
159
159
  message: z.ZodString;
@@ -163,7 +163,7 @@ export declare const FollowUpPayloadSchema: z.ZodObject<{
163
163
  review: "review";
164
164
  none: "none";
165
165
  }>>;
166
- }, z.core.$strip>;
166
+ }, z.core.$strict>;
167
167
  export declare const AskPayloadSchema: z.ZodObject<{
168
168
  question: z.ZodString;
169
169
  context: z.ZodOptional<z.ZodString>;
@@ -173,18 +173,18 @@ export declare const AskPayloadSchema: z.ZodObject<{
173
173
  }>>;
174
174
  choices: z.ZodOptional<z.ZodArray<z.ZodString>>;
175
175
  deadline: z.ZodOptional<z.ZodString>;
176
- }, z.core.$strip>;
176
+ }, z.core.$strict>;
177
177
  export declare const AskResponsePayloadSchema: z.ZodObject<{
178
178
  answer: z.ZodString;
179
179
  choiceIndex: z.ZodOptional<z.ZodNumber>;
180
- }, z.core.$strip>;
180
+ }, z.core.$strict>;
181
181
  export declare const PingPayloadSchema: z.ZodObject<{
182
182
  note: z.ZodOptional<z.ZodString>;
183
- }, z.core.$strip>;
183
+ }, z.core.$strict>;
184
184
  export declare const RetractPayloadSchema: z.ZodObject<{
185
185
  targetMessageId: z.ZodString;
186
186
  reason: z.ZodOptional<z.ZodString>;
187
- }, z.core.$strip>;
187
+ }, z.core.$strict>;
188
188
  export declare const ContextSharePayloadSchema: z.ZodObject<{
189
189
  context: z.ZodString;
190
190
  category: z.ZodEnum<{
@@ -196,7 +196,7 @@ export declare const ContextSharePayloadSchema: z.ZodObject<{
196
196
  }>;
197
197
  referenceId: z.ZodOptional<z.ZodString>;
198
198
  expiresAt: z.ZodOptional<z.ZodString>;
199
- }, z.core.$strip>;
199
+ }, z.core.$strict>;
200
200
  export declare const MultiPartySyncPayloadSchema: z.ZodObject<{
201
201
  enclaveType: z.ZodEnum<{
202
202
  meeting_sync: "meeting_sync";
@@ -204,7 +204,7 @@ export declare const MultiPartySyncPayloadSchema: z.ZodObject<{
204
204
  purpose: z.ZodString;
205
205
  participants: z.ZodArray<z.ZodString>;
206
206
  expiresAt: z.ZodString;
207
- }, z.core.$strip>;
207
+ }, z.core.$strict>;
208
208
  export declare const MessageProvenanceSchema: z.ZodOptional<z.ZodObject<{
209
209
  origin: z.ZodEnum<{
210
210
  human: "human";
@@ -213,9 +213,25 @@ export declare const MessageProvenanceSchema: z.ZodOptional<z.ZodObject<{
213
213
  }>;
214
214
  extensionId: z.ZodString;
215
215
  installationId: z.ZodString;
216
- }, z.core.$strip>>;
216
+ }, z.core.$strict>>;
217
+ /**
218
+ * INK protocol versions a receiver accepts. ink/0.1 is the original wire
219
+ * version; ink/0.2 differs only in the body-signature domain (see
220
+ * src/crypto/sign.ts). The enum is strict: an unknown version is rejected
221
+ * at schema validation, never inferred. Senders still emit ink/0.1 by
222
+ * default; emitting ink/0.2 is a later, negotiated step.
223
+ */
224
+ export declare const INK_PROTOCOL_VERSIONS: readonly ["ink/0.1", "ink/0.2"];
225
+ export declare const ProtocolVersionSchema: z.ZodEnum<{
226
+ "ink/0.1": "ink/0.1";
227
+ "ink/0.2": "ink/0.2";
228
+ }>;
229
+ export type ProtocolVersion = z.infer<typeof ProtocolVersionSchema>;
217
230
  export declare const MessageEnvelopeSchema: z.ZodObject<{
218
- protocol: z.ZodLiteral<"ink/0.1">;
231
+ protocol: z.ZodEnum<{
232
+ "ink/0.1": "ink/0.1";
233
+ "ink/0.2": "ink/0.2";
234
+ }>;
219
235
  id: z.ZodString;
220
236
  correlationId: z.ZodString;
221
237
  createdAt: z.ZodString;
@@ -242,6 +258,8 @@ export declare const MessageEnvelopeSchema: z.ZodObject<{
242
258
  payload: z.ZodUnknown;
243
259
  signature: z.ZodString;
244
260
  signingKeyId: z.ZodOptional<z.ZodString>;
261
+ timestamp: z.ZodOptional<z.ZodString>;
262
+ nonce: z.ZodOptional<z.ZodString>;
245
263
  provenance: z.ZodOptional<z.ZodObject<{
246
264
  origin: z.ZodEnum<{
247
265
  human: "human";
@@ -250,8 +268,8 @@ export declare const MessageEnvelopeSchema: z.ZodObject<{
250
268
  }>;
251
269
  extensionId: z.ZodString;
252
270
  installationId: z.ZodString;
253
- }, z.core.$strip>>;
254
- }, z.core.$strip>;
271
+ }, z.core.$strict>>;
272
+ }, z.core.$strict>;
255
273
  export type MessageEnvelope = z.infer<typeof MessageEnvelopeSchema>;
256
274
  /**
257
275
  * Validate a message envelope AND its payload based on the intent type.
@@ -260,6 +278,10 @@ export type MessageEnvelope = z.infer<typeof MessageEnvelopeSchema>;
260
278
  export declare function validateMessage(raw: unknown): MessageEnvelope;
261
279
  /**
262
280
  * Get the payload schema for a given intent type.
281
+ *
282
+ * Runtime-validates the `intent` argument against IntentTypeSchema so a
283
+ * JS caller cannot pass an arbitrary string and silently get `undefined`
284
+ * back; the function instead throws ZodError on an invalid intent.
263
285
  */
264
286
  export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
265
287
  proposedTimes: z.ZodArray<z.ZodString>;
@@ -277,7 +299,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
277
299
  }>;
278
300
  context: z.ZodOptional<z.ZodString>;
279
301
  location: z.ZodOptional<z.ZodString>;
280
- }, z.core.$strip> | z.ZodObject<{
302
+ }, z.core.$strict> | z.ZodObject<{
281
303
  status: z.ZodEnum<{
282
304
  accepted: "accepted";
283
305
  declined: "declined";
@@ -293,7 +315,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
293
315
  too_busy: "too_busy";
294
316
  deferred: "deferred";
295
317
  }>>;
296
- }, z.core.$strip> | z.ZodObject<{
318
+ }, z.core.$strict> | z.ZodObject<{
297
319
  target: z.ZodString;
298
320
  reason: z.ZodString;
299
321
  context: z.ZodOptional<z.ZodString>;
@@ -301,7 +323,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
301
323
  low: "low";
302
324
  normal: "normal";
303
325
  }>;
304
- }, z.core.$strip> | z.ZodObject<{
326
+ }, z.core.$strict> | z.ZodObject<{
305
327
  status: z.ZodEnum<{
306
328
  declined: "declined";
307
329
  forwarded: "forwarded";
@@ -313,7 +335,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
313
335
  declined: "declined";
314
336
  pending: "pending";
315
337
  }>>;
316
- }, z.core.$strip> | z.ZodObject<{
338
+ }, z.core.$strict> | z.ZodObject<{
317
339
  type: z.ZodEnum<{
318
340
  role: "role";
319
341
  investment: "investment";
@@ -328,7 +350,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
328
350
  matchReason: z.ZodString;
329
351
  expiresAt: z.ZodOptional<z.ZodString>;
330
352
  url: z.ZodOptional<z.ZodString>;
331
- }, z.core.$strip> | z.ZodObject<{
353
+ }, z.core.$strict> | z.ZodObject<{
332
354
  status: z.ZodEnum<{
333
355
  not_interested: "not_interested";
334
356
  interested: "interested";
@@ -352,7 +374,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
352
374
  retract: "retract";
353
375
  multi_party_sync: "multi_party_sync";
354
376
  }>>;
355
- }, z.core.$strip> | z.ZodObject<{
377
+ }, z.core.$strict> | z.ZodObject<{
356
378
  method: z.ZodEnum<{
357
379
  qr: "qr";
358
380
  intro: "intro";
@@ -369,10 +391,10 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
369
391
  timezone: z.ZodString;
370
392
  meetingHours: z.ZodOptional<z.ZodString>;
371
393
  responseSla: z.ZodOptional<z.ZodString>;
372
- }, z.core.$strip>>;
394
+ }, z.core.$strict>>;
373
395
  openTo: z.ZodArray<z.ZodString>;
374
- }, z.core.$strip>;
375
- }, z.core.$strip> | z.ZodObject<{
396
+ }, z.core.$strict>;
397
+ }, z.core.$strict> | z.ZodObject<{
376
398
  status: z.ZodEnum<{
377
399
  accepted: "accepted";
378
400
  declined: "declined";
@@ -386,11 +408,11 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
386
408
  timezone: z.ZodString;
387
409
  meetingHours: z.ZodOptional<z.ZodString>;
388
410
  responseSla: z.ZodOptional<z.ZodString>;
389
- }, z.core.$strip>>;
411
+ }, z.core.$strict>>;
390
412
  openTo: z.ZodArray<z.ZodString>;
391
- }, z.core.$strip>>;
413
+ }, z.core.$strict>>;
392
414
  note: z.ZodOptional<z.ZodString>;
393
- }, z.core.$strip> | z.ZodObject<{
415
+ }, z.core.$strict> | z.ZodObject<{
394
416
  referenceId: z.ZodString;
395
417
  message: z.ZodString;
396
418
  actionRequested: z.ZodOptional<z.ZodEnum<{
@@ -399,7 +421,7 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
399
421
  review: "review";
400
422
  none: "none";
401
423
  }>>;
402
- }, z.core.$strip> | z.ZodObject<{
424
+ }, z.core.$strict> | z.ZodObject<{
403
425
  question: z.ZodString;
404
426
  context: z.ZodOptional<z.ZodString>;
405
427
  responseFormat: z.ZodOptional<z.ZodEnum<{
@@ -408,15 +430,15 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
408
430
  }>>;
409
431
  choices: z.ZodOptional<z.ZodArray<z.ZodString>>;
410
432
  deadline: z.ZodOptional<z.ZodString>;
411
- }, z.core.$strip> | z.ZodObject<{
433
+ }, z.core.$strict> | z.ZodObject<{
412
434
  answer: z.ZodString;
413
435
  choiceIndex: z.ZodOptional<z.ZodNumber>;
414
- }, z.core.$strip> | z.ZodObject<{
436
+ }, z.core.$strict> | z.ZodObject<{
415
437
  note: z.ZodOptional<z.ZodString>;
416
- }, z.core.$strip> | z.ZodObject<{
438
+ }, z.core.$strict> | z.ZodObject<{
417
439
  targetMessageId: z.ZodString;
418
440
  reason: z.ZodOptional<z.ZodString>;
419
- }, z.core.$strip> | z.ZodObject<{
441
+ }, z.core.$strict> | z.ZodObject<{
420
442
  context: z.ZodString;
421
443
  category: z.ZodEnum<{
422
444
  availability: "availability";
@@ -427,11 +449,11 @@ export declare function getPayloadSchema(intent: IntentType): z.ZodObject<{
427
449
  }>;
428
450
  referenceId: z.ZodOptional<z.ZodString>;
429
451
  expiresAt: z.ZodOptional<z.ZodString>;
430
- }, z.core.$strip> | z.ZodObject<{
452
+ }, z.core.$strict> | z.ZodObject<{
431
453
  enclaveType: z.ZodEnum<{
432
454
  meeting_sync: "meeting_sync";
433
455
  }>;
434
456
  purpose: z.ZodString;
435
457
  participants: z.ZodArray<z.ZodString>;
436
458
  expiresAt: z.ZodString;
437
- }, z.core.$strip>;
459
+ }, z.core.$strict>;
@@ -19,35 +19,45 @@ export const IntentTypeSchema = z.enum([
19
19
  "multi_party_sync",
20
20
  ]);
21
21
  // --- Intent Payloads ---
22
+ // Reusable scalar caps. Timestamps cap at 64 chars (ISO-8601 fits in
23
+ // ~30), correlation/message IDs at 256, DIDs at 512. URL fields cap
24
+ // at 2048 BEFORE `.url()` parsing so the parser never runs on attacker-
25
+ // sized strings. Every exported schema is `.strict()` so adopters using
26
+ // the schemas directly (without `validateMessage()`) still get the
27
+ // same unknown-field rejection that the central validator applies.
28
+ const TIMESTAMP_MAX = 64;
29
+ const ID_MAX = 256;
30
+ const DID_MAX = 512;
31
+ const URL_MAX = 2048;
22
32
  export const ScheduleMeetingPayloadSchema = z.object({
23
- proposedTimes: z.array(z.string()).min(1).max(10),
33
+ proposedTimes: z.array(z.string().max(TIMESTAMP_MAX)).min(1).max(10),
24
34
  topic: z.string().max(500),
25
35
  format: z.enum(["video", "phone", "in_person", "async"]),
26
36
  urgency: z.enum(["low", "normal", "urgent"]),
27
37
  context: z.string().max(2000).optional(),
28
38
  location: z.string().max(500).optional(),
29
- });
39
+ }).strict();
30
40
  export const ScheduleMeetingResponsePayloadSchema = z.object({
31
41
  status: z.enum(["accepted", "declined", "countered"]),
32
- confirmedTime: z.string().optional(),
33
- counterTimes: z.array(z.string()).max(10).optional(),
34
- meetingLink: z.string().url().optional(),
42
+ confirmedTime: z.string().max(TIMESTAMP_MAX).optional(),
43
+ counterTimes: z.array(z.string().max(TIMESTAMP_MAX)).max(10).optional(),
44
+ meetingLink: z.string().max(URL_MAX).url().optional(),
35
45
  note: z.string().max(1000).optional(),
36
46
  declineReason: z
37
47
  .enum(["unavailable", "not_interested", "too_busy", "deferred"])
38
48
  .optional(),
39
- });
49
+ }).strict();
40
50
  export const IntroRequestPayloadSchema = z.object({
41
- target: z.string(),
51
+ target: z.string().max(DID_MAX),
42
52
  reason: z.string().max(2000),
43
53
  context: z.string().max(2000).optional(),
44
54
  urgency: z.enum(["low", "normal"]),
45
- });
55
+ }).strict();
46
56
  export const IntroResponsePayloadSchema = z.object({
47
57
  status: z.enum(["forwarded", "declined", "pending_target"]),
48
58
  note: z.string().max(1000).optional(),
49
59
  targetResponse: z.enum(["accepted", "declined", "pending"]).optional(),
50
- });
60
+ }).strict();
51
61
  export const OpportunityPayloadSchema = z.object({
52
62
  type: z.enum([
53
63
  "role",
@@ -61,60 +71,60 @@ export const OpportunityPayloadSchema = z.object({
61
71
  org: z.string().max(200).optional(),
62
72
  description: z.string().max(5000),
63
73
  matchReason: z.string().max(2000),
64
- expiresAt: z.string().optional(),
65
- url: z.string().url().optional(),
66
- });
74
+ expiresAt: z.string().max(TIMESTAMP_MAX).optional(),
75
+ url: z.string().max(URL_MAX).url().optional(),
76
+ }).strict();
67
77
  export const OpportunityResponsePayloadSchema = z.object({
68
78
  status: z.enum(["interested", "not_interested", "maybe_later"]),
69
79
  note: z.string().max(1000).optional(),
70
80
  followUpIntent: IntentTypeSchema.optional(),
71
- });
81
+ }).strict();
72
82
  export const ConnectionRequestPayloadSchema = z.object({
73
83
  method: z.enum(["qr", "intro", "discovery", "import"]),
74
- introducedBy: z.string().optional(),
84
+ introducedBy: z.string().max(DID_MAX).optional(),
75
85
  context: z.string().max(2000),
76
86
  profileSnapshot: ProfileSnapshotSchema,
77
- });
87
+ }).strict();
78
88
  export const ConnectionResponsePayloadSchema = z.object({
79
89
  status: z.enum(["accepted", "declined", "pending"]),
80
90
  profileSnapshot: ProfileSnapshotSchema.optional(),
81
91
  note: z.string().max(1000).optional(),
82
- });
92
+ }).strict();
83
93
  export const FollowUpPayloadSchema = z.object({
84
- referenceId: z.string(),
94
+ referenceId: z.string().max(ID_MAX),
85
95
  message: z.string().max(5000),
86
96
  actionRequested: z.enum(["reply", "schedule", "review", "none"]).optional(),
87
- });
97
+ }).strict();
88
98
  export const AskPayloadSchema = z.object({
89
99
  question: z.string().max(5000),
90
100
  context: z.string().max(2000).optional(),
91
101
  responseFormat: z.enum(["text", "choice"]).optional(),
92
102
  choices: z.array(z.string().max(500)).max(10).optional(),
93
- deadline: z.string().optional(),
94
- });
103
+ deadline: z.string().max(TIMESTAMP_MAX).optional(),
104
+ }).strict();
95
105
  export const AskResponsePayloadSchema = z.object({
96
106
  answer: z.string().max(5000),
97
107
  choiceIndex: z.number().int().min(0).optional(),
98
- });
108
+ }).strict();
99
109
  export const PingPayloadSchema = z.object({
100
110
  note: z.string().max(1000).optional(),
101
- });
111
+ }).strict();
102
112
  export const RetractPayloadSchema = z.object({
103
- targetMessageId: z.string(),
113
+ targetMessageId: z.string().max(ID_MAX),
104
114
  reason: z.string().max(1000).optional(),
105
- });
115
+ }).strict();
106
116
  export const ContextSharePayloadSchema = z.object({
107
117
  context: z.string().max(5000),
108
118
  category: z.enum(["professional_background", "project_update", "expertise", "availability", "general"]),
109
- referenceId: z.string().optional(),
110
- expiresAt: z.string().optional(),
111
- });
119
+ referenceId: z.string().max(ID_MAX).optional(),
120
+ expiresAt: z.string().max(TIMESTAMP_MAX).optional(),
121
+ }).strict();
112
122
  export const MultiPartySyncPayloadSchema = z.object({
113
123
  enclaveType: z.enum(["meeting_sync"]),
114
124
  purpose: z.string().max(500),
115
- participants: z.array(z.string()).min(2).max(20),
116
- expiresAt: z.string(),
117
- });
125
+ participants: z.array(z.string().max(DID_MAX)).min(2).max(20),
126
+ expiresAt: z.string().max(TIMESTAMP_MAX),
127
+ }).strict();
118
128
  // --- Payload discriminated union ---
119
129
  const payloadSchemas = {
120
130
  schedule_meeting: ScheduleMeetingPayloadSchema,
@@ -134,25 +144,48 @@ const payloadSchemas = {
134
144
  multi_party_sync: MultiPartySyncPayloadSchema,
135
145
  };
136
146
  // --- Message Envelope ---
147
+ // Caps for envelope-level fields. Signatures are base64url-encoded
148
+ // Ed25519 (64 bytes raw → 86 chars base64url, plus the legacy keyId=
149
+ // suffix). 256 is comfortable headroom without permitting megabyte
150
+ // signature blobs.
151
+ const SIGNATURE_MAX = 256;
152
+ const KEY_ID_MAX = 128;
137
153
  export const MessageProvenanceSchema = z.object({
138
154
  origin: z.enum(["human", "agent_approved", "agent_autonomous"]),
139
- extensionId: z.string(),
155
+ extensionId: z.string().max(ID_MAX),
140
156
  installationId: z.string().uuid(),
141
- }).optional();
157
+ }).strict().optional();
158
+ /**
159
+ * INK protocol versions a receiver accepts. ink/0.1 is the original wire
160
+ * version; ink/0.2 differs only in the body-signature domain (see
161
+ * src/crypto/sign.ts). The enum is strict: an unknown version is rejected
162
+ * at schema validation, never inferred. Senders still emit ink/0.1 by
163
+ * default; emitting ink/0.2 is a later, negotiated step.
164
+ */
165
+ export const INK_PROTOCOL_VERSIONS = ["ink/0.1", "ink/0.2"];
166
+ export const ProtocolVersionSchema = z.enum(INK_PROTOCOL_VERSIONS);
142
167
  export const MessageEnvelopeSchema = z.object({
143
- protocol: z.literal("ink/0.1"),
144
- id: z.string(),
145
- correlationId: z.string(),
146
- createdAt: z.string(),
147
- expiresAt: z.string().optional(),
148
- from: z.string(),
149
- to: z.string(),
168
+ protocol: ProtocolVersionSchema,
169
+ id: z.string().max(ID_MAX),
170
+ correlationId: z.string().max(ID_MAX),
171
+ createdAt: z.string().max(TIMESTAMP_MAX),
172
+ expiresAt: z.string().max(TIMESTAMP_MAX).optional(),
173
+ from: z.string().max(DID_MAX),
174
+ to: z.string().max(DID_MAX),
150
175
  intent: IntentTypeSchema,
151
176
  payload: z.unknown(),
152
- signature: z.string(),
153
- signingKeyId: z.string().optional(),
177
+ signature: z.string().max(SIGNATURE_MAX),
178
+ signingKeyId: z.string().max(KEY_ID_MAX).optional(),
179
+ // HTTP §3.3 transport-auth metadata that rides alongside the
180
+ // canonical envelope fields. The body-level signature commits to
181
+ // both (they cannot be tampered in transit) and `verifyInkAuth`
182
+ // reads them from the body for freshness + replay checks. Explicit
183
+ // optional capped declarations are required for `.strict()` to keep
184
+ // accepting documented sender envelopes (see README signing example).
185
+ timestamp: z.string().max(TIMESTAMP_MAX).optional(),
186
+ nonce: z.string().max(ID_MAX).optional(),
154
187
  provenance: MessageProvenanceSchema,
155
- });
188
+ }).strict();
156
189
  /**
157
190
  * Validate a message envelope AND its payload based on the intent type.
158
191
  * Returns the validated message or throws a ZodError.
@@ -166,7 +199,12 @@ export function validateMessage(raw) {
166
199
  }
167
200
  /**
168
201
  * Get the payload schema for a given intent type.
202
+ *
203
+ * Runtime-validates the `intent` argument against IntentTypeSchema so a
204
+ * JS caller cannot pass an arbitrary string and silently get `undefined`
205
+ * back; the function instead throws ZodError on an invalid intent.
169
206
  */
170
207
  export function getPayloadSchema(intent) {
208
+ IntentTypeSchema.parse(intent);
171
209
  return payloadSchemas[intent];
172
210
  }
@@ -3,7 +3,7 @@ export declare const AvailabilityConfigSchema: z.ZodObject<{
3
3
  timezone: z.ZodString;
4
4
  meetingHours: z.ZodOptional<z.ZodString>;
5
5
  responseSla: z.ZodOptional<z.ZodString>;
6
- }, z.core.$strip>;
6
+ }, z.core.$strict>;
7
7
  export declare const ProfileSnapshotSchema: z.ZodObject<{
8
8
  headline: z.ZodString;
9
9
  skills: z.ZodArray<z.ZodString>;
@@ -12,9 +12,9 @@ export declare const ProfileSnapshotSchema: z.ZodObject<{
12
12
  timezone: z.ZodString;
13
13
  meetingHours: z.ZodOptional<z.ZodString>;
14
14
  responseSla: z.ZodOptional<z.ZodString>;
15
- }, z.core.$strip>>;
15
+ }, z.core.$strict>>;
16
16
  openTo: z.ZodArray<z.ZodString>;
17
- }, z.core.$strip>;
17
+ }, z.core.$strict>;
18
18
  export declare const ProfileSchema: z.ZodObject<{
19
19
  agentId: z.ZodString;
20
20
  handle: z.ZodString;
@@ -29,9 +29,9 @@ export declare const ProfileSchema: z.ZodObject<{
29
29
  timezone: z.ZodString;
30
30
  meetingHours: z.ZodOptional<z.ZodString>;
31
31
  responseSla: z.ZodOptional<z.ZodString>;
32
- }, z.core.$strip>>;
32
+ }, z.core.$strict>>;
33
33
  openTo: z.ZodArray<z.ZodString>;
34
- }, z.core.$strip>;
34
+ }, z.core.$strict>;
35
35
  connected: z.ZodObject<{
36
36
  headline: z.ZodString;
37
37
  skills: z.ZodArray<z.ZodString>;
@@ -40,9 +40,9 @@ export declare const ProfileSchema: z.ZodObject<{
40
40
  timezone: z.ZodString;
41
41
  meetingHours: z.ZodOptional<z.ZodString>;
42
42
  responseSla: z.ZodOptional<z.ZodString>;
43
- }, z.core.$strip>>;
43
+ }, z.core.$strict>>;
44
44
  openTo: z.ZodArray<z.ZodString>;
45
- }, z.core.$strip>;
45
+ }, z.core.$strict>;
46
46
  custom: z.ZodRecord<z.ZodString, z.ZodObject<{
47
47
  headline: z.ZodString;
48
48
  skills: z.ZodArray<z.ZodString>;
@@ -51,9 +51,9 @@ export declare const ProfileSchema: z.ZodObject<{
51
51
  timezone: z.ZodString;
52
52
  meetingHours: z.ZodOptional<z.ZodString>;
53
53
  responseSla: z.ZodOptional<z.ZodString>;
54
- }, z.core.$strip>>;
54
+ }, z.core.$strict>>;
55
55
  openTo: z.ZodArray<z.ZodString>;
56
- }, z.core.$strip>>;
56
+ }, z.core.$strict>>;
57
57
  }, z.core.$strip>;
58
58
  }, z.core.$strip>;
59
59
  export type AvailabilityConfig = z.infer<typeof AvailabilityConfigSchema>;
@@ -1,16 +1,20 @@
1
1
  import { z } from "zod";
2
2
  export const AvailabilityConfigSchema = z.object({
3
- timezone: z.string(),
4
- meetingHours: z.string().optional(),
5
- responseSla: z.string().optional(),
6
- });
3
+ // IANA timezone name. The longest legitimate value is ~50 chars.
4
+ timezone: z.string().max(64),
5
+ // Free-text availability description ("9-5 PT weekdays") capped
6
+ // to a sane display length. Larger values are almost certainly
7
+ // garbage or an attempted DoS.
8
+ meetingHours: z.string().max(200).optional(),
9
+ responseSla: z.string().max(200).optional(),
10
+ }).strict();
7
11
  export const ProfileSnapshotSchema = z.object({
8
12
  headline: z.string().max(500),
9
13
  skills: z.array(z.string().max(100)).max(50),
10
14
  interests: z.array(z.string().max(100)).max(50),
11
15
  availability: AvailabilityConfigSchema.optional(),
12
16
  openTo: z.array(z.string().max(100)).max(20),
13
- });
17
+ }).strict();
14
18
  export const ProfileSchema = z.object({
15
19
  agentId: z.string(),
16
20
  handle: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adastracomputing/ink",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Library and specification for the INK (Inter-agent Networking Kernel) protocol",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "Ad Astra Computing Inc.",
@@ -49,6 +49,7 @@
49
49
  "lint": "eslint src/ test/ scripts/",
50
50
  "check:surface": "tsx scripts/check-public-surface.ts",
51
51
  "check:pack": "./scripts/check-pack.sh",
52
+ "gen:body-vectors": "tsx scripts/gen-body-signature-vectors.ts",
52
53
  "prepack": "npm run build",
53
54
  "prepublishOnly": "npm run build"
54
55
  },
@@ -7,16 +7,17 @@ Reference test vectors for INK v0.1 signing, encryption, replay protection, hand
7
7
  | File | Covers | Vector count |
8
8
  |------|--------|-------------|
9
9
  | `keys.json` | Fixed Ed25519 and X25519 key pairs for Alice and Bob (hex-encoded) |, |
10
- | `signing.json` | Signature generation and verification (§3.3) | 3 |
10
+ | `signing.json` | Transport-auth signature generation and verification (§3.3) | 3 |
11
+ | `body-signature.json` | Version-keyed body message signature: legacy vs ink/0.2 domain, plus cross-version and tamper cases | 9 |
11
12
  | `encryption.json` | ECIES encryption/decryption (§3.4) | 2 |
12
- | `jcs.json` | JCS canonicalization (RFC 8785) | 4 |
13
+ | `jcs.json` | JCS canonicalization (RFC 8785) | 2 |
13
14
  | `replay.json` | Replay protection acceptance/rejection (§3.5) | 6 |
14
15
  | `receipts-and-audit.json` | Receipt signatures, audit query signatures, hash-chained audit events and fork detection (Auditability §1–§3) | 4 |
15
16
  | `handshake.json` | Challenge (Stage 2a), rejection (Stage 2b) and resolution (Stage 3), valid signatures, path/recipient/body binding failures, replay protection | 22 |
16
17
  | `witness.json` | Audit submit and query with INK transport auth, plus cross-service interop cases | 15 |
17
18
  | `key-rotation.json` | Auth header keyId format, rotated-key verification, historical verification, revoked-key rejection, refresh-on-miss, keyId precedence, unknown keyId fallthrough, audit event signingKeyId tracking | 8 |
18
19
 
19
- **Total: 64 deterministic vectors across 9 families**
20
+ **Total: 71 deterministic vectors across 10 families**
20
21
 
21
22
  ## Vector categories
22
23
 
@@ -0,0 +1,201 @@
1
+ {
2
+ "description": "INK body message signature: domain is keyed off the signed protocol field (ink/0.2 -> ink/sign, else tulpa/sign). Verify each body with the signer public key; signatureVerifies is the expected verifyMessage result.",
3
+ "vectors": [
4
+ {
5
+ "description": "ink/0.1 body signed under the legacy tulpa/sign domain verifies",
6
+ "input": {
7
+ "body": {
8
+ "protocol": "ink/0.1",
9
+ "id": "01HVECTORID0000000000000000",
10
+ "from": "did:key:zAlice",
11
+ "to": "did:key:zBob",
12
+ "intent": "connection_request",
13
+ "payload": {
14
+ "method": "discovery"
15
+ },
16
+ "timestamp": "2026-06-03T00:00:00Z",
17
+ "nonce": "ZmtmaXhlZG5vbmNl",
18
+ "signature": "W44kkGyQFg348NADetWQPq5Ogi7rL_72CwjmK-XVn2hL8sXYuSM7cFaDVidGV5LeK3dmmqW6iu5QiWkm6qboAQ"
19
+ },
20
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
21
+ },
22
+ "expected": {
23
+ "signatureVerifies": true
24
+ }
25
+ },
26
+ {
27
+ "description": "ink/0.2 body signed under the ink/sign domain verifies",
28
+ "input": {
29
+ "body": {
30
+ "protocol": "ink/0.2",
31
+ "id": "01HVECTORID0000000000000000",
32
+ "from": "did:key:zAlice",
33
+ "to": "did:key:zBob",
34
+ "intent": "connection_request",
35
+ "payload": {
36
+ "method": "discovery"
37
+ },
38
+ "timestamp": "2026-06-03T00:00:00Z",
39
+ "nonce": "ZmtmaXhlZG5vbmNl",
40
+ "signature": "UAITw3Qqcg96s4r5tmxHXGpuHCfzJBgxihVRPdR8FYcQKk2nYcYxrV8fRFUH3NB_-z-006tFWgZfzJbFILw4Bg"
41
+ },
42
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
43
+ },
44
+ "expected": {
45
+ "signatureVerifies": true
46
+ }
47
+ },
48
+ {
49
+ "description": "protocol-less body signed under the legacy domain verifies",
50
+ "input": {
51
+ "body": {
52
+ "id": "01HVECTORID0000000000000000",
53
+ "from": "did:key:zAlice",
54
+ "to": "did:key:zBob",
55
+ "intent": "connection_request",
56
+ "payload": {
57
+ "method": "discovery"
58
+ },
59
+ "timestamp": "2026-06-03T00:00:00Z",
60
+ "nonce": "ZmtmaXhlZG5vbmNl",
61
+ "signature": "qP1oxHKCEImzkYT8Iz00N4Crqi5prbKRguKj_zg8YlLyCrE4CmElrNWECEBWBe-8XV_rNB4oMc1wReXwHqOFCA"
62
+ },
63
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
64
+ },
65
+ "expected": {
66
+ "signatureVerifies": true
67
+ }
68
+ },
69
+ {
70
+ "description": "ink/0.2 body relabelled ink/0.1 (domain mismatch) does not verify",
71
+ "input": {
72
+ "body": {
73
+ "protocol": "ink/0.1",
74
+ "id": "01HVECTORID0000000000000000",
75
+ "from": "did:key:zAlice",
76
+ "to": "did:key:zBob",
77
+ "intent": "connection_request",
78
+ "payload": {
79
+ "method": "discovery"
80
+ },
81
+ "timestamp": "2026-06-03T00:00:00Z",
82
+ "nonce": "ZmtmaXhlZG5vbmNl",
83
+ "signature": "UAITw3Qqcg96s4r5tmxHXGpuHCfzJBgxihVRPdR8FYcQKk2nYcYxrV8fRFUH3NB_-z-006tFWgZfzJbFILw4Bg"
84
+ },
85
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
86
+ },
87
+ "expected": {
88
+ "signatureVerifies": false
89
+ }
90
+ },
91
+ {
92
+ "description": "ink/0.1 body relabelled ink/0.2 (domain mismatch) does not verify",
93
+ "input": {
94
+ "body": {
95
+ "protocol": "ink/0.2",
96
+ "id": "01HVECTORID0000000000000000",
97
+ "from": "did:key:zAlice",
98
+ "to": "did:key:zBob",
99
+ "intent": "connection_request",
100
+ "payload": {
101
+ "method": "discovery"
102
+ },
103
+ "timestamp": "2026-06-03T00:00:00Z",
104
+ "nonce": "ZmtmaXhlZG5vbmNl",
105
+ "signature": "W44kkGyQFg348NADetWQPq5Ogi7rL_72CwjmK-XVn2hL8sXYuSM7cFaDVidGV5LeK3dmmqW6iu5QiWkm6qboAQ"
106
+ },
107
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
108
+ },
109
+ "expected": {
110
+ "signatureVerifies": false
111
+ }
112
+ },
113
+ {
114
+ "description": "ink/0.2 body with protocol removed (falls back to legacy domain) does not verify",
115
+ "input": {
116
+ "body": {
117
+ "id": "01HVECTORID0000000000000000",
118
+ "from": "did:key:zAlice",
119
+ "to": "did:key:zBob",
120
+ "intent": "connection_request",
121
+ "payload": {
122
+ "method": "discovery"
123
+ },
124
+ "timestamp": "2026-06-03T00:00:00Z",
125
+ "nonce": "ZmtmaXhlZG5vbmNl",
126
+ "signature": "UAITw3Qqcg96s4r5tmxHXGpuHCfzJBgxihVRPdR8FYcQKk2nYcYxrV8fRFUH3NB_-z-006tFWgZfzJbFILw4Bg"
127
+ },
128
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
129
+ },
130
+ "expected": {
131
+ "signatureVerifies": false
132
+ }
133
+ },
134
+ {
135
+ "description": "ink/0.1 body with a flipped payload field does not verify",
136
+ "input": {
137
+ "body": {
138
+ "protocol": "ink/0.1",
139
+ "id": "01HVECTORID0000000000000000",
140
+ "from": "did:key:zAlice",
141
+ "to": "did:key:zBob",
142
+ "intent": "connection_request",
143
+ "payload": {
144
+ "method": "qr"
145
+ },
146
+ "timestamp": "2026-06-03T00:00:00Z",
147
+ "nonce": "ZmtmaXhlZG5vbmNl",
148
+ "signature": "W44kkGyQFg348NADetWQPq5Ogi7rL_72CwjmK-XVn2hL8sXYuSM7cFaDVidGV5LeK3dmmqW6iu5QiWkm6qboAQ"
149
+ },
150
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
151
+ },
152
+ "expected": {
153
+ "signatureVerifies": false
154
+ }
155
+ },
156
+ {
157
+ "description": "ink/0.2 body signed under the legacy domain does not verify (a verifier must not try both domains)",
158
+ "input": {
159
+ "body": {
160
+ "protocol": "ink/0.2",
161
+ "id": "01HVECTORID0000000000000000",
162
+ "from": "did:key:zAlice",
163
+ "to": "did:key:zBob",
164
+ "intent": "connection_request",
165
+ "payload": {
166
+ "method": "discovery"
167
+ },
168
+ "timestamp": "2026-06-03T00:00:00Z",
169
+ "nonce": "ZmtmaXhlZG5vbmNl",
170
+ "signature": "axpZvQqF8KPG-Mh1q3hnfZhnaSUHbYfToUz-wA_WD7PV0qKOnVHpuTu8DTaR0OTtGBVHoqpoQFlfMVskELfgDg"
171
+ },
172
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
173
+ },
174
+ "expected": {
175
+ "signatureVerifies": false
176
+ }
177
+ },
178
+ {
179
+ "description": "unknown protocol string uses the legacy body-signature domain and verifies",
180
+ "input": {
181
+ "body": {
182
+ "protocol": "ink/0.3",
183
+ "id": "01HVECTORID0000000000000000",
184
+ "from": "did:key:zAlice",
185
+ "to": "did:key:zBob",
186
+ "intent": "connection_request",
187
+ "payload": {
188
+ "method": "discovery"
189
+ },
190
+ "timestamp": "2026-06-03T00:00:00Z",
191
+ "nonce": "ZmtmaXhlZG5vbmNl",
192
+ "signature": "I3JIzt0kg9zncGvRU9nSZ2ldVrE4L9JnolDJAgW4kNUAqmp6bLAfPptfbYttqp1nTX4OB3oCCOOBPFCA7Fw4Cw"
193
+ },
194
+ "signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
195
+ },
196
+ "expected": {
197
+ "signatureVerifies": true
198
+ }
199
+ }
200
+ ]
201
+ }