@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +4 -0
- package/corpus/core/src/client/composer/PromptComposer.tsx +4 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +30 -3
- package/corpus/core/src/integrations/webhook-handler.ts +4 -2
- package/corpus/core/src/secrets/crypto.ts +98 -47
- package/corpus/core/src/secrets/index.ts +2 -1
- package/corpus/core/src/secrets/schema.ts +3 -0
- package/corpus/core/src/secrets/storage.ts +164 -31
- package/corpus/templates/assets/app/components/layout/Layout.tsx +4 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
- package/corpus/templates/assets/changelog/2026-07-13-assets-keeps-the-right-chat-sidebar-closed-when-the-full-pag.md +6 -0
- package/corpus/templates/calendar/AGENTS.md +9 -0
- package/corpus/templates/calendar/actions/list-events.ts +659 -44
- package/corpus/templates/calendar/changelog/2026-07-13-calendar-inventory-reads-report-source-coverage.md +6 -0
- package/corpus/templates/calendar/server/lib/calendar-connector-catalog.ts +8 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +173 -51
- package/corpus/templates/calendar/server/lib/ical-fetcher.ts +13 -2
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/corpus/templates/mail/AGENTS.md +12 -0
- package/corpus/templates/mail/actions/list-emails.ts +486 -4
- package/corpus/templates/mail/changelog/2026-07-13-coverage-aware-connected-inbox-inventory.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +17 -0
- package/corpus/templates/mail/server/lib/google-auth.ts +33 -5
- package/corpus/templates/mail/server/lib/inventory-cursor.ts +406 -0
- package/corpus/templates/mail/server/lib/list-inbox-emails.ts +21 -2
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +4 -9
- package/corpus/templates/mail/server/plugins/db.ts +20 -0
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +2 -0
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +2 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +3 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +20 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +3 -2
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/crypto.d.ts +35 -13
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +78 -41
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +2 -1
- package/dist/secrets/index.js.map +1 -1
- package/dist/secrets/schema.d.ts +20 -1
- package/dist/secrets/schema.d.ts.map +1 -1
- package/dist/secrets/schema.js +3 -0
- package/dist/secrets/schema.js.map +1 -1
- package/dist/secrets/storage.d.ts +7 -7
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +121 -30
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.100.
|
|
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={
|
|
1222
|
+
onClick={() =>
|
|
1223
|
+
group.engine === "codex-cli" && onConnectLocalRuntime
|
|
1224
|
+
? connectLocalRuntime(group.engine)
|
|
1225
|
+
: openLlmSettings()
|
|
1226
|
+
}
|
|
1211
1227
|
>
|
|
1212
|
-
|
|
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
|
-
|
|
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 =
|
|
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}
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
* set, then `SECRETS_ENCRYPTION_KEY`,
|
|
10
|
-
* key lets a local multi-app
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
117
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|