@7h3/protocol 0.5.0 → 0.5.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/README.md +6 -1317
- package/agentAdapter.d.ts +26 -0
- package/auditLog.d.ts +54 -0
- package/capability.d.ts +67 -0
- package/cborCodec.d.ts +34 -0
- package/conformanceVectors.d.ts +20 -0
- package/encryption.d.ts +85 -0
- package/envelopeCbor.d.ts +33 -0
- package/frameworkAdapters.d.ts +72 -0
- package/gateway.d.ts +53 -0
- package/grpcBinding.d.ts +29 -0
- package/httpBinding.d.ts +50 -0
- package/index.d.ts +39 -0
- package/index.js +3662 -0
- package/keyInfra.d.ts +64 -0
- package/keyRegistry.d.ts +9 -0
- package/keyRotation.d.ts +20 -0
- package/mcpGateway.d.ts +37 -0
- package/mcpTransports.d.ts +62 -0
- package/mcpWrapper.d.ts +83 -0
- package/otel.d.ts +65 -0
- package/package.json +13 -121
- package/policyEnforcer.d.ts +50 -0
- package/policyTelemetryFeedback.d.ts +11 -0
- package/{src/protocol.d.ts → protocol.d.ts} +1 -2
- package/protocolAgent.d.ts +58 -0
- package/protocolBinary.d.ts +8 -0
- package/protocolCapabilities.d.ts +24 -0
- package/protocolReplay.d.ts +35 -0
- package/protocolTransport.d.ts +73 -0
- package/queueBinding.d.ts +43 -0
- package/rateLimiter.d.ts +18 -0
- package/{src/redisClient.ts → redisClient.d.ts} +25 -75
- package/replayStores.d.ts +98 -0
- package/revocation.d.ts +71 -0
- package/routePolicy.d.ts +30 -0
- package/runtimePolicy.d.ts +24 -0
- package/runtimePolicyManager.d.ts +15 -0
- package/runtimePolicyPresets.d.ts +11 -0
- package/signedResponse.d.ts +21 -0
- package/stream.d.ts +59 -0
- package/telemetry.d.ts +79 -0
- package/webhookBinding.d.ts +30 -0
- package/wsBinding.d.ts +52 -0
- package/.dockerignore +0 -19
- package/.github/dependabot.yml +0 -32
- package/.github/workflows/ci.yml +0 -31
- package/.github/workflows/publish.yml +0 -59
- package/.github/workflows/scorecard.yml +0 -37
- package/7h3.example.yaml +0 -125
- package/CHANGELOG.md +0 -152
- package/CONTRIBUTING.md +0 -82
- package/Dockerfile +0 -73
- package/GOVERNANCE.md +0 -62
- package/SECURITY.md +0 -70
- package/bench-results/replay-cache-full-1777891033256.json +0 -10
- package/bench-results/replay-cache-full-1777896317488.json +0 -10
- package/bench-results/replay-cache-full-1777900993184.json +0 -10
- package/bench-results/replay-cache-full-1777901019285.json +0 -10
- package/bench-results/replay-cache-quick-1777870170126.json +0 -10
- package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
- package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
- package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
- package/bench-results/wire-codecs-full-1777891019803.json +0 -93
- package/bench-results/wire-codecs-full-1777896260964.json +0 -93
- package/bench-results/wire-codecs-full-1777901004247.json +0 -93
- package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
- package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
- package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
- package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
- package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
- package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
- package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
- package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
- package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
- package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
- package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
- package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
- package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
- package/bin/7h3.ts +0 -406
- package/conformance/7h3_v0_1.json +0 -77
- package/conformance/7h3_v0_1_binary.json +0 -20
- package/conformance/aip_v0_1_binary.json +0 -20
- package/docker-compose.yaml +0 -77
- package/docs/ADOPTION_PLAN.md +0 -120
- package/docs/AGENTS.md +0 -77
- package/docs/AIP_RFC_v0.1.md +0 -97
- package/docs/AI_DECISION_CARD.md +0 -122
- package/docs/AI_RUNTIME_POLICY.json +0 -126
- package/docs/AI_RUNTIME_POLICY.yaml +0 -110
- package/docs/BACKPRESSURE_TUNING.md +0 -65
- package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
- package/docs/BINARY_CODEC_BENCH.md +0 -23
- package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
- package/docs/CLOCK_SKEW_POLICY.md +0 -30
- package/docs/DISTRIBUTED_REPLAY.md +0 -142
- package/docs/FUZZ_CAMPAIGN.md +0 -121
- package/docs/GATEWAY.md +0 -195
- package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
- package/docs/KEY_REVOCATION.md +0 -69
- package/docs/MCP_WRAPPER.md +0 -159
- package/docs/MIGRATION_GUIDE.md +0 -40
- package/docs/OPERATORS.md +0 -184
- package/docs/PERF_REGRESSION_POLICY.md +0 -34
- package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
- package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
- package/docs/RELEASE_GATE.md +0 -25
- package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
- package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
- package/docs/TELEMETRY.md +0 -41
- package/docs/THREAT_MODEL.md +0 -89
- package/docs/VERSIONING_POLICY.md +0 -30
- package/docs/assets/banner-github.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/banner.svg +0 -123
- package/eslint.config.js +0 -15
- package/fuzz/ts/harness-decode.ts +0 -136
- package/fuzz/ts/harness-verify.ts +0 -121
- package/fuzz/ts/run.ts +0 -35
- package/mcp-server/README.md +0 -38
- package/mcp-server/package-lock.json +0 -1187
- package/mcp-server/package.json +0 -35
- package/mcp-server/src/index.ts +0 -236
- package/mcp-server/tsconfig.json +0 -14
- package/scripts/aip-framework-quickstart.ts +0 -110
- package/scripts/aip-mcp-gateway.ts +0 -38
- package/scripts/aip-mcp-wrap-demo.ts +0 -72
- package/scripts/aip-quickstart.ts +0 -60
- package/scripts/bench-diff.ts +0 -118
- package/scripts/bench-protocol-e2e.ts +0 -937
- package/scripts/bench-protocol-openloop.ts +0 -1397
- package/scripts/bench-replay-cache.ts +0 -76
- package/scripts/bench-signature-profiles.ts +0 -180
- package/scripts/bench-wire-codecs.ts +0 -161
- package/scripts/build-binary-conformance.ts +0 -36
- package/scripts/build-release-dashboard.ts +0 -175
- package/scripts/canary-rollout.ts +0 -38
- package/scripts/mcpGatewayCli.test.ts +0 -116
- package/scripts/prepare-aip-package.ts +0 -88
- package/scripts/regen-conformance-sigs.ts +0 -18
- package/scripts/release-gate.ts +0 -19
- package/scripts/validate-runtime-policy.ts +0 -18
- package/sdk/browser/index.test.ts +0 -162
- package/sdk/browser/index.ts +0 -257
- package/sdk/browser/package.json +0 -13
- package/sdk/go/cbor.go +0 -551
- package/sdk/go/cbor_test.go +0 -232
- package/sdk/go/encryption.go +0 -280
- package/sdk/go/encryption_test.go +0 -318
- package/sdk/go/go.mod +0 -7
- package/sdk/go/go.sum +0 -4
- package/sdk/go/http.go +0 -135
- package/sdk/go/protocol.go +0 -324
- package/sdk/go/protocol_test.go +0 -334
- package/sdk/go/replay.go +0 -121
- package/sdk/go/replay_test.go +0 -149
- package/sdk/go/webhook.go +0 -136
- package/sdk/pq/package-lock.json +0 -1358
- package/sdk/pq/package.json +0 -42
- package/sdk/pq/src/index.test.ts +0 -143
- package/sdk/pq/src/index.ts +0 -166
- package/sdk/pq/tsconfig.json +0 -14
- package/sdk/pq/vitest.config.ts +0 -7
- package/sdk/python/README.md +0 -18
- package/sdk/python/protocol_7h3/__init__.py +0 -46
- package/sdk/python/protocol_7h3/encryption.py +0 -252
- package/sdk/python/protocol_7h3/http.py +0 -212
- package/sdk/python/protocol_7h3/keys.py +0 -149
- package/sdk/python/protocol_7h3/pq.py +0 -244
- package/sdk/python/protocol_7h3/protocol.py +0 -525
- package/sdk/python/protocol_7h3/queue.py +0 -118
- package/sdk/python/protocol_7h3/replay.py +0 -98
- package/sdk/python/protocol_7h3/webhook.py +0 -116
- package/sdk/python/pyproject.toml +0 -40
- package/sdk/python/tests/test_conformance.py +0 -110
- package/sdk/python/tests/test_encryption.py +0 -206
- package/sdk/python/tests/test_http.py +0 -305
- package/sdk/python/tests/test_keys.py +0 -417
- package/sdk/python/tests/test_queue.py +0 -120
- package/sdk/python/tests/test_webhook.py +0 -345
- package/sdk/rust/Cargo.lock +0 -371
- package/sdk/rust/Cargo.toml +0 -25
- package/sdk/rust/README.md +0 -31
- package/sdk/rust/fuzz/Cargo.toml +0 -29
- package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
- package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
- package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
- package/sdk/rust/src/http.rs +0 -145
- package/sdk/rust/src/keys.rs +0 -161
- package/sdk/rust/src/lib.rs +0 -688
- package/sdk/rust/src/queue.rs +0 -79
- package/sdk/rust/src/webhook.rs +0 -86
- package/sdk/rust/tests/conformance.rs +0 -148
- package/sdk/rust/tests/gateway.rs +0 -130
- package/sdk/rust/tests/http_webhook_queue.rs +0 -201
- package/sdk/rust/tests/keys.rs +0 -189
- package/sdk/threshold/index.d.ts +0 -68
- package/sdk/threshold/index.d.ts.map +0 -1
- package/sdk/threshold/index.js +0 -254
- package/sdk/threshold/package-lock.json +0 -1361
- package/sdk/threshold/package.json +0 -39
- package/sdk/threshold/src/index.d.ts +0 -68
- package/sdk/threshold/src/index.d.ts.map +0 -1
- package/sdk/threshold/src/index.js +0 -254
- package/sdk/threshold/src/index.test.ts +0 -238
- package/sdk/threshold/src/index.ts +0 -355
- package/sdk/threshold/tsconfig.json +0 -19
- package/sdk/threshold/vitest.config.ts +0 -12
- package/src/agentAdapter.test.ts +0 -48
- package/src/agentAdapter.ts +0 -56
- package/src/auditLog.test.ts +0 -145
- package/src/auditLog.ts +0 -147
- package/src/capability.test.ts +0 -504
- package/src/capability.ts +0 -380
- package/src/cborCodec.test.ts +0 -263
- package/src/cborCodec.ts +0 -339
- package/src/conformance.test.ts +0 -136
- package/src/conformanceVectors.ts +0 -99
- package/src/encryption.test.ts +0 -206
- package/src/encryption.ts +0 -245
- package/src/envelopeCbor.ts +0 -140
- package/src/frameworkAdapters.test.ts +0 -290
- package/src/frameworkAdapters.ts +0 -261
- package/src/gateway.test.ts +0 -343
- package/src/gateway.ts +0 -246
- package/src/grpcBinding.test.ts +0 -211
- package/src/grpcBinding.ts +0 -103
- package/src/httpBinding.test.ts +0 -376
- package/src/httpBinding.ts +0 -189
- package/src/index.ts +0 -39
- package/src/keyInfra.test.ts +0 -278
- package/src/keyInfra.ts +0 -228
- package/src/keyRegistry.ts +0 -59
- package/src/keyRotation.test.ts +0 -78
- package/src/keyRotation.ts +0 -72
- package/src/mcpGateway.test.ts +0 -129
- package/src/mcpGateway.ts +0 -250
- package/src/mcpTransports.test.ts +0 -92
- package/src/mcpTransports.ts +0 -169
- package/src/mcpWrapper.test.ts +0 -179
- package/src/mcpWrapper.ts +0 -206
- package/src/otel.ts +0 -136
- package/src/policyEnforcer.test.ts +0 -99
- package/src/policyEnforcer.ts +0 -169
- package/src/policyTelemetryFeedback.test.ts +0 -25
- package/src/policyTelemetryFeedback.ts +0 -38
- package/src/protocol.bench.ts +0 -37
- package/src/protocol.d.ts.map +0 -1
- package/src/protocol.js +0 -294
- package/src/protocol.test.ts +0 -155
- package/src/protocol.ts +0 -414
- package/src/protocolAgent.test.ts +0 -105
- package/src/protocolAgent.ts +0 -169
- package/src/protocolBinary.test.ts +0 -165
- package/src/protocolBinary.ts +0 -312
- package/src/protocolCapabilities.ts +0 -70
- package/src/protocolFuzz.advanced.test.ts +0 -235
- package/src/protocolFuzz.test.ts +0 -111
- package/src/protocolNegative.test.ts +0 -97
- package/src/protocolReplay.test.ts +0 -71
- package/src/protocolReplay.ts +0 -194
- package/src/protocolTransport.test.ts +0 -556
- package/src/protocolTransport.ts +0 -483
- package/src/queueBinding.test.ts +0 -130
- package/src/queueBinding.ts +0 -102
- package/src/rateLimiter.test.ts +0 -96
- package/src/rateLimiter.ts +0 -46
- package/src/redisIntegration.test.ts +0 -134
- package/src/replayStores.test.ts +0 -273
- package/src/replayStores.ts +0 -215
- package/src/revocation.test.ts +0 -98
- package/src/revocation.ts +0 -0
- package/src/routePolicy.test.ts +0 -87
- package/src/routePolicy.ts +0 -72
- package/src/runtimePolicy.test.ts +0 -49
- package/src/runtimePolicy.ts +0 -81
- package/src/runtimePolicyManager.test.ts +0 -29
- package/src/runtimePolicyManager.ts +0 -50
- package/src/runtimePolicyPresets.ts +0 -43
- package/src/signedResponse.test.ts +0 -111
- package/src/signedResponse.ts +0 -83
- package/src/stream.test.ts +0 -254
- package/src/stream.ts +0 -417
- package/src/telemetry.test.ts +0 -251
- package/src/telemetry.ts +0 -299
- package/src/webhookBinding.test.ts +0 -144
- package/src/webhookBinding.ts +0 -115
- package/src/wsBinding.test.ts +0 -221
- package/src/wsBinding.ts +0 -200
- package/tsconfig.json +0 -15
- package/tsconfig.lib.json +0 -23
- package/vite.lib.config.ts +0 -16
- package/vitest.config.ts +0 -11
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { RuntimeMode } from './policyEnforcer'
|
|
2
|
-
|
|
3
|
-
export type RuntimeEnvironment = 'dev' | 'staging' | 'prod'
|
|
4
|
-
|
|
5
|
-
export interface RuntimeTuningPreset {
|
|
6
|
-
environment: RuntimeEnvironment
|
|
7
|
-
mode: RuntimeMode
|
|
8
|
-
poolSize: number
|
|
9
|
-
batchSize: number
|
|
10
|
-
inflightCap: number
|
|
11
|
-
retryMaxAttempts: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const PRESETS: Record<RuntimeEnvironment, RuntimeTuningPreset> = {
|
|
15
|
-
dev: {
|
|
16
|
-
environment: 'dev',
|
|
17
|
-
mode: 'ws-batch',
|
|
18
|
-
poolSize: 1,
|
|
19
|
-
batchSize: 8,
|
|
20
|
-
inflightCap: 64,
|
|
21
|
-
retryMaxAttempts: 2,
|
|
22
|
-
},
|
|
23
|
-
staging: {
|
|
24
|
-
environment: 'staging',
|
|
25
|
-
mode: 'http-binary-batch',
|
|
26
|
-
poolSize: 2,
|
|
27
|
-
batchSize: 32,
|
|
28
|
-
inflightCap: 256,
|
|
29
|
-
retryMaxAttempts: 3,
|
|
30
|
-
},
|
|
31
|
-
prod: {
|
|
32
|
-
environment: 'prod',
|
|
33
|
-
mode: 'http-binary-batch',
|
|
34
|
-
poolSize: 4,
|
|
35
|
-
batchSize: 64,
|
|
36
|
-
inflightCap: 1024,
|
|
37
|
-
retryMaxAttempts: 3,
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getRuntimeTuningPreset(environment: RuntimeEnvironment): RuntimeTuningPreset {
|
|
42
|
-
return PRESETS[environment]
|
|
43
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
-
import { signResponse, verifyResponse, RESPONSE_HEADER } from './signedResponse'
|
|
3
|
-
import { generateEd25519KeypairBase64Url } from './protocol'
|
|
4
|
-
|
|
5
|
-
let keys: { publicKey: string; privateKey: string }
|
|
6
|
-
let wrongKeys: { publicKey: string; privateKey: string }
|
|
7
|
-
|
|
8
|
-
beforeAll(async () => {
|
|
9
|
-
keys = await generateEd25519KeypairBase64Url()
|
|
10
|
-
wrongKeys = await generateEd25519KeypairBase64Url()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
describe('signedResponse', () => {
|
|
14
|
-
it('round-trip: signs and verifies correctly', async () => {
|
|
15
|
-
const body = 'hello world'
|
|
16
|
-
const { headers } = await signResponse(body, {
|
|
17
|
-
privateKey: keys.privateKey,
|
|
18
|
-
sender: 'gateway-agent',
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const result = await verifyResponse(body, headers, { publicKey: keys.publicKey })
|
|
22
|
-
expect(result.ok).toBe(true)
|
|
23
|
-
expect(result.envelope?.body.intent).toBe('RESULT')
|
|
24
|
-
expect(result.envelope?.body.content).toBe(body)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('fails when body does not match envelope content', async () => {
|
|
28
|
-
const body = 'original body'
|
|
29
|
-
const { headers } = await signResponse(body, {
|
|
30
|
-
privateKey: keys.privateKey,
|
|
31
|
-
sender: 'gateway-agent',
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const result = await verifyResponse('tampered body', headers, { publicKey: keys.publicKey })
|
|
35
|
-
expect(result.ok).toBe(false)
|
|
36
|
-
expect(result.reason).toBe('content-mismatch')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('fails with wrong public key', async () => {
|
|
40
|
-
const body = 'some content'
|
|
41
|
-
const { headers } = await signResponse(body, {
|
|
42
|
-
privateKey: keys.privateKey,
|
|
43
|
-
sender: 'gateway-agent',
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const result = await verifyResponse(body, headers, { publicKey: wrongKeys.publicKey })
|
|
47
|
-
expect(result.ok).toBe(false)
|
|
48
|
-
expect(result.reason).toBe('invalid-signature')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('fails when header is missing', async () => {
|
|
52
|
-
const result = await verifyResponse('body', {}, { publicKey: keys.publicKey })
|
|
53
|
-
expect(result.ok).toBe(false)
|
|
54
|
-
expect(result.reason).toBe('missing-header')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('fails when envelope TTL is expired', async () => {
|
|
58
|
-
// Sign with 1ms ttl — immediately expired
|
|
59
|
-
const body = 'ttl-test'
|
|
60
|
-
const { headers } = await signResponse(body, {
|
|
61
|
-
privateKey: keys.privateKey,
|
|
62
|
-
sender: 'gateway-agent',
|
|
63
|
-
ttlMs: 1,
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// Wait a tick to ensure expiry
|
|
67
|
-
await new Promise(r => setTimeout(r, 10))
|
|
68
|
-
|
|
69
|
-
const result = await verifyResponse(body, headers, { publicKey: keys.publicKey })
|
|
70
|
-
expect(result.ok).toBe(false)
|
|
71
|
-
expect(result.reason).toBe('ttl-expired')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('fails when maxAgeMs exceeded', async () => {
|
|
75
|
-
const body = 'max-age-test'
|
|
76
|
-
const { headers } = await signResponse(body, {
|
|
77
|
-
privateKey: keys.privateKey,
|
|
78
|
-
sender: 'gateway-agent',
|
|
79
|
-
ttlMs: 60_000, // long ttl
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// Wait to ensure age exceeds maxAgeMs
|
|
83
|
-
await new Promise(r => setTimeout(r, 10))
|
|
84
|
-
|
|
85
|
-
const result = await verifyResponse(body, headers, {
|
|
86
|
-
publicKey: keys.publicKey,
|
|
87
|
-
maxAgeMs: 1, // tiny window
|
|
88
|
-
})
|
|
89
|
-
expect(result.ok).toBe(false)
|
|
90
|
-
expect(result.reason).toBe('ttl-expired')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('passes correlationId and recipient through envelope', async () => {
|
|
94
|
-
const body = 'corr-test'
|
|
95
|
-
const { headers } = await signResponse(body, {
|
|
96
|
-
privateKey: keys.privateKey,
|
|
97
|
-
sender: 'gateway-agent',
|
|
98
|
-
recipient: 'client-agent',
|
|
99
|
-
correlationId: 'req-123',
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const result = await verifyResponse(body, headers, { publicKey: keys.publicKey })
|
|
103
|
-
expect(result.ok).toBe(true)
|
|
104
|
-
expect(result.envelope?.body.correlationId).toBe('req-123')
|
|
105
|
-
expect(result.envelope?.header.recipient).toBe('client-agent')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('RESPONSE_HEADER constant is x-7h3-response', () => {
|
|
109
|
-
expect(RESPONSE_HEADER).toBe('x-7h3-response')
|
|
110
|
-
})
|
|
111
|
-
})
|
package/src/signedResponse.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createEnvelope,
|
|
3
|
-
signEnvelopeEd25519,
|
|
4
|
-
verifyEnvelopeEd25519,
|
|
5
|
-
type ProtocolEnvelope,
|
|
6
|
-
} from './protocol'
|
|
7
|
-
|
|
8
|
-
export const RESPONSE_HEADER = 'x-7h3-response'
|
|
9
|
-
|
|
10
|
-
export async function signResponse(
|
|
11
|
-
body: string,
|
|
12
|
-
opts: {
|
|
13
|
-
privateKey: string
|
|
14
|
-
sender: string
|
|
15
|
-
recipient?: string
|
|
16
|
-
correlationId?: string
|
|
17
|
-
ttlMs?: number
|
|
18
|
-
},
|
|
19
|
-
): Promise<{ headers: { 'x-7h3-response': string } }> {
|
|
20
|
-
const envelope = createEnvelope({
|
|
21
|
-
sender: opts.sender,
|
|
22
|
-
recipient: opts.recipient,
|
|
23
|
-
intent: 'RESULT',
|
|
24
|
-
content: body,
|
|
25
|
-
correlationId: opts.correlationId,
|
|
26
|
-
ttlMs: opts.ttlMs ?? 60_000,
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const signed = await signEnvelopeEd25519(envelope, opts.privateKey)
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
headers: {
|
|
33
|
-
[RESPONSE_HEADER]: JSON.stringify(signed),
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function verifyResponse(
|
|
39
|
-
body: string,
|
|
40
|
-
headers: Record<string, string | string[] | undefined>,
|
|
41
|
-
opts: {
|
|
42
|
-
publicKey: string
|
|
43
|
-
maxAgeMs?: number
|
|
44
|
-
},
|
|
45
|
-
): Promise<{ ok: boolean; envelope?: ProtocolEnvelope; reason?: string }> {
|
|
46
|
-
const raw = headers[RESPONSE_HEADER]
|
|
47
|
-
const rawStr = Array.isArray(raw) ? raw[0] : raw
|
|
48
|
-
if (!rawStr) {
|
|
49
|
-
return { ok: false, reason: 'missing-header' }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let envelope: ProtocolEnvelope
|
|
53
|
-
try {
|
|
54
|
-
envelope = JSON.parse(rawStr) as ProtocolEnvelope
|
|
55
|
-
} catch {
|
|
56
|
-
return { ok: false, reason: 'malformed-envelope' }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Check TTL from envelope
|
|
60
|
-
const nowMs = Date.now()
|
|
61
|
-
const { timestampMs, ttlMs } = envelope.header
|
|
62
|
-
if (timestampMs + ttlMs < nowMs) {
|
|
63
|
-
return { ok: false, reason: 'ttl-expired', envelope }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check maxAgeMs if provided
|
|
67
|
-
if (opts.maxAgeMs !== undefined && nowMs - timestampMs > opts.maxAgeMs) {
|
|
68
|
-
return { ok: false, reason: 'ttl-expired', envelope }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Verify signature
|
|
72
|
-
const valid = await verifyEnvelopeEd25519(envelope, opts.publicKey)
|
|
73
|
-
if (!valid) {
|
|
74
|
-
return { ok: false, reason: 'invalid-signature', envelope }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Verify body matches envelope content
|
|
78
|
-
if (envelope.body.content !== body) {
|
|
79
|
-
return { ok: false, reason: 'content-mismatch', envelope }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { ok: true, envelope }
|
|
83
|
-
}
|
package/src/stream.test.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
SignedStreamWriter,
|
|
4
|
-
SignedStreamReader,
|
|
5
|
-
createSignedStream,
|
|
6
|
-
createStreamVerifier,
|
|
7
|
-
signStream,
|
|
8
|
-
verifyStream,
|
|
9
|
-
encodeStreamChunk,
|
|
10
|
-
decodeStreamChunk,
|
|
11
|
-
type StreamChunk,
|
|
12
|
-
type StreamSignerOpts,
|
|
13
|
-
type StreamVerifierOpts,
|
|
14
|
-
} from './stream'
|
|
15
|
-
import { generateEd25519KeypairBase64Url } from './protocol'
|
|
16
|
-
|
|
17
|
-
// ─────────────────── helpers ───────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
async function makeKeyPair() {
|
|
20
|
-
return generateEd25519KeypairBase64Url()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function signerOpts(privateKey: string, nonce?: string): StreamSignerOpts {
|
|
24
|
-
return { privateKey, sender: 'test-agent', nonce: nonce ?? 'fixed-nonce-for-tests', keyId: 'test-key' }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function verifierOpts(publicKey: string): StreamVerifierOpts {
|
|
28
|
-
return { publicKey }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─────────────────── tests ────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
describe('SignedStreamWriter + SignedStreamReader', () => {
|
|
34
|
-
it('test 1: 5-chunk round trip verifies', async () => {
|
|
35
|
-
const kp = await makeKeyPair()
|
|
36
|
-
const writer = new SignedStreamWriter(signerOpts(kp.privateKey))
|
|
37
|
-
const reader = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
38
|
-
|
|
39
|
-
const dataChunks = ['Hello ', 'World', ' from', ' 7h3', ' Protocol']
|
|
40
|
-
const frames: StreamChunk[] = []
|
|
41
|
-
|
|
42
|
-
for (const d of dataChunks) {
|
|
43
|
-
const frame = await writer.writeChunk(d)
|
|
44
|
-
expect(frame.f).toBe(false)
|
|
45
|
-
expect(frame.i).toBe(frames.length)
|
|
46
|
-
expect(typeof frame.h).toBe('string')
|
|
47
|
-
frames.push(frame)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Feed non-final chunks to reader
|
|
51
|
-
for (const frame of frames) {
|
|
52
|
-
const res = await reader.receiveChunk(frame)
|
|
53
|
-
expect(res.ok).toBe(true)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const finalFrame = await writer.finalize()
|
|
57
|
-
expect(finalFrame.f).toBe(true)
|
|
58
|
-
expect(typeof finalFrame.sig).toBe('string')
|
|
59
|
-
expect(finalFrame.kid).toBe('test-key')
|
|
60
|
-
|
|
61
|
-
const result = await reader.finalize(finalFrame)
|
|
62
|
-
expect(result.ok).toBe(true)
|
|
63
|
-
if (result.ok) {
|
|
64
|
-
expect(result.chunkCount).toBe(5)
|
|
65
|
-
// totalBytes = sum of UTF-8 bytes of each data chunk
|
|
66
|
-
const expected = dataChunks.reduce((s, d) => s + new TextEncoder().encode(d).length, 0)
|
|
67
|
-
expect(result.totalBytes).toBe(expected)
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('test 2: single chunk round trip', async () => {
|
|
72
|
-
const kp = await makeKeyPair()
|
|
73
|
-
const writer = new SignedStreamWriter(signerOpts(kp.privateKey))
|
|
74
|
-
const reader = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
75
|
-
|
|
76
|
-
const frame = await writer.writeChunk('only one chunk')
|
|
77
|
-
const res = await reader.receiveChunk(frame)
|
|
78
|
-
expect(res.ok).toBe(true)
|
|
79
|
-
|
|
80
|
-
const finalFrame = await writer.finalize()
|
|
81
|
-
const result = await reader.finalize(finalFrame)
|
|
82
|
-
expect(result.ok).toBe(true)
|
|
83
|
-
if (result.ok) {
|
|
84
|
-
expect(result.chunkCount).toBe(1)
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('test 3: empty stream (just finalize) verifies', async () => {
|
|
89
|
-
const kp = await makeKeyPair()
|
|
90
|
-
const writer = new SignedStreamWriter(signerOpts(kp.privateKey))
|
|
91
|
-
const reader = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
92
|
-
|
|
93
|
-
const finalFrame = await writer.finalize()
|
|
94
|
-
expect(finalFrame.f).toBe(true)
|
|
95
|
-
expect(typeof finalFrame.sig).toBe('string')
|
|
96
|
-
|
|
97
|
-
const result = await reader.finalize(finalFrame)
|
|
98
|
-
expect(result.ok).toBe(true)
|
|
99
|
-
if (result.ok) {
|
|
100
|
-
expect(result.chunkCount).toBe(0)
|
|
101
|
-
expect(result.totalBytes).toBe(0)
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('test 4: tampered chunk data detected (HMAC fails on that chunk, Ed25519 fails at finalize)', async () => {
|
|
106
|
-
const kp = await makeKeyPair()
|
|
107
|
-
const writer = new SignedStreamWriter(signerOpts(kp.privateKey))
|
|
108
|
-
const reader = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
109
|
-
|
|
110
|
-
const frame0 = await writer.writeChunk('legit data')
|
|
111
|
-
const frame1 = await writer.writeChunk('also legit')
|
|
112
|
-
const finalFrame = await writer.finalize()
|
|
113
|
-
|
|
114
|
-
// Tamper with frame0's data
|
|
115
|
-
const tampered: StreamChunk = { ...frame0, d: 'TAMPERED' }
|
|
116
|
-
|
|
117
|
-
// Reader accepts the chunk (sequence is fine; reader can't verify HMAC without private key)
|
|
118
|
-
await reader.receiveChunk(tampered)
|
|
119
|
-
await reader.receiveChunk(frame1)
|
|
120
|
-
|
|
121
|
-
// But the final Ed25519 signature will fail because the content hash is different
|
|
122
|
-
const result = await reader.finalize(finalFrame)
|
|
123
|
-
expect(result.ok).toBe(false)
|
|
124
|
-
if (!result.ok) {
|
|
125
|
-
expect(result.reason).toMatch(/signature verification failed/i)
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('test 5: wrong public key: final Ed25519 fails', async () => {
|
|
130
|
-
const kp = await makeKeyPair()
|
|
131
|
-
const wrongKp = await makeKeyPair()
|
|
132
|
-
|
|
133
|
-
const writer = new SignedStreamWriter(signerOpts(kp.privateKey))
|
|
134
|
-
// Reader uses a DIFFERENT public key
|
|
135
|
-
const reader = new SignedStreamReader(verifierOpts(wrongKp.publicKey))
|
|
136
|
-
|
|
137
|
-
const frame = await writer.writeChunk('data')
|
|
138
|
-
await reader.receiveChunk(frame)
|
|
139
|
-
|
|
140
|
-
const finalFrame = await writer.finalize()
|
|
141
|
-
const result = await reader.finalize(finalFrame)
|
|
142
|
-
expect(result.ok).toBe(false)
|
|
143
|
-
if (!result.ok) {
|
|
144
|
-
expect(result.reason).toMatch(/signature verification failed/i)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('test 6: out-of-order chunk: sequence error', async () => {
|
|
149
|
-
const kp = await makeKeyPair()
|
|
150
|
-
const writer = new SignedStreamWriter(signerOpts(kp.privateKey))
|
|
151
|
-
const reader = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
152
|
-
|
|
153
|
-
const frame0 = await writer.writeChunk('first')
|
|
154
|
-
const frame1 = await writer.writeChunk('second')
|
|
155
|
-
|
|
156
|
-
// Feed frame1 before frame0 (out of order — frame1 has i=1, reader expects i=0)
|
|
157
|
-
const resOoo = await reader.receiveChunk(frame1)
|
|
158
|
-
expect(resOoo.ok).toBe(false)
|
|
159
|
-
if (!resOoo.ok) {
|
|
160
|
-
expect(resOoo.reason).toMatch(/sequence error/i)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Feed in yet another out-of-order delivery: feed frame0, then frame1 again — frame1 will be seen
|
|
164
|
-
// as seq=1 but reader has only accepted 0 so far (via the successful frame0), so now inject a
|
|
165
|
-
// duplicate of frame1 which would be at seq=1 yet again — that should be fine.
|
|
166
|
-
// More importantly: verify that the FIRST out-of-order was caught.
|
|
167
|
-
// The reader did NOT increment on failure, so frame0 (i=0) is now OK.
|
|
168
|
-
const resAfter = await reader.receiveChunk(frame0)
|
|
169
|
-
expect(resAfter.ok).toBe(true) // seq=0 is correct now (reader expected 0 after ooo failure)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('test 7: signStream(array) + verifyStream round trip', async () => {
|
|
173
|
-
const kp = await makeKeyPair()
|
|
174
|
-
const data = ['chunk A', 'chunk B', 'chunk C']
|
|
175
|
-
const chunks = await signStream(data, signerOpts(kp.privateKey))
|
|
176
|
-
|
|
177
|
-
// signStream returns n+1 frames (n data + 1 final)
|
|
178
|
-
expect(chunks).toHaveLength(data.length + 1)
|
|
179
|
-
expect(chunks[chunks.length - 1].f).toBe(true)
|
|
180
|
-
|
|
181
|
-
const result = await verifyStream(chunks, verifierOpts(kp.publicKey))
|
|
182
|
-
expect(result.ok).toBe(true)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('test 8: replay — same StreamChunk in a different stream fails (nonce in HMAC)', async () => {
|
|
186
|
-
const kp = await makeKeyPair()
|
|
187
|
-
|
|
188
|
-
// Stream A
|
|
189
|
-
const writerA = new SignedStreamWriter({ ...signerOpts(kp.privateKey), nonce: 'nonce-A' })
|
|
190
|
-
const chunkA0 = await writerA.writeChunk('data A')
|
|
191
|
-
const finalA = await writerA.finalize()
|
|
192
|
-
|
|
193
|
-
// Stream B — different nonce
|
|
194
|
-
const writerB = new SignedStreamWriter({ ...signerOpts(kp.privateKey), nonce: 'nonce-B' })
|
|
195
|
-
// Don't write any chunk to B, then produce its final frame
|
|
196
|
-
const finalB = await writerB.finalize()
|
|
197
|
-
|
|
198
|
-
// Reader for stream B expects B's content — inject A's chunk into B's reader
|
|
199
|
-
const readerB = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
200
|
-
// Feed chunk from stream A (which has content 'data A') and use B's finalFrame
|
|
201
|
-
await readerB.receiveChunk(chunkA0)
|
|
202
|
-
// finalB was computed over empty content — now we have chunkA0's data accumulated
|
|
203
|
-
const result = await readerB.finalize(finalB)
|
|
204
|
-
expect(result.ok).toBe(false)
|
|
205
|
-
if (!result.ok) {
|
|
206
|
-
expect(result.reason).toMatch(/signature verification failed/i)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Stream A itself verifies fine
|
|
210
|
-
const readerA = new SignedStreamReader(verifierOpts(kp.publicKey))
|
|
211
|
-
await readerA.receiveChunk(chunkA0)
|
|
212
|
-
const resultA = await readerA.finalize(finalA)
|
|
213
|
-
expect(resultA.ok).toBe(true)
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('test 9: large stream — 1000 chunks verify in <500ms', async () => {
|
|
217
|
-
const kp = await makeKeyPair()
|
|
218
|
-
const chunks: string[] = Array.from({ length: 1000 }, (_, i) => `token-${i}`)
|
|
219
|
-
|
|
220
|
-
const start = Date.now()
|
|
221
|
-
const frames = await signStream(chunks, signerOpts(kp.privateKey))
|
|
222
|
-
const result = await verifyStream(frames, verifierOpts(kp.publicKey))
|
|
223
|
-
const elapsed = Date.now() - start
|
|
224
|
-
|
|
225
|
-
expect(result.ok).toBe(true)
|
|
226
|
-
expect(elapsed).toBeLessThan(500)
|
|
227
|
-
if (result.ok) {
|
|
228
|
-
expect(result.chunkCount).toBe(1000)
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
describe('encodeStreamChunk / decodeStreamChunk', () => {
|
|
234
|
-
it('round-trips a non-final chunk', () => {
|
|
235
|
-
const chunk: StreamChunk = { i: 3, d: 'hello', h: 'abc-def', f: false }
|
|
236
|
-
const encoded = encodeStreamChunk(chunk)
|
|
237
|
-
const decoded = decodeStreamChunk(encoded)
|
|
238
|
-
expect(decoded).toEqual(chunk)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
it('round-trips a final chunk', () => {
|
|
242
|
-
const chunk: StreamChunk = { i: 10, d: '', h: 'xxx', f: true, sig: 'yyy', kid: 'mykey' }
|
|
243
|
-
expect(decodeStreamChunk(encodeStreamChunk(chunk))).toEqual(chunk)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('throws on invalid JSON', () => {
|
|
247
|
-
expect(() => decodeStreamChunk('not json')).toThrow(/invalid JSON/i)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('throws on missing required fields', () => {
|
|
251
|
-
expect(() => decodeStreamChunk('{"i":0,"d":"x"}')).toThrow(/missing required fields/i)
|
|
252
|
-
})
|
|
253
|
-
})
|
|
254
|
-
|