@agentforge-io/core 2.0.24 → 2.1.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/factory.js +56 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/services/agent-runner.service.js +18 -4
- package/dist/services/agent.service.d.ts +21 -1
- package/dist/services/agent.service.js +42 -8
- package/dist/services/orchestrator.service.d.ts +40 -1
- package/dist/services/orchestrator.service.js +220 -0
- package/dist/types/agent.types.d.ts +31 -6
- package/package.json +1 -1
- package/dist/adapters/billing/billing-adapter.interface.d.ts +0 -41
- package/dist/adapters/billing/billing-adapter.interface.js +0 -5
- package/dist/adapters/billing/stripe/stripe.adapter.d.ts +0 -30
- package/dist/adapters/billing/stripe/stripe.adapter.js +0 -122
- package/dist/adapters/email/email-adapter.interface.d.ts +0 -25
- package/dist/adapters/email/email-adapter.interface.js +0 -6
- package/dist/adapters/email/noop.adapter.d.ts +0 -10
- package/dist/adapters/email/noop.adapter.js +0 -15
- package/dist/adapters/email/resend.adapter.d.ts +0 -8
- package/dist/adapters/email/resend.adapter.js +0 -39
- package/dist/adapters/upload/noop.adapter.d.ts +0 -9
- package/dist/adapters/upload/noop.adapter.js +0 -14
- package/dist/adapters/upload/s3.adapter.d.ts +0 -38
- package/dist/adapters/upload/s3.adapter.js +0 -69
- package/dist/adapters/upload/upload-adapter.interface.d.ts +0 -37
- package/dist/adapters/upload/upload-adapter.interface.js +0 -15
- package/dist/billing/index.d.ts +0 -12
- package/dist/billing/index.js +0 -28
- package/dist/domain/agent.d.ts +0 -59
- package/dist/domain/agent.js +0 -2
- package/dist/domain/api-key.d.ts +0 -28
- package/dist/domain/api-key.js +0 -2
- package/dist/domain/auth-identity.d.ts +0 -10
- package/dist/domain/auth-identity.js +0 -2
- package/dist/domain/email-token.d.ts +0 -11
- package/dist/domain/email-token.js +0 -2
- package/dist/domain/external-user.d.ts +0 -23
- package/dist/domain/external-user.js +0 -2
- package/dist/domain/plan.d.ts +0 -20
- package/dist/domain/plan.js +0 -2
- package/dist/domain/platform-secret.d.ts +0 -24
- package/dist/domain/platform-secret.js +0 -8
- package/dist/domain/refresh-token.d.ts +0 -15
- package/dist/domain/refresh-token.js +0 -2
- package/dist/domain/subscription.d.ts +0 -21
- package/dist/domain/subscription.js +0 -2
- package/dist/domain/tenant.d.ts +0 -21
- package/dist/domain/tenant.js +0 -2
- package/dist/domain/usage-record.d.ts +0 -15
- package/dist/domain/usage-record.js +0 -2
- package/dist/domain/user.d.ts +0 -43
- package/dist/domain/user.js +0 -2
- package/dist/services/agent-config.service.d.ts +0 -45
- package/dist/services/agent-config.service.js +0 -114
- package/dist/services/api-key.service.d.ts +0 -41
- package/dist/services/api-key.service.js +0 -80
- package/dist/services/auth.service.d.ts +0 -133
- package/dist/services/auth.service.js +0 -411
- package/dist/services/billing.service.d.ts +0 -67
- package/dist/services/billing.service.js +0 -254
- package/dist/services/email-templates.d.ts +0 -18
- package/dist/services/email-templates.js +0 -39
- package/dist/services/email.service.d.ts +0 -26
- package/dist/services/email.service.js +0 -42
- package/dist/services/errors.d.ts +0 -7
- package/dist/services/errors.js +0 -27
- package/dist/services/oauth.service.d.ts +0 -73
- package/dist/services/oauth.service.js +0 -174
- package/dist/services/plan.service.d.ts +0 -54
- package/dist/services/plan.service.js +0 -120
- package/dist/services/refresh-token.service.d.ts +0 -38
- package/dist/services/refresh-token.service.js +0 -73
- package/dist/services/secrets/crypto.d.ts +0 -37
- package/dist/services/secrets/crypto.js +0 -110
- package/dist/services/secrets/known-keys.d.ts +0 -38
- package/dist/services/secrets/known-keys.js +0 -50
- package/dist/services/secrets.service.d.ts +0 -91
- package/dist/services/secrets.service.js +0 -193
- package/dist/services/tenant-billing.service.d.ts +0 -121
- package/dist/services/tenant-billing.service.js +0 -290
- package/dist/services/tenant.service.d.ts +0 -54
- package/dist/services/tenant.service.js +0 -96
- package/dist/services/upload.service.d.ts +0 -37
- package/dist/services/upload.service.js +0 -84
- package/dist/services/usage.service.d.ts +0 -34
- package/dist/services/usage.service.js +0 -108
- package/dist/types/billing.types.d.ts +0 -82
- package/dist/types/billing.types.js +0 -3
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BillingError = exports.BillingService = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Billing for the B2C path: the end user is the payer. Mirrors
|
|
6
|
-
* TenantBillingService, scoped to individual users.
|
|
7
|
-
*/
|
|
8
|
-
class BillingService {
|
|
9
|
-
constructor(users, subscriptions, usageRecords, adapter, usageService, opts) {
|
|
10
|
-
this.users = users;
|
|
11
|
-
this.subscriptions = subscriptions;
|
|
12
|
-
this.usageRecords = usageRecords;
|
|
13
|
-
this.adapter = adapter;
|
|
14
|
-
this.usageService = usageService;
|
|
15
|
-
this.opts = opts;
|
|
16
|
-
}
|
|
17
|
-
// ─── Plans ──────────────────────────────────────────────────────────────
|
|
18
|
-
async getPlans() {
|
|
19
|
-
return this.opts.plans.list();
|
|
20
|
-
}
|
|
21
|
-
async getPlan(planId) {
|
|
22
|
-
const plan = await this.opts.plans.getPlan(planId);
|
|
23
|
-
if (!plan)
|
|
24
|
-
throw new BillingError('plan_not_found', `Plan "${planId}" not found`);
|
|
25
|
-
return plan;
|
|
26
|
-
}
|
|
27
|
-
async getDefaultPlanId() {
|
|
28
|
-
return this.opts.plans.getDefaultId();
|
|
29
|
-
}
|
|
30
|
-
// ─── Checkout ───────────────────────────────────────────────────────────
|
|
31
|
-
async createCheckout(input) {
|
|
32
|
-
const user = await this.users.findById(input.userId);
|
|
33
|
-
if (!user)
|
|
34
|
-
throw new BillingError('not_found', `User "${input.userId}" not found`);
|
|
35
|
-
const plan = await this.getPlan(input.planId);
|
|
36
|
-
if (!plan.stripePriceId) {
|
|
37
|
-
throw new BillingError('plan_not_purchasable', `Plan "${input.planId}" has no stripePriceId`);
|
|
38
|
-
}
|
|
39
|
-
// Lazy-create the Stripe customer for the user.
|
|
40
|
-
let customerId = user.providerCustomerId;
|
|
41
|
-
if (!customerId) {
|
|
42
|
-
customerId = await this.adapter.createCustomer({
|
|
43
|
-
email: user.email,
|
|
44
|
-
name: user.name,
|
|
45
|
-
metadata: { userId: user.id },
|
|
46
|
-
});
|
|
47
|
-
await this.users.update(user.id, { providerCustomerId: customerId });
|
|
48
|
-
}
|
|
49
|
-
const appUrl = input.appUrl ?? this.opts.billing?.appUrl ?? 'http://localhost:3000';
|
|
50
|
-
const successUrl = input.successUrl ??
|
|
51
|
-
this.opts.billing?.successUrl ??
|
|
52
|
-
`${appUrl}/billing/success?session_id={CHECKOUT_SESSION_ID}`;
|
|
53
|
-
const cancelUrl = input.cancelUrl ?? this.opts.billing?.cancelUrl ?? `${appUrl}/billing/cancel`;
|
|
54
|
-
const session = await this.adapter.createCheckoutSession({
|
|
55
|
-
userId: input.userId,
|
|
56
|
-
planId: input.planId,
|
|
57
|
-
stripePriceId: plan.stripePriceId,
|
|
58
|
-
successUrl,
|
|
59
|
-
cancelUrl,
|
|
60
|
-
});
|
|
61
|
-
return {
|
|
62
|
-
sessionId: session.sessionId,
|
|
63
|
-
url: session.url,
|
|
64
|
-
planId: input.planId,
|
|
65
|
-
userId: input.userId,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// ─── Portal ─────────────────────────────────────────────────────────────
|
|
69
|
-
async getPortalUrl(userId, returnUrl) {
|
|
70
|
-
const user = await this.users.findById(userId);
|
|
71
|
-
if (!user?.providerCustomerId) {
|
|
72
|
-
throw new BillingError('no_customer', 'User does not have a billing account');
|
|
73
|
-
}
|
|
74
|
-
const portalReturn = returnUrl ??
|
|
75
|
-
this.opts.billing?.portalReturnUrl ??
|
|
76
|
-
`${this.opts.billing?.appUrl ?? 'http://localhost:3000'}/billing`;
|
|
77
|
-
const url = await this.adapter.getPortalUrl(user.providerCustomerId, portalReturn);
|
|
78
|
-
return { url };
|
|
79
|
-
}
|
|
80
|
-
// ─── Webhook ────────────────────────────────────────────────────────────
|
|
81
|
-
async handleWebhook(payload, signature) {
|
|
82
|
-
const event = await this.adapter.handleWebhook(payload, signature);
|
|
83
|
-
this.opts.logger?.log(`Processing webhook event: ${event.type}`);
|
|
84
|
-
switch (event.type) {
|
|
85
|
-
case 'checkout.session.completed':
|
|
86
|
-
await this.onCheckoutCompleted(event.data);
|
|
87
|
-
break;
|
|
88
|
-
case 'customer.subscription.updated':
|
|
89
|
-
await this.onSubscriptionUpdated(event.data);
|
|
90
|
-
break;
|
|
91
|
-
case 'customer.subscription.deleted':
|
|
92
|
-
await this.onSubscriptionDeleted(event.data);
|
|
93
|
-
break;
|
|
94
|
-
case 'invoice.payment_failed':
|
|
95
|
-
await this.onPaymentFailed(event.data);
|
|
96
|
-
break;
|
|
97
|
-
default:
|
|
98
|
-
this.opts.logger?.debug?.(`Unhandled webhook event: ${event.type}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Same as handleWebhook but accepts an already-verified event. Used by
|
|
103
|
-
* routers that want to dispatch a single event to both the tenant and the
|
|
104
|
-
* user-centric handlers without re-verifying the signature.
|
|
105
|
-
*/
|
|
106
|
-
async handleWebhookEvent(event) {
|
|
107
|
-
switch (event.type) {
|
|
108
|
-
case 'checkout.session.completed':
|
|
109
|
-
await this.onCheckoutCompleted(event.data);
|
|
110
|
-
break;
|
|
111
|
-
case 'customer.subscription.updated':
|
|
112
|
-
await this.onSubscriptionUpdated(event.data);
|
|
113
|
-
break;
|
|
114
|
-
case 'customer.subscription.deleted':
|
|
115
|
-
await this.onSubscriptionDeleted(event.data);
|
|
116
|
-
break;
|
|
117
|
-
case 'invoice.payment_failed':
|
|
118
|
-
await this.onPaymentFailed(event.data);
|
|
119
|
-
break;
|
|
120
|
-
default:
|
|
121
|
-
this.opts.logger?.debug?.(`Unhandled webhook event: ${event.type}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
async onCheckoutCompleted(data) {
|
|
125
|
-
const { metadata, subscription: subscriptionId, customer } = data;
|
|
126
|
-
if (!metadata?.userId || !metadata?.planId)
|
|
127
|
-
return;
|
|
128
|
-
const plan = await this.opts.plans.getPlan(metadata.planId);
|
|
129
|
-
if (!plan)
|
|
130
|
-
return;
|
|
131
|
-
// 30 days default; refined when we can query the provider.
|
|
132
|
-
let periodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
133
|
-
if (subscriptionId) {
|
|
134
|
-
const sub = await this.adapter.getSubscription(subscriptionId);
|
|
135
|
-
periodEnd = sub.currentPeriodEnd;
|
|
136
|
-
}
|
|
137
|
-
const existing = await this.subscriptions.findByUserAndPlan(metadata.userId, metadata.planId);
|
|
138
|
-
if (existing) {
|
|
139
|
-
await this.subscriptions.update(existing.id, {
|
|
140
|
-
status: 'active',
|
|
141
|
-
providerSubscriptionId: subscriptionId,
|
|
142
|
-
providerCustomerId: customer,
|
|
143
|
-
currentPeriodEnd: periodEnd,
|
|
144
|
-
cancelAtPeriodEnd: false,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
await this.subscriptions.create({
|
|
149
|
-
userId: metadata.userId,
|
|
150
|
-
planId: metadata.planId,
|
|
151
|
-
status: 'active',
|
|
152
|
-
providerSubscriptionId: subscriptionId,
|
|
153
|
-
providerCustomerId: customer,
|
|
154
|
-
currentPeriodStart: new Date(),
|
|
155
|
-
currentPeriodEnd: periodEnd,
|
|
156
|
-
cancelAtPeriodEnd: false,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
await this.users.update(metadata.userId, {
|
|
160
|
-
currentPlanId: metadata.planId,
|
|
161
|
-
providerCustomerId: customer,
|
|
162
|
-
});
|
|
163
|
-
this.opts.logger?.log(`User ${metadata.userId} activated plan "${metadata.planId}"`);
|
|
164
|
-
}
|
|
165
|
-
async onSubscriptionUpdated(data) {
|
|
166
|
-
const { id, status, current_period_end, cancel_at_period_end, metadata, items } = data;
|
|
167
|
-
if (!metadata?.userId)
|
|
168
|
-
return;
|
|
169
|
-
const periodEndTs = current_period_end ?? items?.data?.[0]?.current_period_end;
|
|
170
|
-
const periodEnd = typeof periodEndTs === 'number' && Number.isFinite(periodEndTs)
|
|
171
|
-
? new Date(periodEndTs * 1000)
|
|
172
|
-
: undefined;
|
|
173
|
-
await this.subscriptions.updateByProviderId(id, {
|
|
174
|
-
status: status,
|
|
175
|
-
cancelAtPeriodEnd: cancel_at_period_end ?? false,
|
|
176
|
-
...(periodEnd ? { currentPeriodEnd: periodEnd } : {}),
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
async onSubscriptionDeleted(data) {
|
|
180
|
-
const { id, metadata } = data;
|
|
181
|
-
await this.subscriptions.updateByProviderId(id, { status: 'canceled' });
|
|
182
|
-
if (metadata?.userId) {
|
|
183
|
-
const defaultPlanId = await this.getDefaultPlanId();
|
|
184
|
-
await this.users.update(metadata.userId, { currentPlanId: defaultPlanId });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
async onPaymentFailed(data) {
|
|
188
|
-
const { subscription } = data;
|
|
189
|
-
if (subscription) {
|
|
190
|
-
await this.subscriptions.updateByProviderId(subscription, { status: 'past_due' });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// ─── Overview ───────────────────────────────────────────────────────────
|
|
194
|
-
async getOverview(userId) {
|
|
195
|
-
const user = await this.users.findById(userId);
|
|
196
|
-
const subscription = await this.subscriptions.findActiveForUser(userId);
|
|
197
|
-
const usage = await this.usageService.getSummary(userId);
|
|
198
|
-
const planId = user?.currentPlanId ?? (await this.getDefaultPlanId());
|
|
199
|
-
const plan = await this.opts.plans.getPlan(planId);
|
|
200
|
-
return {
|
|
201
|
-
subscription: subscription
|
|
202
|
-
? {
|
|
203
|
-
id: subscription.id,
|
|
204
|
-
userId,
|
|
205
|
-
planId: subscription.planId,
|
|
206
|
-
status: subscription.status,
|
|
207
|
-
providerSubscriptionId: subscription.providerSubscriptionId,
|
|
208
|
-
providerCustomerId: subscription.providerCustomerId,
|
|
209
|
-
currentPeriodStart: subscription.currentPeriodStart ?? new Date(),
|
|
210
|
-
currentPeriodEnd: subscription.currentPeriodEnd ?? new Date(),
|
|
211
|
-
trialEnd: subscription.trialEnd,
|
|
212
|
-
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
|
213
|
-
createdAt: subscription.createdAt,
|
|
214
|
-
updatedAt: subscription.updatedAt,
|
|
215
|
-
}
|
|
216
|
-
: null,
|
|
217
|
-
usage,
|
|
218
|
-
plan: {
|
|
219
|
-
id: planId,
|
|
220
|
-
name: plan?.name ?? planId,
|
|
221
|
-
features: plan?.features ?? [],
|
|
222
|
-
limits: {
|
|
223
|
-
requestsPerMonth: plan?.limits.requestsPerMonth ?? -1,
|
|
224
|
-
tokensPerMonth: plan?.limits.tokensPerMonth ?? -1,
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
// ─── Cancel ─────────────────────────────────────────────────────────────
|
|
230
|
-
async cancelSubscription(userId, immediately = false) {
|
|
231
|
-
const subscription = await this.subscriptions.findActiveForUser(userId);
|
|
232
|
-
if (!subscription?.providerSubscriptionId) {
|
|
233
|
-
throw new BillingError('no_active_subscription', 'No active subscription found');
|
|
234
|
-
}
|
|
235
|
-
await this.adapter.cancelSubscription(subscription.providerSubscriptionId, !immediately);
|
|
236
|
-
await this.subscriptions.update(subscription.id, {
|
|
237
|
-
cancelAtPeriodEnd: !immediately,
|
|
238
|
-
status: immediately ? 'canceled' : subscription.status,
|
|
239
|
-
});
|
|
240
|
-
if (immediately) {
|
|
241
|
-
const defaultPlanId = await this.getDefaultPlanId();
|
|
242
|
-
await this.users.update(userId, { currentPlanId: defaultPlanId });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
exports.BillingService = BillingService;
|
|
247
|
-
class BillingError extends Error {
|
|
248
|
-
constructor(code, message) {
|
|
249
|
-
super(message);
|
|
250
|
-
this.code = code;
|
|
251
|
-
this.status = code === 'not_found' ? 404 : 400;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
exports.BillingError = BillingError;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export declare function verifyEmailTemplate(params: {
|
|
2
|
-
appName?: string;
|
|
3
|
-
url: string;
|
|
4
|
-
expiresInHours: number;
|
|
5
|
-
}): {
|
|
6
|
-
subject: string;
|
|
7
|
-
html: string;
|
|
8
|
-
text: string;
|
|
9
|
-
};
|
|
10
|
-
export declare function passwordResetTemplate(params: {
|
|
11
|
-
appName?: string;
|
|
12
|
-
url: string;
|
|
13
|
-
expiresInMinutes: number;
|
|
14
|
-
}): {
|
|
15
|
-
subject: string;
|
|
16
|
-
html: string;
|
|
17
|
-
text: string;
|
|
18
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.verifyEmailTemplate = verifyEmailTemplate;
|
|
4
|
-
exports.passwordResetTemplate = passwordResetTemplate;
|
|
5
|
-
const wrapper = (body) => `<!doctype html>
|
|
6
|
-
<html><head><meta charset="utf-8" /></head>
|
|
7
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif; background:#f4f4f7; padding:24px; color:#111;">
|
|
8
|
-
<div style="max-width:520px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 2px 8px rgba(0,0,0,0.04);">
|
|
9
|
-
${body}
|
|
10
|
-
</div>
|
|
11
|
-
<p style="text-align:center;color:#888;font-size:12px;margin-top:16px;">Powered by AgentForge</p>
|
|
12
|
-
</body></html>`;
|
|
13
|
-
const button = (url, label) => `<a href="${url}" style="display:inline-block;background:#7c5cff;color:#fff;padding:12px 22px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px;">${label}</a>`;
|
|
14
|
-
function verifyEmailTemplate(params) {
|
|
15
|
-
const appName = params.appName ?? 'your account';
|
|
16
|
-
return {
|
|
17
|
-
subject: 'Confirm your email',
|
|
18
|
-
text: `Confirm your email for ${appName}: ${params.url}\n\nThis link expires in ${params.expiresInHours} hours.`,
|
|
19
|
-
html: wrapper(`
|
|
20
|
-
<h1 style="font-size:20px;margin:0 0 12px;">Confirm your email</h1>
|
|
21
|
-
<p style="margin:0 0 20px;color:#444;line-height:1.5;">Click the button below to verify your email and finish setting up ${appName}.</p>
|
|
22
|
-
${button(params.url, 'Verify email')}
|
|
23
|
-
<p style="margin:24px 0 0;color:#888;font-size:13px;">This link expires in ${params.expiresInHours} hours. If you didn't sign up, you can ignore this email.</p>
|
|
24
|
-
`),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function passwordResetTemplate(params) {
|
|
28
|
-
const appName = params.appName ?? 'your account';
|
|
29
|
-
return {
|
|
30
|
-
subject: 'Reset your password',
|
|
31
|
-
text: `Reset your password for ${appName}: ${params.url}\n\nThis link expires in ${params.expiresInMinutes} minutes.`,
|
|
32
|
-
html: wrapper(`
|
|
33
|
-
<h1 style="font-size:20px;margin:0 0 12px;">Reset your password</h1>
|
|
34
|
-
<p style="margin:0 0 20px;color:#444;line-height:1.5;">We received a request to reset the password for ${appName}. Click below to choose a new one.</p>
|
|
35
|
-
${button(params.url, 'Reset password')}
|
|
36
|
-
<p style="margin:24px 0 0;color:#888;font-size:13px;">This link expires in ${params.expiresInMinutes} minutes. If you didn't request a reset, you can safely ignore this email.</p>
|
|
37
|
-
`),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { EmailAdapter } from '../adapters/email/email-adapter.interface';
|
|
2
|
-
export interface EmailServiceOptions {
|
|
3
|
-
/** Base URL the email links land on; the token is appended as `&token=` (or `?token=` if missing). */
|
|
4
|
-
verifyEmailUrl?: string;
|
|
5
|
-
resetPasswordUrl?: string;
|
|
6
|
-
/** TTL hints used in the email copy (must mirror what AuthService enforces). */
|
|
7
|
-
verifyTokenTtlHours?: number;
|
|
8
|
-
resetTokenTtlMinutes?: number;
|
|
9
|
-
/** When false, helpers do nothing and `enabled` returns false (noop adapter mode). */
|
|
10
|
-
enabled?: boolean;
|
|
11
|
-
}
|
|
12
|
-
export declare class EmailService {
|
|
13
|
-
private readonly adapter;
|
|
14
|
-
private readonly opts;
|
|
15
|
-
constructor(adapter: EmailAdapter, opts?: EmailServiceOptions);
|
|
16
|
-
isEnabled(): boolean;
|
|
17
|
-
sendVerifyEmail(input: {
|
|
18
|
-
to: string;
|
|
19
|
-
token: string;
|
|
20
|
-
}): Promise<void>;
|
|
21
|
-
sendPasswordResetEmail(input: {
|
|
22
|
-
to: string;
|
|
23
|
-
token: string;
|
|
24
|
-
}): Promise<void>;
|
|
25
|
-
private buildUrl;
|
|
26
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EmailService = void 0;
|
|
4
|
-
const email_templates_1 = require("./email-templates");
|
|
5
|
-
class EmailService {
|
|
6
|
-
constructor(adapter, opts = {}) {
|
|
7
|
-
this.adapter = adapter;
|
|
8
|
-
this.opts = opts;
|
|
9
|
-
}
|
|
10
|
-
isEnabled() {
|
|
11
|
-
return this.opts.enabled !== false;
|
|
12
|
-
}
|
|
13
|
-
async sendVerifyEmail(input) {
|
|
14
|
-
const url = this.buildUrl(this.opts.verifyEmailUrl ?? '/verify-email', input.token);
|
|
15
|
-
const ttlHours = this.opts.verifyTokenTtlHours ?? 24;
|
|
16
|
-
const tpl = (0, email_templates_1.verifyEmailTemplate)({ url, expiresInHours: ttlHours });
|
|
17
|
-
await this.adapter.send({
|
|
18
|
-
to: input.to,
|
|
19
|
-
subject: tpl.subject,
|
|
20
|
-
html: tpl.html,
|
|
21
|
-
text: tpl.text,
|
|
22
|
-
tags: { purpose: 'verify_email' },
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
async sendPasswordResetEmail(input) {
|
|
26
|
-
const url = this.buildUrl(this.opts.resetPasswordUrl ?? '/reset-password', input.token);
|
|
27
|
-
const ttlMinutes = this.opts.resetTokenTtlMinutes ?? 60;
|
|
28
|
-
const tpl = (0, email_templates_1.passwordResetTemplate)({ url, expiresInMinutes: ttlMinutes });
|
|
29
|
-
await this.adapter.send({
|
|
30
|
-
to: input.to,
|
|
31
|
-
subject: tpl.subject,
|
|
32
|
-
html: tpl.html,
|
|
33
|
-
text: tpl.text,
|
|
34
|
-
tags: { purpose: 'reset_password' },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
buildUrl(base, token) {
|
|
38
|
-
const sep = base.includes('?') ? '&' : '?';
|
|
39
|
-
return `${base}${sep}token=${encodeURIComponent(token)}`;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
exports.EmailService = EmailService;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export type AuthErrorCode = 'invalid_credentials' | 'account_disabled' | 'account_locked' | 'email_unverified' | 'email_exists' | 'invalid_token' | 'token_expired' | 'token_consumed' | 'invalid_password' | 'no_email_on_file' | 'user_not_found' | 'last_admin';
|
|
2
|
-
/** Framework-agnostic auth errors. Adapters map `status` to their HTTP layer. */
|
|
3
|
-
export declare class AuthError extends Error {
|
|
4
|
-
readonly code: AuthErrorCode;
|
|
5
|
-
readonly status: number;
|
|
6
|
-
constructor(code: AuthErrorCode, message: string);
|
|
7
|
-
}
|
package/dist/services/errors.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuthError = void 0;
|
|
4
|
-
const STATUS = {
|
|
5
|
-
invalid_credentials: 401,
|
|
6
|
-
account_disabled: 401,
|
|
7
|
-
account_locked: 401,
|
|
8
|
-
email_unverified: 401,
|
|
9
|
-
email_exists: 409,
|
|
10
|
-
invalid_token: 400,
|
|
11
|
-
token_expired: 400,
|
|
12
|
-
token_consumed: 400,
|
|
13
|
-
invalid_password: 400,
|
|
14
|
-
no_email_on_file: 400,
|
|
15
|
-
user_not_found: 404,
|
|
16
|
-
last_admin: 409,
|
|
17
|
-
};
|
|
18
|
-
/** Framework-agnostic auth errors. Adapters map `status` to their HTTP layer. */
|
|
19
|
-
class AuthError extends Error {
|
|
20
|
-
constructor(code, message) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.code = code;
|
|
23
|
-
this.name = 'AuthError';
|
|
24
|
-
this.status = STATUS[code];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
exports.AuthError = AuthError;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { OAuthProviderConfig } from '../types/config.types';
|
|
2
|
-
/**
|
|
3
|
-
* Framework-free OAuth 2.0 helper. Provider specs + token exchange +
|
|
4
|
-
* profile normalization live here so every transport (Express, Nest,
|
|
5
|
-
* Fastify, …) drives the same code path. Adapters bring the HTTP shell:
|
|
6
|
-
* - render an authorize redirect with the state we hand back
|
|
7
|
-
* - persist that state (cookie, KV, whatever) until the callback
|
|
8
|
-
* - call validateCallback on the way back in
|
|
9
|
-
*
|
|
10
|
-
* CSRF: the state must round-trip through the user-agent in a way the
|
|
11
|
-
* server can verify. The Express adapter uses an httpOnly cookie scoped
|
|
12
|
-
* to the callback path. The Nest adapter does the same. Custom adapters
|
|
13
|
-
* are free to pick a different mechanism as long as `state` is opaque,
|
|
14
|
-
* single-use, and tied to the originating browser.
|
|
15
|
-
*/
|
|
16
|
-
export interface NormalizedOAuthProfile {
|
|
17
|
-
provider: string;
|
|
18
|
-
providerId: string;
|
|
19
|
-
email?: string;
|
|
20
|
-
emailVerified?: boolean;
|
|
21
|
-
name?: string;
|
|
22
|
-
metadata?: Record<string, unknown>;
|
|
23
|
-
}
|
|
24
|
-
export interface OAuthProviderSpec {
|
|
25
|
-
/** Identifier used in the URL: `/auth/{name}`. */
|
|
26
|
-
name: string;
|
|
27
|
-
authorizeUrl: string;
|
|
28
|
-
tokenUrl: string;
|
|
29
|
-
/** Optional second fetch — useful for GitHub where /user may not include email. */
|
|
30
|
-
fetchProfile: (accessToken: string) => Promise<NormalizedOAuthProfile>;
|
|
31
|
-
/** Extra params for the authorize redirect (scopes, prompts, etc.). */
|
|
32
|
-
authorizeExtras: () => Record<string, string>;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Public name of the state cookie/header per provider. Adapters share the
|
|
36
|
-
* convention so a session started under one transport could (in theory) be
|
|
37
|
-
* resumed under another — and so the operations docs are consistent.
|
|
38
|
-
*/
|
|
39
|
-
export declare const OAUTH_STATE_COOKIE_PREFIX = "af_oauth_state_";
|
|
40
|
-
export declare const OAUTH_STATE_TTL_MS: number;
|
|
41
|
-
/** Build the authorize URL + a fresh random state for the cookie/store. */
|
|
42
|
-
export declare function buildAuthorizeUrl(spec: OAuthProviderSpec, cfg: OAuthProviderConfig): {
|
|
43
|
-
authorizeUrl: string;
|
|
44
|
-
state: string;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Validate the callback against the expected state and exchange the
|
|
48
|
-
* authorization code for an access token + normalized profile.
|
|
49
|
-
*
|
|
50
|
-
* Returns the profile on success. Throws OAuthCallbackError with a stable
|
|
51
|
-
* `.code` on failure — adapters surface the code in the failure redirect.
|
|
52
|
-
*/
|
|
53
|
-
export declare function completeAuthorization(input: {
|
|
54
|
-
spec: OAuthProviderSpec;
|
|
55
|
-
cfg: OAuthProviderConfig;
|
|
56
|
-
/** Querystring `state` sent back by the provider. */
|
|
57
|
-
returnedState: string | undefined;
|
|
58
|
-
/** State we persisted before redirecting to the provider. */
|
|
59
|
-
expectedState: string | undefined;
|
|
60
|
-
/** Querystring `code` from the provider. */
|
|
61
|
-
code: string | undefined;
|
|
62
|
-
/** Querystring `error` from the provider, if any. */
|
|
63
|
-
providerError: string | undefined;
|
|
64
|
-
}): Promise<NormalizedOAuthProfile>;
|
|
65
|
-
/** Stable error shape adapters can pattern-match on for the failure redirect. */
|
|
66
|
-
export declare class OAuthCallbackError extends Error {
|
|
67
|
-
code: string;
|
|
68
|
-
constructor(code: string, message: string);
|
|
69
|
-
}
|
|
70
|
-
export declare const GOOGLE_OAUTH_SPEC: OAuthProviderSpec;
|
|
71
|
-
export declare const GITHUB_OAUTH_SPEC: OAuthProviderSpec;
|
|
72
|
-
/** Look up a built-in spec by name. Returns undefined for unknown names. */
|
|
73
|
-
export declare function getOAuthProviderSpec(name: string): OAuthProviderSpec | undefined;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GITHUB_OAUTH_SPEC = exports.GOOGLE_OAUTH_SPEC = exports.OAuthCallbackError = exports.OAUTH_STATE_TTL_MS = exports.OAUTH_STATE_COOKIE_PREFIX = void 0;
|
|
4
|
-
exports.buildAuthorizeUrl = buildAuthorizeUrl;
|
|
5
|
-
exports.completeAuthorization = completeAuthorization;
|
|
6
|
-
exports.getOAuthProviderSpec = getOAuthProviderSpec;
|
|
7
|
-
const crypto_1 = require("crypto");
|
|
8
|
-
/**
|
|
9
|
-
* Public name of the state cookie/header per provider. Adapters share the
|
|
10
|
-
* convention so a session started under one transport could (in theory) be
|
|
11
|
-
* resumed under another — and so the operations docs are consistent.
|
|
12
|
-
*/
|
|
13
|
-
exports.OAUTH_STATE_COOKIE_PREFIX = 'af_oauth_state_';
|
|
14
|
-
exports.OAUTH_STATE_TTL_MS = 10 * 60 * 1000; // 10 min
|
|
15
|
-
/** Build the authorize URL + a fresh random state for the cookie/store. */
|
|
16
|
-
function buildAuthorizeUrl(spec, cfg) {
|
|
17
|
-
const state = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
18
|
-
const params = new URLSearchParams({
|
|
19
|
-
client_id: cfg.clientId,
|
|
20
|
-
redirect_uri: cfg.callbackURL,
|
|
21
|
-
response_type: 'code',
|
|
22
|
-
state,
|
|
23
|
-
...spec.authorizeExtras(),
|
|
24
|
-
});
|
|
25
|
-
return {
|
|
26
|
-
authorizeUrl: `${spec.authorizeUrl}?${params.toString()}`,
|
|
27
|
-
state,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Validate the callback against the expected state and exchange the
|
|
32
|
-
* authorization code for an access token + normalized profile.
|
|
33
|
-
*
|
|
34
|
-
* Returns the profile on success. Throws OAuthCallbackError with a stable
|
|
35
|
-
* `.code` on failure — adapters surface the code in the failure redirect.
|
|
36
|
-
*/
|
|
37
|
-
async function completeAuthorization(input) {
|
|
38
|
-
if (input.providerError) {
|
|
39
|
-
throw new OAuthCallbackError(input.providerError, 'Provider returned an error');
|
|
40
|
-
}
|
|
41
|
-
if (!input.code) {
|
|
42
|
-
throw new OAuthCallbackError('missing_code', 'authorization code missing');
|
|
43
|
-
}
|
|
44
|
-
if (!input.expectedState || input.expectedState !== input.returnedState) {
|
|
45
|
-
throw new OAuthCallbackError('invalid_state', 'state mismatch');
|
|
46
|
-
}
|
|
47
|
-
let accessToken;
|
|
48
|
-
try {
|
|
49
|
-
accessToken = await exchangeCodeForToken({
|
|
50
|
-
tokenUrl: input.spec.tokenUrl,
|
|
51
|
-
clientId: input.cfg.clientId,
|
|
52
|
-
clientSecret: input.cfg.clientSecret,
|
|
53
|
-
code: input.code,
|
|
54
|
-
redirectUri: input.cfg.callbackURL,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
throw new OAuthCallbackError('token_exchange_failed', err.message);
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
return await input.spec.fetchProfile(accessToken);
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
throw new OAuthCallbackError('profile_fetch_failed', err.message);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/** Stable error shape adapters can pattern-match on for the failure redirect. */
|
|
68
|
-
class OAuthCallbackError extends Error {
|
|
69
|
-
constructor(code, message) {
|
|
70
|
-
super(message);
|
|
71
|
-
this.name = 'OAuthCallbackError';
|
|
72
|
-
this.code = code;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
exports.OAuthCallbackError = OAuthCallbackError;
|
|
76
|
-
// ─── Token exchange ────────────────────────────────────────────────────────
|
|
77
|
-
async function exchangeCodeForToken(input) {
|
|
78
|
-
const body = new URLSearchParams({
|
|
79
|
-
grant_type: 'authorization_code',
|
|
80
|
-
client_id: input.clientId,
|
|
81
|
-
client_secret: input.clientSecret,
|
|
82
|
-
code: input.code,
|
|
83
|
-
redirect_uri: input.redirectUri,
|
|
84
|
-
});
|
|
85
|
-
const res = await fetch(input.tokenUrl, {
|
|
86
|
-
method: 'POST',
|
|
87
|
-
headers: {
|
|
88
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
89
|
-
Accept: 'application/json',
|
|
90
|
-
},
|
|
91
|
-
body: body.toString(),
|
|
92
|
-
});
|
|
93
|
-
if (!res.ok)
|
|
94
|
-
throw new Error(`token endpoint returned ${res.status}`);
|
|
95
|
-
const json = (await res.json());
|
|
96
|
-
if (!json.access_token)
|
|
97
|
-
throw new Error('no access_token in response');
|
|
98
|
-
return json.access_token;
|
|
99
|
-
}
|
|
100
|
-
// ─── Provider specs ────────────────────────────────────────────────────────
|
|
101
|
-
exports.GOOGLE_OAUTH_SPEC = {
|
|
102
|
-
name: 'google',
|
|
103
|
-
authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
104
|
-
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
105
|
-
authorizeExtras: () => ({
|
|
106
|
-
scope: 'openid email profile',
|
|
107
|
-
access_type: 'offline',
|
|
108
|
-
prompt: 'select_account',
|
|
109
|
-
}),
|
|
110
|
-
async fetchProfile(accessToken) {
|
|
111
|
-
const r = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
|
112
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
113
|
-
});
|
|
114
|
-
if (!r.ok)
|
|
115
|
-
throw new Error(`userinfo ${r.status}`);
|
|
116
|
-
const p = (await r.json());
|
|
117
|
-
return {
|
|
118
|
-
provider: 'google',
|
|
119
|
-
providerId: p.sub,
|
|
120
|
-
email: p.email,
|
|
121
|
-
emailVerified: p.email_verified === true,
|
|
122
|
-
name: p.name,
|
|
123
|
-
metadata: { avatar: p.picture, locale: p.locale },
|
|
124
|
-
};
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
exports.GITHUB_OAUTH_SPEC = {
|
|
128
|
-
name: 'github',
|
|
129
|
-
authorizeUrl: 'https://github.com/login/oauth/authorize',
|
|
130
|
-
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
131
|
-
authorizeExtras: () => ({ scope: 'user:email' }),
|
|
132
|
-
async fetchProfile(accessToken) {
|
|
133
|
-
const headers = {
|
|
134
|
-
Authorization: `Bearer ${accessToken}`,
|
|
135
|
-
Accept: 'application/vnd.github+json',
|
|
136
|
-
'User-Agent': 'agentforge',
|
|
137
|
-
};
|
|
138
|
-
const userRes = await fetch('https://api.github.com/user', { headers });
|
|
139
|
-
if (!userRes.ok)
|
|
140
|
-
throw new Error(`/user ${userRes.status}`);
|
|
141
|
-
const u = (await userRes.json());
|
|
142
|
-
// /user.email is null when private. Fetch /user/emails to get the verified primary.
|
|
143
|
-
let email = u.email ?? undefined;
|
|
144
|
-
if (!email) {
|
|
145
|
-
const emailsRes = await fetch('https://api.github.com/user/emails', { headers });
|
|
146
|
-
if (emailsRes.ok) {
|
|
147
|
-
const emails = (await emailsRes.json());
|
|
148
|
-
email =
|
|
149
|
-
emails.find((e) => e.primary && e.verified)?.email ??
|
|
150
|
-
emails.find((e) => e.verified)?.email;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
provider: 'github',
|
|
155
|
-
providerId: String(u.id),
|
|
156
|
-
email,
|
|
157
|
-
emailVerified: !!email, // GitHub only returns verified emails through /user/emails
|
|
158
|
-
name: u.name || u.login,
|
|
159
|
-
metadata: {
|
|
160
|
-
username: u.login,
|
|
161
|
-
avatar: u.avatar_url,
|
|
162
|
-
profileUrl: u.html_url,
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
/** Look up a built-in spec by name. Returns undefined for unknown names. */
|
|
168
|
-
function getOAuthProviderSpec(name) {
|
|
169
|
-
if (name === 'google')
|
|
170
|
-
return exports.GOOGLE_OAUTH_SPEC;
|
|
171
|
-
if (name === 'github')
|
|
172
|
-
return exports.GITHUB_OAUTH_SPEC;
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|