@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,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TenantService = exports.TenantError = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
class TenantError extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status = code === 'forbidden' ? 403 : 404;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.TenantError = TenantError;
|
|
13
|
+
class TenantService {
|
|
14
|
+
constructor(tenants, externalUsers, users) {
|
|
15
|
+
this.tenants = tenants;
|
|
16
|
+
this.externalUsers = externalUsers;
|
|
17
|
+
this.users = users;
|
|
18
|
+
}
|
|
19
|
+
// ─── Tenant CRUD (admin) ─────────────────────────────────────────────────
|
|
20
|
+
async create(input) {
|
|
21
|
+
const tenant = await this.tenants.create({
|
|
22
|
+
id: (0, crypto_1.randomUUID)(),
|
|
23
|
+
name: input.name,
|
|
24
|
+
ownerUserId: input.ownerUserId,
|
|
25
|
+
planId: input.planId ?? 'free',
|
|
26
|
+
isActive: true,
|
|
27
|
+
});
|
|
28
|
+
return tenant;
|
|
29
|
+
}
|
|
30
|
+
async findById(id) {
|
|
31
|
+
return this.tenants.findById(id);
|
|
32
|
+
}
|
|
33
|
+
async listForOwner(ownerUserId, opts = {}) {
|
|
34
|
+
return this.tenants.listForOwner(ownerUserId, opts);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* List every tenant in the system. Controllers MUST gate this behind a
|
|
38
|
+
* platform-admin check before calling — the service intentionally doesn't,
|
|
39
|
+
* so it stays usable from contexts that have already authorized the call.
|
|
40
|
+
*/
|
|
41
|
+
async listAll(opts = {}) {
|
|
42
|
+
return this.tenants.listAll(opts);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Throws if the user isn't the owner. Used by admin endpoints.
|
|
46
|
+
*
|
|
47
|
+
* Pass `allowAnyOwner: true` to skip the owner check — controllers should
|
|
48
|
+
* set this when the caller is a platform admin, so super-admins can manage
|
|
49
|
+
* any tenant. The not-found path still fires when the tenantId is bogus.
|
|
50
|
+
*/
|
|
51
|
+
async assertOwner(tenantId, userId, opts = {}) {
|
|
52
|
+
const tenant = await this.tenants.findById(tenantId);
|
|
53
|
+
if (!tenant)
|
|
54
|
+
throw new TenantError('not_found', 'Tenant not found');
|
|
55
|
+
if (!opts.allowAnyOwner && tenant.ownerUserId !== userId) {
|
|
56
|
+
// Don't reveal existence — return not_found rather than forbidden.
|
|
57
|
+
throw new TenantError('not_found', 'Tenant not found');
|
|
58
|
+
}
|
|
59
|
+
return tenant;
|
|
60
|
+
}
|
|
61
|
+
// ─── External user mapping (B2B request path) ────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Look up (tenantId, externalId) → internal User. Creates the linkage on
|
|
64
|
+
* first use. Also creates a placeholder User row so the rest of the system
|
|
65
|
+
* (conversations, usage, billing) can reference it.
|
|
66
|
+
*/
|
|
67
|
+
async ensureExternalUser(input) {
|
|
68
|
+
const existing = await this.externalUsers.findByExternalId(input.tenantId, input.externalId);
|
|
69
|
+
if (existing)
|
|
70
|
+
return existing;
|
|
71
|
+
const internalId = (0, crypto_1.randomUUID)();
|
|
72
|
+
// Create a User row so conversations/messages/usage can FK to it. The
|
|
73
|
+
// user is "owned" by the tenant — we store metadata for traceability.
|
|
74
|
+
await this.users.create({
|
|
75
|
+
id: internalId,
|
|
76
|
+
email: input.email,
|
|
77
|
+
name: input.name,
|
|
78
|
+
authProvider: `tenant:${input.tenantId}`,
|
|
79
|
+
currentPlanId: 'free',
|
|
80
|
+
creditsBalance: 0,
|
|
81
|
+
isActive: true,
|
|
82
|
+
failedLoginCount: 0,
|
|
83
|
+
metadata: {
|
|
84
|
+
tenantId: input.tenantId,
|
|
85
|
+
externalId: input.externalId,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
return this.externalUsers.create({
|
|
89
|
+
id: internalId,
|
|
90
|
+
tenantId: input.tenantId,
|
|
91
|
+
externalId: input.externalId,
|
|
92
|
+
email: input.email,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.TenantService = TenantService;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentToolDefinition, AnthropicTool, ToolExecutionContext } from '../types/agent.types';
|
|
2
|
+
export interface Logger {
|
|
3
|
+
log: (msg: string) => void;
|
|
4
|
+
warn: (msg: string) => void;
|
|
5
|
+
debug: (msg: string) => void;
|
|
6
|
+
error: (msg: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Framework-free tool registry. Holds the catalog of tools an agent can call
|
|
10
|
+
* and exposes them in Anthropic's tool-schema shape. @agentforge-io/nest wraps
|
|
11
|
+
* a single instance of this class.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ToolRegistryService {
|
|
14
|
+
private readonly tools;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
constructor(opts?: {
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
initialTools?: AgentToolDefinition[];
|
|
19
|
+
});
|
|
20
|
+
register(tool: AgentToolDefinition): void;
|
|
21
|
+
registerMany(tools: AgentToolDefinition[]): void;
|
|
22
|
+
/** Remove a tool from the registry. Returns true when something was
|
|
23
|
+
* actually removed — false when the name was unknown. Used by
|
|
24
|
+
* McpClientService to clean up tools when an MCP server is unregistered. */
|
|
25
|
+
unregister(name: string): boolean;
|
|
26
|
+
getToolsForAgent(agentId: string, toolNames: string[]): AnthropicTool[];
|
|
27
|
+
execute(toolName: string, input: Record<string, unknown>, context: ToolExecutionContext): Promise<string>;
|
|
28
|
+
has(name: string): boolean;
|
|
29
|
+
list(): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Public catalog suitable for a "/tools" discovery endpoint. Excludes the
|
|
32
|
+
* `execute` function (not serializable) and surfaces the per-tool agent
|
|
33
|
+
* allowlist so a UI can show "this tool is only usable by agent X".
|
|
34
|
+
*/
|
|
35
|
+
describe(): ToolDescription[];
|
|
36
|
+
}
|
|
37
|
+
export interface ToolDescription {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
inputSchema: Record<string, unknown>;
|
|
41
|
+
agents?: string[];
|
|
42
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolRegistryService = void 0;
|
|
4
|
+
const noopLogger = {
|
|
5
|
+
log: () => { },
|
|
6
|
+
warn: () => { },
|
|
7
|
+
debug: () => { },
|
|
8
|
+
error: () => { },
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Framework-free tool registry. Holds the catalog of tools an agent can call
|
|
12
|
+
* and exposes them in Anthropic's tool-schema shape. @agentforge-io/nest wraps
|
|
13
|
+
* a single instance of this class.
|
|
14
|
+
*/
|
|
15
|
+
class ToolRegistryService {
|
|
16
|
+
constructor(opts = {}) {
|
|
17
|
+
this.tools = new Map();
|
|
18
|
+
this.logger = opts.logger ?? noopLogger;
|
|
19
|
+
if (opts.initialTools)
|
|
20
|
+
this.registerMany(opts.initialTools);
|
|
21
|
+
}
|
|
22
|
+
register(tool) {
|
|
23
|
+
if (this.tools.has(tool.name)) {
|
|
24
|
+
this.logger.warn(`Tool "${tool.name}" is already registered — overwriting`);
|
|
25
|
+
}
|
|
26
|
+
this.tools.set(tool.name, tool);
|
|
27
|
+
this.logger.debug(`Registered tool: ${tool.name}`);
|
|
28
|
+
}
|
|
29
|
+
registerMany(tools) {
|
|
30
|
+
tools.forEach((t) => this.register(t));
|
|
31
|
+
}
|
|
32
|
+
/** Remove a tool from the registry. Returns true when something was
|
|
33
|
+
* actually removed — false when the name was unknown. Used by
|
|
34
|
+
* McpClientService to clean up tools when an MCP server is unregistered. */
|
|
35
|
+
unregister(name) {
|
|
36
|
+
const existed = this.tools.delete(name);
|
|
37
|
+
if (existed)
|
|
38
|
+
this.logger.debug(`Unregistered tool: ${name}`);
|
|
39
|
+
return existed;
|
|
40
|
+
}
|
|
41
|
+
getToolsForAgent(agentId, toolNames) {
|
|
42
|
+
return toolNames
|
|
43
|
+
.filter((name) => {
|
|
44
|
+
const tool = this.tools.get(name);
|
|
45
|
+
if (!tool) {
|
|
46
|
+
this.logger.warn(`Tool "${name}" requested by agent "${agentId}" is not registered`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (tool.agents && tool.agents.length > 0 && !tool.agents.includes(agentId)) {
|
|
50
|
+
this.logger.warn(`Tool "${name}" is not allowed for agent "${agentId}"`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
})
|
|
55
|
+
.map((name) => {
|
|
56
|
+
const tool = this.tools.get(name);
|
|
57
|
+
return {
|
|
58
|
+
name: tool.name,
|
|
59
|
+
description: tool.description,
|
|
60
|
+
input_schema: tool.inputSchema,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async execute(toolName, input, context) {
|
|
65
|
+
const tool = this.tools.get(toolName);
|
|
66
|
+
if (!tool)
|
|
67
|
+
throw new Error(`Tool "${toolName}" not found in registry`);
|
|
68
|
+
const start = Date.now();
|
|
69
|
+
try {
|
|
70
|
+
this.logger.debug(`Executing tool "${toolName}" for user ${context.userId}`);
|
|
71
|
+
const result = await tool.execute(input, context);
|
|
72
|
+
this.logger.debug(`Tool "${toolName}" completed in ${Date.now() - start}ms`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
this.logger.error(`Tool "${toolName}" failed: ${message}`);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
has(name) {
|
|
82
|
+
return this.tools.has(name);
|
|
83
|
+
}
|
|
84
|
+
list() {
|
|
85
|
+
return Array.from(this.tools.keys());
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Public catalog suitable for a "/tools" discovery endpoint. Excludes the
|
|
89
|
+
* `execute` function (not serializable) and surfaces the per-tool agent
|
|
90
|
+
* allowlist so a UI can show "this tool is only usable by agent X".
|
|
91
|
+
*/
|
|
92
|
+
describe() {
|
|
93
|
+
return Array.from(this.tools.values()).map((t) => ({
|
|
94
|
+
name: t.name,
|
|
95
|
+
description: t.description,
|
|
96
|
+
inputSchema: t.inputSchema,
|
|
97
|
+
agents: t.agents,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.ToolRegistryService = ToolRegistryService;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SignedUploadTarget, UploadAdapter } from '../adapters/upload/upload-adapter.interface';
|
|
2
|
+
export declare class UploadError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
code: 'unsupported_type' | 'too_large' | 'invalid' | 'not_configured';
|
|
5
|
+
constructor(code: UploadError['code'], message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface AvatarSignInput {
|
|
8
|
+
tenantId: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
contentType: string;
|
|
11
|
+
/** Declared size in bytes (best-effort — client computes from File.size).
|
|
12
|
+
* We reject before signing if it exceeds the limit. */
|
|
13
|
+
contentLength?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thin application service over the UploadAdapter. Owns:
|
|
17
|
+
* - MIME / size validation (so we don't sign URLs for forbidden uploads)
|
|
18
|
+
* - Key construction (keeps adapter ignorant of domain conventions)
|
|
19
|
+
* - Error shaping for the HTTP layer
|
|
20
|
+
*
|
|
21
|
+
* Adapter swapping (S3 → R2 → MinIO) doesn't ripple into the controller
|
|
22
|
+
* because everything HTTP-flavored happens here.
|
|
23
|
+
*/
|
|
24
|
+
export declare class UploadService {
|
|
25
|
+
private readonly adapter;
|
|
26
|
+
constructor(adapter: UploadAdapter);
|
|
27
|
+
/**
|
|
28
|
+
* Sign a PUT URL for an agent's avatar. Key shape:
|
|
29
|
+
* agents/<tenantId>/<agentId>/avatar-<uuid>.<ext>
|
|
30
|
+
* The UUID makes a fresh upload not collide with the previous avatar —
|
|
31
|
+
* we keep the old object around (cleanup is a future improvement) so we
|
|
32
|
+
* don't 404 anyone who already had the old URL loaded.
|
|
33
|
+
*/
|
|
34
|
+
signAgentAvatar(input: AvatarSignInput): Promise<SignedUploadTarget>;
|
|
35
|
+
}
|
|
36
|
+
export declare const ALLOWED_AVATAR_MIME_TYPES: string[];
|
|
37
|
+
export declare const MAX_AVATAR_BYTES: number;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_AVATAR_BYTES = exports.ALLOWED_AVATAR_MIME_TYPES = exports.UploadService = exports.UploadError = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
class UploadError extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status =
|
|
10
|
+
code === 'not_configured' ? 501 :
|
|
11
|
+
code === 'too_large' ? 413 :
|
|
12
|
+
code === 'unsupported_type' ? 415 : 400;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.UploadError = UploadError;
|
|
16
|
+
const IMAGE_MIME_WHITELIST = new Set([
|
|
17
|
+
'image/png',
|
|
18
|
+
'image/jpeg',
|
|
19
|
+
'image/webp',
|
|
20
|
+
'image/gif',
|
|
21
|
+
]);
|
|
22
|
+
const MIME_TO_EXT = {
|
|
23
|
+
'image/png': 'png',
|
|
24
|
+
'image/jpeg': 'jpg',
|
|
25
|
+
'image/webp': 'webp',
|
|
26
|
+
'image/gif': 'gif',
|
|
27
|
+
};
|
|
28
|
+
/** Max image upload size (bytes). Avatars are tiny; this catches an upload
|
|
29
|
+
* the user thinks is a 1MB photo but is actually a 30MB original. */
|
|
30
|
+
const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
/**
|
|
32
|
+
* Thin application service over the UploadAdapter. Owns:
|
|
33
|
+
* - MIME / size validation (so we don't sign URLs for forbidden uploads)
|
|
34
|
+
* - Key construction (keeps adapter ignorant of domain conventions)
|
|
35
|
+
* - Error shaping for the HTTP layer
|
|
36
|
+
*
|
|
37
|
+
* Adapter swapping (S3 → R2 → MinIO) doesn't ripple into the controller
|
|
38
|
+
* because everything HTTP-flavored happens here.
|
|
39
|
+
*/
|
|
40
|
+
class UploadService {
|
|
41
|
+
constructor(adapter) {
|
|
42
|
+
this.adapter = adapter;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sign a PUT URL for an agent's avatar. Key shape:
|
|
46
|
+
* agents/<tenantId>/<agentId>/avatar-<uuid>.<ext>
|
|
47
|
+
* The UUID makes a fresh upload not collide with the previous avatar —
|
|
48
|
+
* we keep the old object around (cleanup is a future improvement) so we
|
|
49
|
+
* don't 404 anyone who already had the old URL loaded.
|
|
50
|
+
*/
|
|
51
|
+
async signAgentAvatar(input) {
|
|
52
|
+
if (!input.tenantId || !input.agentId) {
|
|
53
|
+
throw new UploadError('invalid', 'tenantId and agentId are required');
|
|
54
|
+
}
|
|
55
|
+
const ct = input.contentType?.toLowerCase().trim();
|
|
56
|
+
if (!ct || !IMAGE_MIME_WHITELIST.has(ct)) {
|
|
57
|
+
throw new UploadError('unsupported_type', `Unsupported image type "${input.contentType}". Allowed: ${[...IMAGE_MIME_WHITELIST].join(', ')}.`);
|
|
58
|
+
}
|
|
59
|
+
if (input.contentLength !== undefined && input.contentLength > MAX_IMAGE_BYTES) {
|
|
60
|
+
throw new UploadError('too_large', `Avatar must be ${(MAX_IMAGE_BYTES / 1024 / 1024).toFixed(1)} MB or smaller.`);
|
|
61
|
+
}
|
|
62
|
+
const ext = MIME_TO_EXT[ct] ?? 'bin';
|
|
63
|
+
const key = `agents/${input.tenantId}/${input.agentId}/avatar-${(0, crypto_1.randomUUID)()}.${ext}`;
|
|
64
|
+
try {
|
|
65
|
+
return await this.adapter.signUpload({
|
|
66
|
+
key,
|
|
67
|
+
contentType: ct,
|
|
68
|
+
maxSizeBytes: MAX_IMAGE_BYTES,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// NoopUploadAdapter throws this exact message — map it to a sensible
|
|
73
|
+
// 501 instead of leaking the adapter shape to the client.
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
if (msg.includes('Uploads are not configured')) {
|
|
76
|
+
throw new UploadError('not_configured', msg);
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.UploadService = UploadService;
|
|
83
|
+
exports.ALLOWED_AVATAR_MIME_TYPES = [...IMAGE_MIME_WHITELIST];
|
|
84
|
+
exports.MAX_AVATAR_BYTES = MAX_IMAGE_BYTES;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { UserRepository, UsageRecordRepository } from '../repositories';
|
|
2
|
+
import type { CreditsConfig, UsageLimits } from '../types/config.types';
|
|
3
|
+
import type { PlanService } from './plan.service';
|
|
4
|
+
import type { UsageSummary } from '../types/billing.types';
|
|
5
|
+
import type { TokenUsage } from '../types/agent.types';
|
|
6
|
+
export interface UsageServiceOptions {
|
|
7
|
+
plans: PlanService;
|
|
8
|
+
defaultLimits?: UsageLimits;
|
|
9
|
+
credits?: CreditsConfig;
|
|
10
|
+
}
|
|
11
|
+
export declare class UsageService {
|
|
12
|
+
private readonly users;
|
|
13
|
+
private readonly usage;
|
|
14
|
+
private readonly opts;
|
|
15
|
+
constructor(users: UserRepository, usage: UsageRecordRepository, opts: UsageServiceOptions);
|
|
16
|
+
record(params: {
|
|
17
|
+
userId: string;
|
|
18
|
+
conversationId: string;
|
|
19
|
+
messageId: string;
|
|
20
|
+
agentId: string;
|
|
21
|
+
usage: TokenUsage;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
checkLimits(userId: string): Promise<{
|
|
24
|
+
allowed: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
usage: UsageSummary;
|
|
27
|
+
}>;
|
|
28
|
+
getSummary(userId: string): Promise<UsageSummary>;
|
|
29
|
+
/** Idempotent: create a user row if missing. Used by the agent flow. */
|
|
30
|
+
ensureUser(userId: string): Promise<void>;
|
|
31
|
+
private currentBillingPeriod;
|
|
32
|
+
private periodDates;
|
|
33
|
+
private getUserPlan;
|
|
34
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UsageService = void 0;
|
|
4
|
+
class UsageService {
|
|
5
|
+
constructor(users, usage, opts) {
|
|
6
|
+
this.users = users;
|
|
7
|
+
this.usage = usage;
|
|
8
|
+
this.opts = opts;
|
|
9
|
+
}
|
|
10
|
+
async record(params) {
|
|
11
|
+
const period = this.currentBillingPeriod();
|
|
12
|
+
const credits = this.opts.credits;
|
|
13
|
+
const creditsConsumed = credits
|
|
14
|
+
? params.usage.totalTokens / credits.tokensPerCredit + (credits.creditsPerRequest ?? 0)
|
|
15
|
+
: undefined;
|
|
16
|
+
await this.usage.create({
|
|
17
|
+
userId: params.userId,
|
|
18
|
+
conversationId: params.conversationId,
|
|
19
|
+
messageId: params.messageId,
|
|
20
|
+
agentId: params.agentId,
|
|
21
|
+
inputTokens: params.usage.inputTokens,
|
|
22
|
+
outputTokens: params.usage.outputTokens,
|
|
23
|
+
totalTokens: params.usage.totalTokens,
|
|
24
|
+
requestCount: 1,
|
|
25
|
+
creditsConsumed,
|
|
26
|
+
billingPeriod: period,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async checkLimits(userId) {
|
|
30
|
+
const summary = await this.getSummary(userId);
|
|
31
|
+
if (summary.isOverLimit) {
|
|
32
|
+
const reasons = [];
|
|
33
|
+
const { requestsLimit, tokensLimit } = summary;
|
|
34
|
+
if (requestsLimit > 0 && summary.requestsUsed >= requestsLimit) {
|
|
35
|
+
reasons.push(`Monthly request limit reached (${requestsLimit} requests)`);
|
|
36
|
+
}
|
|
37
|
+
if (tokensLimit > 0 && summary.tokensUsed >= tokensLimit) {
|
|
38
|
+
reasons.push(`Monthly token limit reached (${tokensLimit} tokens)`);
|
|
39
|
+
}
|
|
40
|
+
return { allowed: false, reason: reasons.join('; '), usage: summary };
|
|
41
|
+
}
|
|
42
|
+
if (this.opts.credits) {
|
|
43
|
+
const user = await this.users.findById(userId);
|
|
44
|
+
if (user && user.creditsBalance <= 0) {
|
|
45
|
+
return { allowed: false, reason: 'Insufficient credits', usage: summary };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { allowed: true, usage: summary };
|
|
49
|
+
}
|
|
50
|
+
async getSummary(userId) {
|
|
51
|
+
const period = this.currentBillingPeriod();
|
|
52
|
+
const plan = await this.getUserPlan(userId);
|
|
53
|
+
const limits = plan?.limits ?? this.opts.defaultLimits ?? {};
|
|
54
|
+
const summed = await this.usage.sumForPeriod(userId, period);
|
|
55
|
+
const requestsUsed = summed.requestCount;
|
|
56
|
+
const tokensUsed = summed.totalTokens;
|
|
57
|
+
const requestsLimit = limits.requestsPerMonth ?? -1;
|
|
58
|
+
const tokensLimit = limits.tokensPerMonth ?? -1;
|
|
59
|
+
const [periodStart, periodEnd] = this.periodDates(period);
|
|
60
|
+
return {
|
|
61
|
+
userId,
|
|
62
|
+
planId: plan?.id ?? 'free',
|
|
63
|
+
periodStart,
|
|
64
|
+
periodEnd,
|
|
65
|
+
requestsUsed,
|
|
66
|
+
tokensUsed,
|
|
67
|
+
requestsLimit,
|
|
68
|
+
tokensLimit,
|
|
69
|
+
percentUsed: {
|
|
70
|
+
requests: requestsLimit > 0 ? (requestsUsed / requestsLimit) * 100 : 0,
|
|
71
|
+
tokens: tokensLimit > 0 ? (tokensUsed / tokensLimit) * 100 : 0,
|
|
72
|
+
},
|
|
73
|
+
isOverLimit: (requestsLimit > 0 && requestsUsed >= requestsLimit) ||
|
|
74
|
+
(tokensLimit > 0 && tokensUsed >= tokensLimit),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Idempotent: create a user row if missing. Used by the agent flow. */
|
|
78
|
+
async ensureUser(userId) {
|
|
79
|
+
const existing = await this.users.findById(userId);
|
|
80
|
+
if (existing)
|
|
81
|
+
return;
|
|
82
|
+
const defaultPlanId = await this.opts.plans.getDefaultId();
|
|
83
|
+
await this.users.create({
|
|
84
|
+
id: userId,
|
|
85
|
+
currentPlanId: defaultPlanId,
|
|
86
|
+
creditsBalance: this.opts.credits?.initialCredits ?? 0,
|
|
87
|
+
isActive: true,
|
|
88
|
+
failedLoginCount: 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
currentBillingPeriod() {
|
|
92
|
+
const now = new Date();
|
|
93
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
94
|
+
}
|
|
95
|
+
periodDates(period) {
|
|
96
|
+
const [year, month] = period.split('-').map(Number);
|
|
97
|
+
const start = new Date(year, month - 1, 1);
|
|
98
|
+
const end = new Date(year, month, 0, 23, 59, 59);
|
|
99
|
+
return [start, end];
|
|
100
|
+
}
|
|
101
|
+
async getUserPlan(userId) {
|
|
102
|
+
const user = await this.users.findById(userId);
|
|
103
|
+
if (!user)
|
|
104
|
+
return undefined;
|
|
105
|
+
return this.opts.plans.getPlan(user.currentPlanId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.UsageService = UsageService;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
export type MessageRole = 'user' | 'assistant' | 'system';
|
|
3
|
+
export type ConversationStatus = 'active' | 'completed' | 'error' | 'paused';
|
|
4
|
+
export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
|
|
5
|
+
export interface CreateConversationOptions {
|
|
6
|
+
userId: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
/** Run conversation in background queue */
|
|
11
|
+
async?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SendMessageOptions {
|
|
14
|
+
conversationId: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
content: string;
|
|
17
|
+
/** Attach files / images */
|
|
18
|
+
attachments?: MessageAttachment[];
|
|
19
|
+
/** Override agent config for this message */
|
|
20
|
+
overrides?: AgentOverrides;
|
|
21
|
+
/** Run in background queue */
|
|
22
|
+
async?: boolean;
|
|
23
|
+
/** Stream the response */
|
|
24
|
+
stream?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface MessageAttachment {
|
|
27
|
+
type: 'image' | 'document' | 'text';
|
|
28
|
+
/** For images: base64 or URL */
|
|
29
|
+
data: string;
|
|
30
|
+
mediaType?: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'application/pdf' | 'text/plain';
|
|
31
|
+
filename?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface AgentOverrides {
|
|
34
|
+
model?: string;
|
|
35
|
+
temperature?: number;
|
|
36
|
+
maxTokens?: number;
|
|
37
|
+
systemPromptSuffix?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Per-call tool injection. Used by `AgentService.streamMessage` to attach
|
|
40
|
+
* the requesting user's connector tools (Gmail, Drive, Calendar …) for
|
|
41
|
+
* this turn only — they are NOT registered on the global ToolRegistry so
|
|
42
|
+
* one user's auth never leaks to another's.
|
|
43
|
+
*
|
|
44
|
+
* Tools listed here are appended to `agent.tools[]` for this call and
|
|
45
|
+
* routed through a per-call map (the registry doesn't see them).
|
|
46
|
+
*/
|
|
47
|
+
extraTools?: AgentToolDefinition[];
|
|
48
|
+
}
|
|
49
|
+
export interface AgentResponse {
|
|
50
|
+
messageId: string;
|
|
51
|
+
conversationId: string;
|
|
52
|
+
content: string;
|
|
53
|
+
role: 'assistant';
|
|
54
|
+
/** Tool calls made during this turn */
|
|
55
|
+
toolCalls?: ToolCallRecord[];
|
|
56
|
+
/** Subagent delegations made by orchestrator */
|
|
57
|
+
delegations?: SubAgentDelegation[];
|
|
58
|
+
usage: TokenUsage;
|
|
59
|
+
model: string;
|
|
60
|
+
stopReason: string;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
}
|
|
63
|
+
export interface TokenUsage {
|
|
64
|
+
inputTokens: number;
|
|
65
|
+
outputTokens: number;
|
|
66
|
+
totalTokens: number;
|
|
67
|
+
cacheCreationInputTokens?: number;
|
|
68
|
+
cacheReadInputTokens?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface ToolCallRecord {
|
|
71
|
+
toolName: string;
|
|
72
|
+
toolUseId: string;
|
|
73
|
+
input: Record<string, unknown>;
|
|
74
|
+
output?: string;
|
|
75
|
+
error?: string;
|
|
76
|
+
durationMs?: number;
|
|
77
|
+
}
|
|
78
|
+
export interface SubAgentDelegation {
|
|
79
|
+
subAgentId: string;
|
|
80
|
+
task: string;
|
|
81
|
+
result: string;
|
|
82
|
+
usage: TokenUsage;
|
|
83
|
+
}
|
|
84
|
+
export interface AgentToolDefinition {
|
|
85
|
+
name: string;
|
|
86
|
+
description: string;
|
|
87
|
+
/** JSON Schema for the input */
|
|
88
|
+
inputSchema: Record<string, unknown>;
|
|
89
|
+
/** The actual function to execute */
|
|
90
|
+
execute: (input: Record<string, unknown>, context: ToolExecutionContext) => Promise<string>;
|
|
91
|
+
/** Restrict to specific agents */
|
|
92
|
+
agents?: string[];
|
|
93
|
+
}
|
|
94
|
+
export interface ToolExecutionContext {
|
|
95
|
+
userId: string;
|
|
96
|
+
conversationId: string;
|
|
97
|
+
agentId: string;
|
|
98
|
+
messageId: string;
|
|
99
|
+
}
|
|
100
|
+
export type AnthropicMessage = Anthropic.MessageParam;
|
|
101
|
+
export type AnthropicTool = Anthropic.Tool;
|
|
102
|
+
export type AnthropicContentBlock = Anthropic.ContentBlock;
|
|
103
|
+
export type StreamChunk = {
|
|
104
|
+
type: 'text_delta';
|
|
105
|
+
delta: string;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'tool_use_start';
|
|
108
|
+
toolName: string;
|
|
109
|
+
toolUseId: string;
|
|
110
|
+
} | {
|
|
111
|
+
type: 'tool_result';
|
|
112
|
+
toolName: string;
|
|
113
|
+
result: string;
|
|
114
|
+
} | {
|
|
115
|
+
type: 'delegation_start';
|
|
116
|
+
subAgentId: string;
|
|
117
|
+
task: string;
|
|
118
|
+
} | {
|
|
119
|
+
type: 'delegation_result';
|
|
120
|
+
subAgentId: string;
|
|
121
|
+
result: string;
|
|
122
|
+
} | {
|
|
123
|
+
type: 'usage';
|
|
124
|
+
usage: TokenUsage;
|
|
125
|
+
} | {
|
|
126
|
+
type: 'done';
|
|
127
|
+
messageId: string;
|
|
128
|
+
};
|
|
129
|
+
export interface AgentJobPayload {
|
|
130
|
+
jobId: string;
|
|
131
|
+
userId: string;
|
|
132
|
+
conversationId: string;
|
|
133
|
+
messageId: string;
|
|
134
|
+
agentId: string;
|
|
135
|
+
messages: AnthropicMessage[];
|
|
136
|
+
overrides?: AgentOverrides;
|
|
137
|
+
}
|
|
138
|
+
export interface AgentJobResult {
|
|
139
|
+
jobId: string;
|
|
140
|
+
response: AgentResponse;
|
|
141
|
+
completedAt: Date;
|
|
142
|
+
}
|
|
143
|
+
export interface ConversationHistory {
|
|
144
|
+
conversationId: string;
|
|
145
|
+
agentId: string;
|
|
146
|
+
userId: string;
|
|
147
|
+
title?: string;
|
|
148
|
+
status: ConversationStatus;
|
|
149
|
+
messages: MessageRecord[];
|
|
150
|
+
createdAt: Date;
|
|
151
|
+
updatedAt: Date;
|
|
152
|
+
}
|
|
153
|
+
export interface MessageRecord {
|
|
154
|
+
id: string;
|
|
155
|
+
role: MessageRole;
|
|
156
|
+
content: string;
|
|
157
|
+
toolCalls?: ToolCallRecord[];
|
|
158
|
+
usage?: TokenUsage;
|
|
159
|
+
createdAt: Date;
|
|
160
|
+
}
|