@auths-dev/sdk 0.0.1 → 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/Cargo.toml +45 -0
- package/README.md +163 -4
- package/__test__/client.spec.ts +78 -0
- package/__test__/exports.spec.ts +57 -0
- package/__test__/integration.spec.ts +407 -0
- package/__test__/policy.spec.ts +202 -0
- package/__test__/verify.spec.ts +88 -0
- package/build.rs +5 -0
- package/index.d.ts +259 -0
- package/index.js +622 -1
- package/lib/artifacts.ts +124 -0
- package/lib/attestations.ts +126 -0
- package/lib/audit.ts +189 -0
- package/lib/client.ts +293 -0
- package/lib/commits.ts +70 -0
- package/lib/devices.ts +178 -0
- package/lib/errors.ts +306 -0
- package/lib/identity.ts +280 -0
- package/lib/index.ts +125 -0
- package/lib/native.ts +255 -0
- package/lib/org.ts +235 -0
- package/lib/pairing.ts +271 -0
- package/lib/policy.ts +669 -0
- package/lib/signing.ts +204 -0
- package/lib/trust.ts +152 -0
- package/lib/types.ts +179 -0
- package/lib/verify.ts +241 -0
- package/lib/witness.ts +91 -0
- package/npm/darwin-arm64/README.md +3 -0
- package/npm/darwin-arm64/package.json +23 -0
- package/npm/linux-arm64-gnu/README.md +3 -0
- package/npm/linux-arm64-gnu/package.json +26 -0
- package/npm/linux-x64-gnu/README.md +3 -0
- package/npm/linux-x64-gnu/package.json +26 -0
- package/npm/win32-arm64-msvc/README.md +3 -0
- package/npm/win32-arm64-msvc/package.json +23 -0
- package/npm/win32-x64-msvc/README.md +3 -0
- package/npm/win32-x64-msvc/package.json +23 -0
- package/package.json +51 -16
- package/src/artifact.rs +217 -0
- package/src/attestation_query.rs +104 -0
- package/src/audit.rs +128 -0
- package/src/commit_sign.rs +63 -0
- package/src/device.rs +212 -0
- package/src/diagnostics.rs +106 -0
- package/src/error.rs +5 -0
- package/src/helpers.rs +60 -0
- package/src/identity.rs +467 -0
- package/src/lib.rs +26 -0
- package/src/org.rs +430 -0
- package/src/pairing.rs +454 -0
- package/src/policy.rs +147 -0
- package/src/sign.rs +215 -0
- package/src/trust.rs +189 -0
- package/src/types.rs +205 -0
- package/src/verify.rs +447 -0
- package/src/witness.rs +138 -0
- package/tsconfig.json +19 -0
- package/typedoc.json +18 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { tmpdir } from 'os'
|
|
6
|
+
import { Auths } from '../lib/client'
|
|
7
|
+
import type { Identity } from '../lib/identity'
|
|
8
|
+
|
|
9
|
+
const tmpDirs: string[] = []
|
|
10
|
+
|
|
11
|
+
function makeTmpDir(): string {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), 'auths-test-'))
|
|
13
|
+
tmpDirs.push(dir)
|
|
14
|
+
return dir
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
for (const dir of tmpDirs) {
|
|
19
|
+
rmSync(dir, { recursive: true, force: true })
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function makeClient(dir?: string): Auths {
|
|
24
|
+
const repoPath = dir ?? makeTmpDir()
|
|
25
|
+
return new Auths({ repoPath, passphrase: 'Test-pass-123' })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function initGitRepo(dir: string): void {
|
|
29
|
+
mkdirSync(dir, { recursive: true })
|
|
30
|
+
execSync('git init', { cwd: dir, stdio: 'pipe' })
|
|
31
|
+
execSync('git config user.name "Test User"', { cwd: dir, stdio: 'pipe' })
|
|
32
|
+
execSync('git config user.email "test@example.com"', { cwd: dir, stdio: 'pipe' })
|
|
33
|
+
execSync('git config commit.gpgsign false', { cwd: dir, stdio: 'pipe' })
|
|
34
|
+
writeFileSync(join(dir, 'README.md'), '# Test Repo\n')
|
|
35
|
+
execSync('git add .', { cwd: dir, stdio: 'pipe' })
|
|
36
|
+
execSync('git commit -m "initial commit"', { cwd: dir, stdio: 'pipe' })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('identity lifecycle', () => {
|
|
40
|
+
let auths: Auths
|
|
41
|
+
let identity: Identity
|
|
42
|
+
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
auths = makeClient()
|
|
45
|
+
identity = auths.identities.create({ label: 'test-key' })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('creates identity with did:keri prefix', () => {
|
|
49
|
+
expect(identity.did).toMatch(/^did:keri:/)
|
|
50
|
+
expect(identity.keyAlias).toBeDefined()
|
|
51
|
+
expect(identity.publicKey).toBeDefined()
|
|
52
|
+
expect(identity.publicKey.length).toBe(64)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('getPublicKey returns hex string', () => {
|
|
56
|
+
const pk = auths.getPublicKey({ identityDid: identity.did })
|
|
57
|
+
expect(pk).toBe(identity.publicKey)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('delegates an agent', () => {
|
|
61
|
+
const agent = auths.identities.delegateAgent({
|
|
62
|
+
identityDid: identity.did,
|
|
63
|
+
name: 'ci-bot',
|
|
64
|
+
capabilities: ['sign'],
|
|
65
|
+
})
|
|
66
|
+
expect(agent.did).toMatch(/^did:key:/)
|
|
67
|
+
expect(agent.keyAlias).toBeDefined()
|
|
68
|
+
expect(agent.attestation).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('creates standalone agent', () => {
|
|
72
|
+
const agent = auths.identities.createAgent({
|
|
73
|
+
name: 'standalone',
|
|
74
|
+
capabilities: ['sign'],
|
|
75
|
+
})
|
|
76
|
+
expect(agent.did).toMatch(/^did:keri:/)
|
|
77
|
+
expect(agent.keyAlias).toBeDefined()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('device lifecycle', () => {
|
|
82
|
+
it('link and revoke device', () => {
|
|
83
|
+
const auths = makeClient()
|
|
84
|
+
const identity = auths.identities.create({ label: 'dev-test' })
|
|
85
|
+
|
|
86
|
+
const device = auths.devices.link({
|
|
87
|
+
identityDid: identity.did,
|
|
88
|
+
capabilities: ['sign'],
|
|
89
|
+
expiresInDays: 90,
|
|
90
|
+
})
|
|
91
|
+
expect(device.did).toMatch(/^did:key:/)
|
|
92
|
+
expect(device.attestationId).toBeDefined()
|
|
93
|
+
|
|
94
|
+
auths.devices.revoke({
|
|
95
|
+
deviceDid: device.did,
|
|
96
|
+
identityDid: identity.did,
|
|
97
|
+
note: 'test revocation',
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('extend device authorization', () => {
|
|
102
|
+
const auths = makeClient()
|
|
103
|
+
const identity = auths.identities.create({ label: 'ext-test' })
|
|
104
|
+
const device = auths.devices.link({
|
|
105
|
+
identityDid: identity.did,
|
|
106
|
+
capabilities: ['sign'],
|
|
107
|
+
expiresInDays: 30,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const ext = auths.devices.extend({
|
|
111
|
+
deviceDid: device.did,
|
|
112
|
+
identityDid: identity.did,
|
|
113
|
+
days: 60,
|
|
114
|
+
})
|
|
115
|
+
expect(ext.deviceDid).toBe(device.did)
|
|
116
|
+
expect(ext.newExpiresAt).toBeDefined()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('signing', () => {
|
|
121
|
+
let auths: Auths
|
|
122
|
+
let identity: Identity
|
|
123
|
+
|
|
124
|
+
beforeAll(() => {
|
|
125
|
+
auths = makeClient()
|
|
126
|
+
identity = auths.identities.create({ label: 'sign-test' })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('sign as identity returns signature', () => {
|
|
130
|
+
const result = auths.signAs({
|
|
131
|
+
message: Buffer.from('hello world'),
|
|
132
|
+
identityDid: identity.did,
|
|
133
|
+
})
|
|
134
|
+
expect(result.signature).toBeDefined()
|
|
135
|
+
expect(result.signerDid).toBeDefined()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('sign action as identity returns envelope', () => {
|
|
139
|
+
const result = auths.signActionAs({
|
|
140
|
+
actionType: 'tool_call',
|
|
141
|
+
payloadJson: '{"tool":"read_file"}',
|
|
142
|
+
identityDid: identity.did,
|
|
143
|
+
})
|
|
144
|
+
expect(result.envelopeJson).toBeDefined()
|
|
145
|
+
expect(result.signatureHex).toBeDefined()
|
|
146
|
+
expect(result.signerDid).toBeDefined()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('trust', () => {
|
|
151
|
+
it('pin and list', () => {
|
|
152
|
+
const auths = makeClient()
|
|
153
|
+
const identity = auths.identities.create({ label: 'trust-test' })
|
|
154
|
+
|
|
155
|
+
const entry = auths.trust.pin({ did: identity.did, label: 'my-peer' })
|
|
156
|
+
expect(entry.did).toBe(identity.did)
|
|
157
|
+
expect(entry.label).toBe('my-peer')
|
|
158
|
+
expect(entry.trustLevel).toBeDefined()
|
|
159
|
+
|
|
160
|
+
const entries = auths.trust.list()
|
|
161
|
+
expect(entries.length).toBeGreaterThanOrEqual(1)
|
|
162
|
+
expect(entries.some(e => e.did === identity.did)).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('remove pinned identity', () => {
|
|
166
|
+
const auths = makeClient()
|
|
167
|
+
const identity = auths.identities.create({ label: 'trust-rm' })
|
|
168
|
+
auths.trust.pin({ did: identity.did })
|
|
169
|
+
auths.trust.remove(identity.did)
|
|
170
|
+
const result = auths.trust.get(identity.did)
|
|
171
|
+
expect(result).toBeNull()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('get returns null for unknown', () => {
|
|
175
|
+
const auths = makeClient()
|
|
176
|
+
const result = auths.trust.get('did:keri:ENOTREAL')
|
|
177
|
+
expect(result).toBeNull()
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('witness', () => {
|
|
182
|
+
it('add and list witnesses', () => {
|
|
183
|
+
const auths = makeClient()
|
|
184
|
+
auths.identities.create({ label: 'witness-test' })
|
|
185
|
+
|
|
186
|
+
const w = auths.witnesses.add({ url: 'http://witness.example.com:3333' })
|
|
187
|
+
expect(w.url).toBe('http://witness.example.com:3333')
|
|
188
|
+
|
|
189
|
+
const witnesses = auths.witnesses.list()
|
|
190
|
+
expect(witnesses.length).toBe(1)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('remove witness', () => {
|
|
194
|
+
const auths = makeClient()
|
|
195
|
+
auths.identities.create({ label: 'witness-rm' })
|
|
196
|
+
|
|
197
|
+
auths.witnesses.add({ url: 'http://witness.example.com:3333' })
|
|
198
|
+
auths.witnesses.remove('http://witness.example.com:3333')
|
|
199
|
+
|
|
200
|
+
expect(auths.witnesses.list().length).toBe(0)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('duplicate add is idempotent', () => {
|
|
204
|
+
const auths = makeClient()
|
|
205
|
+
auths.identities.create({ label: 'witness-dup' })
|
|
206
|
+
|
|
207
|
+
auths.witnesses.add({ url: 'http://witness.example.com:3333' })
|
|
208
|
+
auths.witnesses.add({ url: 'http://witness.example.com:3333' })
|
|
209
|
+
|
|
210
|
+
expect(auths.witnesses.list().length).toBe(1)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('attestations', () => {
|
|
215
|
+
it('list returns array', () => {
|
|
216
|
+
const auths = makeClient()
|
|
217
|
+
auths.identities.create({ label: 'att-test' })
|
|
218
|
+
const atts = auths.attestations.list()
|
|
219
|
+
expect(Array.isArray(atts)).toBe(true)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('audit', () => {
|
|
224
|
+
it('generates report for unsigned repo', () => {
|
|
225
|
+
const auths = makeClient()
|
|
226
|
+
const gitDir = join(makeTmpDir(), 'git-repo')
|
|
227
|
+
initGitRepo(gitDir)
|
|
228
|
+
|
|
229
|
+
const report = auths.audit.report({ targetRepoPath: gitDir })
|
|
230
|
+
expect(report.summary.total_commits).toBe(1)
|
|
231
|
+
expect(report.summary.unsigned_commits).toBe(1)
|
|
232
|
+
expect(report.summary.signed_commits).toBe(0)
|
|
233
|
+
expect(Array.isArray(report.commits)).toBe(true)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('isCompliant returns false for unsigned', () => {
|
|
237
|
+
const auths = makeClient()
|
|
238
|
+
const gitDir = join(makeTmpDir(), 'git-repo')
|
|
239
|
+
initGitRepo(gitDir)
|
|
240
|
+
expect(auths.audit.isCompliant({ targetRepoPath: gitDir })).toBe(false)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('org', () => {
|
|
245
|
+
it('creates organization', () => {
|
|
246
|
+
const auths = makeClient()
|
|
247
|
+
auths.identities.create({ label: 'org-admin' })
|
|
248
|
+
|
|
249
|
+
const org = auths.orgs.create({ label: 'my-team' })
|
|
250
|
+
expect(org.orgDid).toMatch(/^did:keri:/)
|
|
251
|
+
expect(org.label).toBe('my-team')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('add and list members', () => {
|
|
255
|
+
const adminDir = makeTmpDir()
|
|
256
|
+
const admin = makeClient(adminDir)
|
|
257
|
+
admin.identities.create({ label: 'admin' })
|
|
258
|
+
const org = admin.orgs.create({ label: 'team' })
|
|
259
|
+
|
|
260
|
+
const devDir = makeTmpDir()
|
|
261
|
+
const devClient = makeClient(devDir)
|
|
262
|
+
const devId = devClient.identities.create({ label: 'dev' })
|
|
263
|
+
|
|
264
|
+
const member = admin.orgs.addMember({
|
|
265
|
+
orgDid: org.orgDid,
|
|
266
|
+
memberDid: devId.did,
|
|
267
|
+
role: 'member',
|
|
268
|
+
memberPublicKeyHex: devId.publicKey,
|
|
269
|
+
})
|
|
270
|
+
expect(member.memberDid).toBe(devId.did)
|
|
271
|
+
expect(member.role).toBe('member')
|
|
272
|
+
expect(member.revoked).toBe(false)
|
|
273
|
+
|
|
274
|
+
const members = admin.orgs.listMembers({ orgDid: org.orgDid })
|
|
275
|
+
expect(members.length).toBeGreaterThanOrEqual(1)
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe('doctor', () => {
|
|
280
|
+
it('returns diagnostics string', () => {
|
|
281
|
+
const auths = makeClient()
|
|
282
|
+
const result = auths.doctor()
|
|
283
|
+
expect(typeof result).toBe('string')
|
|
284
|
+
expect(result.length).toBeGreaterThan(0)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('version', () => {
|
|
289
|
+
it('returns version string', () => {
|
|
290
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
291
|
+
const native = require('../index.js')
|
|
292
|
+
expect(typeof native.version()).toBe('string')
|
|
293
|
+
expect(native.version()).toMatch(/^\d+\.\d+\.\d+/)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('pairing', () => {
|
|
298
|
+
it('creates session and stops cleanly', async () => {
|
|
299
|
+
const auths = makeClient()
|
|
300
|
+
auths.identities.create({ label: 'pair-test' })
|
|
301
|
+
|
|
302
|
+
const session = await auths.pairing.createSession({
|
|
303
|
+
bindAddress: '127.0.0.1',
|
|
304
|
+
enableMdns: false,
|
|
305
|
+
capabilities: ['sign:commit'],
|
|
306
|
+
})
|
|
307
|
+
expect(session.shortCode.length).toBe(6)
|
|
308
|
+
expect(session.endpoint).toMatch(/^http:\/\/127\.0\.0\.1:/)
|
|
309
|
+
expect(session.controllerDid).toMatch(/^did:keri:/)
|
|
310
|
+
|
|
311
|
+
await auths.pairing.stop()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('stop is idempotent', async () => {
|
|
315
|
+
const auths = makeClient()
|
|
316
|
+
auths.identities.create({ label: 'pair-stop' })
|
|
317
|
+
|
|
318
|
+
await auths.pairing.createSession({
|
|
319
|
+
bindAddress: '127.0.0.1',
|
|
320
|
+
enableMdns: false,
|
|
321
|
+
})
|
|
322
|
+
await auths.pairing.stop()
|
|
323
|
+
await auths.pairing.stop()
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('multiple concurrent sessions on separate clients', async () => {
|
|
327
|
+
const auths1 = makeClient()
|
|
328
|
+
auths1.identities.create({ label: 'pair-multi-1' })
|
|
329
|
+
|
|
330
|
+
const auths2 = makeClient()
|
|
331
|
+
auths2.identities.create({ label: 'pair-multi-2' })
|
|
332
|
+
|
|
333
|
+
const session1 = await auths1.pairing.createSession({
|
|
334
|
+
bindAddress: '127.0.0.1',
|
|
335
|
+
enableMdns: false,
|
|
336
|
+
})
|
|
337
|
+
const session2 = await auths2.pairing.createSession({
|
|
338
|
+
bindAddress: '127.0.0.1',
|
|
339
|
+
enableMdns: false,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
expect(session1.endpoint).not.toBe(session2.endpoint)
|
|
343
|
+
expect(session1.shortCode).not.toBe(session2.shortCode)
|
|
344
|
+
|
|
345
|
+
await auths1.pairing.stop()
|
|
346
|
+
await auths2.pairing.stop()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('waitForResponse without session throws', async () => {
|
|
350
|
+
const auths = makeClient()
|
|
351
|
+
auths.identities.create({ label: 'pair-no-session' })
|
|
352
|
+
|
|
353
|
+
await expect(auths.pairing.waitForResponse()).rejects.toThrow(
|
|
354
|
+
/No active pairing session/,
|
|
355
|
+
)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('complete without session throws', async () => {
|
|
359
|
+
const auths = makeClient()
|
|
360
|
+
auths.identities.create({ label: 'pair-no-session-complete' })
|
|
361
|
+
|
|
362
|
+
await expect(
|
|
363
|
+
auths.pairing.complete({
|
|
364
|
+
deviceDid: 'did:key:fake',
|
|
365
|
+
devicePublicKeyHex: 'a'.repeat(64),
|
|
366
|
+
}),
|
|
367
|
+
).rejects.toThrow(/No active pairing session/)
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('verify async', () => {
|
|
372
|
+
it('verifyAttestation returns a Promise', async () => {
|
|
373
|
+
const { verifyAttestation } = await import('../lib/verify')
|
|
374
|
+
const result = verifyAttestation('{}', 'a'.repeat(64))
|
|
375
|
+
expect(result).toBeInstanceOf(Promise)
|
|
376
|
+
const resolved = await result
|
|
377
|
+
expect(resolved.valid).toBe(false)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('verifyChain returns a Promise', async () => {
|
|
381
|
+
const { verifyChain } = await import('../lib/verify')
|
|
382
|
+
const result = verifyChain([], 'a'.repeat(64))
|
|
383
|
+
expect(result).toBeInstanceOf(Promise)
|
|
384
|
+
const resolved = await result
|
|
385
|
+
expect(resolved.status).toBeDefined()
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('agent attestation', () => {
|
|
390
|
+
it('createAgent produces a signed attestation with required fields', () => {
|
|
391
|
+
const auths = makeClient()
|
|
392
|
+
auths.identities.create({ label: 'agent-att-test' })
|
|
393
|
+
const agent = auths.identities.createAgent({
|
|
394
|
+
name: 'test-bot',
|
|
395
|
+
capabilities: ['sign'],
|
|
396
|
+
})
|
|
397
|
+
expect(agent.attestation).toBeDefined()
|
|
398
|
+
const att = JSON.parse(agent.attestation)
|
|
399
|
+
expect(att.issuer).toBeDefined()
|
|
400
|
+
expect(att.subject).toBeDefined()
|
|
401
|
+
expect(att.device_signature).toBeDefined()
|
|
402
|
+
expect(att.identity_signature).toBeDefined()
|
|
403
|
+
expect(att.rid).toBeDefined()
|
|
404
|
+
expect(att.version).toBeDefined()
|
|
405
|
+
expect(att.device_public_key).toBeDefined()
|
|
406
|
+
})
|
|
407
|
+
})
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { PolicyBuilder, compilePolicy, evaluatePolicy } from '../lib/policy'
|
|
3
|
+
|
|
4
|
+
describe('PolicyBuilder', () => {
|
|
5
|
+
it('standard factory creates not_revoked + not_expired + capability', () => {
|
|
6
|
+
const json = PolicyBuilder.standard('sign_commit').toJson()
|
|
7
|
+
const parsed = JSON.parse(json)
|
|
8
|
+
expect(parsed.op).toBe('And')
|
|
9
|
+
expect(parsed.args).toHaveLength(3)
|
|
10
|
+
expect(parsed.args[0].op).toBe('NotRevoked')
|
|
11
|
+
expect(parsed.args[1].op).toBe('NotExpired')
|
|
12
|
+
expect(parsed.args[2].op).toBe('HasCapability')
|
|
13
|
+
expect(parsed.args[2].args).toBe('sign_commit')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('fluent chaining builds correct expression', () => {
|
|
17
|
+
const json = new PolicyBuilder()
|
|
18
|
+
.notRevoked()
|
|
19
|
+
.requireCapability('sign')
|
|
20
|
+
.requireIssuer('did:keri:EOrg')
|
|
21
|
+
.requireHuman()
|
|
22
|
+
.maxChainDepth(3)
|
|
23
|
+
.toJson()
|
|
24
|
+
const parsed = JSON.parse(json)
|
|
25
|
+
expect(parsed.op).toBe('And')
|
|
26
|
+
expect(parsed.args).toHaveLength(5)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('anyOf creates OR combinator', () => {
|
|
30
|
+
const a = PolicyBuilder.standard('admin')
|
|
31
|
+
const b = PolicyBuilder.standard('superadmin')
|
|
32
|
+
const json = PolicyBuilder.anyOf(a, b).toJson()
|
|
33
|
+
const parsed = JSON.parse(json)
|
|
34
|
+
expect(parsed.op).toBe('And')
|
|
35
|
+
expect(parsed.args[0].op).toBe('Or')
|
|
36
|
+
expect(parsed.args[0].args).toHaveLength(2)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('negate wraps in Not', () => {
|
|
40
|
+
const json = new PolicyBuilder().notRevoked().negate().toJson()
|
|
41
|
+
const parsed = JSON.parse(json)
|
|
42
|
+
expect(parsed.args[0].op).toBe('Not')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('orPolicy combines two builders', () => {
|
|
46
|
+
const a = new PolicyBuilder().requireCapability('admin')
|
|
47
|
+
const b = new PolicyBuilder().requireCapability('superadmin')
|
|
48
|
+
const json = a.orPolicy(b).toJson()
|
|
49
|
+
const parsed = JSON.parse(json)
|
|
50
|
+
expect(parsed.args[0].op).toBe('Or')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('empty builder throws on build', () => {
|
|
54
|
+
expect(() => new PolicyBuilder().build()).toThrow('empty policy')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('empty builder throws on toJson', () => {
|
|
58
|
+
expect(() => new PolicyBuilder().toJson()).toThrow('empty policy')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('expiresAfter adds correct predicate', () => {
|
|
62
|
+
const json = new PolicyBuilder().expiresAfter(3600).toJson()
|
|
63
|
+
const parsed = JSON.parse(json)
|
|
64
|
+
expect(parsed.args[0].op).toBe('ExpiresAfter')
|
|
65
|
+
expect(parsed.args[0].args).toBe(3600)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('issuedWithin adds correct predicate', () => {
|
|
69
|
+
const json = new PolicyBuilder().issuedWithin(86400).toJson()
|
|
70
|
+
const parsed = JSON.parse(json)
|
|
71
|
+
expect(parsed.args[0].op).toBe('IssuedWithin')
|
|
72
|
+
expect(parsed.args[0].args).toBe(86400)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('requireAllCapabilities adds multiple HasCapability', () => {
|
|
76
|
+
const json = new PolicyBuilder().requireAllCapabilities(['sign', 'deploy']).toJson()
|
|
77
|
+
const parsed = JSON.parse(json)
|
|
78
|
+
expect(parsed.args).toHaveLength(2)
|
|
79
|
+
expect(parsed.args[0].op).toBe('HasCapability')
|
|
80
|
+
expect(parsed.args[1].op).toBe('HasCapability')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('requireAnyCapability creates OR', () => {
|
|
84
|
+
const json = new PolicyBuilder().requireAnyCapability(['sign', 'deploy']).toJson()
|
|
85
|
+
const parsed = JSON.parse(json)
|
|
86
|
+
expect(parsed.args[0].op).toBe('Or')
|
|
87
|
+
expect(parsed.args[0].args).toHaveLength(2)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('requireIssuerIn creates OR of IssuerIs', () => {
|
|
91
|
+
const json = new PolicyBuilder().requireIssuerIn(['did:keri:A', 'did:keri:B']).toJson()
|
|
92
|
+
const parsed = JSON.parse(json)
|
|
93
|
+
expect(parsed.args[0].op).toBe('Or')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('signer type predicates', () => {
|
|
97
|
+
expect(JSON.parse(new PolicyBuilder().requireAgent().toJson()).args[0].op).toBe('IsAgent')
|
|
98
|
+
expect(JSON.parse(new PolicyBuilder().requireHuman().toJson()).args[0].op).toBe('IsHuman')
|
|
99
|
+
expect(JSON.parse(new PolicyBuilder().requireWorkload().toJson()).args[0].op).toBe('IsWorkload')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('scope predicates', () => {
|
|
103
|
+
expect(JSON.parse(new PolicyBuilder().requireRepo('org/repo').toJson()).args[0].op).toBe('RepoIs')
|
|
104
|
+
expect(JSON.parse(new PolicyBuilder().requireEnv('production').toJson()).args[0].op).toBe('EnvIs')
|
|
105
|
+
expect(JSON.parse(new PolicyBuilder().refMatches('refs/heads/*').toJson()).args[0].op).toBe('RefMatches')
|
|
106
|
+
expect(JSON.parse(new PolicyBuilder().pathAllowed(['src/**']).toJson()).args[0].op).toBe('PathAllowed')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('attribute predicates', () => {
|
|
110
|
+
expect(JSON.parse(new PolicyBuilder().attrEquals('team', 'infra').toJson()).args[0].op).toBe('AttrEquals')
|
|
111
|
+
expect(JSON.parse(new PolicyBuilder().attrIn('team', ['infra', 'platform']).toJson()).args[0].op).toBe('AttrIn')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('compilePolicy', () => {
|
|
116
|
+
it('compiles a valid policy expression', () => {
|
|
117
|
+
const result = compilePolicy('{"op":"NotRevoked"}')
|
|
118
|
+
expect(result).toBeDefined()
|
|
119
|
+
expect(typeof result).toBe('string')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('rejects invalid JSON', () => {
|
|
123
|
+
expect(() => compilePolicy('not json')).toThrow()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('rejects unknown op', () => {
|
|
127
|
+
expect(() => compilePolicy('{"op":"BogusOp"}')).toThrow()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('evaluatePolicy', () => {
|
|
132
|
+
it('allows when policy is True', () => {
|
|
133
|
+
const compiled = compilePolicy('{"op":"True"}')
|
|
134
|
+
const decision = evaluatePolicy(compiled, {
|
|
135
|
+
issuer: 'did:keri:ETest',
|
|
136
|
+
subject: 'did:key:zTest',
|
|
137
|
+
})
|
|
138
|
+
expect(decision.outcome).toBe('allow')
|
|
139
|
+
expect(decision.allowed).toBe(true)
|
|
140
|
+
expect(decision.denied).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('denies when policy is False', () => {
|
|
144
|
+
const compiled = compilePolicy('{"op":"False"}')
|
|
145
|
+
const decision = evaluatePolicy(compiled, {
|
|
146
|
+
issuer: 'did:keri:ETest',
|
|
147
|
+
subject: 'did:key:zTest',
|
|
148
|
+
})
|
|
149
|
+
expect(decision.outcome).toBe('deny')
|
|
150
|
+
expect(decision.allowed).toBe(false)
|
|
151
|
+
expect(decision.denied).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('checks capability present', () => {
|
|
155
|
+
const compiled = compilePolicy('{"op":"HasCapability","args":"sign_commit"}')
|
|
156
|
+
const decision = evaluatePolicy(compiled, {
|
|
157
|
+
issuer: 'did:keri:ETest',
|
|
158
|
+
subject: 'did:key:zTest',
|
|
159
|
+
capabilities: ['sign_commit'],
|
|
160
|
+
})
|
|
161
|
+
expect(decision.allowed).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('checks capability missing', () => {
|
|
165
|
+
const compiled = compilePolicy('{"op":"HasCapability","args":"sign_commit"}')
|
|
166
|
+
const decision = evaluatePolicy(compiled, {
|
|
167
|
+
issuer: 'did:keri:ETest',
|
|
168
|
+
subject: 'did:key:zTest',
|
|
169
|
+
capabilities: ['read'],
|
|
170
|
+
})
|
|
171
|
+
expect(decision.denied).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('checks NotRevoked passes', () => {
|
|
175
|
+
const compiled = compilePolicy('{"op":"NotRevoked"}')
|
|
176
|
+
const decision = evaluatePolicy(compiled, {
|
|
177
|
+
issuer: 'did:keri:ETest',
|
|
178
|
+
subject: 'did:key:zTest',
|
|
179
|
+
revoked: false,
|
|
180
|
+
})
|
|
181
|
+
expect(decision.allowed).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('checks NotRevoked denied when revoked', () => {
|
|
185
|
+
const compiled = compilePolicy('{"op":"NotRevoked"}')
|
|
186
|
+
const decision = evaluatePolicy(compiled, {
|
|
187
|
+
issuer: 'did:keri:ETest',
|
|
188
|
+
subject: 'did:key:zTest',
|
|
189
|
+
revoked: true,
|
|
190
|
+
})
|
|
191
|
+
expect(decision.denied).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('PolicyBuilder.evaluate convenience method', () => {
|
|
195
|
+
const decision = PolicyBuilder.standard('sign_commit').evaluate({
|
|
196
|
+
issuer: 'did:keri:ETest',
|
|
197
|
+
subject: 'did:key:zTest',
|
|
198
|
+
capabilities: ['sign_commit'],
|
|
199
|
+
})
|
|
200
|
+
expect(decision.allowed).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
verifyAttestation,
|
|
4
|
+
verifyChain,
|
|
5
|
+
verifyDeviceAuthorization,
|
|
6
|
+
verifyAttestationWithCapability,
|
|
7
|
+
verifyChainWithCapability,
|
|
8
|
+
verifyAtTime,
|
|
9
|
+
verifyAtTimeWithCapability,
|
|
10
|
+
} from '../lib/verify'
|
|
11
|
+
import type { VerificationResult, VerificationReport } from '../lib/verify'
|
|
12
|
+
|
|
13
|
+
describe('verifyAttestation', () => {
|
|
14
|
+
it('invalid JSON returns error result', async () => {
|
|
15
|
+
const result: VerificationResult = await verifyAttestation('not valid json', 'a'.repeat(64))
|
|
16
|
+
expect(result.valid).toBe(false)
|
|
17
|
+
expect(result.error).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('invalid hex key throws VerificationError', async () => {
|
|
21
|
+
await expect(verifyAttestation('{}', 'not-hex')).rejects.toThrow()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('wrong key length throws VerificationError', async () => {
|
|
25
|
+
await expect(verifyAttestation('{}', 'abcd')).rejects.toThrow()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('empty attestation returns invalid', async () => {
|
|
29
|
+
const result = await verifyAttestation('{}', 'a'.repeat(64))
|
|
30
|
+
expect(result.valid).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('verifyChain', () => {
|
|
35
|
+
it('empty chain returns report', async () => {
|
|
36
|
+
const report: VerificationReport = await verifyChain([], 'a'.repeat(64))
|
|
37
|
+
expect(report.status).toBeDefined()
|
|
38
|
+
expect(report.status.statusType).toBeDefined()
|
|
39
|
+
expect(Array.isArray(report.chain)).toBe(true)
|
|
40
|
+
expect(Array.isArray(report.warnings)).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('invalid JSON in chain throws', async () => {
|
|
44
|
+
await expect(verifyChain(['not valid json'], 'a'.repeat(64))).rejects.toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('invalid root key throws', async () => {
|
|
48
|
+
await expect(verifyChain([], 'not-hex')).rejects.toThrow()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('verifyDeviceAuthorization', () => {
|
|
53
|
+
it('empty attestations returns report', async () => {
|
|
54
|
+
const report = await verifyDeviceAuthorization(
|
|
55
|
+
'did:keri:Eidentity', 'did:key:zDevice', [], 'a'.repeat(64),
|
|
56
|
+
)
|
|
57
|
+
expect(report.status).toBeDefined()
|
|
58
|
+
expect(report.status.statusType).not.toBe('Valid')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('verifyAttestationWithCapability', () => {
|
|
63
|
+
it('invalid attestation returns error', async () => {
|
|
64
|
+
const result = await verifyAttestationWithCapability('{}', 'a'.repeat(64), 'sign')
|
|
65
|
+
expect(result.valid).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('verifyChainWithCapability', () => {
|
|
70
|
+
it('empty chain returns report', async () => {
|
|
71
|
+
const report = await verifyChainWithCapability([], 'a'.repeat(64), 'sign')
|
|
72
|
+
expect(report.status).toBeDefined()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('verifyAtTime', () => {
|
|
77
|
+
it('invalid attestation returns error', async () => {
|
|
78
|
+
const result = await verifyAtTime('{}', 'a'.repeat(64), '2025-01-01T00:00:00Z')
|
|
79
|
+
expect(result.valid).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('verifyAtTimeWithCapability', () => {
|
|
84
|
+
it('invalid attestation returns error', async () => {
|
|
85
|
+
const result = await verifyAtTimeWithCapability('{}', 'a'.repeat(64), '2025-01-01T00:00:00Z', 'sign')
|
|
86
|
+
expect(result.valid).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
})
|