@7h3/protocol 0.5.3 → 0.5.6

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