@vlayer/sdk 0.1.0-nightly-20250127-96fc5df → 0.1.0-nightly-20250128-0a32cfc

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.
Files changed (63) hide show
  1. package/dist/api/lib/client.d.ts.map +1 -1
  2. package/dist/api/lib/client.js.map +1 -1
  3. package/dist/api/prover.d.ts.map +1 -1
  4. package/dist/api/prover.js +1 -2
  5. package/dist/api/prover.js.map +1 -1
  6. package/dist/api/v_call.d.ts +1 -1
  7. package/dist/api/v_call.d.ts.map +1 -1
  8. package/dist/api/v_call.js +8 -2
  9. package/dist/api/v_call.js.map +1 -1
  10. package/dist/api/v_getProofReceipt.d.ts.map +1 -1
  11. package/dist/api/v_getProofReceipt.js.map +1 -1
  12. package/dist/api/webProof/createWebProofRequest.d.ts.map +1 -1
  13. package/dist/api/webProof/createWebProofRequest.js.map +1 -1
  14. package/package.json +20 -7
  15. package/src/api/email/dnsResolver.test.ts +19 -0
  16. package/src/api/email/dnsResolver.ts +87 -0
  17. package/src/api/email/parseEmail.test.ts +133 -0
  18. package/src/api/email/parseEmail.ts +55 -0
  19. package/src/api/email/preverify.test.ts +201 -0
  20. package/src/api/email/preverify.ts +70 -0
  21. package/src/api/email/testdata/test_email.txt +21 -0
  22. package/src/api/email/testdata/test_email_multiple_dkims.txt +28 -0
  23. package/src/api/email/testdata/test_email_subdomain.txt +21 -0
  24. package/src/api/email/testdata/test_email_unknown_domain.txt +21 -0
  25. package/src/api/lib/client.test.ts +261 -0
  26. package/src/api/lib/client.ts +191 -0
  27. package/src/api/lib/errors.ts +19 -0
  28. package/src/api/lib/types/ethereum.ts +45 -0
  29. package/src/api/lib/types/index.ts +3 -0
  30. package/src/api/lib/types/viem.ts +26 -0
  31. package/src/api/lib/types/vlayer.ts +156 -0
  32. package/src/api/lib/types/webProofProvider.ts +68 -0
  33. package/src/api/prover.ts +120 -0
  34. package/src/api/utils/prefixAllButNthSubstring.test.ts +24 -0
  35. package/src/api/utils/prefixAllButNthSubstring.ts +13 -0
  36. package/src/api/utils/versions.test.ts +52 -0
  37. package/src/api/utils/versions.ts +31 -0
  38. package/src/api/v_call.ts +58 -0
  39. package/src/api/v_getProofReceipt.ts +48 -0
  40. package/src/api/v_versions.ts +68 -0
  41. package/src/api/webProof/createWebProofRequest.ts +15 -0
  42. package/src/api/webProof/index.ts +3 -0
  43. package/src/api/webProof/providers/extension.test.ts +122 -0
  44. package/src/api/webProof/providers/extension.ts +197 -0
  45. package/src/api/webProof/providers/index.ts +1 -0
  46. package/src/api/webProof/steps/expectUrl.ts +12 -0
  47. package/src/api/webProof/steps/index.ts +11 -0
  48. package/src/api/webProof/steps/notarize.ts +20 -0
  49. package/src/api/webProof/steps/startPage.ts +12 -0
  50. package/src/config/createContext.ts +69 -0
  51. package/src/config/deploy.ts +108 -0
  52. package/src/config/getChainConfirmations.ts +6 -0
  53. package/src/config/getConfig.ts +71 -0
  54. package/src/config/index.ts +5 -0
  55. package/src/config/types.ts +26 -0
  56. package/src/config/writeEnvVariables.ts +28 -0
  57. package/src/index.ts +7 -0
  58. package/src/testHelpers/readFile.ts +3 -0
  59. package/src/web-proof-commons/index.ts +3 -0
  60. package/src/web-proof-commons/types/message.ts +176 -0
  61. package/src/web-proof-commons/types/redaction.test.ts +97 -0
  62. package/src/web-proof-commons/types/redaction.ts +201 -0
  63. package/src/web-proof-commons/utils.ts +11 -0
@@ -0,0 +1,69 @@
1
+ import {
2
+ type Chain,
3
+ createWalletClient,
4
+ http,
5
+ publicActions,
6
+ type CustomTransport,
7
+ custom,
8
+ type PrivateKeyAccount,
9
+ } from "viem";
10
+ import { privateKeyToAccount } from "viem/accounts";
11
+ import { getChainConfirmations } from "./getChainConfirmations";
12
+ import * as chains from "viem/chains";
13
+ import type { EnvConfig, VlayerContextConfig } from "./types";
14
+
15
+ const getChainSpecs = (chainName: string): Chain => {
16
+ try {
17
+ return chains[chainName as keyof typeof chains] as Chain;
18
+ } catch {
19
+ throw Error(`Cannot import ${chainName} from viem/chains`);
20
+ }
21
+ };
22
+
23
+ export const customTransport = custom;
24
+
25
+ const createEthClient = (
26
+ chain: Chain,
27
+ jsonRpcUrl: string,
28
+ transport?: CustomTransport,
29
+ ) =>
30
+ createWalletClient({
31
+ chain,
32
+ transport: transport || http(jsonRpcUrl),
33
+ }).extend(publicActions);
34
+
35
+ export function createContext(config: EnvConfig): {
36
+ chain: Chain;
37
+ account: ReturnType<typeof privateKeyToAccount>;
38
+ jsonRpcUrl: string;
39
+ ethClient: ReturnType<typeof createEthClient>;
40
+ confirmations: number;
41
+ } & EnvConfig;
42
+
43
+ export function createContext(
44
+ config: VlayerContextConfig,
45
+ transport?: CustomTransport,
46
+ ): {
47
+ chain: Chain;
48
+ jsonRpcUrl: string;
49
+ account: PrivateKeyAccount;
50
+ ethClient: ReturnType<typeof createEthClient>;
51
+ confirmations: number;
52
+ } & VlayerContextConfig;
53
+
54
+ export function createContext(
55
+ config: VlayerContextConfig | EnvConfig,
56
+ transport?: CustomTransport,
57
+ ) {
58
+ const chain = getChainSpecs(config.chainName);
59
+ const jsonRpcUrl = config.jsonRpcUrl ?? chain.rpcUrls.default.http[0];
60
+
61
+ return {
62
+ ...config,
63
+ chain,
64
+ account: config.privateKey && privateKeyToAccount(config.privateKey),
65
+ jsonRpcUrl,
66
+ ethClient: createEthClient(chain, jsonRpcUrl, transport),
67
+ confirmations: getChainConfirmations(config.chainName),
68
+ };
69
+ }
@@ -0,0 +1,108 @@
1
+ import { getConfig } from "./getConfig";
2
+ import { createContext } from "./createContext";
3
+ import { type ContractArg, type ContractSpec } from "types/ethereum";
4
+ import { type Address } from "viem";
5
+ import { getChainConfirmations } from "./getChainConfirmations";
6
+ import debug from "debug";
7
+
8
+ const log = debug("vlayer:prover");
9
+
10
+ export const waitForContractDeploy = async ({
11
+ hash,
12
+ }: {
13
+ hash: `0x${string}`;
14
+ }): Promise<Address> => {
15
+ const { ethClient: client } = createContext(getConfig());
16
+ const receipt = await client.waitForTransactionReceipt({
17
+ hash,
18
+ confirmations: getChainConfirmations(client.chain?.name),
19
+ retryCount: 120,
20
+ retryDelay: 1000,
21
+ });
22
+
23
+ if (!receipt.contractAddress || receipt.status !== "success") {
24
+ throw new Error(
25
+ `Cannot get contract address from receipt: ${receipt.status}`,
26
+ );
27
+ }
28
+
29
+ return receipt.contractAddress;
30
+ };
31
+
32
+ export const waitForTransactionReceipt = async ({
33
+ hash,
34
+ }: {
35
+ hash: `0x${string}`;
36
+ }) => {
37
+ const { ethClient } = createContext(getConfig());
38
+ return ethClient.waitForTransactionReceipt({
39
+ hash,
40
+ confirmations: getChainConfirmations(ethClient.chain?.name),
41
+ retryCount: 120,
42
+ retryDelay: 1000,
43
+ });
44
+ };
45
+
46
+ export const deployProver = async ({
47
+ proverSpec,
48
+ proverArgs,
49
+ }: {
50
+ proverSpec: ContractSpec;
51
+ proverArgs?: ContractArg[];
52
+ }) => {
53
+ const config = getConfig();
54
+ const { ethClient, account, chain } = createContext(config);
55
+
56
+ const proverHash = await ethClient.deployContract({
57
+ chain,
58
+ account,
59
+ args: proverArgs,
60
+ abi: proverSpec.abi,
61
+ bytecode: proverSpec.bytecode.object,
62
+ });
63
+ log(`Prover hash: ${proverHash}`);
64
+ const prover = await waitForContractDeploy({ hash: proverHash });
65
+ return prover;
66
+ };
67
+
68
+ export const deployVlayerContracts = async ({
69
+ proverSpec,
70
+ verifierSpec,
71
+ proverArgs,
72
+ verifierArgs,
73
+ }: {
74
+ proverSpec: ContractSpec;
75
+ verifierSpec: ContractSpec;
76
+ proverArgs?: ContractArg[];
77
+ verifierArgs?: ContractArg[];
78
+ }) => {
79
+ log("Starting contract deployment process...");
80
+ const config = getConfig();
81
+ const { chain, ethClient, account } = createContext(config);
82
+
83
+ log("Deploying prover contract...");
84
+ const proverHash = await ethClient.deployContract({
85
+ chain,
86
+ account,
87
+ args: proverArgs,
88
+ abi: proverSpec.abi,
89
+ bytecode: proverSpec.bytecode.object,
90
+ });
91
+ log(`Prover hash: ${proverHash}`);
92
+ const prover = await waitForContractDeploy({ hash: proverHash });
93
+ log(`Prover contract deployed at: ${prover}`);
94
+
95
+ log("Deploying verifier contract...");
96
+ const verifierHash = await ethClient.deployContract({
97
+ chain,
98
+ account,
99
+ args: prover ? [prover, ...(verifierArgs ?? [])] : verifierArgs,
100
+ abi: verifierSpec.abi,
101
+ bytecode: verifierSpec.bytecode.object,
102
+ });
103
+ const verifier = await waitForContractDeploy({ hash: verifierHash });
104
+ log(`Verifier contract deployed at: ${verifier}`);
105
+
106
+ log("Contract deployment completed successfully");
107
+ return { prover, verifier };
108
+ };
@@ -0,0 +1,6 @@
1
+ export const getChainConfirmations = (chainName?: string): number => {
2
+ if (!chainName || chainName.toLowerCase() === "anvil") {
3
+ return 1;
4
+ }
5
+ return 6;
6
+ };
@@ -0,0 +1,71 @@
1
+ import dotenvflow from "dotenv-flow";
2
+ import { type EnvConfig } from "./types";
3
+
4
+ const ensureEnvVariable = (envVar: string) => {
5
+ if (!process.env[envVar]) {
6
+ if (envVar === "EXAMPLES_TEST_PRIVATE_KEY") {
7
+ throw new Error(
8
+ `${envVar} missing. Add a HEX private key with ETH in .env.local for deploy and verify transactions.`,
9
+ );
10
+ }
11
+ throw new Error(`${envVar} is not set`);
12
+ }
13
+ return process.env[envVar];
14
+ };
15
+
16
+ const ensureVlayerEnv = () => {
17
+ try {
18
+ if (!process.env.VLAYER_ENV) {
19
+ throw new Error("VLAYER_ENV is not set. Available options: testnet, dev");
20
+ }
21
+ if (!["testnet", "dev"].includes(process.env.VLAYER_ENV)) {
22
+ throw new Error(
23
+ `Invalid VLAYER_ENV: ${process.env.VLAYER_ENV}. Available options: testnet, anvil, mainnet`,
24
+ );
25
+ }
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
+ } catch (e) {
28
+ return "dev";
29
+ }
30
+
31
+ return process.env.VLAYER_ENV;
32
+ };
33
+
34
+ const dotEnvFlowConfig = () => {
35
+ dotenvflow.config({
36
+ node_env: ensureVlayerEnv(),
37
+ });
38
+ };
39
+
40
+ export const toCamelCase = (str: string) =>
41
+ str
42
+ .toLowerCase()
43
+ .replace(/([-_][a-z])/g, (group) =>
44
+ group.toUpperCase().replace("-", "").replace("_", ""),
45
+ );
46
+
47
+ const envVars = [
48
+ { var: "CHAIN_NAME" },
49
+ { var: "PROVER_URL" },
50
+ { var: "JSON_RPC_URL" },
51
+ { var: "L2_JSON_RPC_URL", optional: true },
52
+ { var: "EXAMPLES_TEST_PRIVATE_KEY", to: "privateKey" },
53
+ { var: "VLAYER_API_TOKEN", to: "token", optional: true },
54
+ ];
55
+
56
+ export const getConfig = () => {
57
+ dotEnvFlowConfig();
58
+ return envVars.reduce((config, envVar) => {
59
+ try {
60
+ return {
61
+ ...config,
62
+ [envVar.to ?? toCamelCase(envVar.var)]: ensureEnvVariable(envVar.var),
63
+ };
64
+ } catch (e) {
65
+ if (envVar.optional) {
66
+ return { ...config };
67
+ }
68
+ throw e;
69
+ }
70
+ }, {} as EnvConfig);
71
+ };
@@ -0,0 +1,5 @@
1
+ export * from "./getConfig";
2
+ export * from "./createContext";
3
+ export * from "./deploy";
4
+ export * from "./writeEnvVariables";
5
+ export * from "./types";
@@ -0,0 +1,26 @@
1
+ // result of env parsing
2
+ // mostly needed by the examples to be able to properly perform
3
+ // pre run deployment
4
+
5
+ export type EnvConfig = {
6
+ chainName: string;
7
+ proverUrl: string;
8
+ jsonRpcUrl: string;
9
+ l2JsonRpcUrl?: string;
10
+ privateKey: `0x${string}`;
11
+ token?: string;
12
+ };
13
+
14
+ // represents what is needed by client to properly
15
+ // work in whole vlayer flow
16
+ // privateKey is optional and used only for anvil
17
+ // to avoid involving metamask into the flow
18
+
19
+ export type VlayerContextConfig = {
20
+ chainName: string;
21
+ jsonRpcUrl: string;
22
+ proverUrl: string;
23
+ wsProxyUrl?: string;
24
+ notaryUrl?: string;
25
+ privateKey?: `0x${string}`;
26
+ };
@@ -0,0 +1,28 @@
1
+ import fs from "fs";
2
+ import dotenv from "dotenv";
3
+ import debug from "debug";
4
+
5
+ const log = debug("vlayer:config");
6
+
7
+ export const writeEnvVariables = async (
8
+ envPath: string,
9
+ overrides: { [key: string]: string | undefined },
10
+ ) => {
11
+ fs.appendFileSync(envPath, "");
12
+ const envFile = Bun.file(envPath);
13
+ let envContent = await envFile.text();
14
+
15
+ if (!envContent) {
16
+ envContent = "";
17
+ }
18
+
19
+ const newEnvs = Object.assign(dotenv.parse(envContent), overrides);
20
+
21
+ const envLines = Object.entries(newEnvs)
22
+ .map(([key, value]) => `${key}=${value}`)
23
+ .join("\n");
24
+
25
+ await Bun.write(envPath, envLines);
26
+
27
+ log(`Successfully updated the ${envPath} with: `, overrides);
28
+ };
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { preverifyEmail } from "./api/email/preverify";
2
+ export { createVlayerClient } from "./api/lib/client";
3
+
4
+ export * from "./api/lib/types";
5
+
6
+ export * from "./web-proof-commons/utils";
7
+ export * from "./web-proof-commons/types/message";
@@ -0,0 +1,3 @@
1
+ import fs from "fs";
2
+
3
+ export const readFile = (path: string) => fs.readFileSync(path).toString();
@@ -0,0 +1,3 @@
1
+ export * from "./types/message";
2
+ export * from "./types/redaction";
3
+ export * from "./utils";
@@ -0,0 +1,176 @@
1
+ import type { Branded } from "../utils";
2
+ import { URLPattern } from "urlpattern-polyfill";
3
+ import { type RedactionConfig } from "./redaction";
4
+ import urlRegex from "url-regex";
5
+ import type { PresentationJSON as TLSNPresentationJSON } from "tlsn-js/src/types";
6
+
7
+ export const EXTENSION_STEP = {
8
+ expectUrl: "expectUrl",
9
+ startPage: "startPage",
10
+ notarize: "notarize",
11
+ } as const;
12
+
13
+ export type ExtensionStep =
14
+ (typeof EXTENSION_STEP)[keyof typeof EXTENSION_STEP];
15
+
16
+ export const enum ExtensionAction {
17
+ RequestWebProof = "RequestWebProof",
18
+ NotifyZkProvingStatus = "NotifyZkProvingStatus",
19
+ }
20
+
21
+ export enum ZkProvingStatus {
22
+ NotStarted = "NotStarted",
23
+ Proving = "Proving",
24
+ Done = "Done",
25
+ Error = "Error",
26
+ }
27
+
28
+ export type MessageToExtension =
29
+ | {
30
+ action: ExtensionAction.RequestWebProof;
31
+ payload: WebProverSessionConfig;
32
+ }
33
+ | {
34
+ action: ExtensionAction.NotifyZkProvingStatus;
35
+ payload: {
36
+ status: ZkProvingStatus;
37
+ };
38
+ };
39
+
40
+ export enum ExtensionMessageType {
41
+ ProofDone = "ProofDone",
42
+ ProofError = "ProofError",
43
+ RedirectBack = "RedirectBack",
44
+ TabOpened = "TabOpened",
45
+ ProofProcessing = "ProofProcessing",
46
+ }
47
+
48
+ export type PresentationJSON = TLSNPresentationJSON;
49
+
50
+ export type ExtensionMessage =
51
+ | {
52
+ type: ExtensionMessageType.ProofDone;
53
+ payload: {
54
+ presentationJSON: PresentationJSON;
55
+ decodedTranscript: {
56
+ sent: string;
57
+ recv: string;
58
+ };
59
+ };
60
+ }
61
+ | { type: ExtensionMessageType.ProofError; payload: { error: string } }
62
+ | { type: ExtensionMessageType.RedirectBack }
63
+ | { type: ExtensionMessageType.TabOpened; payload: { tabId: number } }
64
+ | {
65
+ type: ExtensionMessageType.ProofProcessing;
66
+ payload: {
67
+ // as we dont have progress yet from tlsn this is optional
68
+ progress?: number;
69
+ };
70
+ };
71
+
72
+ export type EmptyWebProverSessionConfig = {
73
+ notaryUrl: null;
74
+ wsProxyUrl: null;
75
+ logoUrl: null;
76
+ steps: never[];
77
+ };
78
+
79
+ export type WebProverSessionConfig =
80
+ | {
81
+ notaryUrl: string;
82
+ wsProxyUrl: string;
83
+ logoUrl: string;
84
+ steps: WebProofStep[];
85
+ }
86
+ | EmptyWebProverSessionConfig;
87
+
88
+ export function isEmptyWebProverSessionConfig(
89
+ config: WebProverSessionConfig,
90
+ ): config is EmptyWebProverSessionConfig {
91
+ return (
92
+ config.notaryUrl === null &&
93
+ config.wsProxyUrl === null &&
94
+ config.logoUrl === null &&
95
+ config.steps.length === 0
96
+ );
97
+ }
98
+
99
+ export type WebProofStep =
100
+ | WebProofStepNotarize
101
+ | WebProofStepExpectUrl
102
+ | WebProofStepStartPage;
103
+
104
+ export type UrlPattern = Branded<string, "UrlPattern">;
105
+
106
+ export type Url = Branded<UrlPattern, "Url">;
107
+
108
+ export type WebProofStepNotarize = Branded<
109
+ {
110
+ url: UrlPattern;
111
+ method: string;
112
+ label: string;
113
+ redact: RedactionConfig;
114
+ step: typeof EXTENSION_STEP.notarize;
115
+ },
116
+ "notarize"
117
+ >;
118
+
119
+ export type WebProofStepStartPage = Branded<
120
+ {
121
+ url: Url;
122
+ label: string;
123
+ step: typeof EXTENSION_STEP.startPage;
124
+ },
125
+ "startPage"
126
+ >;
127
+
128
+ export type WebProofStepExpectUrl = Branded<
129
+ {
130
+ url: UrlPattern;
131
+ label: string;
132
+ step: typeof EXTENSION_STEP.expectUrl;
133
+ },
134
+ "expectUrl"
135
+ >;
136
+
137
+ export enum StepValidationErrors {
138
+ InvalidUrl = "InvalidUrl",
139
+ InvalidUrlPattern = "InvalidUrlPattern",
140
+ }
141
+
142
+ export enum StepValidationErrorMessage {
143
+ InvalidUrl = "Wrong url",
144
+ InvalidUrlPattern = "Wrong url pattern",
145
+ }
146
+
147
+ export class StepValidationError extends Error {
148
+ constructor(message: string, name: StepValidationErrors) {
149
+ super(message);
150
+ this.name = name;
151
+ }
152
+ }
153
+
154
+ export function assertUrl(url: string): asserts url is Url {
155
+ const regex = urlRegex({ strict: true });
156
+ const isUrl = regex.test(url);
157
+ if (!isUrl) {
158
+ throw new StepValidationError(
159
+ `${StepValidationErrorMessage.InvalidUrl}: ${url}`,
160
+ StepValidationErrors.InvalidUrl,
161
+ );
162
+ }
163
+ }
164
+
165
+ export function assertUrlPattern(
166
+ urlPattern: string,
167
+ ): asserts urlPattern is UrlPattern {
168
+ try {
169
+ new URLPattern(urlPattern);
170
+ } catch {
171
+ throw new StepValidationError(
172
+ `${StepValidationErrorMessage.InvalidUrlPattern}: ${urlPattern} `,
173
+ StepValidationErrors.InvalidUrlPattern,
174
+ );
175
+ }
176
+ }
@@ -0,0 +1,97 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { RedactionItemsArray } from "./redaction";
3
+ describe("RedactionConfig", () => {
4
+ test("accepts empty array", () => {
5
+ const testEmpty = RedactionItemsArray.parse([]);
6
+ expect(testEmpty).toBeDefined();
7
+ });
8
+
9
+ test("valid redaction config", () => {
10
+ const testValid = RedactionItemsArray.parse([
11
+ {
12
+ request: {
13
+ headers: ["Authorization"],
14
+ },
15
+ },
16
+ {
17
+ response: {
18
+ headers: ["Content-Type"],
19
+ },
20
+ },
21
+ ]);
22
+ console.log(testValid);
23
+ expect(testValid).toBeDefined();
24
+ });
25
+
26
+ test("invalid when using both request headers and headers_except", () => {
27
+ expect(() => {
28
+ RedactionItemsArray.parse([
29
+ {
30
+ request: {
31
+ headers: ["Authorization"],
32
+ },
33
+ },
34
+ {
35
+ request: {
36
+ headers_except: ["Authorization"],
37
+ },
38
+ },
39
+ ]);
40
+ }).toThrow("Cannot have both request headers and request headers_except");
41
+ });
42
+
43
+ test("invalid when using both response headers and headers_except", () => {
44
+ expect(() => {
45
+ RedactionItemsArray.parse([
46
+ {
47
+ response: {
48
+ headers: ["Content-Type"],
49
+ },
50
+ },
51
+ {
52
+ response: {
53
+ headers_except: ["Authorization"],
54
+ },
55
+ },
56
+ ]);
57
+ }).toThrow("Cannot have both response headers and response headers_except");
58
+ });
59
+
60
+ test("invalid when using both request url_query and url_query_except", () => {
61
+ expect(() => {
62
+ RedactionItemsArray.parse([
63
+ {
64
+ request: {
65
+ url_query: ["page"],
66
+ },
67
+ },
68
+ {
69
+ request: {
70
+ url_query_except: ["limit"],
71
+ },
72
+ },
73
+ ]);
74
+ }).toThrow(
75
+ "Cannot have both request url_query and request url_query_except",
76
+ );
77
+ });
78
+
79
+ test("invalid when using both response json_body and json_body_except", () => {
80
+ expect(() => {
81
+ RedactionItemsArray.parse([
82
+ {
83
+ response: {
84
+ json_body: ["data"],
85
+ },
86
+ },
87
+ {
88
+ response: {
89
+ json_body_except: ["metadata"],
90
+ },
91
+ },
92
+ ]);
93
+ }).toThrow(
94
+ "Cannot have both response json_body and response json_body_except",
95
+ );
96
+ });
97
+ });