diff --git a/.gitignore b/.gitignore index 834336bcf..9d4a1a157 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,10 @@ Thumbs.db .env.production .env.test +config/* +!config/*.example.yaml +!config/*.template.yaml + /rego-build /apps/devtool/storage.json diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..2312dc587 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/.prettierignore b/.prettierignore index 8ef09cc98..3399450d0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ dist/* .nx/cache/* deploy/charts/* /.nx/workspace-data +.next/* # Generated code packages/armory-sdk/src/http/client diff --git a/.prettierrc b/.prettierrc index c42708dec..73f58d72c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,5 +6,13 @@ "singleQuote": true, "tabWidth": 2, "trailingComma": "none", - "useTabs": false + "useTabs": false, + "overrides": [ + { + "files": ["*.yml", "*.yaml"], + "options": { + "quoteProps": "preserve" + } + } + ] } diff --git a/apps/armory/Makefile b/apps/armory/Makefile index 697e9cc49..3a00927be 100644 --- a/apps/armory/Makefile +++ b/apps/armory/Makefile @@ -30,13 +30,15 @@ armory/build: # === Code format === armory/format: - npx nx format:write --projects ${ARMORY_PROJECT_NAME} + npx nx format:write --projects ${ARMORY_PROJECT_NAME} + npx prisma format --schema ${ARMORY_DATABASE_SCHEMA} armory/lint: npx nx lint ${ARMORY_PROJECT_NAME} -- --fix armory/format/check: - npx nx format:check --projects ${ARMORY_PROJECT_NAME} + npx nx format:check --projects ${ARMORY_PROJECT_NAME} + npx prisma format --schema ${ARMORY_DATABASE_SCHEMA} armory/lint/check: npx nx lint ${ARMORY_PROJECT_NAME} diff --git a/apps/armory/src/armory.constant.ts b/apps/armory/src/armory.constant.ts index 5ce9a7fbe..a5b5c47bb 100644 --- a/apps/armory/src/armory.constant.ts +++ b/apps/armory/src/armory.constant.ts @@ -1,10 +1,3 @@ -import { - REQUEST_HEADER_CLIENT_ID, - REQUEST_HEADER_CLIENT_SECRET, - adminApiKeySecurity, - clientIdSecurity, - clientSecretSecurity -} from '@narval/nestjs-shared' import { AssetId } from '@narval/policy-engine-shared' import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common' import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core' @@ -51,20 +44,6 @@ export const DEFAULT_HTTP_MODULE_PROVIDERS = [ ...HTTP_VALIDATION_PIPES ] -// -// Headers -// - -export const REQUEST_HEADER_API_KEY = 'x-api-key' - -// -// API Security -// - -export const ADMIN_SECURITY = adminApiKeySecurity(REQUEST_HEADER_API_KEY) -export const CLIENT_ID_SECURITY = clientIdSecurity(REQUEST_HEADER_CLIENT_ID) -export const CLIENT_SECRET_SECURITY = clientSecretSecurity(REQUEST_HEADER_CLIENT_SECRET) - // // Queues // diff --git a/apps/armory/src/client/__test__/e2e/client.spec.ts b/apps/armory/src/client/__test__/e2e/client.spec.ts index a7054a3b0..6c141cc73 100644 --- a/apps/armory/src/client/__test__/e2e/client.spec.ts +++ b/apps/armory/src/client/__test__/e2e/client.spec.ts @@ -1,7 +1,7 @@ import { ConfigModule, ConfigService } from '@narval/config-module' -import { LoggerModule, OpenTelemetryModule, secret } from '@narval/nestjs-shared' -import { DataStoreConfiguration, HttpSource, PublicClient, Source, SourceType } from '@narval/policy-engine-shared' -import { getPublicKey, privateKeyToJwk } from '@narval/signature' +import { LoggerModule, OpenTelemetryModule, REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' +import { DataStoreConfiguration, HttpSource, Source, SourceType } from '@narval/policy-engine-shared' +import { SigningAlg, getPublicKey, privateKeyToJwk } from '@narval/signature' import { HttpStatus, INestApplication } from '@nestjs/common' import { Test, TestingModule } from '@nestjs/testing' import nock from 'nock' @@ -9,10 +9,10 @@ import request from 'supertest' import { generatePrivateKey } from 'viem/accounts' import { AppService } from '../../../app/core/service/app.service' import { Config, load } from '../../../armory.config' -import { REQUEST_HEADER_API_KEY } from '../../../armory.constant' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' import { ClientModule } from '../../client.module' import { ClientService } from '../../core/service/client.service' +import { PolicyEnginePublicClient } from '../../core/type/client.type' import { CreateClientRequestDto } from '../../http/rest/dto/create-client.dto' // TODO: (@wcalderipe, 16/05/24) Evaluate testcontainers @@ -32,13 +32,26 @@ const mockPolicyEngineServer = (url: string, clientId: string) => { keys: [getPublicKey(privateKeyToJwk(generatePrivateKey()))] } - const createClientResponse: PublicClient = { + const createClientResponse: PolicyEnginePublicClient = { clientId, - clientSecret: secret.generate(), + name: 'Acme', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: secret.generate() + } + }, createdAt: new Date(), updatedAt: new Date(), - signer: { - publicKey: getPublicKey(privateKeyToJwk(generatePrivateKey())) + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'acme-key-ie', + publicKey: getPublicKey(privateKeyToJwk(generatePrivateKey())) + } }, dataStore: { entity: dataStoreConfig, @@ -135,7 +148,7 @@ describe('Client', () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientPayload) const actualClient = await clientService.findById(body.id) @@ -171,7 +184,7 @@ describe('Client', () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientWithGivenPolicyEngine) const actualClient = await clientService.findById(body.id) @@ -187,7 +200,7 @@ describe('Client', () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({ ...createClientPayload, clientSecret }) const actualClient = await clientService.findById(body.id) @@ -201,7 +214,7 @@ describe('Client', () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientPayload) const actualClient = await clientService.findById(body.id) @@ -215,7 +228,7 @@ describe('Client', () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({ ...createClientPayload, useManagedDataStore: true }) const actualClient = await clientService.findById(body.id) @@ -240,7 +253,7 @@ describe('Client', () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({ ...createClientPayload, useManagedDataStore: true, @@ -277,7 +290,7 @@ describe('Client', () => { it('responds with unprocessable entity when payload is invalid', async () => { const { status } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({}) expect(status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY) @@ -286,7 +299,7 @@ describe('Client', () => { it('responds with forbidden when admin api key is invalid', async () => { const { status } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, 'invalid-admin-api-key') + .set(REQUEST_HEADER_ADMIN_API_KEY, 'invalid-admin-api-key') .send({}) expect(status).toEqual(HttpStatus.FORBIDDEN) diff --git a/apps/armory/src/client/core/type/client.type.ts b/apps/armory/src/client/core/type/client.type.ts index f474d9003..8982f467a 100644 --- a/apps/armory/src/client/core/type/client.type.ts +++ b/apps/armory/src/client/core/type/client.type.ts @@ -1,5 +1,5 @@ import { DataStoreConfiguration } from '@narval/policy-engine-shared' -import { jwkSchema, publicKeySchema } from '@narval/signature' +import { jwkSchema, publicKeySchema, SigningAlg } from '@narval/signature' import { z } from 'zod' export const PolicyEngineNode = z.object({ @@ -61,3 +61,38 @@ export const PublicClient = Client.extend({ }) }) export type PublicClient = z.infer + +export const PolicyEnginePublicClient = z.object({ + clientId: z.string(), + name: z.string(), + configurationSource: z.literal('declarative').or(z.literal('dynamic')), // Declarative = comes from config file, Dynamic = created at runtime + baseUrl: z.string().nullable(), + + auth: z.object({ + disabled: z.boolean(), + local: z + .object({ + clientSecret: z.string().nullable() + }) + .nullable() + }), + + dataStore: z.object({ + entity: DataStoreConfiguration, + policy: DataStoreConfiguration + }), + + decisionAttestation: z.object({ + disabled: z.boolean(), + signer: z + .object({ + alg: z.nativeEnum(SigningAlg), + keyId: z.string().nullable().describe('Unique id of the signer key. Matches the kid in both jwks'), + publicKey: publicKeySchema.optional() + }) + .nullable() + }), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() +}) +export type PolicyEnginePublicClient = z.infer diff --git a/apps/armory/src/main.ts b/apps/armory/src/main.ts index a7175923e..ffe992159 100644 --- a/apps/armory/src/main.ts +++ b/apps/armory/src/main.ts @@ -7,14 +7,20 @@ import { instrumentTelemetry } from '@narval/open-telemetry' instrumentTelemetry({ serviceName: 'armory' }) import { ConfigService } from '@narval/config-module' -import { LoggerService, withApiVersion, withCors, withLogger, withSwagger } from '@narval/nestjs-shared' +import { + LoggerService, + securityOptions, + withApiVersion, + withCors, + withLogger, + withSwagger +} from '@narval/nestjs-shared' import { ClassSerializerInterceptor, INestApplication, ValidationPipe } from '@nestjs/common' import { NestFactory, Reflector } from '@nestjs/core' import compression from 'compression' import { json } from 'express' import { lastValueFrom, map, of, switchMap } from 'rxjs' import { Config } from './armory.config' -import { ADMIN_SECURITY, CLIENT_ID_SECURITY, CLIENT_SECRET_SECURITY } from './armory.constant' import { ArmoryModule } from './armory.module' import { ApplicationExceptionFilter } from './shared/filter/application-exception.filter' import { HttpExceptionFilter } from './shared/filter/http-exception.filter' @@ -103,7 +109,7 @@ async function bootstrap(): Promise { title: 'Armory', description: 'Authentication and authorization system for web3.0', version: '1.0', - security: [ADMIN_SECURITY, CLIENT_ID_SECURITY, CLIENT_SECRET_SECURITY] + security: [securityOptions.clientId, securityOptions.clientSecret, securityOptions.adminApiKey] }) ), switchMap((app) => app.listen(port)) diff --git a/apps/armory/src/orchestration/__test__/e2e/authorization-request.spec.ts b/apps/armory/src/orchestration/__test__/e2e/authorization-request.spec.ts index 2da92b3b5..e91a54619 100644 --- a/apps/armory/src/orchestration/__test__/e2e/authorization-request.spec.ts +++ b/apps/armory/src/orchestration/__test__/e2e/authorization-request.spec.ts @@ -409,7 +409,6 @@ describe('Authorization Request', () => { idempotencyKey: '8dcbb7ad-82a2-4eca-b2f0-b1415c1d4a17', evaluations: [], approvals: [], - errors: [], createdAt: new Date(), updatedAt: new Date() } diff --git a/apps/armory/src/orchestration/http/rest/util.ts b/apps/armory/src/orchestration/http/rest/util.ts index ea3644eaf..d02d884f5 100644 --- a/apps/armory/src/orchestration/http/rest/util.ts +++ b/apps/armory/src/orchestration/http/rest/util.ts @@ -17,8 +17,10 @@ export const toCreateAuthorizationRequest = ( const authentication = dto.authentication const approvals = dto.approvals || [] const audience = dto.metadata?.audience + const confirmation = dto.metadata?.confirmation const metadata = { ...(audience && { audience }), + ...(confirmation && { confirmation }), expiresIn: dto.metadata?.expiresIn || TEN_MINUTES, issuedAt: nowSeconds(), issuer: `${clientId}.armory.narval.xyz` diff --git a/apps/armory/src/orchestration/persistence/decode/authorization-request.decode.ts b/apps/armory/src/orchestration/persistence/decode/authorization-request.decode.ts index 607173a35..9ec4db4e9 100644 --- a/apps/armory/src/orchestration/persistence/decode/authorization-request.decode.ts +++ b/apps/armory/src/orchestration/persistence/decode/authorization-request.decode.ts @@ -4,12 +4,12 @@ import { Approvals, AuthorizationRequest, AuthorizationRequestError, + AuthorizationRequestMetadata, Evaluation } from '@narval/policy-engine-shared' import { ApprovalRequirement as ApprovalRequirementModel, - AuthorizationRequestError as AuthorizationRequestErrorModel, - Prisma + AuthorizationRequestError as AuthorizationRequestErrorModel } from '@prisma/client/armory' import { ZodIssueCode, ZodSchema, z } from 'zod' import { ACTION_REQUEST } from '../../orchestration.constant' @@ -33,10 +33,18 @@ const buildSharedAttributes = (model: AuthorizationRequestModel): Omit !error).map((approval) => approval.sig)), evaluations: (model.evaluationLog || []).map(decodeEvaluationLog), - metadata: model.metadata as Prisma.InputJsonObject, - errors: (model.errors || []).map(buildError), createdAt: model.createdAt, - updatedAt: model.updatedAt + updatedAt: model.updatedAt, + ...(model.errors && model.errors.length + ? { + errors: model.errors.map(buildError) + } + : {}), + ...(model.metadata + ? { + metadata: AuthorizationRequestMetadata.parse(model.metadata) + } + : {}) } } diff --git a/apps/armory/src/orchestration/persistence/repository/__test__/integration/authorization-request.repository.spec.ts b/apps/armory/src/orchestration/persistence/repository/__test__/integration/authorization-request.repository.spec.ts index 9b755d9d8..b3c8c795d 100644 --- a/apps/armory/src/orchestration/persistence/repository/__test__/integration/authorization-request.repository.spec.ts +++ b/apps/armory/src/orchestration/persistence/repository/__test__/integration/authorization-request.repository.spec.ts @@ -9,6 +9,7 @@ import { FIXTURE, SignTransaction } from '@narval/policy-engine-shared' +import { Alg, generateJwk, getPublicKey, hash, signJwt } from '@narval/signature' import { Test, TestingModule } from '@nestjs/testing' import { AuthorizationRequestStatus, Client, Prisma } from '@prisma/client/armory' import { omit } from 'lodash/fp' @@ -35,7 +36,7 @@ describe(AuthorizationRequestRepository.name, () => { const authentication = '0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c' - const signMessageRequest: AuthorizationRequest = { + const baseAuthzRequest: AuthorizationRequest = { authentication, id: '6c7e92fc-d2b0-4840-8e9b-485393ecdf89', clientId: client.id, @@ -96,24 +97,24 @@ describe(AuthorizationRequestRepository.name, () => { describe('create', () => { it('creates a new authorization request', async () => { - await repository.create(signMessageRequest) + await repository.create(baseAuthzRequest) const request = await testPrismaService.getClient().authorizationRequest.findFirst({ where: { - id: signMessageRequest.id + id: baseAuthzRequest.id } }) - expect(request).toMatchObject(omit(['evaluations', 'approvals', 'authentication'], signMessageRequest)) + expect(request).toMatchObject(omit(['evaluations', 'approvals', 'authentication'], baseAuthzRequest)) expect(request?.authnSig).toEqual(authentication) }) it('defaults status to CREATED', async () => { - await repository.create(omit('status', signMessageRequest)) + await repository.create(omit('status', baseAuthzRequest)) const request = await testPrismaService.getClient().authorizationRequest.findFirst({ where: { - id: signMessageRequest.id + id: baseAuthzRequest.id } }) @@ -154,7 +155,7 @@ describe(AuthorizationRequestRepository.name, () => { } const { evaluations } = await repository.create({ - ...signMessageRequest, + ...baseAuthzRequest, evaluations: [permit] }) @@ -165,20 +166,20 @@ describe(AuthorizationRequestRepository.name, () => { const approval = 'test-signature' await repository.create({ - ...signMessageRequest, + ...baseAuthzRequest, approvals: [approval] }) const approvals = await testPrismaService.getClient().authorizationRequestApproval.findMany({ where: { - requestId: signMessageRequest.id + requestId: baseAuthzRequest.id } }) expect(approvals.map(omit(['id', 'createdAt', 'error']))).toEqual([ { sig: approval, - requestId: signMessageRequest.id + requestId: baseAuthzRequest.id } ]) }) @@ -191,19 +192,48 @@ describe(AuthorizationRequestRepository.name, () => { } await repository.create({ - ...signMessageRequest, + ...baseAuthzRequest, errors: [error] }) const errors = await testPrismaService.getClient().authorizationRequestError.findMany({ where: { - requestId: signMessageRequest.id + requestId: baseAuthzRequest.id } }) expect(errors.map(omit(['createdAt', 'clientId', 'requestId']))).toEqual([error]) }) + it('creates request with confirmation claim metadata', async () => { + const privateKey = await generateJwk(Alg.EDDSA) + + const authzRequest: AuthorizationRequest = { + ...baseAuthzRequest, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(privateKey), + proof: 'jws', + jws: await signJwt({ requestHash: hash({ it: 'does not matter here' }) }, privateKey) + } + } + }, + request: { + action: Action.SIGN_MESSAGE, + nonce: '99', + resourceId: '3be0c61d-9b41-423f-80b8-ea6f7624d917', + message: 'sign me!' + } + } + + await repository.create(authzRequest) + + const createdAuthzRequest = await repository.findById(authzRequest.id) + + expect(createdAuthzRequest).toEqual(authzRequest) + }) + describe(`when action is ${Action.SIGN_TRANSACTION}`, () => { const signTransaction: SignTransaction = { action: Action.SIGN_TRANSACTION, @@ -218,7 +248,7 @@ describe(AuthorizationRequestRepository.name, () => { } const signTransactionRequest: AuthorizationRequest = { - ...signMessageRequest, + ...baseAuthzRequest, request: signTransaction } @@ -231,23 +261,115 @@ describe(AuthorizationRequestRepository.name, () => { if (authzRequest && authzRequest.request.action === Action.SIGN_TRANSACTION) { expect(authzRequest?.request.transactionRequest.gas).toEqual(signTransaction.transactionRequest.gas) + } else { + fail(`expect authorization request action equals to ${Action.SIGN_TRANSACTION}`) + } + }) + }) + + describe(`when action is ${Action.SIGN_MESSAGE}`, () => { + const authzRequest: AuthorizationRequest = { + ...baseAuthzRequest, + request: { + action: Action.SIGN_MESSAGE, + nonce: '99', + resourceId: '3be0c61d-9b41-423f-80b8-ea6f7624d917', + message: 'sign me!' + } + } + + it('decodes request correctly', async () => { + await repository.create(authzRequest) + + const createdAuthzRequest = await repository.findById(authzRequest.id) + + expect(createdAuthzRequest).toEqual(authzRequest) + }) + }) + + describe(`when action is ${Action.SIGN_RAW}`, () => { + const authzRequest: AuthorizationRequest = { + ...baseAuthzRequest, + request: { + action: Action.SIGN_RAW, + nonce: '99', + resourceId: '3be0c61d-9b41-423f-80b8-ea6f7624d917', + rawMessage: '0x123' } + } + + it('decodes correctly', async () => { + await repository.create(authzRequest) + + const createdAuthzRequest = await repository.findById(authzRequest.id) + + expect(createdAuthzRequest).toEqual(authzRequest) + }) + }) + + describe(`when action is ${Action.SIGN_TYPED_DATA}`, () => { + const authzRequest: AuthorizationRequest = { + ...baseAuthzRequest, + request: { + action: Action.SIGN_TYPED_DATA, + nonce: '99', + resourceId: '3be0c61d-9b41-423f-80b8-ea6f7624d917', + typedData: { + domain: { + name: 'Ether Mail', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' + }, + types: { + Person: [ + { name: 'name', type: 'string' }, + { name: 'account', type: 'address' } + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' } + ] + }, + primaryType: 'Mail', + message: { + from: { + name: 'Cow', + account: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' + }, + to: { + name: 'Bob', + account: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' + }, + contents: 'Hello, Bob!' + } + } + } + } + + it('decodes request correctly', async () => { + await repository.create(authzRequest) + + const createdAuthzRequest = await repository.findById(authzRequest.id) + + expect(createdAuthzRequest).toEqual(authzRequest) }) }) }) describe('update', () => { beforeEach(async () => { - await repository.create(signMessageRequest) + await repository.create(baseAuthzRequest) }) it('updates status', async () => { const authzRequest = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, status: AuthorizationRequestStatus.PERMITTED }) - const actual = await repository.findById(signMessageRequest.id) + const actual = await repository.findById(baseAuthzRequest.id) expect(authzRequest.status).toEqual(AuthorizationRequestStatus.PERMITTED) expect(actual?.status).toEqual(AuthorizationRequestStatus.PERMITTED) @@ -255,7 +377,7 @@ describe(AuthorizationRequestRepository.name, () => { it('appends evaluations', async () => { const authzRequestOne = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, evaluations: [ { id: '404853b2-1338-47f5-be17-a1aa78da8010', @@ -287,7 +409,7 @@ describe(AuthorizationRequestRepository.name, () => { }) const authzRequestTwo = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, evaluations: [ { id: 'cc329386-a2dd-4024-86fd-323a630ed703', @@ -318,7 +440,7 @@ describe(AuthorizationRequestRepository.name, () => { ] }) - const actual = await repository.findById(signMessageRequest.id) + const actual = await repository.findById(baseAuthzRequest.id) expect(authzRequestOne.evaluations.length).toEqual(1) expect(authzRequestTwo.evaluations.length).toEqual(2) @@ -335,16 +457,16 @@ describe(AuthorizationRequestRepository.name, () => { it('appends approvals', async () => { const authzRequestOne = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, approvals: ['test-signature'] }) const authzRequestTwo = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, approvals: ['test-signature'] }) - const actual = await repository.findById(signMessageRequest.id) + const actual = await repository.findById(baseAuthzRequest.id) expect(authzRequestOne.approvals?.length).toEqual(1) expect(authzRequestTwo.approvals?.length).toEqual(2) @@ -353,7 +475,7 @@ describe(AuthorizationRequestRepository.name, () => { it('appends errors', async () => { const authzRequestOne = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, errors: [ { id: 'test-error-id-one', @@ -364,7 +486,7 @@ describe(AuthorizationRequestRepository.name, () => { }) const authzRequestTwo = await repository.update({ - ...signMessageRequest, + ...baseAuthzRequest, errors: [ { id: 'test-error-id-two', @@ -374,7 +496,7 @@ describe(AuthorizationRequestRepository.name, () => { ] }) - const actual = await repository.findById(signMessageRequest.id) + const actual = await repository.findById(baseAuthzRequest.id) expect(authzRequestOne?.errors?.length).toEqual(1) expect(authzRequestTwo?.errors?.length).toEqual(2) diff --git a/apps/armory/src/orchestration/persistence/schema/signature.schema.ts b/apps/armory/src/orchestration/persistence/schema/signature.schema.ts deleted file mode 100644 index 8d1928b34..000000000 --- a/apps/armory/src/orchestration/persistence/schema/signature.schema.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Alg } from '@narval/signature' -import { z } from 'zod' - -export const algSchema = z.nativeEnum(Alg) diff --git a/apps/armory/src/orchestration/persistence/schema/transaction-request.schema.ts b/apps/armory/src/orchestration/persistence/schema/transaction-request.schema.ts index ad96cae17..d3993aeb8 100644 --- a/apps/armory/src/orchestration/persistence/schema/transaction-request.schema.ts +++ b/apps/armory/src/orchestration/persistence/schema/transaction-request.schema.ts @@ -1,15 +1,4 @@ -import { - SerializedTransactionRequest, - TransactionRequest, - addressSchema, - hexSchema -} from '@narval/policy-engine-shared' -import { z } from 'zod' - -export const accessListSchema = z.object({ - address: addressSchema, - storageKeys: z.array(hexSchema) -}) +import { SerializedTransactionRequest, TransactionRequest } from '@narval/policy-engine-shared' export const readTransactionRequestSchema = TransactionRequest diff --git a/apps/armory/src/policy-engine/core/service/__test__/integration/cluster.service.spec.ts b/apps/armory/src/policy-engine/core/service/__test__/integration/cluster.service.spec.ts index d3f8dbb79..b53d4766d 100644 --- a/apps/armory/src/policy-engine/core/service/__test__/integration/cluster.service.spec.ts +++ b/apps/armory/src/policy-engine/core/service/__test__/integration/cluster.service.spec.ts @@ -12,7 +12,6 @@ import { Decision, EvaluationRequest, EvaluationResponse, - PublicClient, Request, Source, SourceType @@ -26,6 +25,7 @@ import { v4 as uuid } from 'uuid' import { generatePrivateKey } from 'viem/accounts' import { generateSignTransactionRequest } from '../../../../../__test__/fixture/authorization-request.fixture' import { Config, load } from '../../../../../armory.config' +import { PolicyEnginePublicClient } from '../../../../../client/core/type/client.type' import { PersistenceModule } from '../../../../../shared/module/persistence/persistence.module' import { TestPrismaService } from '../../../../../shared/module/persistence/service/test-prisma.service' import { PolicyEngineClient } from '../../../../http/client/policy-engine.client' @@ -88,13 +88,26 @@ describe(ClusterService.name, () => { keys: [getPublicKey(privateKeyToJwk(generatePrivateKey()))] } - const createClientResponse: PublicClient = { + const createClientResponse: PolicyEnginePublicClient = { clientId, - clientSecret: secret.generate(), + name: 'Acme', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: secret.generate() + } + }, createdAt: new Date(), updatedAt: new Date(), - signer: { - publicKey: getPublicKey(privateKeyToJwk(generatePrivateKey())) + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'acme-key-ie', + publicKey: getPublicKey(privateKeyToJwk(generatePrivateKey())) + } }, dataStore: { entity: dataStoreConfig, @@ -118,7 +131,7 @@ describe(ClusterService.name, () => { clientId, clientSecret: expect.any(String), url: nodeUrl, - publicKey: createClientResponse.signer.publicKey + publicKey: createClientResponse.decisionAttestation.signer?.publicKey }) }) diff --git a/apps/armory/src/policy-engine/core/service/cluster.service.ts b/apps/armory/src/policy-engine/core/service/cluster.service.ts index d431fb7a6..d0e00d4a2 100644 --- a/apps/armory/src/policy-engine/core/service/cluster.service.ts +++ b/apps/armory/src/policy-engine/core/service/cluster.service.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@narval/config-module' import { LoggerService, MetricService, OTEL_ATTR_CLIENT_ID, TraceService } from '@narval/nestjs-shared' import { Decision, EvaluationRequest, EvaluationResponse } from '@narval/policy-engine-shared' -import { PublicKey, verifyJwt } from '@narval/signature' +import { PublicKey, nowSeconds, verifyJwt } from '@narval/signature' import { HttpStatus, Inject, Injectable } from '@nestjs/common' import { Counter } from '@opentelemetry/api' import { isEmpty } from 'lodash' @@ -68,8 +68,8 @@ export class ClusterService { return { id: uuid(), clientId: client.clientId, - clientSecret: client.clientSecret, - publicKey: client.signer.publicKey, + clientSecret: client.auth.local?.clientSecret, + publicKey: client.decisionAttestation.signer?.publicKey, url: node } } @@ -117,6 +117,11 @@ export class ClusterService { nodes: nodes.map(({ id, url }) => ({ id, url })) }) + evaluation.metadata = { + issuedAt: nowSeconds(), // Ensure we have the SAME issuedAt for all nodes + ...evaluation.metadata + } + const responses = await Promise.all( nodes.map((node) => this.policyEngineClient.evaluate({ @@ -171,16 +176,16 @@ export class ClusterService { this.logger.log('Loaded finalizeEcdsaJwtSignature function', { finalizeEcdsaJwtSignature: this.finalizeEcdsaJwtSignature }) + } else { + throw new ApplicationException({ + message: 'The function finalizeEcdsaJwtSignature from @narval-xyz/armory-mpc-module is undefined', + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR + }) } - - throw new ApplicationException({ - message: 'The function finalizeEcdsaJwtSignature from @narval-xyz/armory-mpc-module is undefined', - suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR - }) } try { - // If MPC, wehave multiple partialSig responses to combine. If they don't + // If MPC, we have multiple partialSig responses to combine. If they don't // have exactly the same decision & accessToken, signature can't be // finalized and it will throw. const jwts = evaluations.map(({ accessToken }) => accessToken?.value).filter(isDefined) @@ -200,6 +205,16 @@ export class ClusterService { context: { jwts } }) } catch (error) { + this.logger.error('Finalizing JWT Error', { + evaluations, + partialTokens: evaluations.reduce( + (acc, { accessToken }, index) => ({ + ...acc, + [index]: accessToken?.value + }), + {} + ) + }) throw new ApplicationException({ message: 'Fail to finalize ECDSA JWT signature', suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, diff --git a/apps/armory/src/policy-engine/http/client/policy-engine.client.ts b/apps/armory/src/policy-engine/http/client/policy-engine.client.ts index df5183a1b..01d00c5b9 100644 --- a/apps/armory/src/policy-engine/http/client/policy-engine.client.ts +++ b/apps/armory/src/policy-engine/http/client/policy-engine.client.ts @@ -3,12 +3,12 @@ import { CreateClient, EvaluationRequest, EvaluationResponse, - PublicClient, SerializedEvaluationRequest } from '@narval/policy-engine-shared' import { HttpService } from '@nestjs/axios' import { HttpStatus, Injectable } from '@nestjs/common' import { catchError, lastValueFrom, map, tap } from 'rxjs' +import { PolicyEnginePublicClient } from '../../../client/core/type/client.type' import { ApplicationException } from '../../../shared/exception/application.exception' export class PolicyEngineClientException extends ApplicationException {} @@ -62,9 +62,7 @@ export class PolicyEngineClient { host: string data: CreateClient adminApiKey: string - clientId?: string - clientSecret?: string - }): Promise { + }): Promise { this.logger.log('Sending create client request', option) return lastValueFrom( @@ -74,7 +72,7 @@ export class PolicyEngineClient { method: 'POST', data: option.data, headers: { - ...this.getHeaders(option), + ...this.getHeaders(option.data), 'x-api-key': option.adminApiKey } }) @@ -87,7 +85,7 @@ export class PolicyEngineClient { response: response.data }) }), - map((response) => PublicClient.parse(response.data)), + map((response) => PolicyEnginePublicClient.parse(response.data)), catchError((error) => { throw new PolicyEngineClientException({ message: 'Create client request failed', diff --git a/apps/armory/src/shared/decorator/api-admin-guard.decorator.ts b/apps/armory/src/shared/decorator/api-admin-guard.decorator.ts index a9c748a35..ef446c991 100644 --- a/apps/armory/src/shared/decorator/api-admin-guard.decorator.ts +++ b/apps/armory/src/shared/decorator/api-admin-guard.decorator.ts @@ -1,14 +1,14 @@ +import { REQUEST_HEADER_ADMIN_API_KEY, securityOptions } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { ADMIN_SECURITY, REQUEST_HEADER_API_KEY } from '../../armory.constant' import { AdminApiKeyGuard } from '../guard/admin-api-key.guard' export function ApiAdminGuard() { return applyDecorators( UseGuards(AdminApiKeyGuard), - ApiSecurity(ADMIN_SECURITY.name), + ApiSecurity(securityOptions.adminApiKey.name), ApiHeader({ - name: REQUEST_HEADER_API_KEY, + name: REQUEST_HEADER_ADMIN_API_KEY, required: true }) ) diff --git a/apps/armory/src/shared/decorator/api-client-id-guard.decorator.ts b/apps/armory/src/shared/decorator/api-client-id-guard.decorator.ts index 04a03375a..1c43b5ff9 100644 --- a/apps/armory/src/shared/decorator/api-client-id-guard.decorator.ts +++ b/apps/armory/src/shared/decorator/api-client-id-guard.decorator.ts @@ -1,13 +1,12 @@ -import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { REQUEST_HEADER_CLIENT_ID, securityOptions } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { CLIENT_ID_SECURITY } from '../../armory.constant' import { ClientIdGuard } from '../guard/client-id.guard' export function ApiClientIdGuard() { return applyDecorators( UseGuards(ClientIdGuard), - ApiSecurity(CLIENT_ID_SECURITY.name), + ApiSecurity(securityOptions.clientId.name), ApiHeader({ name: REQUEST_HEADER_CLIENT_ID, required: true diff --git a/apps/armory/src/shared/decorator/api-client-secret-guard.decorator.ts b/apps/armory/src/shared/decorator/api-client-secret-guard.decorator.ts index 5a739a3bd..8bd8cd781 100644 --- a/apps/armory/src/shared/decorator/api-client-secret-guard.decorator.ts +++ b/apps/armory/src/shared/decorator/api-client-secret-guard.decorator.ts @@ -1,13 +1,12 @@ -import { REQUEST_HEADER_CLIENT_SECRET } from '@narval/nestjs-shared' +import { REQUEST_HEADER_CLIENT_SECRET, securityOptions } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { CLIENT_SECRET_SECURITY } from '../../armory.constant' import { ClientSecretGuard } from '../guard/client-secret.guard' export function ApiClientSecretGuard() { return applyDecorators( UseGuards(ClientSecretGuard), - ApiSecurity(CLIENT_SECRET_SECURITY.name), + ApiSecurity(securityOptions.clientSecret.name), ApiHeader({ required: true, name: REQUEST_HEADER_CLIENT_SECRET diff --git a/apps/armory/src/shared/guard/__test__/unit/admin.guard.spec.ts b/apps/armory/src/shared/guard/__test__/unit/admin.guard.spec.ts index 63c077662..0febcd6a5 100644 --- a/apps/armory/src/shared/guard/__test__/unit/admin.guard.spec.ts +++ b/apps/armory/src/shared/guard/__test__/unit/admin.guard.spec.ts @@ -1,15 +1,14 @@ -import { secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' import { ExecutionContext } from '@nestjs/common' import { mock } from 'jest-mock-extended' import { AppService } from '../../../../app/core/service/app.service' -import { REQUEST_HEADER_API_KEY } from '../../../../armory.constant' import { ApplicationException } from '../../../exception/application.exception' import { AdminApiKeyGuard } from '../../admin-api-key.guard' describe(AdminApiKeyGuard.name, () => { const mockExecutionContext = (apiKey?: string) => { const headers = { - [REQUEST_HEADER_API_KEY]: apiKey + [REQUEST_HEADER_ADMIN_API_KEY]: apiKey } const request = { headers } @@ -35,20 +34,20 @@ describe(AdminApiKeyGuard.name, () => { return serviceMock } - it(`throws an error when ${REQUEST_HEADER_API_KEY} header is missing`, async () => { + it(`throws an error when ${REQUEST_HEADER_ADMIN_API_KEY} header is missing`, async () => { const guard = new AdminApiKeyGuard(mockAppService()) await expect(guard.canActivate(mockExecutionContext())).rejects.toThrow(ApplicationException) }) - it(`returns true when ${REQUEST_HEADER_API_KEY} matches the app admin api key`, async () => { + it(`returns true when ${REQUEST_HEADER_ADMIN_API_KEY} matches the app admin api key`, async () => { const adminApiKey = 'test-admin-api-key' const guard = new AdminApiKeyGuard(mockAppService(adminApiKey)) expect(await guard.canActivate(mockExecutionContext(adminApiKey))).toEqual(true) }) - it(`returns false when ${REQUEST_HEADER_API_KEY} does not matches the app admin api key`, async () => { + it(`returns false when ${REQUEST_HEADER_ADMIN_API_KEY} does not matches the app admin api key`, async () => { const guard = new AdminApiKeyGuard(mockAppService('test-admin-api-key')) expect(await guard.canActivate(mockExecutionContext('another-api-key'))).toEqual(false) diff --git a/apps/armory/src/shared/guard/admin-api-key.guard.ts b/apps/armory/src/shared/guard/admin-api-key.guard.ts index 3a4385a95..c111186c2 100644 --- a/apps/armory/src/shared/guard/admin-api-key.guard.ts +++ b/apps/armory/src/shared/guard/admin-api-key.guard.ts @@ -1,7 +1,6 @@ -import { secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common' import { AppService } from '../../app/core/service/app.service' -import { REQUEST_HEADER_API_KEY } from '../../armory.constant' import { ApplicationException } from '../exception/application.exception' @Injectable() @@ -10,11 +9,11 @@ export class AdminApiKeyGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest() - const apiKey = req.headers[REQUEST_HEADER_API_KEY] + const apiKey = req.headers[REQUEST_HEADER_ADMIN_API_KEY] if (!apiKey) { throw new ApplicationException({ - message: `Missing or invalid ${REQUEST_HEADER_API_KEY} header`, + message: `Missing or invalid ${REQUEST_HEADER_ADMIN_API_KEY} header`, suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED }) } diff --git a/apps/armory/src/shared/module/persistence/seed.ts b/apps/armory/src/shared/module/persistence/seed.ts index efddde962..f61ce8c63 100644 --- a/apps/armory/src/shared/module/persistence/seed.ts +++ b/apps/armory/src/shared/module/persistence/seed.ts @@ -56,9 +56,9 @@ async function main() { } try { - logger.log('Database germinated 🌱') await seeder.seed() } finally { + logger.log('✅ Database seeded') await application.close() } } diff --git a/apps/policy-engine/.env.default b/apps/policy-engine/.env.default index ec2b76f8b..0945c3672 100644 --- a/apps/policy-engine/.env.default +++ b/apps/policy-engine/.env.default @@ -1,15 +1,7 @@ NODE_ENV=development -PORT=3010 - -APP_UID=local-dev-engine-instance-1 - -# OPTIONAL: Sets the admin API key instead of generating a new one during the -# provision. -# -# Key should be hashed, like this: `echo -n "my-api-key" | openssl dgst -sha256 | awk '{print $2}'` -# Plain text API key: engine-admin-api-key -ADMIN_API_KEY=dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c +# Relative path to the config file from where the node process is started, defaults to `./config/policy-engine-config.yaml` +CONFIG_FILE_RELATIVE_PATH="./config/policy-engine-config.local.yaml" # === Database === @@ -25,30 +17,6 @@ APP_DATABASE_PORT=5432 APP_DATABASE_NAME=engine -# === Encryption === - -RESOURCE_PATH=./apps/policy-engine/src/resource - -# Determine the encryption module keyring type. -# Either "awskms" or "raw". -KEYRING_TYPE=raw - -# If using raw keyring, master password for encrypting data -MASTER_PASSWORD=unsafe-local-dev-master-password - -# If using awskms keyring, provide the ARN of the KMS encryption key instead of a master password -MASTER_AWS_KMS_ARN= - -# Either "simple" or "mpc" -SIGNING_PROTOCOL=simple - -# === MPC === - -# If SIGNING_PROTOCOL is set to "mpc", the application MUST configure a TSM. -TSM_URL= -TSM_API_KEY= -TSM_PLAYER_COUNT= - # === OpenTelemetry configuration === # See https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ diff --git a/apps/policy-engine/.env.template b/apps/policy-engine/.env.template new file mode 100644 index 000000000..35de2be38 --- /dev/null +++ b/apps/policy-engine/.env.template @@ -0,0 +1,85 @@ +# Template of all the available ENV variables. + +# === Node environment === + +NODE_ENV=development + +# === Server === + +PORT=3010 + +# === Application === + +APP_UID=local-dev-engine-instance-1 + +# === Config === + +# Absolute path to the config file. If not set, uses the relative path. +# CONFIG_FILE_ABSOLUTE_PATH="/config/vault-config.yaml" + +# Relative path to the config file from where the node process is started, defaults to `./config/vault-config.yaml` +CONFIG_FILE_RELATIVE_PATH="./config/policy-engine-config.local.yaml" + +# === Admin API key === + +# OPTIONAL: Sets the admin API key instead of generating a new one during the +# provision. +# +# Key should be hashed, like this: `echo -n "engine-admin-api-key" | openssl dgst -sha256 | awk '{print $2}'` +# Plain text API key: engine-admin-api-key +ADMIN_API_KEY=dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c + +# === Database === + +# APP db connection string +APP_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/engine?schema=public + +# Migrator db credentials. +# host/port/name should be the same, username&password may be different +APP_DATABASE_USERNAME=postgres +APP_DATABASE_PASSWORD=postgres +APP_DATABASE_HOST=host.docker.internal +APP_DATABASE_PORT=5432 +APP_DATABASE_NAME=engine + +# === Base URL === +# Base URL where the Policy Engine is deployed. +BASE_URL=http://localhost:3010 + +# === Encryption === + +RESOURCE_PATH=./apps/policy-engine/src/resource + +# Determine the encryption module keyring type. +# Either "awskms" or "raw". +KEYRING_TYPE=raw + +# If using raw keyring, master password for encrypting data +MASTER_PASSWORD=unsafe-local-dev-master-password + +# If using awskms keyring, provide the ARN of the KMS encryption key instead of a master password +MASTER_AWS_KMS_ARN= + +# HMAC secret for integrity verification of data in the database. +HMAC_SECRET="unsafe-local-test-hmac-secret" + +# Either "simple" or "mpc" +SIGNING_PROTOCOL=simple + +# === MPC === + +# If SIGNING_PROTOCOL is set to "mpc", the application MUST configure a TSM. +TSM_URL= +TSM_API_KEY= +TSM_PLAYER_COUNT= + +# === OpenTelemetry configuration === + +# See https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ +OTEL_SDK_DISABLED=true +# OTEL Collector container HTTP port. +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +OTEL_LOGS_EXPORTER=otlp +OTEL_LOG_LEVEL=error +OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local diff --git a/apps/policy-engine/.env.test.default b/apps/policy-engine/.env.test.default index 4556a53f8..4fa3af8e2 100644 --- a/apps/policy-engine/.env.test.default +++ b/apps/policy-engine/.env.test.default @@ -11,6 +11,10 @@ APP_UID="local-dev-engine-instance-1" MASTER_PASSWORD="unsafe-local-test-master-password" +HMAC_SECRET="unsafe-local-test-hmac-secret" + +BASE_URL="http://localhost:3010" + KEYRING_TYPE="raw" RESOURCE_PATH=./apps/policy-engine/src/resource diff --git a/apps/policy-engine/Makefile b/apps/policy-engine/Makefile index 29ee09bfb..792f83a4a 100644 --- a/apps/policy-engine/Makefile +++ b/apps/policy-engine/Makefile @@ -43,9 +43,11 @@ policy-engine/lint/check: policy-engine/format: npx nx format:write --projects ${POLICY_ENGINE_PROJECT_NAME} + npx prisma format --schema ${POLICY_ENGINE_DATABASE_SCHEMA} policy-engine/format/check: npx nx format:check --projects ${POLICY_ENGINE_PROJECT_NAME} + npx prisma format --schema ${POLICY_ENGINE_DATABASE_SCHEMA} # === Database === diff --git a/apps/policy-engine/src/client/client.module.ts b/apps/policy-engine/src/client/client.module.ts new file mode 100644 index 000000000..dc6859de3 --- /dev/null +++ b/apps/policy-engine/src/client/client.module.ts @@ -0,0 +1,55 @@ +import { ConfigService } from '@narval/config-module' +import { HttpModule } from '@nestjs/axios' +import { forwardRef, Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common' +import { APP_PIPE } from '@nestjs/core' +import { ZodValidationPipe } from 'nestjs-zod' +import { DataStoreRepositoryFactory } from '../engine/core/factory/data-store-repository.factory' +import { signingServiceFactory } from '../engine/core/factory/signing-service.factory' +import { DataStoreService } from '../engine/core/service/data-store.service' +import { FileSystemDataStoreRepository } from '../engine/persistence/repository/file-system-data-store.repository' +import { HttpDataStoreRepository } from '../engine/persistence/repository/http-data-store.repository' +import { AppModule } from '../policy-engine.module' +import { AdminApiKeyGuard } from '../shared/guard/admin-api-key.guard' +import { KeyValueModule } from '../shared/module/key-value/key-value.module' +import { PersistenceModule } from '../shared/module/persistence/persistence.module' +import { BootstrapService } from './core/service/bootstrap.service' +import { ClientService } from './core/service/client.service' +import { ClientController } from './http/rest/controller/client.controller' +import { ClientRepository } from './persistence/repository/client.repository' + +@Module({ + imports: [HttpModule, KeyValueModule, PersistenceModule, forwardRef(() => AppModule)], + controllers: [ClientController], + providers: [ + AdminApiKeyGuard, + BootstrapService, + DataStoreRepositoryFactory, + DataStoreService, + FileSystemDataStoreRepository, + HttpDataStoreRepository, + ClientRepository, + ClientService, + { + provide: 'SigningService', + useFactory: signingServiceFactory, + inject: [ConfigService] + }, + { + // DEPRECATE: Use Zod generated DTOs to validate request and responses. + provide: APP_PIPE, + useClass: ValidationPipe + }, + { + provide: APP_PIPE, + useClass: ZodValidationPipe + } + ], + exports: [ClientService, ClientRepository] +}) +export class ClientModule implements OnApplicationBootstrap { + constructor(private bootstrapService: BootstrapService) {} + + async onApplicationBootstrap() { + await this.bootstrapService.boot() + } +} diff --git a/apps/policy-engine/src/client/core/service/bootstrap.service.ts b/apps/policy-engine/src/client/core/service/bootstrap.service.ts new file mode 100644 index 000000000..0ff40d811 --- /dev/null +++ b/apps/policy-engine/src/client/core/service/bootstrap.service.ts @@ -0,0 +1,101 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { Config } from '../../../policy-engine.config' +import { Client } from '../../../shared/type/domain.type' +import { ClientService } from './client.service' + +@Injectable() +export class BootstrapService { + constructor( + private configService: ConfigService, + private clientService: ClientService, + private logger: LoggerService + ) {} + + async boot(): Promise { + this.logger.log('Start bootstrap') + + await this.persistDeclarativeClients() + + await this.syncClients() + + // TEMPORARY: Migrate the key-value format of the Client config into the table format. + // Can be removed once this runs once. + await this.clientService.migrateV1Data() + + this.logger.log('Bootstrap end') + } + + private async persistDeclarativeClients(): Promise { + const clients = this.configService.get('clients') + if (!clients) return + + // Given ClientConfig type, build the Client type + const declarativeClients: Client[] = clients.map((client) => ({ + clientId: client.clientId, + name: client.name, + configurationSource: 'declarative', + baseUrl: client.baseUrl, + auth: { + disabled: client.auth.disabled, + local: client.auth.local?.clientSecret + ? { + clientSecret: client.auth.local?.clientSecret + } + : null + }, + dataStore: { + entity: { + data: { + type: 'HTTP', + url: client.dataStore.entity.data.url + }, + signature: { + type: 'HTTP', + url: client.dataStore.entity.signature.url + }, + keys: client.dataStore.entity.publicKeys + }, + policy: { + data: { + type: 'HTTP', + url: client.dataStore.policy.data.url + }, + signature: { + type: 'HTTP', + url: client.dataStore.policy.signature.url + }, + keys: client.dataStore.policy.publicKeys + } + }, + decisionAttestation: { + disabled: client.decisionAttestation.disabled, + signer: client.decisionAttestation.signer || null + }, + createdAt: new Date(), + updatedAt: new Date() + })) + + for (const client of declarativeClients) { + await this.clientService.save(client) + } + } + + private async syncClients(): Promise { + const clients = await this.clientService.findAll() + + this.logger.log('Start syncing clients data stores', { + clientsCount: clients.length + }) + + // TODO: (@wcalderipe, 07/03/24) maybe change the execution to parallel? + for (const client of clients) { + await this.clientService.syncDataStore(client.clientId) + this.logger.log(`Client public key`, { + clientId: client.clientId, + publicKey: client.decisionAttestation.signer?.publicKey + }) + } + } +} diff --git a/apps/policy-engine/src/engine/core/service/client.service.ts b/apps/policy-engine/src/client/core/service/client.service.ts similarity index 79% rename from apps/policy-engine/src/engine/core/service/client.service.ts rename to apps/policy-engine/src/client/core/service/client.service.ts index 08c820d73..2e9b4a456 100644 --- a/apps/policy-engine/src/engine/core/service/client.service.ts +++ b/apps/policy-engine/src/client/core/service/client.service.ts @@ -1,14 +1,13 @@ import { LoggerService, OTEL_ATTR_CLIENT_ID, TraceService, secret } from '@narval/nestjs-shared' import { DataStoreConfiguration, EntityStore, PolicyStore } from '@narval/policy-engine-shared' -import { hash } from '@narval/signature' +import { SigningAlg, hash } from '@narval/signature' import { HttpStatus, Inject, Injectable } from '@nestjs/common' import { SpanStatusCode } from '@opentelemetry/api' -import { v4 as uuid } from 'uuid' +import { DataStoreService } from '../../../engine/core/service/data-store.service' +import { SigningService } from '../../../engine/core/service/signing.service.interface' import { ApplicationException } from '../../../shared/exception/application.exception' import { Client } from '../../../shared/type/domain.type' import { ClientRepository } from '../../persistence/repository/client.repository' -import { DataStoreService } from './data-store.service' -import { SigningService } from './signing.service.interface' @Injectable() export class ClientService { @@ -20,6 +19,35 @@ export class ClientService { @Inject('SigningService') private signingService: SigningService ) {} + // Temporary function to migrate data from V1 to V2 + async migrateV1Data(): Promise { + const clientsV1 = await this.clientRepository.findAllV1() + for (const clientV1 of clientsV1) { + const client: Client = { + clientId: clientV1.clientId, + name: clientV1.clientId, + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: clientV1.clientSecret + } + }, + dataStore: clientV1.dataStore, + decisionAttestation: { + disabled: false, + signer: clientV1.signer + }, + createdAt: clientV1.createdAt, + updatedAt: clientV1.updatedAt + } + this.logger.info('Migrating client', { clientV1, client }) + await this.clientRepository.save(client) + await this.clientRepository.deleteV1(clientV1.clientId) + } + } + async findById(clientId: string): Promise { const span = this.traceService.startSpan(`${ClientService.name}.findById`, { attributes: { @@ -33,7 +61,7 @@ export class ClientService { } async create(input: { - clientId?: string + clientId: string clientSecret?: string unsafeKeyId?: string entityDataStore: DataStoreConfiguration @@ -41,9 +69,26 @@ export class ClientService { allowSelfSignedData?: boolean }): Promise { const span = this.traceService.startSpan(`${ClientService.name}.create`) + const clientId = input.clientId + + const exists = await this.clientRepository.findById(input.clientId) + + if (exists && exists.configurationSource === 'dynamic') { + const exception = new ApplicationException({ + message: 'Client already exist', + suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, + context: { clientId: input.clientId } + }) + + span.recordException(exception) + span.setStatus({ code: SpanStatusCode.ERROR }) + span.end() + + throw exception + } + const now = new Date() const { unsafeKeyId, entityDataStore, policyDataStore, allowSelfSignedData } = input - const clientId = input.clientId || uuid() // If we are generating the secret, we'll want to return the full thing to // the user one time. const fullClientSecret = input.clientSecret || secret.generate() @@ -56,6 +101,7 @@ export class ClientService { const keypair = await this.signingService.generateKey(keyId, sessionId) const signer = { keyId: keypair.publicKey.kid, + alg: SigningAlg.EIP191, ...keypair } @@ -69,12 +115,23 @@ export class ClientService { const client = await this.save( { clientId, - clientSecret, + name: clientId, + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret + } + }, dataStore: { entity: entityDataStore, policy: policyDataStore }, - signer, + decisionAttestation: { + disabled: false, + signer + }, createdAt: now, updatedAt: now }, @@ -82,12 +139,12 @@ export class ClientService { ) span.end() - - return { - ...client, - // If we generated a new secret, we need to include it in the response the first time. - ...(!input.clientSecret ? { clientSecret: fullClientSecret } : {}) + if (!input.clientSecret && !client.auth?.disabled && client.auth?.local?.clientSecret) { + client.auth.local = { + clientSecret: fullClientSecret + } } + return client } async save(client: Client, options?: { syncAfter?: boolean }): Promise { @@ -97,22 +154,6 @@ export class ClientService { const syncAfter = options?.syncAfter ?? true - const exists = await this.clientRepository.findById(client.clientId) - - if (exists) { - const exception = new ApplicationException({ - message: 'Client already exist', - suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, - context: { clientId: client.clientId } - }) - - span.recordException(exception) - span.setStatus({ code: SpanStatusCode.ERROR }) - span.end() - - throw exception - } - try { await this.clientRepository.save(client) diff --git a/apps/policy-engine/src/engine/http/rest/controller/client.controller.ts b/apps/policy-engine/src/client/http/rest/controller/client.controller.ts similarity index 91% rename from apps/policy-engine/src/engine/http/rest/controller/client.controller.ts rename to apps/policy-engine/src/client/http/rest/controller/client.controller.ts index 134f89bd3..cc7242176 100644 --- a/apps/policy-engine/src/engine/http/rest/controller/client.controller.ts +++ b/apps/policy-engine/src/client/http/rest/controller/client.controller.ts @@ -1,11 +1,12 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common' import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { v4 as uuid } from 'uuid' +import { SyncResponseDto } from '../../../../engine/http/rest/dto/sync-response.dto' import { AdminGuard } from '../../../../shared/decorator/admin-guard.decorator' import { ClientGuard } from '../../../../shared/decorator/client-guard.decorator' import { ClientId } from '../../../../shared/decorator/client-id.decorator' import { ClientService } from '../../../core/service/client.service' import { CreateClientRequestDto, CreateClientResponseDto } from '../dto/create-client.dto' -import { SyncResponseDto } from '../dto/sync-response.dto' @Controller({ path: '/clients', @@ -26,7 +27,7 @@ export class ClientController { }) async create(@Body() body: CreateClientRequestDto): Promise { const client = await this.clientService.create({ - clientId: body.clientId, + clientId: body.clientId || uuid(), clientSecret: body.clientSecret, unsafeKeyId: body.keyId, entityDataStore: body.entityDataStore, diff --git a/apps/policy-engine/src/engine/http/rest/dto/create-client.dto.ts b/apps/policy-engine/src/client/http/rest/dto/create-client.dto.ts similarity index 60% rename from apps/policy-engine/src/engine/http/rest/dto/create-client.dto.ts rename to apps/policy-engine/src/client/http/rest/dto/create-client.dto.ts index 9f504f024..14acfef50 100644 --- a/apps/policy-engine/src/engine/http/rest/dto/create-client.dto.ts +++ b/apps/policy-engine/src/client/http/rest/dto/create-client.dto.ts @@ -1,5 +1,6 @@ -import { CreateClient, PublicClient } from '@narval/policy-engine-shared' +import { CreateClient } from '@narval/policy-engine-shared' import { createZodDto } from 'nestjs-zod' +import { PublicClient } from '../../../../shared/type/domain.type' export class CreateClientRequestDto extends createZodDto(CreateClient) {} diff --git a/apps/policy-engine/src/client/persistence/repository/client.repository.ts b/apps/policy-engine/src/client/persistence/repository/client.repository.ts new file mode 100644 index 000000000..1f5486535 --- /dev/null +++ b/apps/policy-engine/src/client/persistence/repository/client.repository.ts @@ -0,0 +1,247 @@ +import { coerce } from '@narval/nestjs-shared' +import { EntityStore, PolicyStore } from '@narval/policy-engine-shared' +import { privateKeySchema } from '@narval/signature' +import { Injectable } from '@nestjs/common' +import { Prisma } from '@prisma/client/policy-engine' +import { compact } from 'lodash/fp' +import { SigningAlg, publicKeySchema } from 'packages/signature/src/lib/types' +import { z } from 'zod' +import { EncryptKeyValueService } from '../../../shared/module/key-value/core/service/encrypt-key-value.service' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { Client, ClientV1 } from '../../../shared/type/domain.type' + +const ClientListIndex = z.array(z.string()) + +export function clientObjectToPrisma(client: Client): Prisma.ClientCreateInput { + return { + clientId: client.clientId, + name: client.name, + configurationSource: client.configurationSource, + baseUrl: client.baseUrl, + authDisabled: client.auth.disabled, + clientSecret: client.auth.local?.clientSecret, + + dataStoreEntityDataUrl: client.dataStore.entity.data.url, + dataStoreEntitySignatureUrl: client.dataStore.entity.signature.url, + dataStoreEntityPublicKeys: JSON.stringify(client.dataStore.entity.keys), + dataStorePolicyDataUrl: client.dataStore.policy.data.url, + dataStorePolicySignatureUrl: client.dataStore.policy.signature.url, + dataStorePolicyPublicKeys: JSON.stringify(client.dataStore.policy.keys), + + decisionAttestationDisabled: client.decisionAttestation.disabled, + signerAlg: client.decisionAttestation.signer?.alg, + signerKeyId: client.decisionAttestation.signer?.keyId, + signerPublicKey: JSON.stringify(client.decisionAttestation.signer?.publicKey), + signerPrivateKey: JSON.stringify(client.decisionAttestation.signer?.privateKey), + + createdAt: client.createdAt, + updatedAt: client.updatedAt + } +} + +function prismaToClientObject(prismaClient: Prisma.ClientGetPayload): Client { + return Client.parse({ + clientId: prismaClient.clientId, + name: prismaClient.name, + configurationSource: prismaClient.configurationSource as 'declarative' | 'dynamic', + baseUrl: prismaClient.baseUrl, + auth: { + disabled: prismaClient.authDisabled, + local: prismaClient.clientSecret + ? { + clientSecret: prismaClient.clientSecret + } + : null + }, + dataStore: { + entity: { + data: { + url: prismaClient.dataStoreEntityDataUrl, + type: prismaClient.dataStoreEntityDataUrl.startsWith('https') ? 'HTTPS' : 'HTTP' + }, + signature: { + url: prismaClient.dataStoreEntitySignatureUrl, + type: prismaClient.dataStoreEntitySignatureUrl.startsWith('https') ? 'HTTPS' : 'HTTP' + }, + keys: JSON.parse(prismaClient.dataStoreEntityPublicKeys) + }, + policy: { + data: { + url: prismaClient.dataStorePolicyDataUrl, + type: prismaClient.dataStorePolicyDataUrl.startsWith('https') ? 'HTTPS' : 'HTTP' + }, + signature: { + url: prismaClient.dataStorePolicySignatureUrl, + type: prismaClient.dataStorePolicySignatureUrl.startsWith('https') ? 'HTTPS' : 'HTTP' + }, + keys: JSON.parse(prismaClient.dataStorePolicyPublicKeys) + } + }, + decisionAttestation: { + disabled: prismaClient.decisionAttestationDisabled, + signer: prismaClient.signerAlg + ? { + alg: prismaClient.signerAlg as SigningAlg, + keyId: prismaClient.signerKeyId, + publicKey: prismaClient.signerPublicKey + ? publicKeySchema.parse(JSON.parse(prismaClient.signerPublicKey)) + : undefined, + privateKey: prismaClient.signerPrivateKey + ? privateKeySchema.parse(JSON.parse(prismaClient.signerPrivateKey)) + : undefined + } + : null + }, + createdAt: prismaClient.createdAt, + updatedAt: prismaClient.updatedAt + }) +} + +@Injectable() +export class ClientRepository { + constructor( + private encryptKeyValueService: EncryptKeyValueService, + private prismaService: PrismaService + ) {} + + async findById(clientId: string): Promise { + const value = await this.prismaService.client.findUnique({ + where: { clientId } + }) + + if (value) { + return prismaToClientObject(value) + } + + return null + } + async findAll(): Promise { + const clients = await this.prismaService.client.findMany({}) + return clients.map(prismaToClientObject) + } + + // Upsert the Client + async save(client: Client): Promise { + const clientData = clientObjectToPrisma(client) + + await this.prismaService.client.upsert({ + where: { clientId: client.clientId }, + update: clientData, + create: clientData + }) + + return client + } + + /** @deprecated */ + async findByIdV1(clientId: string): Promise { + const value = await this.encryptKeyValueService.get(this.getKey(clientId)) + + if (value) { + return coerce.decode(ClientV1, value) + } + + return null + } + + /** @deprecated */ + async saveV1(client: ClientV1): Promise { + await this.encryptKeyValueService.set(this.getKey(client.clientId), coerce.encode(ClientV1, client)) + await this.index(client) + + return client + } + + /** @deprecated */ + async getClientListIndex(): Promise { + const index = await this.encryptKeyValueService.get(this.getIndexKey()) + + if (index) { + return coerce.decode(ClientListIndex, index) + } + + return [] + } + + async saveEntityStore(clientId: string, store: EntityStore): Promise { + return this.encryptKeyValueService.set(this.getEntityStoreKey(clientId), coerce.encode(EntityStore, store)) + } + + async findEntityStore(clientId: string): Promise { + const value = await this.encryptKeyValueService.get(this.getEntityStoreKey(clientId)) + + if (value) { + return coerce.decode(EntityStore, value) + } + + return null + } + + async savePolicyStore(clientId: string, store: PolicyStore): Promise { + return this.encryptKeyValueService.set(this.getPolicyStoreKey(clientId), coerce.encode(PolicyStore, store)) + } + + async findPolicyStore(clientId: string): Promise { + const value = await this.encryptKeyValueService.get(this.getPolicyStoreKey(clientId)) + + if (value) { + return coerce.decode(PolicyStore, value) + } + + return null + } + + // TODO: (@wcalderipe, 07/03/24) we need to rethink this strategy. If we use a + // SQL database, this could generate a massive amount of queries; thus, + // degrading the performance. + // + // An option is to move these general queries `findBy`, findAll`, etc to the + // KeyValeuRepository implementation letting each implementation pick the best + // strategy to solve the problem (e.g. where query in SQL) + /** @deprecated */ + async findAllV1(): Promise { + const ids = await this.getClientListIndex() + const clients = await Promise.all(ids.map((id) => this.findByIdV1(id))) + + return compact(clients) + } + + /** @deprecated */ + getKey(clientId: string): string { + return `client:${clientId}` + } + + /** @deprecated */ + getIndexKey(): string { + return 'client:list-index' + } + + /** @deprecated */ + private async index(client: ClientV1): Promise { + const currentIndex = await this.getClientListIndex() + + await this.encryptKeyValueService.set( + this.getIndexKey(), + coerce.encode(ClientListIndex, [...currentIndex, client.clientId]) + ) + + return true + } + + getEntityStoreKey(clientId: string): string { + return `client:${clientId}:entity-store` + } + + getPolicyStoreKey(clientId: string): string { + return `client:${clientId}:policy-store` + } + + async deleteV1(clientId: string): Promise { + await this.encryptKeyValueService.delete(this.getKey(clientId)) + // Remove the client from the index + const currentIndex = await this.getClientListIndex() + + const newIndex = currentIndex.filter((id) => id !== clientId) + await this.encryptKeyValueService.set(this.getIndexKey(), coerce.encode(ClientListIndex, newIndex)) + } +} diff --git a/apps/policy-engine/src/engine/__test__/e2e/client.spec.ts b/apps/policy-engine/src/engine/__test__/e2e/client.spec.ts index 801cfbc23..2b0ffddc0 100644 --- a/apps/policy-engine/src/engine/__test__/e2e/client.spec.ts +++ b/apps/policy-engine/src/engine/__test__/e2e/client.spec.ts @@ -1,8 +1,8 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' import { EncryptionModuleOptionProvider } from '@narval/encryption-module' import { LoggerModule, OpenTelemetryModule, + REQUEST_HEADER_ADMIN_API_KEY, REQUEST_HEADER_CLIENT_ID, REQUEST_HEADER_CLIENT_SECRET, secret @@ -19,16 +19,16 @@ import { Test, TestingModule } from '@nestjs/testing' import request from 'supertest' import { v4 as uuid } from 'uuid' import { generatePrivateKey } from 'viem/accounts' -import { Config, load } from '../../../policy-engine.config' -import { REQUEST_HEADER_API_KEY } from '../../../policy-engine.constant' +import { ClientService } from '../../../client/core/service/client.service' +import { CreateClientRequestDto } from '../../../client/http/rest/dto/create-client.dto' +import { ClientRepository } from '../../../client/persistence/repository/client.repository' +import { PolicyEngineModule } from '../../../policy-engine.module' +import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository' +import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' import { Client } from '../../../shared/type/domain.type' -import { ClientService } from '../../core/service/client.service' -import { EngineService } from '../../core/service/engine.service' -import { EngineModule } from '../../engine.module' -import { CreateClientRequestDto } from '../../http/rest/dto/create-client.dto' -import { ClientRepository } from '../../persistence/repository/client.repository' +import { ProvisionService } from '../../core/service/provision.service' describe('Client', () => { let app: INestApplication @@ -36,8 +36,6 @@ describe('Client', () => { let testPrismaService: TestPrismaService let clientRepository: ClientRepository let clientService: ClientService - let engineService: EngineService - let configService: ConfigService let dataStoreConfiguration: DataStoreConfiguration let createClientPayload: CreateClientRequestDto @@ -52,16 +50,14 @@ describe('Client', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - OpenTelemetryModule.forTest(), - EngineModule - ] + imports: [PolicyEngineModule] }) + .overrideModule(LoggerModule) + .useModule(LoggerModule.forTest()) + .overrideModule(OpenTelemetryModule) + .useModule(OpenTelemetryModule.forTest()) + .overrideProvider(KeyValueRepository) + .useValue(new InMemoryKeyValueRepository()) .overrideProvider(EncryptionModuleOptionProvider) .useValue({ keyring: getTestRawAesKeyring() @@ -70,11 +66,9 @@ describe('Client', () => { app = module.createNestApplication() - engineService = module.get(EngineService) clientService = module.get(ClientService) clientRepository = module.get(ClientRepository) testPrismaService = module.get(TestPrismaService) - configService = module.get>(ConfigService) const jwk = secp256k1PrivateKeyToJwk(generatePrivateKey()) @@ -102,11 +96,8 @@ describe('Client', () => { beforeEach(async () => { await testPrismaService.truncateAll() - await engineService.save({ - id: configService.get('engine.id'), - masterKey: 'unsafe-test-master-key', - adminApiKey: secret.hash(adminApiKey) - }) + const provisionService = module.get(ProvisionService) + await provisionService.provision(secret.hash(adminApiKey)) jest.spyOn(clientService, 'syncDataStore').mockResolvedValue(true) }) @@ -115,17 +106,29 @@ describe('Client', () => { it('creates a new client', async () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientPayload) const actualClient = await clientRepository.findById(clientId) - const hex = await privateKeyToHex(actualClient?.signer.privateKey as PrivateKey) + const hex = await privateKeyToHex(actualClient?.decisionAttestation.signer?.privateKey as PrivateKey) const actualPublicKey = secp256k1PrivateKeyToPublicJwk(hex) expect(body).toEqual({ ...actualClient, - clientSecret: expect.any(String), - signer: { publicKey: actualPublicKey }, + auth: { + disabled: false, + local: { + clientSecret: expect.any(String) + } + }, + decisionAttestation: { + ...actualClient?.decisionAttestation, + signer: { + publicKey: actualPublicKey, + alg: actualClient?.decisionAttestation.signer?.alg, + keyId: actualClient?.decisionAttestation.signer?.keyId + } + }, createdAt: actualClient?.createdAt.toISOString(), updatedAt: actualClient?.updatedAt.toISOString() }) @@ -137,26 +140,38 @@ describe('Client', () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({ ...createClientPayload, clientSecret }) - expect(body.clientSecret).toEqual(clientSecret) + expect(body.auth.local?.clientSecret).toEqual(clientSecret) }) it('creates a new client with engine key in the entity and policy keys for self-signed data', async () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({ ...createClientPayload, allowSelfSignedData: true }) const actualClient = await clientRepository.findById(clientId) - const hex = await privateKeyToHex(actualClient?.signer.privateKey as PrivateKey) + const hex = await privateKeyToHex(actualClient?.decisionAttestation.signer?.privateKey as PrivateKey) const actualPublicKey = secp256k1PrivateKeyToPublicJwk(hex) expect(body).toEqual({ ...actualClient, - clientSecret: expect.any(String), - signer: { publicKey: actualPublicKey }, + auth: { + disabled: false, + local: { + clientSecret: expect.any(String) + } + }, + decisionAttestation: { + ...actualClient?.decisionAttestation, + signer: { + publicKey: actualPublicKey, + alg: actualClient?.decisionAttestation.signer?.alg, + keyId: actualClient?.decisionAttestation.signer?.keyId + } + }, createdAt: actualClient?.createdAt.toISOString(), updatedAt: actualClient?.updatedAt.toISOString() }) @@ -175,25 +190,24 @@ describe('Client', () => { it('does not expose the signer private key', async () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientPayload) - expect(body.signer.key).not.toBeDefined() - expect(body.signer.type).not.toBeDefined() + expect(body.decisionAttestation.signer.privateKey).not.toBeDefined() // The JWK private key is stored in the key's `d` property. // See also https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.2 - expect(body.signer.publicKey.d).not.toBeDefined() + expect(body.decisionAttestation.signer.publicKey.d).not.toBeDefined() }) it('responds with an error when clientId already exist', async () => { await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientPayload) const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(createClientPayload) expect(body).toEqual({ @@ -206,7 +220,7 @@ describe('Client', () => { it('responds with forbidden when admin api key is invalid', async () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, 'invalid-api-key') + .set(REQUEST_HEADER_ADMIN_API_KEY, 'invalid-api-key') .send(createClientPayload) expect(body).toMatchObject({ @@ -225,7 +239,7 @@ describe('Client', () => { const { body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send({ ...createClientPayload, clientId: uuid() @@ -238,8 +252,7 @@ describe('Client', () => { const { status, body } = await request(app.getHttpServer()) .post('/clients/sync') .set(REQUEST_HEADER_CLIENT_ID, client.clientId) - .set(REQUEST_HEADER_CLIENT_SECRET, client.clientSecret) - .send(createClientPayload) + .set(REQUEST_HEADER_CLIENT_SECRET, client.auth.local?.clientSecret || '') expect(body).toEqual({ success: true }) expect(status).toEqual(HttpStatus.OK) diff --git a/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts b/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts index a47f486e4..f37931fe4 100644 --- a/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts +++ b/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts @@ -1,10 +1,10 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' import { EncryptionModuleOptionProvider } from '@narval/encryption-module' import { LoggerModule, OpenTelemetryModule, REQUEST_HEADER_CLIENT_ID, - REQUEST_HEADER_CLIENT_SECRET + REQUEST_HEADER_CLIENT_SECRET, + secret } from '@narval/nestjs-shared' import { Action, @@ -18,7 +18,7 @@ import { SourceType, Then } from '@narval/policy-engine-shared' -import { PrivateKey, secp256k1PrivateKeyToJwk, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' +import { PrivateKey, SigningAlg, secp256k1PrivateKeyToJwk, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' import { HttpStatus, INestApplication } from '@nestjs/common' import { Test, TestingModule } from '@nestjs/testing' import { randomBytes } from 'crypto' @@ -27,8 +27,8 @@ import request from 'supertest' import { v4 as uuid } from 'uuid' import { generatePrivateKey } from 'viem/accounts' import { sepolia } from 'viem/chains' -import { EngineService } from '../../../engine/core/service/engine.service' -import { Config, load } from '../../../policy-engine.config' +import { ClientService } from '../../../client/core/service/client.service' +import { PolicyEngineModule } from '../../../policy-engine.module' import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository' import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' @@ -44,8 +44,7 @@ import { generateSignUserOperationRequest } from '../../../shared/testing/evaluation.testing' import { Client } from '../../../shared/type/domain.type' -import { ClientService } from '../../core/service/client.service' -import { EngineModule } from '../../engine.module' +import { ProvisionService } from '../../core/service/provision.service' describe('Evaluation', () => { let app: INestApplication @@ -66,16 +65,12 @@ describe('Evaluation', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - OpenTelemetryModule.forTest(), - EngineModule - ] + imports: [PolicyEngineModule] }) + .overrideModule(LoggerModule) + .useModule(LoggerModule.forTest()) + .overrideModule(OpenTelemetryModule) + .useModule(OpenTelemetryModule.forTest()) .overrideProvider(KeyValueRepository) .useValue(new InMemoryKeyValueRepository()) .overrideProvider(EncryptionModuleOptionProvider) @@ -86,8 +81,6 @@ describe('Evaluation', () => { app = module.createNestApplication() - const engineService = module.get(EngineService) - const configService = module.get>(ConfigService) clientService = module.get(ClientService) testPrismaService = module.get(TestPrismaService) @@ -101,24 +94,34 @@ describe('Evaluation', () => { keys: [privateKey] } - await engineService.save({ - id: configService.get('engine.id'), - masterKey: 'unsafe-test-master-key', - adminApiKey - }) + const provisionService = module.get(ProvisionService) + await provisionService.provision(secret.hash(adminApiKey)) const clientSignerKey = generatePrivateKey() client = await clientService.save( { clientId, - clientSecret: randomBytes(42).toString('hex'), + name: 'test-client', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: randomBytes(42).toString('hex') + } + }, dataStore: { entity: dataStoreConfiguration, policy: dataStoreConfiguration }, - signer: { - publicKey: secp256k1PrivateKeyToPublicJwk(clientSignerKey), - privateKey: secp256k1PrivateKeyToJwk(clientSignerKey) + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'test-key-id', + publicKey: secp256k1PrivateKeyToPublicJwk(clientSignerKey), + privateKey: secp256k1PrivateKeyToJwk(clientSignerKey) + } }, createdAt: new Date(), updatedAt: new Date() @@ -146,7 +149,7 @@ describe('Evaluation', () => { const { body } = await request(app.getHttpServer()) .post('/evaluations') .set(REQUEST_HEADER_CLIENT_ID, client.clientId) - .set(REQUEST_HEADER_CLIENT_SECRET, client.clientSecret) + .set(REQUEST_HEADER_CLIENT_SECRET, client.auth.local?.clientSecret || '') .send(serializedPayload) expect(body).toMatchObject({ @@ -197,7 +200,7 @@ describe('Evaluation', () => { const { status, body } = await request(app.getHttpServer()) .post('/evaluations') .set(REQUEST_HEADER_CLIENT_ID, client.clientId) - .set(REQUEST_HEADER_CLIENT_SECRET, client.clientSecret) + .set(REQUEST_HEADER_CLIENT_SECRET, client.auth.local?.clientSecret || '') .send(payload) expect(body).toEqual({ @@ -292,7 +295,7 @@ describe('Evaluation', () => { const { status, body } = await request(app.getHttpServer()) .post('/evaluations') .set(REQUEST_HEADER_CLIENT_ID, client.clientId) - .set(REQUEST_HEADER_CLIENT_SECRET, client.clientSecret) + .set(REQUEST_HEADER_CLIENT_SECRET, client.auth.local?.clientSecret || '') .send(payload) expect(body).toMatchObject({ diff --git a/apps/policy-engine/src/engine/__test__/e2e/provision.spec.ts b/apps/policy-engine/src/engine/__test__/e2e/provision.spec.ts deleted file mode 100644 index 8b8c4081d..000000000 --- a/apps/policy-engine/src/engine/__test__/e2e/provision.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ConfigModule } from '@narval/config-module' -import { LoggerModule, OpenTelemetryModule, secret } from '@narval/nestjs-shared' -import { INestApplication } from '@nestjs/common' -import { Test, TestingModule } from '@nestjs/testing' -import request from 'supertest' -import { Config, load } from '../../../policy-engine.config' -import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' -import { EngineService } from '../../core/service/engine.service' -import { ProvisionService } from '../../core/service/provision.service' -import { EngineModule } from '../../engine.module' - -const ENDPOINT = '/apps/activate' - -const testConfigLoad = (): Config => ({ - ...load(), - engine: { - id: 'local-dev-engine-instance-1', - adminApiKeyHash: undefined - } -}) - -describe('Provision', () => { - let app: INestApplication - let module: TestingModule - let engineService: EngineService - let provisionService: ProvisionService - let testPrismaService: TestPrismaService - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [testConfigLoad], - isGlobal: true - }), - OpenTelemetryModule.forTest(), - EngineModule - ] - }).compile() - - app = module.createNestApplication() - - engineService = app.get(EngineService) - provisionService = app.get(ProvisionService) - testPrismaService = app.get(TestPrismaService) - - await app.init() - }) - - beforeEach(async () => { - await testPrismaService.truncateAll() - await provisionService.provision() - }) - - afterAll(async () => { - await testPrismaService.truncateAll() - await module.close() - await app.close() - }) - - describe(`POST ${ENDPOINT}`, () => { - it('responds with activated app state', async () => { - const { body } = await request(app.getHttpServer()).post(ENDPOINT).send() - - expect(body).toEqual({ - state: 'READY', - app: { - appId: 'local-dev-engine-instance-1', - adminApiKey: expect.any(String) - } - }) - }) - - it('responds already provisioned', async () => { - await request(app.getHttpServer()).post(ENDPOINT).send() - - const { body } = await request(app.getHttpServer()).post(ENDPOINT).send() - - expect(body).toEqual({ state: 'ACTIVATED' }) - }) - - it('does not respond with hashed admin API key', async () => { - const { body } = await request(app.getHttpServer()).post(ENDPOINT).send() - - const actualEngine = await engineService.getEngineOrThrow() - - expect(secret.hash(body.app.adminApiKey)).toEqual(actualEngine.adminApiKey) - }) - }) -}) diff --git a/apps/policy-engine/src/engine/core/factory/open-policy-agent-engine.factory.ts b/apps/policy-engine/src/engine/core/factory/open-policy-agent-engine.factory.ts new file mode 100644 index 000000000..a9834bcb3 --- /dev/null +++ b/apps/policy-engine/src/engine/core/factory/open-policy-agent-engine.factory.ts @@ -0,0 +1,33 @@ +import { ConfigService } from '@narval/config-module' +import { TraceService } from '@narval/nestjs-shared' +import { Entities, Policy } from '@narval/policy-engine-shared' +import { Inject, Injectable } from '@nestjs/common' +import { resolve } from 'path' +import { OpenPolicyAgentEngine } from '../../../open-policy-agent/core/open-policy-agent.engine' +import { Config } from '../../../policy-engine.config' + +@Injectable() +/** + * Factory responsible for creating OpenPolicyAgentEngine instances. + * + * This factory exists primarily to improve the testability of the + * EvaluationService by abstracting engine creation. This allows tests to + * easily mock the engine creation process. + */ +export class OpenPolicyAgentEngineFactory { + constructor( + private configService: ConfigService, + @Inject(TraceService) private traceService: TraceService + ) {} + + async create(entities: Entities, policies: Policy[]): Promise { + return this.traceService.startActiveSpan(`${OpenPolicyAgentEngineFactory.name}.create`, () => { + return new OpenPolicyAgentEngine({ + entities, + policies, + resourcePath: resolve(this.configService.get('resourcePath')), + tracer: this.traceService.getTracer() + }).load() + }) + } +} diff --git a/apps/policy-engine/src/engine/core/factory/signing-service.factory.ts b/apps/policy-engine/src/engine/core/factory/signing-service.factory.ts index d766aea07..f13fe3844 100644 --- a/apps/policy-engine/src/engine/core/factory/signing-service.factory.ts +++ b/apps/policy-engine/src/engine/core/factory/signing-service.factory.ts @@ -25,14 +25,14 @@ const loadArmoryMpcSigningService = () => { } export const signingServiceFactory = async (configService: ConfigService): Promise => { - const protocol = configService.get('signingProtocol') + const protocol = configService.get('decisionAttestation.protocol') if (protocol === 'simple') { return new SimpleSigningService() } if (protocol === 'mpc') { - const tsm = configService.get('tsm') + const tsm = configService.get('decisionAttestation.tsm') if (!tsm) { throw new Error('Missing TSM config') diff --git a/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts index bace43dbf..a858397c0 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts @@ -10,7 +10,7 @@ import { PolicyStore, SourceType } from '@narval/policy-engine-shared' -import { Jwk, PublicKey, getPublicKey, privateKeyToJwk } from '@narval/signature' +import { PrivateKey, PublicKey, getPublicKey, privateKeyToJwk } from '@narval/signature' import { HttpStatus } from '@nestjs/common' import { Test } from '@nestjs/testing' @@ -27,7 +27,7 @@ const UNSAFE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b084 describe(DataStoreService.name, () => { let service: DataStoreService - let privateKey: Jwk + let privateKey: PrivateKey let publicKey: PublicKey let entityStore: EntityStore let policyStore: PolicyStore @@ -39,7 +39,7 @@ describe(DataStoreService.name, () => { beforeEach(async () => { const module = await Test.createTestingModule({ - imports: [HttpModule.forRoot(), LoggerModule.forTest()], + imports: [HttpModule.register(), LoggerModule.forTest()], providers: [DataStoreService, DataStoreRepositoryFactory, HttpDataStoreRepository, FileSystemDataStoreRepository] }).compile() diff --git a/apps/policy-engine/src/engine/core/service/__test__/integration/engine.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/integration/engine.service.spec.ts deleted file mode 100644 index 7577ca760..000000000 --- a/apps/policy-engine/src/engine/core/service/__test__/integration/engine.service.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' -import { secret } from '@narval/nestjs-shared' -import { Test } from '@nestjs/testing' -import { Config, load } from '../../../../../policy-engine.config' -import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' -import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { EngineRepository } from '../../../../persistence/repository/engine.repository' -import { EngineService } from '../../engine.service' - -describe(EngineService.name, () => { - let service: EngineService - let configService: ConfigService - - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ load: [load] })], - providers: [ - EngineService, - EngineRepository, - KeyValueService, - { - provide: KeyValueRepository, - useClass: InMemoryKeyValueRepository - } - ] - }).compile() - - service = module.get(EngineService) - configService = module.get>(ConfigService) - }) - - describe('save', () => { - const id = 'test-engine' - - const adminApiKey = secret.hash('test-admin-api-key') - - const engine = { - id, - adminApiKey, - activated: true - } - - it('returns the given secret key', async () => { - const actualEngine = await service.save(engine) - - expect(actualEngine.adminApiKey).toEqual(engine.adminApiKey) - }) - - // IMPORTANT: The admin API key is hashed by the caller not the service. That - // allows us to have a determistic configuration file which is useful for - // automations like development or cloud set up. - it('does not hash the admin api key', async () => { - jest.spyOn(configService, 'get').mockReturnValue(id) - - await service.save(engine) - - const actualEngine = await service.getEngine() - - expect(actualEngine?.adminApiKey).toEqual(adminApiKey) - }) - }) -}) diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts index 1f504b460..0aea1375d 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts @@ -1,22 +1,18 @@ import { ConfigModule } from '@narval/config-module' -import { EncryptionException, EncryptionService } from '@narval/encryption-module' +import { EncryptionService } from '@narval/encryption-module' import { LoggerModule } from '@narval/nestjs-shared' import { HttpSource, SourceType } from '@narval/policy-engine-shared' -import { Alg, privateKeyToJwk } from '@narval/signature' +import { Alg, SigningAlg, privateKeyToJwk, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' import { Test } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' import { generatePrivateKey } from 'viem/accounts' -import { EngineService } from '../../../../../engine/core/service/engine.service' -import { EngineRepository } from '../../../../../engine/persistence/repository/engine.repository' +import { BootstrapService } from '../../../../../client/core/service/bootstrap.service' +import { ClientService } from '../../../../../client/core/service/client.service' import { load } from '../../../../../policy-engine.config' import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' import { Client } from '../../../../../shared/type/domain.type' -import { BootstrapException } from '../../../exception/bootstrap.exception' -import { BootstrapService } from '../../bootstrap.service' -import { ClientService } from '../../client.service' describe(BootstrapService.name, () => { let bootstrapService: BootstrapService @@ -41,23 +37,53 @@ describe(BootstrapService.name, () => { } } + const clientOneSignerKey = generatePrivateKey() const clientOne: Client = { dataStore, clientId: 'test-client-one-id', - clientSecret: 'unsafe-client-secret', - signer: { - privateKey: privateKeyToJwk(generatePrivateKey(), Alg.ES256K) + name: 'test-client-one', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: 'unsafe-client-secret' + } + }, + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'test-key-id', + publicKey: secp256k1PrivateKeyToPublicJwk(clientOneSignerKey), + privateKey: privateKeyToJwk(clientOneSignerKey, Alg.ES256K) + } }, createdAt: new Date(), updatedAt: new Date() } + const clientTwoSignerKey = generatePrivateKey() const clientTwo: Client = { dataStore, clientId: 'test-client-two-id', - clientSecret: 'unsafe-client-secret', - signer: { - privateKey: privateKeyToJwk(generatePrivateKey(), Alg.ES256K) + name: 'test-client-two', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: 'unsafe-client-secret' + } + }, + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'test-key-id-2', + publicKey: secp256k1PrivateKeyToPublicJwk(clientTwoSignerKey), + privateKey: privateKeyToJwk(clientTwoSignerKey, Alg.ES256K) + } }, createdAt: new Date(), updatedAt: new Date() @@ -80,9 +106,6 @@ describe(BootstrapService.name, () => { ], providers: [ BootstrapService, - EngineRepository, - EngineService, - KeyValueService, { provide: KeyValueRepository, useClass: InMemoryKeyValueRepository @@ -108,20 +131,5 @@ describe(BootstrapService.name, () => { expect(clientServiceMock.syncDataStore).toHaveBeenNthCalledWith(1, clientOne.clientId) expect(clientServiceMock.syncDataStore).toHaveBeenNthCalledWith(2, clientTwo.clientId) }) - - it('checks if the encryption keyring is configured', async () => { - await bootstrapService.boot() - - expect(encryptionServiceMock.getKeyring).toHaveBeenCalledTimes(1) - }) - - it('throws when encryption keyring is not configure', async () => { - encryptionServiceMock.getKeyring.mockImplementation(() => { - throw new EncryptionException('Something went wrong') - }) - - await expect(() => bootstrapService.boot()).rejects.toThrow(BootstrapException) - await expect(() => bootstrapService.boot()).rejects.toThrow('Encryption keyring not found') - }) }) }) diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts index 7aead373c..d61ce82c2 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts @@ -1,23 +1,23 @@ import { EncryptionModule } from '@narval/encryption-module' -import { LoggerModule, StatefulTraceService, TraceService, secret } from '@narval/nestjs-shared' +import { LoggerModule, StatefulTraceService, TraceService } from '@narval/nestjs-shared' import { DataStoreConfiguration, FIXTURE, HttpSource, SourceType } from '@narval/policy-engine-shared' -import { Alg, getPublicKey, privateKeyToJwk } from '@narval/signature' +import { Alg, SigningAlg, getPublicKey, privateKeyToJwk, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' import { Test } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' import { generatePrivateKey } from 'viem/accounts' +import { ClientService } from '../../../../../client/core/service/client.service' +import { ClientRepository } from '../../../../../client/persistence/repository/client.repository' import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service' import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' import { Client } from '../../../../../shared/type/domain.type' -import { ClientRepository } from '../../../../persistence/repository/client.repository' -import { ClientService } from '../../client.service' import { DataStoreService } from '../../data-store.service' import { SimpleSigningService } from '../../signing-basic.service' describe(ClientService.name, () => { let clientService: ClientService - let clientRepository: ClientRepository + let clientRepositoryMock: MockProxy let dataStoreServiceMock: MockProxy const clientId = 'test-client-id' @@ -33,15 +33,30 @@ describe(ClientService.name, () => { keys: [getPublicKey(privateKeyToJwk(generatePrivateKey()))] } + const clientSignerKey = generatePrivateKey() const client: Client = { clientId, - clientSecret: secret.hash('test-client-secret'), + name: 'test-client', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: 'unsafe-client-secret' + } + }, dataStore: { entity: dataStoreConfiguration, policy: dataStoreConfiguration }, - signer: { - privateKey: privateKeyToJwk(generatePrivateKey(), Alg.ES256K) + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'test-key-id', + publicKey: secp256k1PrivateKeyToPublicJwk(clientSignerKey), + privateKey: privateKeyToJwk(clientSignerKey, Alg.ES256K) + } }, createdAt: new Date(), updatedAt: new Date() @@ -60,6 +75,7 @@ describe(ClientService.name, () => { beforeEach(async () => { dataStoreServiceMock = mock() + clientRepositoryMock = mock() dataStoreServiceMock.fetch.mockResolvedValue(stores) const module = await Test.createTestingModule({ @@ -71,7 +87,6 @@ describe(ClientService.name, () => { ], providers: [ ClientService, - ClientRepository, EncryptKeyValueService, { provide: DataStoreService, @@ -88,37 +103,35 @@ describe(ClientService.name, () => { { provide: TraceService, useClass: StatefulTraceService + }, + { + provide: ClientRepository, + useValue: clientRepositoryMock } ] }).compile() clientService = module.get(ClientService) - clientRepository = module.get(ClientRepository) }) describe('save', () => { + beforeEach(async () => { + clientRepositoryMock.save.mockResolvedValue(client) + clientRepositoryMock.findById.mockResolvedValue(client) + }) it('does not hash the client secret because it is already hashed', async () => { await clientService.save(client) const actualClient = await clientService.findById(client.clientId) - expect(actualClient?.clientSecret).toEqual(client.clientSecret) + expect(actualClient?.auth.local?.clientSecret).toEqual(client.auth.local?.clientSecret) }) }) describe('syncDataStore', () => { beforeEach(async () => { - await clientRepository.save(client) - }) - - it('saves entity and policy stores', async () => { - expect(await clientRepository.findEntityStore(clientId)).toEqual(null) - expect(await clientRepository.findPolicyStore(clientId)).toEqual(null) - - await clientService.syncDataStore(clientId) - - expect(await clientRepository.findEntityStore(clientId)).toEqual(stores.entity) - expect(await clientRepository.findPolicyStore(clientId)).toEqual(stores.policy) + clientRepositoryMock.save.mockResolvedValue(client) + clientRepositoryMock.findById.mockResolvedValue(client) }) it('fetches the data stores once', async () => { diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts index 92ba59653..51e8e8300 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts @@ -1,14 +1,26 @@ +import { LoggerModule, OpenTelemetryModule } from '@narval/nestjs-shared' import { + Action, + DataStoreConfiguration, Decision, + EntityUtil, + EvaluationRequest, EvaluationResponse, FIXTURE, GrantPermissionAction, Request, SignTransactionAction } from '@narval/policy-engine-shared' -import { nowSeconds } from '@narval/signature' +import { Alg, PrivateKey, SigningAlg, decodeJwt, generateJwk, getPublicKey } from '@narval/signature' +import { Test } from '@nestjs/testing' +import { MockProxy, mock } from 'jest-mock-extended' +import { ClientService } from '../../../../../client/core/service/client.service' +import { OpenPolicyAgentEngine } from '../../../../../open-policy-agent/core/open-policy-agent.engine' import { ApplicationException } from '../../../../../shared/exception/application.exception' -import { buildPermitTokenPayload } from '../../evaluation.service' +import { Client } from '../../../../../shared/type/domain.type' +import { OpenPolicyAgentEngineFactory } from '../../../factory/open-policy-agent-engine.factory' +import { EvaluationService, buildPermitTokenPayload } from '../../evaluation.service' +import { SimpleSigningService } from '../../signing-basic.service' const ONE_DAY = 86400 @@ -48,27 +60,26 @@ describe('buildPermitTokenPayload for signTransaction action', () => { request } - it('should throw an error if decision is not PERMIT', async () => { + it('throws an error if decision is not PERMIT', async () => { const evaluation: EvaluationResponse = { ...evalResponse, decision: Decision.FORBID } await expect(buildPermitTokenPayload('clientId', evaluation)).rejects.toThrow(ApplicationException) }) - it('should throw an error if principal is missing', async () => { + it('throws an error if principal is missing', async () => { const evaluation: EvaluationResponse = { ...evalResponse, principal: undefined } await expect(buildPermitTokenPayload('clientId', evaluation)).rejects.toThrow(ApplicationException) }) - it('should throw an error if request is missing', async () => { + it('throws an error if request is missing', async () => { const evaluation: EvaluationResponse = { ...evalResponse, request: undefined } await expect(buildPermitTokenPayload('clientId', evaluation)).rejects.toThrow(ApplicationException) }) - it('should return a jwt payload if all conditions are met', async () => { + it('returns a jwt payload if all conditions are met', async () => { const payload = await buildPermitTokenPayload('clientId', evalResponse) expect(payload).toEqual({ cnf, - iat: nowSeconds(), requestHash: '0x608abe908cffeab1fc33edde6b44586f9dacbc9c6fe6f0a13fa307237290ce5a', hashWildcard: [ 'transactionRequest.gas', @@ -96,29 +107,26 @@ describe('buildPermitTokenPayload for grantPermission action', () => { issuer: 'clientId.armory.narval.xyz' } } - it('should throw an error if decision is not PERMIT', async () => { + it('throws an error if decision is not PERMIT', async () => { const evaluation: EvaluationResponse = { ...evalResponse, decision: Decision.FORBID } await expect(buildPermitTokenPayload('clientId', evaluation)).rejects.toThrow(ApplicationException) }) - it('should throw an error if principal is missing', async () => { + it('throws an error if principal is missing', async () => { const evaluation: EvaluationResponse = { ...evalResponse, principal: undefined } await expect(buildPermitTokenPayload('clientId', evaluation)).rejects.toThrow(ApplicationException) }) - it('should throw an error if request is missing', async () => { + it('throws an error if request is missing', async () => { const evaluation: EvaluationResponse = { ...evalResponse, request: undefined } await expect(buildPermitTokenPayload('clientId', evaluation)).rejects.toThrow(ApplicationException) }) - it('should return a jwt payload if all conditions are met', async () => { + it('returns a jwt payload if all conditions are met', async () => { const payload = await buildPermitTokenPayload('clientId', evalResponse) - const iat = nowSeconds() expect(payload).toEqual({ cnf, - iat, - exp: iat + ONE_DAY, iss: 'clientId.armory.narval.xyz', sub: 'test-alice-user-uid', access: [ @@ -130,3 +138,152 @@ describe('buildPermitTokenPayload for grantPermission action', () => { }) }) }) + +describe(EvaluationService.name, () => { + let service: EvaluationService + let client: Client + let clientSignerPrivateKey: PrivateKey + let openPolicyAgentEngineMock: MockProxy + + const evaluationResponse: EvaluationResponse = { + decision: Decision.PERMIT, + request: { + action: Action.SIGN_MESSAGE, + nonce: '99', + resourceId: 'test-resource-id', + message: 'sign me' + }, + accessToken: { + value: '' + }, + principal: FIXTURE.CREDENTIAL.Bob + } + + beforeEach(async () => { + clientSignerPrivateKey = await generateJwk(Alg.ES256K) + + client = { + clientId: 'test-client-id', + name: 'test-client-name', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: 'test-client-secret' + } + }, + dataStore: { + entity: {} as DataStoreConfiguration, + policy: {} as DataStoreConfiguration + }, + decisionAttestation: { + disabled: false, + signer: { + publicKey: getPublicKey(clientSignerPrivateKey), + privateKey: clientSignerPrivateKey, + alg: SigningAlg.EIP191, + keyId: 'test-key-id' + } + }, + createdAt: new Date(), + updatedAt: new Date() + } + + openPolicyAgentEngineMock = mock() + openPolicyAgentEngineMock.evaluate.mockResolvedValue(evaluationResponse) + + const openPolicyAgentEngineFactoryMock = mock() + openPolicyAgentEngineFactoryMock.create.mockResolvedValue(openPolicyAgentEngineMock) + + const clientServiceMock = mock() + clientServiceMock.findById.mockResolvedValue(client) + clientServiceMock.findEntityStore.mockResolvedValue({ + data: EntityUtil.empty(), + signature: '' + }) + clientServiceMock.findPolicyStore.mockResolvedValue({ + data: [], + signature: '' + }) + + const module = await Test.createTestingModule({ + imports: [LoggerModule.forTest(), OpenTelemetryModule.forTest()], + providers: [ + EvaluationService, + { + provide: OpenPolicyAgentEngineFactory, + useValue: openPolicyAgentEngineFactoryMock + }, + { + provide: 'SigningService', + useClass: SimpleSigningService + }, + { + provide: ClientService, + useValue: clientServiceMock + } + ] + }).compile() + + service = module.get(EvaluationService) + }) + + describe('evaluate', () => { + describe('when confirmation (cnf) claim metadata is given', () => { + let bindPrivateKey: PrivateKey + + let request: EvaluationRequest + let response: EvaluationResponse + + beforeEach(async () => { + bindPrivateKey = await generateJwk(Alg.EDDSA) + + const action = { + action: Action.SIGN_MESSAGE, + nonce: '99', + resourceId: 'test-resource-id', + message: 'sign me' + } + + request = { + authentication: 'fake-jwt', + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKey) + } + } + }, + approvals: [], + request: action, + feeds: [] + } + + response = { + decision: Decision.PERMIT, + request: action, + metadata: request.metadata, + accessToken: { + value: '' + }, + principal: FIXTURE.CREDENTIAL.Bob + } + + openPolicyAgentEngineMock.evaluate.mockResolvedValue(response) + }) + + it('adds the public key as the cnf in the access token', async () => { + const response = await service.evaluate(client.clientId, request) + + if (response.accessToken) { + const jwt = decodeJwt(response.accessToken.value) + + expect(jwt.payload.cnf).toEqual(getPublicKey(bindPrivateKey)) + } else { + fail('expect response to contain an access token') + } + }) + }) + }) +}) diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/provision.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/provision.service.spec.ts deleted file mode 100644 index ff08fba2c..000000000 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/provision.service.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { ConfigService } from '@narval/config-module' -import { LoggerModule, secret } from '@narval/nestjs-shared' -import { Test, TestingModule } from '@nestjs/testing' -import { MockProxy, mock } from 'jest-mock-extended' -import { Config } from '../../../../../policy-engine.config' -import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' -import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { EngineRepository } from '../../../../persistence/repository/engine.repository' -import { EngineService } from '../../engine.service' -import { ProvisionService } from '../../provision.service' - -const mockConfigService = (config: { keyring: Config['keyring']; engineId: string; adminApiKeyHash?: string }) => { - const m = mock>() - - m.get.calledWith('keyring').mockReturnValue(config.keyring) - m.get.calledWith('engine.id').mockReturnValue(config.engineId) - m.get.calledWith('engine.adminApiKeyHash').mockReturnValue(config.adminApiKeyHash) - - return m -} - -describe(ProvisionService.name, () => { - let module: TestingModule - let provisionService: ProvisionService - let engineService: EngineService - let configServiceMock: MockProxy> - - const config = { - engineId: 'test-engine-id', - keyring: { - type: 'raw', - masterPassword: 'test-master-password' - } satisfies Config['keyring'] - } - - beforeEach(async () => { - configServiceMock = mockConfigService(config) - - module = await Test.createTestingModule({ - imports: [LoggerModule.forTest()], - providers: [ - ProvisionService, - EngineService, - EngineRepository, - KeyValueService, - { - provide: ConfigService, - useValue: configServiceMock - }, - { - provide: KeyValueRepository, - useClass: InMemoryKeyValueRepository - } - ] - }).compile() - - provisionService = module.get(ProvisionService) - engineService = module.get(EngineService) - }) - - describe('on first boot', () => { - describe('when admin api key is set', () => { - const adminApiKey = 'test-admin-api-key' - - beforeEach(async () => { - configServiceMock.get.calledWith('engine.adminApiKeyHash').mockReturnValue(secret.hash(adminApiKey)) - }) - - it('saves the activated engine', async () => { - await provisionService.provision() - - const actualEngine = await engineService.getEngine() - - expect(actualEngine).toEqual({ - id: config.engineId, - adminApiKey: secret.hash(adminApiKey), - masterKey: expect.any(String) - }) - }) - }) - - describe('when admin api key is not set', () => { - it('saves the provisioned engine', async () => { - await provisionService.provision() - - const actualEngine = await engineService.getEngine() - - expect(actualEngine?.adminApiKey).toEqual(undefined) - expect(actualEngine).toEqual({ - id: config.engineId, - masterKey: expect.any(String) - }) - }) - }) - }) - - describe('on boot', () => { - it('skips provision and returns the existing engine', async () => { - const actualEngine = await engineService.save({ - id: config.engineId, - masterKey: 'test-master-key' - }) - - const engine = await provisionService.provision() - - expect(actualEngine).toEqual(engine) - }) - }) -}) diff --git a/apps/policy-engine/src/engine/core/service/bootstrap.service.ts b/apps/policy-engine/src/engine/core/service/bootstrap.service.ts deleted file mode 100644 index 6a68d277d..000000000 --- a/apps/policy-engine/src/engine/core/service/bootstrap.service.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { EncryptionService } from '@narval/encryption-module' -import { LoggerService } from '@narval/nestjs-shared' -import { Injectable } from '@nestjs/common' -import { BootstrapException } from '../exception/bootstrap.exception' -import { ClientService } from './client.service' - -@Injectable() -export class BootstrapService { - constructor( - private clientService: ClientService, - private encryptionService: EncryptionService, - private logger: LoggerService - ) {} - - async boot(): Promise { - this.logger.log('Start bootstrap') - - await this.checkEncryptionConfiguration() - await this.syncClients() - - this.logger.log('Bootstrap end') - } - - private async checkEncryptionConfiguration(): Promise { - this.logger.log('Check encryption configuration') - - try { - this.encryptionService.getKeyring() - this.logger.log('Encryption keyring configured') - } catch (error) { - throw new BootstrapException('Encryption keyring not found', { origin: error }) - } - } - - private async syncClients(): Promise { - const clients = await this.clientService.findAll() - - this.logger.log('Start syncing clients data stores', { - clientsCount: clients.length - }) - - // TODO: (@wcalderipe, 07/03/24) maybe change the execution to parallel? - for (const client of clients) { - await this.clientService.syncDataStore(client.clientId) - this.logger.log(`Client public key`, { clientId: client.clientId, publicKey: client.signer.publicKey }) - } - } -} diff --git a/apps/policy-engine/src/engine/core/service/data-store.service.ts b/apps/policy-engine/src/engine/core/service/data-store.service.ts index cefce5330..63237a773 100644 --- a/apps/policy-engine/src/engine/core/service/data-store.service.ts +++ b/apps/policy-engine/src/engine/core/service/data-store.service.ts @@ -153,7 +153,7 @@ export class DataStoreService { }): Promise<{ success: true } | { success: false; error: DataStoreException }> { try { const jwt = decodeJwt(params.signature) - const jwk = params.keys.find(({ kid }) => kid === jwt.header.kid) + const jwk = params.keys.find(({ kid }) => kid?.toLowerCase() === jwt.header.kid?.toLowerCase()) if (!jwk) { return { diff --git a/apps/policy-engine/src/engine/core/service/engine.service.ts b/apps/policy-engine/src/engine/core/service/engine.service.ts index 321030f37..8af7306c5 100644 --- a/apps/policy-engine/src/engine/core/service/engine.service.ts +++ b/apps/policy-engine/src/engine/core/service/engine.service.ts @@ -1,15 +1,18 @@ import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' import { Injectable } from '@nestjs/common' import { Config } from '../../../policy-engine.config' import { Engine } from '../../../shared/type/domain.type' import { EngineRepository } from '../../persistence/repository/engine.repository' import { EngineNotProvisionedException } from '../exception/engine-not-provisioned.exception' +import { ProvisionException } from '../exception/provision.exception' @Injectable() export class EngineService { constructor( private configService: ConfigService, - private engineRepository: EngineRepository + private engineRepository: EngineRepository, + private logger: LoggerService ) {} async getEngineOrThrow(): Promise { @@ -23,10 +26,15 @@ export class EngineService { } async getEngine(): Promise { - const engine = await this.engineRepository.findById(this.getId()) + const apps = await this.engineRepository.findAll() - if (engine) { - return engine + const app = apps?.find((app) => app.id === this.getId()) + if (apps?.length && apps.length > 1) { + throw new ProvisionException('Multiple app instances found; this can lead to data corruption') + } + + if (app) { + return app } return null @@ -42,6 +50,26 @@ export class EngineService { } private getId(): string { - return this.configService.get('engine.id') + return this.configService.get('app.id') + } + + /** Temporary migration function, converting the key-value format of the App config into the table format */ + async migrateV1Data(): Promise { + const appV1 = await this.engineRepository.findByIdV1(this.getId()) + const appV2 = await this.engineRepository.findById(this.getId()) + if (appV1 && !appV2) { + this.logger.log('Migrating App V1 data to V2') + const keyring = this.configService.get('keyring') + const app = Engine.parse({ + id: appV1.id, + adminApiKeyHash: appV1.adminApiKey, + encryptionMasterKey: appV1.masterKey, + encryptionKeyringType: appV1.masterKey ? 'raw' : 'awskms', + encryptionMasterAwsKmsArn: keyring.type === 'awskms' ? keyring.encryptionMasterAwsKmsArn : null, + authDisabled: false + }) + await this.engineRepository.save(app) + this.logger.log('App V1 data migrated to V2 Successfully') + } } } diff --git a/apps/policy-engine/src/engine/core/service/evaluation.service.ts b/apps/policy-engine/src/engine/core/service/evaluation.service.ts index c47732af4..8e6a89daa 100644 --- a/apps/policy-engine/src/engine/core/service/evaluation.service.ts +++ b/apps/policy-engine/src/engine/core/service/evaluation.service.ts @@ -1,14 +1,11 @@ -import { ConfigService } from '@narval/config-module' import { TraceService } from '@narval/nestjs-shared' import { Action, Decision, EvaluationRequest, EvaluationResponse } from '@narval/policy-engine-shared' -import { Payload, SigningAlg, hash, nowSeconds, signJwt } from '@narval/signature' +import { Payload, hash, signJwt } from '@narval/signature' import { HttpStatus, Inject, Injectable } from '@nestjs/common' -import { resolve } from 'path' -import { OpenPolicyAgentEngine } from '../../../open-policy-agent/core/open-policy-agent.engine' -import { Config } from '../../../policy-engine.config' +import { ClientService } from '../../../client/core/service/client.service' import { ApplicationException } from '../../../shared/exception/application.exception' +import { OpenPolicyAgentEngineFactory } from '../factory/open-policy-agent-engine.factory' import { buildTransactionRequestHashWildcard } from '../util/wildcard-transaction-fields.util' -import { ClientService } from './client.service' import { SigningService } from './signing.service.interface' export async function buildPermitTokenPayload(clientId: string, evaluation: EvaluationResponse): Promise { @@ -19,6 +16,7 @@ export async function buildPermitTokenPayload(clientId: string, evaluation: Eval context: { clientId } }) } + if (!evaluation.principal) { throw new ApplicationException({ message: 'Principal is missing', @@ -26,6 +24,7 @@ export async function buildPermitTokenPayload(clientId: string, evaluation: Eval context: { clientId } }) } + if (!evaluation.request) { throw new ApplicationException({ message: 'Request is missing', @@ -35,20 +34,36 @@ export async function buildPermitTokenPayload(clientId: string, evaluation: Eval } const { audience, issuer } = evaluation.metadata || {} - const iat = evaluation.metadata?.issuedAt || nowSeconds() - const exp = evaluation.metadata?.expiresIn ? evaluation.metadata.expiresIn + iat : null - + const iat = evaluation.metadata?.issuedAt + const exp = evaluation.metadata?.expiresIn && iat ? evaluation.metadata.expiresIn + iat : null const hashWildcard = buildTransactionRequestHashWildcard(evaluation.request) const payload: Payload = { - sub: evaluation.principal.userId, + // jti: TODO iat, + hashWildcard, + /** + * Confirmation (cnf) claim handling for token binding: + * + * Two scenarios are supported: + * 1. Standard flow: Uses principal's key as the confirmation claim + * 2. Delegation flow: Uses a provided confirmation key for token binding + * + * The delegation flow enables secure token delegation where: + * - A client generates a key pair + * - Passes the public key (metadata.confirmation) to the server + * - Server issues a token bound to this specific key with the auth server + * - Only the holder of the corresponding private key can use the token + * with the resource server + * + * The cnf claim is used in the JWSD header to cryptographically bind the + * token to a specific key. + */ + cnf: evaluation.metadata?.confirmation?.key.jwk || evaluation.principal.key, + sub: evaluation.principal.userId, ...(exp && { exp }), ...(audience && { aud: audience }), - ...(issuer && { iss: issuer }), - hashWildcard, - // jti: TODO - cnf: evaluation.principal.key + ...(issuer && { iss: issuer }) } // Action-specific payload claims @@ -73,10 +88,10 @@ export async function buildPermitTokenPayload(clientId: string, evaluation: Eval @Injectable() export class EvaluationService { constructor( - private configService: ConfigService, - private clientService: ClientService, - @Inject(TraceService) private traceService: TraceService, - @Inject('SigningService') private signingService: SigningService + private readonly clientService: ClientService, + private readonly openPolicyAgentEngineFactory: OpenPolicyAgentEngineFactory, + @Inject(TraceService) private readonly traceService: TraceService, + @Inject('SigningService') private readonly signingService: SigningService ) {} async evaluate(clientId: string, evaluation: EvaluationRequest): Promise { @@ -90,7 +105,7 @@ export class EvaluationService { }) } - if (!client.signer?.publicKey) { + if (!client.decisionAttestation?.signer?.publicKey) { throw new ApplicationException({ message: 'Client signer is not configured', suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, @@ -121,16 +136,9 @@ export class EvaluationService { }) } - const engineLoadSpan = this.traceService.startSpan(`${EvaluationService.name}.evaluate.engineLoad`) // WARN: Loading a new engine is an IO bounded process due to the Rego // transpilation and WASM build. - const engine = await new OpenPolicyAgentEngine({ - entities: entityStore.data, - policies: policyStore.data, - resourcePath: resolve(this.configService.get('resourcePath')), - tracer: this.traceService.getTracer() - }).load() - engineLoadSpan.end() + const engine = await this.openPolicyAgentEngineFactory.create(entityStore.data, policyStore.data) const engineEvaluationSpan = this.traceService.startSpan(`${EvaluationService.name}.evaluate.engineEvaluation`) const evaluationResponse = await engine.evaluate(evaluation) @@ -143,9 +151,9 @@ export class EvaluationService { const jwt = await signJwt( jwtPayload, - client.signer.publicKey, - { alg: SigningAlg.EIP191 }, - this.signingService.buildSignerEip191(client.signer, evaluation.sessionId) + client.decisionAttestation.signer.publicKey, + { alg: client.decisionAttestation.signer.alg }, + this.signingService.buildSignerEip191(client.decisionAttestation.signer, evaluation.sessionId) // TODO: non-EIP191 ) buildAccessTokenSpan.end() diff --git a/apps/policy-engine/src/engine/core/service/provision.service.ts b/apps/policy-engine/src/engine/core/service/provision.service.ts index e44e98f67..f32cfce39 100644 --- a/apps/policy-engine/src/engine/core/service/provision.service.ts +++ b/apps/policy-engine/src/engine/core/service/provision.service.ts @@ -1,6 +1,7 @@ import { ConfigService } from '@narval/config-module' -import { generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module' +import { decryptMasterKey, generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module' import { LoggerService } from '@narval/nestjs-shared' +import { toBytes } from '@narval/policy-engine-shared' import { Injectable } from '@nestjs/common' import { Config } from '../../../policy-engine.config' import { Engine } from '../../../shared/type/domain.type' @@ -20,65 +21,141 @@ export class ProvisionService { private logger: LoggerService ) {} - // NOTE: The `adminApiKeyHash` argument is for test convinience in case it + // Provision the application if it's not already provisioned. + // Update configuration if needed. + // 1. Check if we have an App reference; App is initialized once. + // 2. Check if we have Encryption set up; Encryption is initialized once. + // 3. Auth can be updated, so change it if it's changed. + + // NOTE: The `adminApiKeyHash` argument is for test convenience in case it // needs to provision the application. async provision(adminApiKeyHash?: string): Promise { - const engine = await this.engineService.getEngine() - - const isNotProvisioned = !engine || !engine.adminApiKey - - if (isNotProvisioned) { - this.logger.log('Start app provision') - const provisionedEngine: Engine = await this.withMasterKey(engine || { id: this.getId() }) - - const apiKey = adminApiKeyHash || this.getAdminApiKeyHash() - - if (apiKey) { - return this.engineService.save({ - ...provisionedEngine, - adminApiKey: apiKey - }) + return this.run({ + adminApiKeyHash, + setupEncryption: async (app, thisApp, keyring) => { + await this.setupEncryption(app, thisApp, keyring) + await this.verifyRawEncryption(thisApp, keyring) } + }) + } - return this.engineService.save(provisionedEngine) - } + /** + * Core provisioning logic that sets up or updates the application + * configuration. This includes handling encryption setup and admin + * authentication settings. + * + * @param params Configuration parameters for the provisioning process + * + * @param params.adminApiKeyHash Optional hash of the admin API key for + * authentication + * + * @param params.setupEncryption Optional callback to customize encryption + * setup. Receives current app state, new app config, and keyring + * configuration. **Useful for disabling encryption during tests.** + * + * @returns Promise The provisioned application configuration + * + * @throws ProvisionException If app is already provisioned with different ID + * or if encryption setup fails + */ + protected async run(params: { + adminApiKeyHash?: string + setupEncryption?: (app: Engine | null, thisApp: Engine, keyring: Config['keyring']) => Promise + }): Promise { + const { adminApiKeyHash, setupEncryption } = params + + // TEMPORARY: Migrate the key-value format of the App config into the table format. + // Can be removed once this runs once. + await this.engineService.migrateV1Data() + + // Actually provision the new one; will not overwrite anything if this started from a migration + const app = await this.engineService.getEngine() + const keyring = this.configService.get('keyring') - this.logger.log('App already provisioned') + const thisApp: Engine = { ...(app || { id: this.getId(), encryptionKeyringType: keyring.type }) } + if (app && app.id !== this.getId()) { + throw new ProvisionException('App already provisioned with a different ID', { + current: this.getId(), + saved: app.id + }) + } - return engine - } + if (setupEncryption) { + await setupEncryption(app, thisApp, keyring) + } - private async withMasterKey(engine: Engine): Promise { - if (engine.masterKey) { - this.logger.log('Skip master key set up because it already exists') + // Now set the Auth if needed + thisApp.adminApiKeyHash = adminApiKeyHash || this.getAdminApiKeyHash() || null // fallback to null so we _unset_ it if it's not provided. - return engine + // If we have an app already & the adminApiKeyHash has changed, just log that we're changing it. + if (app && thisApp.adminApiKeyHash !== app?.adminApiKeyHash) { + this.logger.log('Admin API Key has been changed', { + previous: app?.adminApiKeyHash, + current: thisApp.adminApiKeyHash + }) } - const keyring = this.configService.get('keyring') - - if (keyring.type === 'raw') { - this.logger.log('Generate and save engine master key') + // Check if we disabled all auth + thisApp.authDisabled = this.configService.get('app.auth.disabled') + if (thisApp.authDisabled) { + thisApp.adminApiKeyHash = null + } - const { masterPassword } = keyring - const kek = generateKeyEncryptionKey(masterPassword, this.getId()) - const masterKey = await generateMasterKey(kek) + this.logger.log('App configuration saved') - return { ...engine, masterKey } - } else if (keyring.type === 'awskms' && keyring.masterAwsKmsArn) { - this.logger.log('Using AWS KMS for encryption') + return this.engineService.save(thisApp) + } - return engine - } else { - throw new ProvisionException('Unsupported keyring type') + protected async setupEncryption(app: Engine | null, thisApp: Engine, keyring: Config['keyring']) { + // No encryption set up yet, so initialize encryption + if (!app?.encryptionKeyringType || (!app.encryptionMasterKey && !app.encryptionMasterAwsKmsArn)) { + thisApp.encryptionKeyringType = keyring.type + + if (keyring.type === 'awskms' && keyring.encryptionMasterAwsKmsArn) { + this.logger.log('Using AWS KMS for encryption') + thisApp.encryptionMasterAwsKmsArn = keyring.encryptionMasterAwsKmsArn + } else if (keyring.type === 'raw') { + // If we have the masterKey set in config, we'll save that. + // Otherwise, we'll generate a new one. + if (keyring.encryptionMasterKey) { + this.logger.log('Using provided master key') + thisApp.encryptionMasterKey = keyring.encryptionMasterKey + } else { + this.logger.log('Generating master encryption key') + const { encryptionMasterPassword } = keyring + const kek = generateKeyEncryptionKey(encryptionMasterPassword, thisApp.id) + const masterKey = await generateMasterKey(kek) // Encrypted master encryption key + thisApp.encryptionMasterKey = masterKey + } + } else { + throw new ProvisionException('Unsupported keyring type') + } } } - private getAdminApiKeyHash(): string | undefined { - return this.configService.get('engine.adminApiKeyHash') + protected async verifyRawEncryption(thisApp: Engine, keyring: Config['keyring']) { + // if raw encryption, verify the encryptionMasterPassword in config is the valid kek for the encryptionMasterKey + if (thisApp?.encryptionKeyringType === 'raw' && keyring.type === 'raw' && thisApp.encryptionMasterKey) { + try { + const kek = generateKeyEncryptionKey(keyring.encryptionMasterPassword, thisApp.id) + await decryptMasterKey(kek, toBytes(thisApp.encryptionMasterKey)) + this.logger.log('Master Encryption Key Verified') + } catch (error) { + this.logger.error( + 'Master Encryption Key Verification Failed; check the encryptionMasterPassword is the one that encrypted the masterKey', + { error } + ) + throw new ProvisionException('Master Encryption Key Verification Failed', { error }) + } + } } + private getAdminApiKeyHash(): string | null | undefined { + const localAuth = this.configService.get('app.auth.local') + + return localAuth?.adminApiKeyHash + } private getId(): string { - return this.configService.get('engine.id') + return this.configService.get('app.id') } } diff --git a/apps/policy-engine/src/engine/engine.module.ts b/apps/policy-engine/src/engine/engine.module.ts index 7b52f6b29..3577f11f8 100644 --- a/apps/policy-engine/src/engine/engine.module.ts +++ b/apps/policy-engine/src/engine/engine.module.ts @@ -1,53 +1,30 @@ import { ConfigService } from '@narval/config-module' -import { EncryptionModule } from '@narval/encryption-module' -import { HttpModule, LoggerService } from '@narval/nestjs-shared' +import { HttpModule } from '@narval/nestjs-shared' import { Module, ValidationPipe } from '@nestjs/common' import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core' import { ZodSerializerInterceptor, ZodValidationPipe } from 'nestjs-zod' -import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory' +import { ClientModule } from '../client/client.module' import { AdminApiKeyGuard } from '../shared/guard/admin-api-key.guard' import { KeyValueModule } from '../shared/module/key-value/key-value.module' +import { PersistenceModule } from '../shared/module/persistence/persistence.module' import { AppController } from './app.controller' -import { DataStoreRepositoryFactory } from './core/factory/data-store-repository.factory' +import { OpenPolicyAgentEngineFactory } from './core/factory/open-policy-agent-engine.factory' import { signingServiceFactory } from './core/factory/signing-service.factory' -import { BootstrapService } from './core/service/bootstrap.service' -import { ClientService } from './core/service/client.service' -import { DataStoreService } from './core/service/data-store.service' import { EngineService } from './core/service/engine.service' import { EvaluationService } from './core/service/evaluation.service' -import { ProvisionService } from './core/service/provision.service' -import { ClientController } from './http/rest/controller/client.controller' import { EvaluationController } from './http/rest/controller/evaluation.controller' -import { ProvisionController } from './http/rest/controller/provision.controller' -import { ClientRepository } from './persistence/repository/client.repository' import { EngineRepository } from './persistence/repository/engine.repository' -import { FileSystemDataStoreRepository } from './persistence/repository/file-system-data-store.repository' -import { HttpDataStoreRepository } from './persistence/repository/http-data-store.repository' @Module({ - imports: [ - HttpModule.forRoot(), - KeyValueModule, - EncryptionModule.registerAsync({ - imports: [EngineModule], - inject: [ConfigService, EngineService, LoggerService], - useClass: EncryptionModuleOptionFactory - }) - ], - controllers: [ProvisionController, AppController, ClientController, EvaluationController], + imports: [HttpModule.register(), PersistenceModule, KeyValueModule, ClientModule], + controllers: [AppController, EvaluationController], providers: [ AdminApiKeyGuard, - BootstrapService, - DataStoreRepositoryFactory, - DataStoreService, + OpenPolicyAgentEngineFactory, EngineRepository, EngineService, EvaluationService, - FileSystemDataStoreRepository, - HttpDataStoreRepository, - ProvisionService, - ClientRepository, - ClientService, + { provide: 'SigningService', useFactory: signingServiceFactory, @@ -67,6 +44,6 @@ import { HttpDataStoreRepository } from './persistence/repository/http-data-stor useClass: ZodSerializerInterceptor } ], - exports: [EngineService, ProvisionService, BootstrapService] + exports: [EngineService] }) export class EngineModule {} diff --git a/apps/policy-engine/src/engine/http/rest/controller/provision.controller.ts b/apps/policy-engine/src/engine/http/rest/controller/provision.controller.ts deleted file mode 100644 index fd3705da5..000000000 --- a/apps/policy-engine/src/engine/http/rest/controller/provision.controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { secret } from '@narval/nestjs-shared' -import { Controller, Post } from '@nestjs/common' -import { ApiExcludeController } from '@nestjs/swagger' -import { EngineService } from '../../../core/service/engine.service' - -type Response = - | { state: 'ACTIVATED' } - | { - state: 'READY' - app: { - appId: string - adminApiKey?: string - } - } - -@Controller({ - path: '/apps/activate', - version: '1' -}) -@ApiExcludeController() -export class ProvisionController { - constructor(private engineService: EngineService) {} - - @Post() - async activate(): Promise { - const engine = await this.engineService.getEngineOrThrow() - - if (engine.adminApiKey) { - return { state: 'ACTIVATED' } - } - - const adminApiKey = secret.generate() - - await this.engineService.save({ - ...engine, - adminApiKey: secret.hash(adminApiKey) - }) - - return { - state: 'READY', - app: { - appId: engine.id, - adminApiKey - } - } - } -} diff --git a/apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/client.repository.spec.ts similarity index 64% rename from apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts rename to apps/policy-engine/src/engine/persistence/repository/__test__/integration/client.repository.spec.ts index 71aeea7d8..f24781e65 100644 --- a/apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts +++ b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/client.repository.spec.ts @@ -1,25 +1,14 @@ import { EncryptionModule } from '@narval/encryption-module' -import { - Action, - Criterion, - DataStoreConfiguration, - EntityStore, - FIXTURE, - HttpSource, - PolicyStore, - SourceType, - Then -} from '@narval/policy-engine-shared' -import { Alg, getPublicKey, privateKeyToJwk } from '@narval/signature' +import { Action, Criterion, EntityStore, FIXTURE, PolicyStore, Then } from '@narval/policy-engine-shared' import { Test } from '@nestjs/testing' -import { generatePrivateKey } from 'viem/accounts' +import { ClientRepository } from '../../../../../client/persistence/repository/client.repository' import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service' import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' +import { PrismaService } from '../../../../../shared/module/persistence/service/prisma.service' +import { TestPrismaService } from '../../../../../shared/module/persistence/service/test-prisma.service' import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' -import { Client } from '../../../../../shared/type/domain.type' -import { ClientRepository } from '../../client.repository' describe(ClientRepository.name, () => { let repository: ClientRepository @@ -43,6 +32,10 @@ describe(ClientRepository.name, () => { { provide: KeyValueRepository, useValue: inMemoryKeyValueRepository + }, + { + provide: PrismaService, + useValue: TestPrismaService } ] }).compile() @@ -50,51 +43,6 @@ describe(ClientRepository.name, () => { repository = module.get(ClientRepository) }) - describe('save', () => { - const now = new Date() - - const dataStoreSource: HttpSource = { - type: SourceType.HTTP, - url: 'a-url-that-doesnt-need-to-exist-for-the-purpose-of-this-test' - } - - const dataStoreConfiguration: DataStoreConfiguration = { - data: dataStoreSource, - signature: dataStoreSource, - keys: [getPublicKey(privateKeyToJwk(generatePrivateKey(), Alg.ES256K))] - } - - const client: Client = { - clientId, - clientSecret: 'test-client-secret', - signer: { - privateKey: privateKeyToJwk(generatePrivateKey(), Alg.ES256K) - }, - dataStore: { - entity: dataStoreConfiguration, - policy: dataStoreConfiguration - }, - createdAt: now, - updatedAt: now - } - - it('saves a new client', async () => { - await repository.save(client) - - const value = await inMemoryKeyValueRepository.get(repository.getKey(client.clientId)) - const actualClient = await repository.findById(client.clientId) - - expect(value).not.toEqual(null) - expect(client).toEqual(actualClient) - }) - - it('indexes the new client', async () => { - await repository.save(client) - - expect(await repository.getClientListIndex()).toEqual([client.clientId]) - }) - }) - describe('saveEntityStore', () => { const store: EntityStore = { data: FIXTURE.ENTITIES, diff --git a/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts index 2cb5d08a3..58cd2f887 100644 --- a/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts +++ b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts @@ -24,7 +24,7 @@ describe(HttpDataStoreRepository.name, () => { beforeEach(async () => { const module = await Test.createTestingModule({ - imports: [HttpModule.forRoot(), LoggerModule.forTest()], + imports: [HttpModule.register(), LoggerModule.forTest()], providers: [HttpDataStoreRepository] }).compile() diff --git a/apps/policy-engine/src/engine/persistence/repository/__test__/unit/engine.repository.spec.ts b/apps/policy-engine/src/engine/persistence/repository/__test__/unit/engine.repository.spec.ts deleted file mode 100644 index dc514e314..000000000 --- a/apps/policy-engine/src/engine/persistence/repository/__test__/unit/engine.repository.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Test } from '@nestjs/testing' -import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' -import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { Engine } from '../../../../../shared/type/domain.type' -import { EngineRepository } from '../../engine.repository' - -describe(EngineRepository.name, () => { - let repository: EngineRepository - let inMemoryKeyValueRepository: InMemoryKeyValueRepository - - beforeEach(async () => { - inMemoryKeyValueRepository = new InMemoryKeyValueRepository() - - const module = await Test.createTestingModule({ - providers: [ - KeyValueService, - EngineRepository, - { - provide: KeyValueRepository, - useValue: inMemoryKeyValueRepository - } - ] - }).compile() - - repository = module.get(EngineRepository) - }) - - const engine: Engine = { - id: 'test-engine-id', - adminApiKey: 'unsafe-test-admin-api-key', - masterKey: 'unsafe-test-master-key' - } - - describe('save', () => { - it('saves a new engine', async () => { - await repository.save(engine) - - const value = await inMemoryKeyValueRepository.get(repository.getEngineKey(engine.id)) - const actualEngine = await repository.findById(engine.id) - - expect(value).not.toEqual(null) - expect(engine).toEqual(actualEngine) - }) - }) -}) diff --git a/apps/policy-engine/src/engine/persistence/repository/client.repository.ts b/apps/policy-engine/src/engine/persistence/repository/client.repository.ts deleted file mode 100644 index d6beec0d6..000000000 --- a/apps/policy-engine/src/engine/persistence/repository/client.repository.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { coerce } from '@narval/nestjs-shared' -import { EntityStore, PolicyStore } from '@narval/policy-engine-shared' -import { Injectable } from '@nestjs/common' -import { compact } from 'lodash/fp' -import { z } from 'zod' -import { EncryptKeyValueService } from '../../../shared/module/key-value/core/service/encrypt-key-value.service' -import { Client } from '../../../shared/type/domain.type' - -const ClientListIndex = z.array(z.string()) - -@Injectable() -export class ClientRepository { - constructor(private encryptKeyValueService: EncryptKeyValueService) {} - - async findById(clientId: string): Promise { - const value = await this.encryptKeyValueService.get(this.getKey(clientId)) - - if (value) { - return coerce.decode(Client, value) - } - - return null - } - - async save(client: Client): Promise { - await this.encryptKeyValueService.set(this.getKey(client.clientId), coerce.encode(Client, client)) - await this.index(client) - - return client - } - - async getClientListIndex(): Promise { - const index = await this.encryptKeyValueService.get(this.getIndexKey()) - - if (index) { - return coerce.decode(ClientListIndex, index) - } - - return [] - } - - async saveEntityStore(clientId: string, store: EntityStore): Promise { - return this.encryptKeyValueService.set(this.getEntityStoreKey(clientId), coerce.encode(EntityStore, store)) - } - - async findEntityStore(clientId: string): Promise { - const value = await this.encryptKeyValueService.get(this.getEntityStoreKey(clientId)) - - if (value) { - return coerce.decode(EntityStore, value) - } - - return null - } - - async savePolicyStore(clientId: string, store: PolicyStore): Promise { - return this.encryptKeyValueService.set(this.getPolicyStoreKey(clientId), coerce.encode(PolicyStore, store)) - } - - async findPolicyStore(clientId: string): Promise { - const value = await this.encryptKeyValueService.get(this.getPolicyStoreKey(clientId)) - - if (value) { - return coerce.decode(PolicyStore, value) - } - - return null - } - - // TODO: (@wcalderipe, 07/03/24) we need to rethink this strategy. If we use a - // SQL database, this could generate a massive amount of queries; thus, - // degrading the performance. - // - // An option is to move these general queries `findBy`, findAll`, etc to the - // KeyValeuRepository implementation letting each implementation pick the best - // strategy to solve the problem (e.g. where query in SQL) - async findAll(): Promise { - const ids = await this.getClientListIndex() - const clients = await Promise.all(ids.map((id) => this.findById(id))) - - return compact(clients) - } - - async clear(): Promise { - try { - const ids = await this.getClientListIndex() - await Promise.all(ids.map((id) => this.encryptKeyValueService.delete(id))) - - return true - } catch { - return false - } - } - - getKey(clientId: string): string { - return `client:${clientId}` - } - - getIndexKey(): string { - return 'client:list-index' - } - - getEntityStoreKey(clientId: string): string { - return `client:${clientId}:entity-store` - } - - getPolicyStoreKey(clientId: string): string { - return `client:${clientId}:policy-store` - } - - private async index(client: Client): Promise { - const currentIndex = await this.getClientListIndex() - - await this.encryptKeyValueService.set( - this.getIndexKey(), - coerce.encode(ClientListIndex, [...currentIndex, client.clientId]) - ) - - return true - } -} diff --git a/apps/policy-engine/src/engine/persistence/repository/engine.repository.ts b/apps/policy-engine/src/engine/persistence/repository/engine.repository.ts index 21a2253d9..177927c50 100644 --- a/apps/policy-engine/src/engine/persistence/repository/engine.repository.ts +++ b/apps/policy-engine/src/engine/persistence/repository/engine.repository.ts @@ -1,28 +1,75 @@ import { coerce } from '@narval/nestjs-shared' import { Injectable } from '@nestjs/common' import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service' -import { Engine } from '../../../shared/type/domain.type' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { Engine, EngineV1 } from '../../../shared/type/domain.type' @Injectable() export class EngineRepository { - constructor(private keyValueService: KeyValueService) {} + constructor( + private keyValueService: KeyValueService, + private prismaService: PrismaService + ) {} - async findById(id: string): Promise { + /** @deprecated */ + async findByIdV1(id: string): Promise { const value = await this.keyValueService.get(this.getEngineKey(id)) if (value) { - return coerce.decode(Engine, value) + return coerce.decode(EngineV1, value) + } + + return null + } + + async findById(id: string): Promise { + const value = await this.prismaService.engine.findUnique({ where: { id } }) + + if (value) { + return Engine.parse(value) } return null } + async findAll(): Promise { + const values = await this.prismaService.engine.findMany() + + return values.map((value) => Engine.parse(value)) + } + + /** @deprecated */ + async saveV1(engine: EngineV1): Promise { + await this.keyValueService.set(this.getEngineKey(engine.id), coerce.encode(EngineV1, engine)) + + return engine + } + async save(engine: Engine): Promise { - await this.keyValueService.set(this.getEngineKey(engine.id), coerce.encode(Engine, engine)) + const engineData = { + id: engine.id, + encryptionKeyringType: engine.encryptionKeyringType, + encryptionMasterKey: engine.encryptionMasterKey, + encryptionMasterAwsKmsArn: engine.encryptionMasterAwsKmsArn, + authDisabled: !!engine.authDisabled, + adminApiKeyHash: engine.adminApiKeyHash + } + + // You cannot update the encryption details; that will cause data corruption. + // Key rotation must be a separate process. + await this.prismaService.engine.upsert({ + where: { id: engine.id }, + update: { + authDisabled: engine.authDisabled, + adminApiKeyHash: engine.adminApiKeyHash + }, + create: engineData + }) return engine } + /** @deprecated */ getEngineKey(id: string): string { return `engine:${id}` } diff --git a/apps/policy-engine/src/main.ts b/apps/policy-engine/src/main.ts index 85b5ca9e0..2c9832412 100644 --- a/apps/policy-engine/src/main.ts +++ b/apps/policy-engine/src/main.ts @@ -7,13 +7,19 @@ import { instrumentTelemetry } from '@narval/open-telemetry' instrumentTelemetry({ serviceName: 'policy-engine' }) import { ConfigService } from '@narval/config-module' -import { LoggerService, withApiVersion, withCors, withLogger, withSwagger } from '@narval/nestjs-shared' +import { + LoggerService, + securityOptions, + withApiVersion, + withCors, + withLogger, + withSwagger +} from '@narval/nestjs-shared' import { INestApplication, ValidationPipe } from '@nestjs/common' import { NestFactory } from '@nestjs/core' import { json } from 'express' import { lastValueFrom, map, of, switchMap } from 'rxjs' import { Config } from './policy-engine.config' -import { ADMIN_SECURITY, CLIENT_ID_SECURITY, CLIENT_SECRET_SECURITY } from './policy-engine.constant' import { PolicyEngineModule, ProvisionModule } from './policy-engine.module' import { ApplicationExceptionFilter } from './shared/filter/application-exception.filter' import { HttpExceptionFilter } from './shared/filter/http-exception.filter' @@ -67,7 +73,7 @@ async function bootstrap() { // Increase the POST JSON payload size to support bigger data stores. application.use(json({ limit: '50mb' })) - // NOTE: Enable application shutdown lifecyle hooks to ensure connections are + // NOTE: Enable application shutdown lifecycle hooks to ensure connections are // close on exit. application.enableShutdownHooks() @@ -83,7 +89,7 @@ async function bootstrap() { title: 'Policy Engine', description: 'Policy decision point for fine-grained authorization in web3.0', version: '1.0', - security: [ADMIN_SECURITY, CLIENT_ID_SECURITY, CLIENT_SECRET_SECURITY] + security: [securityOptions.adminApiKey, securityOptions.clientId, securityOptions.clientSecret] }) ), switchMap((app) => app.listen(port)) diff --git a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts index 9bc513fdd..dad22039c 100644 --- a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts @@ -1,6 +1,7 @@ import { ConfigModule, ConfigService } from '@narval/config-module' import { Action, + ConfirmationClaimProofMethod, Criterion, Decision, Eip712TypedData, @@ -14,10 +15,20 @@ import { Request, SignMessageAction, Then, + UserRole, ValueOperators, toHex } from '@narval/policy-engine-shared' -import { SigningAlg, buildSignerEip191, hash, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature' +import { + Alg, + SigningAlg, + buildSignerEip191, + generateJwk, + getPublicKey, + hash, + secp256k1PrivateKeyToJwk, + signJwt +} from '@narval/signature' import { Path, PathValue } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import { Config, load } from '../../../../policy-engine.config' @@ -229,6 +240,61 @@ describe('OpenPolicyAgentEngine', () => { expect(response.decision).toEqual(Decision.PERMIT) expect(response.principal).toEqual(FIXTURE.CREDENTIAL.Alice) }) + + it('throws when confirmation claim proof is invalid', async () => { + const e = await new OpenPolicyAgentEngine({ + policies: [ + { + id: 'test-permit-policy-uid', + description: 'test-policy', + when: [ + { + criterion: Criterion.CHECK_PRINCIPAL_ROLE, + args: [UserRole.ROOT, UserRole.ADMIN] + } + ], + then: Then.PERMIT + } + ], + entities: FIXTURE.ENTITIES, + resourcePath: await getConfig('resourcePath') + }).load() + + const request = { + action: Action.SIGN_MESSAGE, + nonce: 'test-nonce', + resourceId: FIXTURE.ACCOUNT.Engineering.id, + message: 'sign me' + } + + const bindPrivateKeyOne = await generateJwk(Alg.EDDSA) + const bindPrivateKeyTwo = await generateJwk(Alg.EDDSA) + + const evaluation: EvaluationRequest = { + authentication: await getJwt({ + privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, + sub: FIXTURE.USER.Alice.id, + request + }), + request, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKeyOne), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + requestHash: hash(request) + }, + bindPrivateKeyTwo + ) + } + } + } + } + + await expect(() => e.evaluate(evaluation)).rejects.toThrow('Invalid confirmation claim jws: Invalid signature') + }) }) describe('decide', () => { diff --git a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts index a6b43c3ae..d6c146553 100644 --- a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts +++ b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts @@ -8,7 +8,8 @@ import { EvaluationRequest, EvaluationResponse, JwtString, - Policy + Policy, + verifyConfirmationClaimProofOfPossession } from '@narval/policy-engine-shared' import { decodeJwt, hash, verifyJwt } from '@narval/signature' import { Intent } from '@narval/transaction-request-intent' @@ -121,6 +122,10 @@ export class OpenPolicyAgentEngine implements Engine { const principalCredential = await this.verifySignature(evaluation.authentication, message) + if (evaluation.metadata?.confirmation) { + await verifyConfirmationClaimProofOfPossession(evaluation) + } + const approvalsCredential = await Promise.all( (evaluation.approvals || []).map((signature) => this.verifySignature(signature, message)) ) diff --git a/apps/policy-engine/src/policy-engine.config.ts b/apps/policy-engine/src/policy-engine.config.ts index 7f87429f4..96ec458e4 100644 --- a/apps/policy-engine/src/policy-engine.config.ts +++ b/apps/policy-engine/src/policy-engine.config.ts @@ -1,3 +1,8 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { privateKeySchema, publicKeySchema, SigningAlg } from '@narval/signature' +import fs from 'fs' +import path from 'path' +import { parse } from 'yaml' import { z } from 'zod' export enum Env { @@ -6,71 +11,283 @@ export enum Env { PRODUCTION = 'production' } -const configSchema = z.object({ - env: z.nativeEnum(Env), - port: z.coerce.number(), - cors: z.array(z.string()).optional(), - resourcePath: z.string(), - database: z.object({ - url: z.string().startsWith('postgresql:') - }), - engine: z.object({ - id: z.string(), - adminApiKeyHash: z.string().optional(), - masterKey: z.string().optional() +const CONFIG_VERSION_LATEST = '1' +const logger = new LoggerService() + +const AppLocalAuthConfigSchema = z.object({ + adminApiKeyHash: z.string().nullable().optional() + // TODO: Add the app-level httpSigning section: +}) + +const LocalAuthConfigSchema = z.object({ + clientSecret: z.string().nullish() +}) + +const AuthConfigSchema = z.object({ + disabled: z.boolean().optional(), + local: LocalAuthConfigSchema.nullable().optional() +}) + +const DataStoreConfigSchema = z.object({ + entity: z.object({ + data: z.object({ + type: z.enum(['HTTP', 'HTTPS']), + url: z.string() + }), + signature: z.object({ + type: z.enum(['HTTP', 'HTTPS']), + url: z.string() + }), + publicKeys: z.array(publicKeySchema) }), - keyring: z.union([ - z.object({ - type: z.literal('raw'), - masterPassword: z.string() + policy: z.object({ + data: z.object({ + type: z.enum(['HTTP', 'HTTPS']), + url: z.string() }), - z.object({ - type: z.literal('awskms'), - masterAwsKmsArn: z.string() + signature: z.object({ + type: z.enum(['HTTP', 'HTTPS']), + url: z.string() + }), + publicKeys: z.array(publicKeySchema) + }) +}) + +const SignerConfigSchema = z.object({ + alg: z.nativeEnum(SigningAlg).optional(), + keyId: z.string().optional(), + publicKey: publicKeySchema.optional(), + privateKey: privateKeySchema.optional() +}) + +const ClientConfigSchema = z.object({ + name: z.string(), + baseUrl: z.string().optional(), + auth: AuthConfigSchema, + dataStore: DataStoreConfigSchema, + decisionAttestation: z.object({ + disabled: z.boolean().optional(), + signer: SignerConfigSchema.optional() + }) +}) +const PolicyEngineConfigSchema = z.object({ + version: z.coerce.string(), + env: z.nativeEnum(Env).nullish(), + port: z.coerce.number().nullish(), + cors: z.array(z.string()).nullish(), + baseUrl: z.string().nullish(), + resourcePath: z.string().nullish(), + app: z + .object({ + id: z.string(), + auth: z.object({ + disabled: z.boolean(), + local: AppLocalAuthConfigSchema.nullable() + }) + }) + .nullish(), + database: z + .object({ + url: z.string() + }) + .nullish(), + keyring: z + .object({ + type: z.enum(['raw', 'awskms']), + encryptionMasterPassword: z.string().nullable().optional(), + encryptionMasterKey: z.string().nullable().optional(), + encryptionMasterAwsKmsArn: z.string().nullable().optional(), + hmacSecret: z.string().nullable().optional() }) - ]), - signingProtocol: z.union([z.literal('simple'), z.literal('mpc')]).default('simple'), - tsm: z + .nullish(), + decisionAttestation: z .object({ - url: z.string(), - apiKey: z.string(), - playerCount: z.coerce.number().default(3) + protocol: z.union([z.literal('simple'), z.literal('mpc')]).nullish(), + tsm: z + .object({ + url: z.string(), + apiKey: z.string(), + playerCount: z.coerce.number().default(3) + }) + .nullish() + .describe('Only required when protocol is mpc. The TSM SDK node config.') }) - .optional() - .describe('Only required when signingProtocol is mpc. The TSM SDK node config.') + .nullish(), + clients: z.record(z.string(), ClientConfigSchema).nullish() }) -export type Config = z.infer +const keyringSchema = z.union([ + z.object({ + type: z.literal('raw'), + encryptionMasterPassword: z.string(), + encryptionMasterKey: z.string().nullable(), + hmacSecret: z.string().nullable() + }), + z.object({ + type: z.literal('awskms'), + encryptionMasterAwsKmsArn: z.string(), + hmacSecret: z.string().nullable() + }) +]) -export const load = (): Config => { - const result = configSchema.safeParse({ - env: process.env.NODE_ENV, - port: process.env.PORT, - cors: process.env.CORS ? process.env.CORS.split(',') : [], - resourcePath: process.env.RESOURCE_PATH, +const LoadConfig = PolicyEngineConfigSchema.transform((yaml, ctx) => { + const appId = process.env.APP_UID || yaml.app?.id + const databaseUrl = process.env.APP_DATABASE_URL || yaml.database?.url + const env = z.nativeEnum(Env).parse(process.env.NODE_ENV || yaml.env) + const port = process.env.PORT || yaml.port + const baseUrl = process.env.BASE_URL || yaml.baseUrl + const resourcePath = process.env.RESOURCE_PATH || yaml.resourcePath + + if (!appId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'APP_UID is required' + }) + return z.NEVER + } + if (!databaseUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'APP_DATABASE_URL is required' + }) + return z.NEVER + } + if (!port) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'PORT is required' + }) + return z.NEVER + } + if (!baseUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'BASE_URL is required' + }) + return z.NEVER + } + if (!resourcePath) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'RESOURCE_PATH is required' + }) + return z.NEVER + } + + // TODO: add Clients + + // process.env.SIGNING_PROTOCOL is for backwards-compatibility + const signingProtocol = + process.env.SIGNING_PROTOCOL || + process.env.DECISION_ATTESTATION_PROTOCOL || + yaml.decisionAttestation?.protocol || + null + let tsmConfig = null + if (signingProtocol === 'mpc') { + const tsmUrl = process.env.TSM_URL || yaml.decisionAttestation?.tsm?.url + const tsmApiKey = process.env.TSM_API_KEY || yaml.decisionAttestation?.tsm?.apiKey + const tsmPlayerCount = process.env.TSM_PLAYER_COUNT || yaml.decisionAttestation?.tsm?.playerCount + if (!tsmUrl || !tsmApiKey || !tsmPlayerCount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'TSM_URL, TSM_API_KEY, and TSM_PLAYER_COUNT are required when attestation is enabled and signingProtocol is mpc' + }) + return z.NEVER + } + tsmConfig = { + url: tsmUrl, + apiKey: tsmApiKey, + playerCount: z.coerce.number().parse(tsmPlayerCount) + } + } + + const clients = Object.entries(yaml.clients || {}).map(([clientId, client]) => ({ + clientId, + name: client.name, + baseUrl: client.baseUrl || null, + configurationSource: 'declarative', + auth: { + disabled: !!client.auth.disabled, + local: client.auth.local + ? { + clientSecret: client.auth.local.clientSecret || null + } + : null + }, + dataStore: client.dataStore, + decisionAttestation: client.decisionAttestation + ? { + disabled: !!client.decisionAttestation.disabled, + signer: { + alg: client.decisionAttestation.signer?.alg || SigningAlg.EIP191, + keyId: client.decisionAttestation.signer?.keyId || null, + publicKey: client.decisionAttestation.signer?.publicKey || undefined, + privateKey: client.decisionAttestation.signer?.privateKey || undefined + } + } + : { disabled: true } + })) + + return { + version: yaml.version || CONFIG_VERSION_LATEST, + env, + port, + cors: z.array(z.string()).parse(process.env.CORS || yaml.cors || []), + baseUrl, + resourcePath, database: { - url: process.env.APP_DATABASE_URL + url: databaseUrl }, - engine: { - id: process.env.APP_UID, - adminApiKeyHash: process.env.ADMIN_API_KEY, - masterKey: process.env.MASTER_KEY + + app: { + id: appId, + auth: { + disabled: yaml.app?.auth.disabled || false, + local: + yaml.app?.auth.local || process.env.ADMIN_API_KEY + ? { + adminApiKeyHash: yaml.app?.auth.local?.adminApiKeyHash || process.env.ADMIN_API_KEY || null + } + : null + } }, - keyring: { - type: process.env.KEYRING_TYPE, - masterAwsKmsArn: process.env.MASTER_AWS_KMS_ARN, - masterPassword: process.env.MASTER_PASSWORD + keyring: keyringSchema.parse({ + type: process.env.KEYRING_TYPE || yaml.keyring?.type, + encryptionMasterPassword: process.env.MASTER_PASSWORD || yaml.keyring?.encryptionMasterPassword || null, + encryptionMasterKey: process.env.MASTER_KEY || yaml.keyring?.encryptionMasterKey || null, + encryptionMasterAwsKmsArn: process.env.MASTER_AWS_KMS_ARN || yaml.keyring?.encryptionMasterAwsKmsArn || null, + hmacSecret: process.env.HMAC_SECRET || yaml.keyring?.hmacSecret || null + }), + decisionAttestation: { + protocol: signingProtocol, + tsm: tsmConfig }, - signingProtocol: process.env.SIGNING_PROTOCOL, - tsm: - process.env.SIGNING_PROTOCOL === 'mpc' - ? { - url: process.env.TSM_URL, - apiKey: process.env.TSM_API_KEY, - playerCount: process.env.TSM_PLAYER_COUNT - } - : undefined - }) + clients + } +}) + +export type Config = z.infer + +export const load = (): Config => { + const configFilePathEnv = process.env.CONFIG_FILE_ABSOLUTE_PATH + const configFileRelativePathEnv = process.env.CONFIG_FILE_RELATIVE_PATH + const filePath = configFilePathEnv + ? path.resolve(configFilePathEnv) + : path.resolve(process.cwd(), configFileRelativePathEnv || 'config/policy-engine-config.yaml') + let yamlConfigRaw = {} + try { + if (fs.existsSync(filePath)) { + const fileContents = fs.readFileSync(filePath, 'utf8') + yamlConfigRaw = parse(fileContents) + } + // If file doesn't exist, we'll use empty object as default + } catch (error) { + logger.warn(`Warning: Could not read config file at ${filePath}: ${error.message}`) + // Continue with empty config + } + + const result = LoadConfig.safeParse(yamlConfigRaw) if (result.success) { return result.data @@ -78,3 +295,7 @@ export const load = (): Config => { throw new Error(`Invalid application configuration: ${result.error.message}`) } + +export const getEnv = (): Env => { + return z.nativeEnum(Env).parse(process.env.NODE_ENV) +} diff --git a/apps/policy-engine/src/policy-engine.constant.ts b/apps/policy-engine/src/policy-engine.constant.ts index f6e742923..2366ea3db 100644 --- a/apps/policy-engine/src/policy-engine.constant.ts +++ b/apps/policy-engine/src/policy-engine.constant.ts @@ -1,17 +1,9 @@ import { RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node' -import { - REQUEST_HEADER_CLIENT_ID, - REQUEST_HEADER_CLIENT_SECRET, - adminApiKeySecurity, - clientIdSecurity, - clientSecretSecurity -} from '@narval/nestjs-shared' // // Headers // -export const REQUEST_HEADER_API_KEY = 'x-api-key' export const REQUEST_HEADER_SESSION_ID = 'x-session-id' // @@ -21,11 +13,3 @@ export const REQUEST_HEADER_SESSION_ID = 'x-session-id' export const ENCRYPTION_KEY_NAMESPACE = 'armory.policy-engine' export const ENCRYPTION_KEY_NAME = 'storage-encryption' export const ENCRYPTION_WRAPPING_SUITE = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING - -// -// API Security -// - -export const ADMIN_SECURITY = adminApiKeySecurity(REQUEST_HEADER_API_KEY) -export const CLIENT_ID_SECURITY = clientIdSecurity(REQUEST_HEADER_CLIENT_ID) -export const CLIENT_SECRET_SECURITY = clientSecretSecurity(REQUEST_HEADER_CLIENT_SECRET) diff --git a/apps/policy-engine/src/policy-engine.module.ts b/apps/policy-engine/src/policy-engine.module.ts index f5037ee11..a8eafb763 100644 --- a/apps/policy-engine/src/policy-engine.module.ts +++ b/apps/policy-engine/src/policy-engine.module.ts @@ -1,45 +1,53 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' -import { EncryptionModule } from '@narval/encryption-module' -import { - HttpLoggerMiddleware, - LoggerModule, - LoggerService, - OpenTelemetryModule, - TrackClientIdMiddleware -} from '@narval/nestjs-shared' -import { - MiddlewareConsumer, - Module, - NestModule, - OnApplicationBootstrap, - OnModuleInit, - ValidationPipe -} from '@nestjs/common' +import { ConfigModule } from '@narval/config-module' +import { EncryptionService } from '@narval/encryption-module' +import { HttpLoggerMiddleware, LoggerModule, OpenTelemetryModule, TrackClientIdMiddleware } from '@narval/nestjs-shared' +import { MiddlewareConsumer, Module, NestModule, OnModuleInit, ValidationPipe } from '@nestjs/common' import { APP_PIPE } from '@nestjs/core' import { ZodValidationPipe } from 'nestjs-zod' -import { BootstrapService } from './engine/core/service/bootstrap.service' +import { ClientModule } from './client/client.module' import { EngineService } from './engine/core/service/engine.service' import { ProvisionService } from './engine/core/service/provision.service' import { EngineModule } from './engine/engine.module' +import { EngineRepository } from './engine/persistence/repository/engine.repository' import { load } from './policy-engine.config' -import { EncryptionModuleOptionFactory } from './shared/factory/encryption-module-option.factory' +import { KeyValueModule } from './shared/module/key-value/key-value.module' +import { PersistenceModule } from './shared/module/persistence/persistence.module' const INFRASTRUCTURE_MODULES = [ LoggerModule, ConfigModule.forRoot({ load: [load], - isGlobal: true - }), - EncryptionModule.registerAsync({ - imports: [EngineModule, LoggerModule], - inject: [ConfigService, EngineService, LoggerService], - useClass: EncryptionModuleOptionFactory + isGlobal: true, + cache: true }), OpenTelemetryModule.forRoot() ] @Module({ - imports: [...INFRASTRUCTURE_MODULES, EngineModule], + imports: [ + PersistenceModule.register({ + imports: [] // Specifically erase the imports, so we do NOT initialize the EncryptionModule + }), + KeyValueModule + ], + providers: [EngineRepository, EngineService, ProvisionService, { provide: EncryptionService, useValue: undefined }], + exports: [EngineService, ProvisionService] +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(HttpLoggerMiddleware).forRoutes('*') + } +} + +@Module({ + imports: [ + ...INFRASTRUCTURE_MODULES, + AppModule, + PersistenceModule.forRoot(), + // Domain + EngineModule, + ClientModule + ], providers: [ { // DEPRECATE: Use Zod generated DTOs to validate request and responses. @@ -52,16 +60,10 @@ const INFRASTRUCTURE_MODULES = [ } ] }) -export class PolicyEngineModule implements OnApplicationBootstrap, NestModule { - constructor(private bootstrapService: BootstrapService) {} - +export class PolicyEngineModule implements NestModule { configure(consumer: MiddlewareConsumer): void { consumer.apply(HttpLoggerMiddleware, TrackClientIdMiddleware).forRoutes('*') } - - async onApplicationBootstrap() { - await this.bootstrapService.boot() - } } // IMPORTANT: To avoid application failure on the first boot due to a missing @@ -75,7 +77,7 @@ export class PolicyEngineModule implements OnApplicationBootstrap, NestModule { // context, dependencies requiring encryption will fail because the keyring was // already set as undefined. @Module({ - imports: [...INFRASTRUCTURE_MODULES, EngineModule] + imports: [...INFRASTRUCTURE_MODULES, AppModule] }) export class ProvisionModule implements OnModuleInit { constructor(private provisionService: ProvisionService) {} diff --git a/apps/policy-engine/src/shared/decorator/admin-guard.decorator.ts b/apps/policy-engine/src/shared/decorator/admin-guard.decorator.ts index 206d460cf..c77c6caf2 100644 --- a/apps/policy-engine/src/shared/decorator/admin-guard.decorator.ts +++ b/apps/policy-engine/src/shared/decorator/admin-guard.decorator.ts @@ -1,15 +1,15 @@ +import { REQUEST_HEADER_ADMIN_API_KEY, securityOptions } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { ADMIN_SECURITY, REQUEST_HEADER_API_KEY } from '../../policy-engine.constant' import { AdminApiKeyGuard } from '../guard/admin-api-key.guard' export function AdminGuard() { return applyDecorators( UseGuards(AdminApiKeyGuard), - ApiSecurity(ADMIN_SECURITY.name), + ApiSecurity(securityOptions.adminApiKey.name), ApiHeader({ required: true, - name: REQUEST_HEADER_API_KEY + name: REQUEST_HEADER_ADMIN_API_KEY }) ) } diff --git a/apps/policy-engine/src/shared/decorator/client-guard.decorator.ts b/apps/policy-engine/src/shared/decorator/client-guard.decorator.ts index 88215b619..15e1b37a8 100644 --- a/apps/policy-engine/src/shared/decorator/client-guard.decorator.ts +++ b/apps/policy-engine/src/shared/decorator/client-guard.decorator.ts @@ -1,14 +1,13 @@ -import { REQUEST_HEADER_CLIENT_ID, REQUEST_HEADER_CLIENT_SECRET } from '@narval/nestjs-shared' +import { REQUEST_HEADER_CLIENT_ID, REQUEST_HEADER_CLIENT_SECRET, securityOptions } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { CLIENT_ID_SECURITY, CLIENT_SECRET_SECURITY } from '../../policy-engine.constant' import { ClientSecretGuard } from '../guard/client-secret.guard' export function ClientGuard() { return applyDecorators( UseGuards(ClientSecretGuard), - ApiSecurity(CLIENT_SECRET_SECURITY.name), - ApiSecurity(CLIENT_ID_SECURITY.name), + ApiSecurity(securityOptions.clientSecret.name), + ApiSecurity(securityOptions.clientId.name), // IMPORTANT: The order in which you define the headers also determines the // order of the function/method arguments in the generated HTTP client. ApiHeader({ diff --git a/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts b/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts index 17b2499c9..ebd23a6c4 100644 --- a/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts +++ b/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts @@ -36,12 +36,12 @@ export class EncryptionModuleOptionFactory { } if (keyringConfig.type === 'raw') { - if (!engine.masterKey) { + if (!engine.encryptionMasterKey) { throw new Error('Master key not set') } - const kek = generateKeyEncryptionKey(keyringConfig.masterPassword, engine.id) - const unencryptedMasterKey = await decryptMasterKey(kek, toBytes(engine.masterKey)) + const kek = generateKeyEncryptionKey(keyringConfig.encryptionMasterPassword, engine.id) + const unencryptedMasterKey = await decryptMasterKey(kek, toBytes(engine.encryptionMasterKey)) return { keyring: new RawAesKeyringNode({ @@ -53,7 +53,7 @@ export class EncryptionModuleOptionFactory { } } else if (keyringConfig.type === 'awskms') { // We have AWS KMS config so we'll use that instead as the MasterKey, which means we don't need a KEK separately - const keyring = new KmsKeyringNode({ generatorKeyId: keyringConfig.masterAwsKmsArn }) + const keyring = new KmsKeyringNode({ generatorKeyId: keyringConfig.encryptionMasterAwsKmsArn }) return { keyring } } diff --git a/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts b/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts index d6a792624..a3ad1d864 100644 --- a/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts +++ b/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts @@ -1,15 +1,15 @@ -import { secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' import { ExecutionContext } from '@nestjs/common' import { mock } from 'jest-mock-extended' import { EngineService } from '../../../../engine/core/service/engine.service' -import { REQUEST_HEADER_API_KEY } from '../../../../policy-engine.constant' import { ApplicationException } from '../../../exception/application.exception' +import { Engine } from '../../../type/domain.type' import { AdminApiKeyGuard } from '../../admin-api-key.guard' describe(AdminApiKeyGuard.name, () => { const mockExecutionContext = (apiKey?: string) => { const headers = { - [REQUEST_HEADER_API_KEY]: apiKey + [REQUEST_HEADER_ADMIN_API_KEY]: apiKey } const request = { headers } @@ -21,11 +21,12 @@ describe(AdminApiKeyGuard.name, () => { } const mockEngineService = (adminApiKey = 'test-admin-api-key') => { - const engine = { - adminApiKey: secret.hash(adminApiKey), + const engine: Engine = { + adminApiKeyHash: secret.hash(adminApiKey), id: 'test-engine-id', - masterKey: 'test-master-key', - activated: true + encryptionKeyringType: 'raw', + encryptionMasterKey: 'test-master-key', + authDisabled: false } const serviceMock = mock() @@ -35,20 +36,20 @@ describe(AdminApiKeyGuard.name, () => { return serviceMock } - it(`throws an error when ${REQUEST_HEADER_API_KEY} header is missing`, async () => { + it(`throws an error when ${REQUEST_HEADER_ADMIN_API_KEY} header is missing`, async () => { const guard = new AdminApiKeyGuard(mockEngineService()) await expect(guard.canActivate(mockExecutionContext())).rejects.toThrow(ApplicationException) }) - it(`returns true when ${REQUEST_HEADER_API_KEY} matches the engine admin api key`, async () => { + it(`returns true when ${REQUEST_HEADER_ADMIN_API_KEY} matches the engine admin api key`, async () => { const adminApiKey = 'test-admin-api-key' const guard = new AdminApiKeyGuard(mockEngineService(adminApiKey)) expect(await guard.canActivate(mockExecutionContext(adminApiKey))).toEqual(true) }) - it(`returns false when ${REQUEST_HEADER_API_KEY} does not match the engine admin api key`, async () => { + it(`returns false when ${REQUEST_HEADER_ADMIN_API_KEY} does not match the engine admin api key`, async () => { const guard = new AdminApiKeyGuard(mockEngineService('test-admin-api-key')) expect(await guard.canActivate(mockExecutionContext('another-api-key'))).toEqual(false) diff --git a/apps/policy-engine/src/shared/guard/__test__/unit/client-secret.guard.spec.ts b/apps/policy-engine/src/shared/guard/__test__/unit/client-secret.guard.spec.ts index 28686f509..d7a2a3c25 100644 --- a/apps/policy-engine/src/shared/guard/__test__/unit/client-secret.guard.spec.ts +++ b/apps/policy-engine/src/shared/guard/__test__/unit/client-secret.guard.spec.ts @@ -1,10 +1,10 @@ import { REQUEST_HEADER_CLIENT_ID, REQUEST_HEADER_CLIENT_SECRET, secret } from '@narval/nestjs-shared' import { HttpSource, SourceType } from '@narval/policy-engine-shared' -import { Alg, privateKeyToJwk } from '@narval/signature' +import { Alg, SigningAlg, privateKeyToJwk, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' import { ExecutionContext } from '@nestjs/common' import { mock } from 'jest-mock-extended' import { generatePrivateKey } from 'viem/accounts' -import { ClientService } from '../../../../engine/core/service/client.service' +import { ClientService } from '../../../../client/core/service/client.service' import { ApplicationException } from '../../../exception/application.exception' import { Client } from '../../../type/domain.type' import { ClientSecretGuard } from '../../client-secret.guard' @@ -32,9 +32,18 @@ describe(ClientSecretGuard.name, () => { url: 'http://9.9.9.9:99/test-data-store' } + const clientSignerKey = generatePrivateKey() const client: Client = { clientId: CLIENT_ID, - clientSecret: secret.hash(clientSecret), + name: 'test-client', + configurationSource: 'dynamic', + baseUrl: null, + auth: { + disabled: false, + local: { + clientSecret: secret.hash(clientSecret) + } + }, dataStore: { entity: { data: dataStoreSource, @@ -47,8 +56,14 @@ describe(ClientSecretGuard.name, () => { keys: [] } }, - signer: { - privateKey: privateKeyToJwk(generatePrivateKey(), Alg.ES256K) + decisionAttestation: { + disabled: false, + signer: { + alg: SigningAlg.EIP191, + keyId: 'test-key-id', + publicKey: secp256k1PrivateKeyToPublicJwk(clientSignerKey), + privateKey: privateKeyToJwk(clientSignerKey, Alg.ES256K) + } }, updatedAt: new Date(), createdAt: new Date() diff --git a/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts b/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts index a5e313564..752619095 100644 --- a/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts +++ b/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts @@ -1,7 +1,6 @@ -import { secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common' import { EngineService } from '../../engine/core/service/engine.service' -import { REQUEST_HEADER_API_KEY } from '../../policy-engine.constant' import { ApplicationException } from '../exception/application.exception' @Injectable() @@ -10,17 +9,17 @@ export class AdminApiKeyGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest() - const apiKey = req.headers[REQUEST_HEADER_API_KEY] + const apiKey = req.headers[REQUEST_HEADER_ADMIN_API_KEY] if (!apiKey) { throw new ApplicationException({ - message: `Missing or invalid ${REQUEST_HEADER_API_KEY} header`, + message: `Missing or invalid ${REQUEST_HEADER_ADMIN_API_KEY} header`, suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED }) } const engine = await this.engineService.getEngineOrThrow() - return engine.adminApiKey === secret.hash(apiKey) + return engine.adminApiKeyHash === secret.hash(apiKey) } } diff --git a/apps/policy-engine/src/shared/guard/client-secret.guard.ts b/apps/policy-engine/src/shared/guard/client-secret.guard.ts index a718cd0e7..070c62e2e 100644 --- a/apps/policy-engine/src/shared/guard/client-secret.guard.ts +++ b/apps/policy-engine/src/shared/guard/client-secret.guard.ts @@ -1,6 +1,6 @@ import { REQUEST_HEADER_CLIENT_ID, REQUEST_HEADER_CLIENT_SECRET, secret } from '@narval/nestjs-shared' import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common' -import { ClientService } from '../../engine/core/service/client.service' +import { ClientService } from '../../client/core/service/client.service' import { ApplicationException } from '../exception/application.exception' @Injectable() @@ -26,6 +26,6 @@ export class ClientSecretGuard implements CanActivate { const client = await this.clientService.findById(clientId) - return client?.clientSecret === secret.hash(clientSecret) + return client?.auth.local?.clientSecret === secret.hash(clientSecret) } } diff --git a/apps/policy-engine/src/shared/module/key-value/key-value.module.ts b/apps/policy-engine/src/shared/module/key-value/key-value.module.ts index 2a652b8d4..56912675b 100644 --- a/apps/policy-engine/src/shared/module/key-value/key-value.module.ts +++ b/apps/policy-engine/src/shared/module/key-value/key-value.module.ts @@ -3,7 +3,7 @@ import { EncryptionModule } from '@narval/encryption-module' import { LoggerService } from '@narval/nestjs-shared' import { Module, forwardRef } from '@nestjs/common' import { EngineService } from '../../../engine/core/service/engine.service' -import { EngineModule } from '../../../engine/engine.module' +import { AppModule } from '../../../policy-engine.module' import { EncryptionModuleOptionFactory } from '../../factory/encryption-module-option.factory' import { PersistenceModule } from '../persistence/persistence.module' import { KeyValueRepository } from './core/repository/key-value.repository' @@ -14,9 +14,11 @@ import { PrismaKeyValueRepository } from './persistence/repository/prisma-key-va @Module({ imports: [ - PersistenceModule, + PersistenceModule.register({ + imports: [] // Specifically erase the imports, so we do NOT initialize the EncryptionModule + }), EncryptionModule.registerAsync({ - imports: [forwardRef(() => EngineModule)], + imports: [forwardRef(() => AppModule)], inject: [ConfigService, EngineService, LoggerService], useClass: EncryptionModuleOptionFactory }) diff --git a/apps/policy-engine/src/shared/module/persistence/exception/parse.exception.ts b/apps/policy-engine/src/shared/module/persistence/exception/parse.exception.ts new file mode 100644 index 000000000..2ff485b9f --- /dev/null +++ b/apps/policy-engine/src/shared/module/persistence/exception/parse.exception.ts @@ -0,0 +1,11 @@ +import { PersistenceException } from './persistence.exception' + +export class ParseException extends PersistenceException { + readonly origin: Error + + constructor(origin: Error) { + super(origin.message) + + this.origin = origin + } +} diff --git a/apps/policy-engine/src/shared/module/persistence/exception/persistence.exception.ts b/apps/policy-engine/src/shared/module/persistence/exception/persistence.exception.ts new file mode 100644 index 000000000..b0faa3fc9 --- /dev/null +++ b/apps/policy-engine/src/shared/module/persistence/exception/persistence.exception.ts @@ -0,0 +1 @@ +export class PersistenceException extends Error {} diff --git a/apps/policy-engine/src/shared/module/persistence/persistence.module.ts b/apps/policy-engine/src/shared/module/persistence/persistence.module.ts index 95d5dcd4c..10ec85878 100644 --- a/apps/policy-engine/src/shared/module/persistence/persistence.module.ts +++ b/apps/policy-engine/src/shared/module/persistence/persistence.module.ts @@ -1,9 +1,43 @@ -import { Module } from '@nestjs/common' +import { ConfigService } from '@narval/config-module' +import { EncryptionModule } from '@narval/encryption-module' +import { LoggerService } from '@narval/nestjs-shared' +import { DynamicModule, ForwardReference, Module, Type, forwardRef } from '@nestjs/common' +import { EngineService } from '../../../engine/core/service/engine.service' +import { AppModule } from '../../../policy-engine.module' +import { EncryptionModuleOptionFactory } from '../../factory/encryption-module-option.factory' import { PrismaService } from './service/prisma.service' import { TestPrismaService } from './service/test-prisma.service' -@Module({ - exports: [PrismaService, TestPrismaService], - providers: [PrismaService, TestPrismaService] -}) -export class PersistenceModule {} +@Module({}) +export class PersistenceModule { + static forRoot(): DynamicModule { + return { + module: PersistenceModule, + global: true, + imports: [ + EncryptionModule.registerAsync({ + imports: [forwardRef(() => AppModule)], + inject: [ConfigService, EngineService, LoggerService], + useClass: EncryptionModuleOptionFactory + }) + ], + providers: [PrismaService, TestPrismaService], + exports: [PrismaService, TestPrismaService] + } + } + + static register(config: { imports?: Array } = {}): DynamicModule { + return { + module: PersistenceModule, + imports: config.imports || [ + EncryptionModule.registerAsync({ + imports: [forwardRef(() => AppModule)], + inject: [ConfigService, EngineService, LoggerService], + useClass: EncryptionModuleOptionFactory + }) + ], + providers: [PrismaService, TestPrismaService], + exports: [PrismaService, TestPrismaService] + } + } +} diff --git a/apps/policy-engine/src/shared/module/persistence/schema/migrations/20250131164306_engine_db_normalization/migration.sql b/apps/policy-engine/src/shared/module/persistence/schema/migrations/20250131164306_engine_db_normalization/migration.sql new file mode 100644 index 000000000..9367e5011 --- /dev/null +++ b/apps/policy-engine/src/shared/module/persistence/schema/migrations/20250131164306_engine_db_normalization/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - You are about to drop the column `admin_api_key` on the `engine` table. All the data in the column will be lost. + - You are about to drop the column `master_key` on the `engine` table. All the data in the column will be lost. + - Added the required column `encryption_keyring_type` to the `engine` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "engine" DROP COLUMN "admin_api_key", +DROP COLUMN "master_key", +ADD COLUMN "admin_api_key_hash" TEXT, +ADD COLUMN "auth_disabled" BOOLEAN, +ADD COLUMN "encryption_keyring_type" TEXT NOT NULL, +ADD COLUMN "encryption_master_aws_kms_arn" TEXT, +ADD COLUMN "encryption_master_key" TEXT; + +-- CreateTable +CREATE TABLE "client" ( + "client_id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "configuration_source" TEXT NOT NULL, + "base_url" TEXT, + "auth_disabled" BOOLEAN NOT NULL, + "client_secret" TEXT, + "data_store_entity_data_url" TEXT NOT NULL, + "data_store_entity_signature_url" TEXT NOT NULL, + "data_store_entity_public_keys" TEXT NOT NULL, + "data_store_policy_data_url" TEXT NOT NULL, + "data_store_policy_signature_url" TEXT NOT NULL, + "data_store_policy_public_keys" TEXT NOT NULL, + "decision_attestation_disabled" BOOLEAN NOT NULL, + "signer_alg" TEXT, + "signer_key_id" TEXT, + "signer_public_key" TEXT, + "signer_private_key" TEXT, + "created_at" TIMESTAMP(3) NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "client_pkey" PRIMARY KEY ("client_id") +); diff --git a/apps/policy-engine/src/shared/module/persistence/schema/migrations/20250131171229_client_hmac/migration.sql b/apps/policy-engine/src/shared/module/persistence/schema/migrations/20250131171229_client_hmac/migration.sql new file mode 100644 index 000000000..fde5828a6 --- /dev/null +++ b/apps/policy-engine/src/shared/module/persistence/schema/migrations/20250131171229_client_hmac/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "client" ADD COLUMN "_integrity" TEXT; diff --git a/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma b/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma index 80ff7df43..cb33a69b1 100644 --- a/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma +++ b/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma @@ -13,13 +13,50 @@ datasource db { } model Engine { - id String @id - masterKey String? @map("master_key") - adminApiKey String? @map("admin_api_key") + id String @id + // Encryption options, possibly set from Config file + encryptionKeyringType String @map("encryption_keyring_type") // raw | awskms + encryptionMasterKey String? @map("encryption_master_key") /// @encrypted by masterPassword KEK + encryptionMasterAwsKmsArn String? @map("encryption_master_aws_kms_arn") // only if type = awskms + // Auth Options, set from Config file + authDisabled Boolean? @map("auth_disabled") + adminApiKeyHash String? @map("admin_api_key_hash") /// hash, not plaintext @@map("engine") } +model Client { + clientId String @id @map("client_id") + name String @map("name") + configurationSource String @map("configuration_source") // declarative | dynamic + baseUrl String? @map("base_url") // If you want to override the used for verifying jwsd/httpsig + authDisabled Boolean @map("auth_disabled") + clientSecret String? @map("client_secret") /// hash, not plaintext + + // Data Store Config + dataStoreEntityDataUrl String @map("data_store_entity_data_url") + dataStoreEntitySignatureUrl String @map("data_store_entity_signature_url") + dataStoreEntityPublicKeys String @map("data_store_entity_public_keys") // stringified json array of public keys + dataStorePolicyDataUrl String @map("data_store_policy_data_url") + dataStorePolicySignatureUrl String @map("data_store_policy_signature_url") + dataStorePolicyPublicKeys String @map("data_store_policy_public_keys") // stringified json array of public keys + + + // Signer Config + decisionAttestationDisabled Boolean @map("decision_attestation_disabled") + signerAlg String? @map("signer_alg") + signerKeyId String? @map("signer_key_id") + signerPublicKey String? @map("signer_public_key") + signerPrivateKey String? @map("signer_private_key") + + createdAt DateTime @map("created_at") + updatedAt DateTime @map("updated_at") + + integrity String? @map("_integrity") + + @@map("client") +} + // TODO: (@wcalderipe, 12/03/23) use hstore extension for better performance. // See https://www.postgresql.org/docs/9.1/hstore.html model KeyValue { diff --git a/apps/policy-engine/src/shared/module/persistence/seed.ts b/apps/policy-engine/src/shared/module/persistence/seed.ts index 83c8585af..48750e260 100644 --- a/apps/policy-engine/src/shared/module/persistence/seed.ts +++ b/apps/policy-engine/src/shared/module/persistence/seed.ts @@ -1,22 +1,13 @@ /* eslint-disable */ import { LoggerService } from '@narval/nestjs-shared' -import { Engine, PrismaClient } from '@prisma/client/policy-engine' +import { PrismaClient } from '@prisma/client/policy-engine' const prisma = new PrismaClient() -const engine: Engine = { - id: '7d704a62-d15e-4382-a826-1eb41563043b', - adminApiKey: 'admin-api-key-xxx', - masterKey: 'master-key-xxx' -} - async function main() { const logger = new LoggerService() logger.log('Seeding Engine database') - await prisma.$transaction(async (txn) => { - // await txn.engine.create({ data: engine }) - }) logger.log('Engine database germinated 🌱') } diff --git a/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts b/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts index 7154fc3ba..f2163bd17 100644 --- a/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts +++ b/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts @@ -1,22 +1,345 @@ import { ConfigService } from '@narval/config-module' +import { EncryptionService } from '@narval/encryption-module' import { LoggerService } from '@narval/nestjs-shared' -import { Inject, Injectable, OnApplicationShutdown, OnModuleDestroy, OnModuleInit } from '@nestjs/common' -import { PrismaClient } from '@prisma/client/policy-engine' +import { Injectable, OnApplicationShutdown, OnModuleDestroy, OnModuleInit, Optional } from '@nestjs/common' +import { hmac } from '@noble/hashes/hmac' +import { sha256 } from '@noble/hashes/sha2' +import { bytesToHex } from '@noble/hashes/utils' +import { Prisma, PrismaClient } from '@prisma/client/policy-engine' +import { canonicalize } from 'packages/signature/src/lib/json.util' import { Config } from '../../../../policy-engine.config' +import { ParseException } from '../exception/parse.exception' + +const ENCRYPTION_PREFIX = 'enc.v1.' // Version prefix helps with future encryption changes +const INTEGRITY_PREFIX = 'hmac.v1.' // Version prefix helps with future integrity changes + +/** + * To encrypt a field, simply reference the Model as the key, and the fields in an array. + * NOTE: encrypted fields MUST be of string type. JSON data should be stringified before/after encryption/decryption; this assumes Strings. + */ +const encryptedModelFields = { + Client: [Prisma.ClientScalarFieldEnum.clientSecret, Prisma.ClientScalarFieldEnum.signerPrivateKey] +} + +const modelWithHmacIntegrity = { + Client: { + [Prisma.ClientScalarFieldEnum.clientId]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.name]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.configurationSource]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.baseUrl]: { + integrity: true, + nullable: true + }, + [Prisma.ClientScalarFieldEnum.authDisabled]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.clientSecret]: { + integrity: true, + nullable: true + }, + [Prisma.ClientScalarFieldEnum.dataStoreEntityDataUrl]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.dataStoreEntitySignatureUrl]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.dataStoreEntityPublicKeys]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.dataStorePolicyDataUrl]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.dataStorePolicySignatureUrl]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.dataStorePolicyPublicKeys]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.decisionAttestationDisabled]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.signerAlg]: { + integrity: true, + nullable: true + }, + [Prisma.ClientScalarFieldEnum.signerKeyId]: { + integrity: true, + nullable: true + }, + [Prisma.ClientScalarFieldEnum.signerPublicKey]: { + integrity: true, + nullable: true + }, + [Prisma.ClientScalarFieldEnum.signerPrivateKey]: { + integrity: true, + nullable: true + }, + [Prisma.ClientScalarFieldEnum.createdAt]: { + integrity: true, + nullable: false + }, + [Prisma.ClientScalarFieldEnum.updatedAt]: { + integrity: true, + nullable: false + } + } +} + +const getHmac = (secret: string, value: Record) => { + const integrity = hmac(sha256, secret, canonicalize(value)) + return `${INTEGRITY_PREFIX}${bytesToHex(integrity)}` +} + +const buildEncryptionExtension = ( + configService: ConfigService, + logger: LoggerService, + encryptionService: EncryptionService +) => { + // Generate the hmac + const hmacSecret = configService.get('keyring.hmacSecret') + if (!hmacSecret) { + logger.error('HMAC secret is not set, integrity verification will not be performed') + throw new Error('HMAC secret is not set, integrity verification will not be performed') + } + + const encryptToString = async (value: string) => { + const encryptedBuffer = await encryptionService.encrypt(value) + const encryptedString = encryptedBuffer.toString('hex') + return `${ENCRYPTION_PREFIX}${encryptedString}` + } + + const decryptToString = async (value: string) => { + if (!value.startsWith(ENCRYPTION_PREFIX)) { + return value + } + const decryptedBuffer = await encryptionService.decrypt(Buffer.from(value.slice(ENCRYPTION_PREFIX.length), 'hex')) + return decryptedBuffer.toString() + } + + return Prisma.defineExtension({ + name: 'encryption', + query: { + async $allOperations({ model, operation, args, query }) { + if (!model || !(model in encryptedModelFields)) { + return query(args) + } + const fields = encryptedModelFields[model as keyof typeof encryptedModelFields] + + // For write operations, encrypt. + const writeOps = ['create', 'upsert', 'update', 'updateMany', 'createMany'] + if (writeOps.includes(operation)) { + let dataToUpdate: Record[] = [] + if (operation === 'upsert') { + if (args.update) { + dataToUpdate.push(args.update) + } + if (args.create) { + dataToUpdate.push(args.create) + } + } else if (Array.isArray(args.data)) { + dataToUpdate = args.data + } else { + dataToUpdate = [args.data] + } + // For each field-to-encrypt, for each object being created, encrypt the field. + await Promise.all( + fields.map(async (field) => { + await Promise.all( + dataToUpdate.map(async (item: Record) => { + if (item[field] && typeof item[field] === 'string') { + item[field] = await encryptToString(item[field] as string) + } + }) + ) + }) + ) + // Data has been encrypted. + // Now, generate the _integrity hmac + // The data must include every field on the model; if not, we will reject this operation. + if (model in modelWithHmacIntegrity) { + const fields = modelWithHmacIntegrity[model as keyof typeof modelWithHmacIntegrity] + for (const data of dataToUpdate) { + // Create object to hold fields that should be covered by integrity + const integrityCovered: Record = {} + + // Iterate through all configured fields for this model + for (const [fieldName, fieldSettings] of Object.entries(fields)) { + // Check if field is required but missing + integrityCovered[fieldName] = data[fieldName] + if (!fieldSettings.nullable && data[fieldName] === undefined) { + logger.error(`Missing required field ${fieldName} in data, needed for integrity hmac`) + throw new Error(`Missing required field ${fieldName} in data, needed for integrity hmac`) + } + if (fieldSettings.nullable && !data[fieldName]) { + // Ensure we capture the null in the integrity object + integrityCovered[fieldName] = null + } + } + + const integrity = getHmac(hmacSecret, integrityCovered) + data.integrity = integrity + } + } + + return query(args) + } + + // For read operations, decrypt. + const readOps = ['findUnique', 'findMany', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow'] as const + type ReadOp = (typeof readOps)[number] + + if (readOps.includes(operation as ReadOp)) { + const result = await query(args) + + // Handle non-record results + if (!result || typeof result === 'number' || 'count' in result) { + return result + } + + // Handle array or single result + const items = Array.isArray(result) ? result : [result] + + await Promise.all( + items.map(async (item: Record) => { + // If it has an `integrity` field, verify it's integrity + let skipIntegrityAndDecryption = false + if (model in modelWithHmacIntegrity) { + const fields = modelWithHmacIntegrity[model as keyof typeof modelWithHmacIntegrity] + // Create object to hold fields that should be covered by integrity + const integrityCovered: Record = {} + + // Iterate through all configured fields for this model + for (const [fieldName, fieldSettings] of Object.entries(fields)) { + integrityCovered[fieldName] = item[fieldName] + + // If there is an integrity field that is in args.select with `false` then skip integrity verification & decryption + if (fieldSettings.integrity && args.select && args.select[fieldName] === false) { + logger.log(`Skipping integrity verification & decryption due to subset of fields queried`) + skipIntegrityAndDecryption = true + return + } + + // Check if field is required but missing + if (!fieldSettings.nullable && item[fieldName] === undefined) { + logger.error( + `Missing required field ${fieldName} in data, needed for integrity hmac, did your query forget it?` + ) + throw new Error(`Missing required field ${fieldName} in data, needed for integrity hmac`) + } + } + + const integrityToVerify = getHmac(hmacSecret, integrityCovered) + if (integrityToVerify !== item.integrity) { + logger.error('Integrity verification failed', { + integrityToVerify, + item, + args + }) + throw new Error('Integrity verification failed') + } + } + // We passed integrity verification, so we can decrypt the fields + if (!skipIntegrityAndDecryption) { + await Promise.all( + fields.map(async (field) => { + if (item[field] && typeof item[field] === 'string') { + item[field] = await decryptToString(item[field] as string) + } + }) + ) + } + }) + ) + + return result + } + + return query(args) + } + } + }) +} @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy, OnApplicationShutdown { constructor( - @Inject(ConfigService) configService: ConfigService, - private logger: LoggerService + configService: ConfigService, + private logger: LoggerService, + @Optional() encryptionService?: EncryptionService ) { const url = configService.get('database.url') - super({ datasources: { db: { url } } }) + if (encryptionService) { + logger.log('Instantiating Prisma encryption extension') + Object.assign(this, this.$extends(buildEncryptionExtension(configService, logger, encryptionService))) + } + } + + static toPrismaJson(value?: T | null): Prisma.InputJsonValue | Prisma.NullTypes.JsonNull { + if (value === null || value === undefined) { + return Prisma.JsonNull + } + + // Handle basic JSON-serializable types. + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) { + return value as Prisma.InputJsonValue + } + + // For objects, ensure they're JSON-serializable. + if (typeof value === 'object') { + try { + return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue + } catch (error) { + throw new ParseException(error) + } + } + + return Prisma.JsonNull + } + + static toStringJson(value?: T | null): string | null { + if (value) { + try { + return JSON.stringify(value) + } catch (error) { + throw new ParseException(error) + } + } + + return null + } + + static toJson(value?: string | null) { + if (value) { + try { + return JSON.parse(value) + } catch (error) { + throw new ParseException(error) + } + } + + return null } async onModuleInit() { @@ -40,7 +363,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul onApplicationShutdown(signal: string) { this.logger.log('Disconnecting from Prisma on application shutdown', signal) - // The $disconnect method returns a promise, so idealy we should wait for it + // The $disconnect method returns a promise, so ideally we should wait for it // to finish. However, the onApplicationShutdown, returns `void` making it // impossible to ensure the database will be properly disconnected before // the shutdown. diff --git a/apps/policy-engine/src/shared/type/domain.type.ts b/apps/policy-engine/src/shared/type/domain.type.ts index b88529f8c..3bdb4ece9 100644 --- a/apps/policy-engine/src/shared/type/domain.type.ts +++ b/apps/policy-engine/src/shared/type/domain.type.ts @@ -1,5 +1,5 @@ import { DataStoreConfiguration } from '@narval/policy-engine-shared' -import { privateKeySchema, publicKeySchema } from '@narval/signature' +import { privateKeySchema, publicKeySchema, SigningAlg } from '@narval/signature' import { z } from 'zod' @@ -7,15 +7,46 @@ export const SignerFunction = z.function().args(z.string()).returns(z.promise(z. export type SignerFunction = z.infer export const SignerConfig = z.object({ + alg: z.nativeEnum(SigningAlg).default(SigningAlg.EIP191), // Temporarily default to EIP191 TODO: remove the default after migration from v1 data + keyId: z.string().nullable().describe('Unique id of the signer key. Matches the kid in both jwks'), publicKey: publicKeySchema.optional(), privateKey: privateKeySchema.optional(), - keyId: z.string().optional().describe('Unique id of the signer key. Matches the kid in both jwks'), signer: SignerFunction.optional() }) export type SignerConfig = z.infer export const Client = z.object({ + clientId: z.string(), + name: z.string(), + configurationSource: z.literal('declarative').or(z.literal('dynamic')), // Declarative = comes from config file, Dynamic = created at runtime + // Override if you want to use a different baseUrl for a single client. + baseUrl: z.string().nullable(), + + auth: z.object({ + disabled: z.boolean(), + local: z + .object({ + clientSecret: z.string().nullable() + }) + .nullable() + }), + + dataStore: z.object({ + entity: DataStoreConfiguration, + policy: DataStoreConfiguration + }), + + decisionAttestation: z.object({ + disabled: z.boolean(), + signer: SignerConfig.nullable() + }), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() +}) +export type Client = z.infer + +export const ClientV1 = z.object({ clientId: z.string(), clientSecret: z.string(), dataStore: z.object({ @@ -26,11 +57,56 @@ export const Client = z.object({ createdAt: z.coerce.date(), updatedAt: z.coerce.date() }) -export type Client = z.infer +export type ClientV1 = z.infer -export const Engine = z.object({ +export const PublicClient = z.object({ + clientId: z.string(), + name: z.string(), + configurationSource: z.literal('declarative').or(z.literal('dynamic')), // Declarative = comes from config file, Dynamic = created at runtime + baseUrl: z.string().nullable(), + + auth: z.object({ + disabled: z.boolean(), + local: z + .object({ + clientSecret: z.string().nullable() + }) + .nullable() + }), + + dataStore: z.object({ + entity: DataStoreConfiguration, + policy: DataStoreConfiguration + }), + + decisionAttestation: z.object({ + disabled: z.boolean(), + signer: z + .object({ + alg: z.nativeEnum(SigningAlg), + keyId: z.string().nullable().describe('Unique id of the signer key. Matches the kid in both jwks'), + publicKey: publicKeySchema.optional() + }) + .nullable() + }), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() +}) +export type PublicClient = z.infer + +export const EngineV1 = z.object({ id: z.string().min(1), adminApiKey: z.string().min(1).optional(), masterKey: z.string().min(1).optional() }) +export type EngineV1 = z.infer + +export const Engine = z.object({ + id: z.string().min(1), + adminApiKeyHash: z.string().min(1).nullish(), + encryptionMasterKey: z.string().min(1).nullish(), + encryptionKeyringType: z.literal('raw').or(z.literal('awskms')), + encryptionMasterAwsKmsArn: z.string().nullish(), + authDisabled: z.boolean().optional() +}) export type Engine = z.infer diff --git a/apps/vault/.env.default b/apps/vault/.env.default index 8081558a9..f902b20ee 100644 --- a/apps/vault/.env.default +++ b/apps/vault/.env.default @@ -1,15 +1,10 @@ -NODE_ENV=development - -PORT=3011 +# Note: this is a default configuration for development. Reference `.env.template` for all available options. +# We prefer to use the config.yaml file, rather than the .env options, so most options are set there. -APP_UID=local-dev-vault-instance-1 +NODE_ENV=development -# OPTIONAL: Sets the admin API key instead of generating a new one during the -# provision. -# -# Key should be hashed, like this: `echo -n "my-api-key" | openssl dgst -sha256 | awk '{print $2}'` -# Plain text API key: vault-admin-api-key -ADMIN_API_KEY=d4a6b4c1cb71dbdb68a1dd429ad737369f74b9e264b9dfa639258753987caaad +# Relative path to the config file from where the node process is started, defaults to `./config/vault-config.yaml` +CONFIG_FILE_RELATIVE_PATH="./config/vault-config.local.yaml" # === Database === @@ -24,21 +19,6 @@ APP_DATABASE_HOST=host.docker.internal APP_DATABASE_PORT=5432 APP_DATABASE_NAME=vault -# === Encryption === - -# Determine the encryption module keyring type. -# Either "awskms" or "raw". -KEYRING_TYPE=raw - -# If using raw keyring, master password for encrypting data -MASTER_PASSWORD=unsafe-local-dev-master-password - -# If using awskms keyring, provide the ARN of the KMS encryption key instead of a master password -MASTER_AWS_KMS_ARN= - -# Base URL where the Vault is deployed. Will be used to verify jwsd request -# signatures. -BASE_URL=http://localhost:3011 # === OpenTelemetry configuration === diff --git a/apps/vault/.env.template b/apps/vault/.env.template new file mode 100644 index 000000000..c5893cda9 --- /dev/null +++ b/apps/vault/.env.template @@ -0,0 +1,71 @@ +# Template of all the available ENV variables. + +# === Node environment === + +NODE_ENV=development + +# === Server === + +PORT=3011 + +# === Application === + +APP_UID=local-dev-vault-instance-1 + +# === Config === + +# Absolute path to the config file. If not set, uses the relative path. +# CONFIG_FILE_ABSOLUTE_PATH="/config/vault-config.yaml" + +# Relative path to the config file from where the node process is started, defaults to `./config/vault-config.yaml` +CONFIG_FILE_RELATIVE_PATH="./config/vault-config.local.yaml" + +# === Admin API key === + +# OPTIONAL: Sets the admin API key used for app-level admin operations (e.g. creating clients) +# +# Key should be hashed, like this: `echo -n "vault-admin-api-key" | openssl dgst -sha256 | awk '{print $2}'` +# Plain text API key: vault-admin-api-key +ADMIN_API_KEY=d4a6b4c1cb71dbdb68a1dd429ad737369f74b9e264b9dfa639258753987caaad + +# === Database === + +# APP db connection string, used for app runtime. +APP_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/vault?schema=public + +# Migrator db credentials. This is needed for db migrations where we can't use the APP_DATABASE_URL. Can be the same or different credentials. +# host/port/name should be the same, username&password may be different +APP_DATABASE_USERNAME=postgres +APP_DATABASE_PASSWORD=postgres +APP_DATABASE_HOST=host.docker.internal +APP_DATABASE_PORT=5432 +APP_DATABASE_NAME=vault + +# === Encryption === + +# Determine the encryption module keyring type. +# Either "awskms" or "raw". +KEYRING_TYPE=raw + +# If using raw keyring, master password for encrypting data +MASTER_PASSWORD=unsafe-local-dev-master-password + +# If using awskms keyring, provide the ARN of the KMS encryption key instead of a master password +# MASTER_AWS_KMS_ARN= + +# HMAC secret for integrity verification of data in the database. +HMAC_SECRET=unsafe-local-dev-hmac-secret + +# Base URL where the Vault is deployed. Will be used to verify jwsd request signatures. +BASE_URL=http://localhost:3011 + +# === OpenTelemetry configuration === + +# See https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ +OTEL_SDK_DISABLED=true +# OTEL Collector container HTTP port. +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +OTEL_LOGS_EXPORTER=otlp +OTEL_LOG_LEVEL=error +OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local diff --git a/apps/vault/.env.test.default b/apps/vault/.env.test.default index bf8094b29..590577911 100644 --- a/apps/vault/.env.test.default +++ b/apps/vault/.env.test.default @@ -11,6 +11,8 @@ APP_UID="local-dev-vault-instance-1" MASTER_PASSWORD="unsafe-local-test-master-password" +HMAC_SECRET="unsafe-local-test-hmac-secret" + KEYRING_TYPE="raw" # MASTER_AWS_KMS_ARN="arn:aws:kms:us-east-2:728783560968:key/f6aa3ddb-47c3-4f31-977d-b93205bb23d1" diff --git a/apps/vault/Makefile b/apps/vault/Makefile index 7b2c4852e..4974208aa 100644 --- a/apps/vault/Makefile +++ b/apps/vault/Makefile @@ -20,6 +20,7 @@ vault/setup: vault/copy-default-env: cp ${VAULT_PROJECT_DIR}/.env.default ${VAULT_PROJECT_DIR}/.env cp ${VAULT_PROJECT_DIR}/.env.test.default ${VAULT_PROJECT_DIR}/.env.test + cp ./config/vault-config.example.yaml ./config/vault-config.local.yaml # === Build === @@ -30,12 +31,17 @@ vault/build: vault/format: npx nx format:write --projects ${VAULT_PROJECT_NAME} + make vault/format/prisma + +vault/format/prisma: + npx prisma format --schema ${VAULT_DATABASE_SCHEMA} vault/lint: npx nx lint ${VAULT_PROJECT_NAME} -- --fix vault/format/check: npx nx format:check --projects ${VAULT_PROJECT_NAME} + npx prisma format --schema ${VAULT_DATABASE_SCHEMA} vault/lint/check: npx nx lint ${VAULT_PROJECT_NAME} diff --git a/apps/vault/jest.config.ts b/apps/vault/jest.config.ts index d19762a82..b3d7101ff 100644 --- a/apps/vault/jest.config.ts +++ b/apps/vault/jest.config.ts @@ -13,8 +13,7 @@ const config: Config = { tsconfig: '/tsconfig.spec.json' } ] - }, - workerThreads: true // EXPERIMENTAL; lets BigInt serialization work + } } export default config diff --git a/apps/vault/jest.setup.ts b/apps/vault/jest.setup.ts index 4fab91e6b..0e4f6088e 100644 --- a/apps/vault/jest.setup.ts +++ b/apps/vault/jest.setup.ts @@ -1,6 +1,6 @@ import dotenv from 'dotenv' import fs from 'fs' -import nock from 'nock' +// import { setupNock } from './test/nock.util' const testEnvFile = `${__dirname}/.env.test` @@ -22,28 +22,5 @@ for (const prop in process.env) { dotenv.config({ path: testEnvFile, override: true }) -// Disable outgoing HTTP requests to avoid flaky tests. -nock.disableNetConnect() - -// Enable outbound HTTP requests to 127.0.0.1 to allow E2E tests with -// supertestwith supertest to work. -const OUTBOUND_HTTP_ALLOWED_HOST = '127.0.0.1' - -nock.enableNetConnect(OUTBOUND_HTTP_ALLOWED_HOST) - -// Jest sometimes translates unmatched errors into obscure JSON circular -// dependency without a proper stack trace. This can lead to hours of -// debugging. To save time, this emitter will consistently log an unmatched -// event allowing engineers to quickly identify the source of the error. -nock.emitter.on('no match', (request) => { - if (request.host && request.host.includes(OUTBOUND_HTTP_ALLOWED_HOST)) { - return - } - - if (request.hostname && request.hostname.includes(OUTBOUND_HTTP_ALLOWED_HOST)) { - return - } - - // eslint-disable-next-line no-console - console.error('Nock: no match for request', request) -}) +// Temp disable it +// setupNock() diff --git a/apps/vault/src/__test__/shared/vault.test.ts b/apps/vault/src/__test__/shared/vault.test.ts new file mode 100644 index 000000000..567641bb1 --- /dev/null +++ b/apps/vault/src/__test__/shared/vault.test.ts @@ -0,0 +1,42 @@ +import { EncryptionModuleOptionProvider } from '@narval/encryption-module' +import { LoggerModule } from '@narval/nestjs-shared' +import { ModuleMetadata } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { Test, TestingModuleBuilder } from '@nestjs/testing' +import { mock } from 'jest-mock-extended' +import { ProvisionService } from '../../provision.service' +import { getTestRawAesKeyring } from '../../shared/testing/encryption.testing' +import { App } from '../../shared/type/domain.type' + +class TestProvisionService extends ProvisionService { + async provision(adminApiKeyHash?: string): Promise { + return this.run({ + adminApiKeyHash, + setupEncryption: undefined + }) + } +} + +export class VaultTest { + static createTestingModule(metadata: ModuleMetadata): TestingModuleBuilder { + const module = Test.createTestingModule(metadata) + .overrideModule(LoggerModule) + .useModule(LoggerModule.forTest()) + .overrideProvider(EncryptionModuleOptionProvider) + .useValue({ + keyring: getTestRawAesKeyring() + }) + // The encryption setup in production takes approximately 550ms. The + // provision is being called on the `beforeEach` hook which drastically + // increases the test time. Since most test cases don't need encryption to + // work correctly, we disable it to save 0.5 seconds per test case. + .overrideProvider(ProvisionService) + .useClass(TestProvisionService) + // Mock the event emitter because we don't want to send a + // connection.activated event after the creation. + .overrideProvider(EventEmitter2) + .useValue(mock()) + + return module + } +} diff --git a/apps/vault/src/app.repository.ts b/apps/vault/src/app.repository.ts new file mode 100644 index 000000000..27246f7fa --- /dev/null +++ b/apps/vault/src/app.repository.ts @@ -0,0 +1,86 @@ +import { coerce } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { KeyMetadata } from './shared/module/key-value/core/repository/key-value.repository' +import { KeyValueService } from './shared/module/key-value/core/service/key-value.service' +import { PrismaService } from './shared/module/persistence/service/prisma.service' +import { App, AppV1, Collection } from './shared/type/domain.type' + +@Injectable() +export class AppRepository { + constructor( + private keyValueService: KeyValueService, + private prismaService: PrismaService + ) {} + + private KEY_PREFIX = Collection.APP + + /** @deprecated */ + getMetadata(): KeyMetadata { + return { + collection: Collection.APP + } + } + + /** @deprecated */ + async findByIdV1(id: string): Promise { + const value = await this.keyValueService.get(this.getKey(id)) + + if (value) { + return coerce.decode(AppV1, value) + } + + return null + } + + async findById(id: string): Promise { + const value = await this.prismaService.vault.findUnique({ where: { id } }) + + if (value) { + return App.parse(value) + } + + return null + } + + async findAll(): Promise { + const values = await this.prismaService.vault.findMany() + + return values.map((value) => App.parse(value)) + } + + /** @deprecated */ + async saveV1(app: AppV1): Promise { + await this.keyValueService.set(this.getKey(app.id), coerce.encode(AppV1, app), this.getMetadata()) + + return app + } + + async save(app: App): Promise { + const appData = { + id: app.id, + encryptionKeyringType: app.encryptionKeyringType, + encryptionMasterKey: app.encryptionMasterKey, + encryptionMasterAwsKmsArn: app.encryptionMasterAwsKmsArn, + authDisabled: app.authDisabled, + adminApiKeyHash: app.adminApiKeyHash + } + + // You cannot update the encryption details; that will cause data corruption. + // Key rotation must be a separate process. + await this.prismaService.vault.upsert({ + where: { id: app.id }, + update: { + authDisabled: app.authDisabled, + adminApiKeyHash: app.adminApiKeyHash + }, + create: appData + }) + + return app + } + + /** @deprecated */ + getKey(id: string): string { + return `${this.KEY_PREFIX}:${id}` + } +} diff --git a/apps/vault/src/app.service.ts b/apps/vault/src/app.service.ts new file mode 100644 index 000000000..1ff07452c --- /dev/null +++ b/apps/vault/src/app.service.ts @@ -0,0 +1,75 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { AppRepository } from './app.repository' +import { Config } from './main.config' +import { App } from './shared/type/domain.type' +import { AppNotProvisionedException } from './vault/core/exception/app-not-provisioned.exception' +import { ProvisionException } from './vault/core/exception/provision.exception' + +@Injectable() +export class AppService { + constructor( + private configService: ConfigService, + private appRepository: AppRepository, + private logger: LoggerService + ) {} + + async getAppOrThrow(): Promise { + const app = await this.getApp() + + if (app) { + return app + } + + throw new AppNotProvisionedException() + } + + async getApp(): Promise { + // Find all & throw if more than one. Only 1 instance is supported. + const apps = await this.appRepository.findAll() + const app = apps?.find((app) => app.id === this.getId()) + if (apps?.length && apps.length > 1) { + throw new ProvisionException('Multiple app instances found; this can lead to data corruption') + } + + if (app) { + return app + } + + return null + } + + // IMPORTANT: The admin API key is hashed by the caller not the service. That + // allows us to have a declarative configuration file which is useful for + // automations like development or cloud set up. + async save(app: App): Promise { + await this.appRepository.save(app) + + return app + } + + private getId(): string { + return this.configService.get('app.id') + } + + /** Temporary migration function, converting the key-value format of the App config into the table format */ + async migrateV1Data(): Promise { + const appV1 = await this.appRepository.findByIdV1(this.getId()) + const appV2 = await this.appRepository.findById(this.getId()) + if (appV1 && !appV2) { + this.logger.log('Migrating App V1 data to V2') + const keyring = this.configService.get('keyring') + const app = App.parse({ + id: appV1.id, + adminApiKeyHash: appV1.adminApiKey, + encryptionMasterKey: appV1.masterKey, + encryptionKeyringType: appV1.masterKey ? 'raw' : 'awskms', + encryptionMasterAwsKmsArn: keyring.type === 'awskms' ? keyring.encryptionMasterAwsKmsArn : null, + authDisabled: false + }) + await this.appRepository.save(app) + this.logger.log('App V1 data migrated to V2 Successfully') + } + } +} diff --git a/apps/vault/src/broker/__test__/e2e/account.spec.ts b/apps/vault/src/broker/__test__/e2e/account.spec.ts new file mode 100644 index 000000000..cf528fb43 --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/account.spec.ts @@ -0,0 +1,177 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { PaginatedAccountsDto } from '../../http/rest/dto/response/paginated-accounts.dto' +import { PaginatedAddressesDto } from '../../http/rest/dto/response/paginated-addresses.dto' +import { ProviderAccountDto } from '../../http/rest/dto/response/provider-account.dto' +import { anchorageAccountOne, anchorageConnectionOne, seed, userPrivateKey } from '../../shared/__test__/fixture' +import { signedRequest } from '../../shared/__test__/request' +import { REQUEST_HEADER_CONNECTION_ID } from '../../shared/constant' +import { TEST_ACCOUNTS } from '../util/mock-data' + +import { VaultTest } from '../../../__test__/shared/vault.test' +import '../../shared/__test__/matcher' + +describe('Account', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + testPrismaService = module.get(TestPrismaService) + provisionService = module.get(ProvisionService) + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + + await seed(module) + + await app.init() + }) + + describe('GET /accounts', () => { + it('returns the list of accounts with addresses for the client', async () => { + const { status, body } = await signedRequest(app, userPrivateKey) + .get('/provider/accounts') + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toMatchZodSchema(PaginatedAccountsDto.schema) + expect(body.data).toHaveLength(2) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns 404 for unknown client', async () => { + const { status } = await signedRequest(app, userPrivateKey) + .get('/provider/accounts') + .set(REQUEST_HEADER_CLIENT_ID, 'unknown-client') + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + }) + + describe('GET /accounts with pagination', () => { + it('returns limited number of accounts when limit parameter is provided', async () => { + const limit = 1 + const { status, body } = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts?limit=${limit}`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toHaveLength(limit) + expect(body.page).toHaveProperty('next') + + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns next page of results using cursor', async () => { + // First request + const firstResponse = await signedRequest(app, userPrivateKey) + .get('/provider/accounts?limit=1') + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(firstResponse.status).toEqual(HttpStatus.OK) + + const cursor = firstResponse.body.page?.next + expect(cursor).toBeDefined() + + // Second request using the cursor + const secondResponse = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts?cursor=${cursor}&limit=1`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(secondResponse.body.data).toHaveLength(1) + expect(secondResponse.body.data[0].accountId).not.toBe(firstResponse.body.data[0].accountId) + + expect(secondResponse.status).toEqual(HttpStatus.OK) + }) + + it('handles ascending createdAt parameter correctly', async () => { + const response = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts?sortOrder=asc`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(response.status).toEqual(HttpStatus.OK) + + const returnedAccounts = response.body.data + expect(returnedAccounts).toHaveLength(TEST_ACCOUNTS.length) + expect(new Date(returnedAccounts[1].createdAt).getTime()).toBeGreaterThanOrEqual( + new Date(returnedAccounts[0].createdAt).getTime() + ) + }) + }) + + describe('GET /accounts/:accountId', () => { + it('returns the account details with addresses', async () => { + const { status, body } = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts/${anchorageAccountOne.accountId}`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toMatchZodSchema(ProviderAccountDto.schema) + expect(body.data.accountId).toEqual(anchorageAccountOne.accountId) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns 404 when account does not exist', async () => { + const { status } = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts/does-not-exist`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + + it('returns 404 when client does not exist', async () => { + const { status } = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts/${anchorageAccountOne.accountId}`) + .set(REQUEST_HEADER_CLIENT_ID, 'does-not-exist') + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + }) + + describe('GET /accounts/:accountId/addresses', () => { + it('returns the list of addresses for the account', async () => { + const { status, body } = await signedRequest(app, userPrivateKey) + .get(`/provider/accounts/${anchorageAccountOne.accountId}/addresses`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toMatchZodSchema(PaginatedAddressesDto.schema) + expect(status).toEqual(HttpStatus.OK) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/address.spec.ts b/apps/vault/src/broker/__test__/e2e/address.spec.ts new file mode 100644 index 000000000..efa9a3d2f --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/address.spec.ts @@ -0,0 +1,144 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { map } from 'lodash' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { PaginatedAddressesDto } from '../../http/rest/dto/response/paginated-addresses.dto' +import { anchorageAddressOne, anchorageConnectionOne, seed, userPrivateKey } from '../../shared/__test__/fixture' +import { signedRequest } from '../../shared/__test__/request' +import { REQUEST_HEADER_CONNECTION_ID } from '../../shared/constant' + +import { VaultTest } from '../../../__test__/shared/vault.test' +import '../../shared/__test__/matcher' + +describe('Address', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + testPrismaService = module.get(TestPrismaService) + provisionService = module.get(ProvisionService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + + await seed(module) + + await app.init() + }) + + describe('GET /addresses', () => { + it('returns the list of addresses for the client', async () => { + const { status, body } = await signedRequest(app, userPrivateKey) + .get(`/provider/addresses`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toMatchZodSchema(PaginatedAddressesDto.schema) + expect(body.data).toHaveLength(2) + expect(status).toEqual(HttpStatus.OK) + }) + }) + + describe('GET /addresses with pagination', () => { + it('returns limited number of addresses when limit parameter is provided', async () => { + const limit = 1 + const { body } = await signedRequest(app, userPrivateKey) + .get('/provider/addresses') + .query({ limit }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toHaveLength(limit) + expect(body.page).toHaveProperty('next') + }) + + it('returns next page of results using cursor', async () => { + const firstResponse = await signedRequest(app, userPrivateKey) + .get('/provider/addresses') + .query({ limit: 1 }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + const cursor = firstResponse.body.page?.next + + expect(cursor).toBeDefined() + + const secondResponse = await signedRequest(app, userPrivateKey) + .get('/provider/addresses') + .query({ cursor, limit: 1 }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(secondResponse.body.data).toHaveLength(1) + expect(secondResponse.body.data[0].addressId).not.toBe(firstResponse.body.data[0].addressId) + }) + + it('handles descending orderBy createdAt parameter correctly', async () => { + const { status, body } = await signedRequest(app, userPrivateKey) + .get('/provider/addresses') + .query({ orderBy: 'createdAt', desc: true }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + const addresses = body.data + + expect(addresses).toHaveLength(2) + expect(map(addresses, 'externalId')).toEqual(['address-external-id-two', 'address-external-id-one']) + expect(status).toEqual(HttpStatus.OK) + }) + }) + + describe('GET /addresses/:addressId', () => { + it('returns the address details', async () => { + const { status, body } = await signedRequest(app, userPrivateKey) + .get(`/provider/addresses/${anchorageAddressOne.addressId}`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toEqual({ + data: { + ...anchorageAddressOne, + createdAt: anchorageAddressOne.createdAt.toISOString(), + updatedAt: anchorageAddressOne.updatedAt.toISOString() + } + }) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns 404 with proper error message for non-existent address', async () => { + const { status } = await signedRequest(app, userPrivateKey) + .get('/provider/addresses/non-existent') + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/asset.spec.ts b/apps/vault/src/broker/__test__/e2e/asset.spec.ts new file mode 100644 index 000000000..148acfc49 --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/asset.spec.ts @@ -0,0 +1,91 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { ClientService } from '../../../client/core/service/client.service' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { Provider } from '../../core/type/provider.type' +import { AssetSeed } from '../../persistence/seed/asset.seed' +import { NetworkSeed } from '../../persistence/seed/network.seed' +import { signedRequest } from '../../shared/__test__/request' +import { TEST_CLIENT_ID, testClient, testUserPrivateJwk } from '../util/mock-data' + +import { VaultTest } from '../../../__test__/shared/vault.test' +import { PaginatedAssetsDto } from '../../http/rest/dto/response/paginated-assets.dto' +import '../../shared/__test__/matcher' + +describe('Asset', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + let clientService: ClientService + let networkSeed: NetworkSeed + let assetSeed: AssetSeed + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + clientService = module.get(ClientService) + networkSeed = module.get(NetworkSeed) + assetSeed = module.get(AssetSeed) + provisionService = module.get(ProvisionService) + testPrismaService = module.get(TestPrismaService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + await clientService.save(testClient) + + await networkSeed.seed() + await assetSeed.seed() + + await app.init() + }) + + describe('GET /assets', () => { + it('returns the list of assets for the specified provider', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/assets') + .query({ provider: Provider.ANCHORAGE }) + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .send() + + expect(body).toMatchZodSchema(PaginatedAssetsDto.schema) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns different assets for different providers', async () => { + const anchorageResponse = await signedRequest(app, testUserPrivateJwk) + .get('/provider/assets') + .query({ provider: Provider.ANCHORAGE }) + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .send() + + const fireblocksResponse = await signedRequest(app, testUserPrivateJwk) + .get('/provider/assets') + .query({ provider: Provider.FIREBLOCKS }) + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .send() + + expect(anchorageResponse.status).toEqual(HttpStatus.OK) + expect(fireblocksResponse.status).toEqual(HttpStatus.OK) + + expect(anchorageResponse.body.data).not.toEqual(fireblocksResponse.body.data) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/connection.spec.ts b/apps/vault/src/broker/__test__/e2e/connection.spec.ts new file mode 100644 index 000000000..e61e66724 --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/connection.spec.ts @@ -0,0 +1,869 @@ +import { EncryptionModuleOptionProvider } from '@narval/encryption-module' +import { LoggerModule, REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { hexSchema } from '@narval/policy-engine-shared' +import { + Alg, + Ed25519PrivateKey, + Hex, + SMALLEST_RSA_MODULUS_LENGTH, + ed25519PublicKeySchema, + generateJwk, + getPublicKey, + privateKeyToHex, + privateKeyToJwk, + privateKeyToPem, + rsaEncrypt, + rsaPublicKeySchema +} from '@narval/signature' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { Test, TestingModule } from '@nestjs/testing' +import { MockProxy, mock } from 'jest-mock-extended' +import { times } from 'lodash' +import request from 'supertest' +import { v4 as uuid } from 'uuid' +import { ClientService } from '../../../client/core/service/client.service' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository' +import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' +import { EncryptionKeyService } from '../../../transit-encryption/core/service/encryption-key.service' +import { AnchorageCredentialService } from '../../core/provider/anchorage/anchorage-credential.service' +import { ConnectionService } from '../../core/service/connection.service' +import { SyncService } from '../../core/service/sync.service' +import { ConnectionStatus } from '../../core/type/connection.type' +import { Provider } from '../../core/type/provider.type' +import { getJwsd, testClient, testUserPrivateJwk } from '../util/mock-data' + +import '../../shared/__test__/matcher' + +describe('Connection', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + + let clientService: ClientService + let connectionService: ConnectionService + let encryptionKeyService: EncryptionKeyService + let eventEmitterMock: MockProxy + let provisionService: ProvisionService + + const url = 'http://provider.narval.xyz' + + const userPrivateJwk = testUserPrivateJwk + const client = testClient + const clientId = testClient.clientId + + beforeAll(async () => { + eventEmitterMock = mock() + eventEmitterMock.emit.mockReturnValue(true) + + // IMPORTANT: Does not use `VaultTest.createTestingModule` because it would + // disable encryption and testing the connection credentials' encryption is + // critical. + module = await Test.createTestingModule({ + imports: [MainModule] + }) + .overrideModule(LoggerModule) + .useModule(LoggerModule.forTest()) + .overrideProvider(SyncService) + .useValue(eventEmitterMock) + .overrideProvider(KeyValueRepository) + .useValue(new InMemoryKeyValueRepository()) + .overrideProvider(EncryptionModuleOptionProvider) + .useValue({ + keyring: getTestRawAesKeyring() + }) + .compile() + + app = module.createNestApplication() + + clientService = module.get(ClientService) + connectionService = module.get(ConnectionService) + encryptionKeyService = module.get(EncryptionKeyService) + provisionService = module.get(ProvisionService) + testPrismaService = module.get(TestPrismaService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + + await clientService.save(client) + + await app.init() + }) + + describe('POST /connections/initiate', () => { + it('initiates a new connection to anchorage', async () => { + const connection = { + provider: Provider.ANCHORAGE, + url, + connectionId: uuid() + } + + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections/initiate') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections/initiate', + payload: connection + }) + ) + .send(connection) + + const { data } = body + + expect(body).toMatchObject({ + data: { + clientId, + connectionId: connection.connectionId, + provider: connection.provider, + status: ConnectionStatus.PENDING + } + }) + + // Ensure it doesn't leak the private key. + expect(data.credentials).toEqual(undefined) + expect(data.privateKey).toEqual(undefined) + + // Ensure it doesn't leak a private key as JWK by including the `d` + // property. + expect(data.publicKey.jwk).toMatchZodSchema(ed25519PublicKeySchema) + expect(data.encryptionPublicKey.jwk).toMatchZodSchema(rsaPublicKeySchema) + + expect(data.publicKey.hex).toMatchZodSchema(hexSchema) + + expect(data.publicKey.keyId).toEqual(expect.any(String)) + expect(data.encryptionPublicKey.keyId).toEqual(expect.any(String)) + expect(data.encryptionPublicKey.pem).toEqual(expect.any(String)) // ensure we also respond w/ the PEM format. + + expect(status).toEqual(HttpStatus.CREATED) + }) + }) + + describe('POST /provider/connections', () => { + it('overrides the existing connection private key when providing a new one on pending connection activation', async () => { + const connectionId = uuid() + const provider = Provider.ANCHORAGE + const label = 'Test Anchorage Connection' + const credentials = { + apiKey: 'test-api-key', + // Adding private key to test override + privateKey: '0x9ead9e0c93e9f4d02a09d4d3fdd35eac82452717a04ca98580302c16485b2480' + } + + // First create a pending connection + const { body: pendingConnection } = await request(app.getHttpServer()) + .post('/provider/connections/initiate') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections/initiate', + payload: { connectionId, provider } + }) + ) + .send({ connectionId, provider }) + + // Encrypt the credentials including the new private key + const encryptedCredentials = await rsaEncrypt( + JSON.stringify(credentials), + pendingConnection.data.encryptionPublicKey.jwk + ) + + // Activate the connection with the new credentials + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: { + provider, + connectionId, + label, + url, + encryptedCredentials + } + }) + ) + .send({ + provider, + connectionId, + label, + url, + encryptedCredentials + }) + + expect(body).toEqual({ + data: { + clientId, + connectionId, + label, + provider, + url, + createdAt: expect.any(String), + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(String) + } + }) + + expect(status).toEqual(HttpStatus.CREATED) + + const createdConnection = await connectionService.findById(clientId, connectionId) + const createdCredential = await connectionService.findCredentials({ ...createdConnection, provider }) + + expect(createdConnection).toMatchObject({ + clientId, + connectionId, + label, + provider, + url, + createdAt: expect.any(Date), + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(Date) + }) + + const givenPrivateKey = privateKeyToJwk(credentials.privateKey as Hex, AnchorageCredentialService.SIGNING_KEY_ALG) + + expect(createdCredential?.publicKey).toEqual(getPublicKey(givenPrivateKey)) + expect(createdCredential?.privateKey).toEqual(givenPrivateKey) + }) + + describe('anchorage', () => { + it('creates a new connection to anchorage with plain credentials', async () => { + const privateKey = await generateJwk(Alg.EDDSA) + const privateKeyHex = await privateKeyToHex(privateKey) + const connectionId = uuid() + const connection = { + provider: Provider.ANCHORAGE, + connectionId, + label: 'Test Anchorage Connection', + url, + credentials: { + apiKey: 'test-api-key', + privateKey: privateKeyHex + } + } + + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: connection + }) + ) + .send(connection) + + const createdConnection = await connectionService.findById(clientId, connection.connectionId) + const createdCredentials = await connectionService.findCredentials(createdConnection) + + expect(body).toEqual({ + data: { + clientId, + connectionId, + url, + createdAt: expect.any(String), + label: connection.label, + provider: connection.provider, + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(String) + } + }) + + expect(status).toEqual(HttpStatus.CREATED) + + expect(createdConnection).toMatchObject({ + clientId, + createdAt: expect.any(Date), + connectionId, + label: connection.label, + provider: connection.provider, + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(Date), + url: connection.url + }) + + expect(createdCredentials).toEqual({ + apiKey: connection.credentials.apiKey, + privateKey, + publicKey: getPublicKey(privateKey as Ed25519PrivateKey) + }) + }) + + it('activates an anchorage pending connection with plain credentials', async () => { + const connectionId = uuid() + const provider = Provider.ANCHORAGE + const label = 'Test Anchorage Connection' + const credentials = { apiKey: 'test-api-key' } + + const { body: pendingConnection } = await request(app.getHttpServer()) + .post('/provider/connections/initiate') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections/initiate', + payload: { connectionId, provider } + }) + ) + .send({ connectionId, provider }) + + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: { + connectionId, + credentials, + label, + provider, + url + } + }) + ) + .send({ + connectionId, + credentials, + label, + provider, + url + }) + + const createdConnection = await connectionService.findById(clientId, connectionId) + const createdCredentials = await connectionService.findCredentials(createdConnection) + + expect(body).toEqual({ + data: { + clientId, + connectionId, + label, + provider, + url, + createdAt: expect.any(String), + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(String) + } + }) + expect(status).toEqual(HttpStatus.CREATED) + + expect(createdConnection).toMatchObject({ + clientId, + connectionId, + label, + provider, + url, + createdAt: expect.any(Date), + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(Date) + }) + + expect(createdCredentials).toMatchObject({ + apiKey: credentials.apiKey, + publicKey: pendingConnection.data.publicKey.jwk + }) + }) + + it('activates an anchorage pending connection with encrypted credentials', async () => { + const connectionId = uuid() + const provider = Provider.ANCHORAGE + const label = 'Test Anchorage Connection' + const credentials = { + apiKey: 'test-api-key' + } + + const { body: pendingConnection } = await request(app.getHttpServer()) + .post('/provider/connections/initiate') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections/initiate', + payload: { connectionId, provider } + }) + ) + .send({ connectionId, provider }) + + const encryptedCredentials = await rsaEncrypt( + JSON.stringify(credentials), + pendingConnection.data.encryptionPublicKey.jwk + ) + + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: { + provider, + connectionId, + label, + url, + encryptedCredentials + } + }) + ) + .send({ + provider, + connectionId, + label, + url, + encryptedCredentials + }) + + expect(body).toEqual({ + data: { + clientId, + connectionId, + label, + provider, + url, + createdAt: expect.any(String), + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(String) + } + }) + expect(status).toEqual(HttpStatus.CREATED) + + const createdConnection = await connectionService.findById(clientId, connectionId) + const createdCredentials = await connectionService.findCredentials(createdConnection) + + expect(createdConnection).toMatchObject({ + clientId, + connectionId, + label, + provider, + url, + createdAt: expect.any(Date), + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(Date) + }) + + expect(createdCredentials).toMatchObject({ + apiKey: credentials.apiKey, + publicKey: pendingConnection.data.publicKey.jwk + }) + }) + }) + + // NOTE: When adding tests for a new provider, focus only on testing + // provider-specific credential formats. Common functionality like + // credential encryption/decryption is already covered by existing tests. + // The main goal is to verify that the API correctly handles the unique + // input credential structure for each provider. + describe('fireblocks', () => { + it('creates a new connection to fireblocks with plain credentials', async () => { + const rsaPrivateKey = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) + const pem = await privateKeyToPem(rsaPrivateKey, Alg.RS256) + const connectionId = uuid() + const connection = { + provider: Provider.FIREBLOCKS, + connectionId, + label: 'Test Fireblocks Connection', + url, + credentials: { + apiKey: 'test-api-key', + privateKey: Buffer.from(pem).toString('base64') + } + } + + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: connection + }) + ) + .send(connection) + + const createdConnection = await connectionService.findById(clientId, connection.connectionId) + const createdCredentials = await connectionService.findCredentials(createdConnection) + + expect(body).toEqual({ + data: { + clientId, + connectionId, + url, + createdAt: expect.any(String), + label: connection.label, + provider: connection.provider, + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(String) + } + }) + + expect(status).toEqual(HttpStatus.CREATED) + + expect(createdConnection).toMatchObject({ + clientId, + createdAt: expect.any(Date), + connectionId, + label: connection.label, + provider: connection.provider, + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(Date), + url: connection.url + }) + + expect(createdCredentials).toEqual({ + apiKey: connection.credentials.apiKey, + privateKey: rsaPrivateKey, + publicKey: getPublicKey(rsaPrivateKey) + }) + }) + }) + describe('bitgo', () => { + it('creates a new connection to bitgo with plain credentials', async () => { + const connectionId = uuid() + const connection = { + provider: Provider.BITGO, + connectionId, + label: 'Test Bitgo Connection', + url, + credentials: { + apiKey: 'test-api-key', + walletPassphrase: 'test-wallet-passphrase' + } + } + + const { status, body } = await request(app.getHttpServer()) + .post('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: connection + }) + ) + .send(connection) + + const createdConnection = await connectionService.findById(clientId, connection.connectionId) + const createdCredentials = await connectionService.findCredentials(createdConnection) + + expect(body).toEqual({ + data: { + clientId, + connectionId, + url, + createdAt: expect.any(String), + label: connection.label, + provider: connection.provider, + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(String) + } + }) + + expect(status).toEqual(HttpStatus.CREATED) + + expect(createdConnection).toMatchObject({ + clientId, + createdAt: expect.any(Date), + connectionId, + label: connection.label, + provider: connection.provider, + status: ConnectionStatus.ACTIVE, + updatedAt: expect.any(Date), + url: connection.url + }) + + expect(createdCredentials).toEqual({ + apiKey: connection.credentials.apiKey, + walletPassphrase: connection.credentials.walletPassphrase + }) + }) + }) + }) + + describe('DELETE /provider/connections/:id', () => { + it('revokes an existing connection', async () => { + const connection = await connectionService.create(clientId, { + connectionId: uuid(), + label: 'test connection', + provider: Provider.ANCHORAGE, + url, + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + + const { status } = await request(app.getHttpServer()) + .delete(`/provider/connections/${connection.connectionId}`) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: `/provider/connections/${connection.connectionId}`, + payload: {}, + htm: 'DELETE' + }) + ) + .send() + + expect(status).toEqual(HttpStatus.NO_CONTENT) + + const updatedConnection = await connectionService.findById(clientId, connection.connectionId) + const credentials = await connectionService.findCredentials(connection) + + expect(updatedConnection.revokedAt).toEqual(expect.any(Date)) + expect(updatedConnection.status).toEqual(ConnectionStatus.REVOKED) + expect(credentials).toEqual(null) + }) + }) + + describe('GET /provider/connections', () => { + beforeEach(async () => { + await Promise.all( + times(3, async () => + connectionService.create(clientId, { + connectionId: uuid(), + label: 'test connection', + provider: Provider.ANCHORAGE, + url, + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + ) + ) + }) + + it('responds with connections from the given client', async () => { + const { status, body } = await request(app.getHttpServer()) + .get('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(body.data.length).toEqual(3) + + expect(body.data[0]).toMatchObject({ + clientId, + url, + connectionId: expect.any(String), + createdAt: expect.any(String), + label: expect.any(String), + provider: Provider.ANCHORAGE, + updatedAt: expect.any(String) + }) + expect(body.data[0]).not.toHaveProperty('credentials') + + expect(status).toEqual(HttpStatus.OK) + }) + + it('responds with limited number of syncs when limit is given', async () => { + const { body } = await request(app.getHttpServer()) + .get('/provider/connections') + .query({ limit: 1 }) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections?limit=1', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(body.data.length).toEqual(1) + expect(body.page).toHaveProperty('next') + }) + + it('responds the next page of results when cursors is given', async () => { + const { body: pageOne } = await request(app.getHttpServer()) + .get('/provider/connections') + .query({ limit: 1 }) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: '/provider/connections?limit=1', + payload: {}, + htm: 'GET' + }) + ) + .send() + + const { body: pageTwo } = await request(app.getHttpServer()) + .get(`/provider/connections?limit=1&cursor=${pageOne.page.next}`) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: `/provider/connections?limit=1&cursor=${pageOne.page.next}`, + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(pageTwo.data.length).toEqual(1) + expect(pageTwo.page).toHaveProperty('next') + }) + + it('throws error if the request was not signed', async () => { + const { status } = await request(app.getHttpServer()) + .get('/provider/connections') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .send() + + expect(status).toEqual(HttpStatus.UNAUTHORIZED) + }) + }) + + describe('GET /provider/connections/:connectionId', () => { + it('responds with the specific connection', async () => { + const connection = await connectionService.create(clientId, { + connectionId: uuid(), + label: 'test connection', + provider: Provider.ANCHORAGE, + url, + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + + const { status, body } = await request(app.getHttpServer()) + .get(`/provider/connections/${connection.connectionId}`) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: `/provider/connections/${connection.connectionId}`, + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(body.data).toMatchObject({ + connectionId: expect.any(String), + clientId, + createdAt: expect.any(String), + updatedAt: expect.any(String), + label: 'test connection', + provider: Provider.ANCHORAGE, + url + }) + expect(body).not.toHaveProperty('credentials') + + expect(status).toEqual(HttpStatus.OK) + }) + }) + + describe('PATCH /provider/connections/:connectionId', () => { + it('updates the given connection', async () => { + const connection = await connectionService.create(clientId, { + connectionId: uuid(), + label: 'test connection', + provider: Provider.ANCHORAGE, + url, + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + const newPrivateKey = await generateJwk(Alg.EDDSA) + const newCredentials = { + apiKey: 'new-api-key', + privateKey: await privateKeyToHex(newPrivateKey) + } + const encryptionKey = await encryptionKeyService.generate(clientId, { + modulusLength: SMALLEST_RSA_MODULUS_LENGTH + }) + const encryptedCredentials = await rsaEncrypt(JSON.stringify(newCredentials), encryptionKey.publicKey) + + const { status, body } = await request(app.getHttpServer()) + .patch(`/provider/connections/${connection.connectionId}`) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk, + requestUrl: `/provider/connections/${connection.connectionId}`, + payload: { + label: 'new label', + encryptedCredentials + }, + htm: 'PATCH' + }) + ) + .send({ + label: 'new label', + encryptedCredentials + }) + + expect(body).toMatchObject({ + data: expect.objectContaining({ + label: 'new label' + }) + }) + + expect(body.data).not.toHaveProperty('credentials') + + expect(status).toEqual(HttpStatus.OK) + + const updatedConnection = await connectionService.findById(clientId, connection.connectionId) + const updatedCredentials = await connectionService.findCredentials({ + ...updatedConnection, + provider: Provider.ANCHORAGE + }) + + expect(updatedCredentials?.apiKey).toEqual(newCredentials.apiKey) + expect(updatedCredentials?.privateKey).toEqual(newPrivateKey) + expect(updatedCredentials?.publicKey).toEqual(getPublicKey(newPrivateKey as Ed25519PrivateKey)) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/known-destinations.spec.ts b/apps/vault/src/broker/__test__/e2e/known-destinations.spec.ts new file mode 100644 index 000000000..4c42d6065 --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/known-destinations.spec.ts @@ -0,0 +1,122 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { Alg, generateJwk, privateKeyToHex } from '@narval/signature' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { v4 as uuid } from 'uuid' +import { ClientService } from '../../../client/core/service/client.service' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { ANCHORAGE_TEST_API_BASE_URL, getHandlers } from '../../core/provider/anchorage/__test__/server-mock/server' +import { ConnectionService } from '../../core/service/connection.service' +import { Provider } from '../../core/type/provider.type' +import { PaginatedKnownDestinationsDto } from '../../http/rest/dto/response/paginated-known-destinations.dto' +import { AssetSeed } from '../../persistence/seed/asset.seed' +import { NetworkSeed } from '../../persistence/seed/network.seed' +import { setupMockServer } from '../../shared/__test__/mock-server' +import { REQUEST_HEADER_CONNECTION_ID } from '../../shared/constant' +import { getJwsd, testClient, testUserPrivateJwk } from '../util/mock-data' + +import { VaultTest } from '../../../__test__/shared/vault.test' +import '../../shared/__test__/matcher' + +describe('Known Destination', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + let clientService: ClientService + let connectionService: ConnectionService + let assetSeed: AssetSeed + let networkSeed: NetworkSeed + + let connectionId: string + + setupMockServer(getHandlers()) + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + assetSeed = module.get(AssetSeed) + clientService = module.get(ClientService) + connectionService = module.get(ConnectionService) + networkSeed = module.get(NetworkSeed) + provisionService = module.get(ProvisionService) + testPrismaService = module.get(TestPrismaService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + + await clientService.save(testClient) + await networkSeed.seed() + await assetSeed.seed() + + await app.init() + + const connection = await connectionService.create(testClient.clientId, { + connectionId: uuid(), + provider: Provider.ANCHORAGE, + url: ANCHORAGE_TEST_API_BASE_URL, + createdAt: new Date(), + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + + connectionId = connection.connectionId + }) + + describe('GET /provider/known-destinations', () => { + it('returns known destinations for a connection', async () => { + const { status, body } = await request(app.getHttpServer()) + .get('/provider/known-destinations') + .set(REQUEST_HEADER_CLIENT_ID, testClient.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connectionId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: `/provider/known-destinations`, + payload: {}, + htm: 'GET' + }) + ) + + expect(body).toMatchZodSchema(PaginatedKnownDestinationsDto.schema) + expect(status).toBe(HttpStatus.OK) + }) + + it('returns 400 when connection id header is missing', async () => { + const { status } = await request(app.getHttpServer()) + .get('/provider/known-destinations') + .set(REQUEST_HEADER_CLIENT_ID, testClient.clientId) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: `/provider/known-destinations`, + payload: {}, + htm: 'GET' + }) + ) + + expect(status).toBe(HttpStatus.BAD_REQUEST) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/network.spec.ts b/apps/vault/src/broker/__test__/e2e/network.spec.ts new file mode 100644 index 000000000..dc874df15 --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/network.spec.ts @@ -0,0 +1,68 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { ClientService } from '../../../client/core/service/client.service' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { PaginatedNetworksDto } from '../../http/rest/dto/response/paginated-networks.dto' +import { NetworkSeed } from '../../persistence/seed/network.seed' +import { signedRequest } from '../../shared/__test__/request' +import { TEST_CLIENT_ID, testClient, testUserPrivateJwk } from '../util/mock-data' + +import { VaultTest } from '../../../__test__/shared/vault.test' +import '../../shared/__test__/matcher' + +describe('Network', () => { + let app: INestApplication + let module: TestingModule + + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + let clientService: ClientService + let networkSeed: NetworkSeed + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + clientService = module.get(ClientService) + networkSeed = module.get(NetworkSeed) + provisionService = module.get(ProvisionService) + testPrismaService = module.get(TestPrismaService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + await clientService.save(testClient) + + await networkSeed.seed() + + await app.init() + }) + + describe('GET /networks', () => { + it('returns the list of networks', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/networks') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .send() + + expect(body).toMatchZodSchema(PaginatedNetworksDto.schema) + expect(body.data).toHaveLength(networkSeed.getNetworks().length) + expect(status).toEqual(HttpStatus.OK) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/proxy.spec.ts b/apps/vault/src/broker/__test__/e2e/proxy.spec.ts new file mode 100644 index 000000000..8b0d0dd7e --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/proxy.spec.ts @@ -0,0 +1,342 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { Alg, generateJwk, privateKeyToHex } from '@narval/signature' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import nock from 'nock' +import request from 'supertest' +import { VaultTest } from '../../../__test__/shared/vault.test' +import { ClientService } from '../../../client/core/service/client.service' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { ConnectionService } from '../../core/service/connection.service' +import { Provider } from '../../core/type/provider.type' +import { REQUEST_HEADER_CONNECTION_ID } from '../../shared/constant' +import { getJwsd, testClient, testUserPrivateJwk } from '../util/mock-data' + +describe('Proxy', () => { + let app: INestApplication + let module: TestingModule + let connectionService: ConnectionService + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + let clientService: ClientService + + const TEST_CLIENT_ID = 'test-client-id' + const TEST_CONNECTION_ID = 'test-connection-id' + const MOCK_API_URL = 'https://api.anchorage-staging.com' + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + testPrismaService = module.get(TestPrismaService) + provisionService = module.get(ProvisionService) + clientService = module.get(ClientService) + connectionService = module.get(ConnectionService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + nock.cleanAll() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + // Clean any pending nock interceptors + nock.cleanAll() + + await provisionService.provision() + await clientService.save(testClient) + + // Create a test connection + await connectionService.create(TEST_CLIENT_ID, { + connectionId: TEST_CONNECTION_ID, + label: 'test connection', + provider: Provider.ANCHORAGE, + url: MOCK_API_URL, + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + + await app.init() + }) + + it('forwards GET request', async () => { + nock(MOCK_API_URL).get('/v2/vaults').reply(200, { data: 'mock response' }) + + const response = await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + + expect(response.status).toBe(HttpStatus.OK) + expect(response.body).toEqual({ data: 'mock response' }) + }) + + it('forwards POST request', async () => { + nock(MOCK_API_URL).post('/v2/wallets').reply(201, { data: 'mock response' }) + + const response = await request(app.getHttpServer()) + .post('/provider/proxy/v2/wallets') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/wallets', + payload: { test: 'data' }, + htm: 'POST' + }) + ) + .send({ test: 'data' }) + + expect(response.body).toEqual({ data: 'mock response' }) + expect(response.status).toBe(HttpStatus.CREATED) + }) + + it('forwards DELETE request', async () => { + nock(MOCK_API_URL).delete('/v2/vaults').reply(204) + + const response = await request(app.getHttpServer()) + .delete('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'DELETE' + }) + ) + .send() + + expect(response.status).toBe(HttpStatus.NO_CONTENT) + }) + + it('forwards PUT request', async () => { + nock(MOCK_API_URL).put('/v2/vaults').reply(200, { data: 'mock response' }) + + const response = await request(app.getHttpServer()) + .put('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: { test: 'data' }, + htm: 'PUT' + }) + ) + .send({ test: 'data' }) + + expect(response.body).toEqual({ data: 'mock response' }) + expect(response.status).toBe(HttpStatus.OK) + }) + + it('forwards PATCH request', async () => { + nock(MOCK_API_URL).patch('/v2/vaults').reply(200, { data: 'mock response' }) + + const response = await request(app.getHttpServer()) + .patch('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: { test: 'data' }, + htm: 'PATCH' + }) + ) + .send({ test: 'data' }) + + expect(response.body).toEqual({ data: 'mock response' }) + expect(response.status).toBe(HttpStatus.OK) + }) + + it('adds api-key header', async () => { + let capturedHeaders: Record = {} + + nock(MOCK_API_URL) + .get('/v2/vaults') + .reply(function () { + capturedHeaders = this.req.headers + return [200, { data: 'mock response' }] + }) + + await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(capturedHeaders['api-access-key']).toBe('test-api-key') + }) + + it(`doesn't leak client-id header`, async () => { + let capturedHeaders: Record = {} + + nock(MOCK_API_URL) + .get('/v2/vaults') + .reply(function () { + capturedHeaders = this.req.headers + return [200, { data: 'mock response' }] + }) + + await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(capturedHeaders[REQUEST_HEADER_CLIENT_ID.toLowerCase()]).toBeUndefined() + }) + + it(`doesn't leak connection-id header`, async () => { + let capturedHeaders: Record = {} + + nock(MOCK_API_URL) + .get('/v2/vaults') + .reply(function () { + capturedHeaders = this.req.headers + return [200, { data: 'mock response' }] + }) + + await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(capturedHeaders[REQUEST_HEADER_CONNECTION_ID.toLowerCase()]).toBeUndefined() + }) + + it(`doesn't tamper with the error response`, async () => { + nock(MOCK_API_URL).get('/v2/vaults').reply(512, { error: 'mock error' }, { 'x-custom-header': 'custom value' }) + + const response = await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(response.body).toEqual({ error: 'mock error' }) + expect(response.status).toBe(512) + expect(response.header['x-custom-header']).toBe('custom value') + }) + + it('throws a connection invalid exception when the connection is not active', async () => { + await connectionService.revoke(TEST_CLIENT_ID, TEST_CONNECTION_ID) + + const response = await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + .send() + + expect(response.body).toEqual({ + message: 'Connection is not active', + context: { connectionId: TEST_CONNECTION_ID, clientId: TEST_CLIENT_ID, status: 'revoked' }, + stack: expect.any(String), + statusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + expect(response.status).toBe(HttpStatus.UNPROCESSABLE_ENTITY) + }) + + it('correctly forwards response headers without conflicts', async () => { + nock(MOCK_API_URL).get('/v2/vaults').reply( + 200, + { data: 'mock response' }, + { + 'transfer-encoding': 'chunked' + } + ) + + const response = await request(app.getHttpServer()) + .get('/provider/proxy/v2/vaults') + .set(REQUEST_HEADER_CLIENT_ID, TEST_CLIENT_ID) + .set(REQUEST_HEADER_CONNECTION_ID, TEST_CONNECTION_ID) + .set( + 'detached-jws', + await getJwsd({ + userPrivateJwk: testUserPrivateJwk, + requestUrl: '/provider/proxy/v2/vaults', + payload: {}, + htm: 'GET' + }) + ) + + // If there's a header conflict, nock will throw before we get here + expect(response.body).toEqual({ data: 'mock response' }) + expect(response.status).toBe(HttpStatus.OK) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/transfer.spec.ts b/apps/vault/src/broker/__test__/e2e/transfer.spec.ts new file mode 100644 index 000000000..e76c26e0a --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/transfer.spec.ts @@ -0,0 +1,474 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { Ed25519PrivateKey, getPublicKey } from '@narval/signature' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { HttpResponse, http } from 'msw' +import request from 'supertest' +import { v4 as uuid } from 'uuid' +import { VaultTest } from '../../../__test__/shared/vault.test' +import { ClientService } from '../../../client/core/service/client.service' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import postAnchorageTransferBadRequest from '../../core/provider/anchorage/__test__/server-mock/response/post-transfer-400.json' +import { ANCHORAGE_TEST_API_BASE_URL, getHandlers } from '../../core/provider/anchorage/__test__/server-mock/server' +import { ConnectionStatus, ConnectionWithCredentials } from '../../core/type/connection.type' +import { Account, Wallet } from '../../core/type/indexed-resources.type' +import { Provider } from '../../core/type/provider.type' +import { + InternalTransfer, + NetworkFeeAttribution, + SendTransfer, + TransferPartyType, + TransferStatus +} from '../../core/type/transfer.type' +import { AccountRepository } from '../../persistence/repository/account.repository' +import { ConnectionRepository } from '../../persistence/repository/connection.repository' +import { TransferRepository } from '../../persistence/repository/transfer.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' +import { AssetSeed } from '../../persistence/seed/asset.seed' +import { NetworkSeed } from '../../persistence/seed/network.seed' +import { setupMockServer } from '../../shared/__test__/mock-server' +import { REQUEST_HEADER_CONNECTION_ID } from '../../shared/constant' +import { getJwsd, testClient, testUserPrivateJwk } from '../util/mock-data' + +const ENDPOINT = '/provider/transfers' + +describe('Transfer', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + let clientService: ClientService + + let accountRepository: AccountRepository + let connectionRepository: ConnectionRepository + let networkSeed: NetworkSeed + let assetSeed: AssetSeed + let transferRepository: TransferRepository + let walletRepository: WalletRepository + + const clientId = testClient.clientId + + const externalId = '008d3ec72558ce907571886df63ef51594b5bd8cf106a0b7fa8f12a30dfc867f' + + const walletId = uuid() + + const eddsaPrivateKey: Ed25519PrivateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0xa6fe705025aa4c48abbb3a1ed679d7dc7d18e7994b4d5cb1884479fddeb2e706', + x: 'U4WSOMzD7gor6jiVz42jT22JGBcfGfzMomt8PFC_-_U', + d: 'evo-fY2BX60V1n3Z690LadH5BvizcM9bESaYk0LsxyQ' + } + + const connection: ConnectionWithCredentials = { + clientId, + connectionId: uuid(), + createdAt: new Date(), + provider: Provider.ANCHORAGE, + status: ConnectionStatus.ACTIVE, + updatedAt: new Date(), + url: ANCHORAGE_TEST_API_BASE_URL, + credentials: { + privateKey: eddsaPrivateKey, + publicKey: getPublicKey(eddsaPrivateKey), + apiKey: 'test-api-key' + } + } + + const accountOne: Account = { + accountId: uuid(), + addresses: [], + clientId, + createdAt: new Date(), + externalId: uuid(), + label: 'Account 1', + networkId: 'BTC', + connectionId: connection.connectionId, + provider: Provider.ANCHORAGE, + updatedAt: new Date(), + walletId + } + + const accountTwo: Account = { + accountId: uuid(), + addresses: [], + clientId, + createdAt: new Date(), + externalId: uuid(), + label: 'Account 2', + networkId: 'BTC', + connectionId: connection.connectionId, + provider: Provider.ANCHORAGE, + updatedAt: new Date(), + walletId + } + + const wallet: Wallet = { + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: uuid(), + label: null, + provider: Provider.ANCHORAGE, + updatedAt: new Date(), + walletId + } + + const internalTransfer: InternalTransfer = { + clientId, + assetExternalId: null, + assetId: 'BTC', + createdAt: new Date(), + customerRefId: null, + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + externalId: uuid(), + externalStatus: null, + grossAmount: '0.00001', + idempotenceId: uuid(), + connectionId: connection.connectionId, + memo: 'Test transfer', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + provider: Provider.ANCHORAGE, + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + status: TransferStatus.SUCCESS, + transferId: uuid() + } + + const mockServer = setupMockServer(getHandlers()) + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + accountRepository = module.get(AccountRepository) + assetSeed = module.get(AssetSeed) + clientService = module.get(ClientService) + connectionRepository = module.get(ConnectionRepository) + networkSeed = module.get(NetworkSeed) + provisionService = module.get(ProvisionService) + testPrismaService = module.get(TestPrismaService) + transferRepository = module.get(TransferRepository) + walletRepository = module.get(WalletRepository) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + + await provisionService.provision() + await clientService.save(testClient) + + await connectionRepository.create(connection) + await walletRepository.bulkCreate([wallet]) + await accountRepository.bulkCreate([accountOne, accountTwo]) + await transferRepository.bulkCreate([internalTransfer]) + + await networkSeed.seed() + await assetSeed.seed() + + await app.init() + }) + + describe(`POST ${ENDPOINT}`, () => { + let requiredPayload: SendTransfer + + beforeEach(() => { + requiredPayload = { + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + amount: '0.0001', + asset: { + assetId: 'BTC' + }, + idempotenceId: uuid() + } + }) + + it('sends transfer to anchorage', async () => { + const { status, body } = await request(app.getHttpServer()) + .post(ENDPOINT) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connection.connectionId) + .set( + 'detached-jws', + await getJwsd({ + payload: requiredPayload, + userPrivateJwk: testUserPrivateJwk, + requestUrl: ENDPOINT, + htm: 'POST' + }) + ) + .send(requiredPayload) + + expect(body).toEqual({ + data: { + clientId, + externalId, + assetId: requiredPayload.asset.assetId, + assetExternalId: requiredPayload.asset.assetId, + createdAt: expect.any(String), + customerRefId: null, + destination: requiredPayload.destination, + externalStatus: expect.any(String), + grossAmount: requiredPayload.amount, + connectionId: connection.connectionId, + idempotenceId: expect.any(String), + memo: null, + networkFeeAttribution: NetworkFeeAttribution.ON_TOP, + provider: accountOne.provider, + providerSpecific: null, + source: requiredPayload.source, + status: TransferStatus.PROCESSING, + transferId: expect.any(String) + } + }) + + expect(status).toEqual(HttpStatus.CREATED) + }) + + it('sends transfer with optional properties', async () => { + const payload = { + ...requiredPayload, + memo: 'Test transfer', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + customerRefId: uuid(), + idempotenceId: uuid() + } + + const { body } = await request(app.getHttpServer()) + .post(ENDPOINT) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connection.connectionId) + .set( + 'detached-jws', + await getJwsd({ + payload, + userPrivateJwk: testUserPrivateJwk, + requestUrl: ENDPOINT, + htm: 'POST' + }) + ) + .send(payload) + + const actualTransfer = await transferRepository.findById(clientId, body.data.transferId) + + expect(actualTransfer.memo).toEqual(payload.memo) + expect(body.data.memo).toEqual(payload.memo) + + expect(actualTransfer.networkFeeAttribution).toEqual(payload.networkFeeAttribution) + expect(body.data.networkFeeAttribution).toEqual(payload.networkFeeAttribution) + + // Invalid fields should not be sent to the provider + expect(actualTransfer.customerRefId).toEqual(null) + expect(body.data.customerRefId).toEqual(null) + + expect(actualTransfer.idempotenceId).toEqual(payload.idempotenceId) + expect(body.data.idempotenceId).toEqual(payload.idempotenceId) + }) + + it('propagates provider http errors', async () => { + mockServer.use( + http.post(`${ANCHORAGE_TEST_API_BASE_URL}/v2/transfers`, () => { + return new HttpResponse(JSON.stringify(postAnchorageTransferBadRequest), { + status: HttpStatus.BAD_REQUEST + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as unknown as any) + }) + ) + + const { status, body } = await request(app.getHttpServer()) + .post(ENDPOINT) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connection.connectionId) + .set( + 'detached-jws', + await getJwsd({ + payload: requiredPayload, + userPrivateJwk: testUserPrivateJwk, + requestUrl: ENDPOINT, + htm: 'POST' + }) + ) + .send(requiredPayload) + + expect(body).toMatchObject({ + statusCode: 400, + message: 'Provider ANCHORAGE responded with 400 error', + context: { + provider: 'anchorage', + error: { + errorType: 'InternalError', + message: "Missing required field 'amount'." + } + } + }) + + expect(status).toEqual(HttpStatus.BAD_REQUEST) + }) + + it('fails if connection header is missing', async () => { + const { body } = await request(app.getHttpServer()) + .post(ENDPOINT) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + payload: requiredPayload, + userPrivateJwk: testUserPrivateJwk, + requestUrl: ENDPOINT, + htm: 'POST' + }) + ) + .send(requiredPayload) + + expect(body).toMatchObject({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'Missing or invalid x-connection-id header' + }) + }) + + it('responds with conflict when idempotence id was already used', async () => { + await request(app.getHttpServer()) + .post(ENDPOINT) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connection.connectionId) + .set( + 'detached-jws', + await getJwsd({ + payload: requiredPayload, + userPrivateJwk: testUserPrivateJwk, + requestUrl: ENDPOINT, + htm: 'POST' + }) + ) + .send(requiredPayload) + + const { status } = await request(app.getHttpServer()) + .post(ENDPOINT) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connection.connectionId) + .set( + 'detached-jws', + await getJwsd({ + payload: requiredPayload, + userPrivateJwk: testUserPrivateJwk, + requestUrl: ENDPOINT, + htm: 'POST' + }) + ) + .send(requiredPayload) + + expect(status).toEqual(HttpStatus.CONFLICT) + }) + }) + + describe(`GET ${ENDPOINT}/:transferId`, () => { + const internalTransfer = { + assetExternalId: null, + assetId: 'BTC', + clientId: connection.clientId, + createdAt: new Date(), + customerRefId: uuid(), + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + externalId: uuid(), + externalStatus: null, + grossAmount: '0.00001', + idempotenceId: uuid(), + memo: 'Test transfer', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + provider: Provider.ANCHORAGE, + connectionId: connection.connectionId, + providerSpecific: null, + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + status: TransferStatus.PROCESSING, + transferId: uuid() + } + + beforeEach(async () => { + await transferRepository.bulkCreate([internalTransfer]) + }) + + it('responds with the specific transfer', async () => { + const { status, body } = await request(app.getHttpServer()) + .get(`${ENDPOINT}/${internalTransfer.transferId}`) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set(REQUEST_HEADER_CONNECTION_ID, connection.connectionId) + .set( + 'detached-jws', + await getJwsd({ + payload: {}, + userPrivateJwk: testUserPrivateJwk, + requestUrl: `${ENDPOINT}/${internalTransfer.transferId}`, + htm: 'GET' + }) + ) + .send() + + expect(body).toEqual({ + data: { + ...internalTransfer, + // NOTE: The status is different from `transfer` because it's coming + // from the Anchorage API. The `findById` merges the state we have in + // the database with the API's. + status: TransferStatus.SUCCESS, + externalStatus: expect.any(String), + createdAt: expect.any(String) + } + }) + + expect(status).toEqual(HttpStatus.OK) + }) + + it('fails if connection header is missing', async () => { + const { body } = await request(app.getHttpServer()) + .get(`${ENDPOINT}/${internalTransfer.transferId}`) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set( + 'detached-jws', + await getJwsd({ + payload: {}, + userPrivateJwk: testUserPrivateJwk, + requestUrl: `${ENDPOINT}/${internalTransfer.transferId}`, + htm: 'GET' + }) + ) + .send() + + expect(body).toMatchObject({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'Missing or invalid x-connection-id header' + }) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/e2e/wallet.spec.ts b/apps/vault/src/broker/__test__/e2e/wallet.spec.ts new file mode 100644 index 000000000..12155a055 --- /dev/null +++ b/apps/vault/src/broker/__test__/e2e/wallet.spec.ts @@ -0,0 +1,324 @@ +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { VaultTest } from '../../../__test__/shared/vault.test' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' +import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { Account, Wallet } from '../../core/type/indexed-resources.type' +import { PaginatedWalletsDto } from '../../http/rest/dto/response/paginated-wallets.dto' +import { ProviderWalletDto } from '../../http/rest/dto/response/provider-wallet.dto' +import { anchorageConnectionOne, anchorageWalletOne, anchorageWalletThree, seed } from '../../shared/__test__/fixture' +import { signedRequest } from '../../shared/__test__/request' +import { REQUEST_HEADER_CONNECTION_ID } from '../../shared/constant' +import { TEST_WALLETS, testUserPrivateJwk } from '../util/mock-data' + +import '../../shared/__test__/matcher' + +describe('Wallet', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let provisionService: ProvisionService + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + testPrismaService = module.get(TestPrismaService) + provisionService = module.get(ProvisionService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await provisionService.provision() + + await seed(module) + + await app.init() + }) + + describe('GET /wallets', () => { + it('returns the list of wallets with accounts', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toMatchZodSchema(PaginatedWalletsDto.schema) + expect(body.data).toHaveLength(3) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns 404 auth error for unknown client', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .set(REQUEST_HEADER_CLIENT_ID, 'unknown-client') + .set(REQUEST_HEADER_CONNECTION_ID, 'unknown-connection-id') + .send() + + expect(body).toEqual({ + message: 'Client not found', + statusCode: HttpStatus.NOT_FOUND, + stack: expect.any(String) + }) + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + }) + + describe('GET /wallets with pagination', () => { + it('returns limited number of wallets when limit parameter is provided', async () => { + const limit = 2 + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ limit }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toHaveLength(limit) + expect(body.page.next).toBeDefined() + // First two wallets should be returned in createdAt descending order + expect(body.data.map((w: Wallet) => w.label)).toEqual(['wallet 3', 'wallet 2']) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns all wallets when limit exceeds total count', async () => { + const limit = 10 + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ limit }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toHaveLength(3) + expect(body.page.next).toBeNull() + expect(status).toEqual(HttpStatus.OK) + }) + + it('navigates through all pages using cursor', async () => { + const limit = 2 + const wallets = [] + let cursor = undefined + + do { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ cursor, limit }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toBe(HttpStatus.OK) + wallets.push(...body.data) + cursor = body.page.next + } while (cursor) + + expect(wallets).toHaveLength(3) + expect(wallets.map((w: Wallet) => w.label)).toEqual(['wallet 3', 'wallet 2', 'wallet 1']) + }) + + it('handles descending order by createdAt parameter correctly', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ sortOrder: 'desc' }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data.map((w: Wallet) => w.label)).toEqual(['wallet 3', 'wallet 2', 'wallet 1']) + expect(status).toEqual(HttpStatus.OK) + }) + + it('handles ascending order by createdAt parameter correctly', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ sortOrder: 'asc' }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data.map((w: Wallet) => w.label)).toEqual(['wallet 1', 'wallet 2', 'wallet 3']) + expect(status).toEqual(HttpStatus.OK) + }) + + describe('GET /wallets pagination with different directions and sort orders', () => { + let cursor: string + + beforeEach(async () => { + // Get initial cursor from latest wallet + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ limit: 1, sortOrder: 'desc' }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toBe(HttpStatus.OK) + + cursor = body.page.next + }) + + it('returns no results when paginating prev in desc order from newest record', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ + cursor, + direction: 'prev', + limit: 2, + sortOrder: 'desc' + }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toEqual([]) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns next older records when paginating next in desc order', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ + cursor, + direction: 'next', + limit: 2, + sortOrder: 'desc' + }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data.map((w: Wallet) => w.label)).toEqual(['wallet 2', 'wallet 1']) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns next newer records when paginating prev in asc order', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ + cursor, + direction: 'prev', + limit: 2, + sortOrder: 'asc' + }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toHaveLength(2) + expect(body.data.map((w: Wallet) => w.label)).toEqual(['wallet 1', 'wallet 2']) + expect(status).toEqual(HttpStatus.OK) + }) + }) + + it('returns empty array when cursor points to first wallet and direction is prev', async () => { + // First get the earliest wallet + const firstRequest = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ + limit: 1, + sortOrder: 'asc' + }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + const cursor = firstRequest.body.page.next + + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get('/provider/wallets') + .query({ + cursor, + sortOrder: 'asc', + direction: 'prev' + }) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data).toHaveLength(0) + expect(body.page.next).toBeNull() + expect(status).toEqual(HttpStatus.OK) + }) + }) + + describe('GET /wallets/:walletId', () => { + it('returns the wallet details with accounts and addresses', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get(`/provider/wallets/${anchorageWalletOne.walletId}`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toMatchZodSchema(ProviderWalletDto.schema) + expect(body.data.walletId).toEqual(anchorageWalletOne.walletId) + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns 404 with proper error message for non-existent wallet', async () => { + const { status } = await signedRequest(app, testUserPrivateJwk) + .get(`/provider/wallets/non-existent`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + + it('returns 404 when accessing wallet from wrong client', async () => { + const wallet = TEST_WALLETS[0] + const { status } = await signedRequest(app, testUserPrivateJwk) + .get(`/provider/wallets/${wallet.id}`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(status).toEqual(HttpStatus.NOT_FOUND) + }) + }) + + describe('GET /wallets/:walletId/accounts', () => { + it('returns the list of accounts for the wallet', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get(`/provider/wallets/${anchorageWalletOne.walletId}/accounts`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body.data.map((a: Account) => a.label)).toEqual(['wallet 1 account 2', 'wallet 1 account 1']) + + expect(status).toEqual(HttpStatus.OK) + }) + + it('returns empty accounts array for wallet with no accounts', async () => { + const { status, body } = await signedRequest(app, testUserPrivateJwk) + .get(`/provider/wallets/${anchorageWalletThree.walletId}/accounts`) + .set(REQUEST_HEADER_CLIENT_ID, anchorageConnectionOne.clientId) + .set(REQUEST_HEADER_CONNECTION_ID, anchorageConnectionOne.connectionId) + .send() + + expect(body).toEqual({ + data: [], + page: { + next: null + } + }) + + expect(status).toBe(HttpStatus.OK) + }) + }) +}) diff --git a/apps/vault/src/broker/__test__/util/map-db-to-returned.ts b/apps/vault/src/broker/__test__/util/map-db-to-returned.ts new file mode 100644 index 000000000..2c7cdd6ba --- /dev/null +++ b/apps/vault/src/broker/__test__/util/map-db-to-returned.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { ProviderAccount, ProviderAddress, ProviderConnection, ProviderWallet } from '@prisma/client/vault' +import { ConnectionRepository } from '../../persistence/repository/connection.repository' +import { TEST_ACCOUNTS, TEST_ADDRESSES, TEST_CONNECTIONS } from './mock-data' + +// Helper function to get expected connection format for API response +export const getExpectedConnection = (model: ProviderConnection) => { + const entity = ConnectionRepository.parseModel(model) + + return { + clientId: entity.clientId, + connectionId: entity.connectionId, + createdAt: entity.createdAt.toISOString(), + label: entity.label, + provider: entity.provider, + status: entity.status, + updatedAt: entity.updatedAt.toISOString(), + url: entity.url + } +} + +// Helper function to get expected address format +export const getExpectedAddress = (address: ProviderAddress) => { + const { id, ...addressWithoutId } = address + return { + ...addressWithoutId, + addressId: address.id, + createdAt: new Date(address.createdAt).toISOString(), + updatedAt: new Date(address.updatedAt).toISOString(), + connectionId: address.connectionId + } +} + +// Helper function to get expected account format with addresses +export const getExpectedAccount = (account: ProviderAccount) => { + const addresses = TEST_ADDRESSES.filter((addr) => addr.accountId === account.id) + const { id, walletId, ...accountWithoutId } = account + return { + ...accountWithoutId, + walletId, + accountId: account.id, + addresses: addresses.map(getExpectedAddress), + createdAt: account.createdAt.toISOString(), + updatedAt: account.updatedAt.toISOString() + } +} + +// Helper function to get expected wallet format with accounts and connections +export const getExpectedWallet = (wallet: ProviderWallet) => { + const accounts = TEST_ACCOUNTS.filter((acc) => acc.walletId === wallet.id) + const connections = TEST_CONNECTIONS.filter((conn) => conn.id === wallet.connectionId).map((c) => ({ + ...c, + credentials: c.credentials ? JSON.stringify(c.credentials) : null, + integrity: null + })) + const { id, connectionId, ...walletWithoutId } = wallet + + const exp = { + ...walletWithoutId, + walletId: wallet.id, + connections: connections.map(getExpectedConnection), + createdAt: wallet.createdAt.toISOString(), + updatedAt: wallet.updatedAt.toISOString(), + accounts: accounts.map(getExpectedAccount) + } + + return exp +} diff --git a/apps/vault/src/broker/__test__/util/mock-data.ts b/apps/vault/src/broker/__test__/util/mock-data.ts new file mode 100644 index 000000000..c4054281d --- /dev/null +++ b/apps/vault/src/broker/__test__/util/mock-data.ts @@ -0,0 +1,369 @@ +import { + buildSignerForAlg, + hash, + hexToBase64Url, + JwsdHeader, + PrivateKey, + secp256k1PrivateKeyToJwk, + secp256k1PrivateKeyToPublicJwk, + signJwsd +} from '@narval/signature' +import { Client } from '../../../shared/type/domain.type' + +const privateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0xaf8dcff8da6aae18c2170f59a0734c8c5c19ca726a1b75993857bd836db00a5f', + x: 'HrmLI5NH3cYp4-HluFGBOcYvARGti_oz0aZMXMzy8m4', + d: 'nq2eDJPp9NAqCdTT_dNerIJFJxegTKmFgDAsFkhbJIA' +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { d: _d, ...publicKey } = privateKey + +export const TEST_CLIENT_ID = 'test-client-id' +export const TEST_DIFFERENT_CLIENT_ID = 'different-client-id' + +const now = new Date() +const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' +export const testUserPrivateJwk = secp256k1PrivateKeyToJwk(PRIVATE_KEY) +export const testUserPublicJWK = secp256k1PrivateKeyToPublicJwk(PRIVATE_KEY) +/* Here's a variant of using an eddsa key to sign it too; just to prove it works with other alg*/ +// const PRIVATE_KEY = '0x0101010101010101010101010101010101010101010101010101010101010101' +// export const testUserPrivateJwk = ed25519PrivateKeyToJwk(PRIVATE_KEY) +// export const testUserPublicJWK = ed25519PrivateKeyToPublicJwk(PRIVATE_KEY) + +export const testClient: Client = { + clientId: TEST_CLIENT_ID, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: [ + { + userId: 'user-1', + publicKey: testUserPublicJWK + } + ] + }, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: [] + }, + pinnedPublicKey: null + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, + createdAt: now, + updatedAt: now +} + +export const testDifferentClient: Client = { + clientId: TEST_DIFFERENT_CLIENT_ID, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: [ + { + userId: 'user-1', + publicKey: testUserPublicJWK + } + ] + }, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: [] + }, + pinnedPublicKey: null + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, + createdAt: now, + updatedAt: now +} + +export const TEST_CONNECTIONS = [ + { + id: 'connection-1', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + url: 'https://api.anchorage.com', + label: 'Test Connection 1', + credentials: { + apiKey: 'test-api-key-1', + privateKey, + publicKey + }, + status: 'active', + createdAt: now, + updatedAt: now, + revokedAt: null + }, + { + id: 'connection-2', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + url: 'https://api.anchorage.com', + label: 'Test Connection 2', + credentials: { + apiKey: 'test-api-key-1', + privateKey, + publicKey + }, + status: 'active', + createdAt: now, + updatedAt: now, + revokedAt: null + }, + { + id: 'connection-3', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + url: 'https://api.anchorage.com', + label: 'Test Connection 3', + credentials: { + apiKey: 'test-api-key-1', + privateKey, + publicKey + }, + status: 'active', + createdAt: now, + updatedAt: now, + revokedAt: null + }, + { + id: 'connection-4', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + url: 'https://api.anchorage.com', + label: 'Test Connection 4', + credentials: { + apiKey: 'test-api-key-1', + privateKey, + publicKey + }, + status: 'active', + createdAt: now, + updatedAt: now, + revokedAt: null + } +] + +export const TEST_WALLETS_WITH_SAME_TIMESTAMP = [ + { + id: 'wallet-6', + clientId: TEST_DIFFERENT_CLIENT_ID, + provider: 'anchorage', + label: 'Test Wallet 1', + externalId: 'ext-wallet-1', + connectionId: 'connection-3', + createdAt: now, + updatedAt: now + }, + { + id: 'wallet-7', + clientId: TEST_DIFFERENT_CLIENT_ID, + provider: 'anchorage', + label: 'Test Wallet 2', + connectionId: 'connection-3', + externalId: 'ext-wallet-2', + createdAt: now, + updatedAt: now + }, + { + id: 'wallet-8', + clientId: TEST_DIFFERENT_CLIENT_ID, + provider: 'anchorage', + connectionId: 'connection-3', + label: 'Test Wallet 3', + externalId: 'ext-wallet-3', + createdAt: now, + updatedAt: now + } +] + +export const TEST_WALLETS = [ + { + id: 'wallet-1', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + label: 'Test Wallet 1', + externalId: 'ext-wallet-1', + connectionId: 'connection-1', + createdAt: now, + updatedAt: now + }, + { + id: 'wallet-2', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + label: 'Test Wallet 2', + externalId: 'ext-wallet-2', + connectionId: 'connection-2', + createdAt: new Date(now.getTime() + 1000), + updatedAt: new Date(now.getTime() + 1000) + }, + { + id: 'wallet-3', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + label: 'Test Wallet 3', + externalId: 'ext-wallet-3', + connectionId: 'connection-4', + createdAt: new Date(now.getTime() + 2000), + updatedAt: new Date(now.getTime() + 2000) + }, + { + id: 'wallet-4', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + label: 'Test Wallet 4', + connectionId: 'connection-4', + externalId: 'ext-wallet-4', + createdAt: new Date(now.getTime() + 3000), + updatedAt: new Date(now.getTime() + 3000) + }, + { + id: 'wallet-5', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + connectionId: 'connection-4', + label: 'Test Wallet 5', + externalId: 'ext-wallet-5', + createdAt: new Date(now.getTime() + 4000), + updatedAt: new Date(now.getTime() + 4000) + } +] + +export const TEST_ACCOUNTS = [ + { + id: 'account-1', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + label: 'Test Account 1', + externalId: 'ext-account-1', + walletId: 'wallet-1', // Linking to wallet-1 + connectionId: 'connection-1', + networkId: '1', + createdAt: now, + updatedAt: now + }, + { + id: 'account-2', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + label: 'Test Account 2', + externalId: 'ext-account-2', + walletId: 'wallet-1', // Linking to wallet-1 + connectionId: 'connection-1', + networkId: '60', + createdAt: now, + updatedAt: now + } +] + +export const TEST_ADDRESSES = [ + { + id: 'address-1', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + externalId: 'ext-address-1', + accountId: 'account-1', // Linking to account-1 + connectionId: 'connection-1', + address: '0x1234567890123456789012345678901234567890', // Example ETH address + createdAt: now, + updatedAt: now + }, + { + id: 'address-2', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + externalId: 'ext-address-2', + accountId: 'account-1', // Another address for account-1 + connectionId: 'connection-1', + address: '0x0987654321098765432109876543210987654321', // Example ETH address + createdAt: now, + updatedAt: now + }, + { + id: 'address-3', + clientId: TEST_CLIENT_ID, + provider: 'anchorage', + externalId: 'ext-address-3', + accountId: 'account-2', // Linking to account-2 + connectionId: 'connection-2', + address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', // Example BTC address + createdAt: now, + updatedAt: now + } +] + +export const getJwsd = async ({ + userPrivateJwk, + baseUrl, + requestUrl, + accessToken, + payload, + htm +}: { + userPrivateJwk: PrivateKey + baseUrl?: string + requestUrl: string + accessToken?: string + payload: object | string + htm?: string +}) => { + const now = Math.floor(Date.now() / 1000) + + const jwsdSigner = await buildSignerForAlg(userPrivateJwk) + const jwsdHeader: JwsdHeader = { + alg: userPrivateJwk.alg, + kid: userPrivateJwk.kid, + typ: 'gnap-binding-jwsd', + htm: htm || 'POST', + uri: `${baseUrl || 'https://vault-test.narval.xyz'}${requestUrl}`, // matches the client baseUrl + request url + created: now, + ath: accessToken ? hexToBase64Url(hash(accessToken)) : undefined + } + + const jwsd = await signJwsd(payload, jwsdHeader, jwsdSigner).then((jws) => { + // Strip out the middle part for size + const parts = jws.split('.') + parts[1] = '' + return parts.join('.') + }) + + return jwsd +} diff --git a/apps/vault/src/broker/broker.module.ts b/apps/vault/src/broker/broker.module.ts new file mode 100644 index 000000000..80629a778 --- /dev/null +++ b/apps/vault/src/broker/broker.module.ts @@ -0,0 +1,144 @@ +import { HttpModule, OpenTelemetryModule } from '@narval/nestjs-shared' +import { CacheModule } from '@nestjs/cache-manager' +import { Module, OnApplicationBootstrap } from '@nestjs/common' +import { APP_FILTER } from '@nestjs/core' +import { EventEmitterModule } from '@nestjs/event-emitter' +import { ClientModule } from '../client/client.module' +import { DEFAULT_HTTP_MODULE_PROVIDERS } from '../shared/constant' +import { ProviderHttpExceptionFilter } from '../shared/filter/provider-http-exception.filter' +import { PersistenceModule } from '../shared/module/persistence/persistence.module' +import { EncryptionKeyService } from '../transit-encryption/core/service/encryption-key.service' +import { EncryptionKeyRepository } from '../transit-encryption/persistence/encryption-key.repository' +import { TransitEncryptionModule } from '../transit-encryption/transit-encryption.module' +import { AnchorageCredentialService } from './core/provider/anchorage/anchorage-credential.service' +import { AnchorageKnownDestinationService } from './core/provider/anchorage/anchorage-known-destination.service' +import { AnchorageProxyService } from './core/provider/anchorage/anchorage-proxy.service' +import { AnchorageScopedSyncService } from './core/provider/anchorage/anchorage-scoped-sync.service' +import { AnchorageTransferService } from './core/provider/anchorage/anchorage-transfer.service' +import { BitgoCredentialService } from './core/provider/bitgo/bitgo-credential.service' +import { FireblocksCredentialService } from './core/provider/fireblocks/fireblocks-credential.service' +import { FireblocksKnownDestinationService } from './core/provider/fireblocks/fireblocks-known-destination.service' +import { FireblocksProxyService } from './core/provider/fireblocks/fireblocks-proxy.service' +import { FireblocksScopedSyncService } from './core/provider/fireblocks/fireblocks-scoped-sync.service' +import { FireblocksTransferService } from './core/provider/fireblocks/fireblocks-transfer.service' +import { AccountService } from './core/service/account.service' +import { AddressService } from './core/service/address.service' +import { AssetService } from './core/service/asset.service' +import { ConnectionService } from './core/service/connection.service' +import { KnownDestinationService } from './core/service/known-destination.service' +import { NetworkService } from './core/service/network.service' +import { ProxyService } from './core/service/proxy.service' +import { RawAccountService } from './core/service/raw-account.service' +import { ScopedSyncService } from './core/service/scoped-sync.service' +import { SyncService } from './core/service/sync.service' +import { TransferAssetService } from './core/service/transfer-asset.service' +import { TransferService } from './core/service/transfer.service' +import { WalletService } from './core/service/wallet.service' +import { ConnectionScopedSyncEventHandler } from './event/handler/connection-scoped-sync.event-handler' +import { AnchorageClient } from './http/client/anchorage.client' +import { FireblocksClient } from './http/client/fireblocks.client' +import { ProviderAccountController } from './http/rest/controller/account.controller' +import { ProviderAddressController } from './http/rest/controller/address.controller' +import { AssetController } from './http/rest/controller/asset.controller' +import { ConnectionController } from './http/rest/controller/connection.controller' +import { KnownDestinationController } from './http/rest/controller/known-destination.controller' +import { NetworkController } from './http/rest/controller/network.controller' +import { ProxyController } from './http/rest/controller/proxy.controller' +import { ScopedSyncController } from './http/rest/controller/scoped-sync.controller' +import { SyncController } from './http/rest/controller/sync.controller' +import { TransferController } from './http/rest/controller/transfer.controller' +import { ProviderWalletController } from './http/rest/controller/wallet.controller' +import { AccountRepository } from './persistence/repository/account.repository' +import { AddressRepository } from './persistence/repository/address.repository' +import { AssetRepository } from './persistence/repository/asset.repository' +import { ConnectionRepository } from './persistence/repository/connection.repository' +import { NetworkRepository } from './persistence/repository/network.repository' +import { ScopedSyncRepository } from './persistence/repository/scoped-sync.repository' +import { SyncRepository } from './persistence/repository/sync.repository' +import { TransferRepository } from './persistence/repository/transfer.repository' +import { WalletRepository } from './persistence/repository/wallet.repository' +import { AssetSeed } from './persistence/seed/asset.seed' +import { NetworkSeed } from './persistence/seed/network.seed' + +@Module({ + imports: [ + CacheModule.register(), + ClientModule, + EventEmitterModule.forRoot(), + HttpModule.register({ retry: { retries: 3 } }), + OpenTelemetryModule.forRoot(), + PersistenceModule, + TransitEncryptionModule + ], + controllers: [ + AssetController, + ConnectionController, + KnownDestinationController, + NetworkController, + ProviderAccountController, + ProviderAddressController, + ProviderWalletController, + ProxyController, + SyncController, + ScopedSyncController, + TransferController + ], + providers: [ + NetworkSeed, + AssetSeed, + ...DEFAULT_HTTP_MODULE_PROVIDERS, + { + provide: APP_FILTER, + useClass: ProviderHttpExceptionFilter + }, + FireblocksCredentialService, + AccountRepository, + AccountService, + RawAccountService, + AddressRepository, + AddressService, + AnchorageClient, + AnchorageCredentialService, + AnchorageKnownDestinationService, + AnchorageProxyService, + AnchorageScopedSyncService, + AnchorageTransferService, + AssetRepository, + AssetService, + BitgoCredentialService, + ConnectionRepository, + ConnectionService, + ConnectionScopedSyncEventHandler, + EncryptionKeyRepository, + EncryptionKeyService, + FireblocksClient, + FireblocksKnownDestinationService, + FireblocksProxyService, + FireblocksTransferService, + FireblocksScopedSyncService, + KnownDestinationService, + NetworkRepository, + NetworkService, + ProxyService, + ScopedSyncRepository, + ScopedSyncService, + SyncRepository, + SyncService, + TransferRepository, + TransferService, + TransferAssetService, + WalletRepository, + WalletService + ] +}) +export class BrokerModule implements OnApplicationBootstrap { + constructor( + private networkSeed: NetworkSeed, + private assetSeed: AssetSeed + ) {} + + async onApplicationBootstrap() { + await this.networkSeed.seed() + await this.assetSeed.seed() + } +} diff --git a/apps/vault/src/broker/core/exception/ambiguity.exception.ts b/apps/vault/src/broker/core/exception/ambiguity.exception.ts new file mode 100644 index 000000000..15c7e6742 --- /dev/null +++ b/apps/vault/src/broker/core/exception/ambiguity.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class AmbiguityException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Cannot resolve request due to data ambiguity', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/asset.exception.ts b/apps/vault/src/broker/core/exception/asset.exception.ts new file mode 100644 index 000000000..aca4f9fc7 --- /dev/null +++ b/apps/vault/src/broker/core/exception/asset.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class AssetException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Asset exception', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/broker.exception.ts b/apps/vault/src/broker/core/exception/broker.exception.ts new file mode 100644 index 000000000..a598da3d2 --- /dev/null +++ b/apps/vault/src/broker/core/exception/broker.exception.ts @@ -0,0 +1,3 @@ +import { ApplicationException } from '../../../shared/exception/application.exception' + +export class BrokerException extends ApplicationException {} diff --git a/apps/vault/src/broker/core/exception/connection-invalid-credentials.exception.ts b/apps/vault/src/broker/core/exception/connection-invalid-credentials.exception.ts new file mode 100644 index 000000000..c62754fb2 --- /dev/null +++ b/apps/vault/src/broker/core/exception/connection-invalid-credentials.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class ConnectionInvalidCredentialsException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Invalid connection credentials', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/connection-invalid-private-key.exception.ts b/apps/vault/src/broker/core/exception/connection-invalid-private-key.exception.ts new file mode 100644 index 000000000..03ab5b711 --- /dev/null +++ b/apps/vault/src/broker/core/exception/connection-invalid-private-key.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class ConnectionInvalidPrivateKeyException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Invalid connection credential private key type', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/connection-invalid-status.exception.ts b/apps/vault/src/broker/core/exception/connection-invalid-status.exception.ts new file mode 100644 index 000000000..4caa1b669 --- /dev/null +++ b/apps/vault/src/broker/core/exception/connection-invalid-status.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ConnectionStatus } from '../type/connection.type' +import { BrokerException } from './broker.exception' + +export class ConnectionInvalidStatusException extends BrokerException { + constructor(context: { from: ConnectionStatus; to: ConnectionStatus; clientId: string; connectionId: string }) { + super({ + message: `Cannot change connection status from ${context.from} to ${context.to}`, + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context + }) + } +} diff --git a/apps/vault/src/broker/core/exception/connection-invalid.exception.ts b/apps/vault/src/broker/core/exception/connection-invalid.exception.ts new file mode 100644 index 000000000..1cc48107a --- /dev/null +++ b/apps/vault/src/broker/core/exception/connection-invalid.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class ConnectionInvalidException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Invalid connection', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/connection-parse.exception.ts b/apps/vault/src/broker/core/exception/connection-parse.exception.ts new file mode 100644 index 000000000..0d7efee51 --- /dev/null +++ b/apps/vault/src/broker/core/exception/connection-parse.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class ConnectionParseException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Fail to parse connection after read', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/invalid-query-string.exception.ts b/apps/vault/src/broker/core/exception/invalid-query-string.exception.ts new file mode 100644 index 000000000..42d969071 --- /dev/null +++ b/apps/vault/src/broker/core/exception/invalid-query-string.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class InvalidQueryStringException extends BrokerException { + constructor(query: string, params?: Partial) { + super({ + message: params?.message || `Query string "${query}" is required`, + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.BAD_REQUEST, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/model-invalid.exception.ts b/apps/vault/src/broker/core/exception/model-invalid.exception.ts new file mode 100644 index 000000000..28ac1a27f --- /dev/null +++ b/apps/vault/src/broker/core/exception/model-invalid.exception.ts @@ -0,0 +1,11 @@ +import { HttpStatus } from '@nestjs/common' +import { BrokerException } from './broker.exception' + +export class ModelInvalidException extends BrokerException { + constructor() { + super({ + message: 'Invalid model', + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR + }) + } +} diff --git a/apps/vault/src/broker/core/exception/not-found.exception.ts b/apps/vault/src/broker/core/exception/not-found.exception.ts new file mode 100644 index 000000000..292c9e2a5 --- /dev/null +++ b/apps/vault/src/broker/core/exception/not-found.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class NotFoundException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Not found', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.NOT_FOUND, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/provider-http.exception.ts b/apps/vault/src/broker/core/exception/provider-http.exception.ts new file mode 100644 index 000000000..9a4b28f30 --- /dev/null +++ b/apps/vault/src/broker/core/exception/provider-http.exception.ts @@ -0,0 +1,36 @@ +import { AxiosError } from 'axios' +import { Provider } from '../type/provider.type' + +type ProviderHttpResponse = { + status: number + body: unknown +} + +type ProviderHttpExceptionParams = { + message?: string + provider: Provider + origin: AxiosError + response: ProviderHttpResponse + context?: unknown +} + +export class ProviderHttpException extends Error { + readonly response: ProviderHttpResponse + readonly provider: Provider + readonly origin: AxiosError + readonly context?: unknown + + constructor({ message, provider, origin, response, context }: ProviderHttpExceptionParams) { + super(message ?? `Provider ${provider.toUpperCase()} responded with ${response.status} error`) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ProviderHttpException) + } + + this.name = this.constructor.name + this.response = response + this.provider = provider + this.origin = origin + this.context = context + } +} diff --git a/apps/vault/src/broker/core/exception/scoped-sync.exception.ts b/apps/vault/src/broker/core/exception/scoped-sync.exception.ts new file mode 100644 index 000000000..1044715df --- /dev/null +++ b/apps/vault/src/broker/core/exception/scoped-sync.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class ScopedSyncException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Fail to sync provider connection', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/sync.exception.ts b/apps/vault/src/broker/core/exception/sync.exception.ts new file mode 100644 index 000000000..813d34b31 --- /dev/null +++ b/apps/vault/src/broker/core/exception/sync.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class SyncException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Fail to sync provider connection', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/update.exception.ts b/apps/vault/src/broker/core/exception/update.exception.ts new file mode 100644 index 000000000..c58474722 --- /dev/null +++ b/apps/vault/src/broker/core/exception/update.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { BrokerException } from './broker.exception' + +export class UpdateException extends BrokerException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Failed to update model', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR, + ...params + }) + } +} diff --git a/apps/vault/src/broker/core/exception/url-parser.exception.ts b/apps/vault/src/broker/core/exception/url-parser.exception.ts new file mode 100644 index 000000000..c1a439202 --- /dev/null +++ b/apps/vault/src/broker/core/exception/url-parser.exception.ts @@ -0,0 +1,12 @@ +import { HttpStatus } from '@nestjs/common' +import { BrokerException } from './broker.exception' + +export class UrlParserException extends BrokerException { + constructor({ url, message }: { url: string; message?: string }) { + super({ + message: message || `Cannot parse url`, + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { url } + }) + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/integration/anchorage-scoped-sync.service.spec.ts b/apps/vault/src/broker/core/provider/anchorage/__test__/integration/anchorage-scoped-sync.service.spec.ts new file mode 100644 index 000000000..7d112042a --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/integration/anchorage-scoped-sync.service.spec.ts @@ -0,0 +1,180 @@ +import { Alg, generateJwk, privateKeyToHex } from '@narval/signature' +import { INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { v4 as uuid } from 'uuid' +import { VaultTest } from '../../../../../../__test__/shared/vault.test' +import { ClientService } from '../../../../../../client/core/service/client.service' +import { MainModule } from '../../../../../../main.module' +import { ProvisionService } from '../../../../../../provision.service' +import { TestPrismaService } from '../../../../../../shared/module/persistence/service/test-prisma.service' +import { testClient } from '../../../../../__test__/util/mock-data' +import { NetworkSeed } from '../../../../../persistence/seed/network.seed' +import { setupMockServer } from '../../../../../shared/__test__/mock-server' +import { ConnectionService } from '../../../../service/connection.service' +import { NetworkService } from '../../../../service/network.service' +import { ConnectionWithCredentials } from '../../../../type/connection.type' +import { Provider } from '../../../../type/provider.type' +import { RawAccountError } from '../../../../type/scoped-sync.type' +import { AnchorageScopedSyncService } from '../../anchorage-scoped-sync.service' +import { ANCHORAGE_TEST_API_BASE_URL, getHandlers } from '../server-mock/server' + +describe(AnchorageScopedSyncService.name, () => { + let app: INestApplication + let module: TestingModule + + let anchorageScopedSyncService: AnchorageScopedSyncService + let clientService: ClientService + let connection: ConnectionWithCredentials + let networkService: NetworkService + let connectionService: ConnectionService + let networkSeed: NetworkSeed + let provisionService: ProvisionService + let testPrismaService: TestPrismaService + + setupMockServer(getHandlers()) + + const clientId = 'test-client-id' + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + testPrismaService = module.get(TestPrismaService) + anchorageScopedSyncService = module.get(AnchorageScopedSyncService) + connectionService = module.get(ConnectionService) + networkService = module.get(NetworkService) + provisionService = module.get(ProvisionService) + networkSeed = module.get(NetworkSeed) + clientService = module.get(ClientService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + + await provisionService.provision() + await clientService.save(testClient) + + connection = await connectionService.create(clientId, { + connectionId: uuid(), + provider: Provider.ANCHORAGE, + url: ANCHORAGE_TEST_API_BASE_URL, + label: 'test active connection', + credentials: { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA)) + } + }) + + await networkSeed.seed() + + await app.init() + }) + + describe('scoped-syncs', () => { + it('returns scoped sync operations for wallets, accounts and addresses based on an anchorage wallet externalId', async () => { + const rawAccounts = [ + { + provider: Provider.ANCHORAGE, + externalId: '6a46a1977959e0529f567e8e927e3895' + } + ] + const networks = await networkService.buildProviderExternalIdIndex(Provider.ANCHORAGE) + + const sync = await anchorageScopedSyncService.scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(1) + expect(sync.accounts.length).toBe(1) + expect(sync.addresses.length).toBe(1) + }) + + // TODO @ptroger: revert that back to 'return empty array' when we completely move towards the scoped connections + it('returns full connection sync when empty rawAccounts are provided', async () => { + const networks = await networkService.buildProviderExternalIdIndex(Provider.ANCHORAGE) + + const sync = await anchorageScopedSyncService.scopeSync({ + connection, + rawAccounts: [], + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(2) + expect(sync.accounts.length).toBe(18) + expect(sync.addresses.length).toBe(18) + }) + + it('adds failure when external resource is not found', async () => { + const rawAccounts = [ + { + provider: Provider.ANCHORAGE, + externalId: 'notFound' + } + ] + const networks = await networkService.buildProviderExternalIdIndex(Provider.ANCHORAGE) + + const sync = await anchorageScopedSyncService.scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(0) + expect(sync.accounts.length).toBe(0) + expect(sync.addresses.length).toBe(0) + expect(sync.failures).toEqual([ + { + rawAccount: rawAccounts[0], + message: 'Anchorage wallet not found', + code: RawAccountError.EXTERNAL_RESOURCE_NOT_FOUND, + externalResourceType: 'wallet', + externalResourceId: rawAccounts[0].externalId + } + ]) + }) + + it('adds failure when network is not found in our list', async () => { + const rawAccounts = [ + { + provider: Provider.ANCHORAGE, + externalId: '6a46a1977959e0529f567e8e927e3895' + } + ] + + const sync = await anchorageScopedSyncService.scopeSync({ + connection, + rawAccounts, + networks: new Map(), + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(0) + expect(sync.accounts.length).toBe(0) + expect(sync.addresses.length).toBe(0) + expect(sync.failures).toEqual([ + { + rawAccount: rawAccounts[0], + message: 'Network for this account is not supported', + code: RawAccountError.UNLISTED_NETWORK, + networkId: 'BTC_S' + } + ]) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/integration/anchorage-transfer.service.spec.ts b/apps/vault/src/broker/core/provider/anchorage/__test__/integration/anchorage-transfer.service.spec.ts new file mode 100644 index 000000000..661947e1a --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/integration/anchorage-transfer.service.spec.ts @@ -0,0 +1,445 @@ +import { Ed25519PrivateKey, getPublicKey } from '@narval/signature' +import { INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { v4 as uuid } from 'uuid' +import { generatePrivateKey, privateKeyToAddress } from 'viem/accounts' +import { VaultTest } from '../../../../../../__test__/shared/vault.test' +import { ClientService } from '../../../../../../client/core/service/client.service' +import { MainModule } from '../../../../../../main.module' +import { ProvisionService } from '../../../../../../provision.service' +import { TestPrismaService } from '../../../../../../shared/module/persistence/service/test-prisma.service' +import { testClient } from '../../../../../__test__/util/mock-data' +import { AccountRepository } from '../../../../../persistence/repository/account.repository' +import { AddressRepository } from '../../../../../persistence/repository/address.repository' +import { ConnectionRepository } from '../../../../../persistence/repository/connection.repository' +import { TransferRepository } from '../../../../../persistence/repository/transfer.repository' +import { WalletRepository } from '../../../../../persistence/repository/wallet.repository' +import { AssetSeed } from '../../../../../persistence/seed/asset.seed' +import { NetworkSeed } from '../../../../../persistence/seed/network.seed' +import { setupMockServer, useRequestSpy } from '../../../../../shared/__test__/mock-server' +import { ConnectionStatus, ConnectionWithCredentials } from '../../../../type/connection.type' +import { Account, Address, Wallet } from '../../../../type/indexed-resources.type' +import { Provider } from '../../../../type/provider.type' +import { + InternalTransfer, + NetworkFeeAttribution, + TransferPartyType, + TransferStatus +} from '../../../../type/transfer.type' +import { AnchorageTransferService } from '../../anchorage-transfer.service' +import { ANCHORAGE_TEST_API_BASE_URL, getHandlers } from '../server-mock/server' + +describe(AnchorageTransferService.name, () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + + let accountRepository: AccountRepository + let addressRepository: AddressRepository + let anchorageTransferService: AnchorageTransferService + let assetSeed: AssetSeed + let clientService: ClientService + let connectionRepository: ConnectionRepository + let networkSeed: NetworkSeed + let provisionService: ProvisionService + let transferRepository: TransferRepository + let walletRepository: WalletRepository + + const mockServer = setupMockServer(getHandlers()) + + const clientId = uuid() + + const externalId = '60a0676772fdbd7a041e9451c61c3cb6b28ee901186e40ac99433308604e2e20' + + const walletId = uuid() + + const eddsaPrivateKey: Ed25519PrivateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0xa6fe705025aa4c48abbb3a1ed679d7dc7d18e7994b4d5cb1884479fddeb2e706', + x: 'U4WSOMzD7gor6jiVz42jT22JGBcfGfzMomt8PFC_-_U', + d: 'evo-fY2BX60V1n3Z690LadH5BvizcM9bESaYk0LsxyQ' + } + + const connection: ConnectionWithCredentials = { + clientId, + connectionId: uuid(), + createdAt: new Date(), + provider: Provider.ANCHORAGE, + status: ConnectionStatus.ACTIVE, + updatedAt: new Date(), + url: ANCHORAGE_TEST_API_BASE_URL, + credentials: { + privateKey: eddsaPrivateKey, + publicKey: getPublicKey(eddsaPrivateKey), + apiKey: 'test-api-key' + } + } + + const accountOne: Account = { + accountId: uuid(), + addresses: [], + clientId, + createdAt: new Date(), + connectionId: connection.connectionId, + externalId: uuid(), + label: 'Account 1', + networkId: 'BITCOIN', + provider: Provider.ANCHORAGE, + updatedAt: new Date(), + walletId + } + + const accountTwo: Account = { + accountId: uuid(), + addresses: [], + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: uuid(), + label: 'Account 2', + networkId: 'BITCOIN', + provider: Provider.ANCHORAGE, + updatedAt: new Date(), + walletId + } + + const address: Address = { + accountId: accountTwo.accountId, + address: '0x2c4895215973cbbd778c32c456c074b99daf8bf1', + connectionId: connection.connectionId, + addressId: uuid(), + clientId, + createdAt: new Date(), + externalId: uuid(), + provider: Provider.ANCHORAGE, + updatedAt: new Date() + } + + const wallet: Wallet = { + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: uuid(), + label: null, + provider: Provider.ANCHORAGE, + updatedAt: new Date(), + walletId + } + + const internalTransfer: InternalTransfer = { + clientId, + customerRefId: null, + idempotenceId: null, + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + externalId, + externalStatus: null, + assetId: 'BTC', + assetExternalId: null, + connectionId: connection.connectionId, + memo: 'Test transfer', + grossAmount: '0.00001', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + provider: Provider.ANCHORAGE, + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + transferId: uuid(), + createdAt: new Date() + } + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + testPrismaService = module.get(TestPrismaService) + anchorageTransferService = module.get(AnchorageTransferService) + provisionService = module.get(ProvisionService) + clientService = module.get(ClientService) + + accountRepository = module.get(AccountRepository) + addressRepository = module.get(AddressRepository) + assetSeed = module.get(AssetSeed) + connectionRepository = module.get(ConnectionRepository) + networkSeed = module.get(NetworkSeed) + transferRepository = module.get(TransferRepository) + walletRepository = module.get(WalletRepository) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + + await provisionService.provision() + await clientService.save(testClient) + await networkSeed.seed() + await assetSeed.seed() + + await connectionRepository.create(connection) + await walletRepository.bulkCreate([wallet]) + await accountRepository.bulkCreate([accountOne, accountTwo]) + await addressRepository.bulkCreate([address]) + await transferRepository.bulkCreate([internalTransfer]) + + await app.init() + }) + + describe('findById', () => { + it('maps data from internal transfer', async () => { + const transfer = await anchorageTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer).toMatchObject({ + assetId: internalTransfer.assetId, + clientId: internalTransfer.clientId, + customerRefId: internalTransfer.customerRefId, + connectionId: connection.connectionId, + destination: internalTransfer.destination, + externalId: internalTransfer.externalId, + idempotenceId: internalTransfer.idempotenceId, + networkFeeAttribution: internalTransfer.networkFeeAttribution, + provider: internalTransfer.provider, + source: internalTransfer.source, + transferId: internalTransfer.transferId + }) + }) + + it('maps gross amount from inbound transfer', async () => { + const transfer = await anchorageTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.grossAmount).toEqual('0.00001') + }) + + it('maps status from inbound transfer', async () => { + const transfer = await anchorageTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.status).toEqual(TransferStatus.SUCCESS) + }) + + it('maps memo from internal transfer when inbound transferMemo is undefined', async () => { + const transfer = await anchorageTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.memo).toEqual(internalTransfer.memo) + }) + + it('maps network fee from inbound anchorage transfer', async () => { + const transfer = await anchorageTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.fees).toEqual([ + { + amount: '0.00001771', + assetId: 'BTC_S', + attribution: internalTransfer.networkFeeAttribution, + type: 'network' + } + ]) + }) + }) + + describe('send', () => { + const requiredSendTransfer = { + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + amount: '0.00005', + asset: { + assetId: 'BTC' + }, + idempotenceId: uuid() + } + + const transferAmlQuestionnaire = { + destinationType: 'SELFHOSTED_WALLET', + recipientType: 'PERSON', + purpose: 'INVESTMENT', + originatorType: 'MY_ORGANIZATION', + selfhostedDescription: 'a wallet description', + recipientFirstName: 'John', + recipientLastName: 'Recipient', + recipientFullName: 'John Recipient Full Name', + recipientCountry: 'US', + recipientStreetAddress: 'Some Recipient Street', + recipientCity: 'New York', + recipientStateProvince: 'NY', + recipientPostalCode: '10101' + } + + it('creates an internal transfer on success', async () => { + const { externalStatus, ...transfer } = await anchorageTransferService.send(connection, requiredSendTransfer) + const actualInternalTransfer = await transferRepository.findById(clientId, transfer.transferId) + + expect(actualInternalTransfer).toMatchObject(transfer) + expect(externalStatus).toEqual(expect.any(String)) + }) + + it('creates with optional properties', async () => { + const sendTransfer = { + ...requiredSendTransfer, + memo: 'Integration test transfer', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + idempotenceId: uuid() + } + + const internalTransfer = await anchorageTransferService.send(connection, sendTransfer) + const actualInternalTransfer = await transferRepository.findById(clientId, internalTransfer.transferId) + + expect(actualInternalTransfer.idempotenceId).toEqual(sendTransfer.idempotenceId) + expect(actualInternalTransfer.memo).toEqual(sendTransfer.memo) + // Anchorage does not support customerRefId. + expect(actualInternalTransfer.customerRefId).toEqual(null) + expect(actualInternalTransfer.networkFeeAttribution).toEqual(sendTransfer.networkFeeAttribution) + }) + + it('defaults network fee attribution to on_top', async () => { + const internalTransfer = await anchorageTransferService.send(connection, requiredSendTransfer) + + expect(internalTransfer.networkFeeAttribution).toEqual(NetworkFeeAttribution.ON_TOP) + }) + + it('calls Anchorage', async () => { + const [spy] = useRequestSpy(mockServer) + const sendTransfer = { + ...requiredSendTransfer, + memo: 'Integration test transfer' + } + + await anchorageTransferService.send(connection, sendTransfer) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + source: { + type: 'WALLET', + id: accountOne.externalId + }, + destination: { + type: 'WALLET', + id: accountTwo.externalId + }, + assetType: sendTransfer.asset.assetId, + amount: sendTransfer.amount, + transferMemo: sendTransfer.memo, + idempotentId: sendTransfer.idempotenceId, + // Default `deductFeeFromAmountIfSameType` to false. + deductFeeFromAmountIfSameType: false + } + }) + ) + }) + + it('calls Anchorage with address', async () => { + const [spy] = useRequestSpy(mockServer) + const address = privateKeyToAddress(generatePrivateKey()) + const sendTransfer = { + ...requiredSendTransfer, + destination: { address }, + transferId: 'test-transfer-id' + } + + await anchorageTransferService.send(connection, sendTransfer) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + source: { + type: 'WALLET', + id: accountOne.externalId + }, + destination: { + type: 'ADDRESS', + id: address + } + }) + }) + ) + }) + + // IMPORTANT: We never send the customerRefId to Anchorage because they + // have deprecated it and it seems to get transfers stuck on their side. + it('does not send customerRefId', async () => { + const [spy] = useRequestSpy(mockServer) + const sendTransfer = { + ...requiredSendTransfer, + customerRefId: uuid() + } + + await anchorageTransferService.send(connection, sendTransfer) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect('customerRefId' in (spy.mock.calls[0][0] as any)).toEqual(false) + }) + + it('handles provider specific', async () => { + const [spy] = useRequestSpy(mockServer) + const sendTransfer = { + ...requiredSendTransfer, + provider: Provider.ANCHORAGE, + providerSpecific: { transferAmlQuestionnaire } + } + + const internalTransfer = await anchorageTransferService.send(connection, sendTransfer) + const actualInternalTransfer = await transferRepository.findById(clientId, internalTransfer.transferId) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ ...sendTransfer.providerSpecific }) + }) + ) + + expect(actualInternalTransfer.providerSpecific).toEqual(sendTransfer.providerSpecific) + }) + + it('maps networkFeeAttribution on_top to deductFeeFromAmountIfSameType false', async () => { + const [spy] = useRequestSpy(mockServer) + + await anchorageTransferService.send(connection, { + ...requiredSendTransfer, + networkFeeAttribution: NetworkFeeAttribution.ON_TOP + }) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + deductFeeFromAmountIfSameType: false + }) + }) + ) + }) + + it('maps networkFeeAttribution deduct to deductFeeFromAmountIfSameType true', async () => { + const [spy] = useRequestSpy(mockServer) + + await anchorageTransferService.send(connection, { + ...requiredSendTransfer, + networkFeeAttribution: NetworkFeeAttribution.DEDUCT + }) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + deductFeeFromAmountIfSameType: true + }) + }) + ) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-transfer-200.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-transfer-200.json new file mode 100644 index 000000000..1ba192755 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-transfer-200.json @@ -0,0 +1,28 @@ +{ + "data": { + "amount": { + "assetType": "BTC_S", + "currentPrice": "104576.34", + "currentUSDValue": "1.05", + "quantity": "0.00001" + }, + "assetType": "BTC_S", + "blockchainTxId": "71ffe98ce4f51b4d12c77626dc455c9223f68b215c631f45ff05958623761e23", + "createdAt": "2024-12-18T14:04:10.519Z", + "destination": { + "id": "afb308d207a76f029257ea6d9d7d58ab", + "type": "WALLET" + }, + "endedAt": "2024-12-18T14:08:13.618Z", + "fee": { + "assetType": "BTC_S", + "quantity": "0.00001771" + }, + "source": { + "id": "6a46a1977959e0529f567e8e927e3895", + "type": "WALLET" + }, + "status": "COMPLETED", + "transferId": "60a0676772fdbd7a041e9451c61c3cb6b28ee901186e40ac99433308604e2e20" + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200-second.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200-second.json new file mode 100644 index 000000000..cd627749f --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200-second.json @@ -0,0 +1,35 @@ +{ + "data": [ + { + "id": "toBeConnected", + "type": "crypto", + "crypto": { + "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "networkId": "ETH", + "assetType": "ETH" + } + }, + { + "id": "neverChanges", + "type": "crypto", + "crypto": { + "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + "networkId": "BTC", + "assetType": "BTC" + } + }, + { + "id": "toBeUpdated", + "type": "crypto", + "crypto": { + "address": "0x8Bc2B8F33e5AeF847B8973Fa669B948A3028D6bd", + "networkId": "ETH", + "assetType": "USDC", + "memo": "new memo" + } + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200-third.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200-third.json new file mode 100644 index 000000000..5985e2f1b --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200-third.json @@ -0,0 +1,16 @@ +{ + "data": [ + { + "id": "toBeConnected", + "type": "crypto", + "crypto": { + "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "networkId": "ETH", + "assetType": "ETH" + } + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200.json new file mode 100644 index 000000000..51dc594bd --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-trusted-destinations-200.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "id": "toBeConnected", + "type": "crypto", + "crypto": { + "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "networkId": "ETH", + "assetType": "ETH" + } + }, + { + "id": "neverChanges", + "type": "crypto", + "crypto": { + "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + "networkId": "BTC", + "assetType": "BTC" + } + }, + { + "id": "toBeUpdated", + "type": "crypto", + "crypto": { + "address": "0x8Bc2B8F33e5AeF847B8973Fa669B948A3028D6bd", + "networkId": "ETH", + "assetType": "USDC" + } + }, + { + "id": "toBeDeleted", + "type": "crypto", + "crypto": { + "address": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "networkId": "XRP", + "assetType": "XRP", + "memo": "123456" + } + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-200-second.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-200-second.json new file mode 100644 index 000000000..c805f30c6 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-200-second.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "accountName": "Narval Account 1", + "assets": [ + { + "assetType": "BTC_S", + "availableBalance": { + "assetType": "BTC_S", + "currentPrice": "100571.636951942", + "currentUSDValue": "502.86", + "quantity": "0.005" + }, + "totalBalance": { + "assetType": "BTC_S", + "currentPrice": "100571.636951942", + "currentUSDValue": "502.86", + "quantity": "0.005" + }, + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1 - renamed", + "walletId": "6a46a1977959e0529f567e8e927e3895" + } + ], + "description": "", + "name": "Vault 1 - renamed", + "type": "VAULT", + "vaultId": "084ff57c0984420efac31723579c94fc" + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-200.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-200.json new file mode 100644 index 000000000..f78c97c44 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-200.json @@ -0,0 +1,61 @@ +{ + "data": [ + { + "accountName": "Narval Account 1", + "assets": [ + { + "assetType": "BTC_S", + "availableBalance": { + "assetType": "BTC_S", + "currentPrice": "100571.636951942", + "currentUSDValue": "502.86", + "quantity": "0.005" + }, + "totalBalance": { + "assetType": "BTC_S", + "currentPrice": "100571.636951942", + "currentUSDValue": "502.86", + "quantity": "0.005" + }, + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "6a46a1977959e0529f567e8e927e3895" + } + ], + "description": "", + "name": "Vault 1", + "type": "VAULT", + "vaultId": "084ff57c0984420efac31723579c94fc" + }, + { + "accountName": "Narval Account 1", + "assets": [ + { + "assetType": "ETHHOL", + "availableBalance": { + "assetType": "ETHHOL", + "currentPrice": "3801.93297363249", + "currentUSDValue": "3798.13", + "quantity": "0.998998527910122" + }, + "totalBalance": { + "assetType": "ETHHOL", + "currentPrice": "3801.93297363249", + "currentUSDValue": "3798.13", + "quantity": "0.998998527910122" + }, + "vaultId": "62547351cea99e827bcd43a513c40e7c", + "vaultName": "Vault 2", + "walletId": "ec9a04d05472e417ceb06e32c66834d1" + } + ], + "description": "Test", + "name": "Vault 2", + "type": "VAULT", + "vaultId": "62547351cea99e827bcd43a513c40e7c" + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-addresses-200.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-addresses-200.json new file mode 100644 index 000000000..2252961c4 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-vaults-addresses-200.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "address": "2N18VkRep3F2z7Ggm8W94nkKdARMfEM3EWa", + "addressId": "96bf95f1b59d50d9d1149057e8c0f9fd", + "addressSignaturePayload": "7b225465787441646472657373223a22324e3138566b5265703346327a3747676d385739346e6b4b6441524d66454d33455761227d", + "signature": "95c4ae4f5ab905bea2d385da09cb87c45bc0be138ba624d06e65ffa7468fd21220c63535abf2e8723ede3e649be65a4a69b183b7cd7d6a592b452f4b9d0ea603", + "walletId": "6a46a1977959e0529f567e8e927e3895" + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-wallet-200.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-wallet-200.json new file mode 100644 index 000000000..5ea634ac0 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-wallet-200.json @@ -0,0 +1,36 @@ +{ + "data": { + "assets": [ + { + "assetType": "BTC_S", + "availableBalance": { + "assetType": "BTC_S", + "currentPrice": "105552.083758084", + "currentUSDValue": "372.77", + "quantity": "0.00353165" + }, + "totalBalance": { + "assetType": "BTC_S", + "currentPrice": "105552.083758084", + "currentUSDValue": "372.77", + "quantity": "0.00353165" + } + } + ], + "depositAddress": { + "address": "2N85qduq5HGsQkAUsUGyouAE8YKz4we6y2h", + "addressId": "7ca733da4a4a665bbb26163c31058a95", + "addressSignaturePayload": "7b225465787441646472657373223a22324e38357164757135484773516b4155735547796f75414538594b7a34776536793268227d", + "addressID": "7ca733da4a4a665bbb26163c31058a95", + "signature": "2339b329a3bc43815b50d5cca805b4f3e87c081068ef5d397763e0095651afa066788f7ab60f0b705d2cf4b0366d31a2f2b2417ae3c78c973c8888be4b7a0f04" + }, + "isArchived": false, + "isDefault": true, + "networkId": "BTC_S", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "6a46a1977959e0529f567e8e927e3895", + "walletName": "Bitcoin Signet Wallet 1" + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-wallets-200.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-wallets-200.json new file mode 100644 index 000000000..3e1faa25d --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/get-wallets-200.json @@ -0,0 +1,363 @@ +{ + "data": [ + { + "assets": [], + "depositAddress": { + "address": "3Hy7Mb34EoXmdVQwdhAj1QZqD5kpH1voR8", + "addressId": "0ba932183dea75b8edef26c8fa806d04", + "addressSignaturePayload": "7b225465787441646472657373223a22334879374d623334456f586d645651776468416a31515a7144356b704831766f5238227d", + "addressID": "0ba932183dea75b8edef26c8fa806d04", + "signature": "0d6648a227057244b287240b385227f4c2220a61504801074b9f3be4a9a1ec07fc452fe958cc70b6bd2e3f83fb013808237aeaa761c3b078330d8a659004fd0c" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "145ff5b10e0e208b9c3adab0a9531f0c", + "walletName": "secondWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "0x81a1e40F018E411063768B04695Fd1a3c4CE11fD", + "addressId": "952837ceae2e623bc2ff0bb747887acf", + "addressSignaturePayload": "7b225465787441646472657373223a22307838316131653430463031384534313130363337363842303436393546643161336334434531316644227d", + "addressID": "952837ceae2e623bc2ff0bb747887acf", + "signature": "a1f5d2f5724fd83d4cbbcd4873ce3fef3ec6911255069ead07eaf4ff24200c214fd08e39ba17d67b88b4ba362219a79d5669719e7ae9797ec660188dcee4e20f" + }, + "isArchived": false, + "isDefault": true, + "networkId": "ETH_ZKSYNC_T", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "234963c4c39cb528ac3b8fa9c9db5280", + "walletName": "ZKsync Sepolia Testnet Wallet 1" + }, + { + "assets": [], + "depositAddress": { + "address": "3FNGj14FtsLZxyUXhogb5gofi28Ft22Y22", + "addressId": "5b3d879c618ff14796b7b96351c862f2", + "addressSignaturePayload": "7b225465787441646472657373223a2233464e476a31344674734c5a78795558686f676235676f6669323846743232593232227d", + "addressID": "5b3d879c618ff14796b7b96351c862f2", + "signature": "25e229c81fc671d046bf205053061a2755d8ab2102ce5150b8a2c59e45dade12a5967d2029fea0717cf599eb4a56ef27be1c20f84bb76ae98d0df02b0a0cd806" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "307258c6006b367a7aa3d5d3665c64b0", + "walletName": "secondWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "0x81a1e40F018E411063768B04695Fd1a3c4CE11fD", + "addressId": "6e4c8306027028ad0d35248059e0eac7", + "addressSignaturePayload": "7b225465787441646472657373223a22307838316131653430463031384534313130363337363842303436393546643161336334434531316644227d", + "addressID": "6e4c8306027028ad0d35248059e0eac7", + "signature": "a1f5d2f5724fd83d4cbbcd4873ce3fef3ec6911255069ead07eaf4ff24200c214fd08e39ba17d67b88b4ba362219a79d5669719e7ae9797ec660188dcee4e20f" + }, + "isArchived": false, + "isDefault": true, + "networkId": "ETH_ARBITRUM_T", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "655d4d4524bbd1cefbf2c0cdd9ebdece", + "walletName": "Arbitrum Sepolia Testnet Wallet 1" + }, + { + "assets": [ + { + "assetType": "BTC_S", + "availableBalance": { + "assetType": "BTC_S", + "currentPrice": "100571.636951942", + "currentUSDValue": "502.86", + "quantity": "0.005" + }, + "totalBalance": { + "assetType": "BTC_S", + "currentPrice": "100571.636951942", + "currentUSDValue": "502.86", + "quantity": "0.005" + } + } + ], + "depositAddress": { + "address": "2N18VkRep3F2z7Ggm8W94nkKdARMfEM3EWa", + "addressId": "96bf95f1b59d50d9d1149057e8c0f9fd", + "addressSignaturePayload": "7b225465787441646472657373223a22324e3138566b5265703346327a3747676d385739346e6b4b6441524d66454d33455761227d", + "addressID": "96bf95f1b59d50d9d1149057e8c0f9fd", + "signature": "95c4ae4f5ab905bea2d385da09cb87c45bc0be138ba624d06e65ffa7468fd21220c63535abf2e8723ede3e649be65a4a69b183b7cd7d6a592b452f4b9d0ea603" + }, + "isArchived": false, + "isDefault": true, + "networkId": "BTC_S", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "6a46a1977959e0529f567e8e927e3895", + "walletName": "Bitcoin Signet Wallet 1" + }, + { + "assets": [], + "depositAddress": { + "address": "0x27211c05145FDc3c916e3377DB1a257364c5380B", + "addressId": "e2a48a472cf729eb8874fc518a8e4cbd", + "addressSignaturePayload": "7b225465787441646472657373223a22307832373231316330353134354644633363393136653333373744423161323537333634633533383042227d", + "addressID": "e2a48a472cf729eb8874fc518a8e4cbd", + "signature": "55ab7af2138ada8bb2bbc7a57a219e0d45f92317d949ac508f3890a57057b15c9b83c0d39c96633bea5916668460f191a2f63b452a6b7e5c409e0e010b92b20c" + }, + "isArchived": false, + "isDefault": true, + "networkId": "ETH", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "7f81de30acbe98e3f6895eb12ab7d16f", + "walletName": "Ethereum Wallet 1" + }, + { + "assets": [], + "depositAddress": { + "address": "3DbSPwCQF9kWqpCHWjHuTY8uBu9PqHStUr", + "addressId": "a569b247f3dfa59fbd4df08d4814132c", + "addressSignaturePayload": "7b225465787441646472657373223a22334462535077435146396b5771704348576a48755459387542753950714853745572227d", + "addressID": "a569b247f3dfa59fbd4df08d4814132c", + "signature": "b4d92f68663365a9931d2ca934635ac6c844e0a73b45c8f25779f6bd330ea6700807fb6c72014d159534902219924586b28db6fabcd982caf73bd533f11b3802" + }, + "isArchived": false, + "isDefault": true, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "97f83a7fae3563895f50e2cbe4b9906c", + "walletName": "firstWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "0x27211c05145FDc3c916e3377DB1a257364c5380B", + "addressId": "e49984c07576e86c622fd28fe44537ee", + "addressSignaturePayload": "7b225465787441646472657373223a22307832373231316330353134354644633363393136653333373744423161323537333634633533383042227d", + "addressID": "e49984c07576e86c622fd28fe44537ee", + "signature": "55ab7af2138ada8bb2bbc7a57a219e0d45f92317d949ac508f3890a57057b15c9b83c0d39c96633bea5916668460f191a2f63b452a6b7e5c409e0e010b92b20c" + }, + "isArchived": false, + "isDefault": true, + "networkId": "POL_POLYGON", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "aba218a001728a6dd12b71cff2f8b5c0", + "walletName": "Polygon Wallet 1" + }, + { + "assets": [], + "depositAddress": { + "address": "3MeQ1PQZhnsJQUfHydE4dtpkri36gvrb8y", + "addressId": "9f5fe77af96429160103d24a61f9c4cb", + "addressSignaturePayload": "7b225465787441646472657373223a22334d65513150515a686e734a51556648796445346474706b72693336677672623879227d", + "addressID": "9f5fe77af96429160103d24a61f9c4cb", + "signature": "046642e46ba6d34c6318ecc25bb7b09f45d36a6ea352302b61ecbb3ef78c527aa33f47a886df351ccbb5cfcf40feb62b21d890c71e66babfd5d5bfe77556410c" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "ae7abb325366f0623f9e192cd491436f", + "walletName": "secondWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "3299b5fNXQortckqRRo7sAa7mgQcVotYPT", + "addressId": "6639c74804e5599860e9f5d8d55c03fb", + "addressSignaturePayload": "7b225465787441646472657373223a22333239396235664e58516f7274636b7152526f37734161376d675163566f74595054227d", + "addressID": "6639c74804e5599860e9f5d8d55c03fb", + "signature": "f295be507f3dcae9aa8a83b2e39b0341c6db16759f2e14bf9067aaf1714db76f61ff97119c032b085b737cfae438548880fbe6ff789b314415f1ca228e5b410d" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "bf4538760915a5022d23f64b988cfedd", + "walletName": "secondWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "3N86DziokoWESv1ZmEVC5ZCYVUnmhFhBNp", + "addressId": "7421b5541e928fe2dc35e5f95453c330", + "addressSignaturePayload": "7b225465787441646472657373223a22334e3836447a696f6b6f57455376315a6d455643355a435956556e6d684668424e70227d", + "addressID": "7421b5541e928fe2dc35e5f95453c330", + "signature": "a42054cfea8d836d93643538ba087c248a973b6fb0c490453d29f0e2b7e8b2a264a063097382a9085836d975fd0eb6770e61fdf3203f12b44573dc55fbb0cf09" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "c3753da1db3d040f2ee295f6f3a91e0c", + "walletName": "firstWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "0x6F28B85E1246ce5A4af89c5C94C0C661D6Cd031a", + "addressId": "f031997f52f848838b1f3b4eb1aa83fa", + "addressSignaturePayload": "7b225465787441646472657373223a22307836463238423835453132343663653541346166383963354339344330433636314436436430333161227d", + "addressID": "f031997f52f848838b1f3b4eb1aa83fa", + "signature": "f3197acd348734317a1b9fa0817f533d93ed2407545cd5606e6c95046ecc0bb4d719d54404fe94a02bd57def3add81cdee9547d93515f32913242e6f26937a0a" + }, + "isArchived": false, + "isDefault": false, + "networkId": "POL_POLYGON", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "c4e5e0936fc1bd3646c3b0af9b2e729b", + "walletName": "Polygon Wallet 2" + }, + { + "assets": [], + "depositAddress": { + "address": "3FGW9JswZNkrLtSfkqfVMekNg1T7tuAkxe", + "addressId": "99a5dc014105cc59304813f8540ccc56", + "addressSignaturePayload": "7b225465787441646472657373223a2233464757394a73775a4e6b724c7453666b7166564d656b4e673154377475416b7865227d", + "addressID": "99a5dc014105cc59304813f8540ccc56", + "signature": "ba0044130fb6668c3bdb24eb51c21ff956829f65b14cd43e7ae288fb20aeb6dce65cb86fbee18cbc92da8f9215630f7c916227f473fa9103edef30020f0d5a08" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "c8e6f9f5e09aeb4f5bbf43bf34ba909e", + "walletName": "firstWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "0x81a1e40F018E411063768B04695Fd1a3c4CE11fD", + "addressId": "6d58eaac11543ba7477294d6981cf897", + "addressSignaturePayload": "7b225465787441646472657373223a22307838316131653430463031384534313130363337363842303436393546643161336334434531316644227d", + "addressID": "6d58eaac11543ba7477294d6981cf897", + "signature": "a1f5d2f5724fd83d4cbbcd4873ce3fef3ec6911255069ead07eaf4ff24200c214fd08e39ba17d67b88b4ba362219a79d5669719e7ae9797ec660188dcee4e20f" + }, + "isArchived": false, + "isDefault": true, + "networkId": "ETHSEP", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "d30a5c8cb83f0be46d7669f81d776a62", + "walletName": "Ethereum Sepolia Wallet 1" + }, + { + "assets": [], + "depositAddress": { + "address": "0x6F28B85E1246ce5A4af89c5C94C0C661D6Cd031a", + "addressId": "4407a91a23f49ca8160af0708373c53a", + "addressSignaturePayload": "7b225465787441646472657373223a22307836463238423835453132343663653541346166383963354339344330433636314436436430333161227d", + "addressID": "4407a91a23f49ca8160af0708373c53a", + "signature": "f3197acd348734317a1b9fa0817f533d93ed2407545cd5606e6c95046ecc0bb4d719d54404fe94a02bd57def3add81cdee9547d93515f32913242e6f26937a0a" + }, + "isArchived": false, + "isDefault": false, + "networkId": "ETH", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "daa5ead0f7f164d2de3c2fb83610f447", + "walletName": "Ethereum Wallet 2" + }, + { + "assets": [], + "depositAddress": { + "address": "3FWW3qkJjpdMZLHc1TDpbaNhYDxW3oYpdA", + "addressId": "dea22db33e3a40784a75971b73558258", + "addressSignaturePayload": "7b225465787441646472657373223a223346575733716b4a6a70644d5a4c48633154447062614e6859447857336f59706441227d", + "addressID": "dea22db33e3a40784a75971b73558258", + "signature": "abbfe2b34c8980b8c6bef797095afbaa9a62cd9197eaaf72a84bfbca755645a04c9d547862e66fdc72a07c9db24edd8c9f99f7fbceb7fe9cd06bdefe74203103" + }, + "isArchived": false, + "isDefault": false, + "networkId": "BTC", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "fa2d82db5fab74ed0c1eec3e57eba2fb", + "walletName": "secondWalletThroughApi!" + }, + { + "assets": [], + "depositAddress": { + "address": "DDzPFiqWrHHYFRQQnh9ka83PYaVrn5zSsE", + "addressId": "59ec6d5ed7be498d54f8cb268011846b", + "addressSignaturePayload": "7b225465787441646472657373223a2244447a504669715772484859465251516e68396b61383350596156726e357a537345227d", + "addressID": "59ec6d5ed7be498d54f8cb268011846b", + "signature": "7b122a9c5b2579b74db77262bc66e2a52e68f5d7246cd94f3f1bb9e381661d8ba5023f32b3d5cc0b04f7dfa3aaaa52fba91c080777b9396f2e314e70c52c8f05" + }, + "isArchived": false, + "isDefault": true, + "networkId": "DOGE", + "type": "WALLET", + "vaultId": "084ff57c0984420efac31723579c94fc", + "vaultName": "Vault 1", + "walletId": "fdd93f7a1f41b98bd6672edfa301803c", + "walletName": "Doge" + }, + { + "assets": [ + { + "assetType": "ETHHOL", + "availableBalance": { + "assetType": "ETHHOL", + "currentPrice": "3801.93297363249", + "currentUSDValue": "3798.13", + "quantity": "0.998998527910122" + }, + "totalBalance": { + "assetType": "ETHHOL", + "currentPrice": "3801.93297363249", + "currentUSDValue": "3798.13", + "quantity": "0.998998527910122" + } + } + ], + "depositAddress": { + "address": "0x4BB7c65f821Ffcd1CAFEF3276E8eDa44474A08D3", + "addressId": "3d3b31a695fcbdf1c8a16c5df62c6516", + "addressSignaturePayload": "7b225465787441646472657373223a22307834424237633635663832314666636431434146454633323736453865446134343437344130384433227d", + "addressID": "3d3b31a695fcbdf1c8a16c5df62c6516", + "signature": "68a971bec8f22480202413fef93e0b0cbc7b6daf43c4c17cc1ed1948dd97742ec9a1a28336f2eaa95c32fd8ce342f00b62fefae00cf7d6a609d1bb21646a9e0d" + }, + "isArchived": false, + "isDefault": true, + "networkId": "ETHHOL", + "type": "WALLET", + "vaultId": "62547351cea99e827bcd43a513c40e7c", + "vaultName": "Vault 2", + "walletId": "ec9a04d05472e417ceb06e32c66834d1", + "walletName": "Ethereum Holešky Wallet 1" + } + ], + "page": { + "next": null + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/post-transfer-201.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/post-transfer-201.json new file mode 100644 index 000000000..d99d733ea --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/post-transfer-201.json @@ -0,0 +1,6 @@ +{ + "data": { + "transferId": "008d3ec72558ce907571886df63ef51594b5bd8cf106a0b7fa8f12a30dfc867f", + "status": "IN_PROGRESS" + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/post-transfer-400.json b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/post-transfer-400.json new file mode 100644 index 000000000..56891b250 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/response/post-transfer-400.json @@ -0,0 +1,4 @@ +{ + "errorType": "InternalError", + "message": "Missing required field 'amount'." +} diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/server.ts b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/server.ts new file mode 100644 index 000000000..70a90ab9b --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/server-mock/server.ts @@ -0,0 +1,74 @@ +import { HttpStatus } from '@nestjs/common' +import { HttpResponse, http } from 'msw' +import getTransferOk from './response/get-transfer-200.json' +import trustedDestinationsSecond from './response/get-trusted-destinations-200-second.json' +import trustedDestinationsThird from './response/get-trusted-destinations-200-third.json' +import trustedDestinationsFirst from './response/get-trusted-destinations-200.json' +import getVaultsOkSecond from './response/get-vaults-200-second.json' +import getVaultsOk from './response/get-vaults-200.json' +import getVaultAddressesOk from './response/get-vaults-addresses-200.json' +import getWalletOk from './response/get-wallet-200.json' +import getWalletsOk from './response/get-wallets-200.json' +import postTransferCreated from './response/post-transfer-201.json' + +export const ANCHORAGE_TEST_API_BASE_URL = 'https://test-mock-api.anchorage.com' + +export const getTrustedDestinationHandlers = (baseUrl = ANCHORAGE_TEST_API_BASE_URL) => { + return { + findAll: http.get(`${baseUrl}/v2/trusted_destinations`, () => { + return new HttpResponse(JSON.stringify(trustedDestinationsFirst)) + }), + + deleteAndUpdate: http.get(`${baseUrl}/v2/trusted_destinations`, () => { + return new HttpResponse(JSON.stringify(trustedDestinationsSecond)) + }), + + connect: http.get(`${baseUrl}/v2/trusted_destinations`, () => { + return new HttpResponse(JSON.stringify(trustedDestinationsThird)) + }) + } +} + +export const getVaultHandlers = (baseUrl = ANCHORAGE_TEST_API_BASE_URL) => { + return { + findAll: http.get(`${baseUrl}/v2/vaults`, () => { + return new HttpResponse(JSON.stringify(getVaultsOk)) + }), + update: http.get(`${baseUrl}/v2/vaults`, () => { + return new HttpResponse(JSON.stringify(getVaultsOkSecond)) + }) + } +} + +export const getHandlers = (baseUrl = ANCHORAGE_TEST_API_BASE_URL) => [ + http.get(`${baseUrl}/v2/vaults/:vaultId/addresses`, () => { + return new HttpResponse(JSON.stringify(getVaultAddressesOk)) + }), + + http.get(`${baseUrl}/v2/wallets`, () => { + return new HttpResponse(JSON.stringify(getWalletsOk)) + }), + + http.get(`${baseUrl}/v2/wallets/6a46a1977959e0529f567e8e927e3895`, () => { + return new HttpResponse(JSON.stringify(getWalletOk)) + }), + + http.get(`${baseUrl}/v2/wallets/notFound`, () => { + return new HttpResponse(JSON.stringify({ errorType: 'NotFound', message: 'Wallet not found' }), { + status: HttpStatus.NOT_FOUND + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as unknown as any) + }), + + http.get(`${baseUrl}/v2/transfers/:transferId`, () => { + return new HttpResponse(JSON.stringify(getTransferOk)) + }), + + http.post(`${baseUrl}/v2/transfers`, () => { + return new HttpResponse(JSON.stringify(postTransferCreated)) + }), + + getVaultHandlers(baseUrl).findAll, + + getTrustedDestinationHandlers(baseUrl).findAll +] diff --git a/apps/vault/src/broker/core/provider/anchorage/__test__/unit/anchorage-credential.service.spec.ts b/apps/vault/src/broker/core/provider/anchorage/__test__/unit/anchorage-credential.service.spec.ts new file mode 100644 index 000000000..05bae6f0e --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/__test__/unit/anchorage-credential.service.spec.ts @@ -0,0 +1,98 @@ +import { + Alg, + Hex, + ed25519PrivateKeySchema, + ed25519PublicKeySchema, + generateJwk, + getPublicKey, + privateKeyToHex +} from '@narval/signature' +import { Test } from '@nestjs/testing' +import { ParseException } from '../../../../../../shared/module/persistence/exception/parse.exception' +import '../../../../../shared/__test__/matcher' +import { ConnectionInvalidPrivateKeyException } from '../../../../exception/connection-invalid-private-key.exception' +import { AnchorageCredentialService } from '../../anchorage-credential.service' +import { AnchorageCredentials, AnchorageInputCredentials } from '../../anchorage.type' + +describe(AnchorageCredentialService.name, () => { + let service: AnchorageCredentialService + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [AnchorageCredentialService] + }).compile() + + service = module.get(AnchorageCredentialService) + }) + + describe('parse', () => { + it('validates and parses raw credentials into typed AnchorageCredentials', async () => { + const privateKey = await generateJwk(Alg.EDDSA) + const validCredentials: AnchorageCredentials = { + privateKey, + publicKey: getPublicKey(privateKey), + apiKey: 'test-api-key' + } + + const result = service.parse(validCredentials) + expect(result).toStrictEqual(validCredentials) + }) + + it('throws error when parsing invalid credentials format', () => { + const invalidCredentials = { + apiKey: 'test-api-key', + // Missing required EdDSA key properties + publicKey: { kty: 'OKP' }, + privateKey: { kty: 'OKP' } + } + + expect(() => service.parse(invalidCredentials)).toThrow(ParseException) + }) + }) + + describe('build', () => { + it('transforms input credentials into provider-ready format', async () => { + const eddsaPrivateKey = await generateJwk(Alg.EDDSA) + const input: AnchorageInputCredentials = { + apiKey: 'test-api-key', + privateKey: await privateKeyToHex(eddsaPrivateKey) + } + + const credentials = await service.build(input) + + expect(credentials).toEqual({ + apiKey: input.apiKey, + privateKey: eddsaPrivateKey, + publicKey: getPublicKey(eddsaPrivateKey) + }) + }) + + it('throws error when input private key is invalid', async () => { + const invalidPrivateKey = 'invalid-key-string' + const input: AnchorageInputCredentials = { + apiKey: 'test-api-key', + privateKey: invalidPrivateKey as Hex + } + + await expect(service.build(input)).rejects.toThrow(ConnectionInvalidPrivateKeyException) + }) + + it('throws error when input private key is undefined', async () => { + const input: AnchorageInputCredentials = { + apiKey: 'test-api-key', + privateKey: undefined + } + + await expect(service.build(input)).rejects.toThrow(ConnectionInvalidPrivateKeyException) + }) + }) + + describe('generate', () => { + it('creates new EdDSA key pair with api credentials', async () => { + const credentials = await service.generate() + + expect(credentials.publicKey).toMatchZodSchema(ed25519PublicKeySchema) + expect(credentials.privateKey).toMatchZodSchema(ed25519PrivateKeySchema) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage-credential.service.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage-credential.service.ts new file mode 100644 index 000000000..00f82e749 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage-credential.service.ts @@ -0,0 +1,64 @@ +import { Alg, generateJwk, getPublicKey, privateKeyToJwk } from '@narval/signature' +import { Injectable } from '@nestjs/common' +import { ParseException } from '../../../../shared/module/persistence/exception/parse.exception' +import { ConnectionInvalidPrivateKeyException } from '../../exception/connection-invalid-private-key.exception' +import { ProviderCredentialService } from '../../type/provider.type' +import { AnchorageCredentials, AnchorageInputCredentials } from './anchorage.type' + +@Injectable() +export class AnchorageCredentialService + implements ProviderCredentialService +{ + static SIGNING_KEY_ALG = Alg.EDDSA + + parse(value: unknown): AnchorageCredentials { + const parse = AnchorageCredentials.safeParse(value) + + if (parse.success) { + return parse.data + } + + throw new ParseException(parse.error) + } + + parseInput(value: unknown): AnchorageInputCredentials { + const parse = AnchorageInputCredentials.safeParse(value) + + if (parse.success) { + return parse.data + } + + throw new ParseException(parse.error) + } + + async build(input: AnchorageInputCredentials): Promise { + if (input.privateKey) { + try { + const privateKey = privateKeyToJwk(input.privateKey, Alg.EDDSA) + const publicKey = getPublicKey(privateKey) + + return { + apiKey: input.apiKey, + privateKey, + publicKey + } + } catch (error) { + throw new ConnectionInvalidPrivateKeyException({ + message: error.message, + origin: error + }) + } + } + + throw new ConnectionInvalidPrivateKeyException() + } + + async generate(): Promise { + const privateKey = await generateJwk(Alg.EDDSA) + + return { + privateKey, + publicKey: getPublicKey(privateKey) + } + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage-known-destination.service.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage-known-destination.service.ts new file mode 100644 index 000000000..9cfe5d455 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage-known-destination.service.ts @@ -0,0 +1,102 @@ +import { LoggerService, PaginatedResult } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { AnchorageClient } from '../../../http/client/anchorage.client' +import { AssetService } from '../../service/asset.service' +import { NetworkService } from '../../service/network.service' +import { Asset } from '../../type/asset.type' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { KnownDestination } from '../../type/known-destination.type' +import { + Provider, + ProviderKnownDestinationPaginationOptions, + ProviderKnownDestinationService +} from '../../type/provider.type' +import { validateConnection } from './anchorage.util' + +@Injectable() +export class AnchorageKnownDestinationService implements ProviderKnownDestinationService { + constructor( + private readonly anchorageClient: AnchorageClient, + private readonly networkService: NetworkService, + private readonly assetService: AssetService, + private readonly logger: LoggerService + ) {} + + async findAll( + connection: ConnectionWithCredentials, + options?: ProviderKnownDestinationPaginationOptions | undefined + ): Promise> { + validateConnection(connection) + + const anchorageTrustedDestinations = await this.anchorageClient.getTrustedDestinations({ + url: connection.url, + limit: options?.limit, + afterId: options?.cursor, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const assetsIndexedByExternalId = await this.assetService.buildProviderExternalIdIndex(Provider.ANCHORAGE) + + const networksIndexedByExternalId = await this.networkService.buildProviderExternalIdIndex(Provider.ANCHORAGE) + + const knownDestinations: KnownDestination[] = [] + + for (const anchorageTrustedDestination of anchorageTrustedDestinations.data) { + const externalId = anchorageTrustedDestination.id + const network = networksIndexedByExternalId.get(anchorageTrustedDestination.crypto.networkId) + + let asset: Asset | undefined + + if (anchorageTrustedDestination.crypto.assetType) { + this.logger.log('Lookup Anchorage trusted destination asset type', { + clientId: connection.clientId, + connectionId: connection.connectionId, + trustedDestination: anchorageTrustedDestination + }) + + asset = assetsIndexedByExternalId.get(anchorageTrustedDestination.crypto.assetType) + } + + if (network) { + const knownDestination = KnownDestination.parse({ + externalId, + address: anchorageTrustedDestination.crypto.address.toLowerCase(), + assetId: asset?.assetId, + externalClassification: null, + clientId: connection.clientId, + connectionId: connection.connectionId, + // NOTE: Anchorage doesn't return a label for trusted destinations. + label: null, + networkId: network.networkId, + provider: Provider.ANCHORAGE + }) + + knownDestinations.push(knownDestination) + } else { + this.logger.warn('Skip Anchorage known destination due to network not found', { + externalId, + externalNetworkId: anchorageTrustedDestination.crypto.networkId, + address: anchorageTrustedDestination.crypto.address, + clientId: connection.clientId, + connectionId: connection.connectionId + }) + } + } + + const last = knownDestinations[knownDestinations.length - 1] + + if (anchorageTrustedDestinations.page.next && last) { + return { + data: knownDestinations, + page: { + next: last.externalId + } + } + } + + return { + data: knownDestinations + } + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage-proxy.service.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage-proxy.service.ts new file mode 100644 index 000000000..07d339954 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage-proxy.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common' +import { AnchorageClient } from '../../../http/client/anchorage.client' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { ProviderProxyService, ProxyRequestOptions, ProxyResponse } from '../../type/provider.type' +import { validateConnection } from './anchorage.util' + +@Injectable() +export class AnchorageProxyService implements ProviderProxyService { + constructor(private readonly anchorageClient: AnchorageClient) {} + + async forward( + connection: ConnectionWithCredentials, + { data, endpoint, method }: ProxyRequestOptions + ): Promise { + validateConnection(connection) + + const { url, credentials } = connection + const { apiKey, privateKey } = credentials + const fullUrl = `${url}${endpoint}` + + const response = await this.anchorageClient.forward({ + url: fullUrl, + method, + data, + apiKey, + signKey: privateKey + }) + + return { + data: response.data, + code: response.status, + headers: response.headers + } + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage-scoped-sync.service.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage-scoped-sync.service.ts new file mode 100644 index 000000000..530aa21d3 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage-scoped-sync.service.ts @@ -0,0 +1,278 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { randomUUID } from 'crypto' +import { chunk, uniqBy } from 'lodash/fp' +import { AnchorageClient, Wallet as AnchorageWallet } from '../../../http/client/anchorage.client' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { Account, Address, Wallet } from '../../type/indexed-resources.type' +import { NetworkMap } from '../../type/network.type' +import { Provider, ProviderScopedSyncService } from '../../type/provider.type' +import { + RawAccount, + RawAccountError, + RawAccountSyncFailure, + ScopedSyncContext, + ScopedSyncResult +} from '../../type/scoped-sync.type' +import { CONCURRENT_ANCHORAGE_REQUESTS, ValidConnection, validateConnection } from './anchorage.util' + +type RawAccountSyncSuccess = { + success: true + wallet: Wallet + account: Account + address: Address +} + +type RawAccountSyncFailed = { + success: false + failure: RawAccountSyncFailure +} + +type RawAccountSyncResult = RawAccountSyncSuccess | RawAccountSyncFailed + +@Injectable() +export class AnchorageScopedSyncService implements ProviderScopedSyncService { + constructor( + private readonly anchorageClient: AnchorageClient, + private readonly logger: LoggerService + ) {} + + private resolveFailure(failure: RawAccountSyncFailure): RawAccountSyncFailed { + this.logger.log('Failed to sync Raw Account', failure) + return { success: false, failure } + } + + private resolveSuccess({ + rawAccount, + wallet, + account, + address + }: { + wallet: Wallet + address: Address + rawAccount: RawAccount + account: Account + }): RawAccountSyncSuccess { + this.logger.log('Successfully fetched and map Raw Account', { + rawAccount, + account + }) + return { + success: true, + wallet, + account, + address + } + } + + private mapAnchorageWalletToNarvalModel( + anchorageWallet: AnchorageWallet, + { + networks, + now, + existingAccounts, + rawAccount, + connection, + existingWallets + }: { + networks: NetworkMap + now: Date + existingAccounts: Account[] + existingWallets: Wallet[] + rawAccount: RawAccount + connection: ValidConnection + } + ): RawAccountSyncResult { + const network = networks.get(anchorageWallet.networkId) + if (!network) { + this.logger.error('Network not found', { + rawAccount, + externalNetwork: anchorageWallet.networkId + }) + return this.resolveFailure({ + rawAccount, + message: 'Network for this account is not supported', + code: RawAccountError.UNLISTED_NETWORK, + networkId: anchorageWallet.networkId + }) + } + const existingAccount = existingAccounts.find((a) => a.externalId === anchorageWallet.walletId) + const existingWallet = existingWallets.find((w) => w.externalId === anchorageWallet.vaultId) + + const walletId = existingWallet?.walletId || existingAccount?.walletId || randomUUID() + const accountId = existingAccount?.accountId || randomUUID() + + const wallet: Wallet = { + accounts: [], + clientId: connection.clientId, + connectionId: connection.connectionId, + createdAt: now, + externalId: anchorageWallet.vaultId, + label: anchorageWallet.walletName, + provider: Provider.ANCHORAGE, + updatedAt: now, + walletId + } + + const account: Account = { + externalId: anchorageWallet.walletId, + accountId, + addresses: [], + clientId: connection.clientId, + connectionId: connection.connectionId, + createdAt: now, + label: anchorageWallet.walletName, + networkId: network.networkId, + provider: Provider.ANCHORAGE, + updatedAt: now, + walletId + } + + const address: Address = { + accountId, + address: anchorageWallet.depositAddress.address, + addressId: randomUUID(), + clientId: connection.clientId, + connectionId: connection.connectionId, + createdAt: now, + externalId: anchorageWallet.depositAddress.addressId, + provider: Provider.ANCHORAGE, + updatedAt: now + } + + return this.resolveSuccess({ rawAccount, wallet, account, address }) + } + + private async syncRawAccount({ + connection, + rawAccount, + networks, + now, + existingAccounts, + existingWallets + }: { + connection: ConnectionWithCredentials + rawAccount: RawAccount + networks: NetworkMap + now: Date + existingAccounts: Account[] + existingWallets: Wallet[] + }): Promise { + validateConnection(connection) + + try { + const anchorageWallet = await this.anchorageClient.getWallet({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + walletId: rawAccount.externalId + }) + + return this.mapAnchorageWalletToNarvalModel(anchorageWallet, { + networks, + now, + existingAccounts, + rawAccount, + connection, + existingWallets + }) + } catch (error) { + if (error.response?.status === HttpStatus.NOT_FOUND) { + return this.resolveFailure({ + rawAccount, + message: 'Anchorage wallet not found', + code: RawAccountError.EXTERNAL_RESOURCE_NOT_FOUND, + externalResourceType: 'wallet', + externalResourceId: rawAccount.externalId + }) + } + throw error + } + } + + async scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts + }: ScopedSyncContext): Promise { + this.logger.log('Sync Anchorage accounts', { + connectionId: connection.connectionId, + clientId: connection.clientId, + url: connection.url + }) + + const now = new Date() + + const chunkedRawAccounts = chunk(CONCURRENT_ANCHORAGE_REQUESTS, rawAccounts) + const results: RawAccountSyncResult[] = [] + + // TODO @ptroger: remove the if block when we completely move towards picking accounts in UI + if (rawAccounts.length === 0) { + validateConnection(connection) + + const anchorageWallets = await this.anchorageClient.getWallets({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const existingWallets: Wallet[] = [] + const mappedAnchorageWallets = anchorageWallets.map((anchorageWallet) => { + const map = this.mapAnchorageWalletToNarvalModel(anchorageWallet, { + networks, + now, + existingAccounts, + rawAccount: { provider: Provider.ANCHORAGE, externalId: anchorageWallet.walletId }, + connection, + existingWallets + }) + map.success && existingWallets.push(map.wallet) + return map + }) + + results.push(...mappedAnchorageWallets) + } else { + const existingWallets: Wallet[] = [] + for (const chunk of chunkedRawAccounts) { + const chunkResults = await Promise.all( + chunk.map(async (rawAccount) => { + const mapResult = await this.syncRawAccount({ + connection, + rawAccount, + networks, + now, + existingAccounts, + existingWallets + }) + mapResult.success && existingWallets.push(mapResult.wallet) + return mapResult + }) + ) + results.push(...chunkResults) + } + } + + const wallets: Wallet[] = [] + const accounts: Account[] = [] + const addresses: Address[] = [] + const failures: RawAccountSyncFailure[] = [] + + for (const result of results) { + if (result.success) { + wallets.push(result.wallet) + accounts.push(result.account) + addresses.push(result.address) + } else { + failures.push(result.failure) + } + } + + return { + wallets: uniqBy('externalId', wallets), + accounts: uniqBy('externalId', accounts), + addresses: uniqBy('externalId', addresses), + failures + } + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage-transfer.service.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage-transfer.service.ts new file mode 100644 index 000000000..393ebb9ea --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage-transfer.service.ts @@ -0,0 +1,368 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { v4 as uuid } from 'uuid' +import { AnchorageClient } from '../../../http/client/anchorage.client' +import { TransferRepository } from '../../../persistence/repository/transfer.repository' +import { AssetException } from '../../exception/asset.exception' +import { BrokerException } from '../../exception/broker.exception' +import { AccountService } from '../../service/account.service' +import { NetworkService } from '../../service/network.service' +import { ResolvedTransferAsset, TransferAssetService } from '../../service/transfer-asset.service' +import { WalletService } from '../../service/wallet.service' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { Network } from '../../type/network.type' +import { Provider, ProviderTransferService } from '../../type/provider.type' +import { ConnectionScope } from '../../type/scope.type' +import { + Destination, + InternalTransfer, + NetworkFeeAttribution, + SendTransfer, + Source, + Transfer, + TransferAsset, + TransferPartyType, + TransferStatus, + isAddressDestination, + isProviderSpecific +} from '../../type/transfer.type' +import { getExternalNetwork } from '../../util/network.util' +import { ValidConnection, transferPartyTypeToAnchorageResourceType, validateConnection } from './anchorage.util' + +@Injectable() +export class AnchorageTransferService implements ProviderTransferService { + constructor( + private readonly anchorageClient: AnchorageClient, + private readonly networkService: NetworkService, + private readonly accountService: AccountService, + private readonly walletService: WalletService, + private readonly transferRepository: TransferRepository, + private readonly transferAssetService: TransferAssetService, + private readonly logger: LoggerService + ) {} + + async findById(connection: ConnectionWithCredentials, transferId: string): Promise { + const { clientId, connectionId } = connection + + const context = { clientId, connectionId, transferId } + + this.logger.log('Find Anchorage transfer by ID', context) + + validateConnection(connection) + + const internalTransfer = await this.transferRepository.findById(connection.clientId, transferId) + + this.logger.log('Found internal transfer by ID', { ...context, internalTransfer }) + + const anchorageTransfer = await this.anchorageClient.getTransferById({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + transferId: internalTransfer.externalId + }) + + this.logger.log('Found remote transfer by external ID', { ...context, anchorageTransfer }) + + const transfer = { + connectionId, + assetExternalId: internalTransfer.assetExternalId, + assetId: internalTransfer.assetId, + clientId: internalTransfer.clientId, + createdAt: new Date(anchorageTransfer.createdAt), + customerRefId: internalTransfer.customerRefId, + destination: internalTransfer.destination, + externalId: internalTransfer.externalId, + externalStatus: anchorageTransfer.status, + grossAmount: anchorageTransfer.amount.quantity, + idempotenceId: internalTransfer.idempotenceId, + memo: anchorageTransfer.transferMemo || internalTransfer.memo || null, + networkFeeAttribution: internalTransfer.networkFeeAttribution, + provider: internalTransfer.provider, + providerSpecific: internalTransfer.providerSpecific, + source: internalTransfer.source, + status: this.mapStatus(anchorageTransfer.status), + transferId: internalTransfer.transferId, + fees: anchorageTransfer.fee + ? [ + { + type: 'network', + attribution: internalTransfer.networkFeeAttribution, + amount: anchorageTransfer.fee?.quantity, + assetId: anchorageTransfer.fee?.assetType + } + ] + : [] + } + + this.logger.log('Combined internal and remote Anchorage transfer', { ...context, transfer }) + + return transfer + } + + private mapStatus(status: string): TransferStatus { + const upperCasedStatus = status.toUpperCase() + const statuses: Record = { + IN_PROGRESS: TransferStatus.PROCESSING, + QUEUED: TransferStatus.PROCESSING, + COMPLETED: TransferStatus.SUCCESS, + FAILED: TransferStatus.FAILED + } + + if (upperCasedStatus in statuses) { + return statuses[upperCasedStatus] + } + + throw new BrokerException({ + message: 'Cannot map Anchorage transfer status', + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + context: { status: upperCasedStatus } + }) + } + + async send(connection: ConnectionWithCredentials, sendTransfer: SendTransfer): Promise { + const { clientId, connectionId } = connection + const context = { clientId, connectionId } + + this.logger.log('Send Anchorage transfer', { ...context, sendTransfer }) + + validateConnection(connection) + + const transferAsset = await this.findTransferAsset(connection, sendTransfer.asset) + if (!transferAsset) { + throw new BrokerException({ + message: 'Transfer asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { asset: sendTransfer.asset } + }) + } + + const source = await this.getSource(connection, sendTransfer.source) + + const destination = await this.getDestination(connection, sendTransfer.destination) + + this.logger.log('Resolved Anchorage transfer source and destination', { ...context, source, destination }) + + // NOTE: Because Anchorage defaults `deductFeeFromAmountIfSameType` to + // false, we default the fee attribution to ON_TOP to match their API's + // behaviour. + const networkFeeAttribution = sendTransfer.networkFeeAttribution || NetworkFeeAttribution.ON_TOP + + const data = { + source, + destination, + assetType: transferAsset.assetExternalId, + amount: sendTransfer.amount, + transferMemo: sendTransfer.memo || null, + idempotentId: sendTransfer.idempotenceId, + deductFeeFromAmountIfSameType: this.getDeductFeeFromAmountIfSameType(networkFeeAttribution), + ...(isProviderSpecific(sendTransfer.providerSpecific) ? { ...sendTransfer.providerSpecific } : {}) + } + + this.logger.log('Send create transfer request to Anchorage', { ...context, data }) + + const anchorageTransfer = await this.anchorageClient.createTransfer({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + data + }) + + this.logger.log('Anchorage transfer created', context) + + const internalTransfer: InternalTransfer = { + assetId: transferAsset.assetId, + assetExternalId: transferAsset.assetExternalId, + clientId: clientId, + createdAt: new Date(), + customerRefId: null, + destination: sendTransfer.destination, + externalId: anchorageTransfer.transferId, + externalStatus: anchorageTransfer.status, + grossAmount: sendTransfer.amount, + idempotenceId: sendTransfer.idempotenceId, + connectionId, + memo: sendTransfer.memo || null, + networkFeeAttribution, + provider: Provider.ANCHORAGE, + providerSpecific: sendTransfer.providerSpecific || null, + source: sendTransfer.source, + status: this.mapStatus(anchorageTransfer.status), + transferId: uuid() + } + + this.logger.log('Create internal transfer', { ...context, internalTransfer }) + + await this.transferRepository.bulkCreate([internalTransfer]) + + return internalTransfer + } + + /** + * Anchorage uses `deductFeeFromAmountIfSameType` that if set to true fees + * will be added to amount requested. + * + * Example: a request to transfer 5 BTC with + * `deductFeeFromAmountIfSameType=false` would result in 5 exactly BTC + * received to the destination and just over 5 BTC spent by the source. + * + * Note: Anchorage API defaults to `false`. + * + * @see https://docs.anchorage.com/reference/createtransfer + */ + private getDeductFeeFromAmountIfSameType(attribution: NetworkFeeAttribution): boolean { + if (attribution === NetworkFeeAttribution.DEDUCT) { + return true + } + + if (attribution === NetworkFeeAttribution.ON_TOP) { + return false + } + + return false + } + + private async getSource(scope: ConnectionScope, source: Source) { + if (source.type === TransferPartyType.WALLET) { + const wallet = await this.walletService.findById(scope, source.id) + + return { + type: transferPartyTypeToAnchorageResourceType(source.type), + id: wallet.externalId + } + } + + if (source.type === TransferPartyType.ACCOUNT) { + const wallet = await this.accountService.findById(scope, source.id) + + return { + type: transferPartyTypeToAnchorageResourceType(source.type), + id: wallet.externalId + } + } + + throw new BrokerException({ + message: 'Cannot resolve Anchorage transfer source', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { scope, source } + }) + } + + private async getDestination(scope: ConnectionScope, destination: Destination) { + if (isAddressDestination(destination)) { + // IMPORTANT: For both known and unknown addresses, we pass them directly + // to Anchorage without validating their existence on our side. If the + // provided address is neither an Anchorage Address nor a Trusted + // Address, we let Anchorage handle the failure. + return { + type: transferPartyTypeToAnchorageResourceType(TransferPartyType.ADDRESS), + id: destination.address + } + } + + if (destination.type === TransferPartyType.WALLET) { + const wallet = await this.walletService.findById(scope, destination.id) + + return { + type: transferPartyTypeToAnchorageResourceType(destination.type), + id: wallet.externalId + } + } + + if (destination.type === TransferPartyType.ACCOUNT) { + const account = await this.accountService.findById(scope, destination.id) + + return { + type: transferPartyTypeToAnchorageResourceType(destination.type), + id: account.externalId + } + } + + throw new BrokerException({ + message: 'Cannot resolve Anchorage transfer destination', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { scope, destination } + }) + } + + private async findTransferAsset( + connection: ValidConnection, + transferAsset: TransferAsset + ): Promise { + const findByExternalIdFallback = async (externalAssetId: string): Promise => { + const anchorageAssetTypes = await this.anchorageClient.getAssetTypes({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const anchorageAssetType = anchorageAssetTypes.find( + ({ assetType }) => assetType.toLowerCase() === externalAssetId.toLowerCase() + ) + if (!anchorageAssetType) { + throw new AssetException({ + message: 'Anchorage asset type not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { transferAsset } + }) + } + + const network = await this.networkService.findByExternalId(Provider.ANCHORAGE, anchorageAssetType.networkId) + if (!network) { + throw new AssetException({ + message: 'Anchorage asset type network not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { transferAsset, anchorageAssetType } + }) + } + + return { + network, + assetExternalId: externalAssetId, + assetId: null + } + } + + const findByOnchainIdFallback = async (network: Network, onchainId: string): Promise => { + const externalNetwork = getExternalNetwork(network, Provider.ANCHORAGE) + if (!externalNetwork) { + throw new AssetException({ + message: 'Network does not support Anchorage', + suggestedHttpStatusCode: HttpStatus.NOT_IMPLEMENTED, + context: { transferAsset, network } + }) + } + + const anchorageAssetTypes = await this.anchorageClient.getAssetTypes({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const anchorageAssetType = anchorageAssetTypes.find( + ({ onchainIdentifier, networkId }) => + onchainIdentifier?.toLowerCase() === onchainId.toLowerCase() && + networkId.toLowerCase() === externalNetwork.externalId.toLowerCase() + ) + if (!anchorageAssetType) { + throw new AssetException({ + message: 'Anchorage asset type not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { transferAsset } + }) + } + + return { + network, + assetId: null, + assetExternalId: anchorageAssetType.assetType + } + } + + return this.transferAssetService.resolve({ + findByExternalIdFallback, + findByOnchainIdFallback, + transferAsset, + provider: Provider.ANCHORAGE + }) + } +} diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage.type.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage.type.ts new file mode 100644 index 000000000..cc54ed4e6 --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage.type.ts @@ -0,0 +1,23 @@ +import { hexSchema } from '@narval/policy-engine-shared' +import { ed25519PrivateKeySchema, ed25519PublicKeySchema } from '@narval/signature' +import { z } from 'zod' + +export const AnchorageInputCredentials = z.object({ + apiKey: z.string(), + privateKey: hexSchema.optional().describe('Ed25519 private key in hex format') +}) +export type AnchorageInputCredentials = z.infer + +export const AnchorageCredentials = z.object({ + apiKey: z.string().optional(), + publicKey: ed25519PublicKeySchema, + privateKey: ed25519PrivateKeySchema +}) +export type AnchorageCredentials = z.infer + +export const AnchorageResourceType = { + VAULT: 'VAULT', + WALLET: 'WALLET', + ADDRESS: 'ADDRESS' +} as const +export type AnchorageResourceType = (typeof AnchorageResourceType)[keyof typeof AnchorageResourceType] diff --git a/apps/vault/src/broker/core/provider/anchorage/anchorage.util.ts b/apps/vault/src/broker/core/provider/anchorage/anchorage.util.ts new file mode 100644 index 000000000..cb0ead59f --- /dev/null +++ b/apps/vault/src/broker/core/provider/anchorage/anchorage.util.ts @@ -0,0 +1,79 @@ +import { Ed25519PrivateKey } from '@narval/signature' +import { HttpStatus } from '@nestjs/common' +import { BrokerException } from '../../exception/broker.exception' +import { ConnectionInvalidException } from '../../exception/connection-invalid.exception' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { Provider } from '../../type/provider.type' +import { TransferPartyType } from '../../type/transfer.type' +import { AnchorageCredentials, AnchorageResourceType } from './anchorage.type' + +export const CONCURRENT_ANCHORAGE_REQUESTS = 5 + +export type ValidConnection = { + url: string + credentials: { + apiKey: string + privateKey: Ed25519PrivateKey + } + clientId: string + connectionId: string +} + +export function validateConnection( + connection: ConnectionWithCredentials +): asserts connection is ConnectionWithCredentials & ValidConnection { + const context = { + clientId: connection.clientId, + connectionId: connection.connectionId, + provider: connection.provider, + status: connection.status, + url: connection.url + } + + if (connection.provider !== Provider.ANCHORAGE) { + throw new ConnectionInvalidException({ + message: 'Invalid connection provider for Anchorage', + context + }) + } + + if (!connection.url) { + throw new ConnectionInvalidException({ + message: 'Anchorage connection missing URL', + context + }) + } + + if (!connection.credentials) { + throw new ConnectionInvalidException({ + message: 'Anchorage connection missing credentials', + context + }) + } + + const credentials = AnchorageCredentials.parse(connection.credentials) + + if (!credentials.apiKey) { + throw new ConnectionInvalidException({ + message: 'Anchorage connection missing API key', + context + }) + } +} + +export const transferPartyTypeToAnchorageResourceType = (type: TransferPartyType): AnchorageResourceType => { + switch (type) { + case TransferPartyType.WALLET: + return AnchorageResourceType.VAULT + case TransferPartyType.ACCOUNT: + return AnchorageResourceType.WALLET + case TransferPartyType.ADDRESS: + return AnchorageResourceType.ADDRESS + default: + throw new BrokerException({ + message: 'Cannot map transfer party to Anchorage resource type', + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + context: { type } + }) + } +} diff --git a/apps/vault/src/broker/core/provider/bitgo/__test__/unit/bitgo-credential.service.spec.ts b/apps/vault/src/broker/core/provider/bitgo/__test__/unit/bitgo-credential.service.spec.ts new file mode 100644 index 000000000..7bd7e87ff --- /dev/null +++ b/apps/vault/src/broker/core/provider/bitgo/__test__/unit/bitgo-credential.service.spec.ts @@ -0,0 +1,81 @@ +import { Test } from '@nestjs/testing' +import '../../../../../shared/__test__/matcher' +import { ConnectionInvalidCredentialsException } from '../../../../exception/connection-invalid-credentials.exception' +import { BitgoCredentialService } from '../../bitgo-credential.service' +import { BitgoCredentials, BitgoInputCredentials } from '../../bitgo.type' + +describe(BitgoCredentialService.name, () => { + let service: BitgoCredentialService + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [BitgoCredentialService] + }).compile() + + service = module.get(BitgoCredentialService) + }) + + describe('parse', () => { + it('validates and parses raw credentials into typed BitgoCredentials', async () => { + const validCredentials: BitgoCredentials = { + apiKey: 'test-api-key', + walletPassphrase: 'test-wallet-passphrase' + } + + const result = service.parse(validCredentials) + expect(result).toStrictEqual(validCredentials) + }) + + it('handles everything being undefined', () => { + const invalidCredentials = {} + + expect(() => service.parse(invalidCredentials)).not.toThrow() + }) + }) + + describe('build', () => { + it('transforms input credentials into provider-ready format', async () => { + const input: BitgoInputCredentials = { + apiKey: 'test-api-key', + walletPassphrase: 'test-wallet-passphrase' + } + + const credentials = await service.build(input) + + expect(credentials).toEqual({ + apiKey: input.apiKey, + walletPassphrase: input.walletPassphrase + }) + }) + + it('accepts undefined walletPassphrase', async () => { + const input: BitgoInputCredentials = { + apiKey: 'test-api-key' + } + + const credentials = await service.build(input) + + expect(credentials).toEqual({ + apiKey: input.apiKey + }) + }) + + it('throws error when input api key is undefined', async () => { + const input = { + apiKey: undefined + } + + await expect(service.build(input as unknown as BitgoInputCredentials)).rejects.toThrow( + ConnectionInvalidCredentialsException + ) + }) + }) + + describe('generate', () => { + it('is a noop & returns empty object', async () => { + const credentials = await service.generate() + + expect(credentials).toEqual({}) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/bitgo/bitgo-credential.service.ts b/apps/vault/src/broker/core/provider/bitgo/bitgo-credential.service.ts new file mode 100644 index 000000000..9b8baf8a8 --- /dev/null +++ b/apps/vault/src/broker/core/provider/bitgo/bitgo-credential.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common' +import { ParseException } from '../../../../shared/module/persistence/exception/parse.exception' +import { ConnectionInvalidCredentialsException } from '../../exception/connection-invalid-credentials.exception' +import { ProviderCredentialService } from '../../type/provider.type' +import { BitgoCredentials, BitgoInputCredentials } from './bitgo.type' + +@Injectable() +export class BitgoCredentialService implements ProviderCredentialService { + parse(value: unknown): BitgoCredentials { + const parse = BitgoCredentials.safeParse(value) + + if (parse.success) { + return parse.data + } + + throw new ParseException(parse.error) + } + + parseInput(value: unknown): BitgoInputCredentials { + const parse = BitgoInputCredentials.safeParse(value) + + if (parse.success) { + return parse.data + } + + throw new ParseException(parse.error) + } + + async build(input: BitgoInputCredentials): Promise { + if (input.apiKey) { + return { + apiKey: input.apiKey, + walletPassphrase: input.walletPassphrase + } + } + + throw new ConnectionInvalidCredentialsException() + } + + async generate(): Promise { + // noop, you can't generate bitgo credentials + return {} + } +} diff --git a/apps/vault/src/broker/core/provider/bitgo/bitgo.type.ts b/apps/vault/src/broker/core/provider/bitgo/bitgo.type.ts new file mode 100644 index 000000000..6f8e0be7e --- /dev/null +++ b/apps/vault/src/broker/core/provider/bitgo/bitgo.type.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +export const BitgoInputCredentials = z.object({ + apiKey: z.string(), + walletPassphrase: z.string().optional() +}) +export type BitgoInputCredentials = z.infer + +export const BitgoCredentials = z.object({ + apiKey: z.string().optional(), + walletPassphrase: z.string().optional() +}) +export type BitgoCredentials = z.infer diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/integration/fireblocks-scoped-sync.service.spec.ts b/apps/vault/src/broker/core/provider/fireblocks/__test__/integration/fireblocks-scoped-sync.service.spec.ts new file mode 100644 index 000000000..ff3c03313 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/integration/fireblocks-scoped-sync.service.spec.ts @@ -0,0 +1,216 @@ +import { INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { v4 as uuid } from 'uuid' +import { VaultTest } from '../../../../../../__test__/shared/vault.test' +import { ClientService } from '../../../../../../client/core/service/client.service' +import { MainModule } from '../../../../../../main.module' +import { ProvisionService } from '../../../../../../provision.service' +import { TestPrismaService } from '../../../../../../shared/module/persistence/service/test-prisma.service' +import { testClient } from '../../../../../__test__/util/mock-data' +import { NetworkSeed } from '../../../../../persistence/seed/network.seed' +import { setupMockServer } from '../../../../../shared/__test__/mock-server' +import { ConnectionService } from '../../../../service/connection.service' +import { NetworkService } from '../../../../service/network.service' +import { ConnectionWithCredentials } from '../../../../type/connection.type' +import { Provider } from '../../../../type/provider.type' +import { RawAccountError } from '../../../../type/scoped-sync.type' +import { + FIREBLOCKS_TEST_API_BASE_URL, + getHandlers, + getVaultAccountHandlers +} from '../../../fireblocks/__test__/server-mock/server' +import { FireblocksScopedSyncService } from '../../fireblocks-scoped-sync.service' +import { buildFireblocksAssetWalletExternalId } from '../../fireblocks.util' + +describe(FireblocksScopedSyncService.name, () => { + let app: INestApplication + let module: TestingModule + + let fireblocksScopedSyncService: FireblocksScopedSyncService + let clientService: ClientService + let connection: ConnectionWithCredentials + let connectionService: ConnectionService + let networkSeed: NetworkSeed + let networkService: NetworkService + let provisionService: ProvisionService + let testPrismaService: TestPrismaService + + const privateKeyPem = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVORyUbWt/yuW4 +29mXtJBC3PEisQN3BQhtKp6jHa89YeWBct43g6tUTwcvdj7HXJOmp1FLbbPzE/pe +HMHtlx2ZTHIcz2nU4c4k6PiN//xGHze7aY8e3G+tVSJLQKRBFXiXWuUjDM5/OYWj +YwCjJptulOjzU6zwc/LW0KNBYQb4VuElJeV2mp17YOwUaLxyR+1I2+6aN5wCK8+M +VHHbo5y4nSNUClK0TdAWP3ZKu6DKZveKCmbY5X3V/mVS52LgvFlVHrSLMUQUkH1Y +ob4vBcw4QT7lAcyo4YKno1Q8mZKNivkWMpw+6R3c2hjPzHdiqYjPkPiMPDC6hJGi +7rh2iG3jAgMBAAECggEAB7wyKrpLd4/bRJkJLEU7JInSX6FPUF6I3zj4F0/I3y+x +fUA3ComGyiCx0Il4HpBftOCGBPf+WrejUg22BVIBm2GYFC58FuJ4MYOYHMKoGr0g +LvbV39c8X+viOhumucu3G7qK7HoW9auXCwXY7JJGej0BtG4ZLIHwUdWwznrgH1sq +ig/mdHy3SWVl0/c7PbLaD+IoHLqVmGgq4ylDdRyNfbvjLFzDptaois0kfZ0JMTF8 ++l0n30ib3Vmv1T/sZaLKj33LY7moQHk/FYp4uAP/5CAfSJQaUWQDDrc3WN7LwE0o +ALeIfAN2KltEcTouCDgBqTD5y72c6IsAIyDlkOxX/QKBgQDysS48botXSH3dF8/2 +gxRhVqYYGx2mU6pqQDOQk8z0Cg5qjs5H7U+xSX1vyaGNfPLKrMXLjJSDKtKT5TN1 +sHXHKjL88I5gGODf4vJncCD7m5wTt1JPzm4mIVZcLILmD3buAn8qMXz64i4tus1e +heVJZBUt360B/iEsJtlS7WsZLwKBgQDg6kGT/jBa6hP5JKVZWbyk5tOxCZsHSaNx +iXzSmKX6bld+b9qlxBsZGOs74lSeSaps8pLO0+Hy+WRvnJF0UNPSJpNT8ppHaHBn +KPWtX1yPj+JWqHLSx5JgsxCAiHSAHpRcR0Yh77WpAHCAstYm+pa2tCZ1sbzSOa3j +jz8L3FuhjQKBgBYTCZqTj3cD7/bROKg6afskj3z30m2ThJefeVE4MFcuJvuIO7kN +G8eLYK5vT5N3/vlyV5dZFRUNKxQqr9CPmVbhPrwFAV46RRH4KYZBC673C247qW/6 +3cf4FkvR/KICXBXwAjMLR0vmkL62FAH5+c4AHXELvEfHHqtOaUwCrlAfAoGBAJ9u +br3xWWWYuD/Lcko8CjT6SuUb4gDweiNpSkoeWsmCnhLKRztqH6tStqzkawcpQN2p +tddW6rvJfSCA47qH8R7uqVDAkAw+RC9cIYqcJoi9fbvf/ETdoy1YwUHbeHm5M4GW +JGi5+xOpdBZGrvdCesNYQEr9itOaf2DnkdFeirWhAoGAMlyskZWy2EW9T4tJgxVz +474FlWrtmVZNXMHO8pgd++JrR8Tgb1maoxENNsIxcWwMRgGLL0FA7sGf+wcxQjHT +l6Yb3VB/SnVMOOLTsE1SvCywjJ8vl8tEmbCoTFlDCJHh+IEB5a2NC4403tL7yPWS +iYDlTZ/pWsEotE2yCl/8krs= +-----END PRIVATE KEY-----` + + const privateKey = Buffer.from(privateKeyPem).toString('base64') + + const clientId = 'test-client-id' + + const mockServer = setupMockServer(getHandlers()) + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + testPrismaService = module.get(TestPrismaService) + fireblocksScopedSyncService = module.get(FireblocksScopedSyncService) + connectionService = module.get(ConnectionService) + networkService = module.get(NetworkService) + provisionService = module.get(ProvisionService) + networkSeed = module.get(NetworkSeed) + clientService = module.get(ClientService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + + await provisionService.provision() + await clientService.save(testClient) + + connection = await connectionService.create(clientId, { + connectionId: uuid(), + provider: Provider.FIREBLOCKS, + url: FIREBLOCKS_TEST_API_BASE_URL, + label: 'test active connection', + credentials: { + apiKey: 'test-api-key', + privateKey + } + }) + + await networkSeed.seed() + + await app.init() + }) + + describe('scoped-syncs', () => { + it('returns scoped sync operations for wallets, accounts and addresses based on an fireblocks wallet externalId', async () => { + const rawAccounts = [ + { + provider: Provider.FIREBLOCKS, + externalId: buildFireblocksAssetWalletExternalId({ vaultId: '3', networkId: 'ETH' }) + } + ] + + const networks = await networkService.buildProviderExternalIdIndex(Provider.FIREBLOCKS) + const sync = await fireblocksScopedSyncService.scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(1) + expect(sync.accounts.length).toBe(1) + expect(sync.addresses.length).toBe(1) + }) + + it('returns empty objects when no rawAccounts are provided', async () => { + const networks = await networkService.buildProviderExternalIdIndex(Provider.FIREBLOCKS) + + const sync = await fireblocksScopedSyncService.scopeSync({ + connection, + rawAccounts: [], + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(0) + expect(sync.accounts.length).toBe(0) + expect(sync.addresses.length).toBe(0) + }) + + it('adds failure when external resource is not found', async () => { + mockServer.use(getVaultAccountHandlers(FIREBLOCKS_TEST_API_BASE_URL).invalid) + const rawAccounts = [ + { + provider: Provider.FIREBLOCKS, + externalId: buildFireblocksAssetWalletExternalId({ vaultId: 'notfound', networkId: 'ETH' }) + } + ] + const networks = await networkService.buildProviderExternalIdIndex(Provider.FIREBLOCKS) + + const sync = await fireblocksScopedSyncService.scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(0) + expect(sync.accounts.length).toBe(0) + expect(sync.addresses.length).toBe(0) + expect(sync.failures).toEqual([ + { + rawAccount: rawAccounts[0], + message: 'Fireblocks Vault Account not found', + code: RawAccountError.EXTERNAL_RESOURCE_NOT_FOUND, + externalResourceType: 'vaultAccount', + externalResourceId: 'NOTFOUND' + } + ]) + }) + + it('adds failure when network is not found in our list', async () => { + const rawAccounts = [ + { + provider: Provider.FIREBLOCKS, + externalId: buildFireblocksAssetWalletExternalId({ vaultId: '3', networkId: 'notFound' }) + } + ] + const networks = await networkService.buildProviderExternalIdIndex(Provider.FIREBLOCKS) + + const sync = await fireblocksScopedSyncService.scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts: [] + }) + + expect(sync.wallets.length).toBe(0) + expect(sync.accounts.length).toBe(0) + expect(sync.addresses.length).toBe(0) + expect(sync.failures).toEqual([ + { + rawAccount: rawAccounts[0], + message: 'Network for this account is not supported', + code: RawAccountError.UNLISTED_NETWORK, + networkId: 'NOTFOUND' + } + ]) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/integration/fireblocks-transfer.service.spec.ts b/apps/vault/src/broker/core/provider/fireblocks/__test__/integration/fireblocks-transfer.service.spec.ts new file mode 100644 index 000000000..c7df2ef86 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/integration/fireblocks-transfer.service.spec.ts @@ -0,0 +1,466 @@ +import { RsaPrivateKey, getPublicKey } from '@narval/signature' +import { INestApplication } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing' +import { randomUUID } from 'crypto' +import { generatePrivateKey, privateKeyToAddress } from 'viem/accounts' +import { VaultTest } from '../../../../../../__test__/shared/vault.test' +import { ClientService } from '../../../../../../client/core/service/client.service' +import { MainModule } from '../../../../../../main.module' +import { ProvisionService } from '../../../../../../provision.service' +import { TestPrismaService } from '../../../../../../shared/module/persistence/service/test-prisma.service' +import { testClient } from '../../../../../__test__/util/mock-data' +import { AccountRepository } from '../../../../../persistence/repository/account.repository' +import { AddressRepository } from '../../../../../persistence/repository/address.repository' +import { ConnectionRepository } from '../../../../../persistence/repository/connection.repository' +import { TransferRepository } from '../../../../../persistence/repository/transfer.repository' +import { WalletRepository } from '../../../../../persistence/repository/wallet.repository' +import { AssetSeed } from '../../../../../persistence/seed/asset.seed' +import { NetworkSeed } from '../../../../../persistence/seed/network.seed' +import { setupMockServer, useRequestSpy } from '../../../../../shared/__test__/mock-server' +import { ConnectionStatus, ConnectionWithCredentials } from '../../../../type/connection.type' +import { Account, Address, Wallet } from '../../../../type/indexed-resources.type' +import { Provider } from '../../../../type/provider.type' +import { + InternalTransfer, + NetworkFeeAttribution, + TransferPartyType, + TransferStatus +} from '../../../../type/transfer.type' +import { FireblocksTransferService } from '../../fireblocks-transfer.service' +import { FIREBLOCKS_TEST_API_BASE_URL, getHandlers } from '../server-mock/server' + +describe(FireblocksTransferService.name, () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + + let accountRepository: AccountRepository + let addressRepository: AddressRepository + let assetSeed: AssetSeed + let clientService: ClientService + let connectionRepository: ConnectionRepository + let fireblocksTransferService: FireblocksTransferService + let networkSeed: NetworkSeed + let provisionService: ProvisionService + let transferRepository: TransferRepository + let walletRepository: WalletRepository + + const mockServer = setupMockServer(getHandlers()) + + const clientId = randomUUID() + + const walletOneId = randomUUID() + + const walletTwoId = randomUUID() + + const rsaPrivate: RsaPrivateKey = { + kty: 'RSA', + alg: 'RS256', + kid: '0x52920ad0d19d7779106bd9d9d600d26c4b976cdb3cbc49decb7fdc29db00b8e9', + n: 'xNdTjWL9hGa4bz4tLKbmFZ4yjQsQzW35-CMS0kno3403jEqg5y2Cs6sLVyPBX4N2hdK5ERPytpf1PrThHqB-eEO6LtEWpENBgFuNIf8DRHrv0tne7dLNxf7sx1aocGRrkgIk4Ws6Is4Ot3whm3-WihmDGnHoogE-EPwVkkSc2FYPXYlNq4htCZXC8_MUI3LuXry2Gn4tna5HsYSehYhfKDD-nfSajeWxdNUv_3wOeSCr9ICm9Udlo7hpIUHQgnX3Nz6kvfGYuweLGoj_ot-oEUCIdlbQqmrfStAclugbM5NI6tY__6wD0z_4ZBjToupXCBlXbYsde6_ZG9xPmYSykw', + e: 'AQAB', + d: 'QU4rIzpXX8jwob-gHzNUHJH6tX6ZWX6GM0P3p5rrztc8Oag8z9XyigdSYNu0-SpVdTqfOcJDgT7TF7XNBms66k2WBJhMCb1iiuJU5ZWEkQC0dmDgLEkHCgx0pAHlKjy2z580ezEm_YsdqNRfFgbze-fQ7kIiazU8UUhBI-DtpHv7baBgsfqEfQ5nCTiURUPmmpiIU74-ZIJWZjBXTOoJNH0EIsJK9IpZzxpeC9mTMTsWTcHKiR3acze1qf-9I97v461TTZ8e33N6YINyr9I4HZuvxlCJdV_lOM3fLvYM9gPvgkPozhVWL3VKR6xa9JpGGHrCRgH92INuviBB_SmF8Q', + p: '9BNku_-t4Df9Dg7M2yjiNgZgcTNKrDnNqexliIUAt67q0tGmSBubjxeI5unDJZ_giXWUR3q-02v7HT5GYx-ZVgKk2lWnbrrm_F7UZW-ueHzeVvQcjDXTk0z8taXzrDJgnIwZIaZ2XSG3P-VPOrXCaMba8GzSq38Gpzi4g3lTO9s', + q: 'znUtwrqdnVew14_aFjNTRgzOQNN8JhkjzJy3aTSLBScK5NbiuUUZBWs5dQ7Nv7aAoDss1-o9XVQZ1DVV-o9UufJtyrPNcvTnC0cWRrtJrSN5YiuUbECU3Uj3OvGxnhx9tsmhDHnMTo50ObPYUbHcIkNaXkf2FVgL84y1JRWdPak', + dp: 'UNDrFeS-6fMf8zurURXkcQcDf_f_za8GDjGcHOwNJMTiNBP-_vlFNMgSKINWfmrFqj4obtKRxOeIKlKoc8HOv8_4TeL2oY95VC8CHOQx3Otbo2cI3NQlziw7sNnWKTo1CyDIYYAAyS2Uw69l4Ia2bIMLk3g0-VwCE_SQA9h0Wuk', + dq: 'VBe6ieSFKn97UnIPfJdvRcsVf6YknUgEIuV6d2mlbnXWpBs6wgf5BxIDl0BuYbYuchVoUJHiaM9Grf8DhEk5U3wBaF0QQ9CpAxjzY-AJRHJ8kJX7oJQ1jmSX_vRPSn2EXx2FcZVyuFSh1pcAd1YgufwBJQHepBb21z7q0a4aG_E', + qi: 'KhZpFs6xfyRIjbJV8Q9gWxqF37ONayIzBpgio5mdAQlZ-FUmaWZ2_2VWP2xvsP48BmwFXydHqewHBqGnZYCQ1ZHXJgD_-KKEejoqS5AJN1pdI0ZKjs7UCfZ4RJ4DH5p0_35gpuKRzzdvcIhl1CjIC5W8o7nhwmLBJ_QAo9e4t9U' + } + + const testApiKey = 'test-api-key' + + const connection: ConnectionWithCredentials = { + clientId, + connectionId: randomUUID(), + createdAt: new Date(), + provider: Provider.FIREBLOCKS, + status: ConnectionStatus.ACTIVE, + updatedAt: new Date(), + url: FIREBLOCKS_TEST_API_BASE_URL, + credentials: { + privateKey: rsaPrivate, + publicKey: getPublicKey(rsaPrivate), + apiKey: testApiKey + } + } + + const accountOne: Account = { + accountId: randomUUID(), + addresses: [], + clientId, + createdAt: new Date(), + externalId: randomUUID(), + connectionId: connection.connectionId, + label: 'Account 1', + networkId: 'POLYGON', + provider: Provider.FIREBLOCKS, + updatedAt: new Date(), + walletId: walletOneId + } + + const accountTwo: Account = { + accountId: randomUUID(), + addresses: [], + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: randomUUID(), + label: 'Account 2', + networkId: 'POLYGON', + provider: Provider.FIREBLOCKS, + updatedAt: new Date(), + walletId: walletTwoId + } + + const walletOne: Wallet = { + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: randomUUID(), + label: null, + provider: Provider.FIREBLOCKS, + updatedAt: new Date(), + walletId: walletOneId + } + + const walletTwo: Wallet = { + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: randomUUID(), + label: null, + provider: Provider.FIREBLOCKS, + updatedAt: new Date(), + walletId: walletTwoId + } + + const address: Address = { + accountId: accountTwo.accountId, + address: '0x2c4895215973cbbd778c32c456c074b99daf8bf1', + addressId: randomUUID(), + clientId, + connectionId: connection.connectionId, + createdAt: new Date(), + externalId: randomUUID(), + provider: Provider.FIREBLOCKS, + updatedAt: new Date() + } + + const internalTransfer: InternalTransfer = { + clientId, + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + customerRefId: null, + idempotenceId: null, + externalId: randomUUID(), + connectionId: connection.connectionId, + externalStatus: null, + assetId: 'MATIC', + assetExternalId: null, + memo: 'Test transfer', + grossAmount: '0.00001', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + provider: Provider.FIREBLOCKS, + transferId: randomUUID(), + createdAt: new Date() + } + + beforeAll(async () => { + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() + + app = module.createNestApplication() + + accountRepository = module.get(AccountRepository) + addressRepository = module.get(AddressRepository) + assetSeed = module.get(AssetSeed) + clientService = module.get(ClientService) + connectionRepository = module.get(ConnectionRepository) + fireblocksTransferService = module.get(FireblocksTransferService) + networkSeed = module.get(NetworkSeed) + provisionService = module.get(ProvisionService) + testPrismaService = module.get(TestPrismaService) + transferRepository = module.get(TransferRepository) + walletRepository = module.get(WalletRepository) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + + await provisionService.provision() + await clientService.save(testClient) + await networkSeed.seed() + await assetSeed.seed() + + await connectionRepository.create(connection) + await walletRepository.bulkCreate([walletOne, walletTwo]) + await accountRepository.bulkCreate([accountOne, accountTwo]) + await addressRepository.bulkCreate([address]) + await transferRepository.bulkCreate([internalTransfer]) + + await app.init() + }) + + describe('findById', () => { + it('maps data from internal transfer', async () => { + const transfer = await fireblocksTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer).toMatchObject({ + assetId: internalTransfer.assetId, + clientId: internalTransfer.clientId, + customerRefId: internalTransfer.customerRefId, + destination: internalTransfer.destination, + externalId: internalTransfer.externalId, + idempotenceId: internalTransfer.idempotenceId, + networkFeeAttribution: internalTransfer.networkFeeAttribution, + provider: internalTransfer.provider, + source: internalTransfer.source, + transferId: internalTransfer.transferId + }) + }) + + it('maps gross amount from inbound transaction', async () => { + const transfer = await fireblocksTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.grossAmount).toEqual('0.001') + }) + + it('maps status from inbound transaction', async () => { + const transfer = await fireblocksTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.status).toEqual(TransferStatus.SUCCESS) + }) + + it('maps memo from internal transfer when inbound note is undefined', async () => { + const transfer = await fireblocksTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.memo).toEqual(internalTransfer.memo) + }) + + it('maps network fee from inbound transaction ', async () => { + const transfer = await fireblocksTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.fees.find(({ type }) => type === 'network')).toEqual({ + amount: '0.002354982918981', + assetId: 'MATIC_POLYGON', + attribution: internalTransfer.networkFeeAttribution, + type: 'network' + }) + }) + + it('maps gas price as a fee from inbound transaction', async () => { + const transfer = await fireblocksTransferService.findById(connection, internalTransfer.transferId) + + expect(transfer.fees.find(({ type }) => type === 'gas-price')).toEqual({ + amount: '112.14204376100001', + assetId: 'MATIC_POLYGON', + type: 'gas-price' + }) + }) + }) + + describe('send', () => { + const requiredSendTransfer = { + source: { + type: TransferPartyType.ACCOUNT, + id: accountOne.accountId + }, + destination: { + type: TransferPartyType.ACCOUNT, + id: accountTwo.accountId + }, + amount: '0.001', + asset: { + externalAssetId: 'MATIC_POLYGON' + }, + idempotenceId: randomUUID() + } + + it('creates an internal transfer on success', async () => { + const { externalStatus, ...transfer } = await fireblocksTransferService.send(connection, requiredSendTransfer) + const actualInternalTransfer = await transferRepository.findById(clientId, transfer.transferId) + + expect(actualInternalTransfer).toMatchObject(transfer) + expect(externalStatus).toEqual(expect.any(String)) + }) + + it('creates with optional properties', async () => { + const sendTransfer = { + ...requiredSendTransfer, + memo: 'Integration test transfer', + customerRefId: 'test-customer-ref-id', + networkFeeAttribution: NetworkFeeAttribution.DEDUCT, + idempotenceId: randomUUID() + } + + const internalTransfer = await fireblocksTransferService.send(connection, sendTransfer) + const actualInternalTransfer = await transferRepository.findById(clientId, internalTransfer.transferId) + + expect(actualInternalTransfer.idempotenceId).toEqual(sendTransfer.idempotenceId) + expect(actualInternalTransfer.memo).toEqual(sendTransfer.memo) + expect(actualInternalTransfer.customerRefId).toEqual(sendTransfer.customerRefId) + expect(actualInternalTransfer.networkFeeAttribution).toEqual(sendTransfer.networkFeeAttribution) + }) + + it('defaults network fee attribution to deduct', async () => { + const internalTransfer = await fireblocksTransferService.send(connection, requiredSendTransfer) + + expect(internalTransfer.networkFeeAttribution).toEqual(NetworkFeeAttribution.DEDUCT) + }) + + it('calls Fireblocks', async () => { + const [spy] = useRequestSpy(mockServer) + const sendTransfer = { + ...requiredSendTransfer, + memo: 'Integration test transfer', + transferId: 'test-transfer-id' + } + + await fireblocksTransferService.send(connection, sendTransfer) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + amount: '0.001', + assetId: 'MATIC_POLYGON', + source: { + type: 'VAULT_ACCOUNT', + id: walletOne.externalId + }, + destination: { + type: 'VAULT_ACCOUNT', + id: walletTwo.externalId + }, + note: 'Integration test transfer', + externalTxId: sendTransfer.transferId, + treatAsGrossAmount: true + }, + headers: expect.objectContaining({ + 'x-api-key': testApiKey, + 'idempotency-key': requiredSendTransfer.idempotenceId + }) + }) + ) + }) + + it('calls Fireblocks with vault account as destination for internal address', async () => { + const [spy] = useRequestSpy(mockServer) + const sendTransfer = { + ...requiredSendTransfer, + destination: { address: address.address }, + transferId: 'test-transfer-id' + } + + await fireblocksTransferService.send(connection, sendTransfer) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + source: { + type: 'VAULT_ACCOUNT', + id: walletOne.externalId + }, + destination: { + type: 'VAULT_ACCOUNT', + id: walletTwo.externalId + } + }) + }) + ) + }) + + it('calls Fireblocks with one-time address', async () => { + const [spy] = useRequestSpy(mockServer) + const address = privateKeyToAddress(generatePrivateKey()) + const sendTransfer = { + ...requiredSendTransfer, + destination: { address }, + transferId: 'test-transfer-id' + } + + await fireblocksTransferService.send(connection, sendTransfer) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + source: { + type: 'VAULT_ACCOUNT', + id: walletOne.externalId + }, + destination: { + type: 'ONE_TIME_ADDRESS', + oneTimeAddress: { address } + } + }) + }) + ) + }) + + it('handles provider specific', async () => { + const [spy] = useRequestSpy(mockServer) + const providerSpecific = { + extraParameters: { + nodeControls: { + type: 'NODE_ROUTER' + }, + rawMessageData: { + messages: [ + { + preHash: { + hashAlgorithm: 'SHA256' + } + } + ] + } + } + } + const sendTransfer = { + ...requiredSendTransfer, + providerSpecific + } + + const internalTransfer = await fireblocksTransferService.send(connection, sendTransfer) + const actualInternalTransfer = await transferRepository.findById(clientId, internalTransfer.transferId) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ ...sendTransfer.providerSpecific }) + }) + ) + + expect(actualInternalTransfer.providerSpecific).toEqual(sendTransfer.providerSpecific) + }) + + it('maps networkFeeAttribution on_top to treatAsGrossAmount false', async () => { + const [spy] = useRequestSpy(mockServer) + + await fireblocksTransferService.send(connection, { + ...requiredSendTransfer, + networkFeeAttribution: NetworkFeeAttribution.ON_TOP + }) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + treatAsGrossAmount: false + }) + }) + ) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-asset-wallets-200.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-asset-wallets-200.json new file mode 100644 index 000000000..464701eac --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-asset-wallets-200.json @@ -0,0 +1,31 @@ +{ + "assetWallets": [ + { + "vaultId": "3483", + "assetId": "MATIC_POLYGON", + "total": "1065.275813068575650415", + "available": "1065.275813068575650415", + "pending": "0", + "staked": "0", + "frozen": "0", + "lockedAmount": "0", + "blockHeight": "65405786", + "blockHash": "0xa25d8a09a0fb05b3a417a8668b6044ab3d87d4e359aee1c339ad5763c163dea3", + "creationTime": "1716233725000" + }, + { + "vaultId": "3483", + "assetId": "ETH-OPT", + "total": "0.001942589115042143", + "available": "0.001942589115042143", + "pending": "0", + "staked": "0", + "frozen": "0", + "lockedAmount": "0", + "blockHeight": "124638003", + "blockHash": "0x6cdcecb4c1190ad0ffbc6ba51d4c9115caf1622fd1b32685165fd3abe247de3e", + "creationTime": "1723119346000" + } + ], + "paging": {} +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-transaction-200.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-transaction-200.json new file mode 100644 index 000000000..d7e8496e8 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-transaction-200.json @@ -0,0 +1,58 @@ +{ + "id": "7ce5e75a-39cb-4d31-8a10-79d69c6f8e0e", + "assetId": "MATIC_POLYGON", + "source": { + "id": "4", + "type": "VAULT_ACCOUNT", + "name": "dev-2", + "subType": "" + }, + "destination": { + "id": "3", + "type": "VAULT_ACCOUNT", + "name": "dev-1", + "subType": "" + }, + "requestedAmount": 0.001, + "amount": 0.001, + "netAmount": 0.001, + "amountUSD": 0.00042585, + "fee": 0.002354982918981, + "networkFee": 0.002354982918981, + "createdAt": 1736775806138, + "lastUpdated": 1736775849692, + "status": "COMPLETED", + "txHash": "0xdd6bba7c8897d64e4dd21e4b395aa45ced7b9eb4b1cd356ad9ffbfa113a3fec1", + "subStatus": "CONFIRMED", + "sourceAddress": "0xe621c7D0D6711742B6B4aEE4cb30048d1Ef1d92e", + "destinationAddress": "0x6b1c79dBF7a7D90d6F4fD623393aFa3DfFd6bE4E", + "destinationAddressDescription": "", + "destinationTag": "", + "signedBy": ["da1e58d6-771c-1584-4eba-8b5ae8761eea"], + "createdBy": "ee425cf5-ca75-4537-b2ce-e1ace302e0a1", + "rejectedBy": "", + "addressType": "", + "note": "", + "exchangeTxId": "", + "feeCurrency": "MATIC_POLYGON", + "operation": "TRANSFER", + "numOfConfirmations": 1, + "amountInfo": { + "amount": "0.001", + "requestedAmount": "0.001", + "netAmount": "0.001", + "amountUSD": "0.00042585" + }, + "feeInfo": { + "networkFee": "0.002354982918981", + "gasPrice": "112.14204376100001" + }, + "signedMessages": [], + "destinations": [], + "blockInfo": { + "blockHeight": "66650299", + "blockHash": "0x62c064832c4a2ff9cc1e00c236427015cda4f639fd03ed62b0e7374bf652807a" + }, + "index": 0, + "assetType": "BASE_ASSET" +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-account-3-200.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-account-3-200.json new file mode 100644 index 000000000..82ad474b6 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-account-3-200.json @@ -0,0 +1,405 @@ +{ + "id": "3", + "name": "dev-1", + "hiddenOnUI": false, + "customerRefId": "dev-guild-id-1", + "autoFuel": false, + "assets": [ + { + "id": "MATIC_POLYGON", + "total": "0.092436133514242454", + "balance": "0.092436133514242454", + "lockedAmount": "0", + "available": "0.092436133514242454", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "66650299", + "blockHash": "0x62c064832c4a2ff9cc1e00c236427015cda4f639fd03ed62b0e7374bf652807a" + }, + { + "id": "RBW_POLYGON_UZYB", + "total": "1.4", + "balance": "1.4", + "lockedAmount": "0", + "available": "1.4", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "48669610", + "blockHash": "0x9788f6b916527311e53c169f4ef0fafca6d10626862ee473a72144837ed0edc1" + }, + { + "id": "ETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "-1" + }, + { + "id": "ETH-AETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "117377373" + }, + { + "id": "AVAX", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "33392936" + }, + { + "id": "BNB_BSC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "30498105" + }, + { + "id": "ETH-OPT", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "107685647" + }, + { + "id": "SGB", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "40869531" + }, + { + "id": "RBTC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5656136" + }, + { + "id": "FTM_FANTOM", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "68378879" + }, + { + "id": "GLMR_GLMR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "4476463" + }, + { + "id": "MOVR_MOVR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5151851" + }, + { + "id": "BASECHAIN_ETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "4216404" + }, + { + "id": "CELO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "23357363", + "blockHash": "0x69338c41b8c8a0d10e23356c1463065ebce05f6db554ffcb387eef4a666eca32" + }, + { + "id": "SMARTBCH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "15805300" + }, + { + "id": "ASIANXT_BESU", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "OAS", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "3669391" + }, + { + "id": "LINEA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "6079647" + }, + { + "id": "SMR_SMR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5106465" + }, + { + "id": "ETH_ZKSYNC_ERA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "37704404" + }, + { + "id": "FLR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "26085228" + }, + { + "id": "EVMOS", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "21794920" + }, + { + "id": "RON", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "35909496" + }, + { + "id": "SX_NETWORK", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "KAVA_KAVA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "10496720" + }, + { + "id": "AURORA_DEV", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "119522841" + }, + { + "id": "CANTO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "10283806" + }, + { + "id": "ASTR_ASTR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "6486323" + }, + { + "id": "WEMIX", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "53327498" + }, + { + "id": "ETH_ZKEVM", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "13758502" + }, + { + "id": "XDAI", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "BLAST", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5376321" + }, + { + "id": "SEI", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "85852495" + }, + { + "id": "CORE_COREDAO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "15420531" + }, + { + "id": "BTC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "872488" + }, + { + "id": "USDC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "21294609" + } + ] +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-account-invalid.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-account-invalid.json new file mode 100644 index 000000000..141a18b01 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-account-invalid.json @@ -0,0 +1,4 @@ +{ + "message": "The Provided Vault Account ID is invalid: 11469", + "code": 11001 +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-accounts-200.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-accounts-200.json new file mode 100644 index 000000000..e948cbda5 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-vault-accounts-200.json @@ -0,0 +1,815 @@ +{ + "accounts": [ + { + "id": "3", + "name": "dev-1", + "hiddenOnUI": false, + "customerRefId": "dev-guild-id-1", + "autoFuel": false, + "assets": [ + { + "id": "MATIC_POLYGON", + "total": "0.092436133514242454", + "balance": "0.092436133514242454", + "lockedAmount": "0", + "available": "0.092436133514242454", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "66650299", + "blockHash": "0x62c064832c4a2ff9cc1e00c236427015cda4f639fd03ed62b0e7374bf652807a" + }, + { + "id": "RBW_POLYGON_UZYB", + "total": "1.4", + "balance": "1.4", + "lockedAmount": "0", + "available": "1.4", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "48669610", + "blockHash": "0x9788f6b916527311e53c169f4ef0fafca6d10626862ee473a72144837ed0edc1" + }, + { + "id": "ETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "-1" + }, + { + "id": "ETH-AETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "117377373" + }, + { + "id": "AVAX", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "33392936" + }, + { + "id": "BNB_BSC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "30498105" + }, + { + "id": "ETH-OPT", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "107685647" + }, + { + "id": "SGB", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "40869531" + }, + { + "id": "RBTC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5656136" + }, + { + "id": "FTM_FANTOM", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "68378879" + }, + { + "id": "GLMR_GLMR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "4476463" + }, + { + "id": "MOVR_MOVR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5151851" + }, + { + "id": "BASECHAIN_ETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "4216404" + }, + { + "id": "CELO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "23357363", + "blockHash": "0x69338c41b8c8a0d10e23356c1463065ebce05f6db554ffcb387eef4a666eca32" + }, + { + "id": "SMARTBCH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "15805300" + }, + { + "id": "ASIANXT_BESU", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "OAS", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "3669391" + }, + { + "id": "LINEA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "6079647" + }, + { + "id": "SMR_SMR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5106465" + }, + { + "id": "ETH_ZKSYNC_ERA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "37704404" + }, + { + "id": "FLR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "26085228" + }, + { + "id": "EVMOS", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "21794920" + }, + { + "id": "RON", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "35909496" + }, + { + "id": "SX_NETWORK", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "KAVA_KAVA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "10496720" + }, + { + "id": "AURORA_DEV", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "119522841" + }, + { + "id": "CANTO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "10283806" + }, + { + "id": "ASTR_ASTR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "6486323" + }, + { + "id": "WEMIX", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "53327498" + }, + { + "id": "ETH_ZKEVM", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "13758502" + }, + { + "id": "XDAI", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "BLAST", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5376321" + }, + { + "id": "SEI", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "85852495" + }, + { + "id": "CORE_COREDAO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "15420531" + }, + { + "id": "BTC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "872488" + }, + { + "id": "USDC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "21294609" + } + ] + }, + { + "id": "3", + "name": "dev-1", + "hiddenOnUI": false, + "customerRefId": "dev-guild-id-1", + "autoFuel": false, + "assets": [ + { + "id": "MATIC_POLYGON", + "total": "0.092436133514242454", + "balance": "0.092436133514242454", + "lockedAmount": "0", + "available": "0.092436133514242454", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "66650299", + "blockHash": "0x62c064832c4a2ff9cc1e00c236427015cda4f639fd03ed62b0e7374bf652807a" + }, + { + "id": "RBW_POLYGON_UZYB", + "total": "1.4", + "balance": "1.4", + "lockedAmount": "0", + "available": "1.4", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "48669610", + "blockHash": "0x9788f6b916527311e53c169f4ef0fafca6d10626862ee473a72144837ed0edc1" + }, + { + "id": "ETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "-1" + }, + { + "id": "ETH-AETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "117377373" + }, + { + "id": "AVAX", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "33392936" + }, + { + "id": "BNB_BSC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "30498105" + }, + { + "id": "ETH-OPT", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "107685647" + }, + { + "id": "SGB", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "40869531" + }, + { + "id": "RBTC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5656136" + }, + { + "id": "FTM_FANTOM", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "68378879" + }, + { + "id": "GLMR_GLMR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "4476463" + }, + { + "id": "MOVR_MOVR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5151851" + }, + { + "id": "BASECHAIN_ETH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "4216404" + }, + { + "id": "CELO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "23357363", + "blockHash": "0x69338c41b8c8a0d10e23356c1463065ebce05f6db554ffcb387eef4a666eca32" + }, + { + "id": "SMARTBCH", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "15805300" + }, + { + "id": "ASIANXT_BESU", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "OAS", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "3669391" + }, + { + "id": "LINEA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "6079647" + }, + { + "id": "SMR_SMR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5106465" + }, + { + "id": "ETH_ZKSYNC_ERA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "37704404" + }, + { + "id": "FLR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "26085228" + }, + { + "id": "EVMOS", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "21794920" + }, + { + "id": "RON", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "35909496" + }, + { + "id": "SX_NETWORK", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "KAVA_KAVA", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "10496720" + }, + { + "id": "AURORA_DEV", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "119522841" + }, + { + "id": "CANTO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "10283806" + }, + { + "id": "ASTR_ASTR", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "6486323" + }, + { + "id": "WEMIX", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "53327498" + }, + { + "id": "ETH_ZKEVM", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "13758502" + }, + { + "id": "XDAI", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0" + }, + { + "id": "BLAST", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "5376321" + }, + { + "id": "SEI", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "85852495" + }, + { + "id": "CORE_COREDAO", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "15420531" + }, + { + "id": "BTC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "872488" + }, + { + "id": "USDC", + "total": "0", + "balance": "0", + "lockedAmount": "0", + "available": "0", + "pending": "0", + "frozen": "0", + "staked": "0", + "blockHeight": "21294609" + } + ] + } + ], + "paging": {} +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-wallet-addresses-ethereum-200.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-wallet-addresses-ethereum-200.json new file mode 100644 index 000000000..e38ab060f --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-wallet-addresses-ethereum-200.json @@ -0,0 +1,16 @@ +{ + "addresses": [ + { + "assetId": "ETH", + "address": "0x5748EA5ca075734bc7ADc9D8046e897c24829A80", + "description": "", + "tag": "", + "type": "Permanent", + "legacyAddress": "", + "enterpriseAddress": "", + "bip44AddressIndex": 0, + "userDefined": false + } + ], + "paging": {} +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-wallet-addresses-matic-200.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-wallet-addresses-matic-200.json new file mode 100644 index 000000000..89e7e557d --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/get-wallet-addresses-matic-200.json @@ -0,0 +1,15 @@ +{ + "addresses": [ + { + "assetId": "MATIC_POLYGON", + "address": "0x6b1c79dBF7a7D90d6F4fD623393aFa3DfFd6bE4E", + "description": "", + "tag": "", + "type": "Permanent", + "legacyAddress": "", + "enterpriseAddress": "", + "bip44AddressIndex": 0, + "userDefined": false + } + ] +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/post-transaction-201.json b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/post-transaction-201.json new file mode 100644 index 000000000..deab3b11f --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/response/post-transaction-201.json @@ -0,0 +1,4 @@ +{ + "id": "7ce5e75a-39cb-4d31-8a10-79d69c6f8e0e", + "status": "SUBMITTED" +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/server.ts b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/server.ts new file mode 100644 index 000000000..46ecad105 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/server-mock/server.ts @@ -0,0 +1,53 @@ +import { HttpStatus } from '@nestjs/common' +import { HttpResponse, http } from 'msw' +import getTransactionOk from './response/get-transaction-200.json' +import vaultAccount3 from './response/get-vault-account-3-200.json' +import vaultAccountNotFound from './response/get-vault-account-invalid.json' +import getWalletAddressesEthOk from './response/get-wallet-addresses-ethereum-200.json' +import getWalletAddressesMaticOk from './response/get-wallet-addresses-matic-200.json' +import postTransactionCreated from './response/post-transaction-201.json' + +export const FIREBLOCKS_TEST_API_BASE_URL = 'https://test-mock-api.fireblocks.com' + +export const getVaultAccount3AddressesHandlers = (baseUrl = FIREBLOCKS_TEST_API_BASE_URL) => { + return { + getMatic: http.get( + `${baseUrl}/v1/vault/accounts/:vaultAccountId/:assetId/addresses_paginated`, + () => new HttpResponse(JSON.stringify(getWalletAddressesMaticOk)) + ), + getEth: http.get( + `${baseUrl}/v1/vault/accounts/:vaultAccountId/:assetId/addresses_paginated`, + () => new HttpResponse(JSON.stringify(getWalletAddressesEthOk)) + ) + } +} + +export const getVaultAccountHandlers = (baseUrl = FIREBLOCKS_TEST_API_BASE_URL) => { + return { + get: http.get(`${baseUrl}/v1/vault/accounts/:vaultAccountId`, () => { + return new HttpResponse(JSON.stringify(vaultAccount3)) + }), + invalid: http.get( + `${baseUrl}/v1/vault/accounts/:vaultAccountId`, + () => + new HttpResponse(JSON.stringify(vaultAccountNotFound), { + status: HttpStatus.BAD_REQUEST + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as unknown as any) + ) + } +} + +export const getHandlers = (baseUrl = FIREBLOCKS_TEST_API_BASE_URL) => [ + http.get(`${baseUrl}/v1/transactions/:txId`, () => { + return new HttpResponse(JSON.stringify(getTransactionOk)) + }), + + http.post(`${baseUrl}/v1/transactions`, () => { + return new HttpResponse(JSON.stringify(postTransactionCreated)) + }), + + getVaultAccountHandlers(baseUrl).get, + + getVaultAccount3AddressesHandlers(baseUrl).getEth +] diff --git a/apps/vault/src/broker/core/provider/fireblocks/__test__/unit/fireblocks-credential.service.spec.ts b/apps/vault/src/broker/core/provider/fireblocks/__test__/unit/fireblocks-credential.service.spec.ts new file mode 100644 index 000000000..a92fff3ff --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/__test__/unit/fireblocks-credential.service.spec.ts @@ -0,0 +1,100 @@ +import { + Alg, + SMALLEST_RSA_MODULUS_LENGTH, + generateJwk, + getPublicKey, + privateKeyToPem, + rsaPrivateKeySchema, + rsaPublicKeySchema +} from '@narval/signature' +import { Test } from '@nestjs/testing' +import { ParseException } from '../../../../../../shared/module/persistence/exception/parse.exception' +import '../../../../../shared/__test__/matcher' +import { ConnectionInvalidPrivateKeyException } from '../../../../exception/connection-invalid-private-key.exception' +import { FireblocksCredentialService } from '../../fireblocks-credential.service' +import { FireblocksCredentials, FireblocksInputCredentials } from '../../fireblocks.type' + +describe('FireblocksCredentialService', () => { + let service: FireblocksCredentialService + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [FireblocksCredentialService] + }).compile() + + service = module.get(FireblocksCredentialService) + }) + + describe('parse', () => { + it('validates and parses raw credentials into typed FireblocksCredentials', async () => { + const privateKey = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) + const validCredentials: FireblocksCredentials = { + privateKey, + publicKey: getPublicKey(privateKey), + apiKey: 'test-api-key' + } + + const result = service.parse(validCredentials) + + expect(result).toStrictEqual(validCredentials) + }) + + it('throws error when parsing invalid credentials format', () => { + const invalidCredentials = { + apiKey: 'test-api-key', + // Missing required RSA key properties + publicKey: { kty: 'RSA' }, + privateKey: { kty: 'RSA' } + } + + expect(() => service.parse(invalidCredentials)).toThrow(ParseException) + }) + }) + + describe('build', () => { + it('transforms input credentials into provider-ready format', async () => { + const rsaPrivateKey = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) + const pem = await privateKeyToPem(rsaPrivateKey, Alg.RS256) + const input: FireblocksInputCredentials = { + apiKey: 'test-api-key', + privateKey: Buffer.from(pem).toString('base64') + } + + const credentials = await service.build(input) + + expect(credentials).toEqual({ + apiKey: input.apiKey, + privateKey: rsaPrivateKey, + publicKey: getPublicKey(rsaPrivateKey) + }) + }) + + it('throws error when input private key is invalid', async () => { + const invalidPrivateKey = 'invalid-base64-string' + const input: FireblocksInputCredentials = { + apiKey: 'test-api-key', + privateKey: invalidPrivateKey + } + + await expect(service.build(input)).rejects.toThrow(ConnectionInvalidPrivateKeyException) + }) + + it('throws error when input private key is undefined', async () => { + const input: FireblocksInputCredentials = { + apiKey: 'test-api-key', + privateKey: undefined + } + + await expect(service.build(input)).rejects.toThrow(ConnectionInvalidPrivateKeyException) + }) + }) + + describe('generate', () => { + it('creates new RSA key pair with api credentials', async () => { + const credentials = await service.generate({ modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) + + expect(credentials.publicKey).toMatchZodSchema(rsaPublicKeySchema) + expect(credentials.privateKey).toMatchZodSchema(rsaPrivateKeySchema) + }) + }) +}) diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks-credential.service.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-credential.service.ts new file mode 100644 index 000000000..7a6fe43f7 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-credential.service.ts @@ -0,0 +1,93 @@ +import { + Alg, + DEFAULT_RSA_MODULUS_LENGTH, + RsaPrivateKey, + generateJwk, + getPublicKey, + privateKeyToPem, + privateRsaPemToJwk +} from '@narval/signature' +import { Injectable } from '@nestjs/common' +import * as forge from 'node-forge' +import { ParseException } from '../../../../shared/module/persistence/exception/parse.exception' +import { ConnectionInvalidPrivateKeyException } from '../../exception/connection-invalid-private-key.exception' +import { ProviderCredentialService } from '../../type/provider.type' +import { FireblocksCredentials, FireblocksInputCredentials } from './fireblocks.type' + +@Injectable() +export class FireblocksCredentialService + implements ProviderCredentialService +{ + parse(value: unknown): FireblocksCredentials { + const parse = FireblocksCredentials.safeParse(value) + + if (parse.success) { + return parse.data + } + + throw new ParseException(parse.error) + } + + parseInput(value: unknown): FireblocksInputCredentials { + const parse = FireblocksInputCredentials.safeParse(value) + + if (parse.success) { + return parse.data + } + + throw new ParseException(parse.error) + } + + async build(input: FireblocksInputCredentials): Promise { + if (input.privateKey) { + try { + const pem = Buffer.from(input.privateKey, 'base64').toString('utf8') + const privateKey = await privateRsaPemToJwk(pem) + const publicKey = getPublicKey(privateKey) + + return { + apiKey: input.apiKey, + privateKey, + publicKey + } + } catch (error) { + throw new ConnectionInvalidPrivateKeyException({ + message: error.message, + origin: error + }) + } + } + + throw new ConnectionInvalidPrivateKeyException() + } + + async generate( + opts?: Options + ): Promise { + const modulusLength = opts?.modulusLength || DEFAULT_RSA_MODULUS_LENGTH + const privateKey = await generateJwk(Alg.RS256, { modulusLength }) + + return { + privateKey, + publicKey: getPublicKey(privateKey) + } + } + + static async signCertificateRequest(privateKey: RsaPrivateKey, organizationName: string): Promise { + const pem = await privateKeyToPem(privateKey, Alg.RS256) + const forgePrivateKey = forge.pki.privateKeyFromPem(pem) + const forgePublicKey = forge.pki.rsa.setPublicKey(forgePrivateKey.n, forgePrivateKey.e) + const csr = forge.pki.createCertificationRequest() + + csr.publicKey = forgePublicKey + csr.setSubject([ + { + name: 'organizationName', + value: organizationName + } + ]) + csr.sign(forgePrivateKey) + + return Buffer.from(forge.pki.certificationRequestToPem(csr)).toString('base64') + } +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks-known-destination.service.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-known-destination.service.ts new file mode 100644 index 000000000..4f860cfb0 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-known-destination.service.ts @@ -0,0 +1,139 @@ +import { LoggerService, PaginatedResult } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { FireblocksClient, WhitelistedWallet } from '../../../http/client/fireblocks.client' +import { BrokerException } from '../../exception/broker.exception' +import { AssetService } from '../../service/asset.service' +import { Asset } from '../../type/asset.type' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { KnownDestination } from '../../type/known-destination.type' +import { + Provider, + ProviderKnownDestinationPaginationOptions, + ProviderKnownDestinationService +} from '../../type/provider.type' +import { WhitelistClassification } from './fireblocks.type' +import { validateConnection } from './fireblocks.util' + +@Injectable() +export class FireblocksKnownDestinationService implements ProviderKnownDestinationService { + constructor( + private readonly fireblocksClient: FireblocksClient, + private readonly assetService: AssetService, + private readonly logger: LoggerService + ) {} + + async findAll( + connection: ConnectionWithCredentials, + // NOTE: Fireblocks doesn't provide pagination on whitelisted resource + // endpoints. + // + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options?: ProviderKnownDestinationPaginationOptions | undefined + ): Promise> { + validateConnection(connection) + + const fireblocksWhitelistedInternalsWallets = await this.fireblocksClient.getWhitelistedInternalWallets({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const fireblocksWhitelistedExternalWallets = await this.fireblocksClient.getWhitelistedExternalWallets({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const fireblocksWhitelistedContracts = await this.fireblocksClient.getWhitelistedContracts({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const assetsIndexedByExternalId = await this.assetService.buildProviderExternalIdIndex(Provider.FIREBLOCKS) + + const knownDestinations: KnownDestination[] = [ + ...this.buildKnownDestinations({ + connection, + assetsIndexedByExternalId, + classification: WhitelistClassification.INTERNAL, + resources: fireblocksWhitelistedInternalsWallets + }), + ...this.buildKnownDestinations({ + connection, + assetsIndexedByExternalId, + classification: WhitelistClassification.EXTERNAL, + resources: fireblocksWhitelistedExternalWallets + }), + ...this.buildKnownDestinations({ + connection, + assetsIndexedByExternalId, + classification: WhitelistClassification.CONTRACT, + resources: fireblocksWhitelistedContracts + }) + ] + + return { data: knownDestinations } + } + + private buildKnownDestinations(params: { + connection: ConnectionWithCredentials + resources: WhitelistedWallet[] + classification: WhitelistClassification + assetsIndexedByExternalId: Map + }): KnownDestination[] { + const { connection, resources, classification, assetsIndexedByExternalId } = params + const knownDestinations: KnownDestination[] = [] + + for (const resource of resources) { + for (const fireblocksAsset of resource.assets) { + // NOTE: We only include assets ready to be used with 'APPROVED' status + // since we don't have any UX to display asset status to users. + if (fireblocksAsset.status === 'APPROVED') { + const asset = assetsIndexedByExternalId.get(fireblocksAsset.id) + const externalId = this.getExternalId(classification, resource.id, fireblocksAsset.id) + + if (asset) { + knownDestinations.push({ + externalId, + address: fireblocksAsset.address.toLowerCase(), + externalClassification: classification, + clientId: connection.clientId, + connectionId: connection.connectionId, + provider: Provider.FIREBLOCKS, + assetId: asset.assetId, + networkId: asset.networkId + }) + } else { + this.logger.warn('Skip Fireblocks known destination due to asset not found', { + externalId, + classification, + clientId: connection.clientId, + connectionId: connection.connectionId, + externalAssetId: fireblocksAsset.id + }) + } + } + } + } + + return knownDestinations + } + + private getExternalId(classification: WhitelistClassification, resourceId: string, assetId: string): string { + switch (classification) { + case WhitelistClassification.INTERNAL: + return `fireblocks/whitelisted-internal-wallet/${resourceId}/asset/${assetId}` + case WhitelistClassification.EXTERNAL: + return `fireblocks/whitelisted-external-wallet/${resourceId}/asset/${assetId}` + case WhitelistClassification.CONTRACT: + return `fireblocks/whitelisted-contract/${resourceId}/asset/${assetId}` + default: + throw new BrokerException({ + message: `Unknown Fireblocks classification ${classification}`, + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + context: { resourceId, assetId } + }) + } + } +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks-proxy.service.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-proxy.service.ts new file mode 100644 index 000000000..9f9dbc5ae --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-proxy.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common' +import { v4 } from 'uuid' +import { FireblocksClient } from '../../../http/client/fireblocks.client' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { ProviderProxyService, ProxyRequestOptions, ProxyResponse } from '../../type/provider.type' +import { validateConnection } from './fireblocks.util' + +@Injectable() +export class FireblocksProxyService implements ProviderProxyService { + constructor(private readonly fireblocksClient: FireblocksClient) {} + + async forward( + connection: ConnectionWithCredentials, + { data, endpoint, method, nonce }: ProxyRequestOptions + ): Promise { + validateConnection(connection) + + const { url, credentials } = connection + const { apiKey, privateKey } = credentials + const fullUrl = `${url}${endpoint}` + + const response = await this.fireblocksClient.forward({ + url: fullUrl, + method, + data, + apiKey, + nonce: nonce || v4(), + signKey: privateKey + }) + + return { + data: response.data, + code: response.status, + headers: response.headers + } + } +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks-scoped-sync.service.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-scoped-sync.service.ts new file mode 100644 index 000000000..fc71960ca --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-scoped-sync.service.ts @@ -0,0 +1,269 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { randomUUID } from 'crypto' +import { chunk, uniqBy } from 'lodash/fp' +import { FIREBLOCKS_API_ERROR_CODES, FireblocksClient, VaultAccount } from '../../../http/client/fireblocks.client' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { Account, Address, Wallet } from '../../type/indexed-resources.type' +import { NetworkMap } from '../../type/network.type' +import { Provider, ProviderScopedSyncService } from '../../type/provider.type' +import { + RawAccount, + RawAccountError, + RawAccountSyncFailure, + ScopedSyncContext, + ScopedSyncResult +} from '../../type/scoped-sync.type' +import { + CONCURRENT_FIREBLOCKS_REQUESTS, + buildFireblocksAssetAddressExternalId, + parseFireblocksAssetWalletExternalId, + validateConnection +} from './fireblocks.util' +type RawAccountSyncSuccess = { + success: true + wallet: Wallet + account: Account + addresses: Address[] +} + +type RawAccountSyncFailed = { + success: false + failure: RawAccountSyncFailure +} + +type RawAccountSyncResult = RawAccountSyncSuccess | RawAccountSyncFailed + +@Injectable() +export class FireblocksScopedSyncService implements ProviderScopedSyncService { + constructor( + private readonly fireblocksClient: FireblocksClient, + private readonly logger: LoggerService + ) {} + + private resolveFailure(failure: RawAccountSyncFailure): RawAccountSyncFailed { + this.logger.log('Failed to sync Raw Account', failure) + return { success: false, failure } + } + + private resolveSuccess({ + rawAccount, + wallet, + account, + addresses + }: { + wallet: Wallet + addresses: Address[] + rawAccount: RawAccount + account: Account + }): RawAccountSyncSuccess { + this.logger.log('Successfully fetched and map Raw Account', { + rawAccount, + account + }) + return { + success: true, + wallet, + account, + addresses + } + } + private async resolveRawAccount({ + connection, + rawAccount, + networks, + now, + existingAccounts, + existingAddresses, + vaultAccount, + existingWallets + }: { + connection: ConnectionWithCredentials + rawAccount: RawAccount + networks: NetworkMap + now: Date + existingAccounts: Account[] + vaultAccount: VaultAccount + existingWallets: Wallet[] + existingAddresses: Address[] + }): Promise { + validateConnection(connection) + const { vaultId, baseAssetId: networkId } = parseFireblocksAssetWalletExternalId(rawAccount.externalId) + const network = networks.get(networkId) + + if (!network) { + this.logger.error('Network not found', { + rawAccount, + externalNetwork: networkId + }) + return this.resolveFailure({ + rawAccount, + message: 'Network for this account is not supported', + code: RawAccountError.UNLISTED_NETWORK, + networkId + }) + } + + const existingAccount = existingAccounts.find((a) => a.externalId === rawAccount.externalId) + const existingWallet = existingWallets.find((a) => a.externalId === vaultId) + + const accountLabel = `${vaultAccount.name} - ${networkId}` + const accountId = existingAccount?.accountId || randomUUID() + const walletId = existingWallet?.walletId || existingAccount?.walletId || randomUUID() + const fireblocksAddresses = await this.fireblocksClient.getAddresses({ + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + url: connection.url, + vaultAccountId: vaultId, + assetId: networkId + }) + + const wallet: Wallet = { + accounts: [], + clientId: connection.clientId, + connectionId: connection.connectionId, + createdAt: now, + externalId: vaultId, + label: vaultAccount.name, + provider: Provider.FIREBLOCKS, + updatedAt: now, + walletId + } + + const account: Account = { + externalId: rawAccount.externalId, + accountId, + addresses: [], + clientId: connection.clientId, + createdAt: now, + connectionId: connection.connectionId, + label: accountLabel, + networkId: network.networkId, + provider: Provider.FIREBLOCKS, + updatedAt: now, + walletId + } + + const addresses: Address[] = fireblocksAddresses + .map((a) => { + const addressExternalId = buildFireblocksAssetAddressExternalId({ + vaultId, + networkId, + address: a.address + }) + const existingAddress = existingAddresses.find((a) => a.externalId === addressExternalId) + if (!existingAddress) { + return { + accountId, + address: a.address, + addressId: randomUUID(), + clientId: connection.clientId, + createdAt: now, + connectionId: connection.connectionId, + externalId: addressExternalId, + provider: Provider.FIREBLOCKS, + updatedAt: now + } + } + return null + }) + .filter((a) => a !== null) as Address[] + + return this.resolveSuccess({ + rawAccount, + wallet, + account, + addresses + }) + } + + private async fetchVaultAccount({ + connection, + rawAccount + }: { + connection: ConnectionWithCredentials + rawAccount: RawAccount + }): Promise<{ vaultAccount: VaultAccount; rawAccount: RawAccount } | RawAccountSyncFailed> { + validateConnection(connection) + const { vaultId } = parseFireblocksAssetWalletExternalId(rawAccount.externalId) + try { + const vaultAccount = await this.fireblocksClient.getVaultAccount({ + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + url: connection.url, + vaultAccountId: vaultId + }) + + return { vaultAccount, rawAccount } + } catch (error) { + if (error?.response?.body?.code === FIREBLOCKS_API_ERROR_CODES.INVALID_SPECIFIED_VAULT_ACCOUNT) { + return this.resolveFailure({ + rawAccount, + message: 'Fireblocks Vault Account not found', + code: RawAccountError.EXTERNAL_RESOURCE_NOT_FOUND, + externalResourceType: 'vaultAccount', + externalResourceId: vaultId + }) + } + throw error + } + } + + async scopeSync({ + connection, + rawAccounts, + networks, + existingAccounts + }: ScopedSyncContext): Promise { + validateConnection(connection) + + const now = new Date() + + const chunkedRawAccounts = chunk(CONCURRENT_FIREBLOCKS_REQUESTS, rawAccounts) + const fetchResults = [] + + for (const currentChunk of chunkedRawAccounts) { + const chunkResults = await Promise.all( + currentChunk.map((rawAccount) => this.fetchVaultAccount({ connection, rawAccount })) + ) + fetchResults.push(...chunkResults) + } + + const wallets: Wallet[] = [] + const accounts: Account[] = [] + const addresses: Address[] = [] + const failures: RawAccountSyncFailure[] = [] + + for (const result of fetchResults) { + if ('success' in result && !result.success) { + failures.push(result.failure) + continue + } else if ('rawAccount' in result) { + const mappedResult = await this.resolveRawAccount({ + connection, + rawAccount: result.rawAccount, + networks, + now, + existingAccounts: [...existingAccounts, ...accounts], + existingWallets: wallets, + existingAddresses: addresses, + vaultAccount: result.vaultAccount + }) + if (mappedResult.success) { + wallets.push(mappedResult.wallet) + accounts.push(mappedResult.account) + addresses.push(...mappedResult.addresses) + } else { + failures.push(mappedResult.failure) + } + } + } + + return { + wallets: uniqBy('externalId', wallets), + accounts: uniqBy('externalId', accounts), + addresses: uniqBy('externalId', addresses), + failures + } + } +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks-transfer.service.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-transfer.service.ts new file mode 100644 index 000000000..ae141445b --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks-transfer.service.ts @@ -0,0 +1,443 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { randomUUID } from 'crypto' +import { CreateTransaction, FireblocksClient } from '../../../http/client/fireblocks.client' +import { TransferRepository } from '../../../persistence/repository/transfer.repository' +import { AssetException } from '../../exception/asset.exception' +import { BrokerException } from '../../exception/broker.exception' +import { AccountService } from '../../service/account.service' +import { AddressService } from '../../service/address.service' +import { AssetService } from '../../service/asset.service' +import { NetworkService } from '../../service/network.service' +import { ResolvedTransferAsset, TransferAssetService } from '../../service/transfer-asset.service' +import { WalletService } from '../../service/wallet.service' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { Network } from '../../type/network.type' +import { Provider, ProviderTransferService } from '../../type/provider.type' +import { ConnectionScope } from '../../type/scope.type' +import { + Destination, + InternalTransfer, + NetworkFeeAttribution, + SendTransfer, + Transfer, + TransferAsset, + TransferPartyType, + TransferStatus, + isAddressDestination, + isProviderSpecific +} from '../../type/transfer.type' +import { getExternalNetwork } from '../../util/network.util' +import { validateConnection } from './fireblocks.util' + +@Injectable() +export class FireblocksTransferService implements ProviderTransferService { + constructor( + private readonly assetService: AssetService, + private readonly fireblocksClient: FireblocksClient, + private readonly networkService: NetworkService, + private readonly walletService: WalletService, + private readonly accountService: AccountService, + private readonly addressService: AddressService, + private readonly transferRepository: TransferRepository, + private readonly transferAssetService: TransferAssetService, + private readonly logger: LoggerService + ) {} + + async findById(connection: ConnectionWithCredentials, transferId: string): Promise { + const { clientId, connectionId } = connection + const context = { clientId, connectionId, transferId } + + this.logger.log('Find Fireblocks transfer by ID', context) + + validateConnection(connection) + + const internalTransfer = await this.transferRepository.findById(connection.clientId, transferId) + + this.logger.log('Found internal transfer by ID', { + clientId, + internalTransfer + }) + + const fireblocksTransaction = await this.fireblocksClient.getTransactionById({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + txId: internalTransfer.externalId + }) + + this.logger.log('Found Fireblocks transaction by ID', context) + + const transfer: Transfer = { + connectionId, + assetExternalId: internalTransfer.assetExternalId, + assetId: internalTransfer.assetId, + clientId: internalTransfer.clientId, + createdAt: new Date(fireblocksTransaction.createdAt), + customerRefId: internalTransfer.customerRefId, + destination: internalTransfer.destination, + externalId: internalTransfer.externalId, + externalStatus: fireblocksTransaction.subStatus || fireblocksTransaction.status || null, + grossAmount: fireblocksTransaction.amountInfo.amount, + idempotenceId: internalTransfer.idempotenceId, + memo: fireblocksTransaction.note || internalTransfer.memo || null, + networkFeeAttribution: internalTransfer.networkFeeAttribution, + provider: internalTransfer.provider, + providerSpecific: internalTransfer.providerSpecific, + source: internalTransfer.source, + status: this.mapStatus(fireblocksTransaction.status), + transferId: internalTransfer.transferId, + fees: [ + ...(fireblocksTransaction.feeInfo.networkFee + ? [ + { + type: 'network', + attribution: internalTransfer.networkFeeAttribution, + amount: fireblocksTransaction.feeInfo.networkFee, + // TODO: (@wcalderipe, 13/01/25): Replace by Narval asset ID + // once it's a thing. + assetId: fireblocksTransaction.feeCurrency + } + ] + : []), + ...(fireblocksTransaction.feeInfo.gasPrice + ? [ + { + type: 'gas-price', + amount: fireblocksTransaction.feeInfo.gasPrice, + // TODO: (@wcalderipe, 13/01/25): Replace by Narval asset ID + // once it's a thing. + assetId: fireblocksTransaction.feeCurrency + } + ] + : []) + ] + } + + this.logger.log('Combined internal and remote Fireblocks transfer', { ...context, transfer }) + + return transfer + } + + async send(connection: ConnectionWithCredentials, sendTransfer: SendTransfer): Promise { + const { clientId, connectionId } = connection + const context = { clientId, connectionId } + + this.logger.log('Send Fireblocks transfer', { ...context, sendTransfer }) + + validateConnection(connection) + + const transferAsset = await this.findTransferAsset(connection, sendTransfer.asset) + if (!transferAsset) { + throw new BrokerException({ + message: 'Transfer asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { asset: sendTransfer.asset } + }) + } + + const source = await this.getSource(connection, sendTransfer) + + const destination = await this.getDestination(connection, transferAsset.network, sendTransfer.destination) + + this.logger.log('Resolved Fireblocks transfer source and destination', { + ...context, + destination, + source, + networkId: transferAsset.network.networkId + }) + + // NOTE: Defaults the `networkFeeAttribution` to deduct to match most + // blockchain behaviors since Fireblocks API isn't specific about what's + // the default value for `treatAsGrossAmount` parameter. + // + // IMPORTANT: This parameter can only be considered if a transaction’s asset is + // a base asset, such as ETH or MATIC. If the asset can’t be used for + // transaction fees, like USDC, this parameter is ignored and the fee is + // deducted from the relevant base asset wallet in the source account. + // + // See https://developers.fireblocks.com/reference/createtransaction + const networkFeeAttribution = sendTransfer.networkFeeAttribution || NetworkFeeAttribution.DEDUCT + + const transferId = sendTransfer.transferId || randomUUID() + + const data: CreateTransaction = { + source, + destination, + amount: sendTransfer.amount, + assetId: transferAsset.assetExternalId, + customerRefId: sendTransfer.customerRefId, + externalTxId: transferId, + note: sendTransfer.memo, + treatAsGrossAmount: this.getTreatAsGrossAmount(networkFeeAttribution), + idempotencyKey: sendTransfer.idempotenceId, + ...(isProviderSpecific(sendTransfer.providerSpecific) ? { ...sendTransfer.providerSpecific } : {}) + } + + this.logger.log('Send create transaction request to Fireblocks', { ...context, data }) + + const createTransactionResponse = await this.fireblocksClient.createTransaction({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + data + }) + + this.logger.log('Fireblocks transaction created', context) + + const internalTransfer: InternalTransfer = { + connectionId, + transferId, + assetId: transferAsset.assetId, + clientId: connection.clientId, + createdAt: new Date(), + customerRefId: sendTransfer.customerRefId || null, + destination: sendTransfer.destination, + assetExternalId: transferAsset.assetExternalId, + externalId: createTransactionResponse.id, + externalStatus: createTransactionResponse.status, + grossAmount: sendTransfer.amount, + idempotenceId: sendTransfer.idempotenceId, + memo: sendTransfer.memo || null, + networkFeeAttribution, + provider: Provider.FIREBLOCKS, + providerSpecific: sendTransfer.providerSpecific || null, + source: sendTransfer.source, + status: this.mapStatus(createTransactionResponse.status) + } + + this.logger.log('Create internal transfer', internalTransfer) + + await this.transferRepository.bulkCreate([internalTransfer]) + + return internalTransfer + } + + /** + * When Fireblocks `treatAsGrossAmount` is set to `true`, the fee will be + * deducted from the requested amount. + * + * Note: This parameter can only be considered if a transaction’s asset is a + * base asset, such as ETH or MATIC. If the asset can’t be used for + * transaction fees, like USDC, this parameter is ignored and the fee is + * deducted from the relevant base asset wallet in the source account. + * + * Example: a request to transfer 5 BTC with `treatAsGrossAmount=false` would + * result in 5 exactly BTC received to the destination and just over 5 BTC + * spent by the source. + * + * @see https://developers.fireblocks.com/reference/createtransaction + */ + private getTreatAsGrossAmount(attribution: NetworkFeeAttribution): boolean { + if (attribution === NetworkFeeAttribution.DEDUCT) { + return true + } + + if (attribution === NetworkFeeAttribution.ON_TOP) { + return false + } + + return false + } + + private async getSource(scope: ConnectionScope, sendTransfer: SendTransfer) { + if (sendTransfer.source.type === TransferPartyType.ACCOUNT) { + const account = await this.accountService.findById(scope, sendTransfer.source.id) + + if (account) { + const wallet = await this.walletService.findById(scope, account.walletId) + + if (wallet) { + return { + type: 'VAULT_ACCOUNT', + id: wallet.externalId + } + } + } + } + + throw new BrokerException({ + message: 'Cannot resolve Fireblocks transfer source', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { source: sendTransfer.source } + }) + } + + private async getDestination(scope: ConnectionScope, network: Network, destination: Destination) { + this.logger.log('Resolved Fireblocks destination', { + scope, + destination, + network: network.networkId + }) + + if (isAddressDestination(destination)) { + const address = await this.addressService.findByAddressAndNetwork( + scope.clientId, + destination.address, + network.networkId + ) + + if (address) { + const account = await this.accountService.findById(scope, address.accountId) + + if (account) { + const wallet = await this.walletService.findById(scope, account.walletId) + + if (wallet) { + return { + type: 'VAULT_ACCOUNT', + id: wallet.externalId + } + } + } + } + + return { + type: 'ONE_TIME_ADDRESS', + oneTimeAddress: { + address: destination.address + } + } + } + + if (destination.type === TransferPartyType.ACCOUNT) { + const account = await this.accountService.findById(scope, destination.id) + + if (account) { + const wallet = await this.walletService.findById(scope, account.walletId) + + if (wallet) { + return { + type: 'VAULT_ACCOUNT', + id: wallet.externalId + } + } + } + } + + throw new BrokerException({ + message: 'Cannot resolve Fireblocks transfer destination', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { destination } + }) + } + + private async findTransferAsset( + connection: ConnectionWithCredentials, + transferAsset: TransferAsset + ): Promise { + validateConnection(connection) + + const findByExternalIdFallback = async (externalAssetId: string): Promise => { + const fbSupportedAssets = await this.fireblocksClient.getSupportedAssets({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const fbSupportedAsset = fbSupportedAssets.find(({ id }) => id.toLowerCase() === externalAssetId.toLowerCase()) + if (!fbSupportedAsset) { + throw new AssetException({ + message: 'Fireblocks supported asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { transferAsset } + }) + } + + const network = await this.networkService.findByExternalId(Provider.FIREBLOCKS, fbSupportedAsset.nativeAsset) + if (!network) { + throw new AssetException({ + message: 'Fireblocks supported asset network not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { transferAsset, fbSupportedAsset } + }) + } + + return { + network, + assetExternalId: externalAssetId, + assetId: null + } + } + + const findByOnchainIdFallback = async (network: Network, onchainId: string): Promise => { + const externalNetwork = getExternalNetwork(network, Provider.FIREBLOCKS) + if (!externalNetwork) { + throw new AssetException({ + message: 'Network does not support Fireblocks', + suggestedHttpStatusCode: HttpStatus.NOT_IMPLEMENTED, + context: { transferAsset, network } + }) + } + + const fbSupportedAssets = await this.fireblocksClient.getSupportedAssets({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + const fbSupportedAsset = fbSupportedAssets.find( + ({ contractAddress, nativeAsset }) => + contractAddress.toLowerCase() === onchainId.toLowerCase() && + nativeAsset.toLowerCase() === externalNetwork.externalId.toLowerCase() + ) + if (!fbSupportedAsset) { + throw new AssetException({ + message: 'Fireblocks supported asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { transferAsset } + }) + } + + return { + network, + assetId: null, + assetExternalId: fbSupportedAsset.id + } + } + + return this.transferAssetService.resolve({ + findByExternalIdFallback, + findByOnchainIdFallback, + transferAsset, + provider: Provider.FIREBLOCKS + }) + } + + private mapStatus(status: string): TransferStatus { + const upperCasedStatus = status.toUpperCase() + + // For FB transaction statuses, see + // https://developers.fireblocks.com/reference/gettransaction + const statuses: Record = { + SUBMITTED: TransferStatus.PROCESSING, + PENDING_AML_SCREENING: TransferStatus.PROCESSING, + PENDING_ENRICHMENT: TransferStatus.PROCESSING, + PENDING_AUTHORIZATION: TransferStatus.PROCESSING, + QUEUED: TransferStatus.PROCESSING, + PENDING_SIGNATURE: TransferStatus.PROCESSING, + PENDING_3RD_PARTY_MANUAL_APPROVAL: TransferStatus.PROCESSING, + PENDING_3RD_PARTY: TransferStatus.PROCESSING, + BROADCASTING: TransferStatus.PROCESSING, + CONFIRMING: TransferStatus.PROCESSING, + CANCELLING: TransferStatus.PROCESSING, + + COMPLETED: TransferStatus.SUCCESS, + + CANCELLED: TransferStatus.FAILED, + BLOCKED: TransferStatus.FAILED, + REJECTED: TransferStatus.FAILED, + FAILED: TransferStatus.FAILED + } + + if (upperCasedStatus in statuses) { + return statuses[upperCasedStatus] + } + + throw new BrokerException({ + message: 'Cannot map Fireblocks transaction status', + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + context: { status: upperCasedStatus } + }) + } +} diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks.type.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks.type.ts new file mode 100644 index 000000000..6ca5d92c7 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks.type.ts @@ -0,0 +1,22 @@ +import { rsaPrivateKeySchema, rsaPublicKeySchema } from '@narval/signature' +import { z } from 'zod' + +export const WhitelistClassification = { + CONTRACT: 'contract', + INTERNAL: 'internal', + EXTERNAL: 'external' +} as const +export type WhitelistClassification = (typeof WhitelistClassification)[keyof typeof WhitelistClassification] + +export const FireblocksInputCredentials = z.object({ + apiKey: z.string(), + privateKey: z.string().optional().describe('RSA private key pem base64 encoded') +}) +export type FireblocksInputCredentials = z.infer + +export const FireblocksCredentials = z.object({ + apiKey: z.string().optional(), + publicKey: rsaPublicKeySchema, + privateKey: rsaPrivateKeySchema +}) +export type FireblocksCredentials = z.infer diff --git a/apps/vault/src/broker/core/provider/fireblocks/fireblocks.util.ts b/apps/vault/src/broker/core/provider/fireblocks/fireblocks.util.ts new file mode 100644 index 000000000..34948cea5 --- /dev/null +++ b/apps/vault/src/broker/core/provider/fireblocks/fireblocks.util.ts @@ -0,0 +1,118 @@ +import { RsaPrivateKey } from '@narval/signature' +import { HttpStatus } from '@nestjs/common' +import { BrokerException } from '../../exception/broker.exception' +import { ConnectionInvalidException } from '../../exception/connection-invalid.exception' +import { ConnectionWithCredentials } from '../../type/connection.type' +import { Provider } from '../../type/provider.type' +import { FireblocksCredentials } from './fireblocks.type' + +export const CONCURRENT_FIREBLOCKS_REQUESTS = 5 + +export function validateConnection( + connection: ConnectionWithCredentials +): asserts connection is ConnectionWithCredentials & { + url: string + credentials: { + apiKey: string + privateKey: RsaPrivateKey + } +} { + const context = { + clientId: connection.clientId, + connectionId: connection.connectionId, + provider: connection.provider, + status: connection.status, + url: connection.url + } + + if (connection.provider !== Provider.FIREBLOCKS) { + throw new ConnectionInvalidException({ + message: 'Invalid connection provider for Fireblocks', + context + }) + } + + if (!connection.url) { + throw new ConnectionInvalidException({ + message: 'Fireblocks connection missing URL', + context + }) + } + + if (!connection.credentials) { + throw new ConnectionInvalidException({ + message: 'Fireblocks connection missing credentials', + context + }) + } + + const credentials = FireblocksCredentials.parse(connection.credentials) + + if (!credentials.apiKey) { + throw new ConnectionInvalidException({ + message: 'Fireblocks connection missing API key', + context + }) + } +} + +export type FireblocksAccountExternalId = `${string}-${string}` +export function buildFireblocksAssetWalletExternalId({ + vaultId, + networkId +}: { + vaultId: string + networkId: string +}): string { + return `${vaultId.toString()}-${networkId}`.toUpperCase() +} + +export type FireblocksAssetWalletId = { vaultId: string; baseAssetId: string } +export function parseFireblocksAssetWalletExternalId(externalId: string): FireblocksAssetWalletId { + const matches = externalId.match(/^([^-]+)-([^-]+)$/) + if (!matches) { + throw new BrokerException({ + message: 'The external ID does not match composed standard for fireblocks', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { externalId } + }) + } + + const [, vaultId, networkId] = matches + return { + vaultId, + baseAssetId: networkId + } +} + +export type FireblocksAddressExternalId = `${string}-${string}-${string}` +export function buildFireblocksAssetAddressExternalId({ + vaultId, + networkId, + address +}: { + vaultId: string + networkId: string + address: string +}): string { + return `${vaultId}-${networkId}-${address}`.toUpperCase() +} + +export type FireblocksAssetAddressId = { vaultId: string; networkId: string; address: string } +export function parseFireblocksAssetAddressExternalId(externalId: string): FireblocksAssetAddressId { + const matches = externalId.match(/^([^-]+)-([^-]+)-([^-]+)$/) + if (!matches) { + throw new BrokerException({ + message: 'The external ID does not match composed standard for fireblocks', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { externalId } + }) + } + + const [, vaultId, networkId, address] = matches + return { + vaultId, + networkId, + address + } +} diff --git a/apps/vault/src/broker/core/service/__test__/integration/asset.service.spec.ts b/apps/vault/src/broker/core/service/__test__/integration/asset.service.spec.ts new file mode 100644 index 000000000..e3d84ae86 --- /dev/null +++ b/apps/vault/src/broker/core/service/__test__/integration/asset.service.spec.ts @@ -0,0 +1,120 @@ +import { ConfigModule } from '@narval/config-module' +import { LoggerModule } from '@narval/nestjs-shared' +import { CacheModule } from '@nestjs/cache-manager' +import { Test } from '@nestjs/testing' +import { PrismaClientKnownRequestError } from '@prisma/client/vault/runtime/library' +import { load } from '../../../../../main.config' +import { AppModule } from '../../../../../main.module' +import { PersistenceModule } from '../../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../../shared/module/persistence/service/test-prisma.service' +import { AssetRepository } from '../../../../persistence/repository/asset.repository' +import { NetworkRepository } from '../../../../persistence/repository/network.repository' +import { AssetException } from '../../../exception/asset.exception' +import { Asset } from '../../../type/asset.type' +import { Network } from '../../../type/network.type' +import { Provider } from '../../../type/provider.type' +import { AssetService } from '../../asset.service' +import { NetworkService } from '../../network.service' + +describe(AssetService.name, () => { + let testPrismaService: TestPrismaService + let assetService: AssetService + let networkService: NetworkService + + const ethereum: Network = { + networkId: 'ETHEREUM', + coinType: 60, + name: 'Ethereum', + externalNetworks: [] + } + + const usdc: Asset = { + assetId: 'USDC', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + networkId: ethereum.networkId, + onchainId: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + externalAssets: [ + { + provider: Provider.FIREBLOCKS, + externalId: 'USDC' + } + ] + } + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [ + AppModule, + PersistenceModule.register({ imports: [] }), + CacheModule.register(), + LoggerModule.forTest(), + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }) + ], + providers: [AssetService, AssetRepository, NetworkService, NetworkRepository] + }).compile() + + testPrismaService = module.get(TestPrismaService) + assetService = module.get(AssetService) + networkService = module.get(NetworkService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await networkService.bulkCreate([ethereum]) + }) + + describe('bulkCreate', () => { + it('creates asset with lowercase onchainId', async () => { + const asset: Asset = { + ...usdc, + onchainId: '0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48', + externalAssets: [] + } + + const created = await assetService.bulkCreate([asset]) + + expect(created[0].onchainId).toEqual(asset.onchainId?.toLowerCase()) + }) + + it('throws AssetException when native asset already exists', async () => { + const eth: Asset = { + assetId: 'ETH', + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + networkId: ethereum.networkId, + onchainId: null, + externalAssets: [] + } + + await assetService.bulkCreate([eth]) + + // NOTE: What makes ETH a native asset is the networkId and onchainId + // being null. + const duplicateEth: Asset = { + ...eth, + assetId: 'ETH_2' + } + + await expect(assetService.bulkCreate([duplicateEth])).rejects.toThrow(AssetException) + }) + + it('throws PrismaClientKnownRequestError when networkId and onchainId already exists', async () => { + await assetService.bulkCreate([usdc]) + + // NOTE: Invariant protected by a unique key in the database. + await expect(assetService.bulkCreate([usdc])).rejects.toThrow(PrismaClientKnownRequestError) + }) + }) +}) diff --git a/apps/vault/src/broker/core/service/__test__/integration/transfer-asset.service.spec.ts b/apps/vault/src/broker/core/service/__test__/integration/transfer-asset.service.spec.ts new file mode 100644 index 000000000..f4bb4f7fe --- /dev/null +++ b/apps/vault/src/broker/core/service/__test__/integration/transfer-asset.service.spec.ts @@ -0,0 +1,242 @@ +import { ConfigModule } from '@narval/config-module' +import { LoggerModule } from '@narval/nestjs-shared' +import { CacheModule } from '@nestjs/cache-manager' +import { Test } from '@nestjs/testing' +import { load } from '../../../../../main.config' +import { AppModule } from '../../../../../main.module' +import { PersistenceModule } from '../../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../../shared/module/persistence/service/test-prisma.service' +import { AssetRepository } from '../../../../persistence/repository/asset.repository' +import { NetworkRepository } from '../../../../persistence/repository/network.repository' +import { AssetException } from '../../../exception/asset.exception' +import { Asset } from '../../../type/asset.type' +import { Network } from '../../../type/network.type' +import { Provider } from '../../../type/provider.type' +import { TransferAsset } from '../../../type/transfer.type' +import { getExternalAsset } from '../../../util/asset.util' +import { AssetService } from '../../asset.service' +import { NetworkService } from '../../network.service' +import { ResolvedTransferAsset, TransferAssetService } from '../../transfer-asset.service' + +describe(TransferAssetService.name, () => { + let testPrismaService: TestPrismaService + let assetService: AssetService + let networkService: NetworkService + let transferAssetService: TransferAssetService + + const ethereum: Network = { + networkId: 'ETHEREUM', + coinType: 60, + name: 'Ethereum', + externalNetworks: [ + { + provider: Provider.FIREBLOCKS, + externalId: 'FB_ETHEREUM' + } + ] + } + + const usdc: Asset = { + assetId: 'USDC', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + networkId: ethereum.networkId, + onchainId: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + externalAssets: [ + { + provider: Provider.FIREBLOCKS, + externalId: 'FB_USDC' + } + ] + } + + const usdcOnFireblocks = getExternalAsset(usdc, Provider.FIREBLOCKS) + + const ether: Asset = { + assetId: 'ETH', + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + networkId: ethereum.networkId, + onchainId: null, + externalAssets: [ + { + provider: Provider.FIREBLOCKS, + externalId: 'FB_ETH' + } + ] + } + + const etherOnFireblocks = getExternalAsset(ether, Provider.FIREBLOCKS) + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [ + AppModule, + PersistenceModule.register({ imports: [] }), + CacheModule.register(), + LoggerModule.forTest(), + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }) + ], + providers: [AssetService, AssetRepository, NetworkService, NetworkRepository, TransferAssetService] + }).compile() + + testPrismaService = module.get(TestPrismaService) + assetService = module.get(AssetService) + networkService = module.get(NetworkService) + transferAssetService = module.get(TransferAssetService) + + await testPrismaService.truncateAll() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + }) + + beforeEach(async () => { + await testPrismaService.truncateAll() + await networkService.bulkCreate([ethereum]) + await assetService.bulkCreate([usdc, ether]) + }) + + describe('resolve', () => { + const baseTransferAsset: TransferAsset = { + assetId: usdc.assetId + } + + it('resolves by provider and externalId when externalAssetId is present', async () => { + const resolvedTransferAsset = await transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + transferAsset: { + ...baseTransferAsset, + externalAssetId: usdcOnFireblocks?.externalId + } + }) + + expect(resolvedTransferAsset).toEqual({ + network: expect.objectContaining(ethereum), + assetId: usdc.assetId, + assetExternalId: usdcOnFireblocks?.externalId + }) + }) + + it('calls fallback when asset is not found by externalId', async () => { + const unlistedAssetId = 'UNLISTED_ASSET_ID' + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const findByExternalIdFallback = async (_externalAssetId: string): Promise => { + return { + network: ethereum, + assetId: null, + assetExternalId: unlistedAssetId + } + } + + const resolvedTransferAsset = await transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + findByExternalIdFallback, + transferAsset: { + ...baseTransferAsset, + externalAssetId: 'UNLISTED_ASSET_ID' + } + }) + + expect(resolvedTransferAsset).toEqual({ + network: ethereum, + assetId: null, + assetExternalId: unlistedAssetId + }) + }) + + it('resolves by assetId when present', async () => { + const resolvedTransferAsset = await transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + transferAsset: baseTransferAsset + }) + + expect(resolvedTransferAsset).toEqual({ + network: expect.objectContaining(ethereum), + assetId: usdc.assetId, + assetExternalId: usdcOnFireblocks?.externalId + }) + }) + + it('resolves by onchainId when address and networkId are set', async () => { + const resolvedTransferAsset = await transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + transferAsset: { + address: usdc.onchainId as string, + networkId: ethereum.networkId + } + }) + + expect(resolvedTransferAsset).toEqual({ + network: expect.objectContaining(ethereum), + assetId: usdc.assetId, + assetExternalId: usdcOnFireblocks?.externalId + }) + }) + + it('calls fallback when asset is not found by address and networkId', async () => { + const unlistedAssetAddress = 'UNLISTED_ASSET_ADDRESS' + + const findByOnchainIdFallback = async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _network: Network, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _externalAssetId: string + ): Promise => { + return { + network: ethereum, + assetId: null, + assetExternalId: unlistedAssetAddress + } + } + + const resolvedTransferAsset = await transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + findByOnchainIdFallback, + transferAsset: { + networkId: ethereum.networkId, + address: unlistedAssetAddress + } + }) + + expect(resolvedTransferAsset).toEqual({ + network: ethereum, + assetId: null, + assetExternalId: unlistedAssetAddress + }) + }) + + it('resolves native asset when only networkId is set', async () => { + const resolvedTransferAsset = await transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + transferAsset: { + networkId: ethereum.networkId + } + }) + + expect(resolvedTransferAsset).toEqual({ + network: expect.objectContaining(ethereum), + assetId: ether.assetId, + assetExternalId: etherOnFireblocks?.externalId + }) + }) + + it('throws AssetException when address is set but networkId is not', async () => { + await expect( + transferAssetService.resolve({ + provider: Provider.FIREBLOCKS, + transferAsset: { + address: usdc.onchainId as string + } + }) + ).rejects.toThrow(AssetException) + }) + }) +}) diff --git a/apps/vault/src/broker/core/service/account.service.ts b/apps/vault/src/broker/core/service/account.service.ts new file mode 100644 index 000000000..534bfbf87 --- /dev/null +++ b/apps/vault/src/broker/core/service/account.service.ts @@ -0,0 +1,42 @@ +import { PaginatedResult, PaginationOptions } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { AccountRepository, FindAllOptions, UpdateAccount } from '../../persistence/repository/account.repository' +import { Account, Address } from '../type/indexed-resources.type' +import { ConnectionScope } from '../type/scope.type' + +@Injectable() +export class AccountService { + constructor(private readonly accountRepository: AccountRepository) {} + + async getAccountAddresses( + clientId: string, + AccountId: string, + options: PaginationOptions + ): Promise> { + return this.accountRepository.findAddressesByAccountId(clientId, AccountId, options) + } + + async bulkCreate(accounts: Account[]): Promise { + return this.accountRepository.bulkCreate(accounts) + } + + async bulkUpsert(accounts: Account[]): Promise { + return this.accountRepository.bulkUpsert(accounts) + } + + async bulkUpdate(updateAccounts: UpdateAccount[]): Promise { + return this.accountRepository.bulkUpdate(updateAccounts) + } + + async update(updateAccount: UpdateAccount): Promise { + return this.accountRepository.update(updateAccount) + } + + async findAll(scope: ConnectionScope, options?: FindAllOptions): Promise> { + return this.accountRepository.findAll(scope, options) + } + + async findById(scope: ConnectionScope, accountId: string): Promise { + return this.accountRepository.findById(scope, accountId) + } +} diff --git a/apps/vault/src/broker/core/service/address.service.ts b/apps/vault/src/broker/core/service/address.service.ts new file mode 100644 index 000000000..89eaaaa51 --- /dev/null +++ b/apps/vault/src/broker/core/service/address.service.ts @@ -0,0 +1,44 @@ +import { PaginatedResult, PaginationOptions } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { AddressRepository, FindAllOptions } from '../../persistence/repository/address.repository' +import { BrokerException } from '../exception/broker.exception' +import { Address } from '../type/indexed-resources.type' +import { ConnectionScope } from '../type/scope.type' + +@Injectable() +export class AddressService { + constructor(private readonly addressRepository: AddressRepository) {} + + async getAddresses(clientId: string, options?: PaginationOptions): Promise> { + return this.addressRepository.findByClientId(clientId, options) + } + + async bulkCreate(addresses: Address[]): Promise { + return this.addressRepository.bulkCreate(addresses) + } + + async findAll(scope: ConnectionScope, opts?: FindAllOptions): Promise> { + return this.addressRepository.findAll(scope, opts) + } + + async findById(scope: ConnectionScope, addressId: string): Promise
{ + return this.addressRepository.findById(scope, addressId) + } + + async findByAddressAndNetwork(clientId: string, address: string, networkId: string): Promise
{ + const addresses = await this.addressRepository.findByAddressAndNetwork(clientId, address, networkId) + + if (addresses.length > 1) { + throw new BrokerException({ + message: 'Cannot resolve the right address due to ambiguity', + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + context: { + clientId, + addresses: addresses.map(({ address, addressId }) => ({ addressId, address })) + } + }) + } + + return addresses.length ? addresses[0] : null + } +} diff --git a/apps/vault/src/broker/core/service/asset.service.ts b/apps/vault/src/broker/core/service/asset.service.ts new file mode 100644 index 000000000..8ce9ecf49 --- /dev/null +++ b/apps/vault/src/broker/core/service/asset.service.ts @@ -0,0 +1,199 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager' +import { HttpStatus, Inject, Injectable } from '@nestjs/common' +import { uniq } from 'lodash' +import { AssetRepository } from '../../persistence/repository/asset.repository' +import { AssetException } from '../exception/asset.exception' +import { Asset, ExternalAsset } from '../type/asset.type' +import { Provider } from '../type/provider.type' +import { isNativeAsset } from '../util/asset.util' + +type FindAllOptions = { + filters?: { + provider?: Provider + } +} + +@Injectable() +export class AssetService { + constructor( + private readonly assetRepository: AssetRepository, + private readonly logger: LoggerService, + @Inject(CACHE_MANAGER) private cacheManager: Cache + ) {} + + async bulkCreate(assets: Asset[]): Promise { + const createdAssets: Asset[] = [] + + for (const asset of assets) { + if (isNativeAsset(asset)) { + // Bypass cache and query repository directly to ensure we check the + // source of truth. + const native = await this.assetRepository.findNative(asset.networkId) + + if (native) { + throw new AssetException({ + message: 'A native asset for the network already exists', + suggestedHttpStatusCode: HttpStatus.CONFLICT, + context: { asset } + }) + } + } + + createdAssets.push(await this.assetRepository.create(asset)) + } + + const providers = uniq( + createdAssets.flatMap(({ externalAssets }) => externalAssets).flatMap(({ provider }) => provider) + ) + + for (const provider of providers) { + const key = this.getListCacheKey(provider) + + this.logger.log('Delete asset list cache after bulk create', { key }) + + await this.cacheManager.del(key) + } + + return createdAssets + } + + async addExternalAsset(assetId: string, externalAsset: ExternalAsset): Promise { + return this.assetRepository.addExternalAsset(assetId, externalAsset) + } + + async bulkAddExternalAsset( + params: { + assetId: string + externalAsset: ExternalAsset + }[] + ): Promise { + return this.assetRepository.bulkAddExternalAsset(params) + } + + async findAll(options?: FindAllOptions): Promise { + // IMPORTANT: If you add new filters to the findAll method, you MUST + // rethink the cache strategy. This will only work as long there's a single + // filter. + const key = this.getListCacheKey(options?.filters?.provider) + + this.logger.log('Read asset list from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Asset list cache not found. Fallback to database', { key }) + + const assets = await this.assetRepository.findAll(options) + + await this.cacheManager.set(key, assets) + + return assets + } + + private getListCacheKey(provider?: Provider) { + if (provider) { + return `asset:list:${provider}` + } + + return 'asset:list' + } + + async buildProviderExternalIdIndex(provider: Provider): Promise> { + const assets = await this.findAll({ filters: { provider } }) + const index = new Map() + + for (const asset of assets) { + for (const externalAsset of asset.externalAssets) { + if (externalAsset.provider === provider) { + index.set(externalAsset.externalId, asset) + } + } + } + + return index + } + + async findById(assetId: string): Promise { + const key = `asset:${assetId.toLowerCase()}` + + this.logger.log('Read asset by ID from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Asset cache not found by ID. Fallback to database', { key }) + + const asset = await this.assetRepository.findById(assetId) + + await this.cacheManager.set(key, asset) + + return asset + } + + async findByExternalId(provider: Provider, externalId: string): Promise { + const key = `asset:${provider}:${externalId.toLowerCase()}` + + this.logger.log('Read asset by provider and external ID from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Asset cache not found by provider and external ID. Fallback to database', { key }) + + const asset = await this.assetRepository.findByExternalId(provider, externalId) + + await this.cacheManager.set(key, asset) + + return asset + } + + async findByOnchainId(networkId: string, onchainId: string): Promise { + const key = `asset:${networkId.toLowerCase()}:${onchainId.toLowerCase()}` + + this.logger.log('Read asset by network and onchain ID from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Asset cache not found by network and onchain ID. Fallback to database', { key }) + + const asset = await this.assetRepository.findByOnchainId(networkId, onchainId.toLowerCase()) + + await this.cacheManager.set(key, asset) + + return asset + } + + async findNative(networkId: string): Promise { + const key = `asset:native:${networkId.toLowerCase()}` + + this.logger.log('Read native asset from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Native asset cache not found. Fallback to database', { key }) + + const asset = await this.assetRepository.findNative(networkId) + + await this.cacheManager.set(key, asset) + + return asset + } +} diff --git a/apps/vault/src/broker/core/service/connection.service.ts b/apps/vault/src/broker/core/service/connection.service.ts new file mode 100644 index 000000000..018ff56d9 --- /dev/null +++ b/apps/vault/src/broker/core/service/connection.service.ts @@ -0,0 +1,386 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService, PaginatedResult } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { randomUUID } from 'crypto' +import { SetRequired } from 'type-fest' +import { Config, Env } from '../../../main.config' +import { EncryptionKeyService } from '../../../transit-encryption/core/service/encryption-key.service' +import { ConnectionRepository, FindAllOptions } from '../../persistence/repository/connection.repository' +import { ConnectionActivatedEvent } from '../../shared/event/connection-activated.event' +import { BrokerException } from '../exception/broker.exception' +import { ConnectionInvalidCredentialsException } from '../exception/connection-invalid-credentials.exception' +import { ConnectionInvalidStatusException } from '../exception/connection-invalid-status.exception' +import { NotFoundException } from '../exception/not-found.exception' +import { AnchorageCredentialService } from '../provider/anchorage/anchorage-credential.service' +import { AnchorageCredentials, AnchorageInputCredentials } from '../provider/anchorage/anchorage.type' +import { BitgoCredentialService } from '../provider/bitgo/bitgo-credential.service' +import { BitgoCredentials, BitgoInputCredentials } from '../provider/bitgo/bitgo.type' +import { FireblocksCredentialService } from '../provider/fireblocks/fireblocks-credential.service' +import { FireblocksCredentials, FireblocksInputCredentials } from '../provider/fireblocks/fireblocks.type' +import { + Connection, + ConnectionStatus, + ConnectionWithCredentials, + CreateConnection, + InitiateConnection, + PendingConnection, + UpdateConnection, + isActiveConnection, + isPendingConnection, + isRevokedConnection +} from '../type/connection.type' +import { Provider, ProviderCredentialService } from '../type/provider.type' + +type ProviderInputCredentialsMap = { + anchorage: AnchorageInputCredentials + fireblocks: FireblocksInputCredentials + bitgo: BitgoInputCredentials +} + +type ProviderCredentialsMap = { + anchorage: AnchorageCredentials + fireblocks: FireblocksCredentials + bitgo: BitgoCredentials +} + +@Injectable() +export class ConnectionService { + private readonly providerCredentialServices: { + [P in Provider]: ProviderCredentialService + } + + constructor( + private readonly connectionRepository: ConnectionRepository, + private readonly encryptionKeyService: EncryptionKeyService, + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2, + private readonly logger: LoggerService, + // Provider Specific Credential Services + fireblocksCredentialService: FireblocksCredentialService, + anchorageCredentialService: AnchorageCredentialService, + bitgoCredentialService: BitgoCredentialService + ) { + this.providerCredentialServices = { + [Provider.ANCHORAGE]: anchorageCredentialService, + [Provider.FIREBLOCKS]: fireblocksCredentialService, + [Provider.BITGO]: bitgoCredentialService + } + } + + async initiate(clientId: string, input: InitiateConnection): Promise { + this.logger.log('Initiate pending connection', { clientId }) + + const now = new Date() + const generatedCredentials = await this.generateProviderCredentials(input.provider) + const encryptionKey = await this.encryptionKeyService.generate(clientId) + const connection = { + clientId, + connectionId: input.connectionId || randomUUID(), + createdAt: now, + credentials: generatedCredentials, + encryptionPublicKey: encryptionKey.publicKey, + provider: input.provider, + revokedAt: undefined, + status: ConnectionStatus.PENDING, + updatedAt: now + } + + await this.connectionRepository.create(connection) + + this.logger.log('Pending connection created', { + clientId, + connection: connection.connectionId + }) + + return { + clientId: connection.clientId, + connectionId: connection.connectionId, + createdAt: connection.createdAt, + encryptionPublicKey: encryptionKey.publicKey, + provider: connection.provider, + status: connection.status, + updatedAt: connection.updatedAt + } + } + + async activate( + clientId: string, + input: SetRequired + ): Promise { + const pendingConnection = await this.connectionRepository.findById(clientId, input.connectionId) + const existingCredentials = await this.findCredentials(pendingConnection) + + if (!existingCredentials) { + throw new ConnectionInvalidCredentialsException({ + message: "Cannot activate a connection that's missing credentials", + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + } + + if (isPendingConnection(pendingConnection)) { + this.logger.log('Activate pending connection', { + clientId, + connectionId: pendingConnection.connectionId + }) + + const now = input.createdAt || new Date() + const provider = pendingConnection.provider + const inputCredentials = await this.getInputCredentials(provider, clientId, input) + let connection = { + ...pendingConnection, + clientId, + connectionId: input.connectionId, + createdAt: pendingConnection.createdAt, + credentials: existingCredentials, + label: input.label, + status: ConnectionStatus.ACTIVE, + updatedAt: now, + url: input.url + } + + // If a private key is provided in the input, it overrides the existing + // private and public key. Otherwise, adds the API key to the + // generated credentials. + if ('privateKey' in inputCredentials && inputCredentials.privateKey) { + this.logger.log('Existing private key is being overridden with a new one', { + clientId, + connectionId: connection.connectionId + }) + + const updatedCredentials = inputCredentials.privateKey + ? await this.buildProviderCredentials(provider, inputCredentials) + : { ...existingCredentials, apiKey: inputCredentials.apiKey } + + connection = { + ...connection, + credentials: updatedCredentials + } + } else { + connection = { + ...connection, + credentials: { + ...inputCredentials, + ...existingCredentials + } + } + } + + await this.connectionRepository.update(connection) + + this.eventEmitter.emit(ConnectionActivatedEvent.EVENT_NAME, new ConnectionActivatedEvent(connection)) + + return connection + } + + this.logger.log("Skip pending connection activation because status it's not pending or missing credentials", { + clientId, + connectionId: pendingConnection.connectionId, + status: pendingConnection.status + }) + + throw new ConnectionInvalidStatusException({ + from: pendingConnection.status, + to: ConnectionStatus.ACTIVE, + clientId, + connectionId: input.connectionId + }) + } + + async create(clientId: string, input: CreateConnection): Promise { + this.logger.log('Create active connection', { clientId }) + + // If a connection ID is provided, check if the connection already exists. + // If it does, activate the connection. + if (input.connectionId) { + if (await this.connectionRepository.exists(clientId, input.connectionId)) { + return this.activate(clientId, { + ...input, + connectionId: input.connectionId + }) + } + } + + const now = input.createdAt || new Date() + const provider = input.provider + const inputCredentials = await this.getInputCredentials(provider, clientId, input) + const credentials = await this.buildProviderCredentials(provider, inputCredentials) + const connection = { + clientId, + credentials, + createdAt: now, + connectionId: input.connectionId || randomUUID(), + label: input.label, + provider: input.provider, + url: input.url, + revokedAt: undefined, + status: ConnectionStatus.ACTIVE, + updatedAt: now + } + + await this.connectionRepository.create(connection) + + this.eventEmitter.emit(ConnectionActivatedEvent.EVENT_NAME, new ConnectionActivatedEvent(connection)) + + return connection + } + + async revoke(clientId: string, connectionId: string): Promise { + const connection = await this.connectionRepository.findById(clientId, connectionId) + + if (isRevokedConnection(connection)) { + this.logger.log("Skip connection revoke because it's already revoked", { + clientId, + connectionId: connection.connectionId + }) + + throw new ConnectionInvalidStatusException({ + from: connection.status, + to: ConnectionStatus.REVOKED, + clientId, + connectionId + }) + } + + if (isActiveConnection(connection) || isPendingConnection(connection)) { + this.logger.log('Revoke active or pending connection', { + clientId, + connectionId: connection.connectionId + }) + + await this.connectionRepository.update({ + ...connection, + clientId, + connectionId: connectionId, + credentials: null, + revokedAt: new Date(), + status: ConnectionStatus.REVOKED + }) + + return true + } + + throw new NotFoundException({ + message: 'Connection not found', + context: { clientId, connectionId } + }) + } + + async update(updateConnection: UpdateConnection): Promise { + const connection = await this.connectionRepository.findById( + updateConnection.clientId, + updateConnection.connectionId + ) + const hasCredentials = updateConnection.credentials || updateConnection.encryptedCredentials + const update = { + ...connection, + // Must include the existing createdAt value for integrity verification. + createdAt: connection.createdAt, + ...updateConnection + } + + if (hasCredentials) { + const inputCredentials = await this.getInputCredentials( + connection.provider, + connection.clientId, + updateConnection + ) + const credentials = await this.buildProviderCredentials(connection.provider, inputCredentials) + + await this.connectionRepository.update({ + ...update, + credentials + }) + } else { + await this.connectionRepository.update(update) + } + + // Strip credentials out of the connection. + return Connection.parse(update) + } + + private async getInputCredentials

( + provider: P, + clientId: string, + input: CreateConnection | UpdateConnection + ): Promise { + if (input.encryptedCredentials) { + const raw = await this.encryptionKeyService.decrypt(clientId, input.encryptedCredentials) + const json = JSON.parse(raw) + + return this.parseProviderInputCredentials(provider, json) + } + + if (input.credentials) { + if (this.configService.get('env') === Env.PRODUCTION) { + throw new BrokerException({ + message: 'Cannot create connection with plain credentials in production', + suggestedHttpStatusCode: HttpStatus.FORBIDDEN + }) + } + + return this.parseProviderInputCredentials(provider, input.credentials) + } + + throw new ConnectionInvalidCredentialsException() + } + + async findById(clientId: string, connectionId: string): Promise { + return this.connectionRepository.findById(clientId, connectionId) + } + + async findAll(clientId: string, options?: FindAllOptions): Promise> { + return this.connectionRepository.findAll(clientId, options) + } + + async findAllWithCredentials( + clientId: string, + options?: FindAllOptions + ): Promise> { + return this.connectionRepository.findAllWithCredentials(clientId, options) + } + + async findWithCredentialsById(clientId: string, connectionId: string): Promise { + return this.connectionRepository.findWithCredentialsById(clientId, connectionId) + } + + async findCredentials

(connection: { + provider: P + connectionId: string + }): Promise { + const json = await this.connectionRepository.findCredentialsJson(connection) + + if (json) { + try { + return this.parseProviderCredentials(connection.provider, json) + } catch (error) { + throw new ConnectionInvalidCredentialsException({ + message: `Invalid stored ${connection.provider} connection`, + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + origin: error + }) + } + } + + return null + } + + parseProviderCredentials

(provider: P, value: unknown): ProviderCredentialsMap[P] { + return this.providerCredentialServices[provider].parse(value) + } + + parseProviderInputCredentials

(provider: P, value: unknown): ProviderInputCredentialsMap[P] { + return this.providerCredentialServices[provider].parseInput(value) + } + + buildProviderCredentials

( + provider: P, + input: ProviderInputCredentialsMap[P] + ): Promise { + return this.providerCredentialServices[provider].build(input) + } + + generateProviderCredentials

(provider: P): Promise { + return this.providerCredentialServices[provider].generate() + } +} diff --git a/apps/vault/src/broker/core/service/known-destination.service.ts b/apps/vault/src/broker/core/service/known-destination.service.ts new file mode 100644 index 000000000..c1dbd8ca5 --- /dev/null +++ b/apps/vault/src/broker/core/service/known-destination.service.ts @@ -0,0 +1,68 @@ +import { LoggerService, PaginatedResult, TraceService } from '@narval/nestjs-shared' +import { HttpStatus, Inject, Injectable, NotImplementedException } from '@nestjs/common' +import { SpanStatusCode } from '@opentelemetry/api' +import { OTEL_ATTR_CONNECTION_PROVIDER } from '../../shared/constant' +import { BrokerException } from '../exception/broker.exception' +import { AnchorageKnownDestinationService } from '../provider/anchorage/anchorage-known-destination.service' +import { FireblocksKnownDestinationService } from '../provider/fireblocks/fireblocks-known-destination.service' +import { isActiveConnection } from '../type/connection.type' +import { KnownDestination as KnownDestinationNext } from '../type/known-destination.type' +import { + Provider, + ProviderKnownDestinationPaginationOptions, + ProviderKnownDestinationService +} from '../type/provider.type' +import { ConnectionScope } from '../type/scope.type' +import { ConnectionService } from './connection.service' + +@Injectable() +export class KnownDestinationService { + constructor( + private readonly connectionService: ConnectionService, + private readonly anchorageKnownDestinationService: AnchorageKnownDestinationService, + private readonly fireblocksKnownDestinationService: FireblocksKnownDestinationService, + private readonly logger: LoggerService, + @Inject(TraceService) private readonly traceService: TraceService + ) {} + + async findAll( + { clientId, connectionId }: ConnectionScope, + options?: ProviderKnownDestinationPaginationOptions + ): Promise> { + this.logger.log('Find provider known destinations', { clientId, connectionId, options }) + + return this.traceService.startActiveSpan(`${KnownDestinationService.name}.send`, async (span) => { + const connection = await this.connectionService.findWithCredentialsById(clientId, connectionId) + + span.setAttribute(OTEL_ATTR_CONNECTION_PROVIDER, connection.provider) + + if (isActiveConnection(connection)) { + return this.getProviderKnownDestinationService(connection.provider).findAll(connection, options) + } + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: 'Cannot find an active connection for the source' + }) + + span.end() + + throw new BrokerException({ + message: 'Cannot find an active connection for the source', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { connectionId, clientId } + }) + }) + } + + getProviderKnownDestinationService(provider: Provider): ProviderKnownDestinationService { + switch (provider) { + case Provider.ANCHORAGE: + return this.anchorageKnownDestinationService + case Provider.FIREBLOCKS: + return this.fireblocksKnownDestinationService + default: + throw new NotImplementedException(`Unsupported known destination for provider ${provider}`) + } + } +} diff --git a/apps/vault/src/broker/core/service/network.service.ts b/apps/vault/src/broker/core/service/network.service.ts new file mode 100644 index 000000000..aafc362e8 --- /dev/null +++ b/apps/vault/src/broker/core/service/network.service.ts @@ -0,0 +1,131 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager' +import { Inject, Injectable } from '@nestjs/common' +import { uniq } from 'lodash' +import { FindAllOptions, NetworkRepository } from '../../persistence/repository/network.repository' +import { ExternalNetwork, Network, NetworkMap } from '../type/network.type' +import { Provider } from '../type/provider.type' + +@Injectable() +export class NetworkService { + constructor( + private readonly networkRepository: NetworkRepository, + private readonly logger: LoggerService, + @Inject(CACHE_MANAGER) private cacheManager: Cache + ) {} + + async bulkCreate(networks: Network[]): Promise { + await this.networkRepository.bulkCreate(networks) + + const providers = uniq( + networks.flatMap(({ externalNetworks }) => externalNetworks).flatMap(({ provider }) => provider) + ) + + for (const provider of providers) { + await this.cacheManager.del(this.getListCacheKey(provider)) + } + + return networks + } + + async addExternalNetwork(networkId: string, externalNetwork: ExternalNetwork): Promise { + await this.networkRepository.addExternalNetwork(networkId, externalNetwork) + + await this.cacheManager.del(this.getListCacheKey(externalNetwork.provider)) + + return externalNetwork + } + + async findAll(options?: FindAllOptions): Promise { + // IMPORTANT: If you add new filters to the findAll method, you MUST + // rethink the cache strategy. This will only work as long there's a single + // filter. + const key = this.getListCacheKey(options?.filters?.provider) + + this.logger.log('Read network list from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Network list cache not found. Fallback to database', { key }) + + const assets = await this.networkRepository.findAll(options) + + await this.cacheManager.set(key, assets) + + return assets + } + + private getListCacheKey(provider?: Provider) { + if (provider) { + return `network:list:${provider}` + } + + return 'network:list' + } + + async findById(networkId: string): Promise { + const key = `network:${networkId.toLowerCase()}` + + this.logger.log('Read network by ID from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Network cache not found by ID. Fallback to database', { key }) + + const asset = await this.networkRepository.findById(networkId) + + await this.cacheManager.set(key, asset) + + return asset + } + + async findByExternalId(provider: Provider, externalId: string): Promise { + const key = `network:${provider}:${externalId.toLowerCase()}` + + this.logger.log('Read network by provider and external ID from cache', { key }) + + const cached = await this.cacheManager.get(key) + + if (cached) { + return cached + } + + this.logger.log('Network cache not found by provider and external ID. Fallback to database', { key }) + + const asset = await this.networkRepository.findByExternalId(provider, externalId) + + await this.cacheManager.set(key, asset) + + return asset + } + + /** + * Builds index structures for O(1) lookups of networks. + * + * This is a one-time O(n) operation at initialization to avoid O(n) array + * traversals on subsequent queries. All lookups become O(1) after indexing + * at the cost of O(n) additional memory. + */ + async buildProviderExternalIdIndex(provider: Provider): Promise { + const networks = await this.findAll({ filters: { provider } }) + const index = new Map() + + for (const network of networks) { + for (const externalNetwork of network.externalNetworks) { + if (externalNetwork.provider === provider) { + index.set(externalNetwork.externalId, network) + } + } + } + + return index + } +} diff --git a/apps/vault/src/broker/core/service/proxy.service.ts b/apps/vault/src/broker/core/service/proxy.service.ts new file mode 100644 index 000000000..b8713db55 --- /dev/null +++ b/apps/vault/src/broker/core/service/proxy.service.ts @@ -0,0 +1,56 @@ +import { Injectable, NotImplementedException } from '@nestjs/common' +import { ConnectionInvalidException } from '../exception/connection-invalid.exception' +import { AnchorageProxyService } from '../provider/anchorage/anchorage-proxy.service' +import { FireblocksProxyService } from '../provider/fireblocks/fireblocks-proxy.service' +import { isActiveConnection } from '../type/connection.type' +import { Provider, ProviderProxyService, ProxyResponse } from '../type/provider.type' +import { ConnectionScope } from '../type/scope.type' +import { ConnectionService } from './connection.service' + +type ProxyRequestOptions = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any + endpoint: string + method: string +} + +@Injectable() +export class ProxyService { + constructor( + private readonly connectionRepository: ConnectionService, + private readonly anchorageProxyService: AnchorageProxyService, + private readonly fireblocksProxyService: FireblocksProxyService + ) {} + + async forward({ clientId, connectionId }: ConnectionScope, options: ProxyRequestOptions): Promise { + const connection = await this.connectionRepository.findById(clientId, connectionId) + + if (!isActiveConnection(connection)) { + throw new ConnectionInvalidException({ + message: 'Connection is not active', + context: { connectionId, clientId, status: connection.status } + }) + } + + const credentials = await this.connectionRepository.findCredentials(connection) + + return this.getProviderProxyService(connection.provider).forward( + { + ...connection, + credentials + }, + options + ) + } + + private getProviderProxyService(provider: Provider): ProviderProxyService { + switch (provider) { + case Provider.ANCHORAGE: + return this.anchorageProxyService + case Provider.FIREBLOCKS: + return this.fireblocksProxyService + default: + throw new NotImplementedException(`Unsupported proxy for provider ${provider}`) + } + } +} diff --git a/apps/vault/src/broker/core/service/raw-account.service.ts b/apps/vault/src/broker/core/service/raw-account.service.ts new file mode 100644 index 000000000..9bdec023c --- /dev/null +++ b/apps/vault/src/broker/core/service/raw-account.service.ts @@ -0,0 +1,273 @@ +import { LoggerService, PaginatedResult, PaginationOptions } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { z } from 'zod' +import { AnchorageClient } from '../../http/client/anchorage.client' +import { FireblocksClient } from '../../http/client/fireblocks.client' +import { AccountRepository } from '../../persistence/repository/account.repository' +import { AssetRepository } from '../../persistence/repository/asset.repository' +import { BrokerException } from '../exception/broker.exception' +import { validateConnection as validateAnchorageConnection } from '../provider/anchorage/anchorage.util' +import { + buildFireblocksAssetWalletExternalId, + validateConnection as validateFireblocksConnection +} from '../provider/fireblocks/fireblocks.util' +import { Asset } from '../type/asset.type' +import { Network } from '../type/network.type' +import { Provider } from '../type/provider.type' +import { ConnectionService } from './connection.service' +import { NetworkService } from './network.service' + +export const RawAccount = z.object({ + provider: z.nativeEnum(Provider), + externalId: z.string(), + label: z.string(), + subLabel: z.string().optional(), + defaultAddress: z.string().optional().describe('The deposit address for the account, if there are multiple'), + network: Network.nullish().describe('The network of the account'), + assets: z + .array( + z.object({ + asset: Asset, + balance: z.string().optional().describe('The balance of the this asset in this account') + }) + ) + .optional() + .describe('The assets of the account') +}) +export type RawAccount = z.infer + +type FindAllOptions = { + filters?: { + networkId?: string + assetId?: string + namePrefix?: string + nameSuffix?: string + includeAddress?: boolean + } + pagination?: PaginationOptions +} + +@Injectable() +export class RawAccountService { + constructor( + private readonly accountRepository: AccountRepository, + private readonly connectionService: ConnectionService, + private readonly networkService: NetworkService, + private readonly assetRepository: AssetRepository, + private readonly anchorageClient: AnchorageClient, + private readonly fireblocksClient: FireblocksClient, + private readonly loggerService: LoggerService + ) {} + + async findAllPaginated( + clientId: string, + connectionId: string, + options?: FindAllOptions + ): Promise> { + const connection = await this.connectionService.findWithCredentialsById(clientId, connectionId) + + const assetFilter = options?.filters?.assetId + ? await this.assetRepository.findById(options.filters.assetId) + : undefined + const networkFilter = options?.filters?.networkId + ? await this.networkService.findById(options.filters.networkId) + : undefined + + if (connection.provider === Provider.ANCHORAGE) { + validateAnchorageConnection(connection) + const wallets = await this.anchorageClient.getWallets({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey + }) + + // Anchorage doesn't have filtering, so we fetch everything & then filter in-memory. + // TODO: add caching to make this faster for repeat queries. + const rawAccounts = await Promise.all( + wallets.map(async (wallet) => ({ + provider: Provider.ANCHORAGE, + externalId: wallet.walletId, + label: wallet.walletName, + subLabel: wallet.vaultName, + defaultAddress: wallet.depositAddress.address, + network: await this.networkService.findByExternalId(Provider.ANCHORAGE, wallet.networkId), + assets: await Promise.all( + wallet.assets.map(async (a) => ({ + asset: await this.assetRepository.findByExternalId(Provider.ANCHORAGE, a.assetType), + balance: a.totalBalance?.quantity + })) + ).then((a) => a.filter((a) => !!a.asset)) + })) + ) + + // Filter the raw accounts based on the filters + const filteredRawAccounts = rawAccounts.filter((a) => { + let matches = true + if (networkFilter) { + matches = a.network?.networkId === networkFilter.networkId + if (!matches) return false + } + if (assetFilter) { + matches = a.assets?.some((a) => a.asset?.assetId === assetFilter.assetId) + if (!matches) return false + } + if (options?.filters?.namePrefix) { + matches = a.label.toLowerCase().startsWith(options.filters.namePrefix.toLowerCase()) + if (!matches) return false + } + if (options?.filters?.nameSuffix) { + matches = a.subLabel.toLowerCase().endsWith(options.filters.nameSuffix.toLowerCase()) + if (!matches) return false + } + return true + }) + + return { + data: filteredRawAccounts.map((a) => RawAccount.parse(a)) + } + } else if (connection.provider === Provider.FIREBLOCKS) { + validateFireblocksConnection(connection) + let fbNetworkFilter: string | undefined + if (networkFilter) { + // Fireblocks doesn't have network filtering, so we'll find the base asset for the network and filter by that. + const baseAsset = await this.assetRepository.findNative(networkFilter.networkId) + fbNetworkFilter = baseAsset?.externalAssets.find((a) => a.provider === Provider.FIREBLOCKS)?.externalId + if (!fbNetworkFilter) { + throw new BrokerException({ + message: 'Fireblocks does not support this network', + suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, + context: { + networkId: networkFilter.networkId + } + }) + } + } + const fbAssetFilter = + assetFilter?.externalAssets.find((a) => a.provider === Provider.FIREBLOCKS)?.externalId || fbNetworkFilter + const response = await this.fireblocksClient.getVaultAccountsV2({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + options: { + namePrefix: options?.filters?.namePrefix, + nameSuffix: options?.filters?.nameSuffix, + limit: options?.pagination?.take, + after: options?.pagination?.cursor?.id, // TODO: Our cursor has too strict of typing, it can't pass-through + assetId: fbAssetFilter + } + }) + // In Fireblocks, a VaultAccount is not network-specific, so we'll map the AssetWallets to get what we call "Accounts" + // Map accounts to our format + const rawAccounts = await Promise.all( + response.accounts.map(async (account) => { + // 1. First map and filter assets + const mappedAssets = await Promise.all( + account.assets + .filter((a) => !a.hiddenOnUI) + .map(async (asset) => ({ + assetId: asset.id, + balance: asset.available, + asset: await this.assetRepository.findByExternalId(Provider.FIREBLOCKS, asset.id) + })) + ) + + // 2. Group assets by network + const assetsByNetwork = mappedAssets.reduce( + (acc, asset) => { + if (!asset.asset?.networkId) return acc + return { + ...acc, + [asset.asset.networkId]: [...(acc[asset.asset.networkId] || []), asset] + } + }, + {} as Record + ) + + // 3. Validate networks and get their Fireblocks IDs + const validNetworks = ( + await Promise.all( + Object.entries(assetsByNetwork).map(async ([networkId, assets]) => { + const network = await this.networkService.findById(networkId) + const fireblocksNetworkId = network?.externalNetworks.find( + (n) => n.provider === Provider.FIREBLOCKS + )?.externalId + + if (!network || !fireblocksNetworkId) return null + + return { + network, + fireblocksNetworkId, + assets + } + }) + ) + ).filter((item): item is NonNullable => item !== null) + + return validNetworks.map(({ network, fireblocksNetworkId, assets }) => { + return { + provider: Provider.FIREBLOCKS, + externalId: buildFireblocksAssetWalletExternalId({ + vaultId: account.id, + networkId: fireblocksNetworkId + }), + label: account.name || account.id, + subLabel: network?.networkId, + defaultAddress: '', // Fireblocks doesn't provide a default address at account level + network: Network.parse(network), + assets: assets + .map((a) => + a.asset + ? { + asset: a.asset, + balance: a.balance + } + : undefined + ) + .filter((a) => !!a) + } + }) + }) + ).then((a) => a.flat()) + + // Fetch the Address for the Base Asset + const rawAccountsWithAddresses = await Promise.all( + rawAccounts.map(async (account) => { + if (!account.network) return account + + // Get the fireblocks base asset for the network + const fireblocksBaseAsset = await this.assetRepository + .findNative(account.network?.networkId) + .then((a) => a?.externalAssets.find((a) => a.provider === Provider.FIREBLOCKS)?.externalId) + if (!fireblocksBaseAsset) return account + + // Fetch the base asset addresses. + const addresses = options?.filters?.includeAddress + ? await this.fireblocksClient.getAddresses({ + url: connection.url, + apiKey: connection.credentials.apiKey, + signKey: connection.credentials.privateKey, + vaultAccountId: account.externalId.split('-')[0], + assetId: fireblocksBaseAsset + }) + : [] + return { + ...account, + defaultAddress: addresses[0]?.address || undefined + } + }) + ) + + return { + data: rawAccountsWithAddresses.map((account) => RawAccount.parse(account)) // TODO: don't re-do the parse. It's an asset typedef because the .filter isn't inferred that it's no longer nullable. + } + } + + throw new BrokerException({ + message: 'Unsupported provider', + suggestedHttpStatusCode: HttpStatus.NOT_IMPLEMENTED, + context: { + provider: connection.provider + } + }) + } +} diff --git a/apps/vault/src/broker/core/service/scoped-sync.service.ts b/apps/vault/src/broker/core/service/scoped-sync.service.ts new file mode 100644 index 000000000..6969b2ca8 --- /dev/null +++ b/apps/vault/src/broker/core/service/scoped-sync.service.ts @@ -0,0 +1,325 @@ +import { LoggerService, PaginatedResult, TraceService } from '@narval/nestjs-shared' +import { HttpStatus, NotImplementedException } from '@nestjs/common' +import { Inject, Injectable } from '@nestjs/common/decorators' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { SpanStatusCode } from '@opentelemetry/api' +import { zip } from 'lodash' +import { v4 as uuid } from 'uuid' +import { FindAllOptions, ScopedSyncRepository } from '../../persistence/repository/scoped-sync.repository' +import { OTEL_ATTR_CONNECTION_ID, OTEL_ATTR_CONNECTION_PROVIDER, OTEL_ATTR_SYNC_ID } from '../../shared/constant' +import { ScopedSyncStartedEvent } from '../../shared/event/scoped-sync-started.event' +import { ScopedSyncException } from '../exception/scoped-sync.exception' +import { AnchorageScopedSyncService } from '../provider/anchorage/anchorage-scoped-sync.service' +import { FireblocksScopedSyncService } from '../provider/fireblocks/fireblocks-scoped-sync.service' +import { ConnectionWithCredentials } from '../type/connection.type' +import { Provider, ProviderScopedSyncService } from '../type/provider.type' +import { ConnectionScope } from '../type/scope.type' +import { + RawAccount, + RawAccountSyncFailure, + ScopedSync, + ScopedSyncResult, + ScopedSyncStarted, + ScopedSyncStatus, + StartScopedSync +} from '../type/scoped-sync.type' +import { AccountService } from './account.service' +import { AddressService } from './address.service' +import { NetworkService } from './network.service' +import { WalletService } from './wallet.service' + +@Injectable() +export class ScopedSyncService { + constructor( + private readonly scopedSyncRepository: ScopedSyncRepository, + private readonly anchorageScopedSyncService: AnchorageScopedSyncService, + private readonly fireblocksScopedSyncService: FireblocksScopedSyncService, + private readonly walletService: WalletService, + private readonly accountService: AccountService, + private readonly addressService: AddressService, + private readonly networkService: NetworkService, + private readonly eventEmitter: EventEmitter2, + private readonly logger: LoggerService, + @Inject(TraceService) private readonly traceService: TraceService + ) {} + + async start(connections: ConnectionWithCredentials[], rawAccounts: RawAccount[]): Promise { + this.logger.log('Start connections scopedSync', { + connectionsCount: connections.length, + connectionIds: connections.map((connectionId) => connectionId) + }) + + const notSyncingConnections: ConnectionWithCredentials[] = [] + + for (const connection of connections) { + const inProgress = await this.scopedSyncRepository.exists({ + connectionId: connection.connectionId, + clientId: connection.clientId, + status: ScopedSyncStatus.PROCESSING + }) + + if (inProgress) { + throw new ScopedSyncException({ + message: 'There is already a Scoped Sync in progress for requested connections', + suggestedHttpStatusCode: HttpStatus.CONFLICT, + context: { + conflictingConnectionId: connection.connectionId + } + }) + } + + notSyncingConnections.push(connection) + } + + if (notSyncingConnections.length) { + const now = new Date() + + const scopedSyncs = await this.scopedSyncRepository.bulkCreate( + notSyncingConnections.map(({ connectionId, clientId }) => + this.toProcessingScopedSync({ + clientId, + connectionId, + createdAt: now, + scopedSyncId: uuid(), + rawAccounts + }) + ) + ) + + for (const [scopedSync, connection] of zip(scopedSyncs, notSyncingConnections)) { + if (scopedSync && connection) { + // NOTE: Emits an event that will delegate the scopedSync process to + // another worker, allowing to unblock the request. The event handler + // will then invoke the `ScopedSyncService.scopedSync` method. + this.eventEmitter.emit(ScopedSyncStartedEvent.EVENT_NAME, new ScopedSyncStartedEvent(scopedSync, connection)) + } + } + + return { started: true, scopedSyncs } + } + + this.logger.log('Skip scopedSync because active connections list is empty') + + return { started: false, scopedSyncs: [] } + } + + async scopedSync(scopedSync: ScopedSync, connection: ConnectionWithCredentials): Promise { + const { clientId, scopedSyncId } = scopedSync + const { provider, connectionId } = connection + const context = { + clientId, + scopedSyncId, + connectionId, + provider + } + + this.logger.log('Scoped Sync connection', context) + + const span = this.traceService.startSpan(`${ScopedSyncService.name}.scopedSync`, { + attributes: { + [OTEL_ATTR_SYNC_ID]: scopedSyncId, + [OTEL_ATTR_CONNECTION_ID]: connectionId, + [OTEL_ATTR_CONNECTION_PROVIDER]: provider + } + }) + + let result: ScopedSyncResult | null = null + + try { + const networks = await this.networkService.buildProviderExternalIdIndex(provider) + const { data: existingAccounts } = await this.accountService.findAll( + { clientId, connectionId }, + { + pagination: { disabled: true } + } + ) + + result = await this.getProviderScopedSyncService(connection.provider).scopeSync({ + rawAccounts: scopedSync.rawAccounts, + connection, + networks, + existingAccounts + }) + } catch (error) { + this.logger.error('ScopedSync connection failed', { ...context, error }) + + span.recordException(error) + span.setStatus({ code: SpanStatusCode.ERROR }) + + return await this.fail(scopedSync, error, result?.failures || []) + } finally { + // The execute method has its own span. + span.end() + } + + if (result.failures.length && !result.accounts.length) { + this.logger.error('ScopedSync connection failed', { ...context, result }) + } + + return await this.execute(scopedSync, result) + } + + private toProcessingScopedSync( + input: StartScopedSync & { connectionId: string; createdAt?: Date; scopedSyncId?: string } + ): ScopedSync { + return { + ...input, + completedAt: undefined, + connectionId: input.connectionId, + createdAt: input.createdAt || new Date(), + status: ScopedSyncStatus.PROCESSING, + scopedSyncId: input.scopedSyncId || uuid(), + rawAccounts: input.rawAccounts + } + } + + async findAll(scope: ConnectionScope, options?: FindAllOptions): Promise> { + return this.scopedSyncRepository.findAll(scope, options) + } + + async findById(scope: ConnectionScope, scopedSyncId: string): Promise { + return this.scopedSyncRepository.findById(scope, scopedSyncId) + } + + // TODO: pessimist lock if there's already a scopedSync in process for the given + async execute(scopedSync: ScopedSync, result: ScopedSyncResult): Promise { + const { scopedSyncId } = scopedSync + + const span = this.traceService.startSpan(`${ScopedSyncService.name}.execute`, { + attributes: { [OTEL_ATTR_SYNC_ID]: scopedSyncId } + }) + + const { wallets, accounts, addresses, failures } = result + + this.logger.log('Raw Account synchronization failures', { + scopedSyncId, + failuresCount: failures?.length, + failures + }) + + this.logger.log('Raw Account synchronization successes', { + wallets: wallets.length, + accounts: accounts.length, + addresses: addresses.length + }) + + try { + await this.walletService.bulkUpsert(wallets) + await this.accountService.bulkUpsert(accounts) + await this.addressService.bulkCreate(addresses) + + const totalItems = result.wallets.length + result.accounts.length + result.addresses.length + if (totalItems === 0) { + throw new ScopedSyncException({ + message: 'No raw account was successfully mapped', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { failures: result.failures } + }) + } + + return await this.complete(scopedSync, result) + } catch (error) { + return await this.fail(scopedSync, error, result.failures) + } finally { + span.end() + } + } + + async complete(scopedSync: ScopedSync, result: ScopedSyncResult): Promise { + const { clientId, scopedSyncId } = scopedSync + + const totalItems = result.wallets.length + result.accounts.length + result.addresses.length + const hasSuccesses = totalItems > 0 + const hasFailures = result.failures.length > 0 + + const status = hasSuccesses && hasFailures ? ScopedSyncStatus.PARTIAL_SUCCESS : ScopedSyncStatus.SUCCESS + + if (status === ScopedSyncStatus.PARTIAL_SUCCESS) { + return await this.partialSuccess(scopedSync, result.failures) + } + + this.logger.log('Scoped Sync completed successfuly', { clientId, scopedSyncId }) + + const span = this.traceService.startSpan(`${ScopedSyncService.name}.complete`, { + attributes: { [OTEL_ATTR_SYNC_ID]: scopedSyncId } + }) + + const completedScopedSync = { + ...scopedSync, + status, + completedAt: scopedSync.completedAt || new Date(), + failures: [] + } + + await this.scopedSyncRepository.update(completedScopedSync) + + span.end() + + return completedScopedSync + } + + async partialSuccess(scopedSync: ScopedSync, failures: RawAccountSyncFailure[]): Promise { + const { clientId, scopedSyncId } = scopedSync + + this.logger.log('Scoped Sync partially successful', { clientId, scopedSyncId }) + + const span = this.traceService.startSpan(`${ScopedSyncService.name}.partial_success`, { + attributes: { [OTEL_ATTR_SYNC_ID]: scopedSyncId } + }) + + const completedScopedSync = { + ...scopedSync, + status: ScopedSyncStatus.PARTIAL_SUCCESS, + completedAt: scopedSync.completedAt || new Date(), + failures + } + + await this.scopedSyncRepository.update(completedScopedSync) + + span.end() + + return completedScopedSync + } + + async fail(scopedSync: ScopedSync, error: Error, failures: RawAccountSyncFailure[]): Promise { + const { clientId, scopedSyncId } = scopedSync + + this.logger.log('Scoped Sync fail', { clientId, scopedSyncId, error }) + + const span = this.traceService.startSpan(`${ScopedSyncService.name}.fail`, { + attributes: { [OTEL_ATTR_SYNC_ID]: scopedSyncId } + }) + + span.recordException(error) + span.setStatus({ code: SpanStatusCode.ERROR }) + + const failedScopedSync = { + ...scopedSync, + status: ScopedSyncStatus.FAILED, + error: { + name: error.name, + message: error.message, + traceId: this.traceService.getActiveSpan()?.spanContext().traceId + }, + completedAt: scopedSync.completedAt || new Date(), + failures + } + + await this.scopedSyncRepository.update(failedScopedSync) + + span.end() + + return failedScopedSync + } + + private getProviderScopedSyncService(provider: Provider): ProviderScopedSyncService { + switch (provider) { + case Provider.ANCHORAGE: + return this.anchorageScopedSyncService + case Provider.FIREBLOCKS: + return this.fireblocksScopedSyncService + default: + throw new NotImplementedException(`Unsupported Scoped Sync for provider ${provider}`) + } + } +} diff --git a/apps/vault/src/broker/core/service/sync.service.ts b/apps/vault/src/broker/core/service/sync.service.ts new file mode 100644 index 000000000..13b305f79 --- /dev/null +++ b/apps/vault/src/broker/core/service/sync.service.ts @@ -0,0 +1,17 @@ +import { PaginatedResult } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common/decorators' +import { FindAllOptions, SyncRepository } from '../../persistence/repository/sync.repository' +import { ConnectionScope } from '../type/scope.type' +import { Sync } from '../type/sync.type' + +@Injectable() +export class SyncService { + constructor(private readonly syncRepository: SyncRepository) {} + async findAll(scope: ConnectionScope, options?: FindAllOptions): Promise> { + return this.syncRepository.findAll(scope, options) + } + + async findById(scope: ConnectionScope, syncId: string): Promise { + return this.syncRepository.findById(scope, syncId) + } +} diff --git a/apps/vault/src/broker/core/service/transfer-asset.service.ts b/apps/vault/src/broker/core/service/transfer-asset.service.ts new file mode 100644 index 000000000..64f6545e6 --- /dev/null +++ b/apps/vault/src/broker/core/service/transfer-asset.service.ts @@ -0,0 +1,264 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { AssetException } from '../exception/asset.exception' +import { Network } from '../type/network.type' +import { Provider } from '../type/provider.type' +import { TransferAsset } from '../type/transfer.type' +import { getExternalAsset } from '../util/asset.util' +import { getExternalNetwork } from '../util/network.util' +import { AssetService } from './asset.service' +import { NetworkService } from './network.service' + +export type ResolvedTransferAsset = { + network: Network + assetId: string | null + assetExternalId: string +} + +export type FindByExternalIdFallback = (externalAssetId: string) => Promise + +export type FindByOnchainIdFallback = (network: Network, onchainId: string) => Promise + +export type ResolveTransferAssetParams = { + transferAsset: TransferAsset + provider: Provider + findByExternalIdFallback?: FindByExternalIdFallback + findByOnchainIdFallback?: FindByOnchainIdFallback +} + +@Injectable() +export class TransferAssetService { + constructor( + private readonly assetService: AssetService, + private readonly networkService: NetworkService, + private readonly logger: LoggerService + ) {} + + /** + * Resolves a transfer asset using the following strategy: + * 1. By external asset ID if provided + * 2. By internal asset ID if provided + * 3. By network ID + onchain address if both provided + * 4. Falls back to native asset of the network if only network ID is provided + * + * For external ID and onchain address resolution, supports fallback + * functions that can handle assets not yet listed. Fallbacks are called only + * after an initial lookup fails to find the asset in our database. + * + * @throws {AssetException} When asset cannot be resolved or required network + * info is missing + */ + async resolve(params: ResolveTransferAssetParams): Promise { + const { transferAsset, provider, findByExternalIdFallback, findByOnchainIdFallback } = params + + this.logger.log(`Resolve ${provider} transfer asset`, { transferAsset, provider }) + + if (transferAsset.externalAssetId) { + return this.findByExternalId(provider, transferAsset.externalAssetId, findByExternalIdFallback) + } + + if (transferAsset.assetId) { + return this.findByAssetId(provider, transferAsset.assetId) + } + + if (!transferAsset.networkId) { + throw new AssetException({ + message: 'Cannot find transfer asset without network ID', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { transferAsset } + }) + } + + if (transferAsset.address) { + return this.findByOnchainId(provider, transferAsset.networkId, transferAsset.address, findByOnchainIdFallback) + } + + return this.findNative(provider, transferAsset.networkId) + } + + private async findByExternalId( + provider: Provider, + externalAssetId: string, + findByExternalIdFallback?: FindByExternalIdFallback + ): Promise { + this.logger.log('Find asset by external ID', { provider, externalAssetId }) + + const asset = await this.assetService.findByExternalId(provider, externalAssetId) + + if (asset) { + const network = await this.networkService.findById(asset.networkId) + if (!network) { + throw new AssetException({ + message: 'Asset network not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { asset } + }) + } + + const externalAsset = getExternalAsset(asset, provider) + if (!externalAsset) { + throw new AssetException({ + message: 'External asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { asset, provider } + }) + } + + return { + network, + assetId: asset.assetId, + assetExternalId: externalAsset.externalId + } + } + + if (findByExternalIdFallback) { + this.logger.log('Asset not listed. Calling given findByExternalIdFallback function', { + provider, + externalAssetId + }) + + return findByExternalIdFallback(externalAssetId) + } + + throw new AssetException({ + message: 'Transfer asset not found by external ID', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { externalAssetId, provider } + }) + } + + private async findByAssetId(provider: Provider, assetId: string): Promise { + this.logger.log('Find asset by ID', { provider, assetId }) + + const asset = await this.assetService.findById(assetId) + if (!asset) { + throw new AssetException({ + message: 'Asset not found by ID', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { assetId } + }) + } + + const externalAsset = getExternalAsset(asset, provider) + if (!externalAsset) { + throw new AssetException({ + message: 'External asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { assetId, asset, provider } + }) + } + + const network = await this.networkService.findById(asset.networkId) + if (!network) { + throw new AssetException({ + message: 'Asset network not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { assetId, asset } + }) + } + + return { + network, + assetId: asset.assetId, + assetExternalId: externalAsset.externalId + } + } + + private async findByOnchainId( + provider: Provider, + networkId: string, + onchainId: string, + findByOnchainIdFallback?: FindByOnchainIdFallback + ): Promise { + this.logger.log('Find asset by network and onchain ID', { provider, networkId, onchainId }) + + const network = await this.networkService.findById(networkId) + if (!network) { + throw new AssetException({ + message: 'Asset network not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { networkId } + }) + } + + const externalNetwork = getExternalNetwork(network, provider) + if (!externalNetwork) { + throw new AssetException({ + message: `Provider ${provider} unlisted network`, + suggestedHttpStatusCode: HttpStatus.NOT_IMPLEMENTED, + context: { provider, network } + }) + } + + const asset = await this.assetService.findByOnchainId(network.networkId, onchainId) + if (asset) { + const externalAsset = getExternalAsset(asset, provider) + if (!externalAsset) { + throw new AssetException({ + message: 'External asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { onchainId, asset, provider } + }) + } + + return { + network, + assetId: asset.assetId, + assetExternalId: externalAsset.externalId + } + } + + if (findByOnchainIdFallback) { + this.logger.log('Asset not listed. Calling given findByOnchainIdFallback function', { + network, + provider, + onchainId + }) + + return findByOnchainIdFallback(network, onchainId) + } + + throw new AssetException({ + message: 'Transfer asset not found by network and onchain ID', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { network, onchainId } + }) + } + + private async findNative(provider: Provider, networkId: string): Promise { + this.logger.log('Find native asset', { provider, networkId }) + + const asset = await this.assetService.findNative(networkId) + if (!asset) { + throw new AssetException({ + message: 'Native asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { networkId } + }) + } + + const externalAsset = getExternalAsset(asset, provider) + if (!externalAsset) { + throw new AssetException({ + message: 'External asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { provider, asset } + }) + } + + const network = await this.networkService.findById(networkId) + if (!network) { + throw new AssetException({ + message: 'Network of native asset not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND, + context: { networkId, asset } + }) + } + + return { + network, + assetId: asset.assetId, + assetExternalId: externalAsset.externalId + } + } +} diff --git a/apps/vault/src/broker/core/service/transfer.service.ts b/apps/vault/src/broker/core/service/transfer.service.ts new file mode 100644 index 000000000..180b23c67 --- /dev/null +++ b/apps/vault/src/broker/core/service/transfer.service.ts @@ -0,0 +1,89 @@ +import { LoggerService, TraceService } from '@narval/nestjs-shared' +import { HttpStatus, Inject, Injectable, NotImplementedException } from '@nestjs/common' +import { SpanStatusCode } from '@opentelemetry/api' +import { TransferRepository } from '../../persistence/repository/transfer.repository' +import { OTEL_ATTR_CONNECTION_PROVIDER } from '../../shared/constant' +import { BrokerException } from '../exception/broker.exception' +import { AnchorageTransferService } from '../provider/anchorage/anchorage-transfer.service' +import { FireblocksTransferService } from '../provider/fireblocks/fireblocks-transfer.service' +import { isActiveConnection } from '../type/connection.type' +import { Provider, ProviderTransferService } from '../type/provider.type' +import { ConnectionScope } from '../type/scope.type' +import { InternalTransfer, SendTransfer } from '../type/transfer.type' +import { ConnectionService } from './connection.service' + +@Injectable() +export class TransferService { + constructor( + private readonly transferRepository: TransferRepository, + private readonly connectionService: ConnectionService, + private readonly anchorageTransferService: AnchorageTransferService, + private readonly fireblocksTransferService: FireblocksTransferService, + private readonly logger: LoggerService, + @Inject(TraceService) private readonly traceService: TraceService + ) {} + + async findById({ clientId, connectionId }: ConnectionScope, transferId: string): Promise { + const span = this.traceService.startSpan(`${TransferService.name}.findById`) + + const connection = await this.connectionService.findWithCredentialsById(clientId, connectionId) + const transfer = await this.getProviderTransferService(connection.provider).findById(connection, transferId) + + span.end() + + return transfer + } + + async bulkCreate(transfers: InternalTransfer[]): Promise { + return this.transferRepository.bulkCreate(transfers) + } + + async send({ clientId, connectionId }: ConnectionScope, sendTransfer: SendTransfer): Promise { + this.logger.log('Send transfer', { clientId, sendTransfer }) + + const span = this.traceService.startSpan(`${TransferService.name}.send`) + + const connection = await this.connectionService.findWithCredentialsById(clientId, connectionId) + + if (isActiveConnection(connection)) { + span.setAttribute(OTEL_ATTR_CONNECTION_PROVIDER, connection.provider) + + if (await this.transferRepository.existsByIdempotenceId(clientId, sendTransfer.idempotenceId)) { + throw new BrokerException({ + message: 'Transfer idempotence ID already used', + suggestedHttpStatusCode: HttpStatus.CONFLICT, + context: { idempotenceId: sendTransfer.idempotenceId } + }) + } + + const transfer = await this.getProviderTransferService(connection.provider).send(connection, sendTransfer) + + span.end() + + return transfer + } + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: 'Cannot find an active connection for the source' + }) + span.end() + + throw new BrokerException({ + message: 'Cannot find an active connection for the source', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + context: { connectionId, clientId } + }) + } + + private getProviderTransferService(provider: Provider): ProviderTransferService { + switch (provider) { + case Provider.ANCHORAGE: + return this.anchorageTransferService + case Provider.FIREBLOCKS: + return this.fireblocksTransferService + default: + throw new NotImplementedException(`Unsupported transfer for provider ${provider}`) + } + } +} diff --git a/apps/vault/src/broker/core/service/wallet.service.ts b/apps/vault/src/broker/core/service/wallet.service.ts new file mode 100644 index 000000000..35138c46e --- /dev/null +++ b/apps/vault/src/broker/core/service/wallet.service.ts @@ -0,0 +1,34 @@ +import { PaginatedResult } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { FindAllOptions, WalletRepository } from '../../persistence/repository/wallet.repository' +import { UpdateWallet, Wallet } from '../type/indexed-resources.type' +import { ConnectionScope } from '../type/scope.type' + +@Injectable() +export class WalletService { + constructor(private readonly walletRepository: WalletRepository) {} + + async bulkCreate(wallets: Wallet[]): Promise { + return this.walletRepository.bulkCreate(wallets) + } + + async bulkUpsert(wallets: Wallet[]): Promise { + return this.walletRepository.bulkUpsert(wallets) + } + + async bulkUpdate(wallets: UpdateWallet[]): Promise { + return Promise.all(wallets.map((wallet) => this.update(wallet))) + } + + async update(wallet: UpdateWallet): Promise { + return this.walletRepository.update(wallet) + } + + async findAll(scope: ConnectionScope, options?: FindAllOptions): Promise> { + return this.walletRepository.findAll(scope, options) + } + + async findById(scope: ConnectionScope, walletId: string): Promise { + return this.walletRepository.findById(scope, walletId) + } +} diff --git a/apps/vault/src/broker/core/type/asset.type.ts b/apps/vault/src/broker/core/type/asset.type.ts new file mode 100644 index 000000000..9f780afb0 --- /dev/null +++ b/apps/vault/src/broker/core/type/asset.type.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' +import { Provider } from './provider.type' + +export const ExternalAsset = z.object({ + externalId: z.string(), + provider: z.nativeEnum(Provider) +}) +export type ExternalAsset = z.infer + +export const Asset = z.object({ + assetId: z.string(), + createdAt: z.date().optional(), + decimals: z.number().nullable(), + externalAssets: z.array(ExternalAsset).default([]), + name: z.string(), + networkId: z.string(), + onchainId: z.string().toLowerCase().nullable(), + symbol: z.string().nullable() +}) +export type Asset = z.infer diff --git a/apps/vault/src/broker/core/type/connection.type.ts b/apps/vault/src/broker/core/type/connection.type.ts new file mode 100644 index 000000000..c7317e5ef --- /dev/null +++ b/apps/vault/src/broker/core/type/connection.type.ts @@ -0,0 +1,110 @@ +import { rsaPublicKeySchema } from '@narval/signature' +import { z } from 'zod' +import { Provider } from './provider.type' + +// +// Type +// + +export const ConnectionStatus = { + PENDING: 'pending', + ACTIVE: 'active', + REVOKED: 'revoked' +} as const +export type ConnectionStatus = (typeof ConnectionStatus)[keyof typeof ConnectionStatus] + +export const ConnectionWithCredentials = z.object({ + clientId: z.string(), + connectionId: z.string(), + createdAt: z.date(), + label: z.string().optional(), + provider: z.nativeEnum(Provider), + revokedAt: z.date().optional(), + status: z.nativeEnum(ConnectionStatus).default(ConnectionStatus.ACTIVE), + updatedAt: z.date(), + url: z.string().url().optional(), + credentials: z.unknown().nullish() +}) +export type ConnectionWithCredentials = z.infer + +// +// Read Connection (without credentials) +// + +const ReadConnection = ConnectionWithCredentials.omit({ credentials: true }).strip() + +export const ActiveConnection = ReadConnection.extend({ + status: z.literal(ConnectionStatus.ACTIVE), + url: z.string().url() +}) +export type ActiveConnection = z.infer + +const RevokedConnection = ReadConnection.extend({ + status: z.literal(ConnectionStatus.REVOKED), + revokedAt: z.date() +}) +export type RevokedConnection = z.infer + +export const PendingConnection = ReadConnection.extend({ + status: z.literal(ConnectionStatus.PENDING), + encryptionPublicKey: rsaPublicKeySchema.optional() +}) +export type PendingConnection = z.infer + +export const Connection = z.discriminatedUnion('status', [ActiveConnection, RevokedConnection, PendingConnection]) +export type Connection = z.infer + +// +// Operation +// + +export const InitiateConnection = z.object({ + connectionId: z.string().optional(), + provider: z.nativeEnum(Provider) +}) +export type InitiateConnection = z.infer + +export const CreateConnection = z.object({ + connectionId: z.string().optional(), + createdAt: z.date().optional(), + encryptedCredentials: z.string().optional().describe('RSA encrypted JSON string of the credentials'), + label: z.string().optional(), + provider: z.nativeEnum(Provider), + url: z.string().url(), + credentials: z.unknown().optional() +}) +export type CreateConnection = z.infer + +export const UpdateConnection = z.object({ + clientId: z.string(), + connectionId: z.string(), + credentials: z.unknown().nullish(), + encryptedCredentials: z.string().optional().describe('RSA encrypted JSON string of the credentials'), + label: z.string().optional(), + status: z.nativeEnum(ConnectionStatus).optional(), + updatedAt: z.date().optional(), + url: z.string().url().optional() +}) +export type UpdateConnection = z.infer + +// +// Type Guard +// + +export const isPendingConnection = ( + connection: Connection | ConnectionWithCredentials +): connection is PendingConnection => { + return connection.status === ConnectionStatus.PENDING +} + +export const isActiveConnection = ( + connection: Connection | ConnectionWithCredentials +): connection is ActiveConnection => { + return connection.status === ConnectionStatus.ACTIVE +} + +export const isRevokedConnection = ( + connection: Connection | ConnectionWithCredentials +): connection is RevokedConnection => { + return connection.status === ConnectionStatus.REVOKED +} diff --git a/apps/vault/src/broker/core/type/indexed-resources.type.ts b/apps/vault/src/broker/core/type/indexed-resources.type.ts new file mode 100644 index 000000000..48632292c --- /dev/null +++ b/apps/vault/src/broker/core/type/indexed-resources.type.ts @@ -0,0 +1,51 @@ +import { z } from 'zod' +import { Provider } from './provider.type' + +export const Address = z.object({ + accountId: z.string(), + address: z.string(), + addressId: z.string(), + clientId: z.string(), + connectionId: z.string(), + createdAt: z.date(), + externalId: z.string(), + provider: z.nativeEnum(Provider), + updatedAt: z.date() +}) +export type Address = z.infer + +export const Account = z.object({ + accountId: z.string(), + addresses: z.array(Address).optional(), + clientId: z.string(), + connectionId: z.string(), + createdAt: z.date(), + externalId: z.string(), + label: z.string().nullable().optional(), + networkId: z.string(), + provider: z.nativeEnum(Provider), + updatedAt: z.date(), + walletId: z.string() +}) +export type Account = z.infer + +export const Wallet = z.object({ + accounts: z.array(Account).optional(), + clientId: z.string(), + connectionId: z.string(), + createdAt: z.date(), + externalId: z.string(), + label: z.string().nullable().optional(), + provider: z.nativeEnum(Provider), + updatedAt: z.date(), + walletId: z.string() +}) +export type Wallet = z.infer + +export const UpdateWallet = Wallet.pick({ + walletId: true, + clientId: true, + label: true, + updatedAt: true +}) +export type UpdateWallet = z.infer diff --git a/apps/vault/src/broker/core/type/known-destination.type.ts b/apps/vault/src/broker/core/type/known-destination.type.ts new file mode 100644 index 000000000..4e703e770 --- /dev/null +++ b/apps/vault/src/broker/core/type/known-destination.type.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' +import { Provider } from './provider.type' + +export const KnownDestination = z.object({ + clientId: z.string(), + connectionId: z.string(), + provider: z.nativeEnum(Provider), + label: z.string().nullable().optional(), + externalId: z.string(), + externalClassification: z.string().nullable().optional(), + address: z.string().toLowerCase(), + assetId: z.string().nullable().optional(), + networkId: z.string() +}) +export type KnownDestination = z.infer diff --git a/apps/vault/src/broker/core/type/network.type.ts b/apps/vault/src/broker/core/type/network.type.ts new file mode 100644 index 000000000..a6e5f03f1 --- /dev/null +++ b/apps/vault/src/broker/core/type/network.type.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' +import { Provider } from './provider.type' + +export const ExternalNetwork = z.object({ + externalId: z.string(), + provider: z.nativeEnum(Provider) +}) +export type ExternalNetwork = z.infer + +export const Network = z.object({ + networkId: z.string(), + coinType: z.number().nullable(), + name: z.string(), + externalNetworks: z.array(ExternalNetwork).default([]), + createdAt: z.coerce.date().optional() +}) +export type Network = z.infer + +export const NetworkMap = z.map(z.string(), Network) +export type NetworkMap = z.infer diff --git a/apps/vault/src/broker/core/type/provider.type.ts b/apps/vault/src/broker/core/type/provider.type.ts new file mode 100644 index 000000000..e374fc00e --- /dev/null +++ b/apps/vault/src/broker/core/type/provider.type.ts @@ -0,0 +1,158 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { PaginatedResult } from '@narval/nestjs-shared' +import { HttpStatus } from '@nestjs/common' +import { ConnectionWithCredentials } from './connection.type' +import { KnownDestination as KnownDestinationNext } from './known-destination.type' +import { ScopedSyncContext, ScopedSyncResult } from './scoped-sync.type' +import { InternalTransfer, SendTransfer, Transfer } from './transfer.type' + +export const Provider = { + ANCHORAGE: 'anchorage', + FIREBLOCKS: 'fireblocks', + BITGO: 'bitgo' +} as const +export type Provider = (typeof Provider)[keyof typeof Provider] + +// +// Sync +// + +export interface ProviderScopedSyncService { + scopeSync(context: ScopedSyncContext): Promise +} + +// +// Transfer +// + +export interface ProviderTransferService { + /** + * Finds a transfer by its ID. + * + * @param connection - The active connection with credentials required for + * accessing the transfer data. + * @param transferId - The unique identifier of the transfer to be retrieved. + * + * @returns A promise that resolves to the transfer object if found. + */ + findById(connection: ConnectionWithCredentials, transferId: string): Promise + + /** + * Sends a transfer using the provided active connection and transfer + * details. + * + * @param connection - The active connection with credentials required for + * sending the transfer. + * @param sendTransfer - The details of the transfer to be sent. + * + * @returns A promise that resolves to the internal transfer object after the + * transfer is successfully sent. + */ + send(connection: ConnectionWithCredentials, sendTransfer: SendTransfer): Promise +} + +// +// Proxy +// + +/** + * Options for making a proxy request, including connection ID, request data, + * endpoint, and HTTP method. + */ +export type ProxyRequestOptions = { + data?: any + nonce?: string + endpoint: string + method: string +} + +/** + * Represents the response from a proxy request, including the response data, + * HTTP status code, and headers. + */ +export type ProxyResponse = { + data: any + code: HttpStatus + headers: Record +} + +export interface ProviderProxyService { + /** + * Forwards a request to a specified endpoint using the provided active + * connection and request options. + * + * @param connection - The active connection with credentials required for + * forwarding the request. + * @param options - The options for the proxy request, including connection + * ID, request data, endpoint, and HTTP method. + * + * @returns A promise that resolves to the proxy response, containing the + * response data, HTTP status code, and headers. + */ + forward(connection: ConnectionWithCredentials, options: ProxyRequestOptions): Promise +} + +// +// Credential +// + +/** + * Defines methods for managing credentials in various formats. This includes + * handling credentials used during data operations, typically in JSON Web Key + * (JWK) format, and credentials in transit, which may be represented as + * strings of private keys. + */ +export interface ProviderCredentialService { + /** + * Parses a value into the final form of credentials used within the + * repository or service. + * + * @param value - The value to be parsed into credentials. + * @returns The parsed credentials. + */ + parse(value: unknown): Credentials + + /** + * Parses input credentials, ensuring the correct format of string + * representations for private keys when necessary. + * + * @param value - The input value to be parsed into input credentials. + * @returns The parsed input credentials. + */ + parseInput(value: unknown): InputCredentials + + /** + * Builds the final form of credentials from input credentials, validating + * and converting them from hexadecimal to JSON Web Key (JWK) format. + * + * @param input - The input credentials to be converted. + * @returns A promise that resolves to the final form of credentials. + */ + build(input: InputCredentials): Promise + + /** + * Generates signing keys for credential operations. + * + * @param options - Provider-specific configuration options for key + * generation. + * @returns A promise that resolves to the generated credentials. + */ + generate>(options?: Options): Promise +} + +// +// Known Destination +// + +export type ProviderKnownDestinationPaginationOptions = { + cursor?: string + limit?: number +} + +export interface ProviderKnownDestinationService { + findAll( + connection: ConnectionWithCredentials, + options?: ProviderKnownDestinationPaginationOptions + ): Promise> +} diff --git a/apps/vault/src/broker/core/type/scope.type.ts b/apps/vault/src/broker/core/type/scope.type.ts new file mode 100644 index 000000000..f6cd215d4 --- /dev/null +++ b/apps/vault/src/broker/core/type/scope.type.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const ConnectionScope = z.object({ + clientId: z.string(), + connectionId: z.string() +}) +export type ConnectionScope = z.infer diff --git a/apps/vault/src/broker/core/type/scoped-sync.type.ts b/apps/vault/src/broker/core/type/scoped-sync.type.ts new file mode 100644 index 000000000..98e0c67b3 --- /dev/null +++ b/apps/vault/src/broker/core/type/scoped-sync.type.ts @@ -0,0 +1,96 @@ +import { z } from 'zod' +import { ConnectionWithCredentials } from './connection.type' +import { Account, Address, Wallet } from './indexed-resources.type' +import { NetworkMap } from './network.type' +import { Provider } from './provider.type' + +export const RawAccount = z.object({ + provider: z.nativeEnum(Provider), + externalId: z.string() +}) +export type RawAccount = z.infer + +export const ScopedSyncStatus = { + PROCESSING: 'processing', + SUCCESS: 'success', + PARTIAL_SUCCESS: 'partial_success', + FAILED: 'failed' +} as const +export type ScopedSyncStatus = (typeof ScopedSyncStatus)[keyof typeof ScopedSyncStatus] + +export const RawAccountError = { + EXTERNAL_RESOURCE_NOT_FOUND: 'EXTERNAL_RESOURCE_NOT_FOUND', + UNLISTED_NETWORK: 'UNLISTED_NETWORK' +} as const +export type RawAccountError = (typeof RawAccountError)[keyof typeof RawAccountError] + +export const RawAccountNetworkNotFoundFailure = z.object({ + code: z.literal(RawAccountError.UNLISTED_NETWORK), + rawAccount: RawAccount, + message: z.string(), + networkId: z.string() +}) +export type RawAccountNetworkNotFoundFailure = z.infer + +export const RawAccountExternalResourceNotFoundFailure = z.object({ + code: z.literal(RawAccountError.EXTERNAL_RESOURCE_NOT_FOUND), + rawAccount: RawAccount, + message: z.string(), + externalResourceType: z.string(), + externalResourceId: z.string() +}) +export type RawAccountExternalResourceNotFoundFailure = z.infer + +export const RawAccountSyncFailure = z.discriminatedUnion('code', [ + RawAccountNetworkNotFoundFailure, + RawAccountExternalResourceNotFoundFailure +]) +export type RawAccountSyncFailure = z.infer + +export const ScopedSyncResult = z.object({ + wallets: z.array(Wallet), + accounts: z.array(Account), + addresses: z.array(Address), + failures: z.array(RawAccountSyncFailure) +}) +export type ScopedSyncResult = z.infer + +export const ScopedSyncContext = z.object({ + connection: ConnectionWithCredentials, + rawAccounts: z.array(RawAccount), + networks: NetworkMap, + existingAccounts: z.array(Account) +}) +export type ScopedSyncContext = z.infer + +export const ScopedSync = z.object({ + clientId: z.string(), + completedAt: z.date().optional(), + connectionId: z.string(), + createdAt: z.date(), + error: z + .object({ + name: z.string().optional(), + message: z.string().optional(), + traceId: z.string().optional() + }) + .optional(), + status: z.nativeEnum(ScopedSyncStatus).default(ScopedSyncStatus.PROCESSING), + scopedSyncId: z.string(), + rawAccounts: z.array(RawAccount), + failures: z.array(RawAccountSyncFailure).optional() +}) +export type ScopedSync = z.infer + +export const StartScopedSync = z.object({ + clientId: z.string(), + connectionId: z.string().describe('The connection to sync.'), + rawAccounts: z.array(RawAccount).describe('The accounts to sync.') +}) +export type StartScopedSync = z.infer + +export const ScopedSyncStarted = z.object({ + started: z.boolean(), + scopedSyncs: z.array(ScopedSync) +}) +export type ScopedSyncStarted = z.infer diff --git a/apps/vault/src/broker/core/type/sync.type.ts b/apps/vault/src/broker/core/type/sync.type.ts new file mode 100644 index 000000000..e638565ac --- /dev/null +++ b/apps/vault/src/broker/core/type/sync.type.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' + +export const SyncStatus = { + PROCESSING: 'processing', + SUCCESS: 'success', + FAILED: 'failed' +} as const +export type SyncStatus = (typeof SyncStatus)[keyof typeof SyncStatus] + +export const Sync = z.object({ + clientId: z.string(), + completedAt: z.date().optional(), + connectionId: z.string(), + createdAt: z.date(), + error: z + .object({ + name: z.string().optional(), + message: z.string().optional(), + traceId: z.string().optional() + }) + .optional(), + status: z.nativeEnum(SyncStatus).default(SyncStatus.PROCESSING), + syncId: z.string() +}) +export type Sync = z.infer + +export const StartSync = z.object({ + clientId: z.string(), + connectionId: z.string().describe('The connection to sync') +}) +export type StartSync = z.infer + +export const SyncStarted = z.object({ + started: z.boolean(), + syncs: z.array(Sync) +}) +export type SyncStarted = z.infer diff --git a/apps/vault/src/broker/core/type/transfer.type.ts b/apps/vault/src/broker/core/type/transfer.type.ts new file mode 100644 index 000000000..e8be4aed8 --- /dev/null +++ b/apps/vault/src/broker/core/type/transfer.type.ts @@ -0,0 +1,144 @@ +import { z } from 'zod' +import { Provider } from './provider.type' + +export const NetworkFeeAttribution = { + ON_TOP: 'on_top', + DEDUCT: 'deduct' +} as const +export type NetworkFeeAttribution = (typeof NetworkFeeAttribution)[keyof typeof NetworkFeeAttribution] + +export const TransferPartyType = { + WALLET: 'wallet', + ACCOUNT: 'account', + ADDRESS: 'address' +} as const +export type TransferPartyType = (typeof TransferPartyType)[keyof typeof TransferPartyType] + +export const TransferParty = z.object({ + id: z.string(), + type: z.nativeEnum(TransferPartyType) +}) +export type TransferParty = z.infer + +export const Source = TransferParty +export type Source = z.infer + +export const AddressDestination = z.object({ + address: z.string() +}) +export type AddressDestination = z.infer + +export const Destination = z.union([TransferParty, AddressDestination]) +export type Destination = z.infer + +export const TransferAsset = z + .object({ + assetId: z.string().optional().describe('ID of the asset. Can be used instead of address+networkId.'), + externalAssetId: z + .string() + .optional() + .describe( + 'ID of the asset on the provider. Can be used to directly specify the asset of the underlying provider.' + ), + address: z + .string() + .optional() + .describe( + 'On-chain address of the asset. If assetId is null, then an empty address means the network Base Asset (e.g. BTC)' + ), + networkId: z.string().optional().describe('Network of the asset. Required if address is provided.') + }) + .describe('The asset being transferred') +export type TransferAsset = z.infer + +export const SendTransfer = z.object({ + transferId: z.string().optional().describe('Sets the transfer ID to an arbitrary value'), + source: Source, + destination: Destination, + amount: z.string(), + /** + * @deprecated use asset instead + */ + assetId: z.string().optional().describe('@deprecated use asset instead'), + asset: TransferAsset, + networkFeeAttribution: z + .nativeEnum(NetworkFeeAttribution) + .optional() + .describe( + [ + 'Controls how network fees are charged.', + 'Example: a request to transfer 1 ETH with networkFeeAttribution=ON_TOP would result in exactly 1 ETH received to the destination and just over 1 ETH spent by the source.', + 'Note: This property is optional and its default always depend on the underlying provider.' + ].join('\n') + ), + customerRefId: z.string().optional(), + idempotenceId: z.string(), + memo: z.string().optional(), + provider: z.nativeEnum(Provider).optional(), + providerSpecific: z.unknown().optional() +}) +export type SendTransfer = z.infer + +export const TransferFee = z.object({ + type: z.string(), + attribution: z.string().optional(), + amount: z.string(), + assetId: z.string() +}) +export type TransferFee = z.infer + +export const TransferStatus = { + PROCESSING: 'processing', + SUCCESS: 'success', + FAILED: 'failed' +} as const +export type TransferStatus = (typeof TransferStatus)[keyof typeof TransferStatus] + +export const InternalTransfer = z.object({ + assetExternalId: z.string().nullable(), + assetId: z.string().nullable(), + clientId: z.string(), + connectionId: z.string(), + createdAt: z.date(), + customerRefId: z.string().nullable(), + destination: Destination, + externalId: z.string(), + externalStatus: z.string().nullable(), + grossAmount: z.string(), + idempotenceId: z.string().nullable(), + memo: z.string().nullable(), + networkFeeAttribution: z.nativeEnum(NetworkFeeAttribution), + provider: z.nativeEnum(Provider), + providerSpecific: z.unknown().nullable(), + source: Source, + // The status is optional for an internal transfer because we query the + // provider to get the most recent status on reads. + // + // If we stored it, it would complicate our system by trying to keep + // distributed systems state in sync – an indexing problem. The Vault **is + // not an indexing solution**. + // + // NOTE: The status is not persisted in the database. + status: z.nativeEnum(TransferStatus).default(TransferStatus.PROCESSING).optional(), + transferId: z.string() +}) +export type InternalTransfer = z.infer + +export const Transfer = InternalTransfer.extend({ + // A transfer always has a status because we check with the provider to + // combine the information from the API and the database. + status: z.nativeEnum(TransferStatus), + fees: z.array(TransferFee) +}) +export type Transfer = z.infer + +export const isAddressDestination = (destination: Destination): destination is AddressDestination => { + return 'address' in destination +} + +/** + * Ensures the provider specific is an object. + */ +export const isProviderSpecific = (value?: unknown): value is Record => { + return typeof value === 'object' && value !== null +} diff --git a/apps/vault/src/broker/core/util/asset.util.ts b/apps/vault/src/broker/core/util/asset.util.ts new file mode 100644 index 000000000..0262fc88b --- /dev/null +++ b/apps/vault/src/broker/core/util/asset.util.ts @@ -0,0 +1,10 @@ +import { Asset, ExternalAsset } from '../type/asset.type' +import { Provider } from '../type/provider.type' + +export const getExternalAsset = (asset: Asset, provider: Provider): ExternalAsset | null => { + return asset.externalAssets.find((externalAsset) => externalAsset.provider === provider) || null +} + +export const isNativeAsset = (asset: Asset): boolean => { + return asset.onchainId === null +} diff --git a/apps/vault/src/broker/core/util/network.util.ts b/apps/vault/src/broker/core/util/network.util.ts new file mode 100644 index 000000000..be649f755 --- /dev/null +++ b/apps/vault/src/broker/core/util/network.util.ts @@ -0,0 +1,30 @@ +import { ExternalNetwork, Network } from '../type/network.type' +import { Provider } from '../type/provider.type' + +export const isNetworkIdFromTestnet = (networkId: string): boolean => { + const id = networkId.toUpperCase() + + return ( + id.includes('TESTNET') || + id.includes('_T') || + id.includes('SEPOLIA') || + id.includes('KOVAN') || + id.includes('HOLESKY') || + id.includes('DEVNET') || + id.includes('BAKLAVA') + ) +} + +/** + * Checks if a network is a testnet based on its SLIP-44 coin type. Based on + * the standard, coin type 1 is reserved to all testnets. + * + * @see https://github.com/satoshilabs/slips/blob/master/slip-0044.md#registered-coin-types + */ +export const isTestnet = (network: Network): boolean => { + return Boolean(network.coinType && network.coinType === 1) +} + +export const getExternalNetwork = (network: Network, provider: Provider): ExternalNetwork | null => { + return network.externalNetworks.find((externalNetwork) => externalNetwork.provider === provider) || null +} diff --git a/apps/vault/src/broker/core/util/user-friendly-key-format.util.ts b/apps/vault/src/broker/core/util/user-friendly-key-format.util.ts new file mode 100644 index 000000000..0d58e3795 --- /dev/null +++ b/apps/vault/src/broker/core/util/user-friendly-key-format.util.ts @@ -0,0 +1,19 @@ +import { PublicKey, RsaPublicKey, publicKeyToHex, publicKeyToPem } from '@narval/signature' + +export const formatPublicKey = async (publicKey: PublicKey) => { + return { + keyId: publicKey.kid, + jwk: publicKey, + hex: await publicKeyToHex(publicKey) + } +} + +export const formatRsaPublicKey = async (rsaPublicKey: RsaPublicKey) => { + const pem = await publicKeyToPem(rsaPublicKey, rsaPublicKey.alg) + + return { + keyId: rsaPublicKey.kid, + jwk: rsaPublicKey, + pem: Buffer.from(pem).toString('base64') + } +} diff --git a/apps/vault/src/broker/event/handler/connection-scoped-sync.event-handler.ts b/apps/vault/src/broker/event/handler/connection-scoped-sync.event-handler.ts new file mode 100644 index 000000000..7dafefb39 --- /dev/null +++ b/apps/vault/src/broker/event/handler/connection-scoped-sync.event-handler.ts @@ -0,0 +1,24 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { ScopedSyncService } from '../../core/service/scoped-sync.service' +import { ScopedSyncStartedEvent } from '../../shared/event/scoped-sync-started.event' + +@Injectable() +export class ConnectionScopedSyncEventHandler { + constructor( + private readonly scopedSyncService: ScopedSyncService, + private readonly logger: LoggerService + ) {} + + @OnEvent(ScopedSyncStartedEvent.EVENT_NAME) + async handleSyncStarted(event: ScopedSyncStartedEvent) { + this.logger.log(`Received ${ScopedSyncStartedEvent.EVENT_NAME} event`, { + clientId: event.connection.clientId, + connectionId: event.connection.connectionId, + rawAccounts: event.sync.rawAccounts + }) + + await this.scopedSyncService.scopedSync(event.sync, event.connection) + } +} diff --git a/apps/vault/src/broker/http/client/__test__/unit/anchorage.client.spec.ts b/apps/vault/src/broker/http/client/__test__/unit/anchorage.client.spec.ts new file mode 100644 index 000000000..d9acfea99 --- /dev/null +++ b/apps/vault/src/broker/http/client/__test__/unit/anchorage.client.spec.ts @@ -0,0 +1,257 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { Alg, privateKeyToJwk } from '@narval/signature' +import { HttpService } from '@nestjs/axios' +import * as nobleEd25519 from '@noble/ed25519' +import { AxiosRequestConfig } from 'axios' +import { mock } from 'jest-mock-extended' +import { UrlParserException } from '../../../../core/exception/url-parser.exception' +import { AnchorageClient } from '../../anchorage.client' + +describe(AnchorageClient.name, () => { + let client: AnchorageClient + + const now = new Date(1234567890) + const nowTimestamp = Math.floor(now.getTime() / 1000) + + beforeEach(() => { + const httpServiceMock = mock() + const loggerServiceMock = mock() + + client = new AnchorageClient(httpServiceMock, loggerServiceMock) + }) + + describe('parseEndpoint', () => { + it('extracts version path from valid URLs', () => { + const testCases = [ + { + input: 'https://api.anchorage.com/v2/accounts', + expected: '/v2/accounts' + }, + { + input: 'https://api.anchorage.com/v1/trading/quotes', + expected: '/v1/trading/quotes' + }, + { + input: 'https://api.anchorage.com/v3/something/nested/path', + expected: '/v3/something/nested/path' + }, + { + input: 'https://api.anchorage.com/v4/something/nested/path?query=param&another=param&yetAnother=param', + expected: '/v4/something/nested/path?query=param&another=param&yetAnother=param' + } + ] + + for (const { input, expected } of testCases) { + expect(client.parseEndpoint(input)).toBe(expected) + } + }) + + it('throws UrlParserException for invalid URLs', () => { + const invalidUrls = [ + 'https://api.anchorage.com/accounts', + 'https://api.anchorage.com/invalidv1/', + 'not-even-an-url' + ] + + for (const url of invalidUrls) { + expect(() => client.parseEndpoint(url)).toThrow(UrlParserException) + } + }) + }) + + describe('buildSignatureMessage', () => { + it('builds GET requests without data', () => { + const request: AxiosRequestConfig = { + method: 'GET', + url: '/v2/accounts' + } + + expect(client.buildSignatureMessage(request, nowTimestamp)).toEqual(`${nowTimestamp}GET/v2/accounts`) + }) + + it('builds post requests with data', () => { + const request: AxiosRequestConfig = { + method: 'POST', + url: '/v2/accounts', + data: { + foo: 'foo', + bar: 'bar' + } + } + + expect(client.buildSignatureMessage(request, nowTimestamp)).toEqual( + `${nowTimestamp}POST/v2/accounts${JSON.stringify(request.data)}` + ) + }) + + it('adds params as query string', () => { + const request: AxiosRequestConfig = { + method: 'POST', + url: '/v2/accounts', + data: undefined, + params: { foo: 'bar' } + } + + expect(client.buildSignatureMessage(request, nowTimestamp)).toEqual(`${nowTimestamp}POST/v2/accounts?foo=bar`) + }) + + it('adds params as query string even if the url already has query params', () => { + const request: AxiosRequestConfig = { + method: 'POST', + url: '/v2/accounts?after=first', + data: undefined, + params: { foo: 'bar' } + } + + expect(client.buildSignatureMessage(request, nowTimestamp)).toEqual( + `${nowTimestamp}POST/v2/accounts?after=first&foo=bar` + ) + }) + + it('does not add ? to the url when params is defined with undefined items', () => { + const request: AxiosRequestConfig = { + method: 'POST', + url: '/v2/accounts', + data: undefined, + params: { + foo: undefined, + bar: undefined + } + } + + expect(client.buildSignatureMessage(request, nowTimestamp)).toEqual(`${nowTimestamp}POST/v2/accounts`) + }) + + it('handles undefined data for non-get requests', () => { + const request: AxiosRequestConfig = { + method: 'POST', + url: '/v2/accounts', + data: undefined + } + + expect(client.buildSignatureMessage(request, nowTimestamp)).toEqual(`${nowTimestamp}POST/v2/accounts`) + }) + + it('handles lower case http methods', () => { + const methods = ['post', 'put', 'patch', 'delete'] + const request: AxiosRequestConfig = { + url: '/v2/test', + data: { + foo: 'foo', + bar: 'bar' + } + } + + for (const method of methods) { + expect(client.buildSignatureMessage({ ...request, method }, nowTimestamp)).toEqual( + `${nowTimestamp}${method.toUpperCase()}/v2/test${JSON.stringify(request.data)}` + ) + } + }) + + it('builds request for all http methods', () => { + const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] + const request: AxiosRequestConfig = { + url: '/v2/test', + data: { + foo: 'foo', + bar: 'bar' + } + } + + for (const method of methods) { + expect(client.buildSignatureMessage({ ...request, method }, nowTimestamp)).toEqual( + `${nowTimestamp}${method}/v2/test${method === 'GET' ? '' : JSON.stringify(request.data)}` + ) + } + }) + }) + + describe('authorize', () => { + const ed25519PrivateKeyHex = '0xe6ad32d225c16074bd4a3b62e28c99dd26136ef341e6368ca05227d1e13822d9' + const signKey = privateKeyToJwk(ed25519PrivateKeyHex, Alg.EDDSA) + + const defaultRequest: AxiosRequestConfig = { + url: 'https://api.anchorage.com/v2/accounts', + method: 'GET', + data: undefined + } + + const apiKey = 'test-api-key' + + it('builds signed get request', async () => { + jest.spyOn(nobleEd25519, 'sign') + + const signedRequest = await client.authorize({ + request: defaultRequest, + signKey, + apiKey, + now + }) + + expect(signedRequest).toEqual({ + url: defaultRequest.url, + method: defaultRequest.method, + headers: { + 'Api-Access-Key': apiKey, + 'Api-Signature': + '6b312c48285544422dc7f4bc44a8f094094453d74fb83f5419c99a2ce1ce79133034b561838b1312d257eb7af5ac8582bfdad319f602f3ff81c484c5a147c50e', + 'Api-Timestamp': nowTimestamp, + 'Content-Type': 'application/json' + }, + data: undefined + }) + + expect(nobleEd25519.sign).toHaveBeenCalledWith( + expect.any(String), + // We need to slice the '0x' prefix from the hex key + ed25519PrivateKeyHex.slice(2) + ) + }) + + it('builds signed post request with data', async () => { + const request = { + ...defaultRequest, + method: 'POST', + data: { test: 'data' } + } + + const signedRequest = await client.authorize({ + request, + signKey, + apiKey, + now + }) + + expect(signedRequest).toEqual({ + url: request.url, + method: request.method, + headers: { + 'Api-Access-Key': apiKey, + 'Api-Signature': + '51f1feffab30a8e8bbad75cb85e99a945db0f71ca0e2dfc9b8f7be0f6ec65b9d5274d1e8eac283be9370a4c6d16bbab84049b58feadfcbcbc499bb816195420d', + 'Api-Timestamp': nowTimestamp, + 'Content-Type': 'application/json' + }, + data: request.data + }) + }) + + it('supports all http methods with appropriate data handling', async () => { + const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] + const data = { test: 'data' } + + for (const method of methods) { + const config = await client.authorize({ + request: { ...defaultRequest, method, data }, + signKey, + apiKey, + now + }) + + expect(config.method).toBe(method) + expect(config.data).toBe(method === 'GET' ? undefined : data) + } + }) + }) +}) diff --git a/apps/vault/src/broker/http/client/__test__/unit/fireblocks.client.spec.ts b/apps/vault/src/broker/http/client/__test__/unit/fireblocks.client.spec.ts new file mode 100644 index 000000000..ef45a35cd --- /dev/null +++ b/apps/vault/src/broker/http/client/__test__/unit/fireblocks.client.spec.ts @@ -0,0 +1,198 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { RsaPrivateKey } from '@narval/signature' +import { HttpService } from '@nestjs/axios' +import axios from 'axios' +import { mock, mockReset } from 'jest-mock-extended' +import { BrokerException } from '../../../../core/exception/broker.exception' +import { UrlParserException } from '../../../../core/exception/url-parser.exception' +import { FireblocksClient } from '../../fireblocks.client' + +jest.mock('axios') + +describe(FireblocksClient.name, () => { + let client: FireblocksClient + const mockedAxios = axios as jest.MockedFunction + + const mockSignKey: RsaPrivateKey = { + kty: 'RSA', + alg: 'RS256', + kid: '0x52920ad0d19d7779106bd9d9d600d26c4b976cdb3cbc49decb7fdc29db00b8e9', + n: 'xNdTjWL9hGa4bz4tLKbmFZ4yjQsQzW35-CMS0kno3403jEqg5y2Cs6sLVyPBX4N2hdK5ERPytpf1PrThHqB-eEO6LtEWpENBgFuNIf8DRHrv0tne7dLNxf7sx1aocGRrkgIk4Ws6Is4Ot3whm3-WihmDGnHoogE-EPwVkkSc2FYPXYlNq4htCZXC8_MUI3LuXry2Gn4tna5HsYSehYhfKDD-nfSajeWxdNUv_3wOeSCr9ICm9Udlo7hpIUHQgnX3Nz6kvfGYuweLGoj_ot-oEUCIdlbQqmrfStAclugbM5NI6tY__6wD0z_4ZBjToupXCBlXbYsde6_ZG9xPmYSykw', + e: 'AQAB', + d: 'QU4rIzpXX8jwob-gHzNUHJH6tX6ZWX6GM0P3p5rrztc8Oag8z9XyigdSYNu0-SpVdTqfOcJDgT7TF7XNBms66k2WBJhMCb1iiuJU5ZWEkQC0dmDgLEkHCgx0pAHlKjy2z580ezEm_YsdqNRfFgbze-fQ7kIiazU8UUhBI-DtpHv7baBgsfqEfQ5nCTiURUPmmpiIU74-ZIJWZjBXTOoJNH0EIsJK9IpZzxpeC9mTMTsWTcHKiR3acze1qf-9I97v461TTZ8e33N6YINyr9I4HZuvxlCJdV_lOM3fLvYM9gPvgkPozhVWL3VKR6xa9JpGGHrCRgH92INuviBB_SmF8Q', + p: '9BNku_-t4Df9Dg7M2yjiNgZgcTNKrDnNqexliIUAt67q0tGmSBubjxeI5unDJZ_giXWUR3q-02v7HT5GYx-ZVgKk2lWnbrrm_F7UZW-ueHzeVvQcjDXTk0z8taXzrDJgnIwZIaZ2XSG3P-VPOrXCaMba8GzSq38Gpzi4g3lTO9s', + q: 'znUtwrqdnVew14_aFjNTRgzOQNN8JhkjzJy3aTSLBScK5NbiuUUZBWs5dQ7Nv7aAoDss1-o9XVQZ1DVV-o9UufJtyrPNcvTnC0cWRrtJrSN5YiuUbECU3Uj3OvGxnhx9tsmhDHnMTo50ObPYUbHcIkNaXkf2FVgL84y1JRWdPak', + dp: 'UNDrFeS-6fMf8zurURXkcQcDf_f_za8GDjGcHOwNJMTiNBP-_vlFNMgSKINWfmrFqj4obtKRxOeIKlKoc8HOv8_4TeL2oY95VC8CHOQx3Otbo2cI3NQlziw7sNnWKTo1CyDIYYAAyS2Uw69l4Ia2bIMLk3g0-VwCE_SQA9h0Wuk', + dq: 'VBe6ieSFKn97UnIPfJdvRcsVf6YknUgEIuV6d2mlbnXWpBs6wgf5BxIDl0BuYbYuchVoUJHiaM9Grf8DhEk5U3wBaF0QQ9CpAxjzY-AJRHJ8kJX7oJQ1jmSX_vRPSn2EXx2FcZVyuFSh1pcAd1YgufwBJQHepBb21z7q0a4aG_E', + qi: 'KhZpFs6xfyRIjbJV8Q9gWxqF37ONayIzBpgio5mdAQlZ-FUmaWZ2_2VWP2xvsP48BmwFXydHqewHBqGnZYCQ1ZHXJgD_-KKEejoqS5AJN1pdI0ZKjs7UCfZ4RJ4DH5p0_35gpuKRzzdvcIhl1CjIC5W8o7nhwmLBJ_QAo9e4t9U' + } + + beforeEach(() => { + const httpServiceMock = mock() + const loggerServiceMock = mock() + + client = new FireblocksClient(httpServiceMock, loggerServiceMock) + mockReset(mockedAxios) + }) + + describe('parseEndpoint', () => { + it('extracts version path from valid URLs', () => { + const testCases = [ + { + input: 'https://api.anchorage.com/v2/accounts', + expected: '/v2/accounts' + }, + { + input: 'https://api.anchorage.com/v1/trading/quotes', + expected: '/v1/trading/quotes' + }, + { + input: 'https://api.anchorage.com/v3/something/nested/path', + expected: '/v3/something/nested/path' + }, + { + input: 'https://api.anchorage.com/v4/something/nested/path?query=param&another=param&yetAnother=param', + expected: '/v4/something/nested/path?query=param&another=param&yetAnother=param' + } + ] + + for (const { input, expected } of testCases) { + expect(client.parseEndpoint(input)).toBe(expected) + } + }) + + it('throws UrlParserException for invalid URLs', () => { + const invalidUrls = [ + 'https://api.anchorage.com/accounts', + 'https://api.anchorage.com/invalidv1/', + 'not-even-an-url' + ] + + for (const url of invalidUrls) { + expect(() => client.parseEndpoint(url)).toThrow(UrlParserException) + } + }) + }) + + describe('validateRequest', () => { + it('throws BrokerException when URL is missing', async () => { + const request: any = { method: 'GET' } + + await expect( + client.authorize({ + request, + apiKey: 'test-api-key', + signKey: mockSignKey + }) + ).rejects.toThrow(BrokerException) + }) + + it('throws BrokerException when method is missing', async () => { + const request: any = { url: 'https://api.fireblocks.com/v1/test' } + + await expect( + client.authorize({ + request, + apiKey: 'test-api-key', + signKey: mockSignKey + }) + ).rejects.toThrow(BrokerException) + }) + }) + + describe('authorize', () => { + const mockDate = new Date('2024-01-01T00:00:00Z') + + it('creates valid authorization headers for GET request', async () => { + const request = { + url: 'https://api.fireblocks.com/v1/test', + method: 'GET' + } + + const result = await client.authorize({ + request, + apiKey: 'test-api-key', + signKey: mockSignKey, + now: mockDate + }) + + // Check header structure + expect(result.headers).toHaveProperty('x-api-key', 'test-api-key') + expect(result.headers).toHaveProperty('authorization') + + // Verify the Bearer token format + expect(result.headers?.authorization).toMatch(/^Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.[A-Za-z0-9-_.+/=]+$/) + }) + + it('creates valid authorization headers for POST request with data', async () => { + const requestData = { test: 'data' } + const request = { + url: 'https://api.fireblocks.com/v1/test', + method: 'POST', + data: requestData + } + + const result = await client.authorize({ + request, + apiKey: 'test-api-key', + signKey: mockSignKey, + now: mockDate + }) + + // Verify the basic structure stays the same for POST + expect(result.headers).toHaveProperty('x-api-key', 'test-api-key') + expect(result.headers?.authorization).toMatch(/^Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.[A-Za-z0-9-_.+/=]+$/) + + // Verify the data is properly set in the request + expect(result.data).toEqual(requestData) + }) + }) + + describe('forward', () => { + it('forwards authorized request to Fireblocks API', async () => { + const mockResponse = { data: 'test-response', status: 200 } + mockedAxios.mockResolvedValue(mockResponse) + + const result = await client.forward({ + url: 'https://api.fireblocks.com/v1/test', + method: 'GET', + apiKey: 'test-api-key', + signKey: mockSignKey, + nonce: 'test-nonce' + }) + + expect(result).toBe(mockResponse) + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'https://api.fireblocks.com/v1/test', + method: 'GET', + responseType: 'stream', + validateStatus: null + }) + ) + }) + + it('forwards POST request with data', async () => { + const mockResponse = { data: 'test-response', status: 200 } + mockedAxios.mockResolvedValue(mockResponse) + + const requestData = { test: 'data' } + + await client.forward({ + url: 'https://api.fireblocks.com/v1/test', + method: 'POST', + data: requestData, + apiKey: 'test-api-key', + signKey: mockSignKey, + nonce: 'test-nonce' + }) + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + data: requestData, + method: 'POST' + }) + ) + }) + }) +}) diff --git a/apps/vault/src/broker/http/client/anchorage.client.ts b/apps/vault/src/broker/http/client/anchorage.client.ts new file mode 100644 index 000000000..8ddc6b6b7 --- /dev/null +++ b/apps/vault/src/broker/http/client/anchorage.client.ts @@ -0,0 +1,725 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { Ed25519PrivateKey, privateKeyToHex } from '@narval/signature' +import { HttpService } from '@nestjs/axios' +import { HttpStatus, Injectable } from '@nestjs/common' +import { sign } from '@noble/ed25519' +import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' +import { isNil, omitBy } from 'lodash' +import { + EMPTY, + Observable, + OperatorFunction, + catchError, + expand, + from, + lastValueFrom, + map, + reduce, + switchMap, + tap +} from 'rxjs' +import { ZodType, z } from 'zod' +import { BrokerException } from '../../core/exception/broker.exception' +import { ProviderHttpException } from '../../core/exception/provider-http.exception' +import { UrlParserException } from '../../core/exception/url-parser.exception' +import { Provider } from '../../core/type/provider.type' + +// +// Response Schema +// + +const AssetType = z.object({ + assetType: z.string(), + decimals: z.number(), + featureSupport: z.array(z.string()), + name: z.string(), + networkId: z.string(), + onchainIdentifier: z.string().optional() +}) +type AssetType = z.infer + +const Amount = z.object({ + quantity: z.string(), + assetType: z.string(), + currentPrice: z.string(), + currentUSDValue: z.string() +}) + +const DepositAddress = z.object({ + address: z.string(), + addressId: z.string(), + addressSignaturePayload: z.string(), + signature: z.string() +}) + +const VaultAsset = z.object({ + walletId: z.string(), + assetType: z.string(), + availableBalance: Amount.optional(), + totalBalance: Amount.optional(), + stakedBalance: Amount.optional(), + unclaimedBalance: Amount.optional(), + vaultId: z.string(), + vaultName: z.string() +}) + +const Vault = z.object({ + vaultId: z.string(), + name: z.string(), + description: z.string(), + type: z.literal('VAULT'), + accountName: z.string(), + assets: z.array(VaultAsset) +}) +type Vault = z.infer + +const WalletAsset = z.object({ + assetType: z.string(), + availableBalance: Amount.optional(), + totalBalance: Amount.optional(), + stakedBalance: Amount.optional(), + unclaimedBalance: Amount.optional() +}) + +const Wallet = z.object({ + walletId: z.string(), + walletName: z.string(), + depositAddress: DepositAddress, + assets: z.array(WalletAsset), + vaultId: z.string(), + vaultName: z.string(), + isDefault: z.boolean(), + isArchived: z.boolean(), + networkId: z.string(), + type: z.literal('WALLET') +}) +export type Wallet = z.infer + +const GetWalletResponse = z.object({ + data: Wallet +}) +type GetWalletResponse = z.infer + +const Address = z.object({ + address: z.string(), + addressId: z.string(), + addressSignaturePayload: z.string(), + signature: z.string(), + walletId: z.string() +}) +type Address = z.infer + +const TransferAmount = z.object({ + assetType: z.string(), + currentPrice: z.string().optional(), + currentUSDValue: z.string().optional(), + quantity: z.string() +}) + +const TransferFee = z.object({ + assetType: z.string(), + quantity: z.string() +}) + +const Resource = z.object({ + id: z.string(), + type: z.string() +}) + +const Transfer = z.object({ + amount: TransferAmount, + assetType: z.string(), + blockchainTxId: z.string().optional(), + createdAt: z.string(), + destination: Resource, + endedAt: z.string().optional(), + fee: TransferFee.optional(), + source: Resource, + status: z.string(), + transferId: z.string(), + transferMemo: z.string().optional() +}) +type Transfer = z.infer + +const CreateTransfer = z.object({ + amount: z.string(), + assetType: z.string(), + deductFeeFromAmountIfSameType: z.boolean(), + destination: Resource, + idempotentId: z.string().max(128).nullable(), + source: Resource, + transferMemo: z.string().nullable() +}) +type CreateTransfer = z.infer + +const CreatedTransfer = z.object({ + transferId: z.string(), + status: z.string() +}) +type CreatedTransfer = z.infer + +const TrustedDestination = z.object({ + id: z.string(), + type: z.literal('crypto'), + crypto: z.object({ + address: z.string(), + networkId: z.string(), + // Asset type is optional. If it is not provided, then the destination will + // accept any network compatible transfer + // e.g: ETH network can accept only one specific token on ETH network + assetType: z.string().optional(), + memo: z.string().optional() + }) +}) +type TrustedDestination = z.infer + +// +// Response Type +// + +const CreateTransferResponse = z.object({ + data: CreatedTransfer +}) +type CreateTransferResponse = z.infer + +const GetVaultsResponse = z.object({ + data: z.array(Vault), + page: z.object({ + next: z.string().nullish() + }) +}) +type GetVaultsResponse = z.infer + +const GetWalletsResponse = z.object({ + data: z.array(Wallet), + page: z.object({ + next: z.string().nullish() + }) +}) +type GetWalletsResponse = z.infer + +const GetVaultAddressesResponse = z.object({ + data: z.array(Address), + page: z.object({ + next: z.string().nullish() + }) +}) +type GetVaultAddressesResponse = z.infer + +const GetTransferResponse = z.object({ + data: Transfer +}) +type GetTransferResponse = z.infer + +const GetTrustedDestinationsResponse = z.object({ + data: z.array(TrustedDestination), + page: z.object({ + next: z.string().nullish() + }) +}) +type GetTrustedDestinationsResponse = z.infer + +const GetAssetTypeResponse = z.object({ + data: z.array(AssetType) +}) +type GetAssetTypeResponse = z.infer + +// +// Request Type +// + +interface RequestOptions { + url: string + apiKey: string + signKey: Ed25519PrivateKey + limit?: number +} + +interface ForwardRequestOptions { + url: string + method: string + data?: unknown + apiKey: string + signKey: Ed25519PrivateKey +} + +@Injectable() +export class AnchorageClient { + constructor( + private readonly httpService: HttpService, + private readonly logger: LoggerService + ) {} + + async forward({ url, method, data, apiKey, signKey }: ForwardRequestOptions): Promise { + const signedRequest = await this.authorize({ + request: { + url, + method, + data, + responseType: 'stream', + // Don't reject on error status codes. Needed to re-throw our exception + validateStatus: null + }, + apiKey, + signKey + }) + + const response = await axios(signedRequest) + + return response + } + + async authorize(opts: { + request: AxiosRequestConfig + apiKey: string + signKey: Ed25519PrivateKey + now?: Date + }): Promise { + const { request, signKey, apiKey } = opts + + this.validateRequest(request) + + const now = opts.now ? opts.now.getTime() : new Date().getTime() + const timestamp = Math.floor(now / 1000) + const message = this.buildSignatureMessage(request, timestamp) + const messageHex = Buffer.from(message, 'utf8').toString('hex') + const signKeyHex = await privateKeyToHex(signKey) + const signature = await sign(messageHex, signKeyHex.slice(2)) + const signatureHex = Buffer.from(signature).toString('hex') + const headers = { + 'Api-Access-Key': apiKey, + 'Api-Signature': signatureHex, + 'Api-Timestamp': timestamp, + 'Content-Type': 'application/json' + } + + const data = request.data && request.method !== 'GET' ? request.data : undefined + + return { + ...request, + headers, + data + } + } + + parseEndpoint(url: string): string { + const regex = /(\/v\d+(?:\/.*)?)$/ + const match = url.match(regex) + + if (!match) { + throw new UrlParserException({ + message: 'No version pattern found in the URL', + url + }) + } + + return match[1] + } + + private validateRequest(request: AxiosRequestConfig): asserts request is AxiosRequestConfig & { + url: string + method: string + } { + if (!request.url) { + throw new BrokerException({ + message: 'Cannot sign a request without an URL', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + } + + if (!request.method) { + throw new BrokerException({ + message: 'Cannot sign a request without a method', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + } + } + + buildSignatureMessage(request: AxiosRequestConfig, timestamp: number): string { + this.validateRequest(request) + + const endpoint = this.parseEndpoint(request.url) + const method = request.method.toUpperCase() + + // Parse existing query params from the endpoint + const [endpointPath, existingParams] = endpoint.split('?') + const searchParams = new URLSearchParams(existingParams || '') + + // Add additional params from request.params + if (request.params) { + Object.entries(omitBy(request.params, isNil)).forEach(([key, value]) => { + searchParams.append(key, String(value)) + }) + } + + const queryString = searchParams.toString() + const path = queryString ? `${endpointPath}?${queryString}` : endpointPath + + return `${timestamp}${method}${path}${request.data && method !== 'GET' ? JSON.stringify(request.data) : ''}` + } + + private sendSignedRequest(opts: { + schema: ZodType + request: AxiosRequestConfig + signKey: Ed25519PrivateKey + apiKey: string + }): Observable { + return from( + this.authorize({ + request: opts.request, + apiKey: opts.apiKey, + signKey: opts.signKey + }) + ).pipe( + switchMap((signedRequest) => + this.httpService.request(signedRequest).pipe( + tap((response) => { + this.logger.log('Received Anchorage response', { + url: opts.request.url, + method: opts.request.method, + nextPage: response.data?.page?.next + }) + }), + map((response) => opts.schema.parse(response.data)) + ) + ) + ) + } + + async getVaults(opts: RequestOptions): Promise { + this.logger.log('Requesting Anchorage vaults page', { + url: opts.url, + limit: opts.limit + }) + + const { apiKey, signKey, url } = opts + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetVaultsResponse, + request: { + url: `${url}/v2/vaults`, + method: 'GET', + params: { + limit: opts.limit + } + }, + apiKey, + signKey + }).pipe( + expand((response) => { + if (response.page.next) { + return this.sendSignedRequest({ + schema: GetVaultsResponse, + request: { + url: `${url}${response.page.next}`, + method: 'GET', + params: { + limit: opts.limit + } + }, + apiKey, + signKey + }) + } + + return EMPTY + }), + tap((response) => { + if (response.page.next) { + this.logger.log('Requesting Anchorage vaults next page', { + url: response.page.next, + limit: opts.limit + }) + } else { + this.logger.log('Reached Anchorage vaults last page') + } + }), + reduce((vaults: Vault[], response) => [...vaults, ...response.data], []), + tap((vaults) => { + this.logger.log('Completed fetching all Anchorage vaults', { + vaultsCount: vaults.length, + url: opts.url + }) + }), + this.handleError('Failed to get Anchorage vaults') + ) + ) + } + + async getWallets(opts: RequestOptions): Promise { + const { apiKey, signKey, url, limit } = opts + + this.logger.log('Requesting Anchorage wallets page', { url, limit }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetWalletsResponse, + request: { + url: `${url}/v2/wallets`, + method: 'GET' + }, + apiKey, + signKey + }).pipe( + expand((response) => { + if (response.page.next) { + return this.sendSignedRequest({ + schema: GetWalletsResponse, + request: { + url: `${url}${response.page.next}`, + method: 'GET' + }, + apiKey, + signKey + }) + } + + return EMPTY + }), + tap((response) => { + if (response.page.next) { + this.logger.log('Requesting Anchorage wallets next page', { + url: response.page.next, + limit + }) + } else { + this.logger.log('Reached Anchorage wallets last page') + } + }), + reduce((wallets: Wallet[], response) => [...wallets, ...response.data], []), + tap((wallets) => { + this.logger.log('Completed fetching all Anchorage wallets', { + url, + walletsCount: wallets.length + }) + }), + this.handleError('Failed to get Anchorage wallets') + ) + ) + } + + async getWallet(opts: RequestOptions & { walletId: string }): Promise { + const { apiKey, signKey, url, walletId } = opts + + this.logger.log('Requesting Anchorage wallet', { url }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetWalletResponse, + request: { + url: `${url}/v2/wallets/${walletId}`, + method: 'GET' + }, + apiKey, + signKey + }).pipe( + map(({ data }) => data), + tap((wallet) => { + this.logger.log('Successfully fetched Anchorage wallet', { url, walletId: wallet.walletId }) + }), + this.handleError('Failed to get Anchorage wallet') + ) + ) + } + + async getTrustedDestinations(opts: RequestOptions & { afterId?: string }): Promise { + this.logger.log('Requesting Anchorage trusted destinations', { + url: opts.url, + limit: opts.limit, + afterId: opts.afterId + }) + const { apiKey, signKey, url } = opts + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetTrustedDestinationsResponse, + request: { + url: `${url}/v2/trusted_destinations`, + method: 'GET', + params: { + ...(opts.limit ? { limit: opts.limit } : {}), + ...(opts.afterId ? { afterId: opts.afterId } : {}) + } + }, + apiKey, + signKey + }).pipe( + tap((response) => { + this.logger.log('Requesting Anchorage trusted destinations', { + url: response.page.next, + limit: opts.limit + }) + }), + this.handleError('Failed to get Anchorage trusted destinations') + ) + ) + } + + async getVaultAddresses(opts: RequestOptions & { vaultId: string; assetType: string }): Promise { + const { apiKey, signKey, url, vaultId, assetType } = opts + + this.logger.log('Requesting Anchorage vault addresses page', { + url, + vaultId, + assetType + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetVaultAddressesResponse, + request: { + url: `${url}/v2/vaults/${vaultId}/addresses`, + method: 'GET', + params: { + assetType + } + }, + apiKey, + signKey + }).pipe( + expand((response) => { + if (response.page.next) { + return this.sendSignedRequest({ + schema: GetVaultAddressesResponse, + request: { + url: `${url}${response.page.next}`, + method: 'GET', + params: { + assetType + } + }, + apiKey, + signKey + }) + } + + return EMPTY + }), + tap((response) => { + if (response.page.next) { + this.logger.log('Requesting Anchorage vault addresses next page', { + url: response.page.next, + vaultId, + assetType + }) + } else { + this.logger.log('Reached Anchorage vault addresses last page') + } + }), + reduce((addresses: Address[], response) => [...addresses, ...response.data], []), + tap((addresses) => { + this.logger.log('Completed fetching all Anchorage vault addresses', { + addressesCount: addresses.length, + vaultId, + assetType, + url + }) + }), + this.handleError('Failed to get Anchorage vault addresses') + ) + ) + } + + async getTransferById(opts: RequestOptions & { transferId: string }): Promise { + const { apiKey, signKey, url, transferId } = opts + + this.logger.log('Requesting Anchorage transfer', { url, transferId }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetTransferResponse, + request: { + url: `${url}/v2/transfers/${transferId}`, + method: 'GET' + }, + apiKey, + signKey + }).pipe( + map((response) => response.data), + tap((transfer) => { + this.logger.log('Successfully fetched Anchorage transfer', { + transferId, + url, + status: transfer.status + }) + }), + this.handleError('Failed to get Anchorage transfer') + ) + ) + } + + async createTransfer(opts: RequestOptions & { data: CreateTransfer }): Promise { + const { apiKey, signKey, url, data } = opts + + this.logger.log('Sending create transfer request to Anchorage', { url, data }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: CreateTransferResponse, + request: { + url: `${url}/v2/transfers`, + method: 'POST', + data + }, + apiKey, + signKey + }).pipe( + map((response) => response.data), + tap((transfer) => { + this.logger.log('Successfully created Anchorage transfer', { + transferId: transfer.transferId, + url + }) + }), + this.handleError('Failed to create Anchorage transfer') + ) + ) + } + + async getAssetTypes(opts: RequestOptions): Promise { + const { apiKey, signKey, url } = opts + + this.logger.log('Request Anchorage asset types', { url }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetAssetTypeResponse, + request: { + url: `${url}/v2/asset-types`, + method: 'GET' + }, + apiKey, + signKey + }).pipe( + map((response) => response.data), + tap((assetTypes) => { + this.logger.log('Successfully fetched Anchorage asset types', { + url, + assetTypesCount: assetTypes.length + }) + }), + this.handleError('Failed to fetch Anchorage asset types') + ) + ) + } + + private handleError(logMessage: string): OperatorFunction { + return catchError((error: unknown): Observable => { + this.logger.error(logMessage, { error }) + + if (error instanceof AxiosError) { + throw new ProviderHttpException({ + provider: Provider.ANCHORAGE, + origin: error, + response: { + status: error.response?.status ?? HttpStatus.INTERNAL_SERVER_ERROR, + body: error.response?.data + } + }) + } + + throw error + }) + } +} diff --git a/apps/vault/src/broker/http/client/fireblocks.client.ts b/apps/vault/src/broker/http/client/fireblocks.client.ts new file mode 100644 index 000000000..8d2f112d6 --- /dev/null +++ b/apps/vault/src/broker/http/client/fireblocks.client.ts @@ -0,0 +1,895 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { RsaPrivateKey, signJwt } from '@narval/signature' +import { HttpService } from '@nestjs/axios' +import { HttpStatus, Injectable } from '@nestjs/common' +import { sha256 } from '@noble/hashes/sha256' +import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' +import { isNil, omitBy } from 'lodash' +import { + EMPTY, + Observable, + OperatorFunction, + catchError, + expand, + from, + lastValueFrom, + map, + reduce, + switchMap, + tap +} from 'rxjs' +import { v4 } from 'uuid' +import { toHex } from 'viem' +import { ZodType, z } from 'zod' +import { BrokerException } from '../../core/exception/broker.exception' +import { ProviderHttpException } from '../../core/exception/provider-http.exception' +import { UrlParserException } from '../../core/exception/url-parser.exception' +import { Provider } from '../../core/type/provider.type' + +interface ForwardRequestOptions { + url: string + method: string + data?: unknown + apiKey: string + signKey: RsaPrivateKey + nonce: string +} + +const RewardsInfo = z.object({ + pendingRewards: z.coerce.number().positive() +}) +type RewardsInfo = z.infer + +export const SupportedAsset = z.object({ + id: z.string(), + name: z.string(), + type: z.string(), + contractAddress: z.string(), + nativeAsset: z.string(), + decimals: z.number(), + issuerAddress: z.string().optional() +}) +export type SupportedAsset = z.infer + +// Fireblocks API is not consistent in the response format for Asset and AssetWallet, but they seem to be the same underlying data +const Asset = z.object({ + // !![This is NOT discretionary.](https://developers.fireblocks.com/docs/list-supported-assets-1) + id: z.string(), + total: z.string(), + available: z.string(), + pending: z.string(), + frozen: z.string(), + lockedAmount: z.string(), + blockHeight: z.string().optional(), + blockHash: z.string().optional(), + rewardsInfo: RewardsInfo.optional(), + hiddenOnUI: z.boolean().optional(), + customerRefId: z.string().optional(), + autoFuel: z.boolean().optional() +}) +type Asset = z.infer + +const VaultAccount = z.object({ + id: z.string(), + name: z.string().optional(), + assets: z.array(Asset), + hiddenOnUI: z.boolean().optional(), + customerRefId: z.string().optional(), + autoFuel: z.boolean() +}) +export type VaultAccount = z.infer + +const Paging = z.object({ + before: z.string().optional(), + after: z.string().optional() +}) +type Paging = z.infer + +const GetVaultAccountsResponse = z.object({ + accounts: z.array(VaultAccount), + paging: Paging +}) +type GetVaultAccountsResponse = z.infer + +// Fireblocks API is not consistent in the response format for Asset and AssetWallet, but they seem to be the same underlying data +const AssetWallet = z.object({ + // id of the vaultAccount + vaultId: z.string(), + // !![This is NOT discretionary.](https://developers.fireblocks.com/docs/list-supported-assets-1) + assetId: z.string(), + available: z.string(), + //The total wallet balance. + // Total = available + pending + lockedAmount + frozen + // In EOS this value includes the network balance, self staking and pending refund. + // For all other coins it is the balance as it appears on the blockchain. + total: z.string(), + pending: z.string(), + staked: z.string(), + frozen: z.string(), + lockedAmount: z.string(), + blockHeight: z.string().nullable(), + blockHash: z.string().nullable(), + creationTimestamp: z.string().optional() +}) +type AssetWallet = z.infer + +const GetVaultWalletsResponse = z.object({ + assetWallets: z.array(AssetWallet), + paging: Paging +}) +type GetVaultWalletsResponse = z.infer + +const AssetAddress = z.object({ + assetId: z.string(), + address: z.string(), + description: z.string().optional(), + tag: z.string().optional(), + type: z.string(), + customerRefId: z.string().optional(), + addressFormat: z.string().optional(), + legacyAddress: z.string(), + enterpriseAddress: z.string(), + bip44AddressIndex: z.number(), + userDefined: z.boolean() +}) +export type AssetAddress = z.infer + +const GetAddressListResponse = z.object({ + addresses: z.array(AssetAddress), + paging: Paging.optional() +}) +type GetAddressListResponse = z.infer + +const WhitelistedAddressAsset = z.object({ + id: z.string(), + balance: z.string().optional(), + lockedAmount: z.string().optional(), + status: z.string(), + address: z.string(), + tag: z.string().optional(), + activationTime: z.string().optional() +}) +type WhitelistedAddressAsset = z.infer + +const WhitelistedWallet = z.object({ + id: z.string(), + name: z.string(), + customerRefId: z.string().optional(), + assets: z.array(WhitelistedAddressAsset) +}) +export type WhitelistedWallet = z.infer + +const GetWhitelistedWalletsResponse = z.array(WhitelistedWallet) +type GetWhitelistedWalletsResponse = z.infer + +const TransactionAmountInfo = z.object({ + amount: z.string(), + requestedAmount: z.string(), + netAmount: z.string().optional(), + amountUSD: z.string() +}) + +const TransactionFeeInfo = z.object({ + networkFee: z.string().optional(), + gasPrice: z.string().optional() +}) +export type TransactionFeeInfo = z.infer + +const TransactionBlockInfo = z.object({ + blockHeight: z.string().optional(), + blockHash: z.string().optional() +}) + +const TransactionParty = z.object({ + id: z.string().nullable(), + type: z.string(), + name: z.string().optional(), + subType: z.string() +}) + +const Transaction = z.object({ + addressType: z.string(), + amount: z.number(), + amountInfo: TransactionAmountInfo, + amountUSD: z.number(), + assetId: z.string(), + assetType: z.string(), + blockInfo: TransactionBlockInfo, + createdAt: z.number(), + createdBy: z.string(), + destination: TransactionParty, + destinationAddress: z.string(), + destinationAddressDescription: z.string(), + destinationTag: z.string(), + destinations: z.array(z.unknown()), + exchangeTxId: z.string(), + fee: z.number(), + feeCurrency: z.string(), + feeInfo: TransactionFeeInfo, + id: z.string(), + index: z.number().optional(), + lastUpdated: z.number(), + netAmount: z.number(), + networkFee: z.number(), + note: z.string(), + numOfConfirmations: z.number().optional(), + operation: z.string(), + rejectedBy: z.string(), + requestedAmount: z.number(), + signedBy: z.array(z.string()), + signedMessages: z.array(z.unknown()), + source: TransactionParty, + sourceAddress: z.string(), + status: z.string(), + subStatus: z.string(), + txHash: z.string() +}) +export type Transaction = z.infer + +const CreateTransactionResponse = z.object({ + id: z.string(), + status: z.string() +}) +type CreateTransactionResponse = z.infer + +interface RequestParams { + apiKey: string + signKey: RsaPrivateKey + url: string +} + +// IMPORTANT: These aren't all FB create transaction parameters. +// See https://developers.fireblocks.com/reference/createtransaction +export interface CreateTransaction { + amount: string + operation?: string + note?: string + externalTxId?: string + assetId: string + source: { + type: string + id?: string + } + destination: { + type: string + id?: string + oneTimeAddress?: { + address: string + } + } + treatAsGrossAmount?: boolean + feeLevel?: string + priorityFee?: string + failOnLowFee?: boolean + maxFee?: string + gasLimit?: string + gasPrice?: string + networkFee?: string + replaceTxByHash?: string + customerRefId?: string + useGasless?: boolean + // Headers + idempotencyKey?: string + requestId?: string +} + +export const FIREBLOCKS_API_ERROR_CODES = { + INVALID_SPECIFIED_VAULT_ACCOUNT: 11001 +} as const +export type FIREBLOCKS_API_ERROR_CODES = (typeof FIREBLOCKS_API_ERROR_CODES)[keyof typeof FIREBLOCKS_API_ERROR_CODES] + +@Injectable() +export class FireblocksClient { + constructor( + private readonly httpService: HttpService, + private readonly logger: LoggerService + ) {} + + async forward({ url, method, data, apiKey, signKey, nonce }: ForwardRequestOptions): Promise { + const signedRequest = await this.authorize({ + request: { + url, + method, + data, + responseType: 'stream', + // Don't reject on error status codes. Needed to re-throw our exception + validateStatus: null + }, + apiKey, + signKey, + nonce + }) + + const response = await axios(signedRequest) + + return response + } + + async authorize(opts: { + request: AxiosRequestConfig + apiKey: string + signKey: RsaPrivateKey + nonce?: string + now?: Date + }): Promise { + const { request, signKey, apiKey } = opts + + this.validateRequest(request) + + const endpoint = this.parseEndpoint(request.url) + + // Subtract 10 seconds to avoid clock skew + const now = Math.floor((opts.now ? opts.now.getTime() : Date.now()) / 1000) - 10 + + const queryParams = new URLSearchParams(omitBy(request.params, isNil)).toString() + const uri = queryParams ? `${endpoint}?${queryParams}` : endpoint + + const exp = now + 30 + + const bodyHash = this.getBodyHash(request) + + const payload = { + uri, + nonce: opts.nonce || v4(), + iat: now, + sub: apiKey, + exp, + bodyHash + } + + const token = await signJwt(payload, signKey) + + const headers = { + ...(request.headers ? request.headers : {}), + 'x-api-key': apiKey, + authorization: `Bearer ${token}` + } + + const data = request.data && request.method !== 'GET' ? request.data : undefined + + return { + ...request, + headers, + data + } + } + + /** + * Returns a hex-encoded SHA-256 hash of the raw HTTP request body. + * + * @see https://developers.fireblocks.com/reference/signing-a-request-jwt-structure#jwt-structure + */ + getBodyHash(request: AxiosRequestConfig): string { + if (request.data) { + return toHex(sha256(JSON.stringify(request.data))).slice(2) + } + + return '' + } + + parseEndpoint(url: string): string { + const regex = /(\/v\d+(?:\/.*)?)$/ + const match = url.match(regex) + + if (!match) { + throw new UrlParserException({ + message: 'No version pattern found in the URL', + url + }) + } + + return match[1] + } + + private handleError(logMessage: string): OperatorFunction { + return catchError((error: unknown): Observable => { + this.logger.error(logMessage, { error }) + + if (error instanceof AxiosError) { + throw new ProviderHttpException({ + provider: Provider.FIREBLOCKS, + origin: error, + response: { + status: error.response?.status ?? HttpStatus.INTERNAL_SERVER_ERROR, + body: error.response?.data + } + }) + } + + throw error + }) + } + + private validateRequest(request: AxiosRequestConfig): asserts request is AxiosRequestConfig & { + url: string + method: string + } { + if (!request.url) { + throw new BrokerException({ + message: 'Cannot sign a request without an URL', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + } + + if (!request.method) { + throw new BrokerException({ + message: 'Cannot sign a request without a method', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + } + } + + private sendSignedRequest(opts: { + schema: ZodType + request: AxiosRequestConfig + signKey: RsaPrivateKey + apiKey: string + }): Observable { + return from( + this.authorize({ + request: opts.request, + apiKey: opts.apiKey, + signKey: opts.signKey + }) + ).pipe( + switchMap((signedRequest) => + this.httpService.request(signedRequest).pipe( + tap((response) => { + this.logger.log('Received Fireblocks response', { + url: opts.request.url, + method: opts.request.method, + nextPage: response.data?.paging?.after + }) + }), + map((response) => opts.schema.parse(response.data)) + ) + ) + ) + } + + async getVaultAccounts(opts: { apiKey: string; signKey: RsaPrivateKey; url: string }): Promise { + this.logger.log('Requesting Fireblocks vault accounts page', { + url: opts.url + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetVaultAccountsResponse, + request: { + url: `${opts.url}/v1/vault/accounts_paged`, + params: { + limit: 500 + }, + method: 'GET' + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + expand((response) => { + if (response.paging.after) { + return this.sendSignedRequest({ + schema: GetVaultAccountsResponse, + request: { + url: `${opts.url}/v1/vault/accounts_paged`, + method: 'GET', + params: { + limit: 500, + after: response.paging.after + } + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }) + } + return EMPTY + }), + tap((response) => { + if (response.paging.after) { + this.logger.log('Requesting Fireblocks vault accounts next page', { + url: opts.url + }) + } else { + this.logger.log('Reached Fireblocks vault accounts last page') + } + }), + reduce((accounts: VaultAccount[], response) => [...accounts, ...response.accounts], []), + tap((accounts) => { + this.logger.log('Completed fetching all vault accounts', { + accountsCount: accounts.length, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks vault accounts') + ) + ) + } + + async getVaultAccountsV2(opts: { + apiKey: string + signKey: RsaPrivateKey + url: string + options?: { + limit?: number + after?: string + namePrefix?: string + nameSuffix?: string + assetId?: string // Fireblocks externalId + } + }): Promise<{ accounts: VaultAccount[]; page?: { cursor?: string } }> { + this.logger.log('Requesting Fireblocks vault accounts page', { + url: opts.url, + options: opts.options + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetVaultAccountsResponse, + request: { + url: `${opts.url}/v1/vault/accounts_paged`, + params: { + limit: opts.options?.limit ?? 500, + after: opts.options?.after, + namePrefix: opts.options?.namePrefix, + nameSuffix: opts.options?.nameSuffix, + assetId: opts.options?.assetId + }, + method: 'GET' + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + map((response) => ({ + accounts: response.accounts, + page: response.paging ? { cursor: response.paging.after } : undefined + })), + tap((response) => { + this.logger.log('Completed fetching vault accounts', { + accountsCount: response.accounts.length, + page: response.page, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks vault accounts') + ) + ) + } + + async getAssetWallets(opts: { apiKey: string; signKey: RsaPrivateKey; url: string }): Promise { + this.logger.log('Requesting Fireblocks asset wallets page', { + url: opts.url + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetVaultWalletsResponse, + request: { + url: `${opts.url}/v1/vault/asset_wallets`, + method: 'GET', + params: { + limit: 1000 + } + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + expand((response) => { + if (response.paging.after) { + return this.sendSignedRequest({ + schema: GetVaultWalletsResponse, + request: { + url: `${opts.url}/v1/vault/asset_wallets`, + method: 'GET', + params: { + after: response.paging.after, + limit: 1000 + } + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }) + } + return EMPTY + }), + tap((response) => { + if (response.paging.after) { + this.logger.log('Requesting Fireblocks asset wallets next page', { + url: opts.url + }) + } else { + this.logger.log('Reached Fireblocks asset wallets last page') + } + }), + reduce((wallets: AssetWallet[], response) => [...wallets, ...response.assetWallets], []), + tap((wallets) => { + this.logger.log('Completed fetching all asset wallets', { + walletsCount: wallets.length, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks asset wallets') + ) + ) + } + + async getAddresses(opts: { + apiKey: string + signKey: RsaPrivateKey + url: string + vaultAccountId: string + assetId: string + }): Promise { + this.logger.log(`Requesting Fireblocks vault ${opts.vaultAccountId} asset ${opts.assetId} addresses page`, { + url: opts.url, + vaultAccountId: opts.vaultAccountId, + assetId: opts.assetId + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetAddressListResponse, + request: { + url: `${opts.url}/v1/vault/accounts/${opts.vaultAccountId}/${opts.assetId}/addresses_paginated`, + method: 'GET', + params: { + limit: 1000 + } + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + expand((response) => { + if (response.paging?.after) { + return this.sendSignedRequest({ + schema: GetAddressListResponse, + request: { + url: `${opts.url}/v1/vault/accounts/${opts.vaultAccountId}/${opts.assetId}/addresses_paginated`, + method: 'GET', + params: { + after: response.paging.after, + limit: 1000 + } + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }) + } + return EMPTY + }), + tap((response) => { + if (response.paging?.after) { + this.logger.log( + `Requesting Fireblocks vault ${opts.vaultAccountId} asset ${opts.assetId} addresses next page`, + { + url: opts.url, + vaultAccountId: opts.vaultAccountId, + assetId: opts.assetId + } + ) + } else { + this.logger.log(`Reached Fireblocks vault ${opts.vaultAccountId} asset ${opts.assetId} addresses last page`) + } + }), + reduce((addresses: AssetAddress[], response) => [...addresses, ...response.addresses], []), + tap((addresses) => { + this.logger.log(`Completed fetching all ${opts.assetId} addresses`, { + addressesCount: addresses.length, + url: opts.url, + vaultAccountId: opts.vaultAccountId, + assetId: opts.assetId + }) + }), + this.handleError(`Failed to get Fireblocks vault ${opts.vaultAccountId} asset ${opts.assetId} addresses`) + ) + ) + } + + async getWhitelistedInternalWallets(opts: { + apiKey: string + signKey: RsaPrivateKey + url: string + }): Promise { + this.logger.log('Requesting Fireblocks whitelisted internal wallets page', { + url: opts.url + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetWhitelistedWalletsResponse, + request: { + url: `${opts.url}/v1/internal_wallets`, + method: 'GET' + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + reduce((wallets: WhitelistedWallet[], response) => [...wallets, ...response], []), + tap((addresses) => { + this.logger.log('Completed fetching all whitelisted internal addresses', { + addressesCount: addresses.length, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks whitelisted internal addresses') + ) + ) + } + + async getWhitelistedExternalWallets(opts: { + apiKey: string + signKey: RsaPrivateKey + url: string + }): Promise { + this.logger.log('Requesting Fireblocks whitelisted external wallets page', { + url: opts.url + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetWhitelistedWalletsResponse, + request: { + url: `${opts.url}/v1/external_wallets`, + method: 'GET' + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + reduce((wallets: WhitelistedWallet[], response) => [...wallets, ...response], []), + tap((addresses) => { + this.logger.log('Completed fetching all whitelisted external addresses', { + addressesCount: addresses.length, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks whitelisted external addresses') + ) + ) + } + + async getWhitelistedContracts(opts: { + apiKey: string + signKey: RsaPrivateKey + url: string + }): Promise { + this.logger.log('Requesting Fireblocks whitelisted contracts page', { + url: opts.url + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: GetWhitelistedWalletsResponse, + request: { + url: `${opts.url}/v1/contracts`, + method: 'GET' + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + reduce((contracts: WhitelistedWallet[], response) => [...contracts, ...response], []), + tap((contracts) => { + this.logger.log('Completed fetching all whitelisted contracts', { + contractsCount: contracts.length, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks whitelisted contracts') + ) + ) + } + + async createTransaction(params: RequestParams & { data: CreateTransaction }): Promise { + const { apiKey, signKey, url } = params + const { idempotencyKey, ...data } = params.data + + this.logger.log('Sending create transaction request to Fireblocks', { url, data: params.data }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: CreateTransactionResponse, + request: { + url: `${url}/v1/transactions`, + method: 'POST', + data, + ...(idempotencyKey + ? { + headers: { + 'idempotency-key': idempotencyKey + } + } + : {}) + }, + apiKey, + signKey + }).pipe( + tap((tx) => { + this.logger.log('Successfully created Fireblocks transaction', { + transactionId: tx.id, + url + }) + }), + this.handleError('Failed to create Fireblocks transaction') + ) + ) + } + + async getTransactionById(params: RequestParams & { txId: string }): Promise { + const { apiKey, signKey, url, txId } = params + + this.logger.log('Request Fireblocks transaction by ID', { url, txId }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: Transaction, + request: { + url: `${url}/v1/transactions/${txId}`, + method: 'GET' + }, + apiKey, + signKey + }).pipe( + tap((tx) => { + this.logger.log('Successfully got Fireblocks transaction', { + transactionId: tx.id, + url + }) + }), + this.handleError('Failed to get Fireblocks transaction by ID') + ) + ) + } + + async getVaultAccount(opts: { + apiKey: string + signKey: RsaPrivateKey + url: string + vaultAccountId: string + }): Promise { + this.logger.log('Requesting vault account per ID', { + url: opts.url + }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: VaultAccount, + request: { + url: `${opts.url}/v1/vault/accounts/${opts.vaultAccountId}`, + method: 'GET' + }, + apiKey: opts.apiKey, + signKey: opts.signKey + }).pipe( + tap((vaultAccount) => { + this.logger.log('Completed fetching Vault Account', { + vaultAccountId: vaultAccount.id, + url: opts.url + }) + }), + this.handleError('Failed to get Fireblocks Vault Account') + ) + ) + } + + async getSupportedAssets(params: RequestParams): Promise { + const { apiKey, signKey, url } = params + + this.logger.log('Request Fireblocks supported assets', { url }) + + return lastValueFrom( + this.sendSignedRequest({ + schema: z.array(SupportedAsset), + request: { + url: `${params.url}/v1/supported_assets`, + method: 'GET' + }, + apiKey, + signKey + }).pipe( + tap((supportedAssets) => { + this.logger.log('Successfully got Fireblocks supported assets', { + supportedAssetsCount: supportedAssets.length + }) + }), + this.handleError('Failed to get Fireblocks supported assets') + ) + ) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/account.controller.ts b/apps/vault/src/broker/http/rest/controller/account.controller.ts new file mode 100644 index 000000000..df4042b6c --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/account.controller.ts @@ -0,0 +1,179 @@ +import { ApiClientIdHeader, Paginated, PaginationOptions, PaginationParam } from '@narval/nestjs-shared' +import { Controller, Get, HttpStatus, Param, Query } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { AccountService } from '../../../core/service/account.service' +import { RawAccountService } from '../../../core/service/raw-account.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { PaginatedAccountsDto } from '../dto/response/paginated-accounts.dto' +import { PaginatedAddressesDto } from '../dto/response/paginated-addresses.dto' +import { PaginatedRawAccountsDto } from '../dto/response/paginated-raw-accounts.dto' +import { ProviderAccountDto } from '../dto/response/provider-account.dto' + +@Controller({ + path: 'accounts', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Account') +export class ProviderAccountController { + constructor( + private readonly accountService: AccountService, + private readonly rawAccountService: RawAccountService + ) {} + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'List the client accounts' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @Paginated({ + type: PaginatedAccountsDto, + description: 'Returns a paginated list of accounts for the client' + }) + async list( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + return PaginatedAccountsDto.create( + await this.accountService.findAll( + { + clientId, + connectionId + }, + { pagination } + ) + ) + } + + @Get('raw') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'List the provider accounts in raw form, used to populate which accounts to connect' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiQuery({ + name: 'namePrefix', + required: false, + description: 'Filter accounts by name prefix' + }) + @ApiQuery({ + name: 'nameSuffix', + required: false, + description: 'Filter accounts by name suffix' + }) + @ApiQuery({ + name: 'networkId', + required: false, + description: 'Filter accounts by network ID' + }) + @ApiQuery({ + name: 'assetId', + required: false, + description: 'Filter accounts by asset ID' + }) + @ApiQuery({ + name: 'includeAddress', + required: false, + type: 'boolean', + description: 'Include address information in the response' + }) + @Paginated({ + type: PaginatedRawAccountsDto, + description: 'Returns a paginated list of raw accounts, used to populate which accounts to connect.' + }) + async listRaw( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @PaginationParam() pagination: PaginationOptions, + @Query('namePrefix') namePrefix?: string, + @Query('nameSuffix') nameSuffix?: string, + @Query('networkId') networkId?: string, + @Query('assetId') assetId?: string, + @Query('includeAddress') includeAddress?: boolean + ): Promise { + return PaginatedRawAccountsDto.create( + await this.rawAccountService.findAllPaginated(clientId, connectionId, { + pagination, + filters: { + namePrefix, + nameSuffix, + networkId, + assetId, + includeAddress + } + }) + ) + } + + @Get(':accountId') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Get a specific account by ID' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiParam({ + name: 'accountId', + description: 'The ID of the account to retrieve' + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ProviderAccountDto + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Account not found' + }) + async getById( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('accountId') accountId: string + ): Promise { + const data = await this.accountService.findById({ clientId, connectionId }, accountId) + + return ProviderAccountDto.create({ data }) + } + + @Get(':accountId/addresses') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'List addresses for a specific account' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiParam({ + name: 'accountId', + description: 'The ID of the account to retrieve addresses for' + }) + @Paginated({ + type: PaginatedAddressesDto, + description: 'Returns a paginated list of addresses for the client' + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Address not found' + }) + async listAddresses( + @ClientId() clientId: string, + @Param('accountId') accountId: string, + @PaginationParam() options: PaginationOptions + ): Promise { + return PaginatedAddressesDto.create(await this.accountService.getAccountAddresses(clientId, accountId, options)) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/address.controller.ts b/apps/vault/src/broker/http/rest/controller/address.controller.ts new file mode 100644 index 000000000..8c01cbf92 --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/address.controller.ts @@ -0,0 +1,83 @@ +import { ApiClientIdHeader, Paginated, PaginationOptions, PaginationParam } from '@narval/nestjs-shared' +import { Controller, Get, HttpStatus, Param } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { AddressService } from '../../../core/service/address.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { PaginatedAddressesDto } from '../dto/response/paginated-addresses.dto' +import { ProviderAddressDto } from '../dto/response/provider-address.dto' + +@Controller({ + path: 'addresses', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Address') +export class ProviderAddressController { + constructor(private readonly addressService: AddressService) {} + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiOperation({ + summary: 'List the client addresss' + }) + @Paginated({ + type: PaginatedAddressesDto, + description: 'Returns a paginated list of addresss for the client' + }) + async list( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + return PaginatedAddressesDto.create( + await this.addressService.findAll( + { + clientId, + connectionId + }, + { + pagination + } + ) + ) + } + + @Get(':addressId') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Get a specific address by ID' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiParam({ + name: 'addressId', + description: 'The ID of the address to retrieve' + }) + @ApiResponse({ + status: HttpStatus.OK, + type: PaginatedAddressesDto + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Address not found' + }) + async getById( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('addressId') addressId: string + ): Promise { + const data = await this.addressService.findById({ clientId, connectionId }, addressId) + + return ProviderAddressDto.create({ data }) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/asset.controller.ts b/apps/vault/src/broker/http/rest/controller/asset.controller.ts new file mode 100644 index 000000000..8ab3fc126 --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/asset.controller.ts @@ -0,0 +1,36 @@ +import { ApiClientIdHeader, Paginated } from '@narval/nestjs-shared' +import { Controller, Get, Query } from '@nestjs/common' +import { ApiOperation, ApiTags } from '@nestjs/swagger' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { AssetService } from '../../../core/service/asset.service' +import { Provider } from '../../../core/type/provider.type' +import { PaginatedAssetsDto } from '../dto/response/paginated-assets.dto' + +@Controller({ + path: 'assets', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Asset') +export class AssetController { + constructor(private readonly assetService: AssetService) {} + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve all assets', + description: 'This endpoint retrieves a list of all available assets.' + }) + @Paginated({ + type: PaginatedAssetsDto, + description: 'The assets were successfully retrieved.' + }) + async list(@Query('provider') provider?: Provider): Promise { + const data = await this.assetService.findAll({ + filters: { provider } + }) + + return PaginatedAssetsDto.create({ data }) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/connection.controller.ts b/apps/vault/src/broker/http/rest/controller/connection.controller.ts new file mode 100644 index 000000000..63100652e --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/connection.controller.ts @@ -0,0 +1,249 @@ +import { ApiClientIdHeader, Paginated, PaginationOptions, PaginationParam } from '@narval/nestjs-shared' +import { Alg, RsaPrivateKey } from '@narval/signature' +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post } from '@nestjs/common' +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { FireblocksCredentialService } from '../../../core/provider/fireblocks/fireblocks-credential.service' +import { AccountService } from '../../../core/service/account.service' +import { ConnectionService } from '../../../core/service/connection.service' +import { WalletService } from '../../../core/service/wallet.service' +import { PendingConnection } from '../../../core/type/connection.type' +import { Provider } from '../../../core/type/provider.type' +import { formatPublicKey, formatRsaPublicKey } from '../../../core/util/user-friendly-key-format.util' +import { CreateConnectionDto } from '../dto/request/create-connection.dto' +import { InitiateConnectionDto } from '../dto/request/initiate-connection.dto' +import { UpdateConnectionDto } from '../dto/request/update-connection.dto' +import { PaginatedAccountsDto } from '../dto/response/paginated-accounts.dto' +import { PaginatedConnectionsDto } from '../dto/response/paginated-connections.dto' +import { PaginatedWalletsDto } from '../dto/response/paginated-wallets.dto' +import { ProviderConnectionDto } from '../dto/response/provider-connection.dto' +import { ProviderPendingConnectionDto } from '../dto/response/provider-pending-connection.dto' + +@Controller({ + path: 'connections', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Connection') +export class ConnectionController { + constructor( + private readonly connectionService: ConnectionService, + private readonly walletService: WalletService, + private readonly accountService: AccountService + ) {} + + @Post('/initiate') + @PermissionGuard(VaultPermission.CONNECTION_WRITE) + @ApiOperation({ + summary: 'Initiate a new provider connection', + description: + 'This endpoint initiates a new connection by generating a public key and an encryption key for secure communication.' + }) + @ApiResponse({ + description: 'Returns the public key and encryption key for the initiated connection.', + status: HttpStatus.CREATED, + type: ProviderPendingConnectionDto + }) + async initiate( + @ClientId() clientId: string, + @Body() body: InitiateConnectionDto + ): Promise { + const pendingConnection = await this.connectionService.initiate(clientId, body) + const publicKey = await this.formatPublicKey(pendingConnection) + + const data = { + ...pendingConnection, + ...(pendingConnection.encryptionPublicKey + ? { + encryptionPublicKey: await formatRsaPublicKey(pendingConnection.encryptionPublicKey) + } + : {}), + ...(publicKey ? { publicKey } : {}) + } + + return ProviderPendingConnectionDto.create({ data }) + } + + private async formatPublicKey(connection: PendingConnection) { + const credentials = await this.connectionService.findCredentials(connection) + + if (credentials && 'publicKey' in credentials) { + const publicKey = await formatPublicKey(credentials.publicKey) + + if (connection.provider === Provider.FIREBLOCKS && credentials.publicKey.alg === Alg.RS256) { + const certificateOrgName = `Narval Fireblocks Connection - Client ${connection.clientId}` + + const csr = await FireblocksCredentialService.signCertificateRequest( + credentials.privateKey as RsaPrivateKey, + certificateOrgName + ) + + return { ...publicKey, csr } + } + + return publicKey + } + + return null + } + + @Post() + @PermissionGuard(VaultPermission.CONNECTION_WRITE) + @ApiOperation({ + summary: 'Store a provider connection securely', + description: + 'This endpoint securely stores the details of a provider connection, ensuring that all sensitive information is encrypted.' + }) + @ApiResponse({ + description: 'Returns a reference to the stored provider connection.', + status: HttpStatus.CREATED, + type: ProviderConnectionDto + }) + async create(@ClientId() clientId: string, @Body() body: CreateConnectionDto): Promise { + const data = await this.connectionService.create(clientId, body) + + return ProviderConnectionDto.create({ data }) + } + + @Delete(':connectionId') + @PermissionGuard(VaultPermission.CONNECTION_WRITE) + @ApiOperation({ + summary: 'Revoke an existing connection', + description: + 'This endpoint revokes an existing connection, effectively terminating any ongoing communication and invalidating the connection credentials.' + }) + @ApiResponse({ + description: 'Indicates that the connection has been successfully revoked. No content is returned in the response.', + status: HttpStatus.NO_CONTENT + }) + @HttpCode(HttpStatus.NO_CONTENT) + async revoke(@ClientId() clientId: string, @Param('connectionId') connectionId: string): Promise { + await this.connectionService.revoke(clientId, connectionId) + } + + @Get(':connectionId') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve a specific connection by ID', + description: + 'This endpoint retrieves the details of a specific connection associated with the client, identified by the ID.' + }) + @ApiResponse({ + description: 'Returns the details of the specified connection.', + type: ProviderConnectionDto, + status: HttpStatus.OK + }) + async getById( + @ClientId() clientId: string, + @Param('connectionId') connectionId: string + ): Promise { + const data = await this.connectionService.findById(clientId, connectionId) + + return ProviderConnectionDto.create({ data }) + } + + @Patch(':connectionId') + @PermissionGuard(VaultPermission.CONNECTION_WRITE) + @ApiOperation({ + summary: 'Update a specific connection by ID', + description: + 'This endpoint updates the details of a specific connection associated with the client, identified by the connection ID.' + }) + @ApiResponse({ + description: 'Returns the updated details of the provider connection.', + status: HttpStatus.OK, + type: ProviderConnectionDto + }) + async update( + @ClientId() clientId: string, + @Param('connectionId') connectionId: string, + @Body() body: UpdateConnectionDto + ): Promise { + const data = await this.connectionService.update({ + ...body, + clientId, + connectionId + }) + + return ProviderConnectionDto.create({ data }) + } + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'List all connections', + description: 'This endpoint retrieves a list of all connections associated with the client.' + }) + @Paginated({ + type: PaginatedConnectionsDto, + description: 'Returns a paginated list of connections associated with the client' + }) + async list( + @ClientId() clientId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + const { data, page } = await this.connectionService.findAll(clientId, { pagination }) + + return PaginatedConnectionsDto.create({ data, page }) + } + + @Get(':connectionId/wallets') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + deprecated: true, + summary: '(DEPRECATED) List wallets for a specific connection', + description: 'Note: use GET /v1/provider/wallets endpoint instead' + }) + @Paginated({ + type: PaginatedWalletsDto, + description: 'Returns a paginated list of wallets associated with the connection' + }) + async listWallets( + @ClientId() clientId: string, + @Param('connectionId') connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + const page = await this.walletService.findAll( + { + clientId, + connectionId + }, + { + pagination + } + ) + + return PaginatedWalletsDto.create(page) + } + + @Get(':connectionId/accounts') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + deprecated: true, + summary: '(DEPRECATED) List accounts for a specific connection', + description: 'Note: use GET /v1/provider/accounts endpoint instead' + }) + @Paginated({ + type: PaginatedAccountsDto, + description: 'Returns a paginated list of accounts associated with the connection' + }) + async listAccounts( + @ClientId() clientId: string, + @Param('connectionId') connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + const page = await this.accountService.findAll( + { + clientId, + connectionId + }, + { + pagination + } + ) + + return PaginatedAccountsDto.create(page) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/known-destination.controller.ts b/apps/vault/src/broker/http/rest/controller/known-destination.controller.ts new file mode 100644 index 000000000..5705e434b --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/known-destination.controller.ts @@ -0,0 +1,51 @@ +import { ApiClientIdHeader, Paginated } from '@narval/nestjs-shared' +import { Controller, Get, Query } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { KnownDestinationService } from '../../../core/service/known-destination.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { PaginatedKnownDestinationsDto } from '../dto/response/paginated-known-destinations.dto' + +@Controller({ + path: 'known-destinations', + version: '1' +}) +@ApiTags('Provider Known Destination') +@ApiClientIdHeader() +export class KnownDestinationController { + constructor(private readonly knownDestinationService: KnownDestinationService) {} + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ summary: 'Get known destinations across providers' }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @Paginated({ + type: PaginatedKnownDestinationsDto, + description: 'Returns a paginated list of known-destinations for the client' + }) + async list( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Query('limit') limit?: number, + @Query('cursor') cursor?: string + ): Promise { + const { data, page } = await this.knownDestinationService.findAll( + { + clientId, + connectionId + }, + { + limit, + cursor + } + ) + + return PaginatedKnownDestinationsDto.create({ data, page }) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/network.controller.ts b/apps/vault/src/broker/http/rest/controller/network.controller.ts new file mode 100644 index 000000000..69f5c7b27 --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/network.controller.ts @@ -0,0 +1,34 @@ +import { ApiClientIdHeader, Paginated } from '@narval/nestjs-shared' +import { Controller, Get, Query } from '@nestjs/common' +import { ApiOperation, ApiTags } from '@nestjs/swagger' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { NetworkService } from '../../../core/service/network.service' +import { Provider } from '../../../core/type/provider.type' +import { PaginatedNetworksDto } from '../dto/response/paginated-networks.dto' + +@Controller({ + path: 'networks', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Network') +export class NetworkController { + constructor(private readonly networkService: NetworkService) {} + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve all networks', + description: 'This endpoint retrieves a list of all available networks.' + }) + @Paginated({ + type: PaginatedNetworksDto, + description: 'The networks were successfully retrieved.' + }) + async list(@Query('provider') provider: Provider): Promise { + const data = await this.networkService.findAll({ filters: { provider } }) + + return PaginatedNetworksDto.create({ data }) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/proxy.controller.ts b/apps/vault/src/broker/http/rest/controller/proxy.controller.ts new file mode 100644 index 000000000..b9e018301 --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/proxy.controller.ts @@ -0,0 +1,209 @@ +import { ApiClientIdHeader } from '@narval/nestjs-shared' +import { + Body, + Controller, + Delete, + Get, + Head, + HttpStatus, + Options, + Param, + Patch, + Post, + Put, + Req, + Res +} from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger' +import { Request, Response } from 'express' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { ProxyService } from '../../../core/service/proxy.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' + +const API_PARAM = ':endpoint(*)' + +const API_OPERATION = { + summary: 'Authorizes and forwards the request to the provider', + description: + 'This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider.' +} as const + +const CONNECTION_HEADER = { + name: REQUEST_HEADER_CONNECTION_ID, + required: true, + description: 'The connection ID used to forward request to provider' +} as const + +const ENDPOINT_PARAM = { + name: 'endpoint', + required: true, + description: 'The raw endpoint path in the provider' +} as const + +const INACTIVE_CONNECTION_RESPONSE = { + status: HttpStatus.PROXY_AUTHENTICATION_REQUIRED, + description: 'Requested connection is not active' +} as const + +@Controller({ + path: 'proxy', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Proxy') +export class ProxyController { + constructor(private readonly proxyService: ProxyService) {} + + private async handleRequest( + clientId: string, + connectionId: string, + endpoint: string, + body: unknown, + request: Request, + res: Response + ) { + const queryString = new URLSearchParams(request.query as Record).toString() + const sanitizedEndpoint = queryString ? `/${endpoint}?${queryString}` : `/${endpoint}` + + const response = await this.proxyService.forward( + { + clientId, + connectionId + }, + { + endpoint: sanitizedEndpoint, + method: request.method, + data: body + } + ) + + res.status(response.code).set(response.headers) + response.data.pipe(res) + } + + // IMPORTANT: The `@All` decorator from NestJS cannot be used here because it + // includes a handler for the SEARCH HTTP method, which causes issues with + // the OpenAPI generator used by the SDK. + + @Get(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async get( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } + + @Post(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async post( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } + + @Put(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async put( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } + + @Patch(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async patch( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } + + @Delete(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async delete( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } + + @Head(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async head( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } + + @Options(API_PARAM) + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader(CONNECTION_HEADER) + @ApiOperation(API_OPERATION) + @ApiParam(ENDPOINT_PARAM) + @ApiResponse(INACTIVE_CONNECTION_RESPONSE) + async options( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('endpoint') endpoint: string, + @Body() body: unknown, + @Req() request: Request, + @Res() res: Response + ) { + return this.handleRequest(clientId, connectionId, endpoint, body, request, res) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/scoped-sync.controller.ts b/apps/vault/src/broker/http/rest/controller/scoped-sync.controller.ts new file mode 100644 index 000000000..69cc2eaff --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/scoped-sync.controller.ts @@ -0,0 +1,105 @@ +import { ApiClientIdHeader, Paginated, PaginationOptions, PaginationParam } from '@narval/nestjs-shared' +import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { ConnectionService } from '../../../core/service/connection.service' +import { ScopedSyncService } from '../../../core/service/scoped-sync.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { StartScopedSyncDto } from '../dto/request/start-scoped-sync.dto' +import { PaginatedScopedSyncsDto } from '../dto/response/paginated-scoped-syncs.dto' +import { ScopedSyncStartedDto } from '../dto/response/scoped-sync-started.dto' +import { ScopedSyncDto } from '../dto/response/scoped-sync.dto' + +@Controller({ + path: 'scoped-syncs', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Scoped Sync') +export class ScopedSyncController { + constructor( + private readonly scopedSyncService: ScopedSyncService, + private readonly connectionService: ConnectionService + ) {} + + @Post() + // ScopedSync is a read operation even though it's a POST. + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Start a scoped synchronization process', + description: 'This endpoint starts scoped synchronization process for the client.' + }) + @ApiResponse({ + description: 'Returns the status of the scoped synchronization process.', + status: HttpStatus.CREATED, + type: ScopedSyncStartedDto + }) + async start(@ClientId() clientId: string, @Body() body: StartScopedSyncDto): Promise { + const connection = await this.connectionService.findWithCredentialsById(clientId, body.connectionId) + const data = await this.scopedSyncService.start([connection], body.rawAccounts) + + return ScopedSyncStartedDto.create({ data }) + } + + @Get(':scopedSyncId') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve a specific scoped synchronization process by ID', + description: + 'This endpoint retrieves the details of a specific scoped synchronization process associated with the client, identified by the scoped sync ID.' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiResponse({ + description: 'Returns the details of the specified synchronization process.', + status: HttpStatus.OK, + type: ScopedSyncDto + }) + async getById( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('scopedSyncId') scopedSyncId: string + ): Promise { + const data = await this.scopedSyncService.findById({ clientId, connectionId }, scopedSyncId) + + return ScopedSyncDto.create({ data }) + } + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve a list of synchronization processes', + description: + 'This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID.' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @Paginated({ + type: PaginatedScopedSyncsDto, + description: 'Returns a paginated list of accounts associated with the connection' + }) + async list( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + return PaginatedScopedSyncsDto.create( + await this.scopedSyncService.findAll( + { + clientId, + connectionId + }, + { + pagination + } + ) + ) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/sync.controller.ts b/apps/vault/src/broker/http/rest/controller/sync.controller.ts new file mode 100644 index 000000000..998dcd496 --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/sync.controller.ts @@ -0,0 +1,52 @@ +import { ApiClientIdHeader, Paginated, PaginationOptions, PaginationParam } from '@narval/nestjs-shared' +import { Controller, Get } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { SyncService } from '../../../core/service/sync.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { PaginatedSyncsDto } from '../dto/response/paginated-syncs.dto' + +@Controller({ + path: 'syncs', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Sync') +export class SyncController { + constructor(private readonly syncService: SyncService) {} + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve a list of synchronization processes', + description: + 'This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID.' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @Paginated({ + type: PaginatedSyncsDto, + description: 'Returns a paginated list of accounts associated with the connection' + }) + async list( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + return PaginatedSyncsDto.create( + await this.syncService.findAll( + { + clientId, + connectionId + }, + { + pagination + } + ) + ) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/transfer.controller.ts b/apps/vault/src/broker/http/rest/controller/transfer.controller.ts new file mode 100644 index 000000000..eff9aeb66 --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/transfer.controller.ts @@ -0,0 +1,73 @@ +import { ApiClientIdHeader } from '@narval/nestjs-shared' +import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { TransferService } from '../../../core/service/transfer.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { SendTransferDto } from '../dto/request/send-transfer.dto' +import { TransferDto } from '../dto/response/transfer.dto' + +@Controller({ + path: 'transfers', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Transfer') +export class TransferController { + constructor(private readonly transferService: TransferService) {} + + @Post() + @PermissionGuard(VaultPermission.CONNECTION_WRITE) + @ApiOperation({ + summary: 'Send a transfer', + description: "This endpoint sends a transfer to the source's provider." + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed', + required: true + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'The transfer was successfully sent.', + type: TransferDto + }) + async send( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Body() body: SendTransferDto + ): Promise { + const internalTransfer = await this.transferService.send({ clientId, connectionId }, body) + + return TransferDto.create({ data: internalTransfer }) + } + + @Get(':transferId') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Retrieve transfer details', + description: 'This endpoint retrieves the details of a specific transfer using its ID.' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed', + required: true + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'The transfer details were successfully retrieved.', + type: TransferDto + }) + async getById( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('transferId') transferId: string + ) { + const internalTransfer = await this.transferService.findById({ clientId, connectionId }, transferId) + + return TransferDto.create({ data: internalTransfer }) + } +} diff --git a/apps/vault/src/broker/http/rest/controller/wallet.controller.ts b/apps/vault/src/broker/http/rest/controller/wallet.controller.ts new file mode 100644 index 000000000..8bf60868d --- /dev/null +++ b/apps/vault/src/broker/http/rest/controller/wallet.controller.ts @@ -0,0 +1,127 @@ +import { ApiClientIdHeader, Paginated, PaginationOptions, PaginationParam } from '@narval/nestjs-shared' +import { Controller, Get, HttpStatus, Param } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../shared/type/domain.type' +import { AccountService } from '../../../core/service/account.service' +import { WalletService } from '../../../core/service/wallet.service' +import { REQUEST_HEADER_CONNECTION_ID } from '../../../shared/constant' +import { ConnectionId } from '../../../shared/decorator/connection-id.decorator' +import { PaginatedAccountsDto } from '../dto/response/paginated-accounts.dto' +import { PaginatedWalletsDto } from '../dto/response/paginated-wallets.dto' +import { ProviderWalletDto } from '../dto/response/provider-wallet.dto' + +@Controller({ + path: 'wallets', + version: '1' +}) +@ApiClientIdHeader() +@ApiTags('Provider Wallet') +export class ProviderWalletController { + constructor( + private readonly walletService: WalletService, + private readonly accountService: AccountService + ) {} + + @Get() + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiOperation({ + summary: 'List the client wallets' + }) + @Paginated({ + type: PaginatedWalletsDto, + description: 'Returns a paginated list of wallets for the client' + }) + async list( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @PaginationParam() pagination: PaginationOptions + ): Promise { + return PaginatedWalletsDto.create( + await this.walletService.findAll( + { + clientId, + connectionId + }, + { pagination } + ) + ) + } + + @Get(':walletId') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'Get a specific wallet by ID' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiParam({ + name: 'walletId', + description: 'The ID of the wallet to retrieve' + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ProviderWalletDto + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Wallet not found' + }) + async getById( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('walletId') walletId: string + ): Promise { + const data = await this.walletService.findById({ clientId, connectionId }, walletId) + + return ProviderWalletDto.create({ data }) + } + + @Get(':walletId/accounts') + @PermissionGuard(VaultPermission.CONNECTION_READ) + @ApiOperation({ + summary: 'List accounts for a specific wallet' + }) + @ApiHeader({ + name: REQUEST_HEADER_CONNECTION_ID, + description: 'The provider connection through which the resource is accessed' + }) + @ApiParam({ + name: 'walletId', + description: 'The ID of the wallet to retrieve accounts for' + }) + @Paginated({ + type: PaginatedAccountsDto, + description: 'Returns a paginated list of accounts' + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Wallet not found' + }) + async listAccounts( + @ClientId() clientId: string, + @ConnectionId() connectionId: string, + @Param('walletId') walletId: string, + @PaginationParam() options: PaginationOptions + ): Promise { + return PaginatedAccountsDto.create( + await this.accountService.findAll( + { + clientId, + connectionId + }, + { + ...options, + filters: { walletId } + } + ) + ) + } +} diff --git a/apps/vault/src/broker/http/rest/dto/request/create-connection.dto.ts b/apps/vault/src/broker/http/rest/dto/request/create-connection.dto.ts new file mode 100644 index 000000000..84517ed1e --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/request/create-connection.dto.ts @@ -0,0 +1,4 @@ +import { createZodDto } from 'nestjs-zod' +import { CreateConnection } from '../../../../core/type/connection.type' + +export class CreateConnectionDto extends createZodDto(CreateConnection) {} diff --git a/apps/vault/src/broker/http/rest/dto/request/initiate-connection.dto.ts b/apps/vault/src/broker/http/rest/dto/request/initiate-connection.dto.ts new file mode 100644 index 000000000..a09ca5d7c --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/request/initiate-connection.dto.ts @@ -0,0 +1,4 @@ +import { createZodDto } from 'nestjs-zod' +import { InitiateConnection } from '../../../../core/type/connection.type' + +export class InitiateConnectionDto extends createZodDto(InitiateConnection) {} diff --git a/apps/vault/src/broker/http/rest/dto/request/send-transfer.dto.ts b/apps/vault/src/broker/http/rest/dto/request/send-transfer.dto.ts new file mode 100644 index 000000000..74426019e --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/request/send-transfer.dto.ts @@ -0,0 +1,4 @@ +import { createZodDto } from 'nestjs-zod' +import { SendTransfer } from '../../../../core/type/transfer.type' + +export class SendTransferDto extends createZodDto(SendTransfer.omit({ transferId: true })) {} diff --git a/apps/vault/src/broker/http/rest/dto/request/start-scoped-sync.dto.ts b/apps/vault/src/broker/http/rest/dto/request/start-scoped-sync.dto.ts new file mode 100644 index 000000000..cde480668 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/request/start-scoped-sync.dto.ts @@ -0,0 +1,4 @@ +import { createZodDto } from 'nestjs-zod' +import { StartScopedSync } from '../../../../core/type/scoped-sync.type' + +export class StartScopedSyncDto extends createZodDto(StartScopedSync.omit({ clientId: true })) {} diff --git a/apps/vault/src/broker/http/rest/dto/request/start-sync.dto.ts b/apps/vault/src/broker/http/rest/dto/request/start-sync.dto.ts new file mode 100644 index 000000000..938084b83 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/request/start-sync.dto.ts @@ -0,0 +1,4 @@ +import { createZodDto } from 'nestjs-zod' +import { StartSync } from '../../../../core/type/sync.type' + +export class StartSyncDto extends createZodDto(StartSync.omit({ clientId: true })) {} diff --git a/apps/vault/src/broker/http/rest/dto/request/update-connection.dto.ts b/apps/vault/src/broker/http/rest/dto/request/update-connection.dto.ts new file mode 100644 index 000000000..aaa3d5387 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/request/update-connection.dto.ts @@ -0,0 +1,10 @@ +import { createZodDto } from 'nestjs-zod' +import { UpdateConnection } from '../../../../core/type/connection.type' + +export class UpdateConnectionDto extends createZodDto( + UpdateConnection.omit({ + // These are passed in the route path and request headers. + connectionId: true, + clientId: true + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-accounts.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-accounts.dto.ts new file mode 100644 index 000000000..76b38fafd --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-accounts.dto.ts @@ -0,0 +1,22 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Account, Address } from '../../../../core/type/indexed-resources.type' + +export class PaginatedAccountsDto extends createZodDto( + z.object({ + data: z.array( + Account.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + addresses: z.array( + Address.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() + }) + ) + }) + ), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-addresses.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-addresses.dto.ts new file mode 100644 index 000000000..72bc05092 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-addresses.dto.ts @@ -0,0 +1,16 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Address } from '../../../../core/type/indexed-resources.type' + +export class PaginatedAddressesDto extends createZodDto( + z.object({ + data: z.array( + Address.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() + }) + ), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-assets.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-assets.dto.ts new file mode 100644 index 000000000..89cee3040 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-assets.dto.ts @@ -0,0 +1,15 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Asset } from '../../../../core/type/asset.type' + +export class PaginatedAssetsDto extends createZodDto( + z.object({ + data: z.array( + Asset.extend({ + createdAt: z.coerce.date().optional() + }) + ), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-connections.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-connections.dto.ts new file mode 100644 index 000000000..36747e49e --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-connections.dto.ts @@ -0,0 +1,11 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Connection } from '../../../../core/type/connection.type' + +export class PaginatedConnectionsDto extends createZodDto( + z.object({ + data: z.array(Connection), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-known-destinations.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-known-destinations.dto.ts new file mode 100644 index 000000000..ad7c848c0 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-known-destinations.dto.ts @@ -0,0 +1,11 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { KnownDestination as KnownDestinationNext } from '../../../../core/type/known-destination.type' + +export class PaginatedKnownDestinationsDto extends createZodDto( + z.object({ + data: z.array(KnownDestinationNext), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-networks.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-networks.dto.ts new file mode 100644 index 000000000..370984061 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-networks.dto.ts @@ -0,0 +1,15 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Network } from '../../../../core/type/network.type' + +export class PaginatedNetworksDto extends createZodDto( + z.object({ + data: z.array( + Network.extend({ + createdAt: z.coerce.date().optional() + }) + ), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-raw-accounts.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-raw-accounts.dto.ts new file mode 100644 index 000000000..fe740aa29 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-raw-accounts.dto.ts @@ -0,0 +1,11 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { RawAccount } from '../../../../core/service/raw-account.service' + +export class PaginatedRawAccountsDto extends createZodDto( + z.object({ + data: z.array(RawAccount), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-scoped-syncs.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-scoped-syncs.dto.ts new file mode 100644 index 000000000..710530d47 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-scoped-syncs.dto.ts @@ -0,0 +1,11 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { ScopedSync } from '../../../../core/type/scoped-sync.type' + +export class PaginatedScopedSyncsDto extends createZodDto( + z.object({ + data: z.array(ScopedSync), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-syncs.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-syncs.dto.ts new file mode 100644 index 000000000..b0ea51922 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-syncs.dto.ts @@ -0,0 +1,11 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Sync } from '../../../../core/type/sync.type' + +export class PaginatedSyncsDto extends createZodDto( + z.object({ + data: z.array(Sync), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/paginated-wallets.dto.ts b/apps/vault/src/broker/http/rest/dto/response/paginated-wallets.dto.ts new file mode 100644 index 000000000..13cff686f --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/paginated-wallets.dto.ts @@ -0,0 +1,28 @@ +import { Page } from '@narval/nestjs-shared' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Account, Address, Wallet } from '../../../../core/type/indexed-resources.type' + +export class PaginatedWalletsDto extends createZodDto( + z.object({ + data: z.array( + Wallet.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + accounts: z.array( + Account.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + addresses: z.array( + Address.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() + }) + ) + }) + ) + }) + ), + page: Page + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/provider-account.dto.ts b/apps/vault/src/broker/http/rest/dto/response/provider-account.dto.ts new file mode 100644 index 000000000..030073191 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/provider-account.dto.ts @@ -0,0 +1,18 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Account, Address } from '../../../../core/type/indexed-resources.type' + +export class ProviderAccountDto extends createZodDto( + z.object({ + data: Account.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + addresses: z.array( + Address.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() + }) + ) + }) + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/provider-address.dto.ts b/apps/vault/src/broker/http/rest/dto/response/provider-address.dto.ts new file mode 100644 index 000000000..b2a4f1a31 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/provider-address.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Address } from '../../../../core/type/indexed-resources.type' + +export class ProviderAddressDto extends createZodDto( + z.object({ + data: Address + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/provider-connection.dto.ts b/apps/vault/src/broker/http/rest/dto/response/provider-connection.dto.ts new file mode 100644 index 000000000..b18511afa --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/provider-connection.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Connection } from '../../../../core/type/connection.type' + +export class ProviderConnectionDto extends createZodDto( + z.object({ + data: Connection + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/provider-known-destination.dto.ts b/apps/vault/src/broker/http/rest/dto/response/provider-known-destination.dto.ts new file mode 100644 index 000000000..7c4289d0c --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/provider-known-destination.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { KnownDestination } from '../../../../core/type/known-destination.type' + +export class KnownDestinationDto extends createZodDto( + z.object({ + data: KnownDestination + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/provider-pending-connection.dto.ts b/apps/vault/src/broker/http/rest/dto/response/provider-pending-connection.dto.ts new file mode 100644 index 000000000..2211bf744 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/provider-pending-connection.dto.ts @@ -0,0 +1,34 @@ +import { hexSchema } from '@narval/policy-engine-shared' +import { publicKeySchema } from '@narval/signature' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { PendingConnection } from '../../../../core/type/connection.type' + +export class ProviderPendingConnectionDto extends createZodDto( + z.object({ + data: PendingConnection.pick({ + clientId: true, + connectionId: true, + provider: true, + status: true, + createdAt: true + }).extend({ + publicKey: z + .object({ + keyId: z.string().optional(), + jwk: publicKeySchema.optional(), + hex: hexSchema.optional(), + csr: z + .string() + .optional() + .describe('Certificate Signing Request PEM format of RSA public key encoded as base64') + }) + .optional(), + encryptionPublicKey: z.object({ + keyId: z.string().optional(), + jwk: publicKeySchema.optional().describe('JWK format of the public key'), + pem: z.string().optional().describe('Base64url encoded PEM public key') + }) + }) + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/provider-wallet.dto.ts b/apps/vault/src/broker/http/rest/dto/response/provider-wallet.dto.ts new file mode 100644 index 000000000..ae410db47 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/provider-wallet.dto.ts @@ -0,0 +1,24 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Account, Address, Wallet } from '../../../../core/type/indexed-resources.type' + +export class ProviderWalletDto extends createZodDto( + z.object({ + data: Wallet.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + accounts: z.array( + Account.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + addresses: z.array( + Address.extend({ + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() + }) + ) + }) + ) + }) + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/scoped-sync-started.dto.ts b/apps/vault/src/broker/http/rest/dto/response/scoped-sync-started.dto.ts new file mode 100644 index 000000000..b170df6f4 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/scoped-sync-started.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { ScopedSyncStarted } from '../../../../core/type/scoped-sync.type' + +export class ScopedSyncStartedDto extends createZodDto( + z.object({ + data: ScopedSyncStarted + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/scoped-sync.dto.ts b/apps/vault/src/broker/http/rest/dto/response/scoped-sync.dto.ts new file mode 100644 index 000000000..d0bed4513 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/scoped-sync.dto.ts @@ -0,0 +1,11 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { RawAccountSyncFailure, ScopedSync } from '../../../../core/type/scoped-sync.type' + +export class ScopedSyncDto extends createZodDto( + z.object({ + data: ScopedSync.extend({ + failures: z.array(RawAccountSyncFailure) + }) + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/sync-started.dto.ts b/apps/vault/src/broker/http/rest/dto/response/sync-started.dto.ts new file mode 100644 index 000000000..6880fcdd4 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/sync-started.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { SyncStarted } from '../../../../core/type/sync.type' + +export class SyncStartedDto extends createZodDto( + z.object({ + data: SyncStarted + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/sync-status.dto.ts b/apps/vault/src/broker/http/rest/dto/response/sync-status.dto.ts new file mode 100644 index 000000000..6880fcdd4 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/sync-status.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { SyncStarted } from '../../../../core/type/sync.type' + +export class SyncStartedDto extends createZodDto( + z.object({ + data: SyncStarted + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/sync.dto.ts b/apps/vault/src/broker/http/rest/dto/response/sync.dto.ts new file mode 100644 index 000000000..e84025fbb --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/sync.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { Sync } from '../../../../core/type/sync.type' + +export class SyncDto extends createZodDto( + z.object({ + data: Sync + }) +) {} diff --git a/apps/vault/src/broker/http/rest/dto/response/transfer.dto.ts b/apps/vault/src/broker/http/rest/dto/response/transfer.dto.ts new file mode 100644 index 000000000..b5ef1c656 --- /dev/null +++ b/apps/vault/src/broker/http/rest/dto/response/transfer.dto.ts @@ -0,0 +1,9 @@ +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' +import { InternalTransfer } from '../../../../core/type/transfer.type' + +export class TransferDto extends createZodDto( + z.object({ + data: InternalTransfer + }) +) {} diff --git a/apps/vault/src/broker/persistence/repository/account.repository.ts b/apps/vault/src/broker/persistence/repository/account.repository.ts new file mode 100644 index 000000000..6ff4edb5a --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/account.repository.ts @@ -0,0 +1,267 @@ +import { + LoggerService, + PaginatedResult, + PaginationOptions, + applyPagination, + getPaginatedResult +} from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { ProviderAccount, ProviderAddress } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { NotFoundException } from '../../core/exception/not-found.exception' +import { Account, Address } from '../../core/type/indexed-resources.type' +import { ConnectionScope } from '../../core/type/scope.type' +import { AddressRepository } from './address.repository' + +type ProviderAccountAndRelations = ProviderAccount & { + addresses: ProviderAddress[] +} + +type FindAllFilters = { + filters?: { + walletId?: string + externalIds?: string[] + } +} + +export type FindAllOptions = FindAllFilters & { pagination?: PaginationOptions } + +export type UpdateAccount = { + clientId: string + accountId: string + label?: string + addresses?: Address[] + updatedAt?: Date +} + +@Injectable() +export class AccountRepository { + constructor( + private prismaService: PrismaService, + private readonly logger: LoggerService + ) {} + + static parseModel(model: ProviderAccountAndRelations): Account { + const { id, ...rest } = model + + return Account.parse({ + ...rest, + accountId: id, + addresses: model.addresses.map(AddressRepository.parseModel) + }) + } + + static parseEntity(account: Account): ProviderAccount { + return { + clientId: account.clientId, + createdAt: account.createdAt, + externalId: account.externalId, + connectionId: account.connectionId, + id: account.accountId, + label: account.label || null, + networkId: account.networkId, + provider: account.provider, + updatedAt: account.updatedAt, + walletId: account.walletId + } + } + + async findByClientId(clientId: string, opts?: PaginationOptions): Promise> { + const pagination = applyPagination(opts) + + const items = await this.prismaService.providerAccount.findMany({ + where: { clientId }, + include: { + addresses: true + }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items, pagination }) + return { + data: data.map(AccountRepository.parseModel), + page + } + } + + async findById({ clientId, connectionId }: ConnectionScope, accountId: string): Promise { + const account = await this.prismaService.providerAccount.findUnique({ + where: { + clientId, + connectionId, + id: accountId + }, + include: { + addresses: true + } + }) + if (!account) { + throw new NotFoundException({ + message: 'Account not found', + context: { accountId } + }) + } + + return AccountRepository.parseModel(account) + } + + async findAddressesByAccountId( + clientId: string, + accountId: string, + pagination: PaginationOptions + ): Promise> { + const account = await this.prismaService.providerAccount.findUnique({ + where: { clientId, id: accountId }, + include: { + addresses: true, + ...pagination + } + }) + if (!account) { + throw new NotFoundException({ + message: 'Account not found', + context: { accountId } + }) + } + const { data, page } = getPaginatedResult({ items: account.addresses, pagination }) + return { + data: data.map(AddressRepository.parseModel), + page + } + } + + async bulkCreate(accounts: Account[]): Promise { + await this.prismaService.providerAccount.createMany({ + data: accounts.map(AccountRepository.parseEntity) + }) + + return accounts + } + + async bulkUpdate(updateAccounts: UpdateAccount[]): Promise { + await Promise.all(updateAccounts.map((u) => this.update(u))) + + return true + } + + async update(updateAccount: UpdateAccount): Promise { + await this.prismaService.providerAccount.update({ + where: { + clientId: updateAccount.clientId, + id: updateAccount.accountId + }, + data: { + label: updateAccount.label, + updatedAt: updateAccount.updatedAt || new Date() + } + }) + + return true + } + + async bulkUpsert(accounts: Account[]): Promise { + const providerAccounts = accounts.map(AccountRepository.parseEntity) + const stats = { + inserted: 0, + updated: 0 + } + + const existingAccounts = await this.prismaService.providerAccount.findMany({ + where: { + OR: providerAccounts.map((account) => ({ + clientId: account.clientId, + connectionId: account.connectionId, + externalId: account.externalId + })) + } + }) + + const results = await this.prismaService.$transaction(async (tx) => { + const operations = await Promise.all( + providerAccounts.map(async (account) => { + const existing = existingAccounts.find( + (a) => + a.clientId === account.clientId && + a.connectionId === account.connectionId && + a.externalId === account.externalId + ) + + const result = await tx.providerAccount.upsert({ + where: { + clientId_connectionId_externalId: { + clientId: account.clientId, + connectionId: account.connectionId, + externalId: account.externalId + } + }, + create: { + ...account + }, + update: { + label: account.label, + updatedAt: account.updatedAt + }, + include: { + addresses: true + } + }) + + if (!existing) { + stats.inserted++ + } else { + stats.updated++ + } + + return result + }) + ) + + return operations + }) + + this.logger.log('Account bulk upsert operation completed:', { + total: accounts.length, + inserted: stats.inserted, + updated: stats.updated + }) + + return results.map(AccountRepository.parseModel) + } + + async findAll( + { clientId, connectionId }: ConnectionScope, + options?: FindAllOptions + ): Promise> { + const pagination = applyPagination(options?.pagination) + + const models = await this.prismaService.providerAccount.findMany({ + where: { + clientId, + connectionId, + ...(options?.filters?.walletId + ? { + walletId: options.filters.walletId + } + : {}), + ...(options?.filters?.externalIds + ? { + externalId: { + in: options.filters.externalIds + } + } + : {}) + }, + include: { + addresses: true + }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items: models, pagination }) + + return { + data: data.map(AccountRepository.parseModel), + page + } + } +} diff --git a/apps/vault/src/broker/persistence/repository/address.repository.ts b/apps/vault/src/broker/persistence/repository/address.repository.ts new file mode 100644 index 000000000..8287e19ed --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/address.repository.ts @@ -0,0 +1,163 @@ +import { + LoggerService, + PaginatedResult, + PaginationOptions, + applyPagination, + getPaginatedResult +} from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { ProviderAddress } from '@prisma/client/vault' +import { z } from 'zod' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { NotFoundException } from '../../core/exception/not-found.exception' +import { Address } from '../../core/type/indexed-resources.type' +import { Provider } from '../../core/type/provider.type' +import { ConnectionScope } from '../../core/type/scope.type' + +type FindAllFilters = { + filters?: { + externalIds?: string[] + addresses?: string[] + provider?: Provider + connectionId?: string + } +} + +export type FindAllOptions = FindAllFilters & { pagination?: PaginationOptions } + +@Injectable() +export class AddressRepository { + constructor( + private prismaService: PrismaService, + private readonly logger: LoggerService + ) {} + + static parseModel(model: ProviderAddress): Address { + const { id, ...rest } = model + + return Address.parse({ + ...rest, + addressId: id, + provider: z.nativeEnum(Provider).parse(model.provider) + }) + } + + static parseEntity(entity: Address): ProviderAddress { + return { + accountId: entity.accountId, + address: entity.address, + clientId: entity.clientId, + connectionId: entity.connectionId, + createdAt: entity.createdAt, + externalId: entity.externalId, + id: entity.addressId, + provider: entity.provider, + updatedAt: entity.updatedAt + } + } + + async findByClientId(clientId: string, opts?: PaginationOptions): Promise> { + const pagination = applyPagination(opts) + + const result = await this.prismaService.providerAddress.findMany({ + where: { clientId }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items: result, pagination }) + return { + data: data.map(AddressRepository.parseModel), + page + } + } + + async findById({ clientId, connectionId }: ConnectionScope, addressId: string): Promise

{ + const address = await this.prismaService.providerAddress.findUnique({ + where: { + clientId, + connectionId, + id: addressId + } + }) + + if (!address) { + throw new NotFoundException({ + message: 'Address not found', + context: { addressId } + }) + } + + return AddressRepository.parseModel(address) + } + + async findByAddressAndNetwork(clientId: string, address: string, networkId: string): Promise { + const models = await this.prismaService.providerAddress.findMany({ + where: { + clientId, + address, + account: { + networkId + } + } + }) + + return models.map(AddressRepository.parseModel) + } + + async findAll({ clientId, connectionId }: ConnectionScope, opts?: FindAllOptions): Promise> { + const pagination = applyPagination(opts?.pagination) + + const models = await this.prismaService.providerAddress.findMany({ + where: { + clientId, + connectionId, + ...(opts?.filters?.provider + ? { + provider: opts.filters.provider + } + : {}), + ...(opts?.filters?.addresses + ? { + address: { + in: opts.filters.addresses + } + } + : {}), + ...(opts?.filters?.externalIds + ? { + externalId: { + in: opts.filters.externalIds + } + } + : {}), + ...(opts?.filters?.connectionId + ? { + connectionId: opts.filters.connectionId + } + : {}) + }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items: models, pagination }) + + return { + data: data.map(AddressRepository.parseModel), + page + } + } + + async bulkCreate(addresses: Address[]): Promise { + const { count } = await this.prismaService.providerAddress.createMany({ + data: addresses.map(AddressRepository.parseEntity), + skipDuplicates: true + }) + + this.logger.log('Address bulk create operation done', { + addressesLength: addresses.length, + addressesCreated: count, + addressesSkipped: addresses.length - count + }) + return addresses + } +} diff --git a/apps/vault/src/broker/persistence/repository/asset.repository.ts b/apps/vault/src/broker/persistence/repository/asset.repository.ts new file mode 100644 index 000000000..486379706 --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/asset.repository.ts @@ -0,0 +1,234 @@ +import { Injectable } from '@nestjs/common' +import { Prisma } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { AssetException } from '../../core/exception/asset.exception' +import { Asset, ExternalAsset } from '../../core/type/asset.type' +import { Provider } from '../../core/type/provider.type' + +type FindAllOptions = { + filters?: { + provider?: Provider + } +} + +@Injectable() +export class AssetRepository { + constructor(private readonly prismaService: PrismaService) {} + + static parseModel( + model: Prisma.AssetGetPayload<{ + include: { + externalAssets: true + } + }> + ): Asset { + return Asset.parse({ + assetId: model.id, + name: model.name, + symbol: model.symbol, + decimals: model.decimals, + networkId: model.networkId, + onchainId: model.onchainId, + createdAt: model.createdAt, + externalAssets: model.externalAssets.map(({ externalId, provider }) => ({ + provider, + externalId + })) + }) + } + + async bulkCreate(assets: Asset[]): Promise { + for (const asset of assets) { + const createdAt = asset.createdAt || new Date() + + const data = { + createdAt, + id: asset.assetId, + name: asset.name, + symbol: asset.symbol, + decimals: asset.decimals, + networkId: asset.networkId, + onchainId: asset.onchainId + } + + await this.prismaService.asset.upsert({ + where: { + id: asset.assetId + }, + create: data, + update: data + }) + + const externalAssets = asset.externalAssets.map(({ provider, externalId }) => ({ + provider, + externalId, + assetId: asset.assetId, + createdAt + })) + + await this.prismaService.providerAsset.createMany({ + data: externalAssets, + skipDuplicates: true + }) + } + + return assets + } + + async create(asset: Asset): Promise { + const parse = Asset.parse(asset) + const createdAt = parse.createdAt || new Date() + + await this.prismaService.asset.create({ + data: { + createdAt, + id: parse.assetId, + name: parse.name, + symbol: parse.symbol, + decimals: parse.decimals, + networkId: parse.networkId, + onchainId: parse.onchainId, + externalAssets: { + createMany: { + data: parse.externalAssets.map(({ provider, externalId }) => ({ + provider, + externalId, + createdAt + })) + } + } + }, + include: { + externalAssets: true + } + }) + + return parse + } + + async addExternalAsset(assetId: string, externalAsset: ExternalAsset): Promise { + await this.prismaService.providerAsset.create({ + data: { + ...externalAsset, + assetId + } + }) + + return externalAsset + } + + async bulkAddExternalAsset( + params: { + assetId: string + externalAsset: ExternalAsset + }[] + ): Promise { + await this.prismaService.providerAsset.createMany({ + data: params.map(({ assetId, externalAsset }) => ({ + ...externalAsset, + assetId + })) + }) + + return true + } + + async findAll(options?: FindAllOptions): Promise { + const models = await this.prismaService.asset.findMany({ + where: { + ...(options?.filters?.provider + ? { + externalAssets: { + some: { + provider: options.filters.provider + } + } + } + : {}) + }, + include: { + externalAssets: true + } + }) + + return models.map(AssetRepository.parseModel) + } + + async findById(assetId: string): Promise { + const model = await this.prismaService.asset.findUnique({ + where: { + id: assetId.toUpperCase() + }, + include: { + externalAssets: true + } + }) + + if (model) { + return AssetRepository.parseModel(model) + } + + return null + } + + async findByExternalId(provider: Provider, externalId: string): Promise { + const model = await this.prismaService.asset.findFirst({ + where: { + externalAssets: { + some: { + provider, + externalId: externalId.toUpperCase() + } + } + }, + include: { + externalAssets: true + } + }) + + if (model) { + return AssetRepository.parseModel(model) + } + + return null + } + + async findByOnchainId(networkId: string, onchainId: string): Promise { + const models = await this.prismaService.asset.findMany({ + where: { + networkId: networkId.toUpperCase(), + onchainId: onchainId.toLowerCase() + }, + include: { + externalAssets: true + } + }) + + return models.length ? AssetRepository.parseModel(models[0]) : null + } + + async findNative(networkId: string): Promise { + const models = await this.prismaService.asset.findMany({ + where: { + networkId, + onchainId: null + }, + include: { + externalAssets: true + } + }) + + // NOTE: This invariant is protected at the service level on create. + if (models.length > 1) { + throw new AssetException({ + message: 'Found more than one native asset for network', + context: { + networkId, + modelIds: models.map(({ id }) => id) + } + }) + } + + return models.length ? AssetRepository.parseModel(models[0]) : null + } +} diff --git a/apps/vault/src/broker/persistence/repository/connection.repository.ts b/apps/vault/src/broker/persistence/repository/connection.repository.ts new file mode 100644 index 000000000..4ade1775b --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/connection.repository.ts @@ -0,0 +1,224 @@ +import { PaginatedResult, PaginationOptions, applyPagination, getPaginatedResult } from '@narval/nestjs-shared' +import { HttpStatus, Injectable } from '@nestjs/common' +import { ProviderConnection } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { BrokerException } from '../../core/exception/broker.exception' +import { ConnectionInvalidCredentialsException } from '../../core/exception/connection-invalid-credentials.exception' +import { ConnectionParseException } from '../../core/exception/connection-parse.exception' +import { NotFoundException } from '../../core/exception/not-found.exception' +import { Connection, ConnectionStatus, ConnectionWithCredentials } from '../../core/type/connection.type' +import { Provider } from '../../core/type/provider.type' + +export type UpdateConnection = { + clientId: string + connectionId: string + credentials?: unknown | null + label?: string + provider: Provider + revokedAt?: Date + status?: ConnectionStatus + updatedAt?: Date + createdAt: Date + url?: string +} + +export type FilterOptions = { + filters?: { + status?: ConnectionStatus + } +} + +export type FindAllOptions = FilterOptions & { pagination?: PaginationOptions } + +export const SELECT_WITHOUT_CREDENTIALS = { + id: true, + clientId: true, + provider: true, + url: true, + label: true, + credentials: false, // DO NOT INCLUDE CREDENTIALS + status: true, + integrity: true, + createdAt: true, + updatedAt: true, + revokedAt: true +} + +@Injectable() +export class ConnectionRepository { + constructor(private readonly prismaService: PrismaService) {} + + static parseModel(model?: Partial | null): Connection { + const parse = Connection.safeParse({ + ...model, + connectionId: model?.id, + // Prisma always returns null for optional fields that don't have a + // value, rather than undefined. This is actually by design and aligns + // with how NULL values work in databases. + label: model?.label || undefined, + revokedAt: model?.revokedAt || undefined, + url: model?.url || undefined + }) + + if (parse.success) { + return parse.data + } + + throw new ConnectionParseException({ + context: { errors: parse.error.errors } + }) + } + + async create(connection: ConnectionWithCredentials): Promise { + const data = { + clientId: connection.clientId, + createdAt: connection.createdAt, + credentials: PrismaService.toStringJson(connection.credentials), + id: connection.connectionId, + label: connection.label || null, + provider: connection.provider, + status: connection.status, + updatedAt: connection.updatedAt, + url: connection.url || null + } + + await this.prismaService.providerConnection.upsert({ + where: { id: data.id }, + create: data, + update: data + }) + + return Connection.parse(connection) + } + + async update(updateConnection: UpdateConnection): Promise { + if (!updateConnection.connectionId) { + throw new BrokerException({ + message: 'Missing connectionId', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) + } + + await this.prismaService.providerConnection.update({ + where: { + id: updateConnection.connectionId, + clientId: updateConnection.clientId + }, + data: { + clientId: updateConnection.clientId, + createdAt: updateConnection.createdAt, + credentials: PrismaService.toStringJson(updateConnection.credentials), + id: updateConnection.connectionId, + label: updateConnection.label, + provider: updateConnection.provider, + revokedAt: updateConnection.revokedAt, + status: updateConnection.status, + updatedAt: updateConnection.updatedAt, + url: updateConnection.url + } + }) + + return true + } + + async findById(clientId: string, connectionId: string): Promise { + const model = await this.prismaService.providerConnection.findUnique({ + where: { clientId, id: connectionId }, + select: SELECT_WITHOUT_CREDENTIALS + }) + + if (model) { + return ConnectionRepository.parseModel(model) + } + + throw new NotFoundException({ context: { clientId, connectionId } }) + } + + async findAll(clientId: string, options?: FindAllOptions): Promise> { + const pagination = applyPagination(options?.pagination) + const models = await this.prismaService.providerConnection.findMany({ + where: { + clientId, + status: options?.filters?.status + }, + select: SELECT_WITHOUT_CREDENTIALS, + ...pagination + }) + const { data, page } = getPaginatedResult({ items: models, pagination }) + + return { + data: data.map((model) => ConnectionRepository.parseModel(model)), + page + } + } + + async exists(clientId: string, id: string): Promise { + const count = await this.prismaService.providerConnection.count({ + where: { id, clientId } + }) + + return count > 0 + } + + async findCredentialsJson({ connectionId }: { connectionId: string }): Promise { + const model = await this.prismaService.providerConnection.findUnique({ + where: { id: connectionId } + }) + + if (model && model.credentials) { + return this.toCredentialsJson(model.credentials) + } + + return null + } + + async findAllWithCredentials( + clientId: string, + options?: FindAllOptions + ): Promise> { + const pagination = applyPagination(options?.pagination) + const models = await this.prismaService.providerConnection.findMany({ + where: { + clientId, + status: options?.filters?.status + }, + ...pagination + }) + const { data, page } = getPaginatedResult({ items: models, pagination }) + + return { + data: data.map((model) => ({ + ...ConnectionRepository.parseModel(model), + credentials: model.credentials ? this.toCredentialsJson(model.credentials) : null + })), + page + } + } + + async findWithCredentialsById(clientId: string, connectionId: string): Promise { + const model = await this.prismaService.providerConnection.findUnique({ + where: { clientId, id: connectionId } + }) + + if (model) { + return { + ...ConnectionRepository.parseModel(model), + credentials: model.credentials ? this.toCredentialsJson(model.credentials) : null + } + } + + throw new NotFoundException({ context: { clientId, connectionId } }) + } + + private toCredentialsJson(value: string) { + try { + return JSON.parse(value) + } catch (error) { + throw new ConnectionInvalidCredentialsException({ + message: `Invalid stored connection credential JSON`, + suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, + origin: error + }) + } + } +} diff --git a/apps/vault/src/broker/persistence/repository/network.repository.ts b/apps/vault/src/broker/persistence/repository/network.repository.ts new file mode 100644 index 000000000..3d775e67c --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/network.repository.ts @@ -0,0 +1,152 @@ +import { Injectable } from '@nestjs/common' +import { Prisma } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { ExternalNetwork, Network } from '../../core/type/network.type' +import { Provider } from '../../core/type/provider.type' + +export type FindAllOptions = { + filters?: { + provider?: Provider + } +} + +@Injectable() +export class NetworkRepository { + constructor(private readonly prismaService: PrismaService) {} + + static parseModel( + model: Prisma.NetworkGetPayload<{ + include: { + externalNetworks: true + } + }> + ): Network { + return Network.parse({ + networkId: model.id, + coinType: model.coinType, + name: model.name, + createdAt: model.createdAt, + externalNetworks: model.externalNetworks.map(({ externalId, provider }) => ({ + provider, + externalId + })) + }) + } + + async bulkCreate(networks: Network[]): Promise { + for (const network of networks) { + const createdAt = network.createdAt || new Date() + + await this.prismaService.network.upsert({ + where: { + id: network.networkId + }, + create: { + createdAt, + id: network.networkId, + coinType: network.coinType, + name: network.name + }, + update: { + createdAt, + id: network.networkId, + coinType: network.coinType, + name: network.name + } + }) + + const externalNetworks = network.externalNetworks.map(({ provider, externalId }) => ({ + provider, + externalId, + networkId: network.networkId, + createdAt + })) + + for (const externalNetwork of externalNetworks) { + await this.prismaService.providerNetwork.upsert({ + where: { + provider_networkId: { + provider: externalNetwork.provider, + networkId: externalNetwork.networkId + } + }, + create: externalNetwork, + update: externalNetwork + }) + } + } + + return networks + } + + async addExternalNetwork(networkId: string, externalNetwork: ExternalNetwork): Promise { + await this.prismaService.providerNetwork.create({ + data: { + ...externalNetwork, + networkId + } + }) + + return externalNetwork + } + + async findAll(options?: FindAllOptions): Promise { + const models = await this.prismaService.network.findMany({ + where: { + ...(options?.filters?.provider + ? { + externalNetworks: { + some: { + provider: options.filters.provider + } + } + } + : {}) + }, + include: { + externalNetworks: true + } + }) + + return models.map(NetworkRepository.parseModel) + } + + async findById(networkId: string): Promise { + const model = await this.prismaService.network.findUnique({ + where: { + id: networkId.toUpperCase() + }, + include: { + externalNetworks: true + } + }) + + if (model) { + return NetworkRepository.parseModel(model) + } + + return null + } + + async findByExternalId(provider: Provider, externalId: string): Promise { + const model = await this.prismaService.network.findFirst({ + where: { + externalNetworks: { + some: { + provider, + externalId: externalId.toUpperCase() + } + } + }, + include: { + externalNetworks: true + } + }) + + if (model) { + return NetworkRepository.parseModel(model) + } + + return null + } +} diff --git a/apps/vault/src/broker/persistence/repository/scoped-sync.repository.ts b/apps/vault/src/broker/persistence/repository/scoped-sync.repository.ts new file mode 100644 index 000000000..41d2d2a3b --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/scoped-sync.repository.ts @@ -0,0 +1,163 @@ +import { PaginatedResult, PaginationOptions, applyPagination, getPaginatedResult } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common/decorators' +import { ProviderScopedSync } from '@prisma/client/vault' +import { z } from 'zod' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { ModelInvalidException } from '../../core/exception/model-invalid.exception' +import { NotFoundException } from '../../core/exception/not-found.exception' +import { ConnectionScope } from '../../core/type/scope.type' +import { RawAccount, RawAccountSyncFailure, ScopedSync, ScopedSyncStatus } from '../../core/type/scoped-sync.type' + +export type FindAllOptions = PaginationOptions & { + filters?: { + status?: ScopedSyncStatus + } + pagination?: PaginationOptions +} + +export type UpdateScopedSync = { + clientId: string + scopedSyncId: string + completedAt?: Date + status?: ScopedSyncStatus + error?: ScopedSync['error'] + failures?: RawAccountSyncFailure[] +} + +const parseErrorEntity = ( + error: ScopedSync['error'] +): Pick => ({ + errorName: error?.name || null, + errorMessage: error?.message || null, + errorTraceId: error?.traceId || null +}) + +@Injectable() +export class ScopedSyncRepository { + constructor(private readonly prismaService: PrismaService) {} + + static parseModel(model?: ProviderScopedSync | null): ScopedSync { + if (model) { + const { id, ...rest } = model + + return ScopedSync.parse({ + ...rest, + scopedSyncId: id, + // Prisma always returns null for optional fields that don't have a + // value, rather than undefined. This is actually by design and aligns + // with how NULL values work in databases. + rawAccounts: model.rawAccounts ? z.array(RawAccount).parse(PrismaService.toJson(model.rawAccounts)) : [], + completedAt: model.completedAt || undefined, + failures: z.array(RawAccountSyncFailure).parse(PrismaService.toJson(model.failedRawAccounts) || []), + error: + model.errorName || model.errorMessage || model.errorTraceId + ? { + name: model.errorName || undefined, + message: model.errorMessage || undefined, + traceId: model.errorTraceId || undefined + } + : undefined + }) + } + + throw new ModelInvalidException() + } + + static parseEntity(entity: ScopedSync): ProviderScopedSync { + return { + id: entity.scopedSyncId, + clientId: entity.clientId, + completedAt: entity.completedAt || null, + connectionId: entity.connectionId, + rawAccounts: z.string().parse(PrismaService.toStringJson(entity.rawAccounts)), + createdAt: entity.createdAt, + status: entity.status, + failedRawAccounts: PrismaService.toStringJson(entity.failures), + ...parseErrorEntity(entity.error) + } + } + + async create(scopedSync: ScopedSync): Promise { + await this.prismaService.providerScopedSync.create({ + data: ScopedSyncRepository.parseEntity(scopedSync) + }) + + return scopedSync + } + + async bulkCreate(scopedSyncs: ScopedSync[]): Promise { + await this.prismaService.providerScopedSync.createMany({ + data: scopedSyncs.map(ScopedSyncRepository.parseEntity) + }) + + return scopedSyncs + } + + async update(updateScopedSync: UpdateScopedSync): Promise { + const failures = updateScopedSync.failures ? PrismaService.toStringJson(updateScopedSync.failures) : null + await this.prismaService.providerScopedSync.update({ + where: { + id: updateScopedSync.scopedSyncId, + clientId: updateScopedSync.clientId + }, + data: { + completedAt: updateScopedSync.completedAt, + status: updateScopedSync.status, + ...(updateScopedSync.error ? parseErrorEntity(updateScopedSync.error) : {}), + failedRawAccounts: failures + } + }) + + return true + } + + async findById({ clientId, connectionId }: ConnectionScope, scopedSyncId: string): Promise { + const model = await this.prismaService.providerScopedSync.findUnique({ + where: { + clientId, + id: scopedSyncId, + connectionId + } + }) + + if (model) { + return ScopedSyncRepository.parseModel(model) + } + + throw new NotFoundException({ context: { clientId, scopedSyncId } }) + } + + async findAll( + { clientId, connectionId }: ConnectionScope, + options?: FindAllOptions + ): Promise> { + const pagination = applyPagination(options?.pagination) + + const models = await this.prismaService.providerScopedSync.findMany({ + where: { + clientId, + status: options?.filters?.status, + connectionId + }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items: models, pagination }) + + return { + data: data.map(ScopedSyncRepository.parseModel), + page + } + } + + async exists({ clientId, connectionId, status }: ConnectionScope & { status?: ScopedSyncStatus }): Promise { + const count = await this.prismaService.providerScopedSync.count({ + where: { + clientId, + connectionId, + ...(status ? { status } : {}) + } + }) + return count > 0 + } +} diff --git a/apps/vault/src/broker/persistence/repository/sync.repository.ts b/apps/vault/src/broker/persistence/repository/sync.repository.ts new file mode 100644 index 000000000..fbaaf3d11 --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/sync.repository.ts @@ -0,0 +1,139 @@ +import { PaginatedResult, PaginationOptions, applyPagination, getPaginatedResult } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common/decorators' +import { ProviderSync } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { ModelInvalidException } from '../../core/exception/model-invalid.exception' +import { NotFoundException } from '../../core/exception/not-found.exception' +import { ConnectionScope } from '../../core/type/scope.type' +import { Sync, SyncStatus } from '../../core/type/sync.type' + +export type FindAllOptions = PaginationOptions & { + filters?: { + status?: SyncStatus + } + pagination?: PaginationOptions +} + +export type UpdateSync = { + clientId: string + syncId: string + completedAt?: Date + status?: SyncStatus + error?: Sync['error'] +} + +const parseErrorEntity = (error: Sync['error']): Pick => ({ + errorName: error?.name || null, + errorMessage: error?.message || null, + errorTraceId: error?.traceId || null +}) + +@Injectable() +export class SyncRepository { + constructor(private readonly prismaService: PrismaService) {} + + static parseModel(model?: ProviderSync | null): Sync { + if (model) { + const { id, ...rest } = model + + return Sync.parse({ + ...rest, + syncId: id, + // Prisma always returns null for optional fields that don't have a + // value, rather than undefined. This is actually by design and aligns + // with how NULL values work in databases. + completedAt: model.completedAt || undefined, + error: + model.errorName || model.errorMessage || model.errorTraceId + ? { + name: model.errorName || undefined, + message: model.errorMessage || undefined, + traceId: model.errorTraceId || undefined + } + : undefined + }) + } + + throw new ModelInvalidException() + } + + static parseEntity(entity: Sync): ProviderSync { + return { + id: entity.syncId, + clientId: entity.clientId, + completedAt: entity.completedAt || null, + connectionId: entity.connectionId, + createdAt: entity.createdAt, + status: entity.status, + ...parseErrorEntity(entity.error) + } + } + + async create(sync: Sync): Promise { + await this.prismaService.providerSync.create({ + data: SyncRepository.parseEntity(sync) + }) + + return sync + } + + async bulkCreate(syncs: Sync[]): Promise { + await this.prismaService.providerSync.createMany({ + data: syncs.map(SyncRepository.parseEntity) + }) + + return syncs + } + + async update(updateSync: UpdateSync): Promise { + await this.prismaService.providerSync.update({ + where: { + id: updateSync.syncId, + clientId: updateSync.clientId + }, + data: { + completedAt: updateSync.completedAt, + status: updateSync.status, + ...(updateSync.error ? parseErrorEntity(updateSync.error) : {}) + } + }) + + return true + } + + async findById({ clientId, connectionId }: ConnectionScope, syncId: string): Promise { + const model = await this.prismaService.providerSync.findUnique({ + where: { + clientId, + connectionId, + id: syncId + } + }) + + if (model) { + return SyncRepository.parseModel(model) + } + + throw new NotFoundException({ context: { clientId, syncId } }) + } + + async findAll({ clientId, connectionId }: ConnectionScope, options?: FindAllOptions): Promise> { + const pagination = applyPagination(options?.pagination) + + const models = await this.prismaService.providerSync.findMany({ + where: { + clientId, + connectionId, + status: options?.filters?.status + }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items: models, pagination }) + + return { + data: data.map(SyncRepository.parseModel), + page + } + } +} diff --git a/apps/vault/src/broker/persistence/repository/transfer.repository.ts b/apps/vault/src/broker/persistence/repository/transfer.repository.ts new file mode 100644 index 000000000..1dcd2f75e --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/transfer.repository.ts @@ -0,0 +1,188 @@ +import { Injectable, NotFoundException } from '@nestjs/common' +import { ProviderTransfer } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { + InternalTransfer, + TransferPartyType, + TransferStatus, + isAddressDestination +} from '../../core/type/transfer.type' + +@Injectable() +export class TransferRepository { + constructor(private prismaService: PrismaService) {} + + static parseEntity(entity: InternalTransfer): ProviderTransfer { + return { + assetExternalId: entity.assetExternalId, + assetId: entity.assetId, + clientId: entity.clientId, + connectionId: entity.connectionId, + createdAt: entity.createdAt, + customerRefId: entity.customerRefId, + externalId: entity.externalId, + grossAmount: entity.grossAmount, + id: entity.transferId, + idempotenceId: entity.idempotenceId, + memo: entity.memo, + networkFeeAttribution: entity.networkFeeAttribution, + provider: entity.provider, + providerSpecific: PrismaService.toStringJson(entity.providerSpecific), + ...(entity.source.type === TransferPartyType.WALLET + ? { + sourceWalletId: entity.source.id + } + : { + sourceWalletId: null + }), + ...(entity.source.type === TransferPartyType.ACCOUNT + ? { + sourceAccountId: entity.source.id + } + : { + sourceAccountId: null + }), + ...(entity.source.type === TransferPartyType.ADDRESS + ? { + sourceAddressId: entity.source.id + } + : { + sourceAddressId: null + }), + ...(!isAddressDestination(entity.destination) && entity.destination.type === TransferPartyType.WALLET + ? { + destinationWalletId: entity.destination.id + } + : { + destinationWalletId: null + }), + ...(!isAddressDestination(entity.destination) && entity.destination.type === TransferPartyType.ACCOUNT + ? { + destinationAccountId: entity.destination.id + } + : { + destinationAccountId: null + }), + ...(!isAddressDestination(entity.destination) && entity.destination.type === TransferPartyType.ADDRESS + ? { + destinationAddressId: entity.destination.id + } + : { + destinationAddressId: null + }), + ...(isAddressDestination(entity.destination) + ? { + destinationAddressRaw: entity.destination.address + } + : { + destinationAddressRaw: null + }) + } + } + + static parseModel(model: ProviderTransfer): InternalTransfer { + return InternalTransfer.parse({ + assetExternalId: model.assetExternalId, + assetId: model.assetId, + clientId: model.clientId, + createdAt: model.createdAt, + customerRefId: model.customerRefId, + externalId: model.externalId, + // The external status is added at runtime. + externalStatus: null, + grossAmount: model.grossAmount, + idempotenceId: model.idempotenceId, + memo: model.memo, + connectionId: model.connectionId, + networkFeeAttribution: model.networkFeeAttribution, + provider: model.provider, + providerSpecific: PrismaService.toJson(model.providerSpecific), + status: TransferStatus.PROCESSING, + transferId: model.id, + source: { + ...(model.sourceWalletId + ? { + type: TransferPartyType.WALLET, + id: model.sourceWalletId + } + : {}), + ...(model.sourceAccountId + ? { + type: TransferPartyType.ACCOUNT, + id: model.sourceAccountId + } + : {}), + ...(model.sourceAddressId + ? { + type: TransferPartyType.ADDRESS, + id: model.sourceAddressId + } + : {}) + }, + destination: { + ...(model.destinationWalletId + ? { + type: TransferPartyType.WALLET, + id: model.destinationWalletId + } + : {}), + ...(model.destinationAccountId + ? { + type: TransferPartyType.ACCOUNT, + id: model.destinationAccountId + } + : {}), + ...(model.destinationAddressId + ? { + type: TransferPartyType.ADDRESS, + id: model.destinationAddressId + } + : {}), + ...(model.destinationAddressRaw + ? { + address: model.destinationAddressRaw + } + : {}) + } + }) + } + + async bulkCreate(transfers: InternalTransfer[]): Promise { + await this.prismaService.providerTransfer.createMany({ + data: transfers.map((transfer) => { + return { + ...TransferRepository.parseEntity(transfer), + providerSpecific: PrismaService.toStringJson(transfer.providerSpecific) + } + }) + }) + + return transfers + } + + async findById(clientId: string, transferId: string): Promise { + const model = await this.prismaService.providerTransfer.findUnique({ + where: { + clientId, + id: transferId + } + }) + + if (model) { + return TransferRepository.parseModel(model) + } + + throw new NotFoundException({ + message: 'Transfer not found', + context: { transferId } + }) + } + + async existsByIdempotenceId(clientId: string, idempotenceId: string): Promise { + const count = await this.prismaService.providerTransfer.count({ + where: { clientId, idempotenceId } + }) + + return count > 0 + } +} diff --git a/apps/vault/src/broker/persistence/repository/wallet.repository.ts b/apps/vault/src/broker/persistence/repository/wallet.repository.ts new file mode 100644 index 000000000..93171694c --- /dev/null +++ b/apps/vault/src/broker/persistence/repository/wallet.repository.ts @@ -0,0 +1,286 @@ +import { + LoggerService, + PaginatedResult, + PaginationOptions, + applyPagination, + getPaginatedResult +} from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { ProviderAccount, ProviderAddress, ProviderWallet } from '@prisma/client/vault' +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { NotFoundException } from '../../core/exception/not-found.exception' +import { Account, UpdateWallet, Wallet } from '../../core/type/indexed-resources.type' +import { ConnectionScope } from '../../core/type/scope.type' +import { AccountRepository } from './account.repository' + +type ProviderWalletsAndRelations = ProviderWallet & { + accounts: Array< + ProviderAccount & { + addresses: ProviderAddress[] + } + > +} + +type FindAllFilters = { + filters?: { + walletIds?: string[] + externalIds?: string[] + } +} + +export type FindAllOptions = FindAllFilters & { pagination?: PaginationOptions } + +export type FindAllPaginatedOptions = PaginationOptions & FindAllFilters + +@Injectable() +export class WalletRepository { + constructor( + private prismaService: PrismaService, + private readonly logger: LoggerService + ) {} + + static parseModel(wallet: ProviderWalletsAndRelations): Wallet { + const { accounts, id, ...walletData } = wallet + + const mappedAccounts = accounts.map((account) => { + return AccountRepository.parseModel(account) + }) + + const parsedWallet = Wallet.parse({ + ...walletData, + walletId: id, + accounts: mappedAccounts + }) + + return parsedWallet + } + + static parseEntity(entity: Wallet): ProviderWallet { + return { + id: entity.walletId, + clientId: entity.clientId, + externalId: entity.externalId, + label: entity.label || null, + createdAt: entity.createdAt, + connectionId: entity.connectionId, + updatedAt: entity.updatedAt, + provider: entity.provider + } + } + + async findByClientId(clientId: string, options?: PaginationOptions): Promise> { + const pagination = applyPagination(options) + + const result = await this.prismaService.providerWallet.findMany({ + where: { clientId }, + include: { + accounts: { + include: { + addresses: true + } + } + }, + ...pagination + }) + + const { data, page } = getPaginatedResult({ items: result, pagination }) + + return { + data: data.map(WalletRepository.parseModel), + page + } + } + + async findById({ clientId, connectionId }: ConnectionScope, id: string): Promise { + const model = await this.prismaService.providerWallet.findUnique({ + where: { + clientId, + connectionId, + id + }, + include: { + accounts: { + include: { + addresses: true + } + } + } + }) + + if (!model) { + throw new NotFoundException({ + message: 'Wallet not found', + context: { walletId: id } + }) + } + + return WalletRepository.parseModel(model) + } + + async findAll( + { clientId, connectionId }: ConnectionScope, + options?: FindAllOptions + ): Promise> { + const pagination = applyPagination(options?.pagination) + const result = await this.prismaService.providerWallet.findMany({ + where: { + clientId, + connectionId, + ...(options?.filters?.walletIds + ? { + id: { + in: options?.filters.walletIds + } + } + : {}) + }, + include: { + accounts: { + include: { + addresses: true + } + } + }, + ...pagination + }) + const { data, page } = getPaginatedResult({ items: result, pagination }) + + return { + data: data.map(WalletRepository.parseModel), + page + } + } + + async findAccountsByWalletId( + clientId: string, + walletId: string, + pagination: PaginationOptions + ): Promise> { + const wallet = await this.prismaService.providerWallet.findUnique({ + where: { clientId, id: walletId }, + include: { + accounts: { + include: { + addresses: true + }, + ...pagination + } + } + }) + if (!wallet) { + throw new NotFoundException({ + message: 'Wallet not found', + context: { walletId } + }) + } + const { data, page } = getPaginatedResult({ items: wallet.accounts, pagination }) + + return { + data: data.map(AccountRepository.parseModel), + page + } + } + + async bulkCreate(wallets: Wallet[]): Promise { + const providerWallets: ProviderWallet[] = wallets.map(WalletRepository.parseEntity) + + await this.prismaService.$transaction(async (tx) => { + await tx.providerWallet.createMany({ + data: providerWallets, + skipDuplicates: true + }) + }) + + return wallets + } + + async bulkUpsert(wallets: Wallet[]): Promise { + const providerWallets: ProviderWallet[] = wallets.map(WalletRepository.parseEntity) + + const stats = { + inserted: 0, + updated: 0 + } + + const existingWallets = await this.prismaService.providerWallet.findMany({ + where: { + OR: providerWallets.map((wallet) => ({ + clientId: wallet.clientId, + connectionId: wallet.connectionId, + externalId: wallet.externalId + })) + } + }) + + const upsertedWallets = await this.prismaService.$transaction(async (tx) => { + const upsertPromises = providerWallets.map(async (wallet) => { + const existing = existingWallets.find( + (w) => + w.clientId === wallet.clientId && + w.connectionId === wallet.connectionId && + w.externalId === wallet.externalId + ) + + const result = tx.providerWallet.upsert({ + where: { + clientId_connectionId_externalId: { + clientId: wallet.clientId, + connectionId: wallet.connectionId, + externalId: wallet.externalId + } + }, + create: { + ...wallet + }, + update: { + label: wallet.label, + updatedAt: wallet.updatedAt + }, + include: { + accounts: { + include: { + addresses: true + } + } + } + }) + + if (!existing) { + stats.inserted++ + } else { + stats.updated++ + } + + return result + }) + return Promise.all(upsertPromises) + }) + + this.logger.log('Wallet bulk upsert operation completed:', { + total: wallets.length, + inserted: stats.inserted, + updated: stats.updated + }) + + return upsertedWallets.map(WalletRepository.parseModel) + } + + async update(wallet: UpdateWallet) { + const model = await this.prismaService.providerWallet.update({ + where: { id: wallet.walletId }, + data: { + updatedAt: wallet.updatedAt, + label: wallet.label + }, + include: { + accounts: { + include: { + addresses: true + } + } + } + }) + + return WalletRepository.parseModel(model) + } +} diff --git a/apps/vault/src/broker/persistence/seed/asset.seed.ts b/apps/vault/src/broker/persistence/seed/asset.seed.ts new file mode 100644 index 000000000..26ea22c94 --- /dev/null +++ b/apps/vault/src/broker/persistence/seed/asset.seed.ts @@ -0,0 +1,488 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { Provider } from '../../core/type/provider.type' +import { AssetRepository } from '../repository/asset.repository' + +@Injectable() +export class AssetSeed { + constructor( + private readonly assetRepository: AssetRepository, + private readonly logger: LoggerService + ) {} + + // IMPORTANT: There's already a data migration for base assets. + // See 20250116101514_add_asset_table_and_data/migration.sql + async seed(): Promise { + const assets = this.getAssets().map((asset) => ({ + networkId: asset.networkId, + assetId: asset.assetId, + name: asset.name, + decimals: asset.decimals, + symbol: asset.symbol, + onchainId: asset.onchainId, + externalAssets: [ + ...(asset.anchorageId + ? [ + { + provider: Provider.ANCHORAGE, + externalId: asset.anchorageId + } + ] + : []), + ...(asset.fireblocksId + ? [ + { + provider: Provider.FIREBLOCKS, + externalId: asset.fireblocksId + } + ] + : []), + ...(asset.bitgoId + ? [ + { + provider: Provider.BITGO, + externalId: asset.bitgoId + } + ] + : []) + ] + })) + + this.logger.log(`Seeding ${assets.length} assets`) + + await this.assetRepository.bulkCreate(assets) + } + + getAssets() { + return [ + { + assetId: '1INCH', + symbol: '1INCH', + decimals: 18, + name: '1inch', + networkId: 'ETHEREUM', + onchainId: '0x111111111117dc0aa78b770fa6a738034120c302', + anchorageId: '1INCH', + fireblocksId: '1INCH', + bitgoId: null + }, + { + assetId: 'AAVE', + symbol: 'AAVE', + decimals: 18, + name: 'Aave', + networkId: 'ETHEREUM', + onchainId: '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', + anchorageId: 'AAVE', + fireblocksId: 'AAVE', + bitgoId: null + }, + { + assetId: 'ARB', + symbol: 'ARB', + decimals: 18, + name: 'Arbitrum', + networkId: 'ETHEREUM', + onchainId: '0xb50721bcf8d664c30412cfbc6cf7a15145234ad1', + anchorageId: 'ARB', + fireblocksId: null, + bitgoId: null + }, + { + assetId: 'ARB_ARB', + symbol: 'ARB', + decimals: 18, + name: 'Arbitrum', + networkId: 'ARBITRUM', + onchainId: '0x912ce59144191c1204e64559fe8253a0e49e6548', + anchorageId: null, + fireblocksId: 'ARB_ARB_FRK9', + bitgoId: 'ARBETH:ARB' + }, + { + assetId: 'ATOM', + symbol: 'ATOM', + decimals: 6, + name: 'Cosmos', + networkId: 'ATOM', + onchainId: null, + anchorageId: 'ATOM', + fireblocksId: 'ATOM_COS', + bitgoId: 'ATOM' + }, + { + assetId: 'BTC', + symbol: 'BTC', + decimals: 8, + name: 'Bitcoin', + networkId: 'BITCOIN', + onchainId: null, + anchorageId: 'BTC', + fireblocksId: 'BTC', + bitgoId: 'BTC' + }, + { + assetId: 'BTC_SIGNET', + symbol: 'BTC_S', + decimals: 8, + name: 'Bitcoin Signet', + networkId: 'BITCOIN_SIGNET', + onchainId: null, + anchorageId: 'BTC_S', + fireblocksId: null, + bitgoId: null + }, + { + assetId: 'DOT', + symbol: 'DOT', + decimals: 10, + name: 'Polkadot', + networkId: 'POLKADOT', + onchainId: null, + anchorageId: null, + fireblocksId: 'DOT', + bitgoId: 'DOT' + }, + { + assetId: 'ETH', + symbol: 'ETH', + decimals: 18, + name: 'Ethereum', + networkId: 'ETHEREUM', + onchainId: null, + anchorageId: 'ETH', + fireblocksId: 'ETH', + bitgoId: 'ETH' + }, + { + assetId: 'ETH_HOLESKY', + symbol: 'ETH', + decimals: 18, + name: 'Holesky Ethereum', + networkId: 'ETHEREUM_HOLESKY', + onchainId: null, + anchorageId: 'ETHHOL', + fireblocksId: 'ETH_TEST6', + bitgoId: 'HTETH' + }, + { + assetId: 'ETH_ARB', + symbol: 'ETH', + decimals: 18, + name: 'Arbitrum Ethereum', + networkId: 'ARBITRUM', + onchainId: null, + anchorageId: null, + fireblocksId: 'ETH-AETH', + bitgoId: 'ARBETH' + }, + { + assetId: 'ETH_ARBITRUM_TEST', + symbol: 'ETH', + decimals: 18, + name: 'Arbitrum Sepolia Testnet', + networkId: 'ARBITRUM_SEPOLIA', + onchainId: null, + anchorageId: 'ETH_ARBITRUM_T', + fireblocksId: 'ETH-AETH_SEPOLIA', + bitgoId: null + }, + { + assetId: 'ETH_OPT', + symbol: 'ETH', + decimals: 18, + name: 'Optimistic Ethereum', + networkId: 'OPTIMISM', + onchainId: null, + anchorageId: null, + fireblocksId: 'ETH-OPT', + bitgoId: 'OPETH' + }, + { + assetId: 'ETH_OPT_KOVAN', + symbol: 'ETH', + decimals: 18, + name: 'Optimistic Ethereum Kovan', + networkId: 'OPTIMISM_KOVAN', + onchainId: null, + anchorageId: null, + fireblocksId: 'ETH-OPT_KOV', + bitgoId: null + }, + { + assetId: 'ETH_OPT_SEPOLIA', + symbol: 'ETH', + decimals: 18, + name: 'Optimistic Ethereum Sepolia', + networkId: 'OPTIMISM_SEPOLIA', + onchainId: null, + anchorageId: null, + fireblocksId: 'ETH-OPT_SEPOLIA', + bitgoId: null + }, + { + assetId: 'ETH_ZKSYNC_TEST', + symbol: 'ETH', + decimals: 18, + name: 'ZKsync Sepolia Testnet', + networkId: 'ZKSYNC_SEPOLIA', + onchainId: null, + anchorageId: 'ETH_ZKSYNC_T', + fireblocksId: 'ETH_ZKSYNC_ERA_SEPOLIA', + bitgoId: null + }, + { + assetId: 'LTC', + symbol: 'LTC', + decimals: 8, + name: 'Litecoin', + networkId: 'LITECOIN', + onchainId: null, + anchorageId: 'LTC', + fireblocksId: 'LTC', + bitgoId: 'LTC' + }, + { + assetId: 'MATIC', + symbol: 'MATIC', + decimals: 18, + name: 'Matic Token', + networkId: 'ETHEREUM', + onchainId: '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0', + anchorageId: 'MATIC', + fireblocksId: 'MATIC', + bitgoId: 'MATIC' + }, + { + assetId: 'MORPHO', + symbol: 'MORPHO', + decimals: 18, + name: 'Morpho Token', + networkId: 'ETHEREUM', + onchainId: '0x9994e35db50125e0df82e4c2dde62496ce330999', + anchorageId: 'MORPHO', + fireblocksId: null, + bitgoId: null + }, + { + assetId: 'POL', + symbol: 'POL', + decimals: 18, + name: 'Polygon Token', + networkId: 'ETHEREUM', + onchainId: '0x455e53cbb86018ac2b8092fdcd39d8444affc3f6', + anchorageId: 'POL', + fireblocksId: 'POL_ETH_9RYQ', + bitgoId: null + }, + { + assetId: 'POL_POLYGON', + symbol: 'POL', + decimals: 18, + name: 'Polygon', + networkId: 'POLYGON', + onchainId: null, + anchorageId: 'POL_POLYGON', + fireblocksId: 'MATIC_POLYGON', + bitgoId: 'POLYGON' + }, + { + assetId: 'PORTAL', + symbol: 'PORTAL', + decimals: 18, + name: 'PORTAL', + networkId: 'ETHEREUM', + onchainId: '0x1bbe973bef3a977fc51cbed703e8ffdefe001fed', + anchorageId: 'PORTAL', + fireblocksId: null, + bitgoId: null + }, + { + assetId: 'SOL', + symbol: 'SOL', + decimals: 9, + name: 'Solana', + networkId: 'SOLANA', + onchainId: null, + anchorageId: null, + fireblocksId: 'SOL', + bitgoId: 'SOL' + }, + { + assetId: 'SOL_DEVNET', + symbol: 'SOL', + decimals: 9, + name: 'Solana Devnet', + networkId: 'SOLONA_DEVNET', + onchainId: null, + anchorageId: 'SOL_TD', + fireblocksId: null, + bitgoId: null + }, + { + assetId: 'SOL_TEST', + symbol: 'SOL', + decimals: 9, + name: 'Solana Testnet', + networkId: 'SOLANA_TESTNET', + onchainId: null, + anchorageId: null, + fireblocksId: 'SOL_TEST', + bitgoId: null + }, + { + assetId: 'SUI_TEST', + symbol: 'SUI', + decimals: 9, + name: 'Sui Test', + networkId: 'SUI_TESTNET', + onchainId: null, + anchorageId: 'SUI_T', + fireblocksId: null, + bitgoId: 'TSUI' + }, + { + assetId: 'SUI', + symbol: 'SUI', + decimals: 9, + name: 'Sui', + networkId: 'SUI', + onchainId: null, + anchorageId: 'SUI', + fireblocksId: null, + bitgoId: 'SUI' + }, + { + assetId: 'SUSHI', + symbol: 'SUSHI', + decimals: 18, + name: 'SushiSwap', + networkId: 'ETHEREUM', + onchainId: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2', + anchorageId: 'SUSHI', + fireblocksId: 'SUSHI', + bitgoId: 'SUSHI' + }, + { + assetId: 'UNI', + symbol: 'UNI', + decimals: 18, + name: 'Uniswap', + networkId: 'ETHEREUM', + onchainId: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + anchorageId: 'UNI', + fireblocksId: 'UNI', + bitgoId: 'UNI' + }, + { + assetId: 'USDC', + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + networkId: 'ETHEREUM', + onchainId: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + anchorageId: 'USDC', + fireblocksId: 'USDC', + bitgoId: 'USDC' + }, + { + assetId: 'USDC_ARBITRUM', + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + networkId: 'ARBITRUM', + onchainId: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + anchorageId: null, + fireblocksId: 'USDC_ARB_3SBJ', + bitgoId: 'ARBETH:USDCV2' + }, + { + assetId: 'USDC_POLYGON', + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + networkId: 'POLYGON', + onchainId: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + anchorageId: null, + fireblocksId: 'USDC_POLYGON', + bitgoId: 'POLYGON:USDCV2' + }, + { + assetId: 'USDT', + symbol: 'USDT', + decimals: 6, + name: 'Tether', + networkId: 'ETHEREUM', + onchainId: '0xdac17f958d2ee523a2206206994597c13d831ec7', + anchorageId: 'USDT', + fireblocksId: 'USDT_ERC20', + bitgoId: 'USDT' + }, + { + assetId: 'USDT_POLYGON', + symbol: 'USDT', + decimals: 6, + name: 'Tether', + networkId: 'POLYGON', + onchainId: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + anchorageId: null, + fireblocksId: 'USDT_POLYGON', + bitgoId: 'OPETH:USDCV2' + }, + { + assetId: 'WBTC', + symbol: 'WBTC', + decimals: 8, + name: 'Wrapped Bitcoin', + networkId: 'ETHEREUM', + onchainId: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', + anchorageId: 'WBTC', + fireblocksId: 'WBTC', + bitgoId: 'WBTC' + }, + { + assetId: 'WETH', + symbol: 'WETH', + decimals: 18, + name: 'Wrapped Ether', + networkId: 'ETHEREUM', + onchainId: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + anchorageId: 'WETH', + fireblocksId: 'WETH', + bitgoId: 'WETH' + }, + { + assetId: 'WETH_HOLESKY', + symbol: 'WETH', + decimals: 18, + name: 'Wrapped Ether', + networkId: 'ETHEREUM_HOLESKY', + onchainId: '0x94373a4919b3240d86ea41593d5eba789fef3848', + anchorageId: null, + fireblocksId: null, + bitgoId: 'TWETH' + }, + { + assetId: 'XRP', + symbol: 'XRP', + decimals: 6, + name: 'Ripple', + networkId: 'RIPPLE', + onchainId: null, + anchorageId: 'XRP', + fireblocksId: 'XRP', + bitgoId: 'XRP' + }, + { + assetId: 'LINK_ZKSYNC_SEPOLIA', + name: 'Chainlink', + symbol: 'LINK', + decimals: 18, + networkId: 'ZKSYNC_SEPOLIA', + onchainId: '0x23a1afd896c8c8876af46adc38521f4432658d1e', + anchorageId: 'LINK_ZKSYNC_T', + fireblocksId: null + } + ] + } +} diff --git a/apps/vault/src/broker/persistence/seed/network.seed.ts b/apps/vault/src/broker/persistence/seed/network.seed.ts new file mode 100644 index 000000000..1fb26ca99 --- /dev/null +++ b/apps/vault/src/broker/persistence/seed/network.seed.ts @@ -0,0 +1,831 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { Injectable } from '@nestjs/common' +import { NetworkService } from '../../core/service/network.service' +import { Provider } from '../../core/type/provider.type' + +@Injectable() +export class NetworkSeed { + constructor( + private readonly networkService: NetworkService, + private readonly logger: LoggerService + ) {} + + async seed(): Promise { + const networks = this.getNetworks().map((network) => ({ + networkId: network.networkId, + coinType: network.coinType, + name: network.name, + externalNetworks: [ + ...(network.anchorageId + ? [ + { + provider: Provider.ANCHORAGE, + externalId: network.anchorageId + } + ] + : []), + ...(network.fireblocksId + ? [ + { + provider: Provider.FIREBLOCKS, + externalId: network.fireblocksId + } + ] + : []), + ...(network.bitgoId + ? [ + { + provider: Provider.BITGO, + externalId: network.bitgoId + } + ] + : []) + ] + })) + + this.logger.log(`Seeding ${networks.length} networks`) + + await this.networkService.bulkCreate(networks) + } + + getNetworks() { + return [ + { + networkId: 'AETH', + name: 'Aetherius', + coinType: 514, + anchorageId: null, + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'AEVO', + name: 'Aevo', + coinType: null, + anchorageId: null, + fireblocksId: 'AEVO', + bitgoId: null + }, + { + networkId: 'AGORIC', + name: 'Agoric', + coinType: 564, + anchorageId: 'BLD', + fireblocksId: null, + bitgoId: 'BLD' + }, + { + networkId: 'ALEPH_ZERO', + name: 'Aleph Zero', + coinType: 643, + anchorageId: null, + fireblocksId: 'ALEPH_ZERO_EVM', + bitgoId: null + }, + { + networkId: 'ALGORAND', + name: 'Algorand', + coinType: 283, + anchorageId: null, + fireblocksId: 'ALGO', + bitgoId: 'ALGO' + }, + { + networkId: 'ALGORAND_TESTNET', + name: 'Algorand Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'ALGO_TEST', + bitgoId: 'TALGO' + }, + { + networkId: 'ALLORA', + name: 'Allora', + coinType: null, + anchorageId: 'ALLO', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'ALLORA_TESTNET', + name: 'Allora Testnet', + coinType: 1, + anchorageId: 'ALLO_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'APTOS', + name: 'Aptos', + coinType: 637, + anchorageId: 'APT', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'APTOS_TESTNET', + name: 'Aptos Testnet', + coinType: 1, + anchorageId: 'APT_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'ARBITRUM', + name: 'Arbitrum', + coinType: 9001, + anchorageId: null, + fireblocksId: 'ETH-AETH', + bitgoId: 'ARBETH' + }, + { + networkId: 'ARBITRUM_SEPOLIA', + name: 'Arbitrum Sepolia Testnet', + coinType: 1, + anchorageId: 'ETH_ARBITRUM_T', + fireblocksId: null, + bitgoId: 'TARBETH' + }, + { + networkId: 'ASTAR', + name: 'Astar', + coinType: 810, + anchorageId: null, + fireblocksId: 'ASTR_ASTR', + bitgoId: null + }, + { + networkId: 'ASTAR_TESTNET', + name: 'Astar Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'ASTR_TEST', + bitgoId: null + }, + { + networkId: 'ATOM', + name: 'Cosmos', + coinType: 118, + anchorageId: 'ATOM', + fireblocksId: 'ATOM_COS', + bitgoId: 'ATOM' + }, + { + networkId: 'ATOM_TESTNET', + name: 'Atom Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'ATOM_COS_TEST', + bitgoId: 'TATOM' + }, + { + networkId: 'AURORA', + name: 'Aurora', + coinType: 2570, + anchorageId: null, + fireblocksId: 'AURORA_DEV', + bitgoId: null + }, + { + networkId: 'AVAX', + name: 'Avalanche', + coinType: 9000, + anchorageId: null, + fireblocksId: 'AVAX', + bitgoId: 'AVAXC' + }, + { + networkId: 'AVAX_TESTNET', + name: 'Avalanche Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'AVAXTEST', + bitgoId: 'TAVAXC' + }, + { + networkId: 'AXELAR', + name: 'Axelar', + coinType: null, + anchorageId: 'AXL', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'AXELAR_TESTNET', + name: 'Axelar Testnet', + coinType: 1, + anchorageId: 'AXL_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'BABYLON', + name: 'Babylon', + coinType: null, + anchorageId: 'BBN', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'BASE', + name: 'Base', + coinType: 8453, + anchorageId: null, + fireblocksId: 'BASECHAIN_ETH', + bitgoId: null + }, + { + networkId: 'BASE_TESTNET', + name: 'Base Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'BASECHAIN_ETH_TEST5', + bitgoId: null + }, + { + networkId: 'BINANCE_SMART_CHAIN', + name: 'Binance Smart Chain', + coinType: 9006, + anchorageId: null, + fireblocksId: 'BNB_BSC', + bitgoId: 'BSC' + }, + { + networkId: 'BINANCE_SMART_CHAIN_TESTNET', + name: 'Binance Smart Chain Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'BNB_TEST', + bitgoId: 'TBSC' + }, + { + networkId: 'BITCOIN', + name: 'Bitcoin', + coinType: 0, + anchorageId: 'BTC', + fireblocksId: 'BTC', + bitgoId: 'BTC' + }, + { + networkId: 'BITCOIN_CASH', + name: 'Bitcoin Cash', + coinType: 145, + anchorageId: 'BCH', + fireblocksId: 'BCH', + bitgoId: 'BCH' + }, + { + networkId: 'BITCOIN_CASH_TESTNET', + name: 'Bitcoin Cash Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'BCH_TEST', + bitgoId: null + }, + { + networkId: 'BITCOIN_SIGNET', + name: 'Bitcoin Signet', + coinType: 1, + anchorageId: 'BTC_S', + fireblocksId: null, + bitgoId: 'TBTCSIG' + }, + { + networkId: 'BITCOIN_SV', + name: 'BitcoinSV', + coinType: 236, + anchorageId: null, + fireblocksId: 'BSV', + bitgoId: null + }, + { + networkId: 'BITCOIN_SV_TESTNET', + name: 'BitcoinSV Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'BSV_TEST', + bitgoId: null + }, + { + networkId: 'BITCOIN_TESTNET', + name: 'Bitcoin Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'BTC_TEST', + bitgoId: null + }, + { + networkId: 'CARDANO', + name: 'Cardano', + coinType: 1815, + anchorageId: null, + fireblocksId: 'ADA', + bitgoId: 'ADA' + }, + { + networkId: 'CARDANO_TESTNET', + name: 'Cardano Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'ADA_TEST', + bitgoId: null + }, + { + networkId: 'CELESTIA', + name: 'Celestia', + coinType: null, + anchorageId: 'TIA', + fireblocksId: null, + bitgoId: 'TIA' + }, + { + networkId: 'CELO', + name: 'Celo', + coinType: 52752, + anchorageId: null, + fireblocksId: 'CELO', + bitgoId: 'CELO' + }, + { + networkId: 'CELO_ALFAJORES', + name: 'Celo Alfajores', + coinType: null, + anchorageId: null, + fireblocksId: 'CELO_ALF', + bitgoId: null + }, + { + networkId: 'CELO_BAKLAVA', + name: 'Celo Baklava', + coinType: 1, + anchorageId: 'CGLD_TB', + fireblocksId: 'CELO_BAK', + bitgoId: null + }, + { + networkId: 'CHILIZ', + name: 'Chiliz', + coinType: null, + anchorageId: null, + fireblocksId: 'CHZ_$CHZ', + bitgoId: null + }, + { + networkId: 'DABACUS', + name: 'Dabacus', + coinType: 521, + anchorageId: null, + fireblocksId: 'ABA', + bitgoId: null + }, + { + networkId: 'DASH', + name: 'Dash', + coinType: 5, + anchorageId: null, + fireblocksId: 'DASH', + bitgoId: 'DASH' + }, + { + networkId: 'DASH_TESTNET', + name: 'Dash Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'DASH_TEST', + bitgoId: null + }, + { + networkId: 'DOGECOIN', + name: 'Dogecoin', + coinType: 3, + anchorageId: 'DOGE', + fireblocksId: 'DOGE', + bitgoId: 'DOGE' + }, + { + networkId: 'DOGECOIN_TESTNET', + name: 'Dogecoin Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'DOGE_TEST', + bitgoId: null + }, + { + networkId: 'DYDX_CHAIN', + name: 'Dydx Chain', + coinType: null, + anchorageId: 'DYDX_CHAIN', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'DYDX_CHAIN_TESTNET', + name: 'Dydx Testnet', + coinType: 1, + anchorageId: 'DYDX_CHAIN_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'EOS', + name: 'EOS', + coinType: 194, + anchorageId: null, + fireblocksId: 'EOS', + bitgoId: 'EOS' + }, + { + networkId: 'EOS_TESTNET', + name: 'EOS Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'EOS_TEST', + bitgoId: null + }, + { + networkId: 'ETHEREUM', + name: 'Ethereum', + coinType: 60, + anchorageId: 'ETH', + fireblocksId: 'ETH', + bitgoId: 'ETH' + }, + { + networkId: 'ETHEREUM_HOLESKY', + name: 'Ethereum Holesky', + coinType: 1, + anchorageId: 'ETHHOL', + fireblocksId: null, + bitgoId: 'HTETH' + }, + { + networkId: 'ETHEREUM_SEPOLIA', + name: 'Ethereum Sepolia', + coinType: 1, + anchorageId: 'ETHSEP', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'EVMOS', + name: 'Evmos', + coinType: null, + anchorageId: 'EVMOS', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'EVMOS_TESTNET', + name: 'Evmos Testnet', + coinType: 1, + anchorageId: 'EVMOS_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'FILECOIN', + name: 'Filecoin', + coinType: 461, + anchorageId: 'FIL', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'FLOW_TESTNET', + name: 'Flow Testnet', + coinType: 1, + anchorageId: 'FLOW_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'LITECOIN', + name: 'Litecoin', + coinType: 2, + anchorageId: 'LTC', + fireblocksId: 'LTC', + bitgoId: 'LTC' + }, + { + networkId: 'LITECOIN_TESTNET', + name: 'Litecoin Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'LTC_TEST', + bitgoId: null + }, + { + networkId: 'NEUTRON', + name: 'Neutron', + coinType: null, + anchorageId: 'NTRN', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'OASIS', + name: 'Oasis', + coinType: 474, + anchorageId: 'ROSE', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'OASYS', + name: 'Oasys', + coinType: 685, + anchorageId: null, + fireblocksId: 'OAS', + bitgoId: 'OAS' + }, + { + networkId: 'OASYS_TESTNET', + name: 'Oasys Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'OAS_TEST', + bitgoId: null + }, + { + networkId: 'OM_MANTRA', + name: 'OM Mantra', + coinType: null, + anchorageId: 'OM_MANTRA', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'OM_MANTRA_TESTNET', + name: 'OM Mantra Testnet', + coinType: 1, + anchorageId: 'OM_MANTRA_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'OPTIMISM', + name: 'Optimism', + coinType: 614, + anchorageId: null, + fireblocksId: 'ETH-OPT', + bitgoId: 'OPETH' + }, + { + networkId: 'OPTIMISM_KOVAN', + name: 'Optimism Kovan', + coinType: 1, + anchorageId: null, + fireblocksId: 'ETH-OPT_KOV', + bitgoId: null + }, + { + networkId: 'OPTIMISM_SEPOLIA', + name: 'Optimism Sepolia', + coinType: 1, + anchorageId: null, + fireblocksId: 'ETH-OPT_SEPOLIA', + bitgoId: null + }, + { + networkId: 'OSMOSIS', + name: 'Osmosis', + coinType: 10000118, + anchorageId: 'OSMO', + fireblocksId: 'OSMO', + bitgoId: 'OSMO' + }, + { + networkId: 'OSMOSIS_TESTNET', + name: 'Osmosis Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'OSMO_TEST', + bitgoId: null + }, + { + networkId: 'PLUME_SEPOLIA', + name: 'Plume Sepolia Testnet', + coinType: 1, + anchorageId: 'ETH_PLUME_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'POLKADOT', + name: 'Polkadot', + coinType: 354, + anchorageId: null, + fireblocksId: 'DOT', + bitgoId: 'DOT' + }, + { + networkId: 'POLYGON', + name: 'Polygon', + coinType: 966, + anchorageId: 'POL_POLYGON', + fireblocksId: 'MATIC_POLYGON', + bitgoId: 'POLYGON' + }, + { + networkId: 'PROVENANCE', + name: 'Provenance', + coinType: 505, + anchorageId: 'HASH', + fireblocksId: null, + bitgoId: 'HASH' + }, + { + networkId: 'RARIMO', + name: 'Rarimo', + coinType: null, + anchorageId: 'RMO', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'RIPPLE', + name: 'Ripple', + coinType: 144, + anchorageId: 'XRP', + fireblocksId: 'XRP', + bitgoId: 'XRP' + }, + { + networkId: 'RIPPLE_TESTNET', + name: 'Ripple Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'XRP_TEST', + bitgoId: null + }, + { + networkId: 'SEI', + name: 'Sei', + coinType: 19000118, + anchorageId: 'SEI', + fireblocksId: 'SEI', + bitgoId: 'SEI' + }, + { + networkId: 'SEI_TESTNET', + name: 'Sei Testnet', + coinType: 1, + anchorageId: 'SEI_T', + fireblocksId: 'SEI_TEST', + bitgoId: null + }, + { + networkId: 'SOLANA', + name: 'Solana', + coinType: 501, + anchorageId: null, + fireblocksId: 'SOL', + bitgoId: 'SOL' + }, + { + networkId: 'SOLANA_TESTNET', + name: 'Solana Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'SOL_TEST', + bitgoId: null + }, + { + networkId: 'SOLONA_DEVNET', + name: 'Solana Devnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'SOL_TD', + bitgoId: 'TSOL' + }, + { + networkId: 'STARKNET', + name: 'Starknet', + coinType: 9004, + anchorageId: 'STRK_STARKNET', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'STARKNET_TESTNET', + name: 'Starknet Testnet', + coinType: 1, + anchorageId: 'STRK_STARKNET_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'STELLAR_LUMENS', + name: 'Stellar Lumens', + coinType: 148, + anchorageId: null, + fireblocksId: 'XLM', + bitgoId: 'XLM' + }, + { + networkId: 'STELLAR_LUMENS_TESTNET', + name: 'Stellar Lumens Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'XLM_TEST', + bitgoId: null + }, + { + networkId: 'STRIDE', + name: 'Stride', + coinType: null, + anchorageId: 'STRD', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'SUI_TESTNET', + name: 'Sui Testnet', + coinType: 1, + anchorageId: 'SUI_T', + fireblocksId: null, + bitgoId: 'TSUI' + }, + { + networkId: 'SUI', + name: 'Sui', + coinType: 784, + anchorageId: 'SUI', + fireblocksId: null, + bitgoId: 'SUI' + }, + { + networkId: 'TELOS', + name: 'Telos', + coinType: 424, + anchorageId: null, + fireblocksId: 'TELOS', + bitgoId: null + }, + { + networkId: 'TELOS_TESTNET', + name: 'Telos Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'TELOS_TEST', + bitgoId: null + }, + { + networkId: 'TEZOS', + name: 'Tezos', + coinType: 1729, + anchorageId: null, + fireblocksId: 'XTZ', + bitgoId: 'XTZ' + }, + { + networkId: 'TEZOS_TESTNET', + name: 'Tezos Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'XTZ_TEST', + bitgoId: null + }, + { + networkId: 'TRON', + name: 'Tron', + coinType: 195, + anchorageId: null, + fireblocksId: 'TRX', + bitgoId: 'TRX' + }, + { + networkId: 'TRON_TESTNET', + name: 'Tron Testnet', + coinType: 1, + anchorageId: null, + fireblocksId: 'TRX_TEST', + bitgoId: null + }, + { + networkId: 'VANA', + name: 'Vana', + coinType: null, + anchorageId: 'VANA_VANA', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'VANA_MOKSHA_TESTNET', + name: 'Vana Moksha Testnet', + coinType: 1, + anchorageId: 'VANA_VANA_MOKSHA_T', + fireblocksId: null, + bitgoId: null + }, + { + networkId: 'ZKSYNC_SEPOLIA', + name: 'ZKsync Sepolia Testnet', + coinType: 1, + anchorageId: 'ETH_ZKSYNC_T', + fireblocksId: null, + bitgoId: null + } + ] + } +} diff --git a/apps/vault/src/broker/shared/__test__/fixture.ts b/apps/vault/src/broker/shared/__test__/fixture.ts new file mode 100644 index 000000000..f2b435054 --- /dev/null +++ b/apps/vault/src/broker/shared/__test__/fixture.ts @@ -0,0 +1,209 @@ +import { privateKeyToHex, secp256k1PrivateKeyToJwk, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' +import { TestingModule } from '@nestjs/testing' +import { randomUUID } from 'crypto' +import { ClientService } from '../../../client/core/service/client.service' +import { Client } from '../../../shared/type/domain.type' +import { ANCHORAGE_TEST_API_BASE_URL } from '../../core/provider/anchorage/__test__/server-mock/server' +import { AnchorageCredentials } from '../../core/provider/anchorage/anchorage.type' +import { ConnectionService } from '../../core/service/connection.service' +import { ConnectionStatus, ConnectionWithCredentials } from '../../core/type/connection.type' +import { Account, Address, Wallet } from '../../core/type/indexed-resources.type' +import { Provider } from '../../core/type/provider.type' +import { AccountRepository } from '../../persistence/repository/account.repository' +import { AddressRepository } from '../../persistence/repository/address.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +export const clientId = randomUUID() + +const now = new Date() + +const USER_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' + +export const userPrivateKey = secp256k1PrivateKeyToJwk(USER_PRIVATE_KEY) +export const userPublicKey = secp256k1PrivateKeyToPublicJwk(USER_PRIVATE_KEY) + +export const client: Client = { + clientId, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: [ + { + userId: 'user-1', + publicKey: userPublicKey + } + ] + }, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: [] + }, + pinnedPublicKey: null + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, + createdAt: now, + updatedAt: now +} + +export const anchorageConnectionOneCredentials: AnchorageCredentials = { + apiKey: 'test-anchorage-api-key-one', + privateKey: { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0x50802454e9997ac331334bdfbc3a2f15826980d39e5ce5292353402dcd21d6f5', + x: 'BLEYbYCvYvA90guTeqCfIXMKdgcO2LiG9u-0h0lnqi4', + d: 'HXNx_HoOCxEbcTLjMY-dbL9psOuE3WFQ68zkd9oeeHw' + }, + publicKey: { + kty: 'OKP', + alg: 'EDDSA', + kid: '0x50802454e9997ac331334bdfbc3a2f15826980d39e5ce5292353402dcd21d6f5', + crv: 'Ed25519', + x: 'BLEYbYCvYvA90guTeqCfIXMKdgcO2LiG9u-0h0lnqi4' + } +} + +export const anchorageConnectionOne: ConnectionWithCredentials = { + clientId, + connectionId: randomUUID(), + provider: Provider.ANCHORAGE, + label: 'Anchorage test connection one', + url: ANCHORAGE_TEST_API_BASE_URL, + status: ConnectionStatus.ACTIVE, + revokedAt: undefined, + createdAt: now, + updatedAt: now, + credentials: anchorageConnectionOneCredentials +} + +export const anchorageWalletOne: Wallet = { + accounts: [], + clientId, + connectionId: anchorageConnectionOne.connectionId, + createdAt: new Date('2024-01-01T00:00:00Z'), + externalId: 'external-id-one', + label: 'wallet 1', + provider: anchorageConnectionOne.provider, + updatedAt: now, + walletId: randomUUID() +} + +export const anchorageWalletTwo: Wallet = { + clientId, + accounts: [], + connectionId: anchorageConnectionOne.connectionId, + createdAt: new Date('2024-01-02T00:00:00Z'), + externalId: 'external-id-two', + label: 'wallet 2', + provider: anchorageConnectionOne.provider, + updatedAt: now, + walletId: randomUUID() +} + +export const anchorageWalletThree: Wallet = { + clientId, + accounts: [], + connectionId: anchorageConnectionOne.connectionId, + createdAt: new Date('2024-01-03T00:00:00Z'), + externalId: 'external-id-three', + label: 'wallet 3', + provider: anchorageConnectionOne.provider, + updatedAt: now, + walletId: randomUUID() +} + +export const anchorageAccountOne: Account = { + clientId, + accountId: randomUUID(), + addresses: [], + createdAt: new Date('2024-01-01T00:00:00Z'), + externalId: 'account-external-id-one', + connectionId: anchorageConnectionOne.connectionId, + label: 'wallet 1 account 1', + networkId: 'BTC', + provider: anchorageConnectionOne.provider, + updatedAt: now, + walletId: anchorageWalletOne.walletId +} + +export const anchorageAccountTwo: Account = { + clientId, + accountId: randomUUID(), + addresses: [], + createdAt: new Date('2024-01-02T00:00:00Z'), + connectionId: anchorageConnectionOne.connectionId, + externalId: 'account-external-id-two', + label: 'wallet 1 account 2', + networkId: 'BTC', + provider: anchorageConnectionOne.provider, + updatedAt: now, + walletId: anchorageWalletOne.walletId +} + +export const anchorageAddressOne: Address = { + clientId, + accountId: anchorageAccountOne.accountId, + address: 'address-one', + addressId: randomUUID(), + connectionId: anchorageConnectionOne.connectionId, + createdAt: new Date('2024-01-01T00:00:00Z'), + externalId: 'address-external-id-one', + provider: anchorageConnectionOne.provider, + updatedAt: now +} + +export const anchorageAddressTwo: Address = { + clientId, + accountId: anchorageAccountOne.accountId, + address: 'address-two', + addressId: randomUUID(), + connectionId: anchorageConnectionOne.connectionId, + createdAt: new Date('2024-01-02T00:00:00Z'), + externalId: 'address-external-id-two', + provider: anchorageConnectionOne.provider, + updatedAt: now +} + +export const seed = async (module: TestingModule) => { + const clientService = module.get(ClientService) + const connectionService = module.get(ConnectionService) + const walletRepository = module.get(WalletRepository) + const accountRepository = module.get(AccountRepository) + const addressRepository = module.get(AddressRepository) + + await clientService.save(client) + + await connectionService.create(client.clientId, { + connectionId: anchorageConnectionOne.connectionId, + createdAt: anchorageConnectionOne.updatedAt, + label: anchorageConnectionOne.label, + provider: anchorageConnectionOne.provider, + url: anchorageConnectionOne.url as string, + credentials: { + apiKey: anchorageConnectionOneCredentials.apiKey, + privateKey: await privateKeyToHex(anchorageConnectionOneCredentials.privateKey) + } + }) + + await walletRepository.bulkCreate([anchorageWalletOne, anchorageWalletTwo, anchorageWalletThree]) + await accountRepository.bulkCreate([anchorageAccountOne, anchorageAccountTwo]) + await addressRepository.bulkCreate([anchorageAddressOne, anchorageAddressTwo]) +} diff --git a/apps/vault/src/broker/shared/__test__/matcher.ts b/apps/vault/src/broker/shared/__test__/matcher.ts new file mode 100644 index 000000000..cd2f6c2d7 --- /dev/null +++ b/apps/vault/src/broker/shared/__test__/matcher.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +import { expect } from '@jest/globals' +import type { MatcherContext } from 'expect' +import { ZodSchema } from 'zod' + +declare global { + namespace jest { + interface Matchers { + /** + * Custom Jest matcher to validate if a given value matches a Zod schema. + * + * @param {unknown} received - The value to be validated against the + * schema. + * @param {ZodSchema} schema - The Zod schema to validate the value + * against. + * @returns {object} - An object containing the result of the validation + * and a message. + * + * @example + * expect({ name: "John", age: 30 }).toMatchZodSchema(userSchema); + */ + toMatchZodSchema(schema: ZodSchema): R + } + } +} + +const toMatchZodSchema = function (this: MatcherContext, received: unknown, schema: ZodSchema) { + const parse = schema.safeParse(received) + + return { + pass: parse.success, + message: () => { + if (parse.success) { + return 'Matched value to schema' + } + + const errors = parse.error.errors.map((error) => { + if (error.path.length) { + return { + message: error.message, + path: error.path + } + } + + return { + message: error.message + } + }) + + return [ + 'Expected value to match schema:', + `Received: ${JSON.stringify(received, null, 2)}`, + `Errors: ${JSON.stringify(errors, null, 2)}` + ].join('\n') + } + } +} + +expect.extend({ toMatchZodSchema }) diff --git a/apps/vault/src/broker/shared/__test__/mock-server.ts b/apps/vault/src/broker/shared/__test__/mock-server.ts new file mode 100644 index 000000000..82f1b1a80 --- /dev/null +++ b/apps/vault/src/broker/shared/__test__/mock-server.ts @@ -0,0 +1,73 @@ +import { afterAll, afterEach, beforeAll, jest } from '@jest/globals' +import { HttpHandler } from 'msw' +import { SetupServerApi, setupServer } from 'msw/node' +import { disableNockProtection, restoreNockProtection } from '../../../../test/nock.util' + +export const getMockServer = (handlers: HttpHandler[]) => setupServer(...handlers) + +export const useRequestSpy = (server: SetupServerApi): [jest.Mock, SetupServerApi] => { + const spy = jest.fn() + + server.events.on('request:start', async ({ request }) => { + // Clone the request to avoid draining any data stream on reads. + const clone = request.clone() + const body = await clone.json() + + spy({ + method: clone.method, + url: clone.url, + headers: Object.fromEntries(request.headers), + body + }) + }) + + // NOTE: Use `spy.mock.calls` to debug the calls recorded by the spy. + return [spy, server] +} + +const attachToJestTestLifecycle = (server: SetupServerApi): SetupServerApi => { + beforeAll(() => { + // Disable nock net protection to allow requests to the mock server. + disableNockProtection() + server.listen({ + // IMPORTANT: Allow requests to 127.0.0.1 to pass through to support + // end-to-end testing with supertest because it boots a local server and + // helps send requests from the test to the server. + onUnhandledRequest: (req) => { + if (req.url.includes('127.0.0.1')) { + return 'bypass' + } + + return 'error' + } + }) + }) + + afterEach(() => server.resetHandlers()) + + afterAll(() => { + restoreNockProtection() + server.close() + }) + + return server +} + +/** + * Sets up a mock server and integrates it with Jest's test lifecycle. + * + * - The server starts on `beforeAll`. + * - HTTP handlers are reset on `afterEach` + * - The server is closed on `afterAll` + * + * IMPORTANT: This function should be used within a test block, such as + * `describe` or `it`. If you only need the server instance without attaching + * it to the test lifecycle, use `getMockServer` instead. + */ +export const setupMockServer = (handlers: HttpHandler[], server?: SetupServerApi): SetupServerApi => { + if (server) { + return attachToJestTestLifecycle(server) + } + + return attachToJestTestLifecycle(getMockServer(handlers)) +} diff --git a/apps/vault/src/broker/shared/__test__/request.ts b/apps/vault/src/broker/shared/__test__/request.ts new file mode 100644 index 000000000..b22799fb6 --- /dev/null +++ b/apps/vault/src/broker/shared/__test__/request.ts @@ -0,0 +1,118 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { JwsdHeader, PrivateKey, buildSignerForAlg, hash, hexToBase64Url, signJwsd } from '@narval/signature' +import { INestApplication } from '@nestjs/common' +import { isUndefined, omitBy } from 'lodash' +import request from 'supertest' + +export const getJwsd = async ({ + userPrivateJwk, + baseUrl, + requestUrl, + accessToken, + payload, + htm +}: { + userPrivateJwk: PrivateKey + baseUrl?: string + requestUrl: string + accessToken?: string + payload: object | string + htm?: string +}) => { + const now = Math.floor(Date.now() / 1000) + + const jwsdSigner = await buildSignerForAlg(userPrivateJwk) + const jwsdHeader: JwsdHeader = { + alg: userPrivateJwk.alg, + kid: userPrivateJwk.kid, + typ: 'gnap-binding-jwsd', + htm: htm || 'POST', + uri: `${baseUrl || 'https://vault-test.narval.xyz'}${requestUrl}`, // matches the client baseUrl + request url + created: now, + ath: accessToken ? hexToBase64Url(hash(accessToken)) : undefined + } + + const jwsd = await signJwsd(payload, jwsdHeader, jwsdSigner).then((jws) => { + // Strip out the middle part for size + const parts = jws.split('.') + parts[1] = '' + return parts.join('.') + }) + + return jwsd +} + +/** + * Creates a wrapper around supertest that automatically handles detached JWS + * signatures for API requests in tests. + * + * This helper ensures that all requests are properly signed with a detached + * JWS signature using the provided private key. It maintains the familiar + * supertest chainable API while handling the complexity of JWS signatures. + * + * @param app - The NestJS application instance to make requests against + * @param privateKey - The private key used to sign the requests + * + * @returns An object with HTTP method functions (get, post, put, delete, + * patch) that return chainable request objects + * + * @example + * const response = await signedRequest(app, userPrivateKey) + * .get('/api/resource') + * .set('header-name', 'value') + * .send() + */ +export const signedRequest = (app: INestApplication, privateKey: PrivateKey) => { + const wrapper = (method: 'get' | 'post' | 'put' | 'delete' | 'patch', url: string) => { + const req = request(app.getHttpServer())[method](url) + + let query: string | null = null + + return { + ...req, + set: function (key: string, value: string) { + if (key === 'detached-jws') { + throw new Error('You cannot override detached-jws with signedRequest') + } + + req.set(key, value) + + return this + }, + query: function (params: Record) { + if (url.includes('?')) { + throw new Error( + 'It seems the given URL already has a query string. Pass query params either in the URL or with the query method' + ) + } + + // Strip out undefined values but not null ones. + const parse = omitBy(params, isUndefined) + + req.query(parse) + query = new URLSearchParams(parse).toString() + + return this + }, + send: async function (body?: any) { + const jws = await getJwsd({ + userPrivateJwk: privateKey, + requestUrl: query ? `${url}?${query}` : url, + payload: body || {}, + htm: method.toUpperCase() + }) + + return req.set('detached-jws', jws).send(body) + } + } + } + + return { + get: (url: string) => wrapper('get', url), + post: (url: string) => wrapper('post', url), + put: (url: string) => wrapper('put', url), + delete: (url: string) => wrapper('delete', url), + patch: (url: string) => wrapper('patch', url) + } +} diff --git a/apps/vault/src/broker/shared/constant.ts b/apps/vault/src/broker/shared/constant.ts new file mode 100644 index 000000000..923531949 --- /dev/null +++ b/apps/vault/src/broker/shared/constant.ts @@ -0,0 +1,13 @@ +// +// OpenTelemetry +// + +export const OTEL_ATTR_SYNC_ID = 'sync.id' +export const OTEL_ATTR_CONNECTION_ID = 'connection.id' +export const OTEL_ATTR_CONNECTION_PROVIDER = 'connection.provider' + +// +// HTTP Header +// + +export const REQUEST_HEADER_CONNECTION_ID = 'x-connection-id' diff --git a/apps/vault/src/broker/shared/decorator/connection-id.decorator.ts b/apps/vault/src/broker/shared/decorator/connection-id.decorator.ts new file mode 100644 index 000000000..3fb8dd791 --- /dev/null +++ b/apps/vault/src/broker/shared/decorator/connection-id.decorator.ts @@ -0,0 +1,12 @@ +import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common' +import { REQUEST_HEADER_CONNECTION_ID } from '../constant' + +export const ConnectionId = createParamDecorator((_data: unknown, context: ExecutionContext): string => { + const req = context.switchToHttp().getRequest() + const connectionId = req.headers[REQUEST_HEADER_CONNECTION_ID] + if (!connectionId || typeof connectionId !== 'string') { + throw new BadRequestException(`Missing or invalid ${REQUEST_HEADER_CONNECTION_ID} header`) + } + + return connectionId +}) diff --git a/apps/vault/src/broker/shared/event/connection-activated.event.ts b/apps/vault/src/broker/shared/event/connection-activated.event.ts new file mode 100644 index 000000000..22440cf18 --- /dev/null +++ b/apps/vault/src/broker/shared/event/connection-activated.event.ts @@ -0,0 +1,7 @@ +import { ConnectionWithCredentials } from '../../core/type/connection.type' + +export class ConnectionActivatedEvent { + static EVENT_NAME = 'connection.activated' + + constructor(public readonly connection: ConnectionWithCredentials) {} +} diff --git a/apps/vault/src/broker/shared/event/scoped-sync-started.event.ts b/apps/vault/src/broker/shared/event/scoped-sync-started.event.ts new file mode 100644 index 000000000..b7b4db4f5 --- /dev/null +++ b/apps/vault/src/broker/shared/event/scoped-sync-started.event.ts @@ -0,0 +1,11 @@ +import { ConnectionWithCredentials } from '../../core/type/connection.type' +import { ScopedSync } from '../../core/type/scoped-sync.type' + +export class ScopedSyncStartedEvent { + static EVENT_NAME = 'scoped.sync.started' + + constructor( + public readonly sync: ScopedSync, + public readonly connection: ConnectionWithCredentials + ) {} +} diff --git a/apps/vault/src/client/__test__/e2e/client.spec.ts b/apps/vault/src/client/__test__/e2e/client.spec.ts index fc9ec29f2..720fb9b98 100644 --- a/apps/vault/src/client/__test__/e2e/client.spec.ts +++ b/apps/vault/src/client/__test__/e2e/client.spec.ts @@ -1,18 +1,19 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' -import { EncryptionModuleOptionProvider } from '@narval/encryption-module' -import { LoggerModule, secret } from '@narval/nestjs-shared' -import { Alg, PrivateKey, generateJwk, rsaPublicKeySchema, secp256k1PublicKeySchema } from '@narval/signature' +import { REQUEST_HEADER_ADMIN_API_KEY } from '@narval/nestjs-shared' +import { + Alg, + SMALLEST_RSA_MODULUS_LENGTH, + generateJwk, + rsaPublicKeySchema, + secp256k1PublicKeySchema +} from '@narval/signature' import { HttpStatus, INestApplication } from '@nestjs/common' -import { Test, TestingModule } from '@nestjs/testing' +import { TestingModule } from '@nestjs/testing' import request from 'supertest' import { v4 as uuid } from 'uuid' -import { Config, load } from '../../../main.config' -import { REQUEST_HEADER_API_KEY } from '../../../main.constant' +import { VaultTest } from '../../../__test__/shared/vault.test' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' -import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' -import { AppService } from '../../../vault/core/service/app.service' -import { ClientModule } from '../../client.module' -import { CreateClientDto } from '../../http/rest/dto/create-client.dto' import { ClientRepository } from '../../persistence/repository/client.repository' describe('Client', () => { @@ -20,43 +21,24 @@ describe('Client', () => { let module: TestingModule let testPrismaService: TestPrismaService let clientRepository: ClientRepository - let appService: AppService - let configService: ConfigService + let provisionService: ProvisionService - const adminApiKey = 'test-admin-api-key' + const adminApiKey = 'test-vault-admin-api-key' beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - ClientModule - ] - }) - .overrideProvider(EncryptionModuleOptionProvider) - .useValue({ - keyring: getTestRawAesKeyring() - }) - .compile() + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() app = module.createNestApplication() - appService = module.get(AppService) + provisionService = module.get(ProvisionService) clientRepository = module.get(ClientRepository) testPrismaService = module.get(TestPrismaService) - configService = module.get>(ConfigService) await testPrismaService.truncateAll() - await appService.save({ - id: configService.get('app.id'), - masterKey: 'test-master-key', - adminApiKey: secret.hash(adminApiKey) - }) - + await provisionService.provision() await app.init() }) @@ -69,7 +51,7 @@ describe('Client', () => { describe('POST /clients', () => { const clientId = uuid() - const payload: CreateClientDto = { + const payload = { clientId, audience: 'https://vault.narval.xyz', issuer: 'https://auth.narval.xyz', @@ -80,7 +62,7 @@ describe('Client', () => { it('creates a new client', async () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(payload) const actualClient = await clientRepository.findById(clientId) @@ -94,7 +76,7 @@ describe('Client', () => { }) it('creates a new client with Engine JWK', async () => { - const newPayload: CreateClientDto = { + const newPayload = { clientId: 'client-2', engineJwk: { kty: 'EC', @@ -107,7 +89,7 @@ describe('Client', () => { } const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(newPayload) const actualClient = await clientRepository.findById('client-2') @@ -121,22 +103,22 @@ describe('Client', () => { }) it('responds with an error when clientId already exist', async () => { - await request(app.getHttpServer()).post('/clients').set(REQUEST_HEADER_API_KEY, adminApiKey).send(payload) + await request(app.getHttpServer()).post('/clients').set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey).send(payload) const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(payload) expect(body.statusCode).toEqual(HttpStatus.BAD_REQUEST) - expect(body.message).toEqual('client already exist') + expect(body.message).toEqual('Client already exist') expect(status).toEqual(HttpStatus.BAD_REQUEST) }) it('responds with forbidden when admin api key is invalid', async () => { const { status, body } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, 'invalid-api-key') + .set(REQUEST_HEADER_ADMIN_API_KEY, 'invalid-api-key') .send(payload) expect(body).toMatchObject({ @@ -147,9 +129,14 @@ describe('Client', () => { }) it('creates a new client with RSA backup public key', async () => { - const rsaBackupKey = rsaPublicKeySchema.parse(await generateJwk(Alg.RS256, { keyId: 'rsaBackupKeyId' })) + const rsaBackupKey = rsaPublicKeySchema.parse( + await generateJwk(Alg.RS256, { + modulusLength: SMALLEST_RSA_MODULUS_LENGTH, + keyId: 'rsaBackupKeyId' + }) + ) - const validClientPayload: CreateClientDto = { + const validClientPayload = { ...payload, clientId: uuid(), backupPublicKey: rsaBackupKey @@ -157,16 +144,14 @@ describe('Client', () => { const { status: rightKeyStatus } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(validClientPayload) expect(rightKeyStatus).toEqual(HttpStatus.CREATED) }) it('responds with unprocessable entity when backup key is not an RSA key', async () => { - const secpBackupKey = secp256k1PublicKeySchema.parse( - await generateJwk(Alg.ES256K, { keyId: 'secpBackupKeyId' }) - ) + const secpBackupKey = secp256k1PublicKeySchema.parse(await generateJwk(Alg.ES256K, { keyId: 'secpBackupKeyId' })) const invalidClientPayload = { ...payload, @@ -176,10 +161,77 @@ describe('Client', () => { const { status: wrongKeyStatus } = await request(app.getHttpServer()) .post('/clients') - .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) .send(invalidClientPayload) expect(wrongKeyStatus).toEqual(HttpStatus.UNPROCESSABLE_ENTITY) }) + + it('creates a new client with token validation disabled & pinned user', async () => { + const newPayload = { + clientId: 'client-3', + name: 'Client 3', + auth: { + local: { + allowedUsers: [ + { + userId: 'user-1', + publicKey: { + kty: 'EC', + crv: 'secp256k1', + alg: 'ES256K', + kid: '0x73d3ed0e92ac09a45d9538980214abb1a36c4943d64ffa53a407683ddf567fc9', + x: 'sxT67JN5KJVnWYyy7xhFNUOk4buvPLrbElHBinuFwmY', + y: 'CzC7IHlsDg9wz-Gqhtc78eC0IEX75upMgrvmS3U6Ad4' + } + } + ] + }, + tokenValidation: { + disabled: true + } + } + } + const { status, body } = await request(app.getHttpServer()) + .post('/clients') + .set(REQUEST_HEADER_ADMIN_API_KEY, adminApiKey) + .send(newPayload) + + expect(status).toEqual(HttpStatus.CREATED) + expect(body).toEqual({ + clientId: newPayload.clientId, + name: newPayload.name, + baseUrl: null, + backupPublicKey: null, + configurationSource: 'dynamic', + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 300, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: newPayload.auth.local.allowedUsers + }, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + pinnedPublicKey: null, + verification: { + audience: null, + issuer: null, + maxTokenAge: null, + requireBoundTokens: true, + allowBearerTokens: false, + allowWildcard: null + } + } + }, + createdAt: expect.any(String), + updatedAt: expect.any(String) + }) + }) }) }) diff --git a/apps/vault/src/client/client.module.ts b/apps/vault/src/client/client.module.ts index 4bc508a25..63386357d 100644 --- a/apps/vault/src/client/client.module.ts +++ b/apps/vault/src/client/client.module.ts @@ -1,18 +1,18 @@ import { HttpModule } from '@nestjs/axios' -import { Module, OnApplicationBootstrap, ValidationPipe, forwardRef } from '@nestjs/common' +import { forwardRef, Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common' import { APP_PIPE } from '@nestjs/core' import { ZodValidationPipe } from 'nestjs-zod' +import { AppModule } from '../main.module' import { AdminApiKeyGuard } from '../shared/guard/admin-api-key.guard' import { KeyValueModule } from '../shared/module/key-value/key-value.module' -import { VaultModule } from '../vault/vault.module' +import { PersistenceModule } from '../shared/module/persistence/persistence.module' import { BootstrapService } from './core/service/bootstrap.service' import { ClientService } from './core/service/client.service' import { ClientController } from './http/rest/controller/client.controller' import { ClientRepository } from './persistence/repository/client.repository' @Module({ - // NOTE: The AdminApiKeyGuard is the only reason we need the VaultModule. - imports: [HttpModule, KeyValueModule, forwardRef(() => VaultModule)], + imports: [HttpModule, KeyValueModule, PersistenceModule, forwardRef(() => AppModule)], controllers: [ClientController], providers: [ AdminApiKeyGuard, diff --git a/apps/vault/src/client/core/service/__test__/unit/client.service.spec.ts b/apps/vault/src/client/core/service/__test__/unit/client.service.spec.ts index e6b31f083..c2780fedf 100644 --- a/apps/vault/src/client/core/service/__test__/unit/client.service.spec.ts +++ b/apps/vault/src/client/core/service/__test__/unit/client.service.spec.ts @@ -1,35 +1,57 @@ -import { EncryptionModule } from '@narval/encryption-module' +import { LoggerModule } from '@narval/nestjs-shared' import { Test } from '@nestjs/testing' +import { MockProxy, mock } from 'jest-mock-extended' import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service' import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' import { Client } from '../../../../../shared/type/domain.type' import { ClientRepository } from '../../../../persistence/repository/client.repository' import { ClientService } from '../../client.service' describe(ClientService.name, () => { let clientService: ClientService + let clientRepositoryMock: MockProxy const clientId = 'test-client-id' const client: Client = { clientId, + auth: { + disabled: true, + local: null, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: null, + maxTokenAge: null, + requireBoundTokens: false, + allowBearerTokens: false, + allowWildcard: null + }, + pinnedPublicKey: null + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), updatedAt: new Date() } beforeEach(async () => { + clientRepositoryMock = mock() + const module = await Test.createTestingModule({ - imports: [ - EncryptionModule.register({ - keyring: getTestRawAesKeyring() - }) - ], + imports: [LoggerModule.forTest()], providers: [ ClientService, - ClientRepository, - EncryptKeyValueService, + { + provide: ClientRepository, + useValue: clientRepositoryMock + }, { provide: KeyValueRepository, useClass: InMemoryKeyValueRepository @@ -42,9 +64,12 @@ describe(ClientService.name, () => { describe('save', () => { it('saves the client', async () => { + clientRepositoryMock.save.mockResolvedValue(client) + const actualClient = await clientService.save(client) expect(actualClient).toEqual(client) + expect(clientRepositoryMock.save).toHaveBeenCalledWith(client, false) }) }) }) diff --git a/apps/vault/src/client/core/service/bootstrap.service.ts b/apps/vault/src/client/core/service/bootstrap.service.ts index a57454448..60c33fe84 100644 --- a/apps/vault/src/client/core/service/bootstrap.service.ts +++ b/apps/vault/src/client/core/service/bootstrap.service.ts @@ -1,20 +1,74 @@ +import { ConfigService } from '@narval/config-module' import { LoggerService } from '@narval/nestjs-shared' import { Injectable } from '@nestjs/common' +import { Config } from '../../../main.config' +import { Client } from '../../../shared/type/domain.type' import { ClientService } from './client.service' @Injectable() export class BootstrapService { constructor( + private configService: ConfigService, private clientService: ClientService, private logger: LoggerService ) {} async boot(): Promise { - this.logger.log('Start app bootstrap') + this.logger.log('Start client bootstrap') + + await this.persistDeclarativeClients() + + // TEMPORARY: Migrate the key-value format of the Client config into the table format. + // Can be removed once this runs once. + await this.clientService.migrateV1Data() await this.syncClients() } + private async persistDeclarativeClients(): Promise { + const clients = this.configService.get('clients') + if (!clients) return + + // Given ClientConfig type, build the Client type + const declarativeClients: Client[] = clients.map((client) => ({ + clientId: client.clientId, + name: client.name, + configurationSource: 'declarative', + backupPublicKey: client.backupPublicKey, + baseUrl: client.baseUrl, + auth: { + disabled: client.auth.disabled, + local: client.auth.local?.httpSigning?.methods?.jwsd + ? { + jwsd: client.auth.local?.httpSigning?.methods?.jwsd || null, + allowedUsersJwksUrl: client.auth.local?.httpSigning?.allowedUsersJwksUrl || null, + allowedUsers: client.auth.local?.httpSigning?.allowedUsers + } + : null, + tokenValidation: { + disabled: !!client.auth.tokenValidation?.disabled, + url: client.auth.tokenValidation?.url || null, + jwksUrl: client.auth.tokenValidation?.jwksUrl || null, + pinnedPublicKey: client.auth.tokenValidation?.publicKey || null, + verification: { + audience: client.auth.tokenValidation?.verification?.audience || null, + issuer: client.auth.tokenValidation?.verification?.issuer || null, + maxTokenAge: client.auth.tokenValidation?.verification?.maxTokenAge || null, + requireBoundTokens: !!client.auth.tokenValidation?.verification?.requireBoundTokens, + allowBearerTokens: !!client.auth.tokenValidation?.verification?.allowBearerTokens, + allowWildcard: client.auth.tokenValidation?.verification?.allowWildcard || null + } + } + }, + createdAt: new Date(), + updatedAt: new Date() + })) + + for (const client of declarativeClients) { + await this.clientService.save(client, true) + } + } + private async syncClients(): Promise { const clients = await this.clientService.findAll() diff --git a/apps/vault/src/client/core/service/client.service.ts b/apps/vault/src/client/core/service/client.service.ts index f25fc8792..392d87bbf 100644 --- a/apps/vault/src/client/core/service/client.service.ts +++ b/apps/vault/src/client/core/service/client.service.ts @@ -1,29 +1,98 @@ +import { LoggerService } from '@narval/nestjs-shared' import { HttpStatus, Injectable } from '@nestjs/common' +import { isDeepStrictEqual } from 'util' import { ApplicationException } from '../../../shared/exception/application.exception' import { Client } from '../../../shared/type/domain.type' import { ClientRepository } from '../../persistence/repository/client.repository' @Injectable() export class ClientService { - constructor(private clientRepository: ClientRepository) {} + constructor( + private clientRepository: ClientRepository, + private logger: LoggerService + ) {} + + // Temporary function to migrate data from V1 to V2 + async migrateV1Data(): Promise { + const clientsV1 = await this.clientRepository.findAllV1() + for (const clientV1 of clientsV1) { + const client: Client = { + clientId: clientV1.clientId, + name: clientV1.clientId, + configurationSource: 'dynamic', + backupPublicKey: clientV1.backupPublicKey || null, + baseUrl: clientV1.baseUrl || null, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 60, // 1 minute; this is seconds + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + pinnedPublicKey: clientV1.engineJwk || null, + verification: { + audience: clientV1.audience || null, + issuer: clientV1.issuer || null, + maxTokenAge: clientV1.maxTokenAge || null, + requireBoundTokens: true, + allowBearerTokens: false, + allowWildcard: [ + 'transactionRequest.maxPriorityFeePerGas', + 'transactionRequest.maxFeePerGas', + 'transactionRequest.gas', + 'transactionRequest.gasPrice', + 'transactionRequest.nonce' + ] + } + } + }, + createdAt: clientV1.createdAt, + updatedAt: clientV1.updatedAt + } + this.logger.info('Migrating client', { clientV1, client }) + await this.clientRepository.save(client) + await this.clientRepository.deleteV1(clientV1.clientId) + } + } async findById(clientId: string): Promise { return this.clientRepository.findById(clientId) } - async save(client: Client): Promise { + async save(client: Client, overwriteAllowedUsers = false): Promise { const exists = await this.clientRepository.findById(client.clientId) - if (exists) { + if (exists && exists.configurationSource === 'dynamic') { throw new ApplicationException({ - message: 'client already exist', + message: 'Client already exist', + suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, + context: { clientId: client.clientId } + }) + } + + // Validate the backupPublicKey is not being changed; it can be unset but not rotated here. + if ( + exists && + exists.backupPublicKey && + client.backupPublicKey && + !isDeepStrictEqual(exists.backupPublicKey, client.backupPublicKey) + ) { + throw new ApplicationException({ + message: 'Cannot change backupPublicKey', suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, context: { clientId: client.clientId } }) } try { - await this.clientRepository.save(client) + await this.clientRepository.save(client, overwriteAllowedUsers) return client } catch (error) { diff --git a/apps/vault/src/client/http/rest/controller/client.controller.ts b/apps/vault/src/client/http/rest/controller/client.controller.ts index eceb3ec53..2a6eaf3eb 100644 --- a/apps/vault/src/client/http/rest/controller/client.controller.ts +++ b/apps/vault/src/client/http/rest/controller/client.controller.ts @@ -26,19 +26,41 @@ export class ClientController { }) async create(@Body() body: CreateClientDto): Promise { const now = new Date() - const engineJwk = body.engineJwk ? publicKeySchema.parse(body.engineJwk) : undefined // Validate the JWK, instead of in DTO const clientId = body.clientId || uuid() + const pinnedPublicKeyRaw = body.auth.tokenValidation.pinnedPublicKey || body.engineJwk || undefined + const pinnedPublicKey = pinnedPublicKeyRaw ? publicKeySchema.parse(pinnedPublicKeyRaw) : undefined // Validate the JWK, instead of in DTO const client = await this.clientService.save({ clientId, - engineJwk, - audience: body.audience, - issuer: body.issuer, - maxTokenAge: body.maxTokenAge, - allowKeyExport: body.allowKeyExport, - allowWildcard: body.allowWildcard, - backupPublicKey: body.backupPublicKey, - baseUrl: body.baseUrl, + name: body.name || clientId, + backupPublicKey: body.backupPublicKey || null, + baseUrl: body.baseUrl || null, + configurationSource: 'dynamic', + auth: { + disabled: false, // We DO NOT allow dynamic clients to have no auth; use declarative client if you need a no-auth deployment + local: { + jwsd: { + maxAge: body.auth.local?.jwsd?.maxAge || 300, + requiredComponents: body.auth.local?.jwsd?.requiredComponents || ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, // Not implemented, so we set to null + allowedUsers: body.auth.local?.allowedUsers || null + }, + tokenValidation: { + disabled: body.auth.tokenValidation.disabled, + url: body.auth.tokenValidation.url || null, + jwksUrl: null, // Not implemented, so we set to null + pinnedPublicKey: pinnedPublicKey || null, + verification: { + audience: body.auth.tokenValidation.verification.audience || body.audience || null, + issuer: body.auth.tokenValidation.verification.issuer || body.issuer || null, + maxTokenAge: body.auth.tokenValidation.verification.maxTokenAge || body.maxTokenAge || null, + requireBoundTokens: body.auth.tokenValidation.verification.requireBoundTokens, // Default to True + allowBearerTokens: body.auth.tokenValidation.verification.allowBearerTokens, // Defaults to False + allowWildcard: body.auth.tokenValidation.verification.allowWildcard || body.allowWildcard || null + } + } + }, createdAt: now, updatedAt: now }) diff --git a/apps/vault/src/client/persistence/repository/__test__/unit/client.repository.spec.ts b/apps/vault/src/client/persistence/repository/__test__/unit/client.repository.spec.ts deleted file mode 100644 index d82e992b0..000000000 --- a/apps/vault/src/client/persistence/repository/__test__/unit/client.repository.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { EncryptionModule } from '@narval/encryption-module' -import { Test } from '@nestjs/testing' -import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' -import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' -import { Client } from '../../../../../shared/type/domain.type' -import { ClientRepository } from '../../client.repository' - -describe(ClientRepository.name, () => { - let repository: ClientRepository - let inMemoryKeyValueRepository: InMemoryKeyValueRepository - - const clientId = 'test-client-id' - - beforeEach(async () => { - inMemoryKeyValueRepository = new InMemoryKeyValueRepository() - - const module = await Test.createTestingModule({ - imports: [ - EncryptionModule.register({ - keyring: getTestRawAesKeyring() - }) - ], - providers: [ - KeyValueService, - ClientRepository, - EncryptKeyValueService, - { - provide: KeyValueRepository, - useValue: inMemoryKeyValueRepository - } - ] - }).compile() - - repository = module.get(ClientRepository) - }) - - describe('save', () => { - const now = new Date() - - const client: Client = { - clientId, - createdAt: now, - updatedAt: now - } - - it('saves a new client', async () => { - await repository.save(client) - - const value = await inMemoryKeyValueRepository.get(repository.getKey(client.clientId)) - const actualClient = await repository.findById(client.clientId) - - expect(value).not.toEqual(null) - expect(client).toEqual(actualClient) - }) - - it('indexes the new client', async () => { - await repository.save(client) - - expect(await repository.getClientIndex()).toEqual([client.clientId]) - }) - }) -}) diff --git a/apps/vault/src/client/persistence/repository/client.repository.ts b/apps/vault/src/client/persistence/repository/client.repository.ts index 3a11b1d2b..fa32c79b8 100644 --- a/apps/vault/src/client/persistence/repository/client.repository.ts +++ b/apps/vault/src/client/persistence/repository/client.repository.ts @@ -1,33 +1,174 @@ import { coerce } from '@narval/nestjs-shared' +import { publicKeySchema, rsaPublicKeySchema } from '@narval/signature' import { Injectable } from '@nestjs/common' +import { Prisma } from '@prisma/client/vault' import { compact } from 'lodash/fp' import { z } from 'zod' import { KeyMetadata } from '../../../shared/module/key-value/core/repository/key-value.repository' import { EncryptKeyValueService } from '../../../shared/module/key-value/core/service/encrypt-key-value.service' -import { Client, Collection } from '../../../shared/type/domain.type' - +import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' +import { Client, ClientLocalAuthAllowedUser, ClientV1, Collection } from '../../../shared/type/domain.type' export const ClientIndex = z.array(z.string()) +export function clientObjectToPrisma(client: Client): Prisma.ClientCreateInput { + return { + clientId: client.clientId, + name: client.name, + configurationSource: client.configurationSource, + authDisabled: client.auth.disabled, + tokenValidationDisabled: client.auth.tokenValidation.disabled, + backupPublicKey: client.backupPublicKey ? JSON.stringify(client.backupPublicKey) : null, + baseUrl: client.baseUrl, + + // JWT Token Validation + authorizationServerUrl: client.auth.tokenValidation.url, + authorizationIssuer: client.auth.tokenValidation.verification.issuer, + authorizationAudience: client.auth.tokenValidation.verification.audience, + authorizationMaxTokenAge: client.auth.tokenValidation.verification.maxTokenAge, + authorizationJwksUrl: client.auth.tokenValidation.jwksUrl, + authorizationPinnedPublicKey: client.auth.tokenValidation.pinnedPublicKey + ? JSON.stringify(client.auth.tokenValidation.pinnedPublicKey) + : null, + authorizationRequireBoundTokens: client.auth.tokenValidation.verification.requireBoundTokens, + authorizationAllowBearerTokens: client.auth.tokenValidation.verification.allowBearerTokens, + authorizationAllowWildcards: client.auth.tokenValidation.verification.allowWildcard?.join(','), + + // Local Authentication Methods + localAuthAllowedUsersJwksUrl: client.auth.local?.allowedUsersJwksUrl || null, + localAuthJwsdEnabled: !!client.auth.local?.jwsd, + jwsdMaxAge: client.auth.local?.jwsd?.maxAge || null, + jwsdRequiredComponents: client.auth.local?.jwsd?.requiredComponents.join(',') || null, + + createdAt: client.createdAt, // Exclude the createdAt so the db generates it. + updatedAt: client.updatedAt + } +} + +function prismaToClientObject( + prismaClient: Prisma.ClientGetPayload<{ include: { localAuthAllowedUsers: true } }> +): Client { + return { + clientId: prismaClient.clientId, + name: prismaClient.name, + configurationSource: prismaClient.configurationSource as 'declarative' | 'dynamic', + backupPublicKey: prismaClient.backupPublicKey + ? rsaPublicKeySchema.parse(JSON.parse(prismaClient.backupPublicKey)) + : null, + baseUrl: prismaClient.baseUrl, + + auth: { + disabled: prismaClient.authDisabled, + local: prismaClient.localAuthJwsdEnabled + ? { + jwsd: { + maxAge: prismaClient.jwsdMaxAge ?? 0, + requiredComponents: prismaClient.jwsdRequiredComponents?.split(',') ?? [] + }, + allowedUsersJwksUrl: prismaClient.localAuthAllowedUsersJwksUrl, + allowedUsers: prismaClient.localAuthAllowedUsers?.length + ? prismaClient.localAuthAllowedUsers?.map((user) => ({ + userId: user.userId, + publicKey: publicKeySchema.parse(JSON.parse(user.publicKey)) + })) + : null + } + : null, + tokenValidation: { + disabled: prismaClient.tokenValidationDisabled, + url: prismaClient.authorizationServerUrl, + jwksUrl: prismaClient.authorizationJwksUrl, + pinnedPublicKey: prismaClient.authorizationPinnedPublicKey + ? publicKeySchema.parse(JSON.parse(prismaClient.authorizationPinnedPublicKey)) + : null, + verification: { + audience: prismaClient.authorizationAudience, + issuer: prismaClient.authorizationIssuer, + maxTokenAge: prismaClient.authorizationMaxTokenAge, + requireBoundTokens: prismaClient.authorizationRequireBoundTokens, + allowBearerTokens: prismaClient.authorizationAllowBearerTokens, + allowWildcard: prismaClient.authorizationAllowWildcards + ? prismaClient.authorizationAllowWildcards.split(',') + : null + } + } + }, + + createdAt: prismaClient.createdAt, + updatedAt: prismaClient.updatedAt + } +} + @Injectable() export class ClientRepository { - constructor(private encryptKeyValueService: EncryptKeyValueService) {} + constructor( + private encryptKeyValueService: EncryptKeyValueService, + private prismaService: PrismaService + ) {} private KEY_PREFIX = Collection.CLIENT async findById(clientId: string): Promise { - const value = await this.encryptKeyValueService.get(this.getKey(clientId)) + const value = await this.prismaService.client.findUnique({ + where: { clientId }, + include: { localAuthAllowedUsers: true } + }) if (value) { - return coerce.decode(Client, value) + return prismaToClientObject(value) } return null } + async findAll(): Promise { + const clients = await this.prismaService.client.findMany({ include: { localAuthAllowedUsers: true } }) + return clients.map(prismaToClientObject) + } + + async saveAllowedUsers( + clientId: string, + allowedUsers: ClientLocalAuthAllowedUser[], + overwrite: boolean + ): Promise { + const allowedUsersInput = allowedUsers.map((user) => ({ + id: `${clientId}:${user.userId}`, + userId: user.userId, + clientId: clientId, + publicKey: JSON.stringify(user.publicKey) + })) + + await this.prismaService.$transaction( + compact([ + overwrite ? this.prismaService.clientLocalAuthAllowedUser.deleteMany({ where: { clientId } }) : undefined, + allowedUsers.length > 0 + ? this.prismaService.clientLocalAuthAllowedUser.createMany({ + data: allowedUsersInput, + skipDuplicates: true + }) + : undefined + ]) + ) + } + + // Upsert the Client and any Allowed Users, optionally overwriting existing "allowedUsers" + async save(client: Client, overwriteAllowedUsers = false): Promise { + const clientData = clientObjectToPrisma(client) + + await this.prismaService.client.upsert({ + where: { clientId: client.clientId }, + update: { ...clientData, createdAt: undefined }, // Ensure we don't overwrite the createdAt + create: clientData + }) + + await this.saveAllowedUsers(client.clientId, client.auth.local?.allowedUsers ?? [], overwriteAllowedUsers) + + return client + } - async save(client: Client): Promise { + /** @deprecated */ + async saveV1(client: ClientV1): Promise { await this.encryptKeyValueService.set( this.getKey(client.clientId), - coerce.encode(Client, client), + coerce.encode(ClientV1, client), this.getMetadata(client.clientId) ) await this.index(client) @@ -35,6 +176,18 @@ export class ClientRepository { return client } + /** @deprecated */ + async findByIdV1(clientId: string): Promise { + const value = await this.encryptKeyValueService.get(this.getKey(clientId)) + + if (value) { + return coerce.decode(ClientV1, value) + } + + return null + } + + /** @deprecated */ async getClientIndex(): Promise { const index = await this.encryptKeyValueService.get(this.getIndexKey()) @@ -52,26 +205,31 @@ export class ClientRepository { // An option is to move these general queries `findBy`, findAll`, etc to the // KeyValeuRepository implementation letting each implementation pick the best // strategy to solve the problem (e.g. where query in SQL) - async findAll(): Promise { + /** @deprecated */ + async findAllV1(): Promise { const ids = await this.getClientIndex() - const clients = await Promise.all(ids.map((id) => this.findById(id))) + const clients = await Promise.all(ids.map((id) => this.findByIdV1(id))) return compact(clients) } + /** @deprecated */ getMetadata(clientId: string): KeyMetadata { return { collection: Collection.CLIENT, clientId } } + /** @deprecated */ getKey(clientId: string): string { return `${this.KEY_PREFIX}:${clientId}` } + /** @deprecated */ getIndexKey(): string { return `${this.KEY_PREFIX}:index` } - private async index(client: Client): Promise { + /** @deprecated */ + private async index(client: ClientV1): Promise { const currentIndex = await this.getClientIndex() await this.encryptKeyValueService.set( @@ -82,4 +240,16 @@ export class ClientRepository { return true } + async deleteV1(clientId: string): Promise { + await this.encryptKeyValueService.delete(this.getKey(clientId)) + // Remove the client from the index + const currentIndex = await this.getClientIndex() + + const newIndex = currentIndex.filter((id) => id !== clientId) + await this.encryptKeyValueService.set( + this.getIndexKey(), + coerce.encode(ClientIndex, newIndex), + this.getMetadata(clientId) // This is ignored on updates + ) + } } diff --git a/apps/vault/src/main.config.ts b/apps/vault/src/main.config.ts index bcd33739a..4bbfa393a 100644 --- a/apps/vault/src/main.config.ts +++ b/apps/vault/src/main.config.ts @@ -1,3 +1,8 @@ +import { LoggerService } from '@narval/nestjs-shared' +import { publicKeySchema, rsaPublicKeySchema } from '@narval/signature' +import fs from 'fs' +import path from 'path' +import { parse } from 'yaml' import { z } from 'zod' export enum Env { @@ -6,54 +11,290 @@ export enum Env { PRODUCTION = 'production' } -const configSchema = z.object({ - env: z.nativeEnum(Env), - port: z.coerce.number(), - cors: z.array(z.string()).optional(), - baseUrl: z.string().optional(), - database: z.object({ - url: z.string().startsWith('postgresql:') - }), - app: z.object({ - id: z.string(), - adminApiKeyHash: z.string().optional(), - masterKey: z.string().optional() +const CONFIG_VERSION_LATEST = '1' +const logger = new LoggerService() + +const JwsdMethodConfigSchema = z.object({ + maxAge: z.number(), + requiredComponents: z.array(z.string()) +}) + +const HttpSigningConfigSchema = z.object({ + methods: z.object({ + jwsd: JwsdMethodConfigSchema.nullable().optional() }), - keyring: z.union([ - z.object({ - type: z.literal('raw'), - masterPassword: z.string() - }), - z.object({ - type: z.literal('awskms'), - masterAwsKmsArn: z.string() + allowedUsersJwksUrl: z.string().nullable().optional(), + allowedUsers: z + .array( + z.object({ + userId: z.string(), + publicKey: publicKeySchema + }) + ) + .nullable() + .optional() +}) + +const LocalAuthConfigSchema = z.object({ + httpSigning: HttpSigningConfigSchema.nullable().optional() +}) + +const AppLocalAuthConfigSchema = z.object({ + adminApiKeyHash: z.string().nullable().optional() + // TODO: Add the app-level httpSigning section: + /** + # HTTP Signing configuration - this is for Service-level authentication. + # https://httpsig.org/ + httpSigning: + # Settings for when THIS service verifies incoming requests + verification: + maxAge: 300 # Reject signatures older than 5 minutes + requiredComponents: # Fail if these aren't included in signature + - "@method" + - "@target-uri" + - "content-digest" + + # [optional]: Known keys for pinning/offline validation + # If set, ONLY these keys will be accepted. + allowedUsers: + # Peer name + - name: armory + # [optional] URL of the peer's JWKS endpoint to verify signatures against. + jwksUrl: https://armory/.well-known/jwks.json + # [optional] Pin specific keys, instead of jwks endpoint + publicKeys: + - kid: "local-dev-armory-instance-1-2024-1" + kty: "EC" + crv: "secp256k1" + alg: "ES256K" + x: "..." + y: "..." + */ +}) + +const TokenValidationVerificationConfigSchema = z.object({ + audience: z.string().nullable().optional(), + issuer: z.string().nullable().optional(), + maxTokenAge: z.number().nullable().optional(), + requireBoundTokens: z.boolean().optional(), + allowBearerTokens: z.boolean().optional(), + allowWildcard: z.array(z.string()).nullable().optional() +}) + +const TokenValidationConfigSchema = z.object({ + disabled: z.boolean().nullable().optional(), + url: z.string().nullable().optional(), + jwksUrl: z.string().nullable().optional(), + publicKey: publicKeySchema.nullable().optional(), + verification: TokenValidationVerificationConfigSchema.nullable().optional() +}) + +const OIDCConfigSchema = z.any() // TODO: Define OIDC schema +const OutgoingAuthConfigSchema = z.any() // TODO: Define outgoing auth schema + +const AuthConfigSchema = z.object({ + disabled: z.boolean().optional(), + oidc: OIDCConfigSchema.nullable().optional(), + local: LocalAuthConfigSchema.nullable().optional(), + tokenValidation: TokenValidationConfigSchema.nullable().optional(), + outgoing: OutgoingAuthConfigSchema.nullable().optional() +}) + +const ClientConfigSchema = z.object({ + name: z.string(), + baseUrl: z.string().optional(), + auth: AuthConfigSchema, + backupPublicKey: rsaPublicKeySchema.optional() +}) +const VaultConfigSchema = z.object({ + version: z.coerce.string(), + env: z.nativeEnum(Env).nullish(), + port: z.coerce.number().nullish(), + cors: z.array(z.string()).nullable().optional(), + baseUrl: z.string().nullish(), + app: z + .object({ + id: z.string(), + auth: z.object({ + disabled: z.boolean(), + oidc: OIDCConfigSchema.nullable(), + local: AppLocalAuthConfigSchema.nullable(), + outgoing: OutgoingAuthConfigSchema.nullable() + }) + }) + .nullish(), + database: z + .object({ + url: z.string() }) - ]) + .nullish(), + keyring: z + .object({ + type: z.enum(['raw', 'awskms']), + encryptionMasterPassword: z.string().nullable().optional(), + encryptionMasterKey: z.string().nullable().optional(), + encryptionMasterAwsKmsArn: z.string().nullable().optional(), + hmacSecret: z.string().nullable().optional() + }) + .nullish(), + clients: z.record(z.string(), ClientConfigSchema).nullish() }) -export type Config = z.infer +const keyringSchema = z.union([ + z.object({ + type: z.literal('raw'), + encryptionMasterPassword: z.string(), + encryptionMasterKey: z.string().nullable(), + hmacSecret: z.string().nullable() + }), + z.object({ + type: z.literal('awskms'), + encryptionMasterAwsKmsArn: z.string(), + hmacSecret: z.string().nullable() + }) +]) -export const load = (): Config => { - const result = configSchema.safeParse({ - env: process.env.NODE_ENV, - port: process.env.PORT, - cors: process.env.CORS ? process.env.CORS.split(',') : [], - // Such as "https://vault.narval.xyz" - baseUrl: process.env.BASE_URL, +const LoadConfig = VaultConfigSchema.transform((yaml, ctx) => { + const appId = process.env.APP_UID || yaml.app?.id + const databaseUrl = process.env.APP_DATABASE_URL || yaml.database?.url + const env = z.nativeEnum(Env).parse(process.env.NODE_ENV || yaml.env) + const port = process.env.PORT || yaml.port + const baseUrl = process.env.BASE_URL || yaml.baseUrl + + if (!appId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'APP_UID is required' + }) + return z.NEVER + } + if (!databaseUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'APP_DATABASE_URL is required' + }) + return z.NEVER + } + if (!port) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'PORT is required' + }) + return z.NEVER + } + if (!baseUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'BASE_URL is required' + }) + return z.NEVER + } + + const clients = Object.entries(yaml.clients || {}).map(([clientId, client]) => ({ + clientId, + name: client.name, + backupPublicKey: client.backupPublicKey || null, + baseUrl: client.baseUrl || null, + configurationSource: 'declarative', + auth: { + disabled: !!client.auth.disabled, + oidc: client.auth.oidc || null, + local: client.auth.local + ? { + httpSigning: client.auth.local.httpSigning + ? { + methods: { + jwsd: client.auth.local.httpSigning.methods.jwsd + ? { + maxAge: client.auth.local.httpSigning.methods.jwsd.maxAge, + requiredComponents: client.auth.local.httpSigning.methods.jwsd.requiredComponents + } + : null + }, + allowedUsersJwksUrl: client.auth.local.httpSigning.allowedUsersJwksUrl || null, + allowedUsers: client.auth.local.httpSigning.allowedUsers || null + } + : null + } + : null, + tokenValidation: client.auth.tokenValidation + ? { + disabled: !!client.auth.tokenValidation.disabled, + url: client.auth.tokenValidation.url || null, + jwksUrl: client.auth.tokenValidation.jwksUrl || null, + publicKey: client.auth.tokenValidation.publicKey || null, + verification: client.auth.tokenValidation.verification + ? { + audience: client.auth.tokenValidation.verification.audience || null, + issuer: client.auth.tokenValidation.verification.issuer || null, + maxTokenAge: client.auth.tokenValidation.verification.maxTokenAge || null, + requireBoundTokens: !!client.auth.tokenValidation.verification.requireBoundTokens, + allowBearerTokens: !!client.auth.tokenValidation.verification.allowBearerTokens, + allowWildcard: client.auth.tokenValidation.verification.allowWildcard || null + } + : null + } + : null, + outgoing: client.auth.outgoing || null + } + })) + + return { + version: yaml.version || CONFIG_VERSION_LATEST, + env, + port, + cors: z.array(z.string()).parse(process.env.CORS || yaml.cors || []), + baseUrl, database: { - url: process.env.APP_DATABASE_URL + url: databaseUrl }, + app: { - id: process.env.APP_UID, - adminApiKeyHash: process.env.ADMIN_API_KEY, - masterKey: process.env.MASTER_KEY + id: appId, + auth: { + disabled: yaml.app?.auth.disabled || false, + local: + yaml.app?.auth.local || process.env.ADMIN_API_KEY + ? { + adminApiKeyHash: yaml.app?.auth.local?.adminApiKeyHash || process.env.ADMIN_API_KEY || null + } + : null + // oidc: yaml.app.auth.oidc, + // outgoing: yaml.app.auth.outgoing, + } }, - keyring: { - type: process.env.KEYRING_TYPE, - masterAwsKmsArn: process.env.MASTER_AWS_KMS_ARN, - masterPassword: process.env.MASTER_PASSWORD + keyring: keyringSchema.parse({ + type: process.env.KEYRING_TYPE || yaml.keyring?.type, + encryptionMasterPassword: process.env.MASTER_PASSWORD || yaml.keyring?.encryptionMasterPassword || null, + encryptionMasterKey: process.env.MASTER_KEY || yaml.keyring?.encryptionMasterKey || null, + encryptionMasterAwsKmsArn: process.env.MASTER_AWS_KMS_ARN || yaml.keyring?.encryptionMasterAwsKmsArn || null, + hmacSecret: process.env.HMAC_SECRET || yaml.keyring?.hmacSecret || null + }), + clients + } +}) + +export type Config = z.output + +export const load = (): Config => { + const configFilePathEnv = process.env.CONFIG_FILE_ABSOLUTE_PATH + const configFileRelativePathEnv = process.env.CONFIG_FILE_RELATIVE_PATH + const filePath = configFilePathEnv + ? path.resolve(configFilePathEnv) + : path.resolve(process.cwd(), configFileRelativePathEnv || 'config/vault-config.yaml') + let yamlConfigRaw = {} + try { + if (fs.existsSync(filePath)) { + const fileContents = fs.readFileSync(filePath, 'utf8') + yamlConfigRaw = parse(fileContents) } - }) + // If file doesn't exist, we'll use empty object as default + } catch (error) { + logger.warn(`Warning: Could not read config file at ${filePath}: ${error.message}`) + // Continue with empty config + } + + const result = LoadConfig.safeParse(yamlConfigRaw) if (result.success) { return result.data @@ -61,3 +302,7 @@ export const load = (): Config => { throw new Error(`Invalid application configuration: ${result.error.message}`) } + +export const getEnv = (): Env => { + return z.nativeEnum(Env).parse(process.env.NODE_ENV) +} diff --git a/apps/vault/src/main.constant.ts b/apps/vault/src/main.constant.ts deleted file mode 100644 index 23749d4bd..000000000 --- a/apps/vault/src/main.constant.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node' -import { adminApiKeySecurity, gnapSecurity } from '@narval/nestjs-shared' - -export const REQUEST_HEADER_API_KEY = 'x-api-key' -export const REQUEST_HEADER_AUTHORIZATION = 'Authorization' - -export const ENCRYPTION_KEY_NAMESPACE = 'armory.vault' -export const ENCRYPTION_KEY_NAME = 'storage-encryption' -export const ENCRYPTION_WRAPPING_SUITE = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING - -export const GNAP_SECURITY = gnapSecurity() -export const ADMIN_API_KEY_SECURITY = adminApiKeySecurity(REQUEST_HEADER_API_KEY) diff --git a/apps/vault/src/main.module.ts b/apps/vault/src/main.module.ts index 4ef645e83..e99616766 100644 --- a/apps/vault/src/main.module.ts +++ b/apps/vault/src/main.module.ts @@ -1,37 +1,60 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' -import { EncryptionModule } from '@narval/encryption-module' +import { ConfigModule } from '@narval/config-module' +import { EncryptionService } from '@narval/encryption-module' import { HttpLoggerMiddleware, LoggerModule, OpenTelemetryModule } from '@narval/nestjs-shared' -import { MiddlewareConsumer, Module, NestModule, OnModuleInit, ValidationPipe, forwardRef } from '@nestjs/common' -import { APP_PIPE } from '@nestjs/core' +import { MiddlewareConsumer, Module, NestModule, OnModuleInit, ValidationPipe } from '@nestjs/common' +import { APP_PIPE, RouterModule } from '@nestjs/core' import { ZodValidationPipe } from 'nestjs-zod' +import { AppRepository } from './app.repository' +import { AppService } from './app.service' +import { BrokerModule } from './broker/broker.module' import { ClientModule } from './client/client.module' import { load } from './main.config' -import { EncryptionModuleOptionFactory } from './shared/factory/encryption-module-option.factory' -import { AppService } from './vault/core/service/app.service' -import { ProvisionService } from './vault/core/service/provision.service' +import { ProvisionService } from './provision.service' +import { KeyValueModule } from './shared/module/key-value/key-value.module' +import { PersistenceModule } from './shared/module/persistence/persistence.module' import { VaultModule } from './vault/vault.module' const INFRASTRUCTURE_MODULES = [ LoggerModule, ConfigModule.forRoot({ load: [load], - isGlobal: true - }), - EncryptionModule.registerAsync({ - imports: [forwardRef(() => VaultModule)], - inject: [ConfigService, AppService], - useClass: EncryptionModuleOptionFactory + isGlobal: true, + cache: true }), OpenTelemetryModule.forRoot() ] @Module({ imports: [ - ...INFRASTRUCTURE_MODULES, + PersistenceModule.register({ + imports: [] // Specifically erase the imports, so we do NOT initialize the EncryptionModule + }), + KeyValueModule + ], + providers: [AppRepository, AppService, ProvisionService, { provide: EncryptionService, useValue: undefined }], + exports: [AppService, ProvisionService] +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(HttpLoggerMiddleware).forRoutes('*') + } +} +@Module({ + imports: [ + ...INFRASTRUCTURE_MODULES, + AppModule, + PersistenceModule.forRoot(), // Domain VaultModule, - ClientModule + ClientModule, + BrokerModule, + RouterModule.register([ + { + path: 'provider', + module: BrokerModule + } + ]) ], providers: [ { @@ -62,7 +85,7 @@ export class MainModule implements NestModule { // context, dependencies requiring encryption will fail because the keyring was // already set as undefined. @Module({ - imports: [...INFRASTRUCTURE_MODULES, VaultModule] + imports: [...INFRASTRUCTURE_MODULES, AppModule] }) export class ProvisionModule implements OnModuleInit { constructor(private provisionService: ProvisionService) {} diff --git a/apps/vault/src/main.ts b/apps/vault/src/main.ts index 5142d6630..46669d4c7 100644 --- a/apps/vault/src/main.ts +++ b/apps/vault/src/main.ts @@ -7,12 +7,18 @@ import { instrumentTelemetry } from '@narval/open-telemetry' instrumentTelemetry({ serviceName: 'vault' }) import { ConfigService } from '@narval/config-module' -import { LoggerService, withApiVersion, withCors, withLogger, withSwagger } from '@narval/nestjs-shared' +import { + LoggerService, + securityOptions, + withApiVersion, + withCors, + withLogger, + withSwagger +} from '@narval/nestjs-shared' import { INestApplication, ValidationPipe } from '@nestjs/common' import { NestFactory } from '@nestjs/core' import { lastValueFrom, map, of, switchMap } from 'rxjs' import { Config } from './main.config' -import { ADMIN_API_KEY_SECURITY, GNAP_SECURITY } from './main.constant' import { MainModule, ProvisionModule } from './main.module' /** @@ -57,9 +63,13 @@ async function bootstrap() { withSwagger({ title: 'Vault', description: - 'Secure storage for private keys and sensitive data, designed to protect your most critical assets in web3.0', + 'Secure Enclave-backed authorization proxy for web3 secrets. Holds encrypted credentials and proxies API requests to custodians and wallet tech providers. Can also generate evm wallet private keys & sign transactions.', version: '1.0', - security: [GNAP_SECURITY, ADMIN_API_KEY_SECURITY] + security: [securityOptions.gnap, securityOptions.adminApiKey, securityOptions.detachedJws], + server: { + url: configService.get('baseUrl'), + description: 'Narval Vault Base Url' + } }) ), switchMap((app) => app.listen(port)) diff --git a/apps/vault/src/provision.service.ts b/apps/vault/src/provision.service.ts new file mode 100644 index 000000000..b007741a0 --- /dev/null +++ b/apps/vault/src/provision.service.ts @@ -0,0 +1,162 @@ +import { ConfigService } from '@narval/config-module' +import { decryptMasterKey, generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module' +import { LoggerService } from '@narval/nestjs-shared' +import { toBytes } from '@narval/policy-engine-shared' +import { Injectable } from '@nestjs/common' +import { AppService } from './app.service' +import { Config } from './main.config' +import { App } from './shared/type/domain.type' +import { ProvisionException } from './vault/core/exception/provision.exception' + +@Injectable() +export class ProvisionService { + // IMPORTANT: The provision service establishes encryption. Therefore, you + // cannot have dependencies that rely on encryption to function. If you do, + // you'll ran into an error due to a missing keyring. + // Any process that requires encryption should be handled in the + // BootstrapService. + constructor( + private configService: ConfigService, + private appService: AppService, + private logger: LoggerService + ) {} + + // Provision the application if it's not already provisioned. + // Update configuration if needed. + // 1. Check if we have an App reference; App is initialized once. + // 2. Check if we have Encryption set up; Encryption is initialized once. + // 3. Auth can be updated, so change it if it's changed. + + // NOTE: The `adminApiKeyHash` argument is for test convenience in case it + // needs to provision the application. + async provision(adminApiKeyHash?: string): Promise { + return this.run({ + adminApiKeyHash, + setupEncryption: async (app, thisApp, keyring) => { + await this.setupEncryption(app, thisApp, keyring) + await this.verifyRawEncryption(thisApp, keyring) + } + }) + } + + /** + * Core provisioning logic that sets up or updates the application + * configuration. This includes handling encryption setup and admin + * authentication settings. + * + * @param params Configuration parameters for the provisioning process + * + * @param params.adminApiKeyHash Optional hash of the admin API key for + * authentication + * + * @param params.setupEncryption Optional callback to customize encryption + * setup. Receives current app state, new app config, and keyring + * configuration. **Useful for disabling encryption during tests.** + * + * @returns Promise The provisioned application configuration + * + * @throws ProvisionException If app is already provisioned with different ID + * or if encryption setup fails + */ + protected async run(params: { + adminApiKeyHash?: string + setupEncryption?: (app: App | null, thisApp: App, keyring: Config['keyring']) => Promise + }): Promise { + const { adminApiKeyHash, setupEncryption } = params + + // TEMPORARY: Migrate the key-value format of the App config into the table format. + // Can be removed once this runs once. + await this.appService.migrateV1Data() + + // Actually provision the new one; will not overwrite anything if this started from a migration + const app = await this.appService.getApp() + const keyring = this.configService.get('keyring') + + const thisApp: App = { ...(app || { id: this.getId(), encryptionKeyringType: keyring.type }) } + if (app && app.id !== this.getId()) { + throw new ProvisionException('App already provisioned with a different ID', { + current: this.getId(), + saved: app.id + }) + } + + if (setupEncryption) { + await setupEncryption(app, thisApp, keyring) + } + + // Now set the Auth if needed + thisApp.adminApiKeyHash = adminApiKeyHash || this.getAdminApiKeyHash() || null // fallback to null so we _unset_ it if it's not provided. + + // If we have an app already & the adminApiKeyHash has changed, just log that we're changing it. + if (app && thisApp.adminApiKeyHash !== app?.adminApiKeyHash) { + this.logger.log('Admin API Key has been changed', { + previous: app?.adminApiKeyHash, + current: thisApp.adminApiKeyHash + }) + } + + // Check if we disabled all auth + thisApp.authDisabled = this.configService.get('app.auth.disabled') + if (thisApp.authDisabled) { + thisApp.adminApiKeyHash = null + } + + this.logger.log('App configuration saved') + + return this.appService.save(thisApp) + } + + protected async setupEncryption(app: App | null, thisApp: App, keyring: Config['keyring']) { + // No encryption set up yet, so initialize encryption + if (!app?.encryptionKeyringType || (!app.encryptionMasterKey && !app.encryptionMasterAwsKmsArn)) { + thisApp.encryptionKeyringType = keyring.type + + if (keyring.type === 'awskms' && keyring.encryptionMasterAwsKmsArn) { + this.logger.log('Using AWS KMS for encryption') + thisApp.encryptionMasterAwsKmsArn = keyring.encryptionMasterAwsKmsArn + } else if (keyring.type === 'raw') { + // If we have the masterKey set in config, we'll save that. + // Otherwise, we'll generate a new one. + if (keyring.encryptionMasterKey) { + this.logger.log('Using provided master key') + thisApp.encryptionMasterKey = keyring.encryptionMasterKey + } else { + this.logger.log('Generating master encryption key') + const { encryptionMasterPassword } = keyring + const kek = generateKeyEncryptionKey(encryptionMasterPassword, thisApp.id) + const masterKey = await generateMasterKey(kek) // Encrypted master encryption key + thisApp.encryptionMasterKey = masterKey + } + } else { + throw new ProvisionException('Unsupported keyring type') + } + } + } + + protected async verifyRawEncryption(thisApp: App, keyring: Config['keyring']) { + // if raw encryption, verify the encryptionMasterPassword in config is the valid kek for the encryptionMasterKey + if (thisApp?.encryptionKeyringType === 'raw' && keyring.type === 'raw' && thisApp.encryptionMasterKey) { + try { + const kek = generateKeyEncryptionKey(keyring.encryptionMasterPassword, thisApp.id) + await decryptMasterKey(kek, toBytes(thisApp.encryptionMasterKey)) + this.logger.log('Master Encryption Key Verified') + } catch (error) { + this.logger.error( + 'Master Encryption Key Verification Failed; check the encryptionMasterPassword is the one that encrypted the masterKey', + { error } + ) + throw new ProvisionException('Master Encryption Key Verification Failed', { error }) + } + } + } + + private getAdminApiKeyHash(): string | null | undefined { + const localAuth = this.configService.get('app.auth.local') + + return localAuth?.adminApiKeyHash + } + + private getId(): string { + return this.configService.get('app.id') + } +} diff --git a/apps/vault/src/shared/constant.ts b/apps/vault/src/shared/constant.ts new file mode 100644 index 000000000..6c7077063 --- /dev/null +++ b/apps/vault/src/shared/constant.ts @@ -0,0 +1,48 @@ +import { RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node' +import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common' +import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core' +import { ZodValidationPipe } from 'nestjs-zod' +import { ApplicationExceptionFilter } from './filter/application-exception.filter' +import { ZodExceptionFilter } from './filter/zod-exception.filter' + +export const REQUEST_HEADER_AUTHORIZATION = 'Authorization' + +export const ENCRYPTION_KEY_NAMESPACE = 'armory.vault' +export const ENCRYPTION_KEY_NAME = 'storage-encryption' +export const ENCRYPTION_WRAPPING_SUITE = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING + +// +// Providers +// + +export const HTTP_VALIDATION_PIPES = [ + { + provide: APP_PIPE, + // Enable transformation after validation for HTTP response serialization. + useFactory: () => new ValidationPipe({ transform: true }) + }, + { + provide: APP_PIPE, + useClass: ZodValidationPipe + } +] + +export const HTTP_EXCEPTION_FILTERS = [ + { + provide: APP_FILTER, + useClass: ApplicationExceptionFilter + }, + { + provide: APP_FILTER, + useClass: ZodExceptionFilter + } +] + +export const DEFAULT_HTTP_MODULE_PROVIDERS = [ + { + provide: APP_INTERCEPTOR, + useClass: ClassSerializerInterceptor + }, + ...HTTP_EXCEPTION_FILTERS, + ...HTTP_VALIDATION_PIPES +] diff --git a/apps/vault/src/shared/decorator/admin-guard.decorator.ts b/apps/vault/src/shared/decorator/admin-guard.decorator.ts index a2217db2d..a02e26401 100644 --- a/apps/vault/src/shared/decorator/admin-guard.decorator.ts +++ b/apps/vault/src/shared/decorator/admin-guard.decorator.ts @@ -1,14 +1,14 @@ +import { REQUEST_HEADER_ADMIN_API_KEY, securityOptions } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { ADMIN_API_KEY_SECURITY, REQUEST_HEADER_API_KEY } from '../../main.constant' import { AdminApiKeyGuard } from '../guard/admin-api-key.guard' export function AdminGuard() { return applyDecorators( UseGuards(AdminApiKeyGuard), - ApiSecurity(ADMIN_API_KEY_SECURITY.name), + ApiSecurity(securityOptions.adminApiKey.name), ApiHeader({ - name: REQUEST_HEADER_API_KEY, + name: REQUEST_HEADER_ADMIN_API_KEY, required: true }) ) diff --git a/apps/vault/src/shared/decorator/client-id.decorator.ts b/apps/vault/src/shared/decorator/client-id.decorator.ts index aa79484bd..87f11a7a0 100644 --- a/apps/vault/src/shared/decorator/client-id.decorator.ts +++ b/apps/vault/src/shared/decorator/client-id.decorator.ts @@ -1,11 +1,11 @@ import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' -import { createParamDecorator, ExecutionContext } from '@nestjs/common' +import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common' export const ClientId = createParamDecorator((_data: unknown, context: ExecutionContext): string => { const req = context.switchToHttp().getRequest() const clientId = req.headers[REQUEST_HEADER_CLIENT_ID] if (!clientId || typeof clientId !== 'string') { - throw new Error(`Missing or invalid ${REQUEST_HEADER_CLIENT_ID} header`) + throw new BadRequestException(`Missing or invalid ${REQUEST_HEADER_CLIENT_ID} header`) } return clientId diff --git a/apps/vault/src/shared/decorator/permission-guard.decorator.ts b/apps/vault/src/shared/decorator/permission-guard.decorator.ts index dcc126b37..085974165 100644 --- a/apps/vault/src/shared/decorator/permission-guard.decorator.ts +++ b/apps/vault/src/shared/decorator/permission-guard.decorator.ts @@ -1,11 +1,16 @@ -import { Permission } from '@narval/armory-sdk' -import { ApiGnapSecurity } from '@narval/nestjs-shared' +import { ApiDetachedJwsSecurity, ApiGnapSecurity } from '@narval/nestjs-shared' import { UseGuards, applyDecorators } from '@nestjs/common' import { Reflector } from '@nestjs/core' import { AuthorizationGuard } from '../guard/authorization.guard' +import { VaultPermission } from '../type/domain.type' -const RequiredPermission = Reflector.createDecorator() +export const RequiredPermission = Reflector.createDecorator() -export function PermissionGuard(...permissions: Permission[]) { - return applyDecorators(RequiredPermission(permissions), UseGuards(AuthorizationGuard), ApiGnapSecurity(permissions)) +export function PermissionGuard(...permissions: VaultPermission[]) { + return applyDecorators( + RequiredPermission(permissions), + UseGuards(AuthorizationGuard), + ApiDetachedJwsSecurity(), + ApiGnapSecurity(permissions) + ) } diff --git a/apps/vault/src/shared/factory/encryption-module-option.factory.ts b/apps/vault/src/shared/factory/encryption-module-option.factory.ts index 76ff73c14..7888eba15 100644 --- a/apps/vault/src/shared/factory/encryption-module-option.factory.ts +++ b/apps/vault/src/shared/factory/encryption-module-option.factory.ts @@ -9,9 +9,9 @@ import { import { LoggerService } from '@narval/nestjs-shared' import { toBytes } from '@narval/policy-engine-shared' import { Injectable } from '@nestjs/common' +import { AppService } from '../../app.service' import { Config } from '../../main.config' -import { ENCRYPTION_KEY_NAME, ENCRYPTION_KEY_NAMESPACE, ENCRYPTION_WRAPPING_SUITE } from '../../main.constant' -import { AppService } from '../../vault/core/service/app.service' +import { ENCRYPTION_KEY_NAME, ENCRYPTION_KEY_NAMESPACE, ENCRYPTION_WRAPPING_SUITE } from '../constant' @Injectable() export class EncryptionModuleOptionFactory { @@ -36,12 +36,12 @@ export class EncryptionModuleOptionFactory { } if (keyringConfig.type === 'raw') { - if (!app.masterKey) { + if (!app.encryptionMasterKey) { throw new Error('Master key not set') } - const kek = generateKeyEncryptionKey(keyringConfig.masterPassword, app.id) - const unencryptedMasterKey = await decryptMasterKey(kek, toBytes(app.masterKey)) + const kek = generateKeyEncryptionKey(keyringConfig.encryptionMasterPassword, app.id) + const unencryptedMasterKey = await decryptMasterKey(kek, toBytes(app.encryptionMasterKey)) return { keyring: new RawAesKeyringNode({ @@ -53,7 +53,7 @@ export class EncryptionModuleOptionFactory { } } else if (keyringConfig.type === 'awskms') { // We have AWS KMS config so we'll use that instead as the MasterKey, which means we don't need a KEK separately - const keyring = new KmsKeyringNode({ generatorKeyId: keyringConfig.masterAwsKmsArn }) + const keyring = new KmsKeyringNode({ generatorKeyId: keyringConfig.encryptionMasterAwsKmsArn }) return { keyring } } diff --git a/apps/vault/src/shared/filter/application-exception.filter.ts b/apps/vault/src/shared/filter/application-exception.filter.ts index d2e8922d0..647bf2b8c 100644 --- a/apps/vault/src/shared/filter/application-exception.filter.ts +++ b/apps/vault/src/shared/filter/application-exception.filter.ts @@ -1,9 +1,10 @@ import { ConfigService } from '@narval/config-module' import { LoggerService } from '@narval/nestjs-shared' -import { ArgumentsHost, Catch, ExceptionFilter, LogLevel } from '@nestjs/common' +import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, LogLevel } from '@nestjs/common' import { Response } from 'express' import { Config, Env } from '../../main.config' import { ApplicationException } from '../../shared/exception/application.exception' +import { HttpException } from '../type/http-exception.type' @Catch(ApplicationException) export class ApplicationExceptionFilter implements ExceptionFilter { @@ -20,33 +21,31 @@ export class ApplicationExceptionFilter implements ExceptionFilter { this.log(exception) - response.status(status).json( - isProduction - ? { - statusCode: status, - message: exception.message, - context: exception.context - } - : { - statusCode: status, - message: exception.message, - context: exception.context, - stack: exception.stack, - ...(exception.origin && { origin: exception.origin }) - } - ) + const body: HttpException = isProduction + ? { + statusCode: status, + message: exception.message, + context: exception.context + } + : { + statusCode: status, + message: exception.message, + context: exception.context, + stack: exception.stack, + ...(exception.origin && { origin: exception.origin }) + } + + response.status(status).json(body) } - // TODO (@wcalderipe, 16/01/24): Unit test the logging logic. For that, we - // must inject the logger in the constructor via dependency injection. private log(exception: ApplicationException) { - const level: LogLevel = exception.getStatus() >= 500 ? 'error' : 'warn' + const level: LogLevel = exception.getStatus() >= HttpStatus.INTERNAL_SERVER_ERROR ? 'error' : 'warn' if (this.logger[level]) { this.logger[level](exception.message, { status: exception.getStatus(), context: exception.context, - stacktrace: exception.stack, + stack: exception.stack, origin: exception.origin }) } diff --git a/apps/vault/src/shared/filter/provider-http-exception.filter.ts b/apps/vault/src/shared/filter/provider-http-exception.filter.ts new file mode 100644 index 000000000..d71c3cbf1 --- /dev/null +++ b/apps/vault/src/shared/filter/provider-http-exception.filter.ts @@ -0,0 +1,62 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' +import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, LogLevel } from '@nestjs/common' +import { Response } from 'express' +import { ProviderHttpException } from '../../broker/core/exception/provider-http.exception' +import { Config, Env } from '../../main.config' +import { HttpException } from '../type/http-exception.type' + +@Catch(ProviderHttpException) +export class ProviderHttpExceptionFilter implements ExceptionFilter { + constructor( + private configService: ConfigService, + private logger: LoggerService + ) {} + + catch(exception: ProviderHttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const isProduction = this.configService.get('env') === Env.PRODUCTION + const status = exception.response.status + + this.log(exception) + + const body: HttpException = isProduction + ? { + statusCode: status, + message: exception.message, + context: this.buildContext(exception) + } + : { + statusCode: status, + message: exception.message, + context: this.buildContext(exception), + stack: exception.stack, + ...(exception.origin && { origin: exception.origin }) + } + + response.status(status).json(body) + } + + private buildContext(exception: ProviderHttpException) { + return { + provider: exception.provider, + error: exception.response.body + } + } + + private log(exception: ProviderHttpException) { + const level: LogLevel = exception.response.status >= HttpStatus.INTERNAL_SERVER_ERROR ? 'error' : 'warn' + + if (this.logger[level]) { + this.logger[level]('Provider HTTP exception', { + context: exception.context, + errorMessage: exception.message, + origin: exception.origin, + provider: exception.provider, + response: exception.response, + stack: exception.stack + }) + } + } +} diff --git a/apps/vault/src/shared/filter/zod-exception.filter.ts b/apps/vault/src/shared/filter/zod-exception.filter.ts index 15d85a826..d4ac76e19 100644 --- a/apps/vault/src/shared/filter/zod-exception.filter.ts +++ b/apps/vault/src/shared/filter/zod-exception.filter.ts @@ -5,6 +5,7 @@ import { Response } from 'express' import { ZodValidationException } from 'nestjs-zod' import { ZodError } from 'zod' import { Config, Env } from '../../main.config' +import { HttpException } from '../type/http-exception.type' // Catch both types, because the zodToDto function will throw a wrapped // ZodValidationError that otherwise isn't picked up here. @@ -23,24 +24,23 @@ export class ZodExceptionFilter implements ExceptionFilter { const zodError = exception instanceof ZodValidationException ? exception.getZodError() : exception - // Log as error level because Zod issues should be handled by the caller. - this.logger.error('Uncaught ZodError', { + this.logger.error("Uncaught ZodError | IF YOU'RE READING THIS, HANDLE THE ERROR IN THE CALLER", { exception: zodError }) - response.status(status).json( - isProduction - ? { - statusCode: status, - message: 'Internal validation error', - context: zodError.flatten() - } - : { - statusCode: status, - message: 'Internal validation error', - context: zodError.flatten(), - stacktrace: zodError.stack - } - ) + const body: HttpException = isProduction + ? { + statusCode: status, + message: 'Validation error', + context: zodError.flatten() + } + : { + statusCode: status, + message: 'Validation error', + context: zodError.flatten(), + stack: zodError.stack + } + + response.status(status).json(body) } } diff --git a/apps/vault/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts b/apps/vault/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts index 46941811e..94f071a9b 100644 --- a/apps/vault/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts +++ b/apps/vault/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts @@ -1,15 +1,14 @@ -import { secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' import { ExecutionContext } from '@nestjs/common' import { mock } from 'jest-mock-extended' -import { REQUEST_HEADER_API_KEY } from '../../../../main.constant' -import { AppService } from '../../../../vault/core/service/app.service' +import { AppService } from '../../../../app.service' import { ApplicationException } from '../../../exception/application.exception' import { AdminApiKeyGuard } from '../../admin-api-key.guard' describe(AdminApiKeyGuard.name, () => { const mockExecutionContext = (apiKey?: string) => { const headers = { - [REQUEST_HEADER_API_KEY]: apiKey + [REQUEST_HEADER_ADMIN_API_KEY]: apiKey } const request = { headers } @@ -22,10 +21,11 @@ describe(AdminApiKeyGuard.name, () => { const mockAppService = (adminApiKey = 'test-admin-api-key') => { const app = { - adminApiKey: secret.hash(adminApiKey), + adminApiKeyHash: secret.hash(adminApiKey), id: 'test-app-id', - masterKey: 'test-master-key', - activated: true + encryptionMasterKey: 'test-master-key', + encryptionKeyringType: 'raw' as const, + authDisabled: false } const serviceMock = mock() @@ -35,20 +35,20 @@ describe(AdminApiKeyGuard.name, () => { return serviceMock } - it(`throws an error when ${REQUEST_HEADER_API_KEY} header is missing`, async () => { + it(`throws an error when ${REQUEST_HEADER_ADMIN_API_KEY} header is missing`, async () => { const guard = new AdminApiKeyGuard(mockAppService()) await expect(guard.canActivate(mockExecutionContext())).rejects.toThrow(ApplicationException) }) - it(`returns true when ${REQUEST_HEADER_API_KEY} matches the app admin api key`, async () => { + it(`returns true when ${REQUEST_HEADER_ADMIN_API_KEY} matches the app admin api key`, async () => { const adminApiKey = 'test-admin-api-key' const guard = new AdminApiKeyGuard(mockAppService(adminApiKey)) expect(await guard.canActivate(mockExecutionContext(adminApiKey))).toEqual(true) }) - it(`returns false when ${REQUEST_HEADER_API_KEY} does not matches the app admin api key`, async () => { + it(`returns false when ${REQUEST_HEADER_ADMIN_API_KEY} does not matches the app admin api key`, async () => { const guard = new AdminApiKeyGuard(mockAppService('test-admin-api-key')) expect(await guard.canActivate(mockExecutionContext('another-api-key'))).toEqual(false) diff --git a/apps/vault/src/shared/guard/__test__/unit/authorization.guard.spec.ts b/apps/vault/src/shared/guard/__test__/unit/authorization.guard.spec.ts new file mode 100644 index 000000000..8920c53b1 --- /dev/null +++ b/apps/vault/src/shared/guard/__test__/unit/authorization.guard.spec.ts @@ -0,0 +1,501 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' +import { FIXTURE } from '@narval/policy-engine-shared' +import { + buildSignerEip191, + hash, + hexToBase64Url, + JwsdHeader, + Payload, + PrivateKey, + privateKeyToHex, + secp256k1PrivateKeyToJwk, + secp256k1PrivateKeyToPublicJwk, + secp256k1PublicKeyToJwk, + SigningAlg, + signJwsd, + signJwt +} from '@narval/signature' +import { ExecutionContext } from '@nestjs/common' +import { Reflector } from '@nestjs/core' +import { mock } from 'jest-mock-extended' +import { ZodError } from 'zod' +import { ClientService } from '../../../../client/core/service/client.service' +import { Config } from '../../../../main.config' +import { Client } from '../../../type/domain.type' +import { AuthorizationGuard } from '../../authorization.guard' + +const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' +// Engine key used to sign the approval request +const enginePrivateJwk = secp256k1PrivateKeyToJwk(PRIVATE_KEY) +const pinnedPublicJWK = secp256k1PrivateKeyToPublicJwk(PRIVATE_KEY) + +const getBaseClient = (): Client => ({ + clientId: 'test-client', + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: true, + allowBearerTokens: false, + allowWildcard: [ + 'path.to.allow', + 'transactionRequest.maxFeePerGas', + 'transactionRequest.maxPriorityFeePerGas', + 'transactionRequest.gas' + ] + }, + pinnedPublicKey: pinnedPublicJWK + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: 'https://vault-test.narval.xyz', + createdAt: new Date(), + updatedAt: new Date() +}) + +const getJwsd = async ({ + userPrivateJwk, + baseUrl, + requestUrl, + accessToken, + payload +}: { + userPrivateJwk: PrivateKey + baseUrl?: string + requestUrl: string + accessToken?: string + payload: object | string +}) => { + const now = Math.floor(Date.now() / 1000) + + const jwsdSigner = buildSignerEip191(await privateKeyToHex(userPrivateJwk)) + const jwsdHeader: JwsdHeader = { + alg: SigningAlg.EIP191, + kid: userPrivateJwk.kid, + typ: 'gnap-binding-jwsd', + htm: 'POST', + uri: `${baseUrl || getBaseClient().baseUrl}${requestUrl}`, // matches the client baseUrl + request url + created: now, + ath: accessToken ? hexToBase64Url(hash(accessToken)) : undefined + } + + const jwsd = await signJwsd(payload, jwsdHeader, jwsdSigner).then((jws) => { + // Strip out the middle part for size + const parts = jws.split('.') + parts[1] = '' + return parts.join('.') + }) + + return jwsd +} + +const getAccessToken = async (request: unknown, opts: object = {}) => { + const payload: Payload = { + requestHash: hash(request), + sub: 'test-root-user-uid', + iss: 'https://armory.narval.xyz', + iat: Math.floor(Date.now() / 1000), + ...opts + } + const signer = buildSignerEip191(PRIVATE_KEY) + + return signJwt(payload, enginePrivateJwk, { alg: SigningAlg.EIP191 }, signer) +} + +describe(AuthorizationGuard.name, () => { + let mockClientService = mock() + let mockConfigService = mock>() + + let mockLogger = mock() + let mockReflector = mock() + + beforeEach(() => { + jest.resetAllMocks() + mockClientService = mock() + mockConfigService = mock>() + mockLogger = mock() + mockReflector = mock() + }) + + describe('canActivate', () => { + const mockExecutionContext = ({ request }: { request?: unknown } = {}) => { + const mockRequest = request || { + headers: {}, + body: {}, + url: '/test', + method: 'GET' + } + return { + switchToHttp: () => ({ + getRequest: () => mockRequest + }), + getHandler: jest.fn() + } as unknown as ExecutionContext + } + + it('throws when client-id header is missing', async () => { + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + await expect(guard.canActivate(mockExecutionContext())).rejects.toThrow('Missing or invalid x-client-id header') + }) + + it('throws when client is not found', async () => { + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const mockRequest = { + headers: { 'x-client-id': 'client-that-does-not-exist' }, + body: {}, + url: '/test', + method: 'GET' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow('Client not found') + }) + + it('passes when client auth is disabled', async () => { + const client = getBaseClient() + client.auth.disabled = true + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + + const mockRequest = { + headers: { 'x-client-id': 'test-client' }, + body: {}, + url: '/test', + method: 'GET' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).resolves.toEqual(true) + expect(mockLogger.warn).toHaveBeenCalled() + }) + + // For this scenario, we don't yet support JWKS, so if you disable tokenValidation you must pin the allowed users + it('throws when client auth is enabled AND token validation is disabled AND no allowed users are configured', async () => { + const client = getBaseClient() + client.auth.tokenValidation.disabled = true + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + + const mockRequest = { + headers: { 'x-client-id': 'test-client' }, + body: {}, + url: '/test', + method: 'GET' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow('No allowed users configured for client') + expect(mockLogger.warn).toHaveBeenCalled() + }) + + it('throws when client auth is enabled AND token validation is disabled AND invalid jwsd for allowedUsers', async () => { + const client = getBaseClient() + client.auth.tokenValidation.disabled = true + client.auth.local = { + jwsd: client.auth.local?.jwsd || { maxAge: 0, requiredComponents: [] }, // default needed to pass TS validation + allowedUsers: [ + { + userId: 'user-1', + publicKey: pinnedPublicJWK + } + ], + allowedUsersJwksUrl: null + } + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + + const mockRequest = { + headers: { 'x-client-id': 'test-client' }, + body: {}, + url: '/test', + method: 'GET' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow('Invalid request signature') + expect(mockLogger.warn).toHaveBeenCalled() + }) + + it('passes when client auth is enabled AND token validation is disabled AND jwsd is valid for allowedUsers', async () => { + const userPrivateJwk = secp256k1PrivateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice) + const userJwk = secp256k1PublicKeyToJwk(FIXTURE.VIEM_ACCOUNT.Alice.publicKey) + const client = getBaseClient() + client.auth.tokenValidation.disabled = true + client.auth.local = { + jwsd: client.auth.local?.jwsd || { maxAge: 0, requiredComponents: [] }, // default needed to pass TS validation + allowedUsers: [ + { + userId: 'user-1', + publicKey: userJwk + } + ], + allowedUsersJwksUrl: null + } + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const jwsd = await getJwsd({ + userPrivateJwk, + requestUrl: '/test', + payload + }) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', 'detached-jws': jwsd }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).resolves.toEqual(true) + }) + + it('throws when token validation is enabled and missing accessToken', async () => { + expect.assertions(2) + const client = getBaseClient() + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const mockRequest = { + headers: { 'x-client-id': 'test-client' }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow(ZodError) + + const mockRequest2 = { + headers: { 'x-client-id': 'test-client', authorization: 'bearer 0000' }, + body: payload, + url: '/test', + method: 'POST' + } + const context2 = mockExecutionContext({ request: mockRequest2 }) + + await expect(guard.canActivate(context2)).rejects.toThrow( + 'Missing or invalid Access Token in Authorization header' + ) + }) + + // JWT Validation + it('throws when no pinnedPublicKey is configured', async () => { + const client = getBaseClient() + client.auth.tokenValidation.pinnedPublicKey = null + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const accessToken = await getAccessToken(payload) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', authorization: `GNAP ${accessToken}` }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow('No engine key configured') + }) + + it('throws when requieBoundTokens is true and token is not bound', async () => { + const userPrivateJwk = secp256k1PrivateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice) + const client = getBaseClient() + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const accessToken = await getAccessToken(payload, { sub: 'user-1' }) + const jwsd = await getJwsd({ + userPrivateJwk, + requestUrl: '/test', + payload, + accessToken + }) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', authorization: `GNAP ${accessToken}`, 'detached-jws': jwsd }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow( + 'Access Token must be bound to a key referenced in the cnf claim' + ) + }) + + it('passes when token is bound but used by a different user', async () => { + const userJwk = secp256k1PublicKeyToJwk(FIXTURE.VIEM_ACCOUNT.Alice.publicKey) + const client = getBaseClient() + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const accessToken = await getAccessToken(payload, { sub: 'user-1', cnf: userJwk }) + const jwsd = await getJwsd({ + userPrivateJwk: secp256k1PrivateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Bob), // DIFFERENT user key + requestUrl: '/test', + payload, + accessToken + }) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', authorization: `GNAP ${accessToken}`, 'detached-jws': jwsd }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow('Invalid signature') + }) + + it('passes when token is valid & request is bound with jwsd', async () => { + const userPrivateJwk = secp256k1PrivateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice) + const userJwk = secp256k1PublicKeyToJwk(FIXTURE.VIEM_ACCOUNT.Alice.publicKey) + const client = getBaseClient() + mockClientService.findById.mockResolvedValue(client) + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const accessToken = await getAccessToken(payload, { sub: 'user-1', cnf: userJwk }) + const jwsd = await getJwsd({ + userPrivateJwk, + requestUrl: '/test', + payload, + accessToken + }) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', authorization: `GNAP ${accessToken}`, 'detached-jws': jwsd }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).resolves.toEqual(true) + }) + + it('passes when token contains required permissions', async () => { + const userPrivateJwk = secp256k1PrivateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice) + const userJwk = secp256k1PublicKeyToJwk(FIXTURE.VIEM_ACCOUNT.Alice.publicKey) + const client = getBaseClient() + mockClientService.findById.mockResolvedValue(client) + + // Mock the reflector to return permissions + mockReflector.get.mockReturnValue(['WALLET_READ']) + + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + const accessToken = await getAccessToken(payload, { + sub: 'user-1', + cnf: userJwk, + access: [ + { + resource: 'vault', + permissions: ['WALLET_READ'] + } + ] + }) + const jwsd = await getJwsd({ + userPrivateJwk, + requestUrl: '/test', + payload, + accessToken + }) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', authorization: `GNAP ${accessToken}`, 'detached-jws': jwsd }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).resolves.toEqual(true) + }) + + it('throws when token does not contain required permissions', async () => { + const userPrivateJwk = secp256k1PrivateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice) + const userJwk = secp256k1PublicKeyToJwk(FIXTURE.VIEM_ACCOUNT.Alice.publicKey) + const client = getBaseClient() + mockClientService.findById.mockResolvedValue(client) + + // Mock the reflector to return permissions + mockReflector.get.mockReturnValue(['WALLET_READ']) + + const guard = new AuthorizationGuard(mockClientService, mockConfigService, mockReflector, mockLogger) + const payload = { + value: 'test-value' + } + + // Token with different permission than required + const accessToken = await getAccessToken(payload, { + sub: 'user-1', + cnf: userJwk, + access: [ + { + resource: 'vault', + permissions: ['WALLET_WRITE'] + } + ] + }) + const jwsd = await getJwsd({ + userPrivateJwk, + requestUrl: '/test', + payload, + accessToken + }) + + const mockRequest = { + headers: { 'x-client-id': 'test-client', authorization: `GNAP ${accessToken}`, 'detached-jws': jwsd }, + body: payload, + url: '/test', + method: 'POST' + } + const context = mockExecutionContext({ request: mockRequest }) + + await expect(guard.canActivate(context)).rejects.toThrow('Invalid permissions') + }) + }) +}) diff --git a/apps/vault/src/shared/guard/admin-api-key.guard.ts b/apps/vault/src/shared/guard/admin-api-key.guard.ts index 2c7e9fbd0..26d1d79bf 100644 --- a/apps/vault/src/shared/guard/admin-api-key.guard.ts +++ b/apps/vault/src/shared/guard/admin-api-key.guard.ts @@ -1,7 +1,6 @@ -import { secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_ADMIN_API_KEY, secret } from '@narval/nestjs-shared' import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common' -import { REQUEST_HEADER_API_KEY } from '../../main.constant' -import { AppService } from '../../vault/core/service/app.service' +import { AppService } from '../../app.service' import { ApplicationException } from '../exception/application.exception' @Injectable() @@ -10,17 +9,17 @@ export class AdminApiKeyGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest() - const apiKey = req.headers[REQUEST_HEADER_API_KEY] + const apiKey = req.headers[REQUEST_HEADER_ADMIN_API_KEY] if (!apiKey) { throw new ApplicationException({ - message: `Missing or invalid ${REQUEST_HEADER_API_KEY} header`, + message: `Missing or invalid ${REQUEST_HEADER_ADMIN_API_KEY} header`, suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED }) } const app = await this.appService.getAppOrThrow() - return app.adminApiKey === secret.hash(apiKey) + return app.adminApiKeyHash === secret.hash(apiKey) } } diff --git a/apps/vault/src/shared/guard/authorization.guard.ts b/apps/vault/src/shared/guard/authorization.guard.ts index 95c7b0abc..81a758e02 100644 --- a/apps/vault/src/shared/guard/authorization.guard.ts +++ b/apps/vault/src/shared/guard/authorization.guard.ts @@ -1,17 +1,21 @@ import { ConfigService } from '@narval/config-module' -import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' -import { JwtVerifyOptions, publicKeySchema, verifyJwsd, verifyJwt } from '@narval/signature' +import { LoggerService, REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { Jwsd, JwtVerifyOptions, PublicKey, publicKeySchema, verifyJwsd, verifyJwt } from '@narval/signature' import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common' import { Reflector } from '@nestjs/core' import { z } from 'zod' import { ClientService } from '../../client/core/service/client.service' import { Config } from '../../main.config' -import { PermissionGuard } from '../decorator/permission-guard.decorator' +import { RequiredPermission } from '../decorator/permission-guard.decorator' import { ApplicationException } from '../exception/application.exception' -import { Client } from '../type/domain.type' +import { Client, VaultPermission } from '../type/domain.type' +// Option 2: Add validation with custom message const AuthorizationHeaderSchema = z.object({ - authorization: z.string() + authorization: z.string({ + required_error: 'Authorization header is required', + invalid_type_error: 'Authorization header must be a string' + }) }) const ONE_MINUTE = 60 @@ -21,7 +25,8 @@ export class AuthorizationGuard implements CanActivate { constructor( private clientService: ClientService, private configService: ConfigService, - private reflector: Reflector + private reflector: Reflector, + private logger: LoggerService ) {} async canActivate(context: ExecutionContext): Promise { @@ -35,6 +40,58 @@ export class AuthorizationGuard implements CanActivate { }) } + const client = await this.clientService.findById(clientId) + if (!client) { + throw new ApplicationException({ + message: 'Client not found', + suggestedHttpStatusCode: HttpStatus.NOT_FOUND + }) + } + + if (client?.auth.disabled) { + this.logger.warn('Client auth disabled -- all request will be permitted') + return true + } + + if (client?.auth.tokenValidation.disabled) { + this.logger.warn('Client token validation disabled -- auth tokens will not be required') + // TODO: should we not return, in case AuthN is used w/out validation? + + // TODO: add JWKS as an option instead of allowedUsers + const allowedUsers = client.auth.local?.allowedUsers + if (!allowedUsers?.length) { + // JWT Validation is disabled, but Auth is enabled; then we MUST have an allow-list of user keys + throw new ApplicationException({ + message: 'No allowed users configured for client', + suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED + }) + } + + // Try each allowed user's public key until we find one that validates + let validJwsd: Jwsd | undefined + for (const user of allowedUsers) { + try { + validJwsd = await this.validateJwsdAuthentication(context, client, user.publicKey) + break + } catch (err) { + // Continue trying other keys + this.logger.warn('Invalid request signature, but could have another allowedUser key', { error: err }) + continue + } + } + + if (!validJwsd) { + throw new ApplicationException({ + message: 'Invalid request signature', + suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED + }) + } + + return true + } + + // Validate the Access Token. + // Expect the header in the format "GNAP " const headers = AuthorizationHeaderSchema.parse(req.headers) const accessToken: string | undefined = headers.authorization.split('GNAP ')[1] @@ -46,17 +103,9 @@ export class AuthorizationGuard implements CanActivate { }) } - const client = await this.clientService.findById(clientId) - - if (!client) { - throw new ApplicationException({ - message: 'Client not found', - suggestedHttpStatusCode: HttpStatus.NOT_FOUND - }) - } - + // Get the Permissions (scopes) required from the request decorator, if it exists. const { request: requestHash } = req.body - const permissions = this.reflector.get(PermissionGuard, context.getHandler()) + const permissions: VaultPermission[] | undefined = this.reflector.get(RequiredPermission, context.getHandler()) const access = permissions && permissions.length > 0 ? [ @@ -68,10 +117,10 @@ export class AuthorizationGuard implements CanActivate { : undefined const opts: JwtVerifyOptions = { - audience: client.audience, - issuer: client.issuer, - maxTokenAge: client.maxTokenAge, - allowWildcard: client.allowWildcard, + audience: client.auth.tokenValidation.verification.audience, + issuer: client.auth.tokenValidation.url, + maxTokenAge: client.auth.tokenValidation.verification.maxTokenAge, + allowWildcard: client.auth.tokenValidation.verification.allowWildcard, ...(requestHash && { requestHash }), ...(access && { access }) } @@ -79,25 +128,65 @@ export class AuthorizationGuard implements CanActivate { return this.validateToken(context, client, accessToken, opts) } + private async validateJwsdAuthentication( + context: ExecutionContext, + client: Client, + publicKey: PublicKey, + accessToken?: string + ): Promise { + const req = context.switchToHttp().getRequest() + + // TODO: support httpsig proof + const jwsdHeader = req.headers['detached-jws'] + + if (!jwsdHeader) { + throw new ApplicationException({ + message: `Missing detached-jws header`, + suggestedHttpStatusCode: HttpStatus.FORBIDDEN + }) + } + + // Will throw if not valid + try { + // TODO: add optionality for requiredComponents; for now, we always require all the jwsd claims (htm, uri, created, ath) + const defaultBaseUrl = this.configService.get('baseUrl') + const jwsd = await verifyJwsd(jwsdHeader, publicKey, { + requestBody: req.body, // Verify the request body + accessToken, // Verify that the ATH matches the access token + uri: `${(client.baseUrl || defaultBaseUrl).replace(/\/+$/, '')}${req.url.replace(/^\/+/, '/')}`, // Verify the request URI; ensure proper url joining + htm: req.method, // Verify the request method + maxTokenAge: client.auth.local?.jwsd.maxAge || ONE_MINUTE + }) + + return jwsd + } catch (err) { + throw new ApplicationException({ + message: err.message, + origin: err, + suggestedHttpStatusCode: HttpStatus.FORBIDDEN + }) + } + } + private async validateToken( context: ExecutionContext, client: Client, token: string, opts: JwtVerifyOptions ): Promise { - const req = context.switchToHttp().getRequest() - - if (!client.engineJwk) { + if (!client.auth.tokenValidation.pinnedPublicKey) { + // TODO: check jwksUrl too + // TODO: check opaque token; ping the auth server to check validity throw new ApplicationException({ message: 'No engine key configured', suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED }) } - const clientJwk = publicKeySchema.parse(client.engineJwk) + const pinnedPublicKey = publicKeySchema.parse(client.auth.tokenValidation.pinnedPublicKey) // Validate the JWT has a valid signature for the expected client key & the request matches - const { payload } = await verifyJwt(token, clientJwk, opts).catch((err) => { + const { payload } = await verifyJwt(token, pinnedPublicKey, opts).catch((err) => { throw new ApplicationException({ message: err.message, origin: err, @@ -105,35 +194,24 @@ export class AuthorizationGuard implements CanActivate { }) }) - // We want to also check the client key in cnf so we can optionally do bound requests + // Signed Request / Bound Token ->> + // If the client has `requireBoundTokens` set, OR if the token has a `cnf` field, the request MUST include a proof of key possession + // If `requireBoundTokens` is set but the token does not have a `cnf`, we consider this _invalid_, because we won't accept unbound tokens. + // This means that this server can only accept bound tokens, and/or the auth server can issue a bound token that shouldn't be accepted without proof. + + if (client.auth.tokenValidation.verification.requireBoundTokens && !payload.cnf) { + throw new ApplicationException({ + message: 'Access Token must be bound to a key referenced in the cnf claim', + suggestedHttpStatusCode: HttpStatus.FORBIDDEN + }) + } + + // We don't require it here, but if the token is bound (cnf claim) then we must verify the request includes a proof of key possession if (payload.cnf) { const boundKey = payload.cnf - const jwsdHeader = req.headers['detached-jws'] - if (!jwsdHeader) { - throw new ApplicationException({ - message: `Missing detached-jws header`, - suggestedHttpStatusCode: HttpStatus.FORBIDDEN - }) - } - - // Will throw if not valid - try { - const defaultBaseUrl = this.configService.get('baseUrl') - await verifyJwsd(jwsdHeader, boundKey, { - requestBody: req.body, // Verify the request body - accessToken: token, // Verify that the ATH matches the access token - uri: `${client.baseUrl || defaultBaseUrl}${req.url}`, // Verify the request URI - htm: req.method, // Verify the request method - maxTokenAge: ONE_MINUTE - }) - } catch (err) { - throw new ApplicationException({ - message: err.message, - origin: err, - suggestedHttpStatusCode: HttpStatus.FORBIDDEN - }) - } + await this.validateJwsdAuthentication(context, client, boundKey, token) + // Valid! } return true diff --git a/apps/vault/src/shared/module/key-value/key-value.module.ts b/apps/vault/src/shared/module/key-value/key-value.module.ts index 38067a354..e488b35bd 100644 --- a/apps/vault/src/shared/module/key-value/key-value.module.ts +++ b/apps/vault/src/shared/module/key-value/key-value.module.ts @@ -2,8 +2,8 @@ import { ConfigService } from '@narval/config-module' import { EncryptionModule } from '@narval/encryption-module' import { LoggerService } from '@narval/nestjs-shared' import { Module, forwardRef } from '@nestjs/common' -import { AppService } from '../../../vault/core/service/app.service' -import { VaultModule } from '../../../vault/vault.module' +import { AppService } from '../../../app.service' +import { AppModule } from '../../../main.module' import { EncryptionModuleOptionFactory } from '../../factory/encryption-module-option.factory' import { PersistenceModule } from '../persistence/persistence.module' import { KeyValueRepository } from './core/repository/key-value.repository' @@ -14,9 +14,11 @@ import { PrismaKeyValueRepository } from './persistence/repository/prisma-key-va @Module({ imports: [ - PersistenceModule, + PersistenceModule.register({ + imports: [] // Specifically erase the imports, so we do NOT initialize the EncryptionModule since KV will handle it's own encryption + }), EncryptionModule.registerAsync({ - imports: [forwardRef(() => VaultModule)], + imports: [forwardRef(() => AppModule)], inject: [ConfigService, AppService, LoggerService], useClass: EncryptionModuleOptionFactory }) diff --git a/apps/vault/src/shared/module/persistence/exception/parse.exception.ts b/apps/vault/src/shared/module/persistence/exception/parse.exception.ts new file mode 100644 index 000000000..2ff485b9f --- /dev/null +++ b/apps/vault/src/shared/module/persistence/exception/parse.exception.ts @@ -0,0 +1,11 @@ +import { PersistenceException } from './persistence.exception' + +export class ParseException extends PersistenceException { + readonly origin: Error + + constructor(origin: Error) { + super(origin.message) + + this.origin = origin + } +} diff --git a/apps/vault/src/shared/module/persistence/exception/persistence.exception.ts b/apps/vault/src/shared/module/persistence/exception/persistence.exception.ts new file mode 100644 index 000000000..b0faa3fc9 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/exception/persistence.exception.ts @@ -0,0 +1 @@ +export class PersistenceException extends Error {} diff --git a/apps/vault/src/shared/module/persistence/persistence.module.ts b/apps/vault/src/shared/module/persistence/persistence.module.ts index 95d5dcd4c..3c48f75a6 100644 --- a/apps/vault/src/shared/module/persistence/persistence.module.ts +++ b/apps/vault/src/shared/module/persistence/persistence.module.ts @@ -1,9 +1,44 @@ -import { Module } from '@nestjs/common' +import { ConfigService } from '@narval/config-module' +import { EncryptionModule } from '@narval/encryption-module' +import { LoggerService } from '@narval/nestjs-shared' +import { DynamicModule, forwardRef, ForwardReference, Module, Type } from '@nestjs/common' +import { AppService } from '../../../app.service' +import { AppModule } from '../../../main.module' +import { EncryptionModuleOptionFactory } from '../../factory/encryption-module-option.factory' import { PrismaService } from './service/prisma.service' +import { SeederService } from './service/seeder.service' import { TestPrismaService } from './service/test-prisma.service' -@Module({ - exports: [PrismaService, TestPrismaService], - providers: [PrismaService, TestPrismaService] -}) -export class PersistenceModule {} +@Module({}) +export class PersistenceModule { + static forRoot(): DynamicModule { + return { + module: PersistenceModule, + global: true, + imports: [ + EncryptionModule.registerAsync({ + imports: [forwardRef(() => AppModule)], + inject: [ConfigService, AppService, LoggerService], + useClass: EncryptionModuleOptionFactory + }) + ], + providers: [PrismaService, TestPrismaService], + exports: [PrismaService, TestPrismaService] + } + } + + static register(config: { imports?: Array } = {}): DynamicModule { + return { + module: PersistenceModule, + imports: config.imports || [ + EncryptionModule.registerAsync({ + imports: [forwardRef(() => AppModule)], + inject: [ConfigService, AppService, LoggerService], + useClass: EncryptionModuleOptionFactory + }) + ], + providers: [PrismaService, TestPrismaService, SeederService], + exports: [PrismaService, TestPrismaService, SeederService] + } + } +} diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241203160823_add_provider_integration_tables/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241203160823_add_provider_integration_tables/migration.sql new file mode 100644 index 000000000..a98962f75 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241203160823_add_provider_integration_tables/migration.sql @@ -0,0 +1,141 @@ +-- CreateTable +CREATE TABLE "provider_wallet" ( + "id" TEXT NOT NULL, + "label" TEXT, + "client_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "provider_wallet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_account" ( + "id" TEXT NOT NULL, + "label" TEXT, + "client_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "wallet_id" TEXT NOT NULL, + "network_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "provider_account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_address" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "account_id" TEXT NOT NULL, + "address" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "provider_address_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_known_destination" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "connection_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "external_classification" TEXT, + "address" TEXT NOT NULL, + "asset_id" TEXT, + "network_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "provider_known_destination_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_connection" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "url" TEXT NOT NULL, + "label" TEXT, + "credentials" JSONB NOT NULL, + "status" TEXT NOT NULL, + "_integrity" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "revoked_at" TIMESTAMP(3), + + CONSTRAINT "provider_connection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_wallet_connection" ( + "client_id" TEXT NOT NULL, + "connection_id" TEXT NOT NULL, + "wallet_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "provider_wallet_connection_pkey" PRIMARY KEY ("client_id","connection_id","wallet_id") +); + +-- CreateTable +CREATE TABLE "provider_sync" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "connection_id" TEXT NOT NULL, + "status" TEXT NOT NULL, + "error_name" TEXT, + "error_message" TEXT, + "error_trace_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completed_at" TIMESTAMP(3), + + CONSTRAINT "provider_sync_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_transfer" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "source_wallet_id" TEXT, + "source_account_id" TEXT, + "source_address_id" TEXT, + "provider" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "provider_transfer_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "provider_account" ADD CONSTRAINT "provider_account_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "provider_wallet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_address" ADD CONSTRAINT "provider_address_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "provider_account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_known_destination" ADD CONSTRAINT "provider_known_destination_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_wallet_connection" ADD CONSTRAINT "provider_wallet_connection_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_wallet_connection" ADD CONSTRAINT "provider_wallet_connection_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "provider_wallet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_sync" ADD CONSTRAINT "provider_sync_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_source_wallet_id_fkey" FOREIGN KEY ("source_wallet_id") REFERENCES "provider_wallet"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_source_account_id_fkey" FOREIGN KEY ("source_account_id") REFERENCES "provider_account"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_source_address_id_fkey" FOREIGN KEY ("source_address_id") REFERENCES "provider_address"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241205125125_set_provider_connection_url_to_nullable/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241205125125_set_provider_connection_url_to_nullable/migration.sql new file mode 100644 index 000000000..f4e93112a --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241205125125_set_provider_connection_url_to_nullable/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - Added the required column `request` to the `provider_transfer` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "provider_connection" ALTER COLUMN "url" DROP NOT NULL, +ALTER COLUMN "credentials" DROP NOT NULL, +ALTER COLUMN "created_at" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "provider_transfer" ADD COLUMN "request" JSONB NOT NULL; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241206093046_add_transit_encryption_key_table/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241206093046_add_transit_encryption_key_table/migration.sql new file mode 100644 index 000000000..1f75f1545 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241206093046_add_transit_encryption_key_table/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "transit_encryption_key" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "private_key" JSONB NOT NULL, + "public_key" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "transit_encryption_key_pkey" PRIMARY KEY ("id") +); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241206135505_normalize_client_and_app/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241206135505_normalize_client_and_app/migration.sql new file mode 100644 index 000000000..14ed084ed --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241206135505_normalize_client_and_app/migration.sql @@ -0,0 +1,60 @@ +/* + Warnings: + + - You are about to drop the column `admin_api_key` on the `vault` table. All the data in the column will be lost. + - You are about to drop the column `master_key` on the `vault` table. All the data in the column will be lost. + - Added the required column `encryption_keyring_type` to the `vault` table without a default value. This is not possible if the table is not empty. + +*/ + +-- AlterTable +ALTER TABLE "vault" DROP COLUMN "admin_api_key", +DROP COLUMN "master_key", +ADD COLUMN "admin_api_key_hash" TEXT, +ADD COLUMN "auth_disabled" BOOLEAN, +ADD COLUMN "encryption_keyring_type" TEXT NOT NULL, +ADD COLUMN "encryption_master_aws_kms_arn" TEXT, +ADD COLUMN "enncryption_master_key" TEXT; + +-- CreateTable +CREATE TABLE "client" ( + "client_id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "configuration_source" TEXT NOT NULL, + "auth_disabled" BOOLEAN NOT NULL, + "token_validation_disabled" BOOLEAN NOT NULL, + "backup_public_key" TEXT, + "base_url" TEXT, + "authorization_server_url" TEXT, + "authorization_issuer" TEXT, + "authorization_audience" TEXT, + "authorization_max_token_age" INTEGER, + "authorization_jwks_url" TEXT, + "authorization_pinned_public_key" TEXT, + "authorization_require_bound_tokens" BOOLEAN NOT NULL, + "authorization_allow_bearer_tokens" BOOLEAN NOT NULL, + "authorization_allow_wildcards" TEXT, + "local_auth_allowed_users_jwks_url" TEXT, + "local_auth_jwsd_enabled" BOOLEAN NOT NULL, + "jwsd_max_age" INTEGER, + "jwsd_required_components" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "client_pkey" PRIMARY KEY ("client_id") +); + +-- CreateTable +CREATE TABLE "client_local_auth_allowed_user" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "public_key" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "client_local_auth_allowed_user_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "client_local_auth_allowed_user" ADD CONSTRAINT "client_local_auth_allowed_user_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "client"("client_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241212175217_encrypted_json_is_stringified/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241212175217_encrypted_json_is_stringified/migration.sql new file mode 100644 index 000000000..f92378e4d --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241212175217_encrypted_json_is_stringified/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "provider_connection" ALTER COLUMN "credentials" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "transit_encryption_key" ALTER COLUMN "private_key" SET DATA TYPE TEXT, +ALTER COLUMN "public_key" SET DATA TYPE TEXT; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241213123626_add_unique_constraint_on_provider_resource_tables/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241213123626_add_unique_constraint_on_provider_resource_tables/migration.sql new file mode 100644 index 000000000..3352e92e8 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241213123626_add_unique_constraint_on_provider_resource_tables/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - A unique constraint covering the columns `[client_id,external_id]` on the table `provider_account` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[client_id,external_id]` on the table `provider_address` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[client_id,external_id]` on the table `provider_known_destination` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[client_id,external_id]` on the table `provider_wallet` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "provider_account_client_id_external_id_key" ON "provider_account"("client_id", "external_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_address_client_id_external_id_key" ON "provider_address"("client_id", "external_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_known_destination_client_id_external_id_key" ON "provider_known_destination"("client_id", "external_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_wallet_client_id_external_id_key" ON "provider_wallet"("client_id", "external_id"); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241217102422_encryption_master_key_spelling/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241217102422_encryption_master_key_spelling/migration.sql new file mode 100644 index 000000000..03b7f5c33 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241217102422_encryption_master_key_spelling/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "provider_connection" ALTER COLUMN "_integrity" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "vault" ADD COLUMN "encryption_master_key" TEXT; +-- Copy over the old spelling to the new spelling +UPDATE vault SET encryption_master_key = enncryption_master_key +WHERE enncryption_master_key IS NOT NULL; + diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241219140123_add_m2m_known_dest_conn_and_label_to_known_dest/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241219140123_add_m2m_known_dest_conn_and_label_to_known_dest/migration.sql new file mode 100644 index 000000000..82237d0b0 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241219140123_add_m2m_known_dest_conn_and_label_to_known_dest/migration.sql @@ -0,0 +1,27 @@ +/* + Warnings: + - You are about to drop the column `connection_id` on the `provider_known_destination` table. All the data in the column will be lost. +*/ + +-- DropForeignKey +ALTER TABLE "provider_known_destination" DROP CONSTRAINT "provider_known_destination_connection_id_fkey"; + +-- AlterTable +ALTER TABLE "provider_known_destination" DROP COLUMN "connection_id", +ADD COLUMN "label" TEXT; + +-- CreateTable +CREATE TABLE "provider_known_destination_connection" ( + "client_id" TEXT NOT NULL, + "connection_id" TEXT NOT NULL, + "known_destination_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "provider_known_destination_connection_pkey" PRIMARY KEY ("client_id","connection_id","known_destination_id") +); + +-- AddForeignKey +ALTER TABLE "provider_known_destination_connection" ADD CONSTRAINT "provider_known_destination_connection_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_known_destination_connection" ADD CONSTRAINT "provider_known_destination_connection_known_destination_id_fkey" FOREIGN KEY ("known_destination_id") REFERENCES "provider_known_destination"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241219150752_cascade_deletion_on_m2m_tables/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241219150752_cascade_deletion_on_m2m_tables/migration.sql new file mode 100644 index 000000000..08a128ddb --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241219150752_cascade_deletion_on_m2m_tables/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "provider_wallet_connection" DROP CONSTRAINT "provider_wallet_connection_connection_id_fkey"; + +-- DropForeignKey +ALTER TABLE "provider_wallet_connection" DROP CONSTRAINT "provider_wallet_connection_wallet_id_fkey"; + +-- AddForeignKey +ALTER TABLE "provider_wallet_connection" ADD CONSTRAINT "provider_wallet_connection_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_wallet_connection" ADD CONSTRAINT "provider_wallet_connection_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "provider_wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241220105006_index_created_at_client_id_on_provider_indexed_data/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241220105006_index_created_at_client_id_on_provider_indexed_data/migration.sql new file mode 100644 index 000000000..b2349ff30 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241220105006_index_created_at_client_id_on_provider_indexed_data/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - A unique constraint covering the columns `[client_id,external_id]` on the table `provider_transfer` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE INDEX "provider_account_created_at_client_id_idx" ON "provider_account"("created_at", "client_id"); + +-- CreateIndex +CREATE INDEX "provider_address_created_at_client_id_idx" ON "provider_address"("created_at", "client_id"); + +-- CreateIndex +CREATE INDEX "provider_connection_created_at_client_id_idx" ON "provider_connection"("created_at", "client_id"); + +-- CreateIndex +CREATE INDEX "provider_known_destination_created_at_client_id_idx" ON "provider_known_destination"("created_at", "client_id"); + +-- CreateIndex +CREATE INDEX "provider_transfer_created_at_client_id_idx" ON "provider_transfer"("created_at", "client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_transfer_client_id_external_id_key" ON "provider_transfer"("client_id", "external_id"); + +-- CreateIndex +CREATE INDEX "provider_wallet_created_at_client_id_idx" ON "provider_wallet"("created_at", "client_id"); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20241223121950_normalize_transfer_provider/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20241223121950_normalize_transfer_provider/migration.sql new file mode 100644 index 000000000..848f13c8f --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20241223121950_normalize_transfer_provider/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - You are about to drop the column `request` on the `provider_transfer` table. All the data in the column will be lost. + - A unique constraint covering the columns `[client_id,idempotence_id]` on the table `provider_transfer` will be added. If there are existing duplicate values, this will fail. + - Added the required column `asset_id` to the `provider_transfer` table without a default value. This is not possible if the table is not empty. + - Added the required column `gross_amount` to the `provider_transfer` table without a default value. This is not possible if the table is not empty. + - Added the required column `network_fee_attribution` to the `provider_transfer` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "provider_known_destination_connection" DROP CONSTRAINT "provider_known_destination_connection_known_destination_id_fkey"; + +-- AlterTable +ALTER TABLE "provider_transfer" DROP COLUMN "request", +ADD COLUMN "asset_id" TEXT NOT NULL, +ADD COLUMN "customer_ref_id" TEXT, +ADD COLUMN "destination_account_id" TEXT, +ADD COLUMN "destination_address_id" TEXT, +ADD COLUMN "destination_address_raw" TEXT, +ADD COLUMN "destination_wallet_id" TEXT, +ADD COLUMN "gross_amount" TEXT NOT NULL, +ADD COLUMN "idempotence_id" TEXT, +ADD COLUMN "memo" TEXT, +ADD COLUMN "network_fee_attribution" TEXT NOT NULL, +ADD COLUMN "provider_specific" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "provider_transfer_client_id_idempotence_id_key" ON "provider_transfer"("client_id", "idempotence_id"); + +-- AddForeignKey +ALTER TABLE "provider_known_destination_connection" ADD CONSTRAINT "provider_known_destination_connection_known_destination_id_fkey" FOREIGN KEY ("known_destination_id") REFERENCES "provider_known_destination"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_destination_wallet_id_fkey" FOREIGN KEY ("destination_wallet_id") REFERENCES "provider_wallet"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_destination_account_id_fkey" FOREIGN KEY ("destination_account_id") REFERENCES "provider_account"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_destination_address_id_fkey" FOREIGN KEY ("destination_address_id") REFERENCES "provider_address"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250107175743_fix_staging_network_id_data/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250107175743_fix_staging_network_id_data/migration.sql new file mode 100644 index 000000000..e62501150 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250107175743_fix_staging_network_id_data/migration.sql @@ -0,0 +1,17 @@ +UPDATE provider_account SET network_id = 'BITCOIN' WHERE network_id = 'BTC'; +UPDATE provider_account SET network_id = 'BITCOIN_SIGNET' WHERE network_id = 'BTC_S'; +UPDATE provider_account SET network_id = 'BITCOIN_CASH' WHERE network_id = 'BCH'; +UPDATE provider_account SET network_id = 'ARBITRUM_SEPOLIA' WHERE network_id = 'ETH_ARBITRUM_T'; +UPDATE provider_account SET network_id = 'ETHEREUM' WHERE network_id = 'ETH'; +UPDATE provider_account SET network_id = 'ETHEREUM_HOLESKY' WHERE network_id = 'ETHHOL'; +UPDATE provider_account SET network_id = 'ETHEREUM_SEPOLIA' WHERE network_id = 'ETHSEP'; +UPDATE provider_account SET network_id = 'POLYGON' WHERE network_id = 'POL_POLYGON'; + +UPDATE provider_known_destination SET network_id = 'BITCOIN' WHERE network_id = 'BTC'; +UPDATE provider_known_destination SET network_id = 'BITCOIN_SIGNET' WHERE network_id = 'BTC_S'; +UPDATE provider_known_destination SET network_id = 'BITCOIN_CASH' WHERE network_id = 'BCH'; +UPDATE provider_known_destination SET network_id = 'ARBITRUM_SEPOLIA' WHERE network_id = 'ETH_ARBITRUM_T'; +UPDATE provider_known_destination SET network_id = 'ETHEREUM' WHERE network_id = 'ETH'; +UPDATE provider_known_destination SET network_id = 'ETHEREUM_HOLESKY' WHERE network_id = 'ETHHOL'; +UPDATE provider_known_destination SET network_id = 'ETHEREUM_SEPOLIA' WHERE network_id = 'ETHSEP'; +UPDATE provider_known_destination SET network_id = 'POLYGON' WHERE network_id = 'POL_POLYGON'; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250115095035_add_network_table_and_data/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250115095035_add_network_table_and_data/migration.sql new file mode 100644 index 000000000..36fa759ca --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250115095035_add_network_table_and_data/migration.sql @@ -0,0 +1,252 @@ +-- CreateTable +CREATE TABLE "network" ( + "id" TEXT NOT NULL, + "coin_type" INTEGER, + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "network_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_network" ( + "external_id" TEXT NOT NULL, + "network_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_network_provider_external_id_key" ON "provider_network"("provider", "external_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_network_provider_network_id_key" ON "provider_network"("provider", "network_id"); + +-- AddForeignKey +ALTER TABLE "provider_network" ADD CONSTRAINT "provider_network_network_id_fkey" FOREIGN KEY ("network_id") REFERENCES "network"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- InsertProviderNetwork +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('AETH',514,'Aetherius','2025-01-13 16:19:11.019'), + ('AEVO',NULL,'Aevo','2025-01-13 16:19:11.024'), + ('AGORIC',564,'Agoric','2025-01-13 16:19:11.026'), + ('ALEPH_ZERO',643,'Aleph Zero','2025-01-13 16:19:11.027'), + ('ALGORAND',283,'Algorand','2025-01-13 16:19:11.029'), + ('ALGORAND_TESTNET',1,'Algorand Testnet','2025-01-13 16:19:11.031'), + ('ALLORA',NULL,'Allora','2025-01-13 16:19:11.032'), + ('ALLORA_TESTNET',1,'Allora Testnet','2025-01-13 16:19:11.034'), + ('APTOS',637,'Aptos','2025-01-13 16:19:11.036'), + ('APTOS_TESTNET',1,'Aptos Testnet','2025-01-13 16:19:11.037'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('ARBITRUM_SEPOLIA',1,'Arbitrum Sepolia Testnet','2025-01-13 16:19:11.039'), + ('ASTAR',810,'Astar','2025-01-13 16:19:11.040'), + ('ASTAR_TESTNET',1,'Astar Testnet','2025-01-13 16:19:11.042'), + ('ATOM',118,'Atom','2025-01-13 16:19:11.043'), + ('ATOM_TESTNET',1,'Atom Testnet','2025-01-13 16:19:11.045'), + ('AURORA',2570,'Aurora','2025-01-13 16:19:11.046'), + ('AVAX',9000,'Avalanche','2025-01-13 16:19:11.047'), + ('AVAX_TESTNET',1,'Avalanche Testnet','2025-01-13 16:19:11.049'), + ('AXELAR',NULL,'Axelar','2025-01-13 16:19:11.050'), + ('AXELAR_TESTNET',1,'Axelar Testnet','2025-01-13 16:19:11.052'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('BABYLON',NULL,'Babylon','2025-01-13 16:19:11.053'), + ('BASE',8453,'Base','2025-01-13 16:19:11.054'), + ('BASE_TESTNET',1,'Base Testnet','2025-01-13 16:19:11.056'), + ('BINANCE_SMART_CHAIN',9006,'Binance Smart Chain','2025-01-13 16:19:11.057'), + ('BINANCE_SMART_CHAIN_TESTNET',1,'Binance Smart Chain Testnet','2025-01-13 16:19:11.058'), + ('BITCOIN',0,'Bitcoin','2025-01-13 16:19:11.059'), + ('BITCOIN_CASH',145,'Bitcoin Cash','2025-01-13 16:19:11.061'), + ('BITCOIN_CASH_TESTNET',1,'Bitcoin Cash Testnet','2025-01-13 16:19:11.062'), + ('BITCOIN_SIGNET',1,'Bitcoin Signet','2025-01-13 16:19:11.063'), + ('BITCOIN_SV',236,'BitcoinSV','2025-01-13 16:19:11.064'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('BITCOIN_SV_TESTNET',1,'BitcoinSV Testnet','2025-01-13 16:19:11.066'), + ('BITCOIN_TESTNET',1,'Bitcoin Testnet','2025-01-13 16:19:11.067'), + ('CARDANO',1815,'Cardano','2025-01-13 16:19:11.068'), + ('CARDANO_TESTNET',1,'Cardano Testnet','2025-01-13 16:19:11.070'), + ('CELESTIA',NULL,'Celestia','2025-01-13 16:19:11.071'), + ('CELO',52752,'Celo','2025-01-13 16:19:11.072'), + ('CELO_ALFAJORES',NULL,'Celo Alfajores','2025-01-13 16:19:11.073'), + ('CELO_BAKLAVA',1,'Celo Baklava','2025-01-13 16:19:11.075'), + ('CHILIZ',NULL,'Chiliz','2025-01-13 16:19:11.076'), + ('DABACUS',521,'Dabacus','2025-01-13 16:19:11.077'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('DOGECOIN',3,'Dogecoin','2025-01-13 16:19:11.079'), + ('DOGECOIN_TESTNET',1,'Dogecoin Testnet','2025-01-13 16:19:11.080'), + ('DYDX_CHAIN',NULL,'Dydx Chain','2025-01-13 16:19:11.081'), + ('DYDX_CHAIN_TESTNET',1,'Dydx Testnet','2025-01-13 16:19:11.083'), + ('ETHEREUM',60,'Ethereum','2025-01-13 16:19:11.084'), + ('ETHEREUM_HOLESKY',1,'Ethereum Holešky','2025-01-13 16:19:11.085'), + ('ETHEREUM_SEPOLIA',1,'Ethereum Sepolia','2025-01-13 16:19:11.086'), + ('EVMOS',NULL,'Evmos','2025-01-13 16:19:11.088'), + ('EVMOS_TESTNET',1,'Evmos Testnet','2025-01-13 16:19:11.089'), + ('FILECOIN',461,'Filecoin','2025-01-13 16:19:11.090'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('FLOW_TESTNET',1,'Flow Testnet','2025-01-13 16:19:11.091'), + ('LITECOIN',2,'Litecoin','2025-01-13 16:19:11.093'), + ('LITECOIN_TESTNET',1,'Litecoin Testnet','2025-01-13 16:19:11.094'), + ('NEUTRON',NULL,'Neutron','2025-01-13 16:19:11.095'), + ('OASIS',474,'Oasis','2025-01-13 16:19:11.097'), + ('OM_MANTRA',NULL,'OM Mantra','2025-01-13 16:19:11.098'), + ('OM_MANTRA_TESTNET',1,'OM Mantra Testnet','2025-01-13 16:19:11.099'), + ('OSMOSIS',10000118,'Osmosis','2025-01-13 16:19:11.101'), + ('PLUME_SEPOLIA',1,'Plume Sepolia Testnet','2025-01-13 16:19:11.102'), + ('POLYGON',966,'Polygon','2025-01-13 16:19:11.103'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('PROVENANCE',505,'Provenance','2025-01-13 16:19:11.104'), + ('RARIMO',NULL,'Rarimo','2025-01-13 16:19:11.106'), + ('RIPPLE',144,'Ripple','2025-01-13 16:19:11.107'), + ('RIPPLE_TESTNET',1,'Ripple Testnet','2025-01-13 16:19:11.108'), + ('SEI',19000118,'Sei','2025-01-13 16:19:11.110'), + ('SEI_TESTNET',1,'Sei Testnet','2025-01-13 16:19:11.111'), + ('SOLANA',501,'Solana','2025-01-13 16:19:11.112'), + ('SOLANA_TESTNET',1,'Solana Testnet','2025-01-13 16:19:11.114'), + ('STARKNET',9004,'Starknet','2025-01-13 16:19:11.115'), + ('STARKNET_TESTNET',1,'Starknet Testnet','2025-01-13 16:19:11.116'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('STELLAR_LUMENS',148,'Stellar Lumens','2025-01-13 16:19:11.117'), + ('STELLAR_LUMENS_TESTNET',1,'Stellar Lumens Testnet','2025-01-13 16:19:11.119'), + ('STRIDE',NULL,'Stride','2025-01-13 16:19:11.120'), + ('SUI_TESTNET',1,'Sui Testnet','2025-01-13 16:19:11.121'), + ('TRON',195,'Tron','2025-01-13 16:19:11.122'), + ('TRON_TESTNET',1,'Tron Testnet','2025-01-13 16:19:11.123'), + ('VANA',NULL,'Vana','2025-01-13 16:19:11.124'), + ('VANA_MOKSHA_TESTNET',1,'Vana Moksha Testnet','2025-01-13 16:19:11.126'), + ('ZKSYNC_SEPOLIA',1,'ZKsync Sepolia Testnet','2025-01-13 16:19:11.127'), + ('POLKADOT',354,'Polkadot','2025-01-13 16:19:11.128'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('EOS',194,'EOS','2025-01-13 16:19:11.129'), + ('EOS_TESTNET',1,'EOS Testnet','2025-01-13 16:19:11.131'), + ('OASYS',685,'Oasys','2025-01-13 16:19:11.132'), + ('OASYS_TESTNET',1,'Oasys Testnet','2025-01-13 16:19:11.133'), + ('OSMOSIS_TESTNET',1,'Osmosis Testnet','2025-01-13 16:19:11.134'), + ('TELOS',424,'Telos','2025-01-13 16:19:11.136'), + ('TELOS_TESTNET',1,'Telos Testnet','2025-01-13 16:19:11.137'), + ('TEZOS',1729,'Tezos','2025-01-13 16:19:11.138'), + ('TEZOS_TESTNET',1,'Tezos Testnet','2025-01-13 16:19:11.139'), + ('DASH',5,'Dash','2025-01-13 16:19:11.140'); +INSERT INTO public.network (id,coin_type,name,created_at) VALUES + ('DASH_TESTNET',1,'Dash Testnet','2025-01-13 16:19:11.141'), + ('OPTIMISM',614,'Optimism','2025-01-13 16:19:11.143'), + ('OPTIMISM_SEPOLIA',1,'Optimism Sepolia','2025-01-13 16:19:11.144'), + ('OPTIMISM_KOVAN',1,'Optimism Kovan','2025-01-13 16:19:11.145'); + +-- InsertProviderExternalNetwork +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('ETH-AETH','AETH','fireblocks','2025-01-13 16:19:11.019'), + ('AEVO','AEVO','fireblocks','2025-01-13 16:19:11.024'), + ('BLD','AGORIC','anchorage','2025-01-13 16:19:11.026'), + ('ALEPH_ZERO_EVM','ALEPH_ZERO','fireblocks','2025-01-13 16:19:11.027'), + ('ALGO','ALGORAND','fireblocks','2025-01-13 16:19:11.029'), + ('ALGO_TEST','ALGORAND_TESTNET','fireblocks','2025-01-13 16:19:11.031'), + ('ALLO','ALLORA','anchorage','2025-01-13 16:19:11.032'), + ('ALLO_T','ALLORA_TESTNET','anchorage','2025-01-13 16:19:11.034'), + ('APT','APTOS','anchorage','2025-01-13 16:19:11.036'), + ('APT_T','APTOS_TESTNET','anchorage','2025-01-13 16:19:11.037'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('ARBITRUM_SEPOLIA','ARBITRUM_SEPOLIA','anchorage','2025-01-13 16:19:11.039'), + ('ASTR_ASTR','ASTAR','fireblocks','2025-01-13 16:19:11.040'), + ('ASTR_TEST','ASTAR_TESTNET','fireblocks','2025-01-13 16:19:11.042'), + ('COSMOS','ATOM','anchorage','2025-01-13 16:19:11.043'), + ('ATOM_COS','ATOM','fireblocks','2025-01-13 16:19:11.043'), + ('ATOM_COS_TEST','ATOM_TESTNET','fireblocks','2025-01-13 16:19:11.045'), + ('AURORA_DEV','AURORA','fireblocks','2025-01-13 16:19:11.046'), + ('AVAX','AVAX','fireblocks','2025-01-13 16:19:11.047'), + ('AVAXTEST','AVAX_TESTNET','fireblocks','2025-01-13 16:19:11.049'), + ('AXL','AXELAR','anchorage','2025-01-13 16:19:11.050'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('AXL_T','AXELAR_TESTNET','anchorage','2025-01-13 16:19:11.052'), + ('BBN','BABYLON','anchorage','2025-01-13 16:19:11.053'), + ('BASECHAIN_ETH','BASE','fireblocks','2025-01-13 16:19:11.054'), + ('BASECHAIN_ETH_TEST5','BASE_TESTNET','fireblocks','2025-01-13 16:19:11.056'), + ('BNB_BSC','BINANCE_SMART_CHAIN','fireblocks','2025-01-13 16:19:11.057'), + ('BNB_TEST','BINANCE_SMART_CHAIN_TESTNET','fireblocks','2025-01-13 16:19:11.058'), + ('BTC','BITCOIN','anchorage','2025-01-13 16:19:11.059'), + ('BTC','BITCOIN','fireblocks','2025-01-13 16:19:11.059'), + ('BCH','BITCOIN_CASH','anchorage','2025-01-13 16:19:11.061'), + ('BCH','BITCOIN_CASH','fireblocks','2025-01-13 16:19:11.061'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('BCH_TEST','BITCOIN_CASH_TESTNET','fireblocks','2025-01-13 16:19:11.062'), + ('BTC_S','BITCOIN_SIGNET','anchorage','2025-01-13 16:19:11.063'), + ('BSV','BITCOIN_SV','fireblocks','2025-01-13 16:19:11.064'), + ('BSV_TEST','BITCOIN_SV_TESTNET','fireblocks','2025-01-13 16:19:11.066'), + ('BTC_TEST','BITCOIN_TESTNET','fireblocks','2025-01-13 16:19:11.067'), + ('ADA','CARDANO','fireblocks','2025-01-13 16:19:11.068'), + ('ADA_TEST','CARDANO_TESTNET','fireblocks','2025-01-13 16:19:11.070'), + ('TIA','CELESTIA','anchorage','2025-01-13 16:19:11.071'), + ('CELO','CELO','fireblocks','2025-01-13 16:19:11.072'), + ('CELO_ALF','CELO_ALFAJORES','fireblocks','2025-01-13 16:19:11.073'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('CGLD_TB','CELO_BAKLAVA','anchorage','2025-01-13 16:19:11.075'), + ('CELO_BAK','CELO_BAKLAVA','fireblocks','2025-01-13 16:19:11.075'), + ('CHZ_$CHZ','CHILIZ','fireblocks','2025-01-13 16:19:11.076'), + ('ABA','DABACUS','fireblocks','2025-01-13 16:19:11.077'), + ('DOGE','DOGECOIN','anchorage','2025-01-13 16:19:11.079'), + ('DOGE','DOGECOIN','fireblocks','2025-01-13 16:19:11.079'), + ('DOGE_TEST','DOGECOIN_TESTNET','fireblocks','2025-01-13 16:19:11.080'), + ('DYDX_CHAIN','DYDX_CHAIN','anchorage','2025-01-13 16:19:11.081'), + ('DYDX_CHAIN_T','DYDX_CHAIN_TESTNET','anchorage','2025-01-13 16:19:11.083'), + ('ETH','ETHEREUM','anchorage','2025-01-13 16:19:11.084'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('ETH','ETHEREUM','fireblocks','2025-01-13 16:19:11.084'), + ('ETHHOL','ETHEREUM_HOLESKY','anchorage','2025-01-13 16:19:11.085'), + ('ETHSEP','ETHEREUM_SEPOLIA','anchorage','2025-01-13 16:19:11.086'), + ('EVMOS','EVMOS','anchorage','2025-01-13 16:19:11.088'), + ('EVMOS_T','EVMOS_TESTNET','anchorage','2025-01-13 16:19:11.089'), + ('FIL','FILECOIN','anchorage','2025-01-13 16:19:11.090'), + ('FLOW_T','FLOW_TESTNET','anchorage','2025-01-13 16:19:11.091'), + ('LTC','LITECOIN','anchorage','2025-01-13 16:19:11.093'), + ('LTC','LITECOIN','fireblocks','2025-01-13 16:19:11.093'), + ('LTC_TEST','LITECOIN_TESTNET','fireblocks','2025-01-13 16:19:11.094'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('NTRN','NEUTRON','anchorage','2025-01-13 16:19:11.095'), + ('OAC','OASIS','anchorage','2025-01-13 16:19:11.097'), + ('OM_MANTRA','OM_MANTRA','anchorage','2025-01-13 16:19:11.098'), + ('OM_MANTRA_T','OM_MANTRA_TESTNET','anchorage','2025-01-13 16:19:11.099'), + ('OSMO','OSMOSIS','anchorage','2025-01-13 16:19:11.101'), + ('OSMO','OSMOSIS','fireblocks','2025-01-13 16:19:11.101'), + ('PLUME_SEPOLIA','PLUME_SEPOLIA','anchorage','2025-01-13 16:19:11.102'), + ('POLYGON','POLYGON','anchorage','2025-01-13 16:19:11.103'), + ('MATIC_POLYGON','POLYGON','fireblocks','2025-01-13 16:19:11.103'), + ('HASH','PROVENANCE','anchorage','2025-01-13 16:19:11.104'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('RMO','RARIMO','anchorage','2025-01-13 16:19:11.106'), + ('XRP','RIPPLE','anchorage','2025-01-13 16:19:11.107'), + ('XRP','RIPPLE','fireblocks','2025-01-13 16:19:11.107'), + ('XRP_TEST','RIPPLE_TESTNET','fireblocks','2025-01-13 16:19:11.108'), + ('SEI','SEI','anchorage','2025-01-13 16:19:11.110'), + ('SEI','SEI','fireblocks','2025-01-13 16:19:11.110'), + ('SEI_T','SEI_TESTNET','anchorage','2025-01-13 16:19:11.111'), + ('SEI_TEST','SEI_TESTNET','fireblocks','2025-01-13 16:19:11.111'), + ('SOL','SOLANA','fireblocks','2025-01-13 16:19:11.112'), + ('SOL_TD','SOLANA_TESTNET','anchorage','2025-01-13 16:19:11.114'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('SOL_TEST','SOLANA_TESTNET','fireblocks','2025-01-13 16:19:11.114'), + ('STARK_STARKNET','STARKNET','anchorage','2025-01-13 16:19:11.115'), + ('STRK_STARKNET_T','STARKNET_TESTNET','anchorage','2025-01-13 16:19:11.116'), + ('XLM','STELLAR_LUMENS','fireblocks','2025-01-13 16:19:11.117'), + ('XLM_TEST','STELLAR_LUMENS_TESTNET','fireblocks','2025-01-13 16:19:11.119'), + ('STRD','STRIDE','anchorage','2025-01-13 16:19:11.120'), + ('SUI_T','SUI_TESTNET','anchorage','2025-01-13 16:19:11.121'), + ('TRX','TRON','fireblocks','2025-01-13 16:19:11.122'), + ('TRX_TEST','TRON_TESTNET','fireblocks','2025-01-13 16:19:11.123'), + ('VANA','VANA','anchorage','2025-01-13 16:19:11.124'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('VANA_MOKSHA_TESTNET','VANA_MOKSHA_TESTNET','anchorage','2025-01-13 16:19:11.126'), + ('ZKSYNC_SEPOLIA','ZKSYNC_SEPOLIA','anchorage','2025-01-13 16:19:11.127'), + ('DOT','POLKADOT','fireblocks','2025-01-13 16:19:11.128'), + ('EOS','EOS','fireblocks','2025-01-13 16:19:11.129'), + ('EOS_TEST','EOS_TESTNET','fireblocks','2025-01-13 16:19:11.131'), + ('OAS','OASYS','fireblocks','2025-01-13 16:19:11.132'), + ('OAS_TEST','OASYS_TESTNET','fireblocks','2025-01-13 16:19:11.133'), + ('OSMO_TEST','OSMOSIS_TESTNET','fireblocks','2025-01-13 16:19:11.134'), + ('TELOS','TELOS','fireblocks','2025-01-13 16:19:11.136'), + ('TELOS_TEST','TELOS_TESTNET','fireblocks','2025-01-13 16:19:11.137'); +INSERT INTO public.provider_network (external_id,network_id,provider,created_at) VALUES + ('XTZ','TEZOS','fireblocks','2025-01-13 16:19:11.138'), + ('XTZ_TEST','TEZOS_TESTNET','fireblocks','2025-01-13 16:19:11.139'), + ('DASH','DASH','fireblocks','2025-01-13 16:19:11.140'), + ('DASH_TEST','DASH_TESTNET','fireblocks','2025-01-13 16:19:11.141'), + ('ETH-OPT','OPTIMISM','fireblocks','2025-01-13 16:19:11.143'), + ('ETH-OPT_SEPOLIA','OPTIMISM_SEPOLIA','fireblocks','2025-01-13 16:19:11.144'), + ('ETH-OPT_KOV','OPTIMISM_KOVAN','fireblocks','2025-01-13 16:19:11.145'); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250116130521_add_provider_sync_table/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250116130521_add_provider_sync_table/migration.sql new file mode 100644 index 000000000..572723484 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250116130521_add_provider_sync_table/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "provider_scoped_sync" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "connection_id" TEXT NOT NULL, + "status" TEXT NOT NULL, + "error_name" TEXT, + "error_message" TEXT, + "error_trace_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completed_at" TIMESTAMP(3), + + CONSTRAINT "provider_scoped_sync_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "provider_scoped_sync" ADD CONSTRAINT "provider_scoped_sync_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250117120539_provider_data_per_connection/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250117120539_provider_data_per_connection/migration.sql new file mode 100644 index 000000000..47165a77b --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250117120539_provider_data_per_connection/migration.sql @@ -0,0 +1,453 @@ +BEGIN; + +-- 1. Add new columns (nullable first) +ALTER TABLE provider_wallet ADD COLUMN connection_id TEXT; +ALTER TABLE provider_account ADD COLUMN connection_id TEXT; +ALTER TABLE provider_address ADD COLUMN connection_id TEXT; +ALTER TABLE provider_known_destination ADD COLUMN connection_id TEXT; +ALTER TABLE provider_transfer ADD COLUMN connection_id TEXT; + + +-- 2. Drop indexes per client_id +DROP INDEX "provider_account_client_id_external_id_key"; +DROP INDEX "provider_address_client_id_external_id_key"; +DROP INDEX "provider_known_destination_client_id_external_id_key"; +DROP INDEX "provider_wallet_client_id_external_id_key"; + + +-- 3. Create temporary tables to store ID mappings +CREATE TEMP TABLE wallet_id_mapping ( + old_id TEXT, + new_id TEXT, + client_id TEXT, + external_id TEXT, + connection_id TEXT +); + +CREATE TEMP TABLE account_id_mapping ( + old_id TEXT, + new_id TEXT, + client_id TEXT, + external_id TEXT, + connection_id TEXT +); + +CREATE TEMP TABLE address_id_mapping ( + old_id TEXT, + new_id TEXT, + client_id TEXT, + external_id TEXT, + connection_id TEXT +); + +CREATE TEMP TABLE known_destination_id_mapping ( + old_id TEXT, + new_id TEXT, + client_id TEXT, + external_id TEXT, + connection_id TEXT +); + +-- 4. For wallets with mapping +WITH inserted_wallets AS ( + INSERT INTO provider_wallet ( + id, + label, + client_id, + external_id, + provider, + created_at, + updated_at, + connection_id + ) + SELECT + gen_random_uuid(), + w.label, + w.client_id, + w.external_id, + w.provider, + w.created_at, + w.updated_at, + pwc.connection_id + FROM provider_wallet w + JOIN provider_wallet_connection pwc ON w.id = pwc.wallet_id + WHERE NOT EXISTS ( + SELECT 1 + FROM provider_wallet w2 + WHERE w2.client_id = w.client_id + AND w2.external_id = w.external_id + AND w2.connection_id = pwc.connection_id + ) + RETURNING id as new_id, client_id, external_id, connection_id +) +INSERT INTO wallet_id_mapping (old_id, new_id, client_id, external_id, connection_id) +SELECT w.id as old_id, i.new_id, i.client_id, i.external_id, i.connection_id +FROM provider_wallet w +JOIN provider_wallet_connection pwc ON w.id = pwc.wallet_id +JOIN inserted_wallets i ON w.client_id = i.client_id + AND w.external_id = i.external_id + AND pwc.connection_id = i.connection_id; + +-- For known destinations with mapping +WITH inserted_known_destinations AS ( + INSERT INTO provider_known_destination ( + id, + client_id, + external_id, + external_classification, + address, + label, + asset_id, + network_id, + created_at, + updated_at, + provider, + connection_id + ) + SELECT + gen_random_uuid(), + kd.client_id, + kd.external_id, + kd.external_classification, + kd.address, + kd.label, + kd.asset_id, + kd.network_id, + kd.created_at, + kd.updated_at, + kd.provider, + kdc.connection_id + FROM provider_known_destination kd + JOIN provider_known_destination_connection kdc ON kd.id = kdc.known_destination_id + WHERE NOT EXISTS ( + SELECT 1 + FROM provider_known_destination kd2 + WHERE kd2.client_id = kd.client_id + AND kd2.external_id = kd.external_id + AND kd2.connection_id = kdc.connection_id + ) + RETURNING id as new_id, client_id, external_id, connection_id +) +INSERT INTO known_destination_id_mapping (old_id, new_id, client_id, external_id, connection_id) +SELECT kd.id as old_id, i.new_id, i.client_id, i.external_id, i.connection_id +FROM provider_known_destination kd +JOIN provider_known_destination_connection kdc ON kd.id = kdc.known_destination_id +JOIN inserted_known_destinations i ON kd.client_id = i.client_id + AND kd.external_id = i.external_id + AND kdc.connection_id = i.connection_id; + +-- For accounts with mapping +WITH inserted_accounts AS ( + INSERT INTO provider_account ( + id, + label, + client_id, + provider, + external_id, + wallet_id, + network_id, + created_at, + updated_at, + connection_id + ) + SELECT + gen_random_uuid(), + a.label, + a.client_id, + a.provider, + a.external_id, + w.id, + a.network_id, + a.created_at, + a.updated_at, + w.connection_id + FROM provider_account a + JOIN provider_wallet w_old ON a.wallet_id = w_old.id + JOIN provider_wallet w ON w.client_id = w_old.client_id + AND w.external_id = w_old.external_id + WHERE NOT EXISTS ( + SELECT 1 + FROM provider_account a2 + WHERE a2.client_id = a.client_id + AND a2.external_id = a.external_id + AND a2.connection_id = w.connection_id + ) + RETURNING id as new_id, client_id, external_id, connection_id +) +INSERT INTO account_id_mapping (old_id, new_id, client_id, external_id, connection_id) +SELECT a.id as old_id, i.new_id, i.client_id, i.external_id, i.connection_id +FROM provider_account a +JOIN provider_wallet w_old ON a.wallet_id = w_old.id +JOIN provider_wallet w ON w.client_id = w_old.client_id + AND w.external_id = w_old.external_id +JOIN inserted_accounts i ON a.client_id = i.client_id + AND a.external_id = i.external_id + AND w.connection_id = i.connection_id; + +-- For addresses with mapping +WITH inserted_addresses AS ( + INSERT INTO provider_address ( + id, + client_id, + provider, + external_id, + account_id, + address, + created_at, + updated_at, + connection_id + ) + SELECT + gen_random_uuid(), + addr.client_id, + addr.provider, + addr.external_id, + a.id, + addr.address, + addr.created_at, + addr.updated_at, + a.connection_id + FROM provider_address addr + JOIN provider_account a_old ON addr.account_id = a_old.id + JOIN provider_account a ON a.client_id = a_old.client_id + AND a.external_id = a_old.external_id + WHERE NOT EXISTS ( + SELECT 1 + FROM provider_address addr2 + WHERE addr2.client_id = addr.client_id + AND addr2.external_id = addr.external_id + AND addr2.connection_id = a.connection_id + ) + RETURNING id as new_id, client_id, external_id, connection_id +) +INSERT INTO address_id_mapping (old_id, new_id, client_id, external_id, connection_id) +SELECT addr.id as old_id, i.new_id, i.client_id, i.external_id, i.connection_id +FROM provider_address addr +JOIN provider_account a_old ON addr.account_id = a_old.id +JOIN provider_account a ON a.client_id = a_old.client_id + AND a.external_id = a_old.external_id +JOIN inserted_addresses i ON addr.client_id = i.client_id + AND addr.external_id = i.external_id + AND a.connection_id = i.connection_id; + + +DO $$ +DECLARE + wallet_count INTEGER; + account_count INTEGER; + address_count INTEGER; + transfer_count INTEGER; + sample_mapping RECORD; +BEGIN + -- Check mapping tables + SELECT COUNT(*) INTO wallet_count FROM wallet_id_mapping; + SELECT COUNT(*) INTO account_count FROM account_id_mapping; + SELECT COUNT(*) INTO address_count FROM address_id_mapping; + SELECT COUNT(*) INTO transfer_count FROM provider_transfer; + + RAISE NOTICE 'Mapping table counts: wallets=%, accounts=%, addresses=%, transfers=%', + wallet_count, account_count, address_count, transfer_count; + + -- Check a sample mapping + SELECT * INTO sample_mapping + FROM ( + SELECT + t.id as old_transfer_id, + t.source_wallet_id as old_source_wallet, + sw_map.new_id as mapped_source_wallet, + t.destination_wallet_id as old_dest_wallet, + dw_map.new_id as mapped_dest_wallet + FROM provider_transfer t + LEFT JOIN wallet_id_mapping sw_map ON t.source_wallet_id = sw_map.old_id + LEFT JOIN wallet_id_mapping dw_map ON t.destination_wallet_id = dw_map.old_id + LIMIT 1 + ) subquery; + + IF sample_mapping IS NOT NULL THEN + RAISE NOTICE 'Sample mapping: %', sample_mapping; + ELSE + RAISE NOTICE 'No sample mapping found'; + END IF; + + -- Check for NULL required fields + PERFORM t.id + FROM provider_transfer t + WHERE client_id IS NULL + OR external_id IS NULL + OR asset_id IS NULL + OR network_fee_attribution IS NULL + OR provider IS NULL; + + IF FOUND THEN + RAISE NOTICE 'Found transfers with NULL required fields'; + ELSE + RAISE NOTICE 'No NULL required fields found'; + END IF; +END $$; + + +-- For transfers (using mappings) +WITH transfer_conn AS ( + SELECT + t.id AS transfer_id, + + -- Get whichever connection_id is available + COALESCE( + sw_map.connection_id, + sa_map.connection_id, + saddr_map.connection_id + ) AS new_connection_id, + + -- “New” source fields from the mapping tables + sw_map.new_id AS new_source_wallet_id, + sa_map.new_id AS new_source_account_id, + saddr_map.new_id AS new_source_address_id + + FROM provider_transfer t + LEFT JOIN wallet_id_mapping sw_map ON t.source_wallet_id = sw_map.old_id + LEFT JOIN account_id_mapping sa_map ON t.source_account_id = sa_map.old_id + LEFT JOIN address_id_mapping saddr_map ON t.source_address_id = saddr_map.old_id +) +UPDATE provider_transfer t +SET + -- If we found a mapping for wallet/account/address, use that new ID; + -- otherwise, keep the existing source_* field. + source_wallet_id = COALESCE(transfer_conn.new_source_wallet_id, t.source_wallet_id), + source_account_id = COALESCE(transfer_conn.new_source_account_id, t.source_account_id), + source_address_id = COALESCE(transfer_conn.new_source_address_id, t.source_address_id), + + -- Overwrite connection_id with whichever was found + connection_id = COALESCE(transfer_conn.new_connection_id, t.connection_id) +FROM transfer_conn +WHERE t.id = transfer_conn.transfer_id; + +DO $$ +DECLARE + old_wallet_count INTEGER; + new_wallet_count INTEGER; + expected_wallet_count INTEGER; +BEGIN + -- Count old wallets + SELECT COUNT(*) INTO old_wallet_count + FROM provider_wallet w + WHERE w.connection_id IS NULL; + + -- Count new wallets + SELECT COUNT(*) INTO new_wallet_count + FROM provider_wallet w + WHERE w.connection_id IS NOT NULL; + + -- Count expected wallets (from wallet connections) + SELECT COUNT(*) INTO expected_wallet_count + FROM provider_wallet_connection; + + -- Verify we have the expected number of new records + IF new_wallet_count != expected_wallet_count THEN + RAISE EXCEPTION 'Migration verification failed: got % new wallets, expected %', + new_wallet_count, expected_wallet_count; + END IF; + + RAISE NOTICE 'Verification passed: % old wallets, % new wallets created', + old_wallet_count, new_wallet_count; +END $$; + +-- 5. Delete old records after successful migration +DELETE FROM provider_transfer WHERE connection_id IS NULL; +DELETE FROM provider_address WHERE connection_id IS NULL; +DELETE FROM provider_account WHERE connection_id IS NULL; +DELETE FROM provider_known_destination WHERE connection_id IS NULL; +DELETE FROM provider_wallet WHERE connection_id IS NULL; + +-- 6. Verify migrations completed +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM provider_wallet WHERE connection_id IS NULL) THEN + RAISE EXCEPTION 'Data migration incomplete: found wallet(s) without connection_id'; + END IF; + IF EXISTS (SELECT 1 FROM provider_known_destination WHERE connection_id IS NULL) THEN + RAISE EXCEPTION 'Data migration incomplete: found known destination(s) without connection_id'; + END IF; + IF EXISTS (SELECT 1 FROM provider_account WHERE connection_id IS NULL) THEN + RAISE EXCEPTION 'Data migration incomplete: found account(s) without connection_id'; + END IF; + IF EXISTS (SELECT 1 FROM provider_address WHERE connection_id IS NULL) THEN + RAISE EXCEPTION 'Data migration incomplete: found address(es) without connection_id'; + END IF; + IF EXISTS (SELECT 1 FROM provider_transfer WHERE connection_id IS NULL) THEN + RAISE EXCEPTION 'Data migration incomplete: found transfer(s) without connection_id'; + END IF; +END $$; + +-- -- 7. Make columns NOT NULL +ALTER TABLE provider_wallet ALTER COLUMN connection_id SET NOT NULL; +ALTER TABLE provider_account ALTER COLUMN connection_id SET NOT NULL; +ALTER TABLE provider_address ALTER COLUMN connection_id SET NOT NULL; +ALTER TABLE provider_known_destination ALTER COLUMN connection_id SET NOT NULL; +ALTER TABLE provider_transfer ALTER COLUMN connection_id SET NOT NULL; + +-- 8. Check for potential violations of new unique constraints +DO $$ +BEGIN + -- Check wallets + IF EXISTS ( + SELECT client_id, external_id, connection_id, COUNT(*) + FROM provider_wallet + GROUP BY client_id, external_id, connection_id + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Found duplicate wallet entries that would violate new unique constraint'; + END IF; + + -- Check accounts + IF EXISTS ( + SELECT client_id, external_id, connection_id, COUNT(*) + FROM provider_account + GROUP BY client_id, external_id, connection_id + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Found duplicate account entries that would violate new unique constraint'; + END IF; + + -- Check addresses + IF EXISTS ( + SELECT client_id, external_id, connection_id, COUNT(*) + FROM provider_address + GROUP BY client_id, external_id, connection_id + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Found duplicate address entries that would violate new unique constraint'; + END IF; + + -- Check known destinations + IF EXISTS ( + SELECT client_id, external_id, connection_id, COUNT(*) + FROM provider_known_destination + GROUP BY client_id, external_id, connection_id + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Found duplicate known destination entries that would violate new unique constraint'; + END IF; +END $$; + +-- 9. Create new unique indexes +CREATE UNIQUE INDEX "provider_account_client_id_connection_id_external_id_key" + ON "provider_account"("client_id", "connection_id", "external_id"); +CREATE UNIQUE INDEX "provider_address_client_id_connection_id_external_id_key" + ON "provider_address"("client_id", "connection_id", "external_id"); +CREATE UNIQUE INDEX "provider_known_destination_client_id_connection_id_external_key" + ON "provider_known_destination"("client_id", "connection_id", "external_id"); +CREATE UNIQUE INDEX "provider_wallet_client_id_connection_id_external_id_key" + ON "provider_wallet"("client_id", "connection_id", "external_id"); + +-- 10. Drop old tables +DROP TABLE provider_wallet_connection; +DROP TABLE provider_known_destination_connection; + +-- 10. Add foreign key constraints +ALTER TABLE "provider_wallet" ADD CONSTRAINT "provider_wallet_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "provider_account" ADD CONSTRAINT "provider_account_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "provider_address" ADD CONSTRAINT "provider_address_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "provider_known_destination" ADD CONSTRAINT "provider_known_destination_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "provider_transfer" ADD CONSTRAINT "provider_transfer_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "provider_connection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +COMMIT; \ No newline at end of file diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250120095438_add_asset_table_and_data/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250120095438_add_asset_table_and_data/migration.sql new file mode 100644 index 000000000..d6b70a122 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250120095438_add_asset_table_and_data/migration.sql @@ -0,0 +1,135 @@ +-- CreateTable +CREATE TABLE "asset" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "symbol" TEXT, + "decimals" INTEGER, + "network_id" TEXT NOT NULL, + "onchain_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "asset_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "provider_asset" ( + "asset_id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "provider_asset_pkey" PRIMARY KEY ("provider","external_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "asset_network_id_onchain_id_key" ON "asset"("network_id", "onchain_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "provider_asset_provider_asset_id_key" ON "provider_asset"("provider", "asset_id"); + +-- AddForeignKey +ALTER TABLE "asset" ADD CONSTRAINT "asset_network_id_fkey" FOREIGN KEY ("network_id") REFERENCES "network"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "provider_asset" ADD CONSTRAINT "provider_asset_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Correct Ethereum Arbitrum external network and Solana Devnet data +INSERT INTO public.network (id,coin_type,name) VALUES + ('ARBITRUM',9001,'Arbitrum'), + ('SOLANA_DEVNET',1,'Solana Devnet'); + +DELETE FROM public.provider_network WHERE external_id = 'SOL_TD' AND network_id = 'SOLANA_TESTNET'; +DELETE FROM public.provider_network WHERE external_id = 'ETH-AETH'; + +INSERT INTO public.provider_network (external_id,network_id,provider) VALUES + ('SOL_TD','SOLANA_DEVNET','anchorage'), + ('ETH-AETH','ARBITRUM','fireblocks'); + +-- Insert initial asset data +INSERT INTO public.asset (id,"name",symbol,decimals,network_id,onchain_id) VALUES + ('1INCH','1inch','1INCH',18,'ETHEREUM','0x111111111117dc0aa78b770fa6a738034120c302'), + ('AAVE','Aave','AAVE',18,'ETHEREUM','0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9'), + ('ARB','Arbitrum','ARB',18,'ETHEREUM','0xb50721bcf8d664c30412cfbc6cf7a15145234ad1'), + ('ARB_ARB','Arbitrum','ARB',18,'ARBITRUM','0x912ce59144191c1204e64559fe8253a0e49e6548'), + ('ATOM','Cosmos','ATOM',6,'ATOM',NULL), + ('BTC','Bitcoin','BTC',8,'BITCOIN',NULL), + ('DOT','Polkadot','DOT',10,'POLKADOT',NULL), + ('ETH','Ethereum','ETH',18,'ETHEREUM',NULL), + ('ETH_ARB','Arbitrum Ethereum','ETH',18,'ARBITRUM',NULL), + ('ETH_ARBITRUM_TEST','Arbitrum Sepolia Testnet','ETH',18,'ARBITRUM_SEPOLIA',NULL), + ('ETH_OPT','Optimistic Ethereum','ETH',18,'OPTIMISM',NULL), + ('ETH_OPT_KOVAN','Optimistic Ethereum Kovan','ETH',18,'OPTIMISM_KOVAN',NULL), + ('ETH_OPT_SEPOLIA','Optimistic Ethereum Sepolia','ETH',18,'OPTIMISM_SEPOLIA',NULL), + ('ETH_ZKSYNC_TEST','ZKsync Sepolia Testnet','ETH',18,'ZKSYNC_SEPOLIA',NULL), + ('LTC','Litecoin','LTC',8,'LITECOIN',NULL), + ('MATIC','Matic Token','MATIC',18,'ETHEREUM','0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0'), + ('MORPHO','Morpho Token','MORPHO',18,'ETHEREUM','0x9994e35db50125e0df82e4c2dde62496ce330999'), + ('POL','Polygon Token','POL',18,'ETHEREUM','0x455e53cbb86018ac2b8092fdcd39d8444affc3f6'), + ('POL_POLYGON','Polygon','POL',18,'POLYGON',NULL), + ('PORTAL','PORTAL','PORTAL',18,'ETHEREUM','0x1bbe973bef3a977fc51cbed703e8ffdefe001fed'), + ('SOL','Solana','SOL',NULL,'SOLANA',NULL), + ('SOL_DEVNET','Solana Devnet','SOL',9,'SOLANA_DEVNET',NULL), + ('SOL_TEST','Solana Testnet','SOL',NULL,'SOLANA_TESTNET',NULL), + ('SUI_TEST','Sui Test','SUI',9,'SUI_TESTNET',NULL), + ('SUSHI','SushiSwap','SUSHI',18,'ETHEREUM','0x6b3595068778dd592e39a122f4f5a5cf09c90fe2'), + ('UNI','Uniswap','UNI',18,'ETHEREUM','0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'), + ('USDC','USD Coin','USDC',6,'ETHEREUM','0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'), + ('USDC_ARBITRUM','USD Coin','USDC',6,'ARBITRUM','0xaf88d065e77c8cc2239327c5edb3a432268e5831'), + ('USDC_POLYGON','USD Coin','USDC',6,'POLYGON','0x2791bca1f2de4661ed88a30c99a7a9449aa84174'), + ('USDT','Tether','USDT',6,'ETHEREUM','0xdac17f958d2ee523a2206206994597c13d831ec7'), + ('USDT_POLYGON','Tether','USDT',6,'POLYGON','0xc2132d05d31c914a87c6611c10748aeb04b58e8f'), + ('WBTC','Wrapped Bitcoin','WBTC',8,'ETHEREUM','0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'), + ('XRP','Ripple','XRP',6,'RIPPLE',NULL); + +-- InsertProviderAsset +INSERT INTO public.provider_asset (asset_id,provider,external_id) VALUES + ('1INCH','anchorage','1INCH'), + ('1INCH','fireblocks','1INCH'), + ('AAVE','anchorage','AAVE'), + ('AAVE','fireblocks','AAVE'), + ('ARB','anchorage','ARB'), + ('ARB_ARB','fireblocks','ARB_ARB_FRK9'), + ('ATOM','anchorage','ATOM'), + ('ATOM','fireblocks','ATOM_COS'), + ('BTC','anchorage','BTC'), + ('BTC','fireblocks','BTC'), + ('DOT','fireblocks','DOT'), + ('ETH','anchorage','ETH'), + ('ETH','fireblocks','ETH'), + ('ETH_ARB','fireblocks','ETH-AETH'), + ('ETH_ARBITRUM_TEST','anchorage','ETH_ARBITRUM_T'), + ('ETH_ARBITRUM_TEST','fireblocks','ETH-AETH_SEPOLIA'), + ('ETH_OPT','fireblocks','ETH-OPT'), + ('ETH_OPT_KOVAN','fireblocks','ETH-OPT_KOV'), + ('ETH_OPT_SEPOLIA','fireblocks','ETH-OPT_SEPOLIA'), + ('ETH_ZKSYNC_TEST','anchorage','ETH_ZKSYNC_T'), + ('ETH_ZKSYNC_TEST','fireblocks','ETH_ZKSYNC_ERA_SEPOLIA'), + ('LTC','anchorage','LTC'), + ('LTC','fireblocks','LTC'), + ('MATIC','anchorage','MATIC'), + ('MATIC','fireblocks','MATIC'), + ('MORPHO','anchorage','MORPHO'), + ('POL','anchorage','POL'), + ('POL','fireblocks','POL_ETH_9RYQ'), + ('POL_POLYGON','anchorage','POL_POLYGON'), + ('POL_POLYGON','fireblocks','MATIC_POLYGON'), + ('PORTAL','anchorage','PORTAL'), + ('SOL','fireblocks','SOL'), + ('SOL_DEVNET','anchorage','SOL_TD'), + ('SOL_TEST','fireblocks','SOL_TEST'), + ('SUI_TEST','anchorage','SUI_T'), + ('SUSHI','anchorage','SUSHI'), + ('SUSHI','fireblocks','SUSHI'), + ('UNI','anchorage','UNI'), + ('UNI','fireblocks','UNI'), + ('USDC','anchorage','USDC'), + ('USDC','fireblocks','USDC'), + ('USDC_ARBITRUM','fireblocks','USDC_ARB_3SBJ'), + ('USDC_POLYGON','fireblocks','USDC_POLYGON'), + ('USDT','anchorage','USDT'), + ('USDT','fireblocks','USDT_ERC20'), + ('USDT_POLYGON','fireblocks','USDT_POLYGON'), + ('WBTC','anchorage','WBTC'), + ('WBTC','fireblocks','WBTC'), + ('XRP','anchorage','XRP'), + ('XRP','fireblocks','XRP'); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250120095738_replace_network_provider_unique_key_by_id/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250120095738_replace_network_provider_unique_key_by_id/migration.sql new file mode 100644 index 000000000..5eda3166c --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250120095738_replace_network_provider_unique_key_by_id/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "provider_network_provider_external_id_key"; + +-- AlterTable +ALTER TABLE "provider_network" ADD CONSTRAINT "provider_network_pkey" PRIMARY KEY ("provider", "external_id"); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250120144703_add_btc_signet_to_assets/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250120144703_add_btc_signet_to_assets/migration.sql new file mode 100644 index 000000000..8daec65fa --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250120144703_add_btc_signet_to_assets/migration.sql @@ -0,0 +1,6 @@ +INSERT INTO public.asset (id,"name",symbol,decimals,network_id) VALUES + ('BTC_SIGNET','Bitcoin Signet','BTC',18,'BITCOIN_SIGNET'); + + INSERT INTO public.provider_asset (asset_id,provider,external_id) VALUES + ('BTC_SIGNET','anchorage','BTC_S'); + \ No newline at end of file diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250121131658_delete_provider_known_destination_table/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250121131658_delete_provider_known_destination_table/migration.sql new file mode 100644 index 000000000..a157d218a --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250121131658_delete_provider_known_destination_table/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the `provider_known_destination` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "provider_known_destination" DROP CONSTRAINT "provider_known_destination_connection_id_fkey"; + +-- DropTable +DROP TABLE "provider_known_destination"; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250122170506_add_asset_chainlink_token_zksync_sepolia/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250122170506_add_asset_chainlink_token_zksync_sepolia/migration.sql new file mode 100644 index 000000000..74fe359a1 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250122170506_add_asset_chainlink_token_zksync_sepolia/migration.sql @@ -0,0 +1,5 @@ +INSERT INTO public.asset (id,"name",symbol,decimals,network_id,onchain_id) VALUES + ('LINK_ZKSYNC_SEPOLIA','Chainlink','LINK',18,'ZKSYNC_SEPOLIA','0x23a1afd896c8c8876af46adc38521f4432658d1e'); + +INSERT INTO public.provider_asset (asset_id,provider,external_id) VALUES + ('LINK_ZKSYNC_SEPOLIA','anchorage','LINK_ZKSYNC_T'); diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250123092543_add_raw_accounts_to_scoped_sync/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250123092543_add_raw_accounts_to_scoped_sync/migration.sql new file mode 100644 index 000000000..4d0928e10 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250123092543_add_raw_accounts_to_scoped_sync/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `raw_accounts` to the `provider_scoped_sync` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "provider_scoped_sync" ADD COLUMN "raw_accounts" TEXT NOT NULL; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250124102305_add_transfer_external_asset_id/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250124102305_add_transfer_external_asset_id/migration.sql new file mode 100644 index 000000000..a64da4748 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250124102305_add_transfer_external_asset_id/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "provider_transfer" ADD COLUMN "asset_external_id" TEXT, +ALTER COLUMN "asset_id" DROP NOT NULL; diff --git a/apps/vault/src/shared/module/persistence/schema/migrations/20250124164857_store_failed_raw_accounts_requests/migration.sql b/apps/vault/src/shared/module/persistence/schema/migrations/20250124164857_store_failed_raw_accounts_requests/migration.sql new file mode 100644 index 000000000..2215327bc --- /dev/null +++ b/apps/vault/src/shared/module/persistence/schema/migrations/20250124164857_store_failed_raw_accounts_requests/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "provider_scoped_sync" ADD COLUMN "failed_raw_accounts" TEXT; diff --git a/apps/vault/src/shared/module/persistence/schema/schema.prisma b/apps/vault/src/shared/module/persistence/schema/schema.prisma index d9602a4b7..db19dee11 100644 --- a/apps/vault/src/shared/module/persistence/schema/schema.prisma +++ b/apps/vault/src/shared/module/persistence/schema/schema.prisma @@ -1,11 +1,11 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] // Output into a separate subdirectory so multiple schemas can be used in a // monorepo. // // Reference: https://github.com/nrwl/nx-recipes/tree/main/nestjs-prisma - output = "../../../../../../../node_modules/@prisma/client/vault" + output = "../../../../../../../node_modules/@prisma/client/vault" } datasource db { @@ -14,9 +14,15 @@ datasource db { } model Vault { - id String @id - masterKey String? @map("master_key") - adminApiKey String? @map("admin_api_key") + id String @id + // Encryption options, possibly set from Config file + encryptionKeyringType String @map("encryption_keyring_type") // raw | awskms + encryptionMasterKeySpelling String? @map("enncryption_master_key") /// @encrypted by masterPassword KEK @ignore + encryptionMasterKey String? @map("encryption_master_key") /// @encrypted by masterPassword KEK + encryptionMasterAwsKmsArn String? @map("encryption_master_aws_kms_arn") // only if type = awskms + // Auth Options, set from Config file + authDisabled Boolean? @map("auth_disabled") + adminApiKeyHash String? @map("admin_api_key_hash") /// hash, not plaintext @@map("vault") } @@ -24,10 +30,289 @@ model Vault { // TODO: (@wcalderipe, 12/03/23) use hstore extension for better performance. // See https://www.postgresql.org/docs/9.1/hstore.html model KeyValue { - key String @id - clientId String? @map("client_id") - collection String - value String + key String @id + clientId String? @map("client_id") + collection String + value String @@map("key_value") } + +model TransitEncryptionKey { + id String @id @default(cuid()) + clientId String @map("client_id") + privateKey String @map("private_key") // @encrypted stringified JSON + publicKey String @map("public_key") // stringified JSON + + createdAt DateTime @default(now()) + + @@map("transit_encryption_key") +} + +model Client { + clientId String @id @map("client_id") + name String @map("name") + configurationSource String @map("configuration_source") // declarative | dynamic + authDisabled Boolean @map("auth_disabled") + tokenValidationDisabled Boolean @map("token_validation_disabled") + backupPublicKey String? @map("backup_public_key") // Stringified JSON + baseUrl String? @map("base_url") // If you want to override the used for verifying jwsd/httpsig + + // JWT Token Validation + authorizationServerUrl String? @map("authorization_server_url") + authorizationIssuer String? @map("authorization_issuer") + authorizationAudience String? @map("authorization_audience") + authorizationMaxTokenAge Int? @map("authorization_max_token_age") + authorizationJwksUrl String? @map("authorization_jwks_url") + authorizationPinnedPublicKey String? @map("authorization_pinned_public_key") // Stringified JSON + authorizationRequireBoundTokens Boolean @map("authorization_require_bound_tokens") + authorizationAllowBearerTokens Boolean @map("authorization_allow_bearer_tokens") + authorizationAllowWildcards String? @map("authorization_allow_wildcards") + + // Local Authentication Methods + // Optionally restrict to specific user credentials from a JWKS endpoint + localAuthAllowedUsersJwksUrl String? @map("local_auth_allowed_users_jwks_url") + localAuthJwsdEnabled Boolean @map("local_auth_jwsd_enabled") // Do we allow JWSD? + jwsdMaxAge Int? @map("jwsd_max_age") + jwsdRequiredComponents String? @map("jwsd_required_components") // comma-separated list: htm,uri,ath,created + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + localAuthAllowedUsers ClientLocalAuthAllowedUser[] + + @@map("client") +} + +model ClientLocalAuthAllowedUser { + id String @id // clientId:userId, to be globally unique + userId String @map("user_id") + clientId String @map("client_id") + publicKey String @map("public_key") // Stringified JSON + client Client @relation(fields: [clientId], references: [clientId]) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("client_local_auth_allowed_user") +} + +model ProviderWallet { + id String @id + label String? + clientId String @map("client_id") + provider String + externalId String @map("external_id") + connectionId String @map("connection_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + connection ProviderConnection? @relation(fields: [connectionId], references: [id]) + accounts ProviderAccount[] + sourceTransfers ProviderTransfer[] @relation("SourceWallet") + destinationTransfers ProviderTransfer[] @relation("DestinationWallet") + + @@unique([clientId, connectionId, externalId]) + @@index([createdAt, clientId]) + @@map("provider_wallet") +} + +model ProviderAccount { + id String @id + label String? + clientId String @map("client_id") + connectionId String @map("connection_id") + externalId String @map("external_id") + provider String + walletId String? @map("wallet_id") + networkId String @map("network_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + connection ProviderConnection @relation(fields: [connectionId], references: [id]) + wallet ProviderWallet? @relation(fields: [walletId], references: [id]) + addresses ProviderAddress[] + + sourceTransfers ProviderTransfer[] @relation("SourceAccount") + destinationTransfers ProviderTransfer[] @relation("DestinationAccount") + + @@unique([clientId, connectionId, externalId]) + @@index([createdAt, clientId]) + @@map("provider_account") +} + +model ProviderAddress { + id String @id + clientId String @map("client_id") + connectionId String @map("connection_id") + provider String + externalId String @map("external_id") + accountId String @map("account_id") + address String + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + connection ProviderConnection @relation(fields: [connectionId], references: [id]) + account ProviderAccount @relation(fields: [accountId], references: [id]) + + sourceTransfers ProviderTransfer[] @relation("SourceAddress") + destinationTransfers ProviderTransfer[] @relation("DestinationAddress") + + @@unique([clientId, connectionId, externalId]) + @@index([createdAt, clientId]) + @@map("provider_address") +} + +model ProviderConnection { + id String @id + clientId String @map("client_id") + provider String + url String? + label String? + credentials String? // @encrypted stringified JSON + status String + integrity String? @map("_integrity") + // IMPORTANT: Don't default DateTime to `now()` on this table because the + // integrity column needs to hash all data at the application level. + createdAt DateTime @map("created_at") + updatedAt DateTime @map("updated_at") + revokedAt DateTime? @map("revoked_at") + + wallets ProviderWallet[] + scopedSyncs ProviderScopedSync[] + accounts ProviderAccount[] + addresses ProviderAddress[] + transfers ProviderTransfer[] + syncs ProviderSync[] + + @@index([createdAt, clientId]) + @@map("provider_connection") +} + +model ProviderSync { + id String @id + clientId String @map("client_id") + connectionId String @map("connection_id") + status String + errorName String? @map("error_name") + errorMessage String? @map("error_message") + errorTraceId String? @map("error_trace_id") + createdAt DateTime @default(now()) @map("created_at") + completedAt DateTime? @map("completed_at") + + connection ProviderConnection @relation(fields: [connectionId], references: [id]) + + @@map("provider_sync") +} + +model ProviderScopedSync { + id String @id + clientId String @map("client_id") + connectionId String @map("connection_id") + status String + rawAccounts String @map("raw_accounts") + + errorName String? @map("error_name") + errorMessage String? @map("error_message") + errorTraceId String? @map("error_trace_id") + createdAt DateTime @default(now()) @map("created_at") + completedAt DateTime? @map("completed_at") + + failedRawAccounts String? @map("failed_raw_accounts") + + connection ProviderConnection @relation(fields: [connectionId], references: [id]) + + @@map("provider_scoped_sync") +} + +model ProviderTransfer { + id String @id + assetId String? @map("asset_id") + assetExternalId String? @map("asset_external_id") + clientId String @map("client_id") + createdAt DateTime @default(now()) @map("created_at") + customerRefId String? @map("customer_ref_id") + destinationAccountId String? @map("destination_account_id") + destinationAddressId String? @map("destination_address_id") + destinationAddressRaw String? @map("destination_address_raw") + destinationWalletId String? @map("destination_wallet_id") + externalId String @map("external_id") + grossAmount String @map("gross_amount") + idempotenceId String? @map("idempotence_id") + memo String? + networkFeeAttribution String @map("network_fee_attribution") + provider String + providerSpecific String? @map("provider_specific") + sourceAccountId String? @map("source_account_id") + sourceAddressId String? @map("source_address_id") + sourceWalletId String? @map("source_wallet_id") + connectionId String @map("connection_id") + + connection ProviderConnection @relation(fields: [connectionId], references: [id]) + sourceWallet ProviderWallet? @relation("SourceWallet", fields: [sourceWalletId], references: [id]) + sourceAccount ProviderAccount? @relation("SourceAccount", fields: [sourceAccountId], references: [id]) + sourceAddress ProviderAddress? @relation("SourceAddress", fields: [sourceAddressId], references: [id]) + destinationWallet ProviderWallet? @relation("DestinationWallet", fields: [destinationWalletId], references: [id]) + destinationAccount ProviderAccount? @relation("DestinationAccount", fields: [destinationAccountId], references: [id]) + destinationAddress ProviderAddress? @relation("DestinationAddress", fields: [destinationAddressId], references: [id]) + + @@unique([clientId, externalId]) + @@unique([clientId, idempotenceId]) + @@index([createdAt, clientId]) + @@map("provider_transfer") +} + +model Network { + id String @id + coinType Int? @map("coin_type") + name String + createdAt DateTime @default(now()) @map("created_at") + + externalNetworks ProviderNetwork[] + assets Asset[] + + @@map("network") +} + +model ProviderNetwork { + externalId String @map("external_id") + networkId String @map("network_id") + provider String + createdAt DateTime @default(now()) @map("created_at") + + network Network? @relation(fields: [networkId], references: [id]) + + @@id([provider, externalId]) + // Prevent duplication of provider supported network. + @@unique([provider, networkId]) + @@map("provider_network") +} + +model Asset { + id String @id + name String + symbol String? + decimals Int? + networkId String @map("network_id") + onchainId String? @map("onchain_id") + createdAt DateTime @default(now()) @map("created_at") + + network Network @relation(fields: [networkId], references: [id]) + externalAssets ProviderAsset[] + + @@unique([networkId, onchainId]) + @@map("asset") +} + +model ProviderAsset { + assetId String @map("asset_id") + provider String + externalId String @map("external_id") + createdAt DateTime @default(now()) @map("created_at") + + asset Asset @relation(fields: [assetId], references: [id]) + + @@id([provider, externalId]) + @@unique([provider, assetId]) + @@map("provider_asset") +} diff --git a/apps/vault/src/shared/module/persistence/seed.ts b/apps/vault/src/shared/module/persistence/seed.ts index ae8615705..471008582 100644 --- a/apps/vault/src/shared/module/persistence/seed.ts +++ b/apps/vault/src/shared/module/persistence/seed.ts @@ -1,32 +1,37 @@ -/* eslint-disable */ import { LoggerService } from '@narval/nestjs-shared' -import { PrismaClient, Vault } from '@prisma/client/vault' +import { NestFactory } from '@nestjs/core' +import { PrismaClient } from '@prisma/client/vault' +import { MainModule } from '../../../main.module' +import { SeederService } from './service/seeder.service' const prisma = new PrismaClient() - -const vault: Vault = { - id: '7d704a62-d15e-4382-a826-1eb41563043b', - adminApiKey: 'admin-api-key-xxx', - masterKey: 'master-key-xxx' -} +const logger = new LoggerService() async function main() { - const logger = new LoggerService() + // Create a standalone application without any network listeners like + // controllers. + // + // See https://docs.nestjs.com/standalone-applications + const application = await NestFactory.createApplicationContext(MainModule) + application.useLogger(application.get(LoggerService)) + const seeder = application.get(SeederService) - logger.log('Seeding Vault database') - await prisma.$transaction(async (txn) => { - // await txn.vault.create({ data: vault }) - }) + logger.log('🌱 Seeding database') - logger.log('Vault database germinated 🌱') + try { + await seeder.seed() + } finally { + logger.log('✅ Database seeded') + await application.close() + } } main() .then(async () => { await prisma.$disconnect() }) - .catch(async (e) => { - console.error(e) + .catch(async (error) => { + logger.error('❌ Seed error', error) await prisma.$disconnect() process.exit(1) }) diff --git a/apps/vault/src/shared/module/persistence/service/prisma.service.ts b/apps/vault/src/shared/module/persistence/service/prisma.service.ts index e7a7ab672..9906f1423 100644 --- a/apps/vault/src/shared/module/persistence/service/prisma.service.ts +++ b/apps/vault/src/shared/module/persistence/service/prisma.service.ts @@ -1,22 +1,311 @@ import { ConfigService } from '@narval/config-module' +import { EncryptionService } from '@narval/encryption-module' import { LoggerService } from '@narval/nestjs-shared' -import { Injectable, OnApplicationShutdown, OnModuleDestroy, OnModuleInit } from '@nestjs/common' -import { PrismaClient } from '@prisma/client/vault' +import { Injectable, OnApplicationShutdown, OnModuleDestroy, OnModuleInit, Optional } from '@nestjs/common' +import { hmac } from '@noble/hashes/hmac' +import { sha256 } from '@noble/hashes/sha2' +import { bytesToHex } from '@noble/hashes/utils' +import { Prisma, PrismaClient } from '@prisma/client/vault' +import { canonicalize } from 'packages/signature/src/lib/json.util' import { Config } from '../../../../main.config' +import { ParseException } from '../exception/parse.exception' + +const ENCRYPTION_PREFIX = 'enc.v1.' // Version prefix helps with future encryption changes +const INTEGRITY_PREFIX = 'hmac.v1.' // Version prefix helps with future integrity changes + +/** + * To encrypt a field, simply reference the Model as the key, and the fields in an array. + * NOTE: encrypted fields MUST be of string type. JSON data should be stringified before/after encryption/decryption; this assumes Strings. + */ +const encryptedModelFields = { + Client: [], + ProviderConnection: [Prisma.ProviderConnectionScalarFieldEnum.credentials], + TransitEncryptionKey: [Prisma.TransitEncryptionKeyScalarFieldEnum.privateKey] +} + +const modelWithHmacIntegrity = { + ProviderConnection: { + [Prisma.ProviderConnectionScalarFieldEnum.id]: { + integrity: true, + nullable: false + }, + [Prisma.ProviderConnectionScalarFieldEnum.clientId]: { + integrity: true, + nullable: false + }, + [Prisma.ProviderConnectionScalarFieldEnum.provider]: { + integrity: true, + nullable: false + }, + [Prisma.ProviderConnectionScalarFieldEnum.url]: { + integrity: true, + nullable: true + }, + [Prisma.ProviderConnectionScalarFieldEnum.label]: { + integrity: true, + nullable: true + }, + [Prisma.ProviderConnectionScalarFieldEnum.credentials]: { + integrity: true, + nullable: true + }, + [Prisma.ProviderConnectionScalarFieldEnum.status]: { + integrity: true, + nullable: false + }, + [Prisma.ProviderConnectionScalarFieldEnum.createdAt]: { + integrity: true, + nullable: false + }, + [Prisma.ProviderConnectionScalarFieldEnum.updatedAt]: { + integrity: true, + nullable: false + }, + [Prisma.ProviderConnectionScalarFieldEnum.revokedAt]: { + integrity: true, + nullable: true + } + } +} + +const getHmac = (secret: string, value: Record) => { + const integrity = hmac(sha256, secret, canonicalize(value)) + return `${INTEGRITY_PREFIX}${bytesToHex(integrity)}` +} + +const buildEncryptionExtension = ( + configService: ConfigService, + logger: LoggerService, + encryptionService: EncryptionService +) => { + // Generate the hmac + const hmacSecret = configService.get('keyring.hmacSecret') + if (!hmacSecret) { + logger.error('HMAC secret is not set, integrity verification will not be performed') + throw new Error('HMAC secret is not set, integrity verification will not be performed') + } + + const encryptToString = async (value: string) => { + const encryptedBuffer = await encryptionService.encrypt(value) + const encryptedString = encryptedBuffer.toString('hex') + return `${ENCRYPTION_PREFIX}${encryptedString}` + } + + const decryptToString = async (value: string) => { + if (!value.startsWith(ENCRYPTION_PREFIX)) { + return value + } + const decryptedBuffer = await encryptionService.decrypt(Buffer.from(value.slice(ENCRYPTION_PREFIX.length), 'hex')) + return decryptedBuffer.toString() + } + + return Prisma.defineExtension({ + name: 'encryption', + query: { + async $allOperations({ model, operation, args, query }) { + if (!model || !(model in encryptedModelFields)) { + return query(args) + } + const fields = encryptedModelFields[model as keyof typeof encryptedModelFields] + + // For write operations, encrypt. + const writeOps = ['create', 'upsert', 'update', 'updateMany', 'createMany'] + if (writeOps.includes(operation)) { + let dataToUpdate: Record[] = [] + if (operation === 'upsert') { + if (args.update) { + dataToUpdate.push(args.update) + } + if (args.create) { + dataToUpdate.push(args.create) + } + } else if (Array.isArray(args.data)) { + dataToUpdate = args.data + } else { + dataToUpdate = [args.data] + } + // For each field-to-encrypt, for each object being created, encrypt the field. + await Promise.all( + fields.map(async (field) => { + await Promise.all( + dataToUpdate.map(async (item: Record) => { + if (item[field] && typeof item[field] === 'string') { + item[field] = await encryptToString(item[field] as string) + } + }) + ) + }) + ) + // Data has been encrypted. + // Now, generate the _integrity hmac + // The data must include every field on the model; if not, we will reject this operation. + if (model in modelWithHmacIntegrity) { + const fields = modelWithHmacIntegrity[model as keyof typeof modelWithHmacIntegrity] + for (const data of dataToUpdate) { + // Create object to hold fields that should be covered by integrity + const integrityCovered: Record = {} + + // Iterate through all configured fields for this model + for (const [fieldName, fieldSettings] of Object.entries(fields)) { + // Check if field is required but missing + integrityCovered[fieldName] = data[fieldName] + if (!fieldSettings.nullable && data[fieldName] === undefined) { + logger.error(`Missing required field ${fieldName} in data, needed for integrity hmac`) + throw new Error(`Missing required field ${fieldName} in data, needed for integrity hmac`) + } + if (fieldSettings.nullable && !data[fieldName]) { + // Ensure we capture the null in the integrity object + integrityCovered[fieldName] = null + } + } + + const integrity = getHmac(hmacSecret, integrityCovered) + data.integrity = integrity + } + } + + return query(args) + } + + // For read operations, decrypt. + const readOps = ['findUnique', 'findMany', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow'] as const + type ReadOp = (typeof readOps)[number] + + if (readOps.includes(operation as ReadOp)) { + const result = await query(args) + + // Handle non-record results + if (!result || typeof result === 'number' || 'count' in result) { + return result + } + + // Handle array or single result + const items = Array.isArray(result) ? result : [result] + + await Promise.all( + items.map(async (item: Record) => { + // If it has an `integrity` field, verify it's integrity + let skipIntegrityAndDecryption = false + if (model in modelWithHmacIntegrity) { + const fields = modelWithHmacIntegrity[model as keyof typeof modelWithHmacIntegrity] + // Create object to hold fields that should be covered by integrity + const integrityCovered: Record = {} + + // Iterate through all configured fields for this model + for (const [fieldName, fieldSettings] of Object.entries(fields)) { + integrityCovered[fieldName] = item[fieldName] + + // If there is an integrity field that is in args.select with `false` then skip integrity verification & decryption + if (fieldSettings.integrity && args.select && args.select[fieldName] === false) { + logger.log(`Skipping integrity verification & decryption due to subset of fields queried`) + skipIntegrityAndDecryption = true + return + } + + // Check if field is required but missing + if (!fieldSettings.nullable && item[fieldName] === undefined) { + logger.error( + `Missing required field ${fieldName} in data, needed for integrity hmac, did your query forget it?` + ) + throw new Error(`Missing required field ${fieldName} in data, needed for integrity hmac`) + } + } + + const integrityToVerify = getHmac(hmacSecret, integrityCovered) + if (integrityToVerify !== item.integrity) { + logger.error('Integrity verification failed', { + integrityToVerify, + item, + args + }) + throw new Error('Integrity verification failed') + } + } + // We passed integrity verification, so we can decrypt the fields + if (!skipIntegrityAndDecryption) { + await Promise.all( + fields.map(async (field) => { + if (item[field] && typeof item[field] === 'string') { + item[field] = await decryptToString(item[field] as string) + } + }) + ) + } + }) + ) + + return result + } + + return query(args) + } + } + }) +} @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy, OnApplicationShutdown { constructor( configService: ConfigService, - private logger: LoggerService + private logger: LoggerService, + @Optional() encryptionService?: EncryptionService ) { const url = configService.get('database.url') - super({ datasources: { db: { url } } }) + if (encryptionService) { + logger.log('Instantiating Prisma encryption extension') + Object.assign(this, this.$extends(buildEncryptionExtension(configService, logger, encryptionService))) + } + } + + static toPrismaJson(value?: T | null): Prisma.InputJsonValue | Prisma.NullTypes.JsonNull { + if (value === null || value === undefined) { + return Prisma.JsonNull + } + + // Handle basic JSON-serializable types. + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) { + return value as Prisma.InputJsonValue + } + + // For objects, ensure they're JSON-serializable. + if (typeof value === 'object') { + try { + return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue + } catch (error) { + throw new ParseException(error) + } + } + + return Prisma.JsonNull + } + + static toStringJson(value?: T | null): string | null { + if (value) { + try { + return JSON.stringify(value) + } catch (error) { + throw new ParseException(error) + } + } + + return null + } + + static toJson(value?: string | null) { + if (value) { + try { + return JSON.parse(value) + } catch (error) { + throw new ParseException(error) + } + } + + return null } async onModuleInit() { @@ -40,7 +329,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul onApplicationShutdown(signal: string) { this.logger.log('Disconnecting from Prisma on application shutdown', signal) - // The $disconnect method returns a promise, so idealy we should wait for it + // The $disconnect method returns a promise, so ideally we should wait for it // to finish. However, the onApplicationShutdown, returns `void` making it // impossible to ensure the database will be properly disconnected before // the shutdown. diff --git a/apps/vault/src/shared/module/persistence/service/seed.service.ts b/apps/vault/src/shared/module/persistence/service/seed.service.ts new file mode 100644 index 000000000..78933d5ec --- /dev/null +++ b/apps/vault/src/shared/module/persistence/service/seed.service.ts @@ -0,0 +1,7 @@ +import { NotImplementedException } from '@nestjs/common' + +export abstract class SeedService { + seed(): Promise { + throw new NotImplementedException() + } +} diff --git a/apps/vault/src/shared/module/persistence/service/seeder.service.ts b/apps/vault/src/shared/module/persistence/service/seeder.service.ts new file mode 100644 index 000000000..f7551a8c6 --- /dev/null +++ b/apps/vault/src/shared/module/persistence/service/seeder.service.ts @@ -0,0 +1,47 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' +import { Inject, Injectable } from '@nestjs/common' +import { ModulesContainer } from '@nestjs/core' +import { Config, Env } from '../../../../main.config' +import { SeedService } from './seed.service' + +@Injectable() +export class SeederService { + constructor( + @Inject(ModulesContainer) private modulesContainer: ModulesContainer, + private configService: ConfigService, + private logger: LoggerService + ) {} + + async seed() { + if (this.configService.get('env') === Env.PRODUCTION) { + throw new Error('Cannot seed production database!') + } + + for (const service of this.getSeedServices()) { + const name = service.constructor.name + + this.logger.log(`🌱 Seeding ${name}`) + + let error: unknown | null = null + try { + await service.seed() + } catch (err) { + this.logger.error(`❌ Error while seeding ${name}`, err) + + error = err + } + + if (!error) { + this.logger.log(`✅ ${name} seeded`) + } + } + } + + private getSeedServices(): SeedService[] { + return Array.from(this.modulesContainer.values()) + .flatMap((module) => Array.from(module.providers.values())) + .map((provider) => provider.instance) + .filter((instance): instance is SeedService => instance instanceof SeedService) + } +} diff --git a/apps/vault/src/shared/module/persistence/service/test-prisma.service.ts b/apps/vault/src/shared/module/persistence/service/test-prisma.service.ts index 0bfa9ad36..564a1e7b6 100644 --- a/apps/vault/src/shared/module/persistence/service/test-prisma.service.ts +++ b/apps/vault/src/shared/module/persistence/service/test-prisma.service.ts @@ -1,5 +1,12 @@ import { Injectable } from '@nestjs/common' import { PrismaClient } from '@prisma/client/vault' +import { + TEST_ACCOUNTS, + TEST_ADDRESSES, + TEST_CONNECTIONS, + TEST_WALLETS, + TEST_WALLETS_WITH_SAME_TIMESTAMP +} from '../../../../broker/__test__/util/mock-data' import { PrismaService } from './prisma.service' @Injectable() @@ -29,4 +36,31 @@ export class TestPrismaService { } } } + + async seedBrokerTestData(): Promise { + const client = this.getClient() + + await client.providerConnection.createMany({ + data: TEST_CONNECTIONS.map((connection) => ({ + ...connection, + credentials: JSON.stringify(connection.credentials) + })) + }) + + await client.providerWallet.createMany({ + data: TEST_WALLETS + }) + + await client.providerWallet.createMany({ + data: TEST_WALLETS_WITH_SAME_TIMESTAMP + }) + + await client.providerAccount.createMany({ + data: TEST_ACCOUNTS + }) + + await client.providerAddress.createMany({ + data: TEST_ADDRESSES + }) + } } diff --git a/apps/vault/src/shared/type/domain.type.ts b/apps/vault/src/shared/type/domain.type.ts index ee438f024..64b4f6feb 100644 --- a/apps/vault/src/shared/type/domain.type.ts +++ b/apps/vault/src/shared/type/domain.type.ts @@ -1,21 +1,74 @@ +import { Permission } from '@narval/armory-sdk' import { addressSchema, hexSchema } from '@narval/policy-engine-shared' import { Alg, Curves, publicKeySchema, rsaPrivateKeySchema, rsaPublicKeySchema } from '@narval/signature' import { z } from 'zod' +export const ClientLocalAuthAllowedUser = z.object({ + userId: z.string(), + publicKey: publicKeySchema +}) +export type ClientLocalAuthAllowedUser = z.infer + export const CreateClientInput = z.object({ clientId: z.string().optional(), + name: z.string().optional(), + baseUrl: z.string().optional(), + backupPublicKey: rsaPublicKeySchema.optional(), + + // New auth options + auth: z + .object({ + local: z + .object({ + jwsd: z + .object({ + maxAge: z.number().default(300), + requiredComponents: z.array(z.string()).default(['htm', 'uri', 'created', 'ath']) + }) + .nullish(), + allowedUsers: z + .array(ClientLocalAuthAllowedUser) + .nullish() + .describe('Pin specific users to be authorized; if set, ONLY these users are allowed') + }) + .nullish(), + tokenValidation: z + .object({ + disabled: z.boolean().default(false), + url: z.string().nullish(), + pinnedPublicKey: publicKeySchema.nullish(), + verification: z + .object({ + audience: z.string().nullish(), + issuer: z.string().nullish(), + maxTokenAge: z.number().nullish(), + requireBoundTokens: z.boolean().default(true), + allowBearerTokens: z.boolean().default(false), + allowWildcard: z.array(z.string()).nullish() + }) + .default({}) + }) + .default({}) + }) + .default({}), + + /** @deprecated use auth.tokenValidation instead */ engineJwk: publicKeySchema.optional(), + /** @deprecated use auth.tokenValidation instead */ audience: z.string().optional(), + /** @deprecated use auth.tokenValidation instead */ issuer: z.string().optional(), + /** @deprecated use auth.tokenValidation instead */ maxTokenAge: z.number().optional(), - backupPublicKey: rsaPublicKeySchema.optional(), - allowKeyExport: z.boolean().optional(), + /** @deprecated use auth.tokenValidation instead */ allowWildcard: z.array(z.string()).optional(), - baseUrl: z.string().optional() + + /** @deprecated, this has not bee implemented */ + allowKeyExport: z.boolean().optional() }) export type CreateClientInput = z.infer -export const Client = z.object({ +export const ClientV1 = z.object({ clientId: z.string(), engineJwk: publicKeySchema.optional(), @@ -37,13 +90,64 @@ export const Client = z.object({ createdAt: z.coerce.date(), updatedAt: z.coerce.date() }) +export type ClientV1 = z.infer + +export const Client = z.object({ + clientId: z.string(), + name: z.string(), + configurationSource: z.literal('declarative').or(z.literal('dynamic')), // Declarative = comes from config file, Dynamic = created at runtime + backupPublicKey: rsaPublicKeySchema.nullable(), + // Override if you want to use a different baseUrl for a single client. + baseUrl: z.string().nullable(), + + auth: z.object({ + disabled: z.boolean(), + local: z + .object({ + jwsd: z.object({ + maxAge: z.number(), + requiredComponents: z.array(z.string()) + }), + allowedUsersJwksUrl: z.string().nullable(), + allowedUsers: z.array(ClientLocalAuthAllowedUser).nullable() + }) + .nullable(), + tokenValidation: z.object({ + disabled: z.boolean(), + url: z.string().nullable(), + jwksUrl: z.string().nullable(), + pinnedPublicKey: publicKeySchema.nullable(), + verification: z.object({ + audience: z.string().nullable(), + issuer: z.string().nullable(), + maxTokenAge: z.number().nullable(), + requireBoundTokens: z.boolean(), + allowBearerTokens: z.boolean(), + allowWildcard: z.array(z.string()).nullable() + }) + }) + }), + + createdAt: z.coerce.date(), + updatedAt: z.coerce.date() +}) export type Client = z.infer -export const App = z.object({ +export const AppV1 = z.object({ id: z.string().min(1), adminApiKey: z.string().min(1).optional(), masterKey: z.string().min(1).optional() }) +export type AppV1 = z.infer + +export const App = z.object({ + id: z.string().min(1), + adminApiKeyHash: z.string().min(1).nullish(), + encryptionMasterKey: z.string().min(1).nullish(), + encryptionKeyringType: z.literal('raw').or(z.literal('awskms')), + encryptionMasterAwsKmsArn: z.string().nullish(), + authDisabled: z.boolean().optional() +}) export type App = z.infer export const Origin = { @@ -132,3 +236,10 @@ export type Algorithm = z.infer export const Curve = z.union([z.literal(Curves.P256), z.literal(Curves.SECP256K1)]) export type Curve = z.infer + +export const VaultPermission = { + ...Permission, + CONNECTION_WRITE: 'connection:write', + CONNECTION_READ: 'connection:read' +} as const +export type VaultPermission = (typeof VaultPermission)[keyof typeof VaultPermission] diff --git a/apps/vault/src/shared/type/http-exception.type.ts b/apps/vault/src/shared/type/http-exception.type.ts new file mode 100644 index 000000000..87163de6a --- /dev/null +++ b/apps/vault/src/shared/type/http-exception.type.ts @@ -0,0 +1,11 @@ +import { HttpStatus } from '@nestjs/common' +import { z } from 'zod' + +export const HttpException = z.object({ + statusCode: z.nativeEnum(HttpStatus), + message: z.string(), + context: z.unknown().optional(), + stack: z.string().optional().describe('In development mode, contains the exception stack trace'), + origin: z.instanceof(Error).optional().describe('In development mode, it may contain the error origin') +}) +export type HttpException = z.infer diff --git a/apps/vault/src/vault/__test__/e2e/encryption-key.spec.ts b/apps/vault/src/transit-encryption/__test__/e2e/encryption-key.spec.ts similarity index 59% rename from apps/vault/src/vault/__test__/e2e/encryption-key.spec.ts rename to apps/vault/src/transit-encryption/__test__/e2e/encryption-key.spec.ts index 785fb3279..5710aa5d8 100644 --- a/apps/vault/src/vault/__test__/e2e/encryption-key.spec.ts +++ b/apps/vault/src/transit-encryption/__test__/e2e/encryption-key.spec.ts @@ -1,5 +1,4 @@ import { Permission } from '@narval/armory-sdk' -import { ConfigModule, ConfigService } from '@narval/config-module' import { EncryptionModuleOptionProvider } from '@narval/encryption-module' import { LoggerModule, REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' import { @@ -14,23 +13,21 @@ import { HttpStatus, INestApplication } from '@nestjs/common' import { Test, TestingModule } from '@nestjs/testing' import request from 'supertest' import { v4 as uuid } from 'uuid' -import { ClientModule } from '../../../client/client.module' import { ClientService } from '../../../client/core/service/client.service' -import { Config, load } from '../../../main.config' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' import { Client } from '../../../shared/type/domain.type' -import { AppService } from '../../core/service/app.service' const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' -describe('Encryption-keys', () => { +describe('Transit Encryption Key', () => { let app: INestApplication let module: TestingModule let testPrismaService: TestPrismaService - let appService: AppService + let provisionService: ProvisionService let clientService: ClientService - let configService: ConfigService const clientId = uuid() @@ -40,10 +37,44 @@ describe('Encryption-keys', () => { const client: Client = { clientId, - engineJwk: clientPublicJWK, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: [ + 'path.to.allow', + 'transactionRequest.maxFeePerGas', + 'transactionRequest.maxPriorityFeePerGas', + 'transactionRequest.gas' + ] + }, + pinnedPublicKey: clientPublicJWK + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), updatedAt: new Date() } + const getAccessToken = async (permissions: Permission[], opts: object = {}) => { const payload: Payload = { sub: 'test-root-user-uid', @@ -64,15 +95,10 @@ describe('Encryption-keys', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - ClientModule - ] + imports: [MainModule] }) + .overrideModule(LoggerModule) + .useModule(LoggerModule.forTest()) .overrideProvider(EncryptionModuleOptionProvider) .useValue({ keyring: getTestRawAesKeyring() @@ -81,10 +107,9 @@ describe('Encryption-keys', () => { app = module.createNestApplication({ logger: false }) - appService = module.get(AppService) + provisionService = module.get(ProvisionService) testPrismaService = module.get(TestPrismaService) clientService = module.get(ClientService) - configService = module.get>(ConfigService) await app.init() }) @@ -98,23 +123,19 @@ describe('Encryption-keys', () => { beforeEach(async () => { await testPrismaService.truncateAll() - await appService.save({ - id: configService.get('app.id'), - masterKey: 'test-master-key', - adminApiKey: 'test-admin-api-key' - }) + await provisionService.provision() await clientService.save(client) }) - describe('POST', () => { + describe('POST /encryption-keys', () => { it('responds with unauthorized when client secret is missing', async () => { const { status } = await request(app.getHttpServer()).post('/encryption-keys').send() expect(status).toEqual(HttpStatus.UNAUTHORIZED) }) - it('generates an RSA keypair', async () => { + it('generates an RSA key pair', async () => { const accessToken = await getAccessToken([Permission.WALLET_IMPORT]) const { status, body } = await request(app.getHttpServer()) @@ -123,7 +144,7 @@ describe('Encryption-keys', () => { .set('authorization', `GNAP ${accessToken}`) .send({}) - expect(body).toEqual({ + expect(body).toMatchObject({ publicKey: expect.objectContaining({ kid: expect.any(String), kty: 'RSA', @@ -133,6 +154,32 @@ describe('Encryption-keys', () => { e: expect.any(String) }) }) + + expect(status).toEqual(HttpStatus.CREATED) + }) + + it('responds with rsa public key in different formats', async () => { + const accessToken = await getAccessToken([Permission.WALLET_IMPORT]) + + const { status, body } = await request(app.getHttpServer()) + .post('/encryption-keys') + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .set('authorization', `GNAP ${accessToken}`) + .send({}) + + expect(body.data).toEqual({ + keyId: expect.any(String), + jwk: expect.objectContaining({ + kid: expect.any(String), + kty: 'RSA', + use: 'enc', + alg: 'RS256', + n: expect.any(String), + e: expect.any(String) + }), + pem: expect.any(String) + }) + expect(status).toEqual(HttpStatus.CREATED) }) }) diff --git a/apps/vault/src/transit-encryption/core/exception/encryption-key.exception.ts b/apps/vault/src/transit-encryption/core/exception/encryption-key.exception.ts new file mode 100644 index 000000000..bb7bbb896 --- /dev/null +++ b/apps/vault/src/transit-encryption/core/exception/encryption-key.exception.ts @@ -0,0 +1,3 @@ +import { ApplicationException } from '../../../shared/exception/application.exception' + +export class EncryptionKeyException extends ApplicationException {} diff --git a/apps/vault/src/transit-encryption/core/exception/invalid-jwe-header.exception.ts b/apps/vault/src/transit-encryption/core/exception/invalid-jwe-header.exception.ts new file mode 100644 index 000000000..773746edd --- /dev/null +++ b/apps/vault/src/transit-encryption/core/exception/invalid-jwe-header.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { EncryptionKeyException } from './encryption-key.exception' + +export class InvalidJweHeaderException extends EncryptionKeyException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Invalid JWE header', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNPROCESSABLE_ENTITY, + ...params + }) + } +} diff --git a/apps/vault/src/transit-encryption/core/exception/not-found.exception.ts b/apps/vault/src/transit-encryption/core/exception/not-found.exception.ts new file mode 100644 index 000000000..db56a9aec --- /dev/null +++ b/apps/vault/src/transit-encryption/core/exception/not-found.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { EncryptionKeyException } from './encryption-key.exception' + +export class NotFoundException extends EncryptionKeyException { + constructor(params?: Partial) { + super({ + message: params?.message || 'Encryption key not found', + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.NOT_FOUND, + ...params + }) + } +} diff --git a/apps/vault/src/transit-encryption/core/exception/unauthorized.exception.ts b/apps/vault/src/transit-encryption/core/exception/unauthorized.exception.ts new file mode 100644 index 000000000..507cd6a92 --- /dev/null +++ b/apps/vault/src/transit-encryption/core/exception/unauthorized.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common' +import { ApplicationExceptionParams } from '../../../shared/exception/application.exception' +import { EncryptionKeyException } from './encryption-key.exception' + +export class UnauthorizedException extends EncryptionKeyException { + constructor(params?: Partial) { + super({ + message: params?.message || "Encryption key doesn't belong to client", + suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.UNAUTHORIZED, + ...params + }) + } +} diff --git a/apps/vault/src/transit-encryption/core/service/__test__/integration/encryption-key.service.spec.ts b/apps/vault/src/transit-encryption/core/service/__test__/integration/encryption-key.service.spec.ts new file mode 100644 index 000000000..c1664cffd --- /dev/null +++ b/apps/vault/src/transit-encryption/core/service/__test__/integration/encryption-key.service.spec.ts @@ -0,0 +1,173 @@ +import { ConfigModule } from '@narval/config-module' +import { EncryptionModuleOptionProvider } from '@narval/encryption-module' +import { LoggerModule } from '@narval/nestjs-shared' +import { + Alg, + RsaPublicKey, + SMALLEST_RSA_MODULUS_LENGTH, + Use, + generateJwk, + getPublicKey, + rsaEncrypt, + rsaPrivateKeySchema, + rsaPrivateKeyToPublicKey, + rsaPublicKeySchema +} from '@narval/signature' +import { Test } from '@nestjs/testing' +import { omit } from 'lodash' +import { v4 as uuid } from 'uuid' +import { load } from '../../../../../main.config' +import { AppModule } from '../../../../../main.module' +import { PersistenceModule } from '../../../../../shared/module/persistence/persistence.module' +import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' +import { EncryptionKeyRepository } from '../../../../persistence/encryption-key.repository' +import { InvalidJweHeaderException } from '../../../exception/invalid-jwe-header.exception' +import { NotFoundException } from '../../../exception/not-found.exception' +import { UnauthorizedException } from '../../../exception/unauthorized.exception' +import { EncryptionKey } from '../../../type/encryption-key.type' +import { EncryptionKeyService } from '../../encryption-key.service' + +const GENERATE_RSA_KEY_OPTIONS: { use: Use; modulusLength: number } = { + use: 'enc', + modulusLength: SMALLEST_RSA_MODULUS_LENGTH +} + +describe(EncryptionKeyService.name, () => { + let encryptionKeyService: EncryptionKeyService + let encryptionKeyRepository: EncryptionKeyRepository + + const clientId = uuid() + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [ + LoggerModule.forTest(), + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + AppModule, + PersistenceModule.forRoot() + ], + providers: [EncryptionKeyService, EncryptionKeyRepository] + }) + .overrideProvider(EncryptionModuleOptionProvider) + .useValue({ + keyring: getTestRawAesKeyring() + }) + .compile() + + encryptionKeyService = module.get(EncryptionKeyService) + encryptionKeyRepository = module.get(EncryptionKeyRepository) + }) + + describe('generate', () => { + it('returns rsa private and public keys', async () => { + const encryptionKey = await encryptionKeyService.generate(clientId) + + expect(rsaPrivateKeySchema.safeParse(encryptionKey.privateKey).success).toEqual(true) + expect(rsaPublicKeySchema.safeParse(encryptionKey.publicKey).success).toEqual(true) + }) + + it('uses private kid as id', async () => { + const { privateKey } = await encryptionKeyService.generate(clientId) + + const encryptionKey = await encryptionKeyRepository.findByKid(privateKey.kid) + + expect(encryptionKey).toMatchObject({ + clientId, + privateKey, + publicKey: getPublicKey(privateKey) + }) + }) + }) + + describe('decrypt', () => { + it('decrypts data using rsa private key', async () => { + const privateKey = await generateJwk(Alg.RS256, { use: 'enc', modulusLength: 2048 }) + const publicKey = rsaPrivateKeyToPublicKey(privateKey) + const encryptionKey = { + clientId, + privateKey, + publicKey, + createdAt: new Date() + } + await encryptionKeyRepository.create(encryptionKey) + + const secret = 'secret message' + const encryptedData = await rsaEncrypt(secret, publicKey) + + const decryptedData = await encryptionKeyService.decrypt(clientId, encryptedData) + expect(decryptedData).toBe(secret) + }) + + it('throws InvalidJweHeaderException when public key kid is missing', async () => { + const privateKey = await generateJwk(Alg.RS256, GENERATE_RSA_KEY_OPTIONS) + const publicKey = omit(rsaPrivateKeyToPublicKey(privateKey), 'kid') + + // Mock the repository to bypass schema validation + jest.spyOn(encryptionKeyRepository, 'create').mockImplementationOnce((encryptionKey) => { + return Promise.resolve({ + ...encryptionKey, + publicKey: omit(publicKey, 'kid') + } as EncryptionKey) + }) + + const encryptionKey = { + clientId, + privateKey, + publicKey, + createdAt: new Date() + } + + await encryptionKeyRepository.create(encryptionKey as EncryptionKey) + const encryptedData = await rsaEncrypt('secret', publicKey as RsaPublicKey) + + try { + await encryptionKeyService.decrypt(clientId, encryptedData) + fail('expected to have thrown InvalidJweHeaderException') + } catch (error) { + expect(error).toBeInstanceOf(InvalidJweHeaderException) + } + }) + + it('throws NotFoundException when encryption key is not found', async () => { + const privateKey = await generateJwk(Alg.RS256, GENERATE_RSA_KEY_OPTIONS) + const publicKey = rsaPrivateKeyToPublicKey(privateKey) + const encryptedData = await rsaEncrypt('secret', publicKey) + + try { + await encryptionKeyService.decrypt(clientId, encryptedData) + fail('expected to have thrown NotFoundException') + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException) + expect(error.context).toEqual({ kid: publicKey.kid }) + } + }) + + it('throws UnauthorizedException when encryption key clientId is different than the given clientId', async () => { + const differentClientId = uuid() + const privateKey = await generateJwk(Alg.RS256, GENERATE_RSA_KEY_OPTIONS) + const publicKey = rsaPrivateKeyToPublicKey(privateKey) + const encryptionKey = { + clientId: differentClientId, + privateKey, + publicKey, + createdAt: new Date() + } + await encryptionKeyRepository.create(encryptionKey) + const encryptedData = await rsaEncrypt('secret', publicKey) + + try { + await encryptionKeyService.decrypt(clientId, encryptedData) + fail('expected to have thrown UnauthorizedException') + } catch (error) { + expect(error).toBeInstanceOf(UnauthorizedException) + expect(error.context).toEqual({ + kid: publicKey.kid, + clientId + }) + } + }) + }) +}) diff --git a/apps/vault/src/transit-encryption/core/service/encryption-key.service.ts b/apps/vault/src/transit-encryption/core/service/encryption-key.service.ts new file mode 100644 index 000000000..027f37781 --- /dev/null +++ b/apps/vault/src/transit-encryption/core/service/encryption-key.service.ts @@ -0,0 +1,87 @@ +import { ConfigService } from '@narval/config-module' +import { LoggerService } from '@narval/nestjs-shared' +import { + Alg, + DEFAULT_RSA_MODULUS_LENGTH, + SMALLEST_RSA_MODULUS_LENGTH, + generateJwk, + rsaDecrypt, + rsaPrivateKeyToPublicKey +} from '@narval/signature' +import { Injectable } from '@nestjs/common' +import { decodeProtectedHeader } from 'jose' +import { Config, Env } from '../../../main.config' +import { EncryptionKeyRepository } from '../../persistence/encryption-key.repository' +import { InvalidJweHeaderException } from '../exception/invalid-jwe-header.exception' +import { NotFoundException } from '../exception/not-found.exception' +import { UnauthorizedException } from '../exception/unauthorized.exception' +import { EncryptionKey } from '../type/encryption-key.type' + +type GenerateOptions = { + modulusLength?: number +} + +@Injectable() +export class EncryptionKeyService { + constructor( + private readonly encryptionKeyRepository: EncryptionKeyRepository, + private readonly configService: ConfigService, + private readonly logger: LoggerService + ) {} + + async generate(clientId: string, opts?: GenerateOptions): Promise { + const modulusLength = this.getRsaModulusLength(opts) + const privateKey = await generateJwk(Alg.RS256, { use: 'enc', modulusLength }) + const publicKey = rsaPrivateKeyToPublicKey(privateKey) + + this.logger.log('Generate RSA encryption key', { + clientId, + modulusLength, + keyId: publicKey.kid + }) + + const encryptionKey = { + clientId, + privateKey, + publicKey, + createdAt: new Date() + } + + return this.encryptionKeyRepository.create(encryptionKey) + } + + private getRsaModulusLength(opts?: GenerateOptions): number { + if (opts?.modulusLength) { + return opts.modulusLength + } + + // Prevents flaky tests due to the high time it takes to generate an RSA + // key 4096. + if (this.configService.get('env') !== Env.PRODUCTION) { + return SMALLEST_RSA_MODULUS_LENGTH + } + + return DEFAULT_RSA_MODULUS_LENGTH + } + + async decrypt(clientId: string, encryptedData: string): Promise { + const header = decodeProtectedHeader(encryptedData) + const kid = header.kid + + if (!kid) { + throw new InvalidJweHeaderException() + } + + const encryptionKey = await this.encryptionKeyRepository.findByKid(kid) + + if (encryptionKey) { + if (encryptionKey.clientId !== clientId) { + throw new UnauthorizedException({ context: { kid, clientId } }) + } + + return rsaDecrypt(encryptedData, encryptionKey.privateKey) + } + + throw new NotFoundException({ context: { kid } }) + } +} diff --git a/apps/vault/src/transit-encryption/core/type/encryption-key.type.ts b/apps/vault/src/transit-encryption/core/type/encryption-key.type.ts new file mode 100644 index 000000000..010c87d8a --- /dev/null +++ b/apps/vault/src/transit-encryption/core/type/encryption-key.type.ts @@ -0,0 +1,10 @@ +import { rsaPrivateKeySchema, rsaPublicKeySchema } from '@narval/signature' +import { z } from 'zod' + +export const EncryptionKey = z.object({ + clientId: z.string(), + publicKey: rsaPublicKeySchema, + privateKey: rsaPrivateKeySchema, + createdAt: z.date() +}) +export type EncryptionKey = z.infer diff --git a/apps/vault/src/transit-encryption/http/rest/controller/dto/response/encryption-key.dto.ts b/apps/vault/src/transit-encryption/http/rest/controller/dto/response/encryption-key.dto.ts new file mode 100644 index 000000000..28a9de6ce --- /dev/null +++ b/apps/vault/src/transit-encryption/http/rest/controller/dto/response/encryption-key.dto.ts @@ -0,0 +1,18 @@ +import { publicKeySchema, rsaPublicKeySchema } from '@narval/signature' +import { createZodDto } from 'nestjs-zod' +import { z } from 'zod' + +export class EncryptionKeyDto extends createZodDto( + z.object({ + /** + * @deprecated Use `data.jwk` instead. Requires Vault encryption flows to + * move from `publicKey` to `data.jwk`. + */ + publicKey: rsaPublicKeySchema.describe('(DEPRECATED: use data.jwk instead) JWK format of the public key'), + data: z.object({ + keyId: z.string().optional(), + jwk: publicKeySchema.optional().describe('JWK format of the public key'), + pem: z.string().optional().describe('Base64url encoded PEM public key') + }) + }) +) {} diff --git a/apps/vault/src/transit-encryption/http/rest/controller/rest/encryption-key.controller.ts b/apps/vault/src/transit-encryption/http/rest/controller/rest/encryption-key.controller.ts new file mode 100644 index 000000000..9aad573fa --- /dev/null +++ b/apps/vault/src/transit-encryption/http/rest/controller/rest/encryption-key.controller.ts @@ -0,0 +1,46 @@ +import { Permission } from '@narval/armory-sdk' +import { ApiClientIdHeader } from '@narval/nestjs-shared' +import { publicKeyToPem } from '@narval/signature' +import { Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { ClientId } from '../../../../../shared/decorator/client-id.decorator' +import { PermissionGuard } from '../../../../../shared/decorator/permission-guard.decorator' +import { VaultPermission } from '../../../../../shared/type/domain.type' +import { EncryptionKeyService } from '../../../../core/service/encryption-key.service' +import { EncryptionKeyDto } from '../dto/response/encryption-key.dto' + +@Controller({ + path: '/encryption-keys', + version: '1' +}) +@PermissionGuard(Permission.WALLET_IMPORT, VaultPermission.CONNECTION_WRITE) +@ApiTags('Encryption Key') +@ApiClientIdHeader() +export class EncryptionKeyController { + constructor(private readonly encryptionKeyService: EncryptionKeyService) {} + + @Post() + @ApiOperation({ + summary: 'Generates an encryption key pair used to secure end-to-end communication containing sensitive information' + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: EncryptionKeyDto + }) + async generate(@ClientId() clientId: string): Promise { + const encryptionKey = await this.encryptionKeyService.generate(clientId) + + const encryptionPem = encryptionKey.publicKey + ? await publicKeyToPem(encryptionKey.publicKey, encryptionKey.publicKey.alg) + : undefined + + return EncryptionKeyDto.create({ + publicKey: encryptionKey.publicKey, + data: { + keyId: encryptionKey.publicKey?.kid, + jwk: encryptionKey.publicKey, + pem: encryptionPem ? Buffer.from(encryptionPem).toString('base64') : undefined + } + }) + } +} diff --git a/apps/vault/src/transit-encryption/persistence/encryption-key.repository.ts b/apps/vault/src/transit-encryption/persistence/encryption-key.repository.ts new file mode 100644 index 000000000..b63af9edf --- /dev/null +++ b/apps/vault/src/transit-encryption/persistence/encryption-key.repository.ts @@ -0,0 +1,40 @@ +import { rsaPrivateKeySchema, rsaPublicKeySchema } from '@narval/signature' +import { Injectable } from '@nestjs/common' +import { PrismaService } from '../../shared/module/persistence/service/prisma.service' +import { EncryptionKey } from '../core/type/encryption-key.type' + +@Injectable() +export class EncryptionKeyRepository { + constructor(private readonly prismaService: PrismaService) {} + + async create(encryptionKey: EncryptionKey): Promise { + await this.prismaService.transitEncryptionKey.create({ + data: { + id: encryptionKey.privateKey.kid, + clientId: encryptionKey.clientId, + privateKey: JSON.stringify(rsaPrivateKeySchema.parse(encryptionKey.privateKey)), + publicKey: JSON.stringify(rsaPublicKeySchema.parse(encryptionKey.publicKey)), + createdAt: encryptionKey.createdAt + } + }) + + return encryptionKey + } + + async findByKid(kid: string): Promise { + const encryptionKey = await this.prismaService.transitEncryptionKey.findUnique({ + where: { id: kid } + }) + + if (encryptionKey) { + // TODO: we have stringified json, so make sure to handle errors + return EncryptionKey.parse({ + ...encryptionKey, + privateKey: JSON.parse(encryptionKey.privateKey), + publicKey: JSON.parse(encryptionKey.publicKey) + }) + } + + return null + } +} diff --git a/apps/vault/src/transit-encryption/transit-encryption.module.ts b/apps/vault/src/transit-encryption/transit-encryption.module.ts new file mode 100644 index 000000000..dc0873800 --- /dev/null +++ b/apps/vault/src/transit-encryption/transit-encryption.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common' +import { ClientModule } from '../client/client.module' +import { PersistenceModule } from '../shared/module/persistence/persistence.module' +import { EncryptionKeyService } from './core/service/encryption-key.service' +import { EncryptionKeyController } from './http/rest/controller/rest/encryption-key.controller' +import { EncryptionKeyRepository } from './persistence/encryption-key.repository' + +@Module({ + imports: [ + PersistenceModule, + // Required by the AuthorizationGuard. + ClientModule + ], + providers: [EncryptionKeyRepository, EncryptionKeyService], + exports: [EncryptionKeyService], + controllers: [EncryptionKeyController] +}) +export class TransitEncryptionModule {} diff --git a/apps/vault/src/vault/__test__/e2e/account.spec.ts b/apps/vault/src/vault/__test__/e2e/account.spec.ts index 47fa00208..51b00eccb 100644 --- a/apps/vault/src/vault/__test__/e2e/account.spec.ts +++ b/apps/vault/src/vault/__test__/e2e/account.spec.ts @@ -1,7 +1,5 @@ import { Permission } from '@narval/armory-sdk' -import { ConfigModule, ConfigService } from '@narval/config-module' -import { EncryptionModuleOptionProvider } from '@narval/encryption-module' -import { LoggerModule, REQUEST_HEADER_CLIENT_ID, secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' import { Payload, RsaPublicKey, @@ -14,16 +12,15 @@ import { signJwt } from '@narval/signature' import { HttpStatus, INestApplication } from '@nestjs/common' -import { Test, TestingModule } from '@nestjs/testing' +import { TestingModule } from '@nestjs/testing' import request from 'supertest' import { v4 as uuid } from 'uuid' -import { ClientModule } from '../../../client/client.module' +import { VaultTest } from '../../../__test__/shared/vault.test' import { ClientService } from '../../../client/core/service/client.service' -import { Config, load } from '../../../main.config' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' -import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' import { Client, Origin } from '../../../shared/type/domain.type' -import { AppService } from '../../core/service/app.service' const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' @@ -31,9 +28,8 @@ describe('Accounts', () => { let app: INestApplication let module: TestingModule let testPrismaService: TestPrismaService - let appService: AppService + let provisionService: ProvisionService let clientService: ClientService - let configService: ConfigService const clientId = uuid() @@ -43,10 +39,44 @@ describe('Accounts', () => { const client: Client = { clientId, - engineJwk: clientPublicJWK, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: [ + 'path.to.allow', + 'transactionRequest.maxFeePerGas', + 'transactionRequest.maxPriorityFeePerGas', + 'transactionRequest.gas' + ] + }, + pinnedPublicKey: clientPublicJWK + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), updatedAt: new Date() } + const getAccessToken = async (permissions: Permission[], opts: object = {}) => { const payload: Payload = { sub: 'test-root-user-uid', @@ -66,28 +96,15 @@ describe('Accounts', () => { } beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - ClientModule - ] - }) - .overrideProvider(EncryptionModuleOptionProvider) - .useValue({ - keyring: getTestRawAesKeyring() - }) - .compile() + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() app = module.createNestApplication({ logger: false }) - appService = module.get(AppService) + provisionService = module.get(ProvisionService) testPrismaService = module.get(TestPrismaService) clientService = module.get(ClientService) - configService = module.get>(ConfigService) await app.init() }) @@ -101,11 +118,7 @@ describe('Accounts', () => { beforeEach(async () => { await testPrismaService.truncateAll() - await appService.save({ - id: configService.get('app.id'), - masterKey: 'test-master-key', - adminApiKey: secret.hash('test-admin-api-key') - }) + await provisionService.provision() await clientService.save(client) }) @@ -113,12 +126,11 @@ describe('Accounts', () => { describe('GET /accounts', () => { it('list all accounts for a specific client', async () => { const secondClientId = uuid() - await clientService.save({ - clientId: secondClientId, - engineJwk: clientPublicJWK, - createdAt: new Date(), - updatedAt: new Date() - }) + const secondClient: Client = { + ...client, + clientId: secondClientId + } + await clientService.save(secondClient) const accessToken = await getAccessToken([Permission.WALLET_READ]) const { body: firstMnemonicRequest } = await request(app.getHttpServer()) diff --git a/apps/vault/src/vault/__test__/e2e/provision.spec.ts b/apps/vault/src/vault/__test__/e2e/provision.spec.ts deleted file mode 100644 index be6db53f9..000000000 --- a/apps/vault/src/vault/__test__/e2e/provision.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ConfigModule } from '@narval/config-module' -import { LoggerModule, secret } from '@narval/nestjs-shared' -import { INestApplication } from '@nestjs/common' -import { Test, TestingModule } from '@nestjs/testing' -import request from 'supertest' -import { Config, load } from '../../../main.config' -import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' -import { AppService } from '../../core/service/app.service' -import { ProvisionService } from '../../core/service/provision.service' -import { VaultModule } from '../../vault.module' - -const ENDPOINT = '/apps/activate' - -const testConfigLoad = (): Config => ({ - ...load(), - app: { - id: 'local-dev-vault-instance-1', - adminApiKeyHash: undefined - } -}) - -describe('Provision', () => { - let app: INestApplication - let module: TestingModule - let appService: AppService - let testPrismaService: TestPrismaService - let provisionService: ProvisionService - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [testConfigLoad], - isGlobal: true - }), - VaultModule - ] - }).compile() - - app = module.createNestApplication() - - appService = app.get(AppService) - provisionService = app.get(ProvisionService) - testPrismaService = app.get(TestPrismaService) - - await app.init() - }) - - beforeEach(async () => { - await testPrismaService.truncateAll() - await provisionService.provision() - }) - - afterAll(async () => { - await testPrismaService.truncateAll() - await module.close() - await app.close() - }) - - describe(`POST ${ENDPOINT}`, () => { - it('responds with activated app state', async () => { - const { body } = await request(app.getHttpServer()).post(ENDPOINT).send() - - expect(body).toEqual({ - state: 'READY', - app: { - appId: 'local-dev-vault-instance-1', - adminApiKey: expect.any(String) - } - }) - }) - - it('responds already activated', async () => { - await request(app.getHttpServer()).post(ENDPOINT).send() - - const { body } = await request(app.getHttpServer()).post(ENDPOINT).send() - - expect(body).toEqual({ state: 'ACTIVATED' }) - }) - - it('does not respond with hashed admin API key', async () => { - const { body } = await request(app.getHttpServer()).post(ENDPOINT).send() - - const actualApp = await appService.getAppOrThrow() - - expect(secret.hash(body.app.adminApiKey)).toEqual(actualApp.adminApiKey) - }) - }) -}) diff --git a/apps/vault/src/vault/__test__/e2e/sign.spec.ts b/apps/vault/src/vault/__test__/e2e/sign.spec.ts index 7685a4c82..ba3ad046a 100644 --- a/apps/vault/src/vault/__test__/e2e/sign.spec.ts +++ b/apps/vault/src/vault/__test__/e2e/sign.spec.ts @@ -1,6 +1,4 @@ -import { ConfigModule } from '@narval/config-module' -import { EncryptionModuleOptionProvider } from '@narval/encryption-module' -import { LoggerModule, REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' import { Action, FIXTURE } from '@narval/policy-engine-shared' import { SigningAlg, @@ -16,20 +14,17 @@ import { type Payload } from '@narval/signature' import { HttpStatus, INestApplication } from '@nestjs/common' -import { Test, TestingModule } from '@nestjs/testing' +import { TestingModule } from '@nestjs/testing' import request from 'supertest' import { v4 as uuid } from 'uuid' import { verifyMessage } from 'viem' +import { VaultTest } from '../../../__test__/shared/vault.test' import { ClientService } from '../../../client/core/service/client.service' -import { load } from '../../../main.config' -import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository' -import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' -import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' import { Client, Origin, PrivateAccount } from '../../../shared/type/domain.type' -import { AppService } from '../../core/service/app.service' import { AccountRepository } from '../../persistence/repository/account.repository' -import { VaultModule } from '../../vault.module' const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' @@ -38,31 +33,83 @@ describe('Sign', () => { let module: TestingModule let testPrismaService: TestPrismaService - const adminApiKey = 'test-admin-api-key' - const clientId = uuid() const clientIdWithoutWildcard = uuid() // Engine key used to sign the approval request const enginePrivateJwk = secp256k1PrivateKeyToJwk(PRIVATE_KEY) const clientPublicJWK = secp256k1PrivateKeyToPublicJwk(PRIVATE_KEY) - const client: Client = { clientId, - engineJwk: clientPublicJWK, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: [ + 'path.to.allow', + 'transactionRequest.maxFeePerGas', + 'transactionRequest.maxPriorityFeePerGas', + 'transactionRequest.gas' + ] + }, + pinnedPublicKey: clientPublicJWK + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), - updatedAt: new Date(), - allowWildcard: [ - 'path.to.allow', - 'transactionRequest.maxFeePerGas', - 'transactionRequest.maxPriorityFeePerGas', - 'transactionRequest.gas' - ] + updatedAt: new Date() } const clientWithoutWildcard: Client = { clientId: clientIdWithoutWildcard, - engineJwk: clientPublicJWK, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: true, + allowBearerTokens: false, + allowWildcard: null + }, + pinnedPublicKey: clientPublicJWK + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), updatedAt: new Date() } @@ -108,37 +155,20 @@ describe('Sign', () => { } beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - VaultModule - ] - }) - .overrideProvider(KeyValueRepository) - .useValue(new InMemoryKeyValueRepository()) - .overrideProvider(EncryptionModuleOptionProvider) - .useValue({ - keyring: getTestRawAesKeyring() - }) - .compile() + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() app = module.createNestApplication({ logger: false }) testPrismaService = module.get(TestPrismaService) - - const appService = module.get(AppService) + const provisionService = module.get(ProvisionService) const clientService = module.get(ClientService) const accountRepository = module.get(AccountRepository) - await appService.save({ - id: 'test-app', - masterKey: 'unsafe-test-master-key', - adminApiKey - }) + await testPrismaService.truncateAll() + + await provisionService.provision() await clientService.save(client) @@ -148,8 +178,6 @@ describe('Sign', () => { await accountRepository.save(clientIdWithoutWildcard, account) - await testPrismaService.truncateAll() - await app.init() }) @@ -189,13 +217,11 @@ describe('Sign', () => { .set('authorization', `GNAP ${accessToken}`) .send(payload) - expect(body).toEqual( - expect.objectContaining({ - context: expect.any(Object), - message: 'Internal validation error', - statusCode: HttpStatus.UNPROCESSABLE_ENTITY - }) - ) + expect(body).toMatchObject({ + context: expect.any(Object), + message: 'Validation error', + statusCode: HttpStatus.UNPROCESSABLE_ENTITY + }) expect(status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY) }) diff --git a/apps/vault/src/vault/__test__/e2e/wallet.spec.ts b/apps/vault/src/vault/__test__/e2e/wallet.spec.ts index 63b461cfe..6c42f3b8c 100644 --- a/apps/vault/src/vault/__test__/e2e/wallet.spec.ts +++ b/apps/vault/src/vault/__test__/e2e/wallet.spec.ts @@ -1,13 +1,11 @@ import { Permission, resourceId } from '@narval/armory-sdk' -import { ConfigModule, ConfigService } from '@narval/config-module' -import { EncryptionModuleOptionProvider } from '@narval/encryption-module' -import { LoggerModule, REQUEST_HEADER_CLIENT_ID, secret } from '@narval/nestjs-shared' +import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' import { Alg, Curves, Payload, - RsaPrivateKey, RsaPublicKey, + SMALLEST_RSA_MODULUS_LENGTH, SigningAlg, buildSignerEip191, generateJwk, @@ -19,18 +17,17 @@ import { signJwt } from '@narval/signature' import { HttpStatus, INestApplication } from '@nestjs/common' -import { Test, TestingModule } from '@nestjs/testing' +import { TestingModule } from '@nestjs/testing' import { generateMnemonic } from '@scure/bip39' import request from 'supertest' import { v4 as uuid } from 'uuid' import { english } from 'viem/accounts' -import { ClientModule } from '../../../client/client.module' +import { VaultTest } from '../../../__test__/shared/vault.test' import { ClientService } from '../../../client/core/service/client.service' -import { Config, load } from '../../../main.config' +import { MainModule } from '../../../main.module' +import { ProvisionService } from '../../../provision.service' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' -import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' import { Client, Origin } from '../../../shared/type/domain.type' -import { AppService } from '../../core/service/app.service' import { ImportService } from '../../core/service/import.service' import { KeyGenerationService } from '../../core/service/key-generation.service' @@ -40,10 +37,9 @@ describe('Generate', () => { let app: INestApplication let module: TestingModule let testPrismaService: TestPrismaService - let appService: AppService + let provisionService: ProvisionService let clientService: ClientService let keyGenService: KeyGenerationService - let configService: ConfigService let importService: ImportService const clientId = uuid() @@ -54,10 +50,39 @@ describe('Generate', () => { const client: Client = { clientId, - engineJwk: clientPublicJWK, + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 600, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + requireBoundTokens: false, // DO NOT REQUIRE BOUND TOKENS; we're testing both payload.cnf bound tokens and unbound here. + allowBearerTokens: false, + allowWildcard: null + }, + pinnedPublicKey: clientPublicJWK + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), updatedAt: new Date() } + const getAccessToken = async (permissions: Permission[], opts: object = {}) => { const payload: Payload = { sub: 'test-root-user-uid', @@ -77,30 +102,17 @@ describe('Generate', () => { } beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - LoggerModule.forTest(), - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - ClientModule - ] - }) - .overrideProvider(EncryptionModuleOptionProvider) - .useValue({ - keyring: getTestRawAesKeyring() - }) - .compile() + module = await VaultTest.createTestingModule({ + imports: [MainModule] + }).compile() app = module.createNestApplication({ logger: false }) - appService = module.get(AppService) testPrismaService = module.get(TestPrismaService) clientService = module.get(ClientService) - configService = module.get>(ConfigService) keyGenService = module.get(KeyGenerationService) importService = module.get(ImportService) + provisionService = module.get(ProvisionService) await app.init() }) @@ -114,11 +126,7 @@ describe('Generate', () => { beforeEach(async () => { await testPrismaService.truncateAll() - await appService.save({ - id: configService.get('app.id'), - masterKey: 'test-master-key', - adminApiKey: secret.hash('test-admin-api-key') - }) + await provisionService.provision() await clientService.save(client) }) @@ -128,10 +136,8 @@ describe('Generate', () => { const accessToken = await getAccessToken([Permission.WALLET_READ]) const secondClientId = uuid() await clientService.save({ - clientId: secondClientId, - engineJwk: clientPublicJWK, - createdAt: new Date(), - updatedAt: new Date() + ...client, + clientId: secondClientId }) const { keyId: firstKeyId } = await keyGenService.generateWallet(clientId, { @@ -203,7 +209,10 @@ describe('Generate', () => { it('saves a backup when client got a backupKey', async () => { const accessToken = await getAccessToken([Permission.WALLET_CREATE]) const keyId = 'backupKeyId' - const backupKey = await generateJwk(Alg.RS256, { keyId }) + const backupKey = await generateJwk(Alg.RS256, { + keyId, + modulusLength: SMALLEST_RSA_MODULUS_LENGTH + }) const clientWithBackupKey: Client = { ...client, diff --git a/apps/vault/src/vault/core/service/__test__/integration/app.service.spec.ts b/apps/vault/src/vault/core/service/__test__/integration/app.service.spec.ts deleted file mode 100644 index 9afe2ece6..000000000 --- a/apps/vault/src/vault/core/service/__test__/integration/app.service.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ConfigModule, ConfigService } from '@narval/config-module' -import { EncryptionModule } from '@narval/encryption-module' -import { secret } from '@narval/nestjs-shared' -import { Test } from '@nestjs/testing' -import { Config, load } from '../../../../../main.config' -import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' -import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' -import { AppRepository } from '../../../../persistence/repository/app.repository' -import { AppService } from '../../app.service' - -describe(AppService.name, () => { - let appService: AppService - let configService: ConfigService - - const app = { - id: 'test-app-id', - masterKey: 'test-master-key', - adminApiKey: secret.hash('test-admin-api-key'), - activated: true - } - - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - EncryptionModule.register({ - keyring: getTestRawAesKeyring() - }) - ], - providers: [ - AppService, - AppRepository, - KeyValueService, - { - provide: KeyValueRepository, - useClass: InMemoryKeyValueRepository - } - ] - }).compile() - - appService = module.get(AppService) - configService = module.get>(ConfigService) - }) - - describe('save', () => { - it('returns the given secret key', async () => { - const actualApp = await appService.save(app) - - expect(actualApp.adminApiKey).toEqual(app.adminApiKey) - }) - - // IMPORTANT: The admin API key is hashed by the caller not the service. That - // allows us to have a determistic configuration file which is useful for - // automations like development or cloud set up. - it('does not hash the secret key', async () => { - jest.spyOn(configService, 'get').mockReturnValue(app.id) - - await appService.save(app) - - const actualApp = await appService.getApp() - - expect(actualApp?.adminApiKey).toEqual(app.adminApiKey) - }) - }) -}) diff --git a/apps/vault/src/vault/core/service/__test__/unit/import.service.spec.ts b/apps/vault/src/vault/core/service/__test__/unit/import.service.spec.ts index 827c718f3..20fa8cdb1 100644 --- a/apps/vault/src/vault/core/service/__test__/unit/import.service.spec.ts +++ b/apps/vault/src/vault/core/service/__test__/unit/import.service.spec.ts @@ -1,3 +1,4 @@ +import { ConfigModule } from '@narval/config-module' import { LoggerModule, MetricService, @@ -7,7 +8,9 @@ import { } from '@narval/nestjs-shared' import { Test, TestingModule } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' +import { load } from '../../../../../main.config' import { Origin, PrivateAccount } from '../../../../../shared/type/domain.type' +import { EncryptionKeyService } from '../../../../../transit-encryption/core/service/encryption-key.service' import { AccountRepository } from '../../../../persistence/repository/account.repository' import { ImportRepository } from '../../../../persistence/repository/import.repository' import { ImportService } from '../../import.service' @@ -27,7 +30,14 @@ describe('ImportService', () => { importRepositoryMock = mock() const module: TestingModule = await Test.createTestingModule({ - imports: [LoggerModule.forTest(), OpenTelemetryModule.forTest()], + imports: [ + LoggerModule.forTest(), + OpenTelemetryModule.forTest(), + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }) + ], providers: [ ImportService, { @@ -49,6 +59,10 @@ describe('ImportService', () => { { provide: KeyGenerationService, useValue: {} + }, + { + provide: EncryptionKeyService, + useValue: mock() } ] }).compile() diff --git a/apps/vault/src/vault/core/service/__test__/unit/key-generation.service.spec.ts b/apps/vault/src/vault/core/service/__test__/unit/key-generation.service.spec.ts index baa34bc03..909460870 100644 --- a/apps/vault/src/vault/core/service/__test__/unit/key-generation.service.spec.ts +++ b/apps/vault/src/vault/core/service/__test__/unit/key-generation.service.spec.ts @@ -5,13 +5,7 @@ import { OpenTelemetryModule, StatefulMetricService } from '@narval/nestjs-shared' -import { - RsaPrivateKey, - generateJwk, - publicKeyToHex, - rsaDecrypt, - secp256k1PrivateKeyToPublicJwk -} from '@narval/signature' +import { Alg, generateJwk, publicKeyToHex, rsaDecrypt, secp256k1PrivateKeyToPublicJwk } from '@narval/signature' import { Test, TestingModule } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' import { v4 as uuid } from 'uuid' @@ -27,12 +21,30 @@ const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47 const clientId = uuid() -// Engine key used to sign the approval request -const clientPublicJWK = secp256k1PrivateKeyToPublicJwk(PRIVATE_KEY) - const client: Client = { clientId, - engineJwk: clientPublicJWK, + auth: { + disabled: true, + local: null, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + verification: { + audience: null, + issuer: null, + maxTokenAge: null, + requireBoundTokens: false, + allowBearerTokens: false, + allowWildcard: null + }, + pinnedPublicKey: null + } + }, + name: 'test-client', + configurationSource: 'dynamic', + backupPublicKey: null, + baseUrl: null, createdAt: new Date(), updatedAt: new Date() } @@ -111,7 +123,7 @@ describe('GenerateService', () => { }) it('returns an encrypted backup if client has an RSA backupKey configured', async () => { - const rsaBackupKey = await generateJwk('RS256') + const rsaBackupKey = await generateJwk(Alg.RS256) clientServiceMock.findById.mockResolvedValue({ ...client, diff --git a/apps/vault/src/vault/core/service/__test__/unit/provision.service.spec.ts b/apps/vault/src/vault/core/service/__test__/unit/provision.service.spec.ts index f93f6c400..5e7ad02ec 100644 --- a/apps/vault/src/vault/core/service/__test__/unit/provision.service.spec.ts +++ b/apps/vault/src/vault/core/service/__test__/unit/provision.service.spec.ts @@ -2,20 +2,22 @@ import { ConfigService } from '@narval/config-module' import { LoggerModule, secret } from '@narval/nestjs-shared' import { Test, TestingModule } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' +import { AppService } from '../../../../../app.service' import { Config } from '../../../../../main.config' +import { ProvisionService } from '../../../../../provision.service' import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { AppRepository } from '../../../../persistence/repository/app.repository' -import { AppService } from '../../app.service' -import { ProvisionService } from '../../provision.service' +import { ProvisionException } from '../../../exception/provision.exception' const mockConfigService = (config: { keyring: Config['keyring']; engineId: string; adminApiKeyHash?: string }) => { const m = mock>() m.get.calledWith('keyring').mockReturnValue(config.keyring) m.get.calledWith('app.id').mockReturnValue(config.engineId) - m.get.calledWith('app.adminApiKeyHash').mockReturnValue(config.adminApiKeyHash) + m.get + .calledWith('app.auth.local') + .mockReturnValue(config.adminApiKeyHash ? { adminApiKeyHash: config.adminApiKeyHash } : null) return m } @@ -23,26 +25,31 @@ const mockConfigService = (config: { keyring: Config['keyring']; engineId: strin describe(ProvisionService.name, () => { let module: TestingModule let provisionService: ProvisionService - let appService: AppService + let appServiceMock: MockProxy let configServiceMock: MockProxy> const config = { engineId: 'test-engine-id', keyring: { type: 'raw', - masterPassword: 'test-master-password' + encryptionMasterPassword: 'test-master-password', + encryptionMasterKey: null, + hmacSecret: 'test-hmac-secret' } satisfies Config['keyring'] } beforeEach(async () => { configServiceMock = mockConfigService(config) + appServiceMock = mock() module = await Test.createTestingModule({ imports: [LoggerModule.forTest()], providers: [ ProvisionService, - AppService, - AppRepository, + { + provide: AppService, + useValue: appServiceMock + }, KeyValueService, { provide: ConfigService, @@ -56,7 +63,6 @@ describe(ProvisionService.name, () => { }).compile() provisionService = module.get(ProvisionService) - appService = module.get(AppService) }) describe('on first boot', () => { @@ -64,18 +70,22 @@ describe(ProvisionService.name, () => { const adminApiKey = 'test-admin-api-key' beforeEach(async () => { - configServiceMock.get.calledWith('app.adminApiKeyHash').mockReturnValue(secret.hash(adminApiKey)) + configServiceMock.get + .calledWith('app.auth.local') + .mockReturnValue({ adminApiKeyHash: secret.hash(adminApiKey) }) + appServiceMock.getApp.mockResolvedValue(null) }) it('saves the activated app', async () => { await provisionService.provision() - const actualApp = await appService.getApp() - - expect(actualApp).toEqual({ + expect(appServiceMock.save).toHaveBeenCalledWith({ id: config.engineId, - adminApiKey: secret.hash(adminApiKey), - masterKey: expect.any(String) + encryptionKeyringType: 'raw', + encryptionMasterKey: expect.any(String), + encryptionMasterAwsKmsArn: undefined, + adminApiKeyHash: secret.hash(adminApiKey), + authDisabled: undefined }) }) }) @@ -84,27 +94,29 @@ describe(ProvisionService.name, () => { it('saves the provisioned app', async () => { await provisionService.provision() - const actualApp = await appService.getApp() - - expect(actualApp?.adminApiKey).toEqual(undefined) - expect(actualApp).toEqual({ + expect(appServiceMock.save).toHaveBeenCalledWith({ id: config.engineId, - masterKey: expect.any(String) + encryptionKeyringType: 'raw', + encryptionMasterKey: expect.any(String), + encryptionMasterAwsKmsArn: undefined, + adminApiKeyHash: null, + authDisabled: undefined }) }) }) }) describe('on boot', () => { - it('skips provision and returns the existing app', async () => { - const actualApp = await appService.save({ + it('fails when masterEncryptionKey is not valid', async () => { + await appServiceMock.getApp.mockResolvedValue({ id: config.engineId, - masterKey: 'test-master-key' + encryptionMasterKey: 'test-master-key', + encryptionKeyringType: 'raw', + encryptionMasterAwsKmsArn: null, + authDisabled: false }) - const engine = await provisionService.provision() - - expect(actualApp).toEqual(engine) + await expect(provisionService.provision()).rejects.toThrow(ProvisionException) }) }) }) diff --git a/apps/vault/src/vault/core/service/app.service.ts b/apps/vault/src/vault/core/service/app.service.ts deleted file mode 100644 index 45bf61e90..000000000 --- a/apps/vault/src/vault/core/service/app.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ConfigService } from '@narval/config-module' -import { Injectable } from '@nestjs/common' -import { Config } from '../../../main.config' -import { App } from '../../../shared/type/domain.type' -import { AppRepository } from '../../persistence/repository/app.repository' -import { AppNotProvisionedException } from '../exception/app-not-provisioned.exception' - -@Injectable() -export class AppService { - constructor( - private configService: ConfigService, - private appRepository: AppRepository - ) {} - - async getAppOrThrow(): Promise { - const app = await this.getApp() - - if (app) { - return app - } - - throw new AppNotProvisionedException() - } - - async getApp(): Promise { - const app = await this.appRepository.findById(this.getId()) - - if (app) { - return app - } - - return null - } - - // IMPORTANT: The admin API key is hashed by the caller not the service. That - // allows us to have a declarative configuration file which is useful for - // automations like development or cloud set up. - async save(app: App): Promise { - await this.appRepository.save(app) - - return app - } - - private getId(): string { - return this.configService.get('app.id') - } -} diff --git a/apps/vault/src/vault/core/service/import.service.ts b/apps/vault/src/vault/core/service/import.service.ts index 5b5a522d0..8658f8634 100644 --- a/apps/vault/src/vault/core/service/import.service.ts +++ b/apps/vault/src/vault/core/service/import.service.ts @@ -1,26 +1,16 @@ import { resourceId } from '@narval/armory-sdk' import { LoggerService, MetricService, OTEL_ATTR_CLIENT_ID } from '@narval/nestjs-shared' import { Hex } from '@narval/policy-engine-shared' -import { - Alg, - RsaPrivateKey, - RsaPublicKey, - generateJwk, - privateKeyToJwk, - publicKeyToHex, - rsaDecrypt, - rsaPrivateKeyToPublicKey -} from '@narval/signature' +import { RsaPublicKey, privateKeyToJwk, publicKeyToHex } from '@narval/signature' import { HttpStatus, Inject, Injectable } from '@nestjs/common' import { Counter } from '@opentelemetry/api' -import { decodeProtectedHeader } from 'jose' import { isHex } from 'viem' import { privateKeyToAddress } from 'viem/accounts' import { ApplicationException } from '../../../shared/exception/application.exception' import { Origin, PrivateAccount } from '../../../shared/type/domain.type' +import { EncryptionKeyService } from '../../../transit-encryption/core/service/encryption-key.service' import { ImportWalletDto } from '../../http/rest/dto/import-wallet.dto' import { AccountRepository } from '../../persistence/repository/account.repository' -import { ImportRepository } from '../../persistence/repository/import.repository' import { getRootKey } from '../util/key-generation.util' import { KeyGenerationService } from './key-generation.service' @@ -32,8 +22,8 @@ export class ImportService { constructor( private accountRepository: AccountRepository, - private importRepository: ImportRepository, private keyGenerationService: KeyGenerationService, + private encryptionKeyService: EncryptionKeyService, private logger: LoggerService, @Inject(MetricService) private metricService: MetricService ) { @@ -42,13 +32,9 @@ export class ImportService { } async generateEncryptionKey(clientId: string): Promise { - const privateKey = await generateJwk(Alg.RS256, { use: 'enc' }) - const publicKey = rsaPrivateKeyToPublicKey(privateKey) + const encryptionKey = await this.encryptionKeyService.generate(clientId) - // Save the privateKey - await this.importRepository.save(clientId, privateKey) - - return publicKey + return encryptionKey.publicKey } async importPrivateKey(clientId: string, privateKey: Hex, accountId?: string): Promise { @@ -72,26 +58,7 @@ export class ImportService { } async #decrypt(clientId: string, encryptedData: string): Promise { - const header = decodeProtectedHeader(encryptedData) - const kid = header.kid - - if (!kid) { - throw new ApplicationException({ - message: 'Missing kid in JWE header', - suggestedHttpStatusCode: HttpStatus.BAD_REQUEST - }) - } - - const encryptionPrivateKey = await this.importRepository.findById(clientId, kid) - - if (!encryptionPrivateKey) { - throw new ApplicationException({ - message: 'Encryption Key Not Found', - suggestedHttpStatusCode: HttpStatus.NOT_FOUND - }) - } - - return rsaDecrypt(encryptedData, encryptionPrivateKey.jwk) + return this.encryptionKeyService.decrypt(clientId, encryptedData) } async importEncryptedPrivateKey( diff --git a/apps/vault/src/vault/core/service/key-generation.service.ts b/apps/vault/src/vault/core/service/key-generation.service.ts index c2ba6d15b..085eb5ce9 100644 --- a/apps/vault/src/vault/core/service/key-generation.service.ts +++ b/apps/vault/src/vault/core/service/key-generation.service.ts @@ -96,7 +96,12 @@ export class KeyGenerationService { }) } - const backup = await this.#maybeEncryptAndSaveBackup(clientId, keyId, mnemonic, client?.backupPublicKey) + const backup = await this.#maybeEncryptAndSaveBackup( + clientId, + keyId, + mnemonic, + client?.backupPublicKey || undefined + ) await this.rootKeyRepository.save(clientId, { keyId, diff --git a/apps/vault/src/vault/core/service/provision.service.ts b/apps/vault/src/vault/core/service/provision.service.ts deleted file mode 100644 index 7bc0edd7d..000000000 --- a/apps/vault/src/vault/core/service/provision.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ConfigService } from '@narval/config-module' -import { generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module' -import { LoggerService } from '@narval/nestjs-shared' -import { Injectable } from '@nestjs/common' -import { Config } from '../../../main.config' -import { App } from '../../../shared/type/domain.type' -import { ProvisionException } from '../exception/provision.exception' -import { AppService } from './app.service' - -@Injectable() -export class ProvisionService { - // IMPORTANT: The provision service establishes encryption. Therefore, you - // cannot have dependencies that rely on encryption to function. If you do, - // you'll ran into an error due to a missing keyring. - // Any process that requires encryption should be handled in the - // BootstrapService. - constructor( - private configService: ConfigService, - private appService: AppService, - private logger: LoggerService - ) {} - - // NOTE: The `adminApiKeyHash` argument is for test convinience in case it - // needs to provision the application. - async provision(adminApiKeyHash?: string): Promise { - const app = await this.appService.getApp() - - const isNotProvisioned = !app || !app.adminApiKey - - if (isNotProvisioned) { - this.logger.log('Start app provision') - - const provisionedApp: App = await this.withMasterKey( - app || { - id: this.getId() - } - ) - - const apiKey = adminApiKeyHash || this.getAdminApiKeyHash() - - if (apiKey) { - this.logger.log('Import admin API key hash') - - return this.appService.save({ - ...provisionedApp, - adminApiKey: apiKey - }) - } - - return this.appService.save(provisionedApp) - } - - this.logger.log('App already provisioned') - - return app - } - - private async withMasterKey(app: App): Promise { - if (app.masterKey) { - this.logger.log('Skip master key set up because it already exists') - - return app - } - - const keyring = this.configService.get('keyring') - - if (keyring.type === 'raw') { - this.logger.log('Generate and save engine master key') - - const { masterPassword } = keyring - const kek = generateKeyEncryptionKey(masterPassword, this.getId()) - const masterKey = await generateMasterKey(kek) - - return { ...app, masterKey } - } else if (keyring.type === 'awskms' && keyring.masterAwsKmsArn) { - this.logger.log('Using AWS KMS for encryption') - - return app - } else { - throw new ProvisionException('Unsupported keyring type') - } - } - - private getAdminApiKeyHash(): string | undefined { - return this.configService.get('app.adminApiKeyHash') - } - - private getId(): string { - return this.configService.get('app.id') - } -} diff --git a/apps/vault/src/vault/http/rest/controller/encryption-key.controller.ts b/apps/vault/src/vault/http/rest/controller/encryption-key.controller.ts deleted file mode 100644 index 2d977fa28..000000000 --- a/apps/vault/src/vault/http/rest/controller/encryption-key.controller.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Permission } from '@narval/armory-sdk' -import { REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared' -import { Controller, HttpStatus, Post } from '@nestjs/common' -import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' -import { ClientId } from '../../../../shared/decorator/client-id.decorator' -import { PermissionGuard } from '../../../../shared/decorator/permission-guard.decorator' -import { ImportService } from '../../../core/service/import.service' -import { EncryptionKeyDto } from '../dto/encryption-key.dto' - -@Controller({ - path: '/encryption-keys', - version: '1' -}) -@PermissionGuard(Permission.WALLET_IMPORT) -@ApiTags('Encryption Key') -@ApiHeader({ - name: REQUEST_HEADER_CLIENT_ID, - required: true -}) -export class EncryptionKeyController { - constructor(private importService: ImportService) {} - - @Post() - @ApiOperation({ - summary: 'Generates an encryption key pair used to secure end-to-end communication containing sensitive information' - }) - @ApiResponse({ - status: HttpStatus.CREATED, - type: EncryptionKeyDto - }) - async generate(@ClientId() clientId: string): Promise { - const publicKey = await this.importService.generateEncryptionKey(clientId) - - return EncryptionKeyDto.create({ publicKey }) - } -} diff --git a/apps/vault/src/vault/http/rest/controller/provision.controller.ts b/apps/vault/src/vault/http/rest/controller/provision.controller.ts deleted file mode 100644 index 87d46ceb1..000000000 --- a/apps/vault/src/vault/http/rest/controller/provision.controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { secret } from '@narval/nestjs-shared' -import { Controller, Post } from '@nestjs/common' -import { ApiExcludeController } from '@nestjs/swagger' -import { AppService } from '../../../core/service/app.service' - -type Response = - | { state: 'ACTIVATED' } - | { - state: 'READY' - app: { - appId: string - adminApiKey?: string - } - } - -@Controller({ - path: '/apps/activate', - version: '1' -}) -@ApiExcludeController() -export class ProvisionController { - constructor(private appService: AppService) {} - - @Post() - async provision(): Promise { - const app = await this.appService.getAppOrThrow() - - if (app.adminApiKey) { - return { state: 'ACTIVATED' } - } - - const adminApiKey = secret.generate() - - await this.appService.save({ - ...app, - adminApiKey: secret.hash(adminApiKey) - }) - - return { - state: 'READY', - app: { - appId: app.id, - adminApiKey - } - } - } -} diff --git a/apps/vault/src/vault/http/rest/dto/encryption-key.dto.ts b/apps/vault/src/vault/http/rest/dto/encryption-key.dto.ts deleted file mode 100644 index b9cbf7d07..000000000 --- a/apps/vault/src/vault/http/rest/dto/encryption-key.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { rsaPublicKeySchema } from '@narval/signature' -import { createZodDto } from 'nestjs-zod' -import { z } from 'zod' - -export class EncryptionKeyDto extends createZodDto( - z.object({ - publicKey: rsaPublicKeySchema - }) -) {} diff --git a/apps/vault/src/vault/persistence/repository/__test__/unit/app.repository.spec.ts b/apps/vault/src/vault/persistence/repository/__test__/unit/app.repository.spec.ts deleted file mode 100644 index e0b3c8286..000000000 --- a/apps/vault/src/vault/persistence/repository/__test__/unit/app.repository.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { EncryptionModule } from '@narval/encryption-module' -import { Test } from '@nestjs/testing' -import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' -import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' -import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing' -import { App } from '../../../../../shared/type/domain.type' -import { AppRepository } from '../../app.repository' - -describe(AppRepository.name, () => { - let repository: AppRepository - let inMemoryKeyValueRepository: InMemoryKeyValueRepository - - beforeEach(async () => { - inMemoryKeyValueRepository = new InMemoryKeyValueRepository() - - const module = await Test.createTestingModule({ - imports: [ - EncryptionModule.register({ - keyring: getTestRawAesKeyring() - }) - ], - providers: [ - KeyValueService, - AppRepository, - { - provide: KeyValueRepository, - useValue: inMemoryKeyValueRepository - } - ] - }).compile() - - repository = module.get(AppRepository) - }) - - describe('save', () => { - const app: App = { - id: 'test-app-id', - adminApiKey: 'unsafe-test-admin-api-key', - masterKey: 'unsafe-test-master-key' - } - - it('saves a new app', async () => { - await repository.save(app) - - const value = await inMemoryKeyValueRepository.get(repository.getKey(app.id)) - const actualApp = await repository.findById(app.id) - - expect(value).not.toEqual(null) - expect(app).toEqual(actualApp) - }) - }) -}) diff --git a/apps/vault/src/vault/persistence/repository/app.repository.ts b/apps/vault/src/vault/persistence/repository/app.repository.ts deleted file mode 100644 index 11389f1d4..000000000 --- a/apps/vault/src/vault/persistence/repository/app.repository.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { coerce } from '@narval/nestjs-shared' -import { Injectable } from '@nestjs/common' -import { KeyMetadata } from '../../../shared/module/key-value/core/repository/key-value.repository' -import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service' -import { App, Collection } from '../../../shared/type/domain.type' - -@Injectable() -export class AppRepository { - constructor(private keyValueService: KeyValueService) {} - - private KEY_PREFIX = Collection.APP - getMetadata(): KeyMetadata { - return { - collection: Collection.APP - } - } - - async findById(id: string): Promise { - const value = await this.keyValueService.get(this.getKey(id)) - - if (value) { - return coerce.decode(App, value) - } - - return null - } - - async save(app: App): Promise { - await this.keyValueService.set(this.getKey(app.id), coerce.encode(App, app), this.getMetadata()) - - return app - } - - getKey(id: string): string { - return `${this.KEY_PREFIX}:${id}` - } -} diff --git a/apps/vault/src/vault/vault.module.ts b/apps/vault/src/vault/vault.module.ts index d024d690c..edb697513 100644 --- a/apps/vault/src/vault/vault.module.ts +++ b/apps/vault/src/vault/vault.module.ts @@ -1,71 +1,43 @@ -import { ConfigService } from '@narval/config-module' -import { EncryptionModule } from '@narval/encryption-module' -import { LoggerService, OpenTelemetryModule, TrackClientIdMiddleware } from '@narval/nestjs-shared' +import { TrackClientIdMiddleware } from '@narval/nestjs-shared' import { HttpModule } from '@nestjs/axios' -import { MiddlewareConsumer, Module, ValidationPipe, forwardRef } from '@nestjs/common' +import { MiddlewareConsumer, Module, ValidationPipe } from '@nestjs/common' import { APP_FILTER, APP_PIPE } from '@nestjs/core' +import { AppRepository } from '../app.repository' import { ClientModule } from '../client/client.module' -import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory' import { ApplicationExceptionFilter } from '../shared/filter/application-exception.filter' import { ZodExceptionFilter } from '../shared/filter/zod-exception.filter' import { NonceGuard } from '../shared/guard/nonce.guard' import { KeyValueModule } from '../shared/module/key-value/key-value.module' import { PersistenceModule } from '../shared/module/persistence/persistence.module' +import { TransitEncryptionModule } from '../transit-encryption/transit-encryption.module' import { AdminService } from './core/service/admin.service' -import { AppService } from './core/service/app.service' import { ImportService } from './core/service/import.service' import { KeyGenerationService } from './core/service/key-generation.service' import { NonceService } from './core/service/nonce.service' -import { ProvisionService } from './core/service/provision.service' import { SigningService } from './core/service/signing.service' import { AccountController } from './http/rest/controller/account.controller' -import { EncryptionKeyController } from './http/rest/controller/encryption-key.controller' -import { ProvisionController } from './http/rest/controller/provision.controller' import { SignController } from './http/rest/controller/sign.controller' import { WalletController } from './http/rest/controller/wallet.controller' import { AccountRepository } from './persistence/repository/account.repository' -import { AppRepository } from './persistence/repository/app.repository' import { BackupRepository } from './persistence/repository/backup.repository' import { ImportRepository } from './persistence/repository/import.repository' import { RootKeyRepository } from './persistence/repository/root-key.repository' import { VaultController } from './vault.controller' -import { VaultService } from './vault.service' @Module({ - imports: [ - HttpModule, - PersistenceModule, - forwardRef(() => KeyValueModule), - EncryptionModule.registerAsync({ - imports: [VaultModule], - inject: [ConfigService, AppService, LoggerService], - useClass: EncryptionModuleOptionFactory - }), - forwardRef(() => ClientModule), - OpenTelemetryModule.forRoot() - ], - controllers: [ - VaultController, - WalletController, - SignController, - ProvisionController, - AccountController, - EncryptionKeyController - ], + imports: [HttpModule, PersistenceModule, KeyValueModule, ClientModule, TransitEncryptionModule], + controllers: [VaultController, WalletController, SignController, AccountController], providers: [ AppRepository, - AppService, AdminService, ImportRepository, ImportService, NonceGuard, NonceService, - ProvisionService, SigningService, KeyGenerationService, RootKeyRepository, BackupRepository, - VaultService, AccountRepository, { provide: APP_PIPE, @@ -80,7 +52,7 @@ import { VaultService } from './vault.service' useClass: ZodExceptionFilter } ], - exports: [AppService, ProvisionService] + exports: [] }) export class VaultModule { configure(consumer: MiddlewareConsumer): void { diff --git a/apps/vault/src/vault/vault.service.ts b/apps/vault/src/vault/vault.service.ts deleted file mode 100644 index 8993d3064..000000000 --- a/apps/vault/src/vault/vault.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EvaluationResponse } from '@narval/policy-engine-shared' -import { Injectable } from '@nestjs/common' -import { SigningService } from './core/service/signing.service' - -@Injectable() -export class VaultService { - constructor(private signingService: SigningService) {} - - async sign(): Promise { - return null - } -} diff --git a/apps/vault/test/nock.util.ts b/apps/vault/test/nock.util.ts new file mode 100644 index 000000000..b769afb2d --- /dev/null +++ b/apps/vault/test/nock.util.ts @@ -0,0 +1,66 @@ +import nock from 'nock' + +type Matcher = string | RegExp | ((host: string) => boolean) + +// Enable outbound HTTP requests to 127.0.0.1 to allow E2E tests with +// supertestwith supertest to work. +let allowedOutboundHttp: Matcher = '127.0.0.1' + +let originalAllowedOutboundHttp: Matcher + +// Store the original state to restore it later. +let originalNetConnect: boolean + +export const getAllowedOutboundHttp = () => allowedOutboundHttp + +export const setAllowedOutboundHttp = (matcher: Matcher) => (allowedOutboundHttp = matcher) + +export const setupNock = () => { + // Disable outgoing HTTP requests to avoid flaky tests. + nock.disableNetConnect() + + nock.enableNetConnect(getAllowedOutboundHttp()) + + // Jest sometimes translates unmatched errors into obscure JSON circular + // dependency without a proper stack trace. This can lead to hours of + // debugging. To save time, this emitter will consistently log an unmatched + // event allowing engineers to quickly identify the source of the error. + nock.emitter.on('no match', (request) => { + if (request.host && request.host.includes(getAllowedOutboundHttp())) { + return + } + + if (request.hostname && request.hostname.includes(getAllowedOutboundHttp())) { + return + } + + // eslint-disable-next-line no-console + console.error('Nock: no match for request', request) + }) +} + +export const disableNockProtection = () => { + // Store original state + originalNetConnect = nock.isActive() + originalAllowedOutboundHttp = getAllowedOutboundHttp() + + // Disable nock restrictions + nock.cleanAll() + nock.restore() + nock.enableNetConnect() +} + +export const restoreNockProtection = () => { + // Clean up any pending nocks + nock.cleanAll() + + // Restore original state + if (originalNetConnect) { + nock.activate() + nock.disableNetConnect() + nock.enableNetConnect(originalAllowedOutboundHttp) + } + + // Re-run the original setup + setupNock() +} diff --git a/config/policy-engine-config.example.yaml b/config/policy-engine-config.example.yaml new file mode 100644 index 000000000..f912992d7 --- /dev/null +++ b/config/policy-engine-config.example.yaml @@ -0,0 +1,168 @@ +# Policy Engine config.yaml, v1 + +##### +# THIS FILE CONTAINS SECRETS; DO NOT COMMIT OUR CONFIG TO SOURCE CONTROL. +##### + +# Config File Version +version: '1' + +# Core service configuration +env: development # Enum: development, test, production +port: 3010 +cors: [] +# Base URL where the Engine is deployed. Used to verify jwsd request signatures. +baseUrl: http://localhost:3010 +# Internal path to open-policy-agent resource files. Should always be this. Only change if you know what you're doing. +resourcePath: './apps/policy-engine/src/resource' + +# Application identity and security +app: + id: local-dev-engine-instance-1 + + auth: + # Disable all auth; only useful in dev + disabled: false + # Local authentication options, either signed requests or basic api key + local: + # [optional]: Sets the admin API key for client provisioning operations + # + # Key should be hashed, like this: `echo -n "engine-admin-api-key" | openssl dgst -sha256 | awk '{print $2}'` + # Plain text API key: engine-admin-api-key + adminApiKeyHash: 'dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c' + +# Encryption Key management configuration +keyring: + # Either "raw" or "awskms" + type: raw + # If type=raw: + # A master password that uses PBKDF2 to derive the Key Encryption Key (KEK) used to encrypt the master key. + encryptionMasterPassword: unsafe-local-dev-master-password + # If type=raw, you can set the master key to be used here. + # It must be an AES-256 key encoded as a hex string, encrypted by the KEK derived from the masterPassword. Ensure you wrap the string in quotes otherwise it will be interpreted as a number. + # If not set, it will be generated during first-boot. + encryptionMasterKey: '0x02057802bb22a3d6a22dd01bbb71238992e70830577198a972c97d681467f2575a55f8009800030003617070001861726d6f72792e656e6372797074696f6e2d6d6f64756c6500156177732d63727970746f2d7075626c69632d6b65790044413464467572374b485a7561566a2f5341384d6e4741324a706b52614a756d71386f4162374537735367344470534a317756726f4e6e6175586f796a6c6d384a72513d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000cb65c5e0f2e79a2fe104c8f100030b4d4e54b0b481bbc7110101fccd9514906cbfbf32549ef9b1c4578c0d4e64ca5ee57ea534bfdcb5c0aaacd41c52dba8902000010004398f6b5221c1a59738bbbcfa23457d2df8180d1929124c8e6e70b95f4e86319c22e0d2dc620c9033dd10dd97c556efeffffffff00000001000000000000000000000001000000207e92b845f7374c08d155138746061a30a2991e5cdc14d420c389185c0631e6888cf7404e677af76f3b2d00ee348d1bda0067306502304a210311734831cc24b1536acf42a9c3f69221173de1a2610c98b2c678d573b3c475aab6a17bd83f25cc229740d720c4023100fbfedabec5166003ec6584dcecf871dae0d48b6031fe2931f8f3489081893f7c80e5fbbcf6e62c844fa8f41e2e1efe7f' + + # If type=awskms: + # The ARN of the AWS KMS key used as the Master Key. + encryptionMasterAwsKmsArn: null + + # HMAC secret for integrity verification of data in the database. Ensure you wrap the string in quotes otherwise it will be interpreted as a number. + hmacSecret: '4ce6d3404a13c971202693b669668e49e2d6d7e190428d0edd99d1cec1536efb' + +# Decision Attestation configuration +decisionAttestation: + protocol: simple # Either "simple" or "mpc" + + # Only required when protocol=mpc + # This is the TSM SDK node configuration & requires the private MPC library to be installed. + # tsm: + # url: https://tsm.example.com + # apiKey: tsm-api-key-here + # playerCount: 3 + +# Declarative clients (tenants) +clients: + # Each key is the client ID + dev-client-1: + name: 'Example Client' + # Override default baseUrl for this client, used for verifying domain-specific signatures + baseUrl: 'http://localhost:3010' # [optional] + + # Whether to identify the principal from the request authentication signature + # If false, the principal id is provided. + identifyPrincipalFromRequest: true # TODO: implement + + # The Client's Signer, used for generating the signatures on Policy Decisions. + decisionAttestation: + # Whether to turn off signed policy decisions + disabled: false + signer: + # [optional] The signing algorithm to sign with. Defaults to EIP191. + alg: 'EIP191' # or 'ES256K', 'ES256', 'EDDSA', 'RS256' #TODO: actually support the others! + # [optional] Unique identifier for the signer key. + # If only keyId is set, then the signing key will be generated using this for the kid. + keyId: 'key-2024-1' + # [optional] The public key for the signing key. Can not be used with `mpc` signing.protocol. + publicKey: + kid: 'key-2024-1' # If keyId is set, this should match. + kty: 'EC' + alg: 'ES256K' + crv: 'secp256k1' + x: 'kmDs8BM_h4YRZfUMGQhD5E9Ih8ZLOb6vdSx5aPVMdKY' + y: 'wqUIXS3YmZXxM_TRDUyTGE5pInwTJxCRtvRzjwRu32o' + # [optional] The private key for the signing key. Can not be used with `mpc` signing.protocol. + privateKey: + kid: 'key-2024-1' # If keyId is set, this should match. + kty: 'EC' + alg: 'ES256K' + crv: 'secp256k1' + x: 'kmDs8BM_h4YRZfUMGQhD5E9Ih8ZLOb6vdSx5aPVMdKY' + y: 'wqUIXS3YmZXxM_TRDUyTGE5pInwTJxCRtvRzjwRu32o' + d: 'hYCWM5W73pwRA0OqNKwMok8H1k_9OCxZOiYWhYI4b4Q' + + # TODO: Add jwt signing options here + + auth: + # Disable all auth; only useful in dev + disabled: false + # Local authentication options, either signed requests or basic api key + local: + # [optional] SHA256 hash of the client secret, passed in `x-client-secret` header + # echo -n "engine-client-secret" | openssl dgst -sha256 | awk '{print $2}' + # TODO: Do we even need this? + clientSecret: 'd8c56539ad31ecb12a1e7334b341a7b3d46dea0b076421add198325efb77f583' + + # TODO: Add other auth & token validation details here + + dataStore: + # Entity Data Store configuration - this is context data used in policy evaluation. + entity: + # The URL to fetch to load the Entity Data. It must include any authentication secrets in the url. + data: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/entities?clientId=client-123&dataSecret=data-secret-plaintext + # The URL to fetch for Signature of the Entity Data. + # Typically the same as the Data url, but possible to store the attestation signature separately. + signature: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/entities?clientId=client-123&dataSecret=data-secret-plaintext + # The pinned public keys for verifying the Entity signature + publicKeys: + - kid: 'data-key-1' # Must match the kid the signer puts in signature jwt header + kty: EC + crv: secp256k1 + alg: ES256K + use: sig + # Can use an evm-encoded address instead of x+y, if signing data w/ a wallet (e.g. Metamask) using EIP191 + addr: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B' + # If you have the full public key, include the x+y even if also using Addr. + x: null + y: null + # Policy Data Store configuration - this is the policy set used for evaluation. + policy: + # The URL to fetch to load the Policy Data. It must include any authentication secrets in the url. + data: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/policies?clientId=client-123&dataSecret=data-secret-plaintext + # The URL to fetch for Signature of the Policy Data. + # Typically the same as the Data url, but possible to store the attestation signature separately. + signature: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/policies?clientId=client-123&dataSecret=data-secret-plaintext + # The pinned public keys for verifying the Policy signature + publicKeys: + - kid: 'data-key-1' # Must match the kid the signer puts in signature jwt header + kty: EC + crv: secp256k1 + alg: ES256K + use: sig + # Can use an evm-encoded address instead of x+y, if signing data w/ a wallet (e.g. Metamask) using EIP191 + addr: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B' + # If you have the full public key, include the x+y even if also using Addr. + x: null + y: null diff --git a/config/policy-engine-config.template.yaml b/config/policy-engine-config.template.yaml new file mode 100644 index 000000000..b8c9b14e4 --- /dev/null +++ b/config/policy-engine-config.template.yaml @@ -0,0 +1,179 @@ +# Policy Engine config.yaml, v1 + +##### +# THIS FILE CONTAINS SECRETS; DO NOT COMMIT OUR CONFIG TO SOURCE CONTROL. +##### + +# Config File Version +version: '1' + +# Core service configuration +env: development # Enum: development, test, production +port: 3010 +cors: [] +# Base URL where the Engine is deployed. Used to verify jwsd request signatures. +baseUrl: http://localhost:3010 +# Internal path to open-policy-agent resource files. Should always be this. Only change if you know what you're doing. +resourcePath: './apps/policy-engine/src/resource' + +# Database configuration. +# database: +# url: postgresql://postgres:postgres@localhost:5432/engine?schema=public + +# Application identity and security +app: + id: local-dev-engine-instance-1 + + auth: + # Disable all auth; only useful in dev + disabled: false + # OIDC configuration to use an external auth provider + oidc: null + # Local authentication options, either signed requests or basic api key + local: + # [optional]: Sets the admin API key for client provisioning operations + # + # Key should be hashed, like this: `echo -n "engine-admin-api-key" | openssl dgst -sha256 | awk '{print $2}'` + # Plain text API key: engine-admin-api-key + adminApiKeyHash: 'dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c' + # Auth config for requests from this service + outgoing: null + +# Encryption Key management configuration +keyring: + # Either "raw" or "awskms" + type: raw + # If type=raw: + # A master password that uses PBKDF2 to derive the Key Encryption Key (KEK) used to encrypt the master key. + encryptionMasterPassword: unsafe-local-dev-master-password + # Encryption master key: + # - Required if type=raw + # - Must be an AES-256 key encoded as hex string + # - Must be encrypted by KEK derived from masterPassword + # - Must be quoted to prevent number interpretation + # - Will auto-generate on first-boot if not set + # encryptionMasterKey: "0x02057802bb22a3d6a22dd01bbb71238992e70830577198a972c97d681467f2575a55f8009800030003617070001861726d6f72792e656e6372797074696f6e2d6d6f64756c6500156177732d63727970746f2d7075626c69632d6b65790044413464467572374b485a7561566a2f5341384d6e4741324a706b52614a756d71386f4162374537735367344470534a317756726f4e6e6175586f796a6c6d384a72513d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000cb65c5e0f2e79a2fe104c8f100030b4d4e54b0b481bbc7110101fccd9514906cbfbf32549ef9b1c4578c0d4e64ca5ee57ea534bfdcb5c0aaacd41c52dba8902000010004398f6b5221c1a59738bbbcfa23457d2df8180d1929124c8e6e70b95f4e86319c22e0d2dc620c9033dd10dd97c556efeffffffff00000001000000000000000000000001000000207e92b845f7374c08d155138746061a30a2991e5cdc14d420c389185c0631e6888cf7404e677af76f3b2d00ee348d1bda0067306502304a210311734831cc24b1536acf42a9c3f69221173de1a2610c98b2c678d573b3c475aab6a17bd83f25cc229740d720c4023100fbfedabec5166003ec6584dcecf871dae0d48b6031fe2931f8f3489081893f7c80e5fbbcf6e62c844fa8f41e2e1efe7f" + + # If type=awskms: + # The ARN of the AWS KMS key used as the Master Key. + encryptionMasterAwsKmsArn: null + + # HMAC secret for integrity verification of data in the database. Ensure you wrap the string in quotes otherwise it will be interpreted as a number. + hmacSecret: '4ce6d3404a13c971202693b669668e49e2d6d7e190428d0edd99d1cec1536efb' + +# Decision Attestation configuration +decisionAttestation: + protocol: simple # Either "simple" or "mpc" + + # Only required when protocol=mpc + # This is the TSM SDK node configuration & requires the private MPC library to be installed. + # tsm: + # url: https://tsm.example.com + # apiKey: tsm-api-key-here + # playerCount: 3 + +# Declarative clients (tenants) +clients: + # Each key is the client ID + dev-client-1: + name: 'Example Client' + # Override default baseUrl for this client, used for verifying domain-specific signatures + baseUrl: 'http://localhost:3010' # [optional] + + # Whether to identify the principal from the request authentication signature + # If false, the principal id is provided. + identifyPrincipalFromRequest: true # TODO: implement + + # The Client's Signer, used for generating the signatures on Policy Decisions. + decisionAttestation: + # Whether to turn off signed policy decisions + disabled: false + signer: + # [optional] The signing algorithm to sign with. Defaults to EIP191. + alg: 'EIP191' # or 'ES256K', 'ES256', 'EDDSA', 'RS256' #TODO: actually support the others! + # [optional] Unique identifier for the signer key. + # If only keyId is set, then the signing key will be generated using this for the kid. + keyId: 'key-2024-1' + # [optional] The public key for the signing key. Can not be used with `mpc` signing.protocol. + publicKey: + kid: 'key-2024-1' # If keyId is set, this should match. + kty: 'EC' + alg: 'ES256K' + crv: 'secp256k1' + x: 'kmDs8BM_h4YRZfUMGQhD5E9Ih8ZLOb6vdSx5aPVMdKY' + y: 'wqUIXS3YmZXxM_TRDUyTGE5pInwTJxCRtvRzjwRu32o' + # [optional] The private key for the signing key. Can not be used with `mpc` signing.protocol. + privateKey: + kid: 'key-2024-1' # If keyId is set, this should match. + kty: 'EC' + alg: 'ES256K' + crv: 'secp256k1' + x: 'kmDs8BM_h4YRZfUMGQhD5E9Ih8ZLOb6vdSx5aPVMdKY' + y: 'wqUIXS3YmZXxM_TRDUyTGE5pInwTJxCRtvRzjwRu32o' + d: 'hYCWM5W73pwRA0OqNKwMok8H1k_9OCxZOiYWhYI4b4Q' + + # TODO: Add jwt signing options here + + auth: + # Disable all auth; only useful in dev + disabled: false + # Local authentication options, either signed requests or basic api key + local: + # [optional] SHA256 hash of the client secret, passed in `x-client-secret` header + # echo -n "engine-client-secret" | openssl dgst -sha256 | awk '{print $2}' + # TODO: Remove; do we even need this? + clientSecret: 'd8c56539ad31ecb12a1e7334b341a7b3d46dea0b076421add198325efb77f583' + + # TODO: Add other auth & token validation details here + + dataStore: + # Entity Data Store configuration - this is context data used in policy evaluation. + entity: + # The URL to fetch to load the Entity Data. It must include any authentication secrets in the url. + data: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/entities?clientId=client-123&dataSecret=data-secret-plaintext + # The URL to fetch for Signature of the Entity Data. + # Typically the same as the Data url, but possible to store the attestation signature separately. + signature: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/entities?clientId=client-123&dataSecret=data-secret-plaintext + # The pinned public keys for verifying the Entity signature + publicKeys: + - kid: 'data-key-1' # Must match the kid the signer puts in signature jwt header + kty: EC + crv: secp256k1 + alg: ES256K + use: sig + # Can use an evm-encoded address instead of x+y, if signing data w/ a wallet (e.g. Metamask) using EIP191 + addr: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B' + # If you have the full public key, include the x+y even if also using Addr. + x: null + y: null + # Policy Data Store configuration - this is the policy set used for evaluation. + policy: + # The URL to fetch to load the Policy Data. It must include any authentication secrets in the url. + data: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/policies?clientId=client-123&dataSecret=data-secret-plaintext + # The URL to fetch for Signature of the Policy Data. + # Typically the same as the Data url, but possible to store the attestation signature separately. + signature: + type: HTTP # or HTTPS + # The dataSecret here must match the dataSecret in the Armory config.yaml + url: http://localhost:3005/v1/data/policies?clientId=client-123&dataSecret=data-secret-plaintext + # The pinned public keys for verifying the Policy signature + publicKeys: + - kid: 'data-key-1' # Must match the kid the signer puts in signature jwt header + kty: EC + crv: secp256k1 + alg: ES256K + use: sig + # Can use an evm-encoded address instead of x+y, if signing data w/ a wallet (e.g. Metamask) using EIP191 + addr: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B' + # If you have the full public key, include the x+y even if also using Addr. + x: null + y: null diff --git a/config/vault-config.example.yaml b/config/vault-config.example.yaml new file mode 100644 index 000000000..94a67dc62 --- /dev/null +++ b/config/vault-config.example.yaml @@ -0,0 +1,155 @@ +# Vault config.yaml, v1 + +##### +# THIS FILE CONTAINS SECRETS; DO NOT COMMIT OUR CONFIG TO SOURCE CONTROL. +##### + +# Config File Version +version: '1' + +# Core service configuration +env: development # Enum: development, test, production +port: 3011 +cors: [] +# Base URL where the Vault is deployed. Used to verify jwsd request signatures. +baseUrl: http://localhost:3011 + +# Application identity and security +app: + id: local-dev-vault-instance-1 + + auth: + # Disable all auth; only useful in dev + disabled: false + # OIDC configuration to use an external auth provider + oidc: null + # Local authentication options, either signed requests or basic api key + local: + # [optional]: Sets the admin API key for client provisioning operations + # + # Key should be hashed, like this: `echo -n "vault-admin-api-key" | openssl dgst -sha256 | awk '{print $2}'` + # Plain text API key: vault-admin-api-key + adminApiKeyHash: d4a6b4c1cb71dbdb68a1dd429ad737369f74b9e264b9dfa639258753987caaad + # Auth config for requests from this service + outgoing: null + +# Encryption Key management configuration +keyring: + # Either "raw" or "awskms" + type: raw + # If type=raw: + # A master password that uses PBKDF2 to derive the Key Encryption Key (KEK) used to encrypt the master key. + encryptionMasterPassword: unsafe-local-dev-master-password + # If type=raw, you can set the master key to be used here. + # It must be an AES-256 key encoded as a hex string, encrypted by the KEK derived from the masterPassword. Ensure you wrap the string in quotes otherwise it will be interpreted as a number. + # If not set, it will be generated during first-boot. + encryptionMasterKey: '0x020578c93e6e65c27de1dddd752a83c6f50a235d90243e15be346c360d2c9d9f64ba17009800030003617070001861726d6f72792e656e6372797074696f6e2d6d6f64756c6500156177732d63727970746f2d7075626c69632d6b6579004441364a5577556f6f6f63476e752b6e6a465477414d56576a516f304b72447a34615a325761515861375236354330464d6567352f69556e4a31386f6a7169533877413d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000c8ea6c76bba04c350fdd918b50030f5de1f7910661ae891dd5ce7d3b2692c0ae51916e08bfe4125842f259ed5bf6a2c9d5e3ec06411fcbc840805623da471020000100077f11eac1ff728005f6870ffe8864c1618aacbc5cc35879a0a59cfdafac822e4c852bb9e4c70ce8c18f8f02067a5641dffffffff00000001000000000000000000000001000000202ed04425860a9f6645bcac8616979af1ff484edf661a2501c85e20126c1530750784fc11ca5f2e660b11930c500c3d870068306602310098765ed9afdba77cccc53d493a99b8553873511e904175dcda86007eccccf0b660273e451fc2b900f3e7a60133d08c14023100f196c9312409263a59ee9ca227698e819887ef53798bf74b523cf9d8ecf909ae32c8d4c055907b3051a810a45a0f6ff7' + + # If type=awskms: + # The ARN of the AWS KMS key used as the Master Key. + encryptionMasterAwsKmsArn: null + + # HMAC secret for integrity verification of data in the database. Ensure you wrap the string in quotes otherwise it will be interpreted as a number. + hmacSecret: '4ce6d3404a13c971202693b669668e49e2d6d7e190428d0edd99d1cec1536efb' + +# Declarative clients (tenants) +clients: + # Each key is the client ID + dev-client-1: + name: 'Example Client' + # Override default baseUrl for this client, used for verifying jwsd/httpsig + baseUrl: 'http://localhost:3011' # [optional] + + auth: + # Disable all auth; only useful in dev + disabled: false + # OIDC configuration to use an external auth provider + oidc: null # TODO: add OIDC + # Local authentication options, either signed requests or basic api key + local: + # HTTP Signing configuration - this is for Service-level authentication. + httpSigning: + # Settings for when THIS service verifies incoming requests + # List of accepted proof methods, supporting httpsig & jwsd. Set null if verification is disabled. + methods: + # Detached JWS + jwsd: + maxAge: 300 + requiredComponents: + - 'htm' # HTTP method + - 'uri' # Target URI + - 'created' # Created timestamp + - 'ath' # Authorization (accessToken) + + ## + # [optional]: Restrict to specific user credentials + # If set, ONLY these keys will be accepted. + # Otherwise, any key can authenticate if it matches the authorized credential in a bound token. + ## + # [optional] URL of the JWKS endpoint that includes any keys that are valid "users" + # allowedUsersJwksUrl: https://armory/.well-known/jwks.json + + # [optional]: List of allowed users, pinning specific keys instead of jwskUr + allowedUsers: + ## Private Key for the below Public Key: + # kty: 'OKP' + # crv: 'Ed25519' + # alg: 'EDDSA' + # kid: '0x153ca7ec6216e131a7872c0330872d81c5388d0a3977960a9ee12267e3fc6b94' + # x: 'P8RMuLMuHwb13OfphR_zcvNsKvZN8kCawYeYtlD9aqI' + # d: 'CmvVBDCA64Oo26jeGTTUHaBSJJ2TuRiWGc7ILn4Z6K8' + + # Hex Private Key: 0x0a6bd5043080eb83a8dba8de1934d41da052249d93b9189619cec82e7e19e8af + # Hex Public Key: 0x3fc44cb8b32e1f06f5dce7e9851ff372f36c2af64df2409ac18798b650fd6aa2 + ## + - userId: dev-user-1 + publicKey: + kty: 'OKP' + crv: 'Ed25519' + alg: 'EDDSA' + kid: '0x153ca7ec6216e131a7872c0330872d81c5388d0a3977960a9ee12267e3fc6b94' + x: 'P8RMuLMuHwb13OfphR_zcvNsKvZN8kCawYeYtlD9aqI' + + # Authorization options, typically this will be used to pin the policy engine's public signing key. + tokenValidation: + # (optional) if you want to disable authorization for this client. DO NOT USE IN PRODUCTION. + disabled: true + # The GNAP Authorization Server that issues tokens + url: 'http://armory' + # [optional] JWKS endpoint for the auth server; used for jwt verification. Or you can pin the key. + # jwksUrl: https://policy-engine/.well-known/jwks.json + + # [optional] Pinned public key; used for jwt verification. + publicKey: + kid: 'ZvO6VhPB2Ebe6qmk2RH5ly6qOb2O' # armory staging dev-client-1 + kty: 'EC' + crv: 'secp256k1' + alg: 'ES256K' + x: 'PismhbGayPpeTjyxi021v6Z3gwaVak1A7l8PQC337XM' + y: 'I2pO8D3v9CFgcZo0ej9jhFHwI3QSVFlaw3l4Gx8QbGI' + + verification: + # JWT verification options + audience: null # [optional] Expected audience in JWTs + issuer: 'dev-client-1.armory.narval.xyz' # [optional] Expected issuer in JWTs + maxTokenAge: 600 # [optional] Maximum age of JWTs in seconds + # Whether tokens must be bound to signing keys + requireBoundTokens: true + # Whether to accept bearer tokens + allowBearerTokens: false + # [optional] Paths that can be omitted from request hashing + allowWildcard: + - 'transactionRequest.maxPriorityFeePerGas' + - 'transactionRequest.maxFeePerGas' + - 'transactionRequest.gas' + - 'transactionRequest.gasPrice' + - 'transactionRequest.nonce' + # Auth config for requests from this service + outgoing: null + + # # Backup key export options + # backupPublicKey: # [optional] RSA public key for backups + # kid: "backup-2024-1" + # kty: "RSA" + # n: "..." + # e: "AQAB" diff --git a/config/vault-config.template.yaml b/config/vault-config.template.yaml new file mode 100644 index 000000000..08625613b --- /dev/null +++ b/config/vault-config.template.yaml @@ -0,0 +1,154 @@ +# Vault config.yaml, v1 + +##### +# THIS FILE CONTAINS SECRETS; DO NOT COMMIT OUR CONFIG TO SOURCE CONTROL. +##### + +# Config File Version +version: '1' + +# Core service configuration +env: development # Enum: development, test, production +port: 3011 +cors: [] +# Base URL where the Vault is deployed. Used to verify jwsd request signatures. +baseUrl: http://localhost:3011 + +# Database configuration +database: + url: postgresql://postgres:postgres@localhost:5432/vault?schema=public + +# Application identity and security +app: + id: local-dev-vault-instance-1 + + auth: + # Disable all auth; only useful in dev + disabled: false + # OIDC configuration to use an external auth provider + oidc: null + # Local authentication options, either signed requests or basic api key + local: + # [optional]: Sets the admin API key for client provisioning operations + # + # Key should be hashed, like this: `echo -n "my-api-key" | openssl dgst -sha256 | awk '{print $2}'` + # Plain text API key: vault-admin-api-key + adminApiKeyHash: d4a6b4c1cb71dbdb68a1dd429ad737369f74b9e264b9dfa639258753987caaad + # Auth config for requests from this service + outgoing: null + +# Encryption Key management configuration +keyring: + # Either "raw" or "awskms" + type: raw + # If type=raw: + # A master password that uses PBKDF2 to derive the Key Encryption Key (KEK) used to encrypt the master key. + encryptionMasterPassword: unsafe-local-dev-master-password + # Encryption master key: + # - Required if type=raw + # - Must be an AES-256 key encoded as hex string + # - Must be encrypted by KEK derived from masterPassword + # - Must be quoted to prevent number interpretation + # - Will auto-generate on first-boot if not set + # encryptionMasterKey: "0x020578c93e6e65c27de1dddd752a83c6f50a235d90243e15be346c360d2c9d9f64ba17009800030003617070001861726d6f72792e656e6372797074696f6e2d6d6f64756c6500156177732d63727970746f2d7075626c69632d6b6579004441364a5577556f6f6f63476e752b6e6a465477414d56576a516f304b72447a34615a325761515861375236354330464d6567352f69556e4a31386f6a7169533877413d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000c8ea6c76bba04c350fdd918b50030f5de1f7910661ae891dd5ce7d3b2692c0ae51916e08bfe4125842f259ed5bf6a2c9d5e3ec06411fcbc840805623da471020000100077f11eac1ff728005f6870ffe8864c1618aacbc5cc35879a0a59cfdafac822e4c852bb9e4c70ce8c18f8f02067a5641dffffffff00000001000000000000000000000001000000202ed04425860a9f6645bcac8616979af1ff484edf661a2501c85e20126c1530750784fc11ca5f2e660b11930c500c3d870068306602310098765ed9afdba77cccc53d493a99b8553873511e904175dcda86007eccccf0b660273e451fc2b900f3e7a60133d08c14023100f196c9312409263a59ee9ca227698e819887ef53798bf74b523cf9d8ecf909ae32c8d4c055907b3051a810a45a0f6ff7" + + # If type=awskms: + # The ARN of the AWS KMS key used as the Master Key. + encryptionMasterAwsKmsArn: null + + # HMAC secret for integrity verification of data in the database. Ensure you wrap the string in quotes otherwise it will be interpreted as a number. + # If not set, will be generated during first boot. + hmacSecret: '4ce6d3404a13c971202693b669668e49e2d6d7e190428d0edd99d1cec1536efb' + +# Declarative clients (tenants) +clients: + # Each key is the client ID + dev-client-1: + name: 'Example Client' + # Override default baseUrl for this client, used for verifying jwsd/httpsig + baseUrl: 'http://localhost:3011' # [optional] + + auth: + # Disable all auth; only useful in dev + disabled: false + # OIDC configuration to use an external auth provider - NOT IMPLEMENTED YET + oidc: null + # Local authentication options, either signed requests or basic api key + local: + # HTTP Signing configuration - this is for Service-level authentication. + httpSigning: + # Settings for when THIS service verifies incoming requests + # List of accepted proof methods, supporting httpsig & jwsd. Set null if verification is disabled. + methods: + # Detached JWS + jwsd: + maxAge: 300 + requiredComponents: + - 'htm' # HTTP method + - 'uri' # Target URI + - 'created' # Created timestamp + - 'ath' # Authorization (accessToken) + + # [optional]: Restrict to specific user credentials + # - If set, ONLY these keys will be accepted. + # - Otherwise, any key can authenticate if it matches the authorized credential in a bound token. + # - # + # [optional] URL of the JWKS endpoint that includes any keys that are valid "users" + allowedUsersJwksUrl: https://armory/.well-known/jwks.json + # [optional]: List of allowed users, pinning specific keys instead of jwskUrl + allowedUsers: + - userId: admin-user-id-1 + publicKey: + kid: 'user-credential-id' + kty: 'EC' + crv: 'secp256k1' + alg: 'ES256K' + x: '...' + y: '...' + + # Authorization options, typically this will be used to pin the policy engine's public signing key. + tokenValidation: + # (optional) if you want to disable authorization for this client. DO NOT USE IN PRODUCTION. + disabled: false + # The GNAP Authorization Server that issues tokens + url: 'http://armory' + # [optional] JWKS endpoint for the auth server; used for jwt verification. Or you can pin the key below. + # jwksUrl: https://policy-engine/.well-known/jwks.json + + # [optional] Pinned public key; used for jwt verification. Must have this OR jwksUrl. + publicKey: + kid: 'policy-engine-client-key-id' + kty: 'EC' + crv: 'secp256k1' + alg: 'ES256K' + x: '...' + y: '...' + + verification: + # JWT verification options + # [optional] Expected audience in JWTs + audience: null + # [optional] Expected issuer in JWTs + issuer: 'dev-client-1.armory.narval.xyz' + # [optional] Maximum age of JWTs in seconds + maxTokenAge: 600 + # Whether tokens must be bound to signing keys + requireBoundTokens: true + # Whether to accept bearer tokens + allowBearerTokens: false + # [optional] Paths that can be omitted from request hashing, e.g. for authorizing txns with variable gas + allowWildcard: + - 'transactionRequest.maxPriorityFeePerGas' + - 'transactionRequest.maxFeePerGas' + - 'transactionRequest.gas' + - 'transactionRequest.gasPrice' + - 'transactionRequest.nonce' + # Auth config for requests from this service + outgoing: null + + # # Backup key export options + # backupPublicKey: # [optional] RSA public key for backups + # kid: "backup-2024-1" + # kty: "RSA" + # n: "..." + # e: "AQAB" diff --git a/deploy/vault.dockerfile b/deploy/vault.dockerfile index 1ccaefa31..9a57da4f7 100644 --- a/deploy/vault.dockerfile +++ b/deploy/vault.dockerfile @@ -1,4 +1,4 @@ -FROM node:21 as build +FROM node:21 AS build # Set the working directory WORKDIR /usr/src/app @@ -19,7 +19,7 @@ RUN make vault/db/generate-types && \ make vault/build && \ rm -rf apps/ && rm -rf packages/ -FROM node:21-slim as final +FROM node:21-slim AS final WORKDIR /usr/src/app RUN apt-get update && apt-get install -y openssl && apt-get clean && rm -rf /var/lib/apt/lists/* @@ -32,7 +32,7 @@ RUN chmod +x ./db-migrator.sh # Copy built application, which includes a pruned package.json # Then install just the dependencies we need for that. COPY --from=build /usr/src/app/dist ./dist -RUN npm ci --prefix ./dist/apps/vault --only=production +RUN npm ci --prefix ./dist/apps/vault --omit=dev COPY --from=build /usr/src/app/node_modules/@prisma/client/vault ./dist/apps/vault/node_modules/@prisma/client/vault ENV NODE_ENV=production diff --git a/docker-compose.mpc.yml b/docker-compose.mpc.yml index d1633ad78..efd7fe49a 100644 --- a/docker-compose.mpc.yml +++ b/docker-compose.mpc.yml @@ -77,9 +77,11 @@ services: - TSM_API_KEY=apikey0 - TSM_PLAYER_COUNT=3 - ADMIN_API_KEY=dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c # engine-admin-api-key + - CONFIG_FILE_ABSOLUTE_PATH=/config/policy-engine-config.local.yaml volumes: - ./packages:/app/packages - ./apps:/app/apps + - ./config:/config depends_on: postgres: condition: service_healthy @@ -104,9 +106,11 @@ services: - TSM_API_KEY=apikey1 - TSM_PLAYER_COUNT=3 - ADMIN_API_KEY=dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c # engine-admin-api-key + - CONFIG_FILE_ABSOLUTE_PATH=/config/policy-engine-config.local.yaml volumes: - ./packages:/app/packages - ./apps:/app/apps + - ./config:/config depends_on: postgres: condition: service_healthy @@ -131,9 +135,11 @@ services: - TSM_API_KEY=apikey2 - TSM_PLAYER_COUNT=3 - ADMIN_API_KEY=dde1fba05d6b0b1a40f2cd9f480f6dcc37a6980bcff3db54377a46b056dc472c # engine-admin-api-key + - CONFIG_FILE_ABSOLUTE_PATH=/config/policy-engine-config.local.yaml volumes: - ./packages:/app/packages - ./apps:/app/apps + - ./config:/config depends_on: postgres: condition: service_healthy diff --git a/examples/approvals-by-spending-limit/0-create-client.ts b/examples/approvals-by-spending-limit/0-create-client.ts new file mode 100644 index 000000000..1aab4efbf --- /dev/null +++ b/examples/approvals-by-spending-limit/0-create-client.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-console */ +import { AuthAdminClient } from '@narval-xyz/armory-sdk' +import { hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared' +import { secp256k1PrivateKeyToPublicJwk } from '@narval-xyz/armory-sdk/signature' +import { CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum } from '@narval-xyz/armory-sdk/src/lib/http/client/auth' +import 'dotenv/config' + +/* +Use this to create a new Client. The URLs are set up for the docker network, so edit this if you're not using docker. +If you already have a Client, move on to `1-setup.ts`. +*/ + +const main = async () => { + console.log('🚀 Starting...\n') + const dataStoreSignerPrivateKey = hexSchema.parse(process.env.DATA_STORE_SIGNER_PRIVATE_KEY) + const adminApiKey = process.env.ADMIN_API_KEY + const vaultHost = process.env.VAULT_HOST + const authHost = process.env.AUTH_HOST + const clientId = process.env.CLIENT_ID + + if (!authHost || !vaultHost || !clientId || !adminApiKey) { + throw new Error('Missing configuration') + } + + const admin = new AuthAdminClient({ + host: authHost, + adminApiKey + }) + + const publicKey = secp256k1PrivateKeyToPublicJwk(dataStoreSignerPrivateKey, process.env.DATA_STORE_SIGNER_ADDRESS) + + const { clientSecret, policyEngine } = await admin.createClient({ + id: clientId, + name: 'Example - ASL2', + useManagedDataStore: true, + dataStore: { + entity: { + data: { + type: authHost.split(':')[0].toUpperCase() as CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum, + url: `http://armory/data/policies?clientId=${clientId}` + }, + signature: { + type: authHost.split(':')[0].toUpperCase() as CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum, + url: `http://armory/data/policies?clientId=${clientId}` + }, + keys: [publicKey] + }, + policy: { + data: { + type: authHost.split(':')[0].toUpperCase() as CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum, + url: `http://armory/data/policies?clientId=${clientId}` + }, + signature: { + type: authHost.split(':')[0].toUpperCase() as CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum, + url: `http://armory/data/policies?clientId=${clientId}` + }, + keys: [publicKey] + } + } + }) + console.log('clientSecret - PUT THIS IN YOUR .env', clientSecret) + + console.log('✅ Setup completed successfully \n') +} + +main().catch(console.error) diff --git a/examples/approvals-by-spending-limit/4-transfer-a-to-b.ts b/examples/approvals-by-spending-limit/4-transfer-a-to-b.ts index 24940b65d..419a60d70 100644 --- a/examples/approvals-by-spending-limit/4-transfer-a-to-b.ts +++ b/examples/approvals-by-spending-limit/4-transfer-a-to-b.ts @@ -2,13 +2,12 @@ import { AuthClient, AuthConfig, - Decision, Request, TransactionRequest, buildSignerEip191, privateKeyToJwk } from '@narval-xyz/armory-sdk' -import { hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared' +import { Decision, hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared' import 'dotenv/config' import { v4 } from 'uuid' diff --git a/examples/approvals-by-spending-limit/6-generate-wallet.ts b/examples/approvals-by-spending-limit/6-generate-wallet.ts new file mode 100644 index 000000000..b78aed55a --- /dev/null +++ b/examples/approvals-by-spending-limit/6-generate-wallet.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-console */ +import { AuthClient, AuthConfig, Permission, buildSignerEip191, privateKeyToJwk } from '@narval-xyz/armory-sdk' +import { Action, hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared' +import 'dotenv/config' +import { v4 } from 'uuid' + +const main = async () => { + console.log(`🚀 Generate Wallet \n`) + const adminUserPrivateKey = hexSchema.parse(process.env.ADMIN_USER_PRIVATE_KEY) + const host = process.env.AUTH_HOST + const clientId = process.env.CLIENT_ID + if (!host || !clientId) { + throw new Error('Missing configuration') + } + + const authJwk = privateKeyToJwk(adminUserPrivateKey) + const signer = buildSignerEip191(adminUserPrivateKey) + const authConfig: AuthConfig = { + host, + clientId, + signer: { + jwk: authJwk, + alg: 'EIP191', + sign: signer + } + } + const auth = new AuthClient(authConfig) + + const nonces = [v4()] + const tokens = await Promise.all( + nonces.map((nonce) => + auth.requestAccessToken({ + action: Action.GRANT_PERMISSION, + resourceId: 'vault', + nonce, + permissions: [Permission.WALLET_CREATE] + }) + ) + ) + + tokens.map((token, i) => { + if (!token) { + console.error('❌ Unauthorized, nonce: ', nonces[i]) + return + } + console.log('🔐 Approval token: \n', token.value) + }) +} + +main().catch(console.error) diff --git a/examples/approvals-by-spending-limit/data.ts b/examples/approvals-by-spending-limit/data.ts index b53e363bb..53a5cea3a 100644 --- a/examples/approvals-by-spending-limit/data.ts +++ b/examples/approvals-by-spending-limit/data.ts @@ -29,26 +29,21 @@ const baseEntities: Partial = { accountType: 'eoa' } ], - userGroups: [ + groups: [ { - id: 'ug-treasury-group' + id: 'treasury-group' } ], - userGroupMembers: [ + groupMembers: [ { - groupId: 'ug-treasury-group', + type: 'user', + groupId: 'treasury-group', userId: '2-member-user-q' - } - ], - accountGroups: [ - { - id: 'ag-treasury-group' - } - ], - accountGroupMembers: [ + }, { - accountId: 'acct-treasury-account-a', - groupId: 'ag-treasury-group' + type: 'account', + groupId: 'treasury-group', + accountId: 'acct-treasury-account-a' } ], addressBook: [] diff --git a/examples/approvals-by-spending-limit/package-lock.json b/examples/approvals-by-spending-limit/package-lock.json index afc2c2f46..fc516cb35 100644 --- a/examples/approvals-by-spending-limit/package-lock.json +++ b/examples/approvals-by-spending-limit/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@narval-xyz/armory-sdk": "0.8.1", + "@narval-xyz/armory-sdk": "0.18.0", "dotenv": "^16.4.5", "minimist": "^1.2.8", "tsx": "^4.16.2", @@ -385,12 +385,15 @@ } }, "node_modules/@narval-xyz/armory-sdk": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@narval-xyz/armory-sdk/-/armory-sdk-0.8.1.tgz", - "integrity": "sha512-LF2OWE2YwZ9Ju3JGSDkPh276/sjcuOBmJzZa40ymZog36XZGhaPCHDjOWRg2OAsrN4A/A5Vue+BSudCt8RxD+w==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@narval-xyz/armory-sdk/-/armory-sdk-0.18.0.tgz", + "integrity": "sha512-jKHeVk36wo/XjxGLogxHz3lQp6cJc9YFxZU423t26LVHFIZs4HFeFwYzhGLRR4XgPu1xDEzYfl9DXuMm6r1jmg==", + "license": "MPL-2.0", "dependencies": { - "@noble/curves": "1.5.0", - "axios": "1.7.2", + "@noble/curves": "1.6.0", + "@noble/ed25519": "1.7.1", + "@noble/hashes": "1.4.0", + "axios": "1.7.7", "jose": "5.5.0", "lodash": "4.17.21", "tslib": "2.6.3", @@ -400,22 +403,27 @@ } }, "node_modules/@narval-xyz/armory-sdk/node_modules/@noble/curves": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz", - "integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "license": "MIT", "dependencies": { - "@noble/hashes": "1.4.0" + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@narval-xyz/armory-sdk/node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "license": "MIT", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -539,6 +547,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.1.tgz", + "integrity": "sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -612,12 +632,14 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -628,6 +650,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -639,6 +662,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -693,15 +717,16 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -712,9 +737,10 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -779,6 +805,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -787,6 +814,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -805,7 +833,8 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", diff --git a/examples/approvals-by-spending-limit/package.json b/examples/approvals-by-spending-limit/package.json index b4d6661c6..d634ab5e6 100644 --- a/examples/approvals-by-spending-limit/package.json +++ b/examples/approvals-by-spending-limit/package.json @@ -13,7 +13,7 @@ "author": "", "license": "ISC", "dependencies": { - "@narval-xyz/armory-sdk": "0.8.1", + "@narval-xyz/armory-sdk": "0.18.0", "dotenv": "^16.4.5", "minimist": "^1.2.8", "tsx": "^4.16.2", diff --git a/examples/unified-api/.gitignore b/examples/unified-api/.gitignore new file mode 100644 index 000000000..49ba8cf09 --- /dev/null +++ b/examples/unified-api/.gitignore @@ -0,0 +1,4 @@ +.env +node_modules +config.yaml +config.*.yaml diff --git a/examples/unified-api/1-connect.ts b/examples/unified-api/1-connect.ts new file mode 100644 index 000000000..976a87745 --- /dev/null +++ b/examples/unified-api/1-connect.ts @@ -0,0 +1,61 @@ +import { rsaPublicKeySchema } from '@narval-xyz/armory-sdk' +import { rsaEncrypt } from '@narval-xyz/armory-sdk/signature' +import dotenv from 'dotenv' +import { config, setConfig, vaultClient } from './vault.client' +dotenv.config() + +const main = async () => { + const apiKey = config.connection.credentials.apiKey + const privateKey = config.connection.credentials.privateKey + const url = config.connection.url + const credentials = { + apiKey, + privateKey + } + + const jwk = await vaultClient.generateEncryptionKey() + const encryptionKey = rsaPublicKeySchema.parse(jwk) + + const encryptedCredentials = await rsaEncrypt(JSON.stringify(credentials), encryptionKey) + + const connection = await vaultClient.createConnection({ + data: { url, encryptedCredentials, provider: 'anchorage' } + }) + + const rawAccounts = await vaultClient.listProviderRawAccounts({ + connectionId: connection.data.connectionId + }) + + console.log( + 'Syncing raw accounts', + rawAccounts.data.map((rawAccount) => `${rawAccount.label} - ${rawAccount.externalId}`) + ) + + await vaultClient.scopedSync({ + data: { + connectionId: connection.data.connectionId, + rawAccounts: rawAccounts.data + } + }) + + // Save the connectionId to the config file + setConfig('connection.id', connection.data.connectionId) + + console.dir(connection.data) +} + +main() + .then(() => console.log('done')) + .catch((error) => { + if ('response' in error) { + console.dir( + { + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } else { + console.error('Error', error) + } + }) diff --git a/examples/unified-api/2-read-wallets.ts b/examples/unified-api/2-read-wallets.ts new file mode 100644 index 000000000..b4922a8d0 --- /dev/null +++ b/examples/unified-api/2-read-wallets.ts @@ -0,0 +1,45 @@ +import { AxiosError } from 'axios' +import { config, vaultClient } from './vault.client' + +const main = async () => { + if (!config.connection.id) { + console.error('No connection.id found in config.yaml. Please connect first.') + process.exit(1) + } + + const { data } = await vaultClient.listProviderWallets({ + connectionId: config.connection.id, + pagination: { limit: 100 } + }) + + console.dir( + data.map((wallet) => ({ + label: wallet.label, + walletId: wallet.walletId, + provider: wallet.provider, + externalId: wallet.externalId, + accounts: (wallet.accounts || []).map((account) => ({ + accountId: account.accountId, + networkId: account.networkId, + label: account.label + })) + })), + { depth: null } + ) +} + +main() + .then(() => console.log('done')) + .catch((error) => { + if (error instanceof AxiosError) { + console.dir( + { + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } else { + console.error(error) + } + }) diff --git a/examples/unified-api/3-read-accounts.ts b/examples/unified-api/3-read-accounts.ts new file mode 100644 index 000000000..d89d5a368 --- /dev/null +++ b/examples/unified-api/3-read-accounts.ts @@ -0,0 +1,32 @@ +import { AxiosError } from 'axios' +import { config, vaultClient } from './vault.client' + +const main = async () => { + if (!config.connection.id) { + console.error('No connection.id found in config.yaml. Please connect first.') + process.exit(1) + } + + const { data } = await vaultClient.listProviderAccounts({ + connectionId: config.connection.id, + pagination: { limit: 100 } + }) + + console.dir(data, { depth: null }) +} + +main() + .then(() => console.log('done')) + .catch((error) => { + if (error instanceof AxiosError) { + console.dir( + { + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } else { + console.error(error) + } + }) diff --git a/examples/unified-api/4-create-transfer.ts b/examples/unified-api/4-create-transfer.ts new file mode 100644 index 000000000..7608b2b01 --- /dev/null +++ b/examples/unified-api/4-create-transfer.ts @@ -0,0 +1,90 @@ +import { v4 as uuid } from 'uuid' +import { config, vaultClient } from './vault.client' + +const main = async () => { + const destination = + config.destinationType && config.destinationId + ? { + type: config.destinationType, + id: config.destinationId + } + : config.destinationAddress + ? { address: config.destinationAddress } + : null + + if (!config.connection.id || !config.sourceId || !destination || !config.assetId || !config.amount) { + console.error('Please provide transfer parameters in config.json') + process.exit(1) + } + + const initiatedTransfer = await vaultClient.sendTransfer({ + connectionId: config.connection.id, + data: { + idempotenceId: uuid(), + source: { + type: 'account', + id: config.sourceId + }, + destination, + asset: { + assetId: config.assetId + }, + amount: config.amount, + providerSpecific: config.destinationAddress + ? { + transferAmlQuestionnaire: { + destinationType: 'SELFHOSTED_WALLET', + recipientType: 'PERSON', + purpose: 'INVESTMENT', + originatorType: 'MY_ORGANIZATION', + selfhostedDescription: 'a wallet description', + recipientFirstName: 'John', + recipientLastName: 'Recipient', + recipientFullName: 'John Recipient Full Name', + recipientCountry: 'US', + recipientStreetAddress: 'Some Recipient Street', + recipientCity: 'New York', + recipientStateProvince: 'NY', + recipientPostalCode: '10101' + } + } + : null + } + }) + + console.dir(initiatedTransfer) + + // Poll transfer status until it's no longer processing + let transfer + + do { + transfer = await vaultClient.getTransfer({ + connectionId: config.connection.id, + transferId: initiatedTransfer.data.transferId + }) + + console.log(`Transfer status: ${transfer.data.status}`) + + if (transfer.data.status === 'processing') { + await new Promise((resolve) => setTimeout(resolve, 2000)) // Wait 2 seconds between polls + } + } while (transfer.data.status === 'processing') + + console.dir(transfer) +} + +main() + .then(() => console.log('done')) + .catch((error) => { + if ('response' in error) { + console.dir( + { + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } else { + console.error('Error', error) + } + }) diff --git a/examples/unified-api/README.md b/examples/unified-api/README.md new file mode 100644 index 000000000..1fb7c9d72 --- /dev/null +++ b/examples/unified-api/README.md @@ -0,0 +1,117 @@ +# Narval Unified API Example Scripts + +## Prerequisites + +- Node.js 18+ +- tsx (`npm i -g tsx`) + +Note: This has only been tested on Mac + +## Narval Setup + +### Project Setup + +Set up the example project + +- `cp config.default.yaml config.yaml` +- `npm install` + +### Narval Credential Setup + +All API requests are signed by a private key. Begin by generating a credential. + +```shell + tsx generate-key.ts + + ## Outputs + # { + # "publicHexKey": "0x432...", // Provide this to Narval + # "privateHex": "0xa288..." // Store this securely + # } +``` + +Provide the PUBLIC key when activating your Narval account. + +> Use the provided invite link to activate your Narval account, then return here with your Client ID. + +Set up the following into your `config.yaml` file: + +```yaml +clientId: "YOUR NARVAL CLIENT ID" +narvalAuthPrivateKey: "YOUR NARVAL AUTH PRIVATE KEY, HEX ENCODED (0x...)" +``` + +### Anchorage API Setup + +To use the scripts, you'll need an API key already registered with Anchorage. + +This requires: + +1. An EDDSA Key Pair (you provide them the hex public key) +2. An API secret key (provided by Anchorage in exchange for your hex public key) + +Generate your EDDSA key pair and create an API key in Anchorage. +If you need to generate a new key pair, you can use the util method mentioned above: `tsx generate-key.ts` + +Note: Remove the `0x` prefix when pasting into Anchorage. + +```shell +tsx generate-key.ts +``` + +> Go create your Anchorage API Key if you don't have one already. When finished, return here to set your config.json + +```yaml +connection: + provider: "anchorage" + url: "https://api.anchorage-staging.com" + id: null + credentials: + apiKey: "YOUR ANCHORAGE API KEY" + privateKey: "YOUR ANCHORAGE API SIGNING PRIVATE KEY, HEX ENCODED (0x...)" +``` + +## Script Usage Guide + +Basic scripts are available for the following operations: + +1. Create Connection + + Creates the connection in our provider unified API: + + ```shell + tsx 1-connect.ts + ``` + +2. List Available Wallets + + Retrieves all readable wallets for this connection: + + ```shell + tsx 2-read-wallets.ts + ``` + +3. List Available Accounts + + Retrieves all readable accounts for this connection: + + ```shell + tsx 3-read-accounts.ts + ``` + +4. Create Transfer + + Creates a transfer between two accounts. + + Using `tsx 3-read-accounts.ts`, you can get the account IDs for the source + and destination. Set the `sourceId`, `destinationId`, `destinationType`, + `assetId`, and `amount` in the `config.yaml` file. `destinationAddress` can + be used for external transfers. + + ```shell + tsx 4-create-transfer.ts + ``` + +## More Info + +Go to the [README-quickstart.md](README-quickstart.md) for a more detailed guide on how to work with the SDK diff --git a/examples/unified-api/config.default.yaml b/examples/unified-api/config.default.yaml new file mode 100644 index 000000000..fcf4b54dc --- /dev/null +++ b/examples/unified-api/config.default.yaml @@ -0,0 +1,14 @@ +clientId: "YOUR NARVAL CLIENT ID" +narvalAuthPrivateKey: "YOUR NARVAL AUTH PRIVATE KEY, HEX ENCODED (0x...)" +baseUrl: "https://vault.armory.playnarval.com" +connection: + url: "https://api.anchorage-staging.com" + id: null + credentials: + apiKey: "YOUR ANCHORAGE API KEY" + privateKey: "YOUR ANCHORAGE API SIGNING PRIVATE KEY, HEX ENCODED (0x...)" +destinationId: null +destinationType: null +destinationAddress: null +amount: null +assetId: null diff --git a/examples/unified-api/generate-key.ts b/examples/unified-api/generate-key.ts new file mode 100644 index 000000000..aab055d8d --- /dev/null +++ b/examples/unified-api/generate-key.ts @@ -0,0 +1,18 @@ +import { Alg, generateJwk } from '@narval-xyz/armory-sdk' +import { privateKeyToHex, publicKeyToHex } from '@narval-xyz/armory-sdk/signature' + +const main = async () => { + const key = await generateJwk(Alg.EDDSA) + const privateKeyHex = await privateKeyToHex(key) + const publicKeyHex = await publicKeyToHex(key) + + console.log({ + key, + privateKeyHex, + publicKeyHex + }) +} + +main() + .then(() => console.log('done')) + .catch(console.error) diff --git a/examples/unified-api/package-lock.json b/examples/unified-api/package-lock.json new file mode 100644 index 000000000..687990ba2 --- /dev/null +++ b/examples/unified-api/package-lock.json @@ -0,0 +1,883 @@ +{ + "name": "unified-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unified-api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@narval-xyz/armory-sdk": "0.18.0", + "dotenv": "16.4.5", + "lodash": "^4.17.21", + "tsx": "4.19.2", + "yaml": "^2.7.0", + "zod": "3.24.1" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@narval-xyz/armory-sdk": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@narval-xyz/armory-sdk/-/armory-sdk-0.18.0.tgz", + "integrity": "sha512-jKHeVk36wo/XjxGLogxHz3lQp6cJc9YFxZU423t26LVHFIZs4HFeFwYzhGLRR4XgPu1xDEzYfl9DXuMm6r1jmg==", + "license": "MPL-2.0", + "dependencies": { + "@noble/curves": "1.6.0", + "@noble/ed25519": "1.7.1", + "@noble/hashes": "1.4.0", + "axios": "1.7.7", + "jose": "5.5.0", + "lodash": "4.17.21", + "tslib": "2.6.3", + "uuid": "9.0.1", + "viem": "2.16.2", + "zod": "3.23.8" + } + }, + "node_modules/@narval-xyz/armory-sdk/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@noble/curves": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "dependencies": { + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/ed25519": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.1.tgz", + "integrity": "sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/abitype": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.4.tgz", + "integrity": "sha512-UivtYZOGJGE8rsrM/N5vdRkUpqEZVmuTumfTuolm7m/6O09wprd958rx8kUBwVAAAhQDveGAgD0GJdBuR8s6tw==", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/isows": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.4.tgz", + "integrity": "sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jose": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.5.0.tgz", + "integrity": "sha512-DUPr/1kYXbuqYpkCj9r66+B4SGCKXCLQ5ZbKCgmn4sJveJqcwNqWtAR56u4KPmpXjrmBO2uNuLdEAEiqIhFNBg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/viem": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.16.2.tgz", + "integrity": "sha512-qor3v1cJFR3jcPtcJxPbKfKURAH2agNf2IWZIaSReV6teNLERiu4Sr7kbqpkIeTAEpiDCVQwg336M+mub1m+pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "1.0.4", + "isows": "1.0.4", + "ws": "8.17.1" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/unified-api/package.json b/examples/unified-api/package.json new file mode 100644 index 000000000..c6a146bc2 --- /dev/null +++ b/examples/unified-api/package.json @@ -0,0 +1,19 @@ +{ + "name": "unified-api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@narval-xyz/armory-sdk": "0.18.0", + "dotenv": "16.4.5", + "lodash": "^4.17.21", + "tsx": "4.19.2", + "yaml": "^2.7.0", + "zod": "3.24.1" + } +} diff --git a/examples/unified-api/tsconfig.json b/examples/unified-api/tsconfig.json new file mode 100644 index 000000000..37d9845f0 --- /dev/null +++ b/examples/unified-api/tsconfig.json @@ -0,0 +1,102 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/examples/unified-api/vault.client.ts b/examples/unified-api/vault.client.ts new file mode 100644 index 000000000..e5a69a7b4 --- /dev/null +++ b/examples/unified-api/vault.client.ts @@ -0,0 +1,117 @@ +import { Hex, privateKeyToJwk, VaultClient } from '@narval-xyz/armory-sdk' +import { buildSignerEdDSA } from '@narval-xyz/armory-sdk/signature' +import dotenv from 'dotenv' +import fs from 'fs' +import { cloneDeep, set } from 'lodash' +import path from 'path' +import * as YAML from 'yaml' +import { z } from 'zod' + +dotenv.config() + +const Config = z.object({ + // Narval variables + clientId: z.string(), + narvalAuthPrivateKey: z.string(), + baseUrl: z.string(), + + // Connection variables + connection: z.object({ + url: z.string(), + id: z.string().nullable(), + credentials: z.object({ + apiKey: z.string(), + privateKey: z.string() + }) + }), + + // Transfer variables + sourceId: z.string().nullable(), + destinationId: z.string().nullable(), + destinationType: z.union([z.literal('account'), z.literal('address')]).nullable(), + destinationAddress: z.string().nullable(), + amount: z.string().nullable(), + assetId: z.string().nullable() +}) +type Config = z.infer + +let config: Config + +const configPath = path.join(__dirname, 'config.yaml') + +try { + if (!fs.existsSync(configPath)) { + const CLIENT_ID = process.env.CLIENT_ID + const NARVAL_AUTH_PRIVATE_KEY_HEX = process.env.NARVAL_AUTH_PRIVATE_KEY + const BASE_URL = process.env.BASE_URL + + if (!CLIENT_ID || !NARVAL_AUTH_PRIVATE_KEY_HEX || !BASE_URL) { + throw new Error('CLIENT_ID or NARVAL_AUTH_PRIVATE_KEY or BASE_URL must be in .env or config.json') + } + + // Default configuration + const defaultConfig = { + clientId: CLIENT_ID, + narvalAuthPrivateKey: NARVAL_AUTH_PRIVATE_KEY_HEX, + baseUrl: BASE_URL, + sourceId: null, + destinationId: null, + destinationType: null, + destinationAddress: null, + amount: null, + assetId: null, + connection: { + id: null, + url: 'https://api.anchorage-staging.com', + credentials: { + apiKey: '', + privateKey: '' + } + } + } + + fs.writeFileSync(configPath, YAML.stringify(defaultConfig)) + + config = defaultConfig + } else { + const configFile = fs.readFileSync(configPath, 'utf8') + config = Config.parse(YAML.parse(configFile)) + } +} catch (error) { + console.error('Error handling config.yaml:', error) + throw error +} + +type KeyPath = T extends object + ? { + [K in keyof T]: K extends string ? (T[K] extends object ? K | `${K}.${KeyPath}` : K) : never + }[keyof T] + : never + +type ValuePath = P extends keyof T + ? T[P] + : P extends `${infer K}.${infer R}` + ? K extends keyof T + ? ValuePath + : never + : never + +export const setConfig =

>(path: P, value: ValuePath) => { + const newConfig = cloneDeep(config) + set(newConfig, path, value) + const validConfig = Config.parse(newConfig) + fs.writeFileSync(configPath, YAML.stringify(validConfig)) + config = validConfig +} + +export const vaultClient = new VaultClient({ + clientId: config.clientId, + signer: { + sign: buildSignerEdDSA(config.narvalAuthPrivateKey), + jwk: privateKeyToJwk(config.narvalAuthPrivateKey as Hex, 'EDDSA'), + alg: 'EDDSA' + }, + host: config.baseUrl +}) + +export { config } diff --git a/package-lock.json b/package-lock.json index 39d7f87ab..808794550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,9 +24,11 @@ "@monaco-editor/react": "4.6.0", "@nestjs/axios": "3.0.2", "@nestjs/bull": "10.2.0", + "@nestjs/cache-manager": "3.0.0", "@nestjs/common": "10.3.10", "@nestjs/config": "3.1.1", "@nestjs/core": "10.4.7", + "@nestjs/event-emitter": "2.1.1", "@nestjs/platform-express": "10.3.9", "@nestjs/swagger": "7.4.0", "@noble/curves": "1.6.0", @@ -50,12 +52,13 @@ "@radix-ui/react-popper": "1.2.0", "@radix-ui/react-radio-group": "1.2.0", "@radix-ui/react-tooltip": "1.0.7", - "@scure/bip32": "1.5.0", + "@scure/bip32": "1.6.2", "@scure/bip39": "1.3.0", "@tanstack/react-query": "5.51.11", "axios": "1.7.7", - "axios-retry": "4.4.2", + "axios-retry": "4.5.0", "bull": "4.16.4", + "cache-manager": "6.3.2", "class-transformer": "0.5.1", "class-validator": "0.14.1", "clsx": "2.1.1", @@ -68,6 +71,7 @@ "lowdb": "7.0.1", "nestjs-zod": "3.0.0", "next": "14.2.4", + "node-forge": "1.3.1", "permissionless": "0.1.44", "prism-react-renderer": "2.3.1", "react": "18.2.0", @@ -85,6 +89,7 @@ "wagmi": "2.12.31", "winston": "3.14.2", "winston-transport": "4.7.0", + "yaml": "2.6.1", "zod": "3.23.8" }, "devDependencies": { @@ -104,7 +109,7 @@ "@nx/node": "19.2.0", "@nx/webpack": "19.6.3", "@nx/workspace": "19.7.3", - "@openapitools/openapi-generator-cli": "2.13.4", + "@openapitools/openapi-generator-cli": "2.15.3", "@swc-node/register": "1.10.9", "@swc/core": "1.7.26", "@swc/helpers": "0.5.11", @@ -123,7 +128,7 @@ "@typescript-eslint/parser": "7.9.0", "autoprefixer": "10.4.20", "babel-jest": "29.7.0", - "dotenv": "16.4.5", + "dotenv": "16.4.7", "dotenv-cli": "7.3.0", "eslint": "8.57.0", "eslint-config-next": "14.2.6", @@ -138,6 +143,7 @@ "jest-environment-node": "29.7.0", "jest-mock-extended": "3.0.5", "lint-staged": "15.2.0", + "msw": "2.6.8", "nock": "13.5.4", "nx": "19.4.4", "postcss": "8.4.47", @@ -1329,12 +1335,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -1519,9 +1526,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -1632,11 +1640,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1705,23 +1714,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-decorators": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", @@ -1743,6 +1735,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.9.tgz", "integrity": "sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1754,41 +1747,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -1877,6 +1835,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.25.9.tgz", "integrity": "sha512-9MhJ/SMTsVqsd69GyQg89lYR4o9T+oDGv5F6IsigxxqFVOyR/IflDLYP8WDI1l8fkhNGGktqkvL5qwNCtGEpgQ==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1903,6 +1862,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2329,13 +2289,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz", - "integrity": "sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", + "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-flow": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-flow": "^7.26.0" }, "engines": { "node": ">=6.9.0" @@ -2742,6 +2703,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2757,6 +2719,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3077,6 +3040,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.25.9.tgz", "integrity": "sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==", + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3144,6 +3108,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.9.tgz", "integrity": "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==", + "license": "MIT", "peer": true, "dependencies": { "clone-deep": "^4.0.1", @@ -3163,6 +3128,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", "peer": true, "dependencies": { "is-plain-object": "^2.0.4", @@ -3177,6 +3143,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", "peer": true, "dependencies": { "commondir": "^1.0.1", @@ -3191,6 +3158,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", "peer": true, "dependencies": { "locate-path": "^3.0.0" @@ -3203,6 +3171,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", "peer": true, "dependencies": { "p-locate": "^3.0.0", @@ -3216,6 +3185,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", "peer": true, "dependencies": { "pify": "^4.0.1", @@ -3229,6 +3199,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "peer": true, "dependencies": { "p-try": "^2.0.0" @@ -3244,6 +3215,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", "peer": true, "dependencies": { "p-limit": "^2.0.0" @@ -3256,6 +3228,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=4" @@ -3265,6 +3238,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -3274,6 +3248,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", "peer": true, "dependencies": { "find-up": "^3.0.0" @@ -3286,6 +3261,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", "peer": true, "bin": { "semver": "bin/semver" @@ -3295,6 +3271,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", "peer": true, "dependencies": { "kind-of": "^6.0.2" @@ -3363,16 +3340,17 @@ }, "node_modules/@babel/traverse--for-generate-function-map": { "name": "@babel/traverse", - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -3381,9 +3359,10 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -3444,6 +3423,47 @@ "@bull-board/api": "5.20.1" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, "node_modules/@coinbase/wallet-sdk": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.2.3.tgz", @@ -4823,6 +4843,147 @@ "deprecated": "Use @eslint/object-schema instead", "devOptional": true }, + "node_modules/@inquirer/confirm": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.0.tgz", + "integrity": "sha512-osaBbIMEqVFjTX5exoqPXs6PilWQdjaLhGtMDXMXg/yxkHXNq43GlxGyTA35lK2HpzUgDN+Cjh/2AmqCN0QJpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.1.tgz", + "integrity": "sha512-rmZVXy9iZvO3ZStEe/ayuuwIJ23LSF13aPMlLMTQARX6lGUBDHGV8UB5i9MRrfy0+mZwt5/9bdy8llszSD3NQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", + "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -4876,6 +5037,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", "peer": true, "engines": { "node": ">=12" @@ -5037,6 +5199,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", "peer": true, "dependencies": { "@jest/types": "^29.6.3" @@ -5369,6 +5532,39 @@ "tslib": "2" } }, + "node_modules/@keyv/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -6254,6 +6450,24 @@ "win32" ] }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.3.tgz", + "integrity": "sha512-USvgCL/uOGFtVa6SVyRrC8kIAedzRohxIXN5LISlg5C5vLZCn7dgMFVSNhSF9cuBEFrm/O2spDWEZeMnw4ZXYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -6707,6 +6921,18 @@ "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.0.tgz", + "integrity": "sha512-csKvxHSQWfC0OiDo0bNEhLqrmYDopHEvRyC81MxV9xFj1AO+rOKocpHa4M1ZGH//6uKFIPGN9oiR0mvZY77APA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", + "cache-manager": ">=6", + "rxjs": "^7.8.1" + } + }, "node_modules/@nestjs/common": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", @@ -6816,6 +7042,19 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, + "node_modules/@nestjs/event-emitter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.1.1.tgz", + "integrity": "sha512-6L6fBOZTyfFlL7Ih/JDdqlCzZeCW0RjCX28wnzGyg/ncv5F/EOeT1dfopQr1loBRQ3LTgu8OWM7n4zLN4xigsg==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", @@ -6839,6 +7078,7 @@ "version": "10.3.9", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.9.tgz", "integrity": "sha512-si/UzobP6YUtYtCT1cSyQYHHzU3yseqYT6l7OHSMVvfG1+TqxaAqI6nmrix02LO+l1YntHRXEs3p+v9a7EfrSQ==", + "license": "MIT", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -6858,7 +7098,8 @@ "node_modules/@nestjs/platform-express/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" }, "node_modules/@nestjs/schematics": { "version": "9.2.0", @@ -10229,6 +10470,16 @@ "node": ">=8" } }, + "node_modules/@nx/next/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/@nx/next/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -11612,6 +11863,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@nx/webpack/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/@nx/webpack/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -12002,6 +12263,31 @@ "node": ">=12" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@open-policy-agent/opa-wasm": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@open-policy-agent/opa-wasm/-/opa-wasm-1.9.0.tgz", @@ -12011,51 +12297,71 @@ "yaml": "^1.10.2" } }, + "node_modules/@open-policy-agent/opa-wasm/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.13.4.tgz", - "integrity": "sha512-4JKyrk55ohQK2FcuZbPdNvxdyXD14jjOIvE8hYjJ+E1cHbRbfXQXbYnjTODFE52Gx8eAxz8C9icuhDYDLn7nww==", + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.15.3.tgz", + "integrity": "sha512-2UBnsDlMt36thhdXxisbA1qReVtbCaw+NCvXoslRXlaJBL4qkAmZUhNeDLNu3LCbwA2PASMWhJSqeLwgwMCitw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@nestjs/axios": "3.0.2", - "@nestjs/common": "10.3.0", - "@nestjs/core": "10.3.0", + "@nestjs/axios": "3.1.1", + "@nestjs/common": "10.4.6", + "@nestjs/core": "10.4.6", "@nuxtjs/opencollective": "0.3.2", - "axios": "1.6.8", + "axios": "1.7.7", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "10.1.0", - "glob": "7.2.3", - "https-proxy-agent": "7.0.4", + "glob": "9.3.5", "inquirer": "8.2.6", "lodash": "4.17.21", + "proxy-agent": "6.4.0", "reflect-metadata": "0.1.13", "rxjs": "7.8.1", - "tslib": "2.6.2" + "tslib": "2.8.1" }, "bin": { "openapi-generator-cli": "main.js" }, "engines": { - "node": ">=10.0.0" + "node": ">=16" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/openapi_generator" } }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/axios": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.1.tgz", + "integrity": "sha512-ySoxrzqX80P1q6LKLKGcgyBd2utg4gbC+4FsJNpXYvILorMlxss/ECNogD9EXLCE4JS5exVFD5ez0nK5hXcNTQ==", + "dev": true, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", - "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", + "version": "10.4.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.6.tgz", + "integrity": "sha512-KkezkZvU9poWaNq4L+lNvx+386hpOxPJkfXBBeSMrcqBOx8kVr36TGN2uYkF4Ta4zNu1KbCjmZbc0rhHSg296g==", "dev": true, "dependencies": { "iterare": "1.2.1", - "tslib": "2.6.2", + "tslib": "2.7.0", "uid": "2.0.2" }, "funding": { @@ -12065,7 +12371,7 @@ "peerDependencies": { "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -12077,18 +12383,24 @@ } } }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", - "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", + "version": "10.4.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.6.tgz", + "integrity": "sha512-zXVPxCNRfO6gAy0yvEDjUxE/8gfZICJFpsl2lZAUH31bPb6m+tXuhUq2mVCTEltyMYQ+DYtRe+fEYM2v152N1g==", "dev": true, "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.2", + "path-to-regexp": "3.3.0", + "tslib": "2.7.0", "uid": "2.0.2" }, "funding": { @@ -12100,7 +12412,7 @@ "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/websockets": "^10.0.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -12115,16 +12427,11 @@ } } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true }, "node_modules/@openapitools/openapi-generator-cli/node_modules/commander": { "version": "8.3.0", @@ -12149,6 +12456,54 @@ "node": ">=12" } }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true + }, "node_modules/@openapitools/openapi-generator-cli/node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -12156,9 +12511,9 @@ "dev": true }, "node_modules/@openapitools/openapi-generator-cli/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, "node_modules/@opentelemetry/api": { @@ -17331,30 +17686,34 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, "node_modules/@react-native/assets-registry": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.3.tgz", - "integrity": "sha512-7Fnc3lzCFFpnoyL1egua6d/qUp0KiIpeSLbfOMln4nI2g2BMzyFHdPjJnpLV2NehmS0omOOkrfRqK5u1F/MXzA==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.77.0.tgz", + "integrity": "sha512-Ms4tYYAMScgINAXIhE4riCFJPPL/yltughHS950l0VP5sm5glbimn9n7RFn9Tc8cipX74/ddbk19+ydK2iDMmA==", + "license": "MIT", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.3.tgz", - "integrity": "sha512-mZ7jmIIg4bUnxCqY3yTOkoHvvzsDyrZgfnIKiTGm5QACrsIGa5eT3pMFpMm2OpxGXRDrTMsYdPXE2rCyDX52VQ==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.77.0.tgz", + "integrity": "sha512-5TYPn1k+jdDOZJU4EVb1kZ0p9TCVICXK3uplRev5Gul57oWesAaiWGZOzfRS3lonWeuR4ij8v8PFfIHOaq0vmA==", + "license": "MIT", "peer": true, "dependencies": { - "@react-native/codegen": "0.76.3" + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.77.0" }, "engines": { "node": ">=18" } }, "node_modules/@react-native/babel-preset": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.3.tgz", - "integrity": "sha512-zi2nPlQf9q2fmfPyzwWEj6DU96v8ziWtEfG7CTAX2PG/Vjfsr94vn/wWrCdhBVvLRQ6Kvd/MFAuDYpxmQwIiVQ==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.77.0.tgz", + "integrity": "sha512-Z4yxE66OvPyQ/iAlaETI1ptRLcDm7Tk6ZLqtCPuUX3AMg+JNgIA86979T4RSk486/JrBUBH5WZe2xjj7eEHXsA==", + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", @@ -17398,8 +17757,8 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.76.3", - "babel-plugin-syntax-hermes-parser": "^0.25.1", + "@react-native/babel-plugin-codegen": "0.77.0", + "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" }, @@ -17410,42 +17769,18 @@ "@babel/core": "*" } }, - "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", - "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", - "peer": true, - "dependencies": { - "hermes-parser": "0.25.1" - } - }, - "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "peer": true - }, - "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "peer": true, - "dependencies": { - "hermes-estree": "0.25.1" - } - }, "node_modules/@react-native/codegen": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.3.tgz", - "integrity": "sha512-oJCH/jbYeGmFJql8/y76gqWCCd74pyug41yzYAjREso1Z7xL88JhDyKMvxEnfhSdMOZYVl479N80xFiXPy3ZYA==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.77.0.tgz", + "integrity": "sha512-rE9lXx41ZjvE8cG7e62y/yGqzUpxnSvJ6me6axiX+aDewmI4ZrddvRGYyxCnawxy5dIBHSnrpZse3P87/4Lm7w==", + "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", - "hermes-parser": "0.23.1", + "hermes-parser": "0.25.1", "invariant": "^2.2.4", - "jscodeshift": "^0.14.0", - "mkdirp": "^0.5.1", + "jscodeshift": "^17.0.0", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, @@ -17460,12 +17795,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", "peer": true }, "node_modules/@react-native/codegen/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -17475,6 +17812,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "peer": true, "dependencies": { "emoji-regex": "^8.0.0", @@ -17489,6 +17827,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "peer": true, "dependencies": { "cliui": "^8.0.1", @@ -17504,20 +17843,20 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.3.tgz", - "integrity": "sha512-vgsLixHS24jR0d0QqPykBWFaC+V8x9cM3cs4oYXw3W199jgBNGP9MWcUJLazD2vzrT/lUTVBVg0rBeB+4XR6fg==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.77.0.tgz", + "integrity": "sha512-GRshwhCHhtupa3yyCbel14SlQligV8ffNYN5L1f8HCo2SeGPsBDNjhj2U+JTrMPnoqpwowPGvkCwyqwqYff4MQ==", + "license": "MIT", "peer": true, "dependencies": { - "@react-native/dev-middleware": "0.76.3", - "@react-native/metro-babel-transformer": "0.76.3", + "@react-native/dev-middleware": "0.77.0", + "@react-native/metro-babel-transformer": "0.77.0", "chalk": "^4.0.0", - "execa": "^5.1.1", + "debug": "^2.2.0", "invariant": "^2.2.4", "metro": "^0.81.0", "metro-config": "^0.81.0", "metro-core": "^0.81.0", - "node-fetch": "^2.2.0", "readline": "^1.3.0", "semver": "^7.1.3" }, @@ -17533,10 +17872,28 @@ } } }, + "node_modules/@react-native/community-cli-plugin/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, "node_modules/@react-native/community-cli-plugin/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "peer": true, "bin": { "semver": "bin/semver.js" @@ -17546,22 +17903,24 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.3.tgz", - "integrity": "sha512-pMHQ3NpPB28RxXciSvm2yD+uDx3pkhzfuWkc7VFgOduyzPSIr0zotUiOJzsAtrj8++bPbOsAraCeQhCqoOTWQw==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.77.0.tgz", + "integrity": "sha512-glOvSEjCbVXw+KtfiOAmrq21FuLE1VsmBsyT7qud4KWbXP43aUEhzn70mWyFuiIdxnzVPKe2u8iWTQTdJksR1w==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/dev-middleware": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.3.tgz", - "integrity": "sha512-b+2IpW40z1/S5Jo5JKrWPmucYU/PzeGyGBZZ/SJvmRnBDaP3txb9yIqNZAII1EWsKNhedh8vyRO5PSuJ9Juqzw==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.77.0.tgz", + "integrity": "sha512-DAlEYujm43O+Dq98KP2XfLSX5c/TEGtt+JBDEIOQewk374uYY52HzRb1+Gj6tNaEj/b33no4GibtdxbO5zmPhg==", + "license": "MIT", "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.76.3", + "@react-native/debugger-frontend": "0.77.0", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", @@ -17569,7 +17928,7 @@ "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", - "serve-static": "^1.13.1", + "serve-static": "^1.16.2", "ws": "^6.2.3" }, "engines": { @@ -17580,21 +17939,34 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "peer": true, "dependencies": { "ms": "2.0.0" } }, + "node_modules/@react-native/dev-middleware/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@react-native/dev-middleware/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", "peer": true }, "node_modules/@react-native/dev-middleware/node_modules/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", "peer": true, "dependencies": { "is-docker": "^2.0.0", @@ -17607,42 +17979,104 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native/dev-middleware/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-native/dev-middleware/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/@react-native/dev-middleware/node_modules/ws": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", "peer": true, "dependencies": { "async-limiter": "~1.0.0" } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.76.3.tgz", - "integrity": "sha512-t0aYZ8ND7+yc+yIm6Yp52bInneYpki6RSIFZ9/LMUzgMKvEB62ptt/7sfho9QkKHCNxE1DJSWIqLIGi/iHHkyg==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.77.0.tgz", + "integrity": "sha512-rmfh93jzbndSq7kihYHUQ/EGHTP8CCd3GDCmg5SbxSOHAaAYx2HZ28ZG7AVcGUsWeXp+e/90zGIyfOzDRx0Zaw==", + "license": "MIT", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/js-polyfills": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.76.3.tgz", - "integrity": "sha512-pubJFArMMrdZiytH+W95KngcSQs+LsxOBsVHkwgMnpBfRUxXPMK4fudtBwWvhnwN76Oe+WhxSq7vOS5XgoPhmw==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.77.0.tgz", + "integrity": "sha512-kHFcMJVkGb3ptj3yg1soUsMHATqal4dh0QTGAbYihngJ6zy+TnP65J3GJq4UlwqFE9K1RZkeCmTwlmyPFHOGvA==", + "license": "MIT", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/metro-babel-transformer": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.3.tgz", - "integrity": "sha512-b2zQPXmW7avw/7zewc9nzMULPIAjsTwN03hskhxHUJH5pzUf7pIklB3FrgYPZrRhJgzHiNl3tOPu7vqiKzBYPg==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.77.0.tgz", + "integrity": "sha512-19GfvhBRKCU3UDWwCnDR4QjIzz3B2ZuwhnxMRwfAgPxz7QY9uKour9RGmBAVUk1Wxi/SP7dLEvWnmnuBO39e2A==", + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.76.3", - "hermes-parser": "0.23.1", + "@react-native/babel-preset": "0.77.0", + "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" }, "engines": { @@ -17653,15 +18087,17 @@ } }, "node_modules/@react-native/normalize-colors": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.3.tgz", - "integrity": "sha512-Yrpmrh4IDEupUUM/dqVxhAN8QW1VEUR3Qrk2lzJC1jB2s46hDe0hrMP2vs12YJqlzshteOthjwXQlY0TgIzgbg==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.77.0.tgz", + "integrity": "sha512-qjmxW3xRZe4T0ZBEaXZNHtuUbRgyfybWijf1yUuQwjBt24tSapmIslwhCjpKidA0p93ssPcepquhY0ykH25mew==", + "license": "MIT", "peer": true }, "node_modules/@react-native/virtualized-lists": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.76.3.tgz", - "integrity": "sha512-wTGv9pVh3vAOWb29xFm+J9VRe9dUcUcb9FyaMLT/Hxa88W4wqa5ZMe1V9UvrrBiA1G5DKjv8/1ZcDsJhyugVKA==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.77.0.tgz", + "integrity": "sha512-ppPtEu9ISO9iuzpA2HBqrfmDpDAnGGduNDVaegadOzbMCPAB3tC9Blxdu9W68LyYlNQILIsP6/FYtLwf7kfNew==", + "license": "MIT", "peer": true, "dependencies": { "invariant": "^2.2.4", @@ -17722,22 +18158,39 @@ } }, "node_modules/@scure/bip32": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", - "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", "dependencies": { - "@noble/curves": "~1.6.0", - "@noble/hashes": "~1.5.0", - "@scure/base": "~1.1.7" + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, @@ -17745,6 +18198,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", @@ -19114,6 +19576,12 @@ "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -19262,6 +19730,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -19716,6 +20191,13 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/superagent": { "version": "8.1.7", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.7.tgz", @@ -20888,6 +21370,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", "peer": true, "dependencies": { "event-target-shim": "^5.0.0" @@ -20974,12 +21457,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "engines": { "node": ">= 14" } @@ -21075,6 +21555,7 @@ "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT", "peer": true }, "node_modules/ansi-align": { @@ -21452,9 +21933,10 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/ast-types": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", - "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", "peer": true, "dependencies": { "tslib": "^2.0.1" @@ -21486,6 +21968,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT", "peer": true }, "node_modules/async-mutex": { @@ -21587,9 +22070,9 @@ } }, "node_modules/axios-retry": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.4.2.tgz", - "integrity": "sha512-2fjo9uDNBQjX8+GMEGOFG5TrLMZ3QijjeRYcgBI2MhrsabnvcIAfLxxIhG7+CCD68vPEQY3IM2llV70ZsrqPvA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", "dependencies": { "is-retry-allowed": "^2.2.0" }, @@ -21606,15 +22089,6 @@ "deep-equal": "^2.0.5" } }, - "node_modules/babel-core": { - "version": "7.0.0-bridge.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", - "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "peer": true, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -21744,6 +22218,16 @@ "node": ">=8" } }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -21781,18 +22265,20 @@ } }, "node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", - "integrity": "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", "peer": true, "dependencies": { - "hermes-parser": "0.23.1" + "hermes-parser": "0.25.1" } }, "node_modules/babel-plugin-transform-flow-enums": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", "peer": true, "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" @@ -21895,6 +22381,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -22250,6 +22745,24 @@ "node": ">= 6.0.0" } }, + "node_modules/cache-manager": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.3.2.tgz", + "integrity": "sha512-VmLouPUrvpm9dfwYB6OE7YVXDZ7BCfbt7hq10EHiBYaW9K9ZthK1bbjDQAtXGDK7d9u8t4G/7dMWSJOwN33msg==", + "license": "MIT", + "dependencies": { + "keyv": "^5.2.3" + } + }, + "node_modules/cache-manager/node_modules/keyv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz", + "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.0.2" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -22297,6 +22810,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", "peer": true, "dependencies": { "callsites": "^2.0.0" @@ -22309,6 +22823,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=4" @@ -22318,6 +22833,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", "peer": true, "dependencies": { "caller-callsite": "^2.0.0" @@ -22594,6 +23110,7 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@types/node": "*", @@ -22620,6 +23137,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@types/node": "*", @@ -22634,6 +23152,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", "peer": true, "bin": { "mkdirp": "bin/cmd.js" @@ -23267,6 +23786,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT", "peer": true }, "node_modules/compare-versions": { @@ -23513,6 +24033,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", "peer": true, "dependencies": { "debug": "2.6.9", @@ -23536,6 +24057,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "peer": true, "dependencies": { "ms": "2.0.0" @@ -23545,6 +24067,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", "peer": true, "dependencies": { "debug": "2.6.9", @@ -23563,12 +24086,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", "peer": true }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", "peer": true, "dependencies": { "ee-first": "1.1.1" @@ -23581,6 +24106,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", "peer": true, "engines": { "node": ">= 0.6" @@ -24268,6 +24794,15 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -24586,6 +25121,32 @@ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/degenerator/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", @@ -24621,12 +25182,6 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "node_modules/denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==", - "peer": true - }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -24944,9 +25499,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "engines": { "node": ">=12" @@ -25021,6 +25576,7 @@ "version": "0.3.21", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.3.21.tgz", "integrity": "sha512-6FiThm7KlTihph8ROhq/BHEglGCJSwq6c8KVgcCcJiNJFNlbrFtfnTqZobVmWIB764mzhZTOBFyinADSXXvTLg==", + "license": "MIT", "peer": true, "dependencies": { "futoin-hkdf": "^1.5.3", @@ -25241,6 +25797,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", "peer": true, "dependencies": { "stackframe": "^1.3.4" @@ -26637,6 +27194,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -26723,6 +27281,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "license": "Apache-2.0", "peer": true }, "node_modules/express": { @@ -27232,12 +27791,14 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT", "peer": true }, "node_modules/flow-parser": { - "version": "0.255.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.255.0.tgz", - "integrity": "sha512-7QHV2m2mIMh6yIMaAPOVbyNEW77IARwO69d4DgvfDCjuORiykdMLf7XBjF7Zeov7Cpe1OXJ8sB6/aaCE3xuRBw==", + "version": "0.259.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.259.1.tgz", + "integrity": "sha512-xiXLmMH2Z7OmdE9Q+MjljUMr/rbemFqZIRxaeZieVScG4HzQrKKhNcCYZbWTGpoN7ZPi7z8ClQbeVPq6t5AszQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.4.0" @@ -27469,6 +28030,16 @@ "node": ">=10" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -27656,6 +28227,7 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", + "license": "Apache-2.0", "peer": true, "engines": { "node": ">=8" @@ -27810,6 +28382,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/github-slugger": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", @@ -28042,6 +28628,16 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "devOptional": true }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -28440,19 +29036,28 @@ "he": "bin/he" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz", - "integrity": "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT", "peer": true }, "node_modules/hermes-parser": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.1.tgz", - "integrity": "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", "peer": true, "dependencies": { - "hermes-estree": "0.23.1" + "hermes-estree": "0.25.1" } }, "node_modules/hexoid": { @@ -28940,11 +29545,11 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -29359,6 +29964,19 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -29566,6 +30184,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -29758,6 +30377,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-npm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", @@ -30355,6 +30981,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -30372,6 +30999,7 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -30431,6 +31059,18 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/jest-circus/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -31314,60 +31954,92 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsc-android": { "version": "250231.0.0", "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", "integrity": "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==", + "license": "BSD-2-Clause", "peer": true }, "node_modules/jsc-safe-url": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD", "peer": true }, "node_modules/jscodeshift": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.14.0.tgz", - "integrity": "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.1.2.tgz", + "integrity": "sha512-uime4vFOiZ1o3ICT4Sm/AbItHEVw2oCxQ3a0egYVy3JMMOctxe07H3SKL1v175YqjMt27jn1N+3+Bj9SKDNgdQ==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/core": "^7.13.16", - "@babel/parser": "^7.13.16", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/preset-flow": "^7.13.13", - "@babel/preset-typescript": "^7.13.0", - "@babel/register": "^7.13.16", - "babel-core": "^7.0.0-bridge.0", - "chalk": "^4.1.2", + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", "flow-parser": "0.*", "graceful-fs": "^4.2.4", - "micromatch": "^4.0.4", + "micromatch": "^4.0.7", "neo-async": "^2.5.0", - "node-dir": "^0.1.17", - "recast": "^0.21.0", - "temp": "^0.8.4", - "write-file-atomic": "^2.3.0" + "picocolors": "^1.0.1", + "recast": "^0.23.9", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" }, "bin": { "jscodeshift": "bin/jscodeshift.js" }, + "engines": { + "node": ">=16" + }, "peerDependencies": { "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jscodeshift/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jscodeshift/node_modules/write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", "peer": true, "dependencies": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/jsdom": { @@ -31480,6 +32152,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT", "peer": true }, "node_modules/json-parse-even-better-errors": { @@ -31979,6 +32652,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", "peer": true, "dependencies": { "debug": "^2.6.9", @@ -31989,6 +32663,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "peer": true, "dependencies": { "ms": "2.0.0" @@ -31998,6 +32673,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", "peer": true }, "node_modules/lilconfig": { @@ -32508,6 +33184,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT", "peer": true }, "node_modules/lodash.uniq": { @@ -32912,6 +33589,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "license": "Apache-2.0", "peer": true }, "node_modules/mdast-util-directive": { @@ -33320,6 +33998,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT", "peer": true }, "node_modules/merge-deep": { @@ -33373,9 +34052,10 @@ } }, "node_modules/metro": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.81.0.tgz", - "integrity": "sha512-kzdzmpL0gKhEthZ9aOV7sTqvg6NuTxDV8SIm9pf9sO8VVEbKrQk5DNcwupOUjgPPFAuKUc2NkT0suyT62hm2xg==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.81.1.tgz", + "integrity": "sha512-fqRu4fg8ONW7VfqWFMGgKAcOuMzyoQah2azv9Y3VyFXAmG+AoTU6YIFWqAADESCGVWuWEIvxTJhMf3jxU6jwjA==", + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.24.7", @@ -33390,33 +34070,31 @@ "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", - "denodeify": "^1.2.1", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", - "hermes-parser": "0.24.0", + "hermes-parser": "0.25.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.6.3", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.81.0", - "metro-cache": "0.81.0", - "metro-cache-key": "0.81.0", - "metro-config": "0.81.0", - "metro-core": "0.81.0", - "metro-file-map": "0.81.0", - "metro-resolver": "0.81.0", - "metro-runtime": "0.81.0", - "metro-source-map": "0.81.0", - "metro-symbolicate": "0.81.0", - "metro-transform-plugins": "0.81.0", - "metro-transform-worker": "0.81.0", + "metro-babel-transformer": "0.81.1", + "metro-cache": "0.81.1", + "metro-cache-key": "0.81.1", + "metro-config": "0.81.1", + "metro-core": "0.81.1", + "metro-file-map": "0.81.1", + "metro-resolver": "0.81.1", + "metro-runtime": "0.81.1", + "metro-source-map": "0.81.1", + "metro-symbolicate": "0.81.1", + "metro-transform-plugins": "0.81.1", + "metro-transform-worker": "0.81.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", - "strip-ansi": "^6.0.0", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" @@ -33429,53 +34107,41 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.81.0.tgz", - "integrity": "sha512-Dc0QWK4wZIeHnyZ3sevWGTnnSkIDDn/SWyfrn99zbKbDOCoCYy71PAn9uCRrP/hduKLJQOy+tebd63Rr9D8tXg==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.81.1.tgz", + "integrity": "sha512-JECKDrQaUnDmj0x/Q/c8c5YwsatVx38Lu+BfCwX9fR8bWipAzkvJocBpq5rOAJRDXRgDcPv2VO4Q4nFYrpYNQg==", + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.24.0", + "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" }, "engines": { "node": ">=18.18" } }, - "node_modules/metro-babel-transformer/node_modules/hermes-estree": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.24.0.tgz", - "integrity": "sha512-LyoXLB7IFzeZW0EvAbGZacbxBN7t6KKSDqFJPo3Ydow7wDlrDjXwsdiAHV6XOdvEN9MEuWXsSIFN4tzpyrXIHw==", - "peer": true - }, - "node_modules/metro-babel-transformer/node_modules/hermes-parser": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.24.0.tgz", - "integrity": "sha512-IJooSvvu2qNRe7oo9Rb04sUT4omtZqZqf9uq9WM25Tb6v3usmvA93UqfnnoWs5V0uYjEl9Al6MNU10MCGKLwpg==", - "peer": true, - "dependencies": { - "hermes-estree": "0.24.0" - } - }, "node_modules/metro-cache": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.81.0.tgz", - "integrity": "sha512-DyuqySicHXkHUDZFVJmh0ygxBSx6pCKUrTcSgb884oiscV/ROt1Vhye+x+OIHcsodyA10gzZtrVtxIFV4l9I4g==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.81.1.tgz", + "integrity": "sha512-Uqcmn6sZ+Y0VJHM88VrG5xCvSeU7RnuvmjPmSOpEcyJJBe02QkfHL05MX2ZyGDTyZdbKCzaX0IijrTe4hN3F0Q==", + "license": "MIT", "peer": true, "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", - "metro-core": "0.81.0" + "metro-core": "0.81.1" }, "engines": { "node": ">=18.18" } }, "node_modules/metro-cache-key": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.81.0.tgz", - "integrity": "sha512-qX/IwtknP9bQZL78OK9xeSvLM/xlGfrs6SlUGgHvrxtmGTRSsxcyqxR+c+7ch1xr05n62Gin/O44QKg5V70rNQ==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.81.1.tgz", + "integrity": "sha512-5fDaHR1yTvpaQuwMAeEoZGsVyvjrkw9IFAS7WixSPvaNY5YfleqoJICPc6hbXFJjvwCCpwmIYFkjqzR/qJ6yqA==", + "license": "MIT", "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -33485,19 +34151,20 @@ } }, "node_modules/metro-config": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.81.0.tgz", - "integrity": "sha512-6CinEaBe3WLpRlKlYXXu8r1UblJhbwD6Gtnoib5U8j6Pjp7XxMG9h/DGMeNp9aGLDu1OieUqiXpFo7O0/rR5Kg==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.81.1.tgz", + "integrity": "sha512-VAAJmxsKIZ+Fz5/z1LVgxa32gE6+2TvrDSSx45g85WoX4EtLmdBGP3DSlpQW3DqFUfNHJCGwMLGXpJnxifd08g==", + "license": "MIT", "peer": true, "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.6.3", - "metro": "0.81.0", - "metro-cache": "0.81.0", - "metro-core": "0.81.0", - "metro-runtime": "0.81.0" + "metro": "0.81.1", + "metro-cache": "0.81.1", + "metro-core": "0.81.1", + "metro-runtime": "0.81.1" }, "engines": { "node": ">=18.18" @@ -33507,6 +34174,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", "peer": true, "dependencies": { "sprintf-js": "~1.0.2" @@ -33516,6 +34184,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", "peer": true, "dependencies": { "import-fresh": "^2.0.0", @@ -33531,6 +34200,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", "peer": true, "dependencies": { "caller-path": "^2.0.0", @@ -33544,6 +34214,7 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", "peer": true, "dependencies": { "argparse": "^1.0.7", @@ -33557,6 +34228,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", "peer": true, "dependencies": { "error-ex": "^1.3.1", @@ -33570,6 +34242,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", "peer": true, "engines": { "node": ">=4" @@ -33579,29 +34252,31 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause", "peer": true }, "node_modules/metro-core": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.81.0.tgz", - "integrity": "sha512-CVkM5YCOAFkNMvJai6KzA0RpztzfEKRX62/PFMOJ9J7K0uq/UkOFLxcgpcncMIrfy0PbfEj811b69tjULUQe1Q==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.81.1.tgz", + "integrity": "sha512-4d2/+02IYqOwJs4dmM0dC8hIZqTzgnx2nzN4GTCaXb3Dhtmi/SJ3v6744zZRnithhN4lxf8TTJSHnQV75M7SSA==", + "license": "MIT", "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.81.0" + "metro-resolver": "0.81.1" }, "engines": { "node": ">=18.18" } }, "node_modules/metro-file-map": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.81.0.tgz", - "integrity": "sha512-zMDI5uYhQCyxbye/AuFx/pAbsz9K+vKL7h1ShUXdN2fz4VUPiyQYRsRqOoVG1DsiCgzd5B6LW0YW77NFpjDQeg==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.81.1.tgz", + "integrity": "sha512-aY72H2ujmRfFxcsbyh83JgqFF+uQ4HFN1VhV2FmcfQG4s1bGKf2Vbkk+vtZ1+EswcBwDZFbkpvAjN49oqwGzAA==", + "license": "MIT", "peer": true, "dependencies": { - "anymatch": "^3.0.3", "debug": "^2.2.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", @@ -33609,21 +34284,18 @@ "invariant": "^2.2.4", "jest-worker": "^29.6.3", "micromatch": "^4.0.4", - "node-abort-controller": "^3.1.1", "nullthrows": "^1.1.1", "walker": "^1.0.7" }, "engines": { "node": ">=18.18" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" } }, "node_modules/metro-file-map/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "peer": true, "dependencies": { "ms": "2.0.0" @@ -33633,12 +34305,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", "peer": true }, "node_modules/metro-minify-terser": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.81.0.tgz", - "integrity": "sha512-U2ramh3W822ZR1nfXgIk+emxsf5eZSg10GbQrT0ZizImK8IZ5BmJY+BHRIkQgHzWFpExOVxC7kWbGL1bZALswA==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.81.1.tgz", + "integrity": "sha512-p/Qz3NNh1nebSqMlxlUALAnESo6heQrnvgHtAuxufRPtKvghnVDq9hGGex8H7z7YYLsqe42PWdt4JxTA3mgkvg==", + "license": "MIT", "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -33649,9 +34323,10 @@ } }, "node_modules/metro-resolver": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.81.0.tgz", - "integrity": "sha512-Uu2Q+buHhm571cEwpPek8egMbdSTqmwT/5U7ZVNpK6Z2ElQBBCxd7HmFAslKXa7wgpTO2FAn6MqGeERbAtVDUA==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.81.1.tgz", + "integrity": "sha512-E61t6fxRoYRkl6Zo3iUfCKW4DYfum/bLjcejXBMt1y3I7LFkK84TCR/Rs9OAwsMCY/7GOPB4+CREYZOtCC7CNA==", + "license": "MIT", "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -33661,9 +34336,10 @@ } }, "node_modules/metro-runtime": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.81.0.tgz", - "integrity": "sha512-6oYB5HOt37RuGz2eV4A6yhcl+PUTwJYLDlY9vhT+aVjbUWI6MdBCf69vc4f5K5Vpt+yOkjy+2LDwLS0ykWFwYw==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.81.1.tgz", + "integrity": "sha512-pqu5j5d01rjF85V/K8SDDJ0NR3dRp6bE3z5bKVVb5O2Rx0nbR9KreUxYALQCRCcQHaYySqCg5fYbGKBHC295YQ==", + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", @@ -33674,9 +34350,10 @@ } }, "node_modules/metro-source-map": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.81.0.tgz", - "integrity": "sha512-TzsVxhH83dyxg4A4+L1nzNO12I7ps5IHLjKGZH3Hrf549eiZivkdjYiq/S5lOB+p2HiQ+Ykcwtmcja95LIC62g==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.81.1.tgz", + "integrity": "sha512-1i8ROpNNiga43F0ZixAXoFE/SS3RqcRDCCslpynb+ytym0VI7pkTH1woAN2HI9pczYtPrp3Nq0AjRpsuY35ieA==", + "license": "MIT", "peer": true, "dependencies": { "@babel/traverse": "^7.25.3", @@ -33684,9 +34361,9 @@ "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.81.0", + "metro-symbolicate": "0.81.1", "nullthrows": "^1.1.1", - "ob1": "0.81.0", + "ob1": "0.81.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -33698,23 +34375,24 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/metro-symbolicate": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.81.0.tgz", - "integrity": "sha512-C/1rWbNTPYp6yzID8IPuQPpVGzJ2rbWYBATxlvQ9dfK5lVNoxcwz77hjcY8ISLsRRR15hyd/zbjCNKPKeNgE1Q==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.81.1.tgz", + "integrity": "sha512-Lgk0qjEigtFtsM7C0miXITbcV47E1ZYIfB+m/hCraihiwRWkNUQEPCWvqZmwXKSwVE5mXA0EzQtghAvQSjZDxw==", + "license": "MIT", "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.81.0", + "metro-source-map": "0.81.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", - "through2": "^2.0.1", "vlq": "^1.0.0" }, "bin": { @@ -33728,15 +34406,17 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/metro-transform-plugins": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.81.0.tgz", - "integrity": "sha512-uErLAPBvttGCrmGSCa0dNHlOTk3uJFVEVWa5WDg6tQ79PRmuYRwzUgLhVzn/9/kyr75eUX3QWXN79Jvu4txt6Q==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.81.1.tgz", + "integrity": "sha512-7L1lI44/CyjIoBaORhY9fVkoNe8hrzgxjSCQ/lQlcfrV31cZb7u0RGOQrKmUX7Bw4FpejrB70ArQ7Mse9mk7+Q==", + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", @@ -33751,9 +34431,10 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.81.0.tgz", - "integrity": "sha512-HrQ0twiruhKy0yA+9nK5bIe3WQXZcC66PXTvRIos61/EASLAP2DzEmW7IxN/MGsfZegN2UzqL2CG38+mOB45vg==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.81.1.tgz", + "integrity": "sha512-M+2hVT3rEy5K7PBmGDgQNq3Zx53TjScOcO/CieyLnCRFtBGWZiSJ2+bLAXXOKyKa/y3bI3i0owxtyxuPGDwbZg==", + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", @@ -33761,13 +34442,13 @@ "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "metro": "0.81.0", - "metro-babel-transformer": "0.81.0", - "metro-cache": "0.81.0", - "metro-cache-key": "0.81.0", - "metro-minify-terser": "0.81.0", - "metro-source-map": "0.81.0", - "metro-transform-plugins": "0.81.0", + "metro": "0.81.1", + "metro-babel-transformer": "0.81.1", + "metro-cache": "0.81.1", + "metro-cache-key": "0.81.1", + "metro-minify-terser": "0.81.1", + "metro-source-map": "0.81.1", + "metro-transform-plugins": "0.81.1", "nullthrows": "^1.1.1" }, "engines": { @@ -33778,12 +34459,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT", "peer": true }, "node_modules/metro/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "peer": true, "dependencies": { "ms": "2.0.0" @@ -33793,27 +34476,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", "peer": true }, - "node_modules/metro/node_modules/hermes-estree": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.24.0.tgz", - "integrity": "sha512-LyoXLB7IFzeZW0EvAbGZacbxBN7t6KKSDqFJPo3Ydow7wDlrDjXwsdiAHV6XOdvEN9MEuWXsSIFN4tzpyrXIHw==", - "peer": true - }, - "node_modules/metro/node_modules/hermes-parser": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.24.0.tgz", - "integrity": "sha512-IJooSvvu2qNRe7oo9Rb04sUT4omtZqZqf9uq9WM25Tb6v3usmvA93UqfnnoWs5V0uYjEl9Al6MNU10MCGKLwpg==", - "peer": true, - "dependencies": { - "hermes-estree": "0.24.0" - } - }, "node_modules/metro/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -33823,12 +34493,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", "peer": true }, "node_modules/metro/node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -33838,6 +34510,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "peer": true, "dependencies": { "emoji-regex": "^8.0.0", @@ -33852,6 +34525,7 @@ "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=8.3.0" @@ -33873,6 +34547,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "peer": true, "dependencies": { "cliui": "^8.0.1", @@ -35752,9 +36427,10 @@ "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, "node_modules/monaco-editor": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT", "peer": true }, "node_modules/motion": { @@ -35812,6 +36488,122 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/msw": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.6.8.tgz", + "integrity": "sha512-nxXxnH6WALZ9a7rsQp4HU2AaD4iGAiouMmE/MY4al7pXTibgA6OZOuKhmN2WBIM6w9qMKwRtX8p2iOb45B2M/Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", + "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/msw/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/multer": { "version": "1.4.4-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", @@ -35971,6 +36763,15 @@ } } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next": { "version": "14.2.4", "resolved": "https://registry.npmjs.org/next/-/next-14.2.4.tgz", @@ -36082,47 +36883,14 @@ "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true }, "node_modules/node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, - "node_modules/node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", - "peer": true, - "dependencies": { - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.10.5" - } - }, - "node_modules/node-dir/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/node-dir/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/node-emoji": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", @@ -36184,6 +36952,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } @@ -36327,6 +37096,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT", "peer": true }, "node_modules/nwsapi": { @@ -36481,9 +37251,10 @@ } }, "node_modules/ob1": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.0.tgz", - "integrity": "sha512-6Cvrkxt1tqaRdWqTAMcVYEiO5i1xcF9y7t06nFdjFqkfPsEloCf8WwhXdwBpNUkVYSQlSGS7cDgVQR86miBfBQ==", + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.1.tgz", + "integrity": "sha512-1PEbvI+AFvOcgdNcO79FtDI1TUO8S3lhiKOyAiyWQF3sFDDKS+aw2/BZvGlArFnSmqckwOOB9chQuIX0/OahoQ==", + "license": "MIT", "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -36835,6 +37606,13 @@ "node": ">=0.10.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/oxc-resolver": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-1.10.2.tgz", @@ -36938,6 +37716,51 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", @@ -37754,18 +38577,6 @@ } } }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", - "dev": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/postcss-loader": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", @@ -38422,6 +39233,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", "peer": true, "dependencies": { "asap": "~2.0.6" @@ -38512,6 +39324,47 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-compare": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.1.tgz", @@ -39127,10 +39980,20 @@ "node": ">=6" } }, + "node_modules/react-dev-utils/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/react-devtools-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.2.tgz", - "integrity": "sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.0.tgz", + "integrity": "sha512-sA8gF/pUhjoGAN3s1Ya43h+F4Q0z7cv9RgqbUfhP7bJI0MbqeshLYFb6hiHgZorovGr8AXqhLi22eQ7V3pru/Q==", + "license": "MIT", "peer": true, "dependencies": { "shell-quote": "^1.6.1", @@ -39141,6 +40004,7 @@ "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=8.3.0" @@ -39241,24 +40105,25 @@ } }, "node_modules/react-native": { - "version": "0.76.3", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.3.tgz", - "integrity": "sha512-0TUhgmlouRNf6yuDIIAdbQl0g1VsONgCMsLs7Et64hjj5VLMCA7np+4dMrZvGZ3wRNqzgeyT9oWJsUm49AcwSQ==", + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.77.0.tgz", + "integrity": "sha512-oCgHLGHFIp6F5UbyHSedyUXrZg6/GPe727freGFvlT7BjPJ3K6yvvdlsp7OEXSAHz6Fe7BI2n5cpUyqmP9Zn+Q==", + "license": "MIT", "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.6.3", - "@react-native/assets-registry": "0.76.3", - "@react-native/codegen": "0.76.3", - "@react-native/community-cli-plugin": "0.76.3", - "@react-native/gradle-plugin": "0.76.3", - "@react-native/js-polyfills": "0.76.3", - "@react-native/normalize-colors": "0.76.3", - "@react-native/virtualized-lists": "0.76.3", + "@react-native/assets-registry": "0.77.0", + "@react-native/codegen": "0.77.0", + "@react-native/community-cli-plugin": "0.77.0", + "@react-native/gradle-plugin": "0.77.0", + "@react-native/js-polyfills": "0.77.0", + "@react-native/normalize-colors": "0.77.0", + "@react-native/virtualized-lists": "0.77.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", - "babel-plugin-syntax-hermes-parser": "^0.23.1", + "babel-plugin-syntax-hermes-parser": "0.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", @@ -39271,11 +40136,10 @@ "memoize-one": "^5.0.0", "metro-runtime": "^0.81.0", "metro-source-map": "^0.81.0", - "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", - "react-devtools-core": "^5.3.1", + "react-devtools-core": "^6.0.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", @@ -39326,6 +40190,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -39338,6 +40203,7 @@ "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", "peer": true, "engines": { "node": ">=18" @@ -39347,12 +40213,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", "peer": true }, "node_modules/react-native/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -39362,6 +40230,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", @@ -39376,27 +40245,31 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT", "peer": true }, "node_modules/react-native/node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", "peer": true }, "node_modules/react-native/node_modules/scheduler": { "version": "0.24.0-canary-efb381bbf-20230505", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "license": "MIT", "peer": true, "dependencies": { "loose-envify": "^1.1.0" } }, "node_modules/react-native/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "peer": true, "bin": { "semver": "bin/semver.js" @@ -39409,6 +40282,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "peer": true, "dependencies": { "emoji-regex": "^8.0.0", @@ -39423,6 +40297,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", "peer": true, "dependencies": { "async-limiter": "~1.0.0" @@ -39432,6 +40307,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "peer": true, "dependencies": { "cliui": "^8.0.1", @@ -39450,6 +40326,7 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -39630,6 +40507,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", + "license": "BSD", "peer": true }, "node_modules/real-require": { @@ -39641,14 +40519,16 @@ } }, "node_modules/recast": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.5.tgz", - "integrity": "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==", + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", + "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "license": "MIT", "peer": true, "dependencies": { - "ast-types": "0.15.2", + "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" }, "engines": { @@ -39659,6 +40539,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -40580,6 +41461,7 @@ "version": "2.17.3", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", "peer": true }, "node_modules/secp256k1": { @@ -40587,6 +41469,7 @@ "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", "hasInstallScript": true, + "license": "MIT", "peer": true, "dependencies": { "elliptic": "^6.5.7", @@ -40601,6 +41484,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT", "peer": true }, "node_modules/section-matter": { @@ -40716,6 +41600,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -41157,6 +42042,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -41210,6 +42105,34 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", @@ -41403,12 +42326,14 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT", "peer": true }, "node_modules/stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", + "license": "MIT", "peer": true, "dependencies": { "type-fest": "^0.7.1" @@ -41421,6 +42346,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", "peer": true, "engines": { "node": ">=8" @@ -41531,6 +42457,13 @@ "node": ">=10.0.0" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -42325,31 +43258,6 @@ "node": ">=6" } }, - "node_modules/temp": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", - "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", - "peer": true, - "dependencies": { - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/terser": { "version": "5.31.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", @@ -42582,6 +43490,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT", "peer": true }, "node_modules/through": { @@ -42590,52 +43499,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "peer": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "peer": true - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "peer": true - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -42655,7 +43518,6 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, "engines": { "node": ">=14.14" } @@ -44183,6 +45045,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT", "peer": true }, "node_modules/w3c-xmlserializer": { @@ -44682,6 +45545,7 @@ "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT", "peer": true }, "node_modules/whatwg-mimetype": { @@ -45080,11 +45944,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -45208,6 +46076,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", @@ -45267,7 +46148,7 @@ }, "packages/armory-sdk": { "name": "@narval-xyz/armory-sdk", - "version": "0.9.1", + "version": "0.20.0", "license": "MPL-2.0", "dependencies": { "@noble/curves": "1.6.0", diff --git a/package.json b/package.json index 4d411845a..7e1b3b360 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@nx/node": "19.2.0", "@nx/webpack": "19.6.3", "@nx/workspace": "19.7.3", - "@openapitools/openapi-generator-cli": "2.13.4", + "@openapitools/openapi-generator-cli": "2.15.3", "@swc-node/register": "1.10.9", "@swc/core": "1.7.26", "@swc/helpers": "0.5.11", @@ -41,7 +41,7 @@ "@typescript-eslint/parser": "7.9.0", "autoprefixer": "10.4.20", "babel-jest": "29.7.0", - "dotenv": "16.4.5", + "dotenv": "16.4.7", "dotenv-cli": "7.3.0", "eslint": "8.57.0", "eslint-config-next": "14.2.6", @@ -56,6 +56,7 @@ "jest-environment-node": "29.7.0", "jest-mock-extended": "3.0.5", "lint-staged": "15.2.0", + "msw": "2.6.8", "nock": "13.5.4", "nx": "19.4.4", "postcss": "8.4.47", @@ -83,9 +84,11 @@ "@monaco-editor/react": "4.6.0", "@nestjs/axios": "3.0.2", "@nestjs/bull": "10.2.0", + "@nestjs/cache-manager": "3.0.0", "@nestjs/common": "10.3.10", "@nestjs/config": "3.1.1", "@nestjs/core": "10.4.7", + "@nestjs/event-emitter": "2.1.1", "@nestjs/platform-express": "10.3.9", "@nestjs/swagger": "7.4.0", "@noble/curves": "1.6.0", @@ -109,12 +112,13 @@ "@radix-ui/react-popper": "1.2.0", "@radix-ui/react-radio-group": "1.2.0", "@radix-ui/react-tooltip": "1.0.7", - "@scure/bip32": "1.5.0", + "@scure/bip32": "1.6.2", "@scure/bip39": "1.3.0", "@tanstack/react-query": "5.51.11", "axios": "1.7.7", - "axios-retry": "4.4.2", + "axios-retry": "4.5.0", "bull": "4.16.4", + "cache-manager": "6.3.2", "class-transformer": "0.5.1", "class-validator": "0.14.1", "clsx": "2.1.1", @@ -127,6 +131,7 @@ "lowdb": "7.0.1", "nestjs-zod": "3.0.0", "next": "14.2.4", + "node-forge": "1.3.1", "permissionless": "0.1.44", "prism-react-renderer": "2.3.1", "react": "18.2.0", @@ -144,6 +149,7 @@ "wagmi": "2.12.31", "winston": "3.14.2", "winston-transport": "4.7.0", + "yaml": "2.6.1", "zod": "3.23.8" }, "optionalDependencies": { @@ -152,4 +158,4 @@ "nx": { "includedScripts": [] } -} \ No newline at end of file +} diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenarii/address-book-management.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/address-book-management.spec.ts similarity index 100% rename from packages/armory-e2e-testing/src/__test__/e2e/scenarii/address-book-management.spec.ts rename to packages/armory-e2e-testing/src/__test__/e2e/scenario/address-book-management.spec.ts diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenarii/approvals-and-spending-limit.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/approvals-and-spending-limit.spec.ts similarity index 100% rename from packages/armory-e2e-testing/src/__test__/e2e/scenarii/approvals-and-spending-limit.spec.ts rename to packages/armory-e2e-testing/src/__test__/e2e/scenario/approvals-and-spending-limit.spec.ts diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenario/bind-access-token.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/bind-access-token.spec.ts new file mode 100644 index 000000000..5076d92c6 --- /dev/null +++ b/packages/armory-e2e-testing/src/__test__/e2e/scenario/bind-access-token.spec.ts @@ -0,0 +1,394 @@ +/* eslint-disable no-console */ +import { + Action, + AuthAdminClient, + AuthClient, + Criterion, + Entities, + EntityStoreClient, + EntityUtil, + Permission, + Policy, + PolicyStoreClient, + Then, + UserEntity, + UserRole, + VaultAdminClient, + VaultClient, + createHttpDataStore, + credential, + resourceId +} from '@narval/armory-sdk' +import { + AccountEntity, + AccountType, + ConfirmationClaimProofMethod, + SignTransactionAction +} from '@narval/policy-engine-shared' +import { Ed25519PrivateKey, buildSignerForAlg, getPublicKey, hash, signJwt } from '@narval/signature' +import { AxiosError } from 'axios' +import { randomUUID } from 'crypto' +import { format } from 'date-fns' +import { v4 as uuid } from 'uuid' +import { Hex } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +const ACCOUNT_PRIVATE_KEY: Hex = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' + +const TEST_TIMEOUT_MS = 30_000 + +jest.setTimeout(TEST_TIMEOUT_MS) + +const getAuthHost = () => 'http://localhost:3005' + +const getAuthAdminApiKey = () => 'armory-admin-api-key' + +const getVaultHost = () => 'http://localhost:3011' + +const getVaultAdminApiKey = () => 'vault-admin-api-key' + +// IMPORTANT: The order of tests matters. +// These tests are meant to be run in series, not in parallel, because they +// represent an end-to-end user journey. +describe('User Journeys', () => { + let ephemeralPrivateKey: Ed25519PrivateKey + let dataStorePrivateKey: Ed25519PrivateKey + let appPrivateKey: Ed25519PrivateKey + let authClient: AuthClient + let vaultClient: VaultClient + + const clientId = uuid() + + const setup = async () => { + try { + const authAdminClient = new AuthAdminClient({ + host: getAuthHost(), + adminApiKey: getAuthAdminApiKey() + }) + + const vaultAdminClient = new VaultAdminClient({ + host: getVaultHost(), + adminApiKey: getVaultAdminApiKey() + }) + + const vaultClient = new VaultClient({ + host: getVaultHost(), + clientId, + signer: { + jwk: getPublicKey(ephemeralPrivateKey), + alg: ephemeralPrivateKey.alg, + sign: await buildSignerForAlg(ephemeralPrivateKey) + } + }) + + const authClient = new AuthClient({ + host: getAuthHost(), + clientId, + signer: { + jwk: getPublicKey(appPrivateKey), + alg: appPrivateKey.alg, + sign: await buildSignerForAlg(appPrivateKey) + }, + pollingTimeoutMs: TEST_TIMEOUT_MS - 10_000, + // In a local AS and PE, 250 ms is equivalent to ~3 requests until + // the job is processed. + pollingIntervalMs: 250 + }) + + const clientAuth = await authAdminClient.createClient({ + id: clientId, + name: `Bind Access Token E2E Test ${format(new Date(), 'dd/MM/yyyy HH:mm:ss')}`, + dataStore: createHttpDataStore({ + host: getAuthHost(), + clientId, + keys: [getPublicKey(dataStorePrivateKey)] + }), + useManagedDataStore: true + }) + + await vaultAdminClient.createClient({ + clientId, + name: `provider-e2e-testing-${clientId}`, + baseUrl: getVaultHost(), + auth: { + local: { + jwsd: { + maxAge: 300, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + } + }, + tokenValidation: { + disabled: false, + url: null, + // IMPORTANT: Pin the policy engine's public key in the token + // validation. + pinnedPublicKey: clientAuth.policyEngine.nodes[0].publicKey, + verification: { + audience: null, + issuer: 'https://armory.narval.xyz', + maxTokenAge: 300, + // IMPORTANT: It must be set to true + requireBoundTokens: true, + // IMPORTANT: It does not need bearer token + allowBearerTokens: false + } + } + } + }) + + const viemAccount = privateKeyToAccount(ACCOUNT_PRIVATE_KEY) + + const account: AccountEntity = { + id: resourceId(viemAccount.address), + address: viemAccount.address, + accountType: AccountType.EOA + } + + const appUser: UserEntity = { + id: uuid(), + role: UserRole.ADMIN + } + + const entities: Entities = { + ...EntityUtil.empty(), + users: [appUser], + credentials: [credential(appUser, getPublicKey(appPrivateKey))], + accounts: [account] + } + + const policies: Policy[] = [ + { + id: uuid(), + description: 'Allows admin to do anything', + when: [ + { + criterion: Criterion.CHECK_PRINCIPAL_ROLE, + args: [UserRole.ADMIN] + } + ], + then: Then.PERMIT + } + ] + + const entityStoreClient = new EntityStoreClient({ + host: getAuthHost(), + clientId: clientAuth.id, + clientSecret: clientAuth.clientSecret, + signer: { + jwk: dataStorePrivateKey, + alg: dataStorePrivateKey.alg, + sign: await buildSignerForAlg(dataStorePrivateKey) + } + }) + + await entityStoreClient.signAndPush(entities) + + const policyStoreClient = new PolicyStoreClient({ + host: getAuthHost(), + clientId: clientAuth.id, + clientSecret: clientAuth.clientSecret, + signer: { + jwk: dataStorePrivateKey, + alg: dataStorePrivateKey.alg, + sign: await buildSignerForAlg(dataStorePrivateKey) + } + }) + + await policyStoreClient.signAndPush(policies) + + return { + vaultClient, + authClient + } + } catch (error) { + if (error instanceof AxiosError) { + console.dir( + { + url: error.config?.url, + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } + + throw error + } + } + + beforeAll(async () => { + // Pin the keys because it's easier to debug a known value. + ephemeralPrivateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0x65a3f312d1fc34e937ca9c1b7fbe5b9f98fb15e2cb15594ec6cd5167e36a58e3', + x: 'n0AX7pAzBhCr6R7dRhPqeGDVIKRaatVjdmL3KX58HGw', + d: 'tl8nZiFTRa5C_yJvL73KFnxDbuUi8h6bUvh28jvXmII' + } + + dataStorePrivateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0xb97232aabc42dbf69f19379a66417f9488520a3d3062bd14932ffc61e6958755', + x: '1e9Qy9e12g_HNNad-BxcgMWl7W7htIZ-M50Xr-RSFSM', + d: 'XxsSP2a3notOyIBr4qWwQl-uNsUqrG8cCXDwLRVR08w' + } + + appPrivateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0xffe1b0b211c314237b26cc5fc346445496a4b0ab65e5c007de46dfcdbf917ced', + x: 'bqV752BughPmE8QWcHc3EQdfrzomMcr_1k2kaj6v3mY', + d: '0ZIIuPFtY6iOuqlp7CcX4yyAIokJtLRqu43voyfyjZg' + } + + const context = await setup() + + authClient = context.authClient + vaultClient = context.vaultClient + }) + + it('I can issue a grant permission bound access token and use it', async () => { + try { + const request = { + action: Action.GRANT_PERMISSION, + resourceId: 'vault', + nonce: randomUUID(), + permissions: ['connection:write', 'connection:read'] + } + + const accessToken = await authClient.requestAccessToken(request, { + metadata: { + confirmation: { + key: { + jwk: getPublicKey(ephemeralPrivateKey), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + requestHash: hash(request) + }, + ephemeralPrivateKey + ) + } + } + } + }) + + await expect(vaultClient.listConnections({ accessToken })).resolves.not.toThrow() + } catch (error) { + if (error instanceof AxiosError) { + console.dir( + { + url: error.config?.url, + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } + + throw error + } + }) + + it('I can issue a bound access token and use it to sign transaction', async () => { + try { + // + // SETUP + // + + const grantPermissionRequest = { + action: Action.GRANT_PERMISSION, + resourceId: 'vault', + nonce: randomUUID(), + permissions: [Permission.WALLET_IMPORT, Permission.WALLET_CREATE] + } + + const importAccountAccessToken = await authClient.requestAccessToken(grantPermissionRequest, { + metadata: { + confirmation: { + key: { + jwk: getPublicKey(ephemeralPrivateKey), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + requestHash: hash(grantPermissionRequest) + }, + ephemeralPrivateKey + ) + } + } + } + }) + + const encryptionKey = await vaultClient.generateEncryptionKey({ accessToken: importAccountAccessToken }) + + const account = await vaultClient.importAccount({ + data: { + privateKey: ACCOUNT_PRIVATE_KEY + }, + accessToken: importAccountAccessToken, + encryptionKey + }) + + // + // SIGN TRANSACTION + // + + const signTxRequest: SignTransactionAction = { + action: Action.SIGN_TRANSACTION, + nonce: uuid(), + resourceId: account.id, + transactionRequest: { + chainId: 1, + data: '0x', + from: account.address, + gas: 5000n, + nonce: 0, + to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23', + type: '2' + } + } + + const signTxAccessToken = await authClient.requestAccessToken(signTxRequest, { + metadata: { + confirmation: { + key: { + jwk: getPublicKey(ephemeralPrivateKey), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + requestHash: hash(signTxRequest) + }, + ephemeralPrivateKey + ) + } + } + } + }) + + const { signature } = await vaultClient.sign({ + data: signTxRequest, + accessToken: signTxAccessToken + }) + + expect(signature).toEqual(expect.any(String)) + } catch (error) { + if (error instanceof AxiosError) { + console.dir( + { + url: error.config?.url, + status: error.response?.status, + body: error.response?.data + }, + { depth: null } + ) + } + + throw error + } + }) +}) diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenarii/defi-interactions.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/defi-interactions.spec.ts similarity index 100% rename from packages/armory-e2e-testing/src/__test__/e2e/scenarii/defi-interactions.spec.ts rename to packages/armory-e2e-testing/src/__test__/e2e/scenario/defi-interactions.spec.ts diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenarii/tiered-eth-transfer-policy.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/tiered-eth-transfer-policy.spec.ts similarity index 100% rename from packages/armory-e2e-testing/src/__test__/e2e/scenarii/tiered-eth-transfer-policy.spec.ts rename to packages/armory-e2e-testing/src/__test__/e2e/scenario/tiered-eth-transfer-policy.spec.ts diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenarii/user-journeys.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/user-journeys.spec.ts similarity index 95% rename from packages/armory-e2e-testing/src/__test__/e2e/scenarii/user-journeys.spec.ts rename to packages/armory-e2e-testing/src/__test__/e2e/scenario/user-journeys.spec.ts index d943f5254..8c767ee6b 100644 --- a/packages/armory-e2e-testing/src/__test__/e2e/scenarii/user-journeys.spec.ts +++ b/packages/armory-e2e-testing/src/__test__/e2e/scenario/user-journeys.spec.ts @@ -218,7 +218,35 @@ describe('User Journeys', () => { expect(clientVault).toEqual({ clientId: clientAuth.id, - engineJwk: clientAuth.policyEngine.nodes[0].publicKey, + name: expect.any(String), + backupPublicKey: null, + baseUrl: null, + configurationSource: 'dynamic', + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 300, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: null + }, + tokenValidation: { + disabled: false, + url: null, + jwksUrl: null, + pinnedPublicKey: clientAuth.policyEngine.nodes[0].publicKey, + verification: { + audience: null, + issuer: null, + maxTokenAge: null, + requireBoundTokens: true, + allowBearerTokens: false, + allowWildcard: null + } + } + }, createdAt: expect.any(String), updatedAt: expect.any(String) }) diff --git a/packages/armory-e2e-testing/src/__test__/e2e/scenario/vault-provider.spec.ts b/packages/armory-e2e-testing/src/__test__/e2e/scenario/vault-provider.spec.ts new file mode 100644 index 000000000..565342aef --- /dev/null +++ b/packages/armory-e2e-testing/src/__test__/e2e/scenario/vault-provider.spec.ts @@ -0,0 +1,152 @@ +import { ClientDto, VaultAdminClient, VaultClient } from '@narval/armory-sdk' +import { + Alg, + Ed25519PrivateKey, + Ed25519PublicKey, + buildSignerForAlg, + generateJwk, + getPublicKey +} from '@narval/signature' +import { InitiateConnectionDtoProviderEnum } from 'packages/armory-sdk/src/lib/http/client/vault/api' +import { v4 as uuid } from 'uuid' + +const TEST_TIMEOUT_MS = 30_000 + +jest.setTimeout(TEST_TIMEOUT_MS) + +let userPrivateKey: Ed25519PrivateKey +let userPublicKey: Ed25519PublicKey + +const VAULT_HOST_URL = 'http://localhost:3011' + +const VAULT_ADMIN_API_KEY = 'vault-admin-api-key' + +// IMPORTANT: The order of tests matters. +// These tests are meant to be run in series, not in parallel, because they +// represent an end-to-end user journey. +describe('User Journeys', () => { + let clientVault: ClientDto + const clientId = uuid() + + beforeAll(async () => { + userPrivateKey = await generateJwk(Alg.EDDSA) + userPublicKey = getPublicKey(userPrivateKey) + }) + + describe('As an admin', () => { + const vaultAdminClient = new VaultAdminClient({ + host: VAULT_HOST_URL, + adminApiKey: VAULT_ADMIN_API_KEY + }) + + it('I can create a new client in the vault', async () => { + clientVault = await vaultAdminClient.createClient({ + clientId, + name: `provider-e2e-testing-${clientId}`, + baseUrl: VAULT_HOST_URL, + auth: { + local: { + jwsd: { + maxAge: 300, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsers: [ + { + userId: 'provider-e2e-testing-user-123', + publicKey: userPublicKey + } + ] + }, + tokenValidation: { + disabled: true + } + } + }) + + expect(clientVault).toEqual({ + clientId, + name: expect.any(String), + backupPublicKey: null, + baseUrl: VAULT_HOST_URL, + configurationSource: 'dynamic', + auth: { + disabled: false, + local: { + jwsd: { + maxAge: 300, + requiredComponents: ['htm', 'uri', 'created', 'ath'] + }, + allowedUsersJwksUrl: null, + allowedUsers: [ + { + userId: 'provider-e2e-testing-user-123', + publicKey: userPublicKey + } + ] + }, + tokenValidation: { + disabled: true, + url: null, + jwksUrl: null, + pinnedPublicKey: null, + verification: { + audience: null, + issuer: null, + maxTokenAge: null, + requireBoundTokens: true, + allowBearerTokens: false, + allowWildcard: null + } + } + }, + createdAt: expect.any(String), + updatedAt: expect.any(String) + }) + }) + }) + + describe('As a client', () => { + let vaultClient: VaultClient + + beforeAll(async () => { + vaultClient = new VaultClient({ + host: VAULT_HOST_URL, + clientId, + signer: { + jwk: userPublicKey, + alg: userPrivateKey.alg, + sign: await buildSignerForAlg(userPrivateKey) + } + }) + }) + + describe('I want to interact with the vault', () => { + it('I can generate an encryption key', async () => { + const encryptionKey = await vaultClient.generateEncryptionKey() + + expect(encryptionKey.alg).toEqual('RS256') + expect(encryptionKey.kty).toEqual('RSA') + expect(encryptionKey.use).toEqual('enc') + }) + + it('I can list connections', async () => { + const connections = await vaultClient.listConnections() + + expect(connections).toEqual({ + data: [], + page: { next: null } + }) + }) + + it('I can initiate a connection', async () => { + const connection = await vaultClient.initiateConnection({ + data: { + provider: InitiateConnectionDtoProviderEnum.Anchorage + } + }) + + expect(connection.data.connectionId).toBeDefined() + }) + }) + }) +}) diff --git a/packages/armory-sdk/Makefile b/packages/armory-sdk/Makefile index 0e9b75e4e..3bcc813d9 100644 --- a/packages/armory-sdk/Makefile +++ b/packages/armory-sdk/Makefile @@ -50,7 +50,8 @@ armory-sdk/generate/-http-client: --input-spec ${INPUT_SPEC} \ --output ${OUTPUT_DIR} \ --generator-name typescript-axios \ - --remove-operation-id-prefix + --remove-operation-id-prefix \ + --additional-properties=useSingleRequestParameter=${USE_SINGLE_REQUEST_PARAMETER} rm -rf ./${OUTPUT_DIR}/.openapi-generator \ ./${OUTPUT_DIR}/.gitignore \ @@ -61,9 +62,11 @@ armory-sdk/generate/-http-client: armory-sdk/generate/auth-client: INPUT_SPEC=http://localhost:3005/docs-yaml \ OUTPUT_DIR=packages/armory-sdk/src/lib/http/client/auth \ + USE_SINGLE_REQUEST_PARAMETER=false \ make armory-sdk/generate/-http-client armory-sdk/generate/vault-client: INPUT_SPEC=http://localhost:3011/docs-yaml \ OUTPUT_DIR=packages/armory-sdk/src/lib/http/client/vault \ + USE_SINGLE_REQUEST_PARAMETER=true \ make armory-sdk/generate/-http-client diff --git a/packages/armory-sdk/README.md b/packages/armory-sdk/README.md index 5ff07d7bd..7a9bf4483 100644 --- a/packages/armory-sdk/README.md +++ b/packages/armory-sdk/README.md @@ -24,8 +24,8 @@ make armory/start/dev make policy-engine/start/dev make vault/start/dev -make armory-sdk/test/e2e -make armory-sdk/test/e2e/watch +make armory-e2e-testing/test/e2e +make armory-e2e-testing/test/e2e/watch ``` The tests MUST run in series because each step depends on state changes from diff --git a/packages/armory-sdk/package.json b/packages/armory-sdk/package.json index a985b7c95..10a50fa99 100644 --- a/packages/armory-sdk/package.json +++ b/packages/armory-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@narval-xyz/armory-sdk", - "version": "0.10.0", + "version": "0.20.0", "license": "MPL-2.0", "publishConfig": { "access": "public" diff --git a/packages/armory-sdk/src/lib/auth/client.ts b/packages/armory-sdk/src/lib/auth/client.ts index f4f9311bf..bc8b985a7 100644 --- a/packages/armory-sdk/src/lib/auth/client.ts +++ b/packages/armory-sdk/src/lib/auth/client.ts @@ -14,10 +14,10 @@ import { v4 as uuid } from 'uuid' import { ArmorySdkException } from '../exceptions' import { ApplicationApi, - AuthorizationApiFactory, + AuthorizationApi, AuthorizationResponseDto, AuthorizationResponseDtoStatusEnum, - ClientApiFactory, + ClientApi, Configuration, CreateClientRequestDto, CreateClientResponseDto, @@ -26,20 +26,12 @@ import { import { polling } from '../shared/promise' import { SignOptions } from '../shared/type' import { AuthorizationResponse } from '../types' -import { - AuthAdminConfig, - AuthClientHttp, - AuthConfig, - AuthorizationHttp, - AuthorizationResult, - Evaluate, - RequestAccessTokenOptions -} from './type' +import { AuthAdminConfig, AuthConfig, AuthorizationResult, Evaluate, RequestAccessTokenOptions } from './type' export class AuthAdminClient { private config: AuthAdminConfig - private clientHttp: AuthClientHttp + private clientHttp: ClientApi constructor(config: AuthAdminConfig) { const httpConfig = new Configuration({ @@ -49,7 +41,8 @@ export class AuthAdminClient { const axiosInstance = axios.create() this.config = config - this.clientHttp = ClientApiFactory(httpConfig, config.host, axiosInstance) + + this.clientHttp = new ClientApi(httpConfig, config.host, axiosInstance) } /** @@ -62,7 +55,10 @@ export class AuthAdminClient { async createClient(input: CreateClientRequestDto): Promise { assert(this.config.adminApiKey !== undefined, 'Missing admin API key') - const { data } = await this.clientHttp.create(this.config.adminApiKey, input) + const { data } = await this.clientHttp.create({ + xApiKey: this.config.adminApiKey, + createClientRequestDto: input + }) return data } @@ -71,7 +67,7 @@ export class AuthAdminClient { export class AuthClient { private config: AuthConfig - private authorizationHttp: AuthorizationHttp + private authorizationHttp: AuthorizationApi private applicationApi: ApplicationApi @@ -84,7 +80,7 @@ export class AuthClient { this.config = AuthConfig.parse(config) - this.authorizationHttp = AuthorizationApiFactory(httpConfig, config.host, axiosInstance) + this.authorizationHttp = new AuthorizationApi(httpConfig, config.host, axiosInstance) this.applicationApi = new ApplicationApi(httpConfig, config.host, axiosInstance) } @@ -116,7 +112,10 @@ export class AuthClient { authentication }) - const { data } = await this.authorizationHttp.evaluate(this.config.clientId, request) + const { data } = await this.authorizationHttp.evaluate({ + xClientId: this.config.clientId, + authorizationRequestDto: request + }) return polling({ fn: async () => AuthorizationRequest.parse(await this.getAuthorizationById(data.id)), @@ -135,7 +134,10 @@ export class AuthClient { * @returns A Promise that resolves to the retrieved AuthorizationResponseDto. */ async getAuthorizationById(id: string): Promise { - const { data } = await this.authorizationHttp.getById(id, this.config.clientId) + const { data } = await this.authorizationHttp.getById({ + xClientId: this.config.clientId, + id + }) return data } @@ -170,10 +172,17 @@ export class AuthClient { * @returns A promise that resolves to the authorization response. */ async approve(requestId: string): Promise { - const res = await this.authorizationHttp.getById(requestId, this.config.clientId) + const res = await this.authorizationHttp.getById({ + xClientId: this.config.clientId, + id: requestId + }) const { request } = AuthorizationResponse.parse(res.data) const signature = await this.signJwtPayload(this.buildJwtPayload(request)) - const { data } = await this.authorizationHttp.approve(requestId, this.config.clientId, { signature }) + const { data } = await this.authorizationHttp.approve({ + xClientId: this.config.clientId, + id: requestId, + approvalDto: { signature } + }) return data } @@ -188,7 +197,10 @@ export class AuthClient { * @returns */ async getAccessToken(requestId: string): Promise { - const res = await this.authorizationHttp.getById(requestId, this.config.clientId) + const res = await this.authorizationHttp.getById({ + xClientId: this.config.clientId, + id: requestId + }) const lastSignature = reverse(res.data.evaluations).find((e) => e.signature !== null)?.signature if (lastSignature) { @@ -266,6 +278,7 @@ export class AuthClient { ): Promise { const authorization = await this.evaluate( { + ...(opts?.metadata && { metadata: opts.metadata }), id: opts?.id || uuid(), approvals: opts?.approvals || [], request: { diff --git a/packages/armory-sdk/src/lib/auth/type.ts b/packages/armory-sdk/src/lib/auth/type.ts index 09105df97..eaa27a361 100644 --- a/packages/armory-sdk/src/lib/auth/type.ts +++ b/packages/armory-sdk/src/lib/auth/type.ts @@ -89,7 +89,7 @@ export type AuthClientHttp = { } export type RequestAccessTokenOptions = SignOptions & - SetOptional, 'id' | 'approvals'> + SetOptional, 'id' | 'approvals' | 'metadata'> export type Evaluate = Omit diff --git a/packages/armory-sdk/src/lib/data-store/client.ts b/packages/armory-sdk/src/lib/data-store/client.ts index bccfc3d1c..b7abf7f5f 100644 --- a/packages/armory-sdk/src/lib/data-store/client.ts +++ b/packages/armory-sdk/src/lib/data-store/client.ts @@ -4,9 +4,9 @@ import assert from 'assert' import axios, { InternalAxiosRequestConfig } from 'axios' import { promisify } from 'util' import * as zlib from 'zlib' -import { Configuration, ManagedDataStoreApiFactory } from '../http/client/auth' +import { Configuration, ManagedDataStoreApi } from '../http/client/auth' import { REQUEST_HEADER_CLIENT_ID, REQUEST_HEADER_CLIENT_SECRET } from '../shared/constant' -import { DataStoreConfig, DataStoreHttp, SetEntityStoreResponse, SetPolicyStoreResponse, SignOptions } from './type' +import { DataStoreConfig, SetEntityStoreResponse, SetPolicyStoreResponse, SignOptions } from './type' const gzip = promisify(zlib.gzip) @@ -47,7 +47,7 @@ export const addCompressionInterceptor = (axiosInstance: any) => { export class EntityStoreClient { private config: DataStoreConfig - private dataStoreHttp: DataStoreHttp + private dataStoreHttp: ManagedDataStoreApi constructor(config: DataStoreConfig) { this.config = config @@ -60,7 +60,7 @@ export class EntityStoreClient { addCompressionInterceptor(axiosInstance) - this.dataStoreHttp = ManagedDataStoreApiFactory(httpConfig, config.host, axiosInstance) + this.dataStoreHttp = new ManagedDataStoreApi(httpConfig, config.host, axiosInstance) } /** @@ -88,9 +88,12 @@ export class EntityStoreClient { * @returns A promise that resolves to the response. */ async push(store: { data: Partial; signature: string }): Promise { - const { data } = await this.dataStoreHttp.setEntities(this.config.clientId, { - data: this.populate(store.data), - signature: store.signature + const { data } = await this.dataStoreHttp.setEntities({ + clientId: this.config.clientId, + setEntityStoreDto: { + data: this.populate(store.data), + signature: store.signature + } }) return data @@ -120,11 +123,16 @@ export class EntityStoreClient { async fetch(): Promise { assert(this.config.clientSecret !== undefined, 'Missing clientSecret') - const { data } = await this.dataStoreHttp.getEntities(this.config.clientId, { - headers: { - [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + const { data } = await this.dataStoreHttp.getEntities( + { + clientId: this.config.clientId + }, + { + headers: { + [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + } } - }) + ) return EntityStore.parse(data.entity) } @@ -137,12 +145,17 @@ export class EntityStoreClient { async sync(): Promise { assert(this.config.clientSecret !== undefined, 'Missing clientSecret') - const { data } = await this.dataStoreHttp.sync(this.config.clientSecret, { - headers: { - [REQUEST_HEADER_CLIENT_ID]: this.config.clientId, - [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + const { data } = await this.dataStoreHttp.sync( + { + xClientSecret: this.config.clientSecret + }, + { + headers: { + [REQUEST_HEADER_CLIENT_ID]: this.config.clientId, + [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + } } - }) + ) return data.latestSync.success } @@ -151,7 +164,7 @@ export class EntityStoreClient { export class PolicyStoreClient { private config: DataStoreConfig - private dataStoreHttp: DataStoreHttp + private dataStoreHttp: ManagedDataStoreApi constructor(config: DataStoreConfig) { this.config = config @@ -164,7 +177,7 @@ export class PolicyStoreClient { addCompressionInterceptor(axiosInstance) - this.dataStoreHttp = ManagedDataStoreApiFactory(httpConfig, config.host, axiosInstance) + this.dataStoreHttp = new ManagedDataStoreApi(httpConfig, config.host, axiosInstance) } /** @@ -185,7 +198,10 @@ export class PolicyStoreClient { * @returns A promise that resolves to the response. */ async push(store: PolicyStore): Promise { - const { data } = await this.dataStoreHttp.setPolicies(this.config.clientId, store) + const { data } = await this.dataStoreHttp.setPolicies({ + clientId: this.config.clientId, + setPolicyStoreDto: store + }) return data } @@ -211,11 +227,16 @@ export class PolicyStoreClient { async fetch(): Promise { assert(this.config.clientSecret !== undefined, 'Missing clientSecret') - const { data } = await this.dataStoreHttp.getPolicies(this.config.clientId, { - headers: { - [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + const { data } = await this.dataStoreHttp.getPolicies( + { + clientId: this.config.clientId + }, + { + headers: { + [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + } } - }) + ) return PolicyStore.parse(data.policy) } @@ -228,12 +249,17 @@ export class PolicyStoreClient { async sync(): Promise { assert(this.config.clientSecret !== undefined, 'Missing clientSecret') - const { data } = await this.dataStoreHttp.sync(this.config.clientSecret, { - headers: { - [REQUEST_HEADER_CLIENT_ID]: this.config.clientId, - [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + const { data } = await this.dataStoreHttp.sync( + { + xClientSecret: this.config.clientSecret + }, + { + headers: { + [REQUEST_HEADER_CLIENT_ID]: this.config.clientId, + [REQUEST_HEADER_CLIENT_SECRET]: this.config.clientSecret + } } - }) + ) return data.latestSync.success } diff --git a/packages/armory-sdk/src/lib/http/client/auth/api.ts b/packages/armory-sdk/src/lib/http/client/auth/api.ts index e034663ce..29d20d399 100644 --- a/packages/armory-sdk/src/lib/http/client/auth/api.ts +++ b/packages/armory-sdk/src/lib/http/client/auth/api.ts @@ -85,6 +85,12 @@ export interface AuthorizationRequestDtoMetadata { * @memberof AuthorizationRequestDtoMetadata */ 'expiresIn'?: number; + /** + * + * @type {AuthorizationRequestDtoMetadataConfirmation} + * @memberof AuthorizationRequestDtoMetadata + */ + 'confirmation'?: AuthorizationRequestDtoMetadataConfirmation; } /** * @type AuthorizationRequestDtoMetadataAudience @@ -93,1597 +99,1649 @@ export interface AuthorizationRequestDtoMetadata { export type AuthorizationRequestDtoMetadataAudience = Array | string; /** - * @type AuthorizationRequestDtoRequest + * Option to bind the access token to a given public key. * @export + * @interface AuthorizationRequestDtoMetadataConfirmation */ -export type AuthorizationRequestDtoRequest = AuthorizationRequestDtoRequestOneOf | AuthorizationRequestDtoRequestOneOf1 | AuthorizationRequestDtoRequestOneOf2 | AuthorizationRequestDtoRequestOneOf3 | AuthorizationRequestDtoRequestOneOf4 | AuthorizationRequestDtoRequestOneOf5; - +export interface AuthorizationRequestDtoMetadataConfirmation { + /** + * + * @type {AuthorizationRequestDtoMetadataConfirmationKey} + * @memberof AuthorizationRequestDtoMetadataConfirmation + */ + 'key': AuthorizationRequestDtoMetadataConfirmationKey; +} /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf + * @interface AuthorizationRequestDtoMetadataConfirmationKey */ -export interface AuthorizationRequestDtoRequestOneOf { +export interface AuthorizationRequestDtoMetadataConfirmationKey { /** * - * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf + * @type {AuthorizationRequestDtoMetadataConfirmationKeyJwk} + * @memberof AuthorizationRequestDtoMetadataConfirmationKey */ - 'action': AuthorizationRequestDtoRequestOneOfActionEnum; + 'jwk': AuthorizationRequestDtoMetadataConfirmationKeyJwk; /** - * + * Specifies the proof method for demonstrating possession of the private key corresponding to the jwk * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf + * @memberof AuthorizationRequestDtoMetadataConfirmationKey */ - 'nonce': string; + 'proof'?: AuthorizationRequestDtoMetadataConfirmationKeyProofEnum; /** - * + * The actual JSON Web Signature value that proves the client possesses the private key. * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf - */ - 'resourceId': string; - /** - * - * @type {AuthorizationRequestDtoRequestOneOfTransactionRequest} - * @memberof AuthorizationRequestDtoRequestOneOf + * @memberof AuthorizationRequestDtoMetadataConfirmationKey */ - 'transactionRequest': AuthorizationRequestDtoRequestOneOfTransactionRequest; + 'jws'?: string; } -export const AuthorizationRequestDtoRequestOneOfActionEnum = { - SignTransaction: 'signTransaction' +export const AuthorizationRequestDtoMetadataConfirmationKeyProofEnum = { + Jws: 'jws' } as const; -export type AuthorizationRequestDtoRequestOneOfActionEnum = typeof AuthorizationRequestDtoRequestOneOfActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOfActionEnum]; +export type AuthorizationRequestDtoMetadataConfirmationKeyProofEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyProofEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyProofEnum]; + +/** + * @type AuthorizationRequestDtoMetadataConfirmationKeyJwk + * JSON Web Key that will be used to bind the access token. This ensures only the holder of the corresponding private key can use the token. + * @export + */ +export type AuthorizationRequestDtoMetadataConfirmationKeyJwk = AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4; /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf1 + * @interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ -export interface AuthorizationRequestDtoRequestOneOf1 { +export interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf { /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf1 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'action': AuthorizationRequestDtoRequestOneOf1ActionEnum; + 'kty': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfKtyEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf1 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'nonce': string; + 'alg': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfAlgEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf1 - */ - 'resourceId': string; - /** - * - * @type {AuthorizationRequestDtoRequestOneOf1Message} - * @memberof AuthorizationRequestDtoRequestOneOf1 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'message': AuthorizationRequestDtoRequestOneOf1Message; -} - -export const AuthorizationRequestDtoRequestOneOf1ActionEnum = { - SignMessage: 'signMessage' -} as const; - -export type AuthorizationRequestDtoRequestOneOf1ActionEnum = typeof AuthorizationRequestDtoRequestOneOf1ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf1ActionEnum]; - -/** - * @type AuthorizationRequestDtoRequestOneOf1Message - * @export - */ -export type AuthorizationRequestDtoRequestOneOf1Message = AuthorizationRequestDtoRequestOneOf1MessageOneOf | string; - -/** - * - * @export - * @interface AuthorizationRequestDtoRequestOneOf1MessageOneOf - */ -export interface AuthorizationRequestDtoRequestOneOf1MessageOneOf { + 'use'?: AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfUseEnum; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf1MessageOneOf + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'raw': any; -} -/** - * - * @export - * @interface AuthorizationRequestDtoRequestOneOf2 - */ -export interface AuthorizationRequestDtoRequestOneOf2 { + 'kid': string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'action': AuthorizationRequestDtoRequestOneOf2ActionEnum; + 'addr'?: string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'nonce': string; + 'crv': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfCrvEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'resourceId': string; + 'x': string; /** * - * @type {AuthorizationRequestDtoRequestOneOf2TypedData} - * @memberof AuthorizationRequestDtoRequestOneOf2 + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf */ - 'typedData': AuthorizationRequestDtoRequestOneOf2TypedData; + 'y': string; } -export const AuthorizationRequestDtoRequestOneOf2ActionEnum = { - SignTypedData: 'signTypedData' +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfKtyEnum = { + Ec: 'EC' } as const; -export type AuthorizationRequestDtoRequestOneOf2ActionEnum = typeof AuthorizationRequestDtoRequestOneOf2ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf2ActionEnum]; +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfKtyEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfKtyEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfKtyEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfAlgEnum = { + Es256K: 'ES256K' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfAlgEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfAlgEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfAlgEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfUseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfUseEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfUseEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfUseEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfCrvEnum = { + Secp256k1: 'secp256k1' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfCrvEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfCrvEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOfCrvEnum]; /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf2TypedData + * @interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ -export interface AuthorizationRequestDtoRequestOneOf2TypedData { - /** - * - * @type {AuthorizationRequestDtoRequestOneOf2TypedDataDomain} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedData - */ - 'domain': AuthorizationRequestDtoRequestOneOf2TypedDataDomain; +export interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 { /** * - * @type {{ [key: string]: Array; }} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedData + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'types': { [key: string]: Array; }; + 'kty': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1KtyEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedData + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'primaryType': string; + 'alg': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1AlgEnum; /** * - * @type {{ [key: string]: any; }} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedData + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'message': { [key: string]: any; }; -} -/** - * - * @export - * @interface AuthorizationRequestDtoRequestOneOf2TypedDataDomain - */ -export interface AuthorizationRequestDtoRequestOneOf2TypedDataDomain { + 'use'?: AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1UseEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'name'?: string; + 'kid': string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'version'?: string; + 'addr'?: string; /** * - * @type {number} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'chainId'?: number; + 'crv': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1CrvEnum; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'verifyingContract'?: any; + 'x': string; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 */ - 'salt'?: any; + 'y': string; } + +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1KtyEnum = { + Ec: 'EC' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1KtyEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1KtyEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1KtyEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1AlgEnum = { + Es256: 'ES256' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1AlgEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1AlgEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1AlgEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1UseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1UseEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1UseEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1UseEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1CrvEnum = { + P256: 'P-256' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1CrvEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1CrvEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1CrvEnum]; + /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner + * @interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ -export interface AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner { +export interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 { /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ - 'name': string; + 'kty': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2KtyEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ - 'type': string; -} -/** - * - * @export - * @interface AuthorizationRequestDtoRequestOneOf3 - */ -export interface AuthorizationRequestDtoRequestOneOf3 { + 'alg': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2AlgEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf3 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ - 'action': AuthorizationRequestDtoRequestOneOf3ActionEnum; + 'use'?: AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2UseEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf3 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ - 'nonce': string; + 'kid': string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf3 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ - 'resourceId': string; + 'addr'?: string; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf3 + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 */ - 'rawMessage': any; + 'n': string; + /** + * + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 + */ + 'e': string; } -export const AuthorizationRequestDtoRequestOneOf3ActionEnum = { - SignRaw: 'signRaw' +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2KtyEnum = { + Rsa: 'RSA' } as const; -export type AuthorizationRequestDtoRequestOneOf3ActionEnum = typeof AuthorizationRequestDtoRequestOneOf3ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf3ActionEnum]; +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2KtyEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2KtyEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2KtyEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2AlgEnum = { + Rs256: 'RS256' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2AlgEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2AlgEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2AlgEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2UseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2UseEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2UseEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2UseEnum]; /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf4 + * @interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 */ -export interface AuthorizationRequestDtoRequestOneOf4 { +export interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 { /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 */ - 'action': AuthorizationRequestDtoRequestOneOf4ActionEnum; + 'kty': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3KtyEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 */ - 'nonce': string; + 'crv': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3CrvEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4 + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 */ - 'resourceId': string; + 'alg': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3AlgEnum; /** * - * @type {AuthorizationRequestDtoRequestOneOf4UserOperation} - * @memberof AuthorizationRequestDtoRequestOneOf4 + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 */ - 'userOperation': AuthorizationRequestDtoRequestOneOf4UserOperation; + 'use'?: AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3UseEnum; + /** + * + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 + */ + 'kid': string; + /** + * + * @type {any} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 + */ + 'addr': any; } -export const AuthorizationRequestDtoRequestOneOf4ActionEnum = { - SignUserOperation: 'signUserOperation' +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3KtyEnum = { + Ec: 'EC' } as const; -export type AuthorizationRequestDtoRequestOneOf4ActionEnum = typeof AuthorizationRequestDtoRequestOneOf4ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf4ActionEnum]; +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3KtyEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3KtyEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3KtyEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3CrvEnum = { + Secp256k1: 'secp256k1' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3CrvEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3CrvEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3CrvEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3AlgEnum = { + Es256K: 'ES256K' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3AlgEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3AlgEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3AlgEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3UseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3UseEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3UseEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3UseEnum]; /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf4UserOperation + * @interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ -export interface AuthorizationRequestDtoRequestOneOf4UserOperation { +export interface AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 { /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @type {string} + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'sender': any; + 'kty': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4KtyEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'nonce': string; - /** - * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation - */ - 'initCode': any; - /** - * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation - */ - 'callData': any; + 'alg': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4AlgEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'callGasLimit': string; + 'use'?: AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4UseEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'verificationGasLimit': string; + 'kid': string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'preVerificationGas': string; + 'addr'?: string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'maxFeePerGas': string; + 'crv': AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4CrvEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation - */ - 'maxPriorityFeePerGas': string; - /** - * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @memberof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4 */ - 'paymasterAndData': any; + 'x': string; +} + +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4KtyEnum = { + Okp: 'OKP' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4KtyEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4KtyEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4KtyEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4AlgEnum = { + Eddsa: 'EDDSA' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4AlgEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4AlgEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4AlgEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4UseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4UseEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4UseEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4UseEnum]; +export const AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4CrvEnum = { + Ed25519: 'Ed25519' +} as const; + +export type AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4CrvEnum = typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4CrvEnum[keyof typeof AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4CrvEnum]; + +/** + * @type AuthorizationRequestDtoRequest + * @export + */ +export type AuthorizationRequestDtoRequest = AuthorizationRequestDtoRequestOneOf | AuthorizationRequestDtoRequestOneOf1 | AuthorizationRequestDtoRequestOneOf2 | AuthorizationRequestDtoRequestOneOf3 | AuthorizationRequestDtoRequestOneOf4 | AuthorizationRequestDtoRequestOneOf5; + +/** + * + * @export + * @interface AuthorizationRequestDtoRequestOneOf + */ +export interface AuthorizationRequestDtoRequestOneOf { /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf */ - 'entryPoint': any; + 'action': AuthorizationRequestDtoRequestOneOfActionEnum; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf */ - 'signature': any; + 'nonce': string; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf */ - 'factoryAddress': any; + 'resourceId': string; /** * - * @type {number} - * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + * @type {AuthorizationRequestDtoRequestOneOfTransactionRequest} + * @memberof AuthorizationRequestDtoRequestOneOf */ - 'chainId': number; + 'transactionRequest': AuthorizationRequestDtoRequestOneOfTransactionRequest; } + +export const AuthorizationRequestDtoRequestOneOfActionEnum = { + SignTransaction: 'signTransaction' +} as const; + +export type AuthorizationRequestDtoRequestOneOfActionEnum = typeof AuthorizationRequestDtoRequestOneOfActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOfActionEnum]; + /** * * @export - * @interface AuthorizationRequestDtoRequestOneOf5 + * @interface AuthorizationRequestDtoRequestOneOf1 */ -export interface AuthorizationRequestDtoRequestOneOf5 { +export interface AuthorizationRequestDtoRequestOneOf1 { /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf5 + * @memberof AuthorizationRequestDtoRequestOneOf1 */ - 'action': AuthorizationRequestDtoRequestOneOf5ActionEnum; + 'action': AuthorizationRequestDtoRequestOneOf1ActionEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf5 + * @memberof AuthorizationRequestDtoRequestOneOf1 */ 'nonce': string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOf5 + * @memberof AuthorizationRequestDtoRequestOneOf1 */ 'resourceId': string; /** * - * @type {Array} - * @memberof AuthorizationRequestDtoRequestOneOf5 + * @type {AuthorizationRequestDtoRequestOneOf1Message} + * @memberof AuthorizationRequestDtoRequestOneOf1 */ - 'permissions': Array; + 'message': AuthorizationRequestDtoRequestOneOf1Message; } -export const AuthorizationRequestDtoRequestOneOf5ActionEnum = { - GrantPermission: 'grantPermission' +export const AuthorizationRequestDtoRequestOneOf1ActionEnum = { + SignMessage: 'signMessage' } as const; -export type AuthorizationRequestDtoRequestOneOf5ActionEnum = typeof AuthorizationRequestDtoRequestOneOf5ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf5ActionEnum]; +export type AuthorizationRequestDtoRequestOneOf1ActionEnum = typeof AuthorizationRequestDtoRequestOneOf1ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf1ActionEnum]; /** - * @type AuthorizationRequestDtoRequestOneOfTransactionRequest + * @type AuthorizationRequestDtoRequestOneOf1Message * @export */ -export type AuthorizationRequestDtoRequestOneOfTransactionRequest = AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf | AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1; +export type AuthorizationRequestDtoRequestOneOf1Message = AuthorizationRequestDtoRequestOneOf1MessageOneOf | string; /** * * @export - * @interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + * @interface AuthorizationRequestDtoRequestOneOf1MessageOneOf */ -export interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf { - /** - * - * @type {number} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf - */ - 'chainId': number; - /** - * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf - */ - 'from': any; - /** - * - * @type {number} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf - */ - 'nonce'?: number; - /** - * - * @type {Array} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf - */ - 'accessList'?: Array; +export interface AuthorizationRequestDtoRequestOneOf1MessageOneOf { /** * * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf - */ - 'data'?: any; - /** - * - * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof AuthorizationRequestDtoRequestOneOf1MessageOneOf */ - 'gas'?: string; + 'raw': any; +} +/** + * + * @export + * @interface AuthorizationRequestDtoRequestOneOf2 + */ +export interface AuthorizationRequestDtoRequestOneOf2 { /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof AuthorizationRequestDtoRequestOneOf2 */ - 'maxFeePerGas'?: string; + 'action': AuthorizationRequestDtoRequestOneOf2ActionEnum; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf - */ - 'maxPriorityFeePerGas'?: string; - /** - * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof AuthorizationRequestDtoRequestOneOf2 */ - 'to'?: any | null; + 'nonce': string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof AuthorizationRequestDtoRequestOneOf2 */ - 'type'?: AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum; + 'resourceId': string; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + * @type {AuthorizationRequestDtoRequestOneOf2TypedData} + * @memberof AuthorizationRequestDtoRequestOneOf2 */ - 'value'?: any; + 'typedData': AuthorizationRequestDtoRequestOneOf2TypedData; } -export const AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = { - _2: '2' +export const AuthorizationRequestDtoRequestOneOf2ActionEnum = { + SignTypedData: 'signTypedData' } as const; -export type AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum[keyof typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum]; +export type AuthorizationRequestDtoRequestOneOf2ActionEnum = typeof AuthorizationRequestDtoRequestOneOf2ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf2ActionEnum]; /** * * @export - * @interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @interface AuthorizationRequestDtoRequestOneOf2TypedData */ -export interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 { +export interface AuthorizationRequestDtoRequestOneOf2TypedData { /** * - * @type {number} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {AuthorizationRequestDtoRequestOneOf2TypedDataDomain} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedData */ - 'chainId': number; + 'domain': AuthorizationRequestDtoRequestOneOf2TypedDataDomain; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {{ [key: string]: Array; }} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedData */ - 'from': any; + 'types': { [key: string]: Array; }; /** * - * @type {number} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedData */ - 'nonce'?: number; + 'primaryType': string; /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {{ [key: string]: any; }} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedData */ - 'data'?: any; + 'message': { [key: string]: any; }; +} +/** + * + * @export + * @interface AuthorizationRequestDtoRequestOneOf2TypedDataDomain + */ +export interface AuthorizationRequestDtoRequestOneOf2TypedDataDomain { /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain */ - 'gas'?: string; + 'name'?: string; /** * * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain */ - 'gasPrice'?: string; + 'version'?: string; /** * - * @type {string} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {number} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain */ - 'type'?: AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum; + 'chainId'?: number; /** * * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain */ - 'to'?: any | null; + 'verifyingContract'?: any; /** * * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataDomain */ - 'value'?: any; + 'salt'?: any; } - -export const AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = { - _0: '0' -} as const; - -export type AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum[keyof typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum]; - /** * * @export - * @interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + * @interface AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner */ -export interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner { +export interface AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner { /** * - * @type {any} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner */ - 'address': any; + 'name': string; /** * - * @type {Array} - * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf2TypedDataTypesValueInner */ - 'storageKeys': Array; + 'type': string; } /** * * @export - * @interface AuthorizationResponseDto + * @interface AuthorizationRequestDtoRequestOneOf3 */ -export interface AuthorizationResponseDto { +export interface AuthorizationRequestDtoRequestOneOf3 { /** * - * @type {Array} - * @memberof AuthorizationResponseDto + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf3 */ - 'approvals'?: Array; + 'action': AuthorizationRequestDtoRequestOneOf3ActionEnum; /** * * @type {string} - * @memberof AuthorizationResponseDto + * @memberof AuthorizationRequestDtoRequestOneOf3 */ - 'authentication': string; + 'nonce': string; /** * * @type {string} - * @memberof AuthorizationResponseDto + * @memberof AuthorizationRequestDtoRequestOneOf3 */ - 'clientId': string; + 'resourceId': string; /** * * @type {any} - * @memberof AuthorizationResponseDto - */ - 'createdAt': any; - /** - * - * @type {Array} - * @memberof AuthorizationResponseDto - */ - 'errors'?: Array; - /** - * - * @type {Array} - * @memberof AuthorizationResponseDto + * @memberof AuthorizationRequestDtoRequestOneOf3 */ - 'evaluations': Array; + 'rawMessage': any; +} + +export const AuthorizationRequestDtoRequestOneOf3ActionEnum = { + SignRaw: 'signRaw' +} as const; + +export type AuthorizationRequestDtoRequestOneOf3ActionEnum = typeof AuthorizationRequestDtoRequestOneOf3ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf3ActionEnum]; + +/** + * + * @export + * @interface AuthorizationRequestDtoRequestOneOf4 + */ +export interface AuthorizationRequestDtoRequestOneOf4 { /** * * @type {string} - * @memberof AuthorizationResponseDto + * @memberof AuthorizationRequestDtoRequestOneOf4 */ - 'id': string; + 'action': AuthorizationRequestDtoRequestOneOf4ActionEnum; /** * * @type {string} - * @memberof AuthorizationResponseDto - */ - 'idempotencyKey'?: string | null; - /** - * - * @type {AuthorizationRequestDtoMetadata} - * @memberof AuthorizationResponseDto - */ - 'metadata'?: AuthorizationRequestDtoMetadata; - /** - * - * @type {AuthorizationRequestDtoRequest} - * @memberof AuthorizationResponseDto + * @memberof AuthorizationRequestDtoRequestOneOf4 */ - 'request': AuthorizationRequestDtoRequest; + 'nonce': string; /** * * @type {string} - * @memberof AuthorizationResponseDto + * @memberof AuthorizationRequestDtoRequestOneOf4 */ - 'status': AuthorizationResponseDtoStatusEnum; + 'resourceId': string; /** * - * @type {any} - * @memberof AuthorizationResponseDto + * @type {AuthorizationRequestDtoRequestOneOf4UserOperation} + * @memberof AuthorizationRequestDtoRequestOneOf4 */ - 'updatedAt': any; + 'userOperation': AuthorizationRequestDtoRequestOneOf4UserOperation; } -export const AuthorizationResponseDtoStatusEnum = { - Created: 'CREATED', - Canceled: 'CANCELED', - Failed: 'FAILED', - Processing: 'PROCESSING', - Approving: 'APPROVING', - Permitted: 'PERMITTED', - Forbidden: 'FORBIDDEN' +export const AuthorizationRequestDtoRequestOneOf4ActionEnum = { + SignUserOperation: 'signUserOperation' } as const; -export type AuthorizationResponseDtoStatusEnum = typeof AuthorizationResponseDtoStatusEnum[keyof typeof AuthorizationResponseDtoStatusEnum]; +export type AuthorizationRequestDtoRequestOneOf4ActionEnum = typeof AuthorizationRequestDtoRequestOneOf4ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf4ActionEnum]; /** * * @export - * @interface AuthorizationResponseDtoErrorsInner + * @interface AuthorizationRequestDtoRequestOneOf4UserOperation */ -export interface AuthorizationResponseDtoErrorsInner { +export interface AuthorizationRequestDtoRequestOneOf4UserOperation { /** * * @type {any} - * @memberof AuthorizationResponseDtoErrorsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'context'?: any; + 'sender': any; /** * * @type {string} - * @memberof AuthorizationResponseDtoErrorsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'id': string; + 'nonce': string; /** * - * @type {string} - * @memberof AuthorizationResponseDtoErrorsInner + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'message': string; + 'initCode': any; + /** + * + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation + */ + 'callData': any; /** * * @type {string} - * @memberof AuthorizationResponseDtoErrorsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'name': string; -} -/** - * - * @export - * @interface AuthorizationResponseDtoEvaluationsInner - */ -export interface AuthorizationResponseDtoEvaluationsInner { + 'callGasLimit': string; /** * * @type {string} - * @memberof AuthorizationResponseDtoEvaluationsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'id': string; + 'verificationGasLimit': string; /** * * @type {string} - * @memberof AuthorizationResponseDtoEvaluationsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'decision': string; + 'preVerificationGas': string; /** * * @type {string} - * @memberof AuthorizationResponseDtoEvaluationsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'signature': string | null; + 'maxFeePerGas': string; /** * - * @type {any} - * @memberof AuthorizationResponseDtoEvaluationsInner + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'transactionRequestIntent'?: any; + 'maxPriorityFeePerGas': string; /** * - * @type {AuthorizationResponseDtoEvaluationsInnerApprovalRequirements} - * @memberof AuthorizationResponseDtoEvaluationsInner + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'approvalRequirements'?: AuthorizationResponseDtoEvaluationsInnerApprovalRequirements; + 'paymasterAndData': any; /** * * @type {any} - * @memberof AuthorizationResponseDtoEvaluationsInner + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'createdAt': any; -} -/** - * - * @export - * @interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirements - */ -export interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirements { + 'entryPoint': any; /** * - * @type {Array} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirements + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'required'?: Array; + 'signature': any; /** * - * @type {Array} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirements + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'missing'?: Array; + 'factoryAddress': any; /** * - * @type {Array} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirements + * @type {number} + * @memberof AuthorizationRequestDtoRequestOneOf4UserOperation */ - 'satisfied'?: Array; + 'chainId': number; } /** * * @export - * @interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner + * @interface AuthorizationRequestDtoRequestOneOf5 */ -export interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner { +export interface AuthorizationRequestDtoRequestOneOf5 { /** * - * @type {number} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf5 */ - 'approvalCount': number; + 'action': AuthorizationRequestDtoRequestOneOf5ActionEnum; /** - * The number of requried approvals + * * @type {string} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner + * @memberof AuthorizationRequestDtoRequestOneOf5 */ - 'approvalEntityType': AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum; + 'nonce': string; /** - * List of entities IDs that must satisfy the requirements - * @type {Array} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner + * + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOf5 */ - 'entityIds': Array; + 'resourceId': string; /** * - * @type {boolean} - * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner + * @type {Array} + * @memberof AuthorizationRequestDtoRequestOneOf5 */ - 'countPrincipal': boolean; + 'permissions': Array; } -export const AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum = { - User: 'Narval::User', - UserRole: 'Narval::UserRole', - UserGroup: 'Narval::UserGroup' +export const AuthorizationRequestDtoRequestOneOf5ActionEnum = { + GrantPermission: 'grantPermission' } as const; -export type AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum = typeof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum[keyof typeof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum]; +export type AuthorizationRequestDtoRequestOneOf5ActionEnum = typeof AuthorizationRequestDtoRequestOneOf5ActionEnum[keyof typeof AuthorizationRequestDtoRequestOneOf5ActionEnum]; + +/** + * @type AuthorizationRequestDtoRequestOneOfTransactionRequest + * @export + */ +export type AuthorizationRequestDtoRequestOneOfTransactionRequest = AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf | AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1; /** * * @export - * @interface CreateClientRequestDto + * @interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ -export interface CreateClientRequestDto { +export interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf { /** * - * @type {string} - * @memberof CreateClientRequestDto + * @type {number} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'id'?: string; + 'chainId': number; /** * - * @type {string} - * @memberof CreateClientRequestDto + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'name': string; + 'from': any; /** * - * @type {boolean} - * @memberof CreateClientRequestDto - */ - 'useManagedDataStore'?: boolean; + * @type {number} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'nonce'?: number; + /** + * + * @type {Array} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'accessList'?: Array; + /** + * + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'data'?: any; /** * * @type {string} - * @memberof CreateClientRequestDto + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'clientSecret'?: string; + 'gas'?: string; /** * - * @type {CreateClientRequestDtoDataStore} - * @memberof CreateClientRequestDto + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'dataStore': CreateClientRequestDtoDataStore; + 'maxFeePerGas'?: string; /** * - * @type {Array} - * @memberof CreateClientRequestDto + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'policyEngineNodes'?: Array; -} -/** - * - * @export - * @interface CreateClientRequestDtoDataStore - */ -export interface CreateClientRequestDtoDataStore { + 'maxPriorityFeePerGas'?: string; /** * - * @type {CreateClientRequestDtoDataStoreEntity} - * @memberof CreateClientRequestDtoDataStore + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'entity': CreateClientRequestDtoDataStoreEntity; + 'to'?: any | null; /** * - * @type {CreateClientRequestDtoDataStoreEntity} - * @memberof CreateClientRequestDtoDataStore + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'policy': CreateClientRequestDtoDataStoreEntity; + 'type'?: AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum; /** - * Whether to include the engine key in the entity and policy keys - * @type {boolean} - * @memberof CreateClientRequestDtoDataStore + * + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf */ - 'allowSelfSignedData'?: boolean; + 'value'?: any; } + +export const AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = { + _2: '2' +} as const; + +export type AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum[keyof typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum]; + /** * * @export - * @interface CreateClientRequestDtoDataStoreEntity + * @interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 */ -export interface CreateClientRequestDtoDataStoreEntity { +export interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 { /** * - * @type {CreateClientRequestDtoDataStoreEntityData} - * @memberof CreateClientRequestDtoDataStoreEntity + * @type {number} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 */ - 'data': CreateClientRequestDtoDataStoreEntityData; + 'chainId': number; /** * - * @type {CreateClientRequestDtoDataStoreEntityData} - * @memberof CreateClientRequestDtoDataStoreEntity + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 */ - 'signature': CreateClientRequestDtoDataStoreEntityData; + 'from': any; /** * - * @type {Array} - * @memberof CreateClientRequestDtoDataStoreEntity + * @type {number} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 */ - 'keys': Array; -} -/** - * @type CreateClientRequestDtoDataStoreEntityData - * @export - */ -export type CreateClientRequestDtoDataStoreEntityData = CreateClientRequestDtoDataStoreEntityDataOneOf | CreateClientRequestDtoDataStoreEntityDataOneOf1; - -/** - * - * @export - * @interface CreateClientRequestDtoDataStoreEntityDataOneOf - */ -export interface CreateClientRequestDtoDataStoreEntityDataOneOf { + 'nonce'?: number; + /** + * + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'data'?: any; /** * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 */ - 'type': CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum; + 'gas'?: string; /** * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 */ - 'url': string; + 'gasPrice'?: string; + /** + * + * @type {string} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'type'?: AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum; + /** + * + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'to'?: any | null; + /** + * + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'value'?: any; } -export const CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum = { - File: 'FILE' +export const AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = { + _0: '0' } as const; -export type CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum = typeof CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum[keyof typeof CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum]; +export type AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum[keyof typeof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum]; /** * * @export - * @interface CreateClientRequestDtoDataStoreEntityDataOneOf1 + * @interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner */ -export interface CreateClientRequestDtoDataStoreEntityDataOneOf1 { - /** - * - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf1 - */ - 'type': CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum; +export interface AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner { /** * - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf1 + * @type {any} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner */ - 'url': string; + 'address': any; /** * - * @type {{ [key: string]: string; }} - * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf1 + * @type {Array} + * @memberof AuthorizationRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner */ - 'headers'?: { [key: string]: string; }; + 'storageKeys': Array; } - -export const CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum = { - Http: 'HTTP', - Https: 'HTTPS' -} as const; - -export type CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum = typeof CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum[keyof typeof CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum]; - /** * * @export - * @interface CreateClientRequestDtoDataStoreEntityKeysInner + * @interface AuthorizationResponseDto */ -export interface CreateClientRequestDtoDataStoreEntityKeysInner { +export interface AuthorizationResponseDto { /** - * Key Type (e.g. RSA or EC - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * + * @type {Array} + * @memberof AuthorizationResponseDto */ - 'kty'?: CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum; + 'approvals'?: Array; /** - * Curve name + * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * @memberof AuthorizationResponseDto */ - 'crv'?: CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum; + 'authentication': string; /** - * Algorithm + * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * @memberof AuthorizationResponseDto */ - 'alg'?: CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum; + 'clientId': string; /** - * Public Key Use - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * + * @type {any} + * @memberof AuthorizationResponseDto */ - 'use'?: CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum; + 'createdAt': any; /** - * Unique key ID - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * + * @type {Array} + * @memberof AuthorizationResponseDto */ - 'kid'?: string; + 'errors'?: Array; /** - * (RSA) Key modulus - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * + * @type {Array} + * @memberof AuthorizationResponseDto */ - 'n'?: string; + 'evaluations': Array; /** - * (RSA) Key exponent + * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * @memberof AuthorizationResponseDto */ - 'e'?: string; + 'id': string; /** - * (EC) X Coordinate + * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * @memberof AuthorizationResponseDto */ - 'x'?: string; + 'idempotencyKey'?: string | null; /** - * (EC) Y Coordinate - * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * + * @type {AuthorizationRequestDtoMetadata} + * @memberof AuthorizationResponseDto */ - 'y'?: string; + 'metadata'?: AuthorizationRequestDtoMetadata; /** - * (EC) Private Key + * + * @type {AuthorizationRequestDtoRequest} + * @memberof AuthorizationResponseDto + */ + 'request': AuthorizationRequestDtoRequest; + /** + * * @type {string} - * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + * @memberof AuthorizationResponseDto */ - 'd'?: string; + 'status': AuthorizationResponseDtoStatusEnum; + /** + * + * @type {any} + * @memberof AuthorizationResponseDto + */ + 'updatedAt': any; } -export const CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum = { - Ec: 'EC', - Rsa: 'RSA', - Okp: 'OKP' -} as const; - -export type CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum]; -export const CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum = { - Secp256k1: 'secp256k1', - P256: 'P-256', - Ed25519: 'Ed25519' -} as const; - -export type CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum]; -export const CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum = { - Es256K: 'ES256K', - Es256: 'ES256', - Rs256: 'RS256', - Eddsa: 'EDDSA' -} as const; - -export type CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum]; -export const CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum = { - Sig: 'sig', - Enc: 'enc' +export const AuthorizationResponseDtoStatusEnum = { + Created: 'CREATED', + Canceled: 'CANCELED', + Failed: 'FAILED', + Processing: 'PROCESSING', + Approving: 'APPROVING', + Permitted: 'PERMITTED', + Forbidden: 'FORBIDDEN' } as const; -export type CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum]; +export type AuthorizationResponseDtoStatusEnum = typeof AuthorizationResponseDtoStatusEnum[keyof typeof AuthorizationResponseDtoStatusEnum]; /** * * @export - * @interface CreateClientResponseDto + * @interface AuthorizationResponseDtoErrorsInner */ -export interface CreateClientResponseDto { +export interface AuthorizationResponseDtoErrorsInner { + /** + * + * @type {any} + * @memberof AuthorizationResponseDtoErrorsInner + */ + 'context'?: any; /** * * @type {string} - * @memberof CreateClientResponseDto + * @memberof AuthorizationResponseDtoErrorsInner */ 'id': string; /** * * @type {string} - * @memberof CreateClientResponseDto + * @memberof AuthorizationResponseDtoErrorsInner + */ + 'message': string; + /** + * + * @type {string} + * @memberof AuthorizationResponseDtoErrorsInner */ 'name': string; +} +/** + * + * @export + * @interface AuthorizationResponseDtoEvaluationsInner + */ +export interface AuthorizationResponseDtoEvaluationsInner { /** - * plaintext secret for authenticating to armory + * * @type {string} - * @memberof CreateClientResponseDto + * @memberof AuthorizationResponseDtoEvaluationsInner */ - 'clientSecret': string; + 'id': string; /** - * plaintext secret for authenticating to data store + * * @type {string} - * @memberof CreateClientResponseDto + * @memberof AuthorizationResponseDtoEvaluationsInner */ - 'dataSecret': string | null; + 'decision': string; /** * - * @type {any} - * @memberof CreateClientResponseDto + * @type {string} + * @memberof AuthorizationResponseDtoEvaluationsInner */ - 'createdAt': any; + 'signature': string | null; /** * * @type {any} - * @memberof CreateClientResponseDto + * @memberof AuthorizationResponseDtoEvaluationsInner */ - 'updatedAt': any; + 'transactionRequestIntent'?: any; /** * - * @type {CreateClientResponseDtoDataStore} - * @memberof CreateClientResponseDto + * @type {AuthorizationResponseDtoEvaluationsInnerApprovalRequirements} + * @memberof AuthorizationResponseDtoEvaluationsInner */ - 'dataStore': CreateClientResponseDtoDataStore; + 'approvalRequirements'?: AuthorizationResponseDtoEvaluationsInnerApprovalRequirements; /** * - * @type {CreateClientResponseDtoPolicyEngine} - * @memberof CreateClientResponseDto + * @type {any} + * @memberof AuthorizationResponseDtoEvaluationsInner */ - 'policyEngine': CreateClientResponseDtoPolicyEngine; + 'createdAt': any; } /** * * @export - * @interface CreateClientResponseDtoDataStore + * @interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirements */ -export interface CreateClientResponseDtoDataStore { - /** - * - * @type {Array} - * @memberof CreateClientResponseDtoDataStore - */ - 'entityPublicKeys': Array; - /** - * - * @type {Array} - * @memberof CreateClientResponseDtoDataStore - */ - 'policyPublicKeys': Array; +export interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirements { /** * - * @type {string} - * @memberof CreateClientResponseDtoDataStore + * @type {Array} + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirements */ - 'entityDataUrl'?: string; + 'required'?: Array; /** - * - * @type {string} - * @memberof CreateClientResponseDtoDataStore - */ - 'policyDataUrl'?: string; -} -/** - * - * @export - * @interface CreateClientResponseDtoPolicyEngine - */ -export interface CreateClientResponseDtoPolicyEngine { + * + * @type {Array} + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirements + */ + 'missing'?: Array; /** * - * @type {Array} - * @memberof CreateClientResponseDtoPolicyEngine + * @type {Array} + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirements */ - 'nodes': Array; + 'satisfied'?: Array; } /** * * @export - * @interface CreateClientResponseDtoPolicyEngineNodesInner + * @interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner */ -export interface CreateClientResponseDtoPolicyEngineNodesInner { - /** - * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInner - */ - 'id': string; +export interface AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner { /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInner + * @type {number} + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner */ - 'clientId': string; + 'approvalCount': number; /** - * + * The number of requried approvals * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInner + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner */ - 'clientSecret'?: string; + 'approvalEntityType': AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum; /** - * - * @type {CreateClientResponseDtoPolicyEngineNodesInnerPublicKey} - * @memberof CreateClientResponseDtoPolicyEngineNodesInner + * List of entities IDs that must satisfy the requirements + * @type {Array} + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner */ - 'publicKey': CreateClientResponseDtoPolicyEngineNodesInnerPublicKey; + 'entityIds': Array; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInner + * @type {boolean} + * @memberof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInner */ - 'url': string; + 'countPrincipal': boolean; } -/** - * @type CreateClientResponseDtoPolicyEngineNodesInnerPublicKey - * @export - */ -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKey = CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf | CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 | CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 | CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 | CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4; + +export const AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum = { + User: 'Narval::User', + UserRole: 'Narval::UserRole', + UserGroup: 'Narval::UserGroup' +} as const; + +export type AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum = typeof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum[keyof typeof AuthorizationResponseDtoEvaluationsInnerApprovalRequirementsRequiredInnerApprovalEntityTypeEnum]; /** * * @export - * @interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @interface CreateClientRequestDto */ -export interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf { +export interface CreateClientRequestDto { /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @memberof CreateClientRequestDto */ - 'kty': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfKtyEnum; + 'id'?: string; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @memberof CreateClientRequestDto */ - 'alg': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfAlgEnum; + 'name': string; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @type {boolean} + * @memberof CreateClientRequestDto */ - 'use'?: CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfUseEnum; + 'useManagedDataStore'?: boolean; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @memberof CreateClientRequestDto */ - 'kid': string; + 'clientSecret'?: string; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @type {CreateClientRequestDtoDataStore} + * @memberof CreateClientRequestDto */ - 'addr'?: string; + 'dataStore': CreateClientRequestDtoDataStore; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @type {Array} + * @memberof CreateClientRequestDto */ - 'crv': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfCrvEnum; + 'policyEngineNodes'?: Array; +} +/** + * + * @export + * @interface CreateClientRequestDtoDataStore + */ +export interface CreateClientRequestDtoDataStore { /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @type {CreateClientRequestDtoDataStoreEntity} + * @memberof CreateClientRequestDtoDataStore */ - 'x': string; + 'entity': CreateClientRequestDtoDataStoreEntity; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf + * @type {CreateClientRequestDtoDataStoreEntity} + * @memberof CreateClientRequestDtoDataStore */ - 'y': string; + 'policy': CreateClientRequestDtoDataStoreEntity; + /** + * Whether to include the engine key in the entity and policy keys + * @type {boolean} + * @memberof CreateClientRequestDtoDataStore + */ + 'allowSelfSignedData'?: boolean; } - -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfKtyEnum = { - Ec: 'EC' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfKtyEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfKtyEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfKtyEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfAlgEnum = { - Es256K: 'ES256K' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfAlgEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfAlgEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfAlgEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfUseEnum = { - Sig: 'sig', - Enc: 'enc' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfUseEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfUseEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfUseEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfCrvEnum = { - Secp256k1: 'secp256k1' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfCrvEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfCrvEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOfCrvEnum]; - /** * * @export - * @interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @interface CreateClientRequestDtoDataStoreEntity */ -export interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 { +export interface CreateClientRequestDtoDataStoreEntity { /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @type {CreateClientRequestDtoDataStoreEntityData} + * @memberof CreateClientRequestDtoDataStoreEntity */ - 'kty': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1KtyEnum; + 'data': CreateClientRequestDtoDataStoreEntityData; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @type {CreateClientRequestDtoDataStoreEntityData} + * @memberof CreateClientRequestDtoDataStoreEntity */ - 'alg': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1AlgEnum; + 'signature': CreateClientRequestDtoDataStoreEntityData; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @type {Array} + * @memberof CreateClientRequestDtoDataStoreEntity */ - 'use'?: CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1UseEnum; + 'keys': Array; +} +/** + * @type CreateClientRequestDtoDataStoreEntityData + * @export + */ +export type CreateClientRequestDtoDataStoreEntityData = CreateClientRequestDtoDataStoreEntityDataOneOf | CreateClientRequestDtoDataStoreEntityDataOneOf1; + +/** + * + * @export + * @interface CreateClientRequestDtoDataStoreEntityDataOneOf + */ +export interface CreateClientRequestDtoDataStoreEntityDataOneOf { /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf */ - 'kid': string; + 'type': CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf */ - 'addr'?: string; + 'url': string; +} + +export const CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum = { + File: 'FILE' +} as const; + +export type CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum = typeof CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum[keyof typeof CreateClientRequestDtoDataStoreEntityDataOneOfTypeEnum]; + +/** + * + * @export + * @interface CreateClientRequestDtoDataStoreEntityDataOneOf1 + */ +export interface CreateClientRequestDtoDataStoreEntityDataOneOf1 { /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf1 */ - 'crv': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1CrvEnum; + 'type': CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf1 */ - 'x': string; + 'url': string; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1 + * @type {{ [key: string]: string; }} + * @memberof CreateClientRequestDtoDataStoreEntityDataOneOf1 */ - 'y': string; + 'headers'?: { [key: string]: string; }; } -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1KtyEnum = { - Ec: 'EC' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1KtyEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1KtyEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1KtyEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1AlgEnum = { - Es256: 'ES256' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1AlgEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1AlgEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1AlgEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1UseEnum = { - Sig: 'sig', - Enc: 'enc' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1UseEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1UseEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1UseEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1CrvEnum = { - P256: 'P-256' +export const CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum = { + Http: 'HTTP', + Https: 'HTTPS' } as const; -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1CrvEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1CrvEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf1CrvEnum]; +export type CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum = typeof CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum[keyof typeof CreateClientRequestDtoDataStoreEntityDataOneOf1TypeEnum]; /** * * @export - * @interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @interface CreateClientRequestDtoDataStoreEntityKeysInner */ -export interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 { +export interface CreateClientRequestDtoDataStoreEntityKeysInner { /** - * + * Key Type (e.g. RSA or EC * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'kty': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2KtyEnum; + 'kty'?: CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum; /** - * + * Curve name * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'alg': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2AlgEnum; + 'crv'?: CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum; /** - * + * Algorithm * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'use'?: CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2UseEnum; + 'alg'?: CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum; /** - * + * Public Key Use * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'kid': string; + 'use'?: CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum; /** - * + * Unique key ID * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'addr'?: string; + 'kid'?: string; /** - * + * (RSA) Key modulus * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'n': string; + 'n'?: string; /** - * + * (RSA) Key exponent * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2 + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner */ - 'e': string; + 'e'?: string; + /** + * (EC) X Coordinate + * @type {string} + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + */ + 'x'?: string; + /** + * (EC) Y Coordinate + * @type {string} + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + */ + 'y'?: string; + /** + * (EC) Private Key + * @type {string} + * @memberof CreateClientRequestDtoDataStoreEntityKeysInner + */ + 'd'?: string; } -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2KtyEnum = { - Rsa: 'RSA' +export const CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum = { + Ec: 'EC', + Rsa: 'RSA', + Okp: 'OKP' } as const; -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2KtyEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2KtyEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2KtyEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2AlgEnum = { - Rs256: 'RS256' +export type CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerKtyEnum]; +export const CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum = { + Secp256k1: 'secp256k1', + P256: 'P-256', + Ed25519: 'Ed25519' +} as const; + +export type CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerCrvEnum]; +export const CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum = { + Es256K: 'ES256K', + Es256: 'ES256', + Rs256: 'RS256', + Eddsa: 'EDDSA' } as const; -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2AlgEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2AlgEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2AlgEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2UseEnum = { +export type CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerAlgEnum]; +export const CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum = { Sig: 'sig', Enc: 'enc' } as const; -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2UseEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2UseEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf2UseEnum]; +export type CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum = typeof CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum[keyof typeof CreateClientRequestDtoDataStoreEntityKeysInnerUseEnum]; /** * * @export - * @interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @interface CreateClientResponseDto */ -export interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 { +export interface CreateClientResponseDto { /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @memberof CreateClientResponseDto */ - 'kty': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3KtyEnum; + 'id': string; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @memberof CreateClientResponseDto */ - 'crv': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3CrvEnum; + 'name': string; /** - * + * plaintext secret for authenticating to armory * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @memberof CreateClientResponseDto */ - 'alg': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3AlgEnum; + 'clientSecret': string; /** - * + * plaintext secret for authenticating to data store * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @memberof CreateClientResponseDto */ - 'use'?: CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3UseEnum; + 'dataSecret': string | null; /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @type {any} + * @memberof CreateClientResponseDto */ - 'kid': string; + 'createdAt': any; /** * * @type {any} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3 + * @memberof CreateClientResponseDto */ - 'addr': any; + 'updatedAt': any; + /** + * + * @type {CreateClientResponseDtoDataStore} + * @memberof CreateClientResponseDto + */ + 'dataStore': CreateClientResponseDtoDataStore; + /** + * + * @type {CreateClientResponseDtoPolicyEngine} + * @memberof CreateClientResponseDto + */ + 'policyEngine': CreateClientResponseDtoPolicyEngine; } - -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3KtyEnum = { - Ec: 'EC' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3KtyEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3KtyEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3KtyEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3CrvEnum = { - Secp256k1: 'secp256k1' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3CrvEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3CrvEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3CrvEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3AlgEnum = { - Es256K: 'ES256K' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3AlgEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3AlgEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3AlgEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3UseEnum = { - Sig: 'sig', - Enc: 'enc' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3UseEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3UseEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf3UseEnum]; - /** * * @export - * @interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @interface CreateClientResponseDtoDataStore */ -export interface CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 { +export interface CreateClientResponseDtoDataStore { /** * - * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @type {Array} + * @memberof CreateClientResponseDtoDataStore + */ + 'entityPublicKeys': Array; + /** + * + * @type {Array} + * @memberof CreateClientResponseDtoDataStore */ - 'kty': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4KtyEnum; + 'policyPublicKeys': Array; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @memberof CreateClientResponseDtoDataStore */ - 'alg': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4AlgEnum; + 'entityDataUrl'?: string; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @memberof CreateClientResponseDtoDataStore + */ + 'policyDataUrl'?: string; +} +/** + * + * @export + * @interface CreateClientResponseDtoPolicyEngine + */ +export interface CreateClientResponseDtoPolicyEngine { + /** + * + * @type {Array} + * @memberof CreateClientResponseDtoPolicyEngine */ - 'use'?: CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4UseEnum; + 'nodes': Array; +} +/** + * + * @export + * @interface CreateClientResponseDtoPolicyEngineNodesInner + */ +export interface CreateClientResponseDtoPolicyEngineNodesInner { /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @memberof CreateClientResponseDtoPolicyEngineNodesInner */ - 'kid': string; + 'id': string; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @memberof CreateClientResponseDtoPolicyEngineNodesInner */ - 'addr'?: string; + 'clientId': string; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @memberof CreateClientResponseDtoPolicyEngineNodesInner + */ + 'clientSecret'?: string; + /** + * + * @type {CreateClientResponseDtoPolicyEngineNodesInnerPublicKey} + * @memberof CreateClientResponseDtoPolicyEngineNodesInner */ - 'crv': CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4CrvEnum; + 'publicKey': CreateClientResponseDtoPolicyEngineNodesInnerPublicKey; /** * * @type {string} - * @memberof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4 + * @memberof CreateClientResponseDtoPolicyEngineNodesInner */ - 'x': string; + 'url': string; } - -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4KtyEnum = { - Okp: 'OKP' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4KtyEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4KtyEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4KtyEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4AlgEnum = { - Eddsa: 'EDDSA' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4AlgEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4AlgEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4AlgEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4UseEnum = { - Sig: 'sig', - Enc: 'enc' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4UseEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4UseEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4UseEnum]; -export const CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4CrvEnum = { - Ed25519: 'Ed25519' -} as const; - -export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4CrvEnum = typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4CrvEnum[keyof typeof CreateClientResponseDtoPolicyEngineNodesInnerPublicKeyOneOf4CrvEnum]; +/** + * @type CreateClientResponseDtoPolicyEngineNodesInnerPublicKey + * @export + */ +export type CreateClientResponseDtoPolicyEngineNodesInnerPublicKey = AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf1 | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf2 | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf3 | AuthorizationRequestDtoMetadataConfirmationKeyJwkOneOf4; /** * @@ -4382,7 +4440,7 @@ export const ApplicationApiFactory = function (configuration?: Configuration, ba * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ping(options?: any): AxiosPromise { + ping(options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.ping(options).then((request) => request(axios, basePath)); }, }; @@ -4443,8 +4501,8 @@ export const AuthorizationApiAxiosParamCreator = function (configuration?: Confi const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication CLIENT_ID required - await setApiKeyToObject(localVarHeaderParameter, "CLIENT_ID", configuration) + // authentication Client-ID required + await setApiKeyToObject(localVarHeaderParameter, "x-client-id", configuration) if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); @@ -4489,8 +4547,8 @@ export const AuthorizationApiAxiosParamCreator = function (configuration?: Confi const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication CLIENT_ID required - await setApiKeyToObject(localVarHeaderParameter, "CLIENT_ID", configuration) + // authentication Client-ID required + await setApiKeyToObject(localVarHeaderParameter, "x-client-id", configuration) if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); @@ -4536,8 +4594,8 @@ export const AuthorizationApiAxiosParamCreator = function (configuration?: Confi const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication CLIENT_ID required - await setApiKeyToObject(localVarHeaderParameter, "CLIENT_ID", configuration) + // authentication Client-ID required + await setApiKeyToObject(localVarHeaderParameter, "x-client-id", configuration) if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); @@ -4620,40 +4678,106 @@ export const AuthorizationApiFactory = function (configuration?: Configuration, /** * * @summary Approves an authorization request - * @param {string} id - * @param {string} xClientId - * @param {ApprovalDto} approvalDto + * @param {AuthorizationApiApproveRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - approve(id: string, xClientId: string, approvalDto: ApprovalDto, options?: any): AxiosPromise { - return localVarFp.approve(id, xClientId, approvalDto, options).then((request) => request(axios, basePath)); + approve(requestParameters: AuthorizationApiApproveRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.approve(requestParameters.id, requestParameters.xClientId, requestParameters.approvalDto, options).then((request) => request(axios, basePath)); }, /** * * @summary Submits a new authorization request for evaluation by the policy engine - * @param {string} xClientId - * @param {AuthorizationRequestDto} authorizationRequestDto + * @param {AuthorizationApiEvaluateRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - evaluate(xClientId: string, authorizationRequestDto: AuthorizationRequestDto, options?: any): AxiosPromise { - return localVarFp.evaluate(xClientId, authorizationRequestDto, options).then((request) => request(axios, basePath)); + evaluate(requestParameters: AuthorizationApiEvaluateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.evaluate(requestParameters.xClientId, requestParameters.authorizationRequestDto, options).then((request) => request(axios, basePath)); }, /** * * @summary Gets an authorization request by ID - * @param {string} id - * @param {string} xClientId + * @param {AuthorizationApiGetByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getById(id: string, xClientId: string, options?: any): AxiosPromise { - return localVarFp.getById(id, xClientId, options).then((request) => request(axios, basePath)); + getById(requestParameters: AuthorizationApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.id, requestParameters.xClientId, options).then((request) => request(axios, basePath)); }, }; }; +/** + * Request parameters for approve operation in AuthorizationApi. + * @export + * @interface AuthorizationApiApproveRequest + */ +export interface AuthorizationApiApproveRequest { + /** + * + * @type {string} + * @memberof AuthorizationApiApprove + */ + readonly id: string + + /** + * + * @type {string} + * @memberof AuthorizationApiApprove + */ + readonly xClientId: string + + /** + * + * @type {ApprovalDto} + * @memberof AuthorizationApiApprove + */ + readonly approvalDto: ApprovalDto +} + +/** + * Request parameters for evaluate operation in AuthorizationApi. + * @export + * @interface AuthorizationApiEvaluateRequest + */ +export interface AuthorizationApiEvaluateRequest { + /** + * + * @type {string} + * @memberof AuthorizationApiEvaluate + */ + readonly xClientId: string + + /** + * + * @type {AuthorizationRequestDto} + * @memberof AuthorizationApiEvaluate + */ + readonly authorizationRequestDto: AuthorizationRequestDto +} + +/** + * Request parameters for getById operation in AuthorizationApi. + * @export + * @interface AuthorizationApiGetByIdRequest + */ +export interface AuthorizationApiGetByIdRequest { + /** + * + * @type {string} + * @memberof AuthorizationApiGetById + */ + readonly id: string + + /** + * + * @type {string} + * @memberof AuthorizationApiGetById + */ + readonly xClientId: string +} + /** * AuthorizationApi - object-oriented interface * @export @@ -4664,41 +4788,37 @@ export class AuthorizationApi extends BaseAPI { /** * * @summary Approves an authorization request - * @param {string} id - * @param {string} xClientId - * @param {ApprovalDto} approvalDto + * @param {AuthorizationApiApproveRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AuthorizationApi */ - public approve(id: string, xClientId: string, approvalDto: ApprovalDto, options?: RawAxiosRequestConfig) { - return AuthorizationApiFp(this.configuration).approve(id, xClientId, approvalDto, options).then((request) => request(this.axios, this.basePath)); + public approve(requestParameters: AuthorizationApiApproveRequest, options?: RawAxiosRequestConfig) { + return AuthorizationApiFp(this.configuration).approve(requestParameters.id, requestParameters.xClientId, requestParameters.approvalDto, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Submits a new authorization request for evaluation by the policy engine - * @param {string} xClientId - * @param {AuthorizationRequestDto} authorizationRequestDto + * @param {AuthorizationApiEvaluateRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AuthorizationApi */ - public evaluate(xClientId: string, authorizationRequestDto: AuthorizationRequestDto, options?: RawAxiosRequestConfig) { - return AuthorizationApiFp(this.configuration).evaluate(xClientId, authorizationRequestDto, options).then((request) => request(this.axios, this.basePath)); + public evaluate(requestParameters: AuthorizationApiEvaluateRequest, options?: RawAxiosRequestConfig) { + return AuthorizationApiFp(this.configuration).evaluate(requestParameters.xClientId, requestParameters.authorizationRequestDto, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Gets an authorization request by ID - * @param {string} id - * @param {string} xClientId + * @param {AuthorizationApiGetByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AuthorizationApi */ - public getById(id: string, xClientId: string, options?: RawAxiosRequestConfig) { - return AuthorizationApiFp(this.configuration).getById(id, xClientId, options).then((request) => request(this.axios, this.basePath)); + public getById(requestParameters: AuthorizationApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return AuthorizationApiFp(this.configuration).getById(requestParameters.id, requestParameters.xClientId, options).then((request) => request(this.axios, this.basePath)); } } @@ -4735,8 +4855,8 @@ export const ClientApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication ADMIN_API_KEY required - await setApiKeyToObject(localVarHeaderParameter, "ADMIN_API_KEY", configuration) + // authentication Admin-API-Key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) if (xApiKey != null) { localVarHeaderParameter['x-api-key'] = String(xApiKey); @@ -4793,17 +4913,37 @@ export const ClientApiFactory = function (configuration?: Configuration, basePat /** * * @summary Creates a new client - * @param {string} xApiKey - * @param {CreateClientRequestDto} createClientRequestDto + * @param {ClientApiCreateRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - create(xApiKey: string, createClientRequestDto: CreateClientRequestDto, options?: any): AxiosPromise { - return localVarFp.create(xApiKey, createClientRequestDto, options).then((request) => request(axios, basePath)); + create(requestParameters: ClientApiCreateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.create(requestParameters.xApiKey, requestParameters.createClientRequestDto, options).then((request) => request(axios, basePath)); }, }; }; +/** + * Request parameters for create operation in ClientApi. + * @export + * @interface ClientApiCreateRequest + */ +export interface ClientApiCreateRequest { + /** + * + * @type {string} + * @memberof ClientApiCreate + */ + readonly xApiKey: string + + /** + * + * @type {CreateClientRequestDto} + * @memberof ClientApiCreate + */ + readonly createClientRequestDto: CreateClientRequestDto +} + /** * ClientApi - object-oriented interface * @export @@ -4814,14 +4954,13 @@ export class ClientApi extends BaseAPI { /** * * @summary Creates a new client - * @param {string} xApiKey - * @param {CreateClientRequestDto} createClientRequestDto + * @param {ClientApiCreateRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ClientApi */ - public create(xApiKey: string, createClientRequestDto: CreateClientRequestDto, options?: RawAxiosRequestConfig) { - return ClientApiFp(this.configuration).create(xApiKey, createClientRequestDto, options).then((request) => request(this.axios, this.basePath)); + public create(requestParameters: ClientApiCreateRequest, options?: RawAxiosRequestConfig) { + return ClientApiFp(this.configuration).create(requestParameters.xApiKey, requestParameters.createClientRequestDto, options).then((request) => request(this.axios, this.basePath)); } } @@ -5015,8 +5154,8 @@ export const ManagedDataStoreApiAxiosParamCreator = function (configuration?: Co const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication CLIENT_SECRET required - await setApiKeyToObject(localVarHeaderParameter, "CLIENT_SECRET", configuration) + // authentication Client-Secret required + await setApiKeyToObject(localVarHeaderParameter, "x-client-secret", configuration) if (xClientSecret != null) { localVarHeaderParameter['x-client-secret'] = String(xClientSecret); @@ -5123,58 +5262,140 @@ export const ManagedDataStoreApiFactory = function (configuration?: Configuratio /** * * @summary Gets the client entities - * @param {string} clientId + * @param {ManagedDataStoreApiGetEntitiesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getEntities(clientId: string, options?: any): AxiosPromise { - return localVarFp.getEntities(clientId, options).then((request) => request(axios, basePath)); + getEntities(requestParameters: ManagedDataStoreApiGetEntitiesRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getEntities(requestParameters.clientId, options).then((request) => request(axios, basePath)); }, /** * * @summary Gets the client policies - * @param {string} clientId + * @param {ManagedDataStoreApiGetPoliciesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPolicies(clientId: string, options?: any): AxiosPromise { - return localVarFp.getPolicies(clientId, options).then((request) => request(axios, basePath)); + getPolicies(requestParameters: ManagedDataStoreApiGetPoliciesRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getPolicies(requestParameters.clientId, options).then((request) => request(axios, basePath)); }, /** * * @summary Sets the client entities - * @param {string} clientId - * @param {SetEntityStoreDto} setEntityStoreDto + * @param {ManagedDataStoreApiSetEntitiesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - setEntities(clientId: string, setEntityStoreDto: SetEntityStoreDto, options?: any): AxiosPromise { - return localVarFp.setEntities(clientId, setEntityStoreDto, options).then((request) => request(axios, basePath)); + setEntities(requestParameters: ManagedDataStoreApiSetEntitiesRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.setEntities(requestParameters.clientId, requestParameters.setEntityStoreDto, options).then((request) => request(axios, basePath)); }, /** * * @summary Sets the client policies - * @param {string} clientId - * @param {SetPolicyStoreDto} setPolicyStoreDto + * @param {ManagedDataStoreApiSetPoliciesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - setPolicies(clientId: string, setPolicyStoreDto: SetPolicyStoreDto, options?: any): AxiosPromise { - return localVarFp.setPolicies(clientId, setPolicyStoreDto, options).then((request) => request(axios, basePath)); + setPolicies(requestParameters: ManagedDataStoreApiSetPoliciesRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.setPolicies(requestParameters.clientId, requestParameters.setPolicyStoreDto, options).then((request) => request(axios, basePath)); }, /** * * @summary Sync the client data store with the engine cluster - * @param {string} xClientSecret + * @param {ManagedDataStoreApiSyncRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - sync(xClientSecret: string, options?: any): AxiosPromise { - return localVarFp.sync(xClientSecret, options).then((request) => request(axios, basePath)); + sync(requestParameters: ManagedDataStoreApiSyncRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.sync(requestParameters.xClientSecret, options).then((request) => request(axios, basePath)); }, }; }; +/** + * Request parameters for getEntities operation in ManagedDataStoreApi. + * @export + * @interface ManagedDataStoreApiGetEntitiesRequest + */ +export interface ManagedDataStoreApiGetEntitiesRequest { + /** + * + * @type {string} + * @memberof ManagedDataStoreApiGetEntities + */ + readonly clientId: string +} + +/** + * Request parameters for getPolicies operation in ManagedDataStoreApi. + * @export + * @interface ManagedDataStoreApiGetPoliciesRequest + */ +export interface ManagedDataStoreApiGetPoliciesRequest { + /** + * + * @type {string} + * @memberof ManagedDataStoreApiGetPolicies + */ + readonly clientId: string +} + +/** + * Request parameters for setEntities operation in ManagedDataStoreApi. + * @export + * @interface ManagedDataStoreApiSetEntitiesRequest + */ +export interface ManagedDataStoreApiSetEntitiesRequest { + /** + * + * @type {string} + * @memberof ManagedDataStoreApiSetEntities + */ + readonly clientId: string + + /** + * + * @type {SetEntityStoreDto} + * @memberof ManagedDataStoreApiSetEntities + */ + readonly setEntityStoreDto: SetEntityStoreDto +} + +/** + * Request parameters for setPolicies operation in ManagedDataStoreApi. + * @export + * @interface ManagedDataStoreApiSetPoliciesRequest + */ +export interface ManagedDataStoreApiSetPoliciesRequest { + /** + * + * @type {string} + * @memberof ManagedDataStoreApiSetPolicies + */ + readonly clientId: string + + /** + * + * @type {SetPolicyStoreDto} + * @memberof ManagedDataStoreApiSetPolicies + */ + readonly setPolicyStoreDto: SetPolicyStoreDto +} + +/** + * Request parameters for sync operation in ManagedDataStoreApi. + * @export + * @interface ManagedDataStoreApiSyncRequest + */ +export interface ManagedDataStoreApiSyncRequest { + /** + * + * @type {string} + * @memberof ManagedDataStoreApiSync + */ + readonly xClientSecret: string +} + /** * ManagedDataStoreApi - object-oriented interface * @export @@ -5185,63 +5406,61 @@ export class ManagedDataStoreApi extends BaseAPI { /** * * @summary Gets the client entities - * @param {string} clientId + * @param {ManagedDataStoreApiGetEntitiesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ManagedDataStoreApi */ - public getEntities(clientId: string, options?: RawAxiosRequestConfig) { - return ManagedDataStoreApiFp(this.configuration).getEntities(clientId, options).then((request) => request(this.axios, this.basePath)); + public getEntities(requestParameters: ManagedDataStoreApiGetEntitiesRequest, options?: RawAxiosRequestConfig) { + return ManagedDataStoreApiFp(this.configuration).getEntities(requestParameters.clientId, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Gets the client policies - * @param {string} clientId + * @param {ManagedDataStoreApiGetPoliciesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ManagedDataStoreApi */ - public getPolicies(clientId: string, options?: RawAxiosRequestConfig) { - return ManagedDataStoreApiFp(this.configuration).getPolicies(clientId, options).then((request) => request(this.axios, this.basePath)); + public getPolicies(requestParameters: ManagedDataStoreApiGetPoliciesRequest, options?: RawAxiosRequestConfig) { + return ManagedDataStoreApiFp(this.configuration).getPolicies(requestParameters.clientId, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Sets the client entities - * @param {string} clientId - * @param {SetEntityStoreDto} setEntityStoreDto + * @param {ManagedDataStoreApiSetEntitiesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ManagedDataStoreApi */ - public setEntities(clientId: string, setEntityStoreDto: SetEntityStoreDto, options?: RawAxiosRequestConfig) { - return ManagedDataStoreApiFp(this.configuration).setEntities(clientId, setEntityStoreDto, options).then((request) => request(this.axios, this.basePath)); + public setEntities(requestParameters: ManagedDataStoreApiSetEntitiesRequest, options?: RawAxiosRequestConfig) { + return ManagedDataStoreApiFp(this.configuration).setEntities(requestParameters.clientId, requestParameters.setEntityStoreDto, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Sets the client policies - * @param {string} clientId - * @param {SetPolicyStoreDto} setPolicyStoreDto + * @param {ManagedDataStoreApiSetPoliciesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ManagedDataStoreApi */ - public setPolicies(clientId: string, setPolicyStoreDto: SetPolicyStoreDto, options?: RawAxiosRequestConfig) { - return ManagedDataStoreApiFp(this.configuration).setPolicies(clientId, setPolicyStoreDto, options).then((request) => request(this.axios, this.basePath)); + public setPolicies(requestParameters: ManagedDataStoreApiSetPoliciesRequest, options?: RawAxiosRequestConfig) { + return ManagedDataStoreApiFp(this.configuration).setPolicies(requestParameters.clientId, requestParameters.setPolicyStoreDto, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Sync the client data store with the engine cluster - * @param {string} xClientSecret + * @param {ManagedDataStoreApiSyncRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ManagedDataStoreApi */ - public sync(xClientSecret: string, options?: RawAxiosRequestConfig) { - return ManagedDataStoreApiFp(this.configuration).sync(xClientSecret, options).then((request) => request(this.axios, this.basePath)); + public sync(requestParameters: ManagedDataStoreApiSyncRequest, options?: RawAxiosRequestConfig) { + return ManagedDataStoreApiFp(this.configuration).sync(requestParameters.xClientSecret, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/packages/armory-sdk/src/lib/http/client/auth/base.ts b/packages/armory-sdk/src/lib/http/client/auth/base.ts index faf10c639..2699806d4 100644 --- a/packages/armory-sdk/src/lib/http/client/auth/base.ts +++ b/packages/armory-sdk/src/lib/http/client/auth/base.ts @@ -19,7 +19,7 @@ import type { Configuration } from './configuration'; import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; import globalAxios from 'axios'; -export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); +export const BASE_PATH = "http://localhost:3005".replace(/\/+$/, ""); /** * diff --git a/packages/armory-sdk/src/lib/http/client/vault/api.ts b/packages/armory-sdk/src/lib/http/client/vault/api.ts index 56fa6c980..9f731f27f 100644 --- a/packages/armory-sdk/src/lib/http/client/vault/api.ts +++ b/packages/armory-sdk/src/lib/http/client/vault/api.ts @@ -2,7 +2,7 @@ /* eslint-disable */ /** * Vault - * Secure storage for private keys and sensitive data, designed to protect your most critical assets in web3.0 + * Secure Enclave-backed authorization proxy for web3 secrets. Holds encrypted credentials and proxies API requests to custodians and wallet tech providers. Can also generate evm wallet private keys & sign transactions. * * The version of the OpenAPI document: 1.0 * @@ -93,65 +93,269 @@ export interface ClientDto { 'clientId': string; /** * - * @type {CreateClientDtoEngineJwk} + * @type {string} * @memberof ClientDto */ - 'engineJwk'?: CreateClientDtoEngineJwk; + 'name': string; /** * - * @type {string} + * @type {ClientDtoConfigurationSource} * @memberof ClientDto */ - 'audience'?: string; + 'configurationSource': ClientDtoConfigurationSource; + /** + * + * @type {ClientDtoBackupPublicKey} + * @memberof ClientDto + */ + 'backupPublicKey': ClientDtoBackupPublicKey | null; /** * * @type {string} * @memberof ClientDto */ - 'issuer'?: string; + 'baseUrl': string | null; /** * - * @type {number} + * @type {ClientDtoAuth} * @memberof ClientDto */ - 'maxTokenAge'?: number; + 'auth': ClientDtoAuth; /** * - * @type {EncryptionKeyDtoPublicKey} + * @type {any} * @memberof ClientDto */ - 'backupPublicKey'?: EncryptionKeyDtoPublicKey; + 'createdAt': any; /** * - * @type {boolean} + * @type {any} * @memberof ClientDto */ - 'allowKeyExport'?: boolean; + 'updatedAt': any; +} +/** + * + * @export + * @interface ClientDtoAuth + */ +export interface ClientDtoAuth { + /** + * + * @type {boolean} + * @memberof ClientDtoAuth + */ + 'disabled': boolean; + /** + * + * @type {ClientDtoAuthLocal} + * @memberof ClientDtoAuth + */ + 'local': ClientDtoAuthLocal | null; + /** + * + * @type {ClientDtoAuthTokenValidation} + * @memberof ClientDtoAuth + */ + 'tokenValidation': ClientDtoAuthTokenValidation; +} +/** + * + * @export + * @interface ClientDtoAuthLocal + */ +export interface ClientDtoAuthLocal { + /** + * + * @type {ClientDtoAuthLocalJwsd} + * @memberof ClientDtoAuthLocal + */ + 'jwsd': ClientDtoAuthLocalJwsd; + /** + * + * @type {string} + * @memberof ClientDtoAuthLocal + */ + 'allowedUsersJwksUrl': string | null; + /** + * + * @type {Array} + * @memberof ClientDtoAuthLocal + */ + 'allowedUsers': Array | null; +} +/** + * + * @export + * @interface ClientDtoAuthLocalJwsd + */ +export interface ClientDtoAuthLocalJwsd { + /** + * + * @type {number} + * @memberof ClientDtoAuthLocalJwsd + */ + 'maxAge': number; /** * * @type {Array} - * @memberof ClientDto + * @memberof ClientDtoAuthLocalJwsd */ - 'allowWildcard'?: Array; + 'requiredComponents': Array; +} +/** + * + * @export + * @interface ClientDtoAuthTokenValidation + */ +export interface ClientDtoAuthTokenValidation { + /** + * + * @type {boolean} + * @memberof ClientDtoAuthTokenValidation + */ + 'disabled': boolean; /** * * @type {string} - * @memberof ClientDto + * @memberof ClientDtoAuthTokenValidation */ - 'baseUrl'?: string; + 'url': string | null; /** * - * @type {any} - * @memberof ClientDto + * @type {string} + * @memberof ClientDtoAuthTokenValidation */ - 'createdAt': any; + 'jwksUrl': string | null; /** * - * @type {any} - * @memberof ClientDto + * @type {CreateClientDtoAuthTokenValidationPinnedPublicKey} + * @memberof ClientDtoAuthTokenValidation */ - 'updatedAt': any; + 'pinnedPublicKey': CreateClientDtoAuthTokenValidationPinnedPublicKey | null; + /** + * + * @type {ClientDtoAuthTokenValidationVerification} + * @memberof ClientDtoAuthTokenValidation + */ + 'verification': ClientDtoAuthTokenValidationVerification; +} +/** + * + * @export + * @interface ClientDtoAuthTokenValidationVerification + */ +export interface ClientDtoAuthTokenValidationVerification { + /** + * + * @type {string} + * @memberof ClientDtoAuthTokenValidationVerification + */ + 'audience': string | null; + /** + * + * @type {string} + * @memberof ClientDtoAuthTokenValidationVerification + */ + 'issuer': string | null; + /** + * + * @type {number} + * @memberof ClientDtoAuthTokenValidationVerification + */ + 'maxTokenAge': number | null; + /** + * + * @type {boolean} + * @memberof ClientDtoAuthTokenValidationVerification + */ + 'requireBoundTokens': boolean; + /** + * + * @type {boolean} + * @memberof ClientDtoAuthTokenValidationVerification + */ + 'allowBearerTokens': boolean; + /** + * + * @type {Array} + * @memberof ClientDtoAuthTokenValidationVerification + */ + 'allowWildcard': Array | null; +} +/** + * + * @export + * @interface ClientDtoBackupPublicKey + */ +export interface ClientDtoBackupPublicKey { + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'kty': ClientDtoBackupPublicKeyKtyEnum; + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'alg': ClientDtoBackupPublicKeyAlgEnum; + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'use'?: ClientDtoBackupPublicKeyUseEnum; + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'kid': string; + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'addr'?: string; + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'n': string; + /** + * + * @type {string} + * @memberof ClientDtoBackupPublicKey + */ + 'e': string; } + +export const ClientDtoBackupPublicKeyKtyEnum = { + Rsa: 'RSA' +} as const; + +export type ClientDtoBackupPublicKeyKtyEnum = typeof ClientDtoBackupPublicKeyKtyEnum[keyof typeof ClientDtoBackupPublicKeyKtyEnum]; +export const ClientDtoBackupPublicKeyAlgEnum = { + Rs256: 'RS256' +} as const; + +export type ClientDtoBackupPublicKeyAlgEnum = typeof ClientDtoBackupPublicKeyAlgEnum[keyof typeof ClientDtoBackupPublicKeyAlgEnum]; +export const ClientDtoBackupPublicKeyUseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type ClientDtoBackupPublicKeyUseEnum = typeof ClientDtoBackupPublicKeyUseEnum[keyof typeof ClientDtoBackupPublicKeyUseEnum]; + +/** + * @type ClientDtoConfigurationSource + * @export + */ +export type ClientDtoConfigurationSource = string; + /** * * @export @@ -166,10 +370,34 @@ export interface CreateClientDto { 'clientId'?: string; /** * - * @type {CreateClientDtoEngineJwk} + * @type {string} + * @memberof CreateClientDto + */ + 'name'?: string; + /** + * + * @type {string} + * @memberof CreateClientDto + */ + 'baseUrl'?: string; + /** + * + * @type {CreateClientDtoBackupPublicKey} + * @memberof CreateClientDto + */ + 'backupPublicKey'?: CreateClientDtoBackupPublicKey; + /** + * + * @type {CreateClientDtoAuth} + * @memberof CreateClientDto + */ + 'auth'?: CreateClientDtoAuth; + /** + * + * @type {CreateClientDtoAuthLocalAllowedUsersInnerPublicKey} * @memberof CreateClientDto */ - 'engineJwk'?: CreateClientDtoEngineJwk; + 'engineJwk'?: CreateClientDtoAuthLocalAllowedUsersInnerPublicKey; /** * * @type {string} @@ -190,383 +418,690 @@ export interface CreateClientDto { 'maxTokenAge'?: number; /** * - * @type {EncryptionKeyDtoPublicKey} + * @type {Array} * @memberof CreateClientDto */ - 'backupPublicKey'?: EncryptionKeyDtoPublicKey; + 'allowWildcard'?: Array; /** * * @type {boolean} * @memberof CreateClientDto */ 'allowKeyExport'?: boolean; +} +/** + * + * @export + * @interface CreateClientDtoAuth + */ +export interface CreateClientDtoAuth { /** * - * @type {Array} - * @memberof CreateClientDto + * @type {CreateClientDtoAuthLocal} + * @memberof CreateClientDtoAuth */ - 'allowWildcard'?: Array; + 'local'?: CreateClientDtoAuthLocal | null; + /** + * + * @type {CreateClientDtoAuthTokenValidation} + * @memberof CreateClientDtoAuth + */ + 'tokenValidation'?: CreateClientDtoAuthTokenValidation; +} +/** + * + * @export + * @interface CreateClientDtoAuthLocal + */ +export interface CreateClientDtoAuthLocal { + /** + * + * @type {CreateClientDtoAuthLocalJwsd} + * @memberof CreateClientDtoAuthLocal + */ + 'jwsd'?: CreateClientDtoAuthLocalJwsd | null; + /** + * Pin specific users to be authorized; if set, ONLY these users are allowed + * @type {Array} + * @memberof CreateClientDtoAuthLocal + */ + 'allowedUsers'?: Array | null; +} +/** + * + * @export + * @interface CreateClientDtoAuthLocalAllowedUsersInner + */ +export interface CreateClientDtoAuthLocalAllowedUsersInner { /** * * @type {string} - * @memberof CreateClientDto + * @memberof CreateClientDtoAuthLocalAllowedUsersInner */ - 'baseUrl'?: string; + 'userId': string; + /** + * + * @type {CreateClientDtoAuthLocalAllowedUsersInnerPublicKey} + * @memberof CreateClientDtoAuthLocalAllowedUsersInner + */ + 'publicKey': CreateClientDtoAuthLocalAllowedUsersInnerPublicKey; } /** - * @type CreateClientDtoEngineJwk + * @type CreateClientDtoAuthLocalAllowedUsersInnerPublicKey * @export */ -export type CreateClientDtoEngineJwk = CreateClientDtoEngineJwkOneOf | CreateClientDtoEngineJwkOneOf1 | CreateClientDtoEngineJwkOneOf2 | CreateClientDtoEngineJwkOneOf3 | EncryptionKeyDtoPublicKey; +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKey = CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 | CreateClientDtoBackupPublicKey; /** * * @export - * @interface CreateClientDtoEngineJwkOneOf + * @interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ -export interface CreateClientDtoEngineJwkOneOf { +export interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf { /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ - 'kty': CreateClientDtoEngineJwkOneOfKtyEnum; + 'kty': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfKtyEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ - 'alg': CreateClientDtoEngineJwkOneOfAlgEnum; + 'alg': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfAlgEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ - 'use'?: CreateClientDtoEngineJwkOneOfUseEnum; + 'use'?: CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfUseEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ 'kid': string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ 'addr'?: string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ - 'crv': CreateClientDtoEngineJwkOneOfCrvEnum; + 'crv': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfCrvEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ 'x': string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf */ 'y': string; } -export const CreateClientDtoEngineJwkOneOfKtyEnum = { +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfKtyEnum = { Ec: 'EC' } as const; -export type CreateClientDtoEngineJwkOneOfKtyEnum = typeof CreateClientDtoEngineJwkOneOfKtyEnum[keyof typeof CreateClientDtoEngineJwkOneOfKtyEnum]; -export const CreateClientDtoEngineJwkOneOfAlgEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfKtyEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfKtyEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfKtyEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfAlgEnum = { Es256K: 'ES256K' } as const; -export type CreateClientDtoEngineJwkOneOfAlgEnum = typeof CreateClientDtoEngineJwkOneOfAlgEnum[keyof typeof CreateClientDtoEngineJwkOneOfAlgEnum]; -export const CreateClientDtoEngineJwkOneOfUseEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfAlgEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfAlgEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfAlgEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfUseEnum = { Sig: 'sig', Enc: 'enc' } as const; -export type CreateClientDtoEngineJwkOneOfUseEnum = typeof CreateClientDtoEngineJwkOneOfUseEnum[keyof typeof CreateClientDtoEngineJwkOneOfUseEnum]; -export const CreateClientDtoEngineJwkOneOfCrvEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfUseEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfUseEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfUseEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfCrvEnum = { Secp256k1: 'secp256k1' } as const; -export type CreateClientDtoEngineJwkOneOfCrvEnum = typeof CreateClientDtoEngineJwkOneOfCrvEnum[keyof typeof CreateClientDtoEngineJwkOneOfCrvEnum]; +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfCrvEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfCrvEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOfCrvEnum]; /** * * @export - * @interface CreateClientDtoEngineJwkOneOf1 + * @interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ -export interface CreateClientDtoEngineJwkOneOf1 { +export interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 { /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ - 'kty': CreateClientDtoEngineJwkOneOf1KtyEnum; + 'kty': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1KtyEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ - 'alg': CreateClientDtoEngineJwkOneOf1AlgEnum; + 'alg': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1AlgEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ - 'use'?: CreateClientDtoEngineJwkOneOf1UseEnum; + 'use'?: CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1UseEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ 'kid': string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ 'addr'?: string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ - 'crv': CreateClientDtoEngineJwkOneOf1CrvEnum; + 'crv': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1CrvEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ 'x': string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf1 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 */ 'y': string; } -export const CreateClientDtoEngineJwkOneOf1KtyEnum = { +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1KtyEnum = { Ec: 'EC' } as const; -export type CreateClientDtoEngineJwkOneOf1KtyEnum = typeof CreateClientDtoEngineJwkOneOf1KtyEnum[keyof typeof CreateClientDtoEngineJwkOneOf1KtyEnum]; -export const CreateClientDtoEngineJwkOneOf1AlgEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1KtyEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1KtyEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1KtyEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1AlgEnum = { Es256: 'ES256' } as const; -export type CreateClientDtoEngineJwkOneOf1AlgEnum = typeof CreateClientDtoEngineJwkOneOf1AlgEnum[keyof typeof CreateClientDtoEngineJwkOneOf1AlgEnum]; -export const CreateClientDtoEngineJwkOneOf1UseEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1AlgEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1AlgEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1AlgEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1UseEnum = { Sig: 'sig', Enc: 'enc' } as const; -export type CreateClientDtoEngineJwkOneOf1UseEnum = typeof CreateClientDtoEngineJwkOneOf1UseEnum[keyof typeof CreateClientDtoEngineJwkOneOf1UseEnum]; -export const CreateClientDtoEngineJwkOneOf1CrvEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1UseEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1UseEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1UseEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1CrvEnum = { P256: 'P-256' } as const; -export type CreateClientDtoEngineJwkOneOf1CrvEnum = typeof CreateClientDtoEngineJwkOneOf1CrvEnum[keyof typeof CreateClientDtoEngineJwkOneOf1CrvEnum]; +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1CrvEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1CrvEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1CrvEnum]; /** * * @export - * @interface CreateClientDtoEngineJwkOneOf2 + * @interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ -export interface CreateClientDtoEngineJwkOneOf2 { +export interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 { /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf2 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ - 'kty': CreateClientDtoEngineJwkOneOf2KtyEnum; + 'kty': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2KtyEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf2 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ - 'crv': CreateClientDtoEngineJwkOneOf2CrvEnum; + 'crv': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2CrvEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf2 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ - 'alg': CreateClientDtoEngineJwkOneOf2AlgEnum; + 'alg': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2AlgEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf2 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ - 'use'?: CreateClientDtoEngineJwkOneOf2UseEnum; + 'use'?: CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2UseEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf2 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ 'kid': string; /** * * @type {any} - * @memberof CreateClientDtoEngineJwkOneOf2 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 */ 'addr': any; } -export const CreateClientDtoEngineJwkOneOf2KtyEnum = { +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2KtyEnum = { Ec: 'EC' } as const; -export type CreateClientDtoEngineJwkOneOf2KtyEnum = typeof CreateClientDtoEngineJwkOneOf2KtyEnum[keyof typeof CreateClientDtoEngineJwkOneOf2KtyEnum]; -export const CreateClientDtoEngineJwkOneOf2CrvEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2KtyEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2KtyEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2KtyEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2CrvEnum = { Secp256k1: 'secp256k1' } as const; -export type CreateClientDtoEngineJwkOneOf2CrvEnum = typeof CreateClientDtoEngineJwkOneOf2CrvEnum[keyof typeof CreateClientDtoEngineJwkOneOf2CrvEnum]; -export const CreateClientDtoEngineJwkOneOf2AlgEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2CrvEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2CrvEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2CrvEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2AlgEnum = { Es256K: 'ES256K' } as const; -export type CreateClientDtoEngineJwkOneOf2AlgEnum = typeof CreateClientDtoEngineJwkOneOf2AlgEnum[keyof typeof CreateClientDtoEngineJwkOneOf2AlgEnum]; -export const CreateClientDtoEngineJwkOneOf2UseEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2AlgEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2AlgEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2AlgEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2UseEnum = { Sig: 'sig', Enc: 'enc' } as const; -export type CreateClientDtoEngineJwkOneOf2UseEnum = typeof CreateClientDtoEngineJwkOneOf2UseEnum[keyof typeof CreateClientDtoEngineJwkOneOf2UseEnum]; +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2UseEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2UseEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2UseEnum]; /** * * @export - * @interface CreateClientDtoEngineJwkOneOf3 + * @interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ -export interface CreateClientDtoEngineJwkOneOf3 { +export interface CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 { /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ - 'kty': CreateClientDtoEngineJwkOneOf3KtyEnum; + 'kty': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3KtyEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ - 'alg': CreateClientDtoEngineJwkOneOf3AlgEnum; + 'alg': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3AlgEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ - 'use'?: CreateClientDtoEngineJwkOneOf3UseEnum; + 'use'?: CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3UseEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ 'kid': string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ 'addr'?: string; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ - 'crv': CreateClientDtoEngineJwkOneOf3CrvEnum; + 'crv': CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3CrvEnum; /** * * @type {string} - * @memberof CreateClientDtoEngineJwkOneOf3 + * @memberof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 */ 'x': string; } -export const CreateClientDtoEngineJwkOneOf3KtyEnum = { +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3KtyEnum = { Okp: 'OKP' } as const; -export type CreateClientDtoEngineJwkOneOf3KtyEnum = typeof CreateClientDtoEngineJwkOneOf3KtyEnum[keyof typeof CreateClientDtoEngineJwkOneOf3KtyEnum]; -export const CreateClientDtoEngineJwkOneOf3AlgEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3KtyEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3KtyEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3KtyEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3AlgEnum = { Eddsa: 'EDDSA' } as const; -export type CreateClientDtoEngineJwkOneOf3AlgEnum = typeof CreateClientDtoEngineJwkOneOf3AlgEnum[keyof typeof CreateClientDtoEngineJwkOneOf3AlgEnum]; -export const CreateClientDtoEngineJwkOneOf3UseEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3AlgEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3AlgEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3AlgEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3UseEnum = { Sig: 'sig', Enc: 'enc' } as const; -export type CreateClientDtoEngineJwkOneOf3UseEnum = typeof CreateClientDtoEngineJwkOneOf3UseEnum[keyof typeof CreateClientDtoEngineJwkOneOf3UseEnum]; -export const CreateClientDtoEngineJwkOneOf3CrvEnum = { +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3UseEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3UseEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3UseEnum]; +export const CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3CrvEnum = { Ed25519: 'Ed25519' } as const; -export type CreateClientDtoEngineJwkOneOf3CrvEnum = typeof CreateClientDtoEngineJwkOneOf3CrvEnum[keyof typeof CreateClientDtoEngineJwkOneOf3CrvEnum]; +export type CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3CrvEnum = typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3CrvEnum[keyof typeof CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3CrvEnum]; /** * * @export - * @interface DeriveAccountDto + * @interface CreateClientDtoAuthLocalJwsd */ -export interface DeriveAccountDto { +export interface CreateClientDtoAuthLocalJwsd { /** * - * @type {string} - * @memberof DeriveAccountDto + * @type {number} + * @memberof CreateClientDtoAuthLocalJwsd */ - 'keyId': string; + 'maxAge'?: number; /** * * @type {Array} - * @memberof DeriveAccountDto - */ - 'derivationPaths'?: Array; - /** - * - * @type {number} - * @memberof DeriveAccountDto + * @memberof CreateClientDtoAuthLocalJwsd */ - 'count'?: number; + 'requiredComponents'?: Array; } /** * * @export - * @interface DeriveAccountResponseDto + * @interface CreateClientDtoAuthTokenValidation */ -export interface DeriveAccountResponseDto { +export interface CreateClientDtoAuthTokenValidation { /** * - * @type {Array} - * @memberof DeriveAccountResponseDto + * @type {boolean} + * @memberof CreateClientDtoAuthTokenValidation */ - 'accounts': Array; + 'disabled'?: boolean; + /** + * + * @type {string} + * @memberof CreateClientDtoAuthTokenValidation + */ + 'url'?: string | null; + /** + * + * @type {CreateClientDtoAuthTokenValidationPinnedPublicKey} + * @memberof CreateClientDtoAuthTokenValidation + */ + 'pinnedPublicKey'?: CreateClientDtoAuthTokenValidationPinnedPublicKey | null; + /** + * + * @type {CreateClientDtoAuthTokenValidationVerification} + * @memberof CreateClientDtoAuthTokenValidation + */ + 'verification'?: CreateClientDtoAuthTokenValidationVerification; } +/** + * @type CreateClientDtoAuthTokenValidationPinnedPublicKey + * @export + */ +export type CreateClientDtoAuthTokenValidationPinnedPublicKey = CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 | CreateClientDtoBackupPublicKey; + /** * * @export - * @interface EncryptionKeyDto + * @interface CreateClientDtoAuthTokenValidationVerification */ -export interface EncryptionKeyDto { +export interface CreateClientDtoAuthTokenValidationVerification { /** * - * @type {EncryptionKeyDtoPublicKey} - * @memberof EncryptionKeyDto + * @type {string} + * @memberof CreateClientDtoAuthTokenValidationVerification */ - 'publicKey': EncryptionKeyDtoPublicKey; + 'audience'?: string | null; + /** + * + * @type {string} + * @memberof CreateClientDtoAuthTokenValidationVerification + */ + 'issuer'?: string | null; + /** + * + * @type {number} + * @memberof CreateClientDtoAuthTokenValidationVerification + */ + 'maxTokenAge'?: number | null; + /** + * + * @type {boolean} + * @memberof CreateClientDtoAuthTokenValidationVerification + */ + 'requireBoundTokens'?: boolean; + /** + * + * @type {boolean} + * @memberof CreateClientDtoAuthTokenValidationVerification + */ + 'allowBearerTokens'?: boolean; + /** + * + * @type {Array} + * @memberof CreateClientDtoAuthTokenValidationVerification + */ + 'allowWildcard'?: Array | null; +} +/** + * + * @export + * @interface CreateClientDtoBackupPublicKey + */ +export interface CreateClientDtoBackupPublicKey { + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'kty': CreateClientDtoBackupPublicKeyKtyEnum; + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'alg': CreateClientDtoBackupPublicKeyAlgEnum; + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'use'?: CreateClientDtoBackupPublicKeyUseEnum; + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'kid': string; + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'addr'?: string; + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'n': string; + /** + * + * @type {string} + * @memberof CreateClientDtoBackupPublicKey + */ + 'e': string; +} + +export const CreateClientDtoBackupPublicKeyKtyEnum = { + Rsa: 'RSA' +} as const; + +export type CreateClientDtoBackupPublicKeyKtyEnum = typeof CreateClientDtoBackupPublicKeyKtyEnum[keyof typeof CreateClientDtoBackupPublicKeyKtyEnum]; +export const CreateClientDtoBackupPublicKeyAlgEnum = { + Rs256: 'RS256' +} as const; + +export type CreateClientDtoBackupPublicKeyAlgEnum = typeof CreateClientDtoBackupPublicKeyAlgEnum[keyof typeof CreateClientDtoBackupPublicKeyAlgEnum]; +export const CreateClientDtoBackupPublicKeyUseEnum = { + Sig: 'sig', + Enc: 'enc' +} as const; + +export type CreateClientDtoBackupPublicKeyUseEnum = typeof CreateClientDtoBackupPublicKeyUseEnum[keyof typeof CreateClientDtoBackupPublicKeyUseEnum]; + +/** + * + * @export + * @interface CreateConnectionDto + */ +export interface CreateConnectionDto { + /** + * + * @type {string} + * @memberof CreateConnectionDto + */ + 'connectionId'?: string; + /** + * + * @type {any} + * @memberof CreateConnectionDto + */ + 'createdAt'?: any; + /** + * RSA encrypted JSON string of the credentials + * @type {string} + * @memberof CreateConnectionDto + */ + 'encryptedCredentials'?: string; + /** + * + * @type {string} + * @memberof CreateConnectionDto + */ + 'label'?: string; + /** + * + * @type {string} + * @memberof CreateConnectionDto + */ + 'provider': CreateConnectionDtoProviderEnum; + /** + * + * @type {string} + * @memberof CreateConnectionDto + */ + 'url': string; + /** + * + * @type {any} + * @memberof CreateConnectionDto + */ + 'credentials'?: any; +} + +export const CreateConnectionDtoProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type CreateConnectionDtoProviderEnum = typeof CreateConnectionDtoProviderEnum[keyof typeof CreateConnectionDtoProviderEnum]; + +/** + * + * @export + * @interface DeriveAccountDto + */ +export interface DeriveAccountDto { + /** + * + * @type {string} + * @memberof DeriveAccountDto + */ + 'keyId': string; + /** + * + * @type {Array} + * @memberof DeriveAccountDto + */ + 'derivationPaths'?: Array; + /** + * + * @type {number} + * @memberof DeriveAccountDto + */ + 'count'?: number; +} +/** + * + * @export + * @interface DeriveAccountResponseDto + */ +export interface DeriveAccountResponseDto { + /** + * + * @type {Array} + * @memberof DeriveAccountResponseDto + */ + 'accounts': Array; +} +/** + * + * @export + * @interface EncryptionKeyDto + */ +export interface EncryptionKeyDto { + /** + * + * @type {EncryptionKeyDtoPublicKey} + * @memberof EncryptionKeyDto + */ + 'publicKey': EncryptionKeyDtoPublicKey; + /** + * + * @type {EncryptionKeyDtoData} + * @memberof EncryptionKeyDto + */ + 'data': EncryptionKeyDtoData; } /** * * @export + * @interface EncryptionKeyDtoData + */ +export interface EncryptionKeyDtoData { + /** + * + * @type {string} + * @memberof EncryptionKeyDtoData + */ + 'keyId'?: string; + /** + * + * @type {EncryptionKeyDtoDataJwk} + * @memberof EncryptionKeyDtoData + */ + 'jwk'?: EncryptionKeyDtoDataJwk; + /** + * Base64url encoded PEM public key + * @type {string} + * @memberof EncryptionKeyDtoData + */ + 'pem'?: string; +} +/** + * @type EncryptionKeyDtoDataJwk + * JWK format of the public key + * @export + */ +export type EncryptionKeyDtoDataJwk = CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf1 | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf2 | CreateClientDtoAuthLocalAllowedUsersInnerPublicKeyOneOf3 | CreateClientDtoBackupPublicKey; + +/** + * (DEPRECATED: use data.jwk instead) JWK format of the public key + * @export * @interface EncryptionKeyDtoPublicKey */ export interface EncryptionKeyDtoPublicKey { @@ -709,756 +1244,7164 @@ export interface ImportWalletDto { /** * * @export - * @interface PongDto + * @interface InitiateConnectionDto */ -export interface PongDto { +export interface InitiateConnectionDto { /** * - * @type {boolean} - * @memberof PongDto + * @type {string} + * @memberof InitiateConnectionDto */ - 'pong': boolean; + 'connectionId'?: string; + /** + * + * @type {string} + * @memberof InitiateConnectionDto + */ + 'provider': InitiateConnectionDtoProviderEnum; } + +export const InitiateConnectionDtoProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type InitiateConnectionDtoProviderEnum = typeof InitiateConnectionDtoProviderEnum[keyof typeof InitiateConnectionDtoProviderEnum]; + /** * * @export - * @interface SignRequestDto + * @interface PaginatedAccountsDto */ -export interface SignRequestDto { +export interface PaginatedAccountsDto { /** * - * @type {SignRequestDtoRequest} - * @memberof SignRequestDto + * @type {Array} + * @memberof PaginatedAccountsDto */ - 'request': SignRequestDtoRequest; + 'data': Array; + /** + * + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedAccountsDto + */ + 'page'?: PaginatedAssetsDtoPage; } -/** - * @type SignRequestDtoRequest - * @export - */ -export type SignRequestDtoRequest = SignRequestDtoRequestOneOf | SignRequestDtoRequestOneOf1 | SignRequestDtoRequestOneOf2 | SignRequestDtoRequestOneOf3 | SignRequestDtoRequestOneOf4; - /** * * @export - * @interface SignRequestDtoRequestOneOf + * @interface PaginatedAddressesDto */ -export interface SignRequestDtoRequestOneOf { +export interface PaginatedAddressesDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf + * @type {Array} + * @memberof PaginatedAddressesDto */ - 'action': SignRequestDtoRequestOneOfActionEnum; + 'data': Array; /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedAddressesDto */ - 'nonce': string; + 'page'?: PaginatedAssetsDtoPage; +} +/** + * + * @export + * @interface PaginatedAssetsDto + */ +export interface PaginatedAssetsDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf + * @type {Array} + * @memberof PaginatedAssetsDto */ - 'resourceId': string; + 'data': Array; /** * - * @type {SignRequestDtoRequestOneOfTransactionRequest} - * @memberof SignRequestDtoRequestOneOf + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedAssetsDto */ - 'transactionRequest': SignRequestDtoRequestOneOfTransactionRequest; + 'page'?: PaginatedAssetsDtoPage; } - -export const SignRequestDtoRequestOneOfActionEnum = { - SignTransaction: 'signTransaction' -} as const; - -export type SignRequestDtoRequestOneOfActionEnum = typeof SignRequestDtoRequestOneOfActionEnum[keyof typeof SignRequestDtoRequestOneOfActionEnum]; - /** * * @export - * @interface SignRequestDtoRequestOneOf1 + * @interface PaginatedAssetsDtoDataInner */ -export interface SignRequestDtoRequestOneOf1 { +export interface PaginatedAssetsDtoDataInner { /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf1 + * @memberof PaginatedAssetsDtoDataInner */ - 'action': SignRequestDtoRequestOneOf1ActionEnum; + 'assetId': string; + /** + * + * @type {any} + * @memberof PaginatedAssetsDtoDataInner + */ + 'createdAt'?: any; + /** + * + * @type {number} + * @memberof PaginatedAssetsDtoDataInner + */ + 'decimals': number | null; + /** + * + * @type {Array} + * @memberof PaginatedAssetsDtoDataInner + */ + 'externalAssets'?: Array; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf1 + * @memberof PaginatedAssetsDtoDataInner */ - 'nonce': string; + 'name': string; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf1 + * @memberof PaginatedAssetsDtoDataInner */ - 'resourceId': string; + 'networkId': string; /** * - * @type {SignRequestDtoRequestOneOf1Message} - * @memberof SignRequestDtoRequestOneOf1 + * @type {string} + * @memberof PaginatedAssetsDtoDataInner */ - 'message': SignRequestDtoRequestOneOf1Message; + 'onchainId': string | null; + /** + * + * @type {string} + * @memberof PaginatedAssetsDtoDataInner + */ + 'symbol': string | null; } - -export const SignRequestDtoRequestOneOf1ActionEnum = { - SignMessage: 'signMessage' -} as const; - -export type SignRequestDtoRequestOneOf1ActionEnum = typeof SignRequestDtoRequestOneOf1ActionEnum[keyof typeof SignRequestDtoRequestOneOf1ActionEnum]; - /** - * @type SignRequestDtoRequestOneOf1Message + * * @export + * @interface PaginatedAssetsDtoDataInnerExternalAssetsInner */ -export type SignRequestDtoRequestOneOf1Message = SignRequestDtoRequestOneOf1MessageOneOf | string; +export interface PaginatedAssetsDtoDataInnerExternalAssetsInner { + /** + * + * @type {string} + * @memberof PaginatedAssetsDtoDataInnerExternalAssetsInner + */ + 'externalId': string; + /** + * + * @type {string} + * @memberof PaginatedAssetsDtoDataInnerExternalAssetsInner + */ + 'provider': PaginatedAssetsDtoDataInnerExternalAssetsInnerProviderEnum; +} + +export const PaginatedAssetsDtoDataInnerExternalAssetsInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type PaginatedAssetsDtoDataInnerExternalAssetsInnerProviderEnum = typeof PaginatedAssetsDtoDataInnerExternalAssetsInnerProviderEnum[keyof typeof PaginatedAssetsDtoDataInnerExternalAssetsInnerProviderEnum]; /** * * @export - * @interface SignRequestDtoRequestOneOf1MessageOneOf + * @interface PaginatedAssetsDtoPage */ -export interface SignRequestDtoRequestOneOf1MessageOneOf { +export interface PaginatedAssetsDtoPage { /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf1MessageOneOf + * @type {string} + * @memberof PaginatedAssetsDtoPage */ - 'raw': any; + 'next': string | null; } /** * * @export - * @interface SignRequestDtoRequestOneOf2 + * @interface PaginatedConnectionsDto */ -export interface SignRequestDtoRequestOneOf2 { +export interface PaginatedConnectionsDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf2 + * @type {Array} + * @memberof PaginatedConnectionsDto */ - 'action': SignRequestDtoRequestOneOf2ActionEnum; + 'data': Array; /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf2 + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedConnectionsDto */ - 'nonce': string; + 'page'?: PaginatedAssetsDtoPage; +} +/** + * + * @export + * @interface PaginatedKnownDestinationsDto + */ +export interface PaginatedKnownDestinationsDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf2 + * @type {Array} + * @memberof PaginatedKnownDestinationsDto */ - 'resourceId': string; + 'data': Array; /** * - * @type {SignRequestDtoRequestOneOf2TypedData} - * @memberof SignRequestDtoRequestOneOf2 + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedKnownDestinationsDto */ - 'typedData': SignRequestDtoRequestOneOf2TypedData; + 'page'?: PaginatedAssetsDtoPage; } - -export const SignRequestDtoRequestOneOf2ActionEnum = { - SignTypedData: 'signTypedData' -} as const; - -export type SignRequestDtoRequestOneOf2ActionEnum = typeof SignRequestDtoRequestOneOf2ActionEnum[keyof typeof SignRequestDtoRequestOneOf2ActionEnum]; - /** * * @export - * @interface SignRequestDtoRequestOneOf2TypedData + * @interface PaginatedKnownDestinationsDtoDataInner */ -export interface SignRequestDtoRequestOneOf2TypedData { +export interface PaginatedKnownDestinationsDtoDataInner { /** * - * @type {SignRequestDtoRequestOneOf2TypedDataDomain} - * @memberof SignRequestDtoRequestOneOf2TypedData + * @type {string} + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'domain': SignRequestDtoRequestOneOf2TypedDataDomain; + 'clientId': string; /** * - * @type {{ [key: string]: Array; }} - * @memberof SignRequestDtoRequestOneOf2TypedData + * @type {string} + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'types': { [key: string]: Array; }; + 'connectionId': string; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf2TypedData + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'primaryType': string; + 'provider': PaginatedKnownDestinationsDtoDataInnerProviderEnum; /** * - * @type {{ [key: string]: any; }} - * @memberof SignRequestDtoRequestOneOf2TypedData + * @type {string} + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'message': { [key: string]: any; }; -} -/** - * - * @export - * @interface SignRequestDtoRequestOneOf2TypedDataDomain - */ -export interface SignRequestDtoRequestOneOf2TypedDataDomain { + 'label'?: string | null; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'name'?: string; + 'externalId': string; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'version'?: string; + 'externalClassification'?: string | null; /** * - * @type {number} - * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + * @type {string} + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'chainId'?: number; + 'address': string; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + * @type {string} + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'verifyingContract'?: any; + 'assetId'?: string | null; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + * @type {string} + * @memberof PaginatedKnownDestinationsDtoDataInner */ - 'salt'?: any; + 'networkId': string; } + +export const PaginatedKnownDestinationsDtoDataInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type PaginatedKnownDestinationsDtoDataInnerProviderEnum = typeof PaginatedKnownDestinationsDtoDataInnerProviderEnum[keyof typeof PaginatedKnownDestinationsDtoDataInnerProviderEnum]; + /** * * @export - * @interface SignRequestDtoRequestOneOf2TypedDataTypesValueInner + * @interface PaginatedNetworksDto */ -export interface SignRequestDtoRequestOneOf2TypedDataTypesValueInner { +export interface PaginatedNetworksDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf2TypedDataTypesValueInner + * @type {Array} + * @memberof PaginatedNetworksDto */ - 'name': string; + 'data': Array; /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf2TypedDataTypesValueInner + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedNetworksDto */ - 'type': string; + 'page'?: PaginatedAssetsDtoPage; } /** * * @export - * @interface SignRequestDtoRequestOneOf3 + * @interface PaginatedNetworksDtoDataInner */ -export interface SignRequestDtoRequestOneOf3 { +export interface PaginatedNetworksDtoDataInner { /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf3 + * @memberof PaginatedNetworksDtoDataInner */ - 'action': SignRequestDtoRequestOneOf3ActionEnum; + 'networkId': string; /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf3 + * @type {number} + * @memberof PaginatedNetworksDtoDataInner */ - 'nonce': string; + 'coinType': number | null; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf3 + * @memberof PaginatedNetworksDtoDataInner */ - 'resourceId': string; + 'name': string; + /** + * + * @type {Array} + * @memberof PaginatedNetworksDtoDataInner + */ + 'externalNetworks'?: Array; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOf3 + * @memberof PaginatedNetworksDtoDataInner */ - 'rawMessage': any; + 'createdAt'?: any; } - -export const SignRequestDtoRequestOneOf3ActionEnum = { - SignRaw: 'signRaw' -} as const; - -export type SignRequestDtoRequestOneOf3ActionEnum = typeof SignRequestDtoRequestOneOf3ActionEnum[keyof typeof SignRequestDtoRequestOneOf3ActionEnum]; - /** * * @export - * @interface SignRequestDtoRequestOneOf4 + * @interface PaginatedRawAccountsDto */ -export interface SignRequestDtoRequestOneOf4 { +export interface PaginatedRawAccountsDto { + /** + * + * @type {Array} + * @memberof PaginatedRawAccountsDto + */ + 'data': Array; + /** + * + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedRawAccountsDto + */ + 'page'?: PaginatedAssetsDtoPage; +} +/** + * + * @export + * @interface PaginatedRawAccountsDtoDataInner + */ +export interface PaginatedRawAccountsDtoDataInner { /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf4 + * @memberof PaginatedRawAccountsDtoDataInner */ - 'action': SignRequestDtoRequestOneOf4ActionEnum; + 'provider': PaginatedRawAccountsDtoDataInnerProviderEnum; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf4 + * @memberof PaginatedRawAccountsDtoDataInner */ - 'nonce': string; + 'externalId': string; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf4 + * @memberof PaginatedRawAccountsDtoDataInner */ - 'resourceId': string; + 'label': string; /** * - * @type {SignRequestDtoRequestOneOf4UserOperation} - * @memberof SignRequestDtoRequestOneOf4 + * @type {string} + * @memberof PaginatedRawAccountsDtoDataInner */ - 'userOperation': SignRequestDtoRequestOneOf4UserOperation; + 'subLabel'?: string; + /** + * The deposit address for the account, if there are multiple + * @type {string} + * @memberof PaginatedRawAccountsDtoDataInner + */ + 'defaultAddress'?: string; + /** + * + * @type {PaginatedRawAccountsDtoDataInnerNetwork} + * @memberof PaginatedRawAccountsDtoDataInner + */ + 'network'?: PaginatedRawAccountsDtoDataInnerNetwork | null; + /** + * The assets of the account + * @type {Array} + * @memberof PaginatedRawAccountsDtoDataInner + */ + 'assets'?: Array; } -export const SignRequestDtoRequestOneOf4ActionEnum = { - SignUserOperation: 'signUserOperation' +export const PaginatedRawAccountsDtoDataInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' } as const; -export type SignRequestDtoRequestOneOf4ActionEnum = typeof SignRequestDtoRequestOneOf4ActionEnum[keyof typeof SignRequestDtoRequestOneOf4ActionEnum]; +export type PaginatedRawAccountsDtoDataInnerProviderEnum = typeof PaginatedRawAccountsDtoDataInnerProviderEnum[keyof typeof PaginatedRawAccountsDtoDataInnerProviderEnum]; /** * * @export - * @interface SignRequestDtoRequestOneOf4UserOperation + * @interface PaginatedRawAccountsDtoDataInnerAssetsInner */ -export interface SignRequestDtoRequestOneOf4UserOperation { +export interface PaginatedRawAccountsDtoDataInnerAssetsInner { /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {PaginatedAssetsDtoDataInner} + * @memberof PaginatedRawAccountsDtoDataInnerAssetsInner */ - 'sender': any; + 'asset': PaginatedAssetsDtoDataInner; + /** + * The balance of the this asset in this account + * @type {string} + * @memberof PaginatedRawAccountsDtoDataInnerAssetsInner + */ + 'balance'?: string; +} +/** + * The network of the account + * @export + * @interface PaginatedRawAccountsDtoDataInnerNetwork + */ +export interface PaginatedRawAccountsDtoDataInnerNetwork { /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @memberof PaginatedRawAccountsDtoDataInnerNetwork */ - 'nonce': string; + 'networkId': string; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {number} + * @memberof PaginatedRawAccountsDtoDataInnerNetwork */ - 'initCode': any; + 'coinType': number | null; + /** + * + * @type {string} + * @memberof PaginatedRawAccountsDtoDataInnerNetwork + */ + 'name': string; + /** + * + * @type {Array} + * @memberof PaginatedRawAccountsDtoDataInnerNetwork + */ + 'externalNetworks'?: Array; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @memberof PaginatedRawAccountsDtoDataInnerNetwork */ - 'callData': any; + 'createdAt'?: any; +} +/** + * + * @export + * @interface PaginatedScopedSyncsDto + */ +export interface PaginatedScopedSyncsDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {Array} + * @memberof PaginatedScopedSyncsDto */ - 'callGasLimit': string; + 'data': Array; /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedScopedSyncsDto */ - 'verificationGasLimit': string; + 'page'?: PaginatedAssetsDtoPage; +} +/** + * + * @export + * @interface PaginatedSyncsDto + */ +export interface PaginatedSyncsDto { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {Array} + * @memberof PaginatedSyncsDto */ - 'preVerificationGas': string; + 'data': Array; /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedSyncsDto */ - 'maxFeePerGas': string; + 'page'?: PaginatedAssetsDtoPage; +} +/** + * + * @export + * @interface PaginatedSyncsDtoDataInner + */ +export interface PaginatedSyncsDtoDataInner { /** * * @type {string} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @memberof PaginatedSyncsDtoDataInner */ - 'maxPriorityFeePerGas': string; + 'clientId': string; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @memberof PaginatedSyncsDtoDataInner */ - 'paymasterAndData': any; + 'completedAt'?: any; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {string} + * @memberof PaginatedSyncsDtoDataInner */ - 'entryPoint': any; + 'connectionId': string; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @memberof PaginatedSyncsDtoDataInner */ - 'signature': any; + 'createdAt': any; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {PaginatedSyncsDtoDataInnerError} + * @memberof PaginatedSyncsDtoDataInner */ - 'factoryAddress': any; + 'error'?: PaginatedSyncsDtoDataInnerError; /** * - * @type {number} - * @memberof SignRequestDtoRequestOneOf4UserOperation + * @type {string} + * @memberof PaginatedSyncsDtoDataInner */ - 'chainId': number; + 'status'?: PaginatedSyncsDtoDataInnerStatusEnum; + /** + * + * @type {string} + * @memberof PaginatedSyncsDtoDataInner + */ + 'syncId': string; } -/** - * @type SignRequestDtoRequestOneOfTransactionRequest - * @export - */ -export type SignRequestDtoRequestOneOfTransactionRequest = SignRequestDtoRequestOneOfTransactionRequestOneOf | SignRequestDtoRequestOneOfTransactionRequestOneOf1; + +export const PaginatedSyncsDtoDataInnerStatusEnum = { + Processing: 'processing', + Success: 'success', + Failed: 'failed' +} as const; + +export type PaginatedSyncsDtoDataInnerStatusEnum = typeof PaginatedSyncsDtoDataInnerStatusEnum[keyof typeof PaginatedSyncsDtoDataInnerStatusEnum]; /** * * @export - * @interface SignRequestDtoRequestOneOfTransactionRequestOneOf + * @interface PaginatedSyncsDtoDataInnerError */ -export interface SignRequestDtoRequestOneOfTransactionRequestOneOf { +export interface PaginatedSyncsDtoDataInnerError { /** * - * @type {number} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @type {string} + * @memberof PaginatedSyncsDtoDataInnerError */ - 'chainId': number; + 'name'?: string; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @type {string} + * @memberof PaginatedSyncsDtoDataInnerError */ - 'from': any; + 'message'?: string; /** * - * @type {number} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @type {string} + * @memberof PaginatedSyncsDtoDataInnerError */ - 'nonce'?: number; + 'traceId'?: string; +} +/** + * + * @export + * @interface PaginatedWalletsDto + */ +export interface PaginatedWalletsDto { /** * - * @type {Array} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @type {Array} + * @memberof PaginatedWalletsDto */ - 'accessList'?: Array; + 'data': Array; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @type {PaginatedAssetsDtoPage} + * @memberof PaginatedWalletsDto */ - 'data'?: any; + 'page'?: PaginatedAssetsDtoPage; +} +/** + * + * @export + * @interface PaginatedWalletsDtoDataInner + */ +export interface PaginatedWalletsDtoDataInner { /** * - * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @type {Array} + * @memberof PaginatedWalletsDtoDataInner */ - 'gas'?: string; + 'accounts': Array; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof PaginatedWalletsDtoDataInner */ - 'maxFeePerGas'?: string; + 'clientId': string; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof PaginatedWalletsDtoDataInner */ - 'maxPriorityFeePerGas'?: string; + 'connectionId': string; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof PaginatedWalletsDtoDataInner */ - 'to'?: any | null; + 'createdAt': any; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof PaginatedWalletsDtoDataInner */ - 'type'?: SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum; + 'externalId': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInner + */ + 'label'?: string | null; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInner + */ + 'provider': PaginatedWalletsDtoDataInnerProviderEnum; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + * @memberof PaginatedWalletsDtoDataInner */ - 'value'?: any; + 'updatedAt': any; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInner + */ + 'walletId': string; } -export const SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = { - _2: '2' +export const PaginatedWalletsDtoDataInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' } as const; -export type SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = typeof SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum[keyof typeof SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum]; +export type PaginatedWalletsDtoDataInnerProviderEnum = typeof PaginatedWalletsDtoDataInnerProviderEnum[keyof typeof PaginatedWalletsDtoDataInnerProviderEnum]; /** * * @export - * @interface SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @interface PaginatedWalletsDtoDataInnerAccountsInner */ -export interface SignRequestDtoRequestOneOfTransactionRequestOneOf1 { +export interface PaginatedWalletsDtoDataInnerAccountsInner { /** * - * @type {number} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'chainId': number; + 'accountId': string; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {Array} + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'from': any; + 'addresses': Array; /** * - * @type {number} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'nonce'?: number; + 'clientId': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInner + */ + 'connectionId': string; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'data'?: any; + 'createdAt': any; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'gas'?: string; + 'externalId': string; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'gasPrice'?: string; + 'label'?: string | null; /** * * @type {string} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'type'?: SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum; + 'networkId': string; /** * - * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'to'?: any | null; + 'provider': PaginatedWalletsDtoDataInnerAccountsInnerProviderEnum; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + * @memberof PaginatedWalletsDtoDataInnerAccountsInner */ - 'value'?: any; + 'updatedAt': any; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInner + */ + 'walletId': string; } -export const SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = { - _0: '0' +export const PaginatedWalletsDtoDataInnerAccountsInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' } as const; -export type SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = typeof SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum[keyof typeof SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum]; +export type PaginatedWalletsDtoDataInnerAccountsInnerProviderEnum = typeof PaginatedWalletsDtoDataInnerAccountsInnerProviderEnum[keyof typeof PaginatedWalletsDtoDataInnerAccountsInnerProviderEnum]; /** * * @export - * @interface SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + * @interface PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner */ -export interface SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner { +export interface PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner { + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'accountId': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'address': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'addressId': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'clientId': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'connectionId': string; /** * * @type {any} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner */ - 'address': any; + 'createdAt': any; /** * - * @type {Array} - * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner */ - 'storageKeys': Array; + 'externalId': string; + /** + * + * @type {string} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'provider': PaginatedWalletsDtoDataInnerAccountsInnerAddressesInnerProviderEnum; + /** + * + * @type {any} + * @memberof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInner + */ + 'updatedAt': any; } + +export const PaginatedWalletsDtoDataInnerAccountsInnerAddressesInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type PaginatedWalletsDtoDataInnerAccountsInnerAddressesInnerProviderEnum = typeof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInnerProviderEnum[keyof typeof PaginatedWalletsDtoDataInnerAccountsInnerAddressesInnerProviderEnum]; + /** * * @export - * @interface SignatureDto + * @interface PongDto */ -export interface SignatureDto { +export interface PongDto { /** * - * @type {any} - * @memberof SignatureDto + * @type {boolean} + * @memberof PongDto */ - 'signature': any; + 'pong': boolean; } /** * * @export - * @interface WalletDto + * @interface ProviderAccountDto */ -export interface WalletDto { - /** - * - * @type {WalletDtoAccount} - * @memberof WalletDto - */ - 'account': WalletDtoAccount; +export interface ProviderAccountDto { /** * - * @type {string} - * @memberof WalletDto + * @type {PaginatedWalletsDtoDataInnerAccountsInner} + * @memberof ProviderAccountDto */ - 'backup'?: string; + 'data': PaginatedWalletsDtoDataInnerAccountsInner; +} +/** + * + * @export + * @interface ProviderConnectionDto + */ +export interface ProviderConnectionDto { /** * - * @type {string} - * @memberof WalletDto + * @type {ProviderConnectionDtoData} + * @memberof ProviderConnectionDto */ - 'keyId': string; + 'data': ProviderConnectionDtoData; } +/** + * @type ProviderConnectionDtoData + * @export + */ +export type ProviderConnectionDtoData = ProviderConnectionDtoDataOneOf | ProviderConnectionDtoDataOneOf1 | ProviderConnectionDtoDataOneOf2; + /** * * @export - * @interface WalletDtoAccount + * @interface ProviderConnectionDtoDataOneOf */ -export interface WalletDtoAccount { +export interface ProviderConnectionDtoDataOneOf { /** * * @type {string} - * @memberof WalletDtoAccount + * @memberof ProviderConnectionDtoDataOneOf */ - 'id': string; + 'clientId': string; /** * - * @type {any} - * @memberof WalletDtoAccount + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf */ - 'publicKey': any; + 'connectionId': string; /** * * @type {any} - * @memberof WalletDtoAccount + * @memberof ProviderConnectionDtoDataOneOf */ - 'address': any; + 'createdAt': any; /** * - * @type {WalletsDtoWalletsInnerOrigin} - * @memberof WalletDtoAccount + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf */ - 'origin': WalletsDtoWalletsInnerOrigin; + 'label'?: string; /** * * @type {string} - * @memberof WalletDtoAccount + * @memberof ProviderConnectionDtoDataOneOf */ - 'keyId'?: string; + 'provider': ProviderConnectionDtoDataOneOfProviderEnum; + /** + * + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf + */ + 'revokedAt'?: any; /** * * @type {string} - * @memberof WalletDtoAccount + * @memberof ProviderConnectionDtoDataOneOf */ - 'derivationPath'?: string; -} -/** - * - * @export - * @interface WalletsDto - */ -export interface WalletsDto { + 'status': ProviderConnectionDtoDataOneOfStatusEnum; /** * - * @type {Array} - * @memberof WalletsDto + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf */ - 'wallets': Array; + 'updatedAt': any; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf + */ + 'url': string; } + +export const ProviderConnectionDtoDataOneOfProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type ProviderConnectionDtoDataOneOfProviderEnum = typeof ProviderConnectionDtoDataOneOfProviderEnum[keyof typeof ProviderConnectionDtoDataOneOfProviderEnum]; +export const ProviderConnectionDtoDataOneOfStatusEnum = { + Active: 'active' +} as const; + +export type ProviderConnectionDtoDataOneOfStatusEnum = typeof ProviderConnectionDtoDataOneOfStatusEnum[keyof typeof ProviderConnectionDtoDataOneOfStatusEnum]; + /** * * @export - * @interface WalletsDtoWalletsInner + * @interface ProviderConnectionDtoDataOneOf1 */ -export interface WalletsDtoWalletsInner { +export interface ProviderConnectionDtoDataOneOf1 { /** * * @type {string} - * @memberof WalletsDtoWalletsInner + * @memberof ProviderConnectionDtoDataOneOf1 */ - 'keyId': string; + 'clientId': string; /** * * @type {string} - * @memberof WalletsDtoWalletsInner + * @memberof ProviderConnectionDtoDataOneOf1 */ - 'curve': string; + 'connectionId': string; /** * - * @type {WalletsDtoWalletsInnerKeyType} - * @memberof WalletsDtoWalletsInner + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf1 */ - 'keyType': WalletsDtoWalletsInnerKeyType; + 'createdAt': any; /** * - * @type {WalletsDtoWalletsInnerOrigin} - * @memberof WalletsDtoWalletsInner + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf1 */ - 'origin': WalletsDtoWalletsInnerOrigin; -} -/** - * @type WalletsDtoWalletsInnerKeyType - * @export - */ -export type WalletsDtoWalletsInnerKeyType = string; - -/** - * @type WalletsDtoWalletsInnerOrigin - * @export - */ -export type WalletsDtoWalletsInnerOrigin = string; - - -/** - * AccountApi - axios parameter creator - * @export + 'label'?: string; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf1 + */ + 'provider': ProviderConnectionDtoDataOneOf1ProviderEnum; + /** + * + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf1 + */ + 'revokedAt': any; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf1 + */ + 'status': ProviderConnectionDtoDataOneOf1StatusEnum; + /** + * + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf1 + */ + 'updatedAt': any; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf1 + */ + 'url'?: string; +} + +export const ProviderConnectionDtoDataOneOf1ProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type ProviderConnectionDtoDataOneOf1ProviderEnum = typeof ProviderConnectionDtoDataOneOf1ProviderEnum[keyof typeof ProviderConnectionDtoDataOneOf1ProviderEnum]; +export const ProviderConnectionDtoDataOneOf1StatusEnum = { + Revoked: 'revoked' +} as const; + +export type ProviderConnectionDtoDataOneOf1StatusEnum = typeof ProviderConnectionDtoDataOneOf1StatusEnum[keyof typeof ProviderConnectionDtoDataOneOf1StatusEnum]; + +/** + * + * @export + * @interface ProviderConnectionDtoDataOneOf2 + */ +export interface ProviderConnectionDtoDataOneOf2 { + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'clientId': string; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'connectionId': string; + /** + * + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'createdAt': any; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'label'?: string; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'provider': ProviderConnectionDtoDataOneOf2ProviderEnum; + /** + * + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'revokedAt'?: any; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'status': ProviderConnectionDtoDataOneOf2StatusEnum; + /** + * + * @type {any} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'updatedAt': any; + /** + * + * @type {string} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'url'?: string; + /** + * + * @type {CreateClientDtoBackupPublicKey} + * @memberof ProviderConnectionDtoDataOneOf2 + */ + 'encryptionPublicKey'?: CreateClientDtoBackupPublicKey; +} + +export const ProviderConnectionDtoDataOneOf2ProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type ProviderConnectionDtoDataOneOf2ProviderEnum = typeof ProviderConnectionDtoDataOneOf2ProviderEnum[keyof typeof ProviderConnectionDtoDataOneOf2ProviderEnum]; +export const ProviderConnectionDtoDataOneOf2StatusEnum = { + Pending: 'pending' +} as const; + +export type ProviderConnectionDtoDataOneOf2StatusEnum = typeof ProviderConnectionDtoDataOneOf2StatusEnum[keyof typeof ProviderConnectionDtoDataOneOf2StatusEnum]; + +/** + * + * @export + * @interface ProviderPendingConnectionDto + */ +export interface ProviderPendingConnectionDto { + /** + * + * @type {ProviderPendingConnectionDtoData} + * @memberof ProviderPendingConnectionDto + */ + 'data': ProviderPendingConnectionDtoData; +} +/** + * + * @export + * @interface ProviderPendingConnectionDtoData + */ +export interface ProviderPendingConnectionDtoData { + /** + * + * @type {string} + * @memberof ProviderPendingConnectionDtoData + */ + 'clientId': string; + /** + * + * @type {string} + * @memberof ProviderPendingConnectionDtoData + */ + 'connectionId': string; + /** + * + * @type {string} + * @memberof ProviderPendingConnectionDtoData + */ + 'provider': ProviderPendingConnectionDtoDataProviderEnum; + /** + * + * @type {string} + * @memberof ProviderPendingConnectionDtoData + */ + 'status': ProviderPendingConnectionDtoDataStatusEnum; + /** + * + * @type {any} + * @memberof ProviderPendingConnectionDtoData + */ + 'createdAt': any; + /** + * + * @type {ProviderPendingConnectionDtoDataPublicKey} + * @memberof ProviderPendingConnectionDtoData + */ + 'publicKey'?: ProviderPendingConnectionDtoDataPublicKey; + /** + * + * @type {EncryptionKeyDtoData} + * @memberof ProviderPendingConnectionDtoData + */ + 'encryptionPublicKey': EncryptionKeyDtoData; +} + +export const ProviderPendingConnectionDtoDataProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type ProviderPendingConnectionDtoDataProviderEnum = typeof ProviderPendingConnectionDtoDataProviderEnum[keyof typeof ProviderPendingConnectionDtoDataProviderEnum]; +export const ProviderPendingConnectionDtoDataStatusEnum = { + Pending: 'pending' +} as const; + +export type ProviderPendingConnectionDtoDataStatusEnum = typeof ProviderPendingConnectionDtoDataStatusEnum[keyof typeof ProviderPendingConnectionDtoDataStatusEnum]; + +/** + * + * @export + * @interface ProviderPendingConnectionDtoDataPublicKey + */ +export interface ProviderPendingConnectionDtoDataPublicKey { + /** + * + * @type {string} + * @memberof ProviderPendingConnectionDtoDataPublicKey + */ + 'keyId'?: string; + /** + * + * @type {CreateClientDtoAuthLocalAllowedUsersInnerPublicKey} + * @memberof ProviderPendingConnectionDtoDataPublicKey + */ + 'jwk'?: CreateClientDtoAuthLocalAllowedUsersInnerPublicKey; + /** + * + * @type {any} + * @memberof ProviderPendingConnectionDtoDataPublicKey + */ + 'hex'?: any; + /** + * Certificate Signing Request PEM format of RSA public key encoded as base64 + * @type {string} + * @memberof ProviderPendingConnectionDtoDataPublicKey + */ + 'csr'?: string; +} +/** + * + * @export + * @interface ProviderWalletDto + */ +export interface ProviderWalletDto { + /** + * + * @type {PaginatedWalletsDtoDataInner} + * @memberof ProviderWalletDto + */ + 'data': PaginatedWalletsDtoDataInner; +} +/** + * + * @export + * @interface ScopedSyncDto + */ +export interface ScopedSyncDto { + /** + * + * @type {ScopedSyncStartedDtoDataScopedSyncsInner} + * @memberof ScopedSyncDto + */ + 'data': ScopedSyncStartedDtoDataScopedSyncsInner; +} +/** + * + * @export + * @interface ScopedSyncStartedDto + */ +export interface ScopedSyncStartedDto { + /** + * + * @type {ScopedSyncStartedDtoData} + * @memberof ScopedSyncStartedDto + */ + 'data': ScopedSyncStartedDtoData; +} +/** + * + * @export + * @interface ScopedSyncStartedDtoData + */ +export interface ScopedSyncStartedDtoData { + /** + * + * @type {boolean} + * @memberof ScopedSyncStartedDtoData + */ + 'started': boolean; + /** + * + * @type {Array} + * @memberof ScopedSyncStartedDtoData + */ + 'scopedSyncs': Array; +} +/** + * + * @export + * @interface ScopedSyncStartedDtoDataScopedSyncsInner + */ +export interface ScopedSyncStartedDtoDataScopedSyncsInner { + /** + * + * @type {string} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'clientId': string; + /** + * + * @type {any} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'completedAt'?: any; + /** + * + * @type {string} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'connectionId': string; + /** + * + * @type {any} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'createdAt': any; + /** + * + * @type {PaginatedSyncsDtoDataInnerError} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'error'?: PaginatedSyncsDtoDataInnerError; + /** + * + * @type {string} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'status'?: ScopedSyncStartedDtoDataScopedSyncsInnerStatusEnum; + /** + * + * @type {string} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'scopedSyncId': string; + /** + * + * @type {Array} + * @memberof ScopedSyncStartedDtoDataScopedSyncsInner + */ + 'rawAccounts': Array; +} + +export const ScopedSyncStartedDtoDataScopedSyncsInnerStatusEnum = { + Processing: 'processing', + Success: 'success', + Failed: 'failed' +} as const; + +export type ScopedSyncStartedDtoDataScopedSyncsInnerStatusEnum = typeof ScopedSyncStartedDtoDataScopedSyncsInnerStatusEnum[keyof typeof ScopedSyncStartedDtoDataScopedSyncsInnerStatusEnum]; + +/** + * + * @export + * @interface SendTransferDto + */ +export interface SendTransferDto { + /** + * + * @type {SendTransferDtoSource} + * @memberof SendTransferDto + */ + 'source': SendTransferDtoSource; + /** + * + * @type {SendTransferDtoDestination} + * @memberof SendTransferDto + */ + 'destination': SendTransferDtoDestination; + /** + * + * @type {string} + * @memberof SendTransferDto + */ + 'amount': string; + /** + * @deprecated use asset instead + * @type {string} + * @memberof SendTransferDto + */ + 'assetId'?: string; + /** + * + * @type {SendTransferDtoAsset} + * @memberof SendTransferDto + */ + 'asset': SendTransferDtoAsset; + /** + * Controls how network fees are charged. Example: a request to transfer 1 ETH with networkFeeAttribution=ON_TOP would result in exactly 1 ETH received to the destination and just over 1 ETH spent by the source. Note: This property is optional and its default always depend on the underlying provider. + * @type {string} + * @memberof SendTransferDto + */ + 'networkFeeAttribution'?: SendTransferDtoNetworkFeeAttributionEnum; + /** + * + * @type {string} + * @memberof SendTransferDto + */ + 'customerRefId'?: string; + /** + * + * @type {string} + * @memberof SendTransferDto + */ + 'idempotenceId': string; + /** + * + * @type {string} + * @memberof SendTransferDto + */ + 'memo'?: string; + /** + * + * @type {string} + * @memberof SendTransferDto + */ + 'provider'?: SendTransferDtoProviderEnum; + /** + * + * @type {any} + * @memberof SendTransferDto + */ + 'providerSpecific'?: any; +} + +export const SendTransferDtoNetworkFeeAttributionEnum = { + OnTop: 'on_top', + Deduct: 'deduct' +} as const; + +export type SendTransferDtoNetworkFeeAttributionEnum = typeof SendTransferDtoNetworkFeeAttributionEnum[keyof typeof SendTransferDtoNetworkFeeAttributionEnum]; +export const SendTransferDtoProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type SendTransferDtoProviderEnum = typeof SendTransferDtoProviderEnum[keyof typeof SendTransferDtoProviderEnum]; + +/** + * The asset being transferred + * @export + * @interface SendTransferDtoAsset + */ +export interface SendTransferDtoAsset { + /** + * ID of the asset. Can be used instead of address+networkId. + * @type {string} + * @memberof SendTransferDtoAsset + */ + 'assetId'?: string; + /** + * ID of the asset on the provider. Can be used to directly specify the asset of the underlying provider. + * @type {string} + * @memberof SendTransferDtoAsset + */ + 'externalAssetId'?: string; + /** + * On-chain address of the asset. If assetId is null, then an empty address means the network Base Asset (e.g. BTC) + * @type {string} + * @memberof SendTransferDtoAsset + */ + 'address'?: string; + /** + * Network of the asset. Required if address is provided. + * @type {string} + * @memberof SendTransferDtoAsset + */ + 'networkId'?: string; +} +/** + * @type SendTransferDtoDestination + * @export + */ +export type SendTransferDtoDestination = SendTransferDtoDestinationOneOf | SendTransferDtoSource; + +/** + * + * @export + * @interface SendTransferDtoDestinationOneOf + */ +export interface SendTransferDtoDestinationOneOf { + /** + * + * @type {string} + * @memberof SendTransferDtoDestinationOneOf + */ + 'address': string; +} +/** + * + * @export + * @interface SendTransferDtoSource + */ +export interface SendTransferDtoSource { + /** + * + * @type {string} + * @memberof SendTransferDtoSource + */ + 'id': string; + /** + * + * @type {string} + * @memberof SendTransferDtoSource + */ + 'type': SendTransferDtoSourceTypeEnum; +} + +export const SendTransferDtoSourceTypeEnum = { + Wallet: 'wallet', + Account: 'account', + Address: 'address' +} as const; + +export type SendTransferDtoSourceTypeEnum = typeof SendTransferDtoSourceTypeEnum[keyof typeof SendTransferDtoSourceTypeEnum]; + +/** + * + * @export + * @interface SignRequestDto + */ +export interface SignRequestDto { + /** + * + * @type {SignRequestDtoRequest} + * @memberof SignRequestDto + */ + 'request': SignRequestDtoRequest; +} +/** + * @type SignRequestDtoRequest + * @export + */ +export type SignRequestDtoRequest = SignRequestDtoRequestOneOf | SignRequestDtoRequestOneOf1 | SignRequestDtoRequestOneOf2 | SignRequestDtoRequestOneOf3 | SignRequestDtoRequestOneOf4; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOf + */ +export interface SignRequestDtoRequestOneOf { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf + */ + 'action': SignRequestDtoRequestOneOfActionEnum; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf + */ + 'nonce': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf + */ + 'resourceId': string; + /** + * + * @type {SignRequestDtoRequestOneOfTransactionRequest} + * @memberof SignRequestDtoRequestOneOf + */ + 'transactionRequest': SignRequestDtoRequestOneOfTransactionRequest; +} + +export const SignRequestDtoRequestOneOfActionEnum = { + SignTransaction: 'signTransaction' +} as const; + +export type SignRequestDtoRequestOneOfActionEnum = typeof SignRequestDtoRequestOneOfActionEnum[keyof typeof SignRequestDtoRequestOneOfActionEnum]; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOf1 + */ +export interface SignRequestDtoRequestOneOf1 { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf1 + */ + 'action': SignRequestDtoRequestOneOf1ActionEnum; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf1 + */ + 'nonce': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf1 + */ + 'resourceId': string; + /** + * + * @type {SignRequestDtoRequestOneOf1Message} + * @memberof SignRequestDtoRequestOneOf1 + */ + 'message': SignRequestDtoRequestOneOf1Message; +} + +export const SignRequestDtoRequestOneOf1ActionEnum = { + SignMessage: 'signMessage' +} as const; + +export type SignRequestDtoRequestOneOf1ActionEnum = typeof SignRequestDtoRequestOneOf1ActionEnum[keyof typeof SignRequestDtoRequestOneOf1ActionEnum]; + +/** + * @type SignRequestDtoRequestOneOf1Message + * @export + */ +export type SignRequestDtoRequestOneOf1Message = SignRequestDtoRequestOneOf1MessageOneOf | string; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOf1MessageOneOf + */ +export interface SignRequestDtoRequestOneOf1MessageOneOf { + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf1MessageOneOf + */ + 'raw': any; +} +/** + * + * @export + * @interface SignRequestDtoRequestOneOf2 + */ +export interface SignRequestDtoRequestOneOf2 { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2 + */ + 'action': SignRequestDtoRequestOneOf2ActionEnum; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2 + */ + 'nonce': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2 + */ + 'resourceId': string; + /** + * + * @type {SignRequestDtoRequestOneOf2TypedData} + * @memberof SignRequestDtoRequestOneOf2 + */ + 'typedData': SignRequestDtoRequestOneOf2TypedData; +} + +export const SignRequestDtoRequestOneOf2ActionEnum = { + SignTypedData: 'signTypedData' +} as const; + +export type SignRequestDtoRequestOneOf2ActionEnum = typeof SignRequestDtoRequestOneOf2ActionEnum[keyof typeof SignRequestDtoRequestOneOf2ActionEnum]; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOf2TypedData + */ +export interface SignRequestDtoRequestOneOf2TypedData { + /** + * + * @type {SignRequestDtoRequestOneOf2TypedDataDomain} + * @memberof SignRequestDtoRequestOneOf2TypedData + */ + 'domain': SignRequestDtoRequestOneOf2TypedDataDomain; + /** + * + * @type {{ [key: string]: Array; }} + * @memberof SignRequestDtoRequestOneOf2TypedData + */ + 'types': { [key: string]: Array; }; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2TypedData + */ + 'primaryType': string; + /** + * + * @type {{ [key: string]: any; }} + * @memberof SignRequestDtoRequestOneOf2TypedData + */ + 'message': { [key: string]: any; }; +} +/** + * + * @export + * @interface SignRequestDtoRequestOneOf2TypedDataDomain + */ +export interface SignRequestDtoRequestOneOf2TypedDataDomain { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + */ + 'name'?: string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + */ + 'version'?: string; + /** + * + * @type {number} + * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + */ + 'chainId'?: number; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + */ + 'verifyingContract'?: any; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf2TypedDataDomain + */ + 'salt'?: any; +} +/** + * + * @export + * @interface SignRequestDtoRequestOneOf2TypedDataTypesValueInner + */ +export interface SignRequestDtoRequestOneOf2TypedDataTypesValueInner { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2TypedDataTypesValueInner + */ + 'name': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf2TypedDataTypesValueInner + */ + 'type': string; +} +/** + * + * @export + * @interface SignRequestDtoRequestOneOf3 + */ +export interface SignRequestDtoRequestOneOf3 { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf3 + */ + 'action': SignRequestDtoRequestOneOf3ActionEnum; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf3 + */ + 'nonce': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf3 + */ + 'resourceId': string; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf3 + */ + 'rawMessage': any; +} + +export const SignRequestDtoRequestOneOf3ActionEnum = { + SignRaw: 'signRaw' +} as const; + +export type SignRequestDtoRequestOneOf3ActionEnum = typeof SignRequestDtoRequestOneOf3ActionEnum[keyof typeof SignRequestDtoRequestOneOf3ActionEnum]; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOf4 + */ +export interface SignRequestDtoRequestOneOf4 { + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4 + */ + 'action': SignRequestDtoRequestOneOf4ActionEnum; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4 + */ + 'nonce': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4 + */ + 'resourceId': string; + /** + * + * @type {SignRequestDtoRequestOneOf4UserOperation} + * @memberof SignRequestDtoRequestOneOf4 + */ + 'userOperation': SignRequestDtoRequestOneOf4UserOperation; +} + +export const SignRequestDtoRequestOneOf4ActionEnum = { + SignUserOperation: 'signUserOperation' +} as const; + +export type SignRequestDtoRequestOneOf4ActionEnum = typeof SignRequestDtoRequestOneOf4ActionEnum[keyof typeof SignRequestDtoRequestOneOf4ActionEnum]; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOf4UserOperation + */ +export interface SignRequestDtoRequestOneOf4UserOperation { + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'sender': any; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'nonce': string; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'initCode': any; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'callData': any; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'callGasLimit': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'verificationGasLimit': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'preVerificationGas': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'maxFeePerGas': string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'maxPriorityFeePerGas': string; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'paymasterAndData': any; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'entryPoint': any; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'signature': any; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'factoryAddress': any; + /** + * + * @type {number} + * @memberof SignRequestDtoRequestOneOf4UserOperation + */ + 'chainId': number; +} +/** + * @type SignRequestDtoRequestOneOfTransactionRequest + * @export + */ +export type SignRequestDtoRequestOneOfTransactionRequest = SignRequestDtoRequestOneOfTransactionRequestOneOf | SignRequestDtoRequestOneOfTransactionRequestOneOf1; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOfTransactionRequestOneOf + */ +export interface SignRequestDtoRequestOneOfTransactionRequestOneOf { + /** + * + * @type {number} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'chainId': number; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'from': any; + /** + * + * @type {number} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'nonce'?: number; + /** + * + * @type {Array} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'accessList'?: Array; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'data'?: any; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'gas'?: string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'maxFeePerGas'?: string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'maxPriorityFeePerGas'?: string; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'to'?: any | null; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'type'?: SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf + */ + 'value'?: any; +} + +export const SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = { + _2: '2' +} as const; + +export type SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum = typeof SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum[keyof typeof SignRequestDtoRequestOneOfTransactionRequestOneOfTypeEnum]; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ +export interface SignRequestDtoRequestOneOfTransactionRequestOneOf1 { + /** + * + * @type {number} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'chainId': number; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'from': any; + /** + * + * @type {number} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'nonce'?: number; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'data'?: any; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'gas'?: string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'gasPrice'?: string; + /** + * + * @type {string} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'type'?: SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'to'?: any | null; + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOf1 + */ + 'value'?: any; +} + +export const SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = { + _0: '0' +} as const; + +export type SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum = typeof SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum[keyof typeof SignRequestDtoRequestOneOfTransactionRequestOneOf1TypeEnum]; + +/** + * + * @export + * @interface SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + */ +export interface SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner { + /** + * + * @type {any} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + */ + 'address': any; + /** + * + * @type {Array} + * @memberof SignRequestDtoRequestOneOfTransactionRequestOneOfAccessListInner + */ + 'storageKeys': Array; +} +/** + * + * @export + * @interface SignatureDto + */ +export interface SignatureDto { + /** + * + * @type {any} + * @memberof SignatureDto + */ + 'signature': any; +} +/** + * + * @export + * @interface StartScopedSyncDto + */ +export interface StartScopedSyncDto { + /** + * The connection to sync. + * @type {string} + * @memberof StartScopedSyncDto + */ + 'connectionId': string; + /** + * The accounts to sync. + * @type {Array} + * @memberof StartScopedSyncDto + */ + 'rawAccounts': Array; +} +/** + * + * @export + * @interface StartScopedSyncDtoRawAccountsInner + */ +export interface StartScopedSyncDtoRawAccountsInner { + /** + * + * @type {string} + * @memberof StartScopedSyncDtoRawAccountsInner + */ + 'provider': StartScopedSyncDtoRawAccountsInnerProviderEnum; + /** + * + * @type {string} + * @memberof StartScopedSyncDtoRawAccountsInner + */ + 'externalId': string; +} + +export const StartScopedSyncDtoRawAccountsInnerProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type StartScopedSyncDtoRawAccountsInnerProviderEnum = typeof StartScopedSyncDtoRawAccountsInnerProviderEnum[keyof typeof StartScopedSyncDtoRawAccountsInnerProviderEnum]; + +/** + * + * @export + * @interface TransferDto + */ +export interface TransferDto { + /** + * + * @type {TransferDtoData} + * @memberof TransferDto + */ + 'data': TransferDtoData; +} +/** + * + * @export + * @interface TransferDtoData + */ +export interface TransferDtoData { + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'assetId': string; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'clientId': string; + /** + * + * @type {any} + * @memberof TransferDtoData + */ + 'createdAt': any; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'customerRefId': string | null; + /** + * + * @type {SendTransferDtoDestination} + * @memberof TransferDtoData + */ + 'destination': SendTransferDtoDestination; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'externalId': string; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'externalStatus': string | null; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'grossAmount': string; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'idempotenceId': string | null; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'connectionId': string; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'memo': string | null; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'networkFeeAttribution': TransferDtoDataNetworkFeeAttributionEnum; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'provider': TransferDtoDataProviderEnum; + /** + * + * @type {any} + * @memberof TransferDtoData + */ + 'providerSpecific': any | null; + /** + * + * @type {SendTransferDtoSource} + * @memberof TransferDtoData + */ + 'source': SendTransferDtoSource; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'status'?: TransferDtoDataStatusEnum; + /** + * + * @type {string} + * @memberof TransferDtoData + */ + 'transferId': string; +} + +export const TransferDtoDataNetworkFeeAttributionEnum = { + OnTop: 'on_top', + Deduct: 'deduct' +} as const; + +export type TransferDtoDataNetworkFeeAttributionEnum = typeof TransferDtoDataNetworkFeeAttributionEnum[keyof typeof TransferDtoDataNetworkFeeAttributionEnum]; +export const TransferDtoDataProviderEnum = { + Anchorage: 'anchorage', + Fireblocks: 'fireblocks', + Bitgo: 'bitgo' +} as const; + +export type TransferDtoDataProviderEnum = typeof TransferDtoDataProviderEnum[keyof typeof TransferDtoDataProviderEnum]; +export const TransferDtoDataStatusEnum = { + Processing: 'processing', + Success: 'success', + Failed: 'failed' +} as const; + +export type TransferDtoDataStatusEnum = typeof TransferDtoDataStatusEnum[keyof typeof TransferDtoDataStatusEnum]; + +/** + * + * @export + * @interface UpdateConnectionDto + */ +export interface UpdateConnectionDto { + /** + * + * @type {any} + * @memberof UpdateConnectionDto + */ + 'credentials'?: any | null; + /** + * RSA encrypted JSON string of the credentials + * @type {string} + * @memberof UpdateConnectionDto + */ + 'encryptedCredentials'?: string; + /** + * + * @type {string} + * @memberof UpdateConnectionDto + */ + 'label'?: string; + /** + * + * @type {string} + * @memberof UpdateConnectionDto + */ + 'status'?: UpdateConnectionDtoStatusEnum; + /** + * + * @type {any} + * @memberof UpdateConnectionDto + */ + 'updatedAt'?: any; + /** + * + * @type {string} + * @memberof UpdateConnectionDto + */ + 'url'?: string; +} + +export const UpdateConnectionDtoStatusEnum = { + Pending: 'pending', + Active: 'active', + Revoked: 'revoked' +} as const; + +export type UpdateConnectionDtoStatusEnum = typeof UpdateConnectionDtoStatusEnum[keyof typeof UpdateConnectionDtoStatusEnum]; + +/** + * + * @export + * @interface WalletDto + */ +export interface WalletDto { + /** + * + * @type {WalletDtoAccount} + * @memberof WalletDto + */ + 'account': WalletDtoAccount; + /** + * + * @type {string} + * @memberof WalletDto + */ + 'backup'?: string; + /** + * + * @type {string} + * @memberof WalletDto + */ + 'keyId': string; +} +/** + * + * @export + * @interface WalletDtoAccount + */ +export interface WalletDtoAccount { + /** + * + * @type {string} + * @memberof WalletDtoAccount + */ + 'id': string; + /** + * + * @type {any} + * @memberof WalletDtoAccount + */ + 'publicKey': any; + /** + * + * @type {any} + * @memberof WalletDtoAccount + */ + 'address': any; + /** + * + * @type {WalletsDtoWalletsInnerOrigin} + * @memberof WalletDtoAccount + */ + 'origin': WalletsDtoWalletsInnerOrigin; + /** + * + * @type {string} + * @memberof WalletDtoAccount + */ + 'keyId'?: string; + /** + * + * @type {string} + * @memberof WalletDtoAccount + */ + 'derivationPath'?: string; +} +/** + * + * @export + * @interface WalletsDto + */ +export interface WalletsDto { + /** + * + * @type {Array} + * @memberof WalletsDto + */ + 'wallets': Array; +} +/** + * + * @export + * @interface WalletsDtoWalletsInner + */ +export interface WalletsDtoWalletsInner { + /** + * + * @type {string} + * @memberof WalletsDtoWalletsInner + */ + 'keyId': string; + /** + * + * @type {string} + * @memberof WalletsDtoWalletsInner + */ + 'curve': string; + /** + * + * @type {WalletsDtoWalletsInnerKeyType} + * @memberof WalletsDtoWalletsInner + */ + 'keyType': WalletsDtoWalletsInnerKeyType; + /** + * + * @type {WalletsDtoWalletsInnerOrigin} + * @memberof WalletsDtoWalletsInner + */ + 'origin': WalletsDtoWalletsInnerOrigin; +} +/** + * @type WalletsDtoWalletsInnerKeyType + * @export + */ +export type WalletsDtoWalletsInnerKeyType = string; + +/** + * @type WalletsDtoWalletsInnerOrigin + * @export + */ +export type WalletsDtoWalletsInnerOrigin = string; + + +/** + * AccountApi - axios parameter creator + * @export + */ +export const AccountApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Add a new account to a wallet + * @param {string} xClientId + * @param {DeriveAccountDto} deriveAccountDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + derive: async (xClientId: string, deriveAccountDto: DeriveAccountDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('derive', 'xClientId', xClientId) + // verify required parameter 'deriveAccountDto' is not null or undefined + assertParamExists('derive', 'deriveAccountDto', deriveAccountDto) + const localVarPath = `/v1/accounts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deriveAccountDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Imports an account + * @param {string} xClientId + * @param {ImportPrivateKeyDto} importPrivateKeyDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + importPrivateKey: async (xClientId: string, importPrivateKeyDto: ImportPrivateKeyDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('importPrivateKey', 'xClientId', xClientId) + // verify required parameter 'importPrivateKeyDto' is not null or undefined + assertParamExists('importPrivateKey', 'importPrivateKeyDto', importPrivateKeyDto) + const localVarPath = `/v1/accounts/import`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(importPrivateKeyDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Lists the client accounts + * @param {string} xClientId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/accounts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AccountApi - functional programming interface + * @export + */ +export const AccountApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AccountApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Add a new account to a wallet + * @param {string} xClientId + * @param {DeriveAccountDto} deriveAccountDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async derive(xClientId: string, deriveAccountDto: DeriveAccountDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.derive(xClientId, deriveAccountDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AccountApi.derive']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Imports an account + * @param {string} xClientId + * @param {ImportPrivateKeyDto} importPrivateKeyDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async importPrivateKey(xClientId: string, importPrivateKeyDto: ImportPrivateKeyDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.importPrivateKey(xClientId, importPrivateKeyDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AccountApi.importPrivateKey']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Lists the client accounts + * @param {string} xClientId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AccountApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * AccountApi - factory interface + * @export + */ +export const AccountApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AccountApiFp(configuration) + return { + /** + * + * @summary Add a new account to a wallet + * @param {AccountApiDeriveRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + derive(requestParameters: AccountApiDeriveRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.derive(requestParameters.xClientId, requestParameters.deriveAccountDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Imports an account + * @param {AccountApiImportPrivateKeyRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + importPrivateKey(requestParameters: AccountApiImportPrivateKeyRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.importPrivateKey(requestParameters.xClientId, requestParameters.importPrivateKeyDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Lists the client accounts + * @param {AccountApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: AccountApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for derive operation in AccountApi. + * @export + * @interface AccountApiDeriveRequest + */ +export interface AccountApiDeriveRequest { + /** + * + * @type {string} + * @memberof AccountApiDerive + */ + readonly xClientId: string + + /** + * + * @type {DeriveAccountDto} + * @memberof AccountApiDerive + */ + readonly deriveAccountDto: DeriveAccountDto + + /** + * + * @type {string} + * @memberof AccountApiDerive + */ + readonly authorization?: string +} + +/** + * Request parameters for importPrivateKey operation in AccountApi. + * @export + * @interface AccountApiImportPrivateKeyRequest + */ +export interface AccountApiImportPrivateKeyRequest { + /** + * + * @type {string} + * @memberof AccountApiImportPrivateKey + */ + readonly xClientId: string + + /** + * + * @type {ImportPrivateKeyDto} + * @memberof AccountApiImportPrivateKey + */ + readonly importPrivateKeyDto: ImportPrivateKeyDto + + /** + * + * @type {string} + * @memberof AccountApiImportPrivateKey + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in AccountApi. + * @export + * @interface AccountApiListRequest + */ +export interface AccountApiListRequest { + /** + * + * @type {string} + * @memberof AccountApiList + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof AccountApiList + */ + readonly authorization?: string +} + +/** + * AccountApi - object-oriented interface + * @export + * @class AccountApi + * @extends {BaseAPI} + */ +export class AccountApi extends BaseAPI { + /** + * + * @summary Add a new account to a wallet + * @param {AccountApiDeriveRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountApi + */ + public derive(requestParameters: AccountApiDeriveRequest, options?: RawAxiosRequestConfig) { + return AccountApiFp(this.configuration).derive(requestParameters.xClientId, requestParameters.deriveAccountDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Imports an account + * @param {AccountApiImportPrivateKeyRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountApi + */ + public importPrivateKey(requestParameters: AccountApiImportPrivateKeyRequest, options?: RawAxiosRequestConfig) { + return AccountApiFp(this.configuration).importPrivateKey(requestParameters.xClientId, requestParameters.importPrivateKeyDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Lists the client accounts + * @param {AccountApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountApi + */ + public list(requestParameters: AccountApiListRequest, options?: RawAxiosRequestConfig) { + return AccountApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ApplicationApi - axios parameter creator + * @export + */ +export const ApplicationApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ping: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/ping`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ApplicationApi - functional programming interface + * @export + */ +export const ApplicationApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ApplicationApiAxiosParamCreator(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async ping(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.ping(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ApplicationApi.ping']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ApplicationApi - factory interface + * @export + */ +export const ApplicationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ApplicationApiFp(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ping(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.ping(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * ApplicationApi - object-oriented interface + * @export + * @class ApplicationApi + * @extends {BaseAPI} + */ +export class ApplicationApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApplicationApi + */ + public ping(options?: RawAxiosRequestConfig) { + return ApplicationApiFp(this.configuration).ping(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ClientApi - axios parameter creator + * @export + */ +export const ClientApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Creates a new client + * @param {string} xApiKey + * @param {CreateClientDto} createClientDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + create: async (xApiKey: string, createClientDto: CreateClientDto, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xApiKey' is not null or undefined + assertParamExists('create', 'xApiKey', xApiKey) + // verify required parameter 'createClientDto' is not null or undefined + assertParamExists('create', 'createClientDto', createClientDto) + const localVarPath = `/v1/clients`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Admin-API-Key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + if (xApiKey != null) { + localVarHeaderParameter['x-api-key'] = String(xApiKey); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createClientDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ClientApi - functional programming interface + * @export + */ +export const ClientApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ClientApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Creates a new client + * @param {string} xApiKey + * @param {CreateClientDto} createClientDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async create(xApiKey: string, createClientDto: CreateClientDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.create(xApiKey, createClientDto, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ClientApi.create']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ClientApi - factory interface + * @export + */ +export const ClientApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ClientApiFp(configuration) + return { + /** + * + * @summary Creates a new client + * @param {ClientApiCreateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + create(requestParameters: ClientApiCreateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.create(requestParameters.xApiKey, requestParameters.createClientDto, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for create operation in ClientApi. + * @export + * @interface ClientApiCreateRequest + */ +export interface ClientApiCreateRequest { + /** + * + * @type {string} + * @memberof ClientApiCreate + */ + readonly xApiKey: string + + /** + * + * @type {CreateClientDto} + * @memberof ClientApiCreate + */ + readonly createClientDto: CreateClientDto +} + +/** + * ClientApi - object-oriented interface + * @export + * @class ClientApi + * @extends {BaseAPI} + */ +export class ClientApi extends BaseAPI { + /** + * + * @summary Creates a new client + * @param {ClientApiCreateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ClientApi + */ + public create(requestParameters: ClientApiCreateRequest, options?: RawAxiosRequestConfig) { + return ClientApiFp(this.configuration).create(requestParameters.xApiKey, requestParameters.createClientDto, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * EncryptionKeyApi - axios parameter creator + * @export + */ +export const EncryptionKeyApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information + * @param {string} xClientId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + generate: async (xClientId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('generate', 'xClientId', xClientId) + const localVarPath = `/v1/encryption-keys`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * EncryptionKeyApi - functional programming interface + * @export + */ +export const EncryptionKeyApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = EncryptionKeyApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information + * @param {string} xClientId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async generate(xClientId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.generate(xClientId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EncryptionKeyApi.generate']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * EncryptionKeyApi - factory interface + * @export + */ +export const EncryptionKeyApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = EncryptionKeyApiFp(configuration) + return { + /** + * + * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information + * @param {EncryptionKeyApiGenerateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + generate(requestParameters: EncryptionKeyApiGenerateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.generate(requestParameters.xClientId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for generate operation in EncryptionKeyApi. + * @export + * @interface EncryptionKeyApiGenerateRequest + */ +export interface EncryptionKeyApiGenerateRequest { + /** + * + * @type {string} + * @memberof EncryptionKeyApiGenerate + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof EncryptionKeyApiGenerate + */ + readonly authorization?: string +} + +/** + * EncryptionKeyApi - object-oriented interface + * @export + * @class EncryptionKeyApi + * @extends {BaseAPI} + */ +export class EncryptionKeyApi extends BaseAPI { + /** + * + * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information + * @param {EncryptionKeyApiGenerateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EncryptionKeyApi + */ + public generate(requestParameters: EncryptionKeyApiGenerateRequest, options?: RawAxiosRequestConfig) { + return EncryptionKeyApiFp(this.configuration).generate(requestParameters.xClientId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderAccountApi - axios parameter creator + * @export + */ +export const ProviderAccountApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Get a specific account by ID + * @param {string} xClientId + * @param {string} accountId The ID of the account to retrieve + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById: async (xClientId: string, accountId: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('getById', 'xClientId', xClientId) + // verify required parameter 'accountId' is not null or undefined + assertParamExists('getById', 'accountId', accountId) + const localVarPath = `/v1/provider/accounts/{accountId}` + .replace(`{${"accountId"}}`, encodeURIComponent(String(accountId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary List the client accounts + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/accounts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary List addresses for a specific account + * @param {string} xClientId + * @param {string} accountId The ID of the account to retrieve addresses for + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listAddresses: async (xClientId: string, accountId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('listAddresses', 'xClientId', xClientId) + // verify required parameter 'accountId' is not null or undefined + assertParamExists('listAddresses', 'accountId', accountId) + const localVarPath = `/v1/provider/accounts/{accountId}/addresses` + .replace(`{${"accountId"}}`, encodeURIComponent(String(accountId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary List the provider accounts in raw form, used to populate which accounts to connect + * @param {string} xClientId + * @param {string} [namePrefix] Filter accounts by name prefix + * @param {string} [nameSuffix] Filter accounts by name suffix + * @param {string} [networkId] Filter accounts by network ID + * @param {string} [assetId] Filter accounts by asset ID + * @param {boolean} [includeAddress] Include address information in the response + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listRaw: async (xClientId: string, namePrefix?: string, nameSuffix?: string, networkId?: string, assetId?: string, includeAddress?: boolean, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('listRaw', 'xClientId', xClientId) + const localVarPath = `/v1/provider/accounts/raw`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (namePrefix !== undefined) { + localVarQueryParameter['namePrefix'] = namePrefix; + } + + if (nameSuffix !== undefined) { + localVarQueryParameter['nameSuffix'] = nameSuffix; + } + + if (networkId !== undefined) { + localVarQueryParameter['networkId'] = networkId; + } + + if (assetId !== undefined) { + localVarQueryParameter['assetId'] = assetId; + } + + if (includeAddress !== undefined) { + localVarQueryParameter['includeAddress'] = includeAddress; + } + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderAccountApi - functional programming interface + * @export + */ +export const ProviderAccountApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderAccountApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Get a specific account by ID + * @param {string} xClientId + * @param {string} accountId The ID of the account to retrieve + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getById(xClientId: string, accountId: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getById(xClientId, accountId, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAccountApi.getById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary List the client accounts + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAccountApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary List addresses for a specific account + * @param {string} xClientId + * @param {string} accountId The ID of the account to retrieve addresses for + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listAddresses(xClientId: string, accountId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listAddresses(xClientId, accountId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAccountApi.listAddresses']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary List the provider accounts in raw form, used to populate which accounts to connect + * @param {string} xClientId + * @param {string} [namePrefix] Filter accounts by name prefix + * @param {string} [nameSuffix] Filter accounts by name suffix + * @param {string} [networkId] Filter accounts by network ID + * @param {string} [assetId] Filter accounts by asset ID + * @param {boolean} [includeAddress] Include address information in the response + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listRaw(xClientId: string, namePrefix?: string, nameSuffix?: string, networkId?: string, assetId?: string, includeAddress?: boolean, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listRaw(xClientId, namePrefix, nameSuffix, networkId, assetId, includeAddress, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAccountApi.listRaw']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderAccountApi - factory interface + * @export + */ +export const ProviderAccountApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderAccountApiFp(configuration) + return { + /** + * + * @summary Get a specific account by ID + * @param {ProviderAccountApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById(requestParameters: ProviderAccountApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.xClientId, requestParameters.accountId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary List the client accounts + * @param {ProviderAccountApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderAccountApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary List addresses for a specific account + * @param {ProviderAccountApiListAddressesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listAddresses(requestParameters: ProviderAccountApiListAddressesRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listAddresses(requestParameters.xClientId, requestParameters.accountId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary List the provider accounts in raw form, used to populate which accounts to connect + * @param {ProviderAccountApiListRawRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listRaw(requestParameters: ProviderAccountApiListRawRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listRaw(requestParameters.xClientId, requestParameters.namePrefix, requestParameters.nameSuffix, requestParameters.networkId, requestParameters.assetId, requestParameters.includeAddress, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getById operation in ProviderAccountApi. + * @export + * @interface ProviderAccountApiGetByIdRequest + */ +export interface ProviderAccountApiGetByIdRequest { + /** + * + * @type {string} + * @memberof ProviderAccountApiGetById + */ + readonly xClientId: string + + /** + * The ID of the account to retrieve + * @type {string} + * @memberof ProviderAccountApiGetById + */ + readonly accountId: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderAccountApiGetById + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderAccountApiGetById + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in ProviderAccountApi. + * @export + * @interface ProviderAccountApiListRequest + */ +export interface ProviderAccountApiListRequest { + /** + * + * @type {string} + * @memberof ProviderAccountApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderAccountApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderAccountApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderAccountApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderAccountApiList + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderAccountApiList + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderAccountApiList + */ + readonly authorization?: string +} + +/** + * Request parameters for listAddresses operation in ProviderAccountApi. + * @export + * @interface ProviderAccountApiListAddressesRequest + */ +export interface ProviderAccountApiListAddressesRequest { + /** + * + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly xClientId: string + + /** + * The ID of the account to retrieve addresses for + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly accountId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderAccountApiListAddresses + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderAccountApiListAddresses + */ + readonly authorization?: string +} + +/** + * Request parameters for listRaw operation in ProviderAccountApi. + * @export + * @interface ProviderAccountApiListRawRequest + */ +export interface ProviderAccountApiListRawRequest { + /** + * + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly xClientId: string + + /** + * Filter accounts by name prefix + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly namePrefix?: string + + /** + * Filter accounts by name suffix + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly nameSuffix?: string + + /** + * Filter accounts by network ID + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly networkId?: string + + /** + * Filter accounts by asset ID + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly assetId?: string + + /** + * Include address information in the response + * @type {boolean} + * @memberof ProviderAccountApiListRaw + */ + readonly includeAddress?: boolean + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderAccountApiListRaw + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderAccountApiListRaw + */ + readonly authorization?: string +} + +/** + * ProviderAccountApi - object-oriented interface + * @export + * @class ProviderAccountApi + * @extends {BaseAPI} + */ +export class ProviderAccountApi extends BaseAPI { + /** + * + * @summary Get a specific account by ID + * @param {ProviderAccountApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAccountApi + */ + public getById(requestParameters: ProviderAccountApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return ProviderAccountApiFp(this.configuration).getById(requestParameters.xClientId, requestParameters.accountId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary List the client accounts + * @param {ProviderAccountApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAccountApi + */ + public list(requestParameters: ProviderAccountApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderAccountApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary List addresses for a specific account + * @param {ProviderAccountApiListAddressesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAccountApi + */ + public listAddresses(requestParameters: ProviderAccountApiListAddressesRequest, options?: RawAxiosRequestConfig) { + return ProviderAccountApiFp(this.configuration).listAddresses(requestParameters.xClientId, requestParameters.accountId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary List the provider accounts in raw form, used to populate which accounts to connect + * @param {ProviderAccountApiListRawRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAccountApi + */ + public listRaw(requestParameters: ProviderAccountApiListRawRequest, options?: RawAxiosRequestConfig) { + return ProviderAccountApiFp(this.configuration).listRaw(requestParameters.xClientId, requestParameters.namePrefix, requestParameters.nameSuffix, requestParameters.networkId, requestParameters.assetId, requestParameters.includeAddress, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderAddressApi - axios parameter creator + * @export + */ +export const ProviderAddressApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Get a specific address by ID + * @param {string} xClientId + * @param {string} addressId The ID of the address to retrieve + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById: async (xClientId: string, addressId: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('getById', 'xClientId', xClientId) + // verify required parameter 'addressId' is not null or undefined + assertParamExists('getById', 'addressId', addressId) + const localVarPath = `/v1/provider/addresses/{addressId}` + .replace(`{${"addressId"}}`, encodeURIComponent(String(addressId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary List the client addresss + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/addresses`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderAddressApi - functional programming interface + * @export + */ +export const ProviderAddressApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderAddressApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Get a specific address by ID + * @param {string} xClientId + * @param {string} addressId The ID of the address to retrieve + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getById(xClientId: string, addressId: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getById(xClientId, addressId, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAddressApi.getById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary List the client addresss + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAddressApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderAddressApi - factory interface + * @export + */ +export const ProviderAddressApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderAddressApiFp(configuration) + return { + /** + * + * @summary Get a specific address by ID + * @param {ProviderAddressApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById(requestParameters: ProviderAddressApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.xClientId, requestParameters.addressId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary List the client addresss + * @param {ProviderAddressApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderAddressApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getById operation in ProviderAddressApi. + * @export + * @interface ProviderAddressApiGetByIdRequest + */ +export interface ProviderAddressApiGetByIdRequest { + /** + * + * @type {string} + * @memberof ProviderAddressApiGetById + */ + readonly xClientId: string + + /** + * The ID of the address to retrieve + * @type {string} + * @memberof ProviderAddressApiGetById + */ + readonly addressId: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderAddressApiGetById + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderAddressApiGetById + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in ProviderAddressApi. + * @export + * @interface ProviderAddressApiListRequest + */ +export interface ProviderAddressApiListRequest { + /** + * + * @type {string} + * @memberof ProviderAddressApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderAddressApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderAddressApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderAddressApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderAddressApiList + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderAddressApiList + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderAddressApiList + */ + readonly authorization?: string +} + +/** + * ProviderAddressApi - object-oriented interface + * @export + * @class ProviderAddressApi + * @extends {BaseAPI} + */ +export class ProviderAddressApi extends BaseAPI { + /** + * + * @summary Get a specific address by ID + * @param {ProviderAddressApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAddressApi + */ + public getById(requestParameters: ProviderAddressApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return ProviderAddressApiFp(this.configuration).getById(requestParameters.xClientId, requestParameters.addressId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary List the client addresss + * @param {ProviderAddressApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAddressApi + */ + public list(requestParameters: ProviderAddressApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderAddressApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderAssetApi - axios parameter creator + * @export + */ +export const ProviderAssetApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * This endpoint retrieves a list of all available assets. + * @summary Retrieve all assets + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/assets`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderAssetApi - functional programming interface + * @export + */ +export const ProviderAssetApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderAssetApiAxiosParamCreator(configuration) + return { + /** + * This endpoint retrieves a list of all available assets. + * @summary Retrieve all assets + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderAssetApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderAssetApi - factory interface + * @export + */ +export const ProviderAssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderAssetApiFp(configuration) + return { + /** + * This endpoint retrieves a list of all available assets. + * @summary Retrieve all assets + * @param {ProviderAssetApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderAssetApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for list operation in ProviderAssetApi. + * @export + * @interface ProviderAssetApiListRequest + */ +export interface ProviderAssetApiListRequest { + /** + * + * @type {string} + * @memberof ProviderAssetApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderAssetApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderAssetApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderAssetApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderAssetApiList + */ + readonly desc?: string + + /** + * + * @type {string} + * @memberof ProviderAssetApiList + */ + readonly authorization?: string +} + +/** + * ProviderAssetApi - object-oriented interface + * @export + * @class ProviderAssetApi + * @extends {BaseAPI} + */ +export class ProviderAssetApi extends BaseAPI { + /** + * This endpoint retrieves a list of all available assets. + * @summary Retrieve all assets + * @param {ProviderAssetApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderAssetApi + */ + public list(requestParameters: ProviderAssetApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderAssetApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderConnectionApi - axios parameter creator + * @export + */ +export const ProviderConnectionApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * This endpoint securely stores the details of a provider connection, ensuring that all sensitive information is encrypted. + * @summary Store a provider connection securely + * @param {string} xClientId + * @param {CreateConnectionDto} createConnectionDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + create: async (xClientId: string, createConnectionDto: CreateConnectionDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('create', 'xClientId', xClientId) + // verify required parameter 'createConnectionDto' is not null or undefined + assertParamExists('create', 'createConnectionDto', createConnectionDto) + const localVarPath = `/v1/provider/connections`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createConnectionDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint retrieves the details of a specific connection associated with the client, identified by the ID. + * @summary Retrieve a specific connection by ID + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById: async (xClientId: string, connectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('getById', 'xClientId', xClientId) + // verify required parameter 'connectionId' is not null or undefined + assertParamExists('getById', 'connectionId', connectionId) + const localVarPath = `/v1/provider/connections/{connectionId}` + .replace(`{${"connectionId"}}`, encodeURIComponent(String(connectionId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint initiates a new connection by generating a public key and an encryption key for secure communication. + * @summary Initiate a new provider connection + * @param {string} xClientId + * @param {InitiateConnectionDto} initiateConnectionDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + initiate: async (xClientId: string, initiateConnectionDto: InitiateConnectionDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('initiate', 'xClientId', xClientId) + // verify required parameter 'initiateConnectionDto' is not null or undefined + assertParamExists('initiate', 'initiateConnectionDto', initiateConnectionDto) + const localVarPath = `/v1/provider/connections/initiate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(initiateConnectionDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint retrieves a list of all connections associated with the client. + * @summary List all connections + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/connections`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Note: use GET /v1/provider/accounts endpoint instead + * @summary (DEPRECATED) List accounts for a specific connection + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + listAccounts: async (xClientId: string, connectionId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('listAccounts', 'xClientId', xClientId) + // verify required parameter 'connectionId' is not null or undefined + assertParamExists('listAccounts', 'connectionId', connectionId) + const localVarPath = `/v1/provider/connections/{connectionId}/accounts` + .replace(`{${"connectionId"}}`, encodeURIComponent(String(connectionId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Note: use GET /v1/provider/wallets endpoint instead + * @summary (DEPRECATED) List wallets for a specific connection + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + listWallets: async (xClientId: string, connectionId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('listWallets', 'xClientId', xClientId) + // verify required parameter 'connectionId' is not null or undefined + assertParamExists('listWallets', 'connectionId', connectionId) + const localVarPath = `/v1/provider/connections/{connectionId}/wallets` + .replace(`{${"connectionId"}}`, encodeURIComponent(String(connectionId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint revokes an existing connection, effectively terminating any ongoing communication and invalidating the connection credentials. + * @summary Revoke an existing connection + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + revoke: async (xClientId: string, connectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('revoke', 'xClientId', xClientId) + // verify required parameter 'connectionId' is not null or undefined + assertParamExists('revoke', 'connectionId', connectionId) + const localVarPath = `/v1/provider/connections/{connectionId}` + .replace(`{${"connectionId"}}`, encodeURIComponent(String(connectionId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint updates the details of a specific connection associated with the client, identified by the connection ID. + * @summary Update a specific connection by ID + * @param {string} xClientId + * @param {string} connectionId + * @param {UpdateConnectionDto} updateConnectionDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + update: async (xClientId: string, connectionId: string, updateConnectionDto: UpdateConnectionDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('update', 'xClientId', xClientId) + // verify required parameter 'connectionId' is not null or undefined + assertParamExists('update', 'connectionId', connectionId) + // verify required parameter 'updateConnectionDto' is not null or undefined + assertParamExists('update', 'updateConnectionDto', updateConnectionDto) + const localVarPath = `/v1/provider/connections/{connectionId}` + .replace(`{${"connectionId"}}`, encodeURIComponent(String(connectionId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateConnectionDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderConnectionApi - functional programming interface + * @export + */ +export const ProviderConnectionApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderConnectionApiAxiosParamCreator(configuration) + return { + /** + * This endpoint securely stores the details of a provider connection, ensuring that all sensitive information is encrypted. + * @summary Store a provider connection securely + * @param {string} xClientId + * @param {CreateConnectionDto} createConnectionDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async create(xClientId: string, createConnectionDto: CreateConnectionDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.create(xClientId, createConnectionDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.create']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint retrieves the details of a specific connection associated with the client, identified by the ID. + * @summary Retrieve a specific connection by ID + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getById(xClientId: string, connectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getById(xClientId, connectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.getById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint initiates a new connection by generating a public key and an encryption key for secure communication. + * @summary Initiate a new provider connection + * @param {string} xClientId + * @param {InitiateConnectionDto} initiateConnectionDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async initiate(xClientId: string, initiateConnectionDto: InitiateConnectionDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.initiate(xClientId, initiateConnectionDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.initiate']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint retrieves a list of all connections associated with the client. + * @summary List all connections + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Note: use GET /v1/provider/accounts endpoint instead + * @summary (DEPRECATED) List accounts for a specific connection + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + async listAccounts(xClientId: string, connectionId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listAccounts(xClientId, connectionId, cursor, limit, orderBy, desc, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.listAccounts']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Note: use GET /v1/provider/wallets endpoint instead + * @summary (DEPRECATED) List wallets for a specific connection + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + async listWallets(xClientId: string, connectionId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listWallets(xClientId, connectionId, cursor, limit, orderBy, desc, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.listWallets']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint revokes an existing connection, effectively terminating any ongoing communication and invalidating the connection credentials. + * @summary Revoke an existing connection + * @param {string} xClientId + * @param {string} connectionId + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async revoke(xClientId: string, connectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.revoke(xClientId, connectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.revoke']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint updates the details of a specific connection associated with the client, identified by the connection ID. + * @summary Update a specific connection by ID + * @param {string} xClientId + * @param {string} connectionId + * @param {UpdateConnectionDto} updateConnectionDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async update(xClientId: string, connectionId: string, updateConnectionDto: UpdateConnectionDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.update(xClientId, connectionId, updateConnectionDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderConnectionApi.update']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderConnectionApi - factory interface + * @export + */ +export const ProviderConnectionApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderConnectionApiFp(configuration) + return { + /** + * This endpoint securely stores the details of a provider connection, ensuring that all sensitive information is encrypted. + * @summary Store a provider connection securely + * @param {ProviderConnectionApiCreateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + create(requestParameters: ProviderConnectionApiCreateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.create(requestParameters.xClientId, requestParameters.createConnectionDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint retrieves the details of a specific connection associated with the client, identified by the ID. + * @summary Retrieve a specific connection by ID + * @param {ProviderConnectionApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById(requestParameters: ProviderConnectionApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.xClientId, requestParameters.connectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint initiates a new connection by generating a public key and an encryption key for secure communication. + * @summary Initiate a new provider connection + * @param {ProviderConnectionApiInitiateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + initiate(requestParameters: ProviderConnectionApiInitiateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.initiate(requestParameters.xClientId, requestParameters.initiateConnectionDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint retrieves a list of all connections associated with the client. + * @summary List all connections + * @param {ProviderConnectionApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderConnectionApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * Note: use GET /v1/provider/accounts endpoint instead + * @summary (DEPRECATED) List accounts for a specific connection + * @param {ProviderConnectionApiListAccountsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + listAccounts(requestParameters: ProviderConnectionApiListAccountsRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listAccounts(requestParameters.xClientId, requestParameters.connectionId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * Note: use GET /v1/provider/wallets endpoint instead + * @summary (DEPRECATED) List wallets for a specific connection + * @param {ProviderConnectionApiListWalletsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + listWallets(requestParameters: ProviderConnectionApiListWalletsRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listWallets(requestParameters.xClientId, requestParameters.connectionId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint revokes an existing connection, effectively terminating any ongoing communication and invalidating the connection credentials. + * @summary Revoke an existing connection + * @param {ProviderConnectionApiRevokeRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + revoke(requestParameters: ProviderConnectionApiRevokeRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.revoke(requestParameters.xClientId, requestParameters.connectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint updates the details of a specific connection associated with the client, identified by the connection ID. + * @summary Update a specific connection by ID + * @param {ProviderConnectionApiUpdateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + update(requestParameters: ProviderConnectionApiUpdateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.update(requestParameters.xClientId, requestParameters.connectionId, requestParameters.updateConnectionDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for create operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiCreateRequest + */ +export interface ProviderConnectionApiCreateRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiCreate + */ + readonly xClientId: string + + /** + * + * @type {CreateConnectionDto} + * @memberof ProviderConnectionApiCreate + */ + readonly createConnectionDto: CreateConnectionDto + + /** + * + * @type {string} + * @memberof ProviderConnectionApiCreate + */ + readonly authorization?: string +} + +/** + * Request parameters for getById operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiGetByIdRequest + */ +export interface ProviderConnectionApiGetByIdRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiGetById + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiGetById + */ + readonly connectionId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiGetById + */ + readonly authorization?: string +} + +/** + * Request parameters for initiate operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiInitiateRequest + */ +export interface ProviderConnectionApiInitiateRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiInitiate + */ + readonly xClientId: string + + /** + * + * @type {InitiateConnectionDto} + * @memberof ProviderConnectionApiInitiate + */ + readonly initiateConnectionDto: InitiateConnectionDto + + /** + * + * @type {string} + * @memberof ProviderConnectionApiInitiate + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiListRequest + */ +export interface ProviderConnectionApiListRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderConnectionApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderConnectionApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderConnectionApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderConnectionApiList + */ + readonly desc?: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiList + */ + readonly authorization?: string +} + +/** + * Request parameters for listAccounts operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiListAccountsRequest + */ +export interface ProviderConnectionApiListAccountsRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiListAccounts + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiListAccounts + */ + readonly connectionId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderConnectionApiListAccounts + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderConnectionApiListAccounts + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderConnectionApiListAccounts + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderConnectionApiListAccounts + */ + readonly desc?: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiListAccounts + */ + readonly authorization?: string +} + +/** + * Request parameters for listWallets operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiListWalletsRequest + */ +export interface ProviderConnectionApiListWalletsRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiListWallets + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiListWallets + */ + readonly connectionId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderConnectionApiListWallets + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderConnectionApiListWallets + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderConnectionApiListWallets + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderConnectionApiListWallets + */ + readonly desc?: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiListWallets + */ + readonly authorization?: string +} + +/** + * Request parameters for revoke operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiRevokeRequest + */ +export interface ProviderConnectionApiRevokeRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiRevoke + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiRevoke + */ + readonly connectionId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiRevoke + */ + readonly authorization?: string +} + +/** + * Request parameters for update operation in ProviderConnectionApi. + * @export + * @interface ProviderConnectionApiUpdateRequest + */ +export interface ProviderConnectionApiUpdateRequest { + /** + * + * @type {string} + * @memberof ProviderConnectionApiUpdate + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderConnectionApiUpdate + */ + readonly connectionId: string + + /** + * + * @type {UpdateConnectionDto} + * @memberof ProviderConnectionApiUpdate + */ + readonly updateConnectionDto: UpdateConnectionDto + + /** + * + * @type {string} + * @memberof ProviderConnectionApiUpdate + */ + readonly authorization?: string +} + +/** + * ProviderConnectionApi - object-oriented interface + * @export + * @class ProviderConnectionApi + * @extends {BaseAPI} + */ +export class ProviderConnectionApi extends BaseAPI { + /** + * This endpoint securely stores the details of a provider connection, ensuring that all sensitive information is encrypted. + * @summary Store a provider connection securely + * @param {ProviderConnectionApiCreateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public create(requestParameters: ProviderConnectionApiCreateRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).create(requestParameters.xClientId, requestParameters.createConnectionDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint retrieves the details of a specific connection associated with the client, identified by the ID. + * @summary Retrieve a specific connection by ID + * @param {ProviderConnectionApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public getById(requestParameters: ProviderConnectionApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).getById(requestParameters.xClientId, requestParameters.connectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint initiates a new connection by generating a public key and an encryption key for secure communication. + * @summary Initiate a new provider connection + * @param {ProviderConnectionApiInitiateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public initiate(requestParameters: ProviderConnectionApiInitiateRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).initiate(requestParameters.xClientId, requestParameters.initiateConnectionDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint retrieves a list of all connections associated with the client. + * @summary List all connections + * @param {ProviderConnectionApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public list(requestParameters: ProviderConnectionApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Note: use GET /v1/provider/accounts endpoint instead + * @summary (DEPRECATED) List accounts for a specific connection + * @param {ProviderConnectionApiListAccountsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public listAccounts(requestParameters: ProviderConnectionApiListAccountsRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).listAccounts(requestParameters.xClientId, requestParameters.connectionId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Note: use GET /v1/provider/wallets endpoint instead + * @summary (DEPRECATED) List wallets for a specific connection + * @param {ProviderConnectionApiListWalletsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public listWallets(requestParameters: ProviderConnectionApiListWalletsRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).listWallets(requestParameters.xClientId, requestParameters.connectionId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint revokes an existing connection, effectively terminating any ongoing communication and invalidating the connection credentials. + * @summary Revoke an existing connection + * @param {ProviderConnectionApiRevokeRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public revoke(requestParameters: ProviderConnectionApiRevokeRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).revoke(requestParameters.xClientId, requestParameters.connectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint updates the details of a specific connection associated with the client, identified by the connection ID. + * @summary Update a specific connection by ID + * @param {ProviderConnectionApiUpdateRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderConnectionApi + */ + public update(requestParameters: ProviderConnectionApiUpdateRequest, options?: RawAxiosRequestConfig) { + return ProviderConnectionApiFp(this.configuration).update(requestParameters.xClientId, requestParameters.connectionId, requestParameters.updateConnectionDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderKnownDestinationApi - axios parameter creator + * @export + */ +export const ProviderKnownDestinationApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Get known destinations across providers + * @param {string} xClientId + * @param {number} [limit] Number of records to return per page + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, limit?: number, cursor?: string, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/known-destinations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderKnownDestinationApi - functional programming interface + * @export + */ +export const ProviderKnownDestinationApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderKnownDestinationApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Get known destinations across providers + * @param {string} xClientId + * @param {number} [limit] Number of records to return per page + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, limit?: number, cursor?: string, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, limit, cursor, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderKnownDestinationApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderKnownDestinationApi - factory interface + * @export + */ +export const ProviderKnownDestinationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderKnownDestinationApiFp(configuration) + return { + /** + * + * @summary Get known destinations across providers + * @param {ProviderKnownDestinationApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderKnownDestinationApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.limit, requestParameters.cursor, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for list operation in ProviderKnownDestinationApi. + * @export + * @interface ProviderKnownDestinationApiListRequest + */ +export interface ProviderKnownDestinationApiListRequest { + /** + * + * @type {string} + * @memberof ProviderKnownDestinationApiList + */ + readonly xClientId: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderKnownDestinationApiList + */ + readonly limit?: number + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderKnownDestinationApiList + */ + readonly cursor?: string + + /** + * Field to order results by + * @type {string} + * @memberof ProviderKnownDestinationApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderKnownDestinationApiList + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderKnownDestinationApiList + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderKnownDestinationApiList + */ + readonly authorization?: string +} + +/** + * ProviderKnownDestinationApi - object-oriented interface + * @export + * @class ProviderKnownDestinationApi + * @extends {BaseAPI} + */ +export class ProviderKnownDestinationApi extends BaseAPI { + /** + * + * @summary Get known destinations across providers + * @param {ProviderKnownDestinationApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderKnownDestinationApi + */ + public list(requestParameters: ProviderKnownDestinationApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderKnownDestinationApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.limit, requestParameters.cursor, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderNetworkApi - axios parameter creator + * @export + */ +export const ProviderNetworkApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * This endpoint retrieves a list of all available networks. + * @summary Retrieve all networks + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/networks`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderNetworkApi - functional programming interface + * @export + */ +export const ProviderNetworkApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderNetworkApiAxiosParamCreator(configuration) + return { + /** + * This endpoint retrieves a list of all available networks. + * @summary Retrieve all networks + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderNetworkApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderNetworkApi - factory interface + * @export + */ +export const ProviderNetworkApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderNetworkApiFp(configuration) + return { + /** + * This endpoint retrieves a list of all available networks. + * @summary Retrieve all networks + * @param {ProviderNetworkApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderNetworkApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for list operation in ProviderNetworkApi. + * @export + * @interface ProviderNetworkApiListRequest + */ +export interface ProviderNetworkApiListRequest { + /** + * + * @type {string} + * @memberof ProviderNetworkApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderNetworkApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderNetworkApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderNetworkApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderNetworkApiList + */ + readonly desc?: string + + /** + * + * @type {string} + * @memberof ProviderNetworkApiList + */ + readonly authorization?: string +} + +/** + * ProviderNetworkApi - object-oriented interface + * @export + * @class ProviderNetworkApi + * @extends {BaseAPI} + */ +export class ProviderNetworkApi extends BaseAPI { + /** + * This endpoint retrieves a list of all available networks. + * @summary Retrieve all networks + * @param {ProviderNetworkApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderNetworkApi + */ + public list(requestParameters: ProviderNetworkApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderNetworkApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderProxyApi - axios parameter creator + * @export + */ +export const ProviderProxyApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + _delete: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('_delete', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('_delete', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('_delete', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + _options: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('_options', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('_options', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('_options', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'OPTIONS', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + get: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('get', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('get', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('get', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + head: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('head', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('head', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('head', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'HEAD', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + patch: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('patch', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('patch', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('patch', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + post: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('post', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('post', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('post', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + put: async (xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('put', 'xClientId', xClientId) + // verify required parameter 'endpoint' is not null or undefined + assertParamExists('put', 'endpoint', endpoint) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('put', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/proxy/{endpoint}` + .replace(`{${"endpoint"}}`, encodeURIComponent(String(endpoint))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderProxyApi - functional programming interface + * @export + */ +export const ProviderProxyApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderProxyApiAxiosParamCreator(configuration) + return { + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async _delete(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator._delete(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi._delete']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async _options(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator._options(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi._options']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async get(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.get(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi.get']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async head(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.head(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi.head']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async patch(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.patch(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi.patch']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async post(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.post(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi.post']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {string} xClientId + * @param {string} endpoint The raw endpoint path in the provider + * @param {string} xConnectionId The connection ID used to forward request to provider + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async put(xClientId: string, endpoint: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.put(xClientId, endpoint, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderProxyApi.put']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderProxyApi - factory interface + * @export + */ +export const ProviderProxyApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderProxyApiFp(configuration) + return { + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiDeleteRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + _delete(requestParameters: ProviderProxyApiDeleteRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp._delete(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiOptionsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + _options(requestParameters: ProviderProxyApiOptionsRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp._options(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiGetRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + get(requestParameters: ProviderProxyApiGetRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.get(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiHeadRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + head(requestParameters: ProviderProxyApiHeadRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.head(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiPatchRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + patch(requestParameters: ProviderProxyApiPatchRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.patch(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiPostRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + post(requestParameters: ProviderProxyApiPostRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.post(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiPutRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + put(requestParameters: ProviderProxyApiPutRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.put(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for _delete operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiDeleteRequest + */ +export interface ProviderProxyApiDeleteRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiDelete + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiDelete + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiDelete + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiDelete + */ + readonly authorization?: string +} + +/** + * Request parameters for _options operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiOptionsRequest + */ +export interface ProviderProxyApiOptionsRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiOptions + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiOptions + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiOptions + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiOptions + */ + readonly authorization?: string +} + +/** + * Request parameters for get operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiGetRequest + */ +export interface ProviderProxyApiGetRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiGet + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiGet + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiGet + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiGet + */ + readonly authorization?: string +} + +/** + * Request parameters for head operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiHeadRequest + */ +export interface ProviderProxyApiHeadRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiHead + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiHead + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiHead + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiHead + */ + readonly authorization?: string +} + +/** + * Request parameters for patch operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiPatchRequest + */ +export interface ProviderProxyApiPatchRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiPatch + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiPatch + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiPatch + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiPatch + */ + readonly authorization?: string +} + +/** + * Request parameters for post operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiPostRequest + */ +export interface ProviderProxyApiPostRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiPost + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiPost + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiPost + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiPost + */ + readonly authorization?: string +} + +/** + * Request parameters for put operation in ProviderProxyApi. + * @export + * @interface ProviderProxyApiPutRequest + */ +export interface ProviderProxyApiPutRequest { + /** + * + * @type {string} + * @memberof ProviderProxyApiPut + */ + readonly xClientId: string + + /** + * The raw endpoint path in the provider + * @type {string} + * @memberof ProviderProxyApiPut + */ + readonly endpoint: string + + /** + * The connection ID used to forward request to provider + * @type {string} + * @memberof ProviderProxyApiPut + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderProxyApiPut + */ + readonly authorization?: string +} + +/** + * ProviderProxyApi - object-oriented interface + * @export + * @class ProviderProxyApi + * @extends {BaseAPI} + */ +export class ProviderProxyApi extends BaseAPI { + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiDeleteRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public _delete(requestParameters: ProviderProxyApiDeleteRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration)._delete(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiOptionsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public _options(requestParameters: ProviderProxyApiOptionsRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration)._options(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiGetRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public get(requestParameters: ProviderProxyApiGetRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration).get(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiHeadRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public head(requestParameters: ProviderProxyApiHeadRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration).head(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiPatchRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public patch(requestParameters: ProviderProxyApiPatchRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration).patch(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiPostRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public post(requestParameters: ProviderProxyApiPostRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration).post(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint uses the connection specified in the header to authorize and forward the request in the path to the provider. + * @summary Authorizes and forwards the request to the provider + * @param {ProviderProxyApiPutRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderProxyApi + */ + public put(requestParameters: ProviderProxyApiPutRequest, options?: RawAxiosRequestConfig) { + return ProviderProxyApiFp(this.configuration).put(requestParameters.xClientId, requestParameters.endpoint, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * ProviderScopedSyncApi - axios parameter creator + * @export */ -export const AccountApiAxiosParamCreator = function (configuration?: Configuration) { +export const ProviderScopedSyncApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * - * @summary Add a new account to a wallet + * This endpoint retrieves the details of a specific scoped synchronization process associated with the client, identified by the scoped sync ID. + * @summary Retrieve a specific scoped synchronization process by ID * @param {string} xClientId - * @param {string} authorization - * @param {DeriveAccountDto} deriveAccountDto + * @param {string} scopedSyncId + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - derive: async (xClientId: string, authorization: string, deriveAccountDto: DeriveAccountDto, options: RawAxiosRequestConfig = {}): Promise => { + getById: async (xClientId: string, scopedSyncId: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined - assertParamExists('derive', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('derive', 'authorization', authorization) - // verify required parameter 'deriveAccountDto' is not null or undefined - assertParamExists('derive', 'deriveAccountDto', deriveAccountDto) - const localVarPath = `/v1/accounts`; + assertParamExists('getById', 'xClientId', xClientId) + // verify required parameter 'scopedSyncId' is not null or undefined + assertParamExists('getById', 'scopedSyncId', scopedSyncId) + const localVarPath = `/v1/provider/scoped-syncs/{scopedSyncId}` + .replace(`{${"scopedSyncId"}}`, encodeURIComponent(String(scopedSyncId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/scoped-syncs`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * This endpoint starts scoped synchronization process for the client. + * @summary Start a scoped synchronization process + * @param {string} xClientId + * @param {StartScopedSyncDto} startScopedSyncDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + start: async (xClientId: string, startScopedSyncDto: StartScopedSyncDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('start', 'xClientId', xClientId) + // verify required parameter 'startScopedSyncDto' is not null or undefined + assertParamExists('start', 'startScopedSyncDto', startScopedSyncDto) + const localVarPath = `/v1/provider/scoped-syncs`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -1472,94 +8415,322 @@ export const AccountApiAxiosParamCreator = function (configuration?: Configurati // authentication GNAP required - if (xClientId != null) { - localVarHeaderParameter['x-client-id'] = String(xClientId); - } + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(startScopedSyncDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ProviderScopedSyncApi - functional programming interface + * @export + */ +export const ProviderScopedSyncApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderScopedSyncApiAxiosParamCreator(configuration) + return { + /** + * This endpoint retrieves the details of a specific scoped synchronization process associated with the client, identified by the scoped sync ID. + * @summary Retrieve a specific scoped synchronization process by ID + * @param {string} xClientId + * @param {string} scopedSyncId + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getById(xClientId: string, scopedSyncId: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getById(xClientId, scopedSyncId, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderScopedSyncApi.getById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderScopedSyncApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint starts scoped synchronization process for the client. + * @summary Start a scoped synchronization process + * @param {string} xClientId + * @param {StartScopedSyncDto} startScopedSyncDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async start(xClientId: string, startScopedSyncDto: StartScopedSyncDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.start(xClientId, startScopedSyncDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderScopedSyncApi.start']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ProviderScopedSyncApi - factory interface + * @export + */ +export const ProviderScopedSyncApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderScopedSyncApiFp(configuration) + return { + /** + * This endpoint retrieves the details of a specific scoped synchronization process associated with the client, identified by the scoped sync ID. + * @summary Retrieve a specific scoped synchronization process by ID + * @param {ProviderScopedSyncApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById(requestParameters: ProviderScopedSyncApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.xClientId, requestParameters.scopedSyncId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes + * @param {ProviderScopedSyncApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderScopedSyncApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint starts scoped synchronization process for the client. + * @summary Start a scoped synchronization process + * @param {ProviderScopedSyncApiStartRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + start(requestParameters: ProviderScopedSyncApiStartRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.start(requestParameters.xClientId, requestParameters.startScopedSyncDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getById operation in ProviderScopedSyncApi. + * @export + * @interface ProviderScopedSyncApiGetByIdRequest + */ +export interface ProviderScopedSyncApiGetByIdRequest { + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiGetById + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiGetById + */ + readonly scopedSyncId: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderScopedSyncApiGetById + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiGetById + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in ProviderScopedSyncApi. + * @export + * @interface ProviderScopedSyncApiListRequest + */ +export interface ProviderScopedSyncApiListRequest { + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderScopedSyncApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderScopedSyncApiList + */ + readonly limit?: number - if (authorization != null) { - localVarHeaderParameter['Authorization'] = String(authorization); - } + /** + * Field to order results by + * @type {string} + * @memberof ProviderScopedSyncApiList + */ + readonly orderBy?: string + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderScopedSyncApiList + */ + readonly desc?: string - - localVarHeaderParameter['Content-Type'] = 'application/json'; + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderScopedSyncApiList + */ + readonly xConnectionId?: string - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(deriveAccountDto, localVarRequestOptions, configuration) + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiList + */ + readonly authorization?: string +} - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @summary Imports an account - * @param {string} xClientId - * @param {string} authorization - * @param {ImportPrivateKeyDto} importPrivateKeyDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - importPrivateKey: async (xClientId: string, authorization: string, importPrivateKeyDto: ImportPrivateKeyDto, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'xClientId' is not null or undefined - assertParamExists('importPrivateKey', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('importPrivateKey', 'authorization', authorization) - // verify required parameter 'importPrivateKeyDto' is not null or undefined - assertParamExists('importPrivateKey', 'importPrivateKeyDto', importPrivateKeyDto) - const localVarPath = `/v1/accounts/import`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } +/** + * Request parameters for start operation in ProviderScopedSyncApi. + * @export + * @interface ProviderScopedSyncApiStartRequest + */ +export interface ProviderScopedSyncApiStartRequest { + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiStart + */ + readonly xClientId: string - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; + /** + * + * @type {StartScopedSyncDto} + * @memberof ProviderScopedSyncApiStart + */ + readonly startScopedSyncDto: StartScopedSyncDto - // authentication GNAP required + /** + * + * @type {string} + * @memberof ProviderScopedSyncApiStart + */ + readonly authorization?: string +} - if (xClientId != null) { - localVarHeaderParameter['x-client-id'] = String(xClientId); - } +/** + * ProviderScopedSyncApi - object-oriented interface + * @export + * @class ProviderScopedSyncApi + * @extends {BaseAPI} + */ +export class ProviderScopedSyncApi extends BaseAPI { + /** + * This endpoint retrieves the details of a specific scoped synchronization process associated with the client, identified by the scoped sync ID. + * @summary Retrieve a specific scoped synchronization process by ID + * @param {ProviderScopedSyncApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderScopedSyncApi + */ + public getById(requestParameters: ProviderScopedSyncApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return ProviderScopedSyncApiFp(this.configuration).getById(requestParameters.xClientId, requestParameters.scopedSyncId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } - if (authorization != null) { - localVarHeaderParameter['Authorization'] = String(authorization); - } + /** + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes + * @param {ProviderScopedSyncApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderScopedSyncApi + */ + public list(requestParameters: ProviderScopedSyncApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderScopedSyncApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + /** + * This endpoint starts scoped synchronization process for the client. + * @summary Start a scoped synchronization process + * @param {ProviderScopedSyncApiStartRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderScopedSyncApi + */ + public start(requestParameters: ProviderScopedSyncApiStartRequest, options?: RawAxiosRequestConfig) { + return ProviderScopedSyncApiFp(this.configuration).start(requestParameters.xClientId, requestParameters.startScopedSyncDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } +} - - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(importPrivateKeyDto, localVarRequestOptions, configuration) - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, +/** + * ProviderSyncApi - axios parameter creator + * @export + */ +export const ProviderSyncApiAxiosParamCreator = function (configuration?: Configuration) { + return { /** - * - * @summary Lists the client accounts + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes * @param {string} xClientId - * @param {string} authorization + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - list: async (xClientId: string, authorization: string, options: RawAxiosRequestConfig = {}): Promise => { + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined assertParamExists('list', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('list', 'authorization', authorization) - const localVarPath = `/v1/accounts`; + const localVarPath = `/v1/provider/syncs`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -1573,10 +8744,33 @@ export const AccountApiAxiosParamCreator = function (configuration?: Configurati // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); } + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + if (authorization != null) { localVarHeaderParameter['Authorization'] = String(authorization); } @@ -1596,168 +8790,214 @@ export const AccountApiAxiosParamCreator = function (configuration?: Configurati }; /** - * AccountApi - functional programming interface + * ProviderSyncApi - functional programming interface * @export */ -export const AccountApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = AccountApiAxiosParamCreator(configuration) +export const ProviderSyncApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderSyncApiAxiosParamCreator(configuration) return { /** - * - * @summary Add a new account to a wallet - * @param {string} xClientId - * @param {string} authorization - * @param {DeriveAccountDto} deriveAccountDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async derive(xClientId: string, authorization: string, deriveAccountDto: DeriveAccountDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.derive(xClientId, authorization, deriveAccountDto, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['AccountApi.derive']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @summary Imports an account - * @param {string} xClientId - * @param {string} authorization - * @param {ImportPrivateKeyDto} importPrivateKeyDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async importPrivateKey(xClientId: string, authorization: string, importPrivateKeyDto: ImportPrivateKeyDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.importPrivateKey(xClientId, authorization, importPrivateKeyDto, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['AccountApi.importPrivateKey']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @summary Lists the client accounts + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes * @param {string} xClientId - * @param {string} authorization + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async list(xClientId: string, authorization: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, authorization, options); + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['AccountApi.list']?.[localVarOperationServerIndex]?.url; + const localVarOperationServerBasePath = operationServerMap['ProviderSyncApi.list']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, } }; /** - * AccountApi - factory interface + * ProviderSyncApi - factory interface * @export */ -export const AccountApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = AccountApiFp(configuration) +export const ProviderSyncApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderSyncApiFp(configuration) return { /** - * - * @summary Add a new account to a wallet - * @param {string} xClientId - * @param {string} authorization - * @param {DeriveAccountDto} deriveAccountDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - derive(xClientId: string, authorization: string, deriveAccountDto: DeriveAccountDto, options?: any): AxiosPromise { - return localVarFp.derive(xClientId, authorization, deriveAccountDto, options).then((request) => request(axios, basePath)); - }, - /** - * - * @summary Imports an account - * @param {string} xClientId - * @param {string} authorization - * @param {ImportPrivateKeyDto} importPrivateKeyDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - importPrivateKey(xClientId: string, authorization: string, importPrivateKeyDto: ImportPrivateKeyDto, options?: any): AxiosPromise { - return localVarFp.importPrivateKey(xClientId, authorization, importPrivateKeyDto, options).then((request) => request(axios, basePath)); - }, - /** - * - * @summary Lists the client accounts - * @param {string} xClientId - * @param {string} authorization + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes + * @param {ProviderSyncApiListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - list(xClientId: string, authorization: string, options?: any): AxiosPromise { - return localVarFp.list(xClientId, authorization, options).then((request) => request(axios, basePath)); + list(requestParameters: ProviderSyncApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, }; }; /** - * AccountApi - object-oriented interface + * Request parameters for list operation in ProviderSyncApi. * @export - * @class AccountApi - * @extends {BaseAPI} + * @interface ProviderSyncApiListRequest */ -export class AccountApi extends BaseAPI { +export interface ProviderSyncApiListRequest { /** * - * @summary Add a new account to a wallet - * @param {string} xClientId - * @param {string} authorization - * @param {DeriveAccountDto} deriveAccountDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AccountApi + * @type {string} + * @memberof ProviderSyncApiList */ - public derive(xClientId: string, authorization: string, deriveAccountDto: DeriveAccountDto, options?: RawAxiosRequestConfig) { - return AccountApiFp(this.configuration).derive(xClientId, authorization, deriveAccountDto, options).then((request) => request(this.axios, this.basePath)); - } + readonly xClientId: string /** - * - * @summary Imports an account - * @param {string} xClientId - * @param {string} authorization - * @param {ImportPrivateKeyDto} importPrivateKeyDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AccountApi + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderSyncApiList */ - public importPrivateKey(xClientId: string, authorization: string, importPrivateKeyDto: ImportPrivateKeyDto, options?: RawAxiosRequestConfig) { - return AccountApiFp(this.configuration).importPrivateKey(xClientId, authorization, importPrivateKeyDto, options).then((request) => request(this.axios, this.basePath)); - } + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderSyncApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderSyncApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderSyncApiList + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderSyncApiList + */ + readonly xConnectionId?: string /** * - * @summary Lists the client accounts - * @param {string} xClientId - * @param {string} authorization + * @type {string} + * @memberof ProviderSyncApiList + */ + readonly authorization?: string +} + +/** + * ProviderSyncApi - object-oriented interface + * @export + * @class ProviderSyncApi + * @extends {BaseAPI} + */ +export class ProviderSyncApi extends BaseAPI { + /** + * This endpoint retrieves a list of synchronization processes associated with the client. Optionally, it can filter the processes by a specific connection ID. + * @summary Retrieve a list of synchronization processes + * @param {ProviderSyncApiListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof AccountApi + * @memberof ProviderSyncApi */ - public list(xClientId: string, authorization: string, options?: RawAxiosRequestConfig) { - return AccountApiFp(this.configuration).list(xClientId, authorization, options).then((request) => request(this.axios, this.basePath)); + public list(requestParameters: ProviderSyncApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderSyncApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } } -/** - * ApplicationApi - axios parameter creator - * @export - */ -export const ApplicationApiAxiosParamCreator = function (configuration?: Configuration) { - return { +/** + * ProviderTransferApi - axios parameter creator + * @export + */ +export const ProviderTransferApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * This endpoint retrieves the details of a specific transfer using its ID. + * @summary Retrieve transfer details + * @param {string} xClientId + * @param {string} transferId + * @param {string} xConnectionId The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById: async (xClientId: string, transferId: string, xConnectionId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('getById', 'xClientId', xClientId) + // verify required parameter 'transferId' is not null or undefined + assertParamExists('getById', 'transferId', transferId) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('getById', 'xConnectionId', xConnectionId) + const localVarPath = `/v1/provider/transfers/{transferId}` + .replace(`{${"transferId"}}`, encodeURIComponent(String(transferId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** - * + * This endpoint sends a transfer to the source\'s provider. + * @summary Send a transfer + * @param {string} xClientId + * @param {string} xConnectionId The provider connection through which the resource is accessed + * @param {SendTransferDto} sendTransferDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ping: async (options: RawAxiosRequestConfig = {}): Promise => { - const localVarPath = `/ping`; + send: async (xClientId: string, xConnectionId: string, sendTransferDto: SendTransferDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('send', 'xClientId', xClientId) + // verify required parameter 'xConnectionId' is not null or undefined + assertParamExists('send', 'xConnectionId', xConnectionId) + // verify required parameter 'sendTransferDto' is not null or undefined + assertParamExists('send', 'sendTransferDto', sendTransferDto) + const localVarPath = `/v1/provider/transfers`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -1765,15 +9005,35 @@ export const ApplicationApiAxiosParamCreator = function (configuration?: Configu baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication GNAP required + + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + localVarHeaderParameter['Content-Type'] = 'application/json'; + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(sendTransferDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -1784,84 +9044,204 @@ export const ApplicationApiAxiosParamCreator = function (configuration?: Configu }; /** - * ApplicationApi - functional programming interface + * ProviderTransferApi - functional programming interface * @export */ -export const ApplicationApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = ApplicationApiAxiosParamCreator(configuration) +export const ProviderTransferApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderTransferApiAxiosParamCreator(configuration) return { /** - * + * This endpoint retrieves the details of a specific transfer using its ID. + * @summary Retrieve transfer details + * @param {string} xClientId + * @param {string} transferId + * @param {string} xConnectionId The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async ping(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.ping(options); + async getById(xClientId: string, transferId: string, xConnectionId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getById(xClientId, transferId, xConnectionId, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['ApplicationApi.ping']?.[localVarOperationServerIndex]?.url; + const localVarOperationServerBasePath = operationServerMap['ProviderTransferApi.getById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * This endpoint sends a transfer to the source\'s provider. + * @summary Send a transfer + * @param {string} xClientId + * @param {string} xConnectionId The provider connection through which the resource is accessed + * @param {SendTransferDto} sendTransferDto + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async send(xClientId: string, xConnectionId: string, sendTransferDto: SendTransferDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.send(xClientId, xConnectionId, sendTransferDto, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderTransferApi.send']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, } }; /** - * ApplicationApi - factory interface + * ProviderTransferApi - factory interface * @export */ -export const ApplicationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = ApplicationApiFp(configuration) +export const ProviderTransferApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderTransferApiFp(configuration) return { /** - * + * This endpoint retrieves the details of a specific transfer using its ID. + * @summary Retrieve transfer details + * @param {ProviderTransferApiGetByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ping(options?: any): AxiosPromise { - return localVarFp.ping(options).then((request) => request(axios, basePath)); + getById(requestParameters: ProviderTransferApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.xClientId, requestParameters.transferId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * This endpoint sends a transfer to the source\'s provider. + * @summary Send a transfer + * @param {ProviderTransferApiSendRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + send(requestParameters: ProviderTransferApiSendRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.send(requestParameters.xClientId, requestParameters.xConnectionId, requestParameters.sendTransferDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, }; }; /** - * ApplicationApi - object-oriented interface + * Request parameters for getById operation in ProviderTransferApi. * @export - * @class ApplicationApi - * @extends {BaseAPI} + * @interface ProviderTransferApiGetByIdRequest */ -export class ApplicationApi extends BaseAPI { +export interface ProviderTransferApiGetByIdRequest { + /** + * + * @type {string} + * @memberof ProviderTransferApiGetById + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof ProviderTransferApiGetById + */ + readonly transferId: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderTransferApiGetById + */ + readonly xConnectionId: string + + /** + * + * @type {string} + * @memberof ProviderTransferApiGetById + */ + readonly authorization?: string +} + +/** + * Request parameters for send operation in ProviderTransferApi. + * @export + * @interface ProviderTransferApiSendRequest + */ +export interface ProviderTransferApiSendRequest { /** * + * @type {string} + * @memberof ProviderTransferApiSend + */ + readonly xClientId: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderTransferApiSend + */ + readonly xConnectionId: string + + /** + * + * @type {SendTransferDto} + * @memberof ProviderTransferApiSend + */ + readonly sendTransferDto: SendTransferDto + + /** + * + * @type {string} + * @memberof ProviderTransferApiSend + */ + readonly authorization?: string +} + +/** + * ProviderTransferApi - object-oriented interface + * @export + * @class ProviderTransferApi + * @extends {BaseAPI} + */ +export class ProviderTransferApi extends BaseAPI { + /** + * This endpoint retrieves the details of a specific transfer using its ID. + * @summary Retrieve transfer details + * @param {ProviderTransferApiGetByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ApplicationApi + * @memberof ProviderTransferApi */ - public ping(options?: RawAxiosRequestConfig) { - return ApplicationApiFp(this.configuration).ping(options).then((request) => request(this.axios, this.basePath)); + public getById(requestParameters: ProviderTransferApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return ProviderTransferApiFp(this.configuration).getById(requestParameters.xClientId, requestParameters.transferId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * This endpoint sends a transfer to the source\'s provider. + * @summary Send a transfer + * @param {ProviderTransferApiSendRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderTransferApi + */ + public send(requestParameters: ProviderTransferApiSendRequest, options?: RawAxiosRequestConfig) { + return ProviderTransferApiFp(this.configuration).send(requestParameters.xClientId, requestParameters.xConnectionId, requestParameters.sendTransferDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } } /** - * ClientApi - axios parameter creator + * ProviderWalletApi - axios parameter creator * @export */ -export const ClientApiAxiosParamCreator = function (configuration?: Configuration) { +export const ProviderWalletApiAxiosParamCreator = function (configuration?: Configuration) { return { /** * - * @summary Creates a new client - * @param {string} xApiKey - * @param {CreateClientDto} createClientDto + * @summary Get a specific wallet by ID + * @param {string} xClientId + * @param {string} walletId The ID of the wallet to retrieve + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - create: async (xApiKey: string, createClientDto: CreateClientDto, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'xApiKey' is not null or undefined - assertParamExists('create', 'xApiKey', xApiKey) - // verify required parameter 'createClientDto' is not null or undefined - assertParamExists('create', 'createClientDto', createClientDto) - const localVarPath = `/v1/clients`; + getById: async (xClientId: string, walletId: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('getById', 'xClientId', xClientId) + // verify required parameter 'walletId' is not null or undefined + assertParamExists('getById', 'walletId', walletId) + const localVarPath = `/v1/provider/wallets/{walletId}` + .replace(`{${"walletId"}}`, encodeURIComponent(String(walletId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -1869,122 +9249,131 @@ export const ClientApiAxiosParamCreator = function (configuration?: Configuratio baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - // authentication ADMIN_API_KEY required - await setApiKeyToObject(localVarHeaderParameter, "ADMIN_API_KEY", configuration) + // authentication GNAP required - if (xApiKey != null) { - localVarHeaderParameter['x-api-key'] = String(xApiKey); + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); } + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } - - localVarHeaderParameter['Content-Type'] = 'application/json'; + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(createClientDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, - } -}; - -/** - * ClientApi - functional programming interface - * @export - */ -export const ClientApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = ClientApiAxiosParamCreator(configuration) - return { /** * - * @summary Creates a new client - * @param {string} xApiKey - * @param {CreateClientDto} createClientDto + * @summary List the client wallets + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async create(xApiKey: string, createClientDto: CreateClientDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.create(xApiKey, createClientDto, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['ClientApi.create']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - } -}; + list: async (xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xClientId' is not null or undefined + assertParamExists('list', 'xClientId', xClientId) + const localVarPath = `/v1/provider/wallets`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } -/** - * ClientApi - factory interface - * @export - */ -export const ClientApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = ClientApiFp(configuration) - return { - /** - * - * @summary Creates a new client - * @param {string} xApiKey - * @param {CreateClientDto} createClientDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - create(xApiKey: string, createClientDto: CreateClientDto, options?: any): AxiosPromise { - return localVarFp.create(xApiKey, createClientDto, options).then((request) => request(axios, basePath)); - }, - }; -}; + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; -/** - * ClientApi - object-oriented interface - * @export - * @class ClientApi - * @extends {BaseAPI} - */ -export class ClientApi extends BaseAPI { - /** - * - * @summary Creates a new client - * @param {string} xApiKey - * @param {CreateClientDto} createClientDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof ClientApi - */ - public create(xApiKey: string, createClientDto: CreateClientDto, options?: RawAxiosRequestConfig) { - return ClientApiFp(this.configuration).create(xApiKey, createClientDto, options).then((request) => request(this.axios, this.basePath)); - } -} + // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } -/** - * EncryptionKeyApi - axios parameter creator - * @export - */ -export const EncryptionKeyApiAxiosParamCreator = function (configuration?: Configuration) { - return { + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + + if (xClientId != null) { + localVarHeaderParameter['x-client-id'] = String(xClientId); + } + + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * - * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information + * @summary List accounts for a specific wallet * @param {string} xClientId - * @param {string} authorization + * @param {string} walletId The ID of the wallet to retrieve accounts for + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generate: async (xClientId: string, authorization: string, options: RawAxiosRequestConfig = {}): Promise => { + listAccounts: async (xClientId: string, walletId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined - assertParamExists('generate', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('generate', 'authorization', authorization) - const localVarPath = `/v1/encryption-keys`; + assertParamExists('listAccounts', 'xClientId', xClientId) + // verify required parameter 'walletId' is not null or undefined + assertParamExists('listAccounts', 'walletId', walletId) + const localVarPath = `/v1/provider/wallets/{walletId}/accounts` + .replace(`{${"walletId"}}`, encodeURIComponent(String(walletId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -1992,16 +9381,39 @@ export const EncryptionKeyApiAxiosParamCreator = function (configuration?: Confi baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + + if (cursor !== undefined) { + localVarQueryParameter['cursor'] = cursor; + } + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (orderBy !== undefined) { + localVarQueryParameter['orderBy'] = orderBy; + } + + if (desc !== undefined) { + localVarQueryParameter['desc'] = desc; + } + if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); } + if (xConnectionId != null) { + localVarHeaderParameter['x-connection-id'] = String(xConnectionId); + } + if (authorization != null) { localVarHeaderParameter['Authorization'] = String(authorization); } @@ -2021,68 +9433,305 @@ export const EncryptionKeyApiAxiosParamCreator = function (configuration?: Confi }; /** - * EncryptionKeyApi - functional programming interface + * ProviderWalletApi - functional programming interface * @export */ -export const EncryptionKeyApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = EncryptionKeyApiAxiosParamCreator(configuration) +export const ProviderWalletApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ProviderWalletApiAxiosParamCreator(configuration) return { /** * - * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information + * @summary Get a specific wallet by ID * @param {string} xClientId - * @param {string} authorization + * @param {string} walletId The ID of the wallet to retrieve + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async generate(xClientId: string, authorization: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.generate(xClientId, authorization, options); + async getById(xClientId: string, walletId: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getById(xClientId, walletId, xConnectionId, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['EncryptionKeyApi.generate']?.[localVarOperationServerIndex]?.url; + const localVarOperationServerBasePath = operationServerMap['ProviderWalletApi.getById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary List the client wallets + * @param {string} xClientId + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async list(xClientId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderWalletApi.list']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary List accounts for a specific wallet + * @param {string} xClientId + * @param {string} walletId The ID of the wallet to retrieve accounts for + * @param {string} [cursor] Cursor for pagination. Use the next cursor from previous response to get next page + * @param {number} [limit] Number of records to return per page + * @param {string} [orderBy] Field to order results by + * @param {string} [desc] Set to \"true\" or \"1\" for descending order + * @param {string} [xConnectionId] The provider connection through which the resource is accessed + * @param {string} [authorization] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listAccounts(xClientId: string, walletId: string, cursor?: string, limit?: number, orderBy?: string, desc?: string, xConnectionId?: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listAccounts(xClientId, walletId, cursor, limit, orderBy, desc, xConnectionId, authorization, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ProviderWalletApi.listAccounts']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, } }; /** - * EncryptionKeyApi - factory interface + * ProviderWalletApi - factory interface * @export */ -export const EncryptionKeyApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = EncryptionKeyApiFp(configuration) +export const ProviderWalletApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ProviderWalletApiFp(configuration) return { /** * - * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information - * @param {string} xClientId - * @param {string} authorization + * @summary Get a specific wallet by ID + * @param {ProviderWalletApiGetByIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getById(requestParameters: ProviderWalletApiGetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getById(requestParameters.xClientId, requestParameters.walletId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary List the client wallets + * @param {ProviderWalletApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + list(requestParameters: ProviderWalletApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary List accounts for a specific wallet + * @param {ProviderWalletApiListAccountsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generate(xClientId: string, authorization: string, options?: any): AxiosPromise { - return localVarFp.generate(xClientId, authorization, options).then((request) => request(axios, basePath)); + listAccounts(requestParameters: ProviderWalletApiListAccountsRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listAccounts(requestParameters.xClientId, requestParameters.walletId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, }; }; /** - * EncryptionKeyApi - object-oriented interface + * Request parameters for getById operation in ProviderWalletApi. * @export - * @class EncryptionKeyApi + * @interface ProviderWalletApiGetByIdRequest + */ +export interface ProviderWalletApiGetByIdRequest { + /** + * + * @type {string} + * @memberof ProviderWalletApiGetById + */ + readonly xClientId: string + + /** + * The ID of the wallet to retrieve + * @type {string} + * @memberof ProviderWalletApiGetById + */ + readonly walletId: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderWalletApiGetById + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderWalletApiGetById + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in ProviderWalletApi. + * @export + * @interface ProviderWalletApiListRequest + */ +export interface ProviderWalletApiListRequest { + /** + * + * @type {string} + * @memberof ProviderWalletApiList + */ + readonly xClientId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderWalletApiList + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderWalletApiList + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderWalletApiList + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderWalletApiList + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderWalletApiList + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderWalletApiList + */ + readonly authorization?: string +} + +/** + * Request parameters for listAccounts operation in ProviderWalletApi. + * @export + * @interface ProviderWalletApiListAccountsRequest + */ +export interface ProviderWalletApiListAccountsRequest { + /** + * + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly xClientId: string + + /** + * The ID of the wallet to retrieve accounts for + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly walletId: string + + /** + * Cursor for pagination. Use the next cursor from previous response to get next page + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly cursor?: string + + /** + * Number of records to return per page + * @type {number} + * @memberof ProviderWalletApiListAccounts + */ + readonly limit?: number + + /** + * Field to order results by + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly orderBy?: string + + /** + * Set to \"true\" or \"1\" for descending order + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly desc?: string + + /** + * The provider connection through which the resource is accessed + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly xConnectionId?: string + + /** + * + * @type {string} + * @memberof ProviderWalletApiListAccounts + */ + readonly authorization?: string +} + +/** + * ProviderWalletApi - object-oriented interface + * @export + * @class ProviderWalletApi * @extends {BaseAPI} */ -export class EncryptionKeyApi extends BaseAPI { +export class ProviderWalletApi extends BaseAPI { /** * - * @summary Generates an encryption key pair used to secure end-to-end communication containing sensitive information - * @param {string} xClientId - * @param {string} authorization + * @summary Get a specific wallet by ID + * @param {ProviderWalletApiGetByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof EncryptionKeyApi + * @memberof ProviderWalletApi + */ + public getById(requestParameters: ProviderWalletApiGetByIdRequest, options?: RawAxiosRequestConfig) { + return ProviderWalletApiFp(this.configuration).getById(requestParameters.xClientId, requestParameters.walletId, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary List the client wallets + * @param {ProviderWalletApiListRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderWalletApi + */ + public list(requestParameters: ProviderWalletApiListRequest, options?: RawAxiosRequestConfig) { + return ProviderWalletApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary List accounts for a specific wallet + * @param {ProviderWalletApiListAccountsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProviderWalletApi */ - public generate(xClientId: string, authorization: string, options?: RawAxiosRequestConfig) { - return EncryptionKeyApiFp(this.configuration).generate(xClientId, authorization, options).then((request) => request(this.axios, this.basePath)); + public listAccounts(requestParameters: ProviderWalletApiListAccountsRequest, options?: RawAxiosRequestConfig) { + return ProviderWalletApiFp(this.configuration).listAccounts(requestParameters.xClientId, requestParameters.walletId, requestParameters.cursor, requestParameters.limit, requestParameters.orderBy, requestParameters.desc, requestParameters.xConnectionId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } } @@ -2098,16 +9747,14 @@ export const SignApiAxiosParamCreator = function (configuration?: Configuration) * * @summary Signs the given request * @param {string} xClientId - * @param {string} authorization * @param {SignRequestDto} signRequestDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - sign: async (xClientId: string, authorization: string, signRequestDto: SignRequestDto, options: RawAxiosRequestConfig = {}): Promise => { + sign: async (xClientId: string, signRequestDto: SignRequestDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined assertParamExists('sign', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('sign', 'authorization', authorization) // verify required parameter 'signRequestDto' is not null or undefined assertParamExists('sign', 'signRequestDto', signRequestDto) const localVarPath = `/v1/sign`; @@ -2124,6 +9771,9 @@ export const SignApiAxiosParamCreator = function (configuration?: Configuration) // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); } @@ -2160,13 +9810,13 @@ export const SignApiFp = function(configuration?: Configuration) { * * @summary Signs the given request * @param {string} xClientId - * @param {string} authorization * @param {SignRequestDto} signRequestDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async sign(xClientId: string, authorization: string, signRequestDto: SignRequestDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.sign(xClientId, authorization, signRequestDto, options); + async sign(xClientId: string, signRequestDto: SignRequestDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.sign(xClientId, signRequestDto, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['SignApi.sign']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -2184,18 +9834,44 @@ export const SignApiFactory = function (configuration?: Configuration, basePath? /** * * @summary Signs the given request - * @param {string} xClientId - * @param {string} authorization - * @param {SignRequestDto} signRequestDto + * @param {SignApiSignRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - sign(xClientId: string, authorization: string, signRequestDto: SignRequestDto, options?: any): AxiosPromise { - return localVarFp.sign(xClientId, authorization, signRequestDto, options).then((request) => request(axios, basePath)); + sign(requestParameters: SignApiSignRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.sign(requestParameters.xClientId, requestParameters.signRequestDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, }; }; +/** + * Request parameters for sign operation in SignApi. + * @export + * @interface SignApiSignRequest + */ +export interface SignApiSignRequest { + /** + * + * @type {string} + * @memberof SignApiSign + */ + readonly xClientId: string + + /** + * + * @type {SignRequestDto} + * @memberof SignApiSign + */ + readonly signRequestDto: SignRequestDto + + /** + * + * @type {string} + * @memberof SignApiSign + */ + readonly authorization?: string +} + /** * SignApi - object-oriented interface * @export @@ -2206,15 +9882,13 @@ export class SignApi extends BaseAPI { /** * * @summary Signs the given request - * @param {string} xClientId - * @param {string} authorization - * @param {SignRequestDto} signRequestDto + * @param {SignApiSignRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SignApi */ - public sign(xClientId: string, authorization: string, signRequestDto: SignRequestDto, options?: RawAxiosRequestConfig) { - return SignApiFp(this.configuration).sign(xClientId, authorization, signRequestDto, options).then((request) => request(this.axios, this.basePath)); + public sign(requestParameters: SignApiSignRequest, options?: RawAxiosRequestConfig) { + return SignApiFp(this.configuration).sign(requestParameters.xClientId, requestParameters.signRequestDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } } @@ -2230,16 +9904,14 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio * * @summary Generates a new wallet * @param {string} xClientId - * @param {string} authorization * @param {GenerateWalletDto} generateWalletDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generate: async (xClientId: string, authorization: string, generateWalletDto: GenerateWalletDto, options: RawAxiosRequestConfig = {}): Promise => { + generate: async (xClientId: string, generateWalletDto: GenerateWalletDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined assertParamExists('generate', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('generate', 'authorization', authorization) // verify required parameter 'generateWalletDto' is not null or undefined assertParamExists('generate', 'generateWalletDto', generateWalletDto) const localVarPath = `/v1/wallets`; @@ -2256,6 +9928,9 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); } @@ -2282,16 +9957,14 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio * * @summary Imports a wallet * @param {string} xClientId - * @param {string} authorization * @param {ImportWalletDto} importWalletDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - importSeed: async (xClientId: string, authorization: string, importWalletDto: ImportWalletDto, options: RawAxiosRequestConfig = {}): Promise => { + importSeed: async (xClientId: string, importWalletDto: ImportWalletDto, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined assertParamExists('importSeed', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('importSeed', 'authorization', authorization) // verify required parameter 'importWalletDto' is not null or undefined assertParamExists('importSeed', 'importWalletDto', importWalletDto) const localVarPath = `/v1/wallets/import`; @@ -2308,6 +9981,9 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); } @@ -2334,15 +10010,13 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio * * @summary List the client wallets * @param {string} xClientId - * @param {string} authorization + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - list: async (xClientId: string, authorization: string, options: RawAxiosRequestConfig = {}): Promise => { + list: async (xClientId: string, authorization?: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'xClientId' is not null or undefined assertParamExists('list', 'xClientId', xClientId) - // verify required parameter 'authorization' is not null or undefined - assertParamExists('list', 'authorization', authorization) const localVarPath = `/v1/wallets`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -2357,6 +10031,9 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio // authentication GNAP required + // authentication Detached-JWS-Signature required + await setApiKeyToObject(localVarHeaderParameter, "detached-jws", configuration) + if (xClientId != null) { localVarHeaderParameter['x-client-id'] = String(xClientId); } @@ -2390,13 +10067,13 @@ export const WalletApiFp = function(configuration?: Configuration) { * * @summary Generates a new wallet * @param {string} xClientId - * @param {string} authorization * @param {GenerateWalletDto} generateWalletDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async generate(xClientId: string, authorization: string, generateWalletDto: GenerateWalletDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.generate(xClientId, authorization, generateWalletDto, options); + async generate(xClientId: string, generateWalletDto: GenerateWalletDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.generate(xClientId, generateWalletDto, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['WalletApi.generate']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -2405,13 +10082,13 @@ export const WalletApiFp = function(configuration?: Configuration) { * * @summary Imports a wallet * @param {string} xClientId - * @param {string} authorization * @param {ImportWalletDto} importWalletDto + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async importSeed(xClientId: string, authorization: string, importWalletDto: ImportWalletDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.importSeed(xClientId, authorization, importWalletDto, options); + async importSeed(xClientId: string, importWalletDto: ImportWalletDto, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.importSeed(xClientId, importWalletDto, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['WalletApi.importSeed']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -2420,11 +10097,11 @@ export const WalletApiFp = function(configuration?: Configuration) { * * @summary List the client wallets * @param {string} xClientId - * @param {string} authorization + * @param {string} [authorization] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async list(xClientId: string, authorization: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async list(xClientId: string, authorization?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.list(xClientId, authorization, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['WalletApi.list']?.[localVarOperationServerIndex]?.url; @@ -2443,41 +10120,113 @@ export const WalletApiFactory = function (configuration?: Configuration, basePat /** * * @summary Generates a new wallet - * @param {string} xClientId - * @param {string} authorization - * @param {GenerateWalletDto} generateWalletDto + * @param {WalletApiGenerateRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generate(xClientId: string, authorization: string, generateWalletDto: GenerateWalletDto, options?: any): AxiosPromise { - return localVarFp.generate(xClientId, authorization, generateWalletDto, options).then((request) => request(axios, basePath)); + generate(requestParameters: WalletApiGenerateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.generate(requestParameters.xClientId, requestParameters.generateWalletDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, /** * * @summary Imports a wallet - * @param {string} xClientId - * @param {string} authorization - * @param {ImportWalletDto} importWalletDto + * @param {WalletApiImportSeedRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - importSeed(xClientId: string, authorization: string, importWalletDto: ImportWalletDto, options?: any): AxiosPromise { - return localVarFp.importSeed(xClientId, authorization, importWalletDto, options).then((request) => request(axios, basePath)); + importSeed(requestParameters: WalletApiImportSeedRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.importSeed(requestParameters.xClientId, requestParameters.importWalletDto, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, /** * * @summary List the client wallets - * @param {string} xClientId - * @param {string} authorization + * @param {WalletApiListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - list(xClientId: string, authorization: string, options?: any): AxiosPromise { - return localVarFp.list(xClientId, authorization, options).then((request) => request(axios, basePath)); + list(requestParameters: WalletApiListRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.list(requestParameters.xClientId, requestParameters.authorization, options).then((request) => request(axios, basePath)); }, }; }; +/** + * Request parameters for generate operation in WalletApi. + * @export + * @interface WalletApiGenerateRequest + */ +export interface WalletApiGenerateRequest { + /** + * + * @type {string} + * @memberof WalletApiGenerate + */ + readonly xClientId: string + + /** + * + * @type {GenerateWalletDto} + * @memberof WalletApiGenerate + */ + readonly generateWalletDto: GenerateWalletDto + + /** + * + * @type {string} + * @memberof WalletApiGenerate + */ + readonly authorization?: string +} + +/** + * Request parameters for importSeed operation in WalletApi. + * @export + * @interface WalletApiImportSeedRequest + */ +export interface WalletApiImportSeedRequest { + /** + * + * @type {string} + * @memberof WalletApiImportSeed + */ + readonly xClientId: string + + /** + * + * @type {ImportWalletDto} + * @memberof WalletApiImportSeed + */ + readonly importWalletDto: ImportWalletDto + + /** + * + * @type {string} + * @memberof WalletApiImportSeed + */ + readonly authorization?: string +} + +/** + * Request parameters for list operation in WalletApi. + * @export + * @interface WalletApiListRequest + */ +export interface WalletApiListRequest { + /** + * + * @type {string} + * @memberof WalletApiList + */ + readonly xClientId: string + + /** + * + * @type {string} + * @memberof WalletApiList + */ + readonly authorization?: string +} + /** * WalletApi - object-oriented interface * @export @@ -2488,42 +10237,37 @@ export class WalletApi extends BaseAPI { /** * * @summary Generates a new wallet - * @param {string} xClientId - * @param {string} authorization - * @param {GenerateWalletDto} generateWalletDto + * @param {WalletApiGenerateRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof WalletApi */ - public generate(xClientId: string, authorization: string, generateWalletDto: GenerateWalletDto, options?: RawAxiosRequestConfig) { - return WalletApiFp(this.configuration).generate(xClientId, authorization, generateWalletDto, options).then((request) => request(this.axios, this.basePath)); + public generate(requestParameters: WalletApiGenerateRequest, options?: RawAxiosRequestConfig) { + return WalletApiFp(this.configuration).generate(requestParameters.xClientId, requestParameters.generateWalletDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Imports a wallet - * @param {string} xClientId - * @param {string} authorization - * @param {ImportWalletDto} importWalletDto + * @param {WalletApiImportSeedRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof WalletApi */ - public importSeed(xClientId: string, authorization: string, importWalletDto: ImportWalletDto, options?: RawAxiosRequestConfig) { - return WalletApiFp(this.configuration).importSeed(xClientId, authorization, importWalletDto, options).then((request) => request(this.axios, this.basePath)); + public importSeed(requestParameters: WalletApiImportSeedRequest, options?: RawAxiosRequestConfig) { + return WalletApiFp(this.configuration).importSeed(requestParameters.xClientId, requestParameters.importWalletDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List the client wallets - * @param {string} xClientId - * @param {string} authorization + * @param {WalletApiListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof WalletApi */ - public list(xClientId: string, authorization: string, options?: RawAxiosRequestConfig) { - return WalletApiFp(this.configuration).list(xClientId, authorization, options).then((request) => request(this.axios, this.basePath)); + public list(requestParameters: WalletApiListRequest, options?: RawAxiosRequestConfig) { + return WalletApiFp(this.configuration).list(requestParameters.xClientId, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/packages/armory-sdk/src/lib/http/client/vault/base.ts b/packages/armory-sdk/src/lib/http/client/vault/base.ts index ee423a9ba..af115d924 100644 --- a/packages/armory-sdk/src/lib/http/client/vault/base.ts +++ b/packages/armory-sdk/src/lib/http/client/vault/base.ts @@ -2,7 +2,7 @@ /* eslint-disable */ /** * Vault - * Secure storage for private keys and sensitive data, designed to protect your most critical assets in web3.0 + * Secure Enclave-backed authorization proxy for web3 secrets. Holds encrypted credentials and proxies API requests to custodians and wallet tech providers. Can also generate evm wallet private keys & sign transactions. * * The version of the OpenAPI document: 1.0 * @@ -19,7 +19,7 @@ import type { Configuration } from './configuration'; import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; import globalAxios from 'axios'; -export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); +export const BASE_PATH = "http://localhost:3011".replace(/\/+$/, ""); /** * diff --git a/packages/armory-sdk/src/lib/http/client/vault/common.ts b/packages/armory-sdk/src/lib/http/client/vault/common.ts index 210c71527..d095f5a22 100644 --- a/packages/armory-sdk/src/lib/http/client/vault/common.ts +++ b/packages/armory-sdk/src/lib/http/client/vault/common.ts @@ -2,7 +2,7 @@ /* eslint-disable */ /** * Vault - * Secure storage for private keys and sensitive data, designed to protect your most critical assets in web3.0 + * Secure Enclave-backed authorization proxy for web3 secrets. Holds encrypted credentials and proxies API requests to custodians and wallet tech providers. Can also generate evm wallet private keys & sign transactions. * * The version of the OpenAPI document: 1.0 * diff --git a/packages/armory-sdk/src/lib/http/client/vault/configuration.ts b/packages/armory-sdk/src/lib/http/client/vault/configuration.ts index af17d53c5..edb19b2a8 100644 --- a/packages/armory-sdk/src/lib/http/client/vault/configuration.ts +++ b/packages/armory-sdk/src/lib/http/client/vault/configuration.ts @@ -2,7 +2,7 @@ /* eslint-disable */ /** * Vault - * Secure storage for private keys and sensitive data, designed to protect your most critical assets in web3.0 + * Secure Enclave-backed authorization proxy for web3 secrets. Holds encrypted credentials and proxies API requests to custodians and wallet tech providers. Can also generate evm wallet private keys & sign transactions. * * The version of the OpenAPI document: 1.0 * diff --git a/packages/armory-sdk/src/lib/http/client/vault/index.ts b/packages/armory-sdk/src/lib/http/client/vault/index.ts index a317f1912..a5ee35e24 100644 --- a/packages/armory-sdk/src/lib/http/client/vault/index.ts +++ b/packages/armory-sdk/src/lib/http/client/vault/index.ts @@ -2,7 +2,7 @@ /* eslint-disable */ /** * Vault - * Secure storage for private keys and sensitive data, designed to protect your most critical assets in web3.0 + * Secure Enclave-backed authorization proxy for web3 secrets. Holds encrypted credentials and proxies API requests to custodians and wallet tech providers. Can also generate evm wallet private keys & sign transactions. * * The version of the OpenAPI document: 1.0 * diff --git a/packages/armory-sdk/src/lib/shared/__test__/unit/gnap.spec.ts b/packages/armory-sdk/src/lib/shared/__test__/unit/gnap.spec.ts index e0ed6b4a2..f39d51521 100644 --- a/packages/armory-sdk/src/lib/shared/__test__/unit/gnap.spec.ts +++ b/packages/armory-sdk/src/lib/shared/__test__/unit/gnap.spec.ts @@ -1,7 +1,16 @@ -import { SigningAlg, buildSignerForAlg, privateKeyToJwk } from '@narval/signature' +import { + SigningAlg, + buildSignerForAlg, + hash, + hexToBase64Url, + privateKeyToHex, + privateKeyToJwk, + secp256k1PrivateKeyToPublicJwk, + verifyJwsd +} from '@narval/signature' import { AxiosHeaders, InternalAxiosRequestConfig } from 'axios' import { generatePrivateKey } from 'viem/accounts' -import { REQUEST_HEADER_DETACHED_JWS, interceptRequestAddDetachedJwsHeader } from '../../gnap' +import { REQUEST_HEADER_DETACHED_JWS, interceptRequestAddDetachedJwsHeader, parseToken } from '../../gnap' import { Signer } from '../../type' describe('interceptRequestAddDetachedJwsHeader', () => { @@ -20,7 +29,8 @@ describe('interceptRequestAddDetachedJwsHeader', () => { interceptor = interceptRequestAddDetachedJwsHeader(signer) }) - it(`adds ${REQUEST_HEADER_DETACHED_JWS} header with jws`, async () => { + it(`adds ${REQUEST_HEADER_DETACHED_JWS} header with jws, bound to Authorization header token`, async () => { + expect.assertions(2) const data = { foo: 'bar' } const config = { transitional: { @@ -50,6 +60,87 @@ describe('interceptRequestAddDetachedJwsHeader', () => { const signature = actualConfig.headers.get(REQUEST_HEADER_DETACHED_JWS) + const verified = await verifyJwsd( + signature as string, + secp256k1PrivateKeyToPublicJwk(await privateKeyToHex(signer.jwk)), + { + uri: 'http://localhost:3011/accounts/import', + htm: 'POST', + maxTokenAge: 60, + requestBody: data + } + ) + + expect(signature).toEqual(expect.any(String)) + expect(verified.header.ath).toEqual( + hexToBase64Url( + hash( + 'eyJhbGciOiJFSVAxOTEiLCJraWQiOiIweDAwNjgxM2ZlNDIwODRhZDUyM2M1ZmJjODY5NTlmNjYxNWY4MjA3NGQ3YTE5YjFiMGNiZTIyYmNkY2I2ODI3ZTUiLCJ0eXAiOiJKV1QifQ.eyJhY2Nlc3MiOlt7InBlcm1pc3Npb25zIjpbIndhbGxldDppbXBvcnQiLCJ3YWxsZXQ6Y3JlYXRlIiwid2FsbGV0OnJlYWQiXSwicmVzb3VyY2UiOiJ2YXVsdCJ9XSwiY25mIjp7ImFsZyI6IkVTMjU2SyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6IjB4YTc2MzQzMTAyYzMwMjM5MTIxYmIxZDUyOWY3ODg3OTI1Y2ZhYTlmMTY4NGIxOWE0NjlmOGQ3YzU1MzgwNThiZiIsImt0eSI6IkVDIiwieCI6Ik11N2kxYTdrSE1ZS3lERzB3NTBsN19HU0pKTjhiSERhWGswQzMxVmItUVkiLCJ5IjoiclhBZVN6RDUteGhZSEY4TFlDX3lkaW5pWEhTbVdnWldFUnU3UlFVUFhtayJ9LCJleHAiOjE3MTkyMTk1ODUsImlhdCI6MTcxOTIxODk4NSwiaXNzIjoiNzI4NmM4MjEtMzg5NC00MDhiLWJmOGEtMzk3N2M5ZDU0MzRkLmFybW9yeS5uYXJ2YWwueHl6Iiwic3ViIjoiZDMzYmRjMDYtODk2My00M2ExLTkyYWQtOTcwZTUyZjRjZTE0In0.xZkmWN3zjbNrZqulkfHz01wFeIGNwGFDvr528s4EnHQ2qStIwBXNeimmtlJRoGQzlPrlrWCCmpS_3PW7VJ1tbBs' + ) + ) + ) + }) + + it(`adds ${REQUEST_HEADER_DETACHED_JWS} header even without Authorization header`, async () => { + // expect.assertions(2) + const data = { foo: 'bar' } + const config = { + transitional: { + silentJSONParsing: true, + forcedJSONParsing: true, + clarifyTimeoutError: false + }, + adapter: ['xhr', 'http', 'fetch'], + timeout: 0, + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN', + maxContentLength: -1, + maxBodyLength: -1, + headers: new AxiosHeaders({ + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'x-client-id': '7286c821-3894-408b-bf8a-3977c9d5434d' + }), + method: 'post', + data: JSON.stringify(data), + url: 'http://localhost:3011/accounts/import' + } + + const actualConfig = await interceptor(config) + + const signature = actualConfig.headers.get(REQUEST_HEADER_DETACHED_JWS) + + const verified = await verifyJwsd( + signature as string, + secp256k1PrivateKeyToPublicJwk(await privateKeyToHex(signer.jwk)), + { + uri: 'http://localhost:3011/accounts/import', + htm: 'POST', + maxTokenAge: 60, + requestBody: data + } + ) + expect(signature).toEqual(expect.any(String)) + expect(verified.header.ath).toBeUndefined() + }) +}) + +describe('parseToken', () => { + it('removes GNAP prefix', () => { + expect(parseToken('GNAP 123AAa4567890')).toEqual('123AAa4567890') + }) + + it('removes bearer prefix', () => { + expect(parseToken('bearer 1234567890')).toEqual('1234567890') + }) + + it('trims whitespace', () => { + expect(parseToken(' GNAP 1234567890 ')).toEqual('1234567890') + }) + + it('handles mixed case', () => { + expect(parseToken('Bearer 1234567890')).toEqual('1234567890') + expect(parseToken('gnap 1234567890')).toEqual('1234567890') }) }) diff --git a/packages/armory-sdk/src/lib/shared/gnap.ts b/packages/armory-sdk/src/lib/shared/gnap.ts index 3e9bb7fa4..d06e4ffa9 100644 --- a/packages/armory-sdk/src/lib/shared/gnap.ts +++ b/packages/armory-sdk/src/lib/shared/gnap.ts @@ -1,5 +1,5 @@ import { AccessToken } from '@narval/policy-engine-shared' -import { Jwk, JwsdHeader, Payload, SigningAlg, hash, hexToBase64Url, signJwsd } from '@narval/signature' +import { Jwk, JwsdHeader, SigningAlg, hash, hexToBase64Url, signJwsd } from '@narval/signature' import assert from 'assert' import { InternalAxiosRequestConfig } from 'axios' import { ArmorySdkException } from '../exceptions' @@ -19,7 +19,7 @@ type BuildJwsdHeader = { htm: Htm jwk: Jwk alg?: SigningAlg - accessToken: AccessToken + accessToken?: AccessToken } const buildJwsdHeader = (args: BuildJwsdHeader): JwsdHeader => { @@ -34,6 +34,7 @@ const buildJwsdHeader = (args: BuildJwsdHeader): JwsdHeader => { } }) } + const now = Math.floor(Date.now() / 1000) // Now in seconds return { alg, @@ -41,14 +42,14 @@ const buildJwsdHeader = (args: BuildJwsdHeader): JwsdHeader => { typ: 'gnap-binding-jwsd', htm, uri, - created: new Date().getTime(), - ath: hexToBase64Url(hash(accessToken.value)) + created: now, + ath: accessToken ? hexToBase64Url(hash(accessToken.value)) : undefined } } export type GetJwsdProof = { - payload: Payload - accessToken: AccessToken + payload: string | object // Request body + accessToken: AccessToken | undefined uri: string htm: Htm signer: Signer @@ -67,9 +68,9 @@ export const getJwsdProof = async (args: GetJwsdProof): Promise => { return parts.join('.') } -export const getBearerToken = ({ value }: AccessToken): string => `GNAP ${value}` +export const prefixGnapToken = ({ value }: AccessToken): string => `GNAP ${value}` -export const parseToken = (value: string): string => value.replace('GNAP ', '') +export const parseToken = (value: string): string => value.trim().replace(/^(GNAP|bearer)\s+/i, '') const getHtm = (method: string): Htm => { switch (method.toLowerCase()) { @@ -90,23 +91,21 @@ export const interceptRequestAddDetachedJwsHeader = assert(config.url !== undefined, 'Missing request URL') assert(config.method !== undefined, 'Missing request method') - const bearerToken = config.headers['Authorization'] || config.headers['authorization'] + const authorizationHeader = config.headers['Authorization'] || config.headers['authorization'] - if (bearerToken) { - const token = parseToken(bearerToken) - const htm = getHtm(config.method) - const payload = config.data ? JSON.parse(config.data) : {} + const token = authorizationHeader ? parseToken(authorizationHeader) : undefined + const htm = getHtm(config.method) + const payload = config.data ? JSON.parse(config.data) : {} - const signature = await getJwsdProof({ - accessToken: { value: token }, - htm, - payload, - signer: signer, - uri: config.url - }) + const signature = await getJwsdProof({ + accessToken: token ? { value: token } : undefined, + htm, + payload, + signer: signer, + uri: config.url + }) - config.headers[REQUEST_HEADER_DETACHED_JWS] = signature - } + config.headers[REQUEST_HEADER_DETACHED_JWS] = signature return config } diff --git a/packages/armory-sdk/src/lib/vault/client.ts b/packages/armory-sdk/src/lib/vault/client.ts index 27316df33..d390e6a90 100644 --- a/packages/armory-sdk/src/lib/vault/client.ts +++ b/packages/armory-sdk/src/lib/vault/client.ts @@ -1,6 +1,6 @@ import { AccessToken, Request, SerializedSignableRequest } from '@narval/policy-engine-shared' import { RsaPublicKey, rsaEncrypt } from '@narval/signature' -import axios from 'axios' +import axios, { AxiosResponse } from 'axios' import { AccountApiFactory, AccountDto, @@ -10,34 +10,71 @@ import { ClientDto, Configuration, CreateClientDto, + CreateConnectionDto, DeriveAccountDto, DeriveAccountResponseDto, EncryptionKeyApiFactory, GenerateWalletDto, ImportPrivateKeyDto, ImportWalletDto, + InitiateConnectionDto, + PaginatedAccountsDto, + PaginatedAddressesDto, + PaginatedAssetsDto, + PaginatedConnectionsDto, + PaginatedKnownDestinationsDto, + PaginatedNetworksDto, + PaginatedRawAccountsDto, + PaginatedScopedSyncsDto, + PaginatedSyncsDto, + PaginatedWalletsDto, PongDto, + ProviderAccountApiFactory, + ProviderAccountDto, + ProviderAddressApiFactory, + ProviderAssetApiFactory, + ProviderConnectionApiFactory, + ProviderConnectionDto, + ProviderKnownDestinationApiFactory, + ProviderNetworkApiFactory, + ProviderPendingConnectionDto, + ProviderProxyApiDeleteRequest, + ProviderProxyApiFactory, + ProviderProxyApiGetRequest, + ProviderProxyApiHeadRequest, + ProviderProxyApiOptionsRequest, + ProviderProxyApiPatchRequest, + ProviderProxyApiPostRequest, + ProviderProxyApiPutRequest, + ProviderScopedSyncApiFactory, + ProviderSyncApiFactory, + ProviderTransferApiFactory, + ProviderWalletApiFactory, + ProviderWalletDto, + ScopedSyncDto, + SendTransferDto, SignApiFactory, SignatureDto, + StartScopedSyncDto, + TransferDto, + UpdateConnectionDto, WalletApiFactory, WalletDto, WalletsDto } from '../http/client/vault' -import { getBearerToken, interceptRequestAddDetachedJwsHeader } from '../shared/gnap' -import { - AccountHttp, - EncryptionKeyHttp, - SignHttp, - VaultAdminConfig, - VaultClientHttp, - VaultConfig, - WalletHttp -} from './type' +import { interceptRequestAddDetachedJwsHeader, prefixGnapToken } from '../shared/gnap' +import { VaultAdminConfig, VaultConfig } from './type' + +interface RequestPagination { + cursor?: string + limit?: number + desc?: 'true' | 'false' +} export class VaultAdminClient { private config: VaultAdminConfig - private clientHttp: VaultClientHttp + private clientHttp constructor(config: VaultAdminConfig) { const httpConfig = new Configuration({ @@ -51,7 +88,10 @@ export class VaultAdminClient { } async createClient(input: CreateClientDto): Promise { - const { data } = await this.clientHttp.create(this.config.adminApiKey, input) + const { data } = await this.clientHttp.create({ + xApiKey: this.config.adminApiKey, + createClientDto: input + }) return data } @@ -60,15 +100,37 @@ export class VaultAdminClient { export class VaultClient { private config: VaultConfig - private encryptionKeyHttp: EncryptionKeyHttp + private encryptionKeyHttp + + private walletHttp + + private accountHttp + + private signHttp + + private providerNetworkHttp + + private providerAssetHttp + + private providerConnectionHttp + + private providerSyncHttp + + private providerScopedSyncHttp + + private providerWalletHttp + + private providerAccountHttp - private walletHttp: WalletHttp + private providerAddressHttp - private accountHttp: AccountHttp + private providerKnownDestinationHttp - private signHttp: SignHttp + private providerTransferHttp - private applicationApi: ApplicationApi + private providerProxyHttp + + private applicationApi constructor(config: VaultConfig) { const httpConfig = new Configuration({ @@ -81,11 +143,24 @@ export class VaultClient { this.config = config + // Local keygen & signing clients this.walletHttp = WalletApiFactory(httpConfig, config.host, axiosInstance) this.encryptionKeyHttp = EncryptionKeyApiFactory(httpConfig, config.host, axiosInstance) this.accountHttp = AccountApiFactory(httpConfig, config.host, axiosInstance) this.signHttp = SignApiFactory(httpConfig, config.host, axiosInstance) + // Provider API clients + this.providerConnectionHttp = ProviderConnectionApiFactory(httpConfig, config.host, axiosInstance) + this.providerSyncHttp = ProviderSyncApiFactory(httpConfig, config.host, axiosInstance) + this.providerScopedSyncHttp = ProviderScopedSyncApiFactory(httpConfig, config.host, axiosInstance) + this.providerWalletHttp = ProviderWalletApiFactory(httpConfig, config.host, axiosInstance) + this.providerAccountHttp = ProviderAccountApiFactory(httpConfig, config.host, axiosInstance) + this.providerAddressHttp = ProviderAddressApiFactory(httpConfig, config.host, axiosInstance) + this.providerKnownDestinationHttp = ProviderKnownDestinationApiFactory(httpConfig, config.host, axiosInstance) + this.providerTransferHttp = ProviderTransferApiFactory(httpConfig, config.host, axiosInstance) + this.providerProxyHttp = ProviderProxyApiFactory(httpConfig, config.host, axiosInstance) + this.providerNetworkHttp = ProviderNetworkApiFactory(httpConfig, config.host, axiosInstance) + this.providerAssetHttp = ProviderAssetApiFactory(httpConfig, config.host, axiosInstance) this.applicationApi = new ApplicationApi(httpConfig, config.host, axiosInstance) } @@ -95,10 +170,13 @@ export class VaultClient { return data } - async generateEncryptionKey({ accessToken }: { accessToken: AccessToken }): Promise { - const token = getBearerToken(accessToken) + async generateEncryptionKey({ accessToken }: { accessToken?: AccessToken } = {}): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined - const { data: encryptionKey } = await this.encryptionKeyHttp.generate(this.config.clientId, token) + const { data: encryptionKey } = await this.encryptionKeyHttp.generate({ + xClientId: this.config.clientId, + authorization: token + }) return encryptionKey.publicKey } @@ -111,9 +189,13 @@ export class VaultClient { accessToken: AccessToken }): Promise { const payload = data || {} - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) - const { data: wallet } = await this.walletHttp.generate(this.config.clientId, token, payload) + const { data: wallet } = await this.walletHttp.generate({ + xClientId: this.config.clientId, + authorization: token, + generateWalletDto: payload + }) return wallet } @@ -127,20 +209,27 @@ export class VaultClient { encryptionKey: RsaPublicKey accessToken: AccessToken }): Promise { - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) const { seed, ...options } = data const encryptedSeed = await rsaEncrypt(seed, encryptionKey) const payload = { ...options, encryptedSeed } - const { data: wallet } = await this.walletHttp.importSeed(this.config.clientId, token, payload) + const { data: wallet } = await this.walletHttp.importSeed({ + xClientId: this.config.clientId, + authorization: token, + importWalletDto: payload + }) return wallet } async listWallets({ accessToken }: { accessToken: AccessToken }): Promise { - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) - const { data: wallets } = await this.walletHttp.list(this.config.clientId, token) + const { data: wallets } = await this.walletHttp.list({ + xClientId: this.config.clientId, + authorization: token + }) return wallets } @@ -152,9 +241,13 @@ export class VaultClient { data: DeriveAccountDto accessToken: AccessToken }): Promise { - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) - const { data: account } = await this.accountHttp.derive(this.config.clientId, token, data) + const { data: account } = await this.accountHttp.derive({ + xClientId: this.config.clientId, + authorization: token, + deriveAccountDto: data + }) return account } @@ -170,30 +263,676 @@ export class VaultClient { }): Promise { const { privateKey, ...options } = data const encryptedPrivateKey = await rsaEncrypt(privateKey, encryptionKey) - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) const payload = { ...options, encryptedPrivateKey } - const { data: account } = await this.accountHttp.importPrivateKey(this.config.clientId, token, payload) + const { data: account } = await this.accountHttp.importPrivateKey({ + xClientId: this.config.clientId, + authorization: token, + importPrivateKeyDto: payload + }) return account } async listAccounts({ accessToken }: { accessToken: AccessToken }): Promise { - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) - const { data: accounts } = await this.accountHttp.list(this.config.clientId, token) + const { data: accounts } = await this.accountHttp.list({ + xClientId: this.config.clientId, + authorization: token + }) return accounts } async sign({ data, accessToken }: { data: Request; accessToken: AccessToken }): Promise { - const token = getBearerToken(accessToken) + const token = prefixGnapToken(accessToken) const parsedRequest = Request.parse(data) - const { data: signature } = await this.signHttp.sign(this.config.clientId, token, { - request: SerializedSignableRequest.parse(parsedRequest) + const { data: signature } = await this.signHttp.sign({ + xClientId: this.config.clientId, + authorization: token, + signRequestDto: { + request: SerializedSignableRequest.parse(parsedRequest) + } }) return signature } + + async listNetworks({ + accessToken, + pagination + }: { + accessToken?: AccessToken + pagination?: RequestPagination + } = {}): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data } = await this.providerNetworkHttp.list({ + xClientId: this.config.clientId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return data + } + + async listAssets({ + accessToken, + pagination + }: { + accessToken?: AccessToken + pagination?: RequestPagination + } = {}): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data } = await this.providerAssetHttp.list({ + xClientId: this.config.clientId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return data + } + + /** + * Provider Connection + */ + + async createConnection({ + data, + accessToken + }: { + data: CreateConnectionDto + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: connection } = await this.providerConnectionHttp.create({ + xClientId: this.config.clientId, + authorization: token, + createConnectionDto: data + }) + + return connection + } + + async initiateConnection({ + data, + accessToken + }: { + data: InitiateConnectionDto + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: connection } = await this.providerConnectionHttp.initiate({ + xClientId: this.config.clientId, + authorization: token, + initiateConnectionDto: data + }) + + return connection + } + + async listConnections({ + accessToken, + pagination + }: { + accessToken?: AccessToken + pagination?: RequestPagination + } = {}): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: connections } = await this.providerConnectionHttp.list({ + xClientId: this.config.clientId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return connections + } + + async getConnection({ + connectionId, + accessToken + }: { + connectionId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: connection } = await this.providerConnectionHttp.getById({ + xClientId: this.config.clientId, + connectionId, + authorization: token + }) + + return connection + } + + async revokeConnection({ + connectionId, + accessToken + }: { + connectionId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + await this.providerConnectionHttp.revoke({ xClientId: this.config.clientId, connectionId, authorization: token }) + } + + async updateConnection({ + connectionId, + data, + accessToken + }: { + connectionId: string + data: UpdateConnectionDto + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: connection } = await this.providerConnectionHttp.update({ + xClientId: this.config.clientId, + connectionId, + authorization: token, + updateConnectionDto: data + }) + + return connection + } + + async listProviderAccounts({ + connectionId, + walletId, + accessToken, + pagination + }: { + connectionId: string + walletId?: string + accessToken?: AccessToken + pagination?: RequestPagination + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + if (walletId) { + const { data: accounts } = await this.providerWalletHttp.listAccounts({ + xClientId: this.config.clientId, + walletId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return accounts + } + + const { data: accounts } = await this.providerAccountHttp.list({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return accounts + } + + async listProviderRawAccounts({ + accessToken, + assetId, + connectionId, + includeAddress, + namePrefix, + nameSuffix, + networkId, + pagination + }: { + accessToken?: AccessToken + assetId?: string + connectionId: string + includeAddress?: boolean + namePrefix?: string + nameSuffix?: string + networkId?: string + pagination?: RequestPagination + walletId?: string + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data } = await this.providerAccountHttp.listRaw({ + assetId, + includeAddress, + namePrefix, + nameSuffix, + networkId, + authorization: token, + cursor: pagination?.cursor, + desc: pagination?.desc, + limit: pagination?.limit, + xClientId: this.config.clientId, + xConnectionId: connectionId + }) + + return data + } + + /** + * @deprecated Use listScopedSyncs() instead. + */ + async listSyncs({ + connectionId, + accessToken, + pagination + }: { + connectionId: string + accessToken?: AccessToken + pagination?: RequestPagination + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: syncs } = await this.providerSyncHttp.list({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return syncs + } + + /** + * Provider Wallet + */ + + async getProviderWallet({ + walletId, + accessToken + }: { + walletId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: wallet } = await this.providerWalletHttp.getById({ + xClientId: this.config.clientId, + walletId, + authorization: token + }) + + return wallet + } + + async listProviderWallets({ + connectionId, + accessToken, + pagination + }: { + connectionId: string + accessToken?: AccessToken + pagination?: RequestPagination + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: wallets } = await this.providerWalletHttp.list({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return wallets + } + + /** + * Provider Account + */ + + async getProviderAccount({ + accountId, + accessToken + }: { + accountId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: account } = await this.providerAccountHttp.getById({ + xClientId: this.config.clientId, + accountId, + authorization: token + }) + + return account + } + + async listProviderAddresses({ + accountId, + accessToken, + pagination + }: { + accountId?: string + accessToken?: AccessToken + pagination?: RequestPagination + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + if (!accountId) { + const { data: addresses } = await this.providerAddressHttp.list({ + xClientId: this.config.clientId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + return addresses + } + + const { data: addresses } = await this.providerAccountHttp.listAddresses({ + xClientId: this.config.clientId, + accountId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return addresses + } + + /** + * Provider Address + */ + + async getProviderAddress({ + addressId, + accessToken + }: { + addressId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: address } = await this.providerAddressHttp.getById({ + xClientId: this.config.clientId, + addressId, + authorization: token + }) + + return address + } + + /** + * Provider Known Destination + */ + + async listProviderKnownDestinations({ + connectionId, + accessToken, + pagination + }: { + connectionId: string + accessToken?: AccessToken + pagination?: RequestPagination + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: knownDestinations } = await this.providerKnownDestinationHttp.list({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return knownDestinations + } + + /** + * Provider Transfer + */ + + async sendTransfer({ + data, + connectionId, + accessToken + }: { + data: SendTransferDto + connectionId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: transfer } = await this.providerTransferHttp.send({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + authorization: token, + sendTransferDto: data + }) + + return transfer + } + + async getTransfer({ + transferId, + connectionId, + accessToken + }: { + transferId: string + connectionId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: transfer } = await this.providerTransferHttp.getById({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + transferId, + authorization: token + }) + + return transfer + } + + /** + * Provider Proxy + */ + + async proxyDelete({ + data, + accessToken + }: { + data: ProviderProxyApiDeleteRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp._delete({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async proxyOptions({ + data, + accessToken + }: { + data: ProviderProxyApiOptionsRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp._options({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async proxyGet({ + data, + accessToken + }: { + data: ProviderProxyApiGetRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp.get({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async proxyPost({ + data, + accessToken + }: { + data: ProviderProxyApiPostRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp.post({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async proxyPut({ + data, + accessToken + }: { + data: ProviderProxyApiPutRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp.put({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async proxyPatch({ + data, + accessToken + }: { + data: ProviderProxyApiPatchRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp.patch({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async proxyHead({ + data, + accessToken + }: { + data: ProviderProxyApiHeadRequest + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const ret = await this.providerProxyHttp.head({ + xClientId: this.config.clientId, + authorization: token, + endpoint: data.endpoint, + xConnectionId: data.xConnectionId + }) + return ret + } + + async scopedSync({ data, accessToken }: { data: StartScopedSyncDto; accessToken?: AccessToken }) { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: sync } = await this.providerScopedSyncHttp.start({ + xClientId: this.config.clientId, + authorization: token, + startScopedSyncDto: data + }) + + return sync + } + + async getScopedSync({ + scopedSyncId, + accessToken, + connectionId + }: { + scopedSyncId: string + connectionId: string + accessToken?: AccessToken + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: sync } = await this.providerScopedSyncHttp.getById({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + scopedSyncId, + authorization: token + }) + + return sync + } + + async listScopedSyncs({ + connectionId, + accessToken, + pagination + }: { + connectionId: string + accessToken?: AccessToken + pagination?: RequestPagination + }): Promise { + const token = accessToken ? prefixGnapToken(accessToken) : undefined + + const { data: scopedSyncs } = await this.providerScopedSyncHttp.list({ + xClientId: this.config.clientId, + xConnectionId: connectionId, + authorization: token, + cursor: pagination?.cursor, + limit: pagination?.limit, + desc: pagination?.desc + }) + + return scopedSyncs + } } diff --git a/packages/armory-sdk/src/lib/vault/type.ts b/packages/armory-sdk/src/lib/vault/type.ts index 053b7df1f..0e673733c 100644 --- a/packages/armory-sdk/src/lib/vault/type.ts +++ b/packages/armory-sdk/src/lib/vault/type.ts @@ -1,22 +1,5 @@ -import { AxiosPromise, RawAxiosRequestConfig } from 'axios' import { z } from 'zod' -import { - AccountDto, - AccountsDto, - CreateClientDto, - ClientDto as CreateVaultClientResponse, - DeriveAccountDto, - DeriveAccountResponseDto, - EncryptionKeyDto, - GenerateWalletDto, - ImportPrivateKeyDto, - ImportWalletDto, - SignRequestDto, - SignatureDto, - WalletDto, - WalletsDto -} from '../http/client/vault' -import { RequiredError } from '../http/client/vault/base' +import { ClientDto as CreateVaultClientResponse } from '../http/client/vault' import { Signer } from '../shared/type' export type { CreateVaultClientResponse } @@ -33,137 +16,3 @@ export const VaultConfig = z.object({ clientId: z.string().describe('The client ID') }) export type VaultConfig = z.infer - -export type VaultClientHttp = { - /** - * Creates a new client - * - * @param {string} apiKey - * @param {CreateClientDto} data - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - create( - apiKey: string, - data: CreateClientDto, - options?: RawAxiosRequestConfig - ): AxiosPromise -} - -export type EncryptionKeyHttp = { - /** - * Generates an encryption key pair used to secure end-to-end - * communication containing sensitive information. - * - * @param {string} clientId - * @param {string} accessToken - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - generate(clientId: string, accessToken: string, options?: RawAxiosRequestConfig): AxiosPromise -} - -export type WalletHttp = { - /** - * Generates a new wallet. - * - * @param {string} clientId - * @param {string} accessToken - * @param {GenerateWalletDto} data - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - generate( - clientId: string, - accessToken: string, - data: GenerateWalletDto, - options?: RawAxiosRequestConfig - ): AxiosPromise - - /** - * Imports a wallet. - * - * @param {string} clientId - * @param {string} accessToken - * @param {ImportWalletDto} data - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - importSeed( - clientId: string, - accessToken: string, - data: ImportWalletDto, - options?: RawAxiosRequestConfig - ): AxiosPromise - - /** - * List the client wallets. - * - * @param {string} clientId - * @param {string} accessToken - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - list(clientId: string, accessToken: string, options?: RawAxiosRequestConfig): AxiosPromise -} - -export type AccountHttp = { - /** - * Add a new account to a wallet - * - * @param {string} clientId - * @param {string} accessToken - * @param {DeriveAccountDto} data - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - derive( - clientId: string, - accessToken: string, - data: DeriveAccountDto, - options?: RawAxiosRequestConfig - ): AxiosPromise - - /** - * Imports an account - * - * @param {string} clientId - * @param {string} accessToken - * @param {ImportPrivateKeyDto} data - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - importPrivateKey( - clientId: string, - accessToken: string, - data: ImportPrivateKeyDto, - options?: RawAxiosRequestConfig - ): AxiosPromise - - /** - * Lists the client accounts - * - * @param {string} clientId - * @param {RawAxiosRequestConfig} [options] Override http request option. - * @throws {RequiredError} - */ - list(clientId: string, accessToken: string, options?: RawAxiosRequestConfig): AxiosPromise -} - -export type SignHttp = { - /** - * Signs the given request. - * - * @param {string} clientId - * @param {string} accessToken - * @param {SignRequestDto} data - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - sign( - clientId: string, - accessToken: string, - data: SignRequestDto, - options?: RawAxiosRequestConfig - ): AxiosPromise -} diff --git a/packages/nestjs-shared/src/lib/__test__/unit/pagination.spec.ts b/packages/nestjs-shared/src/lib/__test__/unit/pagination.spec.ts new file mode 100644 index 000000000..ffdd9170d --- /dev/null +++ b/packages/nestjs-shared/src/lib/__test__/unit/pagination.spec.ts @@ -0,0 +1,255 @@ +import { DEFAULT_QUERY_PAGINATION_LIMIT } from '../../constant' +import { PageCursorEncoder, PaginationQuery } from '../../type/pagination.type' +import { getPaginatedResult, getPaginationQuery } from '../../util/pagination.util' + +describe('Pagination System', () => { + const generateMockData = (count: number) => + Array.from({ length: count }, (_, i) => ({ + id: `id${String(i + 1).padStart(3, '0')}`, + createdAt: new Date(2024, 0, i + 1), + name: `Item ${i + 1}` + })) + + const mockData = generateMockData(50) + + describe('getPaginationQuery', () => { + describe('Forward Pagination', () => { + it('returns default query parameters', () => { + const query = getPaginationQuery({}) + expect(query).toEqual({ + orderBy: undefined, + take: DEFAULT_QUERY_PAGINATION_LIMIT, + skip: undefined, + cursor: undefined + }) + }) + + it('applies custom limit', () => { + const options = PaginationQuery.parse({ + limit: 10, + direction: 'next' + }) + + const query = getPaginationQuery({ options }) + expect(query).toEqual({ + take: 10, + orderBy: undefined, + cursor: undefined, + skip: undefined + }) + }) + + it('handles ascending sort order', () => { + const options = PaginationQuery.parse({ + sortOrder: 'asc', + direction: 'next' + }) + + const query = getPaginationQuery({ options }) + expect(query).toEqual({ + take: 25, + orderBy: undefined, + sortOrder: 'asc', + cursor: undefined, + skip: undefined + }) + }) + }) + + describe('Backward Pagination', () => { + it('handles prev direction with cursor', () => { + const cursor = PageCursorEncoder.parse({ + id: 'id010', + createdAt: new Date('2024-01-10') + }) + + const options = PaginationQuery.parse({ + cursor, + direction: 'prev' + }) + + const query = getPaginationQuery({ options }) + expect(query).toEqual({ + take: -25, + cursor: { + id: 'id010', + createdAt: new Date('2024-01-10') + }, + skip: 1, + orderBy: undefined + }) + }) + }) + }) + + describe('getPaginatedResult', () => { + describe('Edge Cases', () => { + it('returns empty result for no items', () => { + const result = getPaginatedResult({ items: [] }) + expect(result).toEqual({ + data: [], + page: { next: null } + }) + }) + + it('returns cursor to item when take === 1', () => { + const result = getPaginatedResult({ + items: [mockData[0]], + pagination: { take: 1 } + }) + expect(result).toEqual({ + data: [{ ...mockData[0] }], + page: { + next: PageCursorEncoder.parse({ + id: mockData[0].id, + createdAt: mockData[0].createdAt + }) + } + }) + }) + + it('handles items with same timestamp', () => { + const sameTimeItems = [ + { id: 'id001', createdAt: new Date('2024-01-01T00:00:00Z'), name: 'A' }, + { id: 'id002', createdAt: new Date('2024-01-01T00:00:00Z'), name: 'B' }, + { id: 'id003', createdAt: new Date('2024-01-01T00:00:00Z'), name: 'C' } + ] + + const result = getPaginatedResult({ + items: sameTimeItems, + pagination: { take: 3 } + }) + + expect(result.data).toHaveLength(2) + expect(result.page?.next).not.toBeNull() + }) + }) + + describe('Forward Pagination', () => { + it('generates cursor when more items exist', () => { + const items = mockData.slice(0, 11) + const result = getPaginatedResult({ + items, + pagination: { take: 11 } + }) + + expect(result.data).toHaveLength(10) + expect(result.page?.next).not.toBeNull() + + const decodedCursor = Buffer.from(result.page!.next!, 'base64').toString() + const [timestamp] = decodedCursor.split('|') + expect(new Date(timestamp)).toEqual(items[9].createdAt) + }) + }) + + describe('Backward Pagination', () => { + it('paginates through dataset backwards', () => { + const cursor = PageCursorEncoder.parse({ + id: 'id025', + createdAt: new Date('2024-01-25') + }) + + const options = PaginationQuery.parse({ + cursor, + direction: 'prev', + limit: 10 + }) + + const query = getPaginationQuery({ options }) + const items = mockData.slice(10, 20).reverse() // Important: reverse items for prev direction + + const result = getPaginatedResult({ + items, + pagination: query + }) + + expect(result.data).toHaveLength(9) + expect(result.page?.next).not.toBeNull() + }) + + it('handles first page in prev direction', () => { + const cursor = PageCursorEncoder.parse({ + id: 'id010', + createdAt: new Date('2024-01-10') + }) + + const options = PaginationQuery.parse({ + cursor, + direction: 'prev' + }) + + const query = getPaginationQuery({ options }) + const items = mockData.slice(0, 9).reverse() // Important: reverse for prev direction + + const result = getPaginatedResult({ + items, + pagination: { ...query, take: Math.abs(query.take!) } + }) + + expect(result.data).toHaveLength(9) + expect(result.page?.next).toBeNull() + }) + }) + + describe('Bidirectional Navigation', () => { + it('allows switching between next and prev direction', () => { + // Forward pagination + let options = PaginationQuery.parse({ + limit: 10, + direction: 'next' + }) + + let query = getPaginationQuery({ options }) + let items = mockData.slice(0, 10) + let result = getPaginatedResult({ items, pagination: query }) + + expect(result.data).toHaveLength(9) + expect(result.page?.next).not.toBeNull() + + // Backward pagination from last item + const cursor = result.page!.next! + options = PaginationQuery.parse({ + cursor, + limit: 5, + direction: 'prev' + }) + + query = getPaginationQuery({ options }) + items = mockData.slice(0, 5).reverse() + result = getPaginatedResult({ + items, + pagination: { ...query, take: Math.abs(query.take!) } + }) + + expect(result.data).toHaveLength(4) + expect(result.page?.next).not.toBeNull() + }) + }) + + it('generates correct cursor for both directions', () => { + const items = generateMockData(4) + + // Forward pagination + const forwardResult = getPaginatedResult({ + items, + pagination: { take: 4 } + }) + + const forwardCursor = Buffer.from(forwardResult.page!.next!, 'base64').toString() + const [forwardTimestamp, forwardId] = forwardCursor.split('|') + expect(forwardId).toBe('id003') + expect(new Date(forwardTimestamp)).toEqual(items[2].createdAt) + + // Backward pagination + const backwardResult = getPaginatedResult({ + items: [...items].reverse(), + pagination: { take: -4 } + }) + + const backwardCursor = Buffer.from(backwardResult.page!.next!, 'base64').toString() + const [backwardTimestamp, backwardId] = backwardCursor.split('|') + expect(backwardId).toBe('id002') + expect(new Date(backwardTimestamp)).toEqual(items[1].createdAt) + }) + }) +}) diff --git a/packages/nestjs-shared/src/lib/constant.ts b/packages/nestjs-shared/src/lib/constant.ts index 8c96a04e0..ecfafb444 100644 --- a/packages/nestjs-shared/src/lib/constant.ts +++ b/packages/nestjs-shared/src/lib/constant.ts @@ -4,9 +4,29 @@ export const REQUEST_HEADER_CLIENT_ID = 'x-client-id' export const REQUEST_HEADER_CLIENT_SECRET = 'x-client-secret' +export const REQUEST_HEADER_ADMIN_API_KEY = 'x-api-key' +export const REQUEST_HEADER_DETACHED_JWS = 'detached-jws' +export const REQUEST_HEADER_AUTHORIZATION = 'Authorization' // can be GNAP, Bearer, etc // // OpenTelemetry // export const OTEL_ATTR_CLIENT_ID = 'domain.client.id' + +// +// Pagination +// + +export const MIN_QUERY_PAGINATION_LIMIT = 1 +export const DEFAULT_QUERY_PAGINATION_LIMIT = 25 +export const MAX_QUERY_PAGINATION_LIMIT = 100 +export const DEFAULT_SERVICE_PAGINATION_LIMIT = 100 +export const DEFAULT_ORDER_BY = [ + { + createdAt: 'desc' as const + }, + { + id: 'desc' as const + } +] as { [key: string]: 'asc' | 'desc' }[] diff --git a/packages/nestjs-shared/src/lib/decorator/api-client-id-header.decorator.ts b/packages/nestjs-shared/src/lib/decorator/api-client-id-header.decorator.ts new file mode 100644 index 000000000..774539d06 --- /dev/null +++ b/packages/nestjs-shared/src/lib/decorator/api-client-id-header.decorator.ts @@ -0,0 +1,15 @@ +import { applyDecorators } from '@nestjs/common' +import { ApiHeader } from '@nestjs/swagger' +import { REQUEST_HEADER_CLIENT_ID } from '../constant' + +// We support Authentication through a jwsd that does NOT require an access token as well. +// This decorator is simply to flag that a signature is required. If using combined with GNAP, use the api-gnap-security decorator. + +export function ApiClientIdHeader() { + return applyDecorators( + ApiHeader({ + name: REQUEST_HEADER_CLIENT_ID, + required: true + }) + ) +} diff --git a/packages/nestjs-shared/src/lib/decorator/api-detached-jws-security.decorator.ts b/packages/nestjs-shared/src/lib/decorator/api-detached-jws-security.decorator.ts new file mode 100644 index 000000000..0b7e036c4 --- /dev/null +++ b/packages/nestjs-shared/src/lib/decorator/api-detached-jws-security.decorator.ts @@ -0,0 +1,14 @@ +import { applyDecorators } from '@nestjs/common' +import { ApiSecurity } from '@nestjs/swagger' +import { securityOptions } from '../util/with-swagger.util' + +// We support Authentication through a jwsd that does NOT require an access token as well. +// This decorator is simply to flag that a signature is required. If using combined with GNAP, use the api-gnap-security decorator. + +export function ApiDetachedJwsSecurity() { + return applyDecorators( + ApiSecurity({ + [securityOptions.detachedJws.name]: [] + }) + ) +} diff --git a/packages/nestjs-shared/src/lib/decorator/api-gnap-security.decorator.ts b/packages/nestjs-shared/src/lib/decorator/api-gnap-security.decorator.ts index c385f3c9a..94dabe38a 100644 --- a/packages/nestjs-shared/src/lib/decorator/api-gnap-security.decorator.ts +++ b/packages/nestjs-shared/src/lib/decorator/api-gnap-security.decorator.ts @@ -1,13 +1,22 @@ import { applyDecorators } from '@nestjs/common' import { ApiHeader, ApiSecurity } from '@nestjs/swagger' -import { gnapSecurity } from '../util/with-swagger.util' +import { REQUEST_HEADER_AUTHORIZATION } from '../constant' +import { securityOptions } from '../util/with-swagger.util' + +// GNAP tokens are bound, so they require both the Authorization header and a signature +// We currently only support detached JWS, for key proofs, so we add the `detached-jws` header as well. +// In the future, this can also support httpsig & other key proofing options, making the detached-jws header optional. export function ApiGnapSecurity(permissions?: string[]) { return applyDecorators( - ApiSecurity(gnapSecurity().name, permissions), + ApiSecurity({ + [securityOptions.gnap.name]: permissions || [], + [securityOptions.detachedJws.name]: [] + }), + // We have to say the Authorization header is required, because the GNAP scheme isn't known by OpenAPI Generator fully, so it won't generate the code to add it. ApiHeader({ - name: 'Authorization', - required: true + name: REQUEST_HEADER_AUTHORIZATION, + required: false }) ) } diff --git a/packages/nestjs-shared/src/lib/decorator/index.ts b/packages/nestjs-shared/src/lib/decorator/index.ts index 3896a386b..27fd442c0 100644 --- a/packages/nestjs-shared/src/lib/decorator/index.ts +++ b/packages/nestjs-shared/src/lib/decorator/index.ts @@ -1,4 +1,8 @@ +export * from './api-client-id-header.decorator' +export * from './api-detached-jws-security.decorator' export * from './api-gnap-security.decorator' export * from './is-hex-string.decorator' export * from './is-not-empty-array-enum.decorator' export * from './is-not-empty-array-string.decorator' +export * from './pagination-param.decorator' +export * from './pagination.decorator' diff --git a/packages/nestjs-shared/src/lib/decorator/pagination-param.decorator.ts b/packages/nestjs-shared/src/lib/decorator/pagination-param.decorator.ts new file mode 100644 index 000000000..389b5ae61 --- /dev/null +++ b/packages/nestjs-shared/src/lib/decorator/pagination-param.decorator.ts @@ -0,0 +1,10 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common' +import { PaginationOptions, PaginationQuery } from '../type' +import { getPaginationQuery } from '../util' + +export const PaginationParam = createParamDecorator((_data: unknown, context: ExecutionContext): PaginationOptions => { + const req = context.switchToHttp().getRequest() + + const options = PaginationQuery.parse(req.query) + return getPaginationQuery({ options }) +}) diff --git a/packages/nestjs-shared/src/lib/decorator/pagination.decorator.ts b/packages/nestjs-shared/src/lib/decorator/pagination.decorator.ts new file mode 100644 index 000000000..c51852327 --- /dev/null +++ b/packages/nestjs-shared/src/lib/decorator/pagination.decorator.ts @@ -0,0 +1,42 @@ +import { applyDecorators, HttpStatus, Type } from '@nestjs/common' +import { ApiQuery, ApiResponse } from '@nestjs/swagger' + +type PaginatedDecoratorOptions = { + type: Type + summary?: string + description?: string +} + +export function Paginated(options: PaginatedDecoratorOptions) { + return applyDecorators( + ApiQuery({ + name: 'cursor', + required: false, + type: String, + description: 'Cursor for pagination. Use the next cursor from previous response to get next page' + }), + ApiQuery({ + name: 'limit', + required: false, + type: Number, + description: 'Number of records to return per page' + }), + ApiQuery({ + name: 'orderBy', + required: false, + type: String, + description: 'Field to order results by' + }), + ApiQuery({ + name: 'desc', + required: false, + type: String, + description: 'Set to "true" or "1" for descending order' + }), + ApiResponse({ + status: HttpStatus.OK, + description: options.description || 'Successfully retrieved paginated list', + type: options.type + }) + ) +} diff --git a/packages/nestjs-shared/src/lib/module/http/http.module.ts b/packages/nestjs-shared/src/lib/module/http/http.module.ts index 80a2d3a44..cf2a3ca5d 100644 --- a/packages/nestjs-shared/src/lib/module/http/http.module.ts +++ b/packages/nestjs-shared/src/lib/module/http/http.module.ts @@ -1,35 +1,36 @@ import { HttpService, HttpModule as NestHttpModule } from '@nestjs/axios' -import { DynamicModule, Global, Module } from '@nestjs/common' +import { DynamicModule } from '@nestjs/common' import axios, { AxiosRequestConfig } from 'axios' import axiosRetry, { DEFAULT_OPTIONS, IAxiosRetryConfig } from 'axios-retry' interface AxiosRetryOptions { - axiosConfig?: AxiosRequestConfig - axiosRetryConfig?: IAxiosRetryConfig + config?: AxiosRequestConfig + retry?: IAxiosRetryConfig } /** * A module that provides retry functionality for Axios HTTP requests. - * This module can be imported in a NestJS application to enable automatic retry of failed requests. + * + * This module can be imported in a NestJS application to enable automatic + * retry of failed requests. */ -@Global() -@Module({}) export class HttpModule { /** * Creates a dynamic module for the AxiosRetryModule. + * * @param options - Optional configuration options for the retry behavior. * @returns A dynamic module that can be imported in a NestJS application. */ - static forRoot( + static register( options: AxiosRetryOptions = { - axiosRetryConfig: { + retry: { ...DEFAULT_OPTIONS, retries: 0 // Default never retries } } ): DynamicModule { - const axiosInstance = axios.create(options.axiosConfig) - axiosRetry(axiosInstance, options.axiosRetryConfig) + const axiosInstance = axios.create(options.config) + axiosRetry(axiosInstance, options.retry) const axiosProvider = { provide: HttpService, diff --git a/packages/nestjs-shared/src/lib/module/logger/logger.constant.ts b/packages/nestjs-shared/src/lib/module/logger/logger.constant.ts index fc91dfb99..b03c59846 100644 --- a/packages/nestjs-shared/src/lib/module/logger/logger.constant.ts +++ b/packages/nestjs-shared/src/lib/module/logger/logger.constant.ts @@ -6,7 +6,8 @@ export const REDACT_KEYS = [ /^pass$/i, /secret/i, /token/i, - /api[-._]?key/i + /api[-._]?key/i, + /access[-._]?key/i ] export const REDACT_REPLACE = '[REDACTED]' diff --git a/packages/nestjs-shared/src/lib/type/index.ts b/packages/nestjs-shared/src/lib/type/index.ts index b28a74e4b..041e9070b 100644 --- a/packages/nestjs-shared/src/lib/type/index.ts +++ b/packages/nestjs-shared/src/lib/type/index.ts @@ -1 +1,2 @@ export * from './json.type' +export * from './pagination.type' diff --git a/packages/nestjs-shared/src/lib/type/pagination.type.ts b/packages/nestjs-shared/src/lib/type/pagination.type.ts new file mode 100644 index 000000000..a79a7185d --- /dev/null +++ b/packages/nestjs-shared/src/lib/type/pagination.type.ts @@ -0,0 +1,69 @@ +import { z } from 'zod' + +export const Page = z + .object({ + next: z.string().nullable() + }) + .optional() +export type Page = z.infer + +export const PageCursorEncoder = z + .object({ + id: z.string(), + createdAt: z.date() + }) + .transform((data) => { + const cursorData = `${data.createdAt.toISOString()}|${data.id}` + return Buffer.from(cursorData).toString('base64') + }) +export type PageCursorEncoder = z.infer + +export const PageCursorDecoder = z + .string() + .transform((cursor) => Buffer.from(cursor, 'base64').toString()) + .pipe( + z + .string() + .regex(/^[^|]+\|[^|]+$/, 'Cursor must contain exactly one "|"') + .transform((str) => { + const [timestamp, id] = str.split('|') + return { timestamp, id } + }) + .pipe( + z + .object({ + timestamp: z.string().datetime(), + id: z.string().min(1) + }) + .transform(({ timestamp, id }) => ({ + id, + createdAt: new Date(timestamp) + })) + ) + ) +export type PageCursor = z.infer + +export const createPaginatedSchema = (itemSchema: T) => + z.object({ + data: z.array(itemSchema), + page: Page.optional() + }) + +export type PaginatedResult = z.infer>>> + +export const PaginationQuery = z.object({ + cursor: z.string().optional(), + limit: z.coerce.number().min(1).max(100).optional().default(25), + sortOrder: z.enum(['asc', 'desc']).optional(), + direction: z.enum(['prev', 'next']).optional().default('next') +}) +export type PaginationQuery = z.infer + +export type PaginationOptions = { + take?: number + cursor?: PageCursor + skip?: number + sortOrder?: 'asc' | 'desc' + orderBy?: { [key: string]: 'asc' | 'desc' }[] + disabled?: boolean +} diff --git a/packages/nestjs-shared/src/lib/util/index.ts b/packages/nestjs-shared/src/lib/util/index.ts index a2de1c1aa..b43a9b99f 100644 --- a/packages/nestjs-shared/src/lib/util/index.ts +++ b/packages/nestjs-shared/src/lib/util/index.ts @@ -1,6 +1,7 @@ export * as coerce from './coerce.util' export * as secret from './secret.util' +export * from './pagination.util' export * from './with-api-version.util' export * from './with-cors.util' export * from './with-swagger.util' diff --git a/packages/nestjs-shared/src/lib/util/pagination.util.ts b/packages/nestjs-shared/src/lib/util/pagination.util.ts new file mode 100644 index 000000000..092ec366f --- /dev/null +++ b/packages/nestjs-shared/src/lib/util/pagination.util.ts @@ -0,0 +1,105 @@ +import { DEFAULT_ORDER_BY, DEFAULT_QUERY_PAGINATION_LIMIT, DEFAULT_SERVICE_PAGINATION_LIMIT } from '../constant' +import { + PageCursorDecoder, + PageCursorEncoder, + PaginatedResult, + PaginationOptions, + PaginationQuery +} from '../type/pagination.type' + +export function getPaginatedResult({ + items, + pagination +}: { + items: T[] + pagination?: PaginationOptions +}): PaginatedResult { + // If there are no items, return an empty array + // If theres only one item, return an empty array: + // - getPaginatedResult expect take to be incremented by one. Take = 1 || -1 means a request for 0 item. + const take = pagination?.take ? Math.abs(pagination.take) : undefined + + if (!items || items.length === 0 || (take && take < 1)) { + return { + data: [], + page: { next: null } + } + } + + const hasNextPage = items.length === take + + // If there's more data and more than one item, remove the last item from the list, it is the first item of the next page + const data = + hasNextPage && items.length > 1 + ? pagination?.take && pagination.take < 0 + ? items.slice(1) // For prev direction: take last N items + : items.slice(0, -1) // For next direction: take first N items + : items + + let next = null + + // we can safely access processedItems[processedItems.length - 2] because we know for sure: + // - take > 1 + // - processedItems.length > 1 + if (hasNextPage) { + // here protecting against an edge case where take = 1 + // in this case, we return the last item as the next cursor because we didn't took one more item + const lastItem = take === 1 ? items[items.length - 1] : items[items.length - 2] + next = PageCursorEncoder.parse({ id: lastItem.id, createdAt: lastItem.createdAt }) + } + + return { + data, + page: { + next + } + } +} + +export function getPaginationQuery({ options }: { options?: PaginationQuery }): PaginationOptions { + const cursor = options?.cursor ? PageCursorDecoder.parse(options.cursor) : undefined + const multiplier = options?.direction === 'prev' ? -1 : 1 + + let take = DEFAULT_QUERY_PAGINATION_LIMIT + + if (options?.limit && options?.limit > 0) { + take = options.limit * multiplier + } + + return { + take, + cursor, + sortOrder: options?.sortOrder, + skip: options?.cursor ? 1 : undefined, + orderBy: undefined + } +} + +export const applyPagination = ( + pagination?: PaginationOptions +): { + skip?: number + cursor?: { id: string; createdAt: Date } + take?: number + orderBy: { [key: string]: 'asc' | 'desc' }[] +} => { + if (pagination?.disabled === true) { + return { + orderBy: DEFAULT_ORDER_BY + } + } + const multiplier = pagination?.take && pagination?.take < 0 ? -1 : 1 + const skip = pagination?.cursor ? 1 : undefined + const take = (Math.abs(pagination?.take || DEFAULT_SERVICE_PAGINATION_LIMIT) + 1) * multiplier + + let orderBy = DEFAULT_ORDER_BY + + if (pagination?.orderBy) { + orderBy = pagination.orderBy + } else if (pagination?.sortOrder) { + orderBy = [{ createdAt: pagination.sortOrder }, { id: pagination.sortOrder }] + } + + const ret = { take, orderBy, skip, cursor: pagination?.cursor } + return ret +} diff --git a/packages/nestjs-shared/src/lib/util/with-swagger.util.ts b/packages/nestjs-shared/src/lib/util/with-swagger.util.ts index 7211d0fdd..a512e7b9e 100644 --- a/packages/nestjs-shared/src/lib/util/with-swagger.util.ts +++ b/packages/nestjs-shared/src/lib/util/with-swagger.util.ts @@ -2,43 +2,70 @@ import { INestApplication } from '@nestjs/common' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' import { SecuritySchemeObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface' import { patchNestJsSwagger } from 'nestjs-zod' -import { SetRequired } from 'type-fest' - -type Security = SetRequired +import { + REQUEST_HEADER_ADMIN_API_KEY, + REQUEST_HEADER_CLIENT_ID, + REQUEST_HEADER_CLIENT_SECRET, + REQUEST_HEADER_DETACHED_JWS +} from '../constant' // NOTE: See https://swagger.io/docs/specification/authentication to understand // Swagger auth schema. -export const gnapSecurity = (): Security => ({ - name: 'GNAP', +const gnapSecurity: SecuritySchemeObject = { type: 'http', - in: 'header', // A scheme is what sits in front of the token `Authorization: // `. // See https://swagger.io/docs/specification/authentication/ scheme: 'GNAP' -}) +} + +const detachedJwsSecurity: SecuritySchemeObject = { + name: REQUEST_HEADER_DETACHED_JWS, + type: 'apiKey', + in: 'header' +} -export const adminApiKeySecurity = (header: string): Security => ({ - name: 'ADMIN_API_KEY', +const adminApiKeySecurity: SecuritySchemeObject = { + name: REQUEST_HEADER_ADMIN_API_KEY, type: 'apiKey', - in: 'header', - 'x-tokenName': header -}) + in: 'header' +} -export const clientIdSecurity = (header: string): Security => ({ - name: 'CLIENT_ID', +const clientIdSecurity: SecuritySchemeObject = { + name: REQUEST_HEADER_CLIENT_ID, type: 'apiKey', - in: 'header', - 'x-tokenName': header -}) + in: 'header' +} -export const clientSecretSecurity = (header: string): Security => ({ - name: 'CLIENT_SECRET', +const clientSecretSecurity: SecuritySchemeObject = { + name: REQUEST_HEADER_CLIENT_SECRET, type: 'apiKey', - in: 'header', - 'x-tokenName': header -}) + in: 'header' +} + +export const securityOptions = { + gnap: { + name: 'GNAP', + securityScheme: gnapSecurity + }, + detachedJws: { + name: 'Detached-JWS-Signature', + securityScheme: detachedJwsSecurity + }, + adminApiKey: { + name: 'Admin-API-Key', + securityScheme: adminApiKeySecurity + }, + clientId: { + name: 'Client-ID', + securityScheme: clientIdSecurity + }, + clientSecret: { + name: 'Client-Secret', + securityScheme: clientSecretSecurity + } +} /** * Adds Swagger documentation to the application. @@ -47,7 +74,13 @@ export const clientSecretSecurity = (header: string): Security => ({ * @returns The modified INestApplication instance. */ export const withSwagger = - (params: { title: string; description: string; version: string; security?: Security[] }) => + (params: { + title: string + description: string + version: string + security?: { name: string; securityScheme: SecuritySchemeObject }[] + server?: { url: string; description: string } + }) => (app: INestApplication): INestApplication => { // IMPORTANT: This modifies the Nest Swagger module to be compatible with // DTOs created by Zod schemas. The patch MUST be done before the @@ -59,8 +92,9 @@ export const withSwagger = .setTitle(params.title) .setDescription(params.description) .setVersion(params.version) + .addServer(params.server?.url || 'http://localhost:3005', params.server?.description || 'Armory Server') for (const s of security) { - documentBuilder.addSecurity(s.name, s) + documentBuilder.addSecurity(s.name, s.securityScheme) } const document = SwaggerModule.createDocument(app, documentBuilder.build()) diff --git a/packages/policy-engine-shared/src/index.ts b/packages/policy-engine-shared/src/index.ts index b64777d9b..29023286d 100644 --- a/packages/policy-engine-shared/src/index.ts +++ b/packages/policy-engine-shared/src/index.ts @@ -15,6 +15,7 @@ export * from './lib/type/policy.type' export * as EntityUtil from './lib/util/entity.util' export * from './lib/util/caip.util' +export * from './lib/util/confirmation-claim.util' export * from './lib/util/encoding.util' export * from './lib/util/enum.util' export * from './lib/util/evm.util' diff --git a/packages/policy-engine-shared/src/lib/type/authorization-server.type.ts b/packages/policy-engine-shared/src/lib/type/authorization-server.type.ts index 40f629d9a..5373b50b0 100644 --- a/packages/policy-engine-shared/src/lib/type/authorization-server.type.ts +++ b/packages/policy-engine-shared/src/lib/type/authorization-server.type.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { Action, SerializedTransactionRequest, TransactionRequest } from './action.type' -import { Approvals, JwtString, Request, SerializedRequest } from './domain.type' +import { Approvals, ConfirmationClaim, JwtString, Request, SerializedRequest } from './domain.type' export const AuthorizationRequestStatus = { CREATED: 'CREATED', @@ -78,7 +78,8 @@ export type AuthorizationRequestError = z.infer diff --git a/packages/policy-engine-shared/src/lib/type/client.type.ts b/packages/policy-engine-shared/src/lib/type/client.type.ts index a053dce75..de19fae26 100644 --- a/packages/policy-engine-shared/src/lib/type/client.type.ts +++ b/packages/policy-engine-shared/src/lib/type/client.type.ts @@ -1,34 +1,6 @@ -import { privateKeySchema, publicKeySchema } from '@narval/signature' import { z } from 'zod' import { DataStoreConfiguration } from './data-store.type' -export const SignerConfig = z.object({ - publicKey: publicKeySchema.optional(), - privateKey: privateKeySchema.optional() -}) - -export type SignerConfig = z.infer - -export const Client = z.object({ - clientId: z.string(), - clientSecret: z.string(), - dataStore: z.object({ - entity: DataStoreConfiguration, - policy: DataStoreConfiguration - }), - signer: SignerConfig, - createdAt: z.coerce.date(), - updatedAt: z.coerce.date() -}) -export type Client = z.infer - -export const PublicClient = Client.extend({ - signer: z.object({ - publicKey: publicKeySchema - }) -}) -export type PublicClient = z.infer - export const CreateClient = z.object({ clientId: z.string().optional(), clientSecret: z diff --git a/packages/policy-engine-shared/src/lib/type/domain.type.ts b/packages/policy-engine-shared/src/lib/type/domain.type.ts index 510658deb..75e358c28 100644 --- a/packages/policy-engine-shared/src/lib/type/domain.type.ts +++ b/packages/policy-engine-shared/src/lib/type/domain.type.ts @@ -1,3 +1,4 @@ +import { publicKeySchema } from '@narval/signature' import { ZodTypeAny, z } from 'zod' import { credentialEntitySchema } from '../schema/entity.schema' import { ChainAccountId } from '../util/caip.util' @@ -143,12 +144,40 @@ export type Feed = { data?: Data } +export const ConfirmationClaimProofMethod = { + JWS: 'jws' +} as const +export type ConfirmationClaimProofMethod = + (typeof ConfirmationClaimProofMethod)[keyof typeof ConfirmationClaimProofMethod] + +export const ConfirmationClaim = z + .object({ + key: z.object({ + jwk: publicKeySchema.describe( + 'JSON Web Key that will be used to bind the access token. This ensures only the holder of the corresponding private key can use the token.' + ), + proof: z + .literal(ConfirmationClaimProofMethod.JWS) + .optional() + .describe( + 'Specifies the proof method for demonstrating possession of the private key corresponding to the jwk' + ), + jws: z + .string() + .optional() + .describe('The actual JSON Web Signature value that proves the client possesses the private key.') + }) + }) + .describe('Option to bind the access token to a given public key.') +export type ConfirmationClaim = z.infer + export const EvaluationMetadata = z .object({ audience: z.union([z.string(), z.array(z.string())]).optional(), issuer: z.string().optional(), issuedAt: z.number().optional(), - expiresIn: z.number().optional() + expiresIn: z.number().optional(), + confirmation: ConfirmationClaim.optional() }) .describe('Metadata for the grant permission access token') export type EvaluationMetadata = z.infer @@ -167,7 +196,6 @@ export const EvaluationRequest = z.object({ ), metadata: EvaluationMetadata.optional() }) - export type EvaluationRequest = z.infer export const SerializedEvaluationRequest = EvaluationRequest.extend({ diff --git a/packages/policy-engine-shared/src/lib/util/__test__/unit/confirmation-claim.util.spec.ts b/packages/policy-engine-shared/src/lib/util/__test__/unit/confirmation-claim.util.spec.ts new file mode 100644 index 000000000..3f0f41124 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/util/__test__/unit/confirmation-claim.util.spec.ts @@ -0,0 +1,129 @@ +import { Alg, PrivateKey, generateJwk, getPublicKey, hash, signJwt } from '@narval/signature' +import { Action } from '../../../type/action.type' +import { ConfirmationClaimProofMethod, Request } from '../../../type/domain.type' +import { verifyConfirmationClaimProofOfPossession } from '../../confirmation-claim.util' + +const bindPrivateKey: PrivateKey = { + kty: 'OKP', + crv: 'Ed25519', + alg: 'EDDSA', + kid: '0x65a3f312d1fc34e937ca9c1b7fbe5b9f98fb15e2cb15594ec6cd5167e36a58e3', + x: 'n0AX7pAzBhCr6R7dRhPqeGDVIKRaatVjdmL3KX58HGw', + d: 'tl8nZiFTRa5C_yJvL73KFnxDbuUi8h6bUvh28jvXmII' +} + +const request: Request = { + action: Action.SIGN_MESSAGE, + nonce: '123', + message: 'sign me', + resourceId: 'test-resource-id' +} + +describe('verifyConfirmationClaimProofOfPossession', () => { + it('verifies valid proof of possession', async () => { + const result = await verifyConfirmationClaimProofOfPossession({ + authentication: '', + request, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKey), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + requestHash: hash(request) + }, + bindPrivateKey + ) + } + } + } + }) + + expect(result).toBe(true) + }) + + it('returns true when no proof method is specified', async () => { + const result = await verifyConfirmationClaimProofOfPossession({ + authentication: '', + request, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKey) + } + } + } + }) + + expect(result).toBe(true) + }) + + it('throws error when proof method is specified but jws is missing', async () => { + await expect( + verifyConfirmationClaimProofOfPossession({ + authentication: '', + request, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKey), + proof: ConfirmationClaimProofMethod.JWS + } + } + } + }) + ).rejects.toThrow('Missing confirmation claim jws') + }) + + it('throws error when hash mismatch', async () => { + await expect( + verifyConfirmationClaimProofOfPossession({ + authentication: '', + // Source request. + request, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKey), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + // Tampered request. + requestHash: hash({ + ...request, + nonce: 'modified-nonce' + }) + }, + bindPrivateKey + ) + } + } + } + }) + ).rejects.toThrow('Confirmation claim jws hash mismatch') + }) + + it('throws error when JWT verification fails', async () => { + await expect( + verifyConfirmationClaimProofOfPossession({ + authentication: '', + request, + metadata: { + confirmation: { + key: { + jwk: getPublicKey(bindPrivateKey), + proof: ConfirmationClaimProofMethod.JWS, + jws: await signJwt( + { + requestHash: hash(request) + }, + await generateJwk(Alg.EDDSA) + ) + } + } + } + }) + ).rejects.toThrow('Invalid confirmation claim jws') + }) +}) diff --git a/packages/policy-engine-shared/src/lib/util/confirmation-claim.util.ts b/packages/policy-engine-shared/src/lib/util/confirmation-claim.util.ts new file mode 100644 index 000000000..0399fd998 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/util/confirmation-claim.util.ts @@ -0,0 +1,31 @@ +import { JwtError, hash, verifyJwt } from '@narval/signature' +import { EvaluationRequest } from '../type/domain.type' + +export class ConfirmationClaimError extends Error {} + +export const verifyConfirmationClaimProofOfPossession = async (evaluation: EvaluationRequest): Promise => { + if (evaluation.metadata?.confirmation && evaluation.metadata.confirmation?.key.proof) { + const { confirmation } = evaluation.metadata + + if (!confirmation.key.jws) { + throw new ConfirmationClaimError('Missing confirmation claim jws') + } + + try { + const jwt = await verifyJwt(confirmation.key.jws, confirmation.key.jwk) + const message = hash(evaluation.request) + + if (jwt.payload.requestHash !== message) { + throw new ConfirmationClaimError('Confirmation claim jws hash mismatch') + } + } catch (error: unknown) { + if (error instanceof JwtError) { + throw new ConfirmationClaimError(`Invalid confirmation claim jws: ${error.message}`) + } + + throw error + } + } + + return true +} diff --git a/packages/signature/src/lib/__test__/unit/decode.spec.ts b/packages/signature/src/lib/__test__/unit/decode.spec.ts index 8c7f02c43..2e0b584c1 100644 --- a/packages/signature/src/lib/__test__/unit/decode.spec.ts +++ b/packages/signature/src/lib/__test__/unit/decode.spec.ts @@ -16,6 +16,7 @@ describe('decodeJwt', () => { const rawJwt = await signJwt(payload, key, { alg: 'ES256K' }) const jwt = decodeJwt(rawJwt) + expect(jwt).toEqual({ header: { alg: 'ES256K', @@ -34,4 +35,25 @@ describe('decodeJwt', () => { it('throws an error if token is in correct format but not valid base64url encoded data', async () => { expect(() => decodeJwt('invalid.invalid.invalid')).toThrow(JwtError) }) + + it('does not strip arbitrary data from the payload', async () => { + const payloadWithArbitraryData = { + ...payload, + foo: 'foo', + bar: 'bar' + } + const rawJwt = await signJwt(payloadWithArbitraryData, key, { alg: 'ES256K' }) + + const jwt = decodeJwt(rawJwt) + + expect(jwt).toEqual({ + header: { + alg: 'ES256K', + kid: key.kid, + typ: 'JWT' + }, + payload: payloadWithArbitraryData, + signature: rawJwt.split('.')[2] + }) + }) }) diff --git a/packages/signature/src/lib/__test__/unit/encrypt.spec.ts b/packages/signature/src/lib/__test__/unit/encrypt.spec.ts index 1d4292c59..44ac5319a 100644 --- a/packages/signature/src/lib/__test__/unit/encrypt.spec.ts +++ b/packages/signature/src/lib/__test__/unit/encrypt.spec.ts @@ -1,10 +1,10 @@ import { rsaDecrypt, rsaEncrypt } from '../../encrypt' -import { Alg, RsaPrivateKey } from '../../types' +import { Alg } from '../../types' import { generateJwk, rsaPrivateKeyToPublicKey } from '../../utils' describe('encrypt / decrypt', () => { it('should encrypt & decrypt with RS256 key', async () => { - const rsaPrivate = await generateJwk(Alg.RS256, { use: 'enc' }) + const rsaPrivate = await generateJwk(Alg.RS256, { use: 'enc' }) const data = 'myTestDataString' const rsaPublic = rsaPrivateKeyToPublicKey(rsaPrivate) diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts index acecae91a..f4d5c3a7b 100644 --- a/packages/signature/src/lib/__test__/unit/sign.spec.ts +++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts @@ -15,6 +15,7 @@ import { } from '../../sign' import { Alg, Curves, Jwk, KeyTypes, Payload, PrivateKey, SigningAlg } from '../../types' import { + SMALLEST_RSA_MODULUS_LENGTH, base64UrlToBytes, base64UrlToHex, ed25519polyfilled as ed, @@ -107,7 +108,7 @@ describe('sign', () => { }) it('sign RS256 correctly', async () => { - const key = await generateJwk(Alg.RS256) + const key = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) const jwt = await signJwt(payload, key) const verifiedJwt = await verifyJwt(jwt, key) expect(verifiedJwt.payload).toEqual(payload) diff --git a/packages/signature/src/lib/__test__/unit/util.spec.ts b/packages/signature/src/lib/__test__/unit/util.spec.ts index a9c58c0e7..5016c55fe 100644 --- a/packages/signature/src/lib/__test__/unit/util.spec.ts +++ b/packages/signature/src/lib/__test__/unit/util.spec.ts @@ -10,21 +10,29 @@ import { buildSignerEip191, signJwt } from '../../sign' import { Alg, Header, + Hex, Jwk, RsaPrivateKey, Secp256k1PrivateKey, SigningAlg, p256PublicKeySchema, + rsaPublicKeySchema, secp256k1PublicKeySchema } from '../../types' import { + SMALLEST_RSA_MODULUS_LENGTH, ed25519polyfilled, ellipticPrivateKeyToHex, generateJwk, + privateJwkToPem, privateKeyToHex, privateKeyToJwk, + privateKeyToPem, + privateRsaPemToJwk, publicKeyToHex, publicKeyToJwk, + publicKeyToPem, + publicRsaPemToJwk, requestWithoutWildcardFields, rsaPrivateKeyToPublicKey } from '../../utils' @@ -120,7 +128,8 @@ describe('isHeader', () => { describe('generateKeys', () => { it('generate a valid RSA key pair and return it as a JWK', async () => { - const key = await generateJwk(Alg.RS256) + // IMPORTANT: Uses a small length to increase the generation speed on tests. + const key = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) expect(rsaPrivateKeySchema.safeParse(key).success).toBe(true) }) @@ -171,7 +180,7 @@ describe('generateKeys', () => { }) describe('rsaPrivateKeyToPublicKey', () => { it('converts private to public', async () => { - const privateKey = await generateJwk(Alg.RS256, { use: 'enc' }) + const privateKey = await generateJwk(Alg.RS256, { use: 'enc' }) const publicKey = rsaPrivateKeyToPublicKey(privateKey) expect(publicKey).toEqual({ alg: Alg.RS256, @@ -300,6 +309,83 @@ describe('privateKeyToJwk', () => { // }) }) +describe('publicJwkToPem', () => { + const privateJwk = { + kty: 'RSA', + alg: 'RS256', + kid: '0x52920ad0d19d7779106bd9d9d600d26c4b976cdb3cbc49decb7fdc29db00b8e9', + n: 'xNdTjWL9hGa4bz4tLKbmFZ4yjQsQzW35-CMS0kno3403jEqg5y2Cs6sLVyPBX4N2hdK5ERPytpf1PrThHqB-eEO6LtEWpENBgFuNIf8DRHrv0tne7dLNxf7sx1aocGRrkgIk4Ws6Is4Ot3whm3-WihmDGnHoogE-EPwVkkSc2FYPXYlNq4htCZXC8_MUI3LuXry2Gn4tna5HsYSehYhfKDD-nfSajeWxdNUv_3wOeSCr9ICm9Udlo7hpIUHQgnX3Nz6kvfGYuweLGoj_ot-oEUCIdlbQqmrfStAclugbM5NI6tY__6wD0z_4ZBjToupXCBlXbYsde6_ZG9xPmYSykw', + e: 'AQAB', + d: 'QU4rIzpXX8jwob-gHzNUHJH6tX6ZWX6GM0P3p5rrztc8Oag8z9XyigdSYNu0-SpVdTqfOcJDgT7TF7XNBms66k2WBJhMCb1iiuJU5ZWEkQC0dmDgLEkHCgx0pAHlKjy2z580ezEm_YsdqNRfFgbze-fQ7kIiazU8UUhBI-DtpHv7baBgsfqEfQ5nCTiURUPmmpiIU74-ZIJWZjBXTOoJNH0EIsJK9IpZzxpeC9mTMTsWTcHKiR3acze1qf-9I97v461TTZ8e33N6YINyr9I4HZuvxlCJdV_lOM3fLvYM9gPvgkPozhVWL3VKR6xa9JpGGHrCRgH92INuviBB_SmF8Q', + p: '9BNku_-t4Df9Dg7M2yjiNgZgcTNKrDnNqexliIUAt67q0tGmSBubjxeI5unDJZ_giXWUR3q-02v7HT5GYx-ZVgKk2lWnbrrm_F7UZW-ueHzeVvQcjDXTk0z8taXzrDJgnIwZIaZ2XSG3P-VPOrXCaMba8GzSq38Gpzi4g3lTO9s', + q: 'znUtwrqdnVew14_aFjNTRgzOQNN8JhkjzJy3aTSLBScK5NbiuUUZBWs5dQ7Nv7aAoDss1-o9XVQZ1DVV-o9UufJtyrPNcvTnC0cWRrtJrSN5YiuUbECU3Uj3OvGxnhx9tsmhDHnMTo50ObPYUbHcIkNaXkf2FVgL84y1JRWdPak', + dp: 'UNDrFeS-6fMf8zurURXkcQcDf_f_za8GDjGcHOwNJMTiNBP-_vlFNMgSKINWfmrFqj4obtKRxOeIKlKoc8HOv8_4TeL2oY95VC8CHOQx3Otbo2cI3NQlziw7sNnWKTo1CyDIYYAAyS2Uw69l4Ia2bIMLk3g0-VwCE_SQA9h0Wuk', + dq: 'VBe6ieSFKn97UnIPfJdvRcsVf6YknUgEIuV6d2mlbnXWpBs6wgf5BxIDl0BuYbYuchVoUJHiaM9Grf8DhEk5U3wBaF0QQ9CpAxjzY-AJRHJ8kJX7oJQ1jmSX_vRPSn2EXx2FcZVyuFSh1pcAd1YgufwBJQHepBb21z7q0a4aG_E', + qi: 'KhZpFs6xfyRIjbJV8Q9gWxqF37ONayIzBpgio5mdAQlZ-FUmaWZ2_2VWP2xvsP48BmwFXydHqewHBqGnZYCQ1ZHXJgD_-KKEejoqS5AJN1pdI0ZKjs7UCfZ4RJ4DH5p0_35gpuKRzzdvcIhl1CjIC5W8o7nhwmLBJ_QAo9e4t9U' + } + const publicJwk = rsaPublicKeySchema.parse(privateJwk) + it('does pem -> hex round trip for RSA keys', async () => { + const pem = await publicKeyToPem(publicJwk, Alg.RS256) + + const backToJwk = await publicRsaPemToJwk(pem, { kid: publicJwk.kid }) + + expect(backToJwk).toEqual(publicJwk) + }) + it('does pem -> jwk', async () => { + const pem = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxNdTjWL9hGa4bz4tLKbm +FZ4yjQsQzW35+CMS0kno3403jEqg5y2Cs6sLVyPBX4N2hdK5ERPytpf1PrThHqB+ +eEO6LtEWpENBgFuNIf8DRHrv0tne7dLNxf7sx1aocGRrkgIk4Ws6Is4Ot3whm3+W +ihmDGnHoogE+EPwVkkSc2FYPXYlNq4htCZXC8/MUI3LuXry2Gn4tna5HsYSehYhf +KDD+nfSajeWxdNUv/3wOeSCr9ICm9Udlo7hpIUHQgnX3Nz6kvfGYuweLGoj/ot+o +EUCIdlbQqmrfStAclugbM5NI6tY//6wD0z/4ZBjToupXCBlXbYsde6/ZG9xPmYSy +kwIDAQAB +-----END PUBLIC KEY-----` + + const newJwk = await publicRsaPemToJwk(pem, { kid: publicJwk.kid }) + expect(newJwk).toEqual(publicJwk) + }) +}) + +describe('privateJwkToPem', () => { + it('maps round-trip private rsa key to pem', async () => { + const privateKey = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) + const pem = await privateJwkToPem(privateKey) + + const roundTripJwk = await privateRsaPemToJwk(pem) + + expect(roundTripJwk).toEqual(privateKey) + }) + + it('throws when privateJwkToPem fails', async () => { + const invalidJwk = { kty: 'RSA' } as RsaPrivateKey + + await expect(privateKeyToPem(invalidJwk, Alg.RS256)).rejects.toThrow() + }) +}) + +describe('privateKeyToPem', () => { + it('maps round-trip private rsa jwk to pem', async () => { + const privateKey = await generateJwk(Alg.RS256, { modulusLength: SMALLEST_RSA_MODULUS_LENGTH }) + const pem = await privateKeyToPem(privateKey, Alg.RS256) + const roundTripJwk = await privateRsaPemToJwk(pem) + + expect(roundTripJwk).toEqual(privateKey) + }) + + it('throws error when invalid hex is provided', async () => { + const invalidHex = 'invalid-hex' as Hex + await expect(privateKeyToPem(invalidHex, Alg.RS256)).rejects.toThrow() + }) + + it('throws error when invalid jwk is provided', async () => { + // Missing required JWK parameters + const invalidJwk = { kty: 'RSA' } as Jwk + + await expect(privateKeyToPem(invalidJwk, Alg.RS256)).rejects.toThrow() + }) +}) + describe('hashRequestWithoutWildcardFields', () => { const transaction = { chainId: 137, diff --git a/packages/signature/src/lib/decode.ts b/packages/signature/src/lib/decode.ts index 7dbd53fdb..a87d6b4f8 100644 --- a/packages/signature/src/lib/decode.ts +++ b/packages/signature/src/lib/decode.ts @@ -1,6 +1,6 @@ import { JwtError } from './error' -import { Header, Payload } from './schemas' -import { type Jwsd, type Jwt } from './types' +import { Header } from './schemas' +import { Payload, type Jwsd, type Jwt } from './types' import { base64UrlToBytes } from './utils' diff --git a/packages/signature/src/lib/schemas.ts b/packages/signature/src/lib/schemas.ts index 7ee896cd7..fae915861 100644 --- a/packages/signature/src/lib/schemas.ts +++ b/packages/signature/src/lib/schemas.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { addressSchema } from './address.schema' -import { Alg, Curves, KeyTypes, Use } from './types' +import { Alg, Curves, KeyTypes, Payload, Use } from './types' // Base JWK Schema export const jwkBaseSchema = z.object({ @@ -166,7 +166,7 @@ export const Header = z.intersection( ) export const JwsdHeader = z.object({ - alg: z.union([z.literal('ES256K'), z.literal('ES256'), z.literal('RS256'), z.literal('EIP191')]), + alg: z.union([z.literal('ES256K'), z.literal('ES256'), z.literal('RS256'), z.literal('EIP191'), z.literal('EDDSA')]), kid: z.string().min(1).describe('The key ID to identify the signing key.'), typ: z .literal('gnap-binding-jwsd') @@ -186,33 +186,6 @@ export const JwsdHeader = z.object({ ) }) -/** - * Defines the payload of JWT. - * - * @param {string} requestHash - The hashed request. - * @param {string} [iss] - The issuer of the JWT. - * @param {number} [iat] - The time the JWT was issued. - * @param {number} [exp] - The time the JWT expires. - * @param {string} sub - The subject of the JWT. - * @param {string} [aud] - The audience of the JWT. - * @param {string[]} [hashWildcard] - A list of paths that were not hashed in the request. - * @param {string} [jti] - The JWT ID. - * @param {Jwk} cnf - The client-bound key. - * - */ -export const Payload = z.object({ - sub: z.string().optional(), - iat: z.number().optional(), - exp: z.number().optional(), - iss: z.string().optional(), - aud: z.string().optional(), - jti: z.string().optional(), - cnf: publicKeySchema.optional(), - requestHash: z.string().optional(), - hashWildcard: z.array(z.string()).optional(), - data: z.string().optional() -}) - export const Jwt = z.object({ header: Header, payload: Payload, diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts index 8e14c3740..cfaa0026b 100644 --- a/packages/signature/src/lib/sign.ts +++ b/packages/signature/src/lib/sign.ts @@ -287,6 +287,10 @@ export const buildSignerForAlg = async (jwk: Jwk) => { } case SigningAlg.RS256: return buildSignerRs256(privateKey) + case SigningAlg.ED25519: { + const privateKeyHex = await privateKeyToHex(privateKey) + return buildSignerEdDSA(privateKeyHex) + } default: throw new JwtError({ message: 'Unsupported signing algorithm', diff --git a/packages/signature/src/lib/types.ts b/packages/signature/src/lib/types.ts index 02ab01d34..38171b0d1 100644 --- a/packages/signature/src/lib/types.ts +++ b/packages/signature/src/lib/types.ts @@ -18,9 +18,23 @@ export const Curves = { export type Curves = (typeof Curves)[keyof typeof Curves] export const Alg = { - ES256K: 'ES256K', // secp256k1, an Ethereum EOA - ES256: 'ES256', // secp256r1, ecdsa but not ethereum + /** + * SECP256K1 + * + * Examples: + * - Used by Ethereum + */ + ES256K: 'ES256K', + /** + * SECP256R1, ECDSA but not Ethereum + */ + ES256: 'ES256', RS256: 'RS256', + /** + * Examples: + * - Used by Solan + * - Uses SHA512 and doesn't require a pre-hashed payload + */ EDDSA: 'EDDSA' } as const @@ -28,7 +42,13 @@ export type Alg = (typeof Alg)[keyof typeof Alg] export const SigningAlg = { ES256K: 'ES256K', - EIP191: 'EIP191', // ecdsa on secp256k1 with keccak256 & data prefixed w/ \x19Ethereum Signed Message:\n + len(message) + /** + * ECDSA on SECP256K1 with KECCAK256 and data prefixed + * + * Examples: + * - \x19Ethereum Signed Message:\n + len(message) + */ + EIP191: 'EIP191', ES256: 'ES256', RS256: 'RS256', ED25519: 'EDDSA' @@ -341,7 +361,7 @@ export type JwtVerifyOptions = { export type JwsdVerifyOptions = { requestBody: object - accessToken: string + accessToken?: string uri: string htm: string maxTokenAge: number @@ -403,3 +423,5 @@ export type EcdsaSignature = { s: Hex v: bigint } + +export type PemString = `-----BEGIN${string}` diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts index 744207776..79c22981f 100644 --- a/packages/signature/src/lib/utils.ts +++ b/packages/signature/src/lib/utils.ts @@ -4,7 +4,7 @@ import * as ed25519 from '@noble/ed25519' import { sha256 as sha256Hash } from '@noble/hashes/sha256' import { sha512 } from '@noble/hashes/sha512' import { subtle } from 'crypto' -import { exportJWK, generateKeyPair } from 'jose' +import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importJWK, importPKCS8, importSPKI, KeyLike } from 'jose' import { cloneDeep, omit } from 'lodash' import { toHex } from 'viem' import { publicKeyToAddress } from 'viem/utils' @@ -31,12 +31,13 @@ import { P256PublicKey, PrivateKey, PublicKey, + publicKeySchema, + RsaKey, RsaPrivateKey, RsaPublicKey, Secp256k1PrivateKey, Secp256k1PublicKey, - Use, - publicKeySchema + Use } from './types' import { validateJwk } from './validate' @@ -237,6 +238,10 @@ export const stringToBase64Url = (str: string): string => { return base64ToBase64Url(Buffer.from(str).toString('base64')) } +export const base64UrlToString = (base64Url: string): string => { + return Buffer.from(base64UrlToBase64(base64Url), 'base64').toString('utf-8') +} + export const rsaKeyToKid = (jwk: Jwk) => { // Concatenate the 'n' and 'e' values, splitted by ':' const dataToHash = `${jwk.n}:${jwk.e}` @@ -361,28 +366,57 @@ export const privateKeyToHex = async (jwk: Jwk): Promise => { } } -export const privateKeyToJwk = (key: Hex, alg: Alg = Alg.ES256K, keyId?: string): PrivateKey => { +type AlgToPrivateKeyType = { + [Alg.ES256K]: Secp256k1PrivateKey + [Alg.ES256]: P256PrivateKey + [Alg.EDDSA]: Ed25519PrivateKey + [Alg.RS256]: RsaPrivateKey +} + +export const privateKeyToJwk = ( + key: Hex, + alg: A = Alg.ES256K as A, + keyId?: string +): AlgToPrivateKeyType[A] => { switch (alg) { case Alg.ES256K: - return secp256k1PrivateKeyToJwk(key, keyId) + return secp256k1PrivateKeyToJwk(key, keyId) as AlgToPrivateKeyType[A] case Alg.ES256: - return p256PrivateKeyToJwk(key, keyId) + return p256PrivateKeyToJwk(key, keyId) as AlgToPrivateKeyType[A] case Alg.EDDSA: - return ed25519PrivateKeyToJwk(key, keyId) + return ed25519PrivateKeyToJwk(key, keyId) as AlgToPrivateKeyType[A] case Alg.RS256: throw new JwtError({ message: 'Conversion from Hex to JWK not supported for RSA keys' }) + default: + throw new JwtError({ + message: `Unsupported algorithm: ${alg}` + }) } } +export const DEFAULT_RSA_MODULUS_LENGTH = 4096 + +/** + * The smallest modulus lenght required by jose package. Useful to speed + * up tests. + * + * IMPORTANT: DO NOT use it in production. + */ +export const SMALLEST_RSA_MODULUS_LENGTH = 2048 + const generateRsaPrivateKey = async ( opts: { keyId?: string + /** + * IMPORTANT: Increasing the length will significantly increase the + * generation time. + */ modulusLength?: number use?: Use } = { - modulusLength: 2048 + modulusLength: DEFAULT_RSA_MODULUS_LENGTH } ): Promise => { const { privateKey } = await generateKeyPair(Alg.RS256, { @@ -417,39 +451,157 @@ export const rsaPrivateKeyToPublicKey = (jwk: RsaPrivateKey) => { return publicKey } -export const generateJwk = async ( - alg: Alg, +export const generateJwk = async ( + alg: A, opts?: { keyId?: string + /** + * Increase the length of the RSA key. + * + * IMPORTANT: Increasing the length will significantly increase the + * generation time. + */ modulusLength?: number + /** + * RSA Only. + */ use?: Use } -): Promise => { +): Promise => { switch (alg) { case Alg.ES256K: { const privateKeyK1 = toHex(secp256k1.utils.randomPrivateKey()) - return secp256k1PrivateKeyToJwk(privateKeyK1, opts?.keyId) as T + return secp256k1PrivateKeyToJwk(privateKeyK1, opts?.keyId) as AlgToPrivateKeyType[A] } case Alg.ES256: { const privateKeyP256 = toHex(p256.utils.randomPrivateKey()) - return p256PrivateKeyToJwk(privateKeyP256, opts?.keyId) as T + return p256PrivateKeyToJwk(privateKeyP256, opts?.keyId) as AlgToPrivateKeyType[A] } case Alg.RS256: { const jwk = await generateRsaPrivateKey(opts) - return jwk as T + return jwk as AlgToPrivateKeyType[A] } case Alg.EDDSA: { const privateKeyEd25519 = toHex(ed25519.utils.randomPrivateKey()) - return ed25519PrivateKeyToJwk(privateKeyEd25519, opts?.keyId) as T + return ed25519PrivateKeyToJwk(privateKeyEd25519, opts?.keyId) as AlgToPrivateKeyType[A] } default: throw new Error(`Unsupported algorithm: ${alg}`) } } +export const publicJwkToPem = async (jwk: Jwk): Promise => { + switch (jwk.kty) { + case KeyTypes.RSA: + return publicRsaJwkToPem(jwk as RsaKey) + default: + throw new Error('Unsupported key type') + } +} + +export const publicHexToPem = async (publicKey: Hex, alg: Alg): Promise => { + switch (alg) { + case Alg.RS256: + return publicRsaJwkToPem(publicKeyToJwk(publicKey, alg) as RsaKey) + default: + throw new Error('Unsupported algorithm') + } +} + +export const publicKeyToPem = async (publicKey: Jwk | Hex, alg: Alg): Promise => { + if (typeof publicKey === 'string') { + return publicHexToPem(publicKey, alg) + } + + return publicJwkToPem(publicKey) +} + +export const publicRsaJwkToPem = async (rsaJwk: RsaKey): Promise => { + const jk = (await importJWK(rsaJwk, 'RS256')) as KeyLike + const k = await exportSPKI(jk) + return k +} + +export const publicRsaPemToJwk = async (pem: string, opts?: { kid?: string }): Promise => { + const jk = await importSPKI(pem, 'RS256', { + extractable: true + }) + const key = await exportJWK(jk) + + return rsaPublicKeySchema.parse({ + ...key, + alg: 'RS256', + kid: opts?.kid || rsaKeyToKid({ n: key.n, e: key.e }) + }) +} + +export const publicPemToJwk = (pem: string, alg: Alg, kid?: string): T => { + switch (alg) { + case Alg.RS256: + return publicRsaPemToJwk(pem, { kid }) as T + default: + throw new Error('Unsupported algorithm') + } +} + +export const privateJwkToPem = async (jwk: Jwk): Promise => { + switch (jwk.kty) { + case KeyTypes.RSA: + return privateRsaJwkToPem(jwk as RsaKey) + default: + throw new Error('Unsupported key type') + } +} + +export const privateRsaJwkToPem = async (rsaJwk: RsaKey): Promise => { + const jk = (await importJWK(rsaJwk, 'RS256')) as KeyLike + const k = await exportPKCS8(jk) + return k +} + +export const privateRsaPemToJwk = async (pem: string, opts?: { kid?: string }): Promise => { + const jk = await importPKCS8(pem, 'RS256', { + extractable: true + }) + + const key = await exportJWK(jk) + + return rsaPrivateKeySchema.parse({ + ...key, + alg: 'RS256', + kid: opts?.kid || rsaKeyToKid({ n: key.n, e: key.e }) + }) +} + +export const privateKeyToPem = async (privateKey: Jwk | Hex, alg: Alg): Promise => { + if (typeof privateKey === 'string') { + return privateHexToPem(privateKey, alg) + } + + return privateJwkToPem(privateKey) +} + +export const privateHexToPem = async (privateKey: Hex, alg: Alg): Promise => { + switch (alg) { + case Alg.RS256: + return privateRsaJwkToPem(privateKeyToJwk(privateKey, alg) as RsaKey) + default: + throw new Error('Unsupported algorithm') + } +} + export const nowSeconds = (): number => Math.floor(Date.now() / 1000) -export const getPublicKey = (key: Jwk): PublicKey => publicKeySchema.parse(key) +type AlgToPublicKeyType = { + ES256K: Secp256k1PublicKey + ES256: P256PublicKey + EDDSA: Ed25519PublicKey + RS256: RsaPublicKey +} + +export const getPublicKey = >(key: Jwk & { alg: A }): AlgToPublicKeyType[A] => { + return publicKeySchema.parse(key) as AlgToPublicKeyType[A] +} export const requestWithoutWildcardFields = ( request: object, diff --git a/packages/signature/src/lib/verify.ts b/packages/signature/src/lib/verify.ts index 621582f76..22b2c42e5 100644 --- a/packages/signature/src/lib/verify.ts +++ b/packages/signature/src/lib/verify.ts @@ -338,11 +338,22 @@ export function verifyJwsdHeader( context: { header, opts } }) } + + // Ensure the token isn't too old. We also check the created timestamp wasn't created "in the future" (with 3s grace period); + // A future created stamp generally means milliseconds was used instead of seconds const now = nowSeconds() if (jwsdHeader.created && now - jwsdHeader.created > opts.maxTokenAge) { throw new JwtError({ message: 'JWS is too old, created field is too far in the past', - context: { header, opts } + context: { header, opts, now } + }) + } + // Allow for some clock skew (e.g., 15 seconds) + const ALLOWED_CLOCK_SKEW = 15 + if (jwsdHeader.created && now - jwsdHeader.created < -ALLOWED_CLOCK_SKEW) { + throw new JwtError({ + message: 'JWS is too old, created field is too far in the future, did you use milliseconds instead of seconds?', + context: { header, opts, now } }) } @@ -415,7 +426,7 @@ export async function verifyJwsd(jws: string, jwk: PublicKey, opts: JwsdVerifyOp htm: opts.htm, uri: opts.uri, maxTokenAge: opts.maxTokenAge, - ath: hexToBase64Url(hash(opts.accessToken)) + ath: opts.accessToken ? hexToBase64Url(hash(opts.accessToken)) : undefined }) await verifySignature(jwsToVerify, key, header.alg)