@peac/transport-grpc 0.12.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/.turbo/turbo-build.log +25 -0
- package/.turbo/turbo-test.log +15 -0
- package/LICENSE +190 -0
- package/README.md +110 -0
- package/dist/a2a-carrier.d.ts +71 -0
- package/dist/a2a-carrier.d.ts.map +1 -0
- package/dist/index.cjs +324 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +297 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +304 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metadata.d.ts +42 -0
- package/dist/metadata.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/a2a-carrier.ts +158 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +313 -0
- package/src/metadata.ts +85 -0
- package/tests/a2a-carrier.test.ts +208 -0
- package/tests/status-parity.test.ts +361 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +19 -0
- package/vitest.config.d.ts.map +1 -0
- package/vitest.config.js.map +1 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { PeacEvidenceCarrier } from '@peac/kernel';
|
|
3
|
+
import {
|
|
4
|
+
A2AGrpcCarrierAdapter,
|
|
5
|
+
createGrpcCarrierMeta,
|
|
6
|
+
validateOwnMetadataKeys,
|
|
7
|
+
GRPC_MAX_CARRIER_SIZE,
|
|
8
|
+
GrpcMetadataKeys,
|
|
9
|
+
addReceiptToMetadata,
|
|
10
|
+
extractReceiptTypeFromMetadata,
|
|
11
|
+
GRPC_TRANSPORT_VERSION,
|
|
12
|
+
} from '../src/index.js';
|
|
13
|
+
|
|
14
|
+
const VALID_REF = 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
|
15
|
+
|
|
16
|
+
const VALID_CARRIER: PeacEvidenceCarrier = {
|
|
17
|
+
receipt_ref: VALID_REF as PeacEvidenceCarrier['receipt_ref'],
|
|
18
|
+
receipt_jws: 'eyJhbGciOiJFZERTQSIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJ0ZXN0In0.dGVzdA',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('A2AGrpcCarrierAdapter', () => {
|
|
22
|
+
const adapter = new A2AGrpcCarrierAdapter();
|
|
23
|
+
|
|
24
|
+
describe('attach + extract round-trip', () => {
|
|
25
|
+
it('round-trips: attach then extract returns receipt JWS', () => {
|
|
26
|
+
const metadata: Record<string, string | string[] | undefined> = {};
|
|
27
|
+
const attached = adapter.attach(metadata, [VALID_CARRIER]);
|
|
28
|
+
|
|
29
|
+
const extracted = adapter.extract(attached);
|
|
30
|
+
expect(extracted).not.toBeNull();
|
|
31
|
+
expect(extracted!.receipts).toHaveLength(1);
|
|
32
|
+
expect(extracted!.receipts[0].receipt_jws).toBe(VALID_CARRIER.receipt_jws);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('computes real SHA-256 receipt_ref (not a placeholder)', () => {
|
|
36
|
+
const metadata: Record<string, string | string[] | undefined> = {};
|
|
37
|
+
adapter.attach(metadata, [VALID_CARRIER]);
|
|
38
|
+
|
|
39
|
+
const extracted = adapter.extract(metadata);
|
|
40
|
+
expect(extracted).not.toBeNull();
|
|
41
|
+
const ref = extracted!.receipts[0].receipt_ref;
|
|
42
|
+
expect(ref).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
43
|
+
expect(ref).not.toBe('sha256:' + '0'.repeat(64));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('uses first carrier when multiple provided', () => {
|
|
47
|
+
const second: PeacEvidenceCarrier = {
|
|
48
|
+
receipt_ref: VALID_REF as PeacEvidenceCarrier['receipt_ref'],
|
|
49
|
+
receipt_jws: 'eyJzZWNvbmQifQ.eyJ0ZXN0In0.c2Vjb25k',
|
|
50
|
+
};
|
|
51
|
+
const metadata: Record<string, string | string[] | undefined> = {};
|
|
52
|
+
adapter.attach(metadata, [VALID_CARRIER, second]);
|
|
53
|
+
|
|
54
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT]).toBe(VALID_CARRIER.receipt_jws);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('extract', () => {
|
|
59
|
+
it('returns null when no receipt in metadata', () => {
|
|
60
|
+
expect(adapter.extract({})).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns null when receipt key is undefined', () => {
|
|
64
|
+
expect(adapter.extract({ [GrpcMetadataKeys.RECEIPT]: undefined })).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('extracts from string metadata value', () => {
|
|
68
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: VALID_CARRIER.receipt_jws! };
|
|
69
|
+
const result = adapter.extract(metadata);
|
|
70
|
+
expect(result).not.toBeNull();
|
|
71
|
+
expect(result!.receipts[0].receipt_jws).toBe(VALID_CARRIER.receipt_jws);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('extracts first value from array metadata', () => {
|
|
75
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: [VALID_CARRIER.receipt_jws!] };
|
|
76
|
+
const result = adapter.extract(metadata);
|
|
77
|
+
expect(result).not.toBeNull();
|
|
78
|
+
expect(result!.receipts[0].receipt_jws).toBe(VALID_CARRIER.receipt_jws);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns grpc transport meta with 8 KiB default', () => {
|
|
82
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: VALID_CARRIER.receipt_jws! };
|
|
83
|
+
const result = adapter.extract(metadata);
|
|
84
|
+
expect(result!.meta.transport).toBe('grpc');
|
|
85
|
+
expect(result!.meta.format).toBe('embed');
|
|
86
|
+
expect(result!.meta.max_size).toBe(8_192);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects binary metadata key (peac-receipt-bin)', () => {
|
|
90
|
+
const metadata = {
|
|
91
|
+
[GrpcMetadataKeys.RECEIPT]: VALID_CARRIER.receipt_jws!,
|
|
92
|
+
'peac-receipt-bin': 'binary-data',
|
|
93
|
+
};
|
|
94
|
+
const result = adapter.extract(metadata);
|
|
95
|
+
expect(result).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('attach', () => {
|
|
100
|
+
it('returns metadata unchanged when no carriers', () => {
|
|
101
|
+
const metadata = { existing: 'value' };
|
|
102
|
+
const result = adapter.attach(metadata, []);
|
|
103
|
+
expect(result).toBe(metadata);
|
|
104
|
+
expect(result[GrpcMetadataKeys.RECEIPT]).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('throws on carrier exceeding 8 KiB size limit', () => {
|
|
108
|
+
const oversized: PeacEvidenceCarrier = {
|
|
109
|
+
receipt_ref: VALID_REF as PeacEvidenceCarrier['receipt_ref'],
|
|
110
|
+
receipt_jws: 'x'.repeat(10_000),
|
|
111
|
+
};
|
|
112
|
+
expect(() => adapter.attach({}, [oversized])).toThrow(/constraint violation/i);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('sets receipt type metadata key', () => {
|
|
116
|
+
const metadata: Record<string, string | string[] | undefined> = {};
|
|
117
|
+
adapter.attach(metadata, [VALID_CARRIER]);
|
|
118
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT_TYPE]).toBe('interaction-record+jwt');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('validateConstraints', () => {
|
|
123
|
+
it('returns valid for well-formed carrier within 8 KiB', () => {
|
|
124
|
+
const meta = createGrpcCarrierMeta();
|
|
125
|
+
const result = adapter.validateConstraints(VALID_CARRIER, meta);
|
|
126
|
+
expect(result.valid).toBe(true);
|
|
127
|
+
expect(result.violations).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns invalid for oversized carrier', () => {
|
|
131
|
+
const oversized: PeacEvidenceCarrier = {
|
|
132
|
+
receipt_ref: VALID_REF as PeacEvidenceCarrier['receipt_ref'],
|
|
133
|
+
receipt_jws: 'x'.repeat(10_000),
|
|
134
|
+
};
|
|
135
|
+
const meta = createGrpcCarrierMeta();
|
|
136
|
+
const result = adapter.validateConstraints(oversized, meta);
|
|
137
|
+
expect(result.valid).toBe(false);
|
|
138
|
+
expect(result.violations.length).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('passes with explicit larger size override for valid carrier', () => {
|
|
142
|
+
const meta = createGrpcCarrierMeta({ max_size: 65_536 });
|
|
143
|
+
const result = adapter.validateConstraints(VALID_CARRIER, meta);
|
|
144
|
+
expect(result.valid).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('createGrpcCarrierMeta()', () => {
|
|
150
|
+
it('creates default grpc meta with 8 KiB limit', () => {
|
|
151
|
+
const meta = createGrpcCarrierMeta();
|
|
152
|
+
expect(meta.transport).toBe('grpc');
|
|
153
|
+
expect(meta.format).toBe('embed');
|
|
154
|
+
expect(meta.max_size).toBe(8_192);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('accepts overrides', () => {
|
|
158
|
+
const meta = createGrpcCarrierMeta({ format: 'reference', max_size: 65_536 });
|
|
159
|
+
expect(meta.transport).toBe('grpc');
|
|
160
|
+
expect(meta.format).toBe('reference');
|
|
161
|
+
expect(meta.max_size).toBe(65_536);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('GRPC_MAX_CARRIER_SIZE', () => {
|
|
166
|
+
it('is 8 KiB (HTTP/2 header budget)', () => {
|
|
167
|
+
expect(GRPC_MAX_CARRIER_SIZE).toBe(8_192);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('validateOwnMetadataKeys()', () => {
|
|
172
|
+
it('validates all PEAC metadata key constants are ASCII-safe', () => {
|
|
173
|
+
const invalid = validateOwnMetadataKeys();
|
|
174
|
+
expect(invalid).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('addReceiptToMetadata() Wire 0.2 default', () => {
|
|
179
|
+
it('defaults receipt type to interaction-record+jwt (Wire 0.2)', () => {
|
|
180
|
+
const metadata: Record<string, string | string[]> = {};
|
|
181
|
+
addReceiptToMetadata(metadata, 'eyJ...');
|
|
182
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT]).toBe('eyJ...');
|
|
183
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT_TYPE]).toBe('interaction-record+jwt');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('accepts explicit Wire 0.1 receipt type', () => {
|
|
187
|
+
const metadata: Record<string, string | string[]> = {};
|
|
188
|
+
addReceiptToMetadata(metadata, 'eyJ...', 'peac-receipt/0.1');
|
|
189
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT_TYPE]).toBe('peac-receipt/0.1');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('extractReceiptTypeFromMetadata()', () => {
|
|
194
|
+
it('extracts string receipt type', () => {
|
|
195
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT_TYPE]: 'interaction-record+jwt' };
|
|
196
|
+
expect(extractReceiptTypeFromMetadata(metadata)).toBe('interaction-record+jwt');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns null when absent', () => {
|
|
200
|
+
expect(extractReceiptTypeFromMetadata({})).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('GRPC_TRANSPORT_VERSION', () => {
|
|
205
|
+
it('reports 0.12.6', () => {
|
|
206
|
+
expect(GRPC_TRANSPORT_VERSION).toBe('0.12.6');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gRPC Transport Status Code Parity Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that gRPC status codes map correctly to/from HTTP status codes
|
|
5
|
+
* and PEAC error codes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
GrpcStatus,
|
|
11
|
+
httpStatusToGrpc,
|
|
12
|
+
grpcStatusToHttp,
|
|
13
|
+
peacErrorToGrpc,
|
|
14
|
+
createGrpcError,
|
|
15
|
+
extractReceiptFromMetadata,
|
|
16
|
+
addReceiptToMetadata,
|
|
17
|
+
createSuccessResult,
|
|
18
|
+
createFailureResult,
|
|
19
|
+
getStatusName,
|
|
20
|
+
GrpcMetadataKeys,
|
|
21
|
+
GRPC_TRANSPORT_VERSION,
|
|
22
|
+
} from '../src/index.js';
|
|
23
|
+
|
|
24
|
+
describe('gRPC Transport', () => {
|
|
25
|
+
describe('version', () => {
|
|
26
|
+
it('should export correct version', () => {
|
|
27
|
+
expect(GRPC_TRANSPORT_VERSION).toBe('0.12.6');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('GrpcStatus constants', () => {
|
|
32
|
+
it('should define all standard gRPC status codes', () => {
|
|
33
|
+
expect(GrpcStatus.OK).toBe(0);
|
|
34
|
+
expect(GrpcStatus.CANCELLED).toBe(1);
|
|
35
|
+
expect(GrpcStatus.UNKNOWN).toBe(2);
|
|
36
|
+
expect(GrpcStatus.INVALID_ARGUMENT).toBe(3);
|
|
37
|
+
expect(GrpcStatus.DEADLINE_EXCEEDED).toBe(4);
|
|
38
|
+
expect(GrpcStatus.NOT_FOUND).toBe(5);
|
|
39
|
+
expect(GrpcStatus.ALREADY_EXISTS).toBe(6);
|
|
40
|
+
expect(GrpcStatus.PERMISSION_DENIED).toBe(7);
|
|
41
|
+
expect(GrpcStatus.RESOURCE_EXHAUSTED).toBe(8);
|
|
42
|
+
expect(GrpcStatus.FAILED_PRECONDITION).toBe(9);
|
|
43
|
+
expect(GrpcStatus.ABORTED).toBe(10);
|
|
44
|
+
expect(GrpcStatus.OUT_OF_RANGE).toBe(11);
|
|
45
|
+
expect(GrpcStatus.UNIMPLEMENTED).toBe(12);
|
|
46
|
+
expect(GrpcStatus.INTERNAL).toBe(13);
|
|
47
|
+
expect(GrpcStatus.UNAVAILABLE).toBe(14);
|
|
48
|
+
expect(GrpcStatus.DATA_LOSS).toBe(15);
|
|
49
|
+
expect(GrpcStatus.UNAUTHENTICATED).toBe(16);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('httpStatusToGrpc', () => {
|
|
54
|
+
it('should map success statuses to OK', () => {
|
|
55
|
+
expect(httpStatusToGrpc(200)).toBe(GrpcStatus.OK);
|
|
56
|
+
expect(httpStatusToGrpc(201)).toBe(GrpcStatus.OK);
|
|
57
|
+
expect(httpStatusToGrpc(204)).toBe(GrpcStatus.OK);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should map 400 to INVALID_ARGUMENT', () => {
|
|
61
|
+
expect(httpStatusToGrpc(400)).toBe(GrpcStatus.INVALID_ARGUMENT);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should map 401 to UNAUTHENTICATED', () => {
|
|
65
|
+
expect(httpStatusToGrpc(401)).toBe(GrpcStatus.UNAUTHENTICATED);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should map 402 to FAILED_PRECONDITION', () => {
|
|
69
|
+
expect(httpStatusToGrpc(402)).toBe(GrpcStatus.FAILED_PRECONDITION);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should map 403 to PERMISSION_DENIED', () => {
|
|
73
|
+
expect(httpStatusToGrpc(403)).toBe(GrpcStatus.PERMISSION_DENIED);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should map 404 to NOT_FOUND', () => {
|
|
77
|
+
expect(httpStatusToGrpc(404)).toBe(GrpcStatus.NOT_FOUND);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should map 409 to ABORTED', () => {
|
|
81
|
+
expect(httpStatusToGrpc(409)).toBe(GrpcStatus.ABORTED);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should map 429 to RESOURCE_EXHAUSTED', () => {
|
|
85
|
+
expect(httpStatusToGrpc(429)).toBe(GrpcStatus.RESOURCE_EXHAUSTED);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should map 499 to CANCELLED', () => {
|
|
89
|
+
expect(httpStatusToGrpc(499)).toBe(GrpcStatus.CANCELLED);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should map 500 to INTERNAL', () => {
|
|
93
|
+
expect(httpStatusToGrpc(500)).toBe(GrpcStatus.INTERNAL);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should map 501 to UNIMPLEMENTED', () => {
|
|
97
|
+
expect(httpStatusToGrpc(501)).toBe(GrpcStatus.UNIMPLEMENTED);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should map 503 to UNAVAILABLE', () => {
|
|
101
|
+
expect(httpStatusToGrpc(503)).toBe(GrpcStatus.UNAVAILABLE);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should map 504 to DEADLINE_EXCEEDED', () => {
|
|
105
|
+
expect(httpStatusToGrpc(504)).toBe(GrpcStatus.DEADLINE_EXCEEDED);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should map unknown 4xx to INVALID_ARGUMENT', () => {
|
|
109
|
+
expect(httpStatusToGrpc(418)).toBe(GrpcStatus.INVALID_ARGUMENT);
|
|
110
|
+
expect(httpStatusToGrpc(422)).toBe(GrpcStatus.INVALID_ARGUMENT);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should map unknown 5xx to INTERNAL', () => {
|
|
114
|
+
expect(httpStatusToGrpc(502)).toBe(GrpcStatus.INTERNAL);
|
|
115
|
+
expect(httpStatusToGrpc(599)).toBe(GrpcStatus.INTERNAL);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('grpcStatusToHttp', () => {
|
|
120
|
+
it('should map OK to 200', () => {
|
|
121
|
+
expect(grpcStatusToHttp(GrpcStatus.OK)).toBe(200);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should map CANCELLED to 499', () => {
|
|
125
|
+
expect(grpcStatusToHttp(GrpcStatus.CANCELLED)).toBe(499);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should map INVALID_ARGUMENT to 400', () => {
|
|
129
|
+
expect(grpcStatusToHttp(GrpcStatus.INVALID_ARGUMENT)).toBe(400);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should map UNAUTHENTICATED to 401', () => {
|
|
133
|
+
expect(grpcStatusToHttp(GrpcStatus.UNAUTHENTICATED)).toBe(401);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should map FAILED_PRECONDITION to 402', () => {
|
|
137
|
+
expect(grpcStatusToHttp(GrpcStatus.FAILED_PRECONDITION)).toBe(402);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should map PERMISSION_DENIED to 403', () => {
|
|
141
|
+
expect(grpcStatusToHttp(GrpcStatus.PERMISSION_DENIED)).toBe(403);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should map NOT_FOUND to 404', () => {
|
|
145
|
+
expect(grpcStatusToHttp(GrpcStatus.NOT_FOUND)).toBe(404);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should map ABORTED to 409', () => {
|
|
149
|
+
expect(grpcStatusToHttp(GrpcStatus.ABORTED)).toBe(409);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should map RESOURCE_EXHAUSTED to 429', () => {
|
|
153
|
+
expect(grpcStatusToHttp(GrpcStatus.RESOURCE_EXHAUSTED)).toBe(429);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should map INTERNAL to 500', () => {
|
|
157
|
+
expect(grpcStatusToHttp(GrpcStatus.INTERNAL)).toBe(500);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should map UNIMPLEMENTED to 501', () => {
|
|
161
|
+
expect(grpcStatusToHttp(GrpcStatus.UNIMPLEMENTED)).toBe(501);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should map UNAVAILABLE to 503', () => {
|
|
165
|
+
expect(grpcStatusToHttp(GrpcStatus.UNAVAILABLE)).toBe(503);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should map DEADLINE_EXCEEDED to 504', () => {
|
|
169
|
+
expect(grpcStatusToHttp(GrpcStatus.DEADLINE_EXCEEDED)).toBe(504);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('peacErrorToGrpc - HTTP status parity', () => {
|
|
174
|
+
describe('402 Payment Required -> FAILED_PRECONDITION', () => {
|
|
175
|
+
it('should map E_RECEIPT_MISSING', () => {
|
|
176
|
+
expect(peacErrorToGrpc('E_RECEIPT_MISSING')).toBe(GrpcStatus.FAILED_PRECONDITION);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should map E_RECEIPT_INVALID', () => {
|
|
180
|
+
expect(peacErrorToGrpc('E_RECEIPT_INVALID')).toBe(GrpcStatus.FAILED_PRECONDITION);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should map E_RECEIPT_EXPIRED', () => {
|
|
184
|
+
expect(peacErrorToGrpc('E_RECEIPT_EXPIRED')).toBe(GrpcStatus.FAILED_PRECONDITION);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('401 Unauthorized -> UNAUTHENTICATED', () => {
|
|
189
|
+
it('should map E_TAP_SIGNATURE_MISSING', () => {
|
|
190
|
+
expect(peacErrorToGrpc('E_TAP_SIGNATURE_MISSING')).toBe(GrpcStatus.UNAUTHENTICATED);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should map E_TAP_SIGNATURE_INVALID', () => {
|
|
194
|
+
expect(peacErrorToGrpc('E_TAP_SIGNATURE_INVALID')).toBe(GrpcStatus.UNAUTHENTICATED);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should map E_TAP_TIME_INVALID', () => {
|
|
198
|
+
expect(peacErrorToGrpc('E_TAP_TIME_INVALID')).toBe(GrpcStatus.UNAUTHENTICATED);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should map E_TAP_KEY_NOT_FOUND', () => {
|
|
202
|
+
expect(peacErrorToGrpc('E_TAP_KEY_NOT_FOUND')).toBe(GrpcStatus.UNAUTHENTICATED);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should map E_TAP_REPLAY_PROTECTION_REQUIRED', () => {
|
|
206
|
+
expect(peacErrorToGrpc('E_TAP_REPLAY_PROTECTION_REQUIRED')).toBe(
|
|
207
|
+
GrpcStatus.UNAUTHENTICATED
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('400 Bad Request -> INVALID_ARGUMENT', () => {
|
|
213
|
+
it('should map E_TAP_WINDOW_TOO_LARGE', () => {
|
|
214
|
+
expect(peacErrorToGrpc('E_TAP_WINDOW_TOO_LARGE')).toBe(GrpcStatus.INVALID_ARGUMENT);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should map E_TAP_TAG_UNKNOWN', () => {
|
|
218
|
+
expect(peacErrorToGrpc('E_TAP_TAG_UNKNOWN')).toBe(GrpcStatus.INVALID_ARGUMENT);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should map E_TAP_ALGORITHM_INVALID', () => {
|
|
222
|
+
expect(peacErrorToGrpc('E_TAP_ALGORITHM_INVALID')).toBe(GrpcStatus.INVALID_ARGUMENT);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('403 Forbidden -> PERMISSION_DENIED', () => {
|
|
227
|
+
it('should map E_ISSUER_NOT_ALLOWED', () => {
|
|
228
|
+
expect(peacErrorToGrpc('E_ISSUER_NOT_ALLOWED')).toBe(GrpcStatus.PERMISSION_DENIED);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('409 Conflict -> ABORTED', () => {
|
|
233
|
+
it('should map E_TAP_NONCE_REPLAY', () => {
|
|
234
|
+
expect(peacErrorToGrpc('E_TAP_NONCE_REPLAY')).toBe(GrpcStatus.ABORTED);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('500 Internal -> INTERNAL', () => {
|
|
239
|
+
it('should map E_CONFIG_ISSUER_ALLOWLIST_REQUIRED', () => {
|
|
240
|
+
expect(peacErrorToGrpc('E_CONFIG_ISSUER_ALLOWLIST_REQUIRED')).toBe(GrpcStatus.INTERNAL);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should map E_INTERNAL_ERROR', () => {
|
|
244
|
+
expect(peacErrorToGrpc('E_INTERNAL_ERROR')).toBe(GrpcStatus.INTERNAL);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('unknown errors', () => {
|
|
249
|
+
it('should default to INTERNAL for unknown errors', () => {
|
|
250
|
+
expect(peacErrorToGrpc('E_UNKNOWN_ERROR')).toBe(GrpcStatus.INTERNAL);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('createGrpcError', () => {
|
|
256
|
+
it('should create error with correct code', () => {
|
|
257
|
+
const error = createGrpcError('E_RECEIPT_MISSING', 'Receipt required');
|
|
258
|
+
expect(error.code).toBe(GrpcStatus.FAILED_PRECONDITION);
|
|
259
|
+
expect(error.message).toBe('Receipt required');
|
|
260
|
+
expect(error.peacCode).toBe('E_RECEIPT_MISSING');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should include details if provided', () => {
|
|
264
|
+
const error = createGrpcError('E_TAP_NONCE_REPLAY', 'Replay detected', { nonce: 'abc123' });
|
|
265
|
+
expect(error.details).toEqual({ nonce: 'abc123' });
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('metadata utilities', () => {
|
|
270
|
+
describe('extractReceiptFromMetadata', () => {
|
|
271
|
+
it('should extract string receipt', () => {
|
|
272
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: 'eyJ...' };
|
|
273
|
+
expect(extractReceiptFromMetadata(metadata)).toBe('eyJ...');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should extract first element from array', () => {
|
|
277
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: ['eyJ...', 'eyK...'] };
|
|
278
|
+
expect(extractReceiptFromMetadata(metadata)).toBe('eyJ...');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should return null if not present', () => {
|
|
282
|
+
const metadata = {};
|
|
283
|
+
expect(extractReceiptFromMetadata(metadata)).toBeNull();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should return null for undefined', () => {
|
|
287
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: undefined };
|
|
288
|
+
expect(extractReceiptFromMetadata(metadata)).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should return null for empty array', () => {
|
|
292
|
+
const metadata = { [GrpcMetadataKeys.RECEIPT]: [] };
|
|
293
|
+
expect(extractReceiptFromMetadata(metadata)).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('addReceiptToMetadata', () => {
|
|
298
|
+
it('should add receipt and type', () => {
|
|
299
|
+
const metadata: Record<string, string | string[]> = {};
|
|
300
|
+
addReceiptToMetadata(metadata, 'eyJ...');
|
|
301
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT]).toBe('eyJ...');
|
|
302
|
+
expect(metadata[GrpcMetadataKeys.RECEIPT_TYPE]).toBe('interaction-record+jwt');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('verification results', () => {
|
|
308
|
+
describe('createSuccessResult', () => {
|
|
309
|
+
it('should create success without receipt ID', () => {
|
|
310
|
+
const result = createSuccessResult();
|
|
311
|
+
expect(result.ok).toBe(true);
|
|
312
|
+
expect(result.status).toBe(GrpcStatus.OK);
|
|
313
|
+
expect(result.error).toBeUndefined();
|
|
314
|
+
expect(result.receiptId).toBeUndefined();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should create success with receipt ID', () => {
|
|
318
|
+
const result = createSuccessResult('rid_12345');
|
|
319
|
+
expect(result.ok).toBe(true);
|
|
320
|
+
expect(result.status).toBe(GrpcStatus.OK);
|
|
321
|
+
expect(result.receiptId).toBe('rid_12345');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('createFailureResult', () => {
|
|
326
|
+
it('should create failure result', () => {
|
|
327
|
+
const error = createGrpcError('E_RECEIPT_MISSING', 'No receipt');
|
|
328
|
+
const result = createFailureResult(error);
|
|
329
|
+
expect(result.ok).toBe(false);
|
|
330
|
+
expect(result.status).toBe(GrpcStatus.FAILED_PRECONDITION);
|
|
331
|
+
expect(result.error).toBe(error);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('getStatusName', () => {
|
|
337
|
+
it('should return correct names', () => {
|
|
338
|
+
expect(getStatusName(GrpcStatus.OK)).toBe('OK');
|
|
339
|
+
expect(getStatusName(GrpcStatus.UNAUTHENTICATED)).toBe('UNAUTHENTICATED');
|
|
340
|
+
expect(getStatusName(GrpcStatus.FAILED_PRECONDITION)).toBe('FAILED_PRECONDITION');
|
|
341
|
+
expect(getStatusName(GrpcStatus.PERMISSION_DENIED)).toBe('PERMISSION_DENIED');
|
|
342
|
+
expect(getStatusName(GrpcStatus.ABORTED)).toBe('ABORTED');
|
|
343
|
+
expect(getStatusName(GrpcStatus.INTERNAL)).toBe('INTERNAL');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should return UNKNOWN for invalid codes', () => {
|
|
347
|
+
expect(getStatusName(99 as never)).toBe('UNKNOWN');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('metadata keys', () => {
|
|
352
|
+
it('should define standard keys', () => {
|
|
353
|
+
expect(GrpcMetadataKeys.RECEIPT).toBe('peac-receipt');
|
|
354
|
+
expect(GrpcMetadataKeys.RECEIPT_TYPE).toBe('peac-receipt-type');
|
|
355
|
+
expect(GrpcMetadataKeys.TAP_SIGNATURE).toBe('peac-tap-signature');
|
|
356
|
+
expect(GrpcMetadataKeys.TAP_SIGNATURE_INPUT).toBe('peac-tap-signature-input');
|
|
357
|
+
expect(GrpcMetadataKeys.ERROR_CODE).toBe('peac-error-code');
|
|
358
|
+
expect(GrpcMetadataKeys.REQUEST_ID).toBe('peac-request-id');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['cjs', 'esm'],
|
|
6
|
+
dts: false,
|
|
7
|
+
outDir: 'dist',
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
clean: false,
|
|
10
|
+
splitting: false,
|
|
11
|
+
treeshake: true,
|
|
12
|
+
minify: false,
|
|
13
|
+
target: 'es2022',
|
|
14
|
+
platform: 'node',
|
|
15
|
+
external: [/^[^./]/],
|
|
16
|
+
outExtension({ format }) {
|
|
17
|
+
return { js: format === 'cjs' ? '.cjs' : '.mjs' };
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["vitest.config.ts"],"names":[],"mappings":";AAEA,wBAMG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["vitest.config.ts"],"names":[],"mappings":";;AAAA,0CAA6C;AAE7C,kBAAe,IAAA,qBAAY,EAAC;IAC1B,IAAI,EAAE;QACJ,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,CAAC,oBAAoB,CAAC;QAC/B,WAAW,EAAE,KAAK;KACnB;CACF,CAAC,CAAC"}
|