@chibitek/chibi 0.1.157 → 0.1.158

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 (89) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
  2. package/dist/assets/{arc-QwTuMsxp.js → arc-CSHQ9Dx3.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
  6. package/dist/assets/channel-bPN03jIt.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
  33. package/dist/assets/{index-BEJ8EKQt.js → index-CG4AFPb4.js} +1 -1
  34. package/dist/assets/{index-ziyH_uer.js → index-CMcr5k3Z.js} +199 -196
  35. package/dist/assets/index-CZjRcUdn.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
  40. package/dist/assets/{linear-Ch19KVyt.js → linear-hX6nFOQr.js} +1 -1
  41. package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DZObGLN7.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/connector-proxy.js +205 -0
  61. package/dist-server/container-mcp.js +149 -95
  62. package/dist-server/drivers/agents-proxy.js +81 -81
  63. package/dist-server/index.js +4459 -4013
  64. package/dist-server/openmausbot.js +596 -570
  65. package/dist-server/pair-cli.js +596 -570
  66. package/dist-server/server/avatar-subscription.js +51 -0
  67. package/dist-server/server/cli.js +5 -4
  68. package/dist-server/server/colima-docker.js +136 -0
  69. package/dist-server/server/composio.js +1051 -0
  70. package/dist-server/server/connector-proxy.js +249 -0
  71. package/dist-server/server/container-computer.js +31 -63
  72. package/dist-server/server/container-mcp.js +5 -0
  73. package/dist-server/server/engine-failover.js +22 -12
  74. package/dist-server/server/index.js +40 -16
  75. package/dist-server/server/mcp-public-registry.js +325 -0
  76. package/dist-server/server/port-in-use.js +21 -0
  77. package/dist-server/server/session-fallback.js +76 -0
  78. package/dist-server/server/vault/routes.js +43 -0
  79. package/dist-server/server/vault/runtime.js +48 -0
  80. package/dist-server/server/vault/store.js +177 -0
  81. package/dist-server/shared/connector-availability.js +12 -0
  82. package/dist-server/shared/desktop-private-port.js +1 -0
  83. package/dist-server/shared/subscription-bots.js +3 -0
  84. package/dist-server/shared/vault.js +59 -0
  85. package/dist-server/vps-container-mcp.js +85 -83
  86. package/package.json +1 -1
  87. package/dist/assets/channel-D2y0i2ps.js +0 -1
  88. package/dist/assets/index-DGWeJcck.css +0 -1
  89. package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +0 -8
@@ -68,7 +68,7 @@ import { turnTimingFinish, turnTimingMark, turnTimingStart } from "./turn-timing
68
68
  import { ATOMIC_TEAM_PACKAGE_ID, BUILTIN_CHIBI_REFUSALS, builtinChibiPatchRefusal } from "./builtin-chibi.js";
69
69
  import { canAccessTeam, canContactTeamOf, canManagePeer, canReachPeer, canReachTeam, coordinatorSupervises, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, reachesEveryTeam, roomBoundaryHolds, teamStatusPrompt, PEER_ROSTER_MAX, resolveTeammate, roomPeerRosterSystemPrompt, rosterAbout, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
70
70
  import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
71
- import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
71
+ import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, containerRuntimeError, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
72
72
  import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmAutoStartRuntime, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, claudeUserMcpEnabled, userMcpEnabledFor, addonEnabled, skillAuthoringEnabled, sharedComputersEnabled, teamRoomsEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, onDeviceHelpersEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, accountBotGrantPolicy, } from "./config.js";
73
73
  import { sweepThreadEventLogs } from "./thread-retention.js";
74
74
  import { GodModeSwitch } from "./god-mode.js";
@@ -263,6 +263,7 @@ import { createProviderAccountRoutes } from "./routes/provider-accounts.js";
263
263
  import { accountStops, injectedModel, restScope } from "./injected-model-rest.js";
264
264
  import { accountProvider } from "../shared/provider-accounts.js";
265
265
  import { createAccountRoutes } from "./routes/accounts.js";
266
+ import { installNativeVault } from "./vault/runtime.js";
266
267
  import { OAuthAccounts, takeOAuthClients } from "./oauth-accounts.js";
267
268
  import { DesktopAccountVault, FileAccountVault, UnavailableAccountVault, takeAccountsFromEnvironment } from "./oauth-account-vault.js";
268
269
  import { accountConnectorNames } from "./claude-account-connectors.js";
@@ -304,6 +305,7 @@ import { createBoardRoutes } from "./routes/board.js";
304
305
  import { readBoardFromNexus } from "./board/nexus-board.js";
305
306
  import { LocalModelInventory, localModePlacement, machineMemory } from "./local-models-mode.js";
306
307
  import { FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
308
+ import { enterSessionFallback, onSessionFallback, ownOrFallback, sessionFallbackSpentLine, withoutSubscription } from "./session-fallback.js";
307
309
  import { TURN_ERROR_CATEGORIES } from "../shared/runtime-events.js";
308
310
  import { EngineProbes, PROBE_PROMPT, isProbeThread, probeThreadId } from "./engine-probe.js";
309
311
  // [jev-verifier] opt-in Jev answer checks after a model switch (server/jev).
@@ -1154,9 +1156,14 @@ if (!hostedModels) {
1154
1156
  // its add-on's capability, so it loads only while the add-on applies.
1155
1157
  const bundledSkills = [...loadBundledSkills(), ...loadAddonSkillLibraries()];
1156
1158
  const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
1159
+ // Electron's utility-process parent port is private to the desktop main
1160
+ // process. It carries desktop-only messages (approval and browser control,
1161
+ // phone secrets, the account vault) without putting them in the renderer.
1162
+ // Plain Node/dev launches have no parentPort.
1157
1163
  // SAFETY: Electron's utility-process runtime is the only environment that
1158
1164
  // supplies parentPort; plain Node intentionally leaves it absent.
1159
1165
  const utilityParentPort = process.parentPort;
1166
+ installNativeVault(DATA_DIR, utilityParentPort, () => desktopMutationToken, req => authorizedInternalCapability(req.headers.authorization) !== null);
1160
1167
  function postDesktopPrivateMessage(message) {
1161
1168
  if (!utilityParentPort)
1162
1169
  return false;
@@ -3045,12 +3052,19 @@ function turnEngineChoice(botId, threadId, needs, runOn) {
3045
3052
  if (hostedModels || !profile || !task)
3046
3053
  return undefined;
3047
3054
  if (isSubscriptionBot(profile))
3048
- return subscriptionTurnEngineChoice(profile, threadId);
3055
+ return ownOrFallback(threadId, () => subscriptionTurnEngineChoice(profile, threadId), () => sessionFallbackChoice(profile, threadId, needs, runOn), (stop) => stop instanceof SubscriptionStop, (stop) => new SubscriptionStop(sessionFallbackSpentLine(stop.message)));
3049
3056
  if (isPersonalBot(profile))
3050
3057
  return personalTurnEngineChoice(profile, threadId, needs, runOn);
3051
3058
  const projected = store.projectBotForTask(botId, threadId);
3052
3059
  return projected ? ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) : undefined;
3053
3060
  }
3061
+ /** A company subscription bot's conversation on another provider's subscription, same class (server/session-fallback.ts). */
3062
+ function sessionFallbackChoice(profile, threadId, needs, runOn) {
3063
+ const projected = store.projectBotForTask(profile.id, threadId);
3064
+ const choice = isPersonalBot(profile) || !projected ? undefined
3065
+ : ladderTurnEngineChoice(withoutSubscription(profile), withoutSubscription(projected), threadId, needs, runOn, true);
3066
+ return choice && choice.selection.instanceId !== projected.modelSelection.instanceId ? { selection: choice.selection, persist: false } : undefined;
3067
+ }
3054
3068
  /** A room member's engine at turn start, by turnEngineChoice's rules for
3055
3069
  * the bot's own default (a room turn has no task of its own, so the bot's
3056
3070
  * engine mode decides): resting and signed-out engines passed over, a
@@ -3066,7 +3080,7 @@ function roomTurnEngineChoice(bot, threadId) {
3066
3080
  : ladderTurnEngineChoice(bot, projected, threadId);
3067
3081
  return choice && { selection: choice.selection, persist: false };
3068
3082
  }
3069
- function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
3083
+ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn, subscriptionsOnly = false) {
3070
3084
  const candidates = engineSnapshots.current();
3071
3085
  if (!candidates)
3072
3086
  return undefined;
@@ -3075,7 +3089,7 @@ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
3075
3089
  const { ladder } = plan;
3076
3090
  const computer = turnComputer(projected, threadId, runOn);
3077
3091
  const context = { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3078
- keyBandPaused: payPerUse.pausedUntil() !== undefined };
3092
+ keyBandPaused: subscriptionsOnly || payPerUse.pausedUntil() !== undefined };
3079
3093
  const local = (resolution) => localModeTurn(resolution, { tier, context, needs, prefer: [projected.modelSelection.instanceId] });
3080
3094
  const wider = (tried, modelClass) => resolveFailover({
3081
3095
  ladder, classLadder: (cls) => classLadder(classedModels(), cls), failed: projected.modelSelection, context, tried,
@@ -3812,14 +3826,19 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3812
3826
  const computer = projected ? turnComputer(projected, turn.threadId, attempt.runOn) : undefined;
3813
3827
  // A subscription bot fails over only to another account of its own
3814
3828
  // provider on its side of the personal line; with none left it stops with
3815
- // its one line (and the client offers Chibi). Never another provider.
3829
+ // its one line, or a company bot's own conversation takes its session fallback (server/session-fallback.ts).
3816
3830
  const subscriptionMove = isSubscriptionBot(profile)
3817
3831
  ? tried.size >= MAX_ENGINES_PER_TURN || !projected
3818
3832
  ? { line: subscriptionLine(profile, candidates) }
3819
3833
  : subscriptionPlacement({ profile, projected, candidates, tried: new Set([...tried, turn.instanceId]), auto: attempt.persist, modelClass: neededClass })
3820
3834
  : undefined;
3821
- if (subscriptionMove && !subscriptionMove.choice)
3835
+ const fallback = subscriptionMove && !subscriptionMove.choice && projected && !isPersonalBot(profile) && tried.size < MAX_ENGINES_PER_TURN && store.taskByThread(profile.id, turn.threadId);
3836
+ if (fallback && !enterSessionFallback(turn.threadId, false))
3837
+ return { line: sessionFallbackSpentLine(subscriptionMove.line) };
3838
+ if (subscriptionMove && !subscriptionMove.choice && !fallback)
3822
3839
  return { line: subscriptionMove.line };
3840
+ if (fallback)
3841
+ [profile, projected] = [withoutSubscription(profile), withoutSubscription(projected)];
3823
3842
  const personalTurn = isPersonalBot(profile) && !subscriptionMove;
3824
3843
  // The Private Bot prefers personal engines and opted-in local compute,
3825
3844
  // then company compute. startTurn rechecks company cost on every attempt.
@@ -3839,7 +3858,7 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3839
3858
  classLadder: (modelClass) => classLadder(classedModels(), modelClass),
3840
3859
  failed: attempt.selection,
3841
3860
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3842
- keyBandPaused: keyBandPausedFor(turn.threadId) },
3861
+ keyBandPaused: Boolean(fallback) || keyBandPausedFor(turn.threadId) },
3843
3862
  tried,
3844
3863
  modelClass: neededClass,
3845
3864
  mayDrop: true,
@@ -3855,6 +3874,8 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3855
3874
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting } })
3856
3875
  : { ...resolution, local: false };
3857
3876
  const choice = placed.choice && placed.local ? { ...placed.choice, modelClass: "Añejo" } : placed.choice;
3877
+ if (!choice && fallback)
3878
+ return { line: subscriptionMove.line };
3858
3879
  if (!choice && turn.failure?.kind === "provider_error")
3859
3880
  return { passthrough: true };
3860
3881
  if (!choice) {
@@ -3864,11 +3885,11 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3864
3885
  const resting = resolution.skipped.filter((skip) => skip.reason === "resting" || skip.reason === "tried").map((skip) => skip.instanceId);
3865
3886
  const named = restingNamed([turn.instanceId, ...attempt.tried, ...resting]);
3866
3887
  return {
3867
- line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer)),
3888
+ line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer), { capped: tried.size >= MAX_ENGINES_PER_TURN }),
3868
3889
  category: named.some((engine) => engine.needsSignIn || engine.keyExpired) ? "account" : "failover_exhausted",
3869
3890
  };
3870
3891
  }
3871
- if (subscriptionMove) { /* subscriptions only: no pay-per-use episode */ }
3892
+ if (subscriptionMove || fallback) { /* subscriptions only: no pay-per-use episode */ }
3872
3893
  else if (personalMove)
3873
3894
  notePersonalPayPerUse(personalMove);
3874
3895
  else
@@ -6973,15 +6994,15 @@ function localVmSetUp() {
6973
6994
  }
6974
6995
  /** The one gate for starting the container engine (App Settings →
6975
6996
  * Computers): the person's switch, the organisation's policy, and a Local VM
6976
- * actually set up here. Returns what it started ("Colima", "Docker
6977
- * Desktop") once the engine answers, or null. */
6997
+ * actually set up here. Returns what it started (Colima, the only engine
6998
+ * Atomic starts; never Docker Desktop) once it answers, or null. */
6978
6999
  async function startLocalVmRuntime() {
6979
7000
  if (!localVmAutoStartRuntime(cfg) || !managedPolicy.computerAllowed("localVm") || !localVmSetUp())
6980
7001
  return null;
6981
7002
  try {
6982
- const { status, started } = await ensureContainerRuntime();
6983
- if (started)
6984
- console.log(`[local-vm] started ${started}: ${status.daemonUp ? "ready" : "still not answering"}`);
7003
+ const { status, started, error } = await ensureContainerRuntime();
7004
+ if (started || error)
7005
+ console[error ? "error" : "log"](`[local-vm] ${started ? `started ${started}: ${status.daemonUp ? "ready" : "still not answering"}` : "not started"}${error ? `. ${error}` : ""}`);
6985
7006
  return started && status.daemonUp ? started : null;
6986
7007
  }
6987
7008
  catch (error) {
@@ -7493,7 +7514,7 @@ async function unusableAutoPin(bot, threadId, plan, runOn) {
7493
7514
  if (pinned === "vm" && !choice.available && !choice.starting && await startLocalVmRuntime()) {
7494
7515
  choice = await computerChoice(bot, pinned);
7495
7516
  }
7496
- return choice.available || choice.starting ? null : choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
7517
+ return choice.available || choice.starting ? null : (pinned === "vm" ? containerRuntimeError() : null) ?? choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
7497
7518
  }
7498
7519
  function continueComputerSelection(threadId, generation, succeeded) {
7499
7520
  const selection = computerSelectionTurns.get(threadId);
@@ -9632,7 +9653,8 @@ async function startTurn(botId, text, opts) {
9632
9653
  : threadBusy(botId, threadId) ? undefined : turnEngineChoice(botId, threadId, { tools: true, vision: turnHasImages(opts?.userMessage) }, opts?.runOn);
9633
9654
  const tierSelection = engineChoice?.persist ? engineChoice.selection : undefined;
9634
9655
  const projectedBot = store.projectBotForTask(botId, threadId);
9635
- const bot = projectedBot && engineChoice ? { ...projectedBot, modelSelection: engineChoice.selection } : projectedBot;
9656
+ const fallbackBot = projectedBot && !opts?.engine && onSessionFallback(threadId) ? withoutSubscription(projectedBot) : projectedBot;
9657
+ const bot = fallbackBot && engineChoice ? { ...fallbackBot, modelSelection: engineChoice.selection } : fallbackBot;
9636
9658
  if (!bot)
9637
9659
  throw Object.assign(new Error("no such task"), { status: 404 });
9638
9660
  // Routines and legacy peer delivery already have their own completion
@@ -14311,6 +14333,8 @@ async function readyLocalVmForTurn(botId, target, isCurrent = () => true, onNote
14311
14333
  onNote?.(`Started ${started} for the Local VM`);
14312
14334
  status = await containerComputerStatus(undefined, undefined, target);
14313
14335
  }
14336
+ else if (containerRuntimeError() && isCurrent())
14337
+ onNote?.(containerRuntimeError());
14314
14338
  }
14315
14339
  noteLocalVmSeen(target, status);
14316
14340
  if (!isCurrent())
@@ -0,0 +1,325 @@
1
+ // The Plugins window's MCP Marketplace, backed by the official MCP Registry
2
+ // (https://registry.modelcontextprotocol.io, public, no key). This module
3
+ // fetches and normalizes registry pages and turns one entry, or a featured
4
+ // service from shared/mcp-featured.ts, into a config.json `mcpServers` entry.
5
+ // Adding goes through the same rules as every other MCP server: the entry is
6
+ // saved switched off, secrets are kept in its env or headers like any other
7
+ // server's, and nothing here logs a value.
8
+ import { featuredMcpIcon, featuredMcpServer, MCP_FEATURED } from "../shared/mcp-featured.js";
9
+ import { slugMcpName } from "./mcp-registry.js";
10
+ export const MCP_REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
11
+ /** The OAuth bridge Atomic already runs remote servers through (see
12
+ * electron/chibitek-defaults.mjs MCP_REMOTE_PACKAGE; a test keeps them equal).
13
+ * Pinned so its saved sign-ins survive a new mcp-remote release. */
14
+ export const MCP_REMOTE_PACKAGE = "mcp-remote@0.14.3";
15
+ const CACHE_MS = 60 * 60 * 1000;
16
+ const CACHE_MAX = 200;
17
+ const TIMEOUT_MS = 8_000;
18
+ const PAGE_SIZE = 30;
19
+ const OFFICIAL = "io.modelcontextprotocol.registry/official";
20
+ export const REGISTRY_OFFLINE_MESSAGE = "The MCP Registry could not be reached. Check your internet connection and try again.";
21
+ export class RegistryUnavailableError extends Error {
22
+ constructor() {
23
+ super(REGISTRY_OFFLINE_MESSAGE);
24
+ this.name = "RegistryUnavailableError";
25
+ }
26
+ }
27
+ function record(value) {
28
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
29
+ }
30
+ function text(value) {
31
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
32
+ }
33
+ function httpsUrl(value) {
34
+ const raw = text(value);
35
+ if (!raw)
36
+ return undefined;
37
+ try {
38
+ const url = new URL(raw);
39
+ return url.protocol === "https:" ? url.toString() : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ const featuredByRegistryName = new Map(MCP_FEATURED.filter((server) => server.registryName).map((server) => [server.registryName, server]));
46
+ /** "io.github.acme/thing" -> "acme", "com.notion/mcp" -> "notion.com". */
47
+ export function publisherFor(name) {
48
+ const namespace = name.split("/")[0] ?? name;
49
+ if (namespace.startsWith("io.github."))
50
+ return namespace.slice("io.github.".length);
51
+ return namespace.split(".").reverse().join(".");
52
+ }
53
+ /** "notion-mcp-server" -> "Notion Mcp Server" when the entry has no title. */
54
+ function titleFor(name) {
55
+ const leaf = name.split("/").pop() ?? name;
56
+ return leaf.split(/[-_.]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ") || name;
57
+ }
58
+ function githubOwner(repositoryUrl) {
59
+ if (!repositoryUrl)
60
+ return undefined;
61
+ const match = /^https:\/\/github\.com\/([A-Za-z0-9-]{1,39})(?:\/|$)/.exec(repositoryUrl);
62
+ return match?.[1];
63
+ }
64
+ function input(kind, raw) {
65
+ const item = record(raw);
66
+ const name = text(item.name);
67
+ if (!name)
68
+ return null;
69
+ const description = text(item.description);
70
+ return {
71
+ kind,
72
+ name,
73
+ ...(description ? { description: description.slice(0, 300) } : {}),
74
+ required: item.isRequired === true,
75
+ secret: item.isSecret === true,
76
+ ...(kind === "header" && text(item.value) ? { template: text(item.value) } : {}),
77
+ };
78
+ }
79
+ function remotes(raw) {
80
+ if (!Array.isArray(raw))
81
+ return [];
82
+ return raw.flatMap((entry) => {
83
+ const item = record(entry);
84
+ const url = httpsUrl(item.url);
85
+ // A URL with {placeholders} needs per-person values Atomic cannot guess.
86
+ if (!url || /[{}]|%7B|%7D/i.test(url))
87
+ return [];
88
+ const type = item.type === "sse" ? "sse" : item.type === "streamable-http" ? "http" : null;
89
+ if (!type)
90
+ return [];
91
+ const headers = Array.isArray(item.headers) ? item.headers.map((header) => input("header", header)).filter((header) => header !== null) : [];
92
+ return [{ type, url, headers }];
93
+ });
94
+ }
95
+ function packages(raw) {
96
+ if (!Array.isArray(raw))
97
+ return [];
98
+ return raw.flatMap((entry) => {
99
+ const item = record(entry);
100
+ const registryType = text(item.registryType);
101
+ const identifier = text(item.identifier);
102
+ if (!registryType || !identifier)
103
+ return [];
104
+ const env = Array.isArray(item.environmentVariables)
105
+ ? item.environmentVariables.map((variable) => input("env", variable)).filter((variable) => variable !== null)
106
+ : [];
107
+ return [{
108
+ registryType,
109
+ identifier,
110
+ ...(text(item.version) ? { version: text(item.version) } : {}),
111
+ ...(text(record(item.transport).type) ? { transport: text(record(item.transport).type) } : {}),
112
+ ...(text(item.runtimeHint) ? { runtimeHint: text(item.runtimeHint) } : {}),
113
+ env,
114
+ }];
115
+ });
116
+ }
117
+ const SAFE_PACKAGE = /^(@[a-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*$/;
118
+ const SAFE_VERSION = /^[\w.+-]{1,64}$/;
119
+ /** Remote first (nothing to install), then npm, then PyPI. A remote whose
120
+ * headers are all optional is assumed to sign in with OAuth, so it runs
121
+ * through the mcp-remote bridge; one that requires a header (an API key) is
122
+ * kept as a plain URL entry with that header. */
123
+ function installPlan(server) {
124
+ const remote = server.remotes.find((item) => item.type === "http") ?? server.remotes.find((item) => item.type === "sse");
125
+ if (remote)
126
+ return { kind: "remote", remote, oauth: !remote.headers.some((header) => header.required) };
127
+ const stdio = (item) => !item.transport || item.transport === "stdio";
128
+ const npm = server.packages.find((item) => item.registryType === "npm" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
129
+ if (npm)
130
+ return { kind: "npm", pkg: npm };
131
+ const pypi = server.packages.find((item) => item.registryType === "pypi" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
132
+ if (pypi)
133
+ return { kind: "pypi", pkg: pypi };
134
+ return null;
135
+ }
136
+ function planInputs(plan) {
137
+ if (!plan)
138
+ return [];
139
+ const list = plan.kind === "remote"
140
+ ? plan.oauth ? [] : plan.remote.headers.map(({ template: _template, ...header }) => header)
141
+ : plan.pkg.env;
142
+ return [...list].sort((a, b) => Number(b.required) - Number(a.required));
143
+ }
144
+ /** One registry list entry to the shape the renderer shows, or null when it
145
+ * is not the latest version or not usable at all. */
146
+ export function normalizeRegistryEntry(raw) {
147
+ const entry = record(raw);
148
+ const server = record(entry.server);
149
+ const meta = record(record(entry._meta)[OFFICIAL]);
150
+ if (meta.isLatest === false)
151
+ return null;
152
+ if (typeof meta.status === "string" && meta.status !== "active")
153
+ return null;
154
+ const name = text(server.name);
155
+ if (!name)
156
+ return null;
157
+ const repositoryUrl = httpsUrl(record(server.repository).url);
158
+ const websiteUrl = httpsUrl(server.websiteUrl);
159
+ const featured = featuredByRegistryName.get(name);
160
+ const icons = Array.isArray(server.icons) ? server.icons : [];
161
+ const registryIcon = icons.map((icon) => httpsUrl(record(icon).src)).find(Boolean);
162
+ const owner = githubOwner(repositoryUrl);
163
+ const iconUrl = featured
164
+ ? featuredMcpIcon(featured.id)
165
+ : registryIcon ?? (owner ? `https://github.com/${owner}.png?size=64` : undefined);
166
+ const normalized = {
167
+ id: name,
168
+ name,
169
+ title: text(server.title) ?? featured?.name ?? titleFor(name),
170
+ description: (text(server.description) ?? "").slice(0, 400),
171
+ publisher: publisherFor(name),
172
+ version: text(server.version) ?? "",
173
+ ...(repositoryUrl ? { repositoryUrl } : {}),
174
+ ...(websiteUrl ? { websiteUrl } : {}),
175
+ remotes: remotes(server.remotes),
176
+ packages: packages(server.packages),
177
+ ...(iconUrl ? { iconUrl } : {}),
178
+ ...(featured ? { featured: featured.id } : {}),
179
+ };
180
+ const plan = installPlan(normalized);
181
+ return { ...normalized, install: plan?.kind ?? null, inputs: planInputs(plan) };
182
+ }
183
+ /** Normalize a page: latest versions only, one card per server name. */
184
+ export function normalizeRegistryPage(body) {
185
+ const page = record(body);
186
+ const seen = new Set();
187
+ const servers = [];
188
+ for (const raw of Array.isArray(page.servers) ? page.servers : []) {
189
+ const server = normalizeRegistryEntry(raw);
190
+ if (!server || seen.has(server.id))
191
+ continue;
192
+ seen.add(server.id);
193
+ servers.push(server);
194
+ }
195
+ const nextCursor = text(record(page.metadata).nextCursor);
196
+ return { servers, ...(nextCursor ? { nextCursor } : {}) };
197
+ }
198
+ /** A registry client with a one-hour in-memory cache. Every failure (offline,
199
+ * timeout, a registry error) becomes RegistryUnavailableError. */
200
+ export function createMcpPublicRegistry(options = {}) {
201
+ const doFetch = options.fetch ?? fetch;
202
+ const base = options.base ?? MCP_REGISTRY_BASE;
203
+ const now = options.now ?? Date.now;
204
+ const cache = new Map();
205
+ async function getJson(url) {
206
+ const hit = cache.get(url);
207
+ if (hit && now() - hit.at < CACHE_MS)
208
+ return hit.value;
209
+ let response;
210
+ try {
211
+ response = await doFetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS) });
212
+ }
213
+ catch {
214
+ throw new RegistryUnavailableError();
215
+ }
216
+ if (response.status === 404)
217
+ return null;
218
+ if (!response.ok)
219
+ throw new RegistryUnavailableError();
220
+ let value;
221
+ try {
222
+ value = await response.json();
223
+ }
224
+ catch {
225
+ throw new RegistryUnavailableError();
226
+ }
227
+ if (cache.size >= CACHE_MAX)
228
+ cache.delete(cache.keys().next().value);
229
+ cache.set(url, { at: now(), value });
230
+ return value;
231
+ }
232
+ return {
233
+ async search({ search, cursor }) {
234
+ const params = new URLSearchParams({ limit: String(PAGE_SIZE), version: "latest" });
235
+ const query = search?.trim().slice(0, 100);
236
+ if (query)
237
+ params.set("search", query);
238
+ if (cursor)
239
+ params.set("cursor", cursor.slice(0, 300));
240
+ return normalizeRegistryPage(await getJson(`${base}/v0/servers?${params}`));
241
+ },
242
+ async get(id) {
243
+ const body = await getJson(`${base}/v0/servers/${encodeURIComponent(id)}/versions/latest`);
244
+ return body === null ? null : normalizeRegistryEntry(body);
245
+ },
246
+ };
247
+ }
248
+ /** A free config name from the server's own name: "notion", then "notion-2". */
249
+ export function freeMcpName(preferred, taken) {
250
+ const base = slugMcpName(preferred) || "mcp-server";
251
+ if (!taken(base))
252
+ return base;
253
+ for (let n = 2; n < 100; n += 1) {
254
+ const candidate = `${base.slice(0, 32 - String(n).length - 1)}-${n}`;
255
+ if (!taken(candidate))
256
+ return candidate;
257
+ }
258
+ return `${base.slice(0, 23)}-${Date.now().toString(36).slice(-8)}`;
259
+ }
260
+ function bridgeEntry(url) {
261
+ return { command: "npx", args: ["-y", MCP_REMOTE_PACKAGE, url], env: {} };
262
+ }
263
+ function cleanValues(values) {
264
+ const out = {};
265
+ for (const [key, value] of Object.entries(record(values))) {
266
+ if (typeof value === "string" && value.trim())
267
+ out[key] = value.trim();
268
+ }
269
+ return out;
270
+ }
271
+ /** The config entry a featured service becomes: its official URL through the
272
+ * OAuth bridge. */
273
+ export function planFeaturedAdd(featured, taken) {
274
+ return { ok: true, name: freeMcpName(featured.id, taken), entry: bridgeEntry(featured.url), kind: "remote", oauth: true };
275
+ }
276
+ /** The config entry a registry server becomes, with the person's values for
277
+ * its inputs. Required inputs that are missing are refused with the list, so
278
+ * the renderer can ask for them. */
279
+ export function planRegistryAdd(server, rawValues, taken) {
280
+ const values = cleanValues(rawValues);
281
+ const plan = installPlan(server);
282
+ if (!plan) {
283
+ const docker = server.packages.some((item) => item.registryType === "oci" || item.registryType === "docker");
284
+ return {
285
+ ok: false,
286
+ status: 400,
287
+ code: "unsupported",
288
+ error: docker
289
+ ? `${server.title} only ships as a Docker image, which Atomic can't add for you yet. Add it by hand under MCP servers.`
290
+ : `${server.title} doesn't offer a remote address or an npm or PyPI package, so Atomic can't add it for you. Add it by hand under MCP servers.`,
291
+ };
292
+ }
293
+ const inputs = planInputs(plan);
294
+ const missing = inputs.filter((item) => item.required && !values[item.name]);
295
+ if (missing.length) {
296
+ return { ok: false, status: 400, code: "needs_input", inputs, error: `${server.title} needs ${missing.map((item) => item.name).join(", ")} before it can be added.` };
297
+ }
298
+ const name = freeMcpName(server.featured ?? server.name.split("/").pop() ?? server.title, taken);
299
+ if (plan.kind === "remote") {
300
+ if (plan.oauth)
301
+ return { ok: true, name, entry: bridgeEntry(plan.remote.url), kind: "remote", oauth: true };
302
+ const headers = {};
303
+ for (const header of plan.remote.headers) {
304
+ const value = values[header.name];
305
+ if (!value)
306
+ continue;
307
+ // "Bearer {api_key}" -> "Bearer <value>" unless the value already is the whole header.
308
+ headers[header.name] = header.template && /\{[^}]+\}/.test(header.template) && !/\s/.test(value)
309
+ ? header.template.replace(/\{[^}]+\}/, value)
310
+ : value;
311
+ }
312
+ return { ok: true, name, entry: { type: plan.remote.type, url: plan.remote.url, headers }, kind: "remote", oauth: false };
313
+ }
314
+ const env = {};
315
+ for (const variable of plan.pkg.env) {
316
+ if (values[variable.name])
317
+ env[variable.name] = values[variable.name];
318
+ }
319
+ const version = plan.pkg.version && SAFE_VERSION.test(plan.pkg.version) ? plan.pkg.version : undefined;
320
+ if (plan.kind === "npm") {
321
+ return { ok: true, name, entry: { command: "npx", args: ["-y", version ? `${plan.pkg.identifier}@${version}` : plan.pkg.identifier], env }, kind: "npm", oauth: false };
322
+ }
323
+ return { ok: true, name, entry: { command: "uvx", args: [plan.pkg.identifier], env }, kind: "pypi", oauth: false };
324
+ }
325
+ export { featuredMcpServer };
@@ -0,0 +1,21 @@
1
+ import { connect } from "node:net";
2
+ /** True when anything accepts TCP on 127.0.0.1:port, not just an Atomic server. */
3
+ export function portInUse(port) {
4
+ return new Promise((done) => {
5
+ const socket = connect({ host: "127.0.0.1", port });
6
+ socket.setTimeout(1000);
7
+ socket.once("connect", () => { socket.destroy(); done(true); });
8
+ socket.once("timeout", () => { socket.destroy(); done(false); });
9
+ socket.once("error", () => done(false));
10
+ });
11
+ }
12
+ /** The server binds port and port + 1 (webhooks); report the first busy one. */
13
+ export async function reportBusyPort(port, dataDirFlag) {
14
+ for (const candidate of [port, Number(process.env.OMB_WEBHOOK_PORT || port + 1)]) {
15
+ if (!(await portInUse(candidate)))
16
+ continue;
17
+ console.error(`Port ${candidate} is already in use by another app. Choose a free port, for example: chibi start --port ${port + 20}${dataDirFlag}`);
18
+ return true;
19
+ }
20
+ return false;
21
+ }
@@ -0,0 +1,76 @@
1
+ // Session fallback for company subscription bots (docs/chibitek/engine-tiers.md,
2
+ // "Subscription bots: the one failover exception"). When no account of the
3
+ // bot's own provider can take a turn, one conversation may run on the same
4
+ // class from another provider's subscription for up to SESSION_FALLBACK_TURNS
5
+ // turns. The bot's own default never changes; the next turn tries its own
6
+ // provider first again.
7
+ import { SESSION_FALLBACK_TURNS } from "../shared/subscription-bots.js";
8
+ export { SESSION_FALLBACK_TURNS };
9
+ // ponytail: in memory, so a restart gives a conversation its 50 turns again;
10
+ // persist on the task record if that matters.
11
+ const fallbacks = new Map();
12
+ /** The thread's own provider took this turn: it leaves the fallback (its count stays). */
13
+ export function leaveSessionFallback(threadId) {
14
+ const entry = fallbacks.get(threadId);
15
+ if (entry)
16
+ entry.active = false;
17
+ }
18
+ /** Enter (or stay on) the fallback for this thread. A new turn counts one;
19
+ * a failover within a turn already on it counts nothing. False once the
20
+ * conversation has used its turns. */
21
+ export function enterSessionFallback(threadId, newTurn) {
22
+ const entry = fallbacks.get(threadId) ?? { turns: 0, active: false };
23
+ if (entry.active && !newTurn)
24
+ return true;
25
+ if (entry.turns >= SESSION_FALLBACK_TURNS) {
26
+ entry.active = false;
27
+ fallbacks.set(threadId, entry);
28
+ return false;
29
+ }
30
+ entry.turns += 1;
31
+ entry.active = true;
32
+ fallbacks.set(threadId, entry);
33
+ return true;
34
+ }
35
+ /** Turn start for a subscription bot: its own provider's engine (which
36
+ * leaves the fallback), else, on its stop, the fallback's, counted as a
37
+ * turn. No fallback keeps the stop; past the limit, `spent` of it. */
38
+ export function ownOrFallback(threadId, own, fallback, isStop, spent) {
39
+ try {
40
+ const choice = own();
41
+ leaveSessionFallback(threadId);
42
+ return choice;
43
+ }
44
+ catch (stop) {
45
+ if (!isStop(stop))
46
+ throw stop;
47
+ let choice;
48
+ try {
49
+ choice = fallback();
50
+ }
51
+ catch {
52
+ choice = undefined;
53
+ }
54
+ if (choice === undefined)
55
+ throw stop;
56
+ if (!enterSessionFallback(threadId, true))
57
+ throw spent(stop);
58
+ return choice;
59
+ }
60
+ }
61
+ export function onSessionFallback(threadId) {
62
+ return fallbacks.get(threadId)?.active === true;
63
+ }
64
+ export function sessionFallbackTurns(threadId) {
65
+ return fallbacks.get(threadId)?.turns ?? 0;
66
+ }
67
+ /** The bot as a fallback turn runs it: an ordinary company bot, so the
68
+ * ladder and every gate read it without the provider lock. */
69
+ export function withoutSubscription(bot) {
70
+ const { subscription: _subscription, ...rest } = bot;
71
+ return rest;
72
+ }
73
+ /** The line a company subscription bot stops with once its fallback turns are used. */
74
+ export function sessionFallbackSpentLine(line) {
75
+ return `${line} This conversation already used its ${SESSION_FALLBACK_TURNS} turns on another provider; download a handoff or hand it to another bot.`;
76
+ }
@@ -0,0 +1,43 @@
1
+ import { PASS } from "../routes/table.js";
2
+ import { VaultError } from "./store.js";
3
+ /** The owner is supplied by the authenticated host, never from request JSON.
4
+ * Authorization runs on reads too, and is checked again after reading a body. */
5
+ export function createVaultRoutes(deps) {
6
+ return async ({ req, res, path, method, auth, json, readBody }) => {
7
+ if (path !== "/api/vault" && !path.startsWith("/api/vault/"))
8
+ return PASS;
9
+ res.setHeader("cache-control", "no-store");
10
+ const ownerId = deps.owner(req, auth);
11
+ if (!ownerId)
12
+ return json(res, 403, { error: "Only the Vault owner can manage saved items." });
13
+ const store = deps.store();
14
+ if (!store)
15
+ return json(res, 503, { error: "Vault encryption is unavailable. Unlock the secure store and try again." });
16
+ const item = /^\/api\/vault\/([0-9a-f-]{36})$/.exec(path);
17
+ if (path !== "/api/vault" && !item)
18
+ return json(res, 404, { error: "Vault item is unavailable." });
19
+ if (!(path === "/api/vault" ? ["GET", "POST"] : ["PUT", "DELETE"]).includes(method))
20
+ return json(res, 405, { error: "Method not allowed." });
21
+ try {
22
+ if (method === "GET")
23
+ return json(res, 200, { items: store.list(ownerId) });
24
+ const body = method === "DELETE" ? undefined : await readBody(req).catch(() => null);
25
+ if (deps.owner(req, auth) !== ownerId || deps.store() !== store)
26
+ return json(res, 403, { error: "Vault access changed. Try again." });
27
+ if (method === "POST")
28
+ return json(res, 201, { item: store.create(ownerId, body) });
29
+ if (method === "PUT" && item)
30
+ return json(res, 200, { item: store.update(ownerId, item[1], body) });
31
+ if (method === "DELETE" && item) {
32
+ store.remove(ownerId, item[1]);
33
+ return json(res, 200, { removed: true });
34
+ }
35
+ }
36
+ catch (error) {
37
+ // No raw parser, filesystem, crypto or browser error crosses this route.
38
+ if (error instanceof VaultError)
39
+ return json(res, 400, { error: error.message });
40
+ return json(res, 503, { error: "Vault could not complete the request." });
41
+ }
42
+ };
43
+ }