@vellumai/assistant 0.5.11 → 0.5.13
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/Dockerfile +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -25,11 +25,10 @@ import { prepareOAuth2Flow, startOAuth2Flow } from "../security/oauth2.js";
|
|
|
25
25
|
import { getLogger } from "../util/logger.js";
|
|
26
26
|
import type {
|
|
27
27
|
OAuthConnectResult,
|
|
28
|
-
OAuthProviderBehavior,
|
|
29
28
|
OAuthScopePolicy,
|
|
30
29
|
} from "./connect-types.js";
|
|
30
|
+
import { verifyIdentity } from "./identity-verifier.js";
|
|
31
31
|
import { getProvider } from "./oauth-store.js";
|
|
32
|
-
import { getProviderBehavior, resolveService } from "./provider-behaviors.js";
|
|
33
32
|
import { resolveScopes } from "./scope-policy.js";
|
|
34
33
|
import { storeOAuth2Tokens } from "./token-persistence.js";
|
|
35
34
|
|
|
@@ -39,28 +38,6 @@ const log = getLogger("oauth-connect-orchestrator");
|
|
|
39
38
|
// Helpers
|
|
40
39
|
// ---------------------------------------------------------------------------
|
|
41
40
|
|
|
42
|
-
/**
|
|
43
|
-
* Look up the code-side behavioral fields for a provider.
|
|
44
|
-
* Returns an empty object when no behavior is registered.
|
|
45
|
-
*/
|
|
46
|
-
function resolveBehavior(providerKey: string): {
|
|
47
|
-
identityVerifier?: OAuthProviderBehavior["identityVerifier"];
|
|
48
|
-
setup?: OAuthProviderBehavior["setup"];
|
|
49
|
-
setupSkillId?: string;
|
|
50
|
-
postConnectHookId?: string;
|
|
51
|
-
loopbackPort?: number;
|
|
52
|
-
} {
|
|
53
|
-
const behavior = getProviderBehavior(providerKey);
|
|
54
|
-
if (!behavior) return {};
|
|
55
|
-
return {
|
|
56
|
-
identityVerifier: behavior.identityVerifier,
|
|
57
|
-
setup: behavior.setup,
|
|
58
|
-
setupSkillId: behavior.setupSkillId,
|
|
59
|
-
postConnectHookId: behavior.postConnectHookId,
|
|
60
|
-
loopbackPort: behavior.loopbackPort,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
41
|
/** Safely parse a JSON string, returning a fallback on failure or null/undefined input. */
|
|
65
42
|
function safeJsonParse<T>(value: string | null | undefined, fallback: T): T {
|
|
66
43
|
if (value == null) return fallback;
|
|
@@ -76,7 +53,7 @@ function safeJsonParse<T>(value: string | null | undefined, fallback: T): T {
|
|
|
76
53
|
// ---------------------------------------------------------------------------
|
|
77
54
|
|
|
78
55
|
export interface OAuthConnectOptions {
|
|
79
|
-
/**
|
|
56
|
+
/** Canonical service name (e.g. "google", "slack"). */
|
|
80
57
|
service: string;
|
|
81
58
|
/** Scopes to request beyond the provider's defaults. */
|
|
82
59
|
requestedScopes?: string[];
|
|
@@ -119,11 +96,9 @@ export interface OAuthConnectOptions {
|
|
|
119
96
|
export async function orchestrateOAuthConnect(
|
|
120
97
|
options: OAuthConnectOptions,
|
|
121
98
|
): Promise<OAuthConnectResult> {
|
|
122
|
-
const resolvedService = resolveService(options.service);
|
|
123
99
|
log.info(
|
|
124
100
|
{
|
|
125
|
-
|
|
126
|
-
resolvedService,
|
|
101
|
+
service: options.service,
|
|
127
102
|
isInteractive: options.isInteractive,
|
|
128
103
|
hasOpenUrl: !!options.openUrl,
|
|
129
104
|
hasSendToClient: !!options.sendToClient,
|
|
@@ -132,18 +107,15 @@ export async function orchestrateOAuthConnect(
|
|
|
132
107
|
);
|
|
133
108
|
|
|
134
109
|
// Read provider config from the DB
|
|
135
|
-
const providerRow = getProvider(
|
|
110
|
+
const providerRow = getProvider(options.service);
|
|
136
111
|
if (!providerRow) {
|
|
137
112
|
return {
|
|
138
113
|
success: false,
|
|
139
|
-
error: `No OAuth provider registered for "${
|
|
114
|
+
error: `No OAuth provider registered for "${options.service}". Ensure the provider is seeded in the database.`,
|
|
140
115
|
safeError: true,
|
|
141
116
|
};
|
|
142
117
|
}
|
|
143
118
|
|
|
144
|
-
// Behavioral/code-side fields come from the behavior registry
|
|
145
|
-
const behavior = resolveBehavior(resolvedService);
|
|
146
|
-
|
|
147
119
|
// Deserialize JSON fields from the DB row
|
|
148
120
|
const dbDefaultScopes = safeJsonParse<string[]>(
|
|
149
121
|
providerRow.defaultScopes,
|
|
@@ -173,11 +145,11 @@ export async function orchestrateOAuthConnect(
|
|
|
173
145
|
const callbackTransport =
|
|
174
146
|
(providerRow.callbackTransport as "loopback" | "gateway" | null) ??
|
|
175
147
|
"loopback";
|
|
176
|
-
const loopbackPort =
|
|
148
|
+
const loopbackPort = providerRow.loopbackPort ?? undefined;
|
|
177
149
|
|
|
178
150
|
// Resolve scopes via the scope policy engine
|
|
179
151
|
const scopeProfile = {
|
|
180
|
-
service:
|
|
152
|
+
service: options.service,
|
|
181
153
|
defaultScopes: dbDefaultScopes,
|
|
182
154
|
scopePolicy: dbScopePolicy,
|
|
183
155
|
};
|
|
@@ -211,7 +183,7 @@ export async function orchestrateOAuthConnect(
|
|
|
211
183
|
|
|
212
184
|
log.info(
|
|
213
185
|
{
|
|
214
|
-
service:
|
|
186
|
+
service: options.service,
|
|
215
187
|
authUrl,
|
|
216
188
|
tokenUrl,
|
|
217
189
|
scopeCount: finalScopes.length,
|
|
@@ -235,7 +207,7 @@ export async function orchestrateOAuthConnect(
|
|
|
235
207
|
};
|
|
236
208
|
|
|
237
209
|
const storageParams = {
|
|
238
|
-
service:
|
|
210
|
+
service: options.service,
|
|
239
211
|
clientId: options.clientId,
|
|
240
212
|
clientSecret: options.clientSecret,
|
|
241
213
|
userinfoUrl,
|
|
@@ -276,19 +248,12 @@ export async function orchestrateOAuthConnect(
|
|
|
276
248
|
prepared.completion
|
|
277
249
|
.then(async (result) => {
|
|
278
250
|
try {
|
|
279
|
-
let parsedAccountIdentifier: string | undefined;
|
|
280
|
-
|
|
281
251
|
// Parse account identifier from the provider's identity endpoint.
|
|
282
252
|
// Best-effort — format varies by provider and may fail.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
} catch {
|
|
289
|
-
// Non-fatal
|
|
290
|
-
}
|
|
291
|
-
}
|
|
253
|
+
const parsedAccountIdentifier = await verifyIdentity(
|
|
254
|
+
providerRow,
|
|
255
|
+
result.tokens.accessToken,
|
|
256
|
+
);
|
|
292
257
|
|
|
293
258
|
const stored = await storeOAuth2Tokens({
|
|
294
259
|
...storageParams,
|
|
@@ -299,36 +264,36 @@ export async function orchestrateOAuthConnect(
|
|
|
299
264
|
});
|
|
300
265
|
log.info(
|
|
301
266
|
{
|
|
302
|
-
service:
|
|
267
|
+
service: options.service,
|
|
303
268
|
accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
|
|
304
269
|
},
|
|
305
270
|
"Deferred OAuth2 flow completed — tokens stored",
|
|
306
271
|
);
|
|
307
272
|
options.onDeferredComplete?.({
|
|
308
273
|
success: true,
|
|
309
|
-
service:
|
|
274
|
+
service: options.service,
|
|
310
275
|
accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
|
|
311
276
|
});
|
|
312
277
|
} catch (err) {
|
|
313
278
|
log.error(
|
|
314
|
-
{ err, service:
|
|
279
|
+
{ err, service: options.service },
|
|
315
280
|
"Failed to store tokens from deferred OAuth2 flow",
|
|
316
281
|
);
|
|
317
282
|
options.onDeferredComplete?.({
|
|
318
283
|
success: false,
|
|
319
|
-
service:
|
|
284
|
+
service: options.service,
|
|
320
285
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
321
286
|
});
|
|
322
287
|
}
|
|
323
288
|
})
|
|
324
289
|
.catch((err) => {
|
|
325
290
|
log.error(
|
|
326
|
-
{ err, service:
|
|
291
|
+
{ err, service: options.service },
|
|
327
292
|
"Deferred OAuth2 flow failed",
|
|
328
293
|
);
|
|
329
294
|
options.onDeferredComplete?.({
|
|
330
295
|
success: false,
|
|
331
|
-
service:
|
|
296
|
+
service: options.service,
|
|
332
297
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
333
298
|
});
|
|
334
299
|
});
|
|
@@ -338,7 +303,7 @@ export async function orchestrateOAuthConnect(
|
|
|
338
303
|
deferred: true,
|
|
339
304
|
authUrl: prepared.authUrl,
|
|
340
305
|
state: prepared.state,
|
|
341
|
-
service:
|
|
306
|
+
service: options.service,
|
|
342
307
|
};
|
|
343
308
|
} catch (err: unknown) {
|
|
344
309
|
const message =
|
|
@@ -347,7 +312,7 @@ export async function orchestrateOAuthConnect(
|
|
|
347
312
|
: "Unknown error preparing OAuth flow";
|
|
348
313
|
return {
|
|
349
314
|
success: false,
|
|
350
|
-
error: `Error connecting "${
|
|
315
|
+
error: `Error connecting "${options.service}": ${message}`,
|
|
351
316
|
};
|
|
352
317
|
}
|
|
353
318
|
}
|
|
@@ -356,7 +321,7 @@ export async function orchestrateOAuthConnect(
|
|
|
356
321
|
// Interactive path — open browser, block until completion
|
|
357
322
|
// -----------------------------------------------------------------------
|
|
358
323
|
log.info(
|
|
359
|
-
{ service:
|
|
324
|
+
{ service: options.service, callbackTransport, loopbackPort },
|
|
360
325
|
"orchestrateOAuthConnect: entering interactive path",
|
|
361
326
|
);
|
|
362
327
|
try {
|
|
@@ -365,7 +330,7 @@ export async function orchestrateOAuthConnect(
|
|
|
365
330
|
{
|
|
366
331
|
openUrl: (url) => {
|
|
367
332
|
log.info(
|
|
368
|
-
{ service:
|
|
333
|
+
{ service: options.service, urlLength: url.length },
|
|
369
334
|
"orchestrateOAuthConnect: openUrl callback fired, delivering auth URL to client",
|
|
370
335
|
);
|
|
371
336
|
if (options.openUrl) {
|
|
@@ -376,7 +341,7 @@ export async function orchestrateOAuthConnect(
|
|
|
376
341
|
options.sendToClient({
|
|
377
342
|
type: "open_url",
|
|
378
343
|
url,
|
|
379
|
-
title: `Connect ${
|
|
344
|
+
title: `Connect ${options.service}`,
|
|
380
345
|
});
|
|
381
346
|
} else {
|
|
382
347
|
log.warn("orchestrateOAuthConnect: no openUrl or sendToClient available — auth URL will not reach the user");
|
|
@@ -391,25 +356,21 @@ export async function orchestrateOAuthConnect(
|
|
|
391
356
|
);
|
|
392
357
|
|
|
393
358
|
log.info(
|
|
394
|
-
{ service:
|
|
359
|
+
{ service: options.service, grantedScopeCount: grantedScopes.length },
|
|
395
360
|
"orchestrateOAuthConnect: interactive flow completed, exchanged code for tokens",
|
|
396
361
|
);
|
|
397
362
|
|
|
398
363
|
// Parse account identifier from the provider's identity endpoint.
|
|
399
364
|
// Best-effort — format varies by provider and may fail.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
);
|
|
410
|
-
} catch {
|
|
411
|
-
// Non-fatal
|
|
412
|
-
}
|
|
365
|
+
const parsedAccountIdentifier = await verifyIdentity(
|
|
366
|
+
providerRow,
|
|
367
|
+
tokens.accessToken,
|
|
368
|
+
);
|
|
369
|
+
if (parsedAccountIdentifier) {
|
|
370
|
+
log.info(
|
|
371
|
+
{ service: options.service, parsedAccountIdentifier },
|
|
372
|
+
"orchestrateOAuthConnect: identity verified",
|
|
373
|
+
);
|
|
413
374
|
}
|
|
414
375
|
|
|
415
376
|
const { accountInfo } = await storeOAuth2Tokens({
|
|
@@ -421,7 +382,7 @@ export async function orchestrateOAuthConnect(
|
|
|
421
382
|
});
|
|
422
383
|
|
|
423
384
|
log.info(
|
|
424
|
-
{ service:
|
|
385
|
+
{ service: options.service, accountInfo },
|
|
425
386
|
"orchestrateOAuthConnect: tokens stored, connect complete",
|
|
426
387
|
);
|
|
427
388
|
|
|
@@ -435,12 +396,12 @@ export async function orchestrateOAuthConnect(
|
|
|
435
396
|
const message =
|
|
436
397
|
err instanceof Error ? err.message : "Unknown error during OAuth flow";
|
|
437
398
|
log.error(
|
|
438
|
-
{ service:
|
|
399
|
+
{ service: options.service, err },
|
|
439
400
|
"orchestrateOAuthConnect: interactive flow failed",
|
|
440
401
|
);
|
|
441
402
|
return {
|
|
442
403
|
success: false,
|
|
443
|
-
error: `Error connecting "${
|
|
404
|
+
error: `Error connecting "${options.service}": ${message}`,
|
|
444
405
|
};
|
|
445
406
|
}
|
|
446
407
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the OAuth provider extensibility layer.
|
|
3
3
|
*
|
|
4
|
-
* These types are consumed by the
|
|
5
|
-
*
|
|
4
|
+
* These types are consumed by the token persistence module and the
|
|
5
|
+
* credential vault orchestrator.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* All provider configuration — protocol-level OAuth config (authUrl,
|
|
8
|
+
* tokenUrl, scopes, etc.) as well as behavioral config (identity
|
|
9
|
+
* verification, injection templates, setup metadata) — is now stored
|
|
10
|
+
* exclusively in the `oauth_providers` SQLite table and seeded on
|
|
11
|
+
* startup via `seed-providers.ts`.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import type { CredentialInjectionTemplate } from "../tools/credentials/policy-types.js";
|
|
14
|
-
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
16
15
|
// Scope policy
|
|
17
16
|
// ---------------------------------------------------------------------------
|
|
@@ -26,63 +25,6 @@ export interface OAuthScopePolicy {
|
|
|
26
25
|
forbiddenScopes: string[];
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Provider behavior
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Code-side behavioral configuration for a well-known OAuth provider.
|
|
35
|
-
*
|
|
36
|
-
* Protocol-level fields (authUrl, tokenUrl, defaultScopes, scopePolicy,
|
|
37
|
-
* tokenEndpointAuthMethod, callbackTransport, userinfoUrl, extraParams)
|
|
38
|
-
* are stored in the `oauth_providers` DB table. This
|
|
39
|
-
* interface contains only fields that require code references (functions,
|
|
40
|
-
* templates, skill IDs) and cannot be serialised to a DB row.
|
|
41
|
-
*/
|
|
42
|
-
export interface OAuthProviderBehavior {
|
|
43
|
-
/** Canonical service key (e.g. "integration:twitter"). */
|
|
44
|
-
service: string;
|
|
45
|
-
/**
|
|
46
|
-
* Async function that verifies the user's identity after a successful
|
|
47
|
-
* token exchange. Returns a human-readable account identifier (e.g.
|
|
48
|
-
* email or @username) or undefined if verification is not possible.
|
|
49
|
-
*/
|
|
50
|
-
identityVerifier?: (accessToken: string) => Promise<string | undefined>;
|
|
51
|
-
/** ID of a post-connect hook to run after token storage. */
|
|
52
|
-
postConnectHookId?: string;
|
|
53
|
-
/** Injection templates auto-applied to the access_token credential. */
|
|
54
|
-
injectionTemplates?: CredentialInjectionTemplate[];
|
|
55
|
-
/**
|
|
56
|
-
* Metadata for the generic OAuth setup skill. When present, the
|
|
57
|
-
* assistant can guide users through app creation and OAuth connection.
|
|
58
|
-
*/
|
|
59
|
-
setup?: {
|
|
60
|
-
/** Human-readable provider name (e.g. "Discord", "Linear"). */
|
|
61
|
-
displayName: string;
|
|
62
|
-
/** URL of the developer dashboard where the user creates an app. */
|
|
63
|
-
dashboardUrl: string;
|
|
64
|
-
/** What the provider calls its apps (e.g. "Discord Application"). */
|
|
65
|
-
appType: string;
|
|
66
|
-
/** Whether the provider requires a client_secret for token exchange. */
|
|
67
|
-
requiresClientSecret: boolean;
|
|
68
|
-
/** Provider-specific notes the LLM should follow during setup. */
|
|
69
|
-
notes?: string[];
|
|
70
|
-
};
|
|
71
|
-
/**
|
|
72
|
-
* Bundled skill ID that contains provider-specific setup instructions.
|
|
73
|
-
* When present, the guardrail for missing client_secret directs the
|
|
74
|
-
* agent to load this skill rather than embedding instructions inline.
|
|
75
|
-
*/
|
|
76
|
-
setupSkillId?: string;
|
|
77
|
-
/**
|
|
78
|
-
* Fixed port for the loopback OAuth callback server. When set, the
|
|
79
|
-
* server binds to this port instead of an OS-assigned random port.
|
|
80
|
-
* Required for providers that need pre-registered redirect URIs
|
|
81
|
-
* (e.g. Slack, Notion).
|
|
82
|
-
*/
|
|
83
|
-
loopbackPort?: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
28
|
// ---------------------------------------------------------------------------
|
|
87
29
|
// Connect result
|
|
88
30
|
// ---------------------------------------------------------------------------
|
|
@@ -79,13 +79,13 @@ function makeMockClient() {
|
|
|
79
79
|
|
|
80
80
|
function setupDefaults(): void {
|
|
81
81
|
mockProvider = {
|
|
82
|
-
providerKey: "
|
|
82
|
+
providerKey: "google",
|
|
83
83
|
baseUrl: "https://gmail.googleapis.com/gmail/v1/users/me",
|
|
84
84
|
managedServiceConfigKey: null,
|
|
85
85
|
};
|
|
86
86
|
mockConnection = {
|
|
87
87
|
id: "conn-1",
|
|
88
|
-
providerKey: "
|
|
88
|
+
providerKey: "google",
|
|
89
89
|
oauthAppId: "app-1",
|
|
90
90
|
accountInfo: "user@example.com",
|
|
91
91
|
grantedScopes: JSON.stringify(["scope-a", "scope-b"]),
|
|
@@ -122,26 +122,26 @@ describe("resolveOAuthConnection", () => {
|
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
test("returns BYOOAuthConnection when provider has no managedServiceConfigKey", async () => {
|
|
125
|
-
const result = await resolveOAuthConnection("
|
|
125
|
+
const result = await resolveOAuthConnection("google");
|
|
126
126
|
expect(result).toBeInstanceOf(BYOOAuthConnection);
|
|
127
127
|
expect(result.id).toBe("conn-1");
|
|
128
|
-
expect(result.providerKey).toBe("
|
|
128
|
+
expect(result.providerKey).toBe("google");
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
test("returns PlatformOAuthConnection when managed mode is active", async () => {
|
|
132
132
|
mockProvider!.managedServiceConfigKey = "google-oauth";
|
|
133
133
|
|
|
134
|
-
const result = await resolveOAuthConnection("
|
|
134
|
+
const result = await resolveOAuthConnection("google");
|
|
135
135
|
expect(result).toBeInstanceOf(PlatformOAuthConnection);
|
|
136
|
-
expect(result.id).toBe("
|
|
137
|
-
expect(result.providerKey).toBe("
|
|
136
|
+
expect(result.id).toBe("google");
|
|
137
|
+
expect(result.providerKey).toBe("google");
|
|
138
138
|
expect(result.accountInfo).toBeNull();
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test("passes account through to PlatformOAuthConnection", async () => {
|
|
142
142
|
mockProvider!.managedServiceConfigKey = "google-oauth";
|
|
143
143
|
|
|
144
|
-
const result = await resolveOAuthConnection("
|
|
144
|
+
const result = await resolveOAuthConnection("google", {
|
|
145
145
|
account: "user@example.com",
|
|
146
146
|
});
|
|
147
147
|
expect(result).toBeInstanceOf(PlatformOAuthConnection);
|
|
@@ -154,7 +154,7 @@ describe("resolveOAuthConnection", () => {
|
|
|
154
154
|
mode: "your-own",
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
const result = await resolveOAuthConnection("
|
|
157
|
+
const result = await resolveOAuthConnection("google");
|
|
158
158
|
expect(result).toBeInstanceOf(BYOOAuthConnection);
|
|
159
159
|
expect(result.id).toBe("conn-1");
|
|
160
160
|
});
|
|
@@ -164,21 +164,21 @@ describe("resolveOAuthConnection", () => {
|
|
|
164
164
|
mockConnection = undefined;
|
|
165
165
|
mockAccessToken = undefined;
|
|
166
166
|
|
|
167
|
-
const result = await resolveOAuthConnection("
|
|
167
|
+
const result = await resolveOAuthConnection("google");
|
|
168
168
|
expect(result).toBeInstanceOf(PlatformOAuthConnection);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
test("managed path ignores clientId option", async () => {
|
|
172
172
|
mockProvider!.managedServiceConfigKey = "google-oauth";
|
|
173
173
|
|
|
174
|
-
const result = await resolveOAuthConnection("
|
|
174
|
+
const result = await resolveOAuthConnection("google", {
|
|
175
175
|
clientId: "some-client-id",
|
|
176
176
|
});
|
|
177
177
|
expect(result).toBeInstanceOf(PlatformOAuthConnection);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
test("BYO path narrows by clientId when provided", async () => {
|
|
181
|
-
const result = await resolveOAuthConnection("
|
|
181
|
+
const result = await resolveOAuthConnection("google", {
|
|
182
182
|
clientId: "client-1",
|
|
183
183
|
});
|
|
184
184
|
expect(result).toBeInstanceOf(BYOOAuthConnection);
|
|
@@ -187,7 +187,7 @@ describe("resolveOAuthConnection", () => {
|
|
|
187
187
|
|
|
188
188
|
test("BYO path returns no credential when clientId does not match", async () => {
|
|
189
189
|
await expect(
|
|
190
|
-
resolveOAuthConnection("
|
|
190
|
+
resolveOAuthConnection("google", {
|
|
191
191
|
clientId: "wrong-client",
|
|
192
192
|
}),
|
|
193
193
|
).rejects.toThrow(/No active OAuth connection found/);
|
|
@@ -29,7 +29,7 @@ export interface ResolveOAuthConnectionOptions {
|
|
|
29
29
|
* BYO providers resolve from the local SQLite oauth-store and require an
|
|
30
30
|
* active connection row and a stored access token.
|
|
31
31
|
*
|
|
32
|
-
* @param providerKey - Provider identifier (e.g. "
|
|
32
|
+
* @param providerKey - Provider identifier (e.g. "google").
|
|
33
33
|
* Maps to the `provider_key` primary key in the `oauth_providers` table.
|
|
34
34
|
* @param options.clientId - Optional OAuth app client ID. When multiple BYO
|
|
35
35
|
* apps exist for the same provider, narrows the connection lookup to the
|
|
@@ -59,11 +59,9 @@ export async function resolveOAuthConnection(
|
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const providerSlug = providerKey.replace(/^integration:/, "");
|
|
63
|
-
|
|
64
62
|
const connectionId = await resolvePlatformConnectionId({
|
|
65
63
|
client,
|
|
66
|
-
provider:
|
|
64
|
+
provider: providerKey,
|
|
67
65
|
account,
|
|
68
66
|
});
|
|
69
67
|
|
|
@@ -74,6 +72,7 @@ export async function resolveOAuthConnection(
|
|
|
74
72
|
accountInfo: account ?? null,
|
|
75
73
|
client,
|
|
76
74
|
connectionId,
|
|
75
|
+
baseUrl: provider?.baseUrl ?? undefined,
|
|
77
76
|
});
|
|
78
77
|
}
|
|
79
78
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, data-driven identity verifier for OAuth providers.
|
|
3
|
+
*
|
|
4
|
+
* Replaces per-provider hand-coded `identityVerifier` functions with a
|
|
5
|
+
* single function that interprets the declarative identity configuration
|
|
6
|
+
* stored in the `oauth_providers` DB table (identityUrl, identityMethod,
|
|
7
|
+
* identityHeaders, identityBody, identityResponsePaths, identityFormat,
|
|
8
|
+
* identityOkField).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { OAuthProviderRow } from "./oauth-store.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Traverse a nested object by a dot-separated path (e.g. "data.viewer.email"). */
|
|
18
|
+
function getNestedValue(obj: unknown, dotPath: string): unknown {
|
|
19
|
+
const parts = dotPath.split(".");
|
|
20
|
+
let current: unknown = obj;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
23
|
+
current = (current as Record<string, unknown>)[part];
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Safely parse a JSON string, returning a fallback on failure or null/undefined input. */
|
|
29
|
+
function safeJsonParse<T>(value: string | null | undefined, fallback: T): T {
|
|
30
|
+
if (value == null) return fallback;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(value) as T;
|
|
33
|
+
} catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Public API
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Verify the user's identity after a successful OAuth token exchange.
|
|
44
|
+
*
|
|
45
|
+
* Returns a human-readable account identifier (e.g. email, @username, or
|
|
46
|
+
* a formatted string like "@user (team)") or `undefined` when:
|
|
47
|
+
* - the provider has no identity URL configured
|
|
48
|
+
* - the identity request fails or returns a non-OK status
|
|
49
|
+
* - the response cannot be parsed into an identifier
|
|
50
|
+
*
|
|
51
|
+
* This function is intentionally non-throwing — identity verification is
|
|
52
|
+
* always best-effort.
|
|
53
|
+
*/
|
|
54
|
+
export async function verifyIdentity(
|
|
55
|
+
providerRow: OAuthProviderRow,
|
|
56
|
+
accessToken: string,
|
|
57
|
+
): Promise<string | undefined> {
|
|
58
|
+
const { identityUrl: rawUrl } = providerRow;
|
|
59
|
+
if (!rawUrl) return undefined;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Interpolate ${accessToken} in the URL (HubSpot pattern)
|
|
63
|
+
const urlContainsToken = rawUrl.includes("${accessToken}");
|
|
64
|
+
const url = rawUrl.replace("${accessToken}", accessToken);
|
|
65
|
+
|
|
66
|
+
// Build headers
|
|
67
|
+
const parsedHeaders = safeJsonParse<Record<string, string>>(
|
|
68
|
+
providerRow.identityHeaders,
|
|
69
|
+
{},
|
|
70
|
+
);
|
|
71
|
+
const headers: Record<string, string> = {
|
|
72
|
+
...parsedHeaders,
|
|
73
|
+
};
|
|
74
|
+
// Only add the Authorization header if the token is not embedded in the URL
|
|
75
|
+
if (!urlContainsToken) {
|
|
76
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build request init
|
|
80
|
+
const method = providerRow.identityMethod ?? "GET";
|
|
81
|
+
const init: RequestInit = { method, headers };
|
|
82
|
+
|
|
83
|
+
// Add body if present
|
|
84
|
+
if (providerRow.identityBody != null) {
|
|
85
|
+
const bodyValue = safeJsonParse<unknown>(
|
|
86
|
+
providerRow.identityBody,
|
|
87
|
+
providerRow.identityBody,
|
|
88
|
+
);
|
|
89
|
+
if (typeof bodyValue === "string") {
|
|
90
|
+
init.body = bodyValue;
|
|
91
|
+
} else {
|
|
92
|
+
init.body = JSON.stringify(bodyValue);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Make the request
|
|
97
|
+
const resp = await fetch(url, init);
|
|
98
|
+
if (!resp.ok) return undefined;
|
|
99
|
+
|
|
100
|
+
const body: unknown = await resp.json();
|
|
101
|
+
|
|
102
|
+
// Check OK field (Slack pattern: body.ok must be truthy)
|
|
103
|
+
if (providerRow.identityOkField) {
|
|
104
|
+
const okValue = getNestedValue(body, providerRow.identityOkField);
|
|
105
|
+
if (!okValue) return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parse response paths
|
|
109
|
+
const responsePaths = safeJsonParse<string[]>(
|
|
110
|
+
providerRow.identityResponsePaths,
|
|
111
|
+
[],
|
|
112
|
+
);
|
|
113
|
+
if (responsePaths.length === 0) return undefined;
|
|
114
|
+
|
|
115
|
+
const { identityFormat } = providerRow;
|
|
116
|
+
|
|
117
|
+
// Simple mode: no format template — return the first non-null path value
|
|
118
|
+
if (!identityFormat) {
|
|
119
|
+
for (const path of responsePaths) {
|
|
120
|
+
const value = getNestedValue(body, path);
|
|
121
|
+
if (value != null) return String(value);
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Format mode: build a lookup map from all paths, then interpolate
|
|
127
|
+
const pathValues = new Map<string, string | undefined>();
|
|
128
|
+
for (const path of responsePaths) {
|
|
129
|
+
const value = getNestedValue(body, path);
|
|
130
|
+
pathValues.set(path, value != null ? String(value) : undefined);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Replace ${path} tokens in the format string
|
|
134
|
+
let result = identityFormat;
|
|
135
|
+
let allResolved = true;
|
|
136
|
+
for (const [path, value] of pathValues) {
|
|
137
|
+
if (value != null) {
|
|
138
|
+
result = result.replace(`\${${path}}`, value);
|
|
139
|
+
} else {
|
|
140
|
+
allResolved = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (allResolved) return result;
|
|
145
|
+
|
|
146
|
+
// Fallback: if some tokens couldn't be resolved, try cleaning up
|
|
147
|
+
// the format string by removing unresolved tokens and their surrounding
|
|
148
|
+
// punctuation (parentheses, spaces).
|
|
149
|
+
// First, try removing unresolved tokens with their surrounding parens/space
|
|
150
|
+
// e.g. "@${user} (${team})" with missing team -> "@user"
|
|
151
|
+
let cleaned = identityFormat;
|
|
152
|
+
for (const [path, value] of pathValues) {
|
|
153
|
+
if (value != null) {
|
|
154
|
+
cleaned = cleaned.replace(`\${${path}}`, value);
|
|
155
|
+
} else {
|
|
156
|
+
// Remove patterns like " (${path})" or " ${path}" or "(${path})"
|
|
157
|
+
cleaned = cleaned.replace(
|
|
158
|
+
new RegExp(`\\s*\\(?\\$\\{${path.replace(/\./g, "\\.")}\\}\\)?`, "g"),
|
|
159
|
+
"",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
cleaned = cleaned.trim();
|
|
165
|
+
if (cleaned) return cleaned;
|
|
166
|
+
|
|
167
|
+
// Last resort: return the first non-null path value
|
|
168
|
+
for (const value of pathValues.values()) {
|
|
169
|
+
if (value != null) return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return undefined;
|
|
173
|
+
} catch {
|
|
174
|
+
// Non-fatal — identity verification is best-effort
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|