@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.
Files changed (151) hide show
  1. package/dist/adapters/billing/billing-adapter.interface.d.ts +41 -0
  2. package/dist/adapters/billing/billing-adapter.interface.js +5 -0
  3. package/dist/adapters/billing/stripe/stripe.adapter.d.ts +30 -0
  4. package/dist/adapters/billing/stripe/stripe.adapter.js +122 -0
  5. package/dist/adapters/email/email-adapter.interface.d.ts +25 -0
  6. package/dist/adapters/email/email-adapter.interface.js +6 -0
  7. package/dist/adapters/email/noop.adapter.d.ts +10 -0
  8. package/dist/adapters/email/noop.adapter.js +15 -0
  9. package/dist/adapters/email/resend.adapter.d.ts +8 -0
  10. package/dist/adapters/email/resend.adapter.js +39 -0
  11. package/dist/adapters/job-queue/in-memory.d.ts +43 -0
  12. package/dist/adapters/job-queue/in-memory.js +154 -0
  13. package/dist/adapters/job-queue/job-queue.types.d.ts +76 -0
  14. package/dist/adapters/job-queue/job-queue.types.js +5 -0
  15. package/dist/adapters/prepared-stream/prepared-stream.types.d.ts +23 -0
  16. package/dist/adapters/prepared-stream/prepared-stream.types.js +5 -0
  17. package/dist/adapters/rate-limiter/in-memory.d.ts +19 -0
  18. package/dist/adapters/rate-limiter/in-memory.js +63 -0
  19. package/dist/adapters/rate-limiter/rate-limiter.types.d.ts +42 -0
  20. package/dist/adapters/rate-limiter/rate-limiter.types.js +5 -0
  21. package/dist/adapters/rate-limiter/redis.d.ts +31 -0
  22. package/dist/adapters/rate-limiter/redis.js +47 -0
  23. package/dist/adapters/upload/noop.adapter.d.ts +9 -0
  24. package/dist/adapters/upload/noop.adapter.js +14 -0
  25. package/dist/adapters/upload/s3.adapter.d.ts +38 -0
  26. package/dist/adapters/upload/s3.adapter.js +69 -0
  27. package/dist/adapters/upload/upload-adapter.interface.d.ts +37 -0
  28. package/dist/adapters/upload/upload-adapter.interface.js +15 -0
  29. package/dist/ai/index.d.ts +15 -0
  30. package/dist/ai/index.js +43 -0
  31. package/dist/billing/index.d.ts +12 -0
  32. package/dist/billing/index.js +28 -0
  33. package/dist/constants.d.ts +3 -0
  34. package/dist/constants.js +8 -0
  35. package/dist/domain/agent.d.ts +59 -0
  36. package/dist/domain/agent.js +2 -0
  37. package/dist/domain/api-key.d.ts +28 -0
  38. package/dist/domain/api-key.js +2 -0
  39. package/dist/domain/auth-identity.d.ts +10 -0
  40. package/dist/domain/auth-identity.js +2 -0
  41. package/dist/domain/chat-token.d.ts +39 -0
  42. package/dist/domain/chat-token.js +2 -0
  43. package/dist/domain/connector-auth.d.ts +42 -0
  44. package/dist/domain/connector-auth.js +2 -0
  45. package/dist/domain/connector.d.ts +52 -0
  46. package/dist/domain/connector.js +2 -0
  47. package/dist/domain/conversation.d.ts +26 -0
  48. package/dist/domain/conversation.js +2 -0
  49. package/dist/domain/email-token.d.ts +11 -0
  50. package/dist/domain/email-token.js +2 -0
  51. package/dist/domain/external-user.d.ts +23 -0
  52. package/dist/domain/external-user.js +2 -0
  53. package/dist/domain/index.d.ts +5 -0
  54. package/dist/domain/index.js +24 -0
  55. package/dist/domain/mcp-server.d.ts +33 -0
  56. package/dist/domain/mcp-server.js +2 -0
  57. package/dist/domain/plan.d.ts +20 -0
  58. package/dist/domain/plan.js +2 -0
  59. package/dist/domain/platform-secret.d.ts +24 -0
  60. package/dist/domain/platform-secret.js +8 -0
  61. package/dist/domain/refresh-token.d.ts +15 -0
  62. package/dist/domain/refresh-token.js +2 -0
  63. package/dist/domain/subscription.d.ts +21 -0
  64. package/dist/domain/subscription.js +2 -0
  65. package/dist/domain/tenant.d.ts +21 -0
  66. package/dist/domain/tenant.js +2 -0
  67. package/dist/domain/usage-record.d.ts +15 -0
  68. package/dist/domain/usage-record.js +2 -0
  69. package/dist/domain/user.d.ts +43 -0
  70. package/dist/domain/user.js +2 -0
  71. package/dist/factory.d.ts +68 -0
  72. package/dist/factory.js +56 -0
  73. package/dist/index.d.ts +14 -0
  74. package/dist/index.js +59 -0
  75. package/dist/repositories/in-memory.d.ts +30 -0
  76. package/dist/repositories/in-memory.js +82 -0
  77. package/dist/repositories/index.d.ts +67 -0
  78. package/dist/repositories/index.js +16 -0
  79. package/dist/services/agent-config.service.d.ts +45 -0
  80. package/dist/services/agent-config.service.js +114 -0
  81. package/dist/services/agent-job.worker.d.ts +32 -0
  82. package/dist/services/agent-job.worker.js +97 -0
  83. package/dist/services/agent-runner.service.d.ts +35 -0
  84. package/dist/services/agent-runner.service.js +224 -0
  85. package/dist/services/agent.service.d.ts +171 -0
  86. package/dist/services/agent.service.js +329 -0
  87. package/dist/services/api-key.service.d.ts +41 -0
  88. package/dist/services/api-key.service.js +80 -0
  89. package/dist/services/auth.service.d.ts +133 -0
  90. package/dist/services/auth.service.js +411 -0
  91. package/dist/services/billing.service.d.ts +67 -0
  92. package/dist/services/billing.service.js +254 -0
  93. package/dist/services/chat-token.service.d.ts +29 -0
  94. package/dist/services/chat-token.service.js +113 -0
  95. package/dist/services/connector-registry.service.d.ts +156 -0
  96. package/dist/services/connector-registry.service.js +278 -0
  97. package/dist/services/conversation.service.d.ts +47 -0
  98. package/dist/services/conversation.service.js +101 -0
  99. package/dist/services/email-templates.d.ts +18 -0
  100. package/dist/services/email-templates.js +39 -0
  101. package/dist/services/email.service.d.ts +26 -0
  102. package/dist/services/email.service.js +42 -0
  103. package/dist/services/errors.d.ts +7 -0
  104. package/dist/services/errors.js +27 -0
  105. package/dist/services/in-memory-prepared-stream.store.d.ts +13 -0
  106. package/dist/services/in-memory-prepared-stream.store.js +35 -0
  107. package/dist/services/index.d.ts +13 -0
  108. package/dist/services/index.js +40 -0
  109. package/dist/services/mcp-client.service.d.ts +64 -0
  110. package/dist/services/mcp-client.service.js +157 -0
  111. package/dist/services/mcp-server.service.d.ts +44 -0
  112. package/dist/services/mcp-server.service.js +147 -0
  113. package/dist/services/oauth.service.d.ts +73 -0
  114. package/dist/services/oauth.service.js +174 -0
  115. package/dist/services/oauth2.service.d.ts +57 -0
  116. package/dist/services/oauth2.service.js +82 -0
  117. package/dist/services/orchestrator.service.d.ts +45 -0
  118. package/dist/services/orchestrator.service.js +180 -0
  119. package/dist/services/plan.service.d.ts +54 -0
  120. package/dist/services/plan.service.js +120 -0
  121. package/dist/services/prepared-stream.service.d.ts +23 -0
  122. package/dist/services/prepared-stream.service.js +43 -0
  123. package/dist/services/refresh-token.service.d.ts +38 -0
  124. package/dist/services/refresh-token.service.js +73 -0
  125. package/dist/services/secrets/crypto.d.ts +37 -0
  126. package/dist/services/secrets/crypto.js +110 -0
  127. package/dist/services/secrets/known-keys.d.ts +38 -0
  128. package/dist/services/secrets/known-keys.js +50 -0
  129. package/dist/services/secrets.service.d.ts +91 -0
  130. package/dist/services/secrets.service.js +193 -0
  131. package/dist/services/tenant-billing.service.d.ts +121 -0
  132. package/dist/services/tenant-billing.service.js +290 -0
  133. package/dist/services/tenant.service.d.ts +54 -0
  134. package/dist/services/tenant.service.js +96 -0
  135. package/dist/services/tool-registry.service.d.ts +42 -0
  136. package/dist/services/tool-registry.service.js +101 -0
  137. package/dist/services/upload.service.d.ts +37 -0
  138. package/dist/services/upload.service.js +84 -0
  139. package/dist/services/usage.service.d.ts +34 -0
  140. package/dist/services/usage.service.js +108 -0
  141. package/dist/types/agent.types.d.ts +160 -0
  142. package/dist/types/agent.types.js +2 -0
  143. package/dist/types/billing.types.d.ts +82 -0
  144. package/dist/types/billing.types.js +3 -0
  145. package/dist/types/config.types.d.ts +127 -0
  146. package/dist/types/config.types.js +9 -0
  147. package/dist/types/hooks.d.ts +85 -0
  148. package/dist/types/hooks.js +2 -0
  149. package/dist/types/index.d.ts +3 -0
  150. package/dist/types/index.js +19 -0
  151. package/package.json +36 -0
@@ -0,0 +1,254 @@
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;
@@ -0,0 +1,29 @@
1
+ import type { ChatToken, ChatTokenPatch } from '../domain/chat-token';
2
+ import type { ChatTokenRepository } from '../repositories';
3
+ export declare class ChatTokenError extends Error {
4
+ status: number;
5
+ code: 'not_found' | 'invalid' | 'origin_not_allowed' | 'revoked';
6
+ constructor(code: ChatTokenError['code'], message: string);
7
+ }
8
+ export interface CreateChatTokenInput {
9
+ tenantId: string;
10
+ agentId: string;
11
+ name: string;
12
+ allowedOrigins?: string[];
13
+ createdByUserId?: string;
14
+ }
15
+ export declare class ChatTokenService {
16
+ private readonly repo;
17
+ constructor(repo: ChatTokenRepository);
18
+ create(input: CreateChatTokenInput): Promise<ChatToken>;
19
+ listForAgent(tenantId: string, agentId: string): Promise<ChatToken[]>;
20
+ deactivate(token: string): Promise<void>;
21
+ update(token: string, patch: ChatTokenPatch): Promise<void>;
22
+ /**
23
+ * Public-path entry point. Returns the resolved token row, or throws with
24
+ * a code the HTTP layer maps to 403/404. Also pings `last_used_at` so the
25
+ * admin UI can show which tokens are actually live traffic.
26
+ */
27
+ authorize(rawToken: string, origin: string | undefined): Promise<ChatToken>;
28
+ private generateToken;
29
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChatTokenService = exports.ChatTokenError = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class ChatTokenError extends Error {
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.status =
10
+ code === 'not_found' ? 404 : code === 'origin_not_allowed' ? 403 : code === 'revoked' ? 403 : 400;
11
+ }
12
+ }
13
+ exports.ChatTokenError = ChatTokenError;
14
+ /** Public token prefix to make stolen ones easy to spot in logs. 32 hex
15
+ * bytes ≈ 128 bits of entropy — plenty for a public-but-rotatable secret. */
16
+ const TOKEN_PREFIX = 'aft_';
17
+ const TOKEN_BYTES = 32;
18
+ class ChatTokenService {
19
+ constructor(repo) {
20
+ this.repo = repo;
21
+ }
22
+ async create(input) {
23
+ if (!input.tenantId)
24
+ throw new ChatTokenError('invalid', 'tenantId is required');
25
+ if (!input.agentId)
26
+ throw new ChatTokenError('invalid', 'agentId is required');
27
+ if (!input.name?.trim())
28
+ throw new ChatTokenError('invalid', 'name is required');
29
+ const origins = (input.allowedOrigins ?? [])
30
+ .map((o) => o.trim())
31
+ .filter(Boolean);
32
+ for (const o of origins) {
33
+ if (/\s/.test(o)) {
34
+ throw new ChatTokenError('invalid', `Invalid origin: "${o}"`);
35
+ }
36
+ }
37
+ const record = {
38
+ token: this.generateToken(),
39
+ tenantId: input.tenantId,
40
+ agentId: input.agentId,
41
+ name: input.name.trim(),
42
+ allowedOrigins: origins.length > 0 ? origins : undefined,
43
+ isActive: true,
44
+ createdByUserId: input.createdByUserId,
45
+ };
46
+ return this.repo.create(record);
47
+ }
48
+ async listForAgent(tenantId, agentId) {
49
+ return this.repo.listForAgent(tenantId, agentId);
50
+ }
51
+ async deactivate(token) {
52
+ await this.repo.deactivate(token);
53
+ }
54
+ async update(token, patch) {
55
+ await this.repo.update(token, patch);
56
+ }
57
+ /**
58
+ * Public-path entry point. Returns the resolved token row, or throws with
59
+ * a code the HTTP layer maps to 403/404. Also pings `last_used_at` so the
60
+ * admin UI can show which tokens are actually live traffic.
61
+ */
62
+ async authorize(rawToken, origin) {
63
+ if (!rawToken)
64
+ throw new ChatTokenError('not_found', 'Missing chat token');
65
+ const row = await this.repo.findByToken(rawToken);
66
+ if (!row)
67
+ throw new ChatTokenError('not_found', 'Chat token not found');
68
+ if (!row.isActive)
69
+ throw new ChatTokenError('revoked', 'Chat token has been revoked');
70
+ if (row.allowedOrigins && row.allowedOrigins.length > 0) {
71
+ if (!origin) {
72
+ throw new ChatTokenError('origin_not_allowed', 'This widget can only be used from an allowed origin (Origin header missing).');
73
+ }
74
+ if (!matchesAllowedOrigin(origin, row.allowedOrigins)) {
75
+ throw new ChatTokenError('origin_not_allowed', `Origin "${origin}" is not allowed for this widget.`);
76
+ }
77
+ }
78
+ // Fire-and-forget: don't make the request wait on this write.
79
+ this.repo.touch(rawToken).catch(() => { });
80
+ return row;
81
+ }
82
+ generateToken() {
83
+ return `${TOKEN_PREFIX}${(0, crypto_1.randomBytes)(TOKEN_BYTES).toString('hex')}`;
84
+ }
85
+ }
86
+ exports.ChatTokenService = ChatTokenService;
87
+ /**
88
+ * Origin matching: each allowed entry can be either a full origin
89
+ * ("https://acme.com") or just a host ("acme.com"). Exact match only — no
90
+ * wildcards on purpose.
91
+ */
92
+ function matchesAllowedOrigin(origin, allowed) {
93
+ const reqHost = hostnameOf(origin);
94
+ if (!reqHost)
95
+ return false;
96
+ for (const entry of allowed) {
97
+ const allowedHost = hostnameOf(entry) ?? entry.toLowerCase();
98
+ if (allowedHost === reqHost)
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+ function hostnameOf(value) {
104
+ try {
105
+ return new URL(value).hostname.toLowerCase();
106
+ }
107
+ catch {
108
+ // Bare hostname like "acme.com" — URL ctor rejects it without scheme.
109
+ if (/^[a-z0-9.-]+$/i.test(value))
110
+ return value.toLowerCase();
111
+ return null;
112
+ }
113
+ }
@@ -0,0 +1,156 @@
1
+ import type { ConnectorDefinition } from '../domain';
2
+ import type { ConnectorAuthRepository } from '../repositories';
3
+ import type { OAuth2Service } from './oauth2.service';
4
+ import type { ToolRegistryService } from './tool-registry.service';
5
+ import type { AgentToolDefinition } from '../types';
6
+ export declare class ConnectorError extends Error {
7
+ status: number;
8
+ code: 'unknown_connector' | 'not_connected' | 'not_configured' | 'invalid_state' | 'token_exchange_failed';
9
+ constructor(code: ConnectorError['code'], message: string);
10
+ }
11
+ /**
12
+ * Host-supplied symmetric cipher for encrypting OAuth tokens at rest.
13
+ * Implemented in the platform's SecretsModule (AES-256-GCM with MASTER_KEY).
14
+ * Kept as an interface so @agentforge-io/core stays infrastructure-agnostic.
15
+ */
16
+ export interface TokenCipher {
17
+ encrypt(plaintext: string): Buffer;
18
+ decrypt(blob: Buffer): string;
19
+ }
20
+ /**
21
+ * Transient state stashed between `startAuthorize` and the OAuth callback.
22
+ * Persisted in the store so the callback (which arrives as a separate HTTP
23
+ * request) can pick up the connector id and the user it's for.
24
+ */
25
+ export interface AuthorizeState {
26
+ userId: string;
27
+ connectorId: string;
28
+ pkceVerifier?: string;
29
+ createdAt: number;
30
+ }
31
+ /**
32
+ * Pluggable storage for the short-lived state map. Defaults to an
33
+ * in-memory implementation; production deployments behind multiple
34
+ * server instances should plug in a Redis-backed store.
35
+ */
36
+ export interface AuthorizeStateStore {
37
+ set(state: string, value: AuthorizeState): Promise<void> | void;
38
+ consume(state: string): Promise<AuthorizeState | null> | AuthorizeState | null;
39
+ }
40
+ export declare class InMemoryAuthorizeStateStore implements AuthorizeStateStore {
41
+ private readonly map;
42
+ set(state: string, value: AuthorizeState): void;
43
+ consume(state: string): AuthorizeState | null;
44
+ }
45
+ export interface ConnectorStatus {
46
+ connectorId: string;
47
+ name: string;
48
+ description: string;
49
+ category?: string;
50
+ iconUrl?: string;
51
+ /** `true` when the operator has supplied client creds for this provider.
52
+ * When `false`, the user cannot click Connect — the card shows a "Needs
53
+ * admin setup" hint instead. Kept distinct from `connected` so the
54
+ * Directory can render the full catalog even on a freshly-installed
55
+ * platform. */
56
+ configured: boolean;
57
+ connected: boolean;
58
+ accountLabel?: string;
59
+ connectedAt?: Date;
60
+ }
61
+ interface ConnectorRegistryDeps {
62
+ oauth: OAuth2Service;
63
+ authRepo: ConnectorAuthRepository;
64
+ cipher: TokenCipher;
65
+ toolRegistry: ToolRegistryService;
66
+ stateStore?: AuthorizeStateStore;
67
+ /** Refresh access tokens this many seconds before they expire so a
68
+ * tool call started right at the edge doesn't 401 mid-flight. */
69
+ refreshSkewSeconds?: number;
70
+ }
71
+ /**
72
+ * Central registry for OAuth-based connectors. Holds the in-memory map of
73
+ * `ConnectorDefinition`s, drives the OAuth dance via `OAuth2Service`,
74
+ * encrypts/persists tokens via `cipher` + `authRepo`, and on each agent
75
+ * run synthesises per-user tool definitions whose handlers receive a
76
+ * fresh access token.
77
+ *
78
+ * The lifecycle is intentionally not "register tools globally at boot" —
79
+ * doing so would leak one user's tools to another. Instead, callers ask
80
+ * the registry for `toolsForUser(userId)` and merge the result into the
81
+ * ToolRegistry's per-call view.
82
+ */
83
+ export declare class ConnectorRegistryService {
84
+ private readonly deps;
85
+ private readonly defs;
86
+ /**
87
+ * Connectors whose client creds are present. The registry treats the
88
+ * "catalog" (which providers we *support*) as separate from the
89
+ * "configured" set (which ones can actually run an OAuth flow). Hosts
90
+ * call `setConfigured(id, def)` when secrets land and `markUnconfigured`
91
+ * when they're cleared — neither call removes the definition from the
92
+ * catalog, so the Directory always shows the full grid.
93
+ */
94
+ private readonly configured;
95
+ private readonly stateStore;
96
+ private readonly refreshSkewSeconds;
97
+ constructor(deps: ConnectorRegistryDeps);
98
+ /**
99
+ * Register a connector into the catalog. Catalog registration is one-shot
100
+ * (per-id) and unrelated to credential availability — call this at boot
101
+ * for every provider the platform supports. To then mark it as ready for
102
+ * OAuth flows, follow up with `setConfigured()` once creds resolve.
103
+ */
104
+ register(def: ConnectorDefinition): void;
105
+ /**
106
+ * Mark a registered connector as configured AND refresh its definition.
107
+ * The definition is replaced because OAuth `clientId` / `clientSecret`
108
+ * live inside it — when the operator rotates creds the def itself
109
+ * changes. Existing per-user `ConnectorAuth` rows keep working: tokens
110
+ * are provider-issued, not tied to the client secret.
111
+ */
112
+ setConfigured(def: ConnectorDefinition): void;
113
+ /**
114
+ * Mark a connector as unconfigured. The definition stays in the catalog
115
+ * (so the card keeps rendering with a "Needs admin setup" hint) but
116
+ * `startAuthorize` will refuse and per-user tools stop surfacing — we
117
+ * can't refresh tokens without client creds either, so silently dropping
118
+ * tools is friendlier than letting them 401 mid-call.
119
+ */
120
+ markUnconfigured(connectorId: string): boolean;
121
+ /**
122
+ * Hard-remove a connector from the catalog. Rarely needed — used by
123
+ * tests or to retire a deprecated provider. Per-user rows are NOT
124
+ * deleted automatically; the caller is responsible for that cleanup.
125
+ */
126
+ unregister(connectorId: string): boolean;
127
+ isConfigured(connectorId: string): boolean;
128
+ list(): ConnectorDefinition[];
129
+ get(connectorId: string): ConnectorDefinition;
130
+ startAuthorize(connectorId: string, userId: string, redirectUri: string): Promise<{
131
+ url: string;
132
+ }>;
133
+ /**
134
+ * Complete the OAuth callback. Verifies the `state`, exchanges the code
135
+ * for tokens, encrypts them, and upserts the `ConnectorAuth` row.
136
+ *
137
+ * Returns the resolved connector id so the caller can redirect the user
138
+ * back to the directory page with confirmation.
139
+ */
140
+ completeAuthorize(state: string, code: string, redirectUri: string): Promise<{
141
+ connectorId: string;
142
+ userId: string;
143
+ }>;
144
+ listForUser(userId: string): Promise<ConnectorStatus[]>;
145
+ disconnect(userId: string, connectorId: string): Promise<void>;
146
+ /**
147
+ * Returns the toolbelt for `userId`: one `AgentToolDefinition` per tool
148
+ * of every connector the user has authorized. Each handler is bound to a
149
+ * `ConnectorToolContext` whose `getAccessToken` lazy-refreshes when the
150
+ * cached token is close to expiry.
151
+ */
152
+ toolsForUser(userId: string): Promise<AgentToolDefinition[]>;
153
+ private upsertAuth;
154
+ private getAccessToken;
155
+ }
156
+ export {};