@kya-os/mcp-i 1.12.0 → 1.12.2

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.
@@ -94,6 +94,17 @@ export declare class IdentityManager {
94
94
  * Requirements: 4.2, 4.3
95
95
  */
96
96
  private loadProdIdentity;
97
+ /**
98
+ * Derive the real Ed25519 public key from a raw private key seed.
99
+ *
100
+ * Ed25519 public keys are a deterministic function of the private key, so
101
+ * this recovers the actual key rather than approximating one. A previous
102
+ * version of this method returned a SHA-256 digest of the private key as
103
+ * a "placeholder public key" — cryptographically meaningless, and wrong
104
+ * wherever an AgentIdentity's publicKey is later published or used to
105
+ * build a kid (e.g. migrate-identity.ts, well-known.ts).
106
+ */
107
+ private derivePublicKey;
97
108
  /**
98
109
  * Validate identity configuration
99
110
  */
@@ -13,6 +13,11 @@ const fs_1 = require("fs");
13
13
  const path_1 = require("path");
14
14
  const jose_1 = require("jose");
15
15
  const crypto_1 = require("crypto");
16
+ // Minimal DER envelopes Node's crypto module needs to recognize a raw
17
+ // 32-byte Ed25519 key as PKCS8 (private) / SPKI (public). Mirrors the same
18
+ // prefixes used for signing/verification in providers/node-providers.ts.
19
+ const ED25519_PKCS8_PREFIX_HEX = "302e020100300506032b657004220420";
20
+ const ED25519_SPKI_HEADER_BYTES = 12;
16
21
  /**
17
22
  * Error codes for identity management
18
23
  */
@@ -232,14 +237,21 @@ class IdentityManager {
232
237
  error.code = exports.IDENTITY_ERRORS.ENOIDENTITY;
233
238
  throw error;
234
239
  }
235
- // For production, we expect the private key to be base64-encoded
236
- // We'll derive a placeholder public key since we don't need it for signing
237
- // In a real implementation, the public key would be derived properly
238
- const privateKeyBuffer = Buffer.from(env.AGENT_PRIVATE_KEY, "base64");
239
- // Generate a deterministic public key placeholder from the private key
240
- const publicKey = (0, crypto_1.createHash)("sha256")
241
- .update(privateKeyBuffer)
242
- .digest("base64");
240
+ // For production, the private key is base64-encoded (raw 32-byte seed,
241
+ // or a 64-byte seed+public-key pair — see derivePublicKey below). Fail
242
+ // fast with a clear error on malformed input rather than silently
243
+ // producing an identity with unusable key material.
244
+ let publicKey;
245
+ try {
246
+ publicKey = this.derivePublicKey(env.AGENT_PRIVATE_KEY);
247
+ }
248
+ catch (cause) {
249
+ const error = new Error("AGENT_PRIVATE_KEY is not a valid Ed25519 private key " +
250
+ "(must be a base64-encoded 32-byte seed, or a 64-byte seed+public-key pair).");
251
+ error.code = exports.IDENTITY_ERRORS.ENOIDENTITY;
252
+ error.cause = cause;
253
+ throw error;
254
+ }
243
255
  return {
244
256
  did: env.AGENT_DID,
245
257
  kid: env.AGENT_KEY_ID,
@@ -248,6 +260,43 @@ class IdentityManager {
248
260
  createdAt: new Date().toISOString(), // We don't have creation time in prod
249
261
  };
250
262
  }
263
+ /**
264
+ * Derive the real Ed25519 public key from a raw private key seed.
265
+ *
266
+ * Ed25519 public keys are a deterministic function of the private key, so
267
+ * this recovers the actual key rather than approximating one. A previous
268
+ * version of this method returned a SHA-256 digest of the private key as
269
+ * a "placeholder public key" — cryptographically meaningless, and wrong
270
+ * wherever an AgentIdentity's publicKey is later published or used to
271
+ * build a kid (e.g. migrate-identity.ts, well-known.ts).
272
+ */
273
+ derivePublicKey(privateKeyBase64) {
274
+ const privateKeyBuffer = Buffer.from(privateKeyBase64, "base64");
275
+ // Handle both raw 32-byte and full 64-byte (seed + public key) Ed25519
276
+ // private keys, matching providers/node-providers.ts's sign().
277
+ const seed = privateKeyBuffer.length === 64
278
+ ? privateKeyBuffer.subarray(0, 32)
279
+ : privateKeyBuffer;
280
+ // Wrap the raw seed in a minimal PKCS8 DER envelope so Node's crypto
281
+ // module recognizes it as an Ed25519 private key.
282
+ const pkcs8 = Buffer.concat([
283
+ Buffer.from(ED25519_PKCS8_PREFIX_HEX, "hex"),
284
+ seed,
285
+ ]);
286
+ const privateKeyObject = (0, crypto_1.createPrivateKey)({
287
+ key: pkcs8,
288
+ format: "der",
289
+ type: "pkcs8",
290
+ });
291
+ const publicKeyDer = (0, crypto_1.createPublicKey)(privateKeyObject).export({
292
+ format: "der",
293
+ type: "spki",
294
+ });
295
+ // Strip the fixed-length SPKI header to get the raw 32-byte public key.
296
+ return publicKeyDer
297
+ .subarray(ED25519_SPKI_HEADER_BYTES)
298
+ .toString("base64");
299
+ }
251
300
  /**
252
301
  * Validate identity configuration
253
302
  */
@@ -18,7 +18,7 @@ export { type AuthHandshakeConfig, type VerifyOrHintsResult, type AgentReputatio
18
18
  export { createResumeTokenStore, type ResumeTokenStoreConfig, } from "./resume-token-store";
19
19
  export { CloudflareKVResumeTokenStore, type ResumeTokenKVNamespace, } from "./resume-token-store-kv";
20
20
  export { ensureOutboundIdentityBridge } from "./outbound-identity-bridge";
21
- export { type ProofDestination, type ProofBatchQueueConfig, ProofBatchQueue, KTAProofDestination, AgentShieldProofDestination, createProofBatchQueue, } from "./proof-batch-queue";
21
+ export { type ProofDestination, type ProofSubmission, type ProofBatchQueueConfig, ProofBatchQueue, KTAProofDestination, AgentShieldProofDestination, PartialProofSubmissionError, createProofBatchQueue, } from "./proof-batch-queue";
22
22
  export { CloudflareKVDelegationVerifier, } from "./delegation-verifier-kv";
23
23
  export { AgentShieldAPIDelegationVerifier, } from "./delegation-verifier-agentshield";
24
24
  export { MemoryDelegationVerifier, } from "./delegation-verifier-memory";
@@ -6,8 +6,8 @@
6
6
  * canonical audit trails, and well-known endpoints.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.toolProtectionRegistry = exports.AgentShieldToolProtectionSource = exports.FileToolProtectionSource = exports.InlineToolProtectionSource = exports.createToolProtectionResolver = exports.ToolProtectionResolver = exports.MemoryDelegationVerifier = exports.AgentShieldAPIDelegationVerifier = exports.CloudflareKVDelegationVerifier = exports.createProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = exports.ProofBatchQueue = exports.ensureOutboundIdentityBridge = exports.CloudflareKVResumeTokenStore = exports.createResumeTokenStore = exports.MemoryResumeTokenStore = exports.hasSensitiveScopes = exports.verifyOrHints = exports.extractScopes = exports.validateDelegation = exports.checkScopes = exports.createDelegationVerifier = exports.formatVerifyLink = exports.DemoConsole = exports.createDemoManager = exports.DemoManager = exports.createDebugEndpoint = exports.DebugManager = exports.extractDIDFromPath = exports.validateAgentDocument = exports.validateDIDDocument = exports.createWellKnownHandler = exports.WellKnownManager = exports.extractCanonicalData = exports.createProofResponse = exports.ProofGenerator = exports.validateHandshakeFormat = exports.createHandshakeRequest = exports.defaultSessionManager = exports.SessionManager = exports.IDENTITY_ERRORS = exports.ensureIdentity = exports.defaultIdentityManager = exports.IdentityManager = exports.RUNTIME_ERRORS = exports.RuntimeFactory = exports.createMCPIRuntime = exports.MCPIRuntime = exports.MCPINodeRuntimeWrapper = void 0;
10
- exports.getToolProtection = exports.isToolProtected = void 0;
9
+ exports.AgentShieldToolProtectionSource = exports.FileToolProtectionSource = exports.InlineToolProtectionSource = exports.createToolProtectionResolver = exports.ToolProtectionResolver = exports.MemoryDelegationVerifier = exports.AgentShieldAPIDelegationVerifier = exports.CloudflareKVDelegationVerifier = exports.createProofBatchQueue = exports.PartialProofSubmissionError = exports.AgentShieldProofDestination = exports.KTAProofDestination = exports.ProofBatchQueue = exports.ensureOutboundIdentityBridge = exports.CloudflareKVResumeTokenStore = exports.createResumeTokenStore = exports.MemoryResumeTokenStore = exports.hasSensitiveScopes = exports.verifyOrHints = exports.extractScopes = exports.validateDelegation = exports.checkScopes = exports.createDelegationVerifier = exports.formatVerifyLink = exports.DemoConsole = exports.createDemoManager = exports.DemoManager = exports.createDebugEndpoint = exports.DebugManager = exports.extractDIDFromPath = exports.validateAgentDocument = exports.validateDIDDocument = exports.createWellKnownHandler = exports.WellKnownManager = exports.extractCanonicalData = exports.createProofResponse = exports.ProofGenerator = exports.validateHandshakeFormat = exports.createHandshakeRequest = exports.defaultSessionManager = exports.SessionManager = exports.IDENTITY_ERRORS = exports.ensureIdentity = exports.defaultIdentityManager = exports.IdentityManager = exports.RUNTIME_ERRORS = exports.RuntimeFactory = exports.createMCPIRuntime = exports.MCPIRuntime = exports.MCPINodeRuntimeWrapper = void 0;
10
+ exports.getToolProtection = exports.isToolProtected = exports.toolProtectionRegistry = void 0;
11
11
  // Main runtime - Node.js implementation with providers
12
12
  var mcpi_runtime_wrapper_1 = require("./mcpi-runtime-wrapper");
13
13
  Object.defineProperty(exports, "MCPINodeRuntimeWrapper", { enumerable: true, get: function () { return mcpi_runtime_wrapper_1.MCPINodeRuntimeWrapper; } });
@@ -75,6 +75,7 @@ var proof_batch_queue_1 = require("./proof-batch-queue");
75
75
  Object.defineProperty(exports, "ProofBatchQueue", { enumerable: true, get: function () { return proof_batch_queue_1.ProofBatchQueue; } });
76
76
  Object.defineProperty(exports, "KTAProofDestination", { enumerable: true, get: function () { return proof_batch_queue_1.KTAProofDestination; } });
77
77
  Object.defineProperty(exports, "AgentShieldProofDestination", { enumerable: true, get: function () { return proof_batch_queue_1.AgentShieldProofDestination; } });
78
+ Object.defineProperty(exports, "PartialProofSubmissionError", { enumerable: true, get: function () { return proof_batch_queue_1.PartialProofSubmissionError; } });
78
79
  Object.defineProperty(exports, "createProofBatchQueue", { enumerable: true, get: function () { return proof_batch_queue_1.createProofBatchQueue; } });
79
80
  var delegation_verifier_kv_1 = require("./delegation-verifier-kv");
80
81
  Object.defineProperty(exports, "CloudflareKVDelegationVerifier", { enumerable: true, get: function () { return delegation_verifier_kv_1.CloudflareKVDelegationVerifier; } });
@@ -17,24 +17,61 @@
17
17
  * Related: PHASE_1_XMCP_I_SERVER.md Epic 3 (Proof Batching)
18
18
  */
19
19
  import type { DetachedProof } from "@kya-os/mcp/types" with { "resolution-mode": "import" };
20
+ /**
21
+ * A proof plus the tool-call context KTA's submission API requires
22
+ * alongside it. DetachedProof (proof.meta) carries did/kid/ts/nonce/
23
+ * audience/etc, but not which tool was called or how it turned out —
24
+ * that context only exists at the call site, so it has to be threaded
25
+ * through explicitly rather than derived from the proof.
26
+ */
27
+ export interface ProofSubmission {
28
+ proof: DetachedProof;
29
+ toolName: string;
30
+ outcome: "success" | "failure" | "denied";
31
+ }
20
32
  /**
21
33
  * Proof submission destination
22
34
  */
23
35
  export interface ProofDestination {
24
36
  /** Destination name (for logging) */
25
37
  name: string;
26
- /** Submit batch of proofs */
27
- submit(proofs: DetachedProof[]): Promise<void>;
38
+ /** Submit a batch of proof submissions */
39
+ submit(submissions: ProofSubmission[]): Promise<void>;
40
+ }
41
+ /**
42
+ * Thrown by a ProofDestination whose submit() made independent per-item
43
+ * requests (rather than one atomic request for the whole array) when only
44
+ * some of them failed. Callers that retry on failure MUST retry only
45
+ * `failed`, not the original full array — retrying everything would
46
+ * resubmit proofs the destination already accepted.
47
+ */
48
+ export declare class PartialProofSubmissionError extends Error {
49
+ readonly failed: ProofSubmission[];
50
+ readonly succeededCount: number;
51
+ /** The per-item rejection reason for each entry in `failed`, same order. */
52
+ readonly causes?: unknown[] | undefined;
53
+ constructor(message: string, failed: ProofSubmission[], succeededCount: number,
54
+ /** The per-item rejection reason for each entry in `failed`, same order. */
55
+ causes?: unknown[] | undefined);
28
56
  }
29
57
  /**
30
58
  * KTA proof submission destination
59
+ *
60
+ * The live KTA API (POST /api/v1/proofs) accepts exactly one proof per
61
+ * request — there is no /batch endpoint — so a "batch" here means firing
62
+ * one request per submission, not one request for the whole array. Since
63
+ * each request is independent, submit() reports PARTIAL failure via
64
+ * PartialProofSubmissionError rather than treating the whole array as
65
+ * atomic — a caller that blindly retried everything on any rejection
66
+ * would resubmit proofs that already succeeded.
31
67
  */
32
68
  export declare class KTAProofDestination implements ProofDestination {
33
69
  name: string;
34
70
  private apiUrl;
35
71
  private apiKey?;
36
72
  constructor(apiUrl: string, apiKey?: string);
37
- submit(proofs: DetachedProof[]): Promise<void>;
73
+ submit(submissions: ProofSubmission[]): Promise<void>;
74
+ private submitOne;
38
75
  }
39
76
  /**
40
77
  * AgentShield proof submission destination
@@ -47,7 +84,7 @@ export declare class AgentShieldProofDestination implements ProofDestination {
47
84
  private apiUrl;
48
85
  private apiKey;
49
86
  constructor(apiUrl: string, apiKey: string);
50
- submit(proofs: DetachedProof[]): Promise<void>;
87
+ submit(submissions: ProofSubmission[]): Promise<void>;
51
88
  }
52
89
  /**
53
90
  * Proof batch queue configuration
@@ -79,9 +116,26 @@ export declare class ProofBatchQueue {
79
116
  private stats;
80
117
  constructor(config: ProofBatchQueueConfig);
81
118
  /**
82
- * Add proof to queue
119
+ * Add a proof submission to the queue.
120
+ *
121
+ * Accepts a bare DetachedProof for backward compatibility with the
122
+ * pre-#650 API: a plain-JS (or un-upgraded TS) consumer built against
123
+ * the old `enqueue(proof: DetachedProof)` signature would otherwise have
124
+ * `toolName`/`outcome` silently come through as `undefined`.
125
+ */
126
+ enqueue(item: ProofSubmission | DetachedProof): void;
127
+ /**
128
+ * Normalize a bare DetachedProof (the pre-#650 enqueue() shape) into a
129
+ * ProofSubmission. Derives toolName/outcome from the proof's own signed
130
+ * meta when present, rather than a hardcoded literal -- a destination's
131
+ * server-side check compares the submitted toolName/outcome against what
132
+ * was actually signed into the JWS payload, so a literal that disagrees
133
+ * with the real signed value is guaranteed to fail verification. When the
134
+ * proof was never signed with these fields at all (a genuinely pre-#651
135
+ * proof), no fallback can make the submission succeed, so this warns
136
+ * rather than silently queueing a doomed submission.
83
137
  */
84
- enqueue(proof: DetachedProof): void;
138
+ private submissionFromBareProof;
85
139
  /**
86
140
  * Flush queue immediately (submit all queued proofs)
87
141
  */
@@ -18,10 +18,40 @@
18
18
  * Related: PHASE_1_XMCP_I_SERVER.md Epic 3 (Proof Batching)
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.ProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = void 0;
21
+ exports.ProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = exports.PartialProofSubmissionError = void 0;
22
22
  exports.createProofBatchQueue = createProofBatchQueue;
23
+ /**
24
+ * Thrown by a ProofDestination whose submit() made independent per-item
25
+ * requests (rather than one atomic request for the whole array) when only
26
+ * some of them failed. Callers that retry on failure MUST retry only
27
+ * `failed`, not the original full array — retrying everything would
28
+ * resubmit proofs the destination already accepted.
29
+ */
30
+ class PartialProofSubmissionError extends Error {
31
+ failed;
32
+ succeededCount;
33
+ causes;
34
+ constructor(message, failed, succeededCount,
35
+ /** The per-item rejection reason for each entry in `failed`, same order. */
36
+ causes) {
37
+ super(message);
38
+ this.failed = failed;
39
+ this.succeededCount = succeededCount;
40
+ this.causes = causes;
41
+ this.name = "PartialProofSubmissionError";
42
+ }
43
+ }
44
+ exports.PartialProofSubmissionError = PartialProofSubmissionError;
23
45
  /**
24
46
  * KTA proof submission destination
47
+ *
48
+ * The live KTA API (POST /api/v1/proofs) accepts exactly one proof per
49
+ * request — there is no /batch endpoint — so a "batch" here means firing
50
+ * one request per submission, not one request for the whole array. Since
51
+ * each request is independent, submit() reports PARTIAL failure via
52
+ * PartialProofSubmissionError rather than treating the whole array as
53
+ * atomic — a caller that blindly retried everything on any rejection
54
+ * would resubmit proofs that already succeeded.
25
55
  */
26
56
  class KTAProofDestination {
27
57
  name = "KTA";
@@ -31,20 +61,61 @@ class KTAProofDestination {
31
61
  this.apiUrl = apiUrl.replace(/\/$/, "");
32
62
  this.apiKey = apiKey;
33
63
  }
34
- async submit(proofs) {
64
+ async submit(submissions) {
65
+ const results = await Promise.allSettled(submissions.map((submission) => this.submitOne(submission)));
66
+ const failures = results
67
+ .map((result, index) => ({ result, submission: submissions[index] }))
68
+ .filter((entry) => entry.result.status === "rejected");
69
+ if (failures.length > 0) {
70
+ const causes = failures.map((entry) => entry.result.reason);
71
+ const reasons = causes
72
+ .map((reason) => reason instanceof Error ? reason.message : String(reason))
73
+ .join("; ");
74
+ throw new PartialProofSubmissionError(`KTA proof submission failed for ${failures.length}/${submissions.length} proof(s): ${reasons}`, failures.map((entry) => entry.submission), submissions.length - failures.length, causes);
75
+ }
76
+ }
77
+ async submitOne(submission) {
78
+ const { proof, toolName, outcome } = submission;
35
79
  const headers = {
36
80
  "Content-Type": "application/json",
37
81
  };
38
82
  if (this.apiKey) {
39
- headers["X-API-Key"] = this.apiKey;
83
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
40
84
  }
41
- const response = await fetch(`${this.apiUrl}/api/v1/proofs/batch`, {
85
+ const response = await fetch(`${this.apiUrl}/api/v1/proofs`, {
42
86
  method: "POST",
43
87
  headers,
44
- body: JSON.stringify({ proofs }),
88
+ body: JSON.stringify({
89
+ proof,
90
+ toolName,
91
+ outcome,
92
+ // The live API wants these as top-level fields; both are already
93
+ // present on the proof itself (ts is Unix-epoch seconds, audience
94
+ // is documented server-side as the business DID), so derive them
95
+ // rather than requiring the caller to pass duplicates.
96
+ timestamp: proof.meta.ts,
97
+ businessDid: proof.meta.audience,
98
+ }),
45
99
  });
46
100
  if (!response.ok) {
47
- throw new Error(`KTA proof submission failed: ${response.status} ${response.statusText}`);
101
+ // The server overloads 409 for two distinct conditions: a duplicate
102
+ // proofJws (code CONFLICT) means this exact proof is already stored --
103
+ // almost always a client-side timeout after the server accepted a
104
+ // prior attempt, and is idempotent success. A replayed nonce (code
105
+ // NONCE_REUSED) is also a 409 but means the server REFUSED the proof;
106
+ // treating it as success would silently drop a real replay-protection
107
+ // signal. Distinguish via the response body's machine-readable code
108
+ // rather than assuming every 409 means "already stored".
109
+ let code;
110
+ if (response.status === 409) {
111
+ const body = await response.json().catch(() => ({}));
112
+ code = body.code;
113
+ if (code === "CONFLICT") {
114
+ return;
115
+ }
116
+ }
117
+ throw new Error(`KTA proof submission failed: ${response.status} ${response.statusText}` +
118
+ (code ? ` (${code})` : ""));
48
119
  }
49
120
  }
50
121
  }
@@ -63,10 +134,11 @@ class AgentShieldProofDestination {
63
134
  this.apiUrl = apiUrl.replace(/\/$/, "");
64
135
  this.apiKey = apiKey;
65
136
  }
66
- async submit(proofs) {
67
- if (proofs.length === 0) {
137
+ async submit(submissions) {
138
+ if (submissions.length === 0) {
68
139
  return;
69
140
  }
141
+ const proofs = submissions.map((submission) => submission.proof);
70
142
  // Extract session_id from first proof for AgentShield session grouping
71
143
  // AgentShield uses this for analytics and detection monitoring
72
144
  const sessionId = proofs[0]?.meta?.sessionId || "unknown";
@@ -127,14 +199,20 @@ class ProofBatchQueue {
127
199
  this.startRetryTimer();
128
200
  }
129
201
  /**
130
- * Add proof to queue
202
+ * Add a proof submission to the queue.
203
+ *
204
+ * Accepts a bare DetachedProof for backward compatibility with the
205
+ * pre-#650 API: a plain-JS (or un-upgraded TS) consumer built against
206
+ * the old `enqueue(proof: DetachedProof)` signature would otherwise have
207
+ * `toolName`/`outcome` silently come through as `undefined`.
131
208
  */
132
- enqueue(proof) {
209
+ enqueue(item) {
210
+ const submission = "proof" in item ? item : this.submissionFromBareProof(item);
133
211
  if (this.closed) {
134
212
  console.warn("[ProofBatchQueue] Queue is closed, dropping proof");
135
213
  return;
136
214
  }
137
- this.queue.push(proof);
215
+ this.queue.push(submission);
138
216
  this.stats.queued++;
139
217
  if (this.config.debug) {
140
218
  console.error(`[ProofBatchQueue] Enqueued proof (queue size: ${this.queue.length})`);
@@ -144,6 +222,48 @@ class ProofBatchQueue {
144
222
  this.flush();
145
223
  }
146
224
  }
225
+ /**
226
+ * Normalize a bare DetachedProof (the pre-#650 enqueue() shape) into a
227
+ * ProofSubmission. Derives toolName/outcome from the proof's own signed
228
+ * meta when present, rather than a hardcoded literal -- a destination's
229
+ * server-side check compares the submitted toolName/outcome against what
230
+ * was actually signed into the JWS payload, so a literal that disagrees
231
+ * with the real signed value is guaranteed to fail verification. When the
232
+ * proof was never signed with these fields at all (a genuinely pre-#651
233
+ * proof), no fallback can make the submission succeed, so this warns
234
+ * rather than silently queueing a doomed submission.
235
+ */
236
+ submissionFromBareProof(proof) {
237
+ // The published @kya-os/mcp package's DetachedProof/ProofMeta types
238
+ // predate the toolName/submissionOutcome fields added to
239
+ // @kya-os/contracts/proof's ProofMeta (#651) -- read them structurally
240
+ // with a runtime guard rather than waiting on that package to catch up.
241
+ const meta = proof.meta;
242
+ const toolName = meta !== null &&
243
+ typeof meta === "object" &&
244
+ "toolName" in meta &&
245
+ typeof meta.toolName === "string"
246
+ ? meta.toolName
247
+ : undefined;
248
+ const submissionOutcome = meta !== null &&
249
+ typeof meta === "object" &&
250
+ "submissionOutcome" in meta &&
251
+ (meta.submissionOutcome === "success" ||
252
+ meta.submissionOutcome === "failure" ||
253
+ meta.submissionOutcome === "denied")
254
+ ? meta.submissionOutcome
255
+ : undefined;
256
+ if (toolName === undefined || submissionOutcome === undefined) {
257
+ console.warn("[ProofBatchQueue] Enqueued a DetachedProof with no signed toolName/submissionOutcome -- " +
258
+ "the destination's server-side JWS payload check will reject this submission. " +
259
+ "Generate the proof with { toolName, submissionOutcome } options instead.");
260
+ }
261
+ return {
262
+ proof,
263
+ toolName: toolName ?? "unknown",
264
+ outcome: submissionOutcome ?? "success",
265
+ };
266
+ }
147
267
  /**
148
268
  * Flush queue immediately (submit all queued proofs)
149
269
  */
@@ -151,14 +271,14 @@ class ProofBatchQueue {
151
271
  if (this.queue.length === 0) {
152
272
  return;
153
273
  }
154
- const proofs = this.queue.splice(0, this.config.maxBatchSize);
274
+ const submissions = this.queue.splice(0, this.config.maxBatchSize);
155
275
  if (this.config.debug) {
156
- console.error(`[ProofBatchQueue] Flushing ${proofs.length} proofs to ${this.config.destinations.length} destinations`);
276
+ console.error(`[ProofBatchQueue] Flushing ${submissions.length} proofs to ${this.config.destinations.length} destinations`);
157
277
  }
158
278
  // Submit to each destination (fire-and-forget)
159
279
  for (const destination of this.config.destinations) {
160
280
  const batch = {
161
- proofs,
281
+ submissions,
162
282
  destination,
163
283
  retryCount: 0,
164
284
  };
@@ -170,29 +290,42 @@ class ProofBatchQueue {
170
290
  */
171
291
  async submitBatch(batch) {
172
292
  try {
173
- await batch.destination.submit(batch.proofs);
174
- this.stats.submitted += batch.proofs.length;
293
+ await batch.destination.submit(batch.submissions);
294
+ this.stats.submitted += batch.submissions.length;
175
295
  this.stats.batchesSubmitted++;
176
296
  if (this.config.debug) {
177
- console.error(`[ProofBatchQueue] Successfully submitted ${batch.proofs.length} proofs to ${batch.destination.name}`);
297
+ console.error(`[ProofBatchQueue] Successfully submitted ${batch.submissions.length} proofs to ${batch.destination.name}`);
178
298
  }
179
299
  }
180
300
  catch (error) {
181
301
  console.error(`[ProofBatchQueue] Failed to submit to ${batch.destination.name}:`, error);
302
+ // A destination that makes independent per-item requests (KTA) can
303
+ // fail partially. Only retry the submissions that actually failed —
304
+ // retrying the whole batch would resubmit ones already accepted,
305
+ // and would let one permanently-failing proof block good ones from
306
+ // ever being marked submitted.
307
+ const isPartialFailure = error instanceof PartialProofSubmissionError;
308
+ const submissionsToRetry = isPartialFailure
309
+ ? error.failed
310
+ : batch.submissions;
311
+ if (isPartialFailure && error.succeededCount > 0) {
312
+ this.stats.submitted += error.succeededCount;
313
+ }
182
314
  // Retry with exponential backoff
183
315
  if (batch.retryCount < this.config.maxRetries) {
184
316
  batch.retryCount++;
185
317
  const backoffMs = Math.min(1000 * Math.pow(2, batch.retryCount - 1), 16000);
186
318
  batch.nextRetryAt = Date.now() + backoffMs;
319
+ batch.submissions = submissionsToRetry;
187
320
  this.pendingBatches.push(batch);
188
321
  if (this.config.debug) {
189
- console.error(`[ProofBatchQueue] Scheduling retry ${batch.retryCount}/${this.config.maxRetries} in ${backoffMs}ms`);
322
+ console.error(`[ProofBatchQueue] Scheduling retry ${batch.retryCount}/${this.config.maxRetries} in ${backoffMs}ms for ${batch.submissions.length} proof(s)`);
190
323
  }
191
324
  }
192
325
  else {
193
- // Max retries exceeded, drop batch
194
- this.stats.failed += batch.proofs.length;
195
- console.error(`[ProofBatchQueue] Max retries exceeded for ${batch.destination.name}, dropping ${batch.proofs.length} proofs`);
326
+ // Max retries exceeded, drop the still-failing submissions
327
+ this.stats.failed += submissionsToRetry.length;
328
+ console.error(`[ProofBatchQueue] Max retries exceeded for ${batch.destination.name}, dropping ${submissionsToRetry.length} proofs`);
196
329
  }
197
330
  }
198
331
  }