@layerzerolabs/common-canton 0.2.114 → 0.2.115
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 +632 -631
- package/.turbo/turbo-test.log +51 -15
- package/LICENSE +23 -0
- package/dist/{CJ2WHLYC.js → EZIA6KIP.js} +3 -10
- package/dist/EZIA6KIP.js.map +1 -0
- package/dist/PQI7CWCR.js +19 -0
- package/dist/PQI7CWCR.js.map +1 -0
- package/dist/daml-serde.d.ts +1 -8
- package/dist/daml-serde.d.ts.map +1 -1
- package/dist/daml-serde.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +21 -15
- package/src/daml-serde.ts +1 -12
- package/src/index.ts +2 -0
- package/src/types.ts +13 -0
- package/test/daml-serde.test.ts +1 -1
- package/dist/CJ2WHLYC.js.map +0 -1
- package/dist/EYZBXTU6.js +0 -13
- package/dist/EYZBXTU6.js.map +0 -1
- package/test/mocks/scripts/capture-disclosure.ts +0 -252
- package/test/mocks/scripts/capture-external-requests.ts +0 -184
- package/test/mocks/scripts/capture-internal-requests.ts +0 -122
- package/test/mocks/scripts/capture-unhappy-paths.ts +0 -263
- package/test/mocks/scripts/capture-utils.ts +0 -215
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/// <reference types="node" />
|
|
3
|
-
/**
|
|
4
|
-
* Captures error scenarios (unhappy paths) for Canton API interactions.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* tsx test/mocks/scripts/capture-unhappy-paths.ts
|
|
8
|
-
*
|
|
9
|
-
* Output:
|
|
10
|
-
* test/mocks/captured-unhappy-paths.json
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ContractId } from '@daml/types';
|
|
14
|
-
import * as path from 'path';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
MnemonicSignerAdapter,
|
|
18
|
-
SignatureFormat,
|
|
19
|
-
SignatureType,
|
|
20
|
-
} from '@layerzerolabs/ver-signer-adapter';
|
|
21
|
-
import { CantonClient } from '../../../src/client/index.js';
|
|
22
|
-
// Import SDK test contracts
|
|
23
|
-
import { packageId } from '../../../src/daml/sdk-test-0.0.1/lib/index.js';
|
|
24
|
-
import { SimpleAsset } from '../../../src/daml/sdk-test-0.0.1/lib/SdkTest/module.js';
|
|
25
|
-
import type { Command } from '../../../src/open-api/models/index.js';
|
|
26
|
-
import { Configuration } from '../../../src/open-api/runtime.js';
|
|
27
|
-
import { CantonSdk } from '../../../src/sdk/index.js';
|
|
28
|
-
import type { ExternalSignature } from '../../../src/types.js';
|
|
29
|
-
import {
|
|
30
|
-
CANTON_URL,
|
|
31
|
-
type CapturedInteraction,
|
|
32
|
-
createCapturingFetch,
|
|
33
|
-
createOperationContext,
|
|
34
|
-
EXTERNAL_PARTY_DERIVATION_PATH,
|
|
35
|
-
EXTERNAL_PARTY_ID,
|
|
36
|
-
EXTERNAL_PARTY_JWT_TOKEN,
|
|
37
|
-
EXTERNAL_PARTY_KEY_FINGERPRINT,
|
|
38
|
-
EXTERNAL_PARTY_MNEMONIC,
|
|
39
|
-
EXTERNAL_PARTY_USER_ID,
|
|
40
|
-
INTERNAL_PARTY_ID,
|
|
41
|
-
INTERNAL_PARTY_JWT_TOKEN,
|
|
42
|
-
INTERNAL_PARTY_USER_ID,
|
|
43
|
-
printScenarioSummary,
|
|
44
|
-
saveInteractions,
|
|
45
|
-
} from './capture-utils.js';
|
|
46
|
-
|
|
47
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
-
// Helpers
|
|
49
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
async function signWrongData(party: string): Promise<ExternalSignature> {
|
|
52
|
-
const signer = new MnemonicSignerAdapter({
|
|
53
|
-
mnemonicConfig: {
|
|
54
|
-
getMnemonic: async () => ({
|
|
55
|
-
mnemonic: EXTERNAL_PARTY_MNEMONIC,
|
|
56
|
-
path: EXTERNAL_PARTY_DERIVATION_PATH,
|
|
57
|
-
}),
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
const { createHash } = await import('crypto');
|
|
61
|
-
const wrongHash = createHash('sha256').update('this-is-wrong-data').digest();
|
|
62
|
-
|
|
63
|
-
const derSignature = await signer.sign({
|
|
64
|
-
data: wrongHash,
|
|
65
|
-
signatureType: SignatureType.ECDSA,
|
|
66
|
-
format: SignatureFormat.DER,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
party,
|
|
71
|
-
signature: Buffer.from(derSignature).toString('base64'),
|
|
72
|
-
format: 'SIGNATURE_FORMAT_DER',
|
|
73
|
-
signedBy: EXTERNAL_PARTY_KEY_FINGERPRINT,
|
|
74
|
-
signingAlgorithmSpec: 'SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256',
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
-
// Scenarios
|
|
80
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
async function captureNonExistentContract(sdk: CantonSdk, setOp: (n: string) => void) {
|
|
83
|
-
setOp('error_query_nonexistent_contract');
|
|
84
|
-
const fakeId =
|
|
85
|
-
'00' +
|
|
86
|
-
'00'.repeat(65) +
|
|
87
|
-
'ca1212200000000000000000000000000000000000000000000000000000000000000000';
|
|
88
|
-
try {
|
|
89
|
-
await sdk.get(fakeId as ContractId<typeof SimpleAsset>, [INTERNAL_PARTY_ID]);
|
|
90
|
-
} catch {
|
|
91
|
-
/* expected */
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function captureInvalidTemplateId(sdk: CantonSdk, setOp: (n: string) => void) {
|
|
96
|
-
setOp('error_invalid_template_id');
|
|
97
|
-
const cmd: Command = {
|
|
98
|
-
createCommand: {
|
|
99
|
-
templateId: 'invalid-package-id:SdkTest:SimpleAsset',
|
|
100
|
-
createArguments: {
|
|
101
|
-
owner: INTERNAL_PARTY_ID,
|
|
102
|
-
value: '100',
|
|
103
|
-
description: 'test',
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
try {
|
|
108
|
-
await sdk.submit({ actAs: [INTERNAL_PARTY_ID] }, [cmd]);
|
|
109
|
-
} catch {
|
|
110
|
-
/* expected */
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function captureDoubleArchive(sdk: CantonSdk, setOp: (n: string) => void) {
|
|
115
|
-
// Setup: create contract
|
|
116
|
-
setOp('double_archive_setup_create');
|
|
117
|
-
const payload = {
|
|
118
|
-
owner: INTERNAL_PARTY_ID,
|
|
119
|
-
value: '100',
|
|
120
|
-
description: 'double archive test',
|
|
121
|
-
};
|
|
122
|
-
const createCmd = sdk.buildCreate(SimpleAsset as any, payload, packageId);
|
|
123
|
-
const createResult = await sdk.submit({ actAs: [INTERNAL_PARTY_ID] }, [createCmd]);
|
|
124
|
-
|
|
125
|
-
setOp('double_archive_setup_wait');
|
|
126
|
-
const createWait = await sdk.waitForCommand(createResult.commandId, {
|
|
127
|
-
parties: [INTERNAL_PARTY_ID],
|
|
128
|
-
beginExclusive: createResult.completionOffset,
|
|
129
|
-
timeoutMs: 60_000,
|
|
130
|
-
});
|
|
131
|
-
if (createWait.status !== 'accepted') return;
|
|
132
|
-
|
|
133
|
-
setOp('double_archive_setup_fetch_id');
|
|
134
|
-
const update = await sdk.fetchTransaction(createWait.updateId, [INTERNAL_PARTY_ID]);
|
|
135
|
-
const createdWrapper = (update.events ?? []).find((e) => 'CreatedEvent' in e);
|
|
136
|
-
const contractId = (
|
|
137
|
-
createdWrapper && 'CreatedEvent' in createdWrapper
|
|
138
|
-
? createdWrapper.CreatedEvent.contractId
|
|
139
|
-
: undefined
|
|
140
|
-
) as ContractId<typeof SimpleAsset>;
|
|
141
|
-
|
|
142
|
-
// First archive (success)
|
|
143
|
-
setOp('double_archive_first_archive');
|
|
144
|
-
const archiveCmd1 = sdk.buildExercise(SimpleAsset.Archive, contractId, {}, packageId);
|
|
145
|
-
const archiveResult1 = await sdk.submit({ actAs: [INTERNAL_PARTY_ID] }, [archiveCmd1]);
|
|
146
|
-
|
|
147
|
-
setOp('double_archive_first_wait');
|
|
148
|
-
await sdk.waitForCommand(archiveResult1.commandId, {
|
|
149
|
-
parties: [INTERNAL_PARTY_ID],
|
|
150
|
-
beginExclusive: archiveResult1.completionOffset,
|
|
151
|
-
timeoutMs: 60_000,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Second archive (should fail)
|
|
155
|
-
setOp('error_double_archive');
|
|
156
|
-
const archiveCmd2 = sdk.buildExercise(SimpleAsset.Archive, contractId, {}, packageId);
|
|
157
|
-
try {
|
|
158
|
-
await sdk.submit({ actAs: [INTERNAL_PARTY_ID] }, [archiveCmd2]);
|
|
159
|
-
} catch {
|
|
160
|
-
/* expected */
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function captureInvalidSignature(sdk: CantonSdk, setOp: (n: string) => void) {
|
|
165
|
-
setOp('invalid_signature_prepare');
|
|
166
|
-
const payload = {
|
|
167
|
-
owner: EXTERNAL_PARTY_ID,
|
|
168
|
-
value: '200',
|
|
169
|
-
description: 'invalid signature test',
|
|
170
|
-
};
|
|
171
|
-
const createCmd = sdk.buildCreate(SimpleAsset, payload, packageId);
|
|
172
|
-
const prepared = await sdk.prepareForSigning({ actAs: [EXTERNAL_PARTY_ID] }, [createCmd]);
|
|
173
|
-
|
|
174
|
-
setOp('error_invalid_signature');
|
|
175
|
-
const invalidSig = await signWrongData(EXTERNAL_PARTY_ID);
|
|
176
|
-
try {
|
|
177
|
-
await sdk.submitSigned(prepared, [invalidSig]);
|
|
178
|
-
} catch {
|
|
179
|
-
/* expected */
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function captureCommandTimeout(sdk: CantonSdk, setOp: (n: string) => void) {
|
|
184
|
-
setOp('error_command_timeout');
|
|
185
|
-
try {
|
|
186
|
-
await sdk.waitForCommand('fake-command-id', {
|
|
187
|
-
parties: [INTERNAL_PARTY_ID],
|
|
188
|
-
beginExclusive: 0,
|
|
189
|
-
timeoutMs: 2_000,
|
|
190
|
-
});
|
|
191
|
-
} catch {
|
|
192
|
-
/* expected */
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function captureUnauthorizedParty(sdk: CantonSdk, setOp: (n: string) => void) {
|
|
197
|
-
setOp('error_unauthorized_party');
|
|
198
|
-
const fakeParty = 'fake-party::1220' + '00'.repeat(32);
|
|
199
|
-
const payload = {
|
|
200
|
-
owner: fakeParty,
|
|
201
|
-
value: '100',
|
|
202
|
-
description: 'unauthorized test',
|
|
203
|
-
};
|
|
204
|
-
const cmd = sdk.buildCreate(SimpleAsset as any, payload, packageId);
|
|
205
|
-
try {
|
|
206
|
-
await sdk.submit({ actAs: [fakeParty] }, [cmd]);
|
|
207
|
-
} catch {
|
|
208
|
-
/* expected */
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
-
// Main
|
|
214
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
async function main() {
|
|
217
|
-
console.log('Capturing unhappy path requests...');
|
|
218
|
-
|
|
219
|
-
const captured: CapturedInteraction[] = [];
|
|
220
|
-
|
|
221
|
-
// Internal party scenarios
|
|
222
|
-
const internalCtx = createOperationContext();
|
|
223
|
-
const internalConfig = new Configuration({
|
|
224
|
-
basePath: CANTON_URL,
|
|
225
|
-
fetchApi: createCapturingFetch({
|
|
226
|
-
captured,
|
|
227
|
-
getOperation: internalCtx.getOperation,
|
|
228
|
-
scenarioName: 'internal',
|
|
229
|
-
}),
|
|
230
|
-
headers: { Authorization: `Bearer ${INTERNAL_PARTY_JWT_TOKEN}` },
|
|
231
|
-
});
|
|
232
|
-
const internalSdk = new CantonSdk(new CantonClient(internalConfig, INTERNAL_PARTY_USER_ID));
|
|
233
|
-
|
|
234
|
-
await captureNonExistentContract(internalSdk, internalCtx.setOperation);
|
|
235
|
-
await captureInvalidTemplateId(internalSdk, internalCtx.setOperation);
|
|
236
|
-
await captureDoubleArchive(internalSdk, internalCtx.setOperation);
|
|
237
|
-
await captureCommandTimeout(internalSdk, internalCtx.setOperation);
|
|
238
|
-
await captureUnauthorizedParty(internalSdk, internalCtx.setOperation);
|
|
239
|
-
|
|
240
|
-
// External party scenarios
|
|
241
|
-
const externalCtx = createOperationContext();
|
|
242
|
-
const externalConfig = new Configuration({
|
|
243
|
-
basePath: CANTON_URL,
|
|
244
|
-
fetchApi: createCapturingFetch({
|
|
245
|
-
captured,
|
|
246
|
-
getOperation: externalCtx.getOperation,
|
|
247
|
-
scenarioName: 'external',
|
|
248
|
-
}),
|
|
249
|
-
headers: { Authorization: `Bearer ${EXTERNAL_PARTY_JWT_TOKEN}` },
|
|
250
|
-
});
|
|
251
|
-
const externalSdk = new CantonSdk(new CantonClient(externalConfig, EXTERNAL_PARTY_USER_ID));
|
|
252
|
-
|
|
253
|
-
await captureInvalidSignature(externalSdk, externalCtx.setOperation);
|
|
254
|
-
|
|
255
|
-
const outputFile = path.join(import.meta.dirname, '..', 'captured-unhappy-paths.json');
|
|
256
|
-
saveInteractions(outputFile, captured);
|
|
257
|
-
printScenarioSummary(captured);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
main().catch((err) => {
|
|
261
|
-
console.error('Error:', err.message);
|
|
262
|
-
process.exit(1);
|
|
263
|
-
});
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for capture scripts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
|
|
7
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
-
// Configuration
|
|
9
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export const CANTON_URL = process.env.CANTON_URL ?? 'http://127.0.0.1:5013';
|
|
12
|
-
|
|
13
|
-
// Internal party (keys managed by participant)
|
|
14
|
-
export const INTERNAL_PARTY_USER_ID = process.env.INTERNAL_PARTY_USER_ID ?? '<INTERNAL_USER_ID>';
|
|
15
|
-
export const INTERNAL_PARTY_JWT_TOKEN =
|
|
16
|
-
process.env.INTERNAL_PARTY_JWT_TOKEN ?? '<INTERNAL_JWT_TOKEN>';
|
|
17
|
-
export const INTERNAL_PARTY_ID = process.env.INTERNAL_PARTY_ID ?? '<INTERNAL_PARTY_ID>';
|
|
18
|
-
|
|
19
|
-
// External party (keys managed off-chain, signed submissions)
|
|
20
|
-
export const EXTERNAL_PARTY_USER_ID = process.env.EXTERNAL_PARTY_USER_ID ?? '<EXTERNAL_USER_ID>';
|
|
21
|
-
export const EXTERNAL_PARTY_JWT_TOKEN =
|
|
22
|
-
process.env.EXTERNAL_PARTY_JWT_TOKEN ?? '<EXTERNAL_JWT_TOKEN>';
|
|
23
|
-
export const EXTERNAL_PARTY_ID = process.env.EXTERNAL_PARTY_ID ?? '<EXTERNAL_PARTY_ID>';
|
|
24
|
-
export const EXTERNAL_PARTY_KEY_FINGERPRINT =
|
|
25
|
-
process.env.EXTERNAL_PARTY_KEY_FINGERPRINT ?? '<EXTERNAL_KEY_FINGERPRINT>';
|
|
26
|
-
export const EXTERNAL_PARTY_MNEMONIC = process.env.EXTERNAL_PARTY_MNEMONIC ?? '<EXTERNAL_MNEMONIC>';
|
|
27
|
-
export const EXTERNAL_PARTY_DERIVATION_PATH =
|
|
28
|
-
process.env.EXTERNAL_PARTY_DERIVATION_PATH ?? "m/44'/6767'/0'/0/0";
|
|
29
|
-
|
|
30
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
// Types
|
|
32
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
export interface CapturedInteraction {
|
|
35
|
-
name: string;
|
|
36
|
-
scenario?: string;
|
|
37
|
-
timestamp: string;
|
|
38
|
-
request: {
|
|
39
|
-
method: string;
|
|
40
|
-
url: string;
|
|
41
|
-
path: string;
|
|
42
|
-
headers: Record<string, string>;
|
|
43
|
-
body: unknown;
|
|
44
|
-
};
|
|
45
|
-
response: {
|
|
46
|
-
status: number;
|
|
47
|
-
statusText: string;
|
|
48
|
-
headers: Record<string, string>;
|
|
49
|
-
body: unknown;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
-
// Endpoint Name Mapping
|
|
55
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
const ENDPOINT_NAMES: Record<string, string> = {
|
|
58
|
-
'/v2/state/connected-synchronizers': 'get_synchronizers',
|
|
59
|
-
'/v2/state/ledger-end': 'get_ledger_end',
|
|
60
|
-
'/v2/commands/async/submit': 'submit_command',
|
|
61
|
-
'/v2/commands/completions': 'wait_completion',
|
|
62
|
-
'/v2/updates/update-by-id': 'fetch_update',
|
|
63
|
-
'/v2/events/events-by-contract-id': 'get_contract',
|
|
64
|
-
'/v2/interactive-submission/prepare': 'prepare_signing',
|
|
65
|
-
'/v2/interactive-submission/execute': 'submit_signed',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export function getEndpointName(path: string, method: string): string {
|
|
69
|
-
return ENDPOINT_NAMES[path] ?? `${method.toLowerCase()}_${path.replace(/\//g, '_')}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
-
// Operation Context
|
|
74
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
export function createOperationContext() {
|
|
77
|
-
let currentOperation = 'unknown';
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
setOperation(name: string) {
|
|
81
|
-
currentOperation = name;
|
|
82
|
-
},
|
|
83
|
-
getOperation() {
|
|
84
|
-
return currentOperation;
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
-
// Capturing Fetch Wrapper
|
|
91
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
export interface CapturingFetchOptions {
|
|
94
|
-
captured: CapturedInteraction[];
|
|
95
|
-
getOperation: () => string;
|
|
96
|
-
scenarioName?: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function createCapturingFetch(options: CapturingFetchOptions): typeof fetch {
|
|
100
|
-
const { captured, getOperation, scenarioName } = options;
|
|
101
|
-
|
|
102
|
-
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
103
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
104
|
-
const method = init?.method ?? 'GET';
|
|
105
|
-
const path = new URL(url).pathname;
|
|
106
|
-
const requestName = `${getOperation()}__${getEndpointName(path, method)}`;
|
|
107
|
-
|
|
108
|
-
// Parse request body
|
|
109
|
-
let requestBody: unknown = null;
|
|
110
|
-
if (init?.body) {
|
|
111
|
-
try {
|
|
112
|
-
requestBody = JSON.parse(init.body as string);
|
|
113
|
-
} catch {
|
|
114
|
-
requestBody = init.body;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Extract request headers
|
|
119
|
-
const requestHeaders: Record<string, string> = {};
|
|
120
|
-
if (init?.headers) {
|
|
121
|
-
if (init.headers instanceof Headers) {
|
|
122
|
-
init.headers.forEach((value, key) => {
|
|
123
|
-
requestHeaders[key] = value;
|
|
124
|
-
});
|
|
125
|
-
} else if (Array.isArray(init.headers)) {
|
|
126
|
-
init.headers.forEach(([key, value]) => {
|
|
127
|
-
requestHeaders[key] = value;
|
|
128
|
-
});
|
|
129
|
-
} else {
|
|
130
|
-
for (const [key, value] of Object.entries(init.headers)) {
|
|
131
|
-
requestHeaders[key] = value;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Execute real fetch
|
|
137
|
-
const response = await fetch(input, init);
|
|
138
|
-
|
|
139
|
-
// Clone response to read body without consuming
|
|
140
|
-
const cloned = response.clone();
|
|
141
|
-
let responseBody: unknown;
|
|
142
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
143
|
-
|
|
144
|
-
if (contentType.includes('application/json')) {
|
|
145
|
-
try {
|
|
146
|
-
responseBody = await cloned.json();
|
|
147
|
-
} catch {
|
|
148
|
-
responseBody = await cloned.text();
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
responseBody = await cloned.text();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Extract response headers
|
|
155
|
-
const responseHeaders: Record<string, string> = {};
|
|
156
|
-
response.headers.forEach((value, key) => {
|
|
157
|
-
responseHeaders[key] = value;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Store captured interaction
|
|
161
|
-
const interaction: CapturedInteraction = {
|
|
162
|
-
name: requestName,
|
|
163
|
-
timestamp: new Date().toISOString(),
|
|
164
|
-
request: {
|
|
165
|
-
method,
|
|
166
|
-
url,
|
|
167
|
-
path,
|
|
168
|
-
headers: requestHeaders,
|
|
169
|
-
body: requestBody,
|
|
170
|
-
},
|
|
171
|
-
response: {
|
|
172
|
-
status: response.status,
|
|
173
|
-
statusText: response.statusText,
|
|
174
|
-
headers: responseHeaders,
|
|
175
|
-
body: responseBody,
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
if (scenarioName) {
|
|
180
|
-
interaction.scenario = scenarioName;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
captured.push(interaction);
|
|
184
|
-
|
|
185
|
-
return response;
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
-
// File Output
|
|
191
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
export function saveInteractions(outputFile: string, captured: CapturedInteraction[]): void {
|
|
194
|
-
fs.writeFileSync(outputFile, JSON.stringify(captured, null, 2));
|
|
195
|
-
console.log(`\n✓ Saved ${captured.length} interactions to:`);
|
|
196
|
-
console.log(` ${outputFile}`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function printEndpointSummary(captured: CapturedInteraction[]): void {
|
|
200
|
-
const endpoints = new Set(captured.map((c) => `${c.request.method} ${c.request.path}`));
|
|
201
|
-
console.log('\nCaptured endpoints:');
|
|
202
|
-
endpoints.forEach((e) => console.log(` - ${e}`));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function printScenarioSummary(captured: CapturedInteraction[]): void {
|
|
206
|
-
const scenarios = new Map<string, number>();
|
|
207
|
-
for (const interaction of captured) {
|
|
208
|
-
const key = `${interaction.scenario}: ${interaction.response.status}`;
|
|
209
|
-
scenarios.set(key, (scenarios.get(key) ?? 0) + 1);
|
|
210
|
-
}
|
|
211
|
-
console.log('\nCaptured scenarios:');
|
|
212
|
-
scenarios.forEach((count, scenario) => {
|
|
213
|
-
console.log(` - ${scenario} (${count} requests)`);
|
|
214
|
-
});
|
|
215
|
-
}
|