@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,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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });