@decidio/sdk 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/signing.d.ts CHANGED
@@ -3,6 +3,10 @@ export declare function canonicalize(v: unknown): string;
3
3
  /** Generate a fresh Ed25519 keypair LOCALLY. Returns the public `did:key` (register this)
4
4
  * and the PKCS8 private key base64 (keep this in the agent host's env as DECIDIO_AGENT_KEY —
5
5
  * it never goes back to Decidio). */
6
+ /** Derive the public did:key from a stored private key. Lets `doctor` prove that the key on
7
+ * this machine is actually the one registered for this agent — a mismatched restore otherwise
8
+ * looks healthy until a signed confirmation quietly fails to earn its tier weeks later. */
9
+ export declare function didFromPrivateKey(privateKeyBase64: string): string;
6
10
  export declare function generateKeypair(): {
7
11
  did: string;
8
12
  privateKeyBase64: string;
@@ -64,3 +68,55 @@ export declare function assertJsonSafe(value: unknown, path?: string): void;
64
68
  * session tokens while agent-token requests sailed through). localhost/127.0.0.1/[::1] stay
65
69
  * allowed for local dev; https always allowed. Throws before anything is sent. */
66
70
  export declare function assertSafeTransport(url: string, hasCredential: boolean): void;
71
+ /** A signed, DOMAIN-SEPARATED, versioned attestation that THIS agent reported THIS captured
72
+ * response for THIS decision. Distinct from the identity proof (`signConfirm`) in every field
73
+ * that matters, so a report can never be lifted from one decision onto another: the decision id,
74
+ * the workspace, the agent's did, the source, the response digest, the digest algorithm, the
75
+ * canonicalization version, a nonce and an expiry are ALL inside the signed bytes.
76
+ *
77
+ * HONEST SCOPE — say this out loud rather than let a reader assume more: this proves integrity
78
+ * and agent attribution of the REPORT. It does not prove the downstream system committed
79
+ * anything; only the source system can attest that, and the gate's evidence tier is what
80
+ * records how strong that attestation is. */
81
+ export interface ExecutionReport {
82
+ v: 1;
83
+ purpose: "decidio.execution-report";
84
+ decisionId: string;
85
+ agentId: string;
86
+ did: string;
87
+ workspaceId?: string;
88
+ source: string;
89
+ resultDigest: string;
90
+ digestAlg: "sha-256";
91
+ canonicalization: "decidio-jcs-1";
92
+ nonce: string;
93
+ exp: number;
94
+ proof: {
95
+ protected: string;
96
+ signature: string;
97
+ };
98
+ }
99
+ export declare function signExecutionReport(privateKeyBase64: string, fields: {
100
+ decisionId: string;
101
+ agentId: string;
102
+ did: string;
103
+ workspaceId?: string;
104
+ source: string;
105
+ resultDigest: string;
106
+ ttlMs?: number;
107
+ now?: number;
108
+ nonce?: string;
109
+ }): ExecutionReport;
110
+ /** Verify an ExecutionReport LOCALLY — same rules the server applies before persisting it.
111
+ * Returns why it failed rather than a bare false, so a caller can say something useful. */
112
+ export declare function verifyExecutionReport(report: unknown, expect: {
113
+ decisionId: string;
114
+ did?: string;
115
+ now?: number;
116
+ }): {
117
+ valid: boolean;
118
+ reason?: string;
119
+ };
120
+ /** sha-256 over the canonical JSON of an already-canonical value — the digest an ExecutionReport
121
+ * binds. Both languages produce identical bytes for identical values. */
122
+ export declare function resultDigest(canonicalValue: unknown): string;
package/dist/signing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateKeyPairSync, createPrivateKey, sign as edSign, randomUUID } from "node:crypto";
1
+ import { generateKeyPairSync, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, createHash, randomUUID } from "node:crypto";
2
2
  const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
3
3
  function b58encode(buf) {
4
4
  const digits = [0];
@@ -33,6 +33,11 @@ export function canonicalize(v) {
33
33
  const o = v;
34
34
  return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
35
35
  }
36
+ export function didFromPrivateKey(privateKeyBase64) {
37
+ const priv = createPrivateKey({ key: Buffer.from(privateKeyBase64, "base64"), format: "der", type: "pkcs8" });
38
+ const jwk = createPublicKey(priv).export({ format: "jwk" });
39
+ return "did:key:z" + b58encode(Buffer.concat([Buffer.from([0xed, 0x01]), Buffer.from(jwk.x, "base64url")]));
40
+ }
36
41
  export function generateKeypair() {
37
42
  const { privateKey, publicKey } = generateKeyPairSync("ed25519");
38
43
  const jwk = publicKey.export({ format: "jwk" });
@@ -112,3 +117,81 @@ export function assertSafeTransport(url, hasCredential) {
112
117
  throw new Error(`[decidio] refusing to send credentials over plaintext http to ${url.replace(/^(https?:\/\/[^/]+).*$/i, "$1")} — use https, or localhost for local dev. Nothing was sent.`);
113
118
  }
114
119
  }
120
+ export function signExecutionReport(privateKeyBase64, fields) {
121
+ const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, "base64"), format: "der", type: "pkcs8" });
122
+ const nonce = fields.nonce ?? randomUUID();
123
+ const exp = (fields.now ?? Date.now()) + (fields.ttlMs ?? 300_000);
124
+ const body = {
125
+ v: 1,
126
+ purpose: "decidio.execution-report",
127
+ decisionId: fields.decisionId,
128
+ agentId: fields.agentId,
129
+ did: fields.did,
130
+ ...(fields.workspaceId !== undefined ? { workspaceId: fields.workspaceId } : {}),
131
+ source: fields.source,
132
+ resultDigest: fields.resultDigest,
133
+ digestAlg: "sha-256",
134
+ canonicalization: "decidio-jcs-1",
135
+ nonce,
136
+ exp,
137
+ };
138
+ const payload = Buffer.from(canonicalize(body), "utf8").toString("base64url");
139
+ const protectedHdr = Buffer.from(JSON.stringify({ alg: "EdDSA", kid: fields.did, typ: "decidio-execution-report+jws" }), "utf8").toString("base64url");
140
+ const signature = Buffer.from(edSign(null, Buffer.from(`${protectedHdr}.${payload}`, "utf8"), key)).toString("base64url");
141
+ return { ...body, proof: { protected: protectedHdr, signature } };
142
+ }
143
+ export function verifyExecutionReport(report, expect) {
144
+ const r = report;
145
+ if (!r || r.purpose !== "decidio.execution-report" || r.v !== 1)
146
+ return { valid: false, reason: "not a v1 decidio execution report" };
147
+ if (r.decisionId !== expect.decisionId)
148
+ return { valid: false, reason: "report is bound to a DIFFERENT decision" };
149
+ if (expect.did && r.did !== expect.did)
150
+ return { valid: false, reason: "report was signed by a different agent key" };
151
+ if (typeof r.exp !== "number" || (expect.now ?? Date.now()) > r.exp)
152
+ return { valid: false, reason: "report expired" };
153
+ if (r.digestAlg !== "sha-256" || r.canonicalization !== "decidio-jcs-1")
154
+ return { valid: false, reason: "unsupported digest/canonicalization" };
155
+ try {
156
+ const { proof, ...body } = r;
157
+ const payload = Buffer.from(canonicalize(body), "utf8").toString("base64url");
158
+ const mc = b58decode(String(r.did).slice("did:key:z".length));
159
+ if (mc[0] !== 0xed || mc[1] !== 0x01)
160
+ return { valid: false, reason: "issuer did is not Ed25519" };
161
+ const pub = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: Buffer.from(mc.subarray(2)).toString("base64url") }, format: "jwk" });
162
+ const ok = edVerify(null, Buffer.from(`${proof.protected}.${payload}`, "utf8"), pub, Buffer.from(proof.signature, "base64url"));
163
+ return ok ? { valid: true } : { valid: false, reason: "signature does not verify against the agent did" };
164
+ }
165
+ catch (e) {
166
+ return { valid: false, reason: `malformed report (${e?.message ?? e})` };
167
+ }
168
+ }
169
+ export function resultDigest(canonicalValue) {
170
+ return createHash("sha256").update(canonicalize(canonicalValue), "utf8").digest("hex");
171
+ }
172
+ function b58decode(s) {
173
+ const A = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
174
+ const bytes = [0];
175
+ for (const ch of s) {
176
+ const val = A.indexOf(ch);
177
+ if (val < 0)
178
+ throw new Error("bad base58 character");
179
+ let carry = val;
180
+ for (let i = 0; i < bytes.length; i++) {
181
+ carry += bytes[i] * 58;
182
+ bytes[i] = carry & 0xff;
183
+ carry >>= 8;
184
+ }
185
+ while (carry) {
186
+ bytes.push(carry & 0xff);
187
+ carry >>= 8;
188
+ }
189
+ }
190
+ for (const ch of s) {
191
+ if (ch === A[0])
192
+ bytes.push(0);
193
+ else
194
+ break;
195
+ }
196
+ return Buffer.from(bytes.reverse());
197
+ }
package/openapi.yaml CHANGED
@@ -5,7 +5,7 @@
5
5
  openapi: 3.1.0
6
6
  info:
7
7
  title: Decidio Agent Gate
8
- version: 0.1.10
8
+ version: 0.1.11
9
9
  description: >
10
10
  Authorize an AI-agent action (proceed | route | block), confirm the captured outcome, and
11
11
  receive the signed resume webhook when a routed action is decided. Every outcome seals a
@@ -101,6 +101,52 @@ components:
101
101
  requesterIdentity:
102
102
  $ref: "#/components/schemas/RequesterIdentity"
103
103
  description: confirm proof is bound to {action:"confirm", scope:decisionId}
104
+ executionReport:
105
+ $ref: "#/components/schemas/ExecutionReport"
106
+ ExecutionReport:
107
+ type: object
108
+ description: >
109
+ OPTIONAL proof of WHAT was executed: a domain-separated, versioned envelope over a digest
110
+ of the captured response, bound to the decision, the agent and the workspace. The server
111
+ VERIFIES it before persisting anything — it recomputes the digest from `capturedResponse`
112
+ on this same request, requires the signer to be the decision's original requesting agent
113
+ AND still registered and active, and burns the nonce so a report is single-use. A report
114
+ copied onto a different decision, a different agent, or an altered response is refused.
115
+
116
+ A REFUSED REPORT DOES NOT FAIL THE CONFIRM. This envelope grants no authority — the
117
+ evidence tier comes from the response, the identity from the confirm proof — so an
118
+ unverifiable attestation is refused as an ATTESTATION (never stored as evidence, and the
119
+ refusal is itself audited) while the confirmation is still recorded. The response reports
120
+ which happened via `executionReport.verified`; it is never silent. Omit the field entirely
121
+ and confirm behaves exactly as before.
122
+ required: [v, purpose, decisionId, agentId, did, source, resultDigest, digestAlg, canonicalization, nonce, exp, proof]
123
+ properties:
124
+ v: { type: integer, enum: [1] }
125
+ purpose: { type: string, enum: ["decidio.execution-report"], description: domain separation — this signature can never be replayed as another Decidio proof }
126
+ decisionId: { type: string }
127
+ agentId: { type: string }
128
+ did: { type: string, description: "did:key of the signing agent" }
129
+ workspaceId: { type: string, description: tenant binding; present when the agent knows it }
130
+ source: { type: string, enum: [wrapper, agent] }
131
+ resultDigest:
132
+ type: string
133
+ pattern: "^[0-9a-f]{64}$"
134
+ description: lowercase-hex SHA-256 over the canonicalized captured response (the server recomputes it and compares)
135
+ digestAlg: { type: string, enum: ["sha-256"] }
136
+ canonicalization: { type: string, enum: ["decidio-jcs-1"], description: RFC 8785 JCS with Decidio's refusal rules }
137
+ nonce:
138
+ type: string
139
+ pattern: "^[A-Za-z0-9._:-]{8,64}$"
140
+ description: single-use; burned server-side, so a replayed report is refused
141
+ exp:
142
+ type: integer
143
+ description: unix ms expiry. The server also refuses an expiry more than 15 minutes ahead of now — a client-chosen lifetime is not freshness.
144
+ proof:
145
+ type: object
146
+ required: [protected, signature]
147
+ properties:
148
+ protected: { type: string, description: "base64url JWS protected header (EdDSA, kid=did, typ=decidio-execution-report+jws)" }
149
+ signature: { type: string, description: base64url detached signature over protected.payload }
104
150
  ResumeWebhook:
105
151
  type: object
106
152
  description: >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decidio/sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -81,7 +81,7 @@
81
81
  "prepublishOnly": "npm run typecheck && npm run build"
82
82
  },
83
83
  "dependencies": {
84
- "@decidio/verify": "0.1.9"
84
+ "@decidio/verify": "0.1.10"
85
85
  },
86
86
  "author": "OmniTwin Technologies Inc.",
87
87
  "bugs": {