diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 450ed638..955b86b9 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -76,6 +76,15 @@ jobs: REDIS_PORT: '6379' run: pnpm --filter backend run test:cov + - name: Test signing specs (real SDK, no mock) + env: + API_KEY: test-api-key-123 + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/chainforge_test?schema=public + NODE_ENV: test + REDIS_HOST: 127.0.0.1 + REDIS_PORT: '6379' + run: pnpm --filter backend run test:signing + - name: Build run: pnpm --filter backend run build diff --git a/app/backend/package.json b/app/backend/package.json index 839d5d6b..b436942f 100644 --- a/app/backend/package.json +++ b/app/backend/package.json @@ -24,6 +24,7 @@ "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --config ./test/jest-coverage.js --coverage", + "test:signing": "jest --testPathPatterns='soroban\\.adapter\\.signing\\.spec'", "coverage:baseline": "node ./test/generate-coverage-baseline.js", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", diff --git a/app/backend/src/onchain/SOROBAN_INTEGRATION.md b/app/backend/src/onchain/SOROBAN_INTEGRATION.md index 0fa22b71..e572c9a1 100644 --- a/app/backend/src/onchain/SOROBAN_INTEGRATION.md +++ b/app/backend/src/onchain/SOROBAN_INTEGRATION.md @@ -229,6 +229,53 @@ ONCHAIN_ADAPTER=mock ONCHAIN_ADAPTER=soroban ``` +## Signing model (recipient / distributor auth) + +The `aid_escrow` contract's auth is **per-caller**, not per-backend: `claim(id, claimer)` calls `claimer.require_auth()`, and `create_package` calls `require_admin_or_distributor(&operator)`. A backend that signs every envelope with a single `SOROBAN_ADMIN_SECRET_KEY` keypair can only ever satisfy these checks when the admin *is* the caller (e.g. the admin acting as its own recipient). Real recipients and distributors hold their own keys, so the adapter exposes a **client-signing seam** in two phases: + +1. `buildUnsignedClaimTx({ packageId, recipientAddress })` / `buildUnsignedCreatePackageTx({ ... })` — simulate the contract call and return an **unsigned envelope XDR** whose Soroban auth entries demand the caller's signature. The envelope *source account is the admin account*, so the admin sponsors all fees; the recipient/distributor never needs to hold XLM or even have an account. A `transactionHash` (informational) and an `expiresAt` freshness window are returned alongside. +2. The client (mobile WalletConnect / Freighter / a backend service holding the distributor key) signs the auth entries and returns the new envelope XDR. +3. `submitSignedTx({ signedXdr, expectedSigner })` — cryptographically verifies that every auth entry was signed by the account the contract requires (and, when `expectedSigner` is set, exactly that account), signs the envelope with the admin keypair, submits, and polls until confirmed. A claim whose auth entry was signed by the admin keypair — or by any other account — is **rejected before submission**. + +Admin-only operations (`disburse`, `revoke`, `refund`, config, `init`) are unchanged: they still sign with the admin keypair via the internal admin-signed path. The legacy `claimAidPackage` / `createAidPackage` admin-signed methods also remain for the admin-as-caller case; for real end-user keys, use the seam. + +### Fee sponsorship strategy + +Fees are sponsored by making the **admin account the transaction source**. This is strictly simpler than a fee-bump: a fee-bump still requires the inner (recipient-source) transaction to have a valid source account and sequence number, which a fresh recipient cannot provide, and it adds a second envelope to construct. With the admin as source, one envelope covers both the caller's auth entry and the admin's fee payment. The trade-off is sequence-number management: the unsigned envelope pins the admin's sequence at build time, so a concurrent admin-signed submission between `build*` and `submitSignedTx` can invalidate it (`tx_bad_seq`). Submit promptly and retry by rebuilding the unsigned envelope; a shared nonce/sequence manager is a deliberate follow-up. + +### Client-side signing sequence + +The client signs the auth entries (not the envelope — the admin signs that server-side). Using the Stellar JS SDK: + +```ts +import { + TransactionBuilder, + Operation, + SorobanDataBuilder, + authorizeEntry, +} from '@stellar/stellar-sdk'; + +const tx = TransactionBuilder.fromXDR(transactionXdr, networkPassphrase) as Transaction; +const op = tx.operations[0]; +const expiration = op.auth![0].credentials().address().signatureExpirationLedger(); +const signedEntries = await Promise.all( + op.auth!.map(entry => authorizeEntry(entry, recipientKeypair, expiration, networkPassphrase)), +); +const signedXdr = TransactionBuilder.cloneFrom(tx, { + fee: tx.fee, + networkPassphrase, + sorobanData: tx.sorobanData, +}) + .clearOperations() + .addOperation(Operation.invokeHostFunction({ source: op.source, func: op.func, auth: signedEntries })) + .build() + .toXDR(); + +// POST signedXdr (+ expectedSigner) back to the backend → submitSignedTx +``` + +The signature payload is `sha256(HashIdPreimageSorobanAuthorization{ networkId, nonce, invocation, signatureExpirationLedger })` — it does **not** cover the envelope, so the backend can safely re-sign the envelope and the client can rebuild timebounds without invalidating the recipient's signature. The on-chain `signature_expiration_ledger` (set during simulation) is the authoritative expiry, enforced by the network. + ## Error Handling All errors follow the global error format: diff --git a/app/backend/src/onchain/onchain.adapter.mock.spec.ts b/app/backend/src/onchain/onchain.adapter.mock.spec.ts index bc372aec..5ae52c0f 100644 --- a/app/backend/src/onchain/onchain.adapter.mock.spec.ts +++ b/app/backend/src/onchain/onchain.adapter.mock.spec.ts @@ -169,4 +169,53 @@ describe('MockOnchainAdapter', () => { expect(result.metadata?.recipientAddress).toBe(recipientAddress); }); }); + + describe('client-signing seam', () => { + const RECIPIENT = + 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF'; + const OPERATOR = + 'GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'; + + it('buildUnsignedClaimTx returns an unsigned envelope stub', async () => { + const result = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: RECIPIENT, + }); + + expect(result.packageId).toBe('1'); + expect(result.recipientAddress).toBe(RECIPIENT); + expect(result.transactionXdr).toContain('mock-unsigned-claim-xdr'); + expect(result.transactionHash).toHaveLength(64); + expect(result.expiresAt).toBeGreaterThan(Math.floor(Date.now() / 1000)); + expect(result.timestamp).toBeInstanceOf(Date); + }); + + it('buildUnsignedCreatePackageTx returns an unsigned envelope stub', async () => { + const result = await adapter.buildUnsignedCreatePackageTx({ + operatorAddress: OPERATOR, + packageId: '42', + recipientAddress: RECIPIENT, + amount: '250', + tokenAddress: MOCK_TOKEN_ADDRESS, + expiresAt: 1767225600, + }); + + expect(result.packageId).toBe('42'); + expect(result.operatorAddress).toBe(OPERATOR); + expect(result.transactionXdr).toContain('mock-unsigned-create-package'); + expect(result.transactionHash).toHaveLength(64); + }); + + it('submitSignedTx accepts a signed envelope and reports success', async () => { + const result = await adapter.submitSignedTx({ + signedXdr: 'mock-signed-xdr-123', + expectedSigner: RECIPIENT, + }); + + expect(result.status).toBe('success'); + expect(result.transactionHash).toHaveLength(64); + expect(result.metadata?.signer).toBe(RECIPIENT); + expect(result.metadata?.adapter).toBe('mock'); + }); + }); }); diff --git a/app/backend/src/onchain/onchain.adapter.mock.ts b/app/backend/src/onchain/onchain.adapter.mock.ts index 0ab6ed57..7a723190 100644 --- a/app/backend/src/onchain/onchain.adapter.mock.ts +++ b/app/backend/src/onchain/onchain.adapter.mock.ts @@ -29,6 +29,12 @@ import { GetTransactionStatusParams, GetTransactionStatusResult, TxStatus, + BuildUnsignedClaimTxParams, + BuildUnsignedClaimTxResult, + BuildUnsignedCreatePackageParams, + BuildUnsignedCreatePackageResult, + SubmitSignedTxParams, + SubmitSignedTxResult, } from './onchain.adapter'; import { createHash } from 'crypto'; @@ -149,6 +155,58 @@ export class MockOnchainAdapter implements OnchainAdapter { }; } + async buildUnsignedClaimTx( + params: BuildUnsignedClaimTxParams, + ): Promise { + await Promise.resolve(); + return { + packageId: params.packageId, + recipientAddress: params.recipientAddress, + transactionXdr: `mock-unsigned-claim-xdr-${params.packageId}-${params.recipientAddress}`, + transactionHash: this.generateMockHash( + `unsigned-claim-${params.packageId}-${params.recipientAddress}`, + ), + expiresAt: Math.floor(Date.now() / 1000) + 300, + timestamp: new Date(), + }; + } + + async buildUnsignedCreatePackageTx( + params: BuildUnsignedCreatePackageParams, + ): Promise { + await Promise.resolve(); + return { + packageId: params.packageId, + operatorAddress: params.operatorAddress, + transactionXdr: `mock-unsigned-create-package-xdr-${params.packageId}-${params.operatorAddress}`, + transactionHash: this.generateMockHash( + `unsigned-create-package-${params.packageId}-${params.operatorAddress}`, + ), + expiresAt: Math.floor(Date.now() / 1000) + 300, + timestamp: new Date(), + }; + } + + async submitSignedTx( + params: SubmitSignedTxParams, + ): Promise { + await Promise.resolve(); + const transactionHash = this.generateMockHash( + `signed-tx-${params.signedXdr.slice(0, 32)}-${Date.now()}`, + ); + + return { + transactionHash, + status: 'success', + timestamp: new Date(), + metadata: { + contractId: this.mockEscrowAddress, + signer: params.expectedSigner, + adapter: 'mock', + }, + }; + } + async disburseAidPackage( params: DisburseAidPackageParams, ): Promise { diff --git a/app/backend/src/onchain/onchain.adapter.ts b/app/backend/src/onchain/onchain.adapter.ts index cff1305b..c85f88d3 100644 --- a/app/backend/src/onchain/onchain.adapter.ts +++ b/app/backend/src/onchain/onchain.adapter.ts @@ -69,6 +69,82 @@ export interface ClaimAidPackageParams { recipientAddress: string; } +/** + * Client-signing seam: parameters for building an unsigned, simulated + * `claim` transaction. The returned XDR carries the Soroban auth entry the + * recipient must sign; the envelope source is the backend admin account, so + * fees are sponsored by the operator (no XLM required from the recipient). + */ +export interface BuildUnsignedClaimTxParams { + packageId: string; + recipientAddress: string; +} + +export interface BuildUnsignedClaimTxResult { + packageId: string; + recipientAddress: string; + /** + * Base64 XDR of the unsigned envelope. The client signs the Soroban auth + * entries inside it with the recipient key and hands the result back to + * `submitSignedTx`. + */ + transactionXdr: string; + /** Hash of the unsigned envelope, hex-encoded (informational). */ + transactionHash: string; + /** Unix seconds after which the envelope should be treated as stale. */ + expiresAt: number; + timestamp: Date; +} + +/** + * Client-signing seam: parameters for building an unsigned, simulated + * `create_package` transaction whose Soroban auth entry must be signed by the + * operator (distributor) — enabling package creation for a non-admin + * `operatorAddress`. + */ +export interface BuildUnsignedCreatePackageParams { + operatorAddress: string; + packageId: string; + recipientAddress: string; + amount: string; + tokenAddress: string; + expiresAt: number; + metadata?: Record; +} + +export interface BuildUnsignedCreatePackageResult { + packageId: string; + operatorAddress: string; + transactionXdr: string; + transactionHash: string; + expiresAt: number; + timestamp: Date; +} + +/** + * Client-signing seam: submit an envelope whose Soroban auth entries were + * signed off-chain by the required account (recipient for `claim`, operator + * for `create_package`). The adapter verifies the auth-entry signatures, + * signs the envelope with the admin keypair (fee sponsorship), then submits + * and confirms the transaction. + */ +export interface SubmitSignedTxParams { + /** Base64 XDR of the client-signed envelope. */ + signedXdr: string; + /** + * G… public key that must have signed the auth entries. When set, any auth + * entry signed by a different account is rejected before submission. + */ + expectedSigner?: string; +} + +export interface SubmitSignedTxResult { + transactionHash: string; + status: 'success' | 'failed'; + timestamp: Date; + metadata?: Record; +} + export interface ClaimAidPackageResult { packageId: string; transactionHash: string; @@ -233,6 +309,32 @@ export interface OnchainAdapter { params: ClaimAidPackageParams, ): Promise; + /** + * Build an unsigned, simulated `claim` transaction whose Soroban auth entry + * requires the recipient's signature. The recipient signs the returned XDR + * and hands it back to {@link submitSignedTx}. + */ + buildUnsignedClaimTx( + params: BuildUnsignedClaimTxParams, + ): Promise; + + /** + * Build an unsigned, simulated `create_package` transaction whose Soroban + * auth entry requires the operator's signature — the distributor-created + * package path for a non-admin `operatorAddress`. + */ + buildUnsignedCreatePackageTx( + params: BuildUnsignedCreatePackageParams, + ): Promise; + + /** + * Submit a client-signed transaction envelope. Auth-entry signatures are + * verified against the required account, the envelope is signed by the + * admin keypair (fee sponsorship), and the transaction is submitted and + * confirmed before the result is returned. + */ + submitSignedTx(params: SubmitSignedTxParams): Promise; + /** * Disburse an aid package by admin */ diff --git a/app/backend/src/onchain/soroban.adapter.signing.spec.ts b/app/backend/src/onchain/soroban.adapter.signing.spec.ts new file mode 100644 index 00000000..6002d341 --- /dev/null +++ b/app/backend/src/onchain/soroban.adapter.signing.spec.ts @@ -0,0 +1,460 @@ +import { ConfigService } from '@nestjs/config'; +import { SorobanAdapter } from './soroban.adapter'; +import { + xdr, + Keypair, + StrKey, + nativeToScVal, + TransactionBuilder, + SorobanDataBuilder, + Operation, + Account, + Transaction, + authorizeEntry, +} from '@stellar/stellar-sdk'; + +/** + * Client-signing seam tests for issue #428. + * + * The contract's auth model is per-caller: `claim(id, claimer)` calls + * `claimer.require_auth()` and `create_package` calls + * `require_admin_or_distributor(&operator)`. The adapter must therefore be + * able to submit transactions whose Soroban auth entries were signed by the + * recipient / distributor — not by the backend admin keypair. + * + * These tests exercise the seam (`buildUnsignedClaimTx` / + * `buildUnsignedCreatePackageTx` -> client signs -> `submitSignedTx`) against + * the *real* stellar-sdk crypto and XDR codecs; only the RPC server boundary + * (`getAccount` / `simulateTransaction` / `sendTransaction` / `getTransaction`) + * is mocked. The auth-entry signature preimage is the one the SDK's + * `authorizeEntry` uses, so a wrong-signer entry (admin keypair) is detected + * and rejected before submission. + */ + +const CONTRACT_ID = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM'; +const PASSPHRASE = 'Test SDF Network ; September 2015'; +const HASH_64 = 'A'.repeat(64); + +const mockServer = { + getAccount: jest.fn(), + simulateTransaction: jest.fn(), + sendTransaction: jest.fn(), + getTransaction: jest.fn(), +}; + +jest.mock('@stellar/stellar-sdk', () => { + const actual = jest.requireActual('@stellar/stellar-sdk'); + return { + ...actual, + rpc: { + ...actual.rpc, + Server: jest.fn().mockImplementation(() => mockServer), + }, + }; +}); + +const admin = Keypair.random(); +const recipient = Keypair.random(); +const operator = Keypair.random(); +const thirdParty = Keypair.random(); + +/** Build the unsigned Soroban auth entry a real simulation would return. */ +function unsignedAuthEntry( + requiredSigner: Keypair, + method: string, + args: xdr.ScVal[], +): xdr.SorobanAuthorizationEntry { + const invocation = new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: xdr.ScAddress.scAddressTypeContract( + StrKey.decodeContract(CONTRACT_ID) as unknown as xdr.Hash, + ), + functionName: method, + args, + }), + ), + subInvocations: [], + }); + return new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress( + new xdr.SorobanAddressCredentials({ + address: xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(requiredSigner.rawPublicKey()), + ), + nonce: xdr.Int64.fromString('7'), + signatureExpirationLedger: 5000, + signature: xdr.ScVal.scvVoid(), + }), + ), + rootInvocation: invocation, + }); +} + +/** Raw RPC simulation response carrying one unsigned auth entry. */ +function simulationWithEntry( + entry: xdr.SorobanAuthorizationEntry, +): Record { + const txData = new SorobanDataBuilder().setFootprint([], []).build(); + return { + transactionData: txData.toXDR('base64'), + minResourceFee: '100', + results: [ + { + auth: [entry.toXDR('base64')], + xdr: xdr.ScVal.scvVoid().toXDR('base64'), + }, + ], + cost: { cpuInsns: '0', memBytes: '0' }, + latestLedger: 4000, + }; +} + +/** + * Client-side signing: parse the unsigned envelope, sign every auth entry with + * `signer` (exactly what a wallet such as Freighter / WalletConnect does), and + * rebuild the envelope with the signed entries. + */ +function invokeHostOperation(tx: Transaction): Operation.InvokeHostFunction { + const op = tx.operations[0]; + if (op.type !== 'invokeHostFunction') { + throw new Error('expected an invokeHostFunction operation'); + } + return op; +} + +async function signAuthEntries( + unsignedXdr: string, + signer: Keypair, +): Promise { + const clientTx = TransactionBuilder.fromXDR( + unsignedXdr, + PASSPHRASE, + ) as Transaction; + const op = invokeHostOperation(clientTx); + const auth = op.auth!; // the simulated envelope always carries auth entries + const expiration = auth[0] + .credentials() + .address() + .signatureExpirationLedger(); + const signed: xdr.SorobanAuthorizationEntry[] = []; + for (const entry of auth) { + signed.push(await authorizeEntry(entry, signer, expiration, PASSPHRASE)); + } + const rebuilt = TransactionBuilder.cloneFrom(clientTx, { + fee: clientTx.fee, + networkPassphrase: PASSPHRASE, + sorobanData: ( + clientTx as unknown as { sorobanData: xdr.SorobanTransactionData } + ).sorobanData, + }) + .clearOperations() + .addOperation( + Operation.invokeHostFunction({ + source: op.source, + func: op.func, + auth: signed, + }), + ) + .build(); + return rebuilt.toXDR(); +} + +function buildAdapter(): SorobanAdapter { + const config = { + get: jest.fn((key: string, fallback?: unknown) => { + switch (key) { + case 'AID_ESCROW_CONTRACT_ID': + return CONTRACT_ID; + case 'SOROBAN_ADMIN_SECRET_KEY': + return admin.secret(); + case 'STELLAR_RPC_URL': + return 'https://soroban-testnet.stellar.org'; + case 'STELLAR_NETWORK_PASSPHRASE': + return PASSPHRASE; + case 'SOROBAN_NETWORK': + return 'testnet'; + default: + return fallback; + } + }), + } as unknown as ConfigService; + + return new SorobanAdapter(config); +} + +function claimArgs(packageId: number): xdr.ScVal[] { + return [ + nativeToScVal(packageId, { type: 'u64' }), + nativeToScVal(recipient.publicKey(), { type: 'address' }), + ]; +} + +describe('SorobanAdapter client-signing seam (issue #428)', () => { + let adapter: SorobanAdapter; + + beforeEach(() => { + jest.clearAllMocks(); + mockServer.getAccount.mockResolvedValue( + new Account(admin.publicKey(), '100'), + ); + mockServer.sendTransaction.mockResolvedValue({ + status: 'PENDING', + hash: HASH_64, + }); + mockServer.getTransaction.mockResolvedValue({ + status: 'SUCCESS', + returnValue: xdr.ScVal.scvVoid(), + ledger: 4001, + }); + adapter = buildAdapter(); + }); + + describe('buildUnsignedClaimTx', () => { + beforeEach(() => { + mockServer.simulateTransaction.mockResolvedValue( + simulationWithEntry( + unsignedAuthEntry(recipient, 'claim', claimArgs(1)), + ), + ); + }); + + it('returns an unsigned envelope whose auth entry demands the recipient signature', async () => { + const result = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: recipient.publicKey(), + }); + + expect(result.transactionXdr).toBeTruthy(); + expect(result.transactionHash).toMatch(/^[0-9a-f]{64}$/); + expect(result.expiresAt).toBeGreaterThan(Math.floor(Date.now() / 1000)); + expect(result.recipientAddress).toBe(recipient.publicKey()); + + // The returned XDR parses as the same transaction the hash was taken from. + const parsed = TransactionBuilder.fromXDR( + result.transactionXdr, + PASSPHRASE, + ) as Transaction; + expect(parsed.hash().toString('hex')).toBe(result.transactionHash); + + // One auth entry, required signer = recipient, signature still void + // (the recipient has not signed yet). + const entries = invokeHostOperation(parsed).auth!; + expect(entries).toHaveLength(1); + const creds = entries[0].credentials(); + expect( + creds.switch() === + xdr.SorobanCredentialsType.sorobanCredentialsAddress(), + ).toBe(true); + const required = StrKey.encodeEd25519PublicKey( + creds.address().address().accountId().ed25519(), + ); + expect(required).toBe(recipient.publicKey()); + expect( + creds.address().signature().switch() === xdr.ScValType.scvVoid(), + ).toBe(true); + }); + }); + + describe('submitSignedTx for claims', () => { + beforeEach(() => { + mockServer.simulateTransaction.mockResolvedValue( + simulationWithEntry( + unsignedAuthEntry(recipient, 'claim', claimArgs(1)), + ), + ); + }); + + it('submits a recipient-signed claim successfully', async () => { + const unsigned = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: recipient.publicKey(), + }); + const signedXdr = await signAuthEntries( + unsigned.transactionXdr, + recipient, + ); + + const result = await adapter.submitSignedTx({ + signedXdr, + expectedSigner: recipient.publicKey(), + }); + + expect(result.status).toBe('success'); + expect(result.transactionHash).toMatch(/^[A-F0-9]{64}$/); + expect(mockServer.sendTransaction).toHaveBeenCalledTimes(1); + + // The envelope submitted carries the recipient-signed auth entry AND the + // admin envelope signature (fee sponsorship). + const sent = mockServer.sendTransaction.mock.calls[0][0] as Transaction; + expect(sent.signatures).toHaveLength(1); + const sentAuth = invokeHostOperation(sent).auth!; + expect(sentAuth).toHaveLength(1); + const sentCreds = sentAuth[0].credentials(); + expect( + sentCreds.address().signature().switch() === xdr.ScValType.scvVoid(), + ).toBe(false); + }); + + it('rejects a claim whose auth entry was signed by the admin keypair, not the recipient', async () => { + const unsigned = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: recipient.publicKey(), + }); + // The recipient's key is never touched; the admin keypair signs instead. + const signedXdr = await signAuthEntries(unsigned.transactionXdr, admin); + + await expect( + adapter.submitSignedTx({ + signedXdr, + expectedSigner: recipient.publicKey(), + }), + ).rejects.toThrow( + `auth entry signed by ${admin.publicKey()} but the contract requires ${recipient.publicKey()}`, + ); + // The wrong-signer transaction must never reach the network. + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it('rejects a claim signed by a third party when the recipient was expected', async () => { + const unsigned = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: recipient.publicKey(), + }); + const signedXdr = await signAuthEntries( + unsigned.transactionXdr, + thirdParty, + ); + + await expect( + adapter.submitSignedTx({ + signedXdr, + expectedSigner: recipient.publicKey(), + }), + ).rejects.toThrow( + `auth entry signed by ${thirdParty.publicKey()} but the contract requires ${recipient.publicKey()}`, + ); + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it('rejects when the auth entry is valid but expectedSigner points at the wrong account', async () => { + const unsigned = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: recipient.publicKey(), + }); + // Signed by the recipient (the entry's required account) — cryptographically + // valid — but the caller declares a different expectedSigner. + const signedXdr = await signAuthEntries( + unsigned.transactionXdr, + recipient, + ); + + await expect( + adapter.submitSignedTx({ + signedXdr, + expectedSigner: thirdParty.publicKey(), + }), + ).rejects.toThrow( + `auth entry signed by ${recipient.publicKey()}, expected ${thirdParty.publicKey()}`, + ); + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it('rejects an unsigned envelope (no signature applied)', async () => { + const unsigned = await adapter.buildUnsignedClaimTx({ + packageId: '1', + recipientAddress: recipient.publicKey(), + }); + + await expect( + adapter.submitSignedTx({ + signedXdr: unsigned.transactionXdr, + expectedSigner: recipient.publicKey(), + }), + ).rejects.toThrow('auth entry is unsigned'); + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it('rejects an envelope that is not valid XDR', async () => { + await expect( + adapter.submitSignedTx({ + signedXdr: 'this-is-not-base64-xdr!!', + expectedSigner: recipient.publicKey(), + }), + ).rejects.toThrow('not a valid transaction envelope'); + }); + }); + + describe('distributor-created packages', () => { + const createPackageArgs = (): xdr.ScVal[] => [ + nativeToScVal(operator.publicKey(), { type: 'address' }), + nativeToScVal(42, { type: 'u64' }), + nativeToScVal(recipient.publicKey(), { type: 'address' }), + nativeToScVal('250', { type: 'i128' }), + nativeToScVal( + 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4', + { + type: 'address', + }, + ), + nativeToScVal(1767225600, { type: 'u64' }), + nativeToScVal([], { type: 'map' }), + ]; + + beforeEach(() => { + mockServer.simulateTransaction.mockResolvedValue( + simulationWithEntry( + unsignedAuthEntry(operator, 'create_package', createPackageArgs()), + ), + ); + }); + + it('builds and submits a distributor-signed create_package for a non-admin operator', async () => { + const unsigned = await adapter.buildUnsignedCreatePackageTx({ + operatorAddress: operator.publicKey(), + packageId: '42', + recipientAddress: recipient.publicKey(), + amount: '250', + tokenAddress: + 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4', + expiresAt: 1767225600, + }); + expect(unsigned.operatorAddress).toBe(operator.publicKey()); + expect(unsigned.transactionHash).toMatch(/^[0-9a-f]{64}$/); + + const signedXdr = await signAuthEntries( + unsigned.transactionXdr, + operator, + ); + const result = await adapter.submitSignedTx({ + signedXdr, + expectedSigner: operator.publicKey(), + }); + + expect(result.status).toBe('success'); + expect(mockServer.sendTransaction).toHaveBeenCalledTimes(1); + }); + + it('rejects a create_package auth entry signed by the admin instead of the distributor', async () => { + const unsigned = await adapter.buildUnsignedCreatePackageTx({ + operatorAddress: operator.publicKey(), + packageId: '42', + recipientAddress: recipient.publicKey(), + amount: '250', + tokenAddress: + 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4', + expiresAt: 1767225600, + }); + const signedXdr = await signAuthEntries(unsigned.transactionXdr, admin); + + await expect( + adapter.submitSignedTx({ + signedXdr, + expectedSigner: operator.publicKey(), + }), + ).rejects.toThrow('but the contract requires'); + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/backend/src/onchain/soroban.adapter.ts b/app/backend/src/onchain/soroban.adapter.ts index 19bc796f..08fd3525 100644 --- a/app/backend/src/onchain/soroban.adapter.ts +++ b/app/backend/src/onchain/soroban.adapter.ts @@ -9,6 +9,11 @@ import { Keypair, BASE_FEE, xdr, + Address, + StrKey, + Operation, + hash, + Transaction, } from '@stellar/stellar-sdk'; import { OnchainAdapter, @@ -40,6 +45,12 @@ import { GetTransactionStatusParams, GetTransactionStatusResult, TxStatus, + BuildUnsignedClaimTxParams, + BuildUnsignedClaimTxResult, + BuildUnsignedCreatePackageParams, + BuildUnsignedCreatePackageResult, + SubmitSignedTxParams, + SubmitSignedTxResult, } from './onchain.adapter'; import { SorobanErrorMapper } from './utils/soroban-error.mapper'; import { withRetryTimeout } from './utils/retry-with-timeout'; @@ -47,6 +58,13 @@ import { toContractString, toStringRecord } from './utils/contract-value'; @Injectable() export class SorobanAdapter implements OnchainAdapter { + /** + * Client-signing envelopes are stamped with a best-effort freshness window; + * the on-chain `signature_expiration_ledger` (set by the RPC simulation) is + * the authoritative expiry, enforced by the network. + */ + private static readonly UNSIGNED_TX_TTL_SECONDS = 300; + private readonly logger = new Logger(SorobanAdapter.name); private readonly contractId: string; private readonly rpcUrl: string; @@ -132,11 +150,17 @@ export class SorobanAdapter implements OnchainAdapter { return `testnet-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } - private async submitContractOp( + /** + * Build the contract-call transaction, simulate it against the RPC server, + * and assemble the prepared (unsigned) envelope. The envelope source is the + * admin account, so the auth entries returned by the simulation demand the + * *caller's* signature (recipient / operator) while the admin sponsors fees. + */ + private async buildPreparedTransaction( method: string, args: xdr.ScVal[], correlationId: string, - ): Promise<{ hash: string; result: unknown }> { + ): Promise { const server = this.getServer(); const kp = this.getKeypair(); const contract = new Contract(this.contractId); @@ -176,12 +200,22 @@ export class SorobanAdapter implements OnchainAdapter { throw new Error(`Contract simulation error: ${errorMsg}`); } - const preparedTx = SorobanRpc.assembleTransaction(tx, simulation).build(); - preparedTx.sign(kp); + return SorobanRpc.assembleTransaction(tx, simulation).build(); + } + + /** + * Send a prepared transaction, poll until confirmed, and return the hash and + * decoded return value. Fails loudly instead of fabricating success. + */ + private async sendAndConfirm( + preparedTx: Transaction, + correlationId: string, + ): Promise<{ hash: string; result: unknown }> { + const server = this.getServer(); const sendResult = await withRetryTimeout( () => server.sendTransaction(preparedTx), - `sendTransaction(${method})`, + `sendTransaction(${correlationId})`, correlationId, {}, this.logger, @@ -199,7 +233,7 @@ export class SorobanAdapter implements OnchainAdapter { } return getResult; }, - `pollTransaction(${method})`, + `pollTransaction(${correlationId})`, correlationId, { maxRetries: 10, @@ -227,6 +261,21 @@ export class SorobanAdapter implements OnchainAdapter { ); } + private async submitContractOp( + method: string, + args: xdr.ScVal[], + correlationId: string, + ): Promise<{ hash: string; result: unknown }> { + const kp = this.getKeypair(); + const preparedTx = await this.buildPreparedTransaction( + method, + args, + correlationId, + ); + preparedTx.sign(kp); + return this.sendAndConfirm(preparedTx, correlationId); + } + private async simulateReadOnly( method: string, args: xdr.ScVal[], @@ -480,9 +529,18 @@ export class SorobanAdapter implements OnchainAdapter { const cid = this.correlationId(); this.logger.log(`[${cid}] claimAidPackage id=${params.packageId}`); + // The contract ABI is claim(id, claimer): the claimer argument was + // missing here, so the admin-signed claim never matched the entrypoint. + // This path only succeeds on-chain when the admin *is* the recipient; + // otherwise the contract's recipient.require_auth() rejects it, which is + // the honest outcome — use buildUnsignedClaimTx + submitSignedTx for a + // real recipient-signed claim. const { hash } = await this.submitContractOp( 'claim', - [this.scvU64(parseInt(params.packageId, 10))], + [ + this.scvU64(parseInt(params.packageId, 10)), + this.scvAddress(params.recipientAddress), + ], cid, ); @@ -499,6 +557,234 @@ export class SorobanAdapter implements OnchainAdapter { }; } + async buildUnsignedClaimTx( + params: BuildUnsignedClaimTxParams, + ): Promise { + this.ensureConfigured(); + const cid = this.correlationId(); + this.logger.log( + `[${cid}] buildUnsignedClaimTx id=${params.packageId} recipient=${params.recipientAddress}`, + ); + + const tx = await this.buildPreparedTransaction( + 'claim', + [ + this.scvU64(parseInt(params.packageId, 10)), + this.scvAddress(params.recipientAddress), + ], + cid, + ); + + return { + packageId: params.packageId, + recipientAddress: params.recipientAddress, + transactionXdr: tx.toXDR(), + transactionHash: tx.hash().toString('hex'), + expiresAt: + Math.floor(Date.now() / 1000) + SorobanAdapter.UNSIGNED_TX_TTL_SECONDS, + timestamp: new Date(), + }; + } + + async buildUnsignedCreatePackageTx( + params: BuildUnsignedCreatePackageParams, + ): Promise { + this.ensureConfigured(); + const cid = this.correlationId(); + this.logger.log( + `[${cid}] buildUnsignedCreatePackageTx id=${params.packageId} operator=${params.operatorAddress}`, + ); + + const metadata = params.metadata ?? {}; + const tx = await this.buildPreparedTransaction( + 'create_package', + [ + this.scvAddress(params.operatorAddress), + this.scvU64(parseInt(params.packageId, 10)), + this.scvAddress(params.recipientAddress), + this.scvI128(params.amount), + this.scvAddress(params.tokenAddress), + this.scvU64(params.expiresAt), + this.scvMap(metadata), + ], + cid, + ); + + return { + packageId: params.packageId, + operatorAddress: params.operatorAddress, + transactionXdr: tx.toXDR(), + transactionHash: tx.hash().toString('hex'), + expiresAt: + Math.floor(Date.now() / 1000) + SorobanAdapter.UNSIGNED_TX_TTL_SECONDS, + timestamp: new Date(), + }; + } + + async submitSignedTx( + params: SubmitSignedTxParams, + ): Promise { + this.ensureConfigured(); + const cid = this.correlationId(); + this.logger.log( + `[${cid}] submitSignedTx expectedSigner=${params.expectedSigner ?? 'unset'}`, + ); + + const kp = this.getKeypair(); + + let tx: Transaction; + try { + tx = TransactionBuilder.fromXDR( + params.signedXdr, + this.networkPassphrase, + ) as Transaction; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + throw new Error( + `submitSignedTx: signedXdr is not a valid transaction envelope (${msg})`, + ); + } + + if (tx.operations.length !== 1) { + throw new Error( + 'submitSignedTx: expected exactly one operation in the envelope', + ); + } + const operation = tx.operations[0]; + if (operation.type !== 'invokeHostFunction') { + throw new Error( + 'submitSignedTx: expected an invokeHostFunction (contract call) operation', + ); + } + + // Reject before submission if the Soroban auth entries were not signed by + // the account the contract requires (recipient / distributor). + this.verifySorobanAuthEntries(operation, params.expectedSigner); + + // Fee sponsorship: the envelope source is the admin account, so signing it + // with the admin keypair both authorises the transaction and pays fees. + tx.sign(kp); + + const { hash } = await this.sendAndConfirm(tx, cid); + + return { + transactionHash: hash, + status: 'success', + timestamp: new Date(), + metadata: { + contractId: this.contractId, + signer: params.expectedSigner, + }, + }; + } + + /** + * Verify that every Soroban auth entry on the operation carries a valid + * account signature from the account the entry requires — and, when + * `expectedSigner` is set, that this account is exactly the expected one. + * Mirrors the preimage construction the SDK's `authorizeEntry` uses to sign. + */ + private verifySorobanAuthEntries( + operation: Operation.InvokeHostFunction, + expectedSigner?: string, + ): void { + const entries = operation.auth ?? []; + if (entries.length === 0) { + throw new Error( + 'submitSignedTx: the transaction carries no Soroban auth entries to verify', + ); + } + + for (const entry of entries) { + const creds = entry.credentials(); + if ( + creds.switch() !== + xdr.SorobanCredentialsType.sorobanCredentialsAddress() + ) { + throw new Error( + 'submitSignedTx: unsupported auth credential type (expected an account signature)', + ); + } + const addrAuth = creds.address(); + if (addrAuth.signature().switch() === xdr.ScValType.scvVoid()) { + throw new Error( + 'submitSignedTx: auth entry is unsigned — the required signer never signed', + ); + } + + const sigList = this.authEntrySignatureList(addrAuth); + if (sigList.length === 0) { + throw new Error( + 'submitSignedTx: auth entry signature payload is malformed', + ); + } + + const publicKeyBytes = Buffer.from(sigList[0].public_key); + const signer = StrKey.encodeEd25519PublicKey(publicKeyBytes); + const required = Address.fromScAddress(addrAuth.address()).toString(); + + if (required !== signer) { + throw new Error( + `submitSignedTx: auth entry signed by ${signer} but the contract requires ${required}`, + ); + } + if (expectedSigner && signer !== expectedSigner) { + throw new Error( + `submitSignedTx: auth entry signed by ${signer}, expected ${expectedSigner}`, + ); + } + if (!this.verifyAuthEntrySignature(entry, signer)) { + throw new Error( + `submitSignedTx: auth entry signature is not cryptographically valid for ${signer}`, + ); + } + } + } + + /** + * Decode the `[{public_key, signature}]` vec a signed `SorobanAddressCredentials` + * carries. Returns an empty list when the ScVal is missing or malformed. + */ + private authEntrySignatureList( + addrAuth: xdr.SorobanAddressCredentials, + ): Array<{ public_key: Uint8Array; signature: Uint8Array }> { + const value = scValToNative(addrAuth.signature()) as unknown; + if (!Array.isArray(value) || value.length === 0) { + return []; + } + return value as Array<{ + public_key: Uint8Array; + signature: Uint8Array; + }>; + } + + private verifyAuthEntrySignature( + entry: xdr.SorobanAuthorizationEntry, + signer: string, + ): boolean { + const addrAuth = entry.credentials().address(); + const networkId = hash(Buffer.from(this.networkPassphrase)); + const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization( + new xdr.HashIdPreimageSorobanAuthorization({ + networkId, + nonce: addrAuth.nonce(), + invocation: entry.rootInvocation(), + signatureExpirationLedger: addrAuth.signatureExpirationLedger(), + }), + ); + const payload = hash(preimage.toXDR()); + const sigList = this.authEntrySignatureList(addrAuth); + if (sigList.length === 0) { + return false; + } + const signatureBytes = Buffer.from(sigList[0].signature); + try { + return Keypair.fromPublicKey(signer).verify(payload, signatureBytes); + } catch { + return false; + } + } + async disburseAidPackage( params: DisburseAidPackageParams, ): Promise { diff --git a/app/backend/test/coverage-baseline.json b/app/backend/test/coverage-baseline.json index 3c1daee6..9467ef26 100644 --- a/app/backend/test/coverage-baseline.json +++ b/app/backend/test/coverage-baseline.json @@ -99,7 +99,7 @@ ["src/onchain/onchain.adapter.ts",100,100,100,100], ["src/onchain/onchain.processor.ts",-33,-37,-3,-33], ["src/onchain/onchain.service.ts",-12,-8,-4,-12], - ["src/onchain/soroban.adapter.ts",-197,-100,-44,-201], + ["src/onchain/soroban.adapter.ts",-210,-110,-44,-215], ["src/onchain/utils/contract-value.ts",-14,-18,-2,-14], ["src/onchain/utils/retry-with-timeout.ts",-14,-7,-4,-16], ["src/onchain/utils/soroban-error.mapper.ts",-30,-36,-1,-30], diff --git a/app/backend/test/jest-coverage.js b/app/backend/test/jest-coverage.js index 2a4d995a..4a9bd065 100644 --- a/app/backend/test/jest-coverage.js +++ b/app/backend/test/jest-coverage.js @@ -17,7 +17,10 @@ module.exports = { '.*\\.spec\\.ts$', '.*verification-lifecycle\\.e2e-spec\\.ts$', ], - testPathIgnorePatterns: ['/test/idempotency\\.spec\\.ts$'], + testPathIgnorePatterns: [ + '/test/idempotency\\.spec\\.ts$', + '/soroban\\.adapter\\.signing\\.spec\\.ts$', + ], moduleNameMapper: { ...baseConfig.moduleNameMapper, '^cache/(.*)$': '/cache/$1', diff --git a/app/frontend/src/lib/generated/api.ts b/app/frontend/src/lib/generated/api.ts index ed75f2f5..39948479 100644 --- a/app/frontend/src/lib/generated/api.ts +++ b/app/frontend/src/lib/generated/api.ts @@ -114,6 +114,26 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/health/dependencies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Dependency health with on-chain latency + * @description Reports the time taken for an on-chain RPC call as onchain_rpc_ms. Status is "degraded" if it exceeds 5s. + */ + get: operations["HealthController_dependencies_v1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/health/error": { parameters: { query?: never; @@ -151,7 +171,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/ledger/backfill": { + "/api/v1/aid/campaigns": { parameters: { query?: never; header?: never; @@ -161,37 +181,41 @@ export interface paths { get?: never; put?: never; /** - * Trigger ledger backfill job - * @description Start a backfill job to process a range of ledgers and populate missing ledger entries. Idempotent - can be run repeatedly without duplicating data. + * Create a new campaign + * @description Initializes a new aid campaign with provided metadata. Requires appropriate permissions. */ - post: operations["LedgerAdminController_triggerBackfill_v1"]; + post: operations["AidController_createCampaign_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/admin/ledger/backfill/{jobId}": { + "/api/v1/aid/campaigns/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Get backfill job status - * @description Retrieve the current status of a backfill job. - */ - get: operations["LedgerAdminController_getBackfillStatus_v1"]; + get?: never; put?: never; post?: never; - delete?: never; + /** + * Archive a campaign + * @description Soft-archives a campaign, making it invisible to standard listings. + */ + delete: operations["AidController_archiveCampaign_v1"]; options?: never; head?: never; - patch?: never; + /** + * Update a campaign + * @description Modifies an existing campaign. Only provided fields will be updated. + */ + patch: operations["AidController_updateCampaign_v1"]; trace?: never; }; - "/api/v1/admin/ledger/reconcile": { + "/api/v1/aid/claims/{id}/status": { parameters: { query?: never; header?: never; @@ -199,95 +223,99 @@ export interface paths { cookie?: never; }; get?: never; - put?: never; /** - * Trigger ledger reconciliation job - * @description Start a reconciliation job to compare on-chain data against stored records and detect discrepancies. + * Transition a claim status + * @description Moves a claim from one status to another (e.g., pending -> approved). */ - post: operations["LedgerAdminController_triggerReconciliation_v1"]; + put: operations["AidController_transitionClaim_v1"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/admin/ledger/reconcile/{jobId}": { + "/api/v1/aid/webhook": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get reconciliation job status - * @description Retrieve the current status and report of a reconciliation job. + * Webhook for AI task notifications + * @description Receives notifications from the AI service when background tasks complete. */ - get: operations["LedgerAdminController_getReconciliationStatus_v1"]; - put?: never; - post?: never; + post: operations["AidController_handleTaskWebhook_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/jobs/status": { + "/api/v1/onchain/aid-escrow/packages": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get status of all background job queues - * @description Retrieves the count of waiting, active, completed, failed, and delayed jobs for all system queues. + * Create an aid package + * @description Creates a new aid package with specified recipient, amount, and expiration. Only authorized operators can create packages. */ - get: operations["JobsController_getStatus_v1"]; - put?: never; - post?: never; + post: operations["AidEscrowController_createAidPackage_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/jobs/health": { + "/api/v1/onchain/aid-escrow/packages/batch": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get overall health of background job queues - * @description Checks if any core queues are degraded (too many waiting or failed jobs). + * Batch create aid packages + * @description Creates multiple aid packages for multiple recipients in a single transaction. More efficient than individual creation. */ - get: operations["JobsController_getHealth_v1"]; - put?: never; - post?: never; + post: operations["AidEscrowController_batchCreateAidPackages_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/metrics": { + "/api/v1/onchain/aid-escrow/packages/{id}/claim": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["PrometheusController_index_v1"]; + get?: never; put?: never; - post?: never; + /** + * Claim an aid package + * @description Claims an aid package as the recipient, transferring the funds to their wallet. Can only be claimed once. + */ + post: operations["AidEscrowController_claimAidPackage_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/aid/campaigns": { + "/api/v1/onchain/aid-escrow/packages/{id}/disburse": { parameters: { query?: never; header?: never; @@ -297,53 +325,49 @@ export interface paths { get?: never; put?: never; /** - * Create a new campaign - * @description Initializes a new aid campaign with provided metadata. Requires appropriate permissions. + * Disburse an aid package + * @description Disburses an aid package from the admin/operator, transferring funds to the recipient. Admin-only action. */ - post: operations["AidController_createCampaign_v1"]; + post: operations["AidEscrowController_disburseAidPackage_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/aid/campaigns/{id}": { + "/api/v1/onchain/aid-escrow/packages/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; /** - * Archive a campaign - * @description Soft-archives a campaign, making it invisible to standard listings. + * Get aid package details + * @description Retrieves the full details of an aid package including status, amount, and expiration. */ - delete: operations["AidController_archiveCampaign_v1"]; + get: operations["AidEscrowController_getAidPackage_v1"]; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; - /** - * Update a campaign - * @description Modifies an existing campaign. Only provided fields will be updated. - */ - patch: operations["AidController_updateCampaign_v1"]; + patch?: never; trace?: never; }; - "/api/v1/aid/claims/{id}/status": { + "/api/v1/onchain/aid-escrow/stats": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; /** - * Transition a claim status - * @description Moves a claim from one status to another (e.g., pending -> approved). + * Get aid package statistics + * @description Retrieves aggregated statistics for aid packages by token, including total committed, claimed, and expired amounts. */ - put: operations["AidController_transitionClaim_v1"]; + get: operations["AidEscrowController_getAidPackageStats_v1"]; + put?: never; post?: never; delete?: never; options?: never; @@ -351,20 +375,20 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/aid/webhook": { + "/api/v1/onchain/aid-escrow/transactions/{hash}/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Webhook for AI task notifications - * @description Receives notifications from the AI service when background tasks complete. + * Get transaction status + * @description Polls Soroban RPC for the status of a transaction by its hash. Returns a normalized status: pending, succeeded, failed, or unknown. */ - post: operations["AidController_handleTaskWebhook_v1"]; + get: operations["AidEscrowController_getTransactionStatus_v1"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; @@ -779,40 +803,20 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/notifications/outbox": { + "/api/v1/csp-report": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * List stuck notification outbox records - * @description Returns all NotificationOutbox records in pending or enqueued status whose scheduledFor is more than 10 minutes in the past. - */ - get: operations["OutboxController_listStuck_v1"]; + get?: never; put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notifications/outbox/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; /** - * Get a single notification outbox record - * @description Returns the NotificationOutbox record for the given id. + * Receive CSP violation reports + * @description Endpoint to receive and log Content Security Policy violations. */ - get: operations["OutboxController_getOne_v1"]; - put?: never; - post?: never; + post: operations["CspReportController_handleCspReport_v1"]; delete?: never; options?: never; head?: never; @@ -904,6 +908,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/test-error/service-unavailable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Trigger a ServiceUnavailableException */ + get: operations["TestErrorController_getServiceUnavailable_v1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/test-error/not-found": { parameters: { query?: never; @@ -1204,46 +1225,6 @@ export interface paths { patch: operations["ClaimsController_archive_v1"]; trace?: never; }; - "/api/v1/claims/{id}/receipt": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get claim receipt - * @description Generates a shareable receipt for the specified claim. - */ - get: operations["ClaimsController_getReceipt_v1"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/claims/{id}/receipt/share": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Share claim receipt - * @description Generates and optionally sends the claim receipt via email or SMS. - */ - post: operations["ClaimsController_shareReceipt_v1"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/v1/claims/{id}/notes": { parameters: { query?: never; @@ -1328,7 +1309,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/claims/export": { + "/api/v1/claims/{id}/receipt": { parameters: { query?: never; header?: never; @@ -1336,10 +1317,10 @@ export interface paths { cookie?: never; }; /** - * Export claims as CSV - * @description Exports claim records as CSV with support for date range, status, organization, token, and pagination filters. Excludes sensitive recipient data (recipientRef is encrypted and not exported). + * Get claim receipt + * @description Generates a shareable receipt for the specified claim. */ - get: operations["ClaimsController_exportClaims_v1"]; + get: operations["ClaimsController_getReceipt_v1"]; put?: never; post?: never; delete?: never; @@ -1348,27 +1329,27 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/analytics/global-stats": { + "/api/v1/claims/{id}/receipt/share": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get global distribution statistics - * @description Returns aggregated totals and breakdowns by token and region for the dashboard. + * Share claim receipt + * @description Generates and optionally sends the claim receipt via email or SMS. */ - get: operations["AnalyticsController_getGlobalStats_v1"]; - put?: never; - post?: never; + post: operations["ClaimsController_shareReceipt_v1"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/analytics/map-data": { + "/api/v1/claims/export": { parameters: { query?: never; header?: never; @@ -1376,10 +1357,10 @@ export interface paths { cookie?: never; }; /** - * Get map data points - * @description Returns a list of distribution points with coordinates and amounts for map visualization. + * Export claims as CSV + * @description Exports claim records as CSV with support for date range, status, organization, token, and pagination filters. Excludes sensitive recipient data (recipientRef is encrypted and not exported). */ - get: operations["AnalyticsController_getMapData_v1"]; + get: operations["ClaimsController_exportClaims_v1"]; put?: never; post?: never; delete?: never; @@ -1388,7 +1369,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/analytics/map-anonymized": { + "/api/v1/notifications/outbox": { parameters: { query?: never; header?: never; @@ -1396,10 +1377,10 @@ export interface paths { cookie?: never; }; /** - * Get anonymized map data (GeoJSON) - * @description Returns distribution data in GeoJSON format with anonymized locations for privacy. + * List stuck notification outbox records + * @description Returns all NotificationOutbox records in pending or enqueued status whose scheduledFor is more than 10 minutes in the past. */ - get: operations["AnalyticsController_getMapAnonymizedData_v1"]; + get: operations["OutboxController_listStuck_v1"]; put?: never; post?: never; delete?: never; @@ -1408,87 +1389,67 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/onchain/aid-escrow/packages": { + "/api/v1/notifications/outbox/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Create an aid package - * @description Creates a new aid package with specified recipient, amount, and expiration. Only authorized operators can create packages. + * Get a single notification outbox record + * @description Returns the NotificationOutbox record for the given id. */ - post: operations["AidEscrowController_createAidPackage_v1"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/onchain/aid-escrow/packages/batch": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; + get: operations["OutboxController_getOne_v1"]; put?: never; - /** - * Batch create aid packages - * @description Creates multiple aid packages for multiple recipients in a single transaction. More efficient than individual creation. - */ - post: operations["AidEscrowController_batchCreateAidPackages_v1"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/onchain/aid-escrow/packages/{id}/claim": { + "/api/v1/jobs/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Claim an aid package - * @description Claims an aid package as the recipient, transferring the funds to their wallet. Can only be claimed once. + * Get status of all background job queues + * @description Retrieves the count of waiting, active, completed, failed, and delayed jobs for all system queues. */ - post: operations["AidEscrowController_claimAidPackage_v1"]; + get: operations["JobsController_getStatus_v1"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/onchain/aid-escrow/packages/{id}/disburse": { + "/api/v1/jobs/health": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Disburse an aid package - * @description Disburses an aid package from the admin/operator, transferring funds to the recipient. Admin-only action. + * Get overall health of background job queues + * @description Checks if any core queues are degraded (too many waiting or failed jobs). */ - post: operations["AidEscrowController_disburseAidPackage_v1"]; + get: operations["JobsController_getHealth_v1"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/onchain/aid-escrow/packages/{id}": { + "/api/v1/analytics/global-stats": { parameters: { query?: never; header?: never; @@ -1496,10 +1457,10 @@ export interface paths { cookie?: never; }; /** - * Get aid package details - * @description Retrieves the full details of an aid package including status, amount, and expiration. + * Get global distribution statistics + * @description Returns aggregated totals and breakdowns by token and region for the dashboard. */ - get: operations["AidEscrowController_getAidPackage_v1"]; + get: operations["AnalyticsController_getGlobalStats_v1"]; put?: never; post?: never; delete?: never; @@ -1508,7 +1469,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/onchain/aid-escrow/stats": { + "/api/v1/analytics/map-data": { parameters: { query?: never; header?: never; @@ -1516,10 +1477,10 @@ export interface paths { cookie?: never; }; /** - * Get aid package statistics - * @description Retrieves aggregated statistics for aid packages by token, including total committed, claimed, and expired amounts. + * Get map data points + * @description Returns a list of distribution points with coordinates and amounts for map visualization. */ - get: operations["AidEscrowController_getAidPackageStats_v1"]; + get: operations["AnalyticsController_getMapData_v1"]; put?: never; post?: never; delete?: never; @@ -1528,7 +1489,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/onchain/aid-escrow/transactions/{hash}/status": { + "/api/v1/analytics/map-anonymized": { parameters: { query?: never; header?: never; @@ -1536,10 +1497,10 @@ export interface paths { cookie?: never; }; /** - * Get transaction status - * @description Polls Soroban RPC for the status of a transaction by its hash. Returns a normalized status: pending, succeeded, failed, or unknown. + * Get anonymized map data (GeoJSON) + * @description Returns distribution data in GeoJSON format with anonymized locations for privacy. */ - get: operations["AidEscrowController_getTransactionStatus_v1"]; + get: operations["AnalyticsController_getMapAnonymizedData_v1"]; put?: never; post?: never; delete?: never; @@ -1603,6 +1564,77 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/oauth/token": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Issue or refresh OAuth-compatible JWTs + * @description Supports client_credentials using an existing ChainForge API key as client_secret, and refresh_token. No password login flow exists in this backend. + */ + post: operations["TokenController_token_v1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/oauth/userinfo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Return claims for the current JWT principal */ + get: operations["TokenController_userinfo_v1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/oauth/introspect": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Introspect a JWT using RFC 7662 active/inactive shape */ + post: operations["TokenController_introspect_v1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/oauth/revoke": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Revoke a JWT by adding its jti to Redis */ + post: operations["TokenController_revoke_v1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/sessions": { parameters: { query?: never; @@ -2320,6 +2352,102 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/ledger/backfill": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger ledger backfill job + * @description Start a backfill job to process a range of ledgers and populate missing ledger entries. Idempotent - can be run repeatedly without duplicating data. + */ + post: operations["LedgerAdminController_triggerBackfill_v1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/ledger/backfill/{jobId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get backfill job status + * @description Retrieve the current status of a backfill job. + */ + get: operations["LedgerAdminController_getBackfillStatus_v1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/ledger/reconcile": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger ledger reconciliation job + * @description Start a reconciliation job to compare on-chain data against stored records and detect discrepancies. + */ + post: operations["LedgerAdminController_triggerReconciliation_v1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/ledger/reconcile/{jobId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get reconciliation job status + * @description Retrieve the current status and report of a reconciliation job. + */ + get: operations["LedgerAdminController_getReconciliationStatus_v1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PrometheusController_index_v1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -2370,18 +2498,83 @@ export interface components { */ completedAt?: string; }; - StartVerificationDto: { + CreateAidPackageDto: { /** - * @description Verification channel: email or phone - * @example email - * @enum {string} + * @description Unique identifier for the package + * @example pkg_123456789 */ - channel: "email" | "phone"; + packageId: string; /** - * @description Email address (required when channel is email) - * @example user@example.com + * @description Stellar address of the aid recipient + * @example GBUQWP3BOUZX34ULNQG23RQ6F4BFXWBTRSE53XSTE23JMCVOCJGXVSVZ */ - email?: string; + recipientAddress: string; + /** + * @description Amount in stroops (i128 as string to preserve precision) + * @example 1000000000 + */ + amount: string; + /** + * @description Stellar token address + * @example GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ5LKG3FZTSZ3NYNEJBBENSN + */ + tokenAddress: string; + /** + * @description Unix timestamp when the package expires + * @example 1704067200 + */ + expiresAt: number; + /** + * @description Optional metadata as key-value pairs + * @example { + * "campaign_ref": "campaign-123", + * "region": "LATAM" + * } + */ + metadata?: Record; + }; + BatchCreateAidPackagesDto: { + /** + * @description Array of recipient Stellar addresses + * @example [ + * "GBUQWP3BOUZX34ULNQG23RQ6F4BFXWBTRSE53XSTE23JMCVOCJGXVSVZ", + * "GA5ZSEJYB37JRC5AVCIA5MOP4GZ5DA47EL5QRUVLYEK2OOABEXVR5CV7" + * ] + */ + recipientAddresses: string[]; + /** + * @description Array of amounts (in stroops, as strings) + * @example [ + * "1000000000", + * "500000000" + * ] + */ + amounts: string[]; + /** + * @description Stellar token address + * @example GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ5LKG3FZTSZ3NYNEJBBENSN + */ + tokenAddress: string; + /** + * @description Duration in seconds from now until expiration + * @example 2592000 + */ + expiresIn: number; + /** @description Optional metadata as key-value pairs */ + metadata?: Record; + }; + StartVerificationDto: { + /** + * @description Verification channel: email or phone + * @example email + * @enum {string} + */ + channel: "email" | "phone"; + /** + * @description Email address (required when channel is email) + * @example user@example.com + */ + email?: string; /** * @description Phone number (required when channel is phone) * @example +15551234567 @@ -2554,6 +2747,40 @@ export interface components { */ expiresAt?: string; }; + CancelClaimDto: { + /** + * @description ID of the operator performing the cancellation. + * @example operator-uuid + */ + operatorId: string; + /** + * @description Human-readable reason for cancellation. + * @example Recipient relocated; package no longer applicable. + */ + reason?: string; + }; + ReissueClaimDto: { + /** + * @description ID of the operator performing the reissue. + * @example operator-uuid + */ + operatorId: string; + /** + * @description Override amount for the replacement package. Defaults to the original amount when omitted. + * @example 750 + */ + amount?: number; + /** + * @description Override recipient reference for the replacement package. + * @example recipient-ref-new + */ + recipientRef?: string; + /** + * @description Human-readable reason for the reissue. + * @example Corrected amount after field verification. + */ + reason?: string; + }; ClaimReceiptDto: { /** * @description Unique claim identifier @@ -2632,40 +2859,6 @@ export interface components { /** @description Text representation of receipt for sharing */ text: string; }; - CancelClaimDto: { - /** - * @description ID of the operator performing the cancellation. - * @example operator-uuid - */ - operatorId: string; - /** - * @description Human-readable reason for cancellation. - * @example Recipient relocated; package no longer applicable. - */ - reason?: string; - }; - ReissueClaimDto: { - /** - * @description ID of the operator performing the reissue. - * @example operator-uuid - */ - operatorId: string; - /** - * @description Override amount for the replacement package. Defaults to the original amount when omitted. - * @example 750 - */ - amount?: number; - /** - * @description Override recipient reference for the replacement package. - * @example recipient-ref-new - */ - recipientRef?: string; - /** - * @description Human-readable reason for the reissue. - * @example Corrected amount after field verification. - */ - reason?: string; - }; BreakdownEntry: { /** @example USDC */ label: string; @@ -2738,71 +2931,6 @@ export interface components { /** @example 2026-03-30T10:00:00Z */ computedAt: string; }; - CreateAidPackageDto: { - /** - * @description Unique identifier for the package - * @example pkg_123456789 - */ - packageId: string; - /** - * @description Stellar address of the aid recipient - * @example GBUQWP3BOUZX34ULNQG23RQ6F4BFXWBTRSE53XSTE23JMCVOCJGXVSVZ - */ - recipientAddress: string; - /** - * @description Amount in stroops (i128 as string to preserve precision) - * @example 1000000000 - */ - amount: string; - /** - * @description Stellar token address - * @example GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ5LKG3FZTSZ3NYNEJBBENSN - */ - tokenAddress: string; - /** - * @description Unix timestamp when the package expires - * @example 1704067200 - */ - expiresAt: number; - /** - * @description Optional metadata as key-value pairs - * @example { - * "campaign_ref": "campaign-123", - * "region": "LATAM" - * } - */ - metadata?: Record; - }; - BatchCreateAidPackagesDto: { - /** - * @description Array of recipient Stellar addresses - * @example [ - * "GBUQWP3BOUZX34ULNQG23RQ6F4BFXWBTRSE53XSTE23JMCVOCJGXVSVZ", - * "GA5ZSEJYB37JRC5AVCIA5MOP4GZ5DA47EL5QRUVLYEK2OOABEXVR5CV7" - * ] - */ - recipientAddresses: string[]; - /** - * @description Array of amounts (in stroops, as strings) - * @example [ - * "1000000000", - * "500000000" - * ] - */ - amounts: string[]; - /** - * @description Stellar token address - * @example GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ5LKG3FZTSZ3NYNEJBBENSN - */ - tokenAddress: string; - /** - * @description Duration in seconds from now until expiration - * @example 2592000 - */ - expiresIn: number; - /** @description Optional metadata as key-value pairs */ - metadata?: Record; - }; CreateApiKeyDto: { /** * @description Role associated with this API key. @@ -2828,6 +2956,27 @@ export interface components { */ reason?: string; }; + TokenRequestDto: { + /** + * @description OAuth grant type. This backend has no password login flow, so JWT issuance is based on existing API-key client credentials. + * @enum {string} + */ + grant_type: "client_credentials" | "refresh_token"; + /** @description Optional client identifier. API-key records are validated by client_secret. */ + client_id?: string; + /** @description Existing ChainForge API key used as the OAuth client secret. */ + client_secret?: string; + /** @description Refresh token, required when grant_type=refresh_token. */ + refresh_token?: string; + }; + TokenIntrospectionDto: { + /** @description JWT access or refresh token to introspect. */ + token: string; + }; + TokenRevocationDto: { + /** @description JWT access or refresh token to revoke. */ + token: string; + }; SessionStepDefinitionDto: { /** @description Name of the step */ stepName: string; @@ -3089,6 +3238,31 @@ export interface operations { }; }; }; + HealthController_dependencies_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dependency health retrieved. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description On-chain dependency is down. */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; HealthController_triggerError_v1: { parameters: { query?: never; @@ -3132,88 +3306,51 @@ export interface operations { }; }; }; - LedgerAdminController_triggerBackfill_v1: { + AidController_createCampaign_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": { - /** @description Starting ledger sequence number */ - startLedger: number; - /** @description Ending ledger sequence number */ - endLedger: number; - /** @description Optional campaign ID to filter */ - campaignId?: string; - /** @description Number of ledgers to process per batch (default: 100) */ - batchSize?: number; - }; - }; - }; + requestBody?: never; responses: { - /** @description Backfill job queued successfully. */ - 200: { + /** @description Campaign created successfully. */ + 201: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; - }; + content?: never; }; - /** @description Invalid request parameters. */ + /** @description Invalid input parameters. */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Unauthorized - valid JWT token required. */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Access denied - admin role required. */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; }; }; - LedgerAdminController_getBackfillStatus_v1: { + AidController_archiveCampaign_v1: { parameters: { query?: never; header?: never; path: { - /** @description Job ID returned from triggerBackfill */ - jobId: string; + id: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Backfill status retrieved successfully. */ + /** @description Campaign archived successfully. */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Unauthorized - valid JWT token required. */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Access denied - admin role required. */ - 403: { + /** @description The specified campaign was not found. */ + 404: { headers: { [name: string]: unknown; }; @@ -3221,53 +3358,33 @@ export interface operations { }; }; }; - LedgerAdminController_triggerReconciliation_v1: { + AidController_updateCampaign_v1: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** @description Starting ledger sequence number */ - startLedger: number; - /** @description Ending ledger sequence number */ - endLedger: number; - /** @description Optional campaign ID to filter */ - campaignId?: string; - /** @description Threshold percentage for amount mismatch (default: 5) */ - thresholdPercent?: number; - }; + path: { + id: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description Reconciliation job queued successfully. */ + /** @description Campaign updated successfully. */ 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Invalid request parameters. */ - 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Unauthorized - valid JWT token required. */ - 401: { + /** @description Invalid update data. */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Access denied - admin role required. */ - 403: { + /** @description The specified campaign was not found. */ + 404: { headers: { [name: string]: unknown; }; @@ -3275,34 +3392,33 @@ export interface operations { }; }; }; - LedgerAdminController_getReconciliationStatus_v1: { + AidController_transitionClaim_v1: { parameters: { query?: never; header?: never; path: { - /** @description Job ID returned from triggerReconciliation */ - jobId: string; + id: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Reconciliation status retrieved successfully. */ + /** @description Claim status transitioned successfully. */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Unauthorized - valid JWT token required. */ - 401: { + /** @description Invalid status transition requested. */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Access denied - admin role required. */ - 403: { + /** @description The specified claim was not found. */ + 404: { headers: { [name: string]: unknown; }; @@ -3310,53 +3426,66 @@ export interface operations { }; }; }; - JobsController_getStatus_v1: { + AidController_handleTaskWebhook_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AiTaskWebhookDto"]; + }; + }; responses: { - /** @description Queue statuses retrieved successfully. */ + /** @description Webhook received successfully. */ 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; + content?: never; + }; + /** @description Invalid webhook payload. */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - JobsController_getHealth_v1: { + AidEscrowController_createAidPackage_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateAidPackageDto"]; + }; + }; responses: { - 200: { + /** @description Package created successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid input parameters. */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - }; - }; - PrometheusController_index_v1: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { + /** @description Blockchain transaction failed. */ + 500: { headers: { [name: string]: unknown; }; @@ -3364,32 +3493,45 @@ export interface operations { }; }; }; - AidController_createCampaign_v1: { + AidEscrowController_batchCreateAidPackages_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["BatchCreateAidPackagesDto"]; + }; + }; responses: { - /** @description Campaign created successfully. */ + /** @description Packages created successfully. */ 201: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; - /** @description Invalid input parameters. */ + /** @description Invalid input or mismatched arrays. */ 400: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Blockchain transaction failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - AidController_archiveCampaign_v1: { + AidEscrowController_claimAidPackage_v1: { parameters: { query?: never; header?: never; @@ -3400,23 +3542,39 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Campaign archived successfully. */ + /** @description Package claimed successfully. */ 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Package not found or not claimable. */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description The specified campaign was not found. */ + /** @description Package does not exist. */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Blockchain transaction failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - AidController_updateCampaign_v1: { + AidEscrowController_disburseAidPackage_v1: { parameters: { query?: never; header?: never; @@ -3427,30 +3585,39 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Campaign updated successfully. */ + /** @description Package disbursed successfully. */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; - /** @description Invalid update data. */ + /** @description Package not found or not disbursable. */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description The specified campaign was not found. */ + /** @description Package does not exist. */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Blockchain transaction failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - AidController_transitionClaim_v1: { + AidEscrowController_getAidPackage_v1: { parameters: { query?: never; header?: never; @@ -3461,22 +3628,24 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Claim status transitioned successfully. */ + /** @description Package details retrieved successfully. */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; - /** @description Invalid status transition requested. */ - 400: { + /** @description Package not found. */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description The specified claim was not found. */ - 404: { + /** @description Failed to retrieve package. */ + 500: { headers: { [name: string]: unknown; }; @@ -3484,49 +3653,53 @@ export interface operations { }; }; }; - AidController_handleTaskWebhook_v1: { + AidEscrowController_getAidPackageStats_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AiTaskWebhookDto"]; - }; - }; + requestBody?: never; responses: { - /** @description Webhook received successfully. */ + /** @description Statistics retrieved successfully. */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; - /** @description Invalid webhook payload. */ + /** @description Invalid token address. */ 400: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Failed to retrieve statistics. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - VerificationController_enqueueVerification_v1: { + AidEscrowController_getTransactionStatus_v1: { parameters: { query?: never; header?: never; path: { - /** @description Unique identifier of the claim to verify */ - id: string; + hash: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Verification job enqueued successfully. */ - 202: { + /** @description Transaction status retrieved successfully. */ + 200: { headers: { [name: string]: unknown; }; @@ -3534,15 +3707,59 @@ export interface operations { "application/json": unknown; }; }; - /** @description Invalid claim ID or malformed request. */ + /** @description Invalid transaction hash. */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Invalid or missing API key. */ - 401: { + /** @description Transaction not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Failed to retrieve transaction status. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + VerificationController_enqueueVerification_v1: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Unique identifier of the claim to verify */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Verification job enqueued successfully. */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid claim ID or malformed request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or missing API key. */ + 401: { headers: { [name: string]: unknown; }; @@ -4315,7 +4532,7 @@ export interface operations { }; }; }; - OutboxController_listStuck_v1: { + CspReportController_handleCspReport_v1: { parameters: { query?: never; header?: never; @@ -4324,63 +4541,7 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Stuck outbox records returned. */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Missing or invalid API key. */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Insufficient role (requires admin or operator). */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - OutboxController_getOne_v1: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Outbox record returned. */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Missing or invalid API key. */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Insufficient role (requires admin or operator). */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Outbox record not found. */ - 404: { + 201: { headers: { [name: string]: unknown; }; @@ -4478,6 +4639,24 @@ export interface operations { }; }; }; + TestErrorController_getServiceUnavailable_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Service unavailable error triggered. */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; TestErrorController_getNotFound_v1: { parameters: { query?: never; @@ -5105,75 +5284,6 @@ export interface operations { }; }; }; - ClaimsController_getReceipt_v1: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Claim receipt generated successfully. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ClaimReceiptDto"]; - }; - }; - /** @description The specified claim was not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ClaimsController_shareReceipt_v1: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SendReceiptShareDto"]; - }; - }; - responses: { - /** @description Receipt generated and sharing initiated successfully. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ClaimShareResponseDto"]; - }; - }; - /** @description Invalid share parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description The specified claim was not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; ClaimsController_getNotes_v1: { parameters: { query?: never; @@ -5369,50 +5479,28 @@ export interface operations { }; }; }; - ClaimsController_exportClaims_v1: { + ClaimsController_getReceipt_v1: { parameters: { - query?: { - /** @description Items per page (default: 50, max: 200) */ - limit?: unknown; - /** @description Page number (default: 1) */ - page?: unknown; - /** @description Token address filter */ - tokenAddress?: unknown; - /** @description Organization ID filter */ - orgId?: unknown; - /** @description Campaign ID filter */ - campaignId?: unknown; - /** @description Claim status filter */ - status?: unknown; - /** @description End date (ISO string) */ - to?: unknown; - /** @description Start date (ISO string) */ - from?: unknown; - }; + query?: never; header?: never; - path?: never; + path: { + id: string; + }; cookie?: never; }; requestBody?: never; responses: { - /** @description Claims exported successfully. */ + /** @description Claim receipt generated successfully. */ 200: { headers: { [name: string]: unknown; }; content: { - "text/csv": string; - }; - }; - /** @description Missing or invalid authentication credentials. */ - 401: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["ClaimReceiptDto"]; }; - content?: never; }; - /** @description Access denied - operator or admin role required. */ - 403: { + /** @description The specified claim was not found. */ + 404: { headers: { [name: string]: unknown; }; @@ -5420,61 +5508,65 @@ export interface operations { }; }; }; - AnalyticsController_getGlobalStats_v1: { + ClaimsController_shareReceipt_v1: { parameters: { - query?: { - from?: string; - to?: string; - region?: string; - token?: string; - }; + query?: never; header?: never; - path?: never; + path: { + id: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SendReceiptShareDto"]; + }; + }; responses: { - /** @description Global statistics retrieved successfully. */ + /** @description Receipt generated and sharing initiated successfully. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GlobalStatsDto"]; + "application/json": components["schemas"]["ClaimShareResponseDto"]; }; }; - }; - }; - AnalyticsController_getMapData_v1: { - parameters: { - query?: { - region?: string; - token?: string; - status?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Map data points retrieved successfully. */ - 200: { + /** @description Invalid share parameters. */ + 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["MapDataDto"]; + content?: never; + }; + /** @description The specified claim was not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - AnalyticsController_getMapAnonymizedData_v1: { + ClaimsController_exportClaims_v1: { parameters: { query?: { - region?: string; - token?: string; - status?: string; + /** @description Items per page (default: 50, max: 200) */ + limit?: unknown; + /** @description Page number (default: 1) */ + page?: unknown; + /** @description Token address filter */ + tokenAddress?: unknown; + /** @description Organization ID filter */ + orgId?: unknown; + /** @description Campaign ID filter */ + campaignId?: unknown; + /** @description Claim status filter */ + status?: unknown; + /** @description End date (ISO string) */ + to?: unknown; + /** @description Start date (ISO string) */ + from?: unknown; }; header?: never; path?: never; @@ -5482,48 +5574,24 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Anonymized GeoJSON data retrieved successfully. */ + /** @description Claims exported successfully. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GeoJsonFeatureCollection"]; - }; - }; - }; - }; - AidEscrowController_createAidPackage_v1: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateAidPackageDto"]; - }; - }; - responses: { - /** @description Package created successfully. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; + "text/csv": string; }; }; - /** @description Invalid input parameters. */ - 400: { + /** @description Missing or invalid authentication credentials. */ + 401: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Blockchain transaction failed. */ - 500: { + /** @description Access denied - operator or admin role required. */ + 403: { headers: { [name: string]: unknown; }; @@ -5531,37 +5599,31 @@ export interface operations { }; }; }; - AidEscrowController_batchCreateAidPackages_v1: { + OutboxController_listStuck_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["BatchCreateAidPackagesDto"]; - }; - }; + requestBody?: never; responses: { - /** @description Packages created successfully. */ - 201: { + /** @description Stuck outbox records returned. */ + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; - }; + content?: never; }; - /** @description Invalid input or mismatched arrays. */ - 400: { + /** @description Missing or invalid API key. */ + 401: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Blockchain transaction failed. */ - 500: { + /** @description Insufficient role (requires admin or operator). */ + 403: { headers: { [name: string]: unknown; }; @@ -5569,7 +5631,7 @@ export interface operations { }; }; }; - AidEscrowController_claimAidPackage_v1: { + OutboxController_getOne_v1: { parameters: { query?: never; header?: never; @@ -5580,31 +5642,29 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Package claimed successfully. */ + /** @description Outbox record returned. */ 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; - }; + content?: never; }; - /** @description Package not found or not claimable. */ - 400: { + /** @description Missing or invalid API key. */ + 401: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Package does not exist. */ - 404: { + /** @description Insufficient role (requires admin or operator). */ + 403: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Blockchain transaction failed. */ - 500: { + /** @description Outbox record not found. */ + 404: { headers: { [name: string]: unknown; }; @@ -5612,18 +5672,16 @@ export interface operations { }; }; }; - AidEscrowController_disburseAidPackage_v1: { + JobsController_getStatus_v1: { parameters: { query?: never; header?: never; - path: { - id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Package disbursed successfully. */ + /** @description Queue statuses retrieved successfully. */ 200: { headers: { [name: string]: unknown; @@ -5632,58 +5690,109 @@ export interface operations { "application/json": unknown; }; }; - /** @description Package not found or not disbursable. */ - 400: { + }; + }; + JobsController_getHealth_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Package does not exist. */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; + }; + }; + AnalyticsController_getGlobalStats_v1: { + parameters: { + query?: { + from?: string; + to?: string; + region?: string; + token?: string; }; - /** @description Blockchain transaction failed. */ - 500: { + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Global statistics retrieved successfully. */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["GlobalStatsDto"]; + }; }; }; }; - AidEscrowController_getAidPackage_v1: { + AnalyticsController_getMapData_v1: { parameters: { - query?: never; - header?: never; - path: { - id: string; + query?: { + region?: string; + token?: string; + status?: string; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Package details retrieved successfully. */ + /** @description Map data points retrieved successfully. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["MapDataDto"]; }; }; - /** @description Package not found. */ - 404: { + }; + }; + AnalyticsController_getMapAnonymizedData_v1: { + parameters: { + query?: { + region?: string; + token?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Anonymized GeoJSON data retrieved successfully. */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["GeoJsonFeatureCollection"]; + }; }; - /** @description Failed to retrieve package. */ - 500: { + }; + }; + ApiKeysController_list_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description API keys listed. */ + 200: { headers: { [name: string]: unknown; }; @@ -5691,33 +5800,42 @@ export interface operations { }; }; }; - AidEscrowController_getAidPackageStats_v1: { + ApiKeysController_create_v1: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateApiKeyDto"]; + }; + }; responses: { - /** @description Statistics retrieved successfully. */ - 200: { + /** @description API key created. */ + 201: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; - }; + content?: never; }; - /** @description Invalid token address. */ + /** @description Invalid payload. */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Failed to retrieve statistics. */ - 500: { + /** @description Missing or invalid credentials. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient permissions. */ + 403: { headers: { [name: string]: unknown; }; @@ -5725,42 +5843,26 @@ export interface operations { }; }; }; - AidEscrowController_getTransactionStatus_v1: { + ApiKeysController_rotate_v1: { parameters: { query?: never; header?: never; path: { - hash: string; + id: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Transaction status retrieved successfully. */ + /** @description API key rotated. */ 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Invalid transaction hash. */ - 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Transaction not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Failed to retrieve transaction status. */ - 500: { + /** @description Cannot rotate revoked key. */ + 400: { headers: { [name: string]: unknown; }; @@ -5768,16 +5870,22 @@ export interface operations { }; }; }; - ApiKeysController_list_v1: { + ApiKeysController_revoke_v1: { parameters: { query?: never; header?: never; - path?: never; + path: { + id: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["RevokeApiKeyDto"]; + }; + }; responses: { - /** @description API keys listed. */ + /** @description API key revoked. */ 200: { headers: { [name: string]: unknown; @@ -5786,7 +5894,7 @@ export interface operations { }; }; }; - ApiKeysController_create_v1: { + TokenController_token_v1: { parameters: { query?: never; header?: never; @@ -5795,33 +5903,12 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["CreateApiKeyDto"]; + "application/json": components["schemas"]["TokenRequestDto"]; }; }; responses: { - /** @description API key created. */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid payload. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Missing or invalid credentials. */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Insufficient permissions. */ - 403: { + /** @description Token pair issued. */ + 200: { headers: { [name: string]: unknown; }; @@ -5829,26 +5916,37 @@ export interface operations { }; }; }; - ApiKeysController_rotate_v1: { + TokenController_userinfo_v1: { parameters: { query?: never; header?: never; - path: { - id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description API key rotated. */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Cannot rotate revoked key. */ - 400: { + }; + }; + TokenController_introspect_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TokenIntrospectionDto"]; + }; + }; + responses: { + 201: { headers: { [name: string]: unknown; }; @@ -5856,23 +5954,20 @@ export interface operations { }; }; }; - ApiKeysController_revoke_v1: { + TokenController_revoke_v1: { parameters: { query?: never; header?: never; - path: { - id: string; - }; + path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["RevokeApiKeyDto"]; + "application/json": components["schemas"]["TokenRevocationDto"]; }; }; responses: { - /** @description API key revoked. */ - 200: { + 201: { headers: { [name: string]: unknown; }; @@ -6864,4 +6959,199 @@ export interface operations { }; }; }; + LedgerAdminController_triggerBackfill_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Starting ledger sequence number */ + startLedger: number; + /** @description Ending ledger sequence number */ + endLedger: number; + /** @description Optional campaign ID to filter */ + campaignId?: string; + /** @description Number of ledgers to process per batch (default: 100) */ + batchSize?: number; + }; + }; + }; + responses: { + /** @description Backfill job queued successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid request parameters. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized - valid JWT token required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Access denied - admin role required. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + LedgerAdminController_getBackfillStatus_v1: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Job ID returned from triggerBackfill */ + jobId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Backfill status retrieved successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized - valid JWT token required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Access denied - admin role required. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + LedgerAdminController_triggerReconciliation_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Starting ledger sequence number */ + startLedger: number; + /** @description Ending ledger sequence number */ + endLedger: number; + /** @description Optional campaign ID to filter */ + campaignId?: string; + /** @description Threshold percentage for amount mismatch (default: 5) */ + thresholdPercent?: number; + }; + }; + }; + responses: { + /** @description Reconciliation job queued successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid request parameters. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized - valid JWT token required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Access denied - admin role required. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + LedgerAdminController_getReconciliationStatus_v1: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Job ID returned from triggerReconciliation */ + jobId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Reconciliation status retrieved successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized - valid JWT token required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Access denied - admin role required. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PrometheusController_index_v1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; }