From 52216e9cd3730132fbe2c999b62f12a030ab6b30 Mon Sep 17 00:00:00 2001 From: Evan Sangaline Date: Thu, 22 Feb 2024 11:06:36 -0700 Subject: [PATCH] Modularize SDK client Previously, there was no way for users to create new instances of `SindriClient` because both the logger and the internal generated API client operated on a global level. This updates all of that so that instances are fully independent, and adds a `SindriClient.create()` method to initialize new client instances. These can use different credentials, have a different log level, etc. As part of the modularization, I configured openapi-typescript-codegen to use a persistent `request.ts` module that we can customize. This allows our special handling of `FormData` to live outside of the code that gets clobbered with each regeneration. I also hacked in a `logger` field in the `OpenAPIConfig` class which `request.ts` uses to add debug logging around all API requests/responses. I made significant progress on getting the CLI to use the client consistently. All of the commands use it for all logging and requests now, but we still have two parallel implementations of the packaging and polling for circuit deployment so that's a significant last piece that still needs to be addressed. Because the client requests are now logged internally, I was able to remove all of that explicit logging from the CLI commands. This means there's no longer the fancy logic around not logging repeated poll responses if the status hasn't changed, but I think it's worth it to have the request logging be more uniform and universal. Making these changes required regenerating the internal API client, so this pulls in miscellaneous backend API changes that have taken place. Nothing particularly major, adding `noir_version` to Noir circuit responses is one example, but these are mixed in here. Connects #60 Closes #58 Closes #70 Merges #73 --- package.json | 6 +- sindri-manifest.json | 310 +++++++++++------- src/cli/config.ts | 7 +- src/cli/deploy.ts | 77 ++--- src/cli/index.ts | 17 +- src/cli/init.ts | 34 +- src/cli/lint.ts | 40 +-- src/cli/login.ts | 66 ++-- src/cli/logout.ts | 19 +- src/cli/utils.ts | 8 +- src/cli/whoami.ts | 19 +- src/lib/api/ApiClient.ts | 47 +++ src/lib/api/core/AxiosHttpRequest.ts | 25 ++ src/lib/api/core/BaseHttpRequest.ts | 13 + src/lib/api/core/OpenAPI.ts | 8 +- src/lib/api/core/request.ts | 55 +++- src/lib/api/index.ts | 80 ++--- .../api/models/CircomCircuitInfoResponse.ts | 7 +- src/lib/api/models/CircuitInfoResponse.ts | 18 + ...itStatus.ts => ForgeValueErrorResponse.ts} | 7 +- .../api/models/GnarkCircuitInfoResponse.ts | 7 +- .../api/models/Halo2CircuitInfoResponse.ts | 7 +- .../models/{ProofStatus.ts => JobStatus.ts} | 4 +- src/lib/api/models/NoirCircuitInfoResponse.ts | 8 +- src/lib/api/models/ProofInfoResponse.ts | 4 +- src/lib/api/services/AuthorizationService.ts | 27 +- src/lib/api/services/CircuitsService.ts | 66 ++-- src/lib/api/services/InternalService.ts | 21 +- src/lib/api/services/ProofsService.ts | 19 +- src/lib/api/services/TokenService.ts | 17 +- src/lib/client.ts | 116 ++++--- src/lib/config.ts | 44 ++- src/lib/logging.ts | 38 ++- 33 files changed, 736 insertions(+), 505 deletions(-) create mode 100644 src/lib/api/ApiClient.ts create mode 100644 src/lib/api/core/AxiosHttpRequest.ts create mode 100644 src/lib/api/core/BaseHttpRequest.ts create mode 100644 src/lib/api/models/CircuitInfoResponse.ts rename src/lib/api/models/{CircuitStatus.ts => ForgeValueErrorResponse.ts} (55%) rename src/lib/api/models/{ProofStatus.ts => JobStatus.ts} (59%) diff --git a/package.json b/package.json index 7e03099..2fa986b 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,9 @@ "download-sindri-manifest-schema": "nwget https://sindri.app/api/v1/sindri-manifest-schema.json -O sindri-manifest.json && prettier --write sindri-manifest.json", "download-sindri-manifest-schema:dev": "nwget http://localhost/api/v1/sindri-manifest-schema.json -O sindri-manifest.json && prettier --write sindri-manifest.json", "download-sindri-manifest-schema:docker": "nwget http://host.docker.internal/api/v1/sindri-manifest-schema.json -O sindri-manifest.json && prettier --write sindri-manifest.json", - "generate-api": "rm -rf src/lib/api/ && openapi --client axios --input https://sindri.app/api/openapi.json --output src/lib/api/ --useUnionTypes && prettier --write src/lib/api/**/*", - "generate-api:dev": "rm -rf src/lib/api/ && openapi --client axios --input http://localhost/api/openapi.json --output src/lib/api/ --useUnionTypes && prettier --write src/lib/api/**/*", - "generate-api:docker": "rm -rf src/lib/api/ && openapi --client axios --input http://host.docker.internal/api/openapi.json --output src/lib/api/ --useUnionTypes && prettier --write src/lib/api/**/*", + "generate-api": "mv src/lib/api/core/request.ts tmp-request.ts && rm -rf src/lib/api/ && openapi --client axios --input https://sindri.app/api/openapi.json --name ApiClient --output src/lib/api/ --request tmp-request.ts --useUnionTypes && rm tmp-request.ts && prettier --write src/lib/api/**/*", + "generate-api:dev": "mv src/lib/api/core/request.ts tmp-request.ts && rm -rf src/lib/api/ && openapi --client axios --input http://localhost/api/openapi.json --name ApiClient --output src/lib/api/ --request tmp-request.ts --useUnionTypes && rm tmp-request.ts && prettier --write src/lib/api/**/*", + "generate-api:docker": "mv src/lib/api/core/request.ts tmp-request.ts && rm -rf src/lib/api/ && openapi --client axios --input http://host.docker.internal/api/openapi.json --name ApiClient --output src/lib/api/ --request tmp-request.ts --useUnionTypes && rm tmp-request.ts && prettier --write src/lib/api/**/*", "lint": "eslint '**/*.{js,ts}'", "format": "prettier --write '**/*.{js,json,md,ts}'", "test": "yarn build && yarn test:fast", diff --git a/sindri-manifest.json b/sindri-manifest.json index e499c5d..6355692 100644 --- a/sindri-manifest.json +++ b/sindri-manifest.json @@ -2,65 +2,76 @@ "$id": "https://sindri.app/api/v1/sindri-manifest-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "SindriManifest", - "description": "Discriminated union type for `sindri.json` manifest files.\n\nThis is only used for serializing the JSON Schema currently, but it would be nice to use it more\nbroadly once we improve the typing on the union types. We should be able to use\n`SindriManifest.parse_obj()` instead of `get_validate_sindri_manifest()` and other\nsimplifications.", + "description": "Sindri Manifest file schema for `sindri.json` files.", "anyOf": [ { - "$ref": "#/definitions/CircomSindri" + "$ref": "#/definitions/CircomSindriManifest" }, { - "$ref": "#/definitions/GnarkSindri" + "$ref": "#/definitions/GnarkSindriManifest" }, { - "$ref": "#/definitions/Halo2AxiomV022Sindri" + "$ref": "#/definitions/Halo2AxiomV022SindriManifest" }, { - "$ref": "#/definitions/Halo2AxiomV030Sindri" + "$ref": "#/definitions/Halo2AxiomV030SindriManifest" }, { - "$ref": "#/definitions/Halo2ChiquitoSindri" - }, - { - "$ref": "#/definitions/NoirSindri" + "$ref": "#/definitions/NoirSindriManifest" } ], "definitions": { - "SindriCircuitTypeOptions": { - "title": "SindriCircuitTypeOptions", - "description": "circuit_type options", - "enum": ["circom", "gnark", "halo2", "noir"], - "type": "string" - }, "CircomCurveOptions": { "title": "CircomCurveOptions", - "description": "An enumeration.", + "description": "The supported Circom curves.", "enum": ["bn254"], "type": "string" }, "CircomProvingSchemeOptions": { "title": "CircomProvingSchemeOptions", - "description": "An enumeration.", + "description": "The supported Circom proving schemes.", "enum": ["groth16"], "type": "string" }, "CircomWitnessCompilerOptions": { "title": "CircomWitnessCompilerOptions", - "description": "An enumeration.", + "description": "The supported Circom witness compilers.", "enum": ["c++", "wasm"], "type": "string" }, - "CircomSindri": { - "title": "CircomSindri", - "description": "Circom Sindri Manifest", + "CircomSindriManifest": { + "title": "Sindri Manifest for Circom Circuits", + "description": "Sindri Manifest for Circom circuits.", "type": "object", "properties": { "circuitType": { - "$ref": "#/definitions/SindriCircuitTypeOptions" + "title": "Circuit Type", + "description": "The (frontend) development framework that your circuit is written with.", + "enum": ["circom"], + "type": "string" }, "name": { - "title": "Name", + "title": "Circuit Name", + "description": "The circuit name used to uniquely identify the circuit within your organization. Similar to a GitHub project name or a Docker image name.", + "pattern": "^[a-zA-Z0-9_-]+$", + "error_messages": { + "regex": "`name` must be a valid slug." + }, + "type": "string" + }, + "circuitPath": { + "title": "Circuit Path", + "description": "Path to a `.circom` circuit file with a main component (defaults to `./circuit.circom`).", + "default": "./circuit.circom", + "pattern": "^[^/].*\\.circom$", + "error_messages": { + "regex": "`circuit_path` must be a valid relative path to your main `.circom` file." + }, "type": "string" }, "curve": { + "title": "Proving Curve", + "description": "The curve over which the proof is executed.", "default": "bn254", "allOf": [ { @@ -69,6 +80,7 @@ ] }, "provingScheme": { + "description": "The backend proving scheme.", "default": "groth16", "allOf": [ { @@ -77,6 +89,7 @@ ] }, "witnessCompiler": { + "description": "The circuit witness compiler.", "default": "c++", "allOf": [ { @@ -88,9 +101,7 @@ "type": "string", "title": "Sindri Manifest JSON Schema URL", "description": "The URL pointing to a Sindri JSON Manifest schema definition.", - "examples": [ - "https://sindri.app/api/v1/sindri-manifest-schema.json" - ] + "examples": ["https://sindri.app/api/v1/sindri-manifest-schema.json"] } }, "required": ["circuitType", "name"], @@ -98,7 +109,7 @@ }, "GnarkCurveOptions": { "title": "GnarkCurveOptions", - "description": "An enumeration.", + "description": "The supported Gnark curves.", "enum": [ "bls12-377", "bls12-381", @@ -111,33 +122,48 @@ }, "GnarkVersionOptions": { "title": "GnarkVersionOptions", - "description": "An enumeration.", + "description": "The supported Gnark framework versions.", "enum": ["v0.8.1", "v0.9.0"], "type": "string" }, "GnarkProvingSchemeOptions": { "title": "GnarkProvingSchemeOptions", - "description": "An enumeration.", + "description": "The supported Gnark proving schemes.", "enum": ["groth16"], "type": "string" }, - "GnarkSindri": { - "title": "GnarkSindri", - "description": "Gnark Sindri Manifest", + "GnarkSindriManifest": { + "title": "Sindri Manifest for Gnark Circuits", + "description": "Sindri Manifest for Gnark circuits.", "type": "object", "properties": { "circuitType": { - "$ref": "#/definitions/SindriCircuitTypeOptions" + "title": "Circuit Type", + "description": "The (frontend) development framework that your circuit is written with.", + "enum": ["gnark"], + "type": "string" }, "name": { - "title": "Name", + "title": "Circuit Name", + "description": "The circuit name used to uniquely identify the circuit within your organization. Similar to a GitHub project name or a Docker image name.", + "pattern": "^[a-zA-Z0-9_-]+$", + "error_messages": { + "regex": "`name` must be a valid slug." + }, "type": "string" }, "circuitStructName": { "title": "Circuit Struct Name", + "description": "The name of the Go struct which defines your circuit inputs.", + "pattern": "^[A-Z][A-Za-z0-9_]*$", + "error_messages": { + "regex": "`circuitStructName` must be a valid Go exported struct name." + }, "type": "string" }, "curve": { + "title": "Proving Curve", + "description": "The curve over which the proof is executed.", "default": "bn254", "allOf": [ { @@ -146,13 +172,24 @@ ] }, "gnarkVersion": { - "$ref": "#/definitions/GnarkVersionOptions" + "description": "The version of the Gnark framework that your circuit uses.", + "allOf": [ + { + "$ref": "#/definitions/GnarkVersionOptions" + } + ] }, "packageName": { - "title": "Package Name", + "title": "Go Package Name", + "description": "The name of the Go package containing your circuit definition.", + "pattern": "^[a-z][a-z0-9]*$", + "error_messages": { + "regex": "`packageName` must be a valid Go package name." + }, "type": "string" }, "provingScheme": { + "description": "The backend proving scheme.", "default": "groth16", "allOf": [ { @@ -164,9 +201,7 @@ "type": "string", "title": "Sindri Manifest JSON Schema URL", "description": "The URL pointing to a Sindri JSON Manifest schema definition.", - "examples": [ - "https://sindri.app/api/v1/sindri-manifest-schema.json" - ] + "examples": ["https://sindri.app/api/v1/sindri-manifest-schema.json"] } }, "required": [ @@ -178,46 +213,75 @@ ], "additionalProperties": false }, - "Halo2VersionOptions": { - "title": "Halo2VersionOptions", - "description": "An enumeration.", - "enum": ["axiom-v0.2.2", "axiom-v0.3.0", "chiquito"], + "Halo2ProvingSchemeOptions": { + "title": "Halo2ProvingSchemeOptions", + "description": "The supported Halo2 proving schemes.", + "enum": ["shplonk"], "type": "string" }, - "Halo2AxiomV022Sindri": { - "title": "Halo2AxiomV022Sindri", - "description": "Halo2 Axiom V0.2.2 Sindri Manifest", + "Halo2AxiomV022SindriManifest": { + "title": "Sindri Manifest for Axiom v0.2.2 Halo2 Circuits", + "description": "Sindri Manifest for Axiom v0.2.2 circuits built with the Halo2 framework.", "type": "object", "properties": { "circuitType": { - "$ref": "#/definitions/SindriCircuitTypeOptions" + "title": "Circuit Type", + "description": "The (frontend) development framework that your circuit is written with.", + "enum": ["halo2"], + "type": "string" }, "name": { - "title": "Name", + "title": "Circuit Name", + "description": "The circuit name used to uniquely identify the circuit within your organization. Similar to a GitHub project name or a Docker image name.", + "pattern": "^[a-zA-Z0-9_-]+$", + "error_messages": { + "regex": "`name` must be a valid slug." + }, "type": "string" }, "className": { - "title": "Class Name", + "title": "Circuit Class Name", + "description": "The path to your circuit struct definition. (*e.g.* `my-package::my_file::MyCircuitStruct`).", + "pattern": "^([A-Za-z_][A-Za-z0-9_]*::)+[A-Za-z_][A-Za-z0-9_]*$", + "error_messages": { + "regex": "`className` must be a valid and fully qualifed Rust path to a struct including the crate name." + }, "type": "string" }, "degree": { "title": "Degree", + "description": "Specifies that the circuit will have 2^degree rows.", "type": "integer" }, "halo2Version": { - "$ref": "#/definitions/Halo2VersionOptions" + "title": "Halo2 Version", + "description": "The Halo2 frontend that your circuit is written with.", + "enum": ["axiom-v0.2.2"], + "type": "string" }, "packageName": { - "title": "Package Name", + "title": "Rust Package Name", + "description": "The name of the Rust package containing your circuit.", + "pattern": "^[a-z0-9_]+(?:-[a-z0-9_]+)*$", + "error_messages": { + "regex": "`packageName` must be a valid Rust crate name." + }, "type": "string" }, + "provingScheme": { + "description": "The backend proving scheme.", + "default": "shplonk", + "allOf": [ + { + "$ref": "#/definitions/Halo2ProvingSchemeOptions" + } + ] + }, "$schema": { "type": "string", "title": "Sindri Manifest JSON Schema URL", "description": "The URL pointing to a Sindri JSON Manifest schema definition.", - "examples": [ - "https://sindri.app/api/v1/sindri-manifest-schema.json" - ] + "examples": ["https://sindri.app/api/v1/sindri-manifest-schema.json"] } }, "required": [ @@ -230,35 +294,67 @@ ], "additionalProperties": false }, - "Halo2AxiomV030Sindri": { - "title": "Halo2AxiomV030Sindri", - "description": "Halo2 Axiom V0.3.0 Sindri Manifest", + "Halo2AxiomV030SindriManifest": { + "title": "Sindri Manifest for Axiom v0.3.0 Halo2 Circuits", + "description": "Sindri Manifest for Axiom v0.3.0 circuits built with the Halo2 framework.", "type": "object", "properties": { "circuitType": { - "$ref": "#/definitions/SindriCircuitTypeOptions" + "title": "Circuit Type", + "description": "The (frontend) development framework that your circuit is written with.", + "enum": ["halo2"], + "type": "string" }, "name": { - "title": "Name", + "title": "Circuit Name", + "description": "The circuit name used to uniquely identify the circuit within your organization. Similar to a GitHub project name or a Docker image name.", + "pattern": "^[a-zA-Z0-9_-]+$", + "error_messages": { + "regex": "`name` must be a valid slug." + }, "type": "string" }, "className": { - "title": "Class Name", + "title": "Circuit Class Name", + "description": "The path to your circuit struct definition. (*e.g.* `my-package::my_file::MyCircuitStruct`).", + "pattern": "^([A-Za-z_][A-Za-z0-9_]*::)+[A-Za-z_][A-Za-z0-9_]*$", + "error_messages": { + "regex": "`className` must be a valid and fully qualifed Rust path to a struct including the crate name." + }, "type": "string" }, "degree": { "title": "Degree", + "description": "Specifies that the circuit will have 2^degree rows.", "type": "integer" }, "halo2Version": { - "$ref": "#/definitions/Halo2VersionOptions" + "title": "Halo2 Version", + "description": "The Halo2 frontend that your circuit is written with.", + "enum": ["axiom-v0.3.0"], + "type": "string" }, "packageName": { - "title": "Package Name", + "title": "Rust Package Name", + "description": "The name of the Rust package containing your circuit.", + "pattern": "^[a-z0-9_]+(?:-[a-z0-9_]+)*$", + "error_messages": { + "regex": "`packageName` must be a valid Rust crate name." + }, "type": "string" }, + "provingScheme": { + "description": "The backend proving scheme.", + "default": "shplonk", + "allOf": [ + { + "$ref": "#/definitions/Halo2ProvingSchemeOptions" + } + ] + }, "threadBuilder": { "title": "Thread Builder", + "description": "The type of multi-threaded witness generator used. Choose GateThreadBuilder for simple circuits or RlcThreadBuilder for advanced applications that require sources of randomness.", "enum": ["GateThreadBuilder", "RlcThreadBuilder"], "type": "string" }, @@ -266,9 +362,7 @@ "type": "string", "title": "Sindri Manifest JSON Schema URL", "description": "The URL pointing to a Sindri JSON Manifest schema definition.", - "examples": [ - "https://sindri.app/api/v1/sindri-manifest-schema.json" - ] + "examples": ["https://sindri.app/api/v1/sindri-manifest-schema.json"] } }, "required": [ @@ -282,71 +376,40 @@ ], "additionalProperties": false }, - "Halo2ChiquitoSindri": { - "title": "Halo2ChiquitoSindri", - "description": "Halo2 Chiquito Sindri Manifest", - "type": "object", - "properties": { - "circuitType": { - "$ref": "#/definitions/SindriCircuitTypeOptions" - }, - "name": { - "title": "Name", - "type": "string" - }, - "className": { - "title": "Class Name", - "type": "string" - }, - "degree": { - "title": "Degree", - "type": "integer" - }, - "halo2Version": { - "$ref": "#/definitions/Halo2VersionOptions" - }, - "packageName": { - "title": "Package Name", - "type": "string" - }, - "$schema": { - "type": "string", - "title": "Sindri Manifest JSON Schema URL", - "description": "The URL pointing to a Sindri JSON Manifest schema definition.", - "examples": [ - "https://sindri.app/api/v1/sindri-manifest-schema.json" - ] - } - }, - "required": [ - "circuitType", - "name", - "className", - "degree", - "halo2Version", - "packageName" - ], - "additionalProperties": false - }, "NoirProvingSchemeOptions": { "title": "NoirProvingSchemeOptions", - "description": "An enumeration.", + "description": "The supported Noir proving schemes.", "enum": ["barretenberg"], "type": "string" }, - "NoirSindri": { - "title": "NoirSindri", - "description": "Noir Sindri Manifest", + "NoirVersionOptions": { + "title": "NoirVersionOptions", + "description": "The supported Noir Compiler and Prover versions.", + "enum": ["latest", "0.17.0", "0.18.0", "0.19.4", "0.22.0", "0.23.0"], + "type": "string" + }, + "NoirSindriManifest": { + "title": "Sindri Manifest for Noir Circuits", + "description": "Sindri Manifest for Noir circuits.", "type": "object", "properties": { "circuitType": { - "$ref": "#/definitions/SindriCircuitTypeOptions" + "title": "Circuit Type", + "description": "The (frontend) development framework that your circuit is written with.", + "enum": ["noir"], + "type": "string" }, "name": { - "title": "Name", + "title": "Circuit Name", + "description": "The circuit name used to uniquely identify the circuit within your organization. Similar to a GitHub project name or a Docker image name.", + "pattern": "^[a-zA-Z0-9_-]+$", + "error_messages": { + "regex": "`name` must be a valid slug." + }, "type": "string" }, "provingScheme": { + "description": "The backend proving scheme.", "default": "barretenberg", "allOf": [ { @@ -354,13 +417,20 @@ } ] }, + "noirVersion": { + "description": "Noir compiler version (defaults to `latest`).", + "default": "latest", + "allOf": [ + { + "$ref": "#/definitions/NoirVersionOptions" + } + ] + }, "$schema": { "type": "string", "title": "Sindri Manifest JSON Schema URL", "description": "The URL pointing to a Sindri JSON Manifest schema definition.", - "examples": [ - "https://sindri.app/api/v1/sindri-manifest-schema.json" - ] + "examples": ["https://sindri.app/api/v1/sindri-manifest-schema.json"] } }, "required": ["circuitType", "name"], diff --git a/src/cli/config.ts b/src/cli/config.ts index 8d24a96..c0238fa 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -1,14 +1,15 @@ import { Command } from "@commander-js/extra-typings"; -import { Config } from "lib/config"; import { print } from "lib/logging"; +import sindri from "lib"; export const configListCommand = new Command() .name("list") .description("Show the current config.") .action(async () => { - const config = new Config(); - print(config.config); + // Reload the config because the log level was `silent` when the config was initially loaded. + sindri._config!.reload(); + print(sindri._config!.config); }); export const configCommand = new Command() diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index abf59e7..16d574e 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -9,8 +9,8 @@ import walk from "ignore-walk"; import tar from "tar"; import { findFileUpwards } from "cli/utils"; -import { ApiError, CircuitsService, CircuitStatus, OpenAPI } from "lib/api"; -import { logger } from "lib/logging"; +import sindri from "lib"; +import { ApiError } from "lib/api"; export const deployCommand = new Command() .name("deploy") @@ -22,7 +22,7 @@ export const deployCommand = new Command() // Validate the tags and "untagged" option. if (untagged) { if (tags.length !== 1 || tags[0] !== "latest") { - logger.error( + sindri.logger.error( "You cannot use both the `--tag` and `--untagged` options together.", ); return process.exit(1); @@ -30,7 +30,7 @@ export const deployCommand = new Command() } else { for (const tag of tags) { if (!/^[-a-zA-Z0-9_]+$/.test(tag)) { - logger.error( + sindri.logger.error( `"${tag}" is not a valid tag. Tags may only contain alphanumeric characters, ` + "underscores, and hyphens.", ); @@ -42,21 +42,21 @@ export const deployCommand = new Command() // Find `sindri.json` and move into the root of the project directory. const directoryPath = path.resolve(directory); if (!existsSync(directoryPath)) { - logger.error( + sindri.logger.error( `The "${directoryPath}" directory does not exist. Aborting.`, ); return process.exit(1); } const sindriJsonPath = findFileUpwards(/^sindri.json$/i, directoryPath); if (!sindriJsonPath) { - logger.error( + sindri.logger.error( `No "sindri.json" file was found in or above "${directoryPath}". Aborting.`, ); return process.exit(1); } - logger.debug(`Found "sindri.json" at "${sindriJsonPath}".`); + sindri.logger.debug(`Found "sindri.json" at "${sindriJsonPath}".`); const rootDirectory = path.dirname(sindriJsonPath); - logger.debug(`Changing current directory to "${rootDirectory}".`); + sindri.logger.debug(`Changing current directory to "${rootDirectory}".`); process.chdir(rootDirectory); // Load `sindri.json`. @@ -66,26 +66,26 @@ export const deployCommand = new Command() encoding: "utf-8", }); sindriJson = JSON.parse(sindriJsonContent); - logger.debug( + sindri.logger.debug( `Successfully loaded "sindri.json" from "${sindriJsonPath}":`, ); - logger.debug(sindriJson); + sindri.logger.debug(sindriJson); } catch (error) { - logger.fatal( + sindri.logger.fatal( `Error loading "${sindriJsonPath}", perhaps it is not valid JSON?`, ); - logger.error(error); + sindri.logger.error(error); return process.exit(1); } if (!("name" in sindriJson)) { - logger.error('No "name" field found in "sindri.json". Aborting.'); + sindri.logger.error('No "name" field found in "sindri.json". Aborting.'); return process.exit(1); } const circuitName = sindriJson.name; // Check that the API client is authorized. - if (!OpenAPI.TOKEN || !OpenAPI.BASE) { - logger.warn("You must login first with `sindri login`."); + if (!sindri.apiKey || !sindri.baseUrl) { + sindri.logger.warn("You must login first with `sindri login`."); return process.exit(1); } @@ -108,7 +108,7 @@ export const deployCommand = new Command() } const formData = new FormData(); const tarballFilename = `${circuitName}.tar.gz`; - logger.info( + sindri.logger.info( `Creating "${tarballFilename}" package with ${files.length} files.`, ); formData.append( @@ -119,7 +119,9 @@ export const deployCommand = new Command() { gzip: true, onwarn: (code: string, message: string) => { - logger.warn(`While creating tarball: ${code} - ${message}`); + sindri.logger.warn( + `While creating tarball: ${code} - ${message}`, + ); }, prefix: `${circuitName}/`, sync: true, @@ -144,64 +146,57 @@ export const deployCommand = new Command() // Upload the tarball. let circuitId: string | undefined; try { - logger.info("Circuit compilation initiated."); - const response = await CircuitsService.circuitCreate(formData); + sindri.logger.info("Circuit compilation initiated."); + const response = await sindri._client.circuits.circuitCreate(formData); circuitId = response.circuit_id; - logger.debug("/api/v1/circuit/create/ response:"); - logger.debug(response); } catch (error) { if (error instanceof ApiError && error.status === 401) { - logger.error( + sindri.logger.error( "Your credentials are invalid. Please log in again with `sindri login`.", ); } else { - logger.fatal("An unknown error occurred."); - logger.error(error); + sindri.logger.fatal("An unknown error occurred."); + sindri.logger.error(error); return process.exit(1); } } if (!circuitId) { - logger.error("No circuit ID was returned from the API. Aborting."); + sindri.logger.error("No circuit ID was returned from the API. Aborting."); return process.exit(1); } // Poll for circuit compilation to complete. const startTime = Date.now(); - let previousStatus: CircuitStatus | undefined; while (true) { try { - logger.debug("Polling for circuit compilation status."); - const response = await CircuitsService.circuitDetail(circuitId, false); - - // Only log this when the status changes because it's noisy. - if (previousStatus !== response.status) { - previousStatus = response.status; - logger.debug(`/api/v1/circuit/${circuitId}/detail/ response:`); - logger.debug(response); - } + sindri.logger.debug("Polling for circuit compilation status."); + const response = await sindri._client.circuits.circuitDetail( + circuitId, + false, + ); const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1); if (response.status === "Ready") { - logger.info( + sindri.logger.info( `Circuit compiled successfully after ${elapsedSeconds} seconds.`, ); break; } else if (response.status === "Failed") { - logger.error( + sindri.logger.error( `Circuit compilation failed after ${elapsedSeconds} seconds: ` + (response.error ?? "Unknown error."), ); return process.exit(1); } else if (response.status === "Queued") { - logger.debug("Circuit compilation is queued."); + sindri.logger.debug("Circuit compilation is queued."); } else if (response.status === "In Progress") { - logger.debug("Circuit compilation is in progress."); + sindri.logger.debug("Circuit compilation is in progress."); } } catch (error) { - logger.fatal( + sindri.logger.fatal( "An unknown error occurred while polling for compilation to finish.", ); - logger.error(error); + sindri.logger.error(error); return process.exit(1); } diff --git a/src/cli/index.ts b/src/cli/index.ts index 41f924c..3244a29 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,5 +1,4 @@ #! /usr/bin/env node -import assert from "assert"; import { argv, exit } from "process"; import { Command } from "@commander-js/extra-typings"; @@ -13,12 +12,12 @@ import { logoutCommand } from "cli/logout"; import { whoamiCommand } from "cli/whoami"; import { loadPackageJson } from "cli/utils"; import sindri from "lib"; -import { logger } from "lib/logging"; export const program = new Command() .name("sindri") .description("The Sindri CLI client.") .version(loadPackageJson().version ?? "unknown") + .enablePositionalOptions() .option("-d, --debug", "Enable debug logging.", false) .option( "-q, --quiet", @@ -37,23 +36,19 @@ export const program = new Command() // Set the logging level. const { debug, quiet } = command.opts(); if (debug && quiet) { - logger.error( + sindri.logLevel = "error"; + sindri.logger.error( "You cannot specify both the `--debug` and `--quiet` arguments.", ); return exit(1); } if (debug) { - logger.level = "trace"; + sindri.logLevel = "trace"; } else if (quiet) { - logger.level = "silent"; + sindri.logLevel = "silent"; } else { - logger.level = "info"; + sindri.logLevel = "info"; } - logger.debug(`Set log level to "${logger.level}".`); - - // Make sure the client is loaded and initialized before any subcommands run. - // Note that this also initializes the config. - assert(sindri); }); if (require.main === module) { diff --git a/src/cli/init.ts b/src/cli/init.ts index a0861ee..142a2a4 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -7,7 +7,7 @@ import { Command } from "@commander-js/extra-typings"; import { confirm, input, select } from "@inquirer/prompts"; import { scaffoldDirectory } from "cli/utils"; -import { logger } from "lib/logging"; +import sindri from "lib"; export const initCommand = new Command() .name("init") @@ -26,7 +26,7 @@ export const initCommand = new Command() if (!existsSync(directoryPath)) { mkdirSync(directoryPath, { recursive: true }); } else if (!statSync(directoryPath).isDirectory()) { - logger.warn( + sindri.logger.warn( `File "${directoryPath}" exists and is not a directory, aborting.`, ); return process.exit(1); @@ -42,7 +42,7 @@ export const initCommand = new Command() default: false, }); if (!proceed) { - logger.info("Aborting."); + sindri.logger.info("Aborting."); return process.exit(1); } } @@ -259,23 +259,23 @@ export const initCommand = new Command() provingScheme, }); } else { - logger.fatal(`Sorry, ${circuitType} is not yet supported.`); + sindri.logger.fatal(`Sorry, ${circuitType} is not yet supported.`); return process.exit(1); } // Perform the scaffolding. - logger.info( + sindri.logger.info( `Proceeding to generate scaffolded project in "${directoryPath}".`, ); - await scaffoldDirectory("common", directoryPath, context); - await scaffoldDirectory(circuitType, directoryPath, context); + await scaffoldDirectory("common", directoryPath, context, sindri.logger); + await scaffoldDirectory(circuitType, directoryPath, context, sindri.logger); // We use this in `common` right now to keep the directory tracked, we can remove this once we // add files there. const gitKeepFile = path.join(directoryPath, ".gitkeep"); if (existsSync(gitKeepFile)) { rmSync(gitKeepFile); } - logger.info("Project scaffolding successful."); + sindri.logger.info("Project scaffolding successful."); // Install dependencies. if (circuitType === "circom") { @@ -284,13 +284,13 @@ export const initCommand = new Command() execSync("npm --version"); npmInstalled = true; } catch { - logger.warn( + sindri.logger.warn( "NPM is not installed, cannot install circomlib as a dependency. " + "You will need to install NPM and run `npm install` yourself.", ); } if (npmInstalled) { - logger.info("Installing circomlib."); + sindri.logger.info("Installing circomlib."); execSync("npm install", { cwd: directoryPath }); } } @@ -301,7 +301,7 @@ export const initCommand = new Command() execSync("git --version"); gitInstalled = true; } catch { - logger.debug( + sindri.logger.debug( "Git is not installed, skipping git initialization questions.", ); } @@ -312,14 +312,18 @@ export const initCommand = new Command() default: true, }); if (initializeGit) { - logger.info(`Initializing git repository in "${directoryPath}".`); + sindri.logger.info( + `Initializing git repository in "${directoryPath}".`, + ); try { execSync("git init .", { cwd: directoryPath }); execSync("git add .", { cwd: directoryPath }); execSync("git commit -m 'Initial commit.'", { cwd: directoryPath }); - logger.info("Successfully initialized git repository."); + sindri.logger.info("Successfully initialized git repository."); } catch (error) { - logger.error("Error occurred while initializing the git repository."); + sindri.logger.error( + "Error occurred while initializing the git repository.", + ); // Node.js doesn't seem to have a typed version of this error, so we assert it as // something that's at least in the right ballpark. const execError = error as NodeJS.ErrnoException & { @@ -338,7 +342,7 @@ export const initCommand = new Command() execError[key] = ""; } }); - logger.error(execError); + sindri.logger.error(execError); } } } diff --git a/src/cli/lint.ts b/src/cli/lint.ts index 43cbd0d..bfcdea3 100644 --- a/src/cli/lint.ts +++ b/src/cli/lint.ts @@ -7,7 +7,7 @@ import type { Schema } from "jsonschema"; import { Validator as JsonValidator } from "jsonschema"; import { findFileUpwards, loadSindriManifestJsonSchema } from "cli/utils"; -import { logger } from "lib/logging"; +import sindri from "lib"; export const lintCommand = new Command() .name("lint") @@ -29,9 +29,9 @@ export const lintCommand = new Command() if (!sindriManifestJsonSchema) { throw new Error('No "sindri-manifest.json" file found.'); } - logger.debug('Successfully loaded "sindri-manifest.json".'); + sindri.logger.debug('Successfully loaded "sindri-manifest.json".'); } catch (error) { - logger.error( + sindri.logger.error( 'No "sindri-manifest.json" JSON Schema file found. Aborting.', ); return process.exit(1); @@ -40,21 +40,21 @@ export const lintCommand = new Command() // Find `sindri.json` and move into the root of the project directory. const directoryPath = path.resolve(directory); if (!existsSync(directoryPath)) { - logger.error( + sindri.logger.error( `The "${directoryPath}" directory does not exist. Aborting.`, ); return process.exit(1); } const sindriJsonPath = findFileUpwards(/^sindri.json$/i, directoryPath); if (!sindriJsonPath) { - logger.error( + sindri.logger.error( `No "sindri.json" file was found in or above "${directoryPath}". Aborting.`, ); return process.exit(1); } - logger.debug(`Found "sindri.json" at "${sindriJsonPath}".`); + sindri.logger.debug(`Found "sindri.json" at "${sindriJsonPath}".`); const rootDirectory = path.dirname(sindriJsonPath); - logger.debug(`Changing current directory to "${rootDirectory}".`); + sindri.logger.debug(`Changing current directory to "${rootDirectory}".`); process.chdir(directoryPath); // Load `sindri.json`. @@ -64,15 +64,15 @@ export const lintCommand = new Command() encoding: "utf-8", }); sindriJson = JSON.parse(sindriJsonContent); - logger.debug( + sindri.logger.debug( `Successfully loaded "sindri.json" from "${sindriJsonPath}":`, ); - logger.debug(sindriJson); + sindri.logger.debug(sindriJson); } catch (error) { - logger.fatal( + sindri.logger.fatal( `Error loading "${sindriJsonPath}", perhaps it is not valid JSON?`, ); - logger.error(error); + sindri.logger.error(error); return process.exit(1); } @@ -84,9 +84,11 @@ export const lintCommand = new Command() { nestedErrors: true }, ); if (validationStatus.valid) { - logger.info(`Sindri manifest file "${sindriJsonPath}" is valid.`); + sindri.logger.info(`Sindri manifest file "${sindriJsonPath}" is valid.`); } else { - logger.warn(`Sindri manifest file "${sindriJsonPath}" contains errors:`); + sindri.logger.warn( + `Sindri manifest file "${sindriJsonPath}" contains errors:`, + ); for (const error of validationStatus.errors) { const prefix = error.property @@ -95,7 +97,7 @@ export const lintCommand = new Command() (typeof error.schema === "object" && error.schema.title ? `:${error.schema.title}` : ""); - logger.error(`${prefix} ${error.message}`); + sindri.logger.error(`${prefix} ${error.message}`); errorCount += 1; } } @@ -103,24 +105,24 @@ export const lintCommand = new Command() // Check for a project README. const readmePath = path.join(rootDirectory, "README.md"); if (!existsSync(readmePath)) { - logger.warn( + sindri.logger.warn( `No project README was found at "${readmePath}", consider adding one.`, ); warningCount += 1; } else { - logger.debug(`README file found at "${readmePath}".`); + sindri.logger.debug(`README file found at "${readmePath}".`); } // Summarize the errors and warnings. if (errorCount === 0 && warningCount === 0) { - logger.info("No issues found, good job!"); + sindri.logger.info("No issues found, good job!"); } else { - logger.warn( + sindri.logger.warn( `Found ${errorCount + warningCount} problems ` + `(${errorCount} errors, ${warningCount} warnings).`, ); if (errorCount > 0) { - logger.error(`Linting failed with ${errorCount} errors.`); + sindri.logger.error(`Linting failed with ${errorCount} errors.`); return process.exit(1); } } diff --git a/src/cli/login.ts b/src/cli/login.ts index 15bacb4..91052f5 100644 --- a/src/cli/login.ts +++ b/src/cli/login.ts @@ -9,15 +9,9 @@ import { select, } from "@inquirer/prompts"; -import { - ApiError, - AuthorizationService, - InternalService, - OpenAPI, - TokenService, -} from "lib/api"; +import sindri from "lib"; +import { ApiError, type TeamMeResponse } from "lib/api"; import { Config } from "lib/config"; -import { logger } from "lib/logging"; export const loginCommand = new Command() .name("login") @@ -25,40 +19,37 @@ export const loginCommand = new Command() .option( "-u, --base-url ", "The base URL for the Sindri API. Mainly useful for development.", - OpenAPI.BASE, + "https://sindri.app", ) .action(async ({ baseUrl }) => { - // Check if they're already authenticated, and prompt for confirmation if so. const config = new Config(); + // Check if they're already authenticated, and prompt for confirmation if so. const auth = config.auth; - if (auth) { - let authenticated: boolean = false; + if (!auth) { + let teamMeResponse: TeamMeResponse | undefined; try { - const teamMeResult = await InternalService.teamMe(); - logger.debug("/api/v1/team/me/ response:"); - logger.debug(teamMeResult); - authenticated = true; + teamMeResponse = await sindri._client.internal.teamMe(); } catch (error) { if (error instanceof ApiError && error.status === 401) { - logger.warn( + sindri.logger.warn( "Existing credentials found, but invalid. Please continue logging in to update them.", ); } else { - logger.fatal("An unknown error occurred."); - logger.error(error); + sindri.logger.fatal("An unknown error occurred."); + sindri.logger.error(error); return process.exit(1); } } - if (authenticated) { + if (teamMeResponse) { const proceed = await confirm({ message: - `You are already logged in as ${auth.teamSlug} on ${auth.baseUrl}, ` + + `You are already logged in as ${teamMeResponse.team.slug} on ${sindri.baseUrl}, ` + "are you sure you want to proceed?", default: false, }); if (!proceed) { - logger.info("Aborting."); + sindri.logger.info("Aborting."); return; } } @@ -75,19 +66,16 @@ export const loginCommand = new Command() // Generate an API key for one of their teams. try { // Generate a JWT token to authenticate the user. - OpenAPI.BASE = baseUrl; - const tokenResult = await TokenService.bf740E1AControllerObtainToken({ - username, - password, - }); - logger.debug("/api/token/ response:"); - logger.debug(tokenResult); - OpenAPI.TOKEN = tokenResult.access; + sindri._clientConfig.BASE = baseUrl; + const tokenResult = + await sindri._client.token.fd3Aaa7BControllerObtainToken({ + username, + password, + }); + sindri._clientConfig.TOKEN = tokenResult.access; // Fetch their teams and have the user select one. - const userResult = await InternalService.userMeWithJwtAuth(); - logger.debug("/api/v1/user/me/ response:"); - logger.debug(userResult); + const userResult = await sindri._client.internal.userMeWithJwtAuth(); const teamId = await select({ message: "Select a Organization:", choices: userResult.teams.map(({ id, slug }) => ({ @@ -101,14 +89,12 @@ export const loginCommand = new Command() } // Generate an API key. - OpenAPI.HEADERS = { "Sindri-Team-Id": `${teamId}` }; - const apiKeyResult = await AuthorizationService.apikeyGenerate({ + sindri._clientConfig.HEADERS = { "Sindri-Team-Id": `${teamId}` }; + const apiKeyResult = await sindri._client.authorization.apikeyGenerate({ username, password, name, }); - logger.debug("/api/apikey/generate/ response:"); - logger.debug(apiKeyResult); const apiKey = apiKeyResult.api_key; const apiKeyId = apiKeyResult.id; const apiKeyName = apiKeyResult.name; @@ -127,12 +113,12 @@ export const loginCommand = new Command() teamSlug: team.slug, }, }); - logger.info( + sindri.logger.info( "You have successfully authorized the client with your Sindri account.", ); } catch (error) { - logger.fatal("An irrecoverable error occurred."); - logger.error(error); + sindri.logger.fatal("An irrecoverable error occurred."); + sindri.logger.error(error); process.exit(1); } }); diff --git a/src/cli/logout.ts b/src/cli/logout.ts index 59064a3..2ee279f 100644 --- a/src/cli/logout.ts +++ b/src/cli/logout.ts @@ -1,9 +1,8 @@ import { Command } from "@commander-js/extra-typings"; import { confirm } from "@inquirer/prompts"; -import { AuthorizationService } from "lib/api"; +import sindri from "lib"; import { Config } from "lib/config"; -import { logger } from "lib/logging"; export const logoutCommand = new Command() .name("logout") @@ -13,7 +12,7 @@ export const logoutCommand = new Command() const config = new Config(); const auth = config.auth; if (!auth) { - logger.error("You must log in first with `sindri login`."); + sindri.logger.error("You must log in first with `sindri login`."); return; } @@ -24,21 +23,19 @@ export const logoutCommand = new Command() }); if (revokeKey) { try { - const response = await AuthorizationService.apikeyDelete(auth.apiKeyId); - logger.info(`Successfully revoked "${auth.apiKeyName}" key.`); - logger.debug(`/api/v1/apikey/${auth.apiKeyId}/delete/ response:`); - logger.debug(response); + await sindri._client.authorization.apikeyDelete(auth.apiKeyId); + sindri.logger.info(`Successfully revoked "${auth.apiKeyName}" key.`); } catch (error) { - logger.warn( + sindri.logger.warn( `Error revoking "${auth.apiKeyName}" key, proceeding to clear credentials anyway.`, ); - logger.error(error); + sindri.logger.error(error); } } else { - logger.warn("Skipping revocation of existing key."); + sindri.logger.warn("Skipping revocation of existing key."); } // Clear the existing credentials. config.update({ auth: null }); - logger.info("You have successfully logged out."); + sindri.logger.info("You have successfully logged out."); }); diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 1001c20..d21dedf 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -7,7 +7,7 @@ import type { Schema } from "jsonschema"; import nunjucks from "nunjucks"; import type { PackageJson } from "type-fest"; -import { logger } from "lib/logging"; +import type { Logger } from "lib/logging"; const currentFilePath = fileURLToPath(import.meta.url); const currentDirectoryPath = path.dirname(currentFilePath); @@ -114,11 +114,13 @@ export function locatePackageJson(): string { * @param outputDirectory - The path to the output directory where the populated templates will be * written. * @param context - The nunjucks template context. + * @param logger - The logger to use for debug messages. */ export async function scaffoldDirectory( templateDirectory: string, outputDirectory: string, context: object, + logger?: Logger, ): Promise { // Normalize the paths and create the output directory if necessary. const fullOutputDirectory = path.resolve(outputDirectory); @@ -163,7 +165,7 @@ export async function scaffoldDirectory( // Ensure the output directory exists. if (!(await fileExists(outputPath))) { await mkdir(outputPath, { recursive: true }); - logger.debug(`Created directory: "${outputPath}"`); + logger?.debug(`Created directory: "${outputPath}"`); } if (!(await stat(outputPath)).isDirectory()) { throw new Error(`"File ${outputPath} exists and is not a directory.`); @@ -189,7 +191,7 @@ export async function scaffoldDirectory( const template = await readFile(inputPath, { encoding: "utf-8" }); const renderedTemplate = render(template, context); await writeFile(outputPath, renderedTemplate, { encoding: "utf-8" }); - logger.debug(`Rendered "${inputPath}" template to "${outputPath}".`); + logger?.debug(`Rendered "${inputPath}" template to "${outputPath}".`); }; await processPath(fullTemplateDirectory, fullOutputDirectory); } diff --git a/src/cli/whoami.ts b/src/cli/whoami.ts index 19b2542..37a1abf 100644 --- a/src/cli/whoami.ts +++ b/src/cli/whoami.ts @@ -2,32 +2,31 @@ import process from "process"; import { Command } from "@commander-js/extra-typings"; -import { ApiError, InternalService, OpenAPI } from "lib/api"; -import { logger, print } from "lib/logging"; +import sindri from "lib"; +import { ApiError } from "lib/api"; +import { print } from "lib/logging"; export const whoamiCommand = new Command() .name("whoami") .description("Display the currently authorized organization name.") .action(async () => { // Check that the API client is authorized. - if (!OpenAPI.TOKEN || !OpenAPI.BASE) { - logger.warn("You must login first with `sindri login`."); + if (!sindri.apiKey || !sindri.baseUrl) { + sindri.logger.warn("You must login first with `sindri login`."); return process.exit(1); } try { - const response = await InternalService.teamMe(); - logger.debug("/api/v1/team/me/ response:"); - logger.debug(response); + const response = await sindri._client.internal.teamMe(); print(response.team.slug); } catch (error) { if (error instanceof ApiError && error.status === 401) { - logger.error( + sindri.logger.error( "Your credentials are invalid. Please log in again with `sindri login`.", ); } else { - logger.fatal("An unknown error occurred."); - logger.error(error); + sindri.logger.fatal("An unknown error occurred."); + sindri.logger.error(error); return process.exit(1); } } diff --git a/src/lib/api/ApiClient.ts b/src/lib/api/ApiClient.ts new file mode 100644 index 0000000..ab68930 --- /dev/null +++ b/src/lib/api/ApiClient.ts @@ -0,0 +1,47 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BaseHttpRequest } from './core/BaseHttpRequest'; +import type { OpenAPIConfig } from './core/OpenAPI'; +import { AxiosHttpRequest } from './core/AxiosHttpRequest'; + +import { AuthorizationService } from './services/AuthorizationService'; +import { CircuitsService } from './services/CircuitsService'; +import { InternalService } from './services/InternalService'; +import { ProofsService } from './services/ProofsService'; +import { TokenService } from './services/TokenService'; + +type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; + +export class ApiClient { + + public readonly authorization: AuthorizationService; + public readonly circuits: CircuitsService; + public readonly internal: InternalService; + public readonly proofs: ProofsService; + public readonly token: TokenService; + + public readonly request: BaseHttpRequest; + + constructor(config?: Partial, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) { + this.request = new HttpRequest({ + BASE: config?.BASE ?? 'https://sindri.app', + VERSION: config?.VERSION ?? '1.6.7', + WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false, + CREDENTIALS: config?.CREDENTIALS ?? 'include', + TOKEN: config?.TOKEN, + USERNAME: config?.USERNAME, + PASSWORD: config?.PASSWORD, + HEADERS: config?.HEADERS, + ENCODE_PATH: config?.ENCODE_PATH, + }); + + this.authorization = new AuthorizationService(this.request); + this.circuits = new CircuitsService(this.request); + this.internal = new InternalService(this.request); + this.proofs = new ProofsService(this.request); + this.token = new TokenService(this.request); + } +} + diff --git a/src/lib/api/core/AxiosHttpRequest.ts b/src/lib/api/core/AxiosHttpRequest.ts new file mode 100644 index 0000000..f2bc926 --- /dev/null +++ b/src/lib/api/core/AxiosHttpRequest.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from "./ApiRequestOptions"; +import { BaseHttpRequest } from "./BaseHttpRequest"; +import type { CancelablePromise } from "./CancelablePromise"; +import type { OpenAPIConfig } from "./OpenAPI"; +import { request as __request } from "./request"; + +export class AxiosHttpRequest extends BaseHttpRequest { + constructor(config: OpenAPIConfig) { + super(config); + } + + /** + * Request method + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ + public override request(options: ApiRequestOptions): CancelablePromise { + return __request(this.config, options); + } +} diff --git a/src/lib/api/core/BaseHttpRequest.ts b/src/lib/api/core/BaseHttpRequest.ts new file mode 100644 index 0000000..f078115 --- /dev/null +++ b/src/lib/api/core/BaseHttpRequest.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from "./ApiRequestOptions"; +import type { CancelablePromise } from "./CancelablePromise"; +import type { OpenAPIConfig } from "./OpenAPI"; + +export abstract class BaseHttpRequest { + constructor(public readonly config: OpenAPIConfig) {} + + public abstract request(options: ApiRequestOptions): CancelablePromise; +} diff --git a/src/lib/api/core/OpenAPI.ts b/src/lib/api/core/OpenAPI.ts index a1dde9d..d7833fa 100644 --- a/src/lib/api/core/OpenAPI.ts +++ b/src/lib/api/core/OpenAPI.ts @@ -2,6 +2,8 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Logger } from "lib/logging"; // DO NOT REMOVE + import type { ApiRequestOptions } from "./ApiRequestOptions"; type Resolver = (options: ApiRequestOptions) => Promise; @@ -17,11 +19,15 @@ export type OpenAPIConfig = { PASSWORD?: string | Resolver | undefined; HEADERS?: Headers | Resolver | undefined; ENCODE_PATH?: ((path: string) => string) | undefined; + // DO NOT REMOVE + // Shoehorn the logger into the OpenAPIConfig type because it's the only shared data structure + // between the SDK client class and the request methods `requests.ts` module. + logger?: Logger; // DO NOT REMOVE }; export const OpenAPI: OpenAPIConfig = { BASE: "https://sindri.app", - VERSION: "1.5.40", + VERSION: "1.6.7", WITH_CREDENTIALS: false, CREDENTIALS: "include", TOKEN: undefined, diff --git a/src/lib/api/core/request.ts b/src/lib/api/core/request.ts index 4ea423e..93495b3 100644 --- a/src/lib/api/core/request.ts +++ b/src/lib/api/core/request.ts @@ -1,4 +1,10 @@ -/* generated using openapi-typescript-codegen -- do no edit */ +/* This file was originally generated by `openapi-typescript-codegen`, but we've customized it and + * it is no longer regenerated. The key changes: + * + * * Support pre-constructed `FormData` instances, and make `FormData` isomorphic. + * * Add request/response logging for all API requests in `request()`. + */ + /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ @@ -9,7 +15,8 @@ import type { AxiosResponse, AxiosInstance, } from "axios"; -import { FormData } from "lib/isomorphic"; // DO NOT REMOVE OR CHANGE THIS, MANUAL EDIT!!! +// Manual edit to use our isomorphic `FormData`. +import { FormData } from "lib/isomorphic"; import { ApiError } from "./ApiError"; import type { ApiRequestOptions } from "./ApiRequestOptions"; @@ -120,7 +127,6 @@ export const getFormData = ( ): FormData | undefined => { if (options.formData) { // This is a manual edit to allow `FormData` to be passed in directly. - // DO NOT REMOVE THIS! if (options.formData instanceof FormData) { return options.formData; } @@ -171,7 +177,7 @@ export const getHeaders = async ( const username = await resolve(options, config.USERNAME); const password = await resolve(options, config.PASSWORD); const additionalHeaders = await resolve(options, config.HEADERS); - // DO NOT REMOVE THIS, MANUAL EDIT! + // Manual edit to support `FormData` implementations that don't include `getHeaders()`. const formHeaders = (formData && "getHeaders" in formData && @@ -249,7 +255,8 @@ export const sendRequest = async ( onCancel(() => source.cancel("The user aborted a request.")); try { - return await axiosClient.request(requestConfig); + const response = await axiosClient.request(requestConfig); + return response; } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { @@ -332,13 +339,34 @@ export const request = ( axiosClient: AxiosInstance = axios, ): CancelablePromise => { return new CancelablePromise(async (resolve, reject, onCancel) => { + // Get a nicely formatted timedelta to display after requests. + const startTime = Date.now(); + const getElapsedTime = (): string => { + const ellapsedMilliseconds = Date.now() - startTime; + if (ellapsedMilliseconds < 1000) { + return `${ellapsedMilliseconds} ms`; + } + const ellapsedSeconds = ellapsedMilliseconds / 1000; + if (ellapsedSeconds < 60) { + return `${ellapsedSeconds.toFixed(2)} s`; + } + const ellapsedMinutes = ellapsedSeconds / 60; + if (ellapsedMinutes < 60) { + return `${ellapsedMinutes.toFixed(2)} m`; + } + const ellapsedHours = ellapsedMinutes / 60; + return `${ellapsedHours.toFixed(2)} h`; + }; + + const url = getUrl(config, options); + const logPrefix = `${options.method} ${url}`; try { - const url = getUrl(config, options); const formData = getFormData(options); const body = getRequestBody(options); const headers = await getHeaders(config, options, formData); if (!onCancel.isCancelled) { + config.logger?.debug(`${logPrefix} requested`); const response = await sendRequest( config, options, @@ -362,12 +390,27 @@ export const request = ( statusText: response.statusText, body: responseHeader ?? responseBody, }; + const responseMessage = `${logPrefix} ${response.status} ${ + response.statusText + } (${getElapsedTime()})`; + if (!result.body || typeof result.body === "string") { + config.logger?.debug( + `${responseMessage} - ${result.body || ""}`, + ); + } else { + config.logger?.debug(result.body, responseMessage); + } catchErrorCodes(options, result); resolve(result.body); } } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + config.logger?.debug( + `${logPrefix} ERROR (${getElapsedTime()}) - ${errorMessage}`, + ); reject(error); } }); diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index a9aa8c3..732a2ed 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -2,43 +2,47 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export { ApiError } from "./core/ApiError"; -export { CancelablePromise, CancelError } from "./core/CancelablePromise"; -export { OpenAPI } from "./core/OpenAPI"; -export type { OpenAPIConfig } from "./core/OpenAPI"; +export { ApiClient } from './ApiClient'; -export type { ActionResponse } from "./models/ActionResponse"; -export type { APIKeyDoesNotExistResponse } from "./models/APIKeyDoesNotExistResponse"; -export type { APIKeyErrorResponse } from "./models/APIKeyErrorResponse"; -export type { APIKeyResponse } from "./models/APIKeyResponse"; -export type { CircomCircuitInfoResponse } from "./models/CircomCircuitInfoResponse"; -export type { CircuitDoesNotExistResponse } from "./models/CircuitDoesNotExistResponse"; -export type { CircuitStatus } from "./models/CircuitStatus"; -export type { CircuitType } from "./models/CircuitType"; -export type { ComingSoonResponse } from "./models/ComingSoonResponse"; -export type { ForgeInternalErrorResponse } from "./models/ForgeInternalErrorResponse"; -export type { ForgeInvalidUploadResponse } from "./models/ForgeInvalidUploadResponse"; -export type { GnarkCircuitInfoResponse } from "./models/GnarkCircuitInfoResponse"; -export type { Halo2CircuitInfoResponse } from "./models/Halo2CircuitInfoResponse"; -export type { NoirCircuitInfoResponse } from "./models/NoirCircuitInfoResponse"; -export type { ObtainApikeyInput } from "./models/ObtainApikeyInput"; -export type { ProofCannotBeCreatedResponse } from "./models/ProofCannotBeCreatedResponse"; -export type { ProofDoesNotExistResponse } from "./models/ProofDoesNotExistResponse"; -export type { ProofInfoResponse } from "./models/ProofInfoResponse"; -export type { ProofStatus } from "./models/ProofStatus"; -export type { Schema } from "./models/Schema"; -export type { TeamDetail } from "./models/TeamDetail"; -export type { TeamMeResponse } from "./models/TeamMeResponse"; -export type { TokenObtainPairInputSchema } from "./models/TokenObtainPairInputSchema"; -export type { TokenObtainPairOutputSchema } from "./models/TokenObtainPairOutputSchema"; -export type { TokenRefreshInputSchema } from "./models/TokenRefreshInputSchema"; -export type { TokenRefreshOutputSchema } from "./models/TokenRefreshOutputSchema"; -export type { TokenVerifyInputSchema } from "./models/TokenVerifyInputSchema"; -export type { UserMeResponse } from "./models/UserMeResponse"; -export type { ValidationErrorResponse } from "./models/ValidationErrorResponse"; +export { ApiError } from './core/ApiError'; +export { BaseHttpRequest } from './core/BaseHttpRequest'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; -export { AuthorizationService } from "./services/AuthorizationService"; -export { CircuitsService } from "./services/CircuitsService"; -export { InternalService } from "./services/InternalService"; -export { ProofsService } from "./services/ProofsService"; -export { TokenService } from "./services/TokenService"; +export type { ActionResponse } from './models/ActionResponse'; +export type { APIKeyDoesNotExistResponse } from './models/APIKeyDoesNotExistResponse'; +export type { APIKeyErrorResponse } from './models/APIKeyErrorResponse'; +export type { APIKeyResponse } from './models/APIKeyResponse'; +export type { CircomCircuitInfoResponse } from './models/CircomCircuitInfoResponse'; +export type { CircuitDoesNotExistResponse } from './models/CircuitDoesNotExistResponse'; +export type { CircuitInfoResponse } from './models/CircuitInfoResponse'; +export type { CircuitType } from './models/CircuitType'; +export type { ComingSoonResponse } from './models/ComingSoonResponse'; +export type { ForgeInternalErrorResponse } from './models/ForgeInternalErrorResponse'; +export type { ForgeInvalidUploadResponse } from './models/ForgeInvalidUploadResponse'; +export type { ForgeValueErrorResponse } from './models/ForgeValueErrorResponse'; +export type { GnarkCircuitInfoResponse } from './models/GnarkCircuitInfoResponse'; +export type { Halo2CircuitInfoResponse } from './models/Halo2CircuitInfoResponse'; +export type { JobStatus } from './models/JobStatus'; +export type { NoirCircuitInfoResponse } from './models/NoirCircuitInfoResponse'; +export type { ObtainApikeyInput } from './models/ObtainApikeyInput'; +export type { ProofCannotBeCreatedResponse } from './models/ProofCannotBeCreatedResponse'; +export type { ProofDoesNotExistResponse } from './models/ProofDoesNotExistResponse'; +export type { ProofInfoResponse } from './models/ProofInfoResponse'; +export type { Schema } from './models/Schema'; +export type { TeamDetail } from './models/TeamDetail'; +export type { TeamMeResponse } from './models/TeamMeResponse'; +export type { TokenObtainPairInputSchema } from './models/TokenObtainPairInputSchema'; +export type { TokenObtainPairOutputSchema } from './models/TokenObtainPairOutputSchema'; +export type { TokenRefreshInputSchema } from './models/TokenRefreshInputSchema'; +export type { TokenRefreshOutputSchema } from './models/TokenRefreshOutputSchema'; +export type { TokenVerifyInputSchema } from './models/TokenVerifyInputSchema'; +export type { UserMeResponse } from './models/UserMeResponse'; +export type { ValidationErrorResponse } from './models/ValidationErrorResponse'; + +export { AuthorizationService } from './services/AuthorizationService'; +export { CircuitsService } from './services/CircuitsService'; +export { InternalService } from './services/InternalService'; +export { ProofsService } from './services/ProofsService'; +export { TokenService } from './services/TokenService'; diff --git a/src/lib/api/models/CircomCircuitInfoResponse.ts b/src/lib/api/models/CircomCircuitInfoResponse.ts index 2ba6714..7782e25 100644 --- a/src/lib/api/models/CircomCircuitInfoResponse.ts +++ b/src/lib/api/models/CircomCircuitInfoResponse.ts @@ -3,8 +3,7 @@ /* tslint:disable */ /* eslint-disable */ -import type { CircuitStatus } from "./CircuitStatus"; -import type { CircuitType } from "./CircuitType"; +import type { JobStatus } from "./JobStatus"; /** * Response for getting Circom circuit info. @@ -12,11 +11,11 @@ import type { CircuitType } from "./CircuitType"; export type CircomCircuitInfoResponse = { circuit_id: string; circuit_name: string; - circuit_type: CircuitType; + circuit_type: "circom"; date_created: string; num_proofs: number; proving_scheme: string; - status: CircuitStatus; + status: JobStatus; team: string; /** * Total compute time in ISO8601 format. This does not include the Queued time. diff --git a/src/lib/api/models/CircuitInfoResponse.ts b/src/lib/api/models/CircuitInfoResponse.ts new file mode 100644 index 0000000..cd94bd8 --- /dev/null +++ b/src/lib/api/models/CircuitInfoResponse.ts @@ -0,0 +1,18 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CircomCircuitInfoResponse } from "./CircomCircuitInfoResponse"; +import type { GnarkCircuitInfoResponse } from "./GnarkCircuitInfoResponse"; +import type { Halo2CircuitInfoResponse } from "./Halo2CircuitInfoResponse"; +import type { NoirCircuitInfoResponse } from "./NoirCircuitInfoResponse"; + +/** + * Response for getting circuit info. + */ +export type CircuitInfoResponse = + | CircomCircuitInfoResponse + | Halo2CircuitInfoResponse + | GnarkCircuitInfoResponse + | NoirCircuitInfoResponse; diff --git a/src/lib/api/models/CircuitStatus.ts b/src/lib/api/models/ForgeValueErrorResponse.ts similarity index 55% rename from src/lib/api/models/CircuitStatus.ts rename to src/lib/api/models/ForgeValueErrorResponse.ts index f5b5c41..9d32d00 100644 --- a/src/lib/api/models/CircuitStatus.ts +++ b/src/lib/api/models/ForgeValueErrorResponse.ts @@ -4,6 +4,9 @@ /* eslint-disable */ /** - * CircuitStatus choices + * Response for ForgeValueError */ -export type CircuitStatus = "Queued" | "In Progress" | "Ready" | "Failed"; +export type ForgeValueErrorResponse = { + error: string; + message?: string; +}; diff --git a/src/lib/api/models/GnarkCircuitInfoResponse.ts b/src/lib/api/models/GnarkCircuitInfoResponse.ts index 33568db..781d0d2 100644 --- a/src/lib/api/models/GnarkCircuitInfoResponse.ts +++ b/src/lib/api/models/GnarkCircuitInfoResponse.ts @@ -3,8 +3,7 @@ /* tslint:disable */ /* eslint-disable */ -import type { CircuitStatus } from "./CircuitStatus"; -import type { CircuitType } from "./CircuitType"; +import type { JobStatus } from "./JobStatus"; /** * Response for getting Gnark circuit info. @@ -12,11 +11,11 @@ import type { CircuitType } from "./CircuitType"; export type GnarkCircuitInfoResponse = { circuit_id: string; circuit_name: string; - circuit_type: CircuitType; + circuit_type: "gnark"; date_created: string; num_proofs: number; proving_scheme: string; - status: CircuitStatus; + status: JobStatus; team: string; /** * Total compute time in ISO8601 format. This does not include the Queued time. diff --git a/src/lib/api/models/Halo2CircuitInfoResponse.ts b/src/lib/api/models/Halo2CircuitInfoResponse.ts index c266246..cf66663 100644 --- a/src/lib/api/models/Halo2CircuitInfoResponse.ts +++ b/src/lib/api/models/Halo2CircuitInfoResponse.ts @@ -3,8 +3,7 @@ /* tslint:disable */ /* eslint-disable */ -import type { CircuitStatus } from "./CircuitStatus"; -import type { CircuitType } from "./CircuitType"; +import type { JobStatus } from "./JobStatus"; /** * Response for getting Halo2 circuit info. @@ -12,11 +11,11 @@ import type { CircuitType } from "./CircuitType"; export type Halo2CircuitInfoResponse = { circuit_id: string; circuit_name: string; - circuit_type: CircuitType; + circuit_type: "halo2"; date_created: string; num_proofs: number; proving_scheme: string; - status: CircuitStatus; + status: JobStatus; team: string; /** * Total compute time in ISO8601 format. This does not include the Queued time. diff --git a/src/lib/api/models/ProofStatus.ts b/src/lib/api/models/JobStatus.ts similarity index 59% rename from src/lib/api/models/ProofStatus.ts rename to src/lib/api/models/JobStatus.ts index 6908685..c226783 100644 --- a/src/lib/api/models/ProofStatus.ts +++ b/src/lib/api/models/JobStatus.ts @@ -4,6 +4,6 @@ /* eslint-disable */ /** - * ProofStatus choices + * JobStatus choices */ -export type ProofStatus = "Queued" | "In Progress" | "Ready" | "Failed"; +export type JobStatus = "Queued" | "In Progress" | "Ready" | "Failed"; diff --git a/src/lib/api/models/NoirCircuitInfoResponse.ts b/src/lib/api/models/NoirCircuitInfoResponse.ts index 26bd0d1..943509e 100644 --- a/src/lib/api/models/NoirCircuitInfoResponse.ts +++ b/src/lib/api/models/NoirCircuitInfoResponse.ts @@ -3,8 +3,7 @@ /* tslint:disable */ /* eslint-disable */ -import type { CircuitStatus } from "./CircuitStatus"; -import type { CircuitType } from "./CircuitType"; +import type { JobStatus } from "./JobStatus"; /** * Response for getting Noir circuit info. @@ -12,11 +11,11 @@ import type { CircuitType } from "./CircuitType"; export type NoirCircuitInfoResponse = { circuit_id: string; circuit_name: string; - circuit_type: CircuitType; + circuit_type: "noir"; date_created: string; num_proofs: number; proving_scheme: string; - status: CircuitStatus; + status: JobStatus; team: string; /** * Total compute time in ISO8601 format. This does not include the Queued time. @@ -37,4 +36,5 @@ export type NoirCircuitInfoResponse = { acir_opcodes?: number; circuit_size?: number; nargo_package_name: string; + noir_version: string; }; diff --git a/src/lib/api/models/ProofInfoResponse.ts b/src/lib/api/models/ProofInfoResponse.ts index 01e4203..0591001 100644 --- a/src/lib/api/models/ProofInfoResponse.ts +++ b/src/lib/api/models/ProofInfoResponse.ts @@ -4,7 +4,7 @@ /* eslint-disable */ import type { CircuitType } from "./CircuitType"; -import type { ProofStatus } from "./ProofStatus"; +import type { JobStatus } from "./JobStatus"; /** * Response for getting proof info. @@ -16,7 +16,7 @@ export type ProofInfoResponse = { circuit_type: CircuitType; date_created: string; perform_verify: boolean; - status: ProofStatus; + status: JobStatus; team: string; /** * Total compute time in ISO8601 format. This does not include the Queued time. diff --git a/src/lib/api/services/AuthorizationService.ts b/src/lib/api/services/AuthorizationService.ts index dde1047..f90dd38 100644 --- a/src/lib/api/services/AuthorizationService.ts +++ b/src/lib/api/services/AuthorizationService.ts @@ -7,10 +7,11 @@ import type { APIKeyResponse } from "../models/APIKeyResponse"; import type { ObtainApikeyInput } from "../models/ObtainApikeyInput"; import type { CancelablePromise } from "../core/CancelablePromise"; -import { OpenAPI } from "../core/OpenAPI"; -import { request as __request } from "../core/request"; +import type { BaseHttpRequest } from "../core/BaseHttpRequest"; export class AuthorizationService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** * Generate API Key * Generates a long-term API Key from your account's username and password. @@ -18,16 +19,17 @@ export class AuthorizationService { * @returns APIKeyResponse OK * @throws ApiError */ - public static apikeyGenerate( + public apikeyGenerate( requestBody: ObtainApikeyInput, ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/apikey/generate", body: requestBody, mediaType: "application/json", errors: { 401: `Unauthorized`, + 412: `Precondition Failed`, }, }); } @@ -39,15 +41,18 @@ export class AuthorizationService { * @returns APIKeyResponse Created * @throws ApiError */ - public static apikeyGenerateWithAuth( + public apikeyGenerateWithAuth( name: string = "", ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/v1/apikey/generate", query: { name: name, }, + errors: { + 412: `Precondition Failed`, + }, }); } @@ -57,8 +62,8 @@ export class AuthorizationService { * @returns APIKeyResponse OK * @throws ApiError */ - public static apikeyList(): CancelablePromise> { - return __request(OpenAPI, { + public apikeyList(): CancelablePromise> { + return this.httpRequest.request({ method: "GET", url: "/api/v1/apikey/list", errors: { @@ -74,10 +79,8 @@ export class AuthorizationService { * @returns ActionResponse OK * @throws ApiError */ - public static apikeyDelete( - apikeyId: string, - ): CancelablePromise { - return __request(OpenAPI, { + public apikeyDelete(apikeyId: string): CancelablePromise { + return this.httpRequest.request({ method: "DELETE", url: "/api/v1/apikey/{apikey_id}/delete", path: { diff --git a/src/lib/api/services/CircuitsService.ts b/src/lib/api/services/CircuitsService.ts index be1910c..1ae343a 100644 --- a/src/lib/api/services/CircuitsService.ts +++ b/src/lib/api/services/CircuitsService.ts @@ -2,28 +2,25 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import { FormData } from "lib/isomorphic"; // DO NOT REMOVE OR CHANGE THIS, MANUAL EDIT!!! import type { ActionResponse } from "../models/ActionResponse"; -import type { CircomCircuitInfoResponse } from "../models/CircomCircuitInfoResponse"; -import type { GnarkCircuitInfoResponse } from "../models/GnarkCircuitInfoResponse"; -import type { Halo2CircuitInfoResponse } from "../models/Halo2CircuitInfoResponse"; -import type { NoirCircuitInfoResponse } from "../models/NoirCircuitInfoResponse"; +import type { CircuitInfoResponse } from "../models/CircuitInfoResponse"; import type { ProofInfoResponse } from "../models/ProofInfoResponse"; import type { CancelablePromise } from "../core/CancelablePromise"; -import { OpenAPI } from "../core/OpenAPI"; -import { request as __request } from "../core/request"; +import type { BaseHttpRequest } from "../core/BaseHttpRequest"; export class CircuitsService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** * Create Circuit * Create a circuit. * @param formData - * @returns any Created + * @returns CircuitInfoResponse Created * @throws ApiError */ - public static circuitCreate( + public circuitCreate( formData: // This is a manual edit to allow `FormData` to be passed in directly: | FormData // DO NOT REMOVE THIS! | { @@ -33,13 +30,8 @@ export class CircuitsService { */ tags?: Array; }, - ): CancelablePromise< - | CircomCircuitInfoResponse - | Halo2CircuitInfoResponse - | GnarkCircuitInfoResponse - | NoirCircuitInfoResponse - > { - return __request(OpenAPI, { + ): CancelablePromise { + return this.httpRequest.request({ method: "POST", url: "/api/v1/circuit/create", formData: formData, @@ -57,20 +49,13 @@ export class CircuitsService { * Circuit List * Return a list of CircuitInfoResponse for circuits related to user. * @param includeVerificationKey - * @returns any OK + * @returns CircuitInfoResponse OK * @throws ApiError */ - public static circuitList( + public circuitList( includeVerificationKey: boolean = false, - ): CancelablePromise< - Array< - | CircomCircuitInfoResponse - | Halo2CircuitInfoResponse - | GnarkCircuitInfoResponse - | NoirCircuitInfoResponse - > - > { - return __request(OpenAPI, { + ): CancelablePromise> { + return this.httpRequest.request({ method: "GET", url: "/api/v1/circuit/list", query: { @@ -87,19 +72,14 @@ export class CircuitsService { * Get info for existing circuit * @param circuitId * @param includeVerificationKey - * @returns any OK + * @returns CircuitInfoResponse OK * @throws ApiError */ - public static circuitDetail( + public circuitDetail( circuitId: string, includeVerificationKey: boolean = true, - ): CancelablePromise< - | CircomCircuitInfoResponse - | Halo2CircuitInfoResponse - | GnarkCircuitInfoResponse - | NoirCircuitInfoResponse - > { - return __request(OpenAPI, { + ): CancelablePromise { + return this.httpRequest.request({ method: "GET", url: "/api/v1/circuit/{circuit_id}/detail", path: { @@ -122,10 +102,8 @@ export class CircuitsService { * @returns ActionResponse OK * @throws ApiError */ - public static circuitDelete( - circuitId: string, - ): CancelablePromise { - return __request(OpenAPI, { + public circuitDelete(circuitId: string): CancelablePromise { + return this.httpRequest.request({ method: "DELETE", url: "/api/v1/circuit/{circuit_id}/delete", path: { @@ -149,14 +127,14 @@ export class CircuitsService { * @returns ProofInfoResponse OK * @throws ApiError */ - public static circuitProofs( + public circuitProofs( circuitId: string, includeProofInput: boolean = false, includeProof: boolean = false, includePublic: boolean = false, includeVerificationKey: boolean = false, ): CancelablePromise> { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "GET", url: "/api/v1/circuit/{circuit_id}/proofs", path: { @@ -183,7 +161,7 @@ export class CircuitsService { * @returns ProofInfoResponse Created * @throws ApiError */ - public static proofCreate( + public proofCreate( circuitId: string, formData: { /** @@ -200,7 +178,7 @@ export class CircuitsService { prover_implementation?: string; }, ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/v1/circuit/{circuit_id}/prove", path: { diff --git a/src/lib/api/services/InternalService.ts b/src/lib/api/services/InternalService.ts index e8011b6..768e68b 100644 --- a/src/lib/api/services/InternalService.ts +++ b/src/lib/api/services/InternalService.ts @@ -7,10 +7,11 @@ import type { TeamMeResponse } from "../models/TeamMeResponse"; import type { UserMeResponse } from "../models/UserMeResponse"; import type { CancelablePromise } from "../core/CancelablePromise"; -import { OpenAPI } from "../core/OpenAPI"; -import { request as __request } from "../core/request"; +import type { BaseHttpRequest } from "../core/BaseHttpRequest"; export class InternalService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** * Change user password (requires JWT authentication) * Change password for a user. @@ -24,7 +25,7 @@ export class InternalService { * @returns ActionResponse OK * @throws ApiError */ - public static passwordChangeWithJwtAuth(formData: { + public passwordChangeWithJwtAuth(formData: { /** * Old password. */ @@ -34,7 +35,7 @@ export class InternalService { */ new_password: string; }): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/v1/password/change", formData: formData, @@ -51,8 +52,8 @@ export class InternalService { * @returns any OK * @throws ApiError */ - public static sindriManifestSchema(): CancelablePromise> { - return __request(OpenAPI, { + public sindriManifestSchema(): CancelablePromise> { + return this.httpRequest.request({ method: "GET", url: "/api/v1/sindri-manifest-schema.json", }); @@ -64,8 +65,8 @@ export class InternalService { * @returns TeamMeResponse OK * @throws ApiError */ - public static teamMe(): CancelablePromise { - return __request(OpenAPI, { + public teamMe(): CancelablePromise { + return this.httpRequest.request({ method: "GET", url: "/api/v1/team/me", }); @@ -81,8 +82,8 @@ export class InternalService { * @returns UserMeResponse OK * @throws ApiError */ - public static userMeWithJwtAuth(): CancelablePromise { - return __request(OpenAPI, { + public userMeWithJwtAuth(): CancelablePromise { + return this.httpRequest.request({ method: "GET", url: "/api/v1/user/me", }); diff --git a/src/lib/api/services/ProofsService.ts b/src/lib/api/services/ProofsService.ts index f3a07c0..7bcc70b 100644 --- a/src/lib/api/services/ProofsService.ts +++ b/src/lib/api/services/ProofsService.ts @@ -6,10 +6,11 @@ import type { ActionResponse } from "../models/ActionResponse"; import type { ProofInfoResponse } from "../models/ProofInfoResponse"; import type { CancelablePromise } from "../core/CancelablePromise"; -import { OpenAPI } from "../core/OpenAPI"; -import { request as __request } from "../core/request"; +import type { BaseHttpRequest } from "../core/BaseHttpRequest"; export class ProofsService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** * Proof List * Return list of ProofInfoResponse for proofs related to team. @@ -20,13 +21,13 @@ export class ProofsService { * @returns ProofInfoResponse OK * @throws ApiError */ - public static proofList( + public proofList( includeProofInput: boolean = false, includeProof: boolean = false, includePublic: boolean = false, includeVerificationKey: boolean = false, ): CancelablePromise> { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "GET", url: "/api/v1/proof/list", query: { @@ -52,14 +53,14 @@ export class ProofsService { * @returns ProofInfoResponse OK * @throws ApiError */ - public static proofDetail( + public proofDetail( proofId: string, includeProofInput: boolean = true, includeProof: boolean = true, includePublic: boolean = true, includeVerificationKey: boolean = true, ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "GET", url: "/api/v1/proof/{proof_id}/detail", path: { @@ -85,10 +86,8 @@ export class ProofsService { * @returns ActionResponse OK * @throws ApiError */ - public static proofDelete( - proofId: string, - ): CancelablePromise { - return __request(OpenAPI, { + public proofDelete(proofId: string): CancelablePromise { + return this.httpRequest.request({ method: "DELETE", url: "/api/v1/proof/{proof_id}/delete", path: { diff --git a/src/lib/api/services/TokenService.ts b/src/lib/api/services/TokenService.ts index 1054238..34a1eb0 100644 --- a/src/lib/api/services/TokenService.ts +++ b/src/lib/api/services/TokenService.ts @@ -10,20 +10,21 @@ import type { TokenRefreshOutputSchema } from "../models/TokenRefreshOutputSchem import type { TokenVerifyInputSchema } from "../models/TokenVerifyInputSchema"; import type { CancelablePromise } from "../core/CancelablePromise"; -import { OpenAPI } from "../core/OpenAPI"; -import { request as __request } from "../core/request"; +import type { BaseHttpRequest } from "../core/BaseHttpRequest"; export class TokenService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** * Obtain Token * @param requestBody * @returns TokenObtainPairOutputSchema OK * @throws ApiError */ - public static bf740E1AControllerObtainToken( + public fd3Aaa7BControllerObtainToken( requestBody: TokenObtainPairInputSchema, ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/token/pair", body: requestBody, @@ -37,10 +38,10 @@ export class TokenService { * @returns TokenRefreshOutputSchema OK * @throws ApiError */ - public static db93F15ControllerRefreshToken( + public b87E0720ControllerRefreshToken( requestBody: TokenRefreshInputSchema, ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/token/refresh", body: requestBody, @@ -54,10 +55,10 @@ export class TokenService { * @returns Schema OK * @throws ApiError */ - public static abc17FbControllerVerifyToken( + public d1C092ControllerVerifyToken( requestBody: TokenVerifyInputSchema, ): CancelablePromise { - return __request(OpenAPI, { + return this.httpRequest.request({ method: "POST", url: "/api/token/verify", body: requestBody, diff --git a/src/lib/client.ts b/src/lib/client.ts index 6b58859..5cb774b 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -7,14 +7,7 @@ import walk from "ignore-walk"; import tar from "tar"; import Tar from "tar-js"; -import { - CircuitsService, - CircuitStatus, - CircuitType, - OpenAPI, - ProofsService, - ProofStatus, -} from "lib/api"; +import { ApiClient, CircuitType, JobStatus, OpenAPIConfig } from "lib/api"; import type { CircomCircuitInfoResponse, Halo2CircuitInfoResponse, @@ -22,8 +15,8 @@ import type { NoirCircuitInfoResponse, ProofInfoResponse, } from "lib/api"; -import { loadConfig } from "lib/config"; -import { logger, LogLevel } from "lib/logging"; +import { Config } from "lib/config"; +import { createLogger, type Logger, type LogLevel } from "lib/logging"; import { File, FormData } from "lib/isomorphic"; import type { BrowserFile, @@ -33,11 +26,12 @@ import type { } from "lib/isomorphic"; // Re-export types from the API. -export { CircuitStatus, CircuitType, ProofStatus }; export type { CircomCircuitInfoResponse, + CircuitType, GnarkCircuitInfoResponse, Halo2CircuitInfoResponse, + JobStatus, NoirCircuitInfoResponse, ProofInfoResponse, }; @@ -47,6 +41,9 @@ export type CircuitInfoResponse = | GnarkCircuitInfoResponse | NoirCircuitInfoResponse; +// Re-export other internal types. +export type { Logger, LogLevel }; + /** * The options for authenticating with the API. */ @@ -80,6 +77,15 @@ export interface AuthOptions { * // Use the client to interact with the Sindri ZKP service... */ export class SindriClient { + /** @hidden */ + readonly _client: ApiClient; + /** @hidden */ + readonly _clientConfig: OpenAPIConfig; + /** @hidden */ + readonly _config: Config | undefined; + + readonly logger: Logger; + /** * Represents the polling interval in milliseconds used for querying the status of an endpoint. * This value determines the frequency at which the SDK polls an endpoint to check for any changes @@ -119,6 +125,13 @@ export class SindriClient { * @see {@link SindriClient.authorize} for information on retrieving this value. */ constructor(authOptions: AuthOptions = {}) { + this._client = new ApiClient(); + this._clientConfig = this._client.request.config; + this.logger = createLogger(); + if (!process.env.BROWSER_BUILD) { + this._config = new Config(this.logger); + } + this._clientConfig.logger = this.logger; this.authorize(authOptions); } @@ -142,10 +155,13 @@ export class SindriClient { * } */ get apiKey(): string | null { - if (OpenAPI.TOKEN && typeof OpenAPI.TOKEN !== "string") { + if ( + this._clientConfig.TOKEN && + typeof this._clientConfig.TOKEN !== "string" + ) { return null; } - return OpenAPI.TOKEN || null; + return this._clientConfig.TOKEN || null; } /** @@ -160,7 +176,7 @@ export class SindriClient { * console.log(`Current base URL: ${client.baseUrl}`); */ get baseUrl(): string { - return OpenAPI.BASE; + return this._clientConfig.BASE; } /** Retrieves the current log level of the client. The log level determines the verbosity of logs @@ -174,7 +190,7 @@ export class SindriClient { */ get logLevel(): LogLevel { // We don't specify any custom log levels, so we can narrow the type to exclude strings. - return logger.level as LogLevel; + return this.logger.level as LogLevel; } /** @@ -188,7 +204,8 @@ export class SindriClient { * client.logLevel = "debug"; */ set logLevel(level: LogLevel) { - logger.level = level; + this.logger.level = level; + this.logger.debug(`Set log level to "${this.logger.level}".`); } /** @@ -218,23 +235,44 @@ export class SindriClient { */ authorize(authOptions: AuthOptions): boolean { if (process.env.BROWSER_BUILD) { - OpenAPI.BASE = authOptions.baseUrl || "https://sindri.app"; - OpenAPI.TOKEN = authOptions.apiKey; + this._clientConfig.BASE = authOptions.baseUrl || "https://sindri.app"; + this._clientConfig.TOKEN = authOptions.apiKey; } else { - const config = loadConfig(); - OpenAPI.BASE = + this._config!.reload(); + this._clientConfig.BASE = authOptions.baseUrl || process.env.SINDRI_BASE_URL || - config.auth?.baseUrl || - OpenAPI.BASE || + this._config!.auth?.baseUrl || + this._clientConfig.BASE || "https://sindri.app"; - OpenAPI.TOKEN = - authOptions.apiKey || process.env.SINDRI_API_KEY || config.auth?.apiKey; + this._clientConfig.TOKEN = + authOptions.apiKey || + process.env.SINDRI_API_KEY || + this._config!.auth?.apiKey; } - return !!(OpenAPI.BASE && OpenAPI.TOKEN); + return !!(this._clientConfig.BASE && this._clientConfig.TOKEN); } - // }[tags=["latest"]] + /** + * Creates a new {@link SindriClient} client instance. The class itself is not exported, so use + * this method on the exported (or any other) client instance to create a new instance. The new + * instance can be configured and used completely independently from any other instances. For + * example it can use different credentials or a different log level. + * + * @param authOptions - The authentication options for the client, including + * credentials like API keys or tokens. Defaults to an empty object if not provided. + * + * @example + * import sindri from 'sindri'; + * + * // Equivalent to: const myClient = new SindriClient({ ... }); + * const myClient = sindri.create({ apiKey: 'sindri-mykey-1234'}); + * + * @returns The new client instance. + */ + create(authOptions: AuthOptions | undefined): SindriClient { + return new SindriClient(authOptions); + } /** * Asynchronously creates and deploys a new circuit, initiating its compilation process. This @@ -371,7 +409,7 @@ export class SindriClient { cwd: project, gzip: true, onwarn: (code: string, message: string) => { - logger.warn(`While creating tarball: ${code} - ${message}`); + this.logger.warn(`While creating tarball: ${code} - ${message}`); }, prefix: `${circuitName}/`, sync: true, @@ -443,21 +481,21 @@ export class SindriClient { // Note that it's import the boundary matches the Chrome format because the test runner checks // payloads for this format in order to compare non-deterministic gzips. // TODO: These header changes are global, we need to make them local to this request. - const oldHeaders = OpenAPI.HEADERS; - OpenAPI.HEADERS = { + const oldHeaders = this._clientConfig.HEADERS; + this._clientConfig.HEADERS = { "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundary0buQ8d6EhWcs9X9d", }; - const createResponsePromise = CircuitsService.circuitCreate( + const createResponsePromise = this._client.circuits.circuitCreate( formData as NodeFormData, ); const createResponse = await createResponsePromise; - OpenAPI.HEADERS = oldHeaders; + this._clientConfig.HEADERS = oldHeaders; const circuitId = createResponse.circuit_id; let response: CircuitInfoResponse; while (true) { - response = await CircuitsService.circuitDetail(circuitId, false); + response = await this._client.circuits.circuitDetail(circuitId, false); if (response.status === "Ready" || response.status === "Failed") { break; } @@ -488,7 +526,7 @@ export class SindriClient { * console.log("Proofs:', proofs); */ async getAllCircuitProofs(circuitId: string): Promise { - return await CircuitsService.circuitProofs(circuitId); + return await this._client.circuits.circuitProofs(circuitId); } /** @@ -504,7 +542,7 @@ export class SindriClient { * console.log("Circuits:", circuits); */ async getAllCircuits(): Promise { - return await CircuitsService.circuitList(); + return await this._client.circuits.circuitList(); } /** @@ -524,7 +562,7 @@ export class SindriClient { * console.log("How many proofs?", proofs.length); */ async getAllProofs(): Promise { - return await ProofsService.proofList(); + return await this._client.proofs.proofList(); } /** @@ -544,7 +582,7 @@ export class SindriClient { * console.log('Circuit details:', circuit); */ async getCircuit(circuitId: string): Promise { - return await CircuitsService.circuitDetail(circuitId); + return await this._client.circuits.circuitDetail(circuitId); } /** @@ -564,7 +602,7 @@ export class SindriClient { * console.log("Proof details:", proof); */ async getProof(proofId: string): Promise { - return await ProofsService.proofDetail(proofId); + return await this._client.proofs.proofDetail(proofId); } /** @@ -591,12 +629,12 @@ export class SindriClient { circuitId: string, proofInput: string, ): Promise { - const createResponse = await CircuitsService.proofCreate(circuitId, { + const createResponse = await this._client.circuits.proofCreate(circuitId, { proof_input: proofInput, }); let response: ProofInfoResponse; while (true) { - response = await ProofsService.proofDetail(createResponse.proof_id); + response = await this._client.proofs.proofDetail(createResponse.proof_id); if (response.status === "Ready" || response.status === "Failed") { break; } diff --git a/src/lib/config.ts b/src/lib/config.ts index b2551df..a795f15 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -5,8 +5,7 @@ import envPaths from "env-paths"; import _ from "lodash"; import { z } from "zod"; -import { OpenAPI } from "lib/api"; -import { logger } from "lib/logging"; +import { type Logger } from "lib/logging"; const getConfigPath = (): string => { const paths = envPaths("sindri", { @@ -34,26 +33,26 @@ type ConfigSchema = z.infer; const defaultConfig: ConfigSchema = ConfigSchema.parse({}); -export const loadConfig = (): ConfigSchema => { +export const loadConfig = (logger?: Logger): ConfigSchema => { const configPath = getConfigPath(); if (fs.existsSync(configPath)) { - logger.debug(`Loading config from "${configPath}".`); + logger?.debug(`Loading config from "${configPath}".`); try { const configFileContents: string = fs.readFileSync(configPath, { encoding: "utf-8", }); const loadedConfig = ConfigSchema.parse(JSON.parse(configFileContents)); - logger.debug("Config loaded successfully."); + logger?.debug("Config loaded successfully."); return loadedConfig; } catch (error) { - logger.warn( + logger?.warn( `The config schema in "${configPath}" is invalid and will not be used.\n` + `To remove it and start fresh, run:\n rm ${configPath}`, ); - logger.debug(error); + logger?.debug(error); } } - logger.debug( + logger?.debug( `Config file "${configPath}" does not exist, initializing default config.`, ); return _.cloneDeep(defaultConfig); @@ -61,19 +60,11 @@ export const loadConfig = (): ConfigSchema => { export class Config { protected _config!: ConfigSchema; - protected static instance: Config; + protected readonly logger: Logger | undefined; - constructor() { - if (!Config.instance) { - this._config = loadConfig(); - Config.instance = this; - // Prepare API the client with the loaded credentials. - if (this._config.auth) { - OpenAPI.BASE = this._config.auth.baseUrl; - OpenAPI.TOKEN = this._config.auth.apiKey; - } - } - return Config.instance; + constructor(logger?: Logger) { + this.logger = logger; + this.reload(); } get auth(): ConfigSchema["auth"] { @@ -84,10 +75,14 @@ export class Config { return _.cloneDeep(this._config); } + reload() { + this._config = loadConfig(this.logger); + } + update(configData: Partial) { // Merge and validate the configs. - logger.debug("Merging in config update:"); - logger.debug(configData); + this.logger?.debug("Merging in config update:"); + this.logger?.debug(configData); const newConfig: ConfigSchema = _.cloneDeep(this._config); _.merge(newConfig, configData); this._config = ConfigSchema.parse(newConfig); @@ -100,7 +95,10 @@ export class Config { } // Write out the new config. - logger.debug(`Writing merged config to "${configPath}":`, this._config); + this.logger?.debug( + `Writing merged config to "${configPath}":`, + this._config, + ); fs.writeFileSync(configPath, JSON.stringify(this._config, null, 2), { encoding: "utf-8", }); diff --git a/src/lib/logging.ts b/src/lib/logging.ts index 1e383db..abec978 100644 --- a/src/lib/logging.ts +++ b/src/lib/logging.ts @@ -1,6 +1,8 @@ -import pino from "pino"; +import pino, { type BaseLogger as Logger } from "pino"; import pretty from "pino-pretty"; +export type { Logger }; + /** * The minimum log level to print. */ @@ -13,20 +15,24 @@ export type LogLevel = | "debug" | "trace"; -export const logger = pino( - process.env.BROWSER_BUILD - ? { - browser: { asObject: true }, - } - : pretty({ - colorize: true, - destination: 2, - ignore: "hostname,pid", - levelFirst: false, - sync: true, - }), -); - -logger.level = process.env.NODE_ENV === "production" ? "silent" : "info"; +export const createLogger = (level?: LogLevel): Logger => { + const logger = pino( + process.env.BROWSER_BUILD + ? { + browser: { asObject: true }, + } + : pretty({ + colorize: true, + destination: 2, + ignore: "hostname,pid", + levelFirst: false, + sync: true, + }), + ); + logger.level = + level ?? process.env.NODE_ENV === "production" ? "silent" : "info"; + return logger; +}; +export const logger = createLogger(); export const print = console.log;