@7h3/protocol 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/bin/7h3.js +663 -0
- package/gateway.d.ts +17 -1
- package/index.js +338 -330
- package/keyRegistry.d.ts +1 -1
- package/package.json +88 -6
- package/protocol.d.ts +1 -0
- package/queueBinding.d.ts +12 -0
- package/rateLimiter.d.ts +11 -0
package/bin/7h3.js
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { writeFileSync } from 'node:fs';
|
|
5
|
+
const USAGE = `
|
|
6
|
+
7h3 — Protocol CLI (wire version 7h3/0.1)
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
7h3 keygen [--output <file>]
|
|
10
|
+
7h3 sign --private-key <key> --sender <id> [--recipient <id>] [--payload <str>] [--ttl <ms>]
|
|
11
|
+
7h3 verify --public-key <key> --envelope <json>
|
|
12
|
+
7h3 inspect --envelope <json>
|
|
13
|
+
7h3 gateway --upstream <url> [--port <n>] [--public-key <key>] [--require ed25519|none]
|
|
14
|
+
[--sign-responses] [--private-key <key>] [--sender <id>] [--metrics-port <n>]
|
|
15
|
+
7h3 keys serve [--public-key <key>] [--key-id <id>] [--port <n>]
|
|
16
|
+
7h3 add --framework <name> [--sender <id>] [--output <dir>]
|
|
17
|
+
7h3 help
|
|
18
|
+
|
|
19
|
+
Commands:
|
|
20
|
+
keygen Generate an Ed25519 keypair (PKCS8/SPKI, base64url-encoded)
|
|
21
|
+
sign Create and sign a 7h3 envelope
|
|
22
|
+
verify Verify a 7h3 envelope signature
|
|
23
|
+
inspect Pretty-print a 7h3 envelope fields
|
|
24
|
+
gateway Run a verifying HTTP proxy gateway
|
|
25
|
+
keys serve Serve a /.well-known/7h3-keys endpoint
|
|
26
|
+
add Scaffold 7h3 integration into a project (see --framework options)
|
|
27
|
+
help Show this usage table
|
|
28
|
+
`;
|
|
29
|
+
function die(msg) {
|
|
30
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
async function cmdKeygen(argv) {
|
|
34
|
+
const { values } = parseArgs({
|
|
35
|
+
args: argv,
|
|
36
|
+
options: {
|
|
37
|
+
output: { type: 'string', short: 'o' },
|
|
38
|
+
},
|
|
39
|
+
strict: false,
|
|
40
|
+
});
|
|
41
|
+
const { generateEd25519KeypairBase64Url } = await import('@7h3/protocol');
|
|
42
|
+
const { publicKey, privateKey } = await generateEd25519KeypairBase64Url();
|
|
43
|
+
const result = {
|
|
44
|
+
algorithm: 'Ed25519',
|
|
45
|
+
wireVersion: '7h3/0.1',
|
|
46
|
+
publicKey,
|
|
47
|
+
privateKey,
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
warning: 'Keep privateKey secret — it grants signing authority over your agent identity.',
|
|
50
|
+
};
|
|
51
|
+
const json = JSON.stringify(result, null, 2);
|
|
52
|
+
if (values.output) {
|
|
53
|
+
writeFileSync(values.output, json, 'utf8');
|
|
54
|
+
process.stdout.write(`Keypair written to ${values.output}\n`);
|
|
55
|
+
process.stdout.write(` algorithm : Ed25519\n`);
|
|
56
|
+
process.stdout.write(` publicKey : ${publicKey}\n`);
|
|
57
|
+
process.stdout.write(` createdAt : ${result.createdAt}\n`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
process.stdout.write(json + '\n');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function cmdSign(argv) {
|
|
64
|
+
const { values } = parseArgs({
|
|
65
|
+
args: argv,
|
|
66
|
+
options: {
|
|
67
|
+
'private-key': { type: 'string' },
|
|
68
|
+
sender: { type: 'string' },
|
|
69
|
+
recipient: { type: 'string' },
|
|
70
|
+
payload: { type: 'string' },
|
|
71
|
+
ttl: { type: 'string' },
|
|
72
|
+
},
|
|
73
|
+
strict: false,
|
|
74
|
+
});
|
|
75
|
+
const privateKey = values['private-key'];
|
|
76
|
+
const sender = values['sender'];
|
|
77
|
+
if (!privateKey)
|
|
78
|
+
die('--private-key is required');
|
|
79
|
+
if (!sender)
|
|
80
|
+
die('--sender is required');
|
|
81
|
+
const { createEnvelope, signEnvelopeEd25519 } = await import('@7h3/protocol');
|
|
82
|
+
const envelope = createEnvelope({
|
|
83
|
+
sender: sender,
|
|
84
|
+
recipient: values['recipient'],
|
|
85
|
+
intent: 'PING',
|
|
86
|
+
content: values['payload'] ?? '',
|
|
87
|
+
ttlMs: values['ttl'] ? parseInt(values['ttl'], 10) : 60_000,
|
|
88
|
+
});
|
|
89
|
+
const signed = await signEnvelopeEd25519(envelope, privateKey);
|
|
90
|
+
process.stdout.write(JSON.stringify(signed) + '\n');
|
|
91
|
+
}
|
|
92
|
+
async function cmdVerify(argv) {
|
|
93
|
+
const { values } = parseArgs({
|
|
94
|
+
args: argv,
|
|
95
|
+
options: {
|
|
96
|
+
'public-key': { type: 'string' },
|
|
97
|
+
envelope: { type: 'string' },
|
|
98
|
+
},
|
|
99
|
+
strict: false,
|
|
100
|
+
});
|
|
101
|
+
const publicKey = values['public-key'];
|
|
102
|
+
const envelopeJson = values['envelope'];
|
|
103
|
+
if (!publicKey)
|
|
104
|
+
die('--public-key is required');
|
|
105
|
+
if (!envelopeJson)
|
|
106
|
+
die('--envelope is required');
|
|
107
|
+
const { validateEnvelope, verifyEnvelopeEd25519 } = await import('@7h3/protocol');
|
|
108
|
+
let envelope;
|
|
109
|
+
try {
|
|
110
|
+
envelope = JSON.parse(envelopeJson);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
die('--envelope is not valid JSON');
|
|
114
|
+
}
|
|
115
|
+
const diagnostics = validateEnvelope(envelope);
|
|
116
|
+
const errors = diagnostics.filter(d => d.level === 'error');
|
|
117
|
+
const warnings = diagnostics.filter(d => d.level === 'warning');
|
|
118
|
+
if (errors.length > 0) {
|
|
119
|
+
process.stdout.write('INVALID — envelope validation errors:\n');
|
|
120
|
+
for (const e of errors)
|
|
121
|
+
process.stdout.write(` [error] ${e.message}\n`);
|
|
122
|
+
for (const w of warnings)
|
|
123
|
+
process.stdout.write(` [warning] ${w.message}\n`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const valid = await verifyEnvelopeEd25519(envelope, publicKey);
|
|
127
|
+
if (valid) {
|
|
128
|
+
process.stdout.write('Signature valid\n');
|
|
129
|
+
if (warnings.length > 0) {
|
|
130
|
+
for (const w of warnings)
|
|
131
|
+
process.stdout.write(` [warning] ${w.message}\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
process.stdout.write('INVALID — signature verification failed\n');
|
|
136
|
+
process.stdout.write(` alg : ${envelope.signature?.alg ?? 'none'}\n`);
|
|
137
|
+
process.stdout.write(` keyId : ${envelope.signature?.keyId ?? 'none'}\n`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function cmdInspect(argv) {
|
|
142
|
+
const { values } = parseArgs({
|
|
143
|
+
args: argv,
|
|
144
|
+
options: {
|
|
145
|
+
envelope: { type: 'string' },
|
|
146
|
+
},
|
|
147
|
+
strict: false,
|
|
148
|
+
});
|
|
149
|
+
const envelopeJson = values['envelope'];
|
|
150
|
+
if (!envelopeJson)
|
|
151
|
+
die('--envelope is required');
|
|
152
|
+
let envelope;
|
|
153
|
+
try {
|
|
154
|
+
envelope = JSON.parse(envelopeJson);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
die('--envelope is not valid JSON');
|
|
158
|
+
}
|
|
159
|
+
const h = envelope.header ?? {};
|
|
160
|
+
const b = envelope.body ?? {};
|
|
161
|
+
const s = envelope.signature;
|
|
162
|
+
const nowMs = Date.now();
|
|
163
|
+
const expiresMs = (h.timestampMs ?? 0) + (h.ttlMs ?? 0);
|
|
164
|
+
const expired = expiresMs < nowMs;
|
|
165
|
+
const expiresStatus = expired
|
|
166
|
+
? `EXPIRED (${new Date(expiresMs).toISOString()})`
|
|
167
|
+
: `OK — expires ${new Date(expiresMs).toISOString()}`;
|
|
168
|
+
const content = typeof b.content === 'string' ? b.content : '';
|
|
169
|
+
const contentPreview = content.length > 80 ? content.slice(0, 77) + '...' : content;
|
|
170
|
+
process.stdout.write(`Wire Version : ${h.version ?? '(none)'}\n`);
|
|
171
|
+
process.stdout.write(`Message ID : ${h.messageId ?? '(none)'}\n`);
|
|
172
|
+
process.stdout.write(`Sender : ${h.sender ?? '(none)'}\n`);
|
|
173
|
+
process.stdout.write(`Recipient : ${h.recipient ?? '(none)'}\n`);
|
|
174
|
+
process.stdout.write(`Timestamp : ${h.timestampMs ? new Date(h.timestampMs).toISOString() : '(none)'}\n`);
|
|
175
|
+
process.stdout.write(`TTL : ${h.ttlMs ?? '(none)'} ms\n`);
|
|
176
|
+
process.stdout.write(`Expires : ${expiresStatus}\n`);
|
|
177
|
+
process.stdout.write(`Intent : ${b.intent ?? '(none)'}\n`);
|
|
178
|
+
process.stdout.write(`Content : ${contentPreview || '(empty)'}\n`);
|
|
179
|
+
if (s) {
|
|
180
|
+
process.stdout.write(`Sig alg : ${s.alg}\n`);
|
|
181
|
+
process.stdout.write(`Sig keyId : ${s.keyId}\n`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
process.stdout.write(`Signature : (none)\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function cmdGateway(argv) {
|
|
188
|
+
const { values } = parseArgs({
|
|
189
|
+
args: argv,
|
|
190
|
+
options: {
|
|
191
|
+
upstream: { type: 'string' },
|
|
192
|
+
port: { type: 'string' },
|
|
193
|
+
'public-key': { type: 'string' },
|
|
194
|
+
require: { type: 'string' },
|
|
195
|
+
'sign-responses': { type: 'boolean' },
|
|
196
|
+
'private-key': { type: 'string' },
|
|
197
|
+
sender: { type: 'string' },
|
|
198
|
+
'metrics-port': { type: 'string' },
|
|
199
|
+
},
|
|
200
|
+
strict: false,
|
|
201
|
+
});
|
|
202
|
+
const upstream = values['upstream'];
|
|
203
|
+
if (!upstream)
|
|
204
|
+
die('--upstream is required');
|
|
205
|
+
const port = parseInt(values['port'] ?? '8080', 10);
|
|
206
|
+
const publicKey = values['public-key'];
|
|
207
|
+
const requireMode = values['require'] ?? (publicKey ? 'ed25519' : 'none');
|
|
208
|
+
const signResponses = !!(values['sign-responses']);
|
|
209
|
+
const privateKey = values['private-key'];
|
|
210
|
+
const sender = values['sender'];
|
|
211
|
+
const metricsPortRaw = values['metrics-port'];
|
|
212
|
+
const metricsPort = metricsPortRaw ? parseInt(metricsPortRaw, 10) : undefined;
|
|
213
|
+
const { createGateway } = await import('@7h3/protocol/gateway');
|
|
214
|
+
const { createStaticKeyRegistry } = await import('@7h3/protocol/key-registry');
|
|
215
|
+
const keys = {};
|
|
216
|
+
if (publicKey && sender)
|
|
217
|
+
keys[sender] = publicKey;
|
|
218
|
+
const keyRegistry = createStaticKeyRegistry(keys);
|
|
219
|
+
const gateway = createGateway({
|
|
220
|
+
upstream: upstream,
|
|
221
|
+
keyRegistry,
|
|
222
|
+
signResponses: signResponses && !!privateKey,
|
|
223
|
+
privateKey,
|
|
224
|
+
sender,
|
|
225
|
+
defaultPolicy: requireMode === 'none' ? 'allow' : 'deny',
|
|
226
|
+
});
|
|
227
|
+
const server = createServer(async (req, res) => {
|
|
228
|
+
const chunks = [];
|
|
229
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
230
|
+
req.on('end', async () => {
|
|
231
|
+
const body = Buffer.concat(chunks);
|
|
232
|
+
// Flatten headers: string | string[] => string
|
|
233
|
+
const headers = {};
|
|
234
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
235
|
+
if (v === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
headers[k] = Array.isArray(v) ? v.join(', ') : v;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const result = await gateway.handle({
|
|
241
|
+
method: req.method ?? 'GET',
|
|
242
|
+
path: req.url ?? '/',
|
|
243
|
+
headers,
|
|
244
|
+
body: body.length > 0 ? body.toString('utf8') : undefined,
|
|
245
|
+
});
|
|
246
|
+
res.writeHead(result.status, result.headers);
|
|
247
|
+
res.end(result.body);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
251
|
+
res.end(`Gateway error: ${String(err)}`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
server.listen(port, () => {
|
|
256
|
+
process.stderr.write(`7h3 gateway listening on port ${port}\n`);
|
|
257
|
+
process.stderr.write(` upstream : ${upstream}\n`);
|
|
258
|
+
process.stderr.write(` verify mode : ${requireMode}\n`);
|
|
259
|
+
process.stderr.write(` sign-responses: ${signResponses && !!privateKey}\n`);
|
|
260
|
+
if (sender)
|
|
261
|
+
process.stderr.write(` sender : ${sender}\n`);
|
|
262
|
+
});
|
|
263
|
+
// Optional: dedicated metrics server
|
|
264
|
+
if (metricsPort !== undefined) {
|
|
265
|
+
const { metrics: globalMetrics, renderPrometheusText } = await import('@7h3/protocol/telemetry');
|
|
266
|
+
const metricsServer = createServer((req, res) => {
|
|
267
|
+
if (req.url === '/metrics' && req.method === 'GET') {
|
|
268
|
+
const body = renderPrometheusText(globalMetrics);
|
|
269
|
+
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' });
|
|
270
|
+
res.end(body);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
274
|
+
res.end('Not Found');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
metricsServer.listen(metricsPort, () => {
|
|
278
|
+
process.stderr.write(`7h3 metrics listening on :${metricsPort}/metrics\n`);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function cmdKeysServe(argv) {
|
|
283
|
+
const { values } = parseArgs({
|
|
284
|
+
args: argv,
|
|
285
|
+
options: {
|
|
286
|
+
'public-key': { type: 'string' },
|
|
287
|
+
'key-id': { type: 'string' },
|
|
288
|
+
port: { type: 'string' },
|
|
289
|
+
},
|
|
290
|
+
strict: false,
|
|
291
|
+
});
|
|
292
|
+
const publicKey = values['public-key'];
|
|
293
|
+
const keyId = values['key-id'] ?? 'default';
|
|
294
|
+
const port = parseInt(values['port'] ?? '8081', 10);
|
|
295
|
+
const { serveWellKnownKeys } = await import('@7h3/protocol/keys');
|
|
296
|
+
const doc = {
|
|
297
|
+
version: '7h3/0.1',
|
|
298
|
+
updated: Date.now(),
|
|
299
|
+
keys: publicKey
|
|
300
|
+
? [
|
|
301
|
+
{
|
|
302
|
+
id: keyId,
|
|
303
|
+
algorithm: 'Ed25519',
|
|
304
|
+
publicKey,
|
|
305
|
+
created: Date.now(),
|
|
306
|
+
},
|
|
307
|
+
]
|
|
308
|
+
: [],
|
|
309
|
+
};
|
|
310
|
+
const body = serveWellKnownKeys(doc);
|
|
311
|
+
const server = createServer((req, res) => {
|
|
312
|
+
if (req.url === '/.well-known/7h3-keys') {
|
|
313
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
314
|
+
res.end(body);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
318
|
+
res.end('Not Found');
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
server.listen(port, () => {
|
|
322
|
+
process.stderr.write(`7h3 key server listening on port ${port}\n`);
|
|
323
|
+
process.stderr.write(` GET /.well-known/7h3-keys\n`);
|
|
324
|
+
if (publicKey)
|
|
325
|
+
process.stderr.write(` key-id: ${keyId}\n`);
|
|
326
|
+
else
|
|
327
|
+
process.stderr.write(` (no keys configured — empty document)\n`);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// ─── 7h3 add ───────────────────────────────────────────────────────────────────
|
|
331
|
+
const ADD_FRAMEWORKS = ['cloudflare-worker', 'nextjs', 'express', 'hono', 'fastify', 'claude-code', 'opencode', 'codex', 'grok'];
|
|
332
|
+
const FRAMEWORK_SNIPPETS = {
|
|
333
|
+
'cloudflare-worker': (sender) => `// cloudflare/src/worker.ts — 7h3 Gateway Worker
|
|
334
|
+
// Install: npm install @7h3/protocol
|
|
335
|
+
// See: cloudflare/DEPLOY.md for full setup
|
|
336
|
+
|
|
337
|
+
import { createGateway } from '@7h3/protocol/gateway'
|
|
338
|
+
import { KvKeyRegistry } from './kv-key-registry'
|
|
339
|
+
import { KvReplayStore } from './kv-replay-store'
|
|
340
|
+
|
|
341
|
+
interface Env {
|
|
342
|
+
KEY_REGISTRY: KVNamespace
|
|
343
|
+
REPLAY_STORE: KVNamespace
|
|
344
|
+
UPSTREAM_URL: string
|
|
345
|
+
GATEWAY_PRIVATE_KEY?: string
|
|
346
|
+
GATEWAY_SENDER?: string
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export default {
|
|
350
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
351
|
+
const gateway = createGateway({
|
|
352
|
+
upstream: env.UPSTREAM_URL,
|
|
353
|
+
keyRegistry: new KvKeyRegistry(env.KEY_REGISTRY),
|
|
354
|
+
replayStore: new KvReplayStore(env.REPLAY_STORE),
|
|
355
|
+
defaultPolicy: 'deny',
|
|
356
|
+
privateKey: env.GATEWAY_PRIVATE_KEY,
|
|
357
|
+
sender: env.GATEWAY_SENDER ?? '${sender}',
|
|
358
|
+
})
|
|
359
|
+
const url = new URL(request.url)
|
|
360
|
+
const headers: Record<string, string> = {}
|
|
361
|
+
request.headers.forEach((v, k) => { headers[k] = v })
|
|
362
|
+
const result = await gateway.verify({ method: request.method, path: url.pathname, headers })
|
|
363
|
+
if (!result.ok) return new Response(result.reason, { status: result.status })
|
|
364
|
+
return fetch(env.UPSTREAM_URL + url.pathname + url.search, { method: request.method, headers })
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
`,
|
|
368
|
+
'nextjs': (sender) => `// middleware.ts — add to your Next.js project root
|
|
369
|
+
// Install: npm install @7h3/protocol
|
|
370
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
371
|
+
import { verifyHttpEnvelope } from '@7h3/protocol/http'
|
|
372
|
+
import { createStaticKeyRegistry } from '@7h3/protocol/key-registry'
|
|
373
|
+
|
|
374
|
+
// Load trusted peer public keys from env vars
|
|
375
|
+
const PEER_KEYS = Object.fromEntries(
|
|
376
|
+
(process.env.P7H3_TRUSTED_KEYS ?? '').split(',').filter(Boolean)
|
|
377
|
+
.map(pair => pair.split('=') as [string, string])
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
const registry = createStaticKeyRegistry(PEER_KEYS)
|
|
381
|
+
|
|
382
|
+
export async function middleware(req: NextRequest) {
|
|
383
|
+
const headers = Object.fromEntries(req.headers)
|
|
384
|
+
const result = await verifyHttpEnvelope(headers, { keyRegistry: registry })
|
|
385
|
+
if (!result.ok) {
|
|
386
|
+
return NextResponse.json({ error: result.reason }, { status: 401 })
|
|
387
|
+
}
|
|
388
|
+
const response = NextResponse.next()
|
|
389
|
+
response.headers.set('x-7h3-sender', (result as any).envelope?.header?.sender ?? '')
|
|
390
|
+
return response
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const config = { matcher: '/api/:path*' }
|
|
394
|
+
|
|
395
|
+
// Usage: set env var P7H3_TRUSTED_KEYS="agent@example.com=<base64url-pubkey>,..."
|
|
396
|
+
// Generate keypair: npx 7h3 keygen
|
|
397
|
+
// Self-identity: ${sender}
|
|
398
|
+
`,
|
|
399
|
+
'express': (sender) => `// middleware/7h3-auth.ts — add to your Express project
|
|
400
|
+
// Install: npm install @7h3/protocol
|
|
401
|
+
import type { Request, Response, NextFunction } from 'express'
|
|
402
|
+
import { verifyHttpEnvelope } from '@7h3/protocol/http'
|
|
403
|
+
import { createStaticKeyRegistry } from '@7h3/protocol/key-registry'
|
|
404
|
+
|
|
405
|
+
const registry = createStaticKeyRegistry({
|
|
406
|
+
'peer-agent@example.com': process.env.PEER_PUBLIC_KEY ?? '',
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
export async function verify7h3(req: Request, res: Response, next: NextFunction) {
|
|
410
|
+
const result = await verifyHttpEnvelope(
|
|
411
|
+
req.headers as Record<string, string>,
|
|
412
|
+
{ keyRegistry: registry },
|
|
413
|
+
)
|
|
414
|
+
if (!result.ok) return res.status(401).json({ error: result.reason })
|
|
415
|
+
;(req as any).sender7h3 = (result as any).envelope?.header?.sender
|
|
416
|
+
next()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Mount in app.ts:
|
|
420
|
+
// import { verify7h3 } from './middleware/7h3-auth'
|
|
421
|
+
// app.use('/api', verify7h3)
|
|
422
|
+
//
|
|
423
|
+
// Self-identity: ${sender}
|
|
424
|
+
`,
|
|
425
|
+
'hono': (sender) => `// middleware/7h3-auth.ts — add to your Hono project
|
|
426
|
+
// Install: npm install @7h3/protocol hono
|
|
427
|
+
import { createMiddleware } from 'hono/factory'
|
|
428
|
+
import { verifyHttpEnvelope } from '@7h3/protocol/http'
|
|
429
|
+
import { createStaticKeyRegistry } from '@7h3/protocol/key-registry'
|
|
430
|
+
|
|
431
|
+
const registry = createStaticKeyRegistry({
|
|
432
|
+
'peer-agent@example.com': process.env.PEER_PUBLIC_KEY ?? '',
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
export const auth7h3 = createMiddleware(async (c, next) => {
|
|
436
|
+
const headers = Object.fromEntries(c.req.raw.headers)
|
|
437
|
+
const result = await verifyHttpEnvelope(headers, { keyRegistry: registry })
|
|
438
|
+
if (!result.ok) return c.json({ error: result.reason }, 401)
|
|
439
|
+
c.set('sender7h3', (result as any).envelope?.header?.sender ?? '')
|
|
440
|
+
await next()
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
// Mount in your Hono app:
|
|
444
|
+
// import { auth7h3 } from './middleware/7h3-auth'
|
|
445
|
+
// app.use('/api/*', auth7h3)
|
|
446
|
+
//
|
|
447
|
+
// Self-identity: ${sender}
|
|
448
|
+
`,
|
|
449
|
+
'fastify': (sender) => `// plugins/7h3-auth.ts — add to your Fastify project
|
|
450
|
+
// Install: npm install @7h3/protocol fastify
|
|
451
|
+
import fp from 'fastify-plugin'
|
|
452
|
+
import { verifyHttpEnvelope } from '@7h3/protocol/http'
|
|
453
|
+
import { createStaticKeyRegistry } from '@7h3/protocol/key-registry'
|
|
454
|
+
|
|
455
|
+
const registry = createStaticKeyRegistry({
|
|
456
|
+
'peer-agent@example.com': process.env.PEER_PUBLIC_KEY ?? '',
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
export default fp(async (fastify) => {
|
|
460
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
461
|
+
const result = await verifyHttpEnvelope(
|
|
462
|
+
request.headers as Record<string, string>,
|
|
463
|
+
{ keyRegistry: registry },
|
|
464
|
+
)
|
|
465
|
+
if (!result.ok) {
|
|
466
|
+
return reply.code(401).send({ error: result.reason })
|
|
467
|
+
}
|
|
468
|
+
request.sender7h3 = (result as any).envelope?.header?.sender ?? ''
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// In your main file: fastify.register(import('./plugins/7h3-auth'))
|
|
473
|
+
// Self-identity: ${sender}
|
|
474
|
+
`,
|
|
475
|
+
'claude-code': (_sender) => `# Install 7h3 Protocol MCP server in Claude Code
|
|
476
|
+
|
|
477
|
+
## Option 1 — one command (recommended)
|
|
478
|
+
|
|
479
|
+
\`\`\`bash
|
|
480
|
+
claude mcp add 7h3-protocol -- npx -y @7h3/protocol-mcp
|
|
481
|
+
\`\`\`
|
|
482
|
+
|
|
483
|
+
## Option 2 — project .claude/settings.json
|
|
484
|
+
|
|
485
|
+
Copy .claude/settings.example.json to .claude/settings.json:
|
|
486
|
+
|
|
487
|
+
\`\`\`json
|
|
488
|
+
{
|
|
489
|
+
"mcpServers": {
|
|
490
|
+
"7h3-protocol": {
|
|
491
|
+
"command": "npx",
|
|
492
|
+
"args": ["-y", "@7h3/protocol-mcp"]
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
\`\`\`
|
|
497
|
+
|
|
498
|
+
## Available tools after install
|
|
499
|
+
|
|
500
|
+
- 7h3_generate_keypair — Ed25519 keypair
|
|
501
|
+
- 7h3_generate_secret — HMAC secret
|
|
502
|
+
- 7h3_sign — sign a test envelope
|
|
503
|
+
- 7h3_verify — verify an envelope
|
|
504
|
+
- 7h3_scaffold — generate framework integration code
|
|
505
|
+
- 7h3_mcp_config — get config for other editors
|
|
506
|
+
- 7h3_wrap_mcp_server — wrap an MCP handler with 7h3 signing
|
|
507
|
+
`,
|
|
508
|
+
'opencode': (_sender) => `# Install 7h3 Protocol MCP server in Opencode
|
|
509
|
+
|
|
510
|
+
Add to your opencode config (~/.config/opencode/config.json or opencode.json):
|
|
511
|
+
|
|
512
|
+
\`\`\`json
|
|
513
|
+
{
|
|
514
|
+
"mcp": {
|
|
515
|
+
"7h3-protocol": {
|
|
516
|
+
"command": "npx",
|
|
517
|
+
"args": ["-y", "@7h3/protocol-mcp"],
|
|
518
|
+
"type": "stdio"
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
\`\`\`
|
|
523
|
+
|
|
524
|
+
Or one command:
|
|
525
|
+
\`\`\`bash
|
|
526
|
+
opencode mcp add 7h3-protocol npx -y @7h3/protocol-mcp
|
|
527
|
+
\`\`\`
|
|
528
|
+
|
|
529
|
+
After install, ask: "scaffold 7h3 for my hono project" and the AI will call 7h3_scaffold automatically.
|
|
530
|
+
`,
|
|
531
|
+
'codex': (_sender) => `# 7h3 Protocol — GPT Codex integration
|
|
532
|
+
|
|
533
|
+
Codex reads AGENTS.md automatically. This repo has AGENTS.md with:
|
|
534
|
+
- Installation instructions for all SDKs
|
|
535
|
+
- Framework integration snippets (CF Worker, Next.js, Express, Hono, Fastify)
|
|
536
|
+
- All advanced feature examples
|
|
537
|
+
- CLI reference
|
|
538
|
+
|
|
539
|
+
No extra setup needed — just ensure AGENTS.md is in your repo root.
|
|
540
|
+
|
|
541
|
+
## Add to your project's AGENTS.md
|
|
542
|
+
|
|
543
|
+
\`\`\`markdown
|
|
544
|
+
## Security — 7h3 Protocol
|
|
545
|
+
|
|
546
|
+
All inter-agent HTTP calls must include a signed 7h3 envelope.
|
|
547
|
+
|
|
548
|
+
Install: npm install @7h3/protocol
|
|
549
|
+
Docs: see https://github.com/IceMasterT/7h3-protocol-aip
|
|
550
|
+
|
|
551
|
+
Verify incoming requests with verifyHttpEnvelope() from @7h3/protocol/http.
|
|
552
|
+
Sign outgoing requests with signEnvelopeEd25519() from @7h3/protocol.
|
|
553
|
+
\`\`\`
|
|
554
|
+
`,
|
|
555
|
+
'grok': (_sender) => `# 7h3 Protocol — Grok Builder integration
|
|
556
|
+
|
|
557
|
+
Grok Builder reads AGENTS.md. This repo's AGENTS.md includes all integration patterns.
|
|
558
|
+
|
|
559
|
+
## Add to your project's AGENTS.md
|
|
560
|
+
|
|
561
|
+
\`\`\`markdown
|
|
562
|
+
## Security — 7h3 Protocol signing
|
|
563
|
+
|
|
564
|
+
All AI agent messages must use 7h3 Protocol (npm: @7h3/protocol).
|
|
565
|
+
|
|
566
|
+
Key rules:
|
|
567
|
+
- Outgoing: createEnvelope() + signEnvelopeEd25519() → x-7h3-envelope header
|
|
568
|
+
- Incoming: verifyHttpEnvelope() from @7h3/protocol/http
|
|
569
|
+
- Wire version is ALWAYS "7h3/0.1" — never change
|
|
570
|
+
- Generate keys: npx 7h3 keygen
|
|
571
|
+
\`\`\`
|
|
572
|
+
|
|
573
|
+
## MCP config for Grok (if MCP supported)
|
|
574
|
+
|
|
575
|
+
\`\`\`json
|
|
576
|
+
{
|
|
577
|
+
"mcp": {
|
|
578
|
+
"7h3-protocol": {
|
|
579
|
+
"command": "npx",
|
|
580
|
+
"args": ["-y", "@7h3/protocol-mcp"]
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
\`\`\`
|
|
585
|
+
`,
|
|
586
|
+
};
|
|
587
|
+
async function cmdAdd(argv) {
|
|
588
|
+
const { values } = parseArgs({
|
|
589
|
+
args: argv,
|
|
590
|
+
options: {
|
|
591
|
+
framework: { type: 'string', short: 'f' },
|
|
592
|
+
sender: { type: 'string', short: 's' },
|
|
593
|
+
output: { type: 'string', short: 'o' },
|
|
594
|
+
},
|
|
595
|
+
strict: false,
|
|
596
|
+
});
|
|
597
|
+
const framework = values.framework ?? '';
|
|
598
|
+
const sender = values.sender ?? 'agent@example.com';
|
|
599
|
+
if (!framework || !ADD_FRAMEWORKS.includes(framework)) {
|
|
600
|
+
process.stdout.write(`Available frameworks:\n`);
|
|
601
|
+
ADD_FRAMEWORKS.forEach(f => process.stdout.write(` ${f}\n`));
|
|
602
|
+
process.stdout.write(`\nUsage: 7h3 add --framework <name> [--sender <id>]\n`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const snippet = FRAMEWORK_SNIPPETS[framework](sender);
|
|
606
|
+
const out = values.output;
|
|
607
|
+
if (out) {
|
|
608
|
+
const { writeFileSync } = await import('node:fs');
|
|
609
|
+
writeFileSync(out, snippet, 'utf8');
|
|
610
|
+
process.stdout.write(`Written to ${out}\n`);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
process.stdout.write(snippet);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function main() {
|
|
617
|
+
const args = process.argv.slice(2);
|
|
618
|
+
const command = args[0] ?? 'help';
|
|
619
|
+
const rest = args.slice(1);
|
|
620
|
+
switch (command) {
|
|
621
|
+
case 'keygen':
|
|
622
|
+
await cmdKeygen(rest);
|
|
623
|
+
break;
|
|
624
|
+
case 'sign':
|
|
625
|
+
await cmdSign(rest);
|
|
626
|
+
break;
|
|
627
|
+
case 'verify':
|
|
628
|
+
await cmdVerify(rest);
|
|
629
|
+
break;
|
|
630
|
+
case 'inspect':
|
|
631
|
+
await cmdInspect(rest);
|
|
632
|
+
break;
|
|
633
|
+
case 'gateway':
|
|
634
|
+
await cmdGateway(rest);
|
|
635
|
+
break;
|
|
636
|
+
case 'keys': {
|
|
637
|
+
const sub = rest[0];
|
|
638
|
+
if (sub === 'serve') {
|
|
639
|
+
await cmdKeysServe(rest.slice(1));
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
die(`Unknown subcommand 'keys ${sub ?? ''}'. Did you mean 'keys serve'?`);
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case 'add':
|
|
647
|
+
await cmdAdd(rest);
|
|
648
|
+
break;
|
|
649
|
+
case 'help':
|
|
650
|
+
case '--help':
|
|
651
|
+
case '-h':
|
|
652
|
+
process.stdout.write(USAGE);
|
|
653
|
+
break;
|
|
654
|
+
default:
|
|
655
|
+
process.stderr.write(`Unknown command: ${command}\n`);
|
|
656
|
+
process.stdout.write(USAGE);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
main().catch(err => {
|
|
661
|
+
process.stderr.write(`Fatal: ${String(err)}\n`);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
});
|
package/gateway.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type KeyRegistry } from './keyRegistry';
|
|
2
2
|
import { type RoutePolicy } from './routePolicy';
|
|
3
|
-
import { SlidingWindowRateLimiter } from './rateLimiter';
|
|
3
|
+
import { SlidingWindowRateLimiter, type RateLimitStore } from './rateLimiter';
|
|
4
4
|
import type { ReplayStore } from './replayStores';
|
|
5
5
|
export type { KeyRegistry, RoutePolicy };
|
|
6
6
|
export interface GatewayConfig {
|
|
@@ -15,6 +15,14 @@ export interface GatewayConfig {
|
|
|
15
15
|
metricsPath?: string;
|
|
16
16
|
/** Optional distributed replay store — prevents nonce reuse across gateway instances. */
|
|
17
17
|
replayStore?: ReplayStore;
|
|
18
|
+
/**
|
|
19
|
+
* Optional persistent rate-limit store — required for correct rate limiting
|
|
20
|
+
* whenever the gateway is rebuilt per-request (e.g. inside a Workers/Lambda
|
|
21
|
+
* fetch handler). Without it, rate limiting falls back to the in-memory
|
|
22
|
+
* SlidingWindowRateLimiter, which only works if this Gateway instance
|
|
23
|
+
* persists across the requests it's limiting.
|
|
24
|
+
*/
|
|
25
|
+
rateLimitStore?: RateLimitStore;
|
|
18
26
|
/** Optional capability token registry for capability-based auth. */
|
|
19
27
|
capabilityRegistry?: {
|
|
20
28
|
getPublicKey(id: string): Promise<string | null>;
|
|
@@ -50,4 +58,12 @@ declare class Protocol7h3Gateway {
|
|
|
50
58
|
getRateLimiter(): SlidingWindowRateLimiter;
|
|
51
59
|
}
|
|
52
60
|
export declare function createGateway(config: GatewayConfig): Protocol7h3Gateway;
|
|
61
|
+
/**
|
|
62
|
+
* Hardened preset for production deployments: fails fast (rather than
|
|
63
|
+
* silently falling back to permissive defaults) if `defaultPolicy` isn't
|
|
64
|
+
* explicitly `'deny'` or `replayStore` isn't configured. Use this instead of
|
|
65
|
+
* `createGateway()` wherever a misconfiguration should be a deploy-time error,
|
|
66
|
+
* not a runtime security gap discovered later.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createProductionGateway(config: GatewayConfig): Protocol7h3Gateway;
|
|
53
69
|
export { Protocol7h3Gateway };
|