Initial import
This commit is contained in:
16
packages/providers/AGENTS.md
Normal file
16
packages/providers/AGENTS.md
Normal 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.
|
||||
10
packages/providers/README.md
Normal file
10
packages/providers/README.md
Normal 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
|
||||
24
packages/providers/package.json
Normal file
24
packages/providers/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
0
packages/providers/src/.gitkeep
Normal file
0
packages/providers/src/.gitkeep
Normal file
48
packages/providers/src/email.ts
Normal file
48
packages/providers/src/email.ts
Normal 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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
4
packages/providers/src/index.ts
Normal file
4
packages/providers/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./email.js";
|
||||
export * from "./nano-banana.js";
|
||||
export * from "./payments.js";
|
||||
export * from "./telegram.js";
|
||||
147
packages/providers/src/nano-banana.ts
Normal file
147
packages/providers/src/nano-banana.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
55
packages/providers/src/payments.ts
Normal file
55
packages/providers/src/payments.ts
Normal 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),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
88
packages/providers/src/telegram.ts
Normal file
88
packages/providers/src/telegram.ts
Normal 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}.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
11
packages/providers/tsconfig.json
Normal file
11
packages/providers/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user