@agentforge-io/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/billing/billing-adapter.interface.d.ts +41 -0
- package/dist/adapters/billing/billing-adapter.interface.js +5 -0
- package/dist/adapters/billing/stripe/stripe.adapter.d.ts +30 -0
- package/dist/adapters/billing/stripe/stripe.adapter.js +122 -0
- package/dist/adapters/email/email-adapter.interface.d.ts +25 -0
- package/dist/adapters/email/email-adapter.interface.js +6 -0
- package/dist/adapters/email/noop.adapter.d.ts +10 -0
- package/dist/adapters/email/noop.adapter.js +15 -0
- package/dist/adapters/email/resend.adapter.d.ts +8 -0
- package/dist/adapters/email/resend.adapter.js +39 -0
- package/dist/adapters/job-queue/in-memory.d.ts +43 -0
- package/dist/adapters/job-queue/in-memory.js +154 -0
- package/dist/adapters/job-queue/job-queue.types.d.ts +76 -0
- package/dist/adapters/job-queue/job-queue.types.js +5 -0
- package/dist/adapters/prepared-stream/prepared-stream.types.d.ts +23 -0
- package/dist/adapters/prepared-stream/prepared-stream.types.js +5 -0
- package/dist/adapters/rate-limiter/in-memory.d.ts +19 -0
- package/dist/adapters/rate-limiter/in-memory.js +63 -0
- package/dist/adapters/rate-limiter/rate-limiter.types.d.ts +42 -0
- package/dist/adapters/rate-limiter/rate-limiter.types.js +5 -0
- package/dist/adapters/rate-limiter/redis.d.ts +31 -0
- package/dist/adapters/rate-limiter/redis.js +47 -0
- package/dist/adapters/upload/noop.adapter.d.ts +9 -0
- package/dist/adapters/upload/noop.adapter.js +14 -0
- package/dist/adapters/upload/s3.adapter.d.ts +38 -0
- package/dist/adapters/upload/s3.adapter.js +69 -0
- package/dist/adapters/upload/upload-adapter.interface.d.ts +37 -0
- package/dist/adapters/upload/upload-adapter.interface.js +15 -0
- package/dist/ai/index.d.ts +15 -0
- package/dist/ai/index.js +43 -0
- package/dist/billing/index.d.ts +12 -0
- package/dist/billing/index.js +28 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +8 -0
- package/dist/domain/agent.d.ts +59 -0
- package/dist/domain/agent.js +2 -0
- package/dist/domain/api-key.d.ts +28 -0
- package/dist/domain/api-key.js +2 -0
- package/dist/domain/auth-identity.d.ts +10 -0
- package/dist/domain/auth-identity.js +2 -0
- package/dist/domain/chat-token.d.ts +39 -0
- package/dist/domain/chat-token.js +2 -0
- package/dist/domain/connector-auth.d.ts +42 -0
- package/dist/domain/connector-auth.js +2 -0
- package/dist/domain/connector.d.ts +52 -0
- package/dist/domain/connector.js +2 -0
- package/dist/domain/conversation.d.ts +26 -0
- package/dist/domain/conversation.js +2 -0
- package/dist/domain/email-token.d.ts +11 -0
- package/dist/domain/email-token.js +2 -0
- package/dist/domain/external-user.d.ts +23 -0
- package/dist/domain/external-user.js +2 -0
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.js +24 -0
- package/dist/domain/mcp-server.d.ts +33 -0
- package/dist/domain/mcp-server.js +2 -0
- package/dist/domain/plan.d.ts +20 -0
- package/dist/domain/plan.js +2 -0
- package/dist/domain/platform-secret.d.ts +24 -0
- package/dist/domain/platform-secret.js +8 -0
- package/dist/domain/refresh-token.d.ts +15 -0
- package/dist/domain/refresh-token.js +2 -0
- package/dist/domain/subscription.d.ts +21 -0
- package/dist/domain/subscription.js +2 -0
- package/dist/domain/tenant.d.ts +21 -0
- package/dist/domain/tenant.js +2 -0
- package/dist/domain/usage-record.d.ts +15 -0
- package/dist/domain/usage-record.js +2 -0
- package/dist/domain/user.d.ts +43 -0
- package/dist/domain/user.js +2 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +59 -0
- package/dist/repositories/in-memory.d.ts +30 -0
- package/dist/repositories/in-memory.js +82 -0
- package/dist/repositories/index.d.ts +67 -0
- package/dist/repositories/index.js +16 -0
- package/dist/services/agent-config.service.d.ts +45 -0
- package/dist/services/agent-config.service.js +114 -0
- package/dist/services/agent-job.worker.d.ts +32 -0
- package/dist/services/agent-job.worker.js +97 -0
- package/dist/services/agent-runner.service.d.ts +35 -0
- package/dist/services/agent-runner.service.js +224 -0
- package/dist/services/agent.service.d.ts +171 -0
- package/dist/services/agent.service.js +329 -0
- package/dist/services/api-key.service.d.ts +41 -0
- package/dist/services/api-key.service.js +80 -0
- package/dist/services/auth.service.d.ts +133 -0
- package/dist/services/auth.service.js +411 -0
- package/dist/services/billing.service.d.ts +67 -0
- package/dist/services/billing.service.js +254 -0
- package/dist/services/chat-token.service.d.ts +29 -0
- package/dist/services/chat-token.service.js +113 -0
- package/dist/services/connector-registry.service.d.ts +156 -0
- package/dist/services/connector-registry.service.js +278 -0
- package/dist/services/conversation.service.d.ts +47 -0
- package/dist/services/conversation.service.js +101 -0
- package/dist/services/email-templates.d.ts +18 -0
- package/dist/services/email-templates.js +39 -0
- package/dist/services/email.service.d.ts +26 -0
- package/dist/services/email.service.js +42 -0
- package/dist/services/errors.d.ts +7 -0
- package/dist/services/errors.js +27 -0
- package/dist/services/in-memory-prepared-stream.store.d.ts +13 -0
- package/dist/services/in-memory-prepared-stream.store.js +35 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.js +40 -0
- package/dist/services/mcp-client.service.d.ts +64 -0
- package/dist/services/mcp-client.service.js +157 -0
- package/dist/services/mcp-server.service.d.ts +44 -0
- package/dist/services/mcp-server.service.js +147 -0
- package/dist/services/oauth.service.d.ts +73 -0
- package/dist/services/oauth.service.js +174 -0
- package/dist/services/oauth2.service.d.ts +57 -0
- package/dist/services/oauth2.service.js +82 -0
- package/dist/services/orchestrator.service.d.ts +45 -0
- package/dist/services/orchestrator.service.js +180 -0
- package/dist/services/plan.service.d.ts +54 -0
- package/dist/services/plan.service.js +120 -0
- package/dist/services/prepared-stream.service.d.ts +23 -0
- package/dist/services/prepared-stream.service.js +43 -0
- package/dist/services/refresh-token.service.d.ts +38 -0
- package/dist/services/refresh-token.service.js +73 -0
- package/dist/services/secrets/crypto.d.ts +37 -0
- package/dist/services/secrets/crypto.js +110 -0
- package/dist/services/secrets/known-keys.d.ts +38 -0
- package/dist/services/secrets/known-keys.js +50 -0
- package/dist/services/secrets.service.d.ts +91 -0
- package/dist/services/secrets.service.js +193 -0
- package/dist/services/tenant-billing.service.d.ts +121 -0
- package/dist/services/tenant-billing.service.js +290 -0
- package/dist/services/tenant.service.d.ts +54 -0
- package/dist/services/tenant.service.js +96 -0
- package/dist/services/tool-registry.service.d.ts +42 -0
- package/dist/services/tool-registry.service.js +101 -0
- package/dist/services/upload.service.d.ts +37 -0
- package/dist/services/upload.service.js +84 -0
- package/dist/services/usage.service.d.ts +34 -0
- package/dist/services/usage.service.js +108 -0
- package/dist/types/agent.types.d.ts +160 -0
- package/dist/types/agent.types.js +2 -0
- package/dist/types/billing.types.d.ts +82 -0
- package/dist/types/billing.types.js +3 -0
- package/dist/types/config.types.d.ts +127 -0
- package/dist/types/config.types.js +9 -0
- package/dist/types/hooks.d.ts +85 -0
- package/dist/types/hooks.js +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +19 -0
- package/package.json +36 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CheckoutSession, CreateCheckoutParams, CreateCustomerParams, CreateSubscriptionParams, SubscriptionResult, WebhookEvent } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* IBillingAdapter defines the contract for payment providers.
|
|
4
|
+
* Implement this interface to support any payment gateway (Stripe, Paddle, LemonSqueezy, etc.)
|
|
5
|
+
*/
|
|
6
|
+
export interface IBillingAdapter {
|
|
7
|
+
/**
|
|
8
|
+
* Create a Stripe Checkout (or equivalent) session for a subscription or one-time payment.
|
|
9
|
+
*/
|
|
10
|
+
createCheckoutSession(params: CreateCheckoutParams): Promise<CheckoutSession>;
|
|
11
|
+
/**
|
|
12
|
+
* Programmatically create a subscription (without hosted checkout).
|
|
13
|
+
*/
|
|
14
|
+
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Cancel a subscription (optionally at period end).
|
|
17
|
+
*/
|
|
18
|
+
cancelSubscription(subscriptionId: string, atPeriodEnd?: boolean): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Handle an incoming webhook from the payment provider.
|
|
21
|
+
* Returns a normalized WebhookEvent.
|
|
22
|
+
*/
|
|
23
|
+
handleWebhook(payload: Buffer, signature: string): Promise<WebhookEvent>;
|
|
24
|
+
/**
|
|
25
|
+
* Generate a customer portal URL where users can manage their subscription.
|
|
26
|
+
*/
|
|
27
|
+
getPortalUrl(customerId: string, returnUrl: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Create a customer record in the payment provider.
|
|
30
|
+
* Returns the provider-specific customer ID.
|
|
31
|
+
*/
|
|
32
|
+
createCustomer(params: CreateCustomerParams): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Get the current subscription status from the provider.
|
|
35
|
+
*/
|
|
36
|
+
getSubscription(subscriptionId: string): Promise<SubscriptionResult & {
|
|
37
|
+
status: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
/** Token to inject the billing adapter via NestJS DI */
|
|
41
|
+
export declare const BILLING_ADAPTER = "BILLING_ADAPTER";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
2
|
+
import type { IBillingAdapter } from '../billing-adapter.interface';
|
|
3
|
+
import type { CheckoutSession, CreateCheckoutParams, CreateCustomerParams, CreateSubscriptionParams, PlanDefinition, StripeConfig, SubscriptionResult, WebhookEvent } from '../../../types';
|
|
4
|
+
interface MiniLogger {
|
|
5
|
+
debug?: (m: string) => void;
|
|
6
|
+
warn?: (m: string) => void;
|
|
7
|
+
log?: (m: string) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* StripeAdapter implements IBillingAdapter using the Stripe API.
|
|
11
|
+
* Handles subscriptions, pay-per-use checkouts, webhooks, and customer portal.
|
|
12
|
+
*/
|
|
13
|
+
export declare class StripeAdapter implements IBillingAdapter {
|
|
14
|
+
private readonly logger;
|
|
15
|
+
private readonly stripe;
|
|
16
|
+
private readonly plans;
|
|
17
|
+
private readonly webhookSecret;
|
|
18
|
+
constructor(config: StripeConfig, plans?: PlanDefinition[], logger?: MiniLogger);
|
|
19
|
+
createCustomer(params: CreateCustomerParams): Promise<string>;
|
|
20
|
+
createCheckoutSession(params: CreateCheckoutParams): Promise<CheckoutSession>;
|
|
21
|
+
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
22
|
+
cancelSubscription(subscriptionId: string, atPeriodEnd?: boolean): Promise<void>;
|
|
23
|
+
getSubscription(subscriptionId: string): Promise<SubscriptionResult & {
|
|
24
|
+
status: string;
|
|
25
|
+
}>;
|
|
26
|
+
getPortalUrl(customerId: string, returnUrl: string): Promise<string>;
|
|
27
|
+
handleWebhook(payload: Buffer, signature: string): Promise<WebhookEvent>;
|
|
28
|
+
getStripeInstance(): Stripe;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StripeAdapter = void 0;
|
|
7
|
+
const stripe_1 = __importDefault(require("stripe"));
|
|
8
|
+
/**
|
|
9
|
+
* StripeAdapter implements IBillingAdapter using the Stripe API.
|
|
10
|
+
* Handles subscriptions, pay-per-use checkouts, webhooks, and customer portal.
|
|
11
|
+
*/
|
|
12
|
+
class StripeAdapter {
|
|
13
|
+
constructor(config, plans = [], logger) {
|
|
14
|
+
this.logger = logger ?? {};
|
|
15
|
+
this.stripe = new stripe_1.default(config.secretKey, {
|
|
16
|
+
apiVersion: '2023-10-16',
|
|
17
|
+
});
|
|
18
|
+
this.plans = plans;
|
|
19
|
+
this.webhookSecret = config.webhookSecret;
|
|
20
|
+
}
|
|
21
|
+
// ─── Customer ─────────────────────────────────────────────────────────────
|
|
22
|
+
async createCustomer(params) {
|
|
23
|
+
const customer = await this.stripe.customers.create({
|
|
24
|
+
email: params.email,
|
|
25
|
+
name: params.name,
|
|
26
|
+
metadata: params.metadata ?? {},
|
|
27
|
+
});
|
|
28
|
+
this.logger.debug?.(`Created Stripe customer: ${customer.id}`);
|
|
29
|
+
return customer.id;
|
|
30
|
+
}
|
|
31
|
+
// ─── Checkout ─────────────────────────────────────────────────────────────
|
|
32
|
+
async createCheckoutSession(params) {
|
|
33
|
+
// Prefer the caller-provided stripePriceId (DB-backed plans flow).
|
|
34
|
+
// Fall back to looking it up in the static plan list passed to the
|
|
35
|
+
// constructor (legacy / single-tenant flow).
|
|
36
|
+
const fallbackPlan = this.plans.find((p) => p.id === params.planId);
|
|
37
|
+
const stripePriceId = params.stripePriceId ?? fallbackPlan?.stripePriceId;
|
|
38
|
+
const interval = fallbackPlan?.interval ?? 'month';
|
|
39
|
+
if (!stripePriceId) {
|
|
40
|
+
throw new Error(`Plan "${params.planId}" has no Stripe price ID configured`);
|
|
41
|
+
}
|
|
42
|
+
const session = await this.stripe.checkout.sessions.create({
|
|
43
|
+
mode: interval ? 'subscription' : 'payment',
|
|
44
|
+
payment_method_types: ['card'],
|
|
45
|
+
line_items: [{ price: stripePriceId, quantity: 1 }],
|
|
46
|
+
success_url: params.successUrl,
|
|
47
|
+
cancel_url: params.cancelUrl,
|
|
48
|
+
metadata: {
|
|
49
|
+
userId: params.userId,
|
|
50
|
+
planId: params.planId,
|
|
51
|
+
...params.metadata,
|
|
52
|
+
},
|
|
53
|
+
subscription_data: interval
|
|
54
|
+
? { metadata: { userId: params.userId, planId: params.planId } }
|
|
55
|
+
: undefined,
|
|
56
|
+
});
|
|
57
|
+
return { sessionId: session.id, url: session.url };
|
|
58
|
+
}
|
|
59
|
+
// ─── Subscription ─────────────────────────────────────────────────────────
|
|
60
|
+
async createSubscription(params) {
|
|
61
|
+
const subscription = await this.stripe.subscriptions.create({
|
|
62
|
+
customer: params.customerId,
|
|
63
|
+
items: [{ price: params.priceId }],
|
|
64
|
+
trial_period_days: params.trialDays,
|
|
65
|
+
payment_behavior: 'default_incomplete',
|
|
66
|
+
expand: ['latest_invoice.payment_intent'],
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
subscriptionId: subscription.id,
|
|
70
|
+
status: subscription.status,
|
|
71
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async cancelSubscription(subscriptionId, atPeriodEnd = true) {
|
|
75
|
+
if (atPeriodEnd) {
|
|
76
|
+
await this.stripe.subscriptions.update(subscriptionId, {
|
|
77
|
+
cancel_at_period_end: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
await this.stripe.subscriptions.cancel(subscriptionId);
|
|
82
|
+
}
|
|
83
|
+
this.logger.log?.(`Subscription ${subscriptionId} cancelled (atPeriodEnd=${atPeriodEnd})`);
|
|
84
|
+
}
|
|
85
|
+
async getSubscription(subscriptionId) {
|
|
86
|
+
const sub = await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
87
|
+
return {
|
|
88
|
+
subscriptionId: sub.id,
|
|
89
|
+
status: sub.status,
|
|
90
|
+
currentPeriodEnd: new Date(sub.current_period_end * 1000),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ─── Customer Portal ──────────────────────────────────────────────────────
|
|
94
|
+
async getPortalUrl(customerId, returnUrl) {
|
|
95
|
+
const session = await this.stripe.billingPortal.sessions.create({
|
|
96
|
+
customer: customerId,
|
|
97
|
+
return_url: returnUrl,
|
|
98
|
+
});
|
|
99
|
+
return session.url;
|
|
100
|
+
}
|
|
101
|
+
// ─── Webhooks ─────────────────────────────────────────────────────────────
|
|
102
|
+
async handleWebhook(payload, signature) {
|
|
103
|
+
let event;
|
|
104
|
+
try {
|
|
105
|
+
event = this.stripe.webhooks.constructEvent(payload, signature, this.webhookSecret);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
throw new Error(`Stripe webhook verification failed: ${err}`);
|
|
109
|
+
}
|
|
110
|
+
this.logger.debug?.(`Stripe webhook received: ${event.type}`);
|
|
111
|
+
// Normalize to a generic WebhookEvent
|
|
112
|
+
return {
|
|
113
|
+
type: event.type,
|
|
114
|
+
data: event.data.object,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
118
|
+
getStripeInstance() {
|
|
119
|
+
return this.stripe;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.StripeAdapter = StripeAdapter;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic contract for sending email.
|
|
3
|
+
*
|
|
4
|
+
* Default implementation is Resend. Plug in SES/SendGrid/SMTP/Mailgun/etc.
|
|
5
|
+
* via `auth.email.provider = 'custom'` + `customAdapter` in config, or by
|
|
6
|
+
* overriding the EMAIL_ADAPTER provider in the consuming app's module.
|
|
7
|
+
*/
|
|
8
|
+
export interface EmailAdapter {
|
|
9
|
+
send(message: EmailMessage): Promise<EmailSendResult>;
|
|
10
|
+
}
|
|
11
|
+
export interface EmailMessage {
|
|
12
|
+
to: string;
|
|
13
|
+
subject: string;
|
|
14
|
+
html?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
/** Override the default `from` for this single send. */
|
|
17
|
+
from?: string;
|
|
18
|
+
/** Tags / metadata for analytics — adapter may ignore. */
|
|
19
|
+
tags?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export interface EmailSendResult {
|
|
22
|
+
/** Provider's message id, when available. */
|
|
23
|
+
id?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const EMAIL_ADAPTER = "AGENTFORGE_EMAIL_ADAPTER";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_ADAPTER = void 0;
|
|
4
|
+
// String token (not Symbol) so it survives duplicate package instances and
|
|
5
|
+
// works as a NestJS string-based DI provider out of the box.
|
|
6
|
+
exports.EMAIL_ADAPTER = 'AGENTFORGE_EMAIL_ADAPTER';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EmailAdapter, EmailMessage, EmailSendResult } from './email-adapter.interface';
|
|
2
|
+
/**
|
|
3
|
+
* Used when no email config is provided. Throws on send so callers (verify,
|
|
4
|
+
* password-reset) fail loudly instead of silently dropping. AuthService wraps
|
|
5
|
+
* the call in try/catch where appropriate (e.g. password-reset to preserve
|
|
6
|
+
* the always-200 anti-enumeration guarantee).
|
|
7
|
+
*/
|
|
8
|
+
export declare class NoopEmailAdapter implements EmailAdapter {
|
|
9
|
+
send(_message: EmailMessage): Promise<EmailSendResult>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NoopEmailAdapter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Used when no email config is provided. Throws on send so callers (verify,
|
|
6
|
+
* password-reset) fail loudly instead of silently dropping. AuthService wraps
|
|
7
|
+
* the call in try/catch where appropriate (e.g. password-reset to preserve
|
|
8
|
+
* the always-200 anti-enumeration guarantee).
|
|
9
|
+
*/
|
|
10
|
+
class NoopEmailAdapter {
|
|
11
|
+
async send(_message) {
|
|
12
|
+
throw new Error('Email features are not configured. Pass an EmailAdapter (e.g. ResendAdapter) when creating AgentForge.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.NoopEmailAdapter = NoopEmailAdapter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EmailAdapter, EmailMessage, EmailSendResult } from './email-adapter.interface';
|
|
2
|
+
export declare class ResendAdapter implements EmailAdapter {
|
|
3
|
+
private readonly apiKey;
|
|
4
|
+
private readonly defaultFrom;
|
|
5
|
+
private readonly client;
|
|
6
|
+
constructor(apiKey: string, defaultFrom: string);
|
|
7
|
+
send(message: EmailMessage): Promise<EmailSendResult>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResendAdapter = void 0;
|
|
4
|
+
const resend_1 = require("resend");
|
|
5
|
+
class ResendAdapter {
|
|
6
|
+
constructor(apiKey, defaultFrom) {
|
|
7
|
+
this.apiKey = apiKey;
|
|
8
|
+
this.defaultFrom = defaultFrom;
|
|
9
|
+
if (!apiKey)
|
|
10
|
+
throw new Error('ResendAdapter: apiKey is required');
|
|
11
|
+
if (!defaultFrom) {
|
|
12
|
+
throw new Error('ResendAdapter: defaultFrom is required (e.g. "App <noreply@yourdomain.com>")');
|
|
13
|
+
}
|
|
14
|
+
this.client = new resend_1.Resend(apiKey);
|
|
15
|
+
}
|
|
16
|
+
async send(message) {
|
|
17
|
+
const payload = {
|
|
18
|
+
from: message.from ?? this.defaultFrom,
|
|
19
|
+
to: [message.to],
|
|
20
|
+
subject: message.subject,
|
|
21
|
+
};
|
|
22
|
+
if (message.html)
|
|
23
|
+
payload.html = message.html;
|
|
24
|
+
if (message.text)
|
|
25
|
+
payload.text = message.text;
|
|
26
|
+
if (message.tags) {
|
|
27
|
+
payload.tags = Object.entries(message.tags).map(([name, value]) => ({ name, value }));
|
|
28
|
+
}
|
|
29
|
+
if (!payload.html && !payload.text) {
|
|
30
|
+
throw new Error('ResendAdapter: html or text is required');
|
|
31
|
+
}
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const { data, error } = await this.client.emails.send(payload);
|
|
34
|
+
if (error)
|
|
35
|
+
throw new Error(`Resend: ${error.message}`);
|
|
36
|
+
return { id: data?.id };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.ResendAdapter = ResendAdapter;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { EnqueueOptions, JobProcessor, JobQueue, JobStatus, QueueMetrics } from './job-queue.types';
|
|
2
|
+
export interface InMemoryJobQueueOptions {
|
|
3
|
+
/** Max concurrent jobs. Default: 1 (sequential). */
|
|
4
|
+
concurrency?: number;
|
|
5
|
+
/** Default retry attempts on failure. Default: 3. */
|
|
6
|
+
defaultAttempts?: number;
|
|
7
|
+
/** Avg wait estimate per pending job in ms. Default: 5000. */
|
|
8
|
+
estimatedWaitPerJobMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Process-local JobQueue. Holds jobs in a Map, drains them with a small
|
|
12
|
+
* scheduler (default concurrency=1, sequential). Pending jobs are lost on
|
|
13
|
+
* restart — use a persistent adapter (BullMQ etc.) for production.
|
|
14
|
+
*
|
|
15
|
+
* Designed to be a drop-in default so Express adapters can offer /jobs/*
|
|
16
|
+
* endpoints without forcing operators to stand up Redis.
|
|
17
|
+
*/
|
|
18
|
+
export declare class InMemoryJobQueue<P, R> implements JobQueue<P, R> {
|
|
19
|
+
private readonly processor;
|
|
20
|
+
private readonly jobs;
|
|
21
|
+
/** Insertion-order list of waiting jobIds; used to drain FIFO within a priority. */
|
|
22
|
+
private readonly waitingIds;
|
|
23
|
+
private readonly concurrency;
|
|
24
|
+
private readonly defaultAttempts;
|
|
25
|
+
private readonly estimatedWaitPerJobMs;
|
|
26
|
+
private activeCount;
|
|
27
|
+
/** Completed and failed counters, kept separately from `jobs` so the Map
|
|
28
|
+
* can be pruned over time without losing aggregate metrics. */
|
|
29
|
+
private completedCount;
|
|
30
|
+
private failedCount;
|
|
31
|
+
constructor(processor: JobProcessor<P, R>, opts?: InMemoryJobQueueOptions);
|
|
32
|
+
enqueue(payload: P, opts?: EnqueueOptions): Promise<{
|
|
33
|
+
jobId: string;
|
|
34
|
+
estimatedWait: number;
|
|
35
|
+
}>;
|
|
36
|
+
getJobStatus(jobId: string): Promise<JobStatus<R>>;
|
|
37
|
+
cancelJob(jobId: string): Promise<boolean>;
|
|
38
|
+
getMetrics(): Promise<QueueMetrics>;
|
|
39
|
+
private insertByPriority;
|
|
40
|
+
private estimateWait;
|
|
41
|
+
private drain;
|
|
42
|
+
private run;
|
|
43
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryJobQueue = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Process-local JobQueue. Holds jobs in a Map, drains them with a small
|
|
7
|
+
* scheduler (default concurrency=1, sequential). Pending jobs are lost on
|
|
8
|
+
* restart — use a persistent adapter (BullMQ etc.) for production.
|
|
9
|
+
*
|
|
10
|
+
* Designed to be a drop-in default so Express adapters can offer /jobs/*
|
|
11
|
+
* endpoints without forcing operators to stand up Redis.
|
|
12
|
+
*/
|
|
13
|
+
class InMemoryJobQueue {
|
|
14
|
+
constructor(processor, opts = {}) {
|
|
15
|
+
this.processor = processor;
|
|
16
|
+
this.jobs = new Map();
|
|
17
|
+
/** Insertion-order list of waiting jobIds; used to drain FIFO within a priority. */
|
|
18
|
+
this.waitingIds = [];
|
|
19
|
+
this.activeCount = 0;
|
|
20
|
+
/** Completed and failed counters, kept separately from `jobs` so the Map
|
|
21
|
+
* can be pruned over time without losing aggregate metrics. */
|
|
22
|
+
this.completedCount = 0;
|
|
23
|
+
this.failedCount = 0;
|
|
24
|
+
this.concurrency = Math.max(1, opts.concurrency ?? 1);
|
|
25
|
+
this.defaultAttempts = Math.max(1, opts.defaultAttempts ?? 3);
|
|
26
|
+
this.estimatedWaitPerJobMs = opts.estimatedWaitPerJobMs ?? 5000;
|
|
27
|
+
}
|
|
28
|
+
async enqueue(payload, opts = {}) {
|
|
29
|
+
const jobId = opts.jobId ?? (0, crypto_1.randomUUID)();
|
|
30
|
+
if (this.jobs.has(jobId)) {
|
|
31
|
+
// Idempotent: a re-enqueue of the same id returns the existing entry.
|
|
32
|
+
const existing = this.jobs.get(jobId);
|
|
33
|
+
return { jobId, estimatedWait: this.estimateWait(existing.priority) };
|
|
34
|
+
}
|
|
35
|
+
const attempts = Math.max(1, opts.attempts ?? this.defaultAttempts);
|
|
36
|
+
const job = {
|
|
37
|
+
jobId,
|
|
38
|
+
payload,
|
|
39
|
+
priority: opts.priority ?? 0,
|
|
40
|
+
attempts,
|
|
41
|
+
remainingAttempts: attempts,
|
|
42
|
+
enqueuedAt: Date.now(),
|
|
43
|
+
state: 'waiting',
|
|
44
|
+
progress: 0,
|
|
45
|
+
};
|
|
46
|
+
this.jobs.set(jobId, job);
|
|
47
|
+
this.insertByPriority(jobId, job.priority);
|
|
48
|
+
// Schedule the next drain on the microtask queue so callers see the
|
|
49
|
+
// jobId before any work happens.
|
|
50
|
+
queueMicrotask(() => this.drain());
|
|
51
|
+
return { jobId, estimatedWait: this.estimateWait(job.priority) };
|
|
52
|
+
}
|
|
53
|
+
async getJobStatus(jobId) {
|
|
54
|
+
const job = this.jobs.get(jobId);
|
|
55
|
+
if (!job)
|
|
56
|
+
return { status: 'not_found' };
|
|
57
|
+
if (job.state === 'completed') {
|
|
58
|
+
return { status: 'completed', progress: 100, result: job.result };
|
|
59
|
+
}
|
|
60
|
+
if (job.state === 'failed') {
|
|
61
|
+
return { status: 'failed', progress: job.progress, error: job.error };
|
|
62
|
+
}
|
|
63
|
+
return { status: job.state, progress: job.progress };
|
|
64
|
+
}
|
|
65
|
+
async cancelJob(jobId) {
|
|
66
|
+
const job = this.jobs.get(jobId);
|
|
67
|
+
if (!job || job.state !== 'waiting')
|
|
68
|
+
return false;
|
|
69
|
+
const idx = this.waitingIds.indexOf(jobId);
|
|
70
|
+
if (idx >= 0)
|
|
71
|
+
this.waitingIds.splice(idx, 1);
|
|
72
|
+
this.jobs.delete(jobId);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
async getMetrics() {
|
|
76
|
+
return {
|
|
77
|
+
waiting: this.waitingIds.length,
|
|
78
|
+
active: this.activeCount,
|
|
79
|
+
completed: this.completedCount,
|
|
80
|
+
failed: this.failedCount,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// ─── Internal scheduling ───────────────────────────────────────────────
|
|
84
|
+
insertByPriority(jobId, priority) {
|
|
85
|
+
// Higher priority runs first. Stable: within a priority we preserve
|
|
86
|
+
// enqueue order. Linear insert is fine — queues with thousands of
|
|
87
|
+
// pending jobs should use a real backing store anyway.
|
|
88
|
+
const insertAt = this.waitingIds.findIndex((id) => {
|
|
89
|
+
const j = this.jobs.get(id);
|
|
90
|
+
return j ? j.priority < priority : true;
|
|
91
|
+
});
|
|
92
|
+
if (insertAt === -1)
|
|
93
|
+
this.waitingIds.push(jobId);
|
|
94
|
+
else
|
|
95
|
+
this.waitingIds.splice(insertAt, 0, jobId);
|
|
96
|
+
}
|
|
97
|
+
estimateWait(priority) {
|
|
98
|
+
const ahead = this.waitingIds.filter((id) => {
|
|
99
|
+
const j = this.jobs.get(id);
|
|
100
|
+
return j ? j.priority >= priority : false;
|
|
101
|
+
}).length;
|
|
102
|
+
return ahead * this.estimatedWaitPerJobMs;
|
|
103
|
+
}
|
|
104
|
+
drain() {
|
|
105
|
+
while (this.activeCount < this.concurrency && this.waitingIds.length > 0) {
|
|
106
|
+
const jobId = this.waitingIds.shift();
|
|
107
|
+
const job = this.jobs.get(jobId);
|
|
108
|
+
if (!job)
|
|
109
|
+
continue;
|
|
110
|
+
this.activeCount += 1;
|
|
111
|
+
job.state = 'active';
|
|
112
|
+
this.run(job);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async run(job) {
|
|
116
|
+
const ctx = {
|
|
117
|
+
jobId: job.jobId,
|
|
118
|
+
updateProgress: async (percent) => {
|
|
119
|
+
job.progress = Math.max(0, Math.min(100, percent));
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
try {
|
|
123
|
+
const result = await this.processor(job.payload, ctx);
|
|
124
|
+
job.result = result;
|
|
125
|
+
job.progress = 100;
|
|
126
|
+
job.state = 'completed';
|
|
127
|
+
this.completedCount += 1;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
job.remainingAttempts -= 1;
|
|
131
|
+
if (job.remainingAttempts > 0) {
|
|
132
|
+
// Exponential backoff: 2s, 4s, 8s … per attempt index.
|
|
133
|
+
const attemptIdx = job.attempts - job.remainingAttempts;
|
|
134
|
+
const delay = Math.min(60_000, 2000 * 2 ** (attemptIdx - 1));
|
|
135
|
+
job.state = 'waiting';
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
this.insertByPriority(job.jobId, job.priority);
|
|
138
|
+
this.drain();
|
|
139
|
+
}, delay).unref?.();
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
job.error = err instanceof Error ? err.message : String(err);
|
|
143
|
+
job.state = 'failed';
|
|
144
|
+
this.failedCount += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
this.activeCount -= 1;
|
|
149
|
+
// Drain anything that became eligible while we ran.
|
|
150
|
+
this.drain();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.InMemoryJobQueue = InMemoryJobQueue;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic background-job queue contract. AgentForge uses it for the agent-turn
|
|
3
|
+
* pipeline (enqueue an agent run, return immediately, poll for status) but the
|
|
4
|
+
* interface is intentionally domain-agnostic so other long-running operations
|
|
5
|
+
* can reuse it.
|
|
6
|
+
*
|
|
7
|
+
* Two implementations ship with `@agentforge-io/core`:
|
|
8
|
+
* - InMemoryJobQueue — single-process, runs the worker on the same node.
|
|
9
|
+
* Default. Counters and pending jobs are lost on restart.
|
|
10
|
+
* - (BullMQ adapter lives in @agentforge-io/nest, since BullMQ depends on
|
|
11
|
+
* Redis + ioredis which we don't want in framework-free core.)
|
|
12
|
+
*
|
|
13
|
+
* Custom adapters: implement this interface with whatever backing store you
|
|
14
|
+
* want (SQS, Postgres, Cloud Tasks) and pass it as `adapters.jobQueue` to
|
|
15
|
+
* `createAgentForge`.
|
|
16
|
+
*/
|
|
17
|
+
export type JobState = 'waiting' | 'active' | 'completed' | 'failed';
|
|
18
|
+
export interface JobStatus<R> {
|
|
19
|
+
status: JobState | 'not_found';
|
|
20
|
+
/** 0-100. Workers report this via the `ctx.updateProgress` callback. */
|
|
21
|
+
progress?: number;
|
|
22
|
+
/** Set when status === 'completed'. */
|
|
23
|
+
result?: R;
|
|
24
|
+
/** Set when status === 'failed'. */
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface EnqueueOptions {
|
|
28
|
+
/** Override the auto-generated jobId. Adapters MUST treat this as idempotent. */
|
|
29
|
+
jobId?: string;
|
|
30
|
+
/** Higher value = earlier execution. Default: 0. */
|
|
31
|
+
priority?: number;
|
|
32
|
+
/** Maximum retry attempts on failure. Adapter chooses the backoff strategy. */
|
|
33
|
+
attempts?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface QueueMetrics {
|
|
36
|
+
waiting: number;
|
|
37
|
+
active: number;
|
|
38
|
+
completed: number;
|
|
39
|
+
failed: number;
|
|
40
|
+
}
|
|
41
|
+
export interface JobQueue<P, R> {
|
|
42
|
+
/**
|
|
43
|
+
* Submit a payload for background processing. Returns the assigned jobId
|
|
44
|
+
* and a rough wait estimate. Implementations are free to estimate based on
|
|
45
|
+
* queue depth or to return 0 when no estimate is possible.
|
|
46
|
+
*/
|
|
47
|
+
enqueue(payload: P, opts?: EnqueueOptions): Promise<{
|
|
48
|
+
jobId: string;
|
|
49
|
+
estimatedWait: number;
|
|
50
|
+
}>;
|
|
51
|
+
/** Look up a job's current state. Returns 'not_found' for unknown ids. */
|
|
52
|
+
getJobStatus(jobId: string): Promise<JobStatus<R>>;
|
|
53
|
+
/**
|
|
54
|
+
* Remove a still-pending job. Returns true if the job was waiting or
|
|
55
|
+
* delayed and got removed; false if it had already started, finished,
|
|
56
|
+
* or was unknown.
|
|
57
|
+
*/
|
|
58
|
+
cancelJob(jobId: string): Promise<boolean>;
|
|
59
|
+
/** Aggregate counts for monitoring/health checks. */
|
|
60
|
+
getMetrics(): Promise<QueueMetrics>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Context passed to a worker's process function — lets the worker report
|
|
64
|
+
* progress without coupling to a particular queue implementation.
|
|
65
|
+
*/
|
|
66
|
+
export interface JobContext {
|
|
67
|
+
jobId: string;
|
|
68
|
+
updateProgress(percent: number): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The worker function signature every JobQueue impl drives internally.
|
|
72
|
+
* `ctx` lets the worker emit progress updates the queue can surface via
|
|
73
|
+
* `getJobStatus`.
|
|
74
|
+
*/
|
|
75
|
+
export type JobProcessor<P, R> = (payload: P, ctx: JobContext) => Promise<R>;
|
|
76
|
+
export declare const JOB_QUEUE = "AGENTFORGE_JOB_QUEUE";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable storage for short-lived "prepared" stream messages.
|
|
3
|
+
*
|
|
4
|
+
* Default implementation is in-memory (`InMemoryPreparedStreamStore`). For
|
|
5
|
+
* multi-instance deploys swap in a Redis-backed implementation that uses
|
|
6
|
+
* `SETEX` + `GETDEL` so the same atomic take-once semantics hold across nodes.
|
|
7
|
+
*/
|
|
8
|
+
export interface PreparedStreamPayload {
|
|
9
|
+
userId: string;
|
|
10
|
+
conversationId: string;
|
|
11
|
+
content: string;
|
|
12
|
+
expiresAt: Date;
|
|
13
|
+
}
|
|
14
|
+
export interface PreparedStreamStore {
|
|
15
|
+
/** Persist a prepared payload under `streamId` with the given TTL. */
|
|
16
|
+
put(streamId: string, payload: PreparedStreamPayload, ttlMs: number): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Atomically read the payload AND delete it. Returns null if the streamId
|
|
19
|
+
* was not found or expired. The same streamId can never be consumed twice.
|
|
20
|
+
*/
|
|
21
|
+
takeOnce(streamId: string): Promise<PreparedStreamPayload | null>;
|
|
22
|
+
}
|
|
23
|
+
export declare const PREPARED_STREAM_STORE = "AGENTFORGE_PREPARED_STREAM_STORE";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RateLimitOptions, RateLimitResult, RateLimiter } from './rate-limiter.types';
|
|
2
|
+
/**
|
|
3
|
+
* Fixed-window counter. Each key gets a bucket holding the current count and
|
|
4
|
+
* the window-start timestamp; the bucket resets when the window expires.
|
|
5
|
+
*
|
|
6
|
+
* Single-instance only — counters live in process memory. For multi-instance
|
|
7
|
+
* deploys use `RedisRateLimiter`, which shares counters across nodes via INCR.
|
|
8
|
+
*/
|
|
9
|
+
export declare class InMemoryRateLimiter implements RateLimiter {
|
|
10
|
+
private readonly buckets;
|
|
11
|
+
private readonly cleanupInterval;
|
|
12
|
+
constructor(opts?: {
|
|
13
|
+
cleanupIntervalMs?: number;
|
|
14
|
+
});
|
|
15
|
+
consume(key: string, opts: RateLimitOptions): Promise<RateLimitResult>;
|
|
16
|
+
/** Stops the periodic cleanup. Call when shutting down a test or worker. */
|
|
17
|
+
close(): void;
|
|
18
|
+
private evictExpired;
|
|
19
|
+
}
|