@decidio/sdk 0.2.2 → 0.3.3

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
@@ -1,4 +1,11 @@
1
- /** Sorted-key JCS — must match apps/api/src/canonical.ts byte-for-byte. */
1
+ /** Sorted-key JCS — must match apps/api/src/canonical.ts byte-for-byte.
2
+ *
3
+ * ONE IMPLEMENTATION (F-006). This was a module-local copy that reached `JSON.stringify` for every
4
+ * primitive, so it applied NONE of the strict rules the exported canonicalizer enforces — most
5
+ * sharply, it emitted an escaped lone surrogate instead of refusing it. The bytes a bound proof
6
+ * SIGNS and the bytes the digest BINDS are the two places that must never disagree with the value
7
+ * model, and they were the two places that did. Python already delegated (`signing.canonicalize` →
8
+ * `canonical_json`); this is the twin-parity repair on the TypeScript side. */
2
9
  export declare function canonicalize(v: unknown): string;
3
10
  /** Generate a fresh Ed25519 keypair LOCALLY. Returns the public `did:key` (register this)
4
11
  * and the PKCS8 private key base64 (keep this in the agent host's env as DECIDIO_AGENT_KEY —
package/dist/signing.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { generateKeyPairSync, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, createHash, randomUUID } from "node:crypto";
2
+ import { canonicalJson } from "./canonical.js";
2
3
  const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
3
4
  function b58encode(buf) {
4
5
  const digits = [0];
@@ -26,12 +27,7 @@ function b58encode(buf) {
26
27
  return str;
27
28
  }
28
29
  export function canonicalize(v) {
29
- if (v === null || typeof v !== "object")
30
- return JSON.stringify(v);
31
- if (Array.isArray(v))
32
- return `[${v.map(canonicalize).join(",")}]`;
33
- const o = v;
34
- return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
30
+ return canonicalJson(v, "canonicalize(...)");
35
31
  }
36
32
  export function didFromPrivateKey(privateKeyBase64) {
37
33
  const priv = createPrivateKey({ key: Buffer.from(privateKeyBase64, "base64"), format: "der", type: "pkcs8" });
@@ -111,20 +107,6 @@ export function assertJsonSafe(value, path = "args") {
111
107
  walk(value, path);
112
108
  }
113
109
  export function assertSafeTransport(url, hasCredential) {
114
- if (!hasCredential) {
115
- let probe = null;
116
- try {
117
- probe = new URL(url);
118
- }
119
- catch {
120
- probe = null;
121
- }
122
- if (probe && (probe.username || probe.password)) {
123
- throw new Error(`[decidio] the gate URL carries credentials in the URL itself (${probe.protocol}//user:***@${probe.host}). Decidio authenticates with a bearer token, so this is either a mistake or a secret you did not mean to put on the wire — and it is sent on every request regardless of whether an api token is configured. Remove the user:password from the URL. Nothing was sent.`);
124
- }
125
- }
126
- if (!hasCredential)
127
- return;
128
110
  let u;
129
111
  try {
130
112
  u = new URL(url);
@@ -132,9 +114,9 @@ export function assertSafeTransport(url, hasCredential) {
132
114
  catch {
133
115
  const spelled = url.replace(/[\t\r\n]/g, "").replace(/^[\s\u0000-\u001f\u0085\ufeff]+/, "");
134
116
  if (/^http:/i.test(spelled)) {
135
- throw new Error(`[decidio] refusing to send credentials over plaintext http to ${JSON.stringify(url)} — that is not a complete absolute URL, so where the request would actually connect is undefined (the path is appended to it, which can turn it into a remote host). Set a full https URL, or http://localhost for local dev. Nothing was sent.`);
117
+ throw new Error(`[decidio] refusing to ${hasCredential ? "send credentials" : "send a request"} over plaintext http to ${JSON.stringify(url)} — that is not a complete absolute URL, so where the request would actually connect is undefined (the path is appended to it, which can turn it into a remote host). Set a full https URL, or http://localhost for local dev. Nothing was sent.`);
136
118
  }
137
- return;
119
+ throw new Error(`[decidio] the gate URL ${JSON.stringify(url)} is not an absolute URL, so there is no host to send to — where a request would actually connect depends entirely on whichever transport resolves it. Decidio needs a full https URL (or http://localhost for local dev). Nothing was sent.`);
138
120
  }
139
121
  if (u.protocol !== "http:" && u.protocol !== "https:") {
140
122
  throw new Error(`[decidio] the gate URL uses the ${u.protocol.replace(":", "")} scheme, which is not a Decidio endpoint — use https (or http://localhost for local dev). Nothing was sent.`);
@@ -142,13 +124,26 @@ export function assertSafeTransport(url, hasCredential) {
142
124
  if (u.username || u.password) {
143
125
  throw new Error(`[decidio] the gate URL carries credentials in the URL itself (${u.protocol}//user:***@${u.host}). Decidio authenticates with a bearer token; remove the user:password from the URL. Nothing was sent.`);
144
126
  }
127
+ if (u.hostname === "") {
128
+ throw new Error(`[decidio] the gate URL ${JSON.stringify(url)} has no host — there is nowhere for the request to go. Decidio needs a full https URL (or http://localhost for local dev). Nothing was sent.`);
129
+ }
130
+ {
131
+ const spelled = url.replace(/[\t\r\n]/g, "").replace(/^[\s\u0000-\u001f\u0085\ufeff]+/, "").toLowerCase();
132
+ const origin = `${u.protocol}//${u.host}`.toLowerCase();
133
+ const next = spelled.charAt(origin.length);
134
+ if (!spelled.startsWith(origin) || (next !== "" && next !== "/" && next !== "?" && next !== "#" && next !== ":")) {
135
+ throw new Error(`[decidio] the gate URL ${JSON.stringify(url)} is not written in canonical form — this parser reads it as ${origin}, but the raw spelling is what a transport receives, and a parser that normalizes differently can read it as another host entirely. Write it as ${origin}. Nothing was sent.`);
136
+ }
137
+ }
145
138
  if (u.protocol !== "http:")
146
139
  return;
147
140
  const host = u.hostname.toLowerCase();
148
141
  const local = host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
149
142
  if (local)
150
143
  return;
151
- throw new Error(`[decidio] refusing to send credentials over plaintext http to ${u.origin} — use https, or localhost for local dev. Nothing was sent.`);
144
+ throw new Error(hasCredential
145
+ ? `[decidio] refusing to send credentials over plaintext http to ${u.origin} — use https, or localhost for local dev. Nothing was sent.`
146
+ : `[decidio] refusing to talk to ${u.origin} over plaintext http — Decidio is https, and an unauthenticated plaintext reply cannot be trusted to carry an authority decision. Use https, or localhost for local dev. Nothing was sent.`);
152
147
  }
153
148
  export const carriesCredential = (c) => !!c.apiToken || !!(c.agentKey && c.agentDid);
154
149
  export function signExecutionReport(privateKeyBase64, fields) {
package/openapi.yaml CHANGED
@@ -260,6 +260,13 @@ paths:
260
260
  content:
261
261
  application/json:
262
262
  schema: { $ref: "#/components/schemas/StatusResponse" }
263
+ "400":
264
+ description: >
265
+ `INVALID_DECISION_ID` - the id is not a usable decision id (empty, whitespace-padded,
266
+ control characters, a path, or over 128 characters). Deliberately NOT the 404 below: a
267
+ malformed id is not a decision that might exist somewhere else, and answering "unknown
268
+ decision" would send you looking for something that could never have been issued. The
269
+ check runs BEFORE any lookup, so this response reveals nothing about existence.
263
270
  "401": { description: missing/invalid agent token }
264
271
  "404": { description: unknown decision }
265
272
  /agent/confirm:
@@ -272,6 +279,14 @@ paths:
272
279
  evidence. The caller does not choose it and this endpoint does not always grant the top
273
280
  one. The credential sealed at decision time is immutable; evidence accrues on the record,
274
281
  never inside the sealed bytes.
282
+
283
+
284
+ A SUPPLIED-BUT-REFUSED executionReport CAPS THE TIER at `execution_unconfirmed`, whatever
285
+ else the report would have earned — and deliberately BELOW `agent_asserted`. A failed
286
+ attestation is weaker evidence than never claiming one; the opposite ordering would reward
287
+ sending a forged report over sending none. The server verifies the report and derives the
288
+ tier on the SAME path, so a refused attestation can no longer land on a receipt beside
289
+ `application_confirmed`.
275
290
  requestBody:
276
291
  required: true
277
292
  content:
@@ -287,6 +302,29 @@ paths:
287
302
  application/json:
288
303
  schema: { $ref: "#/components/schemas/ConfirmResponse" }
289
304
  "401": { description: missing/invalid agent token }
305
+ "404": { description: unknown decision, or one this agent is not bound to }
306
+ "409":
307
+ description: >
308
+ The gate refuses to attach execution evidence to this decision AT ALL. This is not a
309
+ report failure, and a client must not treat it as one — handle it separately from a
310
+ 5xx. `code` is either:
311
+
312
+ `TERMINALLY_DENIED` — a human or a policy DENIED this decision. If your action ran, it
313
+ ran against a refusal: do NOT re-run it, and investigate how it executed without an
314
+ approval. A denial is terminal and cannot be reconciled or retried.
315
+
316
+ `NOT_AUTHORIZED` — this is not an approved, sealed decision, so there is nothing for
317
+ evidence to attach to yet. If you are confirming early, wait for the approval. If your
318
+ action already ran, it ran WITHOUT authority.
319
+
320
+ Not retryable in either case: the same request will be refused the same way.
321
+ content:
322
+ application/json:
323
+ schema:
324
+ type: object
325
+ properties:
326
+ code: { type: string, enum: [TERMINALLY_DENIED, NOT_AUTHORIZED] }
327
+ message: { type: string }
290
328
  x-webhooks:
291
329
  resume:
292
330
  post:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decidio/sdk",
3
- "version": "0.2.2",
3
+ "version": "0.3.3",
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",
@@ -55,11 +55,6 @@
55
55
  "verifiable-credentials"
56
56
  ],
57
57
  "homepage": "https://decidioai.com/developers/",
58
- "repository": {
59
- "type": "git",
60
- "url": "git+https://github.com/OmniTwin/Decidio.git",
61
- "directory": "packages/decidio-guard"
62
- },
63
58
  "publishConfig": {
64
59
  "access": "public"
65
60
  },
@@ -84,10 +79,10 @@
84
79
  "scripts": {
85
80
  "typecheck": "tsc --noEmit",
86
81
  "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json",
87
- "prepublishOnly": "npm run typecheck && npm run build"
82
+ "prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
88
83
  },
89
84
  "dependencies": {
90
- "@decidio/verify": "0.2.2"
85
+ "@decidio/verify": "0.3.3"
91
86
  },
92
87
  "author": "OmniTwin Technologies Inc.",
93
88
  "bugs": {