@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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/bin/7h3.js +740 -0
- package/gateway.d.ts +36 -2
- package/index.js +818 -684
- 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 +15 -0
- package/webhookBinding.d.ts +24 -0
- package/wsBinding.d.ts +8 -0
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
|
+
});
|