beam-protocol-sdk 0.2.2 → 0.3.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/package.json +3 -23
- package/src/client.ts +383 -0
- package/src/directory.ts +91 -0
- package/src/frames.ts +161 -0
- package/src/identity.ts +85 -0
- package/src/index.ts +25 -0
- package/src/types.ts +71 -0
- package/tests/client.test.ts +217 -0
- package/tests/directory.test.ts +202 -0
- package/tests/frames.test.ts +213 -0
- package/tests/identity.test.ts +130 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +8 -0
- package/README.md +0 -177
- package/dist/client.d.ts +0 -96
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -293
- package/dist/client.js.map +0 -1
- package/dist/directory.d.ts +0 -15
- package/dist/directory.d.ts.map +0 -1
- package/dist/directory.js +0 -82
- package/dist/directory.js.map +0 -1
- package/dist/frames.d.ts +0 -29
- package/dist/frames.d.ts.map +0 -1
- package/dist/frames.js +0 -133
- package/dist/frames.js.map +0 -1
- package/dist/identity.d.ts +0 -19
- package/dist/identity.d.ts.map +0 -1
- package/dist/identity.js +0 -65
- package/dist/identity.js.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -60
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
MAX_FRAME_SIZE,
|
|
4
|
+
REPLAY_WINDOW_MS,
|
|
5
|
+
canonicalizeFrame,
|
|
6
|
+
createIntentFrame,
|
|
7
|
+
createResultFrame,
|
|
8
|
+
signFrame,
|
|
9
|
+
validateIntentFrame,
|
|
10
|
+
validateResultFrame,
|
|
11
|
+
} from '../src/frames.js'
|
|
12
|
+
import { BeamIdentity } from '../src/identity.js'
|
|
13
|
+
import type { IntentFrame } from '../src/types.js'
|
|
14
|
+
|
|
15
|
+
describe('frames', () => {
|
|
16
|
+
const sender = BeamIdentity.generate({ agentName: 'alice', orgName: 'acme' })
|
|
17
|
+
const receiver = BeamIdentity.generate({ agentName: 'bob', orgName: 'acme' })
|
|
18
|
+
|
|
19
|
+
it('createIntentFrame() signs a valid intent frame', () => {
|
|
20
|
+
const frame = createIntentFrame(
|
|
21
|
+
{
|
|
22
|
+
intent: 'task.delegate',
|
|
23
|
+
from: sender.beamId,
|
|
24
|
+
to: receiver.beamId,
|
|
25
|
+
payload: { task: 'Review proposal', priority: 'high' },
|
|
26
|
+
},
|
|
27
|
+
sender,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
expect(frame.signature).toBeTruthy()
|
|
31
|
+
expect(frame.v).toBe('1')
|
|
32
|
+
expect(frame.nonce).toMatch(/^[0-9a-f-]{36}$/)
|
|
33
|
+
expect(validateIntentFrame(frame, sender.publicKeyBase64)).toEqual({ valid: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('signFrame() produces signatures that fail after tampering', () => {
|
|
37
|
+
const frame: IntentFrame = {
|
|
38
|
+
v: '1',
|
|
39
|
+
intent: 'agent.ping',
|
|
40
|
+
from: sender.beamId,
|
|
41
|
+
to: receiver.beamId,
|
|
42
|
+
payload: { message: 'hello' },
|
|
43
|
+
nonce: BeamIdentity.generateNonce(),
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
signFrame(frame, sender.export().privateKeyBase64)
|
|
48
|
+
expect(validateIntentFrame(frame, sender.publicKeyBase64)).toEqual({ valid: true })
|
|
49
|
+
|
|
50
|
+
frame.payload.message = 'tampered'
|
|
51
|
+
expect(validateIntentFrame(frame, sender.publicKeyBase64)).toEqual({
|
|
52
|
+
valid: false,
|
|
53
|
+
error: 'Signature verification failed',
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('createResultFrame() signs a valid result frame', () => {
|
|
58
|
+
const frame = createResultFrame(
|
|
59
|
+
{
|
|
60
|
+
nonce: BeamIdentity.generateNonce(),
|
|
61
|
+
success: true,
|
|
62
|
+
payload: { accepted: true, estimatedCompletion: 'soon' },
|
|
63
|
+
latency: 25,
|
|
64
|
+
},
|
|
65
|
+
receiver,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
expect(frame.signature).toBeTruthy()
|
|
69
|
+
expect(validateResultFrame(frame, receiver.publicKeyBase64)).toEqual({ valid: true })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('validateResultFrame() rejects tampered result frames', () => {
|
|
73
|
+
const frame = createResultFrame(
|
|
74
|
+
{
|
|
75
|
+
nonce: BeamIdentity.generateNonce(),
|
|
76
|
+
success: false,
|
|
77
|
+
error: 'Denied',
|
|
78
|
+
errorCode: 'ACCESS_DENIED',
|
|
79
|
+
},
|
|
80
|
+
receiver,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const tampered = { ...frame, error: 'Allowed now' }
|
|
84
|
+
expect(validateResultFrame(tampered, receiver.publicKeyBase64)).toEqual({
|
|
85
|
+
valid: false,
|
|
86
|
+
error: 'Signature verification failed',
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('validateIntentFrame() rejects missing or malformed fields', () => {
|
|
91
|
+
const validFrame = createIntentFrame(
|
|
92
|
+
{
|
|
93
|
+
intent: 'agent.introduce',
|
|
94
|
+
from: sender.beamId,
|
|
95
|
+
to: receiver.beamId,
|
|
96
|
+
payload: { question: 'Who are you?' },
|
|
97
|
+
},
|
|
98
|
+
sender,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const cases: Array<[string, unknown, string]> = [
|
|
102
|
+
['non-object frame', null, 'Frame must be an object'],
|
|
103
|
+
['wrong protocol version', { ...validFrame, v: '2' }, 'Invalid protocol version'],
|
|
104
|
+
['missing intent', { ...validFrame, intent: '' }, 'Missing or empty intent'],
|
|
105
|
+
['invalid sender id', { ...validFrame, from: 'Alice@acme.beam.directory' }, 'Invalid from Beam ID'],
|
|
106
|
+
['invalid receiver id', { ...validFrame, to: 'bob@example.com' }, 'Invalid to Beam ID'],
|
|
107
|
+
['missing nonce', { ...validFrame, nonce: '' }, 'Missing nonce'],
|
|
108
|
+
['missing timestamp', { ...validFrame, timestamp: 123 }, 'Missing timestamp'],
|
|
109
|
+
['non-object payload', { ...validFrame, payload: [] }, 'Payload must be an object'],
|
|
110
|
+
['missing signature', { ...validFrame, signature: undefined }, 'Missing signature'],
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for (const [, frame, error] of cases) {
|
|
114
|
+
expect(validateIntentFrame(frame, sender.publicKeyBase64)).toEqual({ valid: false, error })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('validateResultFrame() rejects missing required fields', () => {
|
|
119
|
+
const frame = createResultFrame(
|
|
120
|
+
{
|
|
121
|
+
nonce: BeamIdentity.generateNonce(),
|
|
122
|
+
success: true,
|
|
123
|
+
},
|
|
124
|
+
receiver,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(validateResultFrame({ ...frame, success: undefined }, receiver.publicKeyBase64)).toEqual({
|
|
128
|
+
valid: false,
|
|
129
|
+
error: 'Missing success boolean',
|
|
130
|
+
})
|
|
131
|
+
expect(validateResultFrame({ ...frame, nonce: '' }, receiver.publicKeyBase64)).toEqual({
|
|
132
|
+
valid: false,
|
|
133
|
+
error: 'Missing nonce',
|
|
134
|
+
})
|
|
135
|
+
expect(validateResultFrame({ ...frame, signature: undefined }, receiver.publicKeyBase64)).toEqual({
|
|
136
|
+
valid: false,
|
|
137
|
+
error: 'Missing signature',
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('enforces MAX_FRAME_SIZE for intent frames', () => {
|
|
142
|
+
const oversizedFrame = createIntentFrame(
|
|
143
|
+
{
|
|
144
|
+
intent: 'system.broadcast',
|
|
145
|
+
from: sender.beamId,
|
|
146
|
+
to: receiver.beamId,
|
|
147
|
+
payload: { message: 'x'.repeat(MAX_FRAME_SIZE) },
|
|
148
|
+
},
|
|
149
|
+
sender,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const result = validateIntentFrame(oversizedFrame, sender.publicKeyBase64)
|
|
153
|
+
expect(result.valid).toBe(false)
|
|
154
|
+
expect(result.error).toContain(`exceeds limit of ${MAX_FRAME_SIZE} bytes`)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('enforces REPLAY_WINDOW_MS for old and future intent frames', () => {
|
|
158
|
+
const oldTimestamp = new Date(Date.now() - REPLAY_WINDOW_MS - 1_000).toISOString()
|
|
159
|
+
const futureTimestamp = new Date(Date.now() + REPLAY_WINDOW_MS + 1_000).toISOString()
|
|
160
|
+
|
|
161
|
+
const oldFrame = signFrame(
|
|
162
|
+
{
|
|
163
|
+
v: '1',
|
|
164
|
+
intent: 'agent.ping',
|
|
165
|
+
from: sender.beamId,
|
|
166
|
+
to: receiver.beamId,
|
|
167
|
+
payload: { message: 'old' },
|
|
168
|
+
nonce: BeamIdentity.generateNonce(),
|
|
169
|
+
timestamp: oldTimestamp,
|
|
170
|
+
},
|
|
171
|
+
sender.export().privateKeyBase64,
|
|
172
|
+
)
|
|
173
|
+
const futureFrame = signFrame(
|
|
174
|
+
{
|
|
175
|
+
v: '1',
|
|
176
|
+
intent: 'agent.ping',
|
|
177
|
+
from: sender.beamId,
|
|
178
|
+
to: receiver.beamId,
|
|
179
|
+
payload: { message: 'future' },
|
|
180
|
+
nonce: BeamIdentity.generateNonce(),
|
|
181
|
+
timestamp: futureTimestamp,
|
|
182
|
+
},
|
|
183
|
+
sender.export().privateKeyBase64,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(validateIntentFrame(oldFrame, sender.publicKeyBase64)).toEqual({
|
|
187
|
+
valid: false,
|
|
188
|
+
error: 'Frame timestamp outside replay window (±5 minutes)',
|
|
189
|
+
})
|
|
190
|
+
expect(validateIntentFrame(futureFrame, sender.publicKeyBase64)).toEqual({
|
|
191
|
+
valid: false,
|
|
192
|
+
error: 'Frame timestamp outside replay window (±5 minutes)',
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('canonicalizeFrame() is stable regardless of key order', () => {
|
|
197
|
+
const left = {
|
|
198
|
+
payload: { z: 1, a: { d: 4, b: 2 } },
|
|
199
|
+
nonce: '123',
|
|
200
|
+
success: true,
|
|
201
|
+
v: '1',
|
|
202
|
+
}
|
|
203
|
+
const right = {
|
|
204
|
+
v: '1',
|
|
205
|
+
success: true,
|
|
206
|
+
nonce: '123',
|
|
207
|
+
payload: { a: { b: 2, d: 4 }, z: 1 },
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
expect(canonicalizeFrame(left)).toBe(canonicalizeFrame(right))
|
|
211
|
+
expect(canonicalizeFrame(left)).toBe('{"nonce":"123","payload":{"a":{"b":2,"d":4},"z":1},"success":true,"v":"1"}')
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { BeamIdentity } from '../src/identity.js'
|
|
3
|
+
|
|
4
|
+
describe('BeamIdentity', () => {
|
|
5
|
+
it('generate() creates valid beamId format', () => {
|
|
6
|
+
const identity = BeamIdentity.generate({ agentName: 'myagent', orgName: 'myorg' })
|
|
7
|
+
expect(identity.beamId).toBe('myagent@myorg.beam.directory')
|
|
8
|
+
expect(identity.publicKeyBase64).toBeTruthy()
|
|
9
|
+
expect(typeof identity.publicKeyBase64).toBe('string')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('sign() + verify() round-trip works', () => {
|
|
13
|
+
const identity = BeamIdentity.generate({ agentName: 'alice', orgName: 'acme' })
|
|
14
|
+
const data = 'hello beam protocol'
|
|
15
|
+
const signature = identity.sign(data)
|
|
16
|
+
expect(typeof signature).toBe('string')
|
|
17
|
+
expect(signature.length).toBeGreaterThan(0)
|
|
18
|
+
const valid = BeamIdentity.verify(data, signature, identity.publicKeyBase64)
|
|
19
|
+
expect(valid).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('verify() returns false for tampered data', () => {
|
|
23
|
+
const identity = BeamIdentity.generate({ agentName: 'alice', orgName: 'acme' })
|
|
24
|
+
const data = 'original message'
|
|
25
|
+
const signature = identity.sign(data)
|
|
26
|
+
const valid = BeamIdentity.verify('tampered message', signature, identity.publicKeyBase64)
|
|
27
|
+
expect(valid).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('verify() returns false for tampered signature', () => {
|
|
31
|
+
const identity = BeamIdentity.generate({ agentName: 'alice', orgName: 'acme' })
|
|
32
|
+
const data = 'hello beam protocol'
|
|
33
|
+
const signature = identity.sign(data)
|
|
34
|
+
// Flip a character in the signature
|
|
35
|
+
const tampered = signature.slice(0, -4) + 'AAAA'
|
|
36
|
+
const valid = BeamIdentity.verify(data, tampered, identity.publicKeyBase64)
|
|
37
|
+
expect(valid).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('verify() returns false for wrong public key', () => {
|
|
41
|
+
const identity1 = BeamIdentity.generate({ agentName: 'alice', orgName: 'acme' })
|
|
42
|
+
const identity2 = BeamIdentity.generate({ agentName: 'bob', orgName: 'acme' })
|
|
43
|
+
const data = 'hello beam protocol'
|
|
44
|
+
const signature = identity1.sign(data)
|
|
45
|
+
// Verify with wrong key
|
|
46
|
+
const valid = BeamIdentity.verify(data, signature, identity2.publicKeyBase64)
|
|
47
|
+
expect(valid).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('export() + fromData() round-trip works', () => {
|
|
51
|
+
const original = BeamIdentity.generate({ agentName: 'testbot', orgName: 'testorg' })
|
|
52
|
+
const data = original.export()
|
|
53
|
+
|
|
54
|
+
expect(data.beamId).toBe('testbot@testorg.beam.directory')
|
|
55
|
+
expect(typeof data.publicKeyBase64).toBe('string')
|
|
56
|
+
expect(typeof data.privateKeyBase64).toBe('string')
|
|
57
|
+
|
|
58
|
+
const restored = BeamIdentity.fromData(data)
|
|
59
|
+
expect(restored.beamId).toBe(original.beamId)
|
|
60
|
+
expect(restored.publicKeyBase64).toBe(original.publicKeyBase64)
|
|
61
|
+
|
|
62
|
+
// Signing with restored key should verify against original public key
|
|
63
|
+
const message = 'test round trip'
|
|
64
|
+
const sig = restored.sign(message)
|
|
65
|
+
expect(BeamIdentity.verify(message, sig, original.publicKeyBase64)).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('export() produces stable base64 keys', () => {
|
|
69
|
+
const identity = BeamIdentity.generate({ agentName: 'stable', orgName: 'org' })
|
|
70
|
+
const export1 = identity.export()
|
|
71
|
+
const export2 = identity.export()
|
|
72
|
+
expect(export1.publicKeyBase64).toBe(export2.publicKeyBase64)
|
|
73
|
+
expect(export1.privateKeyBase64).toBe(export2.privateKeyBase64)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('parseBeamId()', () => {
|
|
77
|
+
it('returns correct agent/org for valid beam ID', () => {
|
|
78
|
+
const result = BeamIdentity.parseBeamId('myagent@myorg.beam.directory')
|
|
79
|
+
expect(result).not.toBeNull()
|
|
80
|
+
expect(result!.agent).toBe('myagent')
|
|
81
|
+
expect(result!.org).toBe('myorg')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('returns correct result for hyphenated names', () => {
|
|
85
|
+
const result = BeamIdentity.parseBeamId('my-agent@my-org.beam.directory')
|
|
86
|
+
expect(result).not.toBeNull()
|
|
87
|
+
expect(result!.agent).toBe('my-agent')
|
|
88
|
+
expect(result!.org).toBe('my-org')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns correct result for underscore names', () => {
|
|
92
|
+
const result = BeamIdentity.parseBeamId('my_agent@my_org.beam.directory')
|
|
93
|
+
expect(result).not.toBeNull()
|
|
94
|
+
expect(result!.agent).toBe('my_agent')
|
|
95
|
+
expect(result!.org).toBe('my_org')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns null for invalid format - missing @', () => {
|
|
99
|
+
expect(BeamIdentity.parseBeamId('myagentmyorg.beam.directory')).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns null for invalid format - wrong suffix', () => {
|
|
103
|
+
expect(BeamIdentity.parseBeamId('myagent@myorg.beam.com')).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('returns null for invalid format - uppercase letters', () => {
|
|
107
|
+
expect(BeamIdentity.parseBeamId('MyAgent@myorg.beam.directory')).toBeNull()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns null for empty string', () => {
|
|
111
|
+
expect(BeamIdentity.parseBeamId('')).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('returns null for partial match', () => {
|
|
115
|
+
expect(BeamIdentity.parseBeamId('agent@org.beam.directory.extra')).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('generateNonce() returns a UUID v4 string', () => {
|
|
120
|
+
const nonce = BeamIdentity.generateNonce()
|
|
121
|
+
expect(typeof nonce).toBe('string')
|
|
122
|
+
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
123
|
+
expect(nonce).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('generateNonce() returns unique values', () => {
|
|
127
|
+
const nonces = new Set(Array.from({ length: 100 }, () => BeamIdentity.generateNonce()))
|
|
128
|
+
expect(nonces.size).toBe(100)
|
|
129
|
+
})
|
|
130
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
19
|
+
}
|
package/vitest.config.ts
ADDED
package/README.md
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# beam-protocol-sdk · TypeScript SDK
|
|
2
|
-
|
|
3
|
-
> **SMTP for AI Agents** — TypeScript SDK for agent identity, registration, discovery and intent routing via the Beam Protocol.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/beam-protocol-sdk)
|
|
6
|
-
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
7
|
-
[](https://nodejs.org)
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install beam-protocol-sdk
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
import { BeamIdentity, BeamClient } from 'beam-protocol-sdk'
|
|
21
|
-
|
|
22
|
-
// 1. Generate a Beam identity (Ed25519 keypair)
|
|
23
|
-
const identity = BeamIdentity.generate({
|
|
24
|
-
agentName: 'myagent',
|
|
25
|
-
orgName: 'myorg',
|
|
26
|
-
})
|
|
27
|
-
console.log(identity.beamId) // → myagent@myorg.beam.directory
|
|
28
|
-
|
|
29
|
-
// 2. Connect to the directory
|
|
30
|
-
const client = new BeamClient({
|
|
31
|
-
identity: identity.export(),
|
|
32
|
-
directoryUrl: 'wss://dir.beam.directory',
|
|
33
|
-
})
|
|
34
|
-
await client.connect()
|
|
35
|
-
|
|
36
|
-
// 3. Send a structured intent
|
|
37
|
-
const result = await client.send(
|
|
38
|
-
'other@org.beam.directory',
|
|
39
|
-
'payment.status_check',
|
|
40
|
-
{ invoiceId: 'INV-2847' }
|
|
41
|
-
)
|
|
42
|
-
console.log(result.payload)
|
|
43
|
-
// → { status: 'paid', amount: 2185.00, date: '2026-03-05' }
|
|
44
|
-
|
|
45
|
-
// 4. Or just talk in natural language
|
|
46
|
-
const reply = await client.talk(
|
|
47
|
-
'clara@coppen.beam.directory',
|
|
48
|
-
'Was weißt du über Christopher Schnorrenberg?'
|
|
49
|
-
)
|
|
50
|
-
console.log(reply) // Clara uses her real tools to answer
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Concepts
|
|
54
|
-
|
|
55
|
-
### Beam ID
|
|
56
|
-
|
|
57
|
-
Every agent gets a globally unique **Beam ID**:
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
agent@org.beam.directory
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Like email, but for AI agents. Backed by an Ed25519 keypair for cryptographic verification.
|
|
64
|
-
|
|
65
|
-
### Intent Frames
|
|
66
|
-
|
|
67
|
-
Structured JSON messages with built-in security:
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { createIntentFrame, signFrame } from 'beam-protocol-sdk'
|
|
71
|
-
|
|
72
|
-
const frame = createIntentFrame({
|
|
73
|
-
from: 'jarvis@coppen.beam.directory',
|
|
74
|
-
to: 'fischer@coppen.beam.directory',
|
|
75
|
-
intent: 'escalation.request',
|
|
76
|
-
payload: { caseId: 'CASE-0042', priority: 'high' },
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const signed = signFrame(frame, identity.privateKey)
|
|
80
|
-
// → Ed25519 signature, nonce, replay protection included
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Directory
|
|
84
|
-
|
|
85
|
-
Discover agents by org, capability, or connection status:
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
import { BeamDirectory } from 'beam-protocol-sdk'
|
|
89
|
-
|
|
90
|
-
const directory = new BeamDirectory({
|
|
91
|
-
baseUrl: 'https://api.beam.directory',
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// Look up a specific agent
|
|
95
|
-
const agent = await directory.lookup('clara@coppen.beam.directory')
|
|
96
|
-
|
|
97
|
-
// Search by org
|
|
98
|
-
const agents = await directory.search({ org: 'coppen', limit: 10 })
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### Threads
|
|
102
|
-
|
|
103
|
-
Multi-turn conversations between agents:
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
const thread = client.thread('clara@coppen.beam.directory')
|
|
107
|
-
|
|
108
|
-
const r1 = await thread.send('query.customer', { name: 'Chris' })
|
|
109
|
-
// Clara responds with customer data
|
|
110
|
-
|
|
111
|
-
const r2 = await thread.send('query.deals', { customerId: r1.payload.id })
|
|
112
|
-
// Follow-up in the same conversation context
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## API Reference
|
|
116
|
-
|
|
117
|
-
### `BeamIdentity`
|
|
118
|
-
|
|
119
|
-
| Method | Description |
|
|
120
|
-
|---|---|
|
|
121
|
-
| `BeamIdentity.generate(config)` | Create a new identity with Ed25519 keypair |
|
|
122
|
-
| `identity.beamId` | The agent's Beam ID string |
|
|
123
|
-
| `identity.export()` | Serialize for storage/transport |
|
|
124
|
-
| `BeamIdentity.fromExport(data)` | Restore from serialized data |
|
|
125
|
-
|
|
126
|
-
### `BeamClient`
|
|
127
|
-
|
|
128
|
-
| Method | Description |
|
|
129
|
-
|---|---|
|
|
130
|
-
| `new BeamClient(config)` | Create client with identity + directory URL |
|
|
131
|
-
| `client.connect()` | Connect to directory via WebSocket |
|
|
132
|
-
| `client.send(to, intent, payload)` | Send a structured intent frame |
|
|
133
|
-
| `client.talk(to, message)` | Natural language message (no schema needed) |
|
|
134
|
-
| `client.thread(to)` | Start a multi-turn conversation |
|
|
135
|
-
| `client.disconnect()` | Graceful disconnect |
|
|
136
|
-
|
|
137
|
-
### `BeamDirectory`
|
|
138
|
-
|
|
139
|
-
| Method | Description |
|
|
140
|
-
|---|---|
|
|
141
|
-
| `directory.lookup(beamId)` | Find a specific agent |
|
|
142
|
-
| `directory.search(query)` | Search agents by org/capability |
|
|
143
|
-
| `directory.register(name, capabilities)` | Register agent in directory |
|
|
144
|
-
|
|
145
|
-
### Frame Utilities
|
|
146
|
-
|
|
147
|
-
| Function | Description |
|
|
148
|
-
|---|---|
|
|
149
|
-
| `createIntentFrame(opts)` | Build a new intent frame |
|
|
150
|
-
| `createResultFrame(opts)` | Build a result response frame |
|
|
151
|
-
| `signFrame(frame, privateKey)` | Ed25519-sign a frame |
|
|
152
|
-
| `validateIntentFrame(frame)` | Validate structure + replay protection |
|
|
153
|
-
| `MAX_FRAME_SIZE` | 1024 bytes max per frame |
|
|
154
|
-
| `REPLAY_WINDOW_MS` | 5 minute replay window |
|
|
155
|
-
|
|
156
|
-
## Security
|
|
157
|
-
|
|
158
|
-
- **Ed25519 signatures** on every frame
|
|
159
|
-
- **Nonce-based replay protection** (5 min window)
|
|
160
|
-
- **Deny-by-default ACL** — agents must be explicitly authorized
|
|
161
|
-
- **Schema validation** at the directory level
|
|
162
|
-
- **Rate limiting** — 60 intents/min per agent
|
|
163
|
-
|
|
164
|
-
## Examples
|
|
165
|
-
|
|
166
|
-
See [`examples/`](https://github.com/Beam-directory/beam-protocol/tree/main/examples) in the main repo.
|
|
167
|
-
|
|
168
|
-
## Links
|
|
169
|
-
|
|
170
|
-
- 🌐 [beam.directory](https://beam.directory)
|
|
171
|
-
- 📄 [RFC 0001](https://github.com/Beam-directory/beam-protocol/blob/main/spec/RFC-0001.md)
|
|
172
|
-
- 📖 [Full Documentation](https://github.com/Beam-directory/beam-protocol/tree/main/docs)
|
|
173
|
-
- 🐍 [Python SDK](https://pypi.org/project/beam-directory/)
|
|
174
|
-
|
|
175
|
-
## License
|
|
176
|
-
|
|
177
|
-
Apache-2.0
|
package/dist/client.d.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { BeamDirectory } from './directory.js';
|
|
2
|
-
import type { BeamClientConfig, BeamIdString, IntentFrame, ResultFrame, AgentRecord } from './types.js';
|
|
3
|
-
type IntentHandler = (frame: IntentFrame, respond: (options: {
|
|
4
|
-
success: boolean;
|
|
5
|
-
payload?: Record<string, unknown>;
|
|
6
|
-
error?: string;
|
|
7
|
-
errorCode?: string;
|
|
8
|
-
latency?: number;
|
|
9
|
-
}) => ResultFrame) => void | Promise<void>;
|
|
10
|
-
export declare class BeamClient {
|
|
11
|
-
private readonly _identity;
|
|
12
|
-
private readonly _directory;
|
|
13
|
-
private readonly _directoryUrl;
|
|
14
|
-
private _ws;
|
|
15
|
-
private _wsConnected;
|
|
16
|
-
private readonly _pendingResults;
|
|
17
|
-
private readonly _intentHandlers;
|
|
18
|
-
constructor(config: BeamClientConfig);
|
|
19
|
-
get beamId(): BeamIdString;
|
|
20
|
-
get directory(): BeamDirectory;
|
|
21
|
-
register(displayName: string, capabilities: string[]): Promise<AgentRecord>;
|
|
22
|
-
connect(): Promise<void>;
|
|
23
|
-
private _handleMessage;
|
|
24
|
-
send(to: BeamIdString, intent: string, payload?: Record<string, unknown>, timeoutMs?: number): Promise<ResultFrame>;
|
|
25
|
-
private _sendViaWebSocket;
|
|
26
|
-
private _sendViaHttp;
|
|
27
|
-
on(intent: string, handler: IntentHandler): this;
|
|
28
|
-
/**
|
|
29
|
-
* Send a natural language message to another agent.
|
|
30
|
-
* The receiving agent uses its LLM to interpret and respond.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* const reply = await client.talk(
|
|
34
|
-
* 'clara@coppen.beam.directory',
|
|
35
|
-
* 'Was weißt du über Chris Schnorrenberg?'
|
|
36
|
-
* )
|
|
37
|
-
* console.log(reply.message) // Natural language response
|
|
38
|
-
* console.log(reply.structured) // Optional structured data
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* Start a multi-turn conversation thread.
|
|
42
|
-
* Returns a Thread object with a `say()` method for follow-ups.
|
|
43
|
-
*/
|
|
44
|
-
thread(to: BeamIdString, options?: {
|
|
45
|
-
language?: string;
|
|
46
|
-
timeoutMs?: number;
|
|
47
|
-
}): BeamThread;
|
|
48
|
-
talk(to: BeamIdString, message: string, options?: {
|
|
49
|
-
context?: Record<string, unknown>;
|
|
50
|
-
language?: string;
|
|
51
|
-
timeoutMs?: number;
|
|
52
|
-
threadId?: string;
|
|
53
|
-
}): Promise<{
|
|
54
|
-
message: string;
|
|
55
|
-
structured?: Record<string, unknown>;
|
|
56
|
-
threadId?: string;
|
|
57
|
-
raw: ResultFrame;
|
|
58
|
-
}>;
|
|
59
|
-
/**
|
|
60
|
-
* Register a natural language handler.
|
|
61
|
-
* Convenience wrapper that listens for conversation.message intents.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* client.onTalk(async (message, from, respond) => {
|
|
65
|
-
* const answer = await myLLM.generate(message)
|
|
66
|
-
* respond(answer)
|
|
67
|
-
* })
|
|
68
|
-
*/
|
|
69
|
-
onTalk(handler: (message: string, from: BeamIdString, respond: (reply: string, structured?: Record<string, unknown>) => void, frame: IntentFrame) => void | Promise<void>): this;
|
|
70
|
-
disconnect(): void;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* A multi-turn conversation thread between two agents.
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* const chat = client.thread('clara@coppen.beam.directory')
|
|
77
|
-
* const r1 = await chat.say('Was weißt du über Chris?')
|
|
78
|
-
* const r2 = await chat.say('Und seine Pipeline?') // keeps context
|
|
79
|
-
*/
|
|
80
|
-
export declare class BeamThread {
|
|
81
|
-
readonly threadId: string;
|
|
82
|
-
private readonly _client;
|
|
83
|
-
private readonly _to;
|
|
84
|
-
private readonly _language?;
|
|
85
|
-
private readonly _timeoutMs;
|
|
86
|
-
constructor(client: BeamClient, to: BeamIdString, options?: {
|
|
87
|
-
language?: string;
|
|
88
|
-
timeoutMs?: number;
|
|
89
|
-
});
|
|
90
|
-
say(message: string, context?: Record<string, unknown>): Promise<{
|
|
91
|
-
message: string;
|
|
92
|
-
structured?: Record<string, unknown>;
|
|
93
|
-
}>;
|
|
94
|
-
}
|
|
95
|
-
export {};
|
|
96
|
-
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|