@desplega.ai/agent-swarm 1.165.0 → 1.166.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
  2. package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
  4. package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
  5. package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
  6. package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
  7. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
  8. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  9. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
  10. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
  11. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
  12. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  13. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
  14. package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
  15. package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
  16. package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
  17. package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
  18. package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
  19. package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
  20. package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
  21. package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
  22. package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
  23. package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
  24. package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
  25. package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
  26. package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
  27. package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
  28. package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
  29. package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
  30. package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
  31. package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
  32. package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
  33. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  34. package/dist/cli-j2jbr8af.js +516 -0
  35. package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
  36. package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
  37. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  38. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  39. package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
  40. package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
  41. package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
  42. package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
  43. package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
  44. package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
  45. package/dist/cli-wt7ccy4r.js +2654 -0
  46. package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
  47. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  48. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  49. package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
  50. package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
  51. package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
  52. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  53. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  54. package/dist/cli.js +24 -20
  55. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
  56. package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
  57. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
  58. package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
  59. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
  60. package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
  61. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  62. package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
  63. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
  64. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  65. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  66. package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
  67. package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
  68. package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
  69. package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
  70. package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
  71. package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
  72. package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
  73. package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
  74. package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
  75. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  76. package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
  77. package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
  78. package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
  79. package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
  80. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  81. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
  82. package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
  83. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
  84. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
  85. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
  86. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
  87. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
  88. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
  89. package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
  90. package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
  91. package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
  92. package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
  93. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  94. package/openapi.json +5 -2
  95. package/package.json +9 -5
  96. package/src/be/automation-preflight.ts +9 -4
  97. package/src/be/boot-scrub-sweep.ts +302 -0
  98. package/src/be/crypto/key-bootstrap.ts +1 -1
  99. package/src/be/db/approvals.ts +15 -1
  100. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  101. package/src/be/db/config-secrets.ts +31 -0
  102. package/src/be/db/runtime.ts +4 -0
  103. package/src/be/db/script-runs.ts +15 -0
  104. package/src/be/db/tasks/write.ts +1 -1
  105. package/src/be/db-queries/mcp-oauth.ts +5 -0
  106. package/src/be/db-queries/oauth.ts +77 -0
  107. package/src/be/db.ts +162 -59
  108. package/src/be/events.ts +17 -1
  109. package/src/be/memory/index-content.ts +2 -2
  110. package/src/be/memory/providers/sqlite-store.ts +63 -11
  111. package/src/be/memory/types.ts +14 -0
  112. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  113. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  114. package/src/be/scripts/db.ts +28 -4
  115. package/src/be/scripts/source-secrets.ts +37 -0
  116. package/src/be/scrub-json.ts +32 -0
  117. package/src/be/sealed-json.ts +59 -0
  118. package/src/be/secret-registry.ts +131 -0
  119. package/src/be/workflow-replay.ts +77 -0
  120. package/src/http/codex-oauth-device.ts +8 -10
  121. package/src/http/db-query-bounded.ts +3 -0
  122. package/src/http/db-query-shared.ts +107 -15
  123. package/src/http/db-query.ts +10 -2
  124. package/src/http/index.ts +23 -6
  125. package/src/http/script-runs.ts +9 -1
  126. package/src/http/scripts.ts +15 -0
  127. package/src/http/workflows.ts +3 -2
  128. package/src/oauth/mcp-wrapper.ts +1 -1
  129. package/src/providers/acp-adapter.ts +25 -11
  130. package/src/providers/acp-target-catalog.ts +35 -1
  131. package/src/providers/acp-targets.ts +69 -3
  132. package/src/providers/amp-adapter.ts +1 -1
  133. package/src/providers/cursor-adapter.ts +1 -1
  134. package/src/script-workflows/supervisor.ts +11 -2
  135. package/src/scripts-runtime/eval-harness.ts +2 -12
  136. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  137. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  138. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  139. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  140. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  141. package/src/scripts-runtime/executors/registry.ts +2 -0
  142. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  143. package/src/scripts-runtime/script-error-hints.ts +14 -0
  144. package/src/server.ts +3 -1
  145. package/src/tests/acp-adapter.test.ts +127 -2
  146. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  147. package/src/tests/check-text-columns.test.ts +145 -0
  148. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  149. package/src/tests/codex-oauth-device.test.ts +23 -37
  150. package/src/tests/db-query-denylist.test.ts +202 -0
  151. package/src/tests/db-task-read-contract.test.ts +1 -0
  152. package/src/tests/db-task-write-contract.test.ts +1 -0
  153. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  154. package/src/tests/events-scrub.test.ts +88 -0
  155. package/src/tests/fixtures/secret-corpus.ts +320 -0
  156. package/src/tests/script-connections.test.ts +5 -0
  157. package/src/tests/script-executor-conformance.test.ts +3 -0
  158. package/src/tests/script-executor-quickjs.test.ts +442 -0
  159. package/src/tests/script-executor-registry.test.ts +6 -1
  160. package/src/tests/script-runs-http.test.ts +88 -1
  161. package/src/tests/script-runs-scrub.test.ts +275 -0
  162. package/src/tests/scripts-http.test.ts +82 -1
  163. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  164. package/src/tests/sealed-key-guard.test.ts +166 -0
  165. package/src/tests/secret-canary-sweep.test.ts +958 -0
  166. package/src/tests/secret-registry.test.ts +355 -0
  167. package/src/tests/secret-rules.test.ts +230 -0
  168. package/src/tests/secret-scrubber.test.ts +3 -2
  169. package/src/tests/workflow-replay.test.ts +518 -0
  170. package/src/tests/workflow-scrub.test.ts +242 -0
  171. package/src/tools/db-query.ts +1 -1
  172. package/src/tools/inject-learning.ts +1 -1
  173. package/src/tools/workflows/get-workflow-run.ts +3 -2
  174. package/src/types.ts +1 -1
  175. package/src/utils/secret-rules.generated.ts +2055 -0
  176. package/src/utils/secret-scrubber.ts +363 -29
  177. package/src/workflows/engine.ts +39 -4
  178. package/src/workflows/recovery.ts +10 -1
  179. package/src/workflows/resume.ts +62 -3
  180. package/src/workflows/retry-poller.ts +3 -0
  181. package/dist/boot-scrub-logs-246yn076.js +0 -99
  182. package/dist/cli-3a4m1aws.js +0 -572
  183. package/dist/cli-a87b04k8.js +0 -951
  184. package/dist/cli-cvk6yzw5.js +0 -40
  185. package/dist/cli-h1spv8v4.js +0 -427
  186. package/dist/cli-jqt9hrnz.js +0 -168
  187. package/dist/cli-yhmwmv0g.js +0 -9281
  188. package/src/be/boot-scrub-logs.ts +0 -132
@@ -116,7 +116,7 @@ export function resolveEncryptionKey(
116
116
 
117
117
  if (!allowGenerate) {
118
118
  throw new Error(
119
- `Refusing to auto-generate ${KEY_FILENAME} for an existing database with encrypted secret rows. Restore ${ENV_KEY}, ${ENV_KEY_FILE}, or ${keyFilePath} before booting.`,
119
+ `Refusing to auto-generate ${KEY_FILENAME} for an existing database with encrypted secret rows or sealed replay values. Restore ${ENV_KEY}, ${ENV_KEY_FILE}, or ${keyFilePath} before booting.`,
120
120
  );
121
121
  }
122
122
 
@@ -1,4 +1,6 @@
1
+ import { type ScrubbedText, scrubObject, scrubSecrets } from "../../utils/secret-scrubber";
1
2
  import { normalizeDate, normalizeDateRequired } from "../date-utils";
3
+ import { breaksJsonValidity } from "../scrub-json";
2
4
  import { getDbClient } from "./runtime";
3
5
 
4
6
  /**
@@ -126,7 +128,19 @@ export async function recordApprovalVotes(id: string, approvals: ApprovalVote[])
126
128
  SET approvals = ?, updatedAt = ?
127
129
  WHERE id = ? AND status = 'pending'
128
130
  RETURNING id`,
129
- [JSON.stringify(approvals), new Date().toISOString(), id],
131
+ [scrubApprovalVotesJson(approvals), new Date().toISOString(), id],
130
132
  );
131
133
  return row !== null && row !== undefined;
132
134
  }
135
+
136
+ /**
137
+ * Serializes votes for the `approvals` column with free-text answers scrubbed.
138
+ * Falls back to a per-value scrub when a redaction would break the JSON, so
139
+ * readers can always parse the column.
140
+ */
141
+ export function scrubApprovalVotesJson(approvals: ApprovalVote[]): ScrubbedText {
142
+ const raw = JSON.stringify(approvals);
143
+ const scrubbed = scrubSecrets(raw);
144
+ if (!breaksJsonValidity(raw, scrubbed)) return scrubbed;
145
+ return JSON.stringify(scrubObject(approvals)) as ScrubbedText;
146
+ }
@@ -0,0 +1,40 @@
1
+ import { getDbClient } from "./runtime";
2
+
3
+ /**
4
+ * Encrypted Codex device-login flow state. Kept out of `kv_entries` so
5
+ * db-query can deny it by table. Returns null if missing or expired; an
6
+ * expired row is deleted inline.
7
+ */
8
+ export async function getCodexDeviceFlow(
9
+ flowId: string,
10
+ ): Promise<{ state: string; expiresAt: number } | null> {
11
+ const row = await getDbClient().get<{ state: string; expires_at: number }>(
12
+ "SELECT state, expires_at FROM codex_oauth_device_flows WHERE flow_id = ?",
13
+ [flowId],
14
+ );
15
+ if (!row) return null;
16
+ if (row.expires_at <= Date.now()) {
17
+ await getDbClient().run("DELETE FROM codex_oauth_device_flows WHERE flow_id = ?", [flowId]);
18
+ return null;
19
+ }
20
+ return { state: row.state, expiresAt: row.expires_at };
21
+ }
22
+
23
+ /** Upsert one device-login flow and drop every expired flow. */
24
+ export async function upsertCodexDeviceFlow(input: {
25
+ flowId: string;
26
+ state: string;
27
+ expiresAt: number;
28
+ }): Promise<void> {
29
+ const now = Date.now();
30
+ await getDbClient().run("DELETE FROM codex_oauth_device_flows WHERE expires_at <= ?", [now]);
31
+ await getDbClient().run(
32
+ `INSERT INTO codex_oauth_device_flows (flow_id, state, expires_at, created_at, updated_at)
33
+ VALUES (?, ?, ?, ?, ?)
34
+ ON CONFLICT(flow_id) DO UPDATE SET
35
+ state = excluded.state,
36
+ expires_at = excluded.expires_at,
37
+ updated_at = excluded.updated_at`,
38
+ [input.flowId, input.state, input.expiresAt, now, now],
39
+ );
40
+ }
@@ -0,0 +1,31 @@
1
+ import { decryptSecret, getEncryptionKey } from "../crypto";
2
+ import { isReservedConfigKey } from "../swarm-config-guard";
3
+ import { getDbClient } from "./runtime";
4
+
5
+ /**
6
+ * Every stored secret config value (all scopes, incl. derived
7
+ * `connection.<slug>.secret` rows), decrypted, for the secret registry
8
+ * (src/be/secret-registry.ts). Reserved env-only keys are skipped: their live
9
+ * value is in env. A row that fails to decrypt is counted, not thrown, so one
10
+ * bad row cannot hide the rest.
11
+ */
12
+ export async function listStoredConfigSecrets(): Promise<{
13
+ secrets: { key: string; value: string }[];
14
+ failed: number;
15
+ }> {
16
+ const rows = await getDbClient().query<{ key: string; value: string; encrypted: number }>(
17
+ "SELECT key, value, encrypted FROM swarm_config WHERE isSecret = 1 OR encrypted = 1",
18
+ );
19
+ const secrets: { key: string; value: string }[] = [];
20
+ let failed = 0;
21
+ for (const row of rows) {
22
+ if (isReservedConfigKey(row.key)) continue;
23
+ try {
24
+ const value = row.encrypted === 1 ? decryptSecret(row.value, getEncryptionKey()) : row.value;
25
+ if (value) secrets.push({ key: row.key, value });
26
+ } catch {
27
+ failed++;
28
+ }
29
+ }
30
+ return { secrets, failed };
31
+ }
@@ -7,6 +7,7 @@ import { resolveEncryptionKey } from "../crypto";
7
7
  import { createBunSqliteClient, type DbClient } from "../db-client";
8
8
  import { runMigrations } from "../migrations/runner";
9
9
  import { autoEncryptLegacyOAuthSecrets } from "../oauth-encryption-backfill";
10
+ import { sealedReplayExistsSql } from "../sealed-json";
10
11
 
11
12
  let db: Database | null = null;
12
13
  let sqliteVecAvailable = false;
@@ -233,6 +234,9 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
233
234
  WHERE clientSecretEncrypted = 1 AND clientSecret IS NOT NULL
234
235
  UNION ALL
235
236
  SELECT 1 FROM oauth_authorizations WHERE tokensEncrypted = 1
237
+ ${sealedReplayExistsSql()
238
+ .map((arm) => `UNION ALL ${arm}`)
239
+ .join("\n ")}
236
240
  ) AS present`,
237
241
  )
238
242
  .get()?.present ?? 0) === 1;
@@ -0,0 +1,15 @@
1
+ import { openSealedJson } from "../sealed-json";
2
+ import { getDbClient } from "./runtime";
3
+
4
+ /**
5
+ * The exact args a run executes with, or `undefined` when the run has no row.
6
+ * Only the supervisor may call this: everything else reads the redacted view
7
+ * on `ScriptRun.args`.
8
+ */
9
+ export async function getScriptRunExecutionArgs(id: string): Promise<unknown> {
10
+ const row = await getDbClient().get<{ args: string }>(
11
+ "SELECT args FROM script_runs WHERE id = ?",
12
+ [id],
13
+ );
14
+ return row ? openSealedJson(row.args) : undefined;
15
+ }
@@ -87,7 +87,7 @@ export async function createTask(
87
87
  id,
88
88
  defaultAssetKey("task", id),
89
89
  agentId,
90
- task,
90
+ scrubSecrets(task),
91
91
  "pending",
92
92
  source,
93
93
  options?.slackChannelId ?? null,
@@ -2,6 +2,7 @@ import { scrubSecrets } from "../../utils/secret-scrubber";
2
2
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
3
3
  import { normalizeDateRequired } from "../date-utils";
4
4
  import { getDbClient } from "../db";
5
+ import { oauthSecretName, registerStoredSecret } from "../secret-registry";
5
6
  import {
6
7
  type OAuthAuthorizationStatus,
7
8
  updateAuthorizationTokens,
@@ -266,6 +267,10 @@ async function upsertMcpApp(input: {
266
267
  input.dcrClientSecret == null || input.dcrClientSecret === ""
267
268
  ? (existing?.clientSecret ?? null)
268
269
  : encryptSecret(input.dcrClientSecret, getEncryptionKey());
270
+ registerStoredSecret(
271
+ input.dcrClientSecret,
272
+ oauthSecretName(`mcp-${input.mcpServerId}`, "client_secret"),
273
+ );
269
274
  // MCP OAuth applications are DCR-owned storage rows even when the client
270
275
  // credentials were supplied manually or preregistered. Preserve that exact
271
276
  // distinction in metadata for the legacy adapter boundary.
@@ -2,6 +2,7 @@ import type { OAuthApp, OAuthTokens } from "../../tracker/types";
2
2
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
3
3
  import { normalizeDateRequired } from "../date-utils";
4
4
  import { getDbClient } from "../db";
5
+ import { oauthSecretName, registerStoredSecret } from "../secret-registry";
5
6
 
6
7
  type OAuthAppRow = Omit<
7
8
  OAuthApp,
@@ -241,6 +242,7 @@ async function writeOAuthApp(
241
242
  const metadataProvided = data.metadata !== undefined;
242
243
  const lifted = metadataProvided ? storageMetadata(data.metadata as string) : null;
243
244
  const encryptedSecret = encryptSecret(data.clientSecret, getEncryptionKey());
245
+ registerStoredSecret(data.clientSecret, oauthSecretName(provider, "client_secret"));
244
246
  const scopeSeparator =
245
247
  data.scopeSeparator ?? existing?.scopeSeparator ?? (provider === "linear" ? "," : " ");
246
248
  const rotation =
@@ -373,6 +375,79 @@ export async function getAuthorizationById(id: string): Promise<OAuthAuthorizati
373
375
  return row ? normalizeAuthorization(row) : null;
374
376
  }
375
377
 
378
+ /** Register an authorization's new plaintext tokens with the secret registry,
379
+ * named by the owning app's provider (`mcp-<serverId>` for MCP apps). */
380
+ async function registerAuthorizationSecrets(
381
+ appId: string,
382
+ accessToken: string,
383
+ refreshToken: string | null | undefined,
384
+ ): Promise<void> {
385
+ const app = await getDbClient().get<{ provider: string }>(
386
+ "SELECT provider FROM oauth_apps WHERE id = ?",
387
+ [appId],
388
+ );
389
+ const provider = app?.provider ?? appId;
390
+ registerStoredSecret(accessToken, oauthSecretName(provider, "access_token"));
391
+ registerStoredSecret(refreshToken, oauthSecretName(provider, "refresh_token"));
392
+ }
393
+
394
+ export type StoredOAuthSecretKind = "client_secret" | "access_token" | "refresh_token";
395
+
396
+ /**
397
+ * Every stored OAuth credential, decrypted, for the secret registry
398
+ * (src/be/secret-registry.ts): client secrets of all apps (provider and
399
+ * MCP-managed) and the tokens of all authorizations, whatever their status.
400
+ * A row that fails to decrypt is counted, not thrown.
401
+ */
402
+ export async function listStoredOAuthSecrets(): Promise<{
403
+ secrets: { provider: string; kind: StoredOAuthSecretKind; value: string }[];
404
+ failed: number;
405
+ }> {
406
+ const secrets: { provider: string; kind: StoredOAuthSecretKind; value: string }[] = [];
407
+ let failed = 0;
408
+
409
+ const apps = await getDbClient().query<OAuthAppRow>("SELECT * FROM oauth_apps");
410
+ for (const row of apps) {
411
+ try {
412
+ const app = normalizeOAuthApp(row);
413
+ if (app.clientSecret) {
414
+ secrets.push({ provider: app.provider, kind: "client_secret", value: app.clientSecret });
415
+ }
416
+ } catch {
417
+ failed++;
418
+ }
419
+ }
420
+
421
+ const authorizations = await getDbClient().query<OAuthAuthorizationRow & { provider: string }>(
422
+ `SELECT z.*, a.provider AS provider
423
+ FROM oauth_authorizations z
424
+ JOIN oauth_apps a ON a.id = z.appId`,
425
+ );
426
+ for (const row of authorizations) {
427
+ try {
428
+ const authorization = normalizeAuthorization(row);
429
+ if (authorization.accessToken) {
430
+ secrets.push({
431
+ provider: row.provider,
432
+ kind: "access_token",
433
+ value: authorization.accessToken,
434
+ });
435
+ }
436
+ if (authorization.refreshToken) {
437
+ secrets.push({
438
+ provider: row.provider,
439
+ kind: "refresh_token",
440
+ value: authorization.refreshToken,
441
+ });
442
+ }
443
+ } catch {
444
+ failed++;
445
+ }
446
+ }
447
+
448
+ return { secrets, failed };
449
+ }
450
+
376
451
  export async function upsertAuthorization(data: {
377
452
  id?: string;
378
453
  appId: string;
@@ -402,6 +477,7 @@ export async function upsertAuthorization(data: {
402
477
  : data.refreshToken == null
403
478
  ? null
404
479
  : encryptSecret(data.refreshToken, key);
480
+ await registerAuthorizationSecrets(data.appId, data.accessToken, data.refreshToken);
405
481
 
406
482
  if (existing) {
407
483
  await getDbClient().run(
@@ -486,6 +562,7 @@ export async function updateAuthorizationTokens(
486
562
  : data.refreshToken == null
487
563
  ? null
488
564
  : encryptSecret(data.refreshToken, key);
565
+ await registerAuthorizationSecrets(existing.appId, data.accessToken, data.refreshToken);
489
566
  const result = await getDbClient().run(
490
567
  `UPDATE oauth_authorizations SET
491
568
  accessToken = ?, refreshToken = ?, expiresAt = ?, scope = ?,