@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,329 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentService = exports.AgentForbiddenError = void 0;
4
+ class AgentForbiddenError extends Error {
5
+ constructor(reason) {
6
+ super(`Usage limit exceeded: ${reason}`);
7
+ this.status = 403;
8
+ this.code = 'usage_limit';
9
+ }
10
+ }
11
+ exports.AgentForbiddenError = AgentForbiddenError;
12
+ class AgentService {
13
+ constructor(agents, runner, conversations,
14
+ /** When wired, agents created via the admin UI are looked up here first;
15
+ * the hardcoded `agents` array remains a fallback for legacy installs. */
16
+ resolver,
17
+ /** Host-supplied hooks for observing runtime events. The host owns
18
+ * persistence of usage/audit; the SDK only emits. */
19
+ hooks,
20
+ /** When wired, every streamMessage / sendMessage call attaches the
21
+ * authenticated user's connector tools (Gmail, Drive, …) on the fly
22
+ * via `overrides.extraTools`. Optional — connectors are opt-in. */
23
+ connectorRegistry) {
24
+ this.agents = agents;
25
+ this.runner = runner;
26
+ this.conversations = conversations;
27
+ this.resolver = resolver;
28
+ this.hooks = hooks;
29
+ this.connectorRegistry = connectorRegistry;
30
+ }
31
+ /**
32
+ * Fetch the connector tools the user has authorized, swallowing failures.
33
+ * The agent loop must keep working even if a connector's refresh token is
34
+ * dead — the toolbelt just shrinks. Specific tools may still surface their
35
+ * own auth errors when actually invoked.
36
+ */
37
+ async resolveExtraTools(userId) {
38
+ if (!this.connectorRegistry)
39
+ return undefined;
40
+ try {
41
+ const tools = await this.connectorRegistry.toolsForUser(userId);
42
+ return tools.length ? tools : undefined;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ async dispatchUsage(event) {
49
+ if (!this.hooks?.onUsage)
50
+ return;
51
+ try {
52
+ await this.hooks.onUsage(event);
53
+ }
54
+ catch (err) {
55
+ // Hooks observe, they don't control. Failures are logged and swallowed.
56
+ // eslint-disable-next-line no-console
57
+ console.warn('[agentforge] onUsage hook threw:', err);
58
+ }
59
+ }
60
+ async dispatchTurnComplete(event) {
61
+ if (!this.hooks?.onTurnComplete)
62
+ return;
63
+ try {
64
+ await this.hooks.onTurnComplete(event);
65
+ }
66
+ catch (err) {
67
+ // eslint-disable-next-line no-console
68
+ console.warn('[agentforge] onTurnComplete hook threw:', err);
69
+ }
70
+ }
71
+ // ─── Agent catalog ────────────────────────────────────────────────────────
72
+ listAgents(_userId) {
73
+ return this.agents;
74
+ }
75
+ /**
76
+ * Legacy synchronous lookup against the hardcoded `config.agents` array.
77
+ * Kept for backwards compat (existing callers like the orchestrator still
78
+ * use it). New code paths should call `resolveAgent` which also hits the DB.
79
+ */
80
+ getAgent(agentId) {
81
+ const agent = this.agents.find((a) => a.id === agentId);
82
+ if (!agent) {
83
+ const err = Object.assign(new Error(`Agent "${agentId}" not found`), {
84
+ status: 404,
85
+ });
86
+ throw err;
87
+ }
88
+ return agent;
89
+ }
90
+ /**
91
+ * Resolve an agent from any combination of id/slug. Order:
92
+ * 1. `agentSlug` + `tenantId` against the DB. This is the SDK path.
93
+ * 2. `agentId` against the DB (UI may store agentId in conversations).
94
+ * 3. `agentId` against the hardcoded `config.agents` array (fallback).
95
+ *
96
+ * Returns the runtime `AgentDefinition` ready to feed the runner.
97
+ */
98
+ async resolveAgent(params) {
99
+ if (params.agentSlug && params.tenantId && this.resolver) {
100
+ const record = await this.resolver.findBySlug(params.tenantId, params.agentSlug);
101
+ if (record && record.isActive)
102
+ return toAgentDefinition(record);
103
+ }
104
+ if (params.agentId) {
105
+ if (this.resolver) {
106
+ const record = await this.resolver
107
+ .findById(params.agentId)
108
+ .catch(() => null);
109
+ if (record && record.isActive)
110
+ return toAgentDefinition(record);
111
+ }
112
+ // Final fallback: hardcoded SDK array.
113
+ const hardcoded = this.agents.find((a) => a.id === params.agentId);
114
+ if (hardcoded)
115
+ return hardcoded;
116
+ }
117
+ const missing = params.agentSlug ?? params.agentId ?? '<unknown>';
118
+ const err = Object.assign(new Error(`Agent "${missing}" not found`), { status: 404 });
119
+ throw err;
120
+ }
121
+ // ─── Conversations ────────────────────────────────────────────────────────
122
+ async createConversation(params) {
123
+ const agent = await this.resolveAgent({
124
+ agentId: params.agentId,
125
+ agentSlug: params.agentSlug,
126
+ tenantId: params.tenantId,
127
+ });
128
+ const conversation = await this.conversations.create({
129
+ userId: params.userId,
130
+ agentId: agent.id,
131
+ title: params.title,
132
+ metadata: params.metadata,
133
+ });
134
+ if (params.initialMessage) {
135
+ const response = await this.sendMessage({
136
+ conversationId: conversation.id,
137
+ userId: params.userId,
138
+ content: params.initialMessage,
139
+ });
140
+ return { conversation, firstResponse: response };
141
+ }
142
+ return { conversation };
143
+ }
144
+ // ─── Sync send ────────────────────────────────────────────────────────────
145
+ async sendMessage(params) {
146
+ const history = await this.conversations.getAnthropicMessages(params.conversationId, params.userId);
147
+ const conv = await this.conversations.ensureOwned(params.conversationId, params.userId);
148
+ if (conv.status === 'completed') {
149
+ const err = Object.assign(new Error('Conversation has been ended'), { status: 410 });
150
+ throw err;
151
+ }
152
+ const agent = await this.resolveAgent({ agentId: conv.agentId });
153
+ const messages = [...history, { role: 'user', content: params.content }];
154
+ await this.conversations.addMessage({
155
+ conversationId: params.conversationId,
156
+ userId: params.userId,
157
+ role: 'user',
158
+ content: params.content,
159
+ });
160
+ const extraTools = await this.resolveExtraTools(params.userId);
161
+ const response = await this.runner.run(agent, messages, {
162
+ userId: params.userId,
163
+ conversationId: params.conversationId,
164
+ agentId: conv.agentId,
165
+ messageId: 'sync',
166
+ }, { ...(params.overrides ?? {}), extraTools });
167
+ await this.conversations.addMessage({
168
+ conversationId: params.conversationId,
169
+ userId: params.userId,
170
+ role: 'assistant',
171
+ content: response.content,
172
+ toolCalls: response.toolCalls,
173
+ usage: response.usage,
174
+ });
175
+ const now = new Date();
176
+ const usageEvent = {
177
+ version: 1,
178
+ turnId: response.messageId,
179
+ conversationId: params.conversationId,
180
+ agentId: conv.agentId,
181
+ userId: params.userId,
182
+ provider: 'anthropic',
183
+ modelId: agent.model ?? 'unknown',
184
+ inputTokens: response.usage.inputTokens,
185
+ outputTokens: response.usage.outputTokens,
186
+ durationMs: 0,
187
+ at: now,
188
+ };
189
+ await this.dispatchUsage(usageEvent);
190
+ await this.dispatchTurnComplete({
191
+ version: 1,
192
+ turnId: response.messageId,
193
+ conversationId: params.conversationId,
194
+ agentId: conv.agentId,
195
+ userId: params.userId,
196
+ status: 'ok',
197
+ toolCallCount: response.toolCalls?.length ?? 0,
198
+ durationMs: 0,
199
+ at: now,
200
+ });
201
+ return response;
202
+ }
203
+ // ─── Streaming ────────────────────────────────────────────────────────────
204
+ async *streamMessage(params) {
205
+ const history = await this.conversations.getAnthropicMessages(params.conversationId, params.userId);
206
+ const conv = await this.conversations.ensureOwned(params.conversationId, params.userId);
207
+ if (conv.status === 'completed') {
208
+ throw Object.assign(new Error('Conversation has been ended'), { status: 410 });
209
+ }
210
+ const agent = await this.resolveAgent({ agentId: conv.agentId });
211
+ const messages = [...history, { role: 'user', content: params.content }];
212
+ await this.conversations.addMessage({
213
+ conversationId: params.conversationId,
214
+ userId: params.userId,
215
+ role: 'user',
216
+ content: params.content,
217
+ });
218
+ let fullContent = '';
219
+ let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
220
+ const extraTools = await this.resolveExtraTools(params.userId);
221
+ for await (const chunk of this.runner.stream(agent, messages, {
222
+ userId: params.userId,
223
+ conversationId: params.conversationId,
224
+ agentId: conv.agentId,
225
+ messageId: 'streaming',
226
+ }, { ...(params.overrides ?? {}), extraTools })) {
227
+ if (chunk.type === 'text_delta')
228
+ fullContent += chunk.delta;
229
+ if (chunk.type === 'usage')
230
+ finalUsage = chunk.usage;
231
+ yield chunk;
232
+ }
233
+ if (fullContent) {
234
+ await this.conversations.addMessage({
235
+ conversationId: params.conversationId,
236
+ userId: params.userId,
237
+ role: 'assistant',
238
+ content: fullContent,
239
+ usage: finalUsage,
240
+ });
241
+ const now = new Date();
242
+ await this.dispatchUsage({
243
+ version: 1,
244
+ turnId: 'streamed',
245
+ conversationId: params.conversationId,
246
+ agentId: conv.agentId,
247
+ userId: params.userId,
248
+ provider: 'anthropic',
249
+ modelId: agent.model ?? 'unknown',
250
+ inputTokens: finalUsage.inputTokens,
251
+ outputTokens: finalUsage.outputTokens,
252
+ durationMs: 0,
253
+ at: now,
254
+ });
255
+ await this.dispatchTurnComplete({
256
+ version: 1,
257
+ turnId: 'streamed',
258
+ conversationId: params.conversationId,
259
+ agentId: conv.agentId,
260
+ userId: params.userId,
261
+ status: 'ok',
262
+ toolCallCount: 0,
263
+ durationMs: 0,
264
+ at: now,
265
+ });
266
+ }
267
+ }
268
+ // ─── Read-side ────────────────────────────────────────────────────────────
269
+ async getConversation(conversationId, userId) {
270
+ return this.conversations.getHistory(conversationId, userId);
271
+ }
272
+ async listConversations(userId, options) {
273
+ return this.conversations.listForUser(userId, options);
274
+ }
275
+ /**
276
+ * Create a conversation and stream the first assistant reply in one go.
277
+ *
278
+ * Designed for the public chat surface: the widget POSTs a single endpoint
279
+ * and expects a `conversation` SSE event followed by the streamed chunks.
280
+ * The caller (PublicChatController) is responsible for surfacing the
281
+ * conversation id via a custom SSE event before the chunks start flowing.
282
+ */
283
+ async *streamCreateConversation(params) {
284
+ const agent = await this.resolveAgent({
285
+ agentId: params.agentId,
286
+ agentSlug: params.agentSlug,
287
+ tenantId: params.tenantId,
288
+ });
289
+ const conversation = await this.conversations.create({
290
+ userId: params.userId,
291
+ agentId: agent.id,
292
+ title: params.title,
293
+ metadata: params.metadata,
294
+ });
295
+ yield { kind: 'conversation', conversationId: conversation.id };
296
+ for await (const chunk of this.streamMessage({
297
+ conversationId: conversation.id,
298
+ userId: params.userId,
299
+ content: params.content,
300
+ overrides: params.overrides,
301
+ })) {
302
+ yield { kind: 'chunk', chunk };
303
+ }
304
+ }
305
+ }
306
+ exports.AgentService = AgentService;
307
+ /**
308
+ * Map a persisted `AgentRecord` to the runtime `AgentDefinition` the runner
309
+ * expects. The `context` column (plain-text knowledge) is prepended to the
310
+ * system prompt — the cheapest path before RAG is implemented.
311
+ */
312
+ function toAgentDefinition(record) {
313
+ const systemPrompt = record.context
314
+ ? `${record.systemPrompt}\n\n--- Knowledge ---\n${record.context}`
315
+ : record.systemPrompt;
316
+ return {
317
+ id: record.id,
318
+ name: record.name,
319
+ description: record.description,
320
+ model: record.model,
321
+ systemPrompt,
322
+ maxTokens: record.maxTokens,
323
+ temperature: record.temperature,
324
+ topP: record.topP,
325
+ tools: record.tools,
326
+ mcpServers: record.mcpServers,
327
+ metadata: record.metadata,
328
+ };
329
+ }
@@ -0,0 +1,41 @@
1
+ import type { ApiKeyRepository } from '../repositories';
2
+ import type { ApiKey } from '../domain/api-key';
3
+ export interface MintedApiKey {
4
+ /** Plaintext key. Only available at creation time — never persisted. */
5
+ key: string;
6
+ entity: ApiKey;
7
+ }
8
+ /** Operational errors. Adapters map `.status` to an HTTP code. */
9
+ export declare class ApiKeyError extends Error {
10
+ status: number;
11
+ code: 'invalid' | 'revoked' | 'expired';
12
+ constructor(code: 'invalid' | 'revoked' | 'expired', message: string);
13
+ }
14
+ export declare class ApiKeyService {
15
+ private readonly repo;
16
+ constructor(repo: ApiKeyRepository);
17
+ /** Mint a new API key for a tenant. Returns the plaintext key (one-time).
18
+ * Pass `agentId` to scope the key to a specific agent in the tenant —
19
+ * calls with that key only resolve when the requested agent matches. */
20
+ issue(input: {
21
+ tenantId: string;
22
+ agentId?: string;
23
+ name: string;
24
+ scopes?: string[];
25
+ expiresAt?: Date;
26
+ }): Promise<MintedApiKey>;
27
+ /**
28
+ * Validate a presented key. Returns the matching ApiKey row (without
29
+ * touching the plaintext) or throws ApiKeyError. Updates lastUsedAt as a
30
+ * best-effort side-effect (never blocks on the write).
31
+ */
32
+ validate(rawKey: string): Promise<ApiKey>;
33
+ listForTenant(tenantId: string): Promise<ApiKey[]>;
34
+ listForAgent(tenantId: string, agentId: string): Promise<ApiKey[]>;
35
+ revoke(id: string): Promise<void>;
36
+ /** Tells callers (HTTP middleware) whether a header value looks like an API key. */
37
+ static isApiKeyToken(token: string | undefined | null): boolean;
38
+ private hash;
39
+ }
40
+ /** Convenience for `randomUUID` access from external callers. */
41
+ export declare const newApiKeyId: () => `${string}-${string}-${string}-${string}-${string}`;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.newApiKeyId = exports.ApiKeyService = exports.ApiKeyError = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const API_KEY_PREFIX_LIVE = 'ak_live_';
6
+ /** Operational errors. Adapters map `.status` to an HTTP code. */
7
+ class ApiKeyError extends Error {
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.status = 401;
12
+ }
13
+ }
14
+ exports.ApiKeyError = ApiKeyError;
15
+ class ApiKeyService {
16
+ constructor(repo) {
17
+ this.repo = repo;
18
+ }
19
+ /** Mint a new API key for a tenant. Returns the plaintext key (one-time).
20
+ * Pass `agentId` to scope the key to a specific agent in the tenant —
21
+ * calls with that key only resolve when the requested agent matches. */
22
+ async issue(input) {
23
+ const random = (0, crypto_1.randomBytes)(24).toString('hex'); // 192-bit
24
+ const key = `${API_KEY_PREFIX_LIVE}${random}`;
25
+ const keyHash = this.hash(key);
26
+ const keyPrefix = key.slice(0, 16); // "ak_live_a1b2c3d4"
27
+ const entity = await this.repo.create({
28
+ tenantId: input.tenantId,
29
+ agentId: input.agentId,
30
+ name: input.name,
31
+ keyHash,
32
+ keyPrefix,
33
+ scopes: input.scopes,
34
+ expiresAt: input.expiresAt,
35
+ });
36
+ return { key, entity };
37
+ }
38
+ /**
39
+ * Validate a presented key. Returns the matching ApiKey row (without
40
+ * touching the plaintext) or throws ApiKeyError. Updates lastUsedAt as a
41
+ * best-effort side-effect (never blocks on the write).
42
+ */
43
+ async validate(rawKey) {
44
+ if (!rawKey?.startsWith(API_KEY_PREFIX_LIVE)) {
45
+ throw new ApiKeyError('invalid', 'Invalid API key');
46
+ }
47
+ const keyHash = this.hash(rawKey);
48
+ const apiKey = await this.repo.findByHash(keyHash);
49
+ if (!apiKey)
50
+ throw new ApiKeyError('invalid', 'Invalid API key');
51
+ if (apiKey.revokedAt)
52
+ throw new ApiKeyError('revoked', 'API key has been revoked');
53
+ if (apiKey.expiresAt && apiKey.expiresAt <= new Date()) {
54
+ throw new ApiKeyError('expired', 'API key has expired');
55
+ }
56
+ // Best-effort lastUsedAt update — fire and forget.
57
+ this.repo.update(apiKey.id, { lastUsedAt: new Date() }).catch(() => { });
58
+ return apiKey;
59
+ }
60
+ async listForTenant(tenantId) {
61
+ return this.repo.listByTenant(tenantId);
62
+ }
63
+ async listForAgent(tenantId, agentId) {
64
+ return this.repo.listByAgent(tenantId, agentId);
65
+ }
66
+ async revoke(id) {
67
+ await this.repo.update(id, { revokedAt: new Date() });
68
+ }
69
+ /** Tells callers (HTTP middleware) whether a header value looks like an API key. */
70
+ static isApiKeyToken(token) {
71
+ return !!token && token.startsWith(API_KEY_PREFIX_LIVE);
72
+ }
73
+ hash(raw) {
74
+ return (0, crypto_1.createHash)('sha256').update(raw).digest('hex');
75
+ }
76
+ }
77
+ exports.ApiKeyService = ApiKeyService;
78
+ /** Convenience for `randomUUID` access from external callers. */
79
+ const newApiKeyId = () => (0, crypto_1.randomUUID)();
80
+ exports.newApiKeyId = newApiKeyId;
@@ -0,0 +1,133 @@
1
+ import type { UserRepository, UserListOptions, UserListResult, AuthIdentityRepository, EmailTokenRepository } from '../repositories';
2
+ import type { User } from '../domain/user';
3
+ import type { RefreshTokenService } from './refresh-token.service';
4
+ import type { EmailService } from './email.service';
5
+ import type { Logger } from './tool-registry.service';
6
+ export interface AuthUser {
7
+ userId: string;
8
+ email?: string;
9
+ name?: string;
10
+ /** RBAC role from the `af_users.role` column. */
11
+ role?: 'user' | 'platform_admin';
12
+ /** Convenience boolean derived from `role === 'platform_admin'`. */
13
+ isPlatformAdmin?: boolean;
14
+ }
15
+ export interface AuthResult {
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ user: {
19
+ id: string;
20
+ email?: string;
21
+ name?: string;
22
+ };
23
+ }
24
+ export interface SessionMeta {
25
+ userAgent?: string;
26
+ ip?: string;
27
+ }
28
+ export interface AuthServiceOptions {
29
+ jwtSecret: string;
30
+ /** Access-token TTL — string accepted by jsonwebtoken (e.g. '15m'). Default: '15m'. */
31
+ accessTokenTtl?: string;
32
+ /** Lock account after N consecutive failed logins. Default: 5. */
33
+ lockoutThreshold?: number;
34
+ /** Lockout duration in minutes. Default: 15. */
35
+ lockoutMinutes?: number;
36
+ /** When true, login is blocked until the user has clicked the verify link. Default: false. */
37
+ requireEmailVerification?: boolean;
38
+ /** Default plan id assigned at registration. */
39
+ defaultPlanId?: string;
40
+ /** Initial credit balance for newly-registered users. */
41
+ initialCredits?: number;
42
+ /** Verify-email token TTL in hours. Default: 24. */
43
+ verifyTokenTtlHours?: number;
44
+ /** Password-reset token TTL in minutes. Default: 60. */
45
+ resetTokenTtlMinutes?: number;
46
+ /** Hook fired after a user is created via any strategy. */
47
+ onUserCreated?: (user: {
48
+ id: string;
49
+ email?: string;
50
+ name?: string;
51
+ }) => void | Promise<void>;
52
+ logger?: Logger;
53
+ }
54
+ export declare class AuthService {
55
+ private readonly users;
56
+ private readonly identities;
57
+ private readonly emailTokens;
58
+ private readonly refreshTokens;
59
+ private readonly email;
60
+ private readonly opts;
61
+ private readonly logger;
62
+ constructor(users: UserRepository, identities: AuthIdentityRepository, emailTokens: EmailTokenRepository, refreshTokens: RefreshTokenService, email: EmailService, opts: AuthServiceOptions);
63
+ register(params: {
64
+ email: string;
65
+ password: string;
66
+ name?: string;
67
+ }, meta?: SessionMeta): Promise<AuthResult>;
68
+ validateCredentials(email: string, password: string): Promise<User>;
69
+ login(email: string, password: string, meta?: SessionMeta): Promise<AuthResult>;
70
+ refresh(rawToken: string, meta?: SessionMeta): Promise<AuthResult>;
71
+ logout(rawRefreshToken?: string): Promise<void>;
72
+ logoutEverywhere(userId: string): Promise<void>;
73
+ signInWithProvider(profile: {
74
+ provider: string;
75
+ providerId: string;
76
+ email?: string;
77
+ emailVerified?: boolean;
78
+ name?: string;
79
+ metadata?: Record<string, unknown>;
80
+ }, meta?: SessionMeta): Promise<AuthResult>;
81
+ sendVerificationEmail(user: User): Promise<void>;
82
+ resendVerificationEmail(userId: string): Promise<void>;
83
+ verifyEmail(rawToken: string): Promise<{
84
+ userId: string;
85
+ }>;
86
+ /** Always returns success — never reveals whether the email exists. */
87
+ requestPasswordReset(email: string): Promise<void>;
88
+ resetPassword(rawToken: string, newPassword: string): Promise<void>;
89
+ /**
90
+ * Verify a JWT and return the resolved user. Returns null if the token is
91
+ * invalid/expired or the user no longer exists / is disabled.
92
+ */
93
+ /**
94
+ * Resolve a user from their id. Used by adapter-level JWT strategies that
95
+ * receive the decoded token payload from passport-jwt and need to populate
96
+ * `req.user`.
97
+ */
98
+ getById(userId: string): Promise<AuthUser | null>;
99
+ verifyAccessToken(rawToken: string): Promise<AuthUser | null>;
100
+ private toAuthUser;
101
+ /**
102
+ * Update a user's role. Caller is responsible for authorization; this
103
+ * method only enforces invariants:
104
+ * - Demoting a platform_admin is blocked if it would leave zero platform
105
+ * admins, to prevent locking yourself out of admin operations.
106
+ */
107
+ setUserRole(userId: string, role: User['role']): Promise<User>;
108
+ listUsers(opts?: UserListOptions): Promise<UserListResult>;
109
+ /**
110
+ * Look up a user by id and return the full `User` row (role, plan, last
111
+ * login, etc.) — distinct from `getById` which returns the lightweight
112
+ * `AuthUser` projection used in request contexts. Used by admin detail
113
+ * pages. Returns null when not found.
114
+ */
115
+ getFullUserById(userId: string): Promise<User | null>;
116
+ /**
117
+ * Boot-time backfill for instances upgrading to RBAC. If no user holds the
118
+ * `platform_admin` role, the oldest user is promoted so the operator isn't
119
+ * locked out. Idempotent — no-op when at least one admin already exists.
120
+ *
121
+ * Call once from your server bootstrap after the DB is connected.
122
+ */
123
+ ensurePlatformAdminBootstrap(): Promise<void>;
124
+ issueSession(user: User, meta?: SessionMeta): Promise<AuthResult>;
125
+ private signAccess;
126
+ private recordFailedLogin;
127
+ private recordSuccessfulLogin;
128
+ private mintEmailToken;
129
+ private consumeEmailToken;
130
+ private hashToken;
131
+ cleanupExpiredEmailTokens(olderThan?: Date): Promise<number>;
132
+ private runCreatedHook;
133
+ }