@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,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectorRegistryService = exports.InMemoryAuthorizeStateStore = exports.ConnectorError = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
class ConnectorError extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status =
|
|
10
|
+
code === 'unknown_connector' || code === 'not_connected'
|
|
11
|
+
? 404
|
|
12
|
+
: code === 'not_configured'
|
|
13
|
+
? 409
|
|
14
|
+
: 400;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ConnectorError = ConnectorError;
|
|
18
|
+
class InMemoryAuthorizeStateStore {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.map = new Map();
|
|
21
|
+
}
|
|
22
|
+
set(state, value) {
|
|
23
|
+
this.map.set(state, value);
|
|
24
|
+
// 10-minute TTL: trash unconsumed states so dead callbacks don't pile up.
|
|
25
|
+
setTimeout(() => this.map.delete(state), 10 * 60 * 1000).unref?.();
|
|
26
|
+
}
|
|
27
|
+
consume(state) {
|
|
28
|
+
const v = this.map.get(state) ?? null;
|
|
29
|
+
if (v)
|
|
30
|
+
this.map.delete(state);
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.InMemoryAuthorizeStateStore = InMemoryAuthorizeStateStore;
|
|
35
|
+
/**
|
|
36
|
+
* Central registry for OAuth-based connectors. Holds the in-memory map of
|
|
37
|
+
* `ConnectorDefinition`s, drives the OAuth dance via `OAuth2Service`,
|
|
38
|
+
* encrypts/persists tokens via `cipher` + `authRepo`, and on each agent
|
|
39
|
+
* run synthesises per-user tool definitions whose handlers receive a
|
|
40
|
+
* fresh access token.
|
|
41
|
+
*
|
|
42
|
+
* The lifecycle is intentionally not "register tools globally at boot" —
|
|
43
|
+
* doing so would leak one user's tools to another. Instead, callers ask
|
|
44
|
+
* the registry for `toolsForUser(userId)` and merge the result into the
|
|
45
|
+
* ToolRegistry's per-call view.
|
|
46
|
+
*/
|
|
47
|
+
class ConnectorRegistryService {
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
this.defs = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Connectors whose client creds are present. The registry treats the
|
|
53
|
+
* "catalog" (which providers we *support*) as separate from the
|
|
54
|
+
* "configured" set (which ones can actually run an OAuth flow). Hosts
|
|
55
|
+
* call `setConfigured(id, def)` when secrets land and `markUnconfigured`
|
|
56
|
+
* when they're cleared — neither call removes the definition from the
|
|
57
|
+
* catalog, so the Directory always shows the full grid.
|
|
58
|
+
*/
|
|
59
|
+
this.configured = new Set();
|
|
60
|
+
this.stateStore = deps.stateStore ?? new InMemoryAuthorizeStateStore();
|
|
61
|
+
this.refreshSkewSeconds = deps.refreshSkewSeconds ?? 60;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Register a connector into the catalog. Catalog registration is one-shot
|
|
65
|
+
* (per-id) and unrelated to credential availability — call this at boot
|
|
66
|
+
* for every provider the platform supports. To then mark it as ready for
|
|
67
|
+
* OAuth flows, follow up with `setConfigured()` once creds resolve.
|
|
68
|
+
*/
|
|
69
|
+
register(def) {
|
|
70
|
+
if (this.defs.has(def.id)) {
|
|
71
|
+
throw new Error(`Connector "${def.id}" already registered`);
|
|
72
|
+
}
|
|
73
|
+
this.defs.set(def.id, def);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Mark a registered connector as configured AND refresh its definition.
|
|
77
|
+
* The definition is replaced because OAuth `clientId` / `clientSecret`
|
|
78
|
+
* live inside it — when the operator rotates creds the def itself
|
|
79
|
+
* changes. Existing per-user `ConnectorAuth` rows keep working: tokens
|
|
80
|
+
* are provider-issued, not tied to the client secret.
|
|
81
|
+
*/
|
|
82
|
+
setConfigured(def) {
|
|
83
|
+
this.defs.set(def.id, def);
|
|
84
|
+
this.configured.add(def.id);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mark a connector as unconfigured. The definition stays in the catalog
|
|
88
|
+
* (so the card keeps rendering with a "Needs admin setup" hint) but
|
|
89
|
+
* `startAuthorize` will refuse and per-user tools stop surfacing — we
|
|
90
|
+
* can't refresh tokens without client creds either, so silently dropping
|
|
91
|
+
* tools is friendlier than letting them 401 mid-call.
|
|
92
|
+
*/
|
|
93
|
+
markUnconfigured(connectorId) {
|
|
94
|
+
return this.configured.delete(connectorId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Hard-remove a connector from the catalog. Rarely needed — used by
|
|
98
|
+
* tests or to retire a deprecated provider. Per-user rows are NOT
|
|
99
|
+
* deleted automatically; the caller is responsible for that cleanup.
|
|
100
|
+
*/
|
|
101
|
+
unregister(connectorId) {
|
|
102
|
+
this.configured.delete(connectorId);
|
|
103
|
+
return this.defs.delete(connectorId);
|
|
104
|
+
}
|
|
105
|
+
isConfigured(connectorId) {
|
|
106
|
+
return this.configured.has(connectorId);
|
|
107
|
+
}
|
|
108
|
+
list() {
|
|
109
|
+
return Array.from(this.defs.values());
|
|
110
|
+
}
|
|
111
|
+
get(connectorId) {
|
|
112
|
+
const def = this.defs.get(connectorId);
|
|
113
|
+
if (!def) {
|
|
114
|
+
throw new ConnectorError('unknown_connector', `No connector registered with id "${connectorId}"`);
|
|
115
|
+
}
|
|
116
|
+
return def;
|
|
117
|
+
}
|
|
118
|
+
// ─── OAuth flow ──────────────────────────────────────────────────────────
|
|
119
|
+
async startAuthorize(connectorId, userId, redirectUri) {
|
|
120
|
+
const def = this.get(connectorId);
|
|
121
|
+
if (!this.configured.has(connectorId)) {
|
|
122
|
+
throw new ConnectorError('not_configured', `Connector "${connectorId}" is not configured — the operator must ` +
|
|
123
|
+
`set its OAuth credentials before users can connect.`);
|
|
124
|
+
}
|
|
125
|
+
const { url, state, pkceVerifier } = this.deps.oauth.buildAuthorizeUrl(def.oauth, redirectUri);
|
|
126
|
+
await this.stateStore.set(state, {
|
|
127
|
+
userId,
|
|
128
|
+
connectorId,
|
|
129
|
+
pkceVerifier,
|
|
130
|
+
createdAt: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
return { url };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Complete the OAuth callback. Verifies the `state`, exchanges the code
|
|
136
|
+
* for tokens, encrypts them, and upserts the `ConnectorAuth` row.
|
|
137
|
+
*
|
|
138
|
+
* Returns the resolved connector id so the caller can redirect the user
|
|
139
|
+
* back to the directory page with confirmation.
|
|
140
|
+
*/
|
|
141
|
+
async completeAuthorize(state, code, redirectUri) {
|
|
142
|
+
const ctx = await this.stateStore.consume(state);
|
|
143
|
+
if (!ctx) {
|
|
144
|
+
throw new ConnectorError('invalid_state', 'OAuth state is unknown or expired');
|
|
145
|
+
}
|
|
146
|
+
const def = this.get(ctx.connectorId);
|
|
147
|
+
let tokens;
|
|
148
|
+
try {
|
|
149
|
+
tokens = await this.deps.oauth.exchangeCode(def.oauth, code, redirectUri, ctx.pkceVerifier);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
throw new ConnectorError('token_exchange_failed', e.message);
|
|
153
|
+
}
|
|
154
|
+
await this.upsertAuth(ctx.userId, ctx.connectorId, tokens);
|
|
155
|
+
return { connectorId: ctx.connectorId, userId: ctx.userId };
|
|
156
|
+
}
|
|
157
|
+
// ─── Per-user listing ────────────────────────────────────────────────────
|
|
158
|
+
async listForUser(userId) {
|
|
159
|
+
const authed = await this.deps.authRepo.listForUser(userId);
|
|
160
|
+
const byId = new Map(authed.map((a) => [a.connectorId, a]));
|
|
161
|
+
return this.list().map((def) => {
|
|
162
|
+
const a = byId.get(def.id);
|
|
163
|
+
return {
|
|
164
|
+
connectorId: def.id,
|
|
165
|
+
name: def.name,
|
|
166
|
+
description: def.description,
|
|
167
|
+
category: def.category,
|
|
168
|
+
iconUrl: def.iconUrl,
|
|
169
|
+
configured: this.configured.has(def.id),
|
|
170
|
+
connected: !!a && a.isActive,
|
|
171
|
+
accountLabel: a?.accountLabel,
|
|
172
|
+
connectedAt: a?.createdAt,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async disconnect(userId, connectorId) {
|
|
177
|
+
const a = await this.deps.authRepo.findByUserAndConnector(userId, connectorId);
|
|
178
|
+
if (!a)
|
|
179
|
+
throw new ConnectorError('not_connected', 'No active auth row');
|
|
180
|
+
await this.deps.authRepo.delete(a.id);
|
|
181
|
+
}
|
|
182
|
+
// ─── Per-user tool synthesis ─────────────────────────────────────────────
|
|
183
|
+
/**
|
|
184
|
+
* Returns the toolbelt for `userId`: one `AgentToolDefinition` per tool
|
|
185
|
+
* of every connector the user has authorized. Each handler is bound to a
|
|
186
|
+
* `ConnectorToolContext` whose `getAccessToken` lazy-refreshes when the
|
|
187
|
+
* cached token is close to expiry.
|
|
188
|
+
*/
|
|
189
|
+
async toolsForUser(userId) {
|
|
190
|
+
const authed = await this.deps.authRepo.listForUser(userId);
|
|
191
|
+
const tools = [];
|
|
192
|
+
for (const a of authed) {
|
|
193
|
+
if (!a.isActive)
|
|
194
|
+
continue;
|
|
195
|
+
// Skip connectors whose creds were yanked from the secrets vault —
|
|
196
|
+
// we'd 401 inside the refresh step anyway, and the agent gets a
|
|
197
|
+
// useless tool. The auth row is left intact so the user gets their
|
|
198
|
+
// toolbelt back the moment the operator re-supplies creds.
|
|
199
|
+
if (!this.configured.has(a.connectorId))
|
|
200
|
+
continue;
|
|
201
|
+
const def = this.defs.get(a.connectorId);
|
|
202
|
+
if (!def)
|
|
203
|
+
continue;
|
|
204
|
+
const ctx = {
|
|
205
|
+
userId,
|
|
206
|
+
connectorId: a.connectorId,
|
|
207
|
+
getAccessToken: () => this.getAccessToken(a),
|
|
208
|
+
};
|
|
209
|
+
for (const factory of def.tools) {
|
|
210
|
+
tools.push({ ...factory.definition, execute: factory.build(ctx) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return tools;
|
|
214
|
+
}
|
|
215
|
+
// ─── Internals ───────────────────────────────────────────────────────────
|
|
216
|
+
async upsertAuth(userId, connectorId, tokens) {
|
|
217
|
+
const existing = await this.deps.authRepo.findByUserAndConnector(userId, connectorId);
|
|
218
|
+
const expiresAt = tokens.expiresIn
|
|
219
|
+
? new Date(Date.now() + tokens.expiresIn * 1000)
|
|
220
|
+
: undefined;
|
|
221
|
+
const accessTokenEncrypted = this.deps.cipher.encrypt(tokens.accessToken);
|
|
222
|
+
const refreshTokenEncrypted = tokens.refreshToken
|
|
223
|
+
? this.deps.cipher.encrypt(tokens.refreshToken)
|
|
224
|
+
: undefined;
|
|
225
|
+
if (existing) {
|
|
226
|
+
await this.deps.authRepo.update(existing.id, {
|
|
227
|
+
accessTokenEncrypted,
|
|
228
|
+
// Some providers (Google) don't return a refresh_token on
|
|
229
|
+
// re-consent — keep the old one in that case.
|
|
230
|
+
...(refreshTokenEncrypted ? { refreshTokenEncrypted } : {}),
|
|
231
|
+
expiresAt,
|
|
232
|
+
scope: tokens.scope,
|
|
233
|
+
isActive: true,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
await this.deps.authRepo.create({
|
|
238
|
+
id: (0, crypto_1.randomUUID)(),
|
|
239
|
+
userId,
|
|
240
|
+
connectorId,
|
|
241
|
+
accessTokenEncrypted,
|
|
242
|
+
refreshTokenEncrypted,
|
|
243
|
+
expiresAt,
|
|
244
|
+
scope: tokens.scope,
|
|
245
|
+
isActive: true,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async getAccessToken(a) {
|
|
250
|
+
const skewMs = this.refreshSkewSeconds * 1000;
|
|
251
|
+
const stillValid = !a.expiresAt || a.expiresAt.getTime() - Date.now() > skewMs;
|
|
252
|
+
if (stillValid) {
|
|
253
|
+
return this.deps.cipher.decrypt(a.accessTokenEncrypted);
|
|
254
|
+
}
|
|
255
|
+
if (!a.refreshTokenEncrypted) {
|
|
256
|
+
// No refresh token + expired access token = the user has to re-auth.
|
|
257
|
+
// Returning the stale token would just produce a confusing 401 from
|
|
258
|
+
// the provider; surfacing it explicitly is friendlier.
|
|
259
|
+
throw new ConnectorError('not_connected', `Token for ${a.connectorId} expired and no refresh token is available; user must re-authorize`);
|
|
260
|
+
}
|
|
261
|
+
const def = this.get(a.connectorId);
|
|
262
|
+
const refreshPlain = this.deps.cipher.decrypt(a.refreshTokenEncrypted);
|
|
263
|
+
const fresh = await this.deps.oauth.refresh(def.oauth, refreshPlain);
|
|
264
|
+
const expiresAt = fresh.expiresIn
|
|
265
|
+
? new Date(Date.now() + fresh.expiresIn * 1000)
|
|
266
|
+
: undefined;
|
|
267
|
+
await this.deps.authRepo.update(a.id, {
|
|
268
|
+
accessTokenEncrypted: this.deps.cipher.encrypt(fresh.accessToken),
|
|
269
|
+
...(fresh.refreshToken
|
|
270
|
+
? { refreshTokenEncrypted: this.deps.cipher.encrypt(fresh.refreshToken) }
|
|
271
|
+
: {}),
|
|
272
|
+
expiresAt,
|
|
273
|
+
scope: fresh.scope,
|
|
274
|
+
});
|
|
275
|
+
return fresh.accessToken;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
exports.ConnectorRegistryService = ConnectorRegistryService;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ConversationRepository, MessageRepository } from '../repositories';
|
|
2
|
+
import type { Conversation, Message } from '../domain/conversation';
|
|
3
|
+
import type { ConversationHistory, TokenUsage, ToolCallRecord } from '../types/agent.types';
|
|
4
|
+
export declare class ConversationNotFoundError extends Error {
|
|
5
|
+
status: number;
|
|
6
|
+
code: string;
|
|
7
|
+
constructor(id: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class ConversationService {
|
|
10
|
+
private readonly convRepo;
|
|
11
|
+
private readonly msgRepo;
|
|
12
|
+
constructor(convRepo: ConversationRepository, msgRepo: MessageRepository);
|
|
13
|
+
create(params: {
|
|
14
|
+
userId: string;
|
|
15
|
+
agentId: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
}): Promise<Conversation>;
|
|
19
|
+
addMessage(params: {
|
|
20
|
+
conversationId: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
role: 'user' | 'assistant' | 'system';
|
|
23
|
+
content: string;
|
|
24
|
+
toolCalls?: ToolCallRecord[];
|
|
25
|
+
usage?: TokenUsage;
|
|
26
|
+
}): Promise<Message>;
|
|
27
|
+
/** Throws on not-found OR cross-user access. */
|
|
28
|
+
private loadOwned;
|
|
29
|
+
getHistory(conversationId: string, userId: string): Promise<ConversationHistory>;
|
|
30
|
+
getAnthropicMessages(conversationId: string, userId: string): Promise<Array<{
|
|
31
|
+
role: 'user' | 'assistant';
|
|
32
|
+
content: string;
|
|
33
|
+
}>>;
|
|
34
|
+
listForUser(userId: string, options?: {
|
|
35
|
+
agentId?: string;
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
}): Promise<Conversation[]>;
|
|
39
|
+
ensureOwned(conversationId: string, userId: string): Promise<Conversation>;
|
|
40
|
+
/**
|
|
41
|
+
* Mark a conversation as `completed`. Callers should already have verified
|
|
42
|
+
* ownership; this just touches the status column. We keep the transcript
|
|
43
|
+
* untouched — the row stays around for admin/audit views.
|
|
44
|
+
*/
|
|
45
|
+
markCompleted(conversationId: string): Promise<void>;
|
|
46
|
+
private mapMessage;
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConversationService = exports.ConversationNotFoundError = void 0;
|
|
4
|
+
class ConversationNotFoundError extends Error {
|
|
5
|
+
constructor(id) {
|
|
6
|
+
super(`Conversation "${id}" not found`);
|
|
7
|
+
this.status = 404;
|
|
8
|
+
this.code = 'conversation_not_found';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.ConversationNotFoundError = ConversationNotFoundError;
|
|
12
|
+
class ConversationService {
|
|
13
|
+
constructor(convRepo, msgRepo) {
|
|
14
|
+
this.convRepo = convRepo;
|
|
15
|
+
this.msgRepo = msgRepo;
|
|
16
|
+
}
|
|
17
|
+
async create(params) {
|
|
18
|
+
return this.convRepo.create({
|
|
19
|
+
userId: params.userId,
|
|
20
|
+
agentId: params.agentId,
|
|
21
|
+
title: params.title,
|
|
22
|
+
metadata: params.metadata,
|
|
23
|
+
status: 'active',
|
|
24
|
+
totalInputTokens: 0,
|
|
25
|
+
totalOutputTokens: 0,
|
|
26
|
+
messageCount: 0,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async addMessage(params) {
|
|
30
|
+
const msg = await this.msgRepo.create({
|
|
31
|
+
conversationId: params.conversationId,
|
|
32
|
+
userId: params.userId,
|
|
33
|
+
role: params.role,
|
|
34
|
+
content: params.content,
|
|
35
|
+
toolCalls: params.toolCalls,
|
|
36
|
+
usage: params.usage,
|
|
37
|
+
});
|
|
38
|
+
await this.convRepo.updateStats(params.conversationId, {
|
|
39
|
+
addInputTokens: params.usage?.inputTokens,
|
|
40
|
+
addOutputTokens: params.usage?.outputTokens,
|
|
41
|
+
addMessages: 1,
|
|
42
|
+
});
|
|
43
|
+
return msg;
|
|
44
|
+
}
|
|
45
|
+
/** Throws on not-found OR cross-user access. */
|
|
46
|
+
async loadOwned(conversationId, userId) {
|
|
47
|
+
const conv = await this.convRepo.findByIdForUser(conversationId, userId);
|
|
48
|
+
if (!conv) {
|
|
49
|
+
// Ambiguous on purpose — don't reveal whether the conversation exists or
|
|
50
|
+
// belongs to someone else.
|
|
51
|
+
throw new ConversationNotFoundError(conversationId);
|
|
52
|
+
}
|
|
53
|
+
return conv;
|
|
54
|
+
}
|
|
55
|
+
async getHistory(conversationId, userId) {
|
|
56
|
+
const conv = await this.loadOwned(conversationId, userId);
|
|
57
|
+
const messages = await this.msgRepo.listForConversation(conv.id, userId);
|
|
58
|
+
return {
|
|
59
|
+
conversationId: conv.id,
|
|
60
|
+
agentId: conv.agentId,
|
|
61
|
+
userId: conv.userId,
|
|
62
|
+
title: conv.title,
|
|
63
|
+
status: conv.status,
|
|
64
|
+
messages: messages.map(this.mapMessage),
|
|
65
|
+
createdAt: conv.createdAt,
|
|
66
|
+
updatedAt: conv.updatedAt,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async getAnthropicMessages(conversationId, userId) {
|
|
70
|
+
const conv = await this.loadOwned(conversationId, userId);
|
|
71
|
+
const messages = await this.msgRepo.listForConversation(conv.id, userId);
|
|
72
|
+
return messages
|
|
73
|
+
.filter((m) => m.role !== 'system')
|
|
74
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
75
|
+
}
|
|
76
|
+
async listForUser(userId, options = {}) {
|
|
77
|
+
return this.convRepo.listForUser(userId, options);
|
|
78
|
+
}
|
|
79
|
+
async ensureOwned(conversationId, userId) {
|
|
80
|
+
return this.loadOwned(conversationId, userId);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Mark a conversation as `completed`. Callers should already have verified
|
|
84
|
+
* ownership; this just touches the status column. We keep the transcript
|
|
85
|
+
* untouched — the row stays around for admin/audit views.
|
|
86
|
+
*/
|
|
87
|
+
async markCompleted(conversationId) {
|
|
88
|
+
await this.convRepo.updateStats(conversationId, { status: 'completed' });
|
|
89
|
+
}
|
|
90
|
+
mapMessage(msg) {
|
|
91
|
+
return {
|
|
92
|
+
id: msg.id,
|
|
93
|
+
role: msg.role,
|
|
94
|
+
content: msg.content,
|
|
95
|
+
toolCalls: msg.toolCalls,
|
|
96
|
+
usage: msg.usage,
|
|
97
|
+
createdAt: msg.createdAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.ConversationService = ConversationService;
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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;
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PreparedStreamPayload, PreparedStreamStore } from '../adapters/prepared-stream/prepared-stream.types';
|
|
2
|
+
/**
|
|
3
|
+
* Default in-memory implementation. Single-instance deploys only. For multi-
|
|
4
|
+
* instance use the Redis-backed store (in @agentforge-io/nest or your own impl).
|
|
5
|
+
*/
|
|
6
|
+
export declare class InMemoryPreparedStreamStore implements PreparedStreamStore {
|
|
7
|
+
private readonly entries;
|
|
8
|
+
private sweeper?;
|
|
9
|
+
constructor();
|
|
10
|
+
put(streamId: string, payload: PreparedStreamPayload): Promise<void>;
|
|
11
|
+
takeOnce(streamId: string): Promise<PreparedStreamPayload | null>;
|
|
12
|
+
private sweep;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryPreparedStreamStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Default in-memory implementation. Single-instance deploys only. For multi-
|
|
6
|
+
* instance use the Redis-backed store (in @agentforge-io/nest or your own impl).
|
|
7
|
+
*/
|
|
8
|
+
class InMemoryPreparedStreamStore {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.entries = new Map();
|
|
11
|
+
this.sweeper = setInterval(() => this.sweep(), 60_000);
|
|
12
|
+
if (typeof this.sweeper.unref === 'function')
|
|
13
|
+
this.sweeper.unref();
|
|
14
|
+
}
|
|
15
|
+
async put(streamId, payload) {
|
|
16
|
+
this.entries.set(streamId, payload);
|
|
17
|
+
}
|
|
18
|
+
async takeOnce(streamId) {
|
|
19
|
+
const entry = this.entries.get(streamId);
|
|
20
|
+
if (!entry)
|
|
21
|
+
return null;
|
|
22
|
+
this.entries.delete(streamId);
|
|
23
|
+
if (entry.expiresAt <= new Date())
|
|
24
|
+
return null;
|
|
25
|
+
return entry;
|
|
26
|
+
}
|
|
27
|
+
sweep() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const [id, entry] of this.entries) {
|
|
30
|
+
if (entry.expiresAt.getTime() <= now)
|
|
31
|
+
this.entries.delete(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.InMemoryPreparedStreamStore = InMemoryPreparedStreamStore;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { ToolRegistryService, type Logger } from './tool-registry.service';
|
|
2
|
+
export { AgentRunnerService } from './agent-runner.service';
|
|
3
|
+
export { PreparedStreamService, PreparedStreamError, } from './prepared-stream.service';
|
|
4
|
+
export { InMemoryPreparedStreamStore } from './in-memory-prepared-stream.store';
|
|
5
|
+
export { ConversationService, ConversationNotFoundError, } from './conversation.service';
|
|
6
|
+
export { AgentService, AgentForbiddenError, type AgentNotFoundError, } from './agent.service';
|
|
7
|
+
export { OrchestratorService, OrchestratorError, type OrchestratorServiceOptions, } from './orchestrator.service';
|
|
8
|
+
export { AgentJobWorker } from './agent-job.worker';
|
|
9
|
+
export { ChatTokenService, ChatTokenError, type CreateChatTokenInput, } from './chat-token.service';
|
|
10
|
+
export { McpClientService, McpClientError, type McpToolHandle, } from './mcp-client.service';
|
|
11
|
+
export { McpServerService, McpServerError, type CreateMcpServerInput, } from './mcp-server.service';
|
|
12
|
+
export { OAuth2Service, type OAuth2ProviderConfig, type AuthorizeUrlResult, type TokenSet, } from './oauth2.service';
|
|
13
|
+
export { ConnectorRegistryService, ConnectorError, InMemoryAuthorizeStateStore, type AuthorizeState, type AuthorizeStateStore, type ConnectorStatus, type TokenCipher, } from './connector-registry.service';
|