@7h3/protocol 0.5.6 → 0.6.1
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 +74 -1
- package/cborCodec.d.ts +13 -0
- package/index.js +395 -376
- package/package.json +1 -1
- package/protocol.d.ts +10 -0
package/bin/7h3.js
CHANGED
|
@@ -405,8 +405,81 @@ async function cmdKeysServe(argv) {
|
|
|
405
405
|
});
|
|
406
406
|
}
|
|
407
407
|
// ─── 7h3 add ───────────────────────────────────────────────────────────────────
|
|
408
|
-
const ADD_FRAMEWORKS = ['cloudflare-worker', 'nextjs', 'express', 'hono', 'fastify', 'claude-code', 'opencode', 'codex', 'grok'];
|
|
408
|
+
const ADD_FRAMEWORKS = ['webmcp', 'cloudflare-worker', 'nextjs', 'express', 'hono', 'fastify', 'claude-code', 'opencode', 'codex', 'grok'];
|
|
409
409
|
const FRAMEWORK_SNIPPETS = {
|
|
410
|
+
webmcp: (sender) => `// 7h3 — signed, capability-scoped WebMCP tools
|
|
411
|
+
// Install: npm install @7h3/protocol-webmcp @7h3/protocol
|
|
412
|
+
//
|
|
413
|
+
// WebMCP requires a secure context (HTTPS) and tools must be registered in the
|
|
414
|
+
// TOP-LEVEL page — tools inside an iframe are not discoverable by agents.
|
|
415
|
+
|
|
416
|
+
import { guard, isWebMcpSupported } from '@7h3/protocol-webmcp'
|
|
417
|
+
import { generateEd25519KeypairBase64Url } from '@7h3/protocol'
|
|
418
|
+
|
|
419
|
+
if (isWebMcpSupported()) {
|
|
420
|
+
// Per-session key: fine for signing this visitor's grants and receipts. The
|
|
421
|
+
// manifest is signed separately, at deploy time, by a key the browser never sees.
|
|
422
|
+
const { publicKey, privateKey } = await generateEd25519KeypairBase64Url()
|
|
423
|
+
|
|
424
|
+
const g = guard({
|
|
425
|
+
origin: ${JSON.stringify(sender)},
|
|
426
|
+
privateKey,
|
|
427
|
+
publicKey,
|
|
428
|
+
onConfirm: async (tool, input) =>
|
|
429
|
+
window.confirm(\`Allow \${tool.name}?\\n\\n\${JSON.stringify(input, null, 2)}\`),
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// An unguarded read: no scope, so no grant is required.
|
|
433
|
+
await g.registerTool({
|
|
434
|
+
name: 'search_items',
|
|
435
|
+
description: 'Search the catalog',
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: { query: { type: 'string' } },
|
|
439
|
+
required: ['query'],
|
|
440
|
+
additionalProperties: false,
|
|
441
|
+
},
|
|
442
|
+
annotations: { readOnlyHint: true },
|
|
443
|
+
execute: async ({ query }) => searchItems(String(query)),
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// A guarded write. \`scope\` gates it behind a capability; \`limit\` is a ceiling
|
|
447
|
+
// this site will never exceed, whatever a grant says.
|
|
448
|
+
await g.registerTool({
|
|
449
|
+
name: 'place_order',
|
|
450
|
+
description: 'Place an order for the current cart',
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: { cartId: { type: 'string' }, amountCents: { type: 'number' } },
|
|
454
|
+
required: ['cartId', 'amountCents'],
|
|
455
|
+
additionalProperties: false,
|
|
456
|
+
},
|
|
457
|
+
annotations: { destructiveHint: true },
|
|
458
|
+
scope: 'orders/place',
|
|
459
|
+
limit: { field: 'amountCents', max: 500_00 },
|
|
460
|
+
confirm: true,
|
|
461
|
+
execute: async ({ cartId }) => placeOrder(String(cartId)),
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// Wire this to a consent control in your own UI — never grant automatically.
|
|
465
|
+
// The token is held page-side, so it never passes through the agent.
|
|
466
|
+
document.querySelector('#allow-agent')?.addEventListener('click', async () => {
|
|
467
|
+
await g.grant({
|
|
468
|
+
subject: 'browser-agent',
|
|
469
|
+
scopes: ['orders/place'],
|
|
470
|
+
caps: { amountCents: 100_00 }, // bound inside the signed token
|
|
471
|
+
ttlMs: 10 * 60_000, // authority lapses on its own
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// Every call — allowed and refused — lands on a hash-chained signed log.
|
|
476
|
+
g.on((event) => {
|
|
477
|
+
if (event.type === 'call') {
|
|
478
|
+
console.log(event.receipt.outcome, event.receipt.tool, event.receipt.reason ?? '')
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
`,
|
|
410
483
|
'cloudflare-worker': (sender) => `// cloudflare/src/worker.ts — 7h3 Gateway Worker
|
|
411
484
|
// Install: npm install @7h3/protocol
|
|
412
485
|
// See: cloudflare/DEPLOY.md for full setup
|
package/cborCodec.d.ts
CHANGED
|
@@ -21,11 +21,24 @@ export declare class CborEncoder {
|
|
|
21
21
|
private _encodeHeadBytes;
|
|
22
22
|
private _concat;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Maximum nesting depth accepted while decoding.
|
|
26
|
+
*
|
|
27
|
+
* RFC 8949 §10 calls this out explicitly: a decoder that recurses per nesting
|
|
28
|
+
* level turns a handful of attacker bytes into a stack overflow. `0x81` is
|
|
29
|
+
* "array of 1", so 50 KB of repeated `0x81` nests 50 000 deep and blows the
|
|
30
|
+
* stack — and CBOR arrives straight off the wire through the HTTP binding.
|
|
31
|
+
* 64 is far beyond any real envelope, which nests a handful of levels at most.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_CBOR_DEPTH = 64;
|
|
24
34
|
export declare class CborDecoder {
|
|
25
35
|
private data;
|
|
26
36
|
private offset;
|
|
37
|
+
private depth;
|
|
27
38
|
decode(data: Uint8Array): unknown;
|
|
39
|
+
/** Depth-counting wrapper around the recursive decode body. */
|
|
28
40
|
private _decode;
|
|
41
|
+
private _decodeItem;
|
|
29
42
|
private _decodeUint;
|
|
30
43
|
private _readByte;
|
|
31
44
|
private _readBytes;
|