Initial import

This commit is contained in:
sirily
2026-03-10 14:03:52 +03:00
commit 6c0ca4e28b
102 changed files with 6598 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
# AGENTS.md
## Scope
Applies within `packages/providers`.
## Responsibilities
- image provider adapters
- payment processor adapter
- storage adapter
- email adapter
- Telegram transport adapter
## Rules
- Provider adapters classify errors but do not decide subscription or quota policy.
- Balance-fetch APIs for provider keys belong here.
- Distinguish proxy transport failures from provider API failures.

View File

@@ -0,0 +1,10 @@
# packages/providers
Planned external adapter package.
Expected ownership:
- `nano_banana` API adapter
- crypto payment processor adapter
- storage adapter
- email provider adapter
- Telegram transport adapter

View File

@@ -0,0 +1,24 @@
{
"name": "@nproxy/providers",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"check": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@nproxy/domain": "workspace:*"
}
}

View File

View File

@@ -0,0 +1,48 @@
export interface SendEmailInput {
to: string;
subject: string;
text: string;
}
export interface EmailTransport {
send(input: SendEmailInput): Promise<void>;
}
export function createEmailTransport(config: {
provider: string;
from: string;
apiKey: string;
}): EmailTransport {
if (config.provider === "example") {
return {
async send(input) {
console.log(
JSON.stringify({
service: "email",
provider: config.provider,
from: config.from,
to: input.to,
subject: input.subject,
text: input.text,
}),
);
},
};
}
return {
async send(input) {
console.log(
JSON.stringify({
service: "email",
provider: config.provider,
mode: "noop_fallback",
from: config.from,
to: input.to,
subject: input.subject,
text: input.text,
}),
);
},
};
}

View File

@@ -0,0 +1,4 @@
export * from "./email.js";
export * from "./nano-banana.js";
export * from "./payments.js";
export * from "./telegram.js";

View File

@@ -0,0 +1,147 @@
import type {
GenerationRequestRecord,
ProviderFailureKind,
} from "@nproxy/domain";
export interface ProviderExecutionKey {
id: string;
providerCode: string;
label: string;
apiKeyLastFour: string;
}
export interface ProviderExecutionRoute {
kind: "proxy" | "direct";
proxyBaseUrl?: string;
}
export interface GeneratedAssetPayload {
objectKey: string;
mimeType: string;
width?: number;
height?: number;
bytes?: number;
}
export interface SuccessfulGenerationExecution {
ok: true;
assets: GeneratedAssetPayload[];
}
export interface FailedGenerationExecution {
ok: false;
failureKind: ProviderFailureKind;
providerHttpStatus?: number;
providerErrorCode?: string;
providerErrorText?: string;
}
export type ProviderExecutionResult =
| SuccessfulGenerationExecution
| FailedGenerationExecution;
export interface NanoBananaAdapter {
executeGeneration(input: {
request: GenerationRequestRecord;
providerKey: ProviderExecutionKey;
route: ProviderExecutionRoute;
}): Promise<ProviderExecutionResult>;
}
export function createNanoBananaSimulatedAdapter(): NanoBananaAdapter {
return {
async executeGeneration({ request, providerKey, route }) {
const lowerPrompt = request.prompt.toLowerCase();
const simulatedFailure = matchSimulatedFailure(lowerPrompt, route.kind);
if (simulatedFailure) {
return simulatedFailure;
}
const assetCount = request.batchSize;
const safeKeySuffix = providerKey.apiKeyLastFour.replace(/[^a-z0-9]/gi, "").toLowerCase();
const assets = Array.from({ length: assetCount }, (_, index) => ({
objectKey: [
"generated",
request.userId,
request.id,
`${index + 1}-${safeKeySuffix || "key"}.png`,
].join("/"),
mimeType: "image/png",
width: 1024,
height: 1024,
bytes: 512_000,
}));
return {
ok: true,
assets,
};
},
};
}
function matchSimulatedFailure(
prompt: string,
routeKind: ProviderExecutionRoute["kind"],
): FailedGenerationExecution | null {
if (routeKind === "proxy" && prompt.includes("[fail:proxy_transport]")) {
return buildFailure(
"transport",
502,
"proxy_transport_error",
"Simulated proxy transport failure.",
);
}
if (prompt.includes("[fail:transport]")) {
return buildFailure("transport", 502, "transport_error", "Simulated transport failure.");
}
if (prompt.includes("[fail:timeout]")) {
return buildFailure("timeout", 504, "timeout", "Simulated upstream timeout.");
}
if (prompt.includes("[fail:provider_5xx]")) {
return buildFailure("provider_5xx", 503, "provider_5xx", "Simulated provider 5xx.");
}
if (prompt.includes("[fail:provider_4xx_user]")) {
return buildFailure(
"provider_4xx_user",
400,
"invalid_request",
"Simulated provider validation failure.",
);
}
if (prompt.includes("[fail:insufficient_funds]")) {
return buildFailure(
"insufficient_funds",
402,
"insufficient_funds",
"Simulated provider balance exhaustion.",
);
}
if (prompt.includes("[fail:unknown]")) {
return buildFailure("unknown", 500, "unknown_error", "Simulated unknown provider failure.");
}
return null;
}
function buildFailure(
failureKind: ProviderFailureKind,
providerHttpStatus: number,
providerErrorCode: string,
providerErrorText: string,
): FailedGenerationExecution {
return {
ok: false,
failureKind,
providerHttpStatus,
providerErrorCode,
providerErrorText,
};
}

View File

@@ -0,0 +1,55 @@
import { randomUUID } from "node:crypto";
export interface PaymentInvoiceDraft {
userId: string;
planCode: string;
amountUsd: number;
amountCrypto: number;
currency: string;
}
export interface CreatedProviderInvoice {
providerInvoiceId: string;
paymentAddress: string;
amountCrypto: number;
amountUsd: number;
currency: string;
expiresAt: Date;
}
export interface PaymentProviderAdapter {
createInvoice(input: PaymentInvoiceDraft): Promise<CreatedProviderInvoice>;
}
export function createPaymentProviderAdapter(config: {
provider: string;
apiKey: string;
}): PaymentProviderAdapter {
if (config.provider === "example_processor") {
return {
async createInvoice(input) {
return {
providerInvoiceId: `inv_${randomUUID()}`,
paymentAddress: `example_${input.currency.toLowerCase()}_${randomUUID().slice(0, 16)}`,
amountCrypto: input.amountCrypto,
amountUsd: input.amountUsd,
currency: input.currency,
expiresAt: new Date(Date.now() + 30 * 60 * 1000),
};
},
};
}
return {
async createInvoice(input) {
return {
providerInvoiceId: `noop_${randomUUID()}`,
paymentAddress: `noop_${input.currency.toLowerCase()}_${randomUUID().slice(0, 16)}`,
amountCrypto: input.amountCrypto,
amountUsd: input.amountUsd,
currency: input.currency,
expiresAt: new Date(Date.now() + 30 * 60 * 1000),
};
},
};
}

View File

@@ -0,0 +1,88 @@
export interface TelegramUpdateUser {
id: number;
username?: string;
first_name: string;
last_name?: string;
}
export interface TelegramUpdateMessage {
message_id: number;
text?: string;
from?: TelegramUpdateUser;
chat: {
id: number;
};
}
export interface TelegramUpdate {
update_id: number;
message?: TelegramUpdateMessage;
}
export interface TelegramBotTransport {
getUpdates(input: {
offset?: number;
timeoutSeconds: number;
}): Promise<TelegramUpdate[]>;
sendMessage(input: {
chatId: number;
text: string;
}): Promise<void>;
}
export function createTelegramBotApiTransport(botToken: string): TelegramBotTransport {
const baseUrl = `https://api.telegram.org/bot${botToken}`;
return {
async getUpdates({ offset, timeoutSeconds }) {
const response = await fetch(`${baseUrl}/getUpdates`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
timeout: timeoutSeconds,
...(offset !== undefined ? { offset } : {}),
}),
});
const payload = (await response.json()) as {
ok: boolean;
result?: TelegramUpdate[];
description?: string;
};
if (!response.ok || !payload.ok || !payload.result) {
throw new Error(
payload.description ?? `Telegram getUpdates failed with status ${response.status}.`,
);
}
return payload.result;
},
async sendMessage({ chatId, text }) {
const response = await fetch(`${baseUrl}/sendMessage`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
chat_id: chatId,
text,
}),
});
const payload = (await response.json()) as {
ok: boolean;
description?: string;
};
if (!response.ok || !payload.ok) {
throw new Error(
payload.description ?? `Telegram sendMessage failed with status ${response.status}.`,
);
}
},
};
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}