@agent-native/core 0.100.0 → 0.100.2

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 (68) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +4 -0
  5. package/corpus/core/src/client/composer/PromptComposer.tsx +4 -0
  6. package/corpus/core/src/client/composer/TiptapComposer.tsx +30 -3
  7. package/corpus/core/src/integrations/webhook-handler.ts +4 -2
  8. package/corpus/core/src/secrets/crypto.ts +98 -47
  9. package/corpus/core/src/secrets/index.ts +2 -1
  10. package/corpus/core/src/secrets/schema.ts +3 -0
  11. package/corpus/core/src/secrets/storage.ts +164 -31
  12. package/corpus/templates/assets/app/components/layout/Layout.tsx +4 -0
  13. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
  14. package/corpus/templates/assets/changelog/2026-07-13-assets-keeps-the-right-chat-sidebar-closed-when-the-full-pag.md +6 -0
  15. package/corpus/templates/calendar/AGENTS.md +9 -0
  16. package/corpus/templates/calendar/actions/list-events.ts +659 -44
  17. package/corpus/templates/calendar/changelog/2026-07-13-calendar-inventory-reads-report-source-coverage.md +6 -0
  18. package/corpus/templates/calendar/server/lib/calendar-connector-catalog.ts +8 -0
  19. package/corpus/templates/calendar/server/lib/google-calendar.ts +173 -51
  20. package/corpus/templates/calendar/server/lib/ical-fetcher.ts +13 -2
  21. package/corpus/templates/calendar/server/plugins/agent-chat.ts +5 -3
  22. package/corpus/templates/calendar/shared/api.ts +2 -0
  23. package/corpus/templates/mail/AGENTS.md +12 -0
  24. package/corpus/templates/mail/actions/list-emails.ts +486 -4
  25. package/corpus/templates/mail/changelog/2026-07-13-coverage-aware-connected-inbox-inventory.md +6 -0
  26. package/corpus/templates/mail/server/db/schema.ts +17 -0
  27. package/corpus/templates/mail/server/lib/google-auth.ts +33 -5
  28. package/corpus/templates/mail/server/lib/inventory-cursor.ts +406 -0
  29. package/corpus/templates/mail/server/lib/list-inbox-emails.ts +21 -2
  30. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -0
  31. package/corpus/templates/mail/server/plugins/agent-chat.ts +4 -9
  32. package/corpus/templates/mail/server/plugins/db.ts +20 -0
  33. package/dist/client/AssistantChat.d.ts +2 -0
  34. package/dist/client/AssistantChat.d.ts.map +1 -1
  35. package/dist/client/AssistantChat.js +2 -2
  36. package/dist/client/AssistantChat.js.map +1 -1
  37. package/dist/client/composer/PromptComposer.d.ts +2 -0
  38. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  39. package/dist/client/composer/PromptComposer.js +2 -2
  40. package/dist/client/composer/PromptComposer.js.map +1 -1
  41. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  42. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  43. package/dist/client/composer/TiptapComposer.js +20 -5
  44. package/dist/client/composer/TiptapComposer.js.map +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  47. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  48. package/dist/integrations/webhook-handler.js +3 -2
  49. package/dist/integrations/webhook-handler.js.map +1 -1
  50. package/dist/notifications/routes.d.ts +3 -3
  51. package/dist/resources/handlers.d.ts +2 -2
  52. package/dist/secrets/crypto.d.ts +35 -13
  53. package/dist/secrets/crypto.d.ts.map +1 -1
  54. package/dist/secrets/crypto.js +78 -41
  55. package/dist/secrets/crypto.js.map +1 -1
  56. package/dist/secrets/index.d.ts.map +1 -1
  57. package/dist/secrets/index.js +2 -1
  58. package/dist/secrets/index.js.map +1 -1
  59. package/dist/secrets/schema.d.ts +20 -1
  60. package/dist/secrets/schema.d.ts.map +1 -1
  61. package/dist/secrets/schema.js +3 -0
  62. package/dist/secrets/schema.js.map +1 -1
  63. package/dist/secrets/storage.d.ts +7 -7
  64. package/dist/secrets/storage.d.ts.map +1 -1
  65. package/dist/secrets/storage.js +121 -30
  66. package/dist/secrets/storage.js.map +1 -1
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2255
31
- - template files: 5582
31
+ - template files: 5588
@@ -1,5 +1,19 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.100.2
4
+
5
+ ### Patch Changes
6
+
7
+ - f8cf755: Add a native local-runtime sign-in handoff for Codex-backed ChatGPT subscriptions in Agent composer surfaces.
8
+ - f8cf755: Fix Slack integration fallbacks and link replies directly to Dispatch chat threads.
9
+ - f8cf755: Keep workspace-shared app secrets decryptable across sibling apps.
10
+
11
+ ## 0.100.1
12
+
13
+ ### Patch Changes
14
+
15
+ - 037cc3a: Validate explicit Calendar and Mail connector catalog actions through the external MCP lifecycle.
16
+
3
17
  ## 0.100.0
4
18
 
5
19
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.100.0",
3
+ "version": "0.100.2",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1813,6 +1813,8 @@ export interface AssistantChatProps {
1813
1813
  onForkChat?: () => void | boolean | Promise<void | boolean>;
1814
1814
  /** Override Builder/provider connect routing for embedded hosts. */
1815
1815
  onConnectProvider?: () => void;
1816
+ /** Route local runtime setup through the host's native bridge. */
1817
+ onConnectLocalRuntime?: (engine: string) => void;
1816
1818
  /**
1817
1819
  * Controls the shared composer + menu. Sidebar keeps the full menu by default;
1818
1820
  * hosts without the sidebar provider stack can use upload-only.
@@ -2177,6 +2179,7 @@ const AssistantChatInner = forwardRef<
2177
2179
  imageModelMenu,
2178
2180
  onForkChat,
2179
2181
  onConnectProvider,
2182
+ onConnectLocalRuntime,
2180
2183
  plusMenuMode = "full",
2181
2184
  providerStatusChecksEnabled = true,
2182
2185
  loadHistoryRepository,
@@ -5529,6 +5532,7 @@ const AssistantChatInner = forwardRef<
5529
5532
  onEffortChange={onEffortChange}
5530
5533
  imageModelMenu={imageModelMenu}
5531
5534
  onConnectProvider={onConnectProvider}
5535
+ onConnectLocalRuntime={onConnectLocalRuntime}
5532
5536
  toolbarSlot={composerToolbarSlot}
5533
5537
  contextItems={composerContextItems}
5534
5538
  onRemoveContextItem={removeComposerContextItem}
@@ -154,6 +154,8 @@ export interface PromptComposerProps {
154
154
  * Used by the Electron desktop app to route through the native IPC handler.
155
155
  */
156
156
  onConnectProvider?: () => void;
157
+ /** Called when a local runtime needs its native sign-in/setup flow. */
158
+ onConnectLocalRuntime?: (engine: string) => void;
157
159
  /** Imperative handle for focusing the composer. */
158
160
  composerRef?: Ref<TiptapComposerHandle>;
159
161
  }
@@ -474,6 +476,7 @@ function PromptComposerInner({
474
476
  modelStatusChecksEnabled,
475
477
  onTextChange,
476
478
  onConnectProvider,
479
+ onConnectLocalRuntime,
477
480
  composerRef,
478
481
  }: PromptComposerProps) {
479
482
  const localRef = useRef<TiptapComposerHandle>(null);
@@ -656,6 +659,7 @@ function PromptComposerInner({
656
659
  onEffortChange={handleEffortChange}
657
660
  providerConnectStatusEnabled={resolvedModelStatusChecksEnabled}
658
661
  onConnectProvider={onConnectProvider}
662
+ onConnectLocalRuntime={onConnectLocalRuntime}
659
663
  />
660
664
  </AgentComposerFrame>
661
665
  </>
@@ -628,6 +628,8 @@ export interface TiptapComposerProps {
628
628
  * Used by the Electron desktop app to route through the native IPC handler.
629
629
  */
630
630
  onConnectProvider?: () => void;
631
+ /** Route local runtime setup through the host's native bridge. */
632
+ onConnectLocalRuntime?: (engine: string) => void;
631
633
  /**
632
634
  * Optional secondary model menu (e.g. an image-generation model) rendered as
633
635
  * an extra section inside the model picker. Opt-in; omit for chat-only apps.
@@ -807,6 +809,7 @@ function ModeSelector({
807
809
 
808
810
  const FRIENDLY_MODEL_NAMES: Record<string, string> = {
809
811
  auto: "Default model",
812
+ "codex-cli": "ChatGPT subscription",
810
813
  "claude-fable-5": "Fable 5",
811
814
  "kimi-k2-5": "Kimi K2.5",
812
815
  "deepseek-v3-1": "DeepSeek v3.1",
@@ -949,6 +952,7 @@ function ModelSelector({
949
952
  onEffortChange,
950
953
  providerConnectStatusEnabled = true,
951
954
  onConnectProvider,
955
+ onConnectLocalRuntime,
952
956
  imageModel,
953
957
  }: {
954
958
  model: string;
@@ -963,6 +967,7 @@ function ModelSelector({
963
967
  onEffortChange?: (effort: ReasoningEffort) => void;
964
968
  providerConnectStatusEnabled?: boolean;
965
969
  onConnectProvider?: () => void;
970
+ onConnectLocalRuntime?: (engine: string) => void;
966
971
  imageModel?: ComposerImageModelMenu;
967
972
  }) {
968
973
  const [open, setOpen] = useState(false);
@@ -1050,6 +1055,13 @@ function ModelSelector({
1050
1055
  window.dispatchEvent(new CustomEvent("agent-panel:open-settings"));
1051
1056
  setOpen(false);
1052
1057
  }, []);
1058
+ const connectLocalRuntime = useCallback(
1059
+ (engine: string) => {
1060
+ onConnectLocalRuntime?.(engine);
1061
+ setOpen(false);
1062
+ },
1063
+ [onConnectLocalRuntime],
1064
+ );
1053
1065
 
1054
1066
  return (
1055
1067
  <Popover open={open} onOpenChange={setOpen}>
@@ -1207,9 +1219,15 @@ function ModelSelector({
1207
1219
  <button
1208
1220
  type="button"
1209
1221
  className="text-[10px] text-muted-foreground/60 hover:text-foreground cursor-pointer pe-3 py-1.5"
1210
- onClick={openLlmSettings}
1222
+ onClick={() =>
1223
+ group.engine === "codex-cli" && onConnectLocalRuntime
1224
+ ? connectLocalRuntime(group.engine)
1225
+ : openLlmSettings()
1226
+ }
1211
1227
  >
1212
- needs API key
1228
+ {group.engine === "codex-cli" && onConnectLocalRuntime
1229
+ ? "sign in"
1230
+ : "needs API key"}
1213
1231
  </button>
1214
1232
  )}
1215
1233
  </div>
@@ -1220,7 +1238,14 @@ function ModelSelector({
1220
1238
  type="button"
1221
1239
  onClick={() => {
1222
1240
  if (!group.configured) {
1223
- openLlmSettings();
1241
+ if (
1242
+ group.engine === "codex-cli" &&
1243
+ onConnectLocalRuntime
1244
+ ) {
1245
+ connectLocalRuntime(group.engine);
1246
+ } else {
1247
+ openLlmSettings();
1248
+ }
1224
1249
  return;
1225
1250
  }
1226
1251
  onChange(m, group.engine);
@@ -1339,6 +1364,7 @@ export function TiptapComposer({
1339
1364
  onEffortChange,
1340
1365
  providerConnectStatusEnabled,
1341
1366
  onConnectProvider,
1367
+ onConnectLocalRuntime,
1342
1368
  imageModelMenu,
1343
1369
  draftScope,
1344
1370
  contextItems = [],
@@ -2740,6 +2766,7 @@ export function TiptapComposer({
2740
2766
  onEffortChange={onEffortChange}
2741
2767
  providerConnectStatusEnabled={providerConnectStatusEnabled}
2742
2768
  onConnectProvider={onConnectProvider}
2769
+ onConnectLocalRuntime={onConnectLocalRuntime}
2743
2770
  imageModel={imageModelMenu}
2744
2771
  />
2745
2772
  )}
@@ -74,6 +74,8 @@ import {
74
74
  const PROCESSOR_DISPATCH_SETTLE_WAIT_MS = 1_500;
75
75
  const DEFERRED_RESPONSE_DISPATCH_SETTLE_WAIT_MS = 1_500;
76
76
  const DEFERRED_RESPONSE_MAX_HANDLER_MS = 2_500;
77
+ const EMPTY_INTEGRATION_RESPONSE_MESSAGE =
78
+ "The model finished without a visible answer. Try again, or open the thread in Dispatch to inspect the run.";
77
79
 
78
80
  type ToolDoneEvent = { type: "tool_done"; tool: string; result: string };
79
81
 
@@ -972,7 +974,7 @@ async function processIncomingMessage(
972
974
  "If it was a complex analytics question, opening the analytics app " +
973
975
  "directly is the most reliable way to get an answer right now.";
974
976
  } else {
975
- responseText = "(No response)";
977
+ responseText = EMPTY_INTEGRATION_RESPONSE_MESSAGE;
976
978
  }
977
979
  }
978
980
  if (approval?.type === "approval_required") {
@@ -995,7 +997,7 @@ async function processIncomingMessage(
995
997
  }
996
998
  const threadDeepLinkUrl =
997
999
  appBaseUrl && threadId
998
- ? `${appBaseUrl}/?thread=${threadId}`
1000
+ ? `${appBaseUrl}/chat/${encodeURIComponent(threadId)}`
999
1001
  : undefined;
1000
1002
 
1001
1003
  // Format and send back to platform — update the "thinking…"
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Shared AES-256-GCM encryption for secret values at rest.
3
3
  *
4
- * Used by both the framework secrets vault (`app_secrets`) and per-user/per-org
5
- * credentials (`resolveCredential` / `saveCredential`, stored in `settings`) so
6
- * there is a single crypto implementation and a single key story.
4
+ * Used by the framework secrets vault, per-user/per-org credentials
5
+ * (`resolveCredential` / `saveCredential`, stored in `settings`), and other
6
+ * column-level encrypted values. The `app_secrets` storage layer uses the
7
+ * shared-key variant below because workspace-scoped vault rows are readable by
8
+ * sibling apps in the same workspace. When a deployment has not been given
9
+ * shared key material yet, that variant falls back to the app-scoped key so
10
+ * existing single-app deployments can keep reading and writing secrets while
11
+ * they migrate to the shared key.
7
12
  *
8
- * The encryption key is derived from `<APP_NAME>_SECRETS_ENCRYPTION_KEY` when
9
- * set, then `SECRETS_ENCRYPTION_KEY`, then `BETTER_AUTH_SECRET`. The app-scoped
10
- * key lets a local multi-app workspace read one app's encrypted production data
11
- * without replacing the shared local auth secret. In production we refuse to
12
- * start without configured key material a CWD-derived fallback would be
13
- * effectively static (e.g. `/var/task` on Lambda), so anyone with read access
14
- * to the DB could decrypt every secret.
13
+ * The default encryption key is derived from
14
+ * `<APP_NAME>_SECRETS_ENCRYPTION_KEY` when set, then `SECRETS_ENCRYPTION_KEY`,
15
+ * then `BETTER_AUTH_SECRET`. The app-scoped key lets a local multi-app
16
+ * workspace read one app's encrypted production data without replacing the
17
+ * shared local auth secret. In production we refuse to start without
18
+ * configured key material a CWD-derived fallback would be effectively
19
+ * static (e.g. `/var/task` on Lambda), so anyone with read access to the DB
20
+ * could decrypt every secret.
15
21
  *
16
22
  * Encrypted values are tagged `v1:<iv-hex>:<ct-hex>:<tag-hex>`. The `v1:` prefix
17
23
  * lets readers distinguish ciphertext from legacy plaintext during migration.
@@ -67,45 +73,26 @@ function appScopedEncryptionKey(): string | undefined {
67
73
  : undefined;
68
74
  }
69
75
 
70
- /**
71
- * Derive a 32-byte AES key from the configured secret material via SHA-256.
72
- * Re-derived per call (cheap, stateless, and makes rotation easy).
73
- */
74
- export function getSecretEncryptionKey(): Buffer {
76
+ function sharedEncryptionKeyMaterial(): string | undefined {
77
+ if (typeof process === "undefined") return undefined;
78
+ return process.env.SECRETS_ENCRYPTION_KEY || process.env.BETTER_AUTH_SECRET;
79
+ }
80
+
81
+ function deriveSecretEncryptionKey(
82
+ explicit: string | undefined,
83
+ errorMessage: string,
84
+ warningMessage: string,
85
+ ): Buffer {
75
86
  const { createHash } = requireNodeCrypto();
76
- const explicit =
77
- appScopedEncryptionKey() ||
78
- (typeof process === "undefined"
79
- ? undefined
80
- : process.env.SECRETS_ENCRYPTION_KEY) ||
81
- (typeof process === "undefined"
82
- ? undefined
83
- : process.env.BETTER_AUTH_SECRET);
84
87
 
85
88
  if (!explicit) {
86
89
  if (processNodeEnv() === "production") {
87
- const appName =
88
- typeof process === "undefined"
89
- ? undefined
90
- : process.env.APP_NAME?.trim() // guard:allow-env-credential — deploy-level app configuration selects the scoped encryption key.
91
- .toUpperCase()
92
- .replace(/[^A-Z0-9]+/g, "_")
93
- .replace(/^_+|_+$/g, "");
94
- throw new Error(
95
- "[agent-native/secrets] Refusing to start in production without an encryption key. " +
96
- `Set ${appName ? `${appName}_SECRETS_ENCRYPTION_KEY, ` : ""}SECRETS_ENCRYPTION_KEY, or BETTER_AUTH_SECRET in the deploy environment. ` +
97
- "The previous CWD-derived fallback was effectively static (e.g. `/var/task` on Lambda), " +
98
- "which means anyone with read access to the secrets table could decrypt every user's secrets.",
99
- );
90
+ throw new Error(errorMessage);
100
91
  }
101
92
  if (!_warnedFallback) {
102
93
  _warnedFallback = true;
103
94
  // eslint-disable-next-line no-console
104
- console.warn(
105
- "[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using a machine-local fallback. " +
106
- "Set an app-scoped *_SECRETS_ENCRYPTION_KEY, SECRETS_ENCRYPTION_KEY, or BETTER_AUTH_SECRET for production. " +
107
- "Production deploys without one of these env vars now hard-fail.",
108
- );
95
+ console.warn(warningMessage);
109
96
  }
110
97
  }
111
98
 
@@ -113,10 +100,56 @@ export function getSecretEncryptionKey(): Buffer {
113
100
  return createHash("sha256").update(material).digest();
114
101
  }
115
102
 
116
- /** Encrypt a plain-text value. Returns `v1:<iv-hex>:<ct-hex>:<tag-hex>`. */
117
- export function encryptSecretValue(plaintext: string): string {
103
+ /**
104
+ * Derive the key used by generic encrypted values such as credentials and
105
+ * OAuth tokens. App-specific key material is allowed for these app-local
106
+ * values.
107
+ */
108
+ export function getSecretEncryptionKey(): Buffer {
109
+ const appName =
110
+ typeof process === "undefined"
111
+ ? undefined
112
+ : process.env.APP_NAME?.trim() // guard:allow-env-credential — deploy-level app configuration selects the scoped encryption key.
113
+ .toUpperCase()
114
+ .replace(/[^A-Z0-9]+/g, "_")
115
+ .replace(/^_+|_+$/g, "");
116
+ return deriveSecretEncryptionKey(
117
+ appScopedEncryptionKey() || sharedEncryptionKeyMaterial(),
118
+ "[agent-native/secrets] Refusing to start in production without an encryption key. " +
119
+ `Set ${appName ? `${appName}_SECRETS_ENCRYPTION_KEY, ` : ""}SECRETS_ENCRYPTION_KEY, or BETTER_AUTH_SECRET in the deploy environment. ` +
120
+ "The previous CWD-derived fallback was effectively static (e.g. `/var/task` on Lambda), " +
121
+ "which means anyone with read access to the DB could decrypt every secret.",
122
+ "[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using a machine-local fallback. " +
123
+ "Set an app-scoped *_SECRETS_ENCRYPTION_KEY, SECRETS_ENCRYPTION_KEY, or BETTER_AUTH_SECRET for production. " +
124
+ "Production deploys without one of these env vars now hard-fail.",
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Derive the preferred workspace-shared key used by `app_secrets` rows.
130
+ * Unlike generic column-level encryption, workspace vault data should decrypt
131
+ * in sibling apps, so `SECRETS_ENCRYPTION_KEY` / `BETTER_AUTH_SECRET` take
132
+ * precedence. The app-scoped key remains a compatibility fallback for
133
+ * deployments that have not configured shared material yet; once the shared
134
+ * key is configured, writes use it and reads still fall back to old rows.
135
+ */
136
+ export function getSharedSecretEncryptionKey(): Buffer {
137
+ return deriveSecretEncryptionKey(
138
+ sharedEncryptionKeyMaterial() || appScopedEncryptionKey(),
139
+ "[agent-native/secrets] Refusing to start in production without encryption key material for workspace secrets. " +
140
+ "Set SECRETS_ENCRYPTION_KEY, BETTER_AUTH_SECRET, or the app-scoped *_SECRETS_ENCRYPTION_KEY in the deploy environment.",
141
+ "[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using app-scoped or machine-local fallback for workspace secrets. " +
142
+ "Set SECRETS_ENCRYPTION_KEY or BETTER_AUTH_SECRET in every workspace app so sibling apps share vault rows.",
143
+ );
144
+ }
145
+
146
+ /** Whether this deployment has stable workspace-shared key material. */
147
+ export function hasSharedSecretEncryptionKeyMaterial(): boolean {
148
+ return Boolean(sharedEncryptionKeyMaterial());
149
+ }
150
+
151
+ function encryptWithKey(plaintext: string, key: Buffer): string {
118
152
  const { createCipheriv, randomBytes } = requireNodeCrypto();
119
- const key = getSecretEncryptionKey();
120
153
  const iv = randomBytes(12);
121
154
  const cipher = createCipheriv("aes-256-gcm", key, iv);
122
155
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
@@ -124,8 +157,7 @@ export function encryptSecretValue(plaintext: string): string {
124
157
  return `v1:${iv.toString("hex")}:${ct.toString("hex")}:${tag.toString("hex")}`;
125
158
  }
126
159
 
127
- /** Decrypt a value produced by `encryptSecretValue`. Throws on tampering. */
128
- export function decryptSecretValue(encrypted: string): string {
160
+ function decryptWithKey(encrypted: string, key: Buffer): string {
129
161
  const { createDecipheriv } = requireNodeCrypto();
130
162
  if (!encrypted.startsWith("v1:")) {
131
163
  throw new Error("Unrecognised secret encoding");
@@ -134,7 +166,6 @@ export function decryptSecretValue(encrypted: string): string {
134
166
  if (!ivHex || !ctHex || !tagHex) {
135
167
  throw new Error("Corrupt secret payload");
136
168
  }
137
- const key = getSecretEncryptionKey();
138
169
  const decipher = createDecipheriv(
139
170
  "aes-256-gcm",
140
171
  key,
@@ -148,6 +179,26 @@ export function decryptSecretValue(encrypted: string): string {
148
179
  return pt.toString("utf8");
149
180
  }
150
181
 
182
+ /** Encrypt a plain-text value with the generic app-local key. */
183
+ export function encryptSecretValue(plaintext: string): string {
184
+ return encryptWithKey(plaintext, getSecretEncryptionKey());
185
+ }
186
+
187
+ /** Decrypt a value produced by `encryptSecretValue`. Throws on tampering. */
188
+ export function decryptSecretValue(encrypted: string): string {
189
+ return decryptWithKey(encrypted, getSecretEncryptionKey());
190
+ }
191
+
192
+ /** Encrypt a workspace-shared `app_secrets` value. */
193
+ export function encryptSharedSecretValue(plaintext: string): string {
194
+ return encryptWithKey(plaintext, getSharedSecretEncryptionKey());
195
+ }
196
+
197
+ /** Decrypt a workspace-shared `app_secrets` value. */
198
+ export function decryptSharedSecretValue(encrypted: string): string {
199
+ return decryptWithKey(encrypted, getSharedSecretEncryptionKey());
200
+ }
201
+
151
202
  /**
152
203
  * Strict check for a value produced by `encryptSecretValue`: `v1:` followed by
153
204
  * three hex segments. Intentionally strict so a legacy plaintext credential
@@ -38,7 +38,8 @@ export { APP_SECRETS_CREATE_SQL, appSecrets } from "./schema.js";
38
38
 
39
39
  // AES-256-GCM helpers for column-level encryption of per-row secret values
40
40
  // (e.g. a per-record share password) that don't fit the keyed app_secrets /
41
- // credentials stores. Same key story as the vault.
41
+ // credentials stores. These use the app-local key, unlike the workspace-shared
42
+ // app_secrets storage layer.
42
43
  export {
43
44
  encryptSecretValue,
44
45
  decryptSecretValue,
@@ -22,6 +22,8 @@ export const appSecrets = table("app_secrets", {
22
22
  key: text("key").notNull(),
23
23
  /** Encrypted value — never return this through any API. */
24
24
  encryptedValue: text("encrypted_value").notNull(),
25
+ /** Preferred workspace-shared ciphertext; nullable during key migration. */
26
+ sharedEncryptedValue: text("shared_encrypted_value"),
25
27
  /** Optional human-readable description (used for ad-hoc keys). */
26
28
  description: text("description"),
27
29
  /** JSON array of allowed URL origins. Null = allow all. */
@@ -41,6 +43,7 @@ export const APP_SECRETS_CREATE_SQL = `CREATE TABLE IF NOT EXISTS app_secrets (
41
43
  scope_id TEXT NOT NULL,
42
44
  key TEXT NOT NULL,
43
45
  encrypted_value TEXT NOT NULL,
46
+ shared_encrypted_value TEXT,
44
47
  description TEXT,
45
48
  url_allowlist TEXT,
46
49
  created_at INTEGER NOT NULL,