@nobulex/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/express.d.ts +322 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +356 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +15 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/langchain.d.ts +183 -0
- package/dist/adapters/langchain.d.ts.map +1 -0
- package/dist/adapters/langchain.js +203 -0
- package/dist/adapters/langchain.js.map +1 -0
- package/dist/adapters/vercel-ai.d.ts +122 -0
- package/dist/adapters/vercel-ai.d.ts.map +1 -0
- package/dist/adapters/vercel-ai.js +128 -0
- package/dist/adapters/vercel-ai.js.map +1 -0
- package/dist/benchmarks.d.ts +164 -0
- package/dist/benchmarks.d.ts.map +1 -0
- package/dist/benchmarks.js +327 -0
- package/dist/benchmarks.js.map +1 -0
- package/dist/benchmarks.test.d.ts +2 -0
- package/dist/benchmarks.test.d.ts.map +1 -0
- package/dist/benchmarks.test.js +71 -0
- package/dist/benchmarks.test.js.map +1 -0
- package/dist/conformance.d.ts +160 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +1242 -0
- package/dist/conformance.js.map +1 -0
- package/dist/events.d.ts +176 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +208 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +695 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +986 -0
- package/dist/index.test.js.map +1 -0
- package/dist/middleware.d.ts +104 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +222 -0
- package/dist/middleware.js.map +1 -0
- package/dist/middleware.test.d.ts +5 -0
- package/dist/middleware.test.d.ts.map +1 -0
- package/dist/middleware.test.js +735 -0
- package/dist/middleware.test.js.map +1 -0
- package/dist/plugins/auth.d.ts +49 -0
- package/dist/plugins/auth.d.ts.map +1 -0
- package/dist/plugins/auth.js +82 -0
- package/dist/plugins/auth.js.map +1 -0
- package/dist/plugins/cache.d.ts +40 -0
- package/dist/plugins/cache.d.ts.map +1 -0
- package/dist/plugins/cache.js +191 -0
- package/dist/plugins/cache.js.map +1 -0
- package/dist/plugins/index.d.ts +16 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/metrics-plugin.d.ts +32 -0
- package/dist/plugins/metrics-plugin.d.ts.map +1 -0
- package/dist/plugins/metrics-plugin.js +61 -0
- package/dist/plugins/metrics-plugin.js.map +1 -0
- package/dist/plugins/plugins.test.d.ts +8 -0
- package/dist/plugins/plugins.test.d.ts.map +1 -0
- package/dist/plugins/plugins.test.js +640 -0
- package/dist/plugins/plugins.test.js.map +1 -0
- package/dist/plugins/retry-plugin.d.ts +55 -0
- package/dist/plugins/retry-plugin.d.ts.map +1 -0
- package/dist/plugins/retry-plugin.js +133 -0
- package/dist/plugins/retry-plugin.js.map +1 -0
- package/dist/telemetry.d.ts +183 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +241 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +200 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateKeyPair } from '@nobulex/crypto';
|
|
3
|
+
import { verifyCovenant as coreVerifyCovenant } from '@nobulex/core';
|
|
4
|
+
import { verifyIdentity } from '@nobulex/identity';
|
|
5
|
+
import { SteleClient, QuickCovenant,
|
|
6
|
+
// Re-exports: core
|
|
7
|
+
PROTOCOL_VERSION, MAX_CONSTRAINTS, MAX_CHAIN_DEPTH, MAX_DOCUMENT_SIZE, CovenantBuildError, CovenantVerificationError, MemoryChainResolver, canonicalForm, computeId, buildCovenant,
|
|
8
|
+
// Re-exports: crypto
|
|
9
|
+
sha256String, toHex, fromHex, generateId, timestamp,
|
|
10
|
+
// Re-exports: CCL
|
|
11
|
+
parseCCL, evaluateCCL, matchAction, matchResource, serializeCCL, mergeCCL,
|
|
12
|
+
// Re-exports: identity
|
|
13
|
+
DEFAULT_EVOLUTION_POLICY, getLineage, serializeIdentity, deserializeIdentity, } from './index';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
async function makeParties() {
|
|
18
|
+
const issuerKeyPair = await generateKeyPair();
|
|
19
|
+
const beneficiaryKeyPair = await generateKeyPair();
|
|
20
|
+
const issuer = {
|
|
21
|
+
id: 'issuer-1',
|
|
22
|
+
publicKey: issuerKeyPair.publicKeyHex,
|
|
23
|
+
role: 'issuer',
|
|
24
|
+
};
|
|
25
|
+
const beneficiary = {
|
|
26
|
+
id: 'beneficiary-1',
|
|
27
|
+
publicKey: beneficiaryKeyPair.publicKeyHex,
|
|
28
|
+
role: 'beneficiary',
|
|
29
|
+
};
|
|
30
|
+
return { issuerKeyPair, beneficiaryKeyPair, issuer, beneficiary };
|
|
31
|
+
}
|
|
32
|
+
function makeIdentityOptions(kp) {
|
|
33
|
+
return {
|
|
34
|
+
operatorKeyPair: kp,
|
|
35
|
+
model: {
|
|
36
|
+
provider: 'test-provider',
|
|
37
|
+
modelId: 'test-model',
|
|
38
|
+
modelVersion: '1.0',
|
|
39
|
+
},
|
|
40
|
+
capabilities: ['read', 'write', 'execute'],
|
|
41
|
+
deployment: {
|
|
42
|
+
runtime: 'process',
|
|
43
|
+
region: 'us-east-1',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Tests
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
describe('@nobulex/sdk', () => {
|
|
51
|
+
// ── SteleClient constructor ───────────────────────────────────────────
|
|
52
|
+
describe('SteleClient constructor', () => {
|
|
53
|
+
it('creates a client with default options', () => {
|
|
54
|
+
const client = new SteleClient();
|
|
55
|
+
expect(client.keyPair).toBeUndefined();
|
|
56
|
+
expect(client.agentId).toBeUndefined();
|
|
57
|
+
expect(client.strictMode).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('accepts a pre-existing key pair', async () => {
|
|
60
|
+
const kp = await generateKeyPair();
|
|
61
|
+
const client = new SteleClient({ keyPair: kp });
|
|
62
|
+
expect(client.keyPair).toBe(kp);
|
|
63
|
+
});
|
|
64
|
+
it('accepts an agentId', () => {
|
|
65
|
+
const client = new SteleClient({ agentId: 'agent-42' });
|
|
66
|
+
expect(client.agentId).toBe('agent-42');
|
|
67
|
+
});
|
|
68
|
+
it('accepts strictMode flag', () => {
|
|
69
|
+
const client = new SteleClient({ strictMode: true });
|
|
70
|
+
expect(client.strictMode).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('sets all options at once', async () => {
|
|
73
|
+
const kp = await generateKeyPair();
|
|
74
|
+
const client = new SteleClient({
|
|
75
|
+
keyPair: kp,
|
|
76
|
+
agentId: 'my-agent',
|
|
77
|
+
strictMode: true,
|
|
78
|
+
});
|
|
79
|
+
expect(client.keyPair).toBe(kp);
|
|
80
|
+
expect(client.agentId).toBe('my-agent');
|
|
81
|
+
expect(client.strictMode).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ── Key management ────────────────────────────────────────────────────
|
|
85
|
+
describe('SteleClient.generateKeyPair', () => {
|
|
86
|
+
it('generates a valid key pair', async () => {
|
|
87
|
+
const client = new SteleClient();
|
|
88
|
+
const kp = await client.generateKeyPair();
|
|
89
|
+
expect(kp.privateKey).toBeInstanceOf(Uint8Array);
|
|
90
|
+
expect(kp.publicKey).toBeInstanceOf(Uint8Array);
|
|
91
|
+
expect(kp.publicKeyHex).toBeTruthy();
|
|
92
|
+
expect(typeof kp.publicKeyHex).toBe('string');
|
|
93
|
+
});
|
|
94
|
+
it('sets the generated key pair on the client', async () => {
|
|
95
|
+
const client = new SteleClient();
|
|
96
|
+
expect(client.keyPair).toBeUndefined();
|
|
97
|
+
const kp = await client.generateKeyPair();
|
|
98
|
+
expect(client.keyPair).toBe(kp);
|
|
99
|
+
});
|
|
100
|
+
it('overwrites previous key pair when called again', async () => {
|
|
101
|
+
const client = new SteleClient();
|
|
102
|
+
const kp1 = await client.generateKeyPair();
|
|
103
|
+
const kp2 = await client.generateKeyPair();
|
|
104
|
+
expect(kp1.publicKeyHex).not.toBe(kp2.publicKeyHex);
|
|
105
|
+
expect(client.keyPair).toBe(kp2);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// ── Covenant creation ─────────────────────────────────────────────────
|
|
109
|
+
describe('SteleClient.createCovenant', () => {
|
|
110
|
+
it('creates a valid covenant document', async () => {
|
|
111
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
112
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
113
|
+
const doc = await client.createCovenant({
|
|
114
|
+
issuer,
|
|
115
|
+
beneficiary,
|
|
116
|
+
constraints: "permit read on 'data'",
|
|
117
|
+
});
|
|
118
|
+
expect(doc.id).toMatch(/^[0-9a-f]{64}$/);
|
|
119
|
+
expect(doc.version).toBe(PROTOCOL_VERSION);
|
|
120
|
+
expect(doc.issuer.id).toBe('issuer-1');
|
|
121
|
+
expect(doc.beneficiary.id).toBe('beneficiary-1');
|
|
122
|
+
expect(doc.signature).toMatch(/^[0-9a-f]{128}$/);
|
|
123
|
+
});
|
|
124
|
+
it('uses explicit privateKey over client keyPair', async () => {
|
|
125
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
126
|
+
const otherKp = await generateKeyPair();
|
|
127
|
+
const client = new SteleClient({ keyPair: otherKp });
|
|
128
|
+
const doc = await client.createCovenant({
|
|
129
|
+
issuer,
|
|
130
|
+
beneficiary,
|
|
131
|
+
constraints: "permit read on 'data'",
|
|
132
|
+
privateKey: issuerKeyPair.privateKey,
|
|
133
|
+
});
|
|
134
|
+
// Should verify because signature matches issuer's public key
|
|
135
|
+
const result = await coreVerifyCovenant(doc);
|
|
136
|
+
expect(result.valid).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('throws when no private key is available', async () => {
|
|
139
|
+
const { issuer, beneficiary } = await makeParties();
|
|
140
|
+
const client = new SteleClient();
|
|
141
|
+
await expect(client.createCovenant({
|
|
142
|
+
issuer,
|
|
143
|
+
beneficiary,
|
|
144
|
+
constraints: "permit read on 'data'",
|
|
145
|
+
})).rejects.toThrow('No private key available');
|
|
146
|
+
});
|
|
147
|
+
it('passes optional fields through to buildCovenant', async () => {
|
|
148
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
149
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
150
|
+
const doc = await client.createCovenant({
|
|
151
|
+
issuer,
|
|
152
|
+
beneficiary,
|
|
153
|
+
constraints: "permit read on 'data'",
|
|
154
|
+
metadata: { name: 'test-covenant', tags: ['sdk'] },
|
|
155
|
+
expiresAt: '2099-12-31T23:59:59.000Z',
|
|
156
|
+
enforcement: { type: 'monitor', config: {} },
|
|
157
|
+
});
|
|
158
|
+
expect(doc.metadata?.name).toBe('test-covenant');
|
|
159
|
+
expect(doc.expiresAt).toBe('2099-12-31T23:59:59.000Z');
|
|
160
|
+
expect(doc.enforcement?.type).toBe('monitor');
|
|
161
|
+
});
|
|
162
|
+
it('throws a helpful error for empty constraints', async () => {
|
|
163
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
164
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
165
|
+
await expect(client.createCovenant({
|
|
166
|
+
issuer,
|
|
167
|
+
beneficiary,
|
|
168
|
+
constraints: '',
|
|
169
|
+
})).rejects.toThrow('constraints must be a non-empty CCL string');
|
|
170
|
+
});
|
|
171
|
+
it('propagates CovenantBuildError for invalid CCL syntax', async () => {
|
|
172
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
173
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
174
|
+
await expect(client.createCovenant({
|
|
175
|
+
issuer,
|
|
176
|
+
beneficiary,
|
|
177
|
+
constraints: '!!! not valid CCL !!!',
|
|
178
|
+
})).rejects.toThrow(CovenantBuildError);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ── Covenant verification ─────────────────────────────────────────────
|
|
182
|
+
describe('SteleClient.verifyCovenant', () => {
|
|
183
|
+
it('returns valid for a well-formed covenant', async () => {
|
|
184
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
185
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
186
|
+
const doc = await client.createCovenant({
|
|
187
|
+
issuer,
|
|
188
|
+
beneficiary,
|
|
189
|
+
constraints: "permit read on 'data'",
|
|
190
|
+
});
|
|
191
|
+
const result = await client.verifyCovenant(doc);
|
|
192
|
+
expect(result.valid).toBe(true);
|
|
193
|
+
expect(result.checks.length).toBeGreaterThanOrEqual(11);
|
|
194
|
+
});
|
|
195
|
+
it('returns invalid for a tampered covenant', async () => {
|
|
196
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
197
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
198
|
+
const doc = await client.createCovenant({
|
|
199
|
+
issuer,
|
|
200
|
+
beneficiary,
|
|
201
|
+
constraints: "permit read on 'data'",
|
|
202
|
+
});
|
|
203
|
+
const tampered = { ...doc, signature: '00'.repeat(64) };
|
|
204
|
+
const result = await client.verifyCovenant(tampered);
|
|
205
|
+
expect(result.valid).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
it('throws in strict mode on verification failure', async () => {
|
|
208
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
209
|
+
const client = new SteleClient({
|
|
210
|
+
keyPair: issuerKeyPair,
|
|
211
|
+
strictMode: true,
|
|
212
|
+
});
|
|
213
|
+
const doc = await client.createCovenant({
|
|
214
|
+
issuer,
|
|
215
|
+
beneficiary,
|
|
216
|
+
constraints: "permit read on 'data'",
|
|
217
|
+
});
|
|
218
|
+
const tampered = { ...doc, signature: '00'.repeat(64) };
|
|
219
|
+
await expect(client.verifyCovenant(tampered)).rejects.toThrow(CovenantVerificationError);
|
|
220
|
+
});
|
|
221
|
+
it('does not throw in non-strict mode on verification failure', async () => {
|
|
222
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
223
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
224
|
+
const doc = await client.createCovenant({
|
|
225
|
+
issuer,
|
|
226
|
+
beneficiary,
|
|
227
|
+
constraints: "permit read on 'data'",
|
|
228
|
+
});
|
|
229
|
+
const tampered = { ...doc, signature: '00'.repeat(64) };
|
|
230
|
+
const result = await client.verifyCovenant(tampered);
|
|
231
|
+
expect(result.valid).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// ── Countersign ───────────────────────────────────────────────────────
|
|
235
|
+
describe('SteleClient.countersign', () => {
|
|
236
|
+
it('adds a valid countersignature using client key pair', async () => {
|
|
237
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
238
|
+
const auditorKp = await generateKeyPair();
|
|
239
|
+
const createClient = new SteleClient({ keyPair: issuerKeyPair });
|
|
240
|
+
const auditClient = new SteleClient({ keyPair: auditorKp });
|
|
241
|
+
const doc = await createClient.createCovenant({
|
|
242
|
+
issuer,
|
|
243
|
+
beneficiary,
|
|
244
|
+
constraints: "permit read on 'data'",
|
|
245
|
+
});
|
|
246
|
+
const signed = await auditClient.countersign(doc, 'auditor');
|
|
247
|
+
expect(signed.countersignatures).toHaveLength(1);
|
|
248
|
+
expect(signed.countersignatures[0].signerRole).toBe('auditor');
|
|
249
|
+
const result = await coreVerifyCovenant(signed);
|
|
250
|
+
expect(result.valid).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
it('adds a countersignature with an explicit key pair', async () => {
|
|
253
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
254
|
+
const auditorKp = await generateKeyPair();
|
|
255
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
256
|
+
const doc = await client.createCovenant({
|
|
257
|
+
issuer,
|
|
258
|
+
beneficiary,
|
|
259
|
+
constraints: "permit read on 'data'",
|
|
260
|
+
});
|
|
261
|
+
const signed = await client.countersign(doc, 'auditor', auditorKp);
|
|
262
|
+
expect(signed.countersignatures).toHaveLength(1);
|
|
263
|
+
expect(signed.countersignatures[0].signerPublicKey).toBe(auditorKp.publicKeyHex);
|
|
264
|
+
});
|
|
265
|
+
it('defaults to auditor role', async () => {
|
|
266
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
267
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
268
|
+
const doc = await client.createCovenant({
|
|
269
|
+
issuer,
|
|
270
|
+
beneficiary,
|
|
271
|
+
constraints: "permit read on 'data'",
|
|
272
|
+
});
|
|
273
|
+
const signed = await client.countersign(doc);
|
|
274
|
+
expect(signed.countersignatures[0].signerRole).toBe('auditor');
|
|
275
|
+
});
|
|
276
|
+
it('throws when no key pair is available', async () => {
|
|
277
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
278
|
+
const createClient = new SteleClient({ keyPair: issuerKeyPair });
|
|
279
|
+
const noKeyClient = new SteleClient();
|
|
280
|
+
const doc = await createClient.createCovenant({
|
|
281
|
+
issuer,
|
|
282
|
+
beneficiary,
|
|
283
|
+
constraints: "permit read on 'data'",
|
|
284
|
+
});
|
|
285
|
+
await expect(noKeyClient.countersign(doc)).rejects.toThrow('No key pair available');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
// ── Evaluate action ───────────────────────────────────────────────────
|
|
289
|
+
describe('SteleClient.evaluateAction', () => {
|
|
290
|
+
it('permits an action that matches a permit rule', async () => {
|
|
291
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
292
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
293
|
+
const doc = await client.createCovenant({
|
|
294
|
+
issuer,
|
|
295
|
+
beneficiary,
|
|
296
|
+
constraints: "permit read on '/data'",
|
|
297
|
+
});
|
|
298
|
+
const result = await client.evaluateAction(doc, 'read', '/data');
|
|
299
|
+
expect(result.permitted).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it('denies an action that matches a deny rule', async () => {
|
|
302
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
303
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
304
|
+
const doc = await client.createCovenant({
|
|
305
|
+
issuer,
|
|
306
|
+
beneficiary,
|
|
307
|
+
constraints: "deny write on '/system'",
|
|
308
|
+
});
|
|
309
|
+
const result = await client.evaluateAction(doc, 'write', '/system');
|
|
310
|
+
expect(result.permitted).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
it('denies by default when no rules match', async () => {
|
|
313
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
314
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
315
|
+
const doc = await client.createCovenant({
|
|
316
|
+
issuer,
|
|
317
|
+
beneficiary,
|
|
318
|
+
constraints: "permit read on '/data'",
|
|
319
|
+
});
|
|
320
|
+
const result = await client.evaluateAction(doc, 'write', '/data');
|
|
321
|
+
expect(result.permitted).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
it('respects deny-wins over permit at equal specificity', async () => {
|
|
324
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
325
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
326
|
+
const doc = await client.createCovenant({
|
|
327
|
+
issuer,
|
|
328
|
+
beneficiary,
|
|
329
|
+
constraints: "permit write on '/data'\ndeny write on '/data'",
|
|
330
|
+
});
|
|
331
|
+
const result = await client.evaluateAction(doc, 'write', '/data');
|
|
332
|
+
expect(result.permitted).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
it('supports evaluation context', async () => {
|
|
335
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
336
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
337
|
+
const doc = await client.createCovenant({
|
|
338
|
+
issuer,
|
|
339
|
+
beneficiary,
|
|
340
|
+
constraints: "permit read on '/data' when role = 'admin'",
|
|
341
|
+
});
|
|
342
|
+
const admin = await client.evaluateAction(doc, 'read', '/data', { role: 'admin' });
|
|
343
|
+
expect(admin.permitted).toBe(true);
|
|
344
|
+
const user = await client.evaluateAction(doc, 'read', '/data', { role: 'user' });
|
|
345
|
+
expect(user.permitted).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
// ── Identity ──────────────────────────────────────────────────────────
|
|
349
|
+
describe('SteleClient.createIdentity', () => {
|
|
350
|
+
it('creates a valid agent identity', async () => {
|
|
351
|
+
const kp = await generateKeyPair();
|
|
352
|
+
const client = new SteleClient({ keyPair: kp });
|
|
353
|
+
const identity = await client.createIdentity({
|
|
354
|
+
model: {
|
|
355
|
+
provider: 'openai',
|
|
356
|
+
modelId: 'gpt-4',
|
|
357
|
+
modelVersion: '1.0',
|
|
358
|
+
},
|
|
359
|
+
capabilities: ['chat', 'code'],
|
|
360
|
+
deployment: { runtime: 'container' },
|
|
361
|
+
});
|
|
362
|
+
expect(identity.id).toBeTruthy();
|
|
363
|
+
expect(identity.operatorPublicKey).toBe(kp.publicKeyHex);
|
|
364
|
+
expect(identity.capabilities).toEqual(['chat', 'code']);
|
|
365
|
+
expect(identity.version).toBe(1);
|
|
366
|
+
expect(identity.lineage).toHaveLength(1);
|
|
367
|
+
// Verify the identity is cryptographically valid
|
|
368
|
+
const verification = await verifyIdentity(identity);
|
|
369
|
+
expect(verification.valid).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
it('uses explicit operatorKeyPair when provided', async () => {
|
|
372
|
+
const clientKp = await generateKeyPair();
|
|
373
|
+
const operatorKp = await generateKeyPair();
|
|
374
|
+
const client = new SteleClient({ keyPair: clientKp });
|
|
375
|
+
const identity = await client.createIdentity({
|
|
376
|
+
operatorKeyPair: operatorKp,
|
|
377
|
+
model: {
|
|
378
|
+
provider: 'anthropic',
|
|
379
|
+
modelId: 'claude',
|
|
380
|
+
modelVersion: '3',
|
|
381
|
+
},
|
|
382
|
+
capabilities: ['chat'],
|
|
383
|
+
deployment: { runtime: 'process' },
|
|
384
|
+
});
|
|
385
|
+
expect(identity.operatorPublicKey).toBe(operatorKp.publicKeyHex);
|
|
386
|
+
});
|
|
387
|
+
it('throws when no key pair is available', async () => {
|
|
388
|
+
const client = new SteleClient();
|
|
389
|
+
await expect(client.createIdentity({
|
|
390
|
+
model: {
|
|
391
|
+
provider: 'test',
|
|
392
|
+
modelId: 'test',
|
|
393
|
+
},
|
|
394
|
+
capabilities: ['test'],
|
|
395
|
+
deployment: { runtime: 'process' },
|
|
396
|
+
})).rejects.toThrow('No key pair available');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
describe('SteleClient.evolveIdentity', () => {
|
|
400
|
+
it('evolves an identity with new capabilities', async () => {
|
|
401
|
+
const kp = await generateKeyPair();
|
|
402
|
+
const client = new SteleClient({ keyPair: kp });
|
|
403
|
+
const identity = await client.createIdentity(makeIdentityOptions(kp));
|
|
404
|
+
const evolved = await client.evolveIdentity(identity, {
|
|
405
|
+
changeType: 'capability_change',
|
|
406
|
+
description: 'Adding new capability',
|
|
407
|
+
updates: {
|
|
408
|
+
capabilities: ['read', 'write', 'execute', 'admin'],
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
expect(evolved.version).toBe(2);
|
|
412
|
+
expect(evolved.capabilities).toContain('admin');
|
|
413
|
+
expect(evolved.lineage).toHaveLength(2);
|
|
414
|
+
const verification = await verifyIdentity(evolved);
|
|
415
|
+
expect(verification.valid).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
it('throws when no key pair is available', async () => {
|
|
418
|
+
const kp = await generateKeyPair();
|
|
419
|
+
const identityClient = new SteleClient({ keyPair: kp });
|
|
420
|
+
const identity = await identityClient.createIdentity(makeIdentityOptions(kp));
|
|
421
|
+
const noKeyClient = new SteleClient();
|
|
422
|
+
await expect(noKeyClient.evolveIdentity(identity, {
|
|
423
|
+
changeType: 'capability_change',
|
|
424
|
+
description: 'test',
|
|
425
|
+
updates: { capabilities: ['read'] },
|
|
426
|
+
})).rejects.toThrow('No key pair available');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
// ── Chain operations ──────────────────────────────────────────────────
|
|
430
|
+
describe('SteleClient.resolveChain', () => {
|
|
431
|
+
it('resolves a parent-child chain', async () => {
|
|
432
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
433
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
434
|
+
const root = await client.createCovenant({
|
|
435
|
+
issuer,
|
|
436
|
+
beneficiary,
|
|
437
|
+
constraints: "permit read on '/data/**'",
|
|
438
|
+
});
|
|
439
|
+
const child = await client.createCovenant({
|
|
440
|
+
issuer,
|
|
441
|
+
beneficiary,
|
|
442
|
+
constraints: "permit read on '/data/public'",
|
|
443
|
+
chain: { parentId: root.id, relation: 'restricts', depth: 1 },
|
|
444
|
+
});
|
|
445
|
+
const ancestors = await client.resolveChain(child, [root]);
|
|
446
|
+
expect(ancestors).toHaveLength(1);
|
|
447
|
+
expect(ancestors[0].id).toBe(root.id);
|
|
448
|
+
});
|
|
449
|
+
it('returns empty array for root covenant', async () => {
|
|
450
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
451
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
452
|
+
const root = await client.createCovenant({
|
|
453
|
+
issuer,
|
|
454
|
+
beneficiary,
|
|
455
|
+
constraints: "permit read on '/data'",
|
|
456
|
+
});
|
|
457
|
+
const ancestors = await client.resolveChain(root);
|
|
458
|
+
expect(ancestors).toHaveLength(0);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
describe('SteleClient.validateChain', () => {
|
|
462
|
+
it('validates a proper narrowing chain', async () => {
|
|
463
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
464
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
465
|
+
const root = await client.createCovenant({
|
|
466
|
+
issuer,
|
|
467
|
+
beneficiary,
|
|
468
|
+
constraints: "permit read on '/data/**'",
|
|
469
|
+
});
|
|
470
|
+
const child = await client.createCovenant({
|
|
471
|
+
issuer,
|
|
472
|
+
beneficiary,
|
|
473
|
+
constraints: "permit read on '/data/public'",
|
|
474
|
+
chain: { parentId: root.id, relation: 'restricts', depth: 1 },
|
|
475
|
+
});
|
|
476
|
+
const result = await client.validateChain([root, child]);
|
|
477
|
+
expect(result.valid).toBe(true);
|
|
478
|
+
expect(result.results).toHaveLength(2);
|
|
479
|
+
expect(result.narrowingViolations).toHaveLength(0);
|
|
480
|
+
});
|
|
481
|
+
it('detects narrowing violations', async () => {
|
|
482
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
483
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
484
|
+
const root = await client.createCovenant({
|
|
485
|
+
issuer,
|
|
486
|
+
beneficiary,
|
|
487
|
+
constraints: "deny write on '/system/**'",
|
|
488
|
+
});
|
|
489
|
+
const child = await client.createCovenant({
|
|
490
|
+
issuer,
|
|
491
|
+
beneficiary,
|
|
492
|
+
constraints: "permit write on '/system/config'",
|
|
493
|
+
chain: { parentId: root.id, relation: 'restricts', depth: 1 },
|
|
494
|
+
});
|
|
495
|
+
const result = await client.validateChain([root, child]);
|
|
496
|
+
expect(result.valid).toBe(false);
|
|
497
|
+
expect(result.narrowingViolations.length).toBeGreaterThan(0);
|
|
498
|
+
});
|
|
499
|
+
it('validates each document individually', async () => {
|
|
500
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
501
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
502
|
+
const doc = await client.createCovenant({
|
|
503
|
+
issuer,
|
|
504
|
+
beneficiary,
|
|
505
|
+
constraints: "permit read on '/data'",
|
|
506
|
+
});
|
|
507
|
+
const result = await client.validateChain([doc]);
|
|
508
|
+
expect(result.valid).toBe(true);
|
|
509
|
+
expect(result.results).toHaveLength(1);
|
|
510
|
+
expect(result.results[0].valid).toBe(true);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
// ── CCL utilities ─────────────────────────────────────────────────────
|
|
514
|
+
describe('SteleClient CCL utilities', () => {
|
|
515
|
+
it('parseCCL parses valid CCL', () => {
|
|
516
|
+
const client = new SteleClient();
|
|
517
|
+
const doc = client.parseCCL("permit read on '/data'");
|
|
518
|
+
expect(doc.permits).toHaveLength(1);
|
|
519
|
+
expect(doc.permits[0].action).toBe('read');
|
|
520
|
+
});
|
|
521
|
+
it('parseCCL throws on invalid CCL', () => {
|
|
522
|
+
const client = new SteleClient();
|
|
523
|
+
expect(() => client.parseCCL('!!! invalid !!!')).toThrow();
|
|
524
|
+
});
|
|
525
|
+
it('mergeCCL merges two CCL documents', () => {
|
|
526
|
+
const client = new SteleClient();
|
|
527
|
+
const a = client.parseCCL("permit read on '/data'");
|
|
528
|
+
const b = client.parseCCL("deny write on '/system'");
|
|
529
|
+
const merged = client.mergeCCL(a, b);
|
|
530
|
+
expect(merged.permits.length).toBeGreaterThanOrEqual(1);
|
|
531
|
+
expect(merged.denies.length).toBeGreaterThanOrEqual(1);
|
|
532
|
+
});
|
|
533
|
+
it('serializeCCL round-trips with parseCCL', () => {
|
|
534
|
+
const client = new SteleClient();
|
|
535
|
+
const source = "permit read on '/data'";
|
|
536
|
+
const doc = client.parseCCL(source);
|
|
537
|
+
const serialized = client.serializeCCL(doc);
|
|
538
|
+
// Parse the serialized output to verify it's valid
|
|
539
|
+
const reparsed = client.parseCCL(serialized);
|
|
540
|
+
expect(reparsed.permits).toHaveLength(1);
|
|
541
|
+
expect(reparsed.permits[0].action).toBe('read');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
// ── Event system ──────────────────────────────────────────────────────
|
|
545
|
+
describe('SteleClient event system', () => {
|
|
546
|
+
it('emits covenant:created event', async () => {
|
|
547
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
548
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
549
|
+
const events = [];
|
|
550
|
+
client.on('covenant:created', (e) => events.push(e));
|
|
551
|
+
await client.createCovenant({
|
|
552
|
+
issuer,
|
|
553
|
+
beneficiary,
|
|
554
|
+
constraints: "permit read on 'data'",
|
|
555
|
+
});
|
|
556
|
+
expect(events).toHaveLength(1);
|
|
557
|
+
expect(events[0].type).toBe('covenant:created');
|
|
558
|
+
expect(events[0].document.id).toBeTruthy();
|
|
559
|
+
expect(events[0].timestamp).toBeTruthy();
|
|
560
|
+
});
|
|
561
|
+
it('emits covenant:verified event', async () => {
|
|
562
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
563
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
564
|
+
const doc = await client.createCovenant({
|
|
565
|
+
issuer,
|
|
566
|
+
beneficiary,
|
|
567
|
+
constraints: "permit read on 'data'",
|
|
568
|
+
});
|
|
569
|
+
const events = [];
|
|
570
|
+
client.on('covenant:verified', (e) => events.push(e));
|
|
571
|
+
await client.verifyCovenant(doc);
|
|
572
|
+
expect(events).toHaveLength(1);
|
|
573
|
+
expect(events[0].type).toBe('covenant:verified');
|
|
574
|
+
expect(events[0].result.valid).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
it('emits covenant:countersigned event', async () => {
|
|
577
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
578
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
579
|
+
const doc = await client.createCovenant({
|
|
580
|
+
issuer,
|
|
581
|
+
beneficiary,
|
|
582
|
+
constraints: "permit read on 'data'",
|
|
583
|
+
});
|
|
584
|
+
const events = [];
|
|
585
|
+
client.on('covenant:countersigned', (e) => events.push(e));
|
|
586
|
+
await client.countersign(doc, 'auditor');
|
|
587
|
+
expect(events).toHaveLength(1);
|
|
588
|
+
expect(events[0].signerRole).toBe('auditor');
|
|
589
|
+
});
|
|
590
|
+
it('emits identity:created event', async () => {
|
|
591
|
+
const kp = await generateKeyPair();
|
|
592
|
+
const client = new SteleClient({ keyPair: kp });
|
|
593
|
+
const events = [];
|
|
594
|
+
client.on('identity:created', (e) => events.push(e));
|
|
595
|
+
await client.createIdentity(makeIdentityOptions(kp));
|
|
596
|
+
expect(events).toHaveLength(1);
|
|
597
|
+
expect(events[0].type).toBe('identity:created');
|
|
598
|
+
expect(events[0].identity.id).toBeTruthy();
|
|
599
|
+
});
|
|
600
|
+
it('emits identity:evolved event', async () => {
|
|
601
|
+
const kp = await generateKeyPair();
|
|
602
|
+
const client = new SteleClient({ keyPair: kp });
|
|
603
|
+
const identity = await client.createIdentity(makeIdentityOptions(kp));
|
|
604
|
+
const events = [];
|
|
605
|
+
client.on('identity:evolved', (e) => events.push(e));
|
|
606
|
+
await client.evolveIdentity(identity, {
|
|
607
|
+
changeType: 'capability_change',
|
|
608
|
+
description: 'test evolve',
|
|
609
|
+
updates: { capabilities: ['read'] },
|
|
610
|
+
});
|
|
611
|
+
expect(events).toHaveLength(1);
|
|
612
|
+
expect(events[0].type).toBe('identity:evolved');
|
|
613
|
+
expect(events[0].changeType).toBe('capability_change');
|
|
614
|
+
});
|
|
615
|
+
it('emits chain:resolved event', async () => {
|
|
616
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
617
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
618
|
+
const root = await client.createCovenant({
|
|
619
|
+
issuer,
|
|
620
|
+
beneficiary,
|
|
621
|
+
constraints: "permit read on '/data'",
|
|
622
|
+
});
|
|
623
|
+
// Clear any listeners from createCovenant
|
|
624
|
+
const events = [];
|
|
625
|
+
client.on('chain:resolved', (e) => events.push(e));
|
|
626
|
+
await client.resolveChain(root);
|
|
627
|
+
expect(events).toHaveLength(1);
|
|
628
|
+
expect(events[0].type).toBe('chain:resolved');
|
|
629
|
+
});
|
|
630
|
+
it('emits chain:validated event', async () => {
|
|
631
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
632
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
633
|
+
const doc = await client.createCovenant({
|
|
634
|
+
issuer,
|
|
635
|
+
beneficiary,
|
|
636
|
+
constraints: "permit read on '/data'",
|
|
637
|
+
});
|
|
638
|
+
const events = [];
|
|
639
|
+
client.on('chain:validated', (e) => events.push(e));
|
|
640
|
+
await client.validateChain([doc]);
|
|
641
|
+
expect(events).toHaveLength(1);
|
|
642
|
+
expect(events[0].type).toBe('chain:validated');
|
|
643
|
+
expect(events[0].result.valid).toBe(true);
|
|
644
|
+
});
|
|
645
|
+
it('emits evaluation:completed event', async () => {
|
|
646
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
647
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
648
|
+
const doc = await client.createCovenant({
|
|
649
|
+
issuer,
|
|
650
|
+
beneficiary,
|
|
651
|
+
constraints: "permit read on '/data'",
|
|
652
|
+
});
|
|
653
|
+
const events = [];
|
|
654
|
+
client.on('evaluation:completed', (e) => events.push(e));
|
|
655
|
+
await client.evaluateAction(doc, 'read', '/data');
|
|
656
|
+
expect(events).toHaveLength(1);
|
|
657
|
+
expect(events[0].type).toBe('evaluation:completed');
|
|
658
|
+
expect(events[0].action).toBe('read');
|
|
659
|
+
expect(events[0].resource).toBe('/data');
|
|
660
|
+
expect(events[0].result.permitted).toBe(true);
|
|
661
|
+
});
|
|
662
|
+
it('on() returns a disposer function', async () => {
|
|
663
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
664
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
665
|
+
let count = 0;
|
|
666
|
+
const dispose = client.on('covenant:created', () => { count++; });
|
|
667
|
+
await client.createCovenant({
|
|
668
|
+
issuer,
|
|
669
|
+
beneficiary,
|
|
670
|
+
constraints: "permit read on 'data'",
|
|
671
|
+
});
|
|
672
|
+
expect(count).toBe(1);
|
|
673
|
+
dispose();
|
|
674
|
+
await client.createCovenant({
|
|
675
|
+
issuer,
|
|
676
|
+
beneficiary,
|
|
677
|
+
constraints: "permit read on 'data'",
|
|
678
|
+
});
|
|
679
|
+
expect(count).toBe(1); // Not incremented after dispose
|
|
680
|
+
});
|
|
681
|
+
it('off() removes a specific handler', async () => {
|
|
682
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
683
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
684
|
+
let count = 0;
|
|
685
|
+
const handler = () => { count++; };
|
|
686
|
+
client.on('covenant:created', handler);
|
|
687
|
+
await client.createCovenant({
|
|
688
|
+
issuer,
|
|
689
|
+
beneficiary,
|
|
690
|
+
constraints: "permit read on 'data'",
|
|
691
|
+
});
|
|
692
|
+
expect(count).toBe(1);
|
|
693
|
+
client.off('covenant:created', handler);
|
|
694
|
+
await client.createCovenant({
|
|
695
|
+
issuer,
|
|
696
|
+
beneficiary,
|
|
697
|
+
constraints: "permit read on 'data'",
|
|
698
|
+
});
|
|
699
|
+
expect(count).toBe(1);
|
|
700
|
+
});
|
|
701
|
+
it('removeAllListeners() clears handlers for a specific event', async () => {
|
|
702
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
703
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
704
|
+
let count = 0;
|
|
705
|
+
client.on('covenant:created', () => { count++; });
|
|
706
|
+
client.on('covenant:created', () => { count++; });
|
|
707
|
+
await client.createCovenant({
|
|
708
|
+
issuer,
|
|
709
|
+
beneficiary,
|
|
710
|
+
constraints: "permit read on 'data'",
|
|
711
|
+
});
|
|
712
|
+
expect(count).toBe(2);
|
|
713
|
+
client.removeAllListeners('covenant:created');
|
|
714
|
+
await client.createCovenant({
|
|
715
|
+
issuer,
|
|
716
|
+
beneficiary,
|
|
717
|
+
constraints: "permit read on 'data'",
|
|
718
|
+
});
|
|
719
|
+
expect(count).toBe(2); // Not incremented
|
|
720
|
+
});
|
|
721
|
+
it('removeAllListeners() without args clears all events', async () => {
|
|
722
|
+
const client = new SteleClient();
|
|
723
|
+
let created = 0;
|
|
724
|
+
let verified = 0;
|
|
725
|
+
client.on('covenant:created', () => { created++; });
|
|
726
|
+
client.on('covenant:verified', () => { verified++; });
|
|
727
|
+
client.removeAllListeners();
|
|
728
|
+
// Handlers should be gone -- no errors, but no events
|
|
729
|
+
// We can verify by checking the handlers have no effect
|
|
730
|
+
expect(created).toBe(0);
|
|
731
|
+
expect(verified).toBe(0);
|
|
732
|
+
});
|
|
733
|
+
it('supports multiple handlers for the same event', async () => {
|
|
734
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
735
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
736
|
+
const calls = [];
|
|
737
|
+
client.on('covenant:created', () => calls.push('handler1'));
|
|
738
|
+
client.on('covenant:created', () => calls.push('handler2'));
|
|
739
|
+
await client.createCovenant({
|
|
740
|
+
issuer,
|
|
741
|
+
beneficiary,
|
|
742
|
+
constraints: "permit read on 'data'",
|
|
743
|
+
});
|
|
744
|
+
expect(calls).toEqual(['handler1', 'handler2']);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
// ── QuickCovenant ─────────────────────────────────────────────────────
|
|
748
|
+
describe('QuickCovenant', () => {
|
|
749
|
+
describe('QuickCovenant.permit', () => {
|
|
750
|
+
it('creates a valid permit covenant', async () => {
|
|
751
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
752
|
+
const doc = await QuickCovenant.permit('read', '/data', issuer, beneficiary, issuerKeyPair.privateKey);
|
|
753
|
+
expect(doc.constraints).toBe("permit read on '/data'");
|
|
754
|
+
const result = await coreVerifyCovenant(doc);
|
|
755
|
+
expect(result.valid).toBe(true);
|
|
756
|
+
});
|
|
757
|
+
it('creates a permit covenant with dotted actions', async () => {
|
|
758
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
759
|
+
const doc = await QuickCovenant.permit('file.read', '/documents/**', issuer, beneficiary, issuerKeyPair.privateKey);
|
|
760
|
+
expect(doc.constraints).toBe("permit file.read on '/documents/**'");
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
describe('QuickCovenant.deny', () => {
|
|
764
|
+
it('creates a valid deny covenant', async () => {
|
|
765
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
766
|
+
const doc = await QuickCovenant.deny('write', '/system', issuer, beneficiary, issuerKeyPair.privateKey);
|
|
767
|
+
expect(doc.constraints).toBe("deny write on '/system'");
|
|
768
|
+
const result = await coreVerifyCovenant(doc);
|
|
769
|
+
expect(result.valid).toBe(true);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
describe('QuickCovenant.standard', () => {
|
|
773
|
+
it('creates a standard covenant with three rules', async () => {
|
|
774
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
775
|
+
const doc = await QuickCovenant.standard(issuer, beneficiary, issuerKeyPair.privateKey);
|
|
776
|
+
const result = await coreVerifyCovenant(doc);
|
|
777
|
+
expect(result.valid).toBe(true);
|
|
778
|
+
// Parse constraints and verify they include the expected rules
|
|
779
|
+
const cclDoc = parseCCL(doc.constraints);
|
|
780
|
+
expect(cclDoc.permits.length).toBeGreaterThanOrEqual(1);
|
|
781
|
+
expect(cclDoc.denies.length).toBeGreaterThanOrEqual(1);
|
|
782
|
+
expect(cclDoc.limits.length).toBeGreaterThanOrEqual(1);
|
|
783
|
+
});
|
|
784
|
+
it('the standard covenant permits reads', async () => {
|
|
785
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
786
|
+
const doc = await QuickCovenant.standard(issuer, beneficiary, issuerKeyPair.privateKey);
|
|
787
|
+
const cclDoc = parseCCL(doc.constraints);
|
|
788
|
+
const result = evaluateCCL(cclDoc, 'read', '/anything');
|
|
789
|
+
expect(result.permitted).toBe(true);
|
|
790
|
+
});
|
|
791
|
+
it('the standard covenant denies writes to /system/**', async () => {
|
|
792
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
793
|
+
const doc = await QuickCovenant.standard(issuer, beneficiary, issuerKeyPair.privateKey);
|
|
794
|
+
const cclDoc = parseCCL(doc.constraints);
|
|
795
|
+
const result = evaluateCCL(cclDoc, 'write', '/system/config');
|
|
796
|
+
expect(result.permitted).toBe(false);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
// ── Re-exports from @nobulex/core ───────────────────────────────────────
|
|
801
|
+
describe('re-exports from @nobulex/core', () => {
|
|
802
|
+
it('exports PROTOCOL_VERSION constant', () => {
|
|
803
|
+
expect(PROTOCOL_VERSION).toBe('1.0');
|
|
804
|
+
});
|
|
805
|
+
it('exports MAX_CONSTRAINTS constant', () => {
|
|
806
|
+
expect(MAX_CONSTRAINTS).toBe(1000);
|
|
807
|
+
});
|
|
808
|
+
it('exports MAX_CHAIN_DEPTH constant', () => {
|
|
809
|
+
expect(MAX_CHAIN_DEPTH).toBe(16);
|
|
810
|
+
});
|
|
811
|
+
it('exports MAX_DOCUMENT_SIZE constant', () => {
|
|
812
|
+
expect(MAX_DOCUMENT_SIZE).toBe(1_048_576);
|
|
813
|
+
});
|
|
814
|
+
it('exports CovenantBuildError class', () => {
|
|
815
|
+
const err = new CovenantBuildError('test', 'field');
|
|
816
|
+
expect(err).toBeInstanceOf(Error);
|
|
817
|
+
expect(err.name).toBe('CovenantBuildError');
|
|
818
|
+
expect(err.field).toBe('field');
|
|
819
|
+
});
|
|
820
|
+
it('exports CovenantVerificationError class', () => {
|
|
821
|
+
const err = new CovenantVerificationError('test', []);
|
|
822
|
+
expect(err).toBeInstanceOf(Error);
|
|
823
|
+
expect(err.name).toBe('CovenantVerificationError');
|
|
824
|
+
});
|
|
825
|
+
it('exports MemoryChainResolver class', async () => {
|
|
826
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
827
|
+
const doc = await buildCovenant({
|
|
828
|
+
issuer,
|
|
829
|
+
beneficiary,
|
|
830
|
+
constraints: "permit read on 'data'",
|
|
831
|
+
privateKey: issuerKeyPair.privateKey,
|
|
832
|
+
});
|
|
833
|
+
const resolver = new MemoryChainResolver();
|
|
834
|
+
resolver.add(doc);
|
|
835
|
+
const resolved = await resolver.resolve(doc.id);
|
|
836
|
+
expect(resolved).toEqual(doc);
|
|
837
|
+
});
|
|
838
|
+
it('exports canonicalForm and computeId functions', async () => {
|
|
839
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
840
|
+
const doc = await buildCovenant({
|
|
841
|
+
issuer,
|
|
842
|
+
beneficiary,
|
|
843
|
+
constraints: "permit read on 'data'",
|
|
844
|
+
privateKey: issuerKeyPair.privateKey,
|
|
845
|
+
});
|
|
846
|
+
const form = canonicalForm(doc);
|
|
847
|
+
expect(typeof form).toBe('string');
|
|
848
|
+
const id = computeId(doc);
|
|
849
|
+
expect(id).toBe(doc.id);
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
// ── Re-exports from @nobulex/crypto ─────────────────────────────────────
|
|
853
|
+
describe('re-exports from @nobulex/crypto', () => {
|
|
854
|
+
it('exports sha256String', () => {
|
|
855
|
+
const hash = sha256String('hello');
|
|
856
|
+
expect(hash).toMatch(/^[0-9a-f]{64}$/);
|
|
857
|
+
});
|
|
858
|
+
it('exports toHex and fromHex', () => {
|
|
859
|
+
const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
860
|
+
const hex = toHex(bytes);
|
|
861
|
+
expect(hex).toBe('deadbeef');
|
|
862
|
+
const decoded = fromHex(hex);
|
|
863
|
+
expect(decoded).toEqual(bytes);
|
|
864
|
+
});
|
|
865
|
+
it('exports generateId', () => {
|
|
866
|
+
const id = generateId();
|
|
867
|
+
expect(id).toMatch(/^[0-9a-f]{32}$/);
|
|
868
|
+
});
|
|
869
|
+
it('exports timestamp', () => {
|
|
870
|
+
const ts = timestamp();
|
|
871
|
+
expect(new Date(ts).toISOString()).toBe(ts);
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
// ── Re-exports from @nobulex/ccl ────────────────────────────────────────
|
|
875
|
+
describe('re-exports from @nobulex/ccl', () => {
|
|
876
|
+
it('exports parseCCL', () => {
|
|
877
|
+
const doc = parseCCL("permit read on '/data'");
|
|
878
|
+
expect(doc.permits).toHaveLength(1);
|
|
879
|
+
});
|
|
880
|
+
it('exports evaluateCCL', () => {
|
|
881
|
+
const doc = parseCCL("permit read on '/data'");
|
|
882
|
+
const result = evaluateCCL(doc, 'read', '/data');
|
|
883
|
+
expect(result.permitted).toBe(true);
|
|
884
|
+
});
|
|
885
|
+
it('exports matchAction', () => {
|
|
886
|
+
expect(matchAction('file.read', 'file.read')).toBe(true);
|
|
887
|
+
expect(matchAction('file.*', 'file.read')).toBe(true);
|
|
888
|
+
expect(matchAction('file.read', 'file.write')).toBe(false);
|
|
889
|
+
});
|
|
890
|
+
it('exports matchResource', () => {
|
|
891
|
+
expect(matchResource('/data/**', '/data/foo/bar')).toBe(true);
|
|
892
|
+
expect(matchResource('/data/*', '/data/foo')).toBe(true);
|
|
893
|
+
expect(matchResource('/data/foo', '/data/bar')).toBe(false);
|
|
894
|
+
});
|
|
895
|
+
it('exports mergeCCL', () => {
|
|
896
|
+
const a = parseCCL("permit read on '/data'");
|
|
897
|
+
const b = parseCCL("deny write on '/system'");
|
|
898
|
+
const merged = mergeCCL(a, b);
|
|
899
|
+
expect(merged.statements.length).toBeGreaterThan(0);
|
|
900
|
+
});
|
|
901
|
+
it('exports serializeCCL', () => {
|
|
902
|
+
const doc = parseCCL("permit read on '/data'");
|
|
903
|
+
const serialized = serializeCCL(doc);
|
|
904
|
+
expect(serialized).toContain('permit');
|
|
905
|
+
expect(serialized).toContain('read');
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
// ── Re-exports from @nobulex/identity ───────────────────────────────────
|
|
909
|
+
describe('re-exports from @nobulex/identity', () => {
|
|
910
|
+
it('exports DEFAULT_EVOLUTION_POLICY', () => {
|
|
911
|
+
expect(DEFAULT_EVOLUTION_POLICY.minorUpdate).toBe(0.95);
|
|
912
|
+
expect(DEFAULT_EVOLUTION_POLICY.modelVersionChange).toBe(0.80);
|
|
913
|
+
expect(DEFAULT_EVOLUTION_POLICY.fullRebuild).toBe(0.00);
|
|
914
|
+
});
|
|
915
|
+
it('exports getLineage', async () => {
|
|
916
|
+
const kp = await generateKeyPair();
|
|
917
|
+
const client = new SteleClient({ keyPair: kp });
|
|
918
|
+
const identity = await client.createIdentity(makeIdentityOptions(kp));
|
|
919
|
+
const lineage = getLineage(identity);
|
|
920
|
+
expect(lineage).toHaveLength(1);
|
|
921
|
+
expect(lineage[0].changeType).toBe('created');
|
|
922
|
+
});
|
|
923
|
+
it('exports serializeIdentity and deserializeIdentity', async () => {
|
|
924
|
+
const kp = await generateKeyPair();
|
|
925
|
+
const client = new SteleClient({ keyPair: kp });
|
|
926
|
+
const identity = await client.createIdentity(makeIdentityOptions(kp));
|
|
927
|
+
const json = serializeIdentity(identity);
|
|
928
|
+
expect(typeof json).toBe('string');
|
|
929
|
+
const restored = deserializeIdentity(json);
|
|
930
|
+
expect(restored.id).toBe(identity.id);
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
// ── Integration: full lifecycle ───────────────────────────────────────
|
|
934
|
+
describe('integration: full covenant lifecycle', () => {
|
|
935
|
+
it('create -> verify -> countersign -> verify -> evaluate', async () => {
|
|
936
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
937
|
+
const auditorKp = await generateKeyPair();
|
|
938
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
939
|
+
// Create
|
|
940
|
+
const doc = await client.createCovenant({
|
|
941
|
+
issuer,
|
|
942
|
+
beneficiary,
|
|
943
|
+
constraints: "permit read on '/data/**'\ndeny write on '/system/**'",
|
|
944
|
+
metadata: { name: 'integration-test' },
|
|
945
|
+
});
|
|
946
|
+
// Verify
|
|
947
|
+
const v1 = await client.verifyCovenant(doc);
|
|
948
|
+
expect(v1.valid).toBe(true);
|
|
949
|
+
// Countersign
|
|
950
|
+
const signed = await client.countersign(doc, 'auditor', auditorKp);
|
|
951
|
+
expect(signed.countersignatures).toHaveLength(1);
|
|
952
|
+
// Verify after countersign
|
|
953
|
+
const v2 = await client.verifyCovenant(signed);
|
|
954
|
+
expect(v2.valid).toBe(true);
|
|
955
|
+
// Evaluate permitted action
|
|
956
|
+
const readResult = await client.evaluateAction(signed, 'read', '/data/public');
|
|
957
|
+
expect(readResult.permitted).toBe(true);
|
|
958
|
+
// Evaluate denied action
|
|
959
|
+
const writeResult = await client.evaluateAction(signed, 'write', '/system/config');
|
|
960
|
+
expect(writeResult.permitted).toBe(false);
|
|
961
|
+
});
|
|
962
|
+
it('create chain -> validate -> resolve', async () => {
|
|
963
|
+
const { issuerKeyPair, issuer, beneficiary } = await makeParties();
|
|
964
|
+
const client = new SteleClient({ keyPair: issuerKeyPair });
|
|
965
|
+
const root = await client.createCovenant({
|
|
966
|
+
issuer,
|
|
967
|
+
beneficiary,
|
|
968
|
+
constraints: "permit read on '/data/**'",
|
|
969
|
+
});
|
|
970
|
+
const child = await client.createCovenant({
|
|
971
|
+
issuer,
|
|
972
|
+
beneficiary,
|
|
973
|
+
constraints: "permit read on '/data/reports'",
|
|
974
|
+
chain: { parentId: root.id, relation: 'restricts', depth: 1 },
|
|
975
|
+
});
|
|
976
|
+
// Validate chain
|
|
977
|
+
const chainResult = await client.validateChain([root, child]);
|
|
978
|
+
expect(chainResult.valid).toBe(true);
|
|
979
|
+
// Resolve chain
|
|
980
|
+
const ancestors = await client.resolveChain(child, [root]);
|
|
981
|
+
expect(ancestors).toHaveLength(1);
|
|
982
|
+
expect(ancestors[0].id).toBe(root.id);
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
//# sourceMappingURL=index.test.js.map
|