@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,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GITHUB_OAUTH_SPEC = exports.GOOGLE_OAUTH_SPEC = exports.OAuthCallbackError = exports.OAUTH_STATE_TTL_MS = exports.OAUTH_STATE_COOKIE_PREFIX = void 0;
|
|
4
|
+
exports.buildAuthorizeUrl = buildAuthorizeUrl;
|
|
5
|
+
exports.completeAuthorization = completeAuthorization;
|
|
6
|
+
exports.getOAuthProviderSpec = getOAuthProviderSpec;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
/**
|
|
9
|
+
* Public name of the state cookie/header per provider. Adapters share the
|
|
10
|
+
* convention so a session started under one transport could (in theory) be
|
|
11
|
+
* resumed under another — and so the operations docs are consistent.
|
|
12
|
+
*/
|
|
13
|
+
exports.OAUTH_STATE_COOKIE_PREFIX = 'af_oauth_state_';
|
|
14
|
+
exports.OAUTH_STATE_TTL_MS = 10 * 60 * 1000; // 10 min
|
|
15
|
+
/** Build the authorize URL + a fresh random state for the cookie/store. */
|
|
16
|
+
function buildAuthorizeUrl(spec, cfg) {
|
|
17
|
+
const state = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
18
|
+
const params = new URLSearchParams({
|
|
19
|
+
client_id: cfg.clientId,
|
|
20
|
+
redirect_uri: cfg.callbackURL,
|
|
21
|
+
response_type: 'code',
|
|
22
|
+
state,
|
|
23
|
+
...spec.authorizeExtras(),
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
authorizeUrl: `${spec.authorizeUrl}?${params.toString()}`,
|
|
27
|
+
state,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate the callback against the expected state and exchange the
|
|
32
|
+
* authorization code for an access token + normalized profile.
|
|
33
|
+
*
|
|
34
|
+
* Returns the profile on success. Throws OAuthCallbackError with a stable
|
|
35
|
+
* `.code` on failure — adapters surface the code in the failure redirect.
|
|
36
|
+
*/
|
|
37
|
+
async function completeAuthorization(input) {
|
|
38
|
+
if (input.providerError) {
|
|
39
|
+
throw new OAuthCallbackError(input.providerError, 'Provider returned an error');
|
|
40
|
+
}
|
|
41
|
+
if (!input.code) {
|
|
42
|
+
throw new OAuthCallbackError('missing_code', 'authorization code missing');
|
|
43
|
+
}
|
|
44
|
+
if (!input.expectedState || input.expectedState !== input.returnedState) {
|
|
45
|
+
throw new OAuthCallbackError('invalid_state', 'state mismatch');
|
|
46
|
+
}
|
|
47
|
+
let accessToken;
|
|
48
|
+
try {
|
|
49
|
+
accessToken = await exchangeCodeForToken({
|
|
50
|
+
tokenUrl: input.spec.tokenUrl,
|
|
51
|
+
clientId: input.cfg.clientId,
|
|
52
|
+
clientSecret: input.cfg.clientSecret,
|
|
53
|
+
code: input.code,
|
|
54
|
+
redirectUri: input.cfg.callbackURL,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new OAuthCallbackError('token_exchange_failed', err.message);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await input.spec.fetchProfile(accessToken);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new OAuthCallbackError('profile_fetch_failed', err.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Stable error shape adapters can pattern-match on for the failure redirect. */
|
|
68
|
+
class OAuthCallbackError extends Error {
|
|
69
|
+
constructor(code, message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = 'OAuthCallbackError';
|
|
72
|
+
this.code = code;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.OAuthCallbackError = OAuthCallbackError;
|
|
76
|
+
// ─── Token exchange ────────────────────────────────────────────────────────
|
|
77
|
+
async function exchangeCodeForToken(input) {
|
|
78
|
+
const body = new URLSearchParams({
|
|
79
|
+
grant_type: 'authorization_code',
|
|
80
|
+
client_id: input.clientId,
|
|
81
|
+
client_secret: input.clientSecret,
|
|
82
|
+
code: input.code,
|
|
83
|
+
redirect_uri: input.redirectUri,
|
|
84
|
+
});
|
|
85
|
+
const res = await fetch(input.tokenUrl, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
89
|
+
Accept: 'application/json',
|
|
90
|
+
},
|
|
91
|
+
body: body.toString(),
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok)
|
|
94
|
+
throw new Error(`token endpoint returned ${res.status}`);
|
|
95
|
+
const json = (await res.json());
|
|
96
|
+
if (!json.access_token)
|
|
97
|
+
throw new Error('no access_token in response');
|
|
98
|
+
return json.access_token;
|
|
99
|
+
}
|
|
100
|
+
// ─── Provider specs ────────────────────────────────────────────────────────
|
|
101
|
+
exports.GOOGLE_OAUTH_SPEC = {
|
|
102
|
+
name: 'google',
|
|
103
|
+
authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
104
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
105
|
+
authorizeExtras: () => ({
|
|
106
|
+
scope: 'openid email profile',
|
|
107
|
+
access_type: 'offline',
|
|
108
|
+
prompt: 'select_account',
|
|
109
|
+
}),
|
|
110
|
+
async fetchProfile(accessToken) {
|
|
111
|
+
const r = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
|
112
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
113
|
+
});
|
|
114
|
+
if (!r.ok)
|
|
115
|
+
throw new Error(`userinfo ${r.status}`);
|
|
116
|
+
const p = (await r.json());
|
|
117
|
+
return {
|
|
118
|
+
provider: 'google',
|
|
119
|
+
providerId: p.sub,
|
|
120
|
+
email: p.email,
|
|
121
|
+
emailVerified: p.email_verified === true,
|
|
122
|
+
name: p.name,
|
|
123
|
+
metadata: { avatar: p.picture, locale: p.locale },
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
exports.GITHUB_OAUTH_SPEC = {
|
|
128
|
+
name: 'github',
|
|
129
|
+
authorizeUrl: 'https://github.com/login/oauth/authorize',
|
|
130
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
131
|
+
authorizeExtras: () => ({ scope: 'user:email' }),
|
|
132
|
+
async fetchProfile(accessToken) {
|
|
133
|
+
const headers = {
|
|
134
|
+
Authorization: `Bearer ${accessToken}`,
|
|
135
|
+
Accept: 'application/vnd.github+json',
|
|
136
|
+
'User-Agent': 'agentforge',
|
|
137
|
+
};
|
|
138
|
+
const userRes = await fetch('https://api.github.com/user', { headers });
|
|
139
|
+
if (!userRes.ok)
|
|
140
|
+
throw new Error(`/user ${userRes.status}`);
|
|
141
|
+
const u = (await userRes.json());
|
|
142
|
+
// /user.email is null when private. Fetch /user/emails to get the verified primary.
|
|
143
|
+
let email = u.email ?? undefined;
|
|
144
|
+
if (!email) {
|
|
145
|
+
const emailsRes = await fetch('https://api.github.com/user/emails', { headers });
|
|
146
|
+
if (emailsRes.ok) {
|
|
147
|
+
const emails = (await emailsRes.json());
|
|
148
|
+
email =
|
|
149
|
+
emails.find((e) => e.primary && e.verified)?.email ??
|
|
150
|
+
emails.find((e) => e.verified)?.email;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
provider: 'github',
|
|
155
|
+
providerId: String(u.id),
|
|
156
|
+
email,
|
|
157
|
+
emailVerified: !!email, // GitHub only returns verified emails through /user/emails
|
|
158
|
+
name: u.name || u.login,
|
|
159
|
+
metadata: {
|
|
160
|
+
username: u.login,
|
|
161
|
+
avatar: u.avatar_url,
|
|
162
|
+
profileUrl: u.html_url,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
/** Look up a built-in spec by name. Returns undefined for unknown names. */
|
|
168
|
+
function getOAuthProviderSpec(name) {
|
|
169
|
+
if (name === 'google')
|
|
170
|
+
return exports.GOOGLE_OAUTH_SPEC;
|
|
171
|
+
if (name === 'github')
|
|
172
|
+
return exports.GITHUB_OAUTH_SPEC;
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint + scope configuration for a single OAuth2 provider. The
|
|
3
|
+
* ConnectorRegistry holds one of these per registered connector.
|
|
4
|
+
*
|
|
5
|
+
* Why per-connector and not global: each provider has different scopes,
|
|
6
|
+
* different authorize/token URLs, and may require provider-specific extras
|
|
7
|
+
* like Google's `access_type=offline` for refresh tokens.
|
|
8
|
+
*/
|
|
9
|
+
export interface OAuth2ProviderConfig {
|
|
10
|
+
authorizeUrl: string;
|
|
11
|
+
tokenUrl: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
/** Space-delimited scopes when building the authorize URL. */
|
|
15
|
+
scopes: string[];
|
|
16
|
+
/** Extra query params merged into the authorize URL (e.g. Google's
|
|
17
|
+
* `access_type=offline`, `prompt=consent`). */
|
|
18
|
+
authorizeExtras?: Record<string, string>;
|
|
19
|
+
/** PKCE: defaults to true for security, but providers that don't support
|
|
20
|
+
* it (rare in 2026) can opt out. */
|
|
21
|
+
usePkce?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface AuthorizeUrlResult {
|
|
24
|
+
url: string;
|
|
25
|
+
state: string;
|
|
26
|
+
/** Opaque payload the caller must persist (e.g. in a cookie/session) and
|
|
27
|
+
* hand back to `exchangeCode` so PKCE verification can complete. */
|
|
28
|
+
pkceVerifier?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TokenSet {
|
|
31
|
+
accessToken: string;
|
|
32
|
+
refreshToken?: string;
|
|
33
|
+
/** Seconds remaining when issued; absolute expiry is computed by the
|
|
34
|
+
* caller against `Date.now()` to avoid clock drift inside this service. */
|
|
35
|
+
expiresIn?: number;
|
|
36
|
+
scope?: string;
|
|
37
|
+
tokenType?: string;
|
|
38
|
+
}
|
|
39
|
+
interface OAuth2ServiceDeps {
|
|
40
|
+
/** Override for tests. */
|
|
41
|
+
fetchImpl?: typeof fetch;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Provider-agnostic OAuth2 dance. Holds zero per-user state — callers are
|
|
45
|
+
* responsible for storing the issued tokens. Designed so that the same
|
|
46
|
+
* instance can serve every connector (Google, Slack, Notion, …); the
|
|
47
|
+
* provider config is passed in on each call.
|
|
48
|
+
*/
|
|
49
|
+
export declare class OAuth2Service {
|
|
50
|
+
private readonly fetchImpl;
|
|
51
|
+
constructor(deps?: OAuth2ServiceDeps);
|
|
52
|
+
buildAuthorizeUrl(cfg: OAuth2ProviderConfig, redirectUri: string): AuthorizeUrlResult;
|
|
53
|
+
exchangeCode(cfg: OAuth2ProviderConfig, code: string, redirectUri: string, pkceVerifier?: string): Promise<TokenSet>;
|
|
54
|
+
refresh(cfg: OAuth2ProviderConfig, refreshToken: string): Promise<TokenSet>;
|
|
55
|
+
private postToken;
|
|
56
|
+
}
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OAuth2Service = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Provider-agnostic OAuth2 dance. Holds zero per-user state — callers are
|
|
7
|
+
* responsible for storing the issued tokens. Designed so that the same
|
|
8
|
+
* instance can serve every connector (Google, Slack, Notion, …); the
|
|
9
|
+
* provider config is passed in on each call.
|
|
10
|
+
*/
|
|
11
|
+
class OAuth2Service {
|
|
12
|
+
constructor(deps = {}) {
|
|
13
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
14
|
+
}
|
|
15
|
+
buildAuthorizeUrl(cfg, redirectUri) {
|
|
16
|
+
const state = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
17
|
+
const usePkce = cfg.usePkce !== false;
|
|
18
|
+
const params = {
|
|
19
|
+
response_type: 'code',
|
|
20
|
+
client_id: cfg.clientId,
|
|
21
|
+
redirect_uri: redirectUri,
|
|
22
|
+
scope: cfg.scopes.join(' '),
|
|
23
|
+
state,
|
|
24
|
+
...(cfg.authorizeExtras ?? {}),
|
|
25
|
+
};
|
|
26
|
+
let pkceVerifier;
|
|
27
|
+
if (usePkce) {
|
|
28
|
+
pkceVerifier = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
29
|
+
const challenge = (0, crypto_1.createHash)('sha256').update(pkceVerifier).digest('base64url');
|
|
30
|
+
params.code_challenge = challenge;
|
|
31
|
+
params.code_challenge_method = 'S256';
|
|
32
|
+
}
|
|
33
|
+
const url = new URL(cfg.authorizeUrl);
|
|
34
|
+
for (const [k, v] of Object.entries(params))
|
|
35
|
+
url.searchParams.set(k, v);
|
|
36
|
+
return { url: url.toString(), state, pkceVerifier };
|
|
37
|
+
}
|
|
38
|
+
async exchangeCode(cfg, code, redirectUri, pkceVerifier) {
|
|
39
|
+
const body = new URLSearchParams({
|
|
40
|
+
grant_type: 'authorization_code',
|
|
41
|
+
code,
|
|
42
|
+
redirect_uri: redirectUri,
|
|
43
|
+
client_id: cfg.clientId,
|
|
44
|
+
client_secret: cfg.clientSecret,
|
|
45
|
+
});
|
|
46
|
+
if (pkceVerifier)
|
|
47
|
+
body.set('code_verifier', pkceVerifier);
|
|
48
|
+
return this.postToken(cfg.tokenUrl, body);
|
|
49
|
+
}
|
|
50
|
+
async refresh(cfg, refreshToken) {
|
|
51
|
+
const body = new URLSearchParams({
|
|
52
|
+
grant_type: 'refresh_token',
|
|
53
|
+
refresh_token: refreshToken,
|
|
54
|
+
client_id: cfg.clientId,
|
|
55
|
+
client_secret: cfg.clientSecret,
|
|
56
|
+
});
|
|
57
|
+
return this.postToken(cfg.tokenUrl, body);
|
|
58
|
+
}
|
|
59
|
+
async postToken(tokenUrl, body) {
|
|
60
|
+
const res = await this.fetchImpl(tokenUrl, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
64
|
+
accept: 'application/json',
|
|
65
|
+
},
|
|
66
|
+
body: body.toString(),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const text = await res.text().catch(() => '');
|
|
70
|
+
throw new Error(`OAuth2 token endpoint ${tokenUrl} returned ${res.status}: ${text}`);
|
|
71
|
+
}
|
|
72
|
+
const json = (await res.json());
|
|
73
|
+
return {
|
|
74
|
+
accessToken: json.access_token,
|
|
75
|
+
refreshToken: json.refresh_token,
|
|
76
|
+
expiresIn: json.expires_in,
|
|
77
|
+
scope: json.scope,
|
|
78
|
+
tokenType: json.token_type,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.OAuth2Service = OAuth2Service;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AgentResponse, AnthropicMessage, SubAgentDelegation, ToolExecutionContext } from '../types/agent.types';
|
|
2
|
+
import type { AgentDefinition, AnthropicConfig } from '../types/config.types';
|
|
3
|
+
import type { AgentRunnerService } from './agent-runner.service';
|
|
4
|
+
import type { Logger } from './tool-registry.service';
|
|
5
|
+
/**
|
|
6
|
+
* Operational error. Adapters map `.status` to an HTTP code.
|
|
7
|
+
*/
|
|
8
|
+
export declare class OrchestratorError extends Error {
|
|
9
|
+
status: number;
|
|
10
|
+
code: 'agent_not_found';
|
|
11
|
+
constructor(code: 'agent_not_found', message: string);
|
|
12
|
+
}
|
|
13
|
+
export interface OrchestratorServiceOptions {
|
|
14
|
+
agents: AgentDefinition[];
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Multi-agent workflows. Orchestrator agents can delegate tasks to specialized
|
|
19
|
+
* subagents via auto-generated delegation tools that Claude sees as ordinary
|
|
20
|
+
* tools. The orchestrator runs the loop, invokes the subagent through the
|
|
21
|
+
* AgentRunnerService, and accumulates token usage across both layers.
|
|
22
|
+
*
|
|
23
|
+
* Framework-free; the Nest binding wraps this in a factory provider, the
|
|
24
|
+
* Express binding constructs it directly via createAgentForge().
|
|
25
|
+
*/
|
|
26
|
+
export declare class OrchestratorService {
|
|
27
|
+
private readonly anthropicConfig;
|
|
28
|
+
private readonly runner;
|
|
29
|
+
private readonly agentsMap;
|
|
30
|
+
private readonly client;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
constructor(anthropicConfig: AnthropicConfig, runner: AgentRunnerService, opts: OrchestratorServiceOptions);
|
|
33
|
+
/**
|
|
34
|
+
* Run an agent. Orchestrators automatically get delegation tools injected.
|
|
35
|
+
* Non-orchestrator agents fall straight through to the runner.
|
|
36
|
+
*/
|
|
37
|
+
run(agentId: string, messages: AnthropicMessage[], context: ToolExecutionContext): Promise<AgentResponse & {
|
|
38
|
+
delegations?: SubAgentDelegation[];
|
|
39
|
+
}>;
|
|
40
|
+
private runOrchestratorLoop;
|
|
41
|
+
private buildDelegationTools;
|
|
42
|
+
private getAgent;
|
|
43
|
+
listAgents(): AgentDefinition[];
|
|
44
|
+
getAgentById(id: string): AgentDefinition | undefined;
|
|
45
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OrchestratorService = exports.OrchestratorError = void 0;
|
|
7
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const noopLogger = {
|
|
10
|
+
log: () => { }, warn: () => { }, debug: () => { }, error: () => { },
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Operational error. Adapters map `.status` to an HTTP code.
|
|
14
|
+
*/
|
|
15
|
+
class OrchestratorError extends Error {
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.status = 404;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.OrchestratorError = OrchestratorError;
|
|
23
|
+
/**
|
|
24
|
+
* Multi-agent workflows. Orchestrator agents can delegate tasks to specialized
|
|
25
|
+
* subagents via auto-generated delegation tools that Claude sees as ordinary
|
|
26
|
+
* tools. The orchestrator runs the loop, invokes the subagent through the
|
|
27
|
+
* AgentRunnerService, and accumulates token usage across both layers.
|
|
28
|
+
*
|
|
29
|
+
* Framework-free; the Nest binding wraps this in a factory provider, the
|
|
30
|
+
* Express binding constructs it directly via createAgentForge().
|
|
31
|
+
*/
|
|
32
|
+
class OrchestratorService {
|
|
33
|
+
constructor(anthropicConfig, runner, opts) {
|
|
34
|
+
this.anthropicConfig = anthropicConfig;
|
|
35
|
+
this.runner = runner;
|
|
36
|
+
this.agentsMap = new Map();
|
|
37
|
+
this.client = new sdk_1.default({
|
|
38
|
+
apiKey: anthropicConfig.apiKey,
|
|
39
|
+
baseURL: anthropicConfig.baseURL,
|
|
40
|
+
});
|
|
41
|
+
this.logger = opts.logger ?? noopLogger;
|
|
42
|
+
for (const agent of opts.agents) {
|
|
43
|
+
this.agentsMap.set(agent.id, agent);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run an agent. Orchestrators automatically get delegation tools injected.
|
|
48
|
+
* Non-orchestrator agents fall straight through to the runner.
|
|
49
|
+
*/
|
|
50
|
+
async run(agentId, messages, context) {
|
|
51
|
+
const agent = this.getAgent(agentId);
|
|
52
|
+
if (!agent.canOrchestrate || !agent.subAgents?.length) {
|
|
53
|
+
return this.runner.run(agent, messages, context);
|
|
54
|
+
}
|
|
55
|
+
this.logger.debug(`Running orchestrator "${agentId}" with subagents: ${agent.subAgents.join(', ')}`);
|
|
56
|
+
return this.runOrchestratorLoop(agent, messages, context);
|
|
57
|
+
}
|
|
58
|
+
async runOrchestratorLoop(orchestrator, messages, context) {
|
|
59
|
+
const delegations = [];
|
|
60
|
+
const delegationTools = this.buildDelegationTools(orchestrator);
|
|
61
|
+
const model = orchestrator.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
|
|
62
|
+
const maxTokens = orchestrator.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
|
|
63
|
+
const anthropicTools = delegationTools.map((t) => ({
|
|
64
|
+
name: t.name,
|
|
65
|
+
description: t.description,
|
|
66
|
+
input_schema: t.inputSchema,
|
|
67
|
+
}));
|
|
68
|
+
let currentMessages = [...messages];
|
|
69
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
70
|
+
let finalContent = '';
|
|
71
|
+
// ─── Agentic loop ──────────────────────────────────────────────────────
|
|
72
|
+
while (true) {
|
|
73
|
+
const response = await this.client.messages.create({
|
|
74
|
+
model,
|
|
75
|
+
max_tokens: maxTokens,
|
|
76
|
+
system: orchestrator.systemPrompt,
|
|
77
|
+
messages: currentMessages,
|
|
78
|
+
tools: anthropicTools,
|
|
79
|
+
});
|
|
80
|
+
totalUsage = {
|
|
81
|
+
inputTokens: totalUsage.inputTokens + response.usage.input_tokens,
|
|
82
|
+
outputTokens: totalUsage.outputTokens + response.usage.output_tokens,
|
|
83
|
+
totalTokens: totalUsage.totalTokens +
|
|
84
|
+
response.usage.input_tokens +
|
|
85
|
+
response.usage.output_tokens,
|
|
86
|
+
};
|
|
87
|
+
if (response.stop_reason === 'tool_use') {
|
|
88
|
+
const assistantMsg = { role: 'assistant', content: response.content };
|
|
89
|
+
currentMessages = [...currentMessages, assistantMsg];
|
|
90
|
+
const toolResults = [];
|
|
91
|
+
for (const block of response.content) {
|
|
92
|
+
if (block.type !== 'tool_use')
|
|
93
|
+
continue;
|
|
94
|
+
const delegateTool = delegationTools.find((t) => t.name === block.name);
|
|
95
|
+
if (!delegateTool)
|
|
96
|
+
continue;
|
|
97
|
+
const { task } = block.input;
|
|
98
|
+
const { subAgentId } = delegateTool;
|
|
99
|
+
this.logger.log(`Orchestrator "${orchestrator.id}" → "${subAgentId}": ${task.slice(0, 80)}...`);
|
|
100
|
+
const subAgent = this.getAgent(subAgentId);
|
|
101
|
+
const subMessages = [{ role: 'user', content: task }];
|
|
102
|
+
const subResult = await this.runner.run(subAgent, subMessages, {
|
|
103
|
+
...context,
|
|
104
|
+
agentId: subAgentId,
|
|
105
|
+
});
|
|
106
|
+
totalUsage.inputTokens += subResult.usage.inputTokens;
|
|
107
|
+
totalUsage.outputTokens += subResult.usage.outputTokens;
|
|
108
|
+
totalUsage.totalTokens += subResult.usage.totalTokens;
|
|
109
|
+
delegations.push({
|
|
110
|
+
subAgentId,
|
|
111
|
+
task,
|
|
112
|
+
result: subResult.content,
|
|
113
|
+
usage: subResult.usage,
|
|
114
|
+
});
|
|
115
|
+
toolResults.push({
|
|
116
|
+
type: 'tool_result',
|
|
117
|
+
tool_use_id: block.id,
|
|
118
|
+
content: subResult.content,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
currentMessages = [...currentMessages, { role: 'user', content: toolResults }];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
finalContent = response.content
|
|
125
|
+
.filter((b) => b.type === 'text')
|
|
126
|
+
.map((b) => b.text)
|
|
127
|
+
.join('');
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
messageId: (0, crypto_1.randomUUID)(),
|
|
133
|
+
conversationId: context.conversationId,
|
|
134
|
+
content: finalContent,
|
|
135
|
+
role: 'assistant',
|
|
136
|
+
delegations: delegations.length > 0 ? delegations : undefined,
|
|
137
|
+
usage: totalUsage,
|
|
138
|
+
model,
|
|
139
|
+
stopReason: 'end_turn',
|
|
140
|
+
createdAt: new Date(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
144
|
+
buildDelegationTools(orchestrator) {
|
|
145
|
+
return (orchestrator.subAgents ?? []).map((subAgentId) => {
|
|
146
|
+
const sub = this.agentsMap.get(subAgentId);
|
|
147
|
+
const description = sub
|
|
148
|
+
? `Delegate a task to the "${sub.name}" specialist agent. ${sub.description ?? ''}`
|
|
149
|
+
: `Delegate a task to subagent "${subAgentId}"`;
|
|
150
|
+
return {
|
|
151
|
+
name: `delegate_to_${subAgentId.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
152
|
+
description,
|
|
153
|
+
subAgentId,
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
task: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description: 'The specific task or question to send to this specialist',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: ['task'],
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
getAgent(agentId) {
|
|
168
|
+
const agent = this.agentsMap.get(agentId);
|
|
169
|
+
if (!agent)
|
|
170
|
+
throw new OrchestratorError('agent_not_found', `Agent "${agentId}" not found`);
|
|
171
|
+
return agent;
|
|
172
|
+
}
|
|
173
|
+
listAgents() {
|
|
174
|
+
return Array.from(this.agentsMap.values());
|
|
175
|
+
}
|
|
176
|
+
getAgentById(id) {
|
|
177
|
+
return this.agentsMap.get(id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.OrchestratorService = OrchestratorService;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PlanRepository } from '../repositories';
|
|
2
|
+
import type { Plan, NewPlan, PlanPatch } from '../domain/plan';
|
|
3
|
+
import type { PlanDefinition } from '../types/config.types';
|
|
4
|
+
export interface PlanServiceOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Plans to seed the DB with on first boot (i.e. when `af_plans` is empty).
|
|
7
|
+
* After the table has at least one row, this seed is ignored — the DB is
|
|
8
|
+
* the source of truth. Pass your `config.billing.plans` here so existing
|
|
9
|
+
* deployments migrate transparently.
|
|
10
|
+
*/
|
|
11
|
+
seedPlans?: PlanDefinition[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads and writes plan rows from the DB, with an in-process cache invalidated
|
|
15
|
+
* on every mutation. Hot-path reads (`getPlan(id)`) are O(n) over a tiny set,
|
|
16
|
+
* which is fine for the few plans most apps need.
|
|
17
|
+
*
|
|
18
|
+
* Single-instance assumption: when running multiple API instances, mutations
|
|
19
|
+
* via admin endpoints invalidate only the local cache. The next read on other
|
|
20
|
+
* instances still serves stale data until their own TTL expires. For most B2B
|
|
21
|
+
* scenarios (plan edits are rare, eventually-consistent is OK) this is fine;
|
|
22
|
+
* if you need strict consistency, set `cacheTtlMs: 0`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class PlanService {
|
|
25
|
+
private readonly repo;
|
|
26
|
+
private readonly opts;
|
|
27
|
+
private cache;
|
|
28
|
+
private cacheLoadedAt;
|
|
29
|
+
/** Default TTL: 60 seconds. */
|
|
30
|
+
private readonly cacheTtlMs;
|
|
31
|
+
constructor(repo: PlanRepository, opts?: PlanServiceOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Call once at boot. Seeds the table from `opts.seedPlans` if empty, then
|
|
34
|
+
* warms the cache. Safe to call multiple times.
|
|
35
|
+
*/
|
|
36
|
+
initialize(): Promise<void>;
|
|
37
|
+
list(): Promise<Plan[]>;
|
|
38
|
+
listAll(): Promise<Plan[]>;
|
|
39
|
+
getPlan(id: string): Promise<Plan | undefined>;
|
|
40
|
+
getDefault(): Promise<Plan | undefined>;
|
|
41
|
+
getDefaultId(): Promise<string>;
|
|
42
|
+
create(plan: NewPlan): Promise<Plan>;
|
|
43
|
+
update(id: string, patch: PlanPatch): Promise<Plan>;
|
|
44
|
+
deactivate(id: string): Promise<void>;
|
|
45
|
+
/** Force the next read to bypass the cache. Useful in tests. */
|
|
46
|
+
invalidate(): void;
|
|
47
|
+
private refresh;
|
|
48
|
+
private isCacheFresh;
|
|
49
|
+
}
|
|
50
|
+
export declare class PlanServiceError extends Error {
|
|
51
|
+
status: number;
|
|
52
|
+
code: 'plan_exists' | 'plan_not_found' | 'plan_is_default';
|
|
53
|
+
constructor(code: 'plan_exists' | 'plan_not_found' | 'plan_is_default', message: string);
|
|
54
|
+
}
|