@dorigjo/besa 0.1.0-alpha.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dorigjo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,345 @@
1
+ # Besa
2
+
3
+ Signed trust infrastructure for AI-agent tools.
4
+
5
+ > **Alpha / developer preview — not production-ready.**
6
+ >
7
+ > Besa is currently an early alpha (`0.1.0-alpha.0`). APIs, file formats, receipt formats, and behavior may change without notice.
8
+ >
9
+ > Do not use Besa to protect production systems, production secrets, customer data, or real signing keys yet.
10
+ >
11
+ > The key under `.besa/` is a local demo key.
12
+
13
+ Besa signs MCP-style tool manifests, verifies them before use, admits or denies tool calls against policy, and issues signed tamper-evident receipts.
14
+
15
+ Besa is the trust layer for AI-agent tools.
16
+
17
+ ## What it does
18
+
19
+ * Signs tool manifests with Ed25519.
20
+ * Verifies signed manifests before runtime use.
21
+ * Allows or denies tool calls with reason codes.
22
+ * Blocks destructive high-risk tools by default.
23
+ * Tracks local per-tool usage with a mini ActionMeter.
24
+ * Creates signed receipts for admission decisions.
25
+
26
+ Flow:
27
+
28
+ ```text
29
+ manifest.yaml -> sign -> verify -> admit -> receipt
30
+ ```
31
+
32
+ ## Why it matters
33
+
34
+ AI agents increasingly call external tools, APIs, MCP servers, and internal systems.
35
+
36
+ The important question is not only whether an agent can call a tool.
37
+
38
+ The important questions are:
39
+
40
+ * Which tool is the agent allowed to call?
41
+ * Who signed the declared capability?
42
+ * Has the manifest changed?
43
+ * Was the call allowed or denied?
44
+ * Is there a receipt proving the decision?
45
+
46
+ Besa turns those answers into signed artifacts.
47
+
48
+ ## Quickstart
49
+
50
+ Install dependencies:
51
+
52
+ ```bash
53
+ npm install
54
+ ```
55
+
56
+ Build:
57
+
58
+ ```bash
59
+ npm run build
60
+ ```
61
+
62
+ Run tests:
63
+
64
+ ```bash
65
+ npm test
66
+ ```
67
+
68
+ Run the smoke test:
69
+
70
+ ```bash
71
+ npm run smoke
72
+ ```
73
+
74
+ The smoke test runs the full CLI flow: build, load, sign, verify, admit allow, admit deny, and receipt creation.
75
+
76
+ ## CLI commands
77
+
78
+ Load a manifest:
79
+
80
+ ```bash
81
+ node dist/index.js load examples/manifest.yaml
82
+ ```
83
+
84
+ Sign a manifest:
85
+
86
+ ```bash
87
+ node dist/index.js sign examples/manifest.yaml
88
+ ```
89
+
90
+ Verify a signed manifest:
91
+
92
+ ```bash
93
+ node dist/index.js verify examples/manifest.signed.json
94
+ ```
95
+
96
+ Admit a safe tool:
97
+
98
+ ```bash
99
+ node dist/index.js admit examples/manifest.signed.json crm.lookup
100
+ ```
101
+
102
+ Deny a dangerous tool:
103
+
104
+ ```bash
105
+ node dist/index.js admit examples/manifest.signed.json crm.delete
106
+ ```
107
+
108
+ Create a signed receipt:
109
+
110
+ ```bash
111
+ node dist/index.js receipt crm.lookup
112
+ ```
113
+
114
+ Expected behavior:
115
+
116
+ * `crm.lookup` -> allow / `ALLOWED`
117
+ * `crm.delete` -> deny / `RISK_BLOCKED`
118
+
119
+ ### Grant-aware admission (optional)
120
+
121
+ Besa can scope a tool call to a specific agent. Add a `grants.yaml` listing which `agentId` may use which tools:
122
+
123
+ ```
124
+ grants:
125
+ - agentId: agent-alpha
126
+ tools:
127
+ - crm.lookup
128
+ ```
129
+
130
+ Then pass `--agent` and `--grants` to `admit` or `receipt`:
131
+
132
+ ```
133
+ node dist/index.js admit examples/manifest.signed.json crm.lookup --agent agent-alpha --grants examples/grants.yaml
134
+ ```
135
+
136
+ - `--agent <id>`: the id of the calling agent.
137
+ - `--grants <file>`: the grants file to check against.
138
+ - If the agent is not granted the tool, admission is denied (`TOOL_NOT_GRANTED`, or `AGENT_NOT_FOUND` for an unknown agent), and the receipt records `agentId` and `grantReasonCode`.
139
+
140
+ Grants are **optional and backward-compatible**: without `--grants`, admission behaves exactly as before.
141
+
142
+ ## Core concepts
143
+
144
+ ### Tool Manifest
145
+
146
+ A YAML or JSON file that declares a tool server and its tools.
147
+
148
+ Each tool has:
149
+
150
+ * name
151
+ * description
152
+ * capability
153
+ * risk
154
+ * scopes
155
+ * budgetLimit
156
+ * inputSchema
157
+
158
+ Capabilities:
159
+
160
+ * read
161
+ * write
162
+ * destructive
163
+
164
+ Risk levels:
165
+
166
+ * low
167
+ * medium
168
+ * high
169
+
170
+ ### Signed Manifest
171
+
172
+ A manifest signed with Ed25519.
173
+
174
+ The signed manifest includes:
175
+
176
+ * manifest
177
+ * manifestHash
178
+ * algorithm
179
+ * publicKey
180
+ * publicKeyId
181
+ * signature
182
+ * signedAt
183
+
184
+ ### Admission Decision
185
+
186
+ Besa evaluates whether a tool call should be allowed or denied.
187
+
188
+ Reason codes include:
189
+
190
+ * `ALLOWED`
191
+ * `TOOL_NOT_FOUND`
192
+ * `RISK_BLOCKED`
193
+ * `BUDGET_EXCEEDED`
194
+
195
+ ### Mini ActionMeter
196
+
197
+ Besa tracks local call counts per tool.
198
+
199
+ This allows simple budget enforcement through `budgetLimit`.
200
+
201
+ ### Signed Receipt
202
+
203
+ A receipt proves what decision was made.
204
+
205
+ A receipt includes:
206
+
207
+ * receiptId
208
+ * manifestHash
209
+ * toolName
210
+ * decision
211
+ * reasonCode
212
+ * timestamp
213
+ * requestHash
214
+ * publicKeyId
215
+ * algorithm
216
+ * signature
217
+
218
+ ## SDK usage
219
+
220
+ Import Besa from the SDK:
221
+
222
+ ```ts
223
+ import {
224
+ loadManifest,
225
+ generateKeyPair,
226
+ signManifest,
227
+ verifySignedManifest,
228
+ admit,
229
+ createReceipt,
230
+ verifyReceipt,
231
+ } from "besa";
232
+ ```
233
+
234
+ Basic flow:
235
+
236
+ ```ts
237
+ const manifest = loadManifest("examples/manifest.yaml");
238
+
239
+ const keypair = generateKeyPair();
240
+
241
+ const signed = signManifest(manifest, keypair);
242
+
243
+ const verified = verifySignedManifest(signed);
244
+
245
+ if (!verified.valid) {
246
+ throw new Error(verified.reasonCode);
247
+ }
248
+
249
+ const decision = admit(signed, "crm.lookup");
250
+
251
+ const receipt = createReceipt(signed, decision, keypair);
252
+
253
+ const receiptResult = verifyReceipt(receipt);
254
+
255
+ if (!receiptResult.valid) {
256
+ throw new Error(receiptResult.reasonCode);
257
+ }
258
+ ```
259
+
260
+ ## Security
261
+
262
+ Never commit `.besa/`.
263
+
264
+ The `.besa/` folder contains local trust artifacts, including the Ed25519 private key.
265
+
266
+ Ignored local artifacts:
267
+
268
+ * `.besa/`
269
+ * `.besa/key.json`
270
+ * `.besa/meter.json`
271
+ * `.besa/receipts/`
272
+ * `examples/manifest.signed.json`
273
+
274
+ The local key generated by this MVP is a demo key. Rotate keys before real usage.
275
+
276
+ See:
277
+
278
+ * [SECURITY.md](SECURITY.md)
279
+ * [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md)
280
+
281
+ ## MVP limitations
282
+
283
+ This is an MVP and alpha developer preview.
284
+
285
+ Current limitations:
286
+
287
+ * local key storage only
288
+ * local JSON meter only
289
+ * no hosted registry
290
+ * no SaaS backend
291
+ * no dashboard
292
+ * no remote verifier API
293
+ * no hosted receipts API
294
+ * no distributed replay protection
295
+ * no key rotation
296
+ * no key revocation
297
+ * one default policy
298
+
299
+ Default policy:
300
+
301
+ * destructive + high risk = denied
302
+
303
+ ## What Besa is not
304
+
305
+ Besa is currently an alpha trust layer for AI-agent tool control and evidence.
306
+
307
+ It is not:
308
+
309
+ * a hosted SaaS
310
+ * a dashboard or UI
311
+ * a full MCP gateway
312
+ * production key management
313
+ * a compliance certification product
314
+ * a replacement for identity, authorization, audit storage, or security monitoring
315
+ * ready for production secrets or production systems
316
+
317
+ ## Release docs
318
+
319
+ * [SECURITY.md](SECURITY.md) — security policy, key handling, and vulnerability reporting
320
+ * [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) — assets, threats, mitigations, and current MVP limitations
321
+ * [docs/RELEASE_CHECKLIST.md](docs/RELEASE_CHECKLIST.md) — pre-release gates before tagging or publishing
322
+ * [CHANGELOG.md](CHANGELOG.md) — notable changes by version
323
+
324
+ ## Roadmap
325
+
326
+ Planned next layers:
327
+
328
+ * hosted key management
329
+ * remote verifier API
330
+ * policy packs
331
+ * MCP gateway integration
332
+ * enterprise audit export
333
+ * receipts API
334
+ * usage-based ActionMeter
335
+ * organization-level trust registry
336
+
337
+ ## Positioning
338
+
339
+ Besa is signed trust infrastructure for AI-agent tools.
340
+
341
+ It is not another chatbot.
342
+
343
+ It is not another dashboard.
344
+
345
+ It is a trust layer for agentic execution.
@@ -0,0 +1,18 @@
1
+ import type { AdmissionDecision, Manifest, ToolDefinition } from "./types.js";
2
+ export declare const REASON: {
3
+ readonly ALLOWED: "ALLOWED";
4
+ readonly TOOL_NOT_FOUND: "TOOL_NOT_FOUND";
5
+ readonly RISK_BLOCKED: "RISK_BLOCKED";
6
+ readonly BUDGET_EXCEEDED: "BUDGET_EXCEEDED";
7
+ };
8
+ export interface AdmissionPolicy {
9
+ denyDestructiveHighRisk: boolean;
10
+ }
11
+ export declare const DEFAULT_POLICY: AdmissionPolicy;
12
+ export type MeterState = Record<string, number>;
13
+ export declare function findTool(manifest: Manifest, toolName: string): ToolDefinition | undefined;
14
+ export declare function admit(manifest: Manifest, toolName: string, callCount: number, policy?: AdmissionPolicy): AdmissionDecision;
15
+ export declare function loadMeter(path: string): MeterState;
16
+ export declare function saveMeter(path: string, state: MeterState): void;
17
+ export declare function getCount(state: MeterState, toolName: string): number;
18
+ export declare function increment(state: MeterState, toolName: string): MeterState;
package/dist/admit.js ADDED
@@ -0,0 +1,76 @@
1
+ import { dirname } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ export const REASON = {
4
+ ALLOWED: "ALLOWED",
5
+ TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
6
+ RISK_BLOCKED: "RISK_BLOCKED",
7
+ BUDGET_EXCEEDED: "BUDGET_EXCEEDED"
8
+ };
9
+ export const DEFAULT_POLICY = {
10
+ denyDestructiveHighRisk: true
11
+ };
12
+ export function findTool(manifest, toolName) {
13
+ return manifest.tools.find((tool) => tool.name === toolName);
14
+ }
15
+ export function admit(manifest, toolName, callCount, policy = DEFAULT_POLICY) {
16
+ const tool = findTool(manifest, toolName);
17
+ if (!tool) {
18
+ return deny(toolName, REASON.TOOL_NOT_FOUND, `tool '${toolName}' is not declared in the manifest`);
19
+ }
20
+ if (policy.denyDestructiveHighRisk &&
21
+ tool.capability === "destructive" &&
22
+ tool.risk === "high") {
23
+ return deny(toolName, REASON.RISK_BLOCKED, "destructive high-risk tool is blocked by policy");
24
+ }
25
+ if (callCount >= tool.budgetLimit) {
26
+ return deny(toolName, REASON.BUDGET_EXCEEDED, `call count ${callCount} has reached budget limit ${tool.budgetLimit}`);
27
+ }
28
+ return {
29
+ decision: "allow",
30
+ reasonCode: REASON.ALLOWED,
31
+ toolName,
32
+ detail: "tool call admitted"
33
+ };
34
+ }
35
+ function deny(toolName, reasonCode, detail) {
36
+ return {
37
+ decision: "deny",
38
+ reasonCode,
39
+ toolName,
40
+ detail
41
+ };
42
+ }
43
+ export function loadMeter(path) {
44
+ if (!existsSync(path)) {
45
+ return {};
46
+ }
47
+ try {
48
+ const raw = JSON.parse(readFileSync(path, "utf8"));
49
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
50
+ return {};
51
+ }
52
+ const state = {};
53
+ for (const [key, value] of Object.entries(raw)) {
54
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
55
+ state[key] = value;
56
+ }
57
+ }
58
+ return state;
59
+ }
60
+ catch {
61
+ return {};
62
+ }
63
+ }
64
+ export function saveMeter(path, state) {
65
+ mkdirSync(dirname(path), { recursive: true });
66
+ writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
67
+ }
68
+ export function getCount(state, toolName) {
69
+ return state[toolName] ?? 0;
70
+ }
71
+ export function increment(state, toolName) {
72
+ return {
73
+ ...state,
74
+ [toolName]: getCount(state, toolName) + 1
75
+ };
76
+ }
@@ -0,0 +1,12 @@
1
+ import { type KeyObject } from "node:crypto";
2
+ export interface KeyPair {
3
+ publicKeyDer: string;
4
+ privateKeyDer: string;
5
+ }
6
+ export declare function canonicalize(value: unknown): string;
7
+ export declare function sha256Hex(data: string): string;
8
+ export declare function hashObject(value: unknown): string;
9
+ export declare function generateKeyPair(): KeyPair;
10
+ export declare function publicKeyFromDer(publicKeyDer: string): KeyObject;
11
+ export declare function privateKeyFromDer(privateKeyDer: string): KeyObject;
12
+ export declare function publicKeyId(publicKeyDer: string): string;
package/dist/crypto.js ADDED
@@ -0,0 +1,48 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
2
+ function sortValue(value) {
3
+ if (Array.isArray(value)) {
4
+ return value.map(sortValue);
5
+ }
6
+ if (value !== null && typeof value === "object") {
7
+ const input = value;
8
+ const output = {};
9
+ for (const key of Object.keys(input).sort()) {
10
+ output[key] = sortValue(input[key]);
11
+ }
12
+ return output;
13
+ }
14
+ return value;
15
+ }
16
+ export function canonicalize(value) {
17
+ return JSON.stringify(sortValue(value));
18
+ }
19
+ export function sha256Hex(data) {
20
+ return createHash("sha256").update(data, "utf8").digest("hex");
21
+ }
22
+ export function hashObject(value) {
23
+ return sha256Hex(canonicalize(value));
24
+ }
25
+ export function generateKeyPair() {
26
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
27
+ return {
28
+ publicKeyDer: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
29
+ privateKeyDer: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64")
30
+ };
31
+ }
32
+ export function publicKeyFromDer(publicKeyDer) {
33
+ return createPublicKey({
34
+ key: Buffer.from(publicKeyDer, "base64"),
35
+ type: "spki",
36
+ format: "der"
37
+ });
38
+ }
39
+ export function privateKeyFromDer(privateKeyDer) {
40
+ return createPrivateKey({
41
+ key: Buffer.from(privateKeyDer, "base64"),
42
+ type: "pkcs8",
43
+ format: "der"
44
+ });
45
+ }
46
+ export function publicKeyId(publicKeyDer) {
47
+ return sha256Hex(publicKeyDer).slice(0, 16);
48
+ }
@@ -0,0 +1,15 @@
1
+ import type { Grant, GrantDecision, GrantSet } from "./types.js";
2
+ export declare const GRANT_REASON: {
3
+ readonly GRANTED: "GRANT_OK";
4
+ readonly AGENT_NOT_FOUND: "AGENT_NOT_FOUND";
5
+ readonly TOOL_NOT_GRANTED: "TOOL_NOT_GRANTED";
6
+ };
7
+ export interface GrantValidationResult {
8
+ ok: boolean;
9
+ grantSet?: GrantSet;
10
+ errors: string[];
11
+ }
12
+ export declare function validateGrantSet(raw: unknown): GrantValidationResult;
13
+ export declare function loadGrants(path: string): GrantSet;
14
+ export declare function findGrant(grantSet: GrantSet, agentId: string): Grant | undefined;
15
+ export declare function checkGrant(grantSet: GrantSet, agentId: string, toolName: string): GrantDecision;
package/dist/grant.js ADDED
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { extname } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ export const GRANT_REASON = {
5
+ GRANTED: "GRANT_OK",
6
+ AGENT_NOT_FOUND: "AGENT_NOT_FOUND",
7
+ TOOL_NOT_GRANTED: "TOOL_NOT_GRANTED",
8
+ };
9
+ function isObject(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+ function isNonEmptyString(value) {
13
+ return typeof value === "string" && value.trim().length > 0;
14
+ }
15
+ export function validateGrantSet(raw) {
16
+ const errors = [];
17
+ if (!isObject(raw)) {
18
+ return {
19
+ ok: false,
20
+ errors: ["grant set must be an object"],
21
+ };
22
+ }
23
+ if (!Array.isArray(raw.grants) || raw.grants.length === 0) {
24
+ errors.push("grants must be a non-empty array");
25
+ }
26
+ else {
27
+ const seenAgents = new Set();
28
+ raw.grants.forEach((grant, index) => {
29
+ const path = `grants[${index}]`;
30
+ if (!isObject(grant)) {
31
+ errors.push(`${path} must be an object`);
32
+ return;
33
+ }
34
+ if (!isNonEmptyString(grant.agentId)) {
35
+ errors.push(`${path}.agentId must be a non-empty string`);
36
+ }
37
+ else if (seenAgents.has(grant.agentId)) {
38
+ errors.push(`${path}.agentId must be unique`);
39
+ }
40
+ else {
41
+ seenAgents.add(grant.agentId);
42
+ }
43
+ if (!Array.isArray(grant.tools) ||
44
+ grant.tools.length === 0 ||
45
+ !grant.tools.every((tool) => isNonEmptyString(tool))) {
46
+ errors.push(`${path}.tools must be a non-empty array of non-empty strings`);
47
+ }
48
+ });
49
+ }
50
+ if (errors.length > 0) {
51
+ return {
52
+ ok: false,
53
+ errors,
54
+ };
55
+ }
56
+ return {
57
+ ok: true,
58
+ grantSet: raw,
59
+ errors: [],
60
+ };
61
+ }
62
+ export function loadGrants(path) {
63
+ const source = readFileSync(path, "utf8");
64
+ const raw = extname(path).toLowerCase() === ".json" ? JSON.parse(source) : parseYaml(source);
65
+ const result = validateGrantSet(raw);
66
+ if (!result.ok || !result.grantSet) {
67
+ throw new Error(`Invalid grant set:\n - ${result.errors.join("\n - ")}`);
68
+ }
69
+ return result.grantSet;
70
+ }
71
+ export function findGrant(grantSet, agentId) {
72
+ return grantSet.grants.find((grant) => grant.agentId === agentId);
73
+ }
74
+ export function checkGrant(grantSet, agentId, toolName) {
75
+ const grant = findGrant(grantSet, agentId);
76
+ if (!grant) {
77
+ return {
78
+ granted: false,
79
+ reasonCode: GRANT_REASON.AGENT_NOT_FOUND,
80
+ agentId,
81
+ toolName,
82
+ detail: `no grant found for agent '${agentId}'`,
83
+ };
84
+ }
85
+ if (!grant.tools.includes(toolName)) {
86
+ return {
87
+ granted: false,
88
+ reasonCode: GRANT_REASON.TOOL_NOT_GRANTED,
89
+ agentId,
90
+ toolName,
91
+ detail: `agent '${agentId}' is not granted tool '${toolName}'`,
92
+ };
93
+ }
94
+ return {
95
+ granted: true,
96
+ reasonCode: GRANT_REASON.GRANTED,
97
+ agentId,
98
+ toolName,
99
+ detail: `agent '${agentId}' is granted tool '${toolName}'`,
100
+ };
101
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import type { AdmissionDecision } from "./types.js";
3
+ export declare function grantGate(toolName: string): AdmissionDecision | undefined;
package/dist/index.js ADDED
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { generateKeyPair } from "./crypto.js";
5
+ import { loadManifest } from "./manifest.js";
6
+ import { createReceipt, signManifest, verifySignedManifest } from "./signing.js";
7
+ import { admit, getCount, increment, loadMeter, saveMeter } from "./admit.js";
8
+ import { checkGrant, loadGrants } from "./grant.js";
9
+ const BESA_DIR = ".besa";
10
+ const KEY_PATH = join(BESA_DIR, "key.json");
11
+ const METER_PATH = join(BESA_DIR, "meter.json");
12
+ const ACTIVE_MANIFEST_PATH = join(BESA_DIR, "active-manifest.json");
13
+ const RECEIPTS_DIR = join(BESA_DIR, "receipts");
14
+ function readJson(path) {
15
+ return JSON.parse(readFileSync(path, "utf8"));
16
+ }
17
+ function writeJson(path, value) {
18
+ writeFileSync(path, JSON.stringify(value, null, 2) + "\n", "utf8");
19
+ }
20
+ function ensureBesaDir() {
21
+ mkdirSync(BESA_DIR, { recursive: true });
22
+ }
23
+ function loadOrCreateKeyPair() {
24
+ ensureBesaDir();
25
+ if (existsSync(KEY_PATH)) {
26
+ return readJson(KEY_PATH);
27
+ }
28
+ const keypair = generateKeyPair();
29
+ writeJson(KEY_PATH, keypair);
30
+ return keypair;
31
+ }
32
+ function printJson(label, value) {
33
+ console.log("");
34
+ console.log(label + ":");
35
+ console.log(JSON.stringify(value, null, 2));
36
+ }
37
+ function signedOutPath(manifestPath) {
38
+ if (manifestPath.endsWith(".yaml")) {
39
+ return manifestPath.slice(0, -5) + ".signed.json";
40
+ }
41
+ if (manifestPath.endsWith(".yml")) {
42
+ return manifestPath.slice(0, -4) + ".signed.json";
43
+ }
44
+ if (manifestPath.endsWith(".json")) {
45
+ return manifestPath.slice(0, -5) + ".signed.json";
46
+ }
47
+ return manifestPath + ".signed.json";
48
+ }
49
+ function flagValue(name) {
50
+ const index = process.argv.indexOf(name);
51
+ return index >= 0 ? process.argv[index + 1] : undefined;
52
+ }
53
+ function positionals(args) {
54
+ const values = [];
55
+ const flagsWithValues = new Set(["--agent", "--grants"]);
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const value = args[index];
58
+ if (value && flagsWithValues.has(value)) {
59
+ index += 1;
60
+ continue;
61
+ }
62
+ if (value) {
63
+ values.push(value);
64
+ }
65
+ }
66
+ return values;
67
+ }
68
+ function cmdKeys() {
69
+ const keypair = loadOrCreateKeyPair();
70
+ printJson("keypair", {
71
+ publicKeyDer: keypair.publicKeyDer,
72
+ privateKeyDerPath: KEY_PATH,
73
+ });
74
+ console.log("");
75
+ console.log("OK: keypair ready at " + KEY_PATH);
76
+ }
77
+ function cmdLoad(file) {
78
+ const manifest = loadManifest(file);
79
+ printJson("manifest", manifest);
80
+ console.log("");
81
+ console.log("OK: loaded " + String(manifest.tools.length) + " tool(s) from " + file);
82
+ }
83
+ function cmdSign(file) {
84
+ const manifest = loadManifest(file);
85
+ const keypair = loadOrCreateKeyPair();
86
+ const signed = signManifest(manifest, keypair);
87
+ const out = signedOutPath(file);
88
+ writeJson(out, signed);
89
+ ensureBesaDir();
90
+ writeJson(ACTIVE_MANIFEST_PATH, signed);
91
+ printJson("signedManifest", signed);
92
+ console.log("");
93
+ console.log("OK: signed -> " + out + " with publicKeyId " + signed.publicKeyId);
94
+ }
95
+ function cmdVerify(file) {
96
+ const signed = readJson(file);
97
+ const result = verifySignedManifest(signed);
98
+ printJson("verify", result);
99
+ if (!result.valid) {
100
+ process.exitCode = 1;
101
+ console.log("");
102
+ console.log("DENY: " + result.reasonCode);
103
+ return;
104
+ }
105
+ console.log("");
106
+ console.log("OK: " + result.detail);
107
+ }
108
+ function denyFromVerification(toolName, reasonCode, detail) {
109
+ return {
110
+ decision: "deny",
111
+ reasonCode,
112
+ toolName,
113
+ detail,
114
+ };
115
+ }
116
+ export function grantGate(toolName) {
117
+ const grantsPath = flagValue("--grants");
118
+ if (!grantsPath) {
119
+ return undefined;
120
+ }
121
+ const agentId = flagValue("--agent") ?? "";
122
+ const grant = checkGrant(loadGrants(grantsPath), agentId, toolName);
123
+ return {
124
+ decision: grant.granted ? "allow" : "deny",
125
+ reasonCode: grant.reasonCode,
126
+ toolName,
127
+ detail: grant.detail,
128
+ agentId,
129
+ };
130
+ }
131
+ function cmdAdmit(file, toolName) {
132
+ const signed = readJson(file);
133
+ const verified = verifySignedManifest(signed);
134
+ if (!verified.valid) {
135
+ const denied = denyFromVerification(toolName, verified.reasonCode, verified.detail);
136
+ printJson("admission", denied);
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+ const grantDecision = grantGate(toolName);
141
+ if (grantDecision && grantDecision.decision === "deny") {
142
+ printJson("admission", grantDecision);
143
+ process.exitCode = 1;
144
+ return;
145
+ }
146
+ const meter = loadMeter(METER_PATH);
147
+ const count = getCount(meter, toolName);
148
+ const decision = admit(signed.manifest, toolName, count);
149
+ if (grantDecision?.agentId) {
150
+ decision.agentId = grantDecision.agentId;
151
+ }
152
+ printJson("admission", decision);
153
+ if (decision.decision === "deny") {
154
+ process.exitCode = 1;
155
+ }
156
+ }
157
+ function cmdReceipt(toolName, file) {
158
+ const signedPath = file ?? ACTIVE_MANIFEST_PATH;
159
+ if (!existsSync(signedPath)) {
160
+ throw new Error("no signed manifest found at " + signedPath + "; run besa sign <manifest> first");
161
+ }
162
+ const signed = readJson(signedPath);
163
+ const keypair = loadOrCreateKeyPair();
164
+ const verified = verifySignedManifest(signed);
165
+ let decision;
166
+ let grantReasonCode;
167
+ if (!verified.valid) {
168
+ decision = denyFromVerification(toolName, verified.reasonCode, verified.detail);
169
+ }
170
+ else {
171
+ const grantDecision = grantGate(toolName);
172
+ grantReasonCode = grantDecision?.reasonCode;
173
+ if (grantDecision && grantDecision.decision === "deny") {
174
+ decision = grantDecision;
175
+ }
176
+ else {
177
+ const meter = loadMeter(METER_PATH);
178
+ decision = admit(signed.manifest, toolName, getCount(meter, toolName));
179
+ if (grantDecision?.agentId) {
180
+ decision.agentId = grantDecision.agentId;
181
+ }
182
+ if (decision.decision === "allow") {
183
+ saveMeter(METER_PATH, increment(meter, toolName));
184
+ }
185
+ }
186
+ }
187
+ const receipt = createReceipt({
188
+ manifestHash: signed.manifestHash,
189
+ toolName,
190
+ decision: decision.decision,
191
+ reasonCode: decision.reasonCode,
192
+ request: {
193
+ toolName,
194
+ signedManifest: signedPath,
195
+ },
196
+ agentId: decision.agentId,
197
+ grantReasonCode,
198
+ }, keypair);
199
+ mkdirSync(RECEIPTS_DIR, { recursive: true });
200
+ const receiptPath = join(RECEIPTS_DIR, receipt.receiptId + ".json");
201
+ writeJson(receiptPath, receipt);
202
+ printJson("receipt", receipt);
203
+ console.log("");
204
+ console.log(decision.decision.toUpperCase() + ": " + decision.reasonCode + " -> " + receiptPath);
205
+ if (decision.decision === "deny") {
206
+ process.exitCode = 1;
207
+ }
208
+ }
209
+ function usage() {
210
+ console.log([
211
+ "Besa - signed trust infrastructure for AI-agent tools",
212
+ "",
213
+ "Usage:",
214
+ " besa keys",
215
+ " besa load <manifest.yaml>",
216
+ " besa sign <manifest.yaml>",
217
+ " besa verify <manifest.signed.json>",
218
+ " besa admit <manifest.signed.json> <tool-name> [--agent <agent-id> --grants <grants.yaml>]",
219
+ " besa receipt <tool-name> [manifest.signed.json] [--agent <agent-id> --grants <grants.yaml>]",
220
+ "",
221
+ "Examples:",
222
+ " besa keys",
223
+ " besa load examples/manifest.yaml",
224
+ " besa sign examples/manifest.yaml",
225
+ " besa verify examples/manifest.signed.json",
226
+ " besa admit examples/manifest.signed.json crm.lookup",
227
+ " besa admit examples/manifest.signed.json crm.lookup --agent agent-alpha --grants examples/grants.yaml",
228
+ " besa admit examples/manifest.signed.json crm.delete --agent agent-alpha --grants examples/grants.yaml",
229
+ " besa receipt crm.lookup examples/manifest.signed.json",
230
+ " besa receipt crm.lookup examples/manifest.signed.json --agent agent-alpha --grants examples/grants.yaml",
231
+ ].join("\n"));
232
+ }
233
+ function requireArgs(args, expected, command) {
234
+ if (args.length < expected) {
235
+ throw new Error(command + " requires " + String(expected) + " argument(s)");
236
+ }
237
+ }
238
+ function main(argv) {
239
+ const command = argv[0] ?? "";
240
+ const args = positionals(argv.slice(1));
241
+ try {
242
+ switch (command) {
243
+ case "keys":
244
+ cmdKeys();
245
+ break;
246
+ case "load":
247
+ requireArgs(args, 1, command);
248
+ cmdLoad(args[0] ?? "");
249
+ break;
250
+ case "sign":
251
+ requireArgs(args, 1, command);
252
+ cmdSign(args[0] ?? "");
253
+ break;
254
+ case "verify":
255
+ requireArgs(args, 1, command);
256
+ cmdVerify(args[0] ?? "");
257
+ break;
258
+ case "admit":
259
+ requireArgs(args, 2, command);
260
+ cmdAdmit(args[0] ?? "", args[1] ?? "");
261
+ break;
262
+ case "receipt":
263
+ requireArgs(args, 1, command);
264
+ cmdReceipt(args[0] ?? "", args[1]);
265
+ break;
266
+ case "":
267
+ case "help":
268
+ case "--help":
269
+ case "-h":
270
+ usage();
271
+ break;
272
+ default:
273
+ console.error("Unknown command: " + command);
274
+ usage();
275
+ process.exitCode = 1;
276
+ break;
277
+ }
278
+ }
279
+ catch (error) {
280
+ const message = error instanceof Error ? error.message : String(error);
281
+ console.error("Error: " + message);
282
+ process.exitCode = 1;
283
+ }
284
+ }
285
+ main(process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ import type { Manifest } from "./types.js";
2
+ export interface ValidationResult {
3
+ ok: boolean;
4
+ manifest?: Manifest;
5
+ errors: string[];
6
+ }
7
+ export declare function validateManifest(raw: unknown): ValidationResult;
8
+ export declare function loadManifest(path: string): Manifest;
@@ -0,0 +1,106 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { extname } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ const CAPABILITIES = ["read", "write", "destructive"];
5
+ const RISKS = ["low", "medium", "high"];
6
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7
+ function isObject(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ function isNonEmptyString(value) {
11
+ return typeof value === "string" && value.trim().length > 0;
12
+ }
13
+ function isHttpUrl(value) {
14
+ if (typeof value !== "string")
15
+ return false;
16
+ try {
17
+ const url = new URL(value);
18
+ return url.protocol === "http:" || url.protocol === "https:";
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function isIsoDate(value) {
25
+ return (typeof value === "string" &&
26
+ ISO_DATE.test(value) &&
27
+ !Number.isNaN(Date.parse(value)));
28
+ }
29
+ export function validateManifest(raw) {
30
+ const errors = [];
31
+ if (!isObject(raw)) {
32
+ return { ok: false, errors: ["manifest must be an object"] };
33
+ }
34
+ if (!isNonEmptyString(raw.serverName)) {
35
+ errors.push("serverName must be a non-empty string");
36
+ }
37
+ if (!isNonEmptyString(raw.serverVersion)) {
38
+ errors.push("serverVersion must be a non-empty string");
39
+ }
40
+ if (!isHttpUrl(raw.serverUrl)) {
41
+ errors.push("serverUrl must be a valid http(s) URL");
42
+ }
43
+ if (!isIsoDate(raw.createdAt)) {
44
+ errors.push("createdAt must be a valid ISO-8601 date-time");
45
+ }
46
+ if (!Array.isArray(raw.tools) || raw.tools.length === 0) {
47
+ errors.push("tools must be a non-empty array");
48
+ }
49
+ else {
50
+ const seen = new Set();
51
+ raw.tools.forEach((tool, index) => {
52
+ validateTool(tool, index, errors);
53
+ if (isObject(tool) && typeof tool.name === "string") {
54
+ if (seen.has(tool.name)) {
55
+ errors.push(`tools[${index}].name '${tool.name}' is a duplicate tool name`);
56
+ }
57
+ seen.add(tool.name);
58
+ }
59
+ });
60
+ }
61
+ if (errors.length > 0) {
62
+ return { ok: false, errors };
63
+ }
64
+ return { ok: true, manifest: raw, errors: [] };
65
+ }
66
+ function validateTool(tool, index, errors) {
67
+ const path = `tools[${index}]`;
68
+ if (!isObject(tool)) {
69
+ errors.push(`${path} must be an object`);
70
+ return;
71
+ }
72
+ if (!isNonEmptyString(tool.name)) {
73
+ errors.push(`${path}.name must be a non-empty string`);
74
+ }
75
+ if (typeof tool.description !== "string") {
76
+ errors.push(`${path}.description must be a string`);
77
+ }
78
+ if (!CAPABILITIES.includes(tool.capability)) {
79
+ errors.push(`${path}.capability must be one of ${CAPABILITIES.join(", ")}`);
80
+ }
81
+ if (!RISKS.includes(tool.risk)) {
82
+ errors.push(`${path}.risk must be one of ${RISKS.join(", ")}`);
83
+ }
84
+ if (!Array.isArray(tool.scopes) ||
85
+ tool.scopes.length === 0 ||
86
+ !tool.scopes.every((scope) => isNonEmptyString(scope))) {
87
+ errors.push(`${path}.scopes must be a non-empty array of non-empty strings`);
88
+ }
89
+ if (typeof tool.budgetLimit !== "number" ||
90
+ !Number.isSafeInteger(tool.budgetLimit) ||
91
+ tool.budgetLimit < 0) {
92
+ errors.push(`${path}.budgetLimit must be a safe non-negative integer`);
93
+ }
94
+ if (!isObject(tool.inputSchema)) {
95
+ errors.push(`${path}.inputSchema must be an object`);
96
+ }
97
+ }
98
+ export function loadManifest(path) {
99
+ const source = readFileSync(path, "utf8");
100
+ const raw = extname(path) === ".json" ? JSON.parse(source) : parseYaml(source);
101
+ const result = validateManifest(raw);
102
+ if (!result.ok || !result.manifest) {
103
+ throw new Error(`Invalid manifest:\n - ${result.errors.join("\n - ")}`);
104
+ }
105
+ return result.manifest;
106
+ }
package/dist/sdk.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./crypto.js";
3
+ export * from "./manifest.js";
4
+ export * from "./signing.js";
5
+ export * from "./admit.js";
6
+ export * from "./grant.js";
package/dist/sdk.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./crypto.js";
3
+ export * from "./manifest.js";
4
+ export * from "./signing.js";
5
+ export * from "./admit.js";
6
+ export * from "./grant.js";
@@ -0,0 +1,21 @@
1
+ import type { Decision, Manifest, Receipt, SignedManifest } from "./types.js";
2
+ import { type KeyPair } from "./crypto.js";
3
+ export interface VerifyResult {
4
+ valid: boolean;
5
+ reasonCode: string;
6
+ detail: string;
7
+ }
8
+ export interface ReceiptInput {
9
+ manifestHash: string;
10
+ toolName: string;
11
+ decision: Decision;
12
+ reasonCode: string;
13
+ request: unknown;
14
+ agentId?: string;
15
+ grantReasonCode?: string;
16
+ }
17
+ export declare function hashManifest(manifest: Manifest): string;
18
+ export declare function signManifest(manifest: Manifest, keypair: KeyPair): SignedManifest;
19
+ export declare function verifySignedManifest(signed: SignedManifest): VerifyResult;
20
+ export declare function createReceipt(input: ReceiptInput, keypair: KeyPair): Receipt;
21
+ export declare function verifyReceipt(receipt: Receipt, publicKeyDer: string): boolean;
@@ -0,0 +1,100 @@
1
+ import { randomUUID, sign as ed25519Sign, verify as ed25519Verify } from "node:crypto";
2
+ import { canonicalize, hashObject, privateKeyFromDer, publicKeyFromDer, publicKeyId } from "./crypto.js";
3
+ export function hashManifest(manifest) {
4
+ return hashObject(manifest);
5
+ }
6
+ export function signManifest(manifest, keypair) {
7
+ const canonical = canonicalize(manifest);
8
+ const signature = ed25519Sign(null, Buffer.from(canonical, "utf8"), privateKeyFromDer(keypair.privateKeyDer));
9
+ return {
10
+ manifest,
11
+ manifestHash: hashManifest(manifest),
12
+ algorithm: "ed25519",
13
+ publicKey: keypair.publicKeyDer,
14
+ publicKeyId: publicKeyId(keypair.publicKeyDer),
15
+ signature: signature.toString("base64"),
16
+ signedAt: new Date().toISOString()
17
+ };
18
+ }
19
+ export function verifySignedManifest(signed) {
20
+ if (signed.algorithm !== "ed25519") {
21
+ return {
22
+ valid: false,
23
+ reasonCode: "E_ALGORITHM_UNSUPPORTED",
24
+ detail: "only ed25519 signed manifests are supported"
25
+ };
26
+ }
27
+ const canonical = canonicalize(signed.manifest);
28
+ const expectedHash = hashManifest(signed.manifest);
29
+ if (expectedHash !== signed.manifestHash) {
30
+ return {
31
+ valid: false,
32
+ reasonCode: "E_MANIFEST_HASH_MISMATCH",
33
+ detail: "manifest content does not match stored hash"
34
+ };
35
+ }
36
+ if (publicKeyId(signed.publicKey) !== signed.publicKeyId) {
37
+ return {
38
+ valid: false,
39
+ reasonCode: "E_PUBLIC_KEY_ID_MISMATCH",
40
+ detail: "publicKeyId does not match publicKey"
41
+ };
42
+ }
43
+ try {
44
+ const valid = ed25519Verify(null, Buffer.from(canonical, "utf8"), publicKeyFromDer(signed.publicKey), Buffer.from(signed.signature, "base64"));
45
+ if (!valid) {
46
+ return {
47
+ valid: false,
48
+ reasonCode: "E_SIGNATURE_INVALID",
49
+ detail: "signature does not verify against the public key"
50
+ };
51
+ }
52
+ return {
53
+ valid: true,
54
+ reasonCode: "OK",
55
+ detail: "manifest signature is valid"
56
+ };
57
+ }
58
+ catch {
59
+ return {
60
+ valid: false,
61
+ reasonCode: "E_SIGNATURE_CHECK_FAILED",
62
+ detail: "signature verification failed"
63
+ };
64
+ }
65
+ }
66
+ export function createReceipt(input, keypair) {
67
+ const body = {
68
+ receiptId: "rcpt_" + randomUUID(),
69
+ manifestHash: input.manifestHash,
70
+ toolName: input.toolName,
71
+ decision: input.decision,
72
+ reasonCode: input.reasonCode,
73
+ timestamp: new Date().toISOString(),
74
+ requestHash: hashObject(input.request ?? {}),
75
+ agentId: input.agentId,
76
+ grantReasonCode: input.grantReasonCode,
77
+ publicKeyId: publicKeyId(keypair.publicKeyDer),
78
+ algorithm: "ed25519"
79
+ };
80
+ const signature = ed25519Sign(null, Buffer.from(canonicalize(body), "utf8"), privateKeyFromDer(keypair.privateKeyDer));
81
+ return {
82
+ ...body,
83
+ signature: signature.toString("base64")
84
+ };
85
+ }
86
+ export function verifyReceipt(receipt, publicKeyDer) {
87
+ if (receipt.algorithm !== "ed25519") {
88
+ return false;
89
+ }
90
+ if (publicKeyId(publicKeyDer) !== receipt.publicKeyId) {
91
+ return false;
92
+ }
93
+ const { signature, ...body } = receipt;
94
+ try {
95
+ return ed25519Verify(null, Buffer.from(canonicalize(body), "utf8"), publicKeyFromDer(publicKeyDer), Buffer.from(signature, "base64"));
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
@@ -0,0 +1,63 @@
1
+ export type CapabilityType = "read" | "write" | "destructive";
2
+ export type RiskLevel = "low" | "medium" | "high";
3
+ export type Decision = "allow" | "deny";
4
+ export interface ToolDefinition {
5
+ name: string;
6
+ description: string;
7
+ capability: CapabilityType;
8
+ risk: RiskLevel;
9
+ scopes: string[];
10
+ budgetLimit: number;
11
+ inputSchema: Record<string, unknown>;
12
+ }
13
+ export interface Manifest {
14
+ serverName: string;
15
+ serverVersion: string;
16
+ serverUrl: string;
17
+ createdAt: string;
18
+ tools: ToolDefinition[];
19
+ }
20
+ export interface SignedManifest {
21
+ manifest: Manifest;
22
+ manifestHash: string;
23
+ algorithm: "ed25519";
24
+ publicKey: string;
25
+ publicKeyId: string;
26
+ signature: string;
27
+ signedAt: string;
28
+ }
29
+ export interface AdmissionDecision {
30
+ decision: Decision;
31
+ reasonCode: string;
32
+ toolName: string;
33
+ detail: string;
34
+ agentId?: string;
35
+ }
36
+ export interface Receipt {
37
+ receiptId: string;
38
+ manifestHash: string;
39
+ toolName: string;
40
+ decision: Decision;
41
+ reasonCode: string;
42
+ timestamp: string;
43
+ requestHash: string;
44
+ publicKeyId: string;
45
+ algorithm: "ed25519";
46
+ agentId?: string;
47
+ grantReasonCode?: string;
48
+ signature: string;
49
+ }
50
+ export interface Grant {
51
+ agentId: string;
52
+ tools: string[];
53
+ }
54
+ export interface GrantSet {
55
+ grants: Grant[];
56
+ }
57
+ export interface GrantDecision {
58
+ granted: boolean;
59
+ reasonCode: string;
60
+ agentId: string;
61
+ toolName: string;
62
+ detail: string;
63
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ grants:
2
+ - agentId: agent-alpha
3
+ tools:
4
+ - crm.lookup
@@ -0,0 +1,34 @@
1
+ serverName: acme-crm
2
+ serverVersion: 1.0.0
3
+ serverUrl: https://tools.acme.example/mcp
4
+ createdAt: 2026-06-12T00:00:00Z
5
+ tools:
6
+ - name: crm.lookup
7
+ description: Look up a customer record by id or email.
8
+ capability: read
9
+ risk: low
10
+ scopes:
11
+ - crm:read
12
+ budgetLimit: 100
13
+ inputSchema:
14
+ type: object
15
+ properties:
16
+ query:
17
+ type: string
18
+ required:
19
+ - query
20
+ - name: crm.delete
21
+ description: Permanently delete a customer record.
22
+ capability: destructive
23
+ risk: high
24
+ scopes:
25
+ - crm:write
26
+ - crm:admin
27
+ budgetLimit: 5
28
+ inputSchema:
29
+ type: object
30
+ properties:
31
+ id:
32
+ type: string
33
+ required:
34
+ - id
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@dorigjo/besa",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Signed trust infrastructure for AI-agent tools.",
5
+ "type": "module",
6
+ "main": "./dist/sdk.js",
7
+ "types": "./dist/sdk.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/sdk.d.ts",
11
+ "default": "./dist/sdk.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "besa": "./dist/index.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "npm run build && node --test dist/tests/*.test.js",
20
+ "smoke": "node scripts/smoke.mjs"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "ai-agents",
25
+ "trust",
26
+ "ed25519",
27
+ "signing",
28
+ "manifest",
29
+ "receipt"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/dorigjo/besa.git"
34
+ },
35
+ "author": "dorigjo",
36
+ "license": "MIT",
37
+ "dependencies": {
38
+ "yaml": "^2.5.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.0.0",
42
+ "typescript": "^5.6.2"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "files": [
48
+ "dist/*.js",
49
+ "dist/*.d.ts",
50
+ "examples/manifest.yaml",
51
+ "examples/grants.yaml"
52
+ ],
53
+ "bugs": {
54
+ "url": "https://github.com/dorigjo/besa/issues"
55
+ },
56
+ "homepage": "https://github.com/dorigjo/besa#readme"
57
+ }