@agentforge-io/core 2.0.10 → 2.0.11

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.
@@ -96,17 +96,27 @@ class AgentService {
96
96
  * Returns the runtime `AgentDefinition` ready to feed the runner.
97
97
  */
98
98
  async resolveAgent(params) {
99
+ // Public-chat slug lookup MUST gate on isActive — a deactivated
100
+ // agent shouldn't be reachable from a customer-facing widget.
99
101
  if (params.agentSlug && params.tenantId && this.resolver) {
100
102
  const record = await this.resolver.findBySlug(params.tenantId, params.agentSlug);
101
103
  if (record && record.isActive)
102
104
  return toAgentDefinition(record);
103
105
  }
106
+ // Direct-id lookup does NOT gate on isActive. The caller has the
107
+ // opaque uuid because they already created a conversation against
108
+ // that agent (admin UI, system Agent Assist seed, internal job…).
109
+ // Refusing to stream the next turn just because someone toggled
110
+ // the agent inactive between turns surfaces as the opaque
111
+ // "Agent <uuid> not found" the admin saw in prod — actively
112
+ // hostile to operators. Leave inactive-filtering to the layer
113
+ // that handed out the agentId in the first place.
104
114
  if (params.agentId) {
105
115
  if (this.resolver) {
106
116
  const record = await this.resolver
107
117
  .findById(params.agentId)
108
118
  .catch(() => null);
109
- if (record && record.isActive)
119
+ if (record)
110
120
  return toAgentDefinition(record);
111
121
  }
112
122
  // Final fallback: hardcoded SDK array.
@@ -19,6 +19,23 @@ export interface OAuth2ProviderConfig {
19
19
  /** PKCE: defaults to true for security, but providers that don't support
20
20
  * it (rare in 2026) can opt out. */
21
21
  usePkce?: boolean;
22
+ /**
23
+ * Optional extractor for non-standard token response shapes. The default
24
+ * reads `access_token` / `refresh_token` / etc. directly off the JSON
25
+ * body — RFC 6749 compliant providers (Google, HubSpot, ClickUp) don't
26
+ * need this.
27
+ *
28
+ * Slack's `oauth.v2.access` is the notable exception: when the consent
29
+ * grants only user scopes, the user token lands at
30
+ * `json.authed_user.access_token`, not at the root. Connectors with
31
+ * non-standard envelopes provide this hook to map their response into
32
+ * the canonical `TokenSet` the registry persists.
33
+ *
34
+ * If the extractor returns `accessToken: ''` (or any falsy string), the
35
+ * service treats the exchange as failed and surfaces a clear error
36
+ * instead of letting an empty string poison the encrypted-token row.
37
+ */
38
+ tokenExtractor?: (json: unknown) => TokenSet;
22
39
  }
23
40
  export interface AuthorizeUrlResult {
24
41
  url: string;
@@ -45,7 +45,7 @@ class OAuth2Service {
45
45
  });
46
46
  if (pkceVerifier)
47
47
  body.set('code_verifier', pkceVerifier);
48
- return this.postToken(cfg.tokenUrl, body);
48
+ return this.postToken(cfg.tokenUrl, body, cfg.tokenExtractor);
49
49
  }
50
50
  async refresh(cfg, refreshToken) {
51
51
  const body = new URLSearchParams({
@@ -54,9 +54,9 @@ class OAuth2Service {
54
54
  client_id: cfg.clientId,
55
55
  client_secret: cfg.clientSecret,
56
56
  });
57
- return this.postToken(cfg.tokenUrl, body);
57
+ return this.postToken(cfg.tokenUrl, body, cfg.tokenExtractor);
58
58
  }
59
- async postToken(tokenUrl, body) {
59
+ async postToken(tokenUrl, body, extractor) {
60
60
  const res = await this.fetchImpl(tokenUrl, {
61
61
  method: 'POST',
62
62
  headers: {
@@ -70,13 +70,30 @@ class OAuth2Service {
70
70
  throw new Error(`OAuth2 token endpoint ${tokenUrl} returned ${res.status}: ${text}`);
71
71
  }
72
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
- };
73
+ const tokens = extractor ? extractor(json) : defaultExtractor(json);
74
+ // Guard against accidentally persisting an empty-string token — that
75
+ // would crash the cipher with "Received undefined" downstream and the
76
+ // operator would chase a confusing stack trace. Better to fail fast
77
+ // here with a descriptive error so the connector author knows their
78
+ // extractor (or the default) missed the token.
79
+ if (!tokens.accessToken) {
80
+ throw new Error(`OAuth2 token endpoint ${tokenUrl} returned a response without an access token. ` +
81
+ 'If the provider uses a non-standard envelope, supply `tokenExtractor` on the ConnectorDefinition.');
82
+ }
83
+ return tokens;
80
84
  }
81
85
  }
82
86
  exports.OAuth2Service = OAuth2Service;
87
+ /** RFC 6749 standard shape — used when the connector doesn't supply a
88
+ * custom extractor. Mirrors the original behavior before the
89
+ * `tokenExtractor` hook was added. */
90
+ function defaultExtractor(json) {
91
+ const j = json;
92
+ return {
93
+ accessToken: j.access_token ?? '',
94
+ refreshToken: j.refresh_token,
95
+ expiresIn: j.expires_in,
96
+ scope: j.scope,
97
+ tokenType: j.token_type,
98
+ };
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/core",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "Framework-free AI runtime SDK. Owns: agent loop (Anthropic), conversations, tools, streaming, agent-job queue, SdkHooks. Identity, billing, infra (email/uploads/secrets) live in the host's modules — not here.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",