@7h3/protocol 0.5.4 → 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 +81 -4
- package/gateway.d.ts +19 -1
- package/index.js +652 -526
- package/package.json +1 -1
- package/rateLimiter.d.ts +4 -0
- package/webhookBinding.d.ts +24 -0
- package/wsBinding.d.ts +8 -0
package/bin/7h3.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
|
-
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import { writeFileSync, readFileSync } from 'node:fs';
|
|
5
5
|
const USAGE = `
|
|
6
6
|
7h3 — Protocol CLI (wire version 7h3/0.1)
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
7h3 keygen [--output <file>]
|
|
10
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)
|
|
11
12
|
7h3 verify --public-key <key> --envelope <json>
|
|
12
13
|
7h3 inspect --envelope <json>
|
|
13
14
|
7h3 gateway --upstream <url> [--port <n>] [--public-key <key>] [--require ed25519|none]
|
|
14
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)
|
|
15
18
|
7h3 keys serve [--public-key <key>] [--key-id <id>] [--port <n>]
|
|
16
19
|
7h3 add --framework <name> [--sender <id>] [--output <dir>]
|
|
17
20
|
7h3 help
|
|
18
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
|
+
|
|
19
27
|
Commands:
|
|
20
28
|
keygen Generate an Ed25519 keypair (PKCS8/SPKI, base64url-encoded)
|
|
21
29
|
sign Create and sign a 7h3 envelope
|
|
@@ -30,6 +38,28 @@ function die(msg) {
|
|
|
30
38
|
process.stderr.write(`Error: ${msg}\n`);
|
|
31
39
|
process.exit(1);
|
|
32
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
|
+
}
|
|
33
63
|
async function cmdKeygen(argv) {
|
|
34
64
|
const { values } = parseArgs({
|
|
35
65
|
args: argv,
|
|
@@ -65,6 +95,7 @@ async function cmdSign(argv) {
|
|
|
65
95
|
args: argv,
|
|
66
96
|
options: {
|
|
67
97
|
'private-key': { type: 'string' },
|
|
98
|
+
'private-key-file': { type: 'string' },
|
|
68
99
|
sender: { type: 'string' },
|
|
69
100
|
recipient: { type: 'string' },
|
|
70
101
|
payload: { type: 'string' },
|
|
@@ -72,10 +103,10 @@ async function cmdSign(argv) {
|
|
|
72
103
|
},
|
|
73
104
|
strict: false,
|
|
74
105
|
});
|
|
75
|
-
const privateKey = values['private-key'];
|
|
106
|
+
const privateKey = resolveSecretArg('private-key', values['private-key'], values['private-key-file'], 'P7H3_PRIVATE_KEY');
|
|
76
107
|
const sender = values['sender'];
|
|
77
108
|
if (!privateKey)
|
|
78
|
-
die('--private-key is required');
|
|
109
|
+
die('--private-key (or --private-key-file / P7H3_PRIVATE_KEY) is required');
|
|
79
110
|
if (!sender)
|
|
80
111
|
die('--sender is required');
|
|
81
112
|
const { createEnvelope, signEnvelopeEd25519 } = await import('@7h3/protocol');
|
|
@@ -194,8 +225,10 @@ async function cmdGateway(argv) {
|
|
|
194
225
|
require: { type: 'string' },
|
|
195
226
|
'sign-responses': { type: 'boolean' },
|
|
196
227
|
'private-key': { type: 'string' },
|
|
228
|
+
'private-key-file': { type: 'string' },
|
|
197
229
|
sender: { type: 'string' },
|
|
198
230
|
'metrics-port': { type: 'string' },
|
|
231
|
+
'allow-unverified': { type: 'boolean' },
|
|
199
232
|
},
|
|
200
233
|
strict: false,
|
|
201
234
|
});
|
|
@@ -206,16 +239,53 @@ async function cmdGateway(argv) {
|
|
|
206
239
|
const publicKey = values['public-key'];
|
|
207
240
|
const requireMode = values['require'] ?? (publicKey ? 'ed25519' : 'none');
|
|
208
241
|
const signResponses = !!(values['sign-responses']);
|
|
209
|
-
const privateKey = values['private-key'];
|
|
242
|
+
const privateKey = resolveSecretArg('private-key', values['private-key'], values['private-key-file'], 'GATEWAY_PRIVATE_KEY');
|
|
210
243
|
const sender = values['sender'];
|
|
211
244
|
const metricsPortRaw = values['metrics-port'];
|
|
212
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
|
+
}
|
|
213
255
|
const { createGateway } = await import('@7h3/protocol/gateway');
|
|
214
256
|
const { createStaticKeyRegistry } = await import('@7h3/protocol/key-registry');
|
|
215
257
|
const keys = {};
|
|
216
258
|
if (publicKey && sender)
|
|
217
259
|
keys[sender] = publicKey;
|
|
218
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
|
+
}
|
|
219
289
|
const gateway = createGateway({
|
|
220
290
|
upstream: upstream,
|
|
221
291
|
keyRegistry,
|
|
@@ -223,6 +293,7 @@ async function cmdGateway(argv) {
|
|
|
223
293
|
privateKey,
|
|
224
294
|
sender,
|
|
225
295
|
defaultPolicy: requireMode === 'none' ? 'allow' : 'deny',
|
|
296
|
+
replayStore,
|
|
226
297
|
});
|
|
227
298
|
const server = createServer(async (req, res) => {
|
|
228
299
|
const chunks = [];
|
|
@@ -252,6 +323,10 @@ async function cmdGateway(argv) {
|
|
|
252
323
|
}
|
|
253
324
|
});
|
|
254
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)}`));
|
|
255
330
|
server.listen(port, () => {
|
|
256
331
|
process.stderr.write(`7h3 gateway listening on port ${port}\n`);
|
|
257
332
|
process.stderr.write(` upstream : ${upstream}\n`);
|
|
@@ -274,6 +349,7 @@ async function cmdGateway(argv) {
|
|
|
274
349
|
res.end('Not Found');
|
|
275
350
|
}
|
|
276
351
|
});
|
|
352
|
+
metricsServer.on('error', (err) => die(`metrics server: ${String(err)}`));
|
|
277
353
|
metricsServer.listen(metricsPort, () => {
|
|
278
354
|
process.stderr.write(`7h3 metrics listening on :${metricsPort}/metrics\n`);
|
|
279
355
|
});
|
|
@@ -318,6 +394,7 @@ async function cmdKeysServe(argv) {
|
|
|
318
394
|
res.end('Not Found');
|
|
319
395
|
}
|
|
320
396
|
});
|
|
397
|
+
server.on('error', (err) => die(`key server: ${String(err)}`));
|
|
321
398
|
server.listen(port, () => {
|
|
322
399
|
process.stderr.write(`7h3 key server listening on port ${port}\n`);
|
|
323
400
|
process.stderr.write(` GET /.well-known/7h3-keys\n`);
|
package/gateway.d.ts
CHANGED
|
@@ -46,13 +46,31 @@ export type GatewayVerifyOutcome = {
|
|
|
46
46
|
envelopeId?: string;
|
|
47
47
|
} | {
|
|
48
48
|
ok: false;
|
|
49
|
-
status: 401 | 403 | 429;
|
|
49
|
+
status: 400 | 401 | 403 | 429;
|
|
50
50
|
reason: string;
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a request path before it's used for both policy matching and
|
|
54
|
+
* upstream forwarding. Without this, a path like `/public/../admin/secret`
|
|
55
|
+
* matches a permissive `/public/**` policy (or no policy at all, under
|
|
56
|
+
* `defaultPolicy: 'allow'`) as a literal string, is forwarded unverified,
|
|
57
|
+
* and then gets collapsed by the URL parser inside `fetch()` on the way out
|
|
58
|
+
* — landing on `/admin/secret` at the upstream with zero verification ever
|
|
59
|
+
* having been performed against the path that's actually reached. Matching
|
|
60
|
+
* and forwarding must both operate on the same fully-normalized path so
|
|
61
|
+
* there's no gap between what was checked and what was sent.
|
|
62
|
+
*
|
|
63
|
+
* Returns null for anything that isn't a clean absolute path — including a
|
|
64
|
+
* `..` that would escape above the root, or percent-encoding that doesn't
|
|
65
|
+
* settle after a bounded number of decode passes (double-encoding is a
|
|
66
|
+
* classic way to smuggle a traversal past a single decode).
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeGatewayPath(rawPath: string): string | null;
|
|
52
69
|
declare class Protocol7h3Gateway {
|
|
53
70
|
private config;
|
|
54
71
|
private rateLimiter;
|
|
55
72
|
constructor(config: GatewayConfig);
|
|
73
|
+
private checkSenderAndRateLimit;
|
|
56
74
|
verify(req: GatewayRequest): Promise<GatewayVerifyOutcome>;
|
|
57
75
|
handle(req: GatewayRequest): Promise<GatewayResponse>;
|
|
58
76
|
getRateLimiter(): SlidingWindowRateLimiter;
|