From 48abb5932d9da15d1037dcba921561b6107de939 Mon Sep 17 00:00:00 2001
From: Manos Liolios
Date: Thu, 21 May 2026 10:13:50 +0300
Subject: [PATCH 1/6] add lint + e2e test scaffold, move tests to
---
packages/mpp/biome.json | 32 +
packages/mpp/package.json | 14 +-
packages/mpp/src/client.test.ts | 116 --
packages/mpp/src/client.ts | 18 +-
packages/mpp/src/server.ts | 49 +-
packages/mpp/src/utils.ts | 5 +-
packages/mpp/test/e2e/globalSetup.ts | 55 +
packages/mpp/test/e2e/setup.ts | 44 +
packages/mpp/test/e2e/setupEnv.ts | 9 +
packages/mpp/test/e2e/sui-payment.test.ts | 100 ++
packages/mpp/test/unit/client.test.ts | 135 ++
.../mpp/{src => test/unit}/server.test.ts | 124 +-
packages/mpp/{src => test/unit}/utils.test.ts | 4 +-
packages/mpp/vitest.config.ts | 2 +-
packages/mpp/vitest.e2e.config.ts | 12 +
pnpm-lock.yaml | 1194 ++++++++++++++++-
16 files changed, 1711 insertions(+), 202 deletions(-)
create mode 100644 packages/mpp/biome.json
delete mode 100644 packages/mpp/src/client.test.ts
create mode 100644 packages/mpp/test/e2e/globalSetup.ts
create mode 100644 packages/mpp/test/e2e/setup.ts
create mode 100644 packages/mpp/test/e2e/setupEnv.ts
create mode 100644 packages/mpp/test/e2e/sui-payment.test.ts
create mode 100644 packages/mpp/test/unit/client.test.ts
rename packages/mpp/{src => test/unit}/server.test.ts (69%)
rename packages/mpp/{src => test/unit}/utils.test.ts (84%)
create mode 100644 packages/mpp/vitest.e2e.config.ts
diff --git a/packages/mpp/biome.json b/packages/mpp/biome.json
new file mode 100644
index 0000000..2e7ff48
--- /dev/null
+++ b/packages/mpp/biome.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
+ "files": {
+ "ignore": [
+ "node_modules",
+ "dist",
+ ".pnpm-store",
+ "*.lock",
+ "*.toml",
+ "**/move/**/build"
+ ]
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space"
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "semicolons": "always"
+ }
+ }
+}
diff --git a/packages/mpp/package.json b/packages/mpp/package.json
index 10b136a..6c015bb 100644
--- a/packages/mpp/package.json
+++ b/packages/mpp/package.json
@@ -23,10 +23,7 @@
"require": "./dist/server.cjs"
}
},
- "files": [
- "dist",
- "README.md"
- ],
+ "files": ["dist", "README.md"],
"keywords": [
"mpp",
"sui",
@@ -49,9 +46,13 @@
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
- "lint": "eslint src/",
+ "check": "biome check .",
+ "check:fix": "biome check --write .",
+ "lint": "biome check .",
+ "lint:fix": "biome check --write .",
"clean": "rm -rf dist"
},
"dependencies": {
@@ -60,8 +61,9 @@
"zod": "^4.3.6"
},
"devDependencies": {
+ "@biomejs/biome": "^1.9.0",
"@types/node": "^20",
- "eslint": "^9",
+ "testcontainers": "^12.0.0",
"tsup": "^8",
"typescript": "^5",
"vitest": "^3"
diff --git a/packages/mpp/src/client.test.ts b/packages/mpp/src/client.test.ts
deleted file mode 100644
index fc19028..0000000
--- a/packages/mpp/src/client.test.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-
-vi.mock('@mysten/sui/transactions', () => {
- const coinWithBalance = vi.fn(() => 'coin_with_balance_result');
- const Transaction = vi.fn().mockImplementation(() => ({
- setSender: vi.fn(),
- transferObjects: vi.fn(),
- build: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
- }));
- return { Transaction, coinWithBalance };
-});
-
-const mockExecuteTransaction = vi.fn();
-
-const mockClient = {
- core: {
- executeTransaction: mockExecuteTransaction,
- },
-};
-
-const mockSigner = {
- toSuiAddress: () => '0xagent_address',
- signTransaction: vi.fn().mockResolvedValue({ bytes: 'mock_bytes', signature: 'mock_sig' }),
-};
-
-describe('client createCredential', () => {
- let suiFn: typeof import('./client.js').sui;
-
- beforeEach(async () => {
- vi.clearAllMocks();
- const mod = await import('./client.js');
- suiFn = mod.sui;
- });
-
- it('builds transaction with coinWithBalance and executes it', async () => {
- mockExecuteTransaction.mockResolvedValue({
- Transaction: { digest: '0xtxdigest' },
- });
-
- const clientMethod = suiFn({
- client: mockClient as any,
- signer: mockSigner as any,
- });
-
- const challenge = {
- request: {
- amount: '0.01',
- currency: '0x::usdc::USDC',
- recipient: '0xrecipient',
- },
- };
-
- try {
- await (clientMethod as any).createCredential({ challenge });
- } catch {
- // Credential.serialize may not be available in test — we're testing TX building
- }
-
- expect(mockSigner.signTransaction).toHaveBeenCalled();
- expect(mockExecuteTransaction).toHaveBeenCalledWith({
- transaction: expect.any(Uint8Array),
- signatures: ['mock_sig'],
- include: { effects: true },
- });
- });
-
- it('throws when transaction execution fails', async () => {
- mockExecuteTransaction.mockResolvedValue({
- FailedTransaction: { status: { error: 'out of gas' } },
- });
-
- const clientMethod = suiFn({
- client: mockClient as any,
- signer: mockSigner as any,
- });
-
- const challenge = {
- request: {
- amount: '0.01',
- currency: '0x::usdc::USDC',
- recipient: '0xrecipient',
- },
- };
-
- await expect(
- (clientMethod as any).createCredential({ challenge }),
- ).rejects.toThrow('Payment transaction failed');
- });
-
- it('uses custom execute when provided', async () => {
- const customExecute = vi.fn().mockResolvedValue({ digest: '0xcustom', effects: {} });
-
- const clientMethod = suiFn({
- client: mockClient as any,
- signer: mockSigner as any,
- execute: customExecute,
- });
-
- const challenge = {
- request: {
- amount: '1.00',
- currency: '0x::usdc::USDC',
- recipient: '0xrecipient',
- },
- };
-
- try {
- await (clientMethod as any).createCredential({ challenge });
- } catch {
- // Credential.serialize may not be available in test
- }
-
- expect(customExecute).toHaveBeenCalled();
- expect(mockExecuteTransaction).not.toHaveBeenCalled();
- });
-});
diff --git a/packages/mpp/src/client.ts b/packages/mpp/src/client.ts
index c23d91f..341d485 100644
--- a/packages/mpp/src/client.ts
+++ b/packages/mpp/src/client.ts
@@ -1,7 +1,7 @@
-import { Method, Credential } from 'mppx';
import type { ClientWithCoreApi } from '@mysten/sui/client';
import type { Signer } from '@mysten/sui/cryptography';
-import { coinWithBalance, Transaction } from '@mysten/sui/transactions';
+import { Transaction, coinWithBalance } from '@mysten/sui/transactions';
+import { Credential, Method } from 'mppx';
import { suiCharge } from './method.js';
import { parseAmountToRaw } from './utils.js';
@@ -17,6 +17,8 @@ export interface SuiChargeOptions {
execute?: (tx: Transaction) => Promise<{ digest: string }>;
}
+type TransactionResult = { digest: string };
+
export function sui(options: SuiChargeOptions) {
const address = options.signer.toSuiAddress();
const decimals = options.decimals ?? 6;
@@ -32,7 +34,7 @@ export function sui(options: SuiChargeOptions) {
const payment = coinWithBalance({ balance: amountRaw, type: currency });
tx.transferObjects([payment], recipient);
- let result;
+ let result: TransactionResult;
try {
if (options.execute) {
result = await options.execute(tx);
@@ -45,9 +47,15 @@ export function sui(options: SuiChargeOptions) {
include: { effects: true },
});
if (execResult.FailedTransaction) {
- throw new Error(execResult.FailedTransaction.status.error?.message ?? 'Transaction failed');
+ throw new Error(
+ execResult.FailedTransaction.status.error?.message ??
+ 'Transaction failed',
+ );
+ }
+ if (!execResult.Transaction) {
+ throw new Error('Transaction failed');
}
- result = execResult.Transaction!;
+ result = execResult.Transaction;
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
diff --git a/packages/mpp/src/server.ts b/packages/mpp/src/server.ts
index 7d820ee..6201e50 100644
--- a/packages/mpp/src/server.ts
+++ b/packages/mpp/src/server.ts
@@ -1,9 +1,10 @@
-import { Method, Receipt } from 'mppx';
import { SuiGrpcClient } from '@mysten/sui/grpc';
+import { getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
import { normalizeSuiAddress } from '@mysten/sui/utils';
+import { Method, Receipt } from 'mppx';
+import { InMemoryDigestStore } from './in-memory-digest-store.js';
import { suiCharge } from './method.js';
import { parseAmountToRaw, withRetry } from './utils.js';
-import { InMemoryDigestStore } from './in-memory-digest-store.js';
export { suiCharge } from './method.js';
export { SUI_USDC_TYPE } from './constants.js';
@@ -28,7 +29,7 @@ export interface SuiServerOptions {
/** Number of decimal places for the currency (default: 6, e.g. USDC). */
decimals?: number;
rpcUrl?: string;
- network?: 'mainnet' | 'testnet' | 'devnet';
+ network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
/** Digest store for replay protection. Required in production. Falls back to in-memory in dev. */
store?: DigestStore;
/** Called after successful on-chain verification with payment data. */
@@ -40,11 +41,14 @@ let _defaultStore: DigestStore | undefined;
function resolveStore(options: SuiServerOptions): DigestStore {
if (options.store) return options.store;
- if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
+ if (
+ typeof process !== 'undefined' &&
+ process.env?.NODE_ENV === 'production'
+ ) {
throw new Error(
'[suimpp] DigestStore is required in production. ' +
- 'Provide a Redis or DB-backed store via SuiServerOptions.store. ' +
- 'The default in-memory store is single-instance only and unsafe for multi-instance deployments.',
+ 'Provide a Redis or DB-backed store via SuiServerOptions.store. ' +
+ 'The default in-memory store is single-instance only and unsafe for multi-instance deployments.',
);
}
@@ -52,7 +56,7 @@ function resolveStore(options: SuiServerOptions): DigestStore {
_defaultStore = new InMemoryDigestStore();
console.warn(
'[suimpp] No DigestStore provided. Using in-memory store. ' +
- 'This is NOT safe for production or multi-instance deployments.',
+ 'This is NOT safe for production or multi-instance deployments.',
);
}
return _defaultStore;
@@ -61,8 +65,13 @@ function resolveStore(options: SuiServerOptions): DigestStore {
export function sui(options: SuiServerOptions) {
const network = options.network ?? 'mainnet';
const decimals = options.decimals ?? 6;
+ const baseUrl =
+ options.rpcUrl ??
+ (network === 'localnet'
+ ? 'http://127.0.0.1:9000'
+ : getJsonRpcFullnodeUrl(network));
const client = new SuiGrpcClient({
- baseUrl: options.rpcUrl ?? `https://fullnode.${network}.sui.io:443`,
+ baseUrl,
network,
});
@@ -85,10 +94,15 @@ export function sui(options: SuiServerOptions) {
);
}
- const tx = await withRetry(
- () => client.core.getTransaction({ digest, include: { balanceChanges: true } }),
+ const tx = await withRetry(() =>
+ client.core.getTransaction({
+ digest,
+ include: { balanceChanges: true },
+ }),
).catch(() => {
- throw new Error(`Could not find the referenced transaction [${digest}]`);
+ throw new Error(
+ `Could not find the referenced transaction [${digest}]`,
+ );
});
const resolved = tx.Transaction ?? tx.FailedTransaction;
@@ -104,13 +118,14 @@ export function sui(options: SuiServerOptions) {
);
if (!payment) {
- throw new Error(
- 'Payment not found in transaction balance changes',
- );
+ throw new Error('Payment not found in transaction balance changes');
}
const transferredRaw = BigInt(payment.amount);
- const requestedRaw = parseAmountToRaw(credential.challenge.request.amount, decimals);
+ const requestedRaw = parseAmountToRaw(
+ credential.challenge.request.amount,
+ decimals,
+ );
if (transferredRaw < requestedRaw) {
throw new Error(
`Transferred ${transferredRaw} < requested ${requestedRaw} (raw units)`,
@@ -138,7 +153,9 @@ export function sui(options: SuiServerOptions) {
};
if (options.onPayment) {
- try { options.onPayment(report); } catch {}
+ try {
+ options.onPayment(report);
+ } catch {}
}
return receipt;
diff --git a/packages/mpp/src/utils.ts b/packages/mpp/src/utils.ts
index d08108c..f003215 100644
--- a/packages/mpp/src/utils.ts
+++ b/packages/mpp/src/utils.ts
@@ -14,7 +14,10 @@ export function parseAmountToRaw(amount: string, decimals: number): bigint {
*/
export async function withRetry(
fn: () => Promise,
- { attempts = 5, baseDelayMs = 1000 }: { attempts?: number; baseDelayMs?: number } = {},
+ {
+ attempts = 5,
+ baseDelayMs = 1000,
+ }: { attempts?: number; baseDelayMs?: number } = {},
): Promise {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
diff --git a/packages/mpp/test/e2e/globalSetup.ts b/packages/mpp/test/e2e/globalSetup.ts
new file mode 100644
index 0000000..bf334bf
--- /dev/null
+++ b/packages/mpp/test/e2e/globalSetup.ts
@@ -0,0 +1,55 @@
+import { GenericContainer, type StartedTestContainer } from 'testcontainers';
+import type { TestProject } from 'vitest/node';
+
+declare module 'vitest' {
+ export interface ProvidedContext {
+ localnetPort: number;
+ faucetPort: number;
+ suiToolsContainerId: string;
+ }
+}
+
+const SUI_TOOLS_TAG =
+ process.env.SUI_TOOLS_TAG ||
+ (process.arch === 'arm64'
+ ? '951cae315d8b252131836a331bcc16b89eb340d6-arm64'
+ : '951cae315d8b252131836a331bcc16b89eb340d6');
+
+export default async function setup(project: TestProject) {
+ const containers: StartedTestContainer[] = [];
+
+ console.log('Starting Sui localnet container...');
+ const localnet = await new GenericContainer(
+ `mysten/sui-tools:${SUI_TOOLS_TAG}`,
+ )
+ .withCommand(['sui', 'start', '--with-faucet', '--force-regenesis'])
+ .withExposedPorts(9000, 9123)
+ .withLogConsumer((stream) => {
+ stream.on('data', (data) => {
+ const msg = data.toString();
+ if (
+ msg.includes('error') ||
+ msg.includes('Fullnode') ||
+ msg.includes('faucet')
+ ) {
+ console.log(msg.trimEnd());
+ }
+ });
+ })
+ .start();
+ containers.push(localnet);
+
+ project.provide('localnetPort', localnet.getMappedPort(9000));
+ project.provide('faucetPort', localnet.getMappedPort(9123));
+ project.provide('suiToolsContainerId', localnet.getId());
+
+ console.log(
+ `Sui localnet ready: fullnode=${localnet.getMappedPort(9000)}, faucet=${localnet.getMappedPort(9123)}`,
+ );
+
+ return async () => {
+ await Promise.allSettled(
+ containers.reverse().map((container) => container.stop()),
+ );
+ };
+}
diff --git a/packages/mpp/test/e2e/setup.ts b/packages/mpp/test/e2e/setup.ts
new file mode 100644
index 0000000..f27c71e
--- /dev/null
+++ b/packages/mpp/test/e2e/setup.ts
@@ -0,0 +1,44 @@
+import {
+ FaucetRateLimitError,
+ requestSuiFromFaucetV2,
+} from '@mysten/sui/faucet';
+import { SuiGrpcClient } from '@mysten/sui/grpc';
+
+const DEFAULT_FAUCET_URL = process.env.FAUCET_URL ?? 'http://127.0.0.1:9123';
+const DEFAULT_FULLNODE_URL =
+ process.env.LOCALNET_FULLNODE_URL ?? 'http://127.0.0.1:9000';
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function getFullnodeUrl() {
+ return DEFAULT_FULLNODE_URL;
+}
+
+export function getClient(): SuiGrpcClient {
+ return new SuiGrpcClient({
+ network: 'localnet',
+ baseUrl: DEFAULT_FULLNODE_URL,
+ });
+}
+
+export async function fundAddress(address: string) {
+ let lastError: unknown;
+
+ for (let attempt = 0; attempt < 6; attempt++) {
+ try {
+ await requestSuiFromFaucetV2({
+ host: DEFAULT_FAUCET_URL,
+ recipient: address,
+ });
+ return;
+ } catch (error) {
+ if (error instanceof FaucetRateLimitError) throw error;
+ lastError = error;
+ await delay(1000 * (attempt + 1));
+ }
+ }
+
+ throw lastError;
+}
diff --git a/packages/mpp/test/e2e/setupEnv.ts b/packages/mpp/test/e2e/setupEnv.ts
new file mode 100644
index 0000000..7d4e06d
--- /dev/null
+++ b/packages/mpp/test/e2e/setupEnv.ts
@@ -0,0 +1,9 @@
+import { inject } from 'vitest';
+
+for (const [key, value] of Object.entries({
+ LOCALNET_FULLNODE_URL: `http://127.0.0.1:${inject('localnetPort')}`,
+ FAUCET_URL: `http://127.0.0.1:${inject('faucetPort')}`,
+ NODE_ENV: 'test',
+})) {
+ process.env[key] = value;
+}
diff --git a/packages/mpp/test/e2e/sui-payment.test.ts b/packages/mpp/test/e2e/sui-payment.test.ts
new file mode 100644
index 0000000..10338a4
--- /dev/null
+++ b/packages/mpp/test/e2e/sui-payment.test.ts
@@ -0,0 +1,100 @@
+import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
+import { SUI_TYPE_ARG } from '@mysten/sui/utils';
+import { Challenge, Credential } from 'mppx';
+import { beforeAll, describe, expect, it, vi } from 'vitest';
+import { sui as createSuiClient } from '../../src/client.js';
+import { InMemoryDigestStore } from '../../src/in-memory-digest-store.js';
+import { suiCharge } from '../../src/method.js';
+import { sui as createSuiServer } from '../../src/server.js';
+import { fundAddress, getClient, getFullnodeUrl } from './setup.js';
+
+const PAYMENT_AMOUNT = '0.01';
+const SUI_DECIMALS = 9;
+
+type SuiChargeRequest = {
+ amount: string;
+ currency: string;
+ recipient: string;
+};
+type SuiChargeChallenge = Challenge.Challenge<
+ SuiChargeRequest,
+ 'charge',
+ 'sui'
+>;
+type SuiChargeCredential = Credential.Credential<
+ { digest: string },
+ SuiChargeChallenge
+>;
+
+describe('Sui localnet e2e payment', () => {
+ const localnetClient = getClient();
+ const payerKeypair = Ed25519Keypair.generate();
+ const recipientKeypair = Ed25519Keypair.generate();
+ const recipient = recipientKeypair.getPublicKey().toSuiAddress();
+
+ beforeAll(async () => {
+ await fundAddress(payerKeypair.getPublicKey().toSuiAddress());
+ });
+
+ it('creates an on-chain credential and verifies the payment once', async () => {
+ const onPayment = vi.fn();
+ const challenge = Challenge.fromMethod(suiCharge, {
+ id: 'sui-localnet-e2e-payment',
+ realm: 'suimpp-e2e',
+ request: {
+ amount: PAYMENT_AMOUNT,
+ currency: SUI_TYPE_ARG,
+ recipient,
+ },
+ }) as SuiChargeChallenge;
+ const clientMethod = createSuiClient({
+ client: localnetClient,
+ signer: payerKeypair,
+ decimals: SUI_DECIMALS,
+ });
+ const serverMethod = createSuiServer({
+ currency: SUI_TYPE_ARG,
+ recipient,
+ decimals: SUI_DECIMALS,
+ rpcUrl: getFullnodeUrl(),
+ network: 'localnet',
+ store: new InMemoryDigestStore(),
+ onPayment,
+ });
+
+ const authorization = await clientMethod.createCredential({ challenge });
+ const credential = Credential.deserialize<{ digest: string }>(
+ authorization,
+ ) as SuiChargeCredential;
+ await localnetClient.waitForTransaction({
+ digest: credential.payload.digest,
+ });
+
+ const receipt = await serverMethod.verify({
+ credential,
+ request: challenge.request,
+ });
+
+ expect(receipt).toMatchObject({
+ method: 'sui',
+ reference: credential.payload.digest,
+ status: 'success',
+ });
+ expect(onPayment).toHaveBeenCalledWith(
+ expect.objectContaining({
+ digest: credential.payload.digest,
+ recipient,
+ amount: PAYMENT_AMOUNT,
+ currency: SUI_TYPE_ARG,
+ network: 'localnet',
+ }),
+ );
+
+ await expect(
+ serverMethod.verify({
+ credential,
+ request: challenge.request,
+ }),
+ ).rejects.toThrow('Digest already used');
+ });
+});
diff --git a/packages/mpp/test/unit/client.test.ts b/packages/mpp/test/unit/client.test.ts
new file mode 100644
index 0000000..ef90d6b
--- /dev/null
+++ b/packages/mpp/test/unit/client.test.ts
@@ -0,0 +1,135 @@
+import type { Challenge } from 'mppx';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type {
+ SuiChargeOptions,
+ sui as createSuiClient,
+} from '../../src/client.js';
+
+vi.mock('@mysten/sui/transactions', () => {
+ const coinWithBalance = vi.fn(() => 'coin_with_balance_result');
+ const Transaction = vi.fn().mockImplementation(() => ({
+ setSender: vi.fn(),
+ transferObjects: vi.fn(),
+ build: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
+ }));
+ return { Transaction, coinWithBalance };
+});
+
+const mockExecuteTransaction = vi.fn();
+
+const mockClient = {
+ core: {
+ executeTransaction: mockExecuteTransaction,
+ },
+};
+
+const mockSigner = {
+ toSuiAddress: () => '0xagent_address',
+ signTransaction: vi
+ .fn()
+ .mockResolvedValue({ bytes: 'mock_bytes', signature: 'mock_sig' }),
+};
+
+const typedMockClient = mockClient as unknown as SuiChargeOptions['client'];
+const typedMockSigner = mockSigner as unknown as SuiChargeOptions['signer'];
+
+type SuiChargeRequest = {
+ amount: string;
+ currency: string;
+ recipient: string;
+};
+type SuiChargeChallenge = Challenge.Challenge<
+ SuiChargeRequest,
+ 'charge',
+ 'sui'
+>;
+
+function buildChallenge(amount = '0.01'): SuiChargeChallenge {
+ return {
+ id: 'test-challenge',
+ intent: 'charge',
+ method: 'sui',
+ realm: 'test',
+ request: {
+ amount,
+ currency: '0x::usdc::USDC',
+ recipient: '0xrecipient',
+ },
+ };
+}
+
+describe('client createCredential', () => {
+ let suiFn: typeof createSuiClient;
+
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ const mod = await import('../../src/client.js');
+ suiFn = mod.sui;
+ });
+
+ it('builds transaction with coinWithBalance and executes it', async () => {
+ mockExecuteTransaction.mockResolvedValue({
+ Transaction: { digest: '0xtxdigest' },
+ });
+
+ const clientMethod = suiFn({
+ client: typedMockClient,
+ signer: typedMockSigner,
+ });
+
+ const challenge = buildChallenge();
+
+ try {
+ await clientMethod.createCredential({ challenge });
+ } catch {
+ // Credential.serialize may not be available in test; this verifies TX building.
+ }
+
+ expect(mockSigner.signTransaction).toHaveBeenCalled();
+ expect(mockExecuteTransaction).toHaveBeenCalledWith({
+ transaction: expect.any(Uint8Array),
+ signatures: ['mock_sig'],
+ include: { effects: true },
+ });
+ });
+
+ it('throws when transaction execution fails', async () => {
+ mockExecuteTransaction.mockResolvedValue({
+ FailedTransaction: { status: { error: 'out of gas' } },
+ });
+
+ const clientMethod = suiFn({
+ client: typedMockClient,
+ signer: typedMockSigner,
+ });
+
+ const challenge = buildChallenge();
+
+ await expect(clientMethod.createCredential({ challenge })).rejects.toThrow(
+ 'Payment transaction failed',
+ );
+ });
+
+ it('uses custom execute when provided', async () => {
+ const customExecute = vi
+ .fn()
+ .mockResolvedValue({ digest: '0xcustom', effects: {} });
+
+ const clientMethod = suiFn({
+ client: typedMockClient,
+ signer: typedMockSigner,
+ execute: customExecute,
+ });
+
+ const challenge = buildChallenge('1.00');
+
+ try {
+ await clientMethod.createCredential({ challenge });
+ } catch {
+ // Credential.serialize may not be available in test.
+ }
+
+ expect(customExecute).toHaveBeenCalled();
+ expect(mockExecuteTransaction).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/mpp/src/server.test.ts b/packages/mpp/test/unit/server.test.ts
similarity index 69%
rename from packages/mpp/src/server.test.ts
rename to packages/mpp/test/unit/server.test.ts
index 0973ce3..cdb5b64 100644
--- a/packages/mpp/src/server.test.ts
+++ b/packages/mpp/test/unit/server.test.ts
@@ -1,10 +1,30 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { InMemoryDigestStore } from './in-memory-digest-store.js';
-
-const USDC_TYPE = '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC';
+import type { Challenge, Credential, Method } from 'mppx';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { InMemoryDigestStore } from '../../src/in-memory-digest-store.js';
+import type { suiCharge } from '../../src/method.js';
+import type { DigestStore, sui as createSuiServer } from '../../src/server.js';
+
+const USDC_TYPE =
+ '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC';
const RECIPIENT = '0xrecipient_address';
const SENDER = '0xsender_address';
+type SuiServerMethod = Method.Server;
+type SuiChargeRequest = {
+ amount: string;
+ currency: string;
+ recipient: string;
+};
+type SuiChargeChallenge = Challenge.Challenge<
+ SuiChargeRequest,
+ 'charge',
+ 'sui'
+>;
+type SuiCredential = Credential.Credential<
+ { digest: string },
+ SuiChargeChallenge
+>;
+
function buildMockTx({
success = true,
coinType = USDC_TYPE,
@@ -32,10 +52,17 @@ function buildMockTx({
: { Transaction: undefined, FailedTransaction: txData };
}
-function buildCredential(digest = '0xdigest123', amount = '0.01') {
+function buildCredential(
+ digest = '0xdigest123',
+ amount = '0.01',
+): SuiCredential {
return {
payload: { digest },
challenge: {
+ id: 'test-challenge',
+ intent: 'charge',
+ method: 'sui',
+ realm: 'test',
request: {
amount,
currency: USDC_TYPE,
@@ -45,6 +72,16 @@ function buildCredential(digest = '0xdigest123', amount = '0.01') {
};
}
+function verifyPayment(
+ serverMethod: SuiServerMethod,
+ credential = buildCredential(),
+) {
+ return serverMethod.verify({
+ credential,
+ request: credential.challenge.request,
+ });
+}
+
const mockGetTransaction = vi.fn();
vi.mock('@mysten/sui/grpc', () => ({
@@ -60,12 +97,12 @@ vi.mock('@mysten/sui/utils', () => ({
}));
describe('server verify', () => {
- let suiFn: typeof import('./server.js').sui;
+ let suiFn: typeof createSuiServer;
const originalEnv = process.env.NODE_ENV;
beforeEach(async () => {
vi.clearAllMocks();
- const mod = await import('./server.js');
+ const mod = await import('../../src/server.js');
suiFn = mod.sui;
});
@@ -82,9 +119,7 @@ describe('server verify', () => {
store: new InMemoryDigestStore(),
});
- const result = await (serverMethod as any).verify({
- credential: buildCredential(),
- });
+ const result = await verifyPayment(serverMethod);
expect(result.reference).toBe('0xdigest123');
expect(result.status).toBe('success');
@@ -99,9 +134,9 @@ describe('server verify', () => {
store: new InMemoryDigestStore(),
});
- await expect(
- (serverMethod as any).verify({ credential: buildCredential() }),
- ).rejects.toThrow('Transaction failed on-chain');
+ await expect(verifyPayment(serverMethod)).rejects.toThrow(
+ 'Transaction failed on-chain',
+ );
});
it('rejects when payment not sent to recipient', async () => {
@@ -115,9 +150,9 @@ describe('server verify', () => {
store: new InMemoryDigestStore(),
});
- await expect(
- (serverMethod as any).verify({ credential: buildCredential() }),
- ).rejects.toThrow('Payment not found');
+ await expect(verifyPayment(serverMethod)).rejects.toThrow(
+ 'Payment not found',
+ );
});
it('rejects when amount is less than requested', async () => {
@@ -129,9 +164,7 @@ describe('server verify', () => {
store: new InMemoryDigestStore(),
});
- await expect(
- (serverMethod as any).verify({ credential: buildCredential() }),
- ).rejects.toThrow('Transferred');
+ await expect(verifyPayment(serverMethod)).rejects.toThrow('Transferred');
});
it('rejects when no balance changes', async () => {
@@ -149,19 +182,19 @@ describe('server verify', () => {
store: new InMemoryDigestStore(),
});
- await expect(
- (serverMethod as any).verify({ credential: buildCredential() }),
- ).rejects.toThrow('Payment not found');
+ await expect(verifyPayment(serverMethod)).rejects.toThrow(
+ 'Payment not found',
+ );
});
});
describe('digest replay protection', () => {
- let suiFn: typeof import('./server.js').sui;
+ let suiFn: typeof createSuiServer;
const originalEnv = process.env.NODE_ENV;
beforeEach(async () => {
vi.clearAllMocks();
- const mod = await import('./server.js');
+ const mod = await import('../../src/server.js');
suiFn = mod.sui;
});
@@ -179,9 +212,7 @@ describe('digest replay protection', () => {
store,
});
- const result = await (serverMethod as any).verify({
- credential: buildCredential(),
- });
+ const result = await verifyPayment(serverMethod);
expect(result.reference).toBe('0xdigest123');
expect(result.status).toBe('success');
@@ -197,14 +228,10 @@ describe('digest replay protection', () => {
store,
});
- await (serverMethod as any).verify({
- credential: buildCredential(),
- });
+ await verifyPayment(serverMethod);
await expect(
- (serverMethod as any).verify({
- credential: buildCredential('0xdigest123', '0.02'),
- }),
+ verifyPayment(serverMethod, buildCredential('0xdigest123', '0.02')),
).rejects.toThrow('Digest already used');
});
@@ -218,9 +245,7 @@ describe('digest replay protection', () => {
});
mockGetTransaction.mockResolvedValue(buildMockTx());
- await (serverMethod as any).verify({
- credential: buildCredential('0xdigest123'),
- });
+ await verifyPayment(serverMethod, buildCredential('0xdigest123'));
mockGetTransaction.mockResolvedValue({
Transaction: {
@@ -233,9 +258,10 @@ describe('digest replay protection', () => {
},
});
- const result = await (serverMethod as any).verify({
- credential: buildCredential('0xdigest456'),
- });
+ const result = await verifyPayment(
+ serverMethod,
+ buildCredential('0xdigest456'),
+ );
expect(result.reference).toBe('0xdigest456');
});
@@ -250,30 +276,26 @@ describe('digest replay protection', () => {
store,
});
- await (serverMethod as any).verify({
- credential: buildCredential(),
- });
+ await verifyPayment(serverMethod);
await new Promise((r) => setTimeout(r, 100));
- const result = await (serverMethod as any).verify({
- credential: buildCredential(),
- });
+ const result = await verifyPayment(serverMethod);
expect(result.status).toBe('success');
});
it('throws on missing store in production', () => {
process.env.NODE_ENV = 'production';
- expect(() =>
- suiFn({ currency: USDC_TYPE, recipient: RECIPIENT }),
- ).toThrow('DigestStore is required in production');
+ expect(() => suiFn({ currency: USDC_TYPE, recipient: RECIPIENT })).toThrow(
+ 'DigestStore is required in production',
+ );
});
it('marks digest before returning receipt (store.set failure = no free call)', async () => {
const store = {
has: vi.fn().mockResolvedValue(false),
set: vi.fn().mockRejectedValue(new Error('Redis down')),
- } satisfies import('./server.js').DigestStore;
+ } satisfies DigestStore;
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
@@ -282,9 +304,7 @@ describe('digest replay protection', () => {
store,
});
- await expect(
- (serverMethod as any).verify({ credential: buildCredential() }),
- ).rejects.toThrow('Redis down');
+ await expect(verifyPayment(serverMethod)).rejects.toThrow('Redis down');
expect(store.set).toHaveBeenCalledWith('0xdigest123');
});
diff --git a/packages/mpp/src/utils.test.ts b/packages/mpp/test/unit/utils.test.ts
similarity index 84%
rename from packages/mpp/src/utils.test.ts
rename to packages/mpp/test/unit/utils.test.ts
index 24a83f9..5cb4bf9 100644
--- a/packages/mpp/src/utils.test.ts
+++ b/packages/mpp/test/unit/utils.test.ts
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest';
-import { parseAmountToRaw } from './utils.js';
+import { describe, expect, it } from 'vitest';
+import { parseAmountToRaw } from '../../src/utils.js';
describe('parseAmountToRaw', () => {
it('converts whole number', () => {
diff --git a/packages/mpp/vitest.config.ts b/packages/mpp/vitest.config.ts
index e68a190..810703e 100644
--- a/packages/mpp/vitest.config.ts
+++ b/packages/mpp/vitest.config.ts
@@ -4,7 +4,7 @@ export default defineConfig({
test: {
globals: true,
environment: 'node',
- include: ['src/**/*.test.ts'],
+ include: ['test/unit/**/*.test.ts'],
testTimeout: 30_000,
},
});
diff --git a/packages/mpp/vitest.e2e.config.ts b/packages/mpp/vitest.e2e.config.ts
new file mode 100644
index 0000000..a3d2034
--- /dev/null
+++ b/packages/mpp/vitest.e2e.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['test/e2e/**/*.test.ts'],
+ setupFiles: ['test/e2e/setupEnv.ts'],
+ globalSetup: ['test/e2e/globalSetup.ts'],
+ hookTimeout: 600_000,
+ testTimeout: 120_000,
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 25f4ad0..ed357b2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -89,12 +89,15 @@ importers:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
+ '@biomejs/biome':
+ specifier: ^1.9.0
+ version: 1.9.4
'@types/node':
specifier: ^20
version: 20.19.37
- eslint:
- specifier: ^9
- version: 9.39.4(jiti@2.6.1)
+ testcontainers:
+ specifier: ^12.0.0
+ version: 12.0.0
tsup:
specifier: ^8
version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3)
@@ -146,6 +149,62 @@ packages:
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
+ '@balena/dockerignore@1.0.2':
+ resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
+
+ '@biomejs/biome@1.9.4':
+ resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
+ engines: {node: '>=14.21.3'}
+ hasBin: true
+
+ '@biomejs/cli-darwin-arm64@1.9.4':
+ resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@biomejs/cli-darwin-x64@1.9.4':
+ resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@biomejs/cli-linux-arm64-musl@1.9.4':
+ resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@biomejs/cli-linux-arm64@1.9.4':
+ resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@biomejs/cli-linux-x64-musl@1.9.4':
+ resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+
+ '@biomejs/cli-linux-x64@1.9.4':
+ resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+
+ '@biomejs/cli-win32-arm64@1.9.4':
+ resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@biomejs/cli-win32-x64@1.9.4':
+ resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [win32]
+
'@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
@@ -368,6 +427,20 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+ '@grpc/grpc-js@1.14.4':
+ resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==}
+ engines: {node: '>=12.10.0'}
+
+ '@grpc/proto-loader@0.7.15':
+ resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ '@grpc/proto-loader@0.8.1':
+ resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
'@hono/node-server@1.19.12':
resolution: {integrity: sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==}
engines: {node: '>=18.14.1'}
@@ -531,6 +604,10 @@ packages:
cpu: [x64]
os: [win32]
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -547,6 +624,12 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@js-sdsl/ordered-map@4.4.2':
+ resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
+
+ '@kwsites/file-exists@1.1.1':
+ resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
+
'@modelcontextprotocol/sdk@1.28.0':
resolution: {integrity: sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==}
engines: {node: '>=18'}
@@ -638,6 +721,10 @@ packages:
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
'@prisma/client@6.19.2':
resolution: {integrity: sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==}
engines: {node: '>=18.18'}
@@ -677,6 +764,36 @@ packages:
'@protobuf-ts/runtime@2.11.1':
resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==}
+ '@protobufjs/aspromise@1.1.2':
+ resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
+
+ '@protobufjs/base64@1.1.2':
+ resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
+
+ '@protobufjs/codegen@2.0.5':
+ resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
+
+ '@protobufjs/eventemitter@1.1.0':
+ resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
+
+ '@protobufjs/fetch@1.1.1':
+ resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==}
+
+ '@protobufjs/float@1.0.2':
+ resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
+
+ '@protobufjs/inquire@1.1.2':
+ resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==}
+
+ '@protobufjs/path@1.1.2':
+ resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
+
+ '@protobufjs/pool@1.1.0':
+ resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
+
+ '@protobufjs/utf8@1.1.1':
+ resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==}
+
'@readme/better-ajv-errors@2.4.0':
resolution: {integrity: sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg==}
engines: {node: '>=18'}
@@ -978,12 +1095,21 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/docker-modem@3.0.6':
+ resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
+
+ '@types/dockerode@4.0.1':
+ resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ '@types/node@18.19.130':
+ resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
+
'@types/node@20.19.37':
resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==}
@@ -998,6 +1124,15 @@ packages:
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+ '@types/ssh2-streams@0.1.13':
+ resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==}
+
+ '@types/ssh2@0.5.52':
+ resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==}
+
+ '@types/ssh2@1.15.5':
+ resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==}
+
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
@@ -1038,6 +1173,10 @@ packages:
zod:
optional: true
+ abort-controller@3.0.0:
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+ engines: {node: '>=6.5'}
+
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -1074,23 +1213,110 @@ packages:
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+ archiver-utils@5.0.2:
+ resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
+ engines: {node: '>= 14'}
+
+ archiver@7.0.1:
+ resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==}
+ engines: {node: '>= 14'}
+
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ asn1@0.2.6:
+ resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ async-lock@1.4.1:
+ resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
+
+ async@3.2.6:
+ resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
+
+ b4a@1.8.1:
+ resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
+ peerDependencies:
+ react-native-b4a: '*'
+ peerDependenciesMeta:
+ react-native-b4a:
+ optional: true
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ bare-events@2.8.3:
+ resolution: {integrity: sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==}
+ peerDependencies:
+ bare-abort-controller: '*'
+ peerDependenciesMeta:
+ bare-abort-controller:
+ optional: true
+
+ bare-fs@4.7.1:
+ resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==}
+ engines: {bare: '>=1.16.0'}
+ peerDependencies:
+ bare-buffer: '*'
+ peerDependenciesMeta:
+ bare-buffer:
+ optional: true
+
+ bare-os@3.9.1:
+ resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==}
+ engines: {bare: '>=1.14.0'}
+
+ bare-path@3.0.0:
+ resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==}
+
+ bare-stream@2.13.1:
+ resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==}
+ peerDependencies:
+ bare-abort-controller: '*'
+ bare-buffer: '*'
+ bare-events: '*'
+ peerDependenciesMeta:
+ bare-abort-controller:
+ optional: true
+ bare-buffer:
+ optional: true
+ bare-events:
+ optional: true
+
+ bare-url@2.4.3:
+ resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ bcrypt-pbkdf@1.0.2:
+ resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
+
+ bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
@@ -1098,12 +1324,33 @@ packages:
brace-expansion@1.1.13:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
+ brace-expansion@2.1.0:
+ resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
+
+ buffer-crc32@1.0.0:
+ resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
+ engines: {node: '>=8.0.0'}
+
+ buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+ buildcheck@0.0.7:
+ resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
+ engines: {node: '>=10.0.0'}
+
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
+ byline@5.0.0:
+ resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==}
+ engines: {node: '>=0.10.0'}
+
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@@ -1151,6 +1398,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
+ chownr@1.1.4:
+ resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
@@ -1160,6 +1410,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1171,6 +1425,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
+ compress-commons@6.0.2:
+ resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
+ engines: {node: '>= 14'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1200,10 +1458,26 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
+ core-util-is@1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
+ cpu-features@0.0.10:
+ resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
+ engines: {node: '>=10.0.0'}
+
+ crc-32@1.2.2:
+ resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
+ engines: {node: '>=0.8'}
+ hasBin: true
+
+ crc32-stream@6.0.0:
+ resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
+ engines: {node: '>= 14'}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1245,6 +1519,18 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ docker-compose@1.4.2:
+ resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==}
+ engines: {node: '>= 6.0.0'}
+
+ docker-modem@5.0.7:
+ resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==}
+ engines: {node: '>= 8.0'}
+
+ dockerode@5.0.0:
+ resolution: {integrity: sha512-C52mvJ+7lcyhWNfrzVfFsbTrBfy/ezE9FGEYLpu17FUeBcCkxERk9nN7uDl/478ynDiQ4U+5DbQC2vENHkVEtQ==}
+ engines: {node: '>= 14.17'}
+
dotenv@16.6.1:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
@@ -1253,12 +1539,21 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
effect@3.18.4:
resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==}
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
empathic@2.0.0:
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
engines: {node: '>=14'}
@@ -1267,6 +1562,9 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'}
@@ -1291,6 +1589,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@@ -1347,9 +1649,20 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
+ event-target-shim@5.0.1:
+ resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+ engines: {node: '>=6'}
+
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+ events-universal@1.0.1:
+ resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
+
+ events@3.3.0:
+ resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+ engines: {node: '>=0.8.x'}
+
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
@@ -1382,6 +1695,9 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-fifo@1.3.2:
+ resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
+
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -1422,6 +1738,10 @@ packages:
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -1430,6 +1750,9 @@ packages:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
+ fs-constants@1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1438,10 +1761,18 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
+ get-port@7.2.0:
+ resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==}
+ engines: {node: '>=16'}
+
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
@@ -1454,6 +1785,11 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ hasBin: true
+
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -1499,6 +1835,9 @@ packages:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1530,6 +1869,10 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -1537,6 +1880,13 @@ packages:
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+ is-stream@2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+
+ isarray@1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1545,6 +1895,9 @@ packages:
peerDependencies:
ws: '*'
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@@ -1588,6 +1941,10 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ lazystream@1.0.1:
+ resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
+ engines: {node: '>= 0.6.3'}
+
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -1681,12 +2038,24 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
+ lodash.camelcase@4.3.0:
+ resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
+ long@5.3.2:
+ resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
+
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1713,6 +2082,26 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+ minimatch@5.1.9:
+ resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
+ engines: {node: '>=10'}
+
+ minimatch@9.0.9:
+ resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+
+ mkdirp@3.0.1:
+ resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
@@ -1741,6 +2130,9 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+ nan@2.27.0:
+ resolution: {integrity: sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==}
+
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1777,6 +2169,10 @@ packages:
node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
nypm@0.6.5:
resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==}
engines: {node: '>=18'}
@@ -1831,6 +2227,9 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -1847,6 +2246,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
path-to-regexp@8.4.0:
resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==}
@@ -1924,10 +2327,31 @@ packages:
typescript:
optional: true
+ process-nextick-args@2.0.1:
+ resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
+ process@0.11.10:
+ resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
+ engines: {node: '>= 0.6.0'}
+
+ proper-lockfile@4.1.2:
+ resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
+
+ properties-reader@3.0.1:
+ resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==}
+ engines: {node: '>=18'}
+
+ protobufjs@7.6.0:
+ resolution: {integrity: sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==}
+ engines: {node: '>=12.0.0'}
+
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
+ pump@3.0.4:
+ resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -1959,10 +2383,28 @@ packages:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
+ readable-stream@2.3.8:
+ resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
+ readable-stream@4.7.0:
+ resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ readdir-glob@1.1.3:
+ resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
+
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@@ -1975,6 +2417,10 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
+ retry@0.12.0:
+ resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
+ engines: {node: '>= 4'}
+
rollup@4.60.1:
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -1984,6 +2430,12 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -2037,6 +2489,13 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2045,6 +2504,16 @@ packages:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
+ split-ca@1.0.1:
+ resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
+
+ ssh-remote-port-forward@1.0.4:
+ resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==}
+
+ ssh2@1.17.0:
+ resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
+ engines: {node: '>=10.16.0'}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -2055,6 +2524,31 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+ streamx@2.25.0:
+ resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ string_decoder@1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
+ string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -2094,6 +2588,28 @@ packages:
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
engines: {node: '>=6'}
+ tar-fs@2.1.4:
+ resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
+
+ tar-fs@3.1.2:
+ resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==}
+
+ tar-stream@2.2.0:
+ resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+ engines: {node: '>=6'}
+
+ tar-stream@3.2.0:
+ resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==}
+
+ teex@1.0.1:
+ resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
+
+ testcontainers@12.0.0:
+ resolution: {integrity: sha512-/PdRvFvuHPwX126HR7RO0cEgLD3Nr8sWZyWSv54ei92TT79BubUkOCU5uwTc8ufTsTGQf0v6nyvZJVVVyR9Uqw==}
+
+ text-decoder@1.2.7:
+ resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
+
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -2127,6 +2643,10 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
+ tmp@0.2.5:
+ resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
+ engines: {node: '>=14.14'}
+
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
@@ -2167,6 +2687,9 @@ packages:
resolution: {integrity: sha512-FlJ8OD5Qcp0jTAM7E4a/RhUzRNds2GzKlyxHKA6N247VLy628rrxAGlMpIXSz6VB430+TiQDJ/SMl6PL1lu6wQ==}
hasBin: true
+ tweetnacl@0.14.5:
+ resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2183,9 +2706,16 @@ packages:
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
+ undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ undici@7.25.0:
+ resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
+ engines: {node: '>=20.18.1'}
+
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@@ -2193,6 +2723,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
valibot@1.3.1:
resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==}
peerDependencies:
@@ -2300,6 +2833,14 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@@ -2315,15 +2856,31 @@ packages:
utf-8-validate:
optional: true
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
yaml@2.8.3:
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
engines: {node: '>= 14.6'}
hasBin: true
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zip-stream@6.0.1:
+ resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
+ engines: {node: '>= 14'}
+
zod-to-json-schema@3.25.2:
resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
peerDependencies:
@@ -2363,6 +2920,43 @@ snapshots:
'@babel/runtime@7.29.2': {}
+ '@balena/dockerignore@1.0.2': {}
+
+ '@biomejs/biome@1.9.4':
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 1.9.4
+ '@biomejs/cli-darwin-x64': 1.9.4
+ '@biomejs/cli-linux-arm64': 1.9.4
+ '@biomejs/cli-linux-arm64-musl': 1.9.4
+ '@biomejs/cli-linux-x64': 1.9.4
+ '@biomejs/cli-linux-x64-musl': 1.9.4
+ '@biomejs/cli-win32-arm64': 1.9.4
+ '@biomejs/cli-win32-x64': 1.9.4
+
+ '@biomejs/cli-darwin-arm64@1.9.4':
+ optional: true
+
+ '@biomejs/cli-darwin-x64@1.9.4':
+ optional: true
+
+ '@biomejs/cli-linux-arm64-musl@1.9.4':
+ optional: true
+
+ '@biomejs/cli-linux-arm64@1.9.4':
+ optional: true
+
+ '@biomejs/cli-linux-x64-musl@1.9.4':
+ optional: true
+
+ '@biomejs/cli-linux-x64@1.9.4':
+ optional: true
+
+ '@biomejs/cli-win32-arm64@1.9.4':
+ optional: true
+
+ '@biomejs/cli-win32-x64@1.9.4':
+ optional: true
+
'@emnapi/runtime@1.9.1':
dependencies:
tslib: 2.8.1
@@ -2509,6 +3103,25 @@ snapshots:
dependencies:
graphql: 16.13.2
+ '@grpc/grpc-js@1.14.4':
+ dependencies:
+ '@grpc/proto-loader': 0.8.1
+ '@js-sdsl/ordered-map': 4.4.2
+
+ '@grpc/proto-loader@0.7.15':
+ dependencies:
+ lodash.camelcase: 4.3.0
+ long: 5.3.2
+ protobufjs: 7.6.0
+ yargs: 17.7.2
+
+ '@grpc/proto-loader@0.8.1':
+ dependencies:
+ lodash.camelcase: 4.3.0
+ long: 5.3.2
+ protobufjs: 7.6.0
+ yargs: 17.7.2
+
'@hono/node-server@1.19.12(hono@4.12.9)':
dependencies:
hono: 4.12.9
@@ -2623,6 +3236,15 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.2.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -2642,6 +3264,14 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@js-sdsl/ordered-map@4.4.2': {}
+
+ '@kwsites/file-exists@1.1.1':
+ dependencies:
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
'@modelcontextprotocol/sdk@1.28.0(zod@4.3.6)':
dependencies:
'@hono/node-server': 1.19.12(hono@4.12.9)
@@ -2735,6 +3365,9 @@ snapshots:
'@noble/hashes@2.0.1': {}
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
'@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)':
optionalDependencies:
prisma: 6.19.2(typescript@5.9.3)
@@ -2781,6 +3414,28 @@ snapshots:
'@protobuf-ts/runtime@2.11.1': {}
+ '@protobufjs/aspromise@1.1.2': {}
+
+ '@protobufjs/base64@1.1.2': {}
+
+ '@protobufjs/codegen@2.0.5': {}
+
+ '@protobufjs/eventemitter@1.1.0': {}
+
+ '@protobufjs/fetch@1.1.1':
+ dependencies:
+ '@protobufjs/aspromise': 1.1.2
+
+ '@protobufjs/float@1.0.2': {}
+
+ '@protobufjs/inquire@1.1.2': {}
+
+ '@protobufjs/path@1.1.2': {}
+
+ '@protobufjs/pool@1.1.0': {}
+
+ '@protobufjs/utf8@1.1.1': {}
+
'@readme/better-ajv-errors@2.4.0(ajv@8.18.0)':
dependencies:
'@babel/code-frame': 7.29.0
@@ -3014,10 +3669,25 @@ snapshots:
'@types/deep-eql@4.0.2': {}
+ '@types/docker-modem@3.0.6':
+ dependencies:
+ '@types/node': 22.19.15
+ '@types/ssh2': 1.15.5
+
+ '@types/dockerode@4.0.1':
+ dependencies:
+ '@types/docker-modem': 3.0.6
+ '@types/node': 22.19.15
+ '@types/ssh2': 1.15.5
+
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
+ '@types/node@18.19.130':
+ dependencies:
+ undici-types: 5.26.5
+
'@types/node@20.19.37':
dependencies:
undici-types: 6.21.0
@@ -3034,6 +3704,19 @@ snapshots:
dependencies:
csstype: 3.2.3
+ '@types/ssh2-streams@0.1.13':
+ dependencies:
+ '@types/node': 22.19.15
+
+ '@types/ssh2@0.5.52':
+ dependencies:
+ '@types/node': 22.19.15
+ '@types/ssh2-streams': 0.1.13
+
+ '@types/ssh2@1.15.5':
+ dependencies:
+ '@types/node': 18.19.130
+
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
@@ -3081,6 +3764,10 @@ snapshots:
typescript: 5.9.3
zod: 4.3.6
+ abort-controller@3.0.0:
+ dependencies:
+ event-target-shim: 5.0.1
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
@@ -3114,18 +3801,102 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
+ ansi-styles@6.2.3: {}
+
any-promise@1.3.0: {}
+ archiver-utils@5.0.2:
+ dependencies:
+ glob: 10.5.0
+ graceful-fs: 4.2.11
+ is-stream: 2.0.1
+ lazystream: 1.0.1
+ lodash: 4.18.1
+ normalize-path: 3.0.0
+ readable-stream: 4.7.0
+
+ archiver@7.0.1:
+ dependencies:
+ archiver-utils: 5.0.2
+ async: 3.2.6
+ buffer-crc32: 1.0.0
+ readable-stream: 4.7.0
+ readdir-glob: 1.1.3
+ tar-stream: 3.2.0
+ zip-stream: 6.0.1
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - react-native-b4a
+
argparse@2.0.1: {}
+ asn1@0.2.6:
+ dependencies:
+ safer-buffer: 2.1.2
+
assertion-error@2.0.1: {}
+ async-lock@1.4.1: {}
+
+ async@3.2.6: {}
+
+ b4a@1.8.1: {}
+
balanced-match@1.0.2: {}
+ bare-events@2.8.3: {}
+
+ bare-fs@4.7.1:
+ dependencies:
+ bare-events: 2.8.3
+ bare-path: 3.0.0
+ bare-stream: 2.13.1(bare-events@2.8.3)
+ bare-url: 2.4.3
+ fast-fifo: 1.3.2
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+
+ bare-os@3.9.1: {}
+
+ bare-path@3.0.0:
+ dependencies:
+ bare-os: 3.9.1
+
+ bare-stream@2.13.1(bare-events@2.8.3):
+ dependencies:
+ streamx: 2.25.0
+ teex: 1.0.1
+ optionalDependencies:
+ bare-events: 2.8.3
+ transitivePeerDependencies:
+ - react-native-b4a
+
+ bare-url@2.4.3:
+ dependencies:
+ bare-path: 3.0.0
+
+ base64-js@1.5.1: {}
+
+ bcrypt-pbkdf@1.0.2:
+ dependencies:
+ tweetnacl: 0.14.5
+
+ bl@4.1.0:
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
body-parser@2.2.2:
dependencies:
bytes: 3.1.2
@@ -3145,11 +3916,32 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
+ brace-expansion@2.1.0:
+ dependencies:
+ balanced-match: 1.0.2
+
+ buffer-crc32@1.0.0: {}
+
+ buffer@5.7.1:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ buildcheck@0.0.7:
+ optional: true
+
bundle-require@5.1.0(esbuild@0.27.4):
dependencies:
esbuild: 0.27.4
load-tsconfig: 0.2.5
+ byline@5.0.0: {}
+
bytes@3.1.2: {}
c12@3.1.0:
@@ -3202,6 +3994,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
+ chownr@1.1.4: {}
+
citty@0.1.6:
dependencies:
consola: 3.4.2
@@ -3210,6 +4004,12 @@ snapshots:
client-only@0.0.1: {}
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -3218,6 +4018,14 @@ snapshots:
commander@4.1.1: {}
+ compress-commons@6.0.2:
+ dependencies:
+ crc-32: 1.2.2
+ crc32-stream: 6.0.0
+ is-stream: 2.0.1
+ normalize-path: 3.0.0
+ readable-stream: 4.7.0
+
concat-map@0.0.1: {}
confbox@0.1.8: {}
@@ -3234,11 +4042,26 @@ snapshots:
cookie@0.7.2: {}
+ core-util-is@1.0.3: {}
+
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
+ cpu-features@0.0.10:
+ dependencies:
+ buildcheck: 0.0.7
+ nan: 2.27.0
+ optional: true
+
+ crc-32@1.2.2: {}
+
+ crc32-stream@6.0.0:
+ dependencies:
+ crc-32: 1.2.2
+ readable-stream: 4.7.0
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -3265,6 +4088,30 @@ snapshots:
detect-libc@2.1.2: {}
+ docker-compose@1.4.2:
+ dependencies:
+ yaml: 2.8.3
+
+ docker-modem@5.0.7:
+ dependencies:
+ debug: 4.4.3
+ readable-stream: 3.6.2
+ split-ca: 1.0.1
+ ssh2: 1.17.0
+ transitivePeerDependencies:
+ - supports-color
+
+ dockerode@5.0.0:
+ dependencies:
+ '@balena/dockerignore': 1.0.2
+ '@grpc/grpc-js': 1.14.4
+ '@grpc/proto-loader': 0.7.15
+ docker-modem: 5.0.7
+ protobufjs: 7.6.0
+ tar-fs: 2.1.4
+ transitivePeerDependencies:
+ - supports-color
+
dotenv@16.6.1: {}
dunder-proto@1.0.1:
@@ -3273,6 +4120,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ eastasianwidth@0.2.0: {}
+
ee-first@1.1.1: {}
effect@3.18.4:
@@ -3280,10 +4129,18 @@ snapshots:
'@standard-schema/spec': 1.1.0
fast-check: 3.23.2
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
empathic@2.0.0: {}
encodeurl@2.0.0: {}
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
enhanced-resolve@5.20.1:
dependencies:
graceful-fs: 4.2.11
@@ -3328,6 +4185,8 @@ snapshots:
'@esbuild/win32-ia32': 0.27.4
'@esbuild/win32-x64': 0.27.4
+ escalade@3.2.0: {}
+
escape-html@1.0.3: {}
escape-string-regexp@4.0.0: {}
@@ -3406,8 +4265,18 @@ snapshots:
etag@1.8.1: {}
+ event-target-shim@5.0.1: {}
+
eventemitter3@5.0.1: {}
+ events-universal@1.0.1:
+ dependencies:
+ bare-events: 2.8.3
+ transitivePeerDependencies:
+ - bare-abort-controller
+
+ events@3.3.0: {}
+
eventsource-parser@3.0.6: {}
eventsource@3.0.7:
@@ -3462,6 +4331,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
+ fast-fifo@1.3.2: {}
+
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
@@ -3505,15 +4376,24 @@ snapshots:
flatted@3.4.2: {}
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
forwarded@0.2.0: {}
fresh@2.0.0: {}
+ fs-constants@1.0.0: {}
+
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
+ get-caller-file@2.0.5: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3527,6 +4407,8 @@ snapshots:
hasown: 2.0.2
math-intrinsics: 1.1.0
+ get-port@7.2.0: {}
+
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
@@ -3545,6 +4427,15 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob@10.5.0:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.9
+ minipass: 7.1.3
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
globals@14.0.0: {}
gopd@1.2.0: {}
@@ -3587,6 +4478,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
+ ieee754@1.2.1: {}
+
ignore@5.3.2: {}
import-fresh@3.3.1:
@@ -3617,18 +4510,30 @@ snapshots:
is-extglob@2.1.1: {}
+ is-fullwidth-code-point@3.0.0: {}
+
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-promise@4.0.0: {}
+ is-stream@2.0.1: {}
+
+ isarray@1.0.0: {}
+
isexe@2.0.0: {}
isows@1.0.7(ws@8.18.3):
dependencies:
ws: 8.18.3
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
jiti@2.6.1: {}
jose@6.2.2: {}
@@ -3659,6 +4564,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
+ lazystream@1.0.1:
+ dependencies:
+ readable-stream: 2.3.8
+
leven@3.1.0: {}
levn@0.4.1:
@@ -3725,10 +4634,18 @@ snapshots:
dependencies:
p-locate: 5.0.0
+ lodash.camelcase@4.3.0: {}
+
lodash.merge@4.6.2: {}
+ lodash@4.18.1: {}
+
+ long@5.3.2: {}
+
loupe@3.2.1: {}
+ lru-cache@10.4.3: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3749,6 +4666,20 @@ snapshots:
dependencies:
brace-expansion: 1.1.13
+ minimatch@5.1.9:
+ dependencies:
+ brace-expansion: 2.1.0
+
+ minimatch@9.0.9:
+ dependencies:
+ brace-expansion: 2.1.0
+
+ minipass@7.1.3: {}
+
+ mkdirp-classic@0.5.3: {}
+
+ mkdirp@3.0.1: {}
+
mlly@1.8.2:
dependencies:
acorn: 8.16.0
@@ -3782,6 +4713,9 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
+ nan@2.27.0:
+ optional: true
+
nanoid@3.3.11: {}
natural-compare@1.4.0: {}
@@ -3813,6 +4747,8 @@ snapshots:
node-fetch-native@1.6.7: {}
+ normalize-path@3.0.0: {}
+
nypm@0.6.5:
dependencies:
citty: 0.2.1
@@ -3882,6 +4818,8 @@ snapshots:
dependencies:
p-limit: 3.1.0
+ package-json-from-dist@1.0.1: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -3892,6 +4830,11 @@ snapshots:
path-key@3.1.1: {}
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.3
+
path-to-regexp@8.4.0: {}
pathe@2.0.3: {}
@@ -3953,11 +4896,48 @@ snapshots:
transitivePeerDependencies:
- magicast
+ process-nextick-args@2.0.1: {}
+
+ process@0.11.10: {}
+
+ proper-lockfile@4.1.2:
+ dependencies:
+ graceful-fs: 4.2.11
+ retry: 0.12.0
+ signal-exit: 3.0.7
+
+ properties-reader@3.0.1:
+ dependencies:
+ '@kwsites/file-exists': 1.1.1
+ mkdirp: 3.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ protobufjs@7.6.0:
+ dependencies:
+ '@protobufjs/aspromise': 1.1.2
+ '@protobufjs/base64': 1.1.2
+ '@protobufjs/codegen': 2.0.5
+ '@protobufjs/eventemitter': 1.1.0
+ '@protobufjs/fetch': 1.1.1
+ '@protobufjs/float': 1.0.2
+ '@protobufjs/inquire': 1.1.2
+ '@protobufjs/path': 1.1.2
+ '@protobufjs/pool': 1.1.0
+ '@protobufjs/utf8': 1.1.1
+ '@types/node': 22.19.15
+ long: 5.3.2
+
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
+ pump@3.0.4:
+ dependencies:
+ end-of-stream: 1.4.5
+ once: 1.4.0
+
punycode@2.3.1: {}
pure-rand@6.1.0: {}
@@ -3987,14 +4967,46 @@ snapshots:
react@19.2.4: {}
+ readable-stream@2.3.8:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ readable-stream@4.7.0:
+ dependencies:
+ abort-controller: 3.0.0
+ buffer: 6.0.3
+ events: 3.3.0
+ process: 0.11.10
+ string_decoder: 1.3.0
+
+ readdir-glob@1.1.3:
+ dependencies:
+ minimatch: 5.1.9
+
readdirp@4.1.2: {}
+ require-directory@2.1.1: {}
+
require-from-string@2.0.2: {}
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
+ retry@0.12.0: {}
+
rollup@4.60.1:
dependencies:
'@types/estree': 1.0.8
@@ -4036,6 +5048,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ safe-buffer@5.1.2: {}
+
+ safe-buffer@5.2.1: {}
+
safer-buffer@2.1.2: {}
scheduler@0.27.0: {}
@@ -4138,16 +5154,72 @@ snapshots:
siginfo@2.0.0: {}
+ signal-exit@3.0.7: {}
+
+ signal-exit@4.1.0: {}
+
source-map-js@1.2.1: {}
source-map@0.7.6: {}
+ split-ca@1.0.1: {}
+
+ ssh-remote-port-forward@1.0.4:
+ dependencies:
+ '@types/ssh2': 0.5.52
+ ssh2: 1.17.0
+
+ ssh2@1.17.0:
+ dependencies:
+ asn1: 0.2.6
+ bcrypt-pbkdf: 1.0.2
+ optionalDependencies:
+ cpu-features: 0.0.10
+ nan: 2.27.0
+
stackback@0.0.2: {}
statuses@2.0.2: {}
std-env@3.10.0: {}
+ streamx@2.25.0:
+ dependencies:
+ events-universal: 1.0.1
+ fast-fifo: 1.3.2
+ text-decoder: 1.2.7
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.2.0
+
+ string_decoder@1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
+ string_decoder@1.3.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
strip-json-comments@3.1.1: {}
strip-literal@3.1.0:
@@ -4179,6 +5251,80 @@ snapshots:
tapable@2.3.2: {}
+ tar-fs@2.1.4:
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.4
+ tar-stream: 2.2.0
+
+ tar-fs@3.1.2:
+ dependencies:
+ pump: 3.0.4
+ tar-stream: 3.2.0
+ optionalDependencies:
+ bare-fs: 4.7.1
+ bare-path: 3.0.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - react-native-b4a
+
+ tar-stream@2.2.0:
+ dependencies:
+ bl: 4.1.0
+ end-of-stream: 1.4.5
+ fs-constants: 1.0.0
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
+ tar-stream@3.2.0:
+ dependencies:
+ b4a: 1.8.1
+ bare-fs: 4.7.1
+ fast-fifo: 1.3.2
+ streamx: 2.25.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - react-native-b4a
+
+ teex@1.0.1:
+ dependencies:
+ streamx: 2.25.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+
+ testcontainers@12.0.0:
+ dependencies:
+ '@balena/dockerignore': 1.0.2
+ '@types/dockerode': 4.0.1
+ archiver: 7.0.1
+ async-lock: 1.4.1
+ byline: 5.0.0
+ debug: 4.4.3
+ docker-compose: 1.4.2
+ dockerode: 5.0.0
+ get-port: 7.2.0
+ proper-lockfile: 4.1.2
+ properties-reader: 3.0.1
+ ssh-remote-port-forward: 1.0.4
+ tar-fs: 3.1.2
+ tmp: 0.2.5
+ undici: 7.25.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - react-native-b4a
+ - supports-color
+
+ text-decoder@1.2.7:
+ dependencies:
+ b4a: 1.8.1
+ transitivePeerDependencies:
+ - react-native-b4a
+
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@@ -4204,6 +5350,8 @@ snapshots:
tinyspy@4.0.4: {}
+ tmp@0.2.5: {}
+
toidentifier@1.0.1: {}
tokenx@1.3.0: {}
@@ -4251,6 +5399,8 @@ snapshots:
'@turbo/windows-64': 2.8.21
'@turbo/windows-arm64': 2.8.21
+ tweetnacl@0.14.5: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -4265,14 +5415,20 @@ snapshots:
ufo@1.6.3: {}
+ undici-types@5.26.5: {}
+
undici-types@6.21.0: {}
+ undici@7.25.0: {}
+
unpipe@1.0.0: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
+ util-deprecate@1.0.2: {}
+
valibot@1.3.1(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
@@ -4384,14 +5540,46 @@ snapshots:
word-wrap@1.2.5: {}
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 5.1.2
+ strip-ansi: 7.2.0
+
wrappy@1.0.2: {}
ws@8.18.3: {}
+ y18n@5.0.8: {}
+
yaml@2.8.3: {}
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
yocto-queue@0.1.0: {}
+ zip-stream@6.0.1:
+ dependencies:
+ archiver-utils: 5.0.2
+ compress-commons: 6.0.2
+ readable-stream: 4.7.0
+
zod-to-json-schema@3.25.2(zod@4.3.6):
dependencies:
zod: 4.3.6
From 370ef53b56a7c51c89b89250d7b8a5eecb07d357 Mon Sep 17 00:00:00 2001
From: Manos Liolios
Date: Thu, 21 May 2026 10:14:41 +0300
Subject: [PATCH 2/6] nit
---
packages/mpp/test/unit/server.test.ts | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/packages/mpp/test/unit/server.test.ts b/packages/mpp/test/unit/server.test.ts
index cdb5b64..00b50f5 100644
--- a/packages/mpp/test/unit/server.test.ts
+++ b/packages/mpp/test/unit/server.test.ts
@@ -2,10 +2,12 @@ import type { Challenge, Credential, Method } from 'mppx';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { InMemoryDigestStore } from '../../src/in-memory-digest-store.js';
import type { suiCharge } from '../../src/method.js';
-import type { DigestStore, sui as createSuiServer } from '../../src/server.js';
+import {
+ type DigestStore,
+ SUI_USDC_TYPE,
+ type sui as createSuiServer,
+} from '../../src/server.js';
-const USDC_TYPE =
- '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC';
const RECIPIENT = '0xrecipient_address';
const SENDER = '0xsender_address';
@@ -27,7 +29,7 @@ type SuiCredential = Credential.Credential<
function buildMockTx({
success = true,
- coinType = USDC_TYPE,
+ coinType = SUI_USDC_TYPE,
recipientAddr = RECIPIENT,
amount = '10000',
senderAddr = SENDER,
From 59fef937238743957f60c1d782dc043cb32236ae Mon Sep 17 00:00:00 2001
From: Manos Liolios
Date: Thu, 21 May 2026 10:16:59 +0300
Subject: [PATCH 3/6] nit
---
packages/mpp/test/unit/server.test.ts | 32 +++++++++++++--------------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/packages/mpp/test/unit/server.test.ts b/packages/mpp/test/unit/server.test.ts
index 00b50f5..2aa5be4 100644
--- a/packages/mpp/test/unit/server.test.ts
+++ b/packages/mpp/test/unit/server.test.ts
@@ -67,7 +67,7 @@ function buildCredential(
realm: 'test',
request: {
amount,
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
},
},
@@ -116,7 +116,7 @@ describe('server verify', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -131,7 +131,7 @@ describe('server verify', () => {
mockGetTransaction.mockResolvedValue(buildMockTx({ success: false }));
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -147,7 +147,7 @@ describe('server verify', () => {
);
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -161,7 +161,7 @@ describe('server verify', () => {
mockGetTransaction.mockResolvedValue(buildMockTx({ amount: '5000' }));
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -179,7 +179,7 @@ describe('server verify', () => {
});
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -209,7 +209,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store,
});
@@ -225,7 +225,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store,
});
@@ -241,7 +241,7 @@ describe('digest replay protection', () => {
const store = new InMemoryDigestStore();
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store,
});
@@ -254,8 +254,8 @@ describe('digest replay protection', () => {
digest: '0xdigest456',
status: { success: true },
balanceChanges: [
- { coinType: USDC_TYPE, address: RECIPIENT, amount: '10000' },
- { coinType: USDC_TYPE, address: SENDER, amount: '-10000' },
+ { coinType: SUI_USDC_TYPE, address: RECIPIENT, amount: '10000' },
+ { coinType: SUI_USDC_TYPE, address: SENDER, amount: '-10000' },
],
},
});
@@ -273,7 +273,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store,
});
@@ -288,9 +288,9 @@ describe('digest replay protection', () => {
it('throws on missing store in production', () => {
process.env.NODE_ENV = 'production';
- expect(() => suiFn({ currency: USDC_TYPE, recipient: RECIPIENT })).toThrow(
- 'DigestStore is required in production',
- );
+ expect(() =>
+ suiFn({ currency: SUI_USDC_TYPE, recipient: RECIPIENT }),
+ ).toThrow('DigestStore is required in production');
});
it('marks digest before returning receipt (store.set failure = no free call)', async () => {
@@ -301,7 +301,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: USDC_TYPE,
+ currency: SUI_USDC_TYPE,
recipient: RECIPIENT,
store,
});
From dfcff897f4e6ef5283d76ed66b9df7c50487b580 Mon Sep 17 00:00:00 2001
From: Manos Liolios
Date: Thu, 21 May 2026 11:35:16 +0300
Subject: [PATCH 4/6] Adds grief protection for payments
---
packages/mpp/src/client.ts | 16 +++-
packages/mpp/src/method.ts | 1 +
packages/mpp/src/proof.ts | 39 +++++++++
packages/mpp/src/server.ts | 30 ++++++-
packages/mpp/test/e2e/sui-payment.test.ts | 70 ++++++++++++++---
packages/mpp/test/unit/client.test.ts | 38 ++++++---
packages/mpp/test/unit/server.test.ts | 96 ++++++++++++++++++++++-
7 files changed, 258 insertions(+), 32 deletions(-)
create mode 100644 packages/mpp/src/proof.ts
diff --git a/packages/mpp/src/client.ts b/packages/mpp/src/client.ts
index 341d485..1e5aae4 100644
--- a/packages/mpp/src/client.ts
+++ b/packages/mpp/src/client.ts
@@ -3,6 +3,7 @@ import type { Signer } from '@mysten/sui/cryptography';
import { Transaction, coinWithBalance } from '@mysten/sui/transactions';
import { Credential, Method } from 'mppx';
import { suiCharge } from './method.js';
+import { createSuiPaymentProofBytes } from './proof.js';
import { parseAmountToRaw } from './utils.js';
export { suiCharge } from './method.js';
@@ -20,7 +21,6 @@ export interface SuiChargeOptions {
type TransactionResult = { digest: string };
export function sui(options: SuiChargeOptions) {
- const address = options.signer.toSuiAddress();
const decimals = options.decimals ?? 6;
return Method.toClient(suiCharge, {
@@ -29,7 +29,7 @@ export function sui(options: SuiChargeOptions) {
const amountRaw = parseAmountToRaw(amount, decimals);
const tx = new Transaction();
- tx.setSender(address);
+ tx.setSender(options.signer.toSuiAddress());
const payment = coinWithBalance({ balance: amountRaw, type: currency });
tx.transferObjects([payment], recipient);
@@ -62,9 +62,19 @@ export function sui(options: SuiChargeOptions) {
throw new Error(`Payment transaction failed: ${msg}`);
}
+ const proof = await options.signer.signPersonalMessage(
+ createSuiPaymentProofBytes({
+ challenge,
+ digest: result.digest,
+ }),
+ );
+
return Credential.serialize({
challenge,
- payload: { digest: result.digest },
+ payload: {
+ digest: result.digest,
+ signature: proof.signature,
+ },
});
},
});
diff --git a/packages/mpp/src/method.ts b/packages/mpp/src/method.ts
index 0406df7..f3fc505 100644
--- a/packages/mpp/src/method.ts
+++ b/packages/mpp/src/method.ts
@@ -7,6 +7,7 @@ export const suiCharge = Method.from({
credential: {
payload: z.object({
digest: z.string(),
+ signature: z.string(),
}),
},
request: z.object({
diff --git a/packages/mpp/src/proof.ts b/packages/mpp/src/proof.ts
new file mode 100644
index 0000000..6bf5a37
--- /dev/null
+++ b/packages/mpp/src/proof.ts
@@ -0,0 +1,39 @@
+import type { Challenge } from 'mppx';
+
+export interface SuiPaymentProofMessage {
+ challenge: Challenge.Challenge<
+ {
+ amount: string;
+ currency: string;
+ recipient: string;
+ },
+ 'charge',
+ 'sui'
+ >;
+ digest: string;
+}
+
+const textEncoder = new TextEncoder();
+
+export function createSuiPaymentProofMessage({
+ challenge,
+ digest,
+}: SuiPaymentProofMessage): string {
+ return JSON.stringify({
+ domain: 'suimpp.sui.payment-proof',
+ version: 1,
+ method: challenge.method,
+ intent: challenge.intent,
+ challengeId: challenge.id,
+ amount: challenge.request.amount,
+ currency: challenge.request.currency,
+ recipient: challenge.request.recipient,
+ digest,
+ });
+}
+
+export function createSuiPaymentProofBytes(
+ message: SuiPaymentProofMessage,
+): Uint8Array {
+ return textEncoder.encode(createSuiPaymentProofMessage(message));
+}
diff --git a/packages/mpp/src/server.ts b/packages/mpp/src/server.ts
index 6201e50..0201646 100644
--- a/packages/mpp/src/server.ts
+++ b/packages/mpp/src/server.ts
@@ -1,9 +1,11 @@
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
import { normalizeSuiAddress } from '@mysten/sui/utils';
+import { verifyPersonalMessageSignature } from '@mysten/sui/verify';
import { Method, Receipt } from 'mppx';
import { InMemoryDigestStore } from './in-memory-digest-store.js';
import { suiCharge } from './method.js';
+import { createSuiPaymentProofBytes } from './proof.js';
import { parseAmountToRaw, withRetry } from './utils.js';
export { suiCharge } from './method.js';
@@ -97,7 +99,7 @@ export function sui(options: SuiServerOptions) {
const tx = await withRetry(() =>
client.core.getTransaction({
digest,
- include: { balanceChanges: true },
+ include: { balanceChanges: true, transaction: true },
}),
).catch(() => {
throw new Error(
@@ -132,6 +134,28 @@ export function sui(options: SuiServerOptions) {
);
}
+ const publicKey = await verifyPersonalMessageSignature(
+ createSuiPaymentProofBytes({
+ challenge: credential.challenge,
+ digest,
+ }),
+ credential.payload.signature,
+ ).catch(() => {
+ throw new Error('Invalid payment proof signature');
+ });
+ const sender = resolved.transaction?.sender;
+ if (!sender) {
+ throw new Error('Transaction sender not found');
+ }
+ if (
+ normalizeSuiAddress(publicKey.toSuiAddress()) !==
+ normalizeSuiAddress(sender)
+ ) {
+ throw new Error(
+ 'Payment proof signer does not match transaction sender',
+ );
+ }
+
await digestStore.set(digest);
const receipt = Receipt.from({
@@ -143,9 +167,7 @@ export function sui(options: SuiServerOptions) {
const report: PaymentReport = {
digest,
- sender: resolved.balanceChanges.find(
- (bc) => bc.coinType === options.currency && BigInt(bc.amount) < 0n,
- )?.address,
+ sender,
recipient: options.recipient,
amount: credential.challenge.request.amount,
currency: options.currency,
diff --git a/packages/mpp/test/e2e/sui-payment.test.ts b/packages/mpp/test/e2e/sui-payment.test.ts
index 10338a4..14e7252 100644
--- a/packages/mpp/test/e2e/sui-payment.test.ts
+++ b/packages/mpp/test/e2e/sui-payment.test.ts
@@ -5,7 +5,11 @@ import { beforeAll, describe, expect, it, vi } from 'vitest';
import { sui as createSuiClient } from '../../src/client.js';
import { InMemoryDigestStore } from '../../src/in-memory-digest-store.js';
import { suiCharge } from '../../src/method.js';
-import { sui as createSuiServer } from '../../src/server.js';
+import { createSuiPaymentProofBytes } from '../../src/proof.js';
+import {
+ type PaymentReport,
+ sui as createSuiServer,
+} from '../../src/server.js';
import { fundAddress, getClient, getFullnodeUrl } from './setup.js';
const PAYMENT_AMOUNT = '0.01';
@@ -22,13 +26,14 @@ type SuiChargeChallenge = Challenge.Challenge<
'sui'
>;
type SuiChargeCredential = Credential.Credential<
- { digest: string },
+ { digest: string; signature: string },
SuiChargeChallenge
>;
describe('Sui localnet e2e payment', () => {
const localnetClient = getClient();
const payerKeypair = Ed25519Keypair.generate();
+ const thiefKeypair = Ed25519Keypair.generate();
const recipientKeypair = Ed25519Keypair.generate();
const recipient = recipientKeypair.getPublicKey().toSuiAddress();
@@ -36,10 +41,9 @@ describe('Sui localnet e2e payment', () => {
await fundAddress(payerKeypair.getPublicKey().toSuiAddress());
});
- it('creates an on-chain credential and verifies the payment once', async () => {
- const onPayment = vi.fn();
- const challenge = Challenge.fromMethod(suiCharge, {
- id: 'sui-localnet-e2e-payment',
+ function buildChallenge(id: string): SuiChargeChallenge {
+ return Challenge.fromMethod(suiCharge, {
+ id,
realm: 'suimpp-e2e',
request: {
amount: PAYMENT_AMOUNT,
@@ -47,12 +51,10 @@ describe('Sui localnet e2e payment', () => {
recipient,
},
}) as SuiChargeChallenge;
- const clientMethod = createSuiClient({
- client: localnetClient,
- signer: payerKeypair,
- decimals: SUI_DECIMALS,
- });
- const serverMethod = createSuiServer({
+ }
+
+ function buildServerMethod(onPayment?: (report: PaymentReport) => void) {
+ return createSuiServer({
currency: SUI_TYPE_ARG,
recipient,
decimals: SUI_DECIMALS,
@@ -61,15 +63,32 @@ describe('Sui localnet e2e payment', () => {
store: new InMemoryDigestStore(),
onPayment,
});
+ }
+ async function createCredential(challenge: SuiChargeChallenge) {
+ const clientMethod = createSuiClient({
+ client: localnetClient,
+ signer: payerKeypair,
+ decimals: SUI_DECIMALS,
+ });
const authorization = await clientMethod.createCredential({ challenge });
const credential = Credential.deserialize<{ digest: string }>(
authorization,
) as SuiChargeCredential;
+
await localnetClient.waitForTransaction({
digest: credential.payload.digest,
});
+ return credential;
+ }
+
+ it('creates an on-chain credential and verifies the payment once', async () => {
+ const onPayment = vi.fn();
+ const challenge = buildChallenge('sui-localnet-e2e-payment');
+ const serverMethod = buildServerMethod(onPayment);
+ const credential = await createCredential(challenge);
+
const receipt = await serverMethod.verify({
credential,
request: challenge.request,
@@ -97,4 +116,31 @@ describe('Sui localnet e2e payment', () => {
}),
).rejects.toThrow('Digest already used');
});
+
+ it('rejects a stolen proof signed by a different key', async () => {
+ const challenge = buildChallenge('sui-localnet-e2e-stolen-proof');
+ const serverMethod = buildServerMethod();
+ const credential = await createCredential(challenge);
+
+ const stolenProof = await thiefKeypair.signPersonalMessage(
+ createSuiPaymentProofBytes({
+ challenge,
+ digest: credential.payload.digest,
+ }),
+ );
+ const stolenCredential: SuiChargeCredential = {
+ ...credential,
+ payload: {
+ ...credential.payload,
+ signature: stolenProof.signature,
+ },
+ };
+
+ await expect(
+ serverMethod.verify({
+ credential: stolenCredential,
+ request: challenge.request,
+ }),
+ ).rejects.toThrow('Payment proof signer does not match transaction sender');
+ });
});
diff --git a/packages/mpp/test/unit/client.test.ts b/packages/mpp/test/unit/client.test.ts
index ef90d6b..20c4c8d 100644
--- a/packages/mpp/test/unit/client.test.ts
+++ b/packages/mpp/test/unit/client.test.ts
@@ -1,4 +1,5 @@
import type { Challenge } from 'mppx';
+import { Credential } from 'mppx';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type {
SuiChargeOptions,
@@ -25,6 +26,9 @@ const mockClient = {
const mockSigner = {
toSuiAddress: () => '0xagent_address',
+ signPersonalMessage: vi
+ .fn()
+ .mockResolvedValue({ bytes: 'proof_bytes', signature: 'proof_sig' }),
signTransaction: vi
.fn()
.mockResolvedValue({ bytes: 'mock_bytes', signature: 'mock_sig' }),
@@ -79,18 +83,25 @@ describe('client createCredential', () => {
const challenge = buildChallenge();
- try {
- await clientMethod.createCredential({ challenge });
- } catch {
- // Credential.serialize may not be available in test; this verifies TX building.
- }
+ const authorization = await clientMethod.createCredential({ challenge });
+ const credential = Credential.deserialize<{
+ digest: string;
+ signature: string;
+ }>(authorization);
expect(mockSigner.signTransaction).toHaveBeenCalled();
+ expect(mockSigner.signPersonalMessage).toHaveBeenCalledWith(
+ expect.any(Uint8Array),
+ );
expect(mockExecuteTransaction).toHaveBeenCalledWith({
transaction: expect.any(Uint8Array),
signatures: ['mock_sig'],
include: { effects: true },
});
+ expect(credential.payload).toEqual({
+ digest: '0xtxdigest',
+ signature: 'proof_sig',
+ });
});
it('throws when transaction execution fails', async () => {
@@ -123,13 +134,20 @@ describe('client createCredential', () => {
const challenge = buildChallenge('1.00');
- try {
- await clientMethod.createCredential({ challenge });
- } catch {
- // Credential.serialize may not be available in test.
- }
+ const authorization = await clientMethod.createCredential({ challenge });
+ const credential = Credential.deserialize<{
+ digest: string;
+ signature: string;
+ }>(authorization);
expect(customExecute).toHaveBeenCalled();
+ expect(mockSigner.signPersonalMessage).toHaveBeenCalledWith(
+ expect.any(Uint8Array),
+ );
expect(mockExecuteTransaction).not.toHaveBeenCalled();
+ expect(credential.payload).toMatchObject({
+ digest: '0xcustom',
+ signature: 'proof_sig',
+ });
});
});
diff --git a/packages/mpp/test/unit/server.test.ts b/packages/mpp/test/unit/server.test.ts
index 2aa5be4..e553f79 100644
--- a/packages/mpp/test/unit/server.test.ts
+++ b/packages/mpp/test/unit/server.test.ts
@@ -23,7 +23,7 @@ type SuiChargeChallenge = Challenge.Challenge<
'sui'
>;
type SuiCredential = Credential.Credential<
- { digest: string },
+ { digest: string; signature: string },
SuiChargeChallenge
>;
@@ -43,6 +43,9 @@ function buildMockTx({
const txData = {
digest: '0xdigest123',
status: { success },
+ transaction: {
+ sender: senderAddr,
+ },
balanceChanges: [
{ coinType, address: recipientAddr, amount },
{ coinType, address: senderAddr, amount: `-${amount}` },
@@ -57,9 +60,10 @@ function buildMockTx({
function buildCredential(
digest = '0xdigest123',
amount = '0.01',
+ signature = 'proof_sig',
): SuiCredential {
return {
- payload: { digest },
+ payload: { digest, signature },
challenge: {
id: 'test-challenge',
intent: 'charge',
@@ -84,7 +88,12 @@ function verifyPayment(
});
}
-const mockGetTransaction = vi.fn();
+const { mockGetTransaction, mockVerifyPersonalMessageSignature } = vi.hoisted(
+ () => ({
+ mockGetTransaction: vi.fn(),
+ mockVerifyPersonalMessageSignature: vi.fn(),
+ }),
+);
vi.mock('@mysten/sui/grpc', () => ({
SuiGrpcClient: vi.fn().mockImplementation(() => ({
@@ -98,12 +107,19 @@ vi.mock('@mysten/sui/utils', () => ({
normalizeSuiAddress: vi.fn((addr: string) => addr.toLowerCase()),
}));
+vi.mock('@mysten/sui/verify', () => ({
+ verifyPersonalMessageSignature: mockVerifyPersonalMessageSignature,
+}));
+
describe('server verify', () => {
let suiFn: typeof createSuiServer;
const originalEnv = process.env.NODE_ENV;
beforeEach(async () => {
vi.clearAllMocks();
+ mockVerifyPersonalMessageSignature.mockResolvedValue({
+ toSuiAddress: () => SENDER,
+ });
const mod = await import('../../src/server.js');
suiFn = mod.sui;
});
@@ -188,6 +204,74 @@ describe('server verify', () => {
'Payment not found',
);
});
+
+ it('rejects when proof signature is invalid', async () => {
+ mockGetTransaction.mockResolvedValue(buildMockTx());
+ mockVerifyPersonalMessageSignature.mockRejectedValue(
+ new Error('bad signature'),
+ );
+
+ const store = {
+ has: vi.fn().mockResolvedValue(false),
+ set: vi.fn(),
+ } satisfies DigestStore;
+ const serverMethod = suiFn({
+ currency: SUI_USDC_TYPE,
+ recipient: RECIPIENT,
+ store,
+ });
+
+ await expect(verifyPayment(serverMethod)).rejects.toThrow(
+ 'Invalid payment proof signature',
+ );
+ expect(store.set).not.toHaveBeenCalled();
+ });
+
+ it('rejects when proof signer is not the transaction sender', async () => {
+ mockGetTransaction.mockResolvedValue(buildMockTx());
+ mockVerifyPersonalMessageSignature.mockResolvedValue({
+ toSuiAddress: () => '0xthief',
+ });
+
+ const store = {
+ has: vi.fn().mockResolvedValue(false),
+ set: vi.fn(),
+ } satisfies DigestStore;
+ const serverMethod = suiFn({
+ currency: SUI_USDC_TYPE,
+ recipient: RECIPIENT,
+ store,
+ });
+
+ await expect(verifyPayment(serverMethod)).rejects.toThrow(
+ 'Payment proof signer does not match transaction sender',
+ );
+ expect(store.set).not.toHaveBeenCalled();
+ });
+
+ it('rejects when transaction sender is missing', async () => {
+ mockGetTransaction.mockResolvedValue({
+ Transaction: {
+ digest: '0xdigest123',
+ status: { success: true },
+ transaction: undefined,
+ balanceChanges: [
+ { coinType: SUI_USDC_TYPE, address: RECIPIENT, amount: '10000' },
+ { coinType: SUI_USDC_TYPE, address: SENDER, amount: '-10000' },
+ ],
+ },
+ });
+
+ const serverMethod = suiFn({
+ currency: SUI_USDC_TYPE,
+ recipient: RECIPIENT,
+ store: new InMemoryDigestStore(),
+ });
+
+ await expect(verifyPayment(serverMethod)).rejects.toThrow(
+ 'Transaction sender not found',
+ );
+ });
});
describe('digest replay protection', () => {
@@ -196,6 +280,9 @@ describe('digest replay protection', () => {
beforeEach(async () => {
vi.clearAllMocks();
+ mockVerifyPersonalMessageSignature.mockResolvedValue({
+ toSuiAddress: () => SENDER,
+ });
const mod = await import('../../src/server.js');
suiFn = mod.sui;
});
@@ -253,6 +340,9 @@ describe('digest replay protection', () => {
Transaction: {
digest: '0xdigest456',
status: { success: true },
+ transaction: {
+ sender: SENDER,
+ },
balanceChanges: [
{ coinType: SUI_USDC_TYPE, address: RECIPIENT, amount: '10000' },
{ coinType: SUI_USDC_TYPE, address: SENDER, amount: '-10000' },
From 469e98a2a3edb4ffe0f4ab6e3b9b4ed9a52b945f Mon Sep 17 00:00:00 2001
From: Manos Liolios
Date: Thu, 21 May 2026 12:12:44 +0300
Subject: [PATCH 5/6] make store required for replay protection
---
README.md | 11 ++++---
apps/suimpp/app/components/CodeBlocks.tsx | 3 +-
apps/suimpp/app/docs/DocsContent.tsx | 16 +++++++---
packages/mpp/README.md | 17 +++++++---
packages/mpp/src/server.ts | 39 ++++++++---------------
packages/mpp/test/unit/server.test.ts | 18 +++--------
6 files changed, 51 insertions(+), 53 deletions(-)
diff --git a/README.md b/README.md
index baefd94..172999e 100644
--- a/README.md
+++ b/README.md
@@ -26,13 +26,14 @@ npm install @suimpp/mpp mppx
```
```typescript
-import { sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
import { Mppx } from 'mppx';
const mppx = Mppx.create({
methods: [sui({
currency: '0xdba...::usdc::USDC',
recipient: '0xYOUR_ADDRESS',
+ store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
});
@@ -87,7 +88,8 @@ Agent Server Sui
│ └─ TX confirmed ←──────────────────────────────────────────────│
│ digest: "Hp4oHHs..." │ │
│ │ │
- │── Retry + credential {digest} ──>│ │
+ │── Retry + credential ───────────>│ │
+ │ {digest, signature} │ │
│ │── getTransaction(digest) ──>│
│ │ verify: success, amount, │
│ │ recipient matches │
@@ -171,12 +173,12 @@ Payments are reported by the gateway (application layer), not by the library dir
└─────────────────────┘
```
-**Why this pattern?** The `verify()` callback has on-chain data (sender from balance changes) but no HTTP context (which endpoint was called). The gateway's charge wrapper has HTTP context but no on-chain data. The `onPayment` callback bridges the two layers using the digest as the join key.
+**Why this pattern?** The `verify()` callback has on-chain data (transaction sender, digest, amount, recipient) but no HTTP context (which endpoint was called). The gateway's charge wrapper has HTTP context but no on-chain data. The `onPayment` callback bridges the two layers using the digest as the join key.
**Implementation:**
```typescript
-import { sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
import type { PaymentReport } from '@suimpp/mpp/server';
// 1. Library emits on-chain data via callback
@@ -186,6 +188,7 @@ const mppx = Mppx.create({
methods: [sui({
currency: SUI_USDC_TYPE,
recipient: TREASURY_ADDRESS,
+ store: new InMemoryDigestStore(), // Use Redis/DB in production.
network: 'mainnet',
onPayment: (report) => {
pendingReports.set(report.digest, report);
diff --git a/apps/suimpp/app/components/CodeBlocks.tsx b/apps/suimpp/app/components/CodeBlocks.tsx
index 9ee0a16..b0d8d13 100644
--- a/apps/suimpp/app/components/CodeBlocks.tsx
+++ b/apps/suimpp/app/components/CodeBlocks.tsx
@@ -66,13 +66,14 @@ const res = await mpp.fetch(
);`;
const SERVER_CODE = `import { Mppx } from 'mppx/nextjs';
-import { sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
const mpp = Mppx.create({
realm: 'api.example.com',
methods: [sui({
currency: SUI_USDC_TYPE,
recipient: '0x...',
+ store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
});
diff --git a/apps/suimpp/app/docs/DocsContent.tsx b/apps/suimpp/app/docs/DocsContent.tsx
index fcb3b7d..3498f73 100644
--- a/apps/suimpp/app/docs/DocsContent.tsx
+++ b/apps/suimpp/app/docs/DocsContent.tsx
@@ -47,12 +47,13 @@ const res = await mpp.fetch('https://api.example.com/v1/generate', {
│
│ + payment credential │── verify TX on-chain via gRPC
- │ (Sui tx digest) │
+ │ (digest + signature) │
│<── 200 OK + data ────────│
```
@@ -120,11 +126,12 @@ No facilitator. No intermediary. The server verifies the Sui transaction directl
Creates a Sui payment method for the server.
```typescript
-import { sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
const method = sui({
currency: SUI_USDC, // Sui coin type (e.g. USDC)
recipient: '0xYOUR_ADDR', // Where payments are sent
+ store: new InMemoryDigestStore(), // Required. Use Redis/DB in production.
decimals: 6, // Optional: currency decimals (default: 6)
rpcUrl: '...', // Optional: custom gRPC endpoint
network: 'mainnet', // Optional: 'mainnet' | 'testnet' | 'devnet'
@@ -136,6 +143,8 @@ Verification checks:
- Transaction succeeded on-chain
- Payment sent to correct recipient (address-normalized comparison)
- Amount >= requested (BigInt precision, no floating-point)
+- Payment proof signature matches the transaction sender
+- Digest has not been used before according to the required `store`
## Client API
diff --git a/packages/mpp/src/server.ts b/packages/mpp/src/server.ts
index 0201646..1209052 100644
--- a/packages/mpp/src/server.ts
+++ b/packages/mpp/src/server.ts
@@ -3,12 +3,12 @@ import { getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
import { normalizeSuiAddress } from '@mysten/sui/utils';
import { verifyPersonalMessageSignature } from '@mysten/sui/verify';
import { Method, Receipt } from 'mppx';
-import { InMemoryDigestStore } from './in-memory-digest-store.js';
import { suiCharge } from './method.js';
import { createSuiPaymentProofBytes } from './proof.js';
import { parseAmountToRaw, withRetry } from './utils.js';
export { suiCharge } from './method.js';
+export { InMemoryDigestStore } from './in-memory-digest-store.js';
export { SUI_USDC_TYPE } from './constants.js';
export interface DigestStore {
@@ -32,46 +32,33 @@ export interface SuiServerOptions {
decimals?: number;
rpcUrl?: string;
network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
- /** Digest store for replay protection. Required in production. Falls back to in-memory in dev. */
- store?: DigestStore;
+ /** Digest store for replay protection. Use a shared durable store in production. */
+ store: DigestStore;
/** Called after successful on-chain verification with payment data. */
onPayment?: (report: PaymentReport) => void;
}
-let _defaultStore: DigestStore | undefined;
-
function resolveStore(options: SuiServerOptions): DigestStore {
- if (options.store) return options.store;
-
- if (
- typeof process !== 'undefined' &&
- process.env?.NODE_ENV === 'production'
- ) {
+ if (!options.store) {
throw new Error(
- '[suimpp] DigestStore is required in production. ' +
+ '[suimpp] DigestStore is required. ' +
'Provide a Redis or DB-backed store via SuiServerOptions.store. ' +
- 'The default in-memory store is single-instance only and unsafe for multi-instance deployments.',
+ 'Use InMemoryDigestStore explicitly only for local development or tests.',
);
}
-
- if (!_defaultStore) {
- _defaultStore = new InMemoryDigestStore();
- console.warn(
- '[suimpp] No DigestStore provided. Using in-memory store. ' +
- 'This is NOT safe for production or multi-instance deployments.',
- );
+ if (!options.store.has || typeof options.store.has !== 'function') {
+ throw new Error('[suimpp] DigestStore must implement has method');
+ }
+ if (!options.store.set || typeof options.store.set !== 'function') {
+ throw new Error('[suimpp] DigestStore must implement set method');
}
- return _defaultStore;
+ return options.store;
}
export function sui(options: SuiServerOptions) {
const network = options.network ?? 'mainnet';
const decimals = options.decimals ?? 6;
- const baseUrl =
- options.rpcUrl ??
- (network === 'localnet'
- ? 'http://127.0.0.1:9000'
- : getJsonRpcFullnodeUrl(network));
+ const baseUrl = options.rpcUrl ?? getJsonRpcFullnodeUrl(network);
const client = new SuiGrpcClient({
baseUrl,
network,
diff --git a/packages/mpp/test/unit/server.test.ts b/packages/mpp/test/unit/server.test.ts
index e553f79..23a4290 100644
--- a/packages/mpp/test/unit/server.test.ts
+++ b/packages/mpp/test/unit/server.test.ts
@@ -1,5 +1,5 @@
import type { Challenge, Credential, Method } from 'mppx';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { InMemoryDigestStore } from '../../src/in-memory-digest-store.js';
import type { suiCharge } from '../../src/method.js';
import {
@@ -113,7 +113,6 @@ vi.mock('@mysten/sui/verify', () => ({
describe('server verify', () => {
let suiFn: typeof createSuiServer;
- const originalEnv = process.env.NODE_ENV;
beforeEach(async () => {
vi.clearAllMocks();
@@ -124,10 +123,6 @@ describe('server verify', () => {
suiFn = mod.sui;
});
- afterEach(() => {
- process.env.NODE_ENV = originalEnv;
- });
-
it('accepts valid payment with correct amount', async () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
@@ -276,7 +271,6 @@ describe('server verify', () => {
describe('digest replay protection', () => {
let suiFn: typeof createSuiServer;
- const originalEnv = process.env.NODE_ENV;
beforeEach(async () => {
vi.clearAllMocks();
@@ -287,10 +281,6 @@ describe('digest replay protection', () => {
suiFn = mod.sui;
});
- afterEach(() => {
- process.env.NODE_ENV = originalEnv;
- });
-
it('accepts a valid digest on first use', async () => {
const store = new InMemoryDigestStore();
mockGetTransaction.mockResolvedValue(buildMockTx());
@@ -376,11 +366,11 @@ describe('digest replay protection', () => {
expect(result.status).toBe('success');
});
- it('throws on missing store in production', () => {
- process.env.NODE_ENV = 'production';
+ it('throws on missing store', () => {
expect(() =>
+ // @ts-expect-error Runtime guard protects JavaScript callers too.
suiFn({ currency: SUI_USDC_TYPE, recipient: RECIPIENT }),
- ).toThrow('DigestStore is required in production');
+ ).toThrow('DigestStore is required');
});
it('marks digest before returning receipt (store.set failure = no free call)', async () => {
From 9ee77d7617e98f0e4d3e9cd43c30a9f54be95a53 Mon Sep 17 00:00:00 2001
From: Manos Liolios
Date: Thu, 21 May 2026 14:02:37 +0300
Subject: [PATCH 6/6] Use address balances, free tier, and expose more than
USDC in the currency list
---
README.md | 15 ++--
apps/suimpp/app/components/CodeBlocks.tsx | 8 +-
apps/suimpp/app/docs/DocsContent.tsx | 21 ++---
packages/mpp/README.md | 36 ++++-----
packages/mpp/package.json | 2 +-
packages/mpp/src/client.ts | 49 ++++++++----
packages/mpp/src/constants.ts | 26 +++++++
packages/mpp/src/index.ts | 10 ++-
packages/mpp/src/server.ts | 33 +++++---
packages/mpp/test/e2e/globalSetup.ts | 5 +-
packages/mpp/test/e2e/sui-payment.test.ts | 25 +++++-
packages/mpp/test/unit/client.test.ts | 91 ++++++++++++++++++----
packages/mpp/test/unit/server.test.ts | 95 +++++++++++++++++++----
packages/mpp/test/unit/utils.test.ts | 17 ++++
pnpm-lock.yaml | 28 +++----
15 files changed, 351 insertions(+), 110 deletions(-)
diff --git a/README.md b/README.md
index 172999e..d761c95 100644
--- a/README.md
+++ b/README.md
@@ -26,12 +26,12 @@ npm install @suimpp/mpp mppx
```
```typescript
-import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
import { Mppx } from 'mppx';
const mppx = Mppx.create({
methods: [sui({
- currency: '0xdba...::usdc::USDC',
+ currency: USDC,
recipient: '0xYOUR_ADDRESS',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
@@ -45,16 +45,19 @@ export const GET = mppx.charge({ amount: '0.01' })(
### Make Payments (Client)
```typescript
-import { sui } from '@suimpp/mpp/client';
+import { USDC, sui } from '@suimpp/mpp/client';
import { Mppx } from 'mppx/client';
const mppx = Mppx.create({
- methods: [sui({ client, signer })],
+ methods: [sui({ client, signer, currency: USDC })],
});
const response = await mppx.fetch('https://api.example.com/resource');
```
+`@suimpp/mpp` exports `USDC`, `USDC_TESTNET`, and `SUI_DOLLAR`
+currency presets. The SDK handles gasless tier behavior automatically.
+
### Validate a Server
```bash
@@ -178,7 +181,7 @@ Payments are reported by the gateway (application layer), not by the library dir
**Implementation:**
```typescript
-import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
import type { PaymentReport } from '@suimpp/mpp/server';
// 1. Library emits on-chain data via callback
@@ -186,7 +189,7 @@ const pendingReports = new Map();
const mppx = Mppx.create({
methods: [sui({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: TREASURY_ADDRESS,
store: new InMemoryDigestStore(), // Use Redis/DB in production.
network: 'mainnet',
diff --git a/apps/suimpp/app/components/CodeBlocks.tsx b/apps/suimpp/app/components/CodeBlocks.tsx
index b0d8d13..806e8e4 100644
--- a/apps/suimpp/app/components/CodeBlocks.tsx
+++ b/apps/suimpp/app/components/CodeBlocks.tsx
@@ -53,11 +53,11 @@ function CodeBlock({
}
const CLIENT_CODE = `import { Mppx } from 'mppx/client';
-import { sui } from '@suimpp/mpp/client';
+import { USDC, sui } from '@suimpp/mpp/client';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const mpp = Mppx.create({
- methods: [sui({ client, signer })],
+ methods: [sui({ client, signer, currency: USDC })],
});
const res = await mpp.fetch(
@@ -66,12 +66,12 @@ const res = await mpp.fetch(
);`;
const SERVER_CODE = `import { Mppx } from 'mppx/nextjs';
-import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';
+import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
const mpp = Mppx.create({
realm: 'api.example.com',
methods: [sui({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: '0x...',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
diff --git a/apps/suimpp/app/docs/DocsContent.tsx b/apps/suimpp/app/docs/DocsContent.tsx
index 3498f73..1ba6313 100644
--- a/apps/suimpp/app/docs/DocsContent.tsx
+++ b/apps/suimpp/app/docs/DocsContent.tsx
@@ -29,10 +29,10 @@ export function DocsContent() {
@@ -227,12 +230,12 @@ const data = await response.json();`}
{ // Optional: custom execution (gas sponsor, etc.)
return myGasManager.execute(tx);
},
@@ -169,20 +166,23 @@ const method = sui({
|--------|------|----------|-------------|
| `client` | `ClientWithCoreApi` | Yes | Any Sui client implementing the core API |
| `signer` | `Signer` | Yes | Any `Signer` from `@mysten/sui/cryptography` — `Ed25519Keypair` works |
-| `decimals` | `number` | No | Decimal places for the currency (default: 6) |
+| `currency` | `Currency` | Yes | Single-currency metadata including coin type and decimals |
| `execute` | `(tx: Transaction) => Promise<{ digest: string }>` | No | Override transaction execution (e.g. gas sponsor/manager) |
-The client uses the [`coinWithBalance`](https://sdk.mystenlabs.com/sui/transaction-building/intents) intent to automatically resolve, merge, and split coins for the exact payment amount, then signs and broadcasts the transaction (or delegates to `execute` if provided).
+The client builds a `0x2::coin::send_funds` transaction for the exact payment amount, then signs and broadcasts it (or delegates to `execute` if provided).
## Constants
-### `SUI_USDC_TYPE`
+### Known currencies
-The Sui coin type for Circle-issued USDC on mainnet.
+The package exports common `Currency` presets and their raw coin type strings.
```typescript
-import { SUI_USDC_TYPE } from '@suimpp/mpp';
-// '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC'
+import { SUI_DOLLAR, USDC, USDC_TESTNET } from '@suimpp/mpp';
+
+USDC; // { type: SUI_USDC_TYPE, decimals: 6 }
+USDC_TESTNET; // { type: SUI_USDC_TESTNET_TYPE, decimals: 6 }
+SUI_DOLLAR; // { type: SUI_DOLLAR_TYPE, decimals: 6 }
```
## Utilities
@@ -210,7 +210,7 @@ MPP is chain-agnostic. We chose Sui because agent payments need:
## Testing
```bash
-pnpm --filter @suimpp/mpp test # 13 tests
+pnpm --filter @suimpp/mpp test # 29 tests
pnpm --filter @suimpp/mpp typecheck
```
diff --git a/packages/mpp/package.json b/packages/mpp/package.json
index 6c015bb..4a78fae 100644
--- a/packages/mpp/package.json
+++ b/packages/mpp/package.json
@@ -56,7 +56,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
- "@mysten/sui": "^2",
+ "@mysten/sui": "^2.17.0",
"mppx": "^0.4.9",
"zod": "^4.3.6"
},
diff --git a/packages/mpp/src/client.ts b/packages/mpp/src/client.ts
index 1e5aae4..0e52366 100644
--- a/packages/mpp/src/client.ts
+++ b/packages/mpp/src/client.ts
@@ -1,19 +1,27 @@
import type { ClientWithCoreApi } from '@mysten/sui/client';
import type { Signer } from '@mysten/sui/cryptography';
-import { Transaction, coinWithBalance } from '@mysten/sui/transactions';
+import { Transaction } from '@mysten/sui/transactions';
import { Credential, Method } from 'mppx';
+import type { Currency } from './constants.js';
import { suiCharge } from './method.js';
import { createSuiPaymentProofBytes } from './proof.js';
import { parseAmountToRaw } from './utils.js';
export { suiCharge } from './method.js';
-export { SUI_USDC_TYPE } from './constants.js';
+export {
+ SUI_DOLLAR,
+ SUI_DOLLAR_TYPE,
+ SUI_USDC_TESTNET_TYPE,
+ SUI_USDC_TYPE,
+ USDC,
+ USDC_TESTNET,
+} from './constants.js';
+export type { Currency } from './constants.js';
export interface SuiChargeOptions {
client: ClientWithCoreApi;
signer: Signer;
- /** Number of decimal places for the currency (default: 6, e.g. USDC). */
- decimals?: number;
+ currency: Currency;
/** Override transaction execution (e.g. to route through a gas manager). */
execute?: (tx: Transaction) => Promise<{ digest: string }>;
}
@@ -21,31 +29,42 @@ export interface SuiChargeOptions {
type TransactionResult = { digest: string };
export function sui(options: SuiChargeOptions) {
- const decimals = options.decimals ?? 6;
+ if (!options.currency) {
+ throw new Error('[suimpp] Currency is required');
+ }
return Method.toClient(suiCharge, {
async createCredential({ challenge }) {
const { amount, currency, recipient } = challenge.request;
- const amountRaw = parseAmountToRaw(amount, decimals);
+ if (currency !== options.currency.type) {
+ throw new Error(`Unsupported currency: ${currency}`);
+ }
+ const amountRaw = parseAmountToRaw(amount, options.currency.decimals);
const tx = new Transaction();
tx.setSender(options.signer.toSuiAddress());
- const payment = coinWithBalance({ balance: amountRaw, type: currency });
- tx.transferObjects([payment], recipient);
+ // `send_funds` so that the recipient receives the balance in ABs (not coins)
+ tx.moveCall({
+ target: '0x2::balance::send_funds',
+ arguments: [
+ tx.balance({ type: options.currency.type, balance: amountRaw }),
+ tx.pure.address(recipient),
+ ],
+ typeArguments: [options.currency.type],
+ });
let result: TransactionResult;
try {
if (options.execute) {
result = await options.execute(tx);
} else {
- const built = await tx.build({ client: options.client });
- const { signature } = await options.signer.signTransaction(built);
- const execResult = await options.client.core.executeTransaction({
- transaction: built,
- signatures: [signature],
- include: { effects: true },
- });
+ const execResult =
+ await options.client.core.signAndExecuteTransaction({
+ transaction: await tx.build({ client: options.client }),
+ signer: options.signer,
+ include: { effects: true },
+ });
if (execResult.FailedTransaction) {
throw new Error(
execResult.FailedTransaction.status.error?.message ??
diff --git a/packages/mpp/src/constants.ts b/packages/mpp/src/constants.ts
index 1d59a66..f56609d 100644
--- a/packages/mpp/src/constants.ts
+++ b/packages/mpp/src/constants.ts
@@ -1,2 +1,28 @@
+export interface Currency {
+ type: string;
+ decimals: number;
+}
+
export const SUI_USDC_TYPE =
'0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC';
+
+export const SUI_USDC_TESTNET_TYPE =
+ '0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC';
+
+export const SUI_DOLLAR_TYPE =
+ '0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI';
+
+export const USDC = {
+ type: SUI_USDC_TYPE,
+ decimals: 6,
+} as const satisfies Currency;
+
+export const USDC_TESTNET = {
+ type: SUI_USDC_TESTNET_TYPE,
+ decimals: 6,
+} as const satisfies Currency;
+
+export const SUI_DOLLAR = {
+ type: SUI_DOLLAR_TYPE,
+ decimals: 6,
+} as const satisfies Currency;
diff --git a/packages/mpp/src/index.ts b/packages/mpp/src/index.ts
index ae41860..257433c 100644
--- a/packages/mpp/src/index.ts
+++ b/packages/mpp/src/index.ts
@@ -5,4 +5,12 @@ export { sui as suiServer } from './server.js';
export type { SuiServerOptions, PaymentReport, DigestStore } from './server.js';
export { InMemoryDigestStore } from './in-memory-digest-store.js';
export { parseAmountToRaw, withRetry } from './utils.js';
-export { SUI_USDC_TYPE } from './constants.js';
+export {
+ SUI_DOLLAR,
+ SUI_DOLLAR_TYPE,
+ SUI_USDC_TESTNET_TYPE,
+ SUI_USDC_TYPE,
+ USDC,
+ USDC_TESTNET,
+} from './constants.js';
+export type { Currency } from './constants.js';
diff --git a/packages/mpp/src/server.ts b/packages/mpp/src/server.ts
index 1209052..311e94e 100644
--- a/packages/mpp/src/server.ts
+++ b/packages/mpp/src/server.ts
@@ -3,13 +3,22 @@ import { getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
import { normalizeSuiAddress } from '@mysten/sui/utils';
import { verifyPersonalMessageSignature } from '@mysten/sui/verify';
import { Method, Receipt } from 'mppx';
+import type { Currency } from './constants.js';
import { suiCharge } from './method.js';
import { createSuiPaymentProofBytes } from './proof.js';
import { parseAmountToRaw, withRetry } from './utils.js';
export { suiCharge } from './method.js';
export { InMemoryDigestStore } from './in-memory-digest-store.js';
-export { SUI_USDC_TYPE } from './constants.js';
+export {
+ SUI_DOLLAR,
+ SUI_DOLLAR_TYPE,
+ SUI_USDC_TESTNET_TYPE,
+ SUI_USDC_TYPE,
+ USDC,
+ USDC_TESTNET,
+} from './constants.js';
+export type { Currency } from './constants.js';
export interface DigestStore {
has(digest: string): Promise;
@@ -26,10 +35,8 @@ export interface PaymentReport {
}
export interface SuiServerOptions {
- currency: string;
+ currency: Currency;
recipient: string;
- /** Number of decimal places for the currency (default: 6, e.g. USDC). */
- decimals?: number;
rpcUrl?: string;
network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
/** Digest store for replay protection. Use a shared durable store in production. */
@@ -56,8 +63,11 @@ function resolveStore(options: SuiServerOptions): DigestStore {
}
export function sui(options: SuiServerOptions) {
+ if (!options.currency) {
+ throw new Error('[suimpp] Currency is required');
+ }
+
const network = options.network ?? 'mainnet';
- const decimals = options.decimals ?? 6;
const baseUrl = options.rpcUrl ?? getJsonRpcFullnodeUrl(network);
const client = new SuiGrpcClient({
baseUrl,
@@ -69,12 +79,17 @@ export function sui(options: SuiServerOptions) {
return Method.toServer(suiCharge, {
defaults: {
- currency: options.currency,
+ currency: options.currency.type,
recipient: options.recipient,
},
async verify({ credential }) {
const digest = credential.payload.digest;
+ if (credential.challenge.request.currency !== options.currency.type) {
+ throw new Error(
+ `Unsupported currency: ${credential.challenge.request.currency}`,
+ );
+ }
const alreadyUsed = await digestStore.has(digest);
if (alreadyUsed) {
@@ -101,7 +116,7 @@ export function sui(options: SuiServerOptions) {
const payment = resolved.balanceChanges.find(
(bc) =>
- bc.coinType === options.currency &&
+ bc.coinType === options.currency.type &&
normalizeSuiAddress(bc.address) === normalizedRecipient &&
BigInt(bc.amount) > 0n,
);
@@ -113,7 +128,7 @@ export function sui(options: SuiServerOptions) {
const transferredRaw = BigInt(payment.amount);
const requestedRaw = parseAmountToRaw(
credential.challenge.request.amount,
- decimals,
+ options.currency.decimals,
);
if (transferredRaw < requestedRaw) {
throw new Error(
@@ -157,7 +172,7 @@ export function sui(options: SuiServerOptions) {
sender,
recipient: options.recipient,
amount: credential.challenge.request.amount,
- currency: options.currency,
+ currency: options.currency.type,
network,
};
diff --git a/packages/mpp/test/e2e/globalSetup.ts b/packages/mpp/test/e2e/globalSetup.ts
index bf334bf..7b329e2 100644
--- a/packages/mpp/test/e2e/globalSetup.ts
+++ b/packages/mpp/test/e2e/globalSetup.ts
@@ -9,11 +9,12 @@ declare module 'vitest' {
}
}
+const SUI_TOOLS_BASE_TAG = 'd0b6a5d9663a5e3e4cfe4a04fdc92130f8ed502c';
const SUI_TOOLS_TAG =
process.env.SUI_TOOLS_TAG ||
(process.arch === 'arm64'
- ? '951cae315d8b252131836a331bcc16b89eb340d6-arm64'
- : '951cae315d8b252131836a331bcc16b89eb340d6');
+ ? `${SUI_TOOLS_BASE_TAG}-arm64`
+ : SUI_TOOLS_BASE_TAG);
export default async function setup(project: TestProject) {
const containers: StartedTestContainer[] = [];
diff --git a/packages/mpp/test/e2e/sui-payment.test.ts b/packages/mpp/test/e2e/sui-payment.test.ts
index 14e7252..2022f19 100644
--- a/packages/mpp/test/e2e/sui-payment.test.ts
+++ b/packages/mpp/test/e2e/sui-payment.test.ts
@@ -8,6 +8,7 @@ import { suiCharge } from '../../src/method.js';
import { createSuiPaymentProofBytes } from '../../src/proof.js';
import {
type PaymentReport,
+ SUI_USDC_TYPE,
sui as createSuiServer,
} from '../../src/server.js';
import { fundAddress, getClient, getFullnodeUrl } from './setup.js';
@@ -55,9 +56,8 @@ describe('Sui localnet e2e payment', () => {
function buildServerMethod(onPayment?: (report: PaymentReport) => void) {
return createSuiServer({
- currency: SUI_TYPE_ARG,
+ currency: { type: SUI_TYPE_ARG, decimals: SUI_DECIMALS },
recipient,
- decimals: SUI_DECIMALS,
rpcUrl: getFullnodeUrl(),
network: 'localnet',
store: new InMemoryDigestStore(),
@@ -69,7 +69,7 @@ describe('Sui localnet e2e payment', () => {
const clientMethod = createSuiClient({
client: localnetClient,
signer: payerKeypair,
- decimals: SUI_DECIMALS,
+ currency: { type: SUI_TYPE_ARG, decimals: SUI_DECIMALS },
});
const authorization = await clientMethod.createCredential({ challenge });
const credential = Credential.deserialize<{ digest: string }>(
@@ -143,4 +143,23 @@ describe('Sui localnet e2e payment', () => {
}),
).rejects.toThrow('Payment proof signer does not match transaction sender');
});
+
+ it('rejects a payment made with the wrong currency', async () => {
+ const challenge = buildChallenge('sui-localnet-e2e-wrong-currency');
+ const credential = await createCredential(challenge);
+ const serverMethod = createSuiServer({
+ currency: { type: SUI_USDC_TYPE, decimals: 6 },
+ recipient,
+ rpcUrl: getFullnodeUrl(),
+ network: 'localnet',
+ store: new InMemoryDigestStore(),
+ });
+
+ await expect(
+ serverMethod.verify({
+ credential,
+ request: challenge.request,
+ }),
+ ).rejects.toThrow(`Unsupported currency: ${SUI_TYPE_ARG}`);
+ });
});
diff --git a/packages/mpp/test/unit/client.test.ts b/packages/mpp/test/unit/client.test.ts
index 20c4c8d..501378c 100644
--- a/packages/mpp/test/unit/client.test.ts
+++ b/packages/mpp/test/unit/client.test.ts
@@ -6,21 +6,33 @@ import type {
sui as createSuiClient,
} from '../../src/client.js';
+const { mockBalance, mockBuild, mockMoveCall, mockPureAddress } = vi.hoisted(
+ () => ({
+ mockBalance: vi.fn(() => 'balance_result'),
+ mockBuild: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
+ mockMoveCall: vi.fn(),
+ mockPureAddress: vi.fn(() => 'recipient_address_result'),
+ }),
+);
+
vi.mock('@mysten/sui/transactions', () => {
- const coinWithBalance = vi.fn(() => 'coin_with_balance_result');
const Transaction = vi.fn().mockImplementation(() => ({
setSender: vi.fn(),
- transferObjects: vi.fn(),
- build: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
+ balance: mockBalance,
+ pure: {
+ address: mockPureAddress,
+ },
+ moveCall: mockMoveCall,
+ build: mockBuild,
}));
- return { Transaction, coinWithBalance };
+ return { Transaction };
});
-const mockExecuteTransaction = vi.fn();
+const mockSignAndExecuteTransaction = vi.fn();
const mockClient = {
core: {
- executeTransaction: mockExecuteTransaction,
+ signAndExecuteTransaction: mockSignAndExecuteTransaction,
},
};
@@ -47,6 +59,7 @@ type SuiChargeChallenge = Challenge.Challenge<
'charge',
'sui'
>;
+const USDC = { type: '0x::usdc::USDC', decimals: 6 };
function buildChallenge(amount = '0.01'): SuiChargeChallenge {
return {
@@ -71,14 +84,15 @@ describe('client createCredential', () => {
suiFn = mod.sui;
});
- it('builds transaction with coinWithBalance and executes it', async () => {
- mockExecuteTransaction.mockResolvedValue({
+ it('builds transaction with send_funds and executes it', async () => {
+ mockSignAndExecuteTransaction.mockResolvedValue({
Transaction: { digest: '0xtxdigest' },
});
const clientMethod = suiFn({
client: typedMockClient,
signer: typedMockSigner,
+ currency: USDC,
});
const challenge = buildChallenge();
@@ -89,13 +103,18 @@ describe('client createCredential', () => {
signature: string;
}>(authorization);
- expect(mockSigner.signTransaction).toHaveBeenCalled();
expect(mockSigner.signPersonalMessage).toHaveBeenCalledWith(
expect.any(Uint8Array),
);
- expect(mockExecuteTransaction).toHaveBeenCalledWith({
+ expect(mockMoveCall).toHaveBeenCalledWith({
+ target: '0x2::balance::send_funds',
+ arguments: ['balance_result', 'recipient_address_result'],
+ typeArguments: ['0x::usdc::USDC'],
+ });
+ expect(mockBuild).toHaveBeenCalledWith({ client: typedMockClient });
+ expect(mockSignAndExecuteTransaction).toHaveBeenCalledWith({
transaction: expect.any(Uint8Array),
- signatures: ['mock_sig'],
+ signer: typedMockSigner,
include: { effects: true },
});
expect(credential.payload).toEqual({
@@ -105,13 +124,14 @@ describe('client createCredential', () => {
});
it('throws when transaction execution fails', async () => {
- mockExecuteTransaction.mockResolvedValue({
+ mockSignAndExecuteTransaction.mockResolvedValue({
FailedTransaction: { status: { error: 'out of gas' } },
});
const clientMethod = suiFn({
client: typedMockClient,
signer: typedMockSigner,
+ currency: USDC,
});
const challenge = buildChallenge();
@@ -121,6 +141,50 @@ describe('client createCredential', () => {
);
});
+ it('uses decimals from configured Currency', async () => {
+ mockSignAndExecuteTransaction.mockResolvedValue({
+ Transaction: { digest: '0xtxdigest' },
+ });
+
+ const clientMethod = suiFn({
+ client: typedMockClient,
+ signer: typedMockSigner,
+ currency: { type: '0x::usdc::USDC', decimals: 2 },
+ });
+
+ await clientMethod.createCredential({
+ challenge: buildChallenge('1.23'),
+ });
+
+ expect(mockBalance).toHaveBeenCalledWith({
+ balance: 123n,
+ type: '0x::usdc::USDC',
+ });
+ });
+
+ it('requires configured currency metadata', async () => {
+ mockSignAndExecuteTransaction.mockResolvedValue({
+ Transaction: { digest: '0xtxdigest' },
+ });
+
+ expect(() =>
+ // @ts-expect-error Runtime guard protects JavaScript callers too.
+ suiFn({ client: typedMockClient, signer: typedMockSigner }),
+ ).toThrow('Currency is required');
+ });
+
+ it('rejects an unsupported configured currency', async () => {
+ const clientMethod = suiFn({
+ client: typedMockClient,
+ signer: typedMockSigner,
+ currency: { type: '0x::eurc::EURC', decimals: 6 },
+ });
+
+ await expect(
+ clientMethod.createCredential({ challenge: buildChallenge() }),
+ ).rejects.toThrow('Unsupported currency: 0x::usdc::USDC');
+ });
+
it('uses custom execute when provided', async () => {
const customExecute = vi
.fn()
@@ -129,6 +193,7 @@ describe('client createCredential', () => {
const clientMethod = suiFn({
client: typedMockClient,
signer: typedMockSigner,
+ currency: USDC,
execute: customExecute,
});
@@ -144,7 +209,7 @@ describe('client createCredential', () => {
expect(mockSigner.signPersonalMessage).toHaveBeenCalledWith(
expect.any(Uint8Array),
);
- expect(mockExecuteTransaction).not.toHaveBeenCalled();
+ expect(mockSignAndExecuteTransaction).not.toHaveBeenCalled();
expect(credential.payload).toMatchObject({
digest: '0xcustom',
signature: 'proof_sig',
diff --git a/packages/mpp/test/unit/server.test.ts b/packages/mpp/test/unit/server.test.ts
index 23a4290..f7338d1 100644
--- a/packages/mpp/test/unit/server.test.ts
+++ b/packages/mpp/test/unit/server.test.ts
@@ -5,11 +5,13 @@ import type { suiCharge } from '../../src/method.js';
import {
type DigestStore,
SUI_USDC_TYPE,
+ USDC,
type sui as createSuiServer,
} from '../../src/server.js';
const RECIPIENT = '0xrecipient_address';
const SENDER = '0xsender_address';
+const ALT_CURRENCY = '0x::eurc::EURC';
type SuiServerMethod = Method.Server;
type SuiChargeRequest = {
@@ -61,6 +63,7 @@ function buildCredential(
digest = '0xdigest123',
amount = '0.01',
signature = 'proof_sig',
+ currency = SUI_USDC_TYPE,
): SuiCredential {
return {
payload: { digest, signature },
@@ -71,7 +74,7 @@ function buildCredential(
realm: 'test',
request: {
amount,
- currency: SUI_USDC_TYPE,
+ currency,
recipient: RECIPIENT,
},
},
@@ -127,7 +130,7 @@ describe('server verify', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -142,7 +145,7 @@ describe('server verify', () => {
mockGetTransaction.mockResolvedValue(buildMockTx({ success: false }));
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -158,7 +161,7 @@ describe('server verify', () => {
);
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -172,7 +175,7 @@ describe('server verify', () => {
mockGetTransaction.mockResolvedValue(buildMockTx({ amount: '5000' }));
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -180,6 +183,68 @@ describe('server verify', () => {
await expect(verifyPayment(serverMethod)).rejects.toThrow('Transferred');
});
+ it('accepts a configured non-default currency', async () => {
+ const onPayment = vi.fn();
+ mockGetTransaction.mockResolvedValue(
+ buildMockTx({ coinType: ALT_CURRENCY, amount: '123' }),
+ );
+
+ const serverMethod = suiFn({
+ currency: { type: ALT_CURRENCY, decimals: 2 },
+ recipient: RECIPIENT,
+ store: new InMemoryDigestStore(),
+ onPayment,
+ });
+
+ const credential = buildCredential(
+ '0xdigest123',
+ '1.23',
+ 'proof_sig',
+ ALT_CURRENCY,
+ );
+ const result = await verifyPayment(serverMethod, credential);
+
+ expect(result.status).toBe('success');
+ expect(onPayment).toHaveBeenCalledWith(
+ expect.objectContaining({ currency: ALT_CURRENCY, amount: '1.23' }),
+ );
+ });
+
+ it('rejects an unconfigured challenge currency', async () => {
+ const serverMethod = suiFn({
+ currency: USDC,
+ recipient: RECIPIENT,
+ store: new InMemoryDigestStore(),
+ });
+
+ await expect(
+ verifyPayment(
+ serverMethod,
+ buildCredential('0xdigest123', '0.01', 'proof_sig', ALT_CURRENCY),
+ ),
+ ).rejects.toThrow(`Unsupported currency: ${ALT_CURRENCY}`);
+ expect(mockGetTransaction).not.toHaveBeenCalled();
+ });
+
+ it('uses configured currency decimals for amount checks', async () => {
+ mockGetTransaction.mockResolvedValue(
+ buildMockTx({ coinType: ALT_CURRENCY, amount: '122' }),
+ );
+
+ const serverMethod = suiFn({
+ currency: { type: ALT_CURRENCY, decimals: 2 },
+ recipient: RECIPIENT,
+ store: new InMemoryDigestStore(),
+ });
+
+ await expect(
+ verifyPayment(
+ serverMethod,
+ buildCredential('0xdigest123', '1.23', 'proof_sig', ALT_CURRENCY),
+ ),
+ ).rejects.toThrow('Transferred 122 < requested 123');
+ });
+
it('rejects when no balance changes', async () => {
mockGetTransaction.mockResolvedValue({
Transaction: {
@@ -190,7 +255,7 @@ describe('server verify', () => {
});
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -211,7 +276,7 @@ describe('server verify', () => {
set: vi.fn(),
} satisfies DigestStore;
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
@@ -233,7 +298,7 @@ describe('server verify', () => {
set: vi.fn(),
} satisfies DigestStore;
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
@@ -258,7 +323,7 @@ describe('server verify', () => {
});
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store: new InMemoryDigestStore(),
});
@@ -286,7 +351,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
@@ -302,7 +367,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
@@ -318,7 +383,7 @@ describe('digest replay protection', () => {
const store = new InMemoryDigestStore();
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
@@ -353,7 +418,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
@@ -369,7 +434,7 @@ describe('digest replay protection', () => {
it('throws on missing store', () => {
expect(() =>
// @ts-expect-error Runtime guard protects JavaScript callers too.
- suiFn({ currency: SUI_USDC_TYPE, recipient: RECIPIENT }),
+ suiFn({ currency: USDC, recipient: RECIPIENT }),
).toThrow('DigestStore is required');
});
@@ -381,7 +446,7 @@ describe('digest replay protection', () => {
mockGetTransaction.mockResolvedValue(buildMockTx());
const serverMethod = suiFn({
- currency: SUI_USDC_TYPE,
+ currency: USDC,
recipient: RECIPIENT,
store,
});
diff --git a/packages/mpp/test/unit/utils.test.ts b/packages/mpp/test/unit/utils.test.ts
index 5cb4bf9..b3977e6 100644
--- a/packages/mpp/test/unit/utils.test.ts
+++ b/packages/mpp/test/unit/utils.test.ts
@@ -1,4 +1,10 @@
import { describe, expect, it } from 'vitest';
+import {
+ SUI_DOLLAR,
+ SUI_DOLLAR_TYPE,
+ USDC,
+ USDC_TESTNET,
+} from '../../src/constants.js';
import { parseAmountToRaw } from '../../src/utils.js';
describe('parseAmountToRaw', () => {
@@ -22,3 +28,14 @@ describe('parseAmountToRaw', () => {
expect(parseAmountToRaw('0.0000001', 6)).toBe(0n);
});
});
+
+describe('known currencies', () => {
+ it('exports individual Currency presets', () => {
+ expect(USDC).toMatchObject({ decimals: 6 });
+ expect(USDC_TESTNET).toMatchObject({ decimals: 6 });
+ expect(SUI_DOLLAR).toEqual({
+ type: SUI_DOLLAR_TYPE,
+ decimals: 6,
+ });
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ed357b2..761a9a4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -80,8 +80,8 @@ importers:
packages/mpp:
dependencies:
'@mysten/sui':
- specifier: ^2
- version: 2.13.0(typescript@5.9.3)
+ specifier: ^2.17.0
+ version: 2.17.0(typescript@5.9.3)
mppx:
specifier: ^0.4.9
version: 0.4.12(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(express@5.2.1)(hono@4.12.9)(openapi-types@12.1.3)(typescript@5.9.3)(viem@2.47.6(typescript@5.9.3)(zod@4.3.6))
@@ -640,15 +640,15 @@ packages:
'@cfworker/json-schema':
optional: true
- '@mysten/bcs@2.0.3':
- resolution: {integrity: sha512-dwcaL4HNAsEGpU3hKUAsXgCZp9l6++e2A3THpzoYZ8e7bsy4XH1V0dXD5dIzgNcVZiZfb6ZnDMG+gdF6+1WOQA==}
+ '@mysten/bcs@2.0.5':
+ resolution: {integrity: sha512-Dop9Xq36DPLlsmIDQZvUg4uJeBBxIGirgp2OXGaEff+mtLKFBoW2HnE3aSTSSpiMQH9XRhjwtiM16zFJhFqz0Q==}
- '@mysten/sui@2.13.0':
- resolution: {integrity: sha512-GG/XBUqrplPM1jEFMCiv7xA15qo5laaVmCVcdsZsJhzZInDJhxgV7IxFvkgrzA8bwIMhPMAA+m0CTXxznaS3eA==}
+ '@mysten/sui@2.17.0':
+ resolution: {integrity: sha512-TrS1BCPm4V6rC69MBejmcqnKIyJ5t1iksax4XA9jWarZxAAp9LMmdxEBebejyDT/yAJxYIf3fNm5WBkHE2qnIw==}
engines: {node: '>=22'}
- '@mysten/utils@0.3.1':
- resolution: {integrity: sha512-36KhxG284uhDdSnlkyNaS6fzKTX9FpP2WQWOwUKIRsqQFFIm2ooCf2TP1IuqrtMpkairwpiWkAS0eg7cpemVzg==}
+ '@mysten/utils@0.3.3':
+ resolution: {integrity: sha512-gVHn5toh24eXXLEyBknwwM2F/tbDgqPX0yj77KtHjuoYUallcQ9vSwGfGsAy39IcTB259Yprt/ZOEwdup+zrpA==}
'@next/env@15.5.14':
resolution: {integrity: sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==}
@@ -3294,16 +3294,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@mysten/bcs@2.0.3':
+ '@mysten/bcs@2.0.5':
dependencies:
- '@mysten/utils': 0.3.1
+ '@mysten/utils': 0.3.3
'@scure/base': 2.0.0
- '@mysten/sui@2.13.0(typescript@5.9.3)':
+ '@mysten/sui@2.17.0(typescript@5.9.3)':
dependencies:
'@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2)
- '@mysten/bcs': 2.0.3
- '@mysten/utils': 0.3.1
+ '@mysten/bcs': 2.0.5
+ '@mysten/utils': 0.3.3
'@noble/curves': 2.0.1
'@noble/hashes': 2.0.1
'@protobuf-ts/grpcweb-transport': 2.11.1
@@ -3321,7 +3321,7 @@ snapshots:
- '@gql.tada/vue-support'
- typescript
- '@mysten/utils@0.3.1':
+ '@mysten/utils@0.3.3':
dependencies:
'@scure/base': 2.0.0