@intent-systems/nexus 2026.1.5-3 → 2026.1.5-5

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 (144) hide show
  1. package/dist/agents/agent-id.js +41 -0
  2. package/dist/agents/auth-profiles.js +114 -25
  3. package/dist/agents/identity-state.js +79 -0
  4. package/dist/agents/model-auth.js +1 -0
  5. package/dist/agents/model-fallback.js +15 -9
  6. package/dist/agents/model-selection.js +1 -1
  7. package/dist/agents/models-config.js +17 -11
  8. package/dist/agents/pi-embedded-runner.js +101 -9
  9. package/dist/agents/sandbox.js +12 -3
  10. package/dist/agents/skill-runner.js +29 -4
  11. package/dist/agents/skill-usage.js +114 -11
  12. package/dist/agents/skills-status.js +4 -4
  13. package/dist/agents/skills.js +18 -7
  14. package/dist/agents/subagent-registry.js +25 -11
  15. package/dist/agents/system-prompt.js +16 -0
  16. package/dist/agents/tool-policy.js +19 -3
  17. package/dist/agents/tools/browser-tool.js +5 -2
  18. package/dist/agents/tools/image-tool.js +93 -8
  19. package/dist/agents/tools/sessions-announce-target.js +5 -1
  20. package/dist/agents/workspace.js +55 -46
  21. package/dist/auto-reply/command-detection.js +2 -1
  22. package/dist/auto-reply/reply/directive-handling.js +153 -28
  23. package/dist/auto-reply/reply/directives.js +17 -2
  24. package/dist/auto-reply/reply/model-selection.js +8 -3
  25. package/dist/auto-reply/reply/queue.js +2 -2
  26. package/dist/auto-reply/reply.js +1 -1
  27. package/dist/auto-reply/thinking.js +15 -0
  28. package/dist/browser/chrome.js +1 -1
  29. package/dist/browser/client.js +2 -0
  30. package/dist/browser/config.js +6 -2
  31. package/dist/browser/pw-tools-core.js +3 -0
  32. package/dist/browser/routes/agent.js +14 -0
  33. package/dist/canvas-host/server.js +1 -1
  34. package/dist/capabilities/detector.js +245 -0
  35. package/dist/capabilities/registry.js +99 -0
  36. package/dist/channels/location.js +44 -0
  37. package/dist/channels/web/index.js +2 -0
  38. package/dist/cli/cloud-cli.js +12 -7
  39. package/dist/cli/credential-cli.js +139 -17
  40. package/dist/cli/gateway-cli.js +1 -1
  41. package/dist/cli/log-cli.js +25 -0
  42. package/dist/cli/pairing-cli.js +1 -1
  43. package/dist/cli/program.js +58 -6
  44. package/dist/cli/run-main.js +1 -1
  45. package/dist/cli/skills-cli.js +144 -21
  46. package/dist/cli/skills-hub-cli.js +59 -29
  47. package/dist/cli/tool-connector-cli.js +99 -24
  48. package/dist/cli/upstream-sync-cli.js +253 -96
  49. package/dist/cli/usage-cli.js +14 -0
  50. package/dist/commands/auth-choice-options.js +6 -1
  51. package/dist/commands/auth-choice.js +157 -5
  52. package/dist/commands/bootstrap-preset.js +10 -6
  53. package/dist/commands/capabilities.js +33 -6
  54. package/dist/commands/claude-md.js +3 -2
  55. package/dist/commands/config-view.js +1 -1
  56. package/dist/commands/configure.js +4 -4
  57. package/dist/commands/credential.js +497 -36
  58. package/dist/commands/cursor-rules.js +39 -19
  59. package/dist/commands/doctor.js +5 -4
  60. package/dist/commands/identity.js +28 -31
  61. package/dist/commands/init.js +15 -18
  62. package/dist/commands/log.js +134 -0
  63. package/dist/commands/models/fallbacks.js +1 -1
  64. package/dist/commands/models/image-fallbacks.js +1 -1
  65. package/dist/commands/models/list.js +1 -1
  66. package/dist/commands/models/scan.js +1 -1
  67. package/dist/commands/onboard-auth.js +27 -2
  68. package/dist/commands/onboard-eve-identity.js +7 -8
  69. package/dist/commands/onboard-non-interactive.js +4 -2
  70. package/dist/commands/onboard-quickstart.js +18 -11
  71. package/dist/commands/quest-state.js +271 -0
  72. package/dist/commands/quest.js +53 -13
  73. package/dist/commands/reset.js +1 -1
  74. package/dist/commands/sessions-ingest.js +5 -4
  75. package/dist/commands/setup.js +4 -2
  76. package/dist/commands/skills-manifest.js +2 -2
  77. package/dist/commands/status.js +179 -61
  78. package/dist/commands/suggestions.js +1 -1
  79. package/dist/commands/usage-tracking.js +32 -0
  80. package/dist/commands/usage-upload.js +6 -1
  81. package/dist/config/defaults.js +1 -3
  82. package/dist/config/includes.js +5 -7
  83. package/dist/config/io.js +88 -16
  84. package/dist/config/legacy.js +4 -2
  85. package/dist/config/paths.js +16 -0
  86. package/dist/config/sessions.js +9 -5
  87. package/dist/config/zod-schema.js +4 -3
  88. package/dist/control-plane/broker/broker.js +1022 -0
  89. package/dist/control-plane/compaction.js +282 -0
  90. package/dist/control-plane/factory.js +31 -0
  91. package/dist/control-plane/index.js +10 -0
  92. package/dist/control-plane/odu/agents.js +192 -0
  93. package/dist/control-plane/odu/interaction-tools.js +208 -0
  94. package/dist/control-plane/odu/prompt-loader.js +95 -0
  95. package/dist/control-plane/odu/runtime.js +479 -0
  96. package/dist/control-plane/odu/types.js +6 -0
  97. package/dist/control-plane/odu-control-plane.js +316 -0
  98. package/dist/control-plane/single-agent.js +249 -0
  99. package/dist/control-plane/types.js +11 -0
  100. package/dist/credentials/store.js +449 -0
  101. package/dist/gateway/server-browser.js +5 -4
  102. package/dist/gateway/server-methods/cron.js +11 -1
  103. package/dist/gateway/server.js +14 -7
  104. package/dist/infra/bonjour.js +1 -1
  105. package/dist/infra/event-log.js +8 -2
  106. package/dist/infra/path-env.js +1 -2
  107. package/dist/infra/provider-usage.auth.js +5 -3
  108. package/dist/infra/provider-usage.fetch.claude.js +16 -6
  109. package/dist/infra/provider-usage.fetch.minimax.js +8 -3
  110. package/dist/infra/provider-usage.js +9 -5
  111. package/dist/infra/restart.js +2 -2
  112. package/dist/infra/usage-settings.js +78 -0
  113. package/dist/infra/usage-suggestions.js +17 -5
  114. package/dist/infra/usage-upload.js +38 -1
  115. package/dist/infra/voicewake.js +2 -2
  116. package/dist/logging/redact.js +109 -0
  117. package/dist/markdown/fences.js +58 -0
  118. package/dist/media/image-ops.js +3 -1
  119. package/dist/memory/embeddings.js +146 -0
  120. package/dist/memory/index.js +3 -0
  121. package/dist/memory/internal.js +163 -0
  122. package/dist/pairing/pairing-store.js +218 -0
  123. package/dist/plugins/cli.js +42 -0
  124. package/dist/plugins/discovery.js +253 -0
  125. package/dist/plugins/install.js +181 -0
  126. package/dist/plugins/loader.js +290 -0
  127. package/dist/plugins/registry.js +105 -0
  128. package/dist/plugins/status.js +29 -0
  129. package/dist/plugins/tools.js +39 -0
  130. package/dist/plugins/types.js +1 -0
  131. package/dist/providers/github-copilot-auth.js +1 -1
  132. package/dist/routing/resolve-route.js +144 -0
  133. package/dist/routing/session-key.js +65 -0
  134. package/dist/sessions/send-policy.js +5 -5
  135. package/dist/slack/monitor.js +22 -1
  136. package/dist/telegram/reaction-level.js +2 -1
  137. package/dist/utils/provider-utils.js +28 -0
  138. package/dist/utils.js +4 -3
  139. package/dist/wizard/onboarding.js +29 -7
  140. package/package.json +4 -29
  141. package/patches/@mariozechner__pi-ai.patch +215 -0
  142. package/patches/playwright-core@1.57.0.patch +13 -0
  143. package/patches/qrcode-terminal.patch +12 -0
  144. package/scripts/postinstall.js +202 -0
@@ -1,5 +1,10 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fsSync from "node:fs";
1
4
  import fs from "node:fs/promises";
2
- import { buildCredentialIndex, listCredentialEntries, readCredentialIndex, readCredentialRecord, resolveCredentialIndexPath, resolveCredentialPath, resolveCredentialsDir, storeKeychainSecret, writeCredentialIndex, writeCredentialRecord, } from "../credentials/store.js";
5
+ import path from "node:path";
6
+ import { buildCredentialIndex, listCredentialEntries, readCredentialIndex, readCredentialRecord, resolveDefaultEnvVar, resolveCredentialIndexPath, resolveCredentialPath, resolveCredentialsDir, resolveCredentialValue, storeKeychainSecret, writeCredentialIndex, writeCredentialRecord, } from "../credentials/store.js";
7
+ import { resolveUserPath } from "../utils.js";
3
8
  const KNOWN_ENV_SPECS = [
4
9
  { env: "ANTHROPIC_API_KEY", service: "anthropic", type: "api_key" },
5
10
  { env: "OPENAI_API_KEY", service: "openai", type: "api_key" },
@@ -28,7 +33,9 @@ function looksLikePath(value) {
28
33
  const trimmed = value.trim();
29
34
  if (!trimmed)
30
35
  return false;
31
- if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("~/"))
36
+ if (trimmed.startsWith("/") ||
37
+ trimmed.startsWith("./") ||
38
+ trimmed.startsWith("~/"))
32
39
  return true;
33
40
  if (trimmed.includes(":/") || trimmed.includes("\\"))
34
41
  return true;
@@ -38,7 +45,7 @@ function detectValuePattern(value) {
38
45
  const trimmed = value.trim();
39
46
  if (!trimmed)
40
47
  return null;
41
- if (/^sk-ant-/.test(trimmed)) {
48
+ if (trimmed.startsWith("sk-ant-")) {
42
49
  return { service: "anthropic", type: "api_key", reason: "pattern:sk-ant" };
43
50
  }
44
51
  if (/^sk-[A-Za-z0-9]/.test(trimmed)) {
@@ -135,8 +142,23 @@ export function scanCredentialEnv(opts) {
135
142
  }
136
143
  return { deep, known, discovered, missing };
137
144
  }
145
+ function mergeIndexMetadata(params) {
146
+ const merged = { ...params.base };
147
+ if (params.existing) {
148
+ merged.order = params.existing.order;
149
+ merged.lastGood = params.existing.lastGood;
150
+ merged.usageStats = params.existing.usageStats;
151
+ }
152
+ return merged;
153
+ }
138
154
  export async function listCredentials(params = {}) {
139
- const index = (await readCredentialIndex()) ?? buildCredentialIndex(await listCredentialEntries());
155
+ const entries = await listCredentialEntries();
156
+ const existing = await readCredentialIndex();
157
+ const index = mergeIndexMetadata({
158
+ base: buildCredentialIndex(entries),
159
+ existing,
160
+ });
161
+ await writeCredentialIndex(index);
140
162
  if (params.service) {
141
163
  const key = params.service.trim();
142
164
  return {
@@ -148,7 +170,11 @@ export async function listCredentials(params = {}) {
148
170
  }
149
171
  export async function scanCredentials() {
150
172
  const entries = await listCredentialEntries();
151
- const index = buildCredentialIndex(entries);
173
+ const existing = await readCredentialIndex();
174
+ const index = mergeIndexMetadata({
175
+ base: buildCredentialIndex(entries),
176
+ existing,
177
+ });
152
178
  await writeCredentialIndex(index);
153
179
  return index;
154
180
  }
@@ -160,49 +186,121 @@ export async function getCredential(params) {
160
186
  }
161
187
  return { filePath, record };
162
188
  }
189
+ export async function getCredentialValue(params) {
190
+ const result = await getCredential(params);
191
+ if (!result)
192
+ return null;
193
+ const resolved = await resolveCredentialValue(result.record);
194
+ if (!resolved)
195
+ return null;
196
+ return { ...result, value: resolved.value, field: resolved.field };
197
+ }
198
+ function buildFallbackPayload(params) {
199
+ const value = params.value?.trim() ?? "";
200
+ if (!value) {
201
+ throw new Error("Missing credential value.");
202
+ }
203
+ if (params.type === "api_key") {
204
+ return { value };
205
+ }
206
+ if (params.type === "token") {
207
+ if (!params.refreshToken && params.expiresAt === undefined) {
208
+ return { value };
209
+ }
210
+ return {
211
+ value: JSON.stringify({
212
+ token: value,
213
+ refreshToken: params.refreshToken,
214
+ expiresAt: params.expiresAt,
215
+ }),
216
+ format: "json",
217
+ };
218
+ }
219
+ return {
220
+ value: JSON.stringify({
221
+ accessToken: value,
222
+ refreshToken: params.refreshToken,
223
+ expiresAt: params.expiresAt,
224
+ }),
225
+ format: "json",
226
+ };
227
+ }
163
228
  export async function addCredential(params) {
164
229
  const authId = params.authId?.trim() || params.type;
230
+ let storage;
231
+ if (params.storage.provider === "keychain") {
232
+ storage = {
233
+ provider: "keychain",
234
+ service: params.storage.serviceName ?? `nexus.${params.service}`,
235
+ account: params.storage.accountName ?? params.account,
236
+ };
237
+ }
238
+ else if (params.storage.provider === "1password") {
239
+ storage = {
240
+ provider: "1password",
241
+ vault: params.storage.vault,
242
+ item: params.storage.item,
243
+ fields: params.storage.fields,
244
+ };
245
+ }
246
+ else if (params.storage.provider === "env") {
247
+ storage = {
248
+ provider: "env",
249
+ var: params.storage.var,
250
+ };
251
+ }
252
+ else {
253
+ const command = params.storage.command ?? params.storage.syncCommand;
254
+ if (!command || !command.trim()) {
255
+ throw new Error("External storage requires --command or --sync-command.");
256
+ }
257
+ storage = {
258
+ provider: "external",
259
+ command,
260
+ format: params.storage.format,
261
+ jsonPath: params.storage.jsonPath,
262
+ };
263
+ }
165
264
  const record = {
166
265
  owner: params.owner ?? "user",
167
266
  type: params.type,
168
267
  configuredAt: new Date().toISOString(),
169
- storage: params.storage.provider === "plaintext"
170
- ? { provider: "plaintext" }
171
- : params.storage.provider === "keychain"
172
- ? {
173
- provider: "keychain",
174
- service: params.storage.serviceName ?? `nexus.${params.service}`,
175
- account: params.storage.accountName ?? params.account,
176
- }
177
- : params.storage.provider === "1password"
178
- ? {
179
- provider: "1password",
180
- vault: params.storage.vault,
181
- item: params.storage.item,
182
- fields: params.storage.fields,
183
- }
184
- : { provider: "external", syncCommand: params.storage.syncCommand },
268
+ storage,
185
269
  ...(params.expiresAt ? { expiresAt: params.expiresAt } : {}),
186
270
  };
187
- if (params.storage.provider === "plaintext") {
188
- const value = params.storage.value?.trim();
189
- if (params.type === "api_key")
190
- record.key = value;
191
- if (params.type === "token")
192
- record.token = value;
193
- if (params.type === "oauth")
194
- record.accessToken = value;
195
- if (params.refreshToken)
196
- record.refreshToken = params.refreshToken;
197
- }
198
271
  if (params.storage.provider === "keychain") {
199
- const stored = await storeKeychainSecret({
200
- service: record.storage.provider === "keychain" ? record.storage.service : `nexus.${params.service}`,
201
- account: record.storage.provider === "keychain" ? record.storage.account : params.account,
272
+ const payload = buildFallbackPayload({
273
+ type: params.type,
202
274
  value: params.storage.value,
275
+ refreshToken: params.refreshToken,
276
+ expiresAt: params.expiresAt,
277
+ });
278
+ const stored = await storeKeychainSecret({
279
+ service: record.storage.provider === "keychain"
280
+ ? record.storage.service
281
+ : `nexus.${params.service}`,
282
+ account: record.storage.provider === "keychain"
283
+ ? record.storage.account
284
+ : params.account,
285
+ value: payload.value,
203
286
  });
204
287
  if (!stored) {
205
- throw new Error("Failed to store keychain secret.");
288
+ const envVar = resolveDefaultEnvVar({
289
+ service: params.service,
290
+ type: params.type,
291
+ });
292
+ process.env[envVar] = payload.value;
293
+ record.storage = {
294
+ provider: "env",
295
+ var: envVar,
296
+ ...(payload.format ? { format: payload.format } : {}),
297
+ };
298
+ }
299
+ else if (payload.format === "json") {
300
+ record.storage = {
301
+ ...record.storage,
302
+ format: "json",
303
+ };
206
304
  }
207
305
  }
208
306
  const filePath = resolveCredentialPath(params.service, params.account, authId);
@@ -210,6 +308,180 @@ export async function addCredential(params) {
210
308
  await scanCredentials();
211
309
  return { filePath, record };
212
310
  }
311
+ function quoteShellValue(value) {
312
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
313
+ }
314
+ function resolveClaudeCliCredentialsPath() {
315
+ return path.join(resolveUserPath("~"), ".claude", ".credentials.json");
316
+ }
317
+ function resolveCodexHomePath() {
318
+ const configured = process.env.CODEX_HOME;
319
+ const home = configured
320
+ ? resolveUserPath(configured)
321
+ : resolveUserPath("~/.codex");
322
+ try {
323
+ return fsSync.realpathSync.native(home);
324
+ }
325
+ catch {
326
+ return home;
327
+ }
328
+ }
329
+ function computeCodexKeychainAccount(codexHome) {
330
+ const hash = createHash("sha256").update(codexHome).digest("hex");
331
+ return `cli|${hash.slice(0, 16)}`;
332
+ }
333
+ function extractClaudeAccessToken(payload) {
334
+ if (!payload || typeof payload !== "object")
335
+ return null;
336
+ const claudeOauth = payload.claudeAiOauth;
337
+ if (!claudeOauth || typeof claudeOauth !== "object")
338
+ return null;
339
+ const access = claudeOauth.accessToken;
340
+ if (typeof access !== "string" || !access.trim())
341
+ return null;
342
+ return access.trim();
343
+ }
344
+ function extractCodexAccessToken(payload) {
345
+ if (!payload || typeof payload !== "object")
346
+ return null;
347
+ const tokens = payload.tokens;
348
+ if (!tokens || typeof tokens !== "object")
349
+ return null;
350
+ const access = tokens.access_token;
351
+ if (typeof access !== "string" || !access.trim())
352
+ return null;
353
+ return access.trim();
354
+ }
355
+ function safeParseJson(raw) {
356
+ try {
357
+ return JSON.parse(raw);
358
+ }
359
+ catch {
360
+ return null;
361
+ }
362
+ }
363
+ async function readJsonFile(filePath) {
364
+ try {
365
+ const raw = await fs.readFile(filePath, "utf-8");
366
+ return JSON.parse(raw);
367
+ }
368
+ catch {
369
+ return null;
370
+ }
371
+ }
372
+ async function resolveClaudeCliExternalStorage(params) {
373
+ const jsonPath = "claudeAiOauth.accessToken";
374
+ if (process.platform === "darwin" && params.allowKeychainPrompt !== false) {
375
+ try {
376
+ const raw = execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
377
+ const parsed = safeParseJson(raw);
378
+ if (extractClaudeAccessToken(parsed)) {
379
+ return {
380
+ command: `security find-generic-password -s ${quoteShellValue("Claude Code-credentials")} -w`,
381
+ format: "json",
382
+ jsonPath,
383
+ };
384
+ }
385
+ }
386
+ catch {
387
+ // ignore
388
+ }
389
+ }
390
+ const filePath = resolveClaudeCliCredentialsPath();
391
+ const parsed = await readJsonFile(filePath);
392
+ if (extractClaudeAccessToken(parsed)) {
393
+ return {
394
+ command: `cat ${quoteShellValue(filePath)}`,
395
+ format: "json",
396
+ jsonPath,
397
+ };
398
+ }
399
+ return null;
400
+ }
401
+ async function resolveCodexCliExternalStorage(params) {
402
+ const jsonPath = "tokens.access_token";
403
+ const codexHome = resolveCodexHomePath();
404
+ if (process.platform === "darwin" && params.allowKeychainPrompt !== false) {
405
+ const account = computeCodexKeychainAccount(codexHome);
406
+ try {
407
+ const raw = execFileSync("security", ["find-generic-password", "-s", "Codex Auth", "-a", account, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
408
+ const parsed = safeParseJson(raw);
409
+ if (extractCodexAccessToken(parsed)) {
410
+ return {
411
+ command: `security find-generic-password -s ${quoteShellValue("Codex Auth")} -a ${quoteShellValue(account)} -w`,
412
+ format: "json",
413
+ jsonPath,
414
+ };
415
+ }
416
+ }
417
+ catch {
418
+ // ignore
419
+ }
420
+ }
421
+ const authPath = path.join(codexHome, "auth.json");
422
+ const parsed = await readJsonFile(authPath);
423
+ if (extractCodexAccessToken(parsed)) {
424
+ return {
425
+ command: `cat ${quoteShellValue(authPath)}`,
426
+ format: "json",
427
+ jsonPath,
428
+ };
429
+ }
430
+ return null;
431
+ }
432
+ export async function importCliCredential(params) {
433
+ const source = params.source;
434
+ const owner = params.owner ?? "user";
435
+ const service = source === "claude-cli" ? "anthropic" : "openai-codex";
436
+ const accountDefault = source === "claude-cli" ? "claude-cli" : "codex-cli";
437
+ const account = params.account?.trim() || accountDefault;
438
+ if (!account) {
439
+ throw new Error("Account is required to import CLI credentials.");
440
+ }
441
+ const storage = source === "claude-cli"
442
+ ? await resolveClaudeCliExternalStorage({
443
+ allowKeychainPrompt: params.allowKeychainPrompt,
444
+ })
445
+ : await resolveCodexCliExternalStorage({
446
+ allowKeychainPrompt: params.allowKeychainPrompt,
447
+ });
448
+ if (!storage) {
449
+ throw new Error(source === "claude-cli"
450
+ ? "No Claude CLI credentials found in Keychain or ~/.claude/.credentials.json."
451
+ : `No Codex CLI credentials found in Keychain or ${path.join(resolveCodexHomePath(), "auth.json")}.`);
452
+ }
453
+ const authId = "oauth";
454
+ const filePath = resolveCredentialPath(service, account, authId);
455
+ const existing = await readCredentialRecord(filePath);
456
+ if (existing && !params.force) {
457
+ const existingSource = existing.metadata?.externalSource;
458
+ if (existing.storage.provider !== "external" || existingSource !== source) {
459
+ throw new Error(`Credential already exists at ${filePath}. Use --force to overwrite.`);
460
+ }
461
+ }
462
+ const record = {
463
+ owner,
464
+ type: "oauth",
465
+ configuredAt: new Date().toISOString(),
466
+ storage: {
467
+ provider: "external",
468
+ command: storage.command,
469
+ format: storage.format,
470
+ jsonPath: storage.jsonPath,
471
+ },
472
+ metadata: {
473
+ externalSource: source,
474
+ },
475
+ };
476
+ await writeCredentialRecord(service, account, authId, record);
477
+ await scanCredentials();
478
+ return {
479
+ filePath,
480
+ record,
481
+ profileId: `${service}:${account}`,
482
+ source,
483
+ };
484
+ }
213
485
  export async function removeCredential(params) {
214
486
  const filePath = resolveCredentialPath(params.service, params.account, params.authId);
215
487
  await fs.rm(filePath, { force: true });
@@ -234,3 +506,192 @@ export function getCredentialPaths() {
234
506
  indexPath: resolveCredentialIndexPath(),
235
507
  };
236
508
  }
509
+ async function fetchWithTimeout(input, init, timeoutMs) {
510
+ const controller = new AbortController();
511
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
512
+ try {
513
+ return await fetch(input, { ...init, signal: controller.signal });
514
+ }
515
+ finally {
516
+ clearTimeout(timeout);
517
+ }
518
+ }
519
+ async function verifyTokenAgainstApi(params) {
520
+ const service = params.service.toLowerCase();
521
+ const token = params.token;
522
+ try {
523
+ if (service === "anthropic") {
524
+ const res = await fetchWithTimeout("https://api.anthropic.com/v1/models", {
525
+ method: "GET",
526
+ headers: {
527
+ "x-api-key": token,
528
+ "anthropic-version": "2023-06-01",
529
+ },
530
+ }, 10_000);
531
+ if (!res.ok) {
532
+ return { ok: false, error: `anthropic ${res.status}`, status: res.status };
533
+ }
534
+ return { ok: true };
535
+ }
536
+ if (service === "openai" || service === "openai-codex") {
537
+ const res = await fetchWithTimeout("https://api.openai.com/v1/models", {
538
+ method: "GET",
539
+ headers: {
540
+ Authorization: `Bearer ${token}`,
541
+ },
542
+ }, 10_000);
543
+ if (!res.ok) {
544
+ return { ok: false, error: `openai ${res.status}`, status: res.status };
545
+ }
546
+ return { ok: true };
547
+ }
548
+ if (service === "gemini") {
549
+ const res = await fetchWithTimeout(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(token)}`, { method: "GET" }, 10_000);
550
+ if (!res.ok) {
551
+ return { ok: false, error: `gemini ${res.status}`, status: res.status };
552
+ }
553
+ return { ok: true };
554
+ }
555
+ if (service === "github") {
556
+ const res = await fetchWithTimeout("https://api.github.com/user", {
557
+ method: "GET",
558
+ headers: {
559
+ Authorization: `Bearer ${token}`,
560
+ "User-Agent": "nexus",
561
+ },
562
+ }, 10_000);
563
+ if (!res.ok) {
564
+ return { ok: false, error: `github ${res.status}`, status: res.status };
565
+ }
566
+ return { ok: true };
567
+ }
568
+ if (service === "slack") {
569
+ const res = await fetchWithTimeout("https://slack.com/api/auth.test", {
570
+ method: "GET",
571
+ headers: {
572
+ Authorization: `Bearer ${token}`,
573
+ },
574
+ }, 10_000);
575
+ const body = (await res.json().catch(() => ({})));
576
+ if (!res.ok || body.ok === false) {
577
+ return { ok: false, error: "slack auth failed", status: res.status };
578
+ }
579
+ return { ok: true };
580
+ }
581
+ if (service === "discord") {
582
+ const attempt = async (auth) => {
583
+ const res = await fetchWithTimeout("https://discord.com/api/v10/users/@me", {
584
+ method: "GET",
585
+ headers: { Authorization: auth },
586
+ }, 10_000);
587
+ return res;
588
+ };
589
+ const botRes = await attempt(`Bot ${token}`);
590
+ if (botRes.ok)
591
+ return { ok: true };
592
+ const userRes = await attempt(`Bearer ${token}`);
593
+ if (userRes.ok)
594
+ return { ok: true };
595
+ return {
596
+ ok: false,
597
+ error: `discord ${userRes.status}`,
598
+ status: userRes.status,
599
+ };
600
+ }
601
+ if (service === "brave-search") {
602
+ const res = await fetchWithTimeout("https://api.search.brave.com/res/v1/web/search?q=nexus&count=1", {
603
+ method: "GET",
604
+ headers: {
605
+ "X-Subscription-Token": token,
606
+ },
607
+ }, 10_000);
608
+ if (!res.ok) {
609
+ return {
610
+ ok: false,
611
+ error: `brave-search ${res.status}`,
612
+ status: res.status,
613
+ };
614
+ }
615
+ return { ok: true };
616
+ }
617
+ if (service === "google-oauth" || service === "gog" || service === "google") {
618
+ const res = await fetchWithTimeout(`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${encodeURIComponent(token)}`, { method: "GET" }, 10_000);
619
+ if (!res.ok) {
620
+ return {
621
+ ok: false,
622
+ error: `google-oauth ${res.status}`,
623
+ status: res.status,
624
+ };
625
+ }
626
+ return { ok: true };
627
+ }
628
+ }
629
+ catch (err) {
630
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
631
+ }
632
+ return { ok: true };
633
+ }
634
+ export async function verifyCredentials(params) {
635
+ const service = params.service.trim();
636
+ if (!service)
637
+ return null;
638
+ const entries = await listCredentialEntries();
639
+ const matching = entries.filter((entry) => entry.service.toLowerCase() === service.toLowerCase());
640
+ if (matching.length === 0)
641
+ return null;
642
+ const verifiedAt = new Date().toISOString();
643
+ const checked = [];
644
+ for (const entry of matching) {
645
+ const record = entry.record;
646
+ if (record.type === "config") {
647
+ checked.push({
648
+ account: entry.account,
649
+ authId: entry.authId,
650
+ status: "skipped",
651
+ });
652
+ continue;
653
+ }
654
+ const resolved = await resolveCredentialValue(record);
655
+ if (!resolved) {
656
+ record.lastError = "missing credential value";
657
+ record.lastVerified = verifiedAt;
658
+ await writeCredentialRecord(entry.service, entry.account, entry.authId, record);
659
+ checked.push({
660
+ account: entry.account,
661
+ authId: entry.authId,
662
+ status: "error",
663
+ error: "missing credential value",
664
+ });
665
+ continue;
666
+ }
667
+ const outcome = await verifyTokenAgainstApi({
668
+ service,
669
+ token: resolved.value,
670
+ });
671
+ record.lastVerified = verifiedAt;
672
+ record.lastError = outcome.ok ? null : outcome.error ?? "verification failed";
673
+ await writeCredentialRecord(entry.service, entry.account, entry.authId, record);
674
+ checked.push({
675
+ account: entry.account,
676
+ authId: entry.authId,
677
+ status: outcome.ok ? "ok" : "error",
678
+ error: outcome.ok ? undefined : record.lastError ?? undefined,
679
+ });
680
+ }
681
+ await scanCredentials();
682
+ const broken = checked
683
+ .filter((item) => item.status === "error")
684
+ .map((item) => ({
685
+ account: item.account,
686
+ auths: [item.authId],
687
+ lastError: item.error,
688
+ }));
689
+ return {
690
+ ok: broken.length === 0,
691
+ service,
692
+ accounts: matching.length,
693
+ broken,
694
+ checked,
695
+ verifiedAt,
696
+ };
697
+ }