@agent-native/core 0.228.2-nightly-20261113152307 → 0.228.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/dist/cli/skills-content/an-skill.d.ts +1 -1
- package/dist/cli/skills-content/an-skill.js +16 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/schema-snapshot.generated.js +2 -1
- package/dist/framework-package-versions.json +11 -11
- package/dist/identity/rekey.js +2 -0
- package/dist/mcp/embed-app.js +259 -3
- package/dist/mcp/oauth-route.js +7 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/api-keys.js +8 -7
- package/dist/secrets/index.d.ts +2 -1
- package/dist/secrets/index.js +2 -1
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/secrets/routes.js +7 -8
- package/dist/secrets/schema.d.ts +51 -0
- package/dist/secrets/schema.js +3 -0
- package/dist/secrets/storage.d.ts +83 -1
- package/dist/secrets/storage.js +194 -6
- package/dist/secrets/usage.js +2 -4
- package/dist/secrets/vault-managed.d.ts +17 -0
- package/dist/secrets/vault-managed.js +19 -0
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/builder-signup-experiment.d.ts +4 -4
- package/dist/server/credential-provider.js +7 -7
- package/dist/server/first-touch-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
- package/docs/content/security.mdx +1 -1
- package/docs/content/template-dispatch-developers.mdx +7 -7
- package/docs/content/template-dispatch-reference.mdx +16 -10
- package/package.json +4 -4
- package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
package/dist/secrets/routes.js
CHANGED
|
@@ -51,17 +51,16 @@ import { PERSONAL_PROVIDER_KEYS_RESTRICTED_ERROR_CODE, resolvePersonalProviderKe
|
|
|
51
51
|
import { canonicalSecretKey, secretKeyNames } from "./key-aliases.js";
|
|
52
52
|
import { isManagedDeleteAllowed, managedDeleteRefusal, resolveSecretManagedBy, } from "./managed-keys.js";
|
|
53
53
|
import { listRequiredSecrets, getRequiredSecret, } from "./register.js";
|
|
54
|
-
import { writeAppSecret, updateAppSecretUrlAllowlist, deleteAppSecret, last4, listAppSecretsForScope, readAppSecretMeta,
|
|
54
|
+
import { writeAppSecret, updateAppSecretUrlAllowlist, deleteAppSecret, last4, listAppSecretsForScope, readAppSecretMeta, } from "./storage.js";
|
|
55
55
|
import { registeredUrlAllowlist, validateUrlAllowlist, } from "./substitution.js";
|
|
56
56
|
import { describeSecretUsage } from "./usage.js";
|
|
57
|
+
import { isVaultManagedSecret } from "./vault-managed.js";
|
|
57
58
|
export const SECRET_REJECTED_ERROR_CODE = "secret_rejected";
|
|
58
59
|
export const SECRET_VALIDATION_UNAVAILABLE_ERROR_CODE = "secret_validation_unavailable";
|
|
59
|
-
function secretSource(scope,
|
|
60
|
+
function secretSource(scope, meta) {
|
|
60
61
|
if (scope === "user")
|
|
61
62
|
return "personal";
|
|
62
|
-
return
|
|
63
|
-
? "vault"
|
|
64
|
-
: "workspace";
|
|
63
|
+
return isVaultManagedSecret(meta) ? "vault" : "workspace";
|
|
65
64
|
}
|
|
66
65
|
const NOT_RESOLVED = {
|
|
67
66
|
value: null,
|
|
@@ -216,7 +215,7 @@ export function createListSecretsHandler() {
|
|
|
216
215
|
const meta = await readAppSecretMeta(hit);
|
|
217
216
|
base.last4 = meta?.last4 || last4(effective.value);
|
|
218
217
|
base.updatedAt = meta?.updatedAt;
|
|
219
|
-
base.source = secretSource(hit.scope, meta
|
|
218
|
+
base.source = secretSource(hit.scope, meta);
|
|
220
219
|
base.managedHere = hit.scope === secret.scope && hit.scopeId === scopeId;
|
|
221
220
|
if (base.managedHere && secret.scope === "user") {
|
|
222
221
|
const shared = await asRequestUser(event, () => resolveSecretWithAliasesDetailed(secret.key, {
|
|
@@ -233,7 +232,7 @@ export function createListSecretsHandler() {
|
|
|
233
232
|
scopeId: shared.scopeId,
|
|
234
233
|
})
|
|
235
234
|
: null;
|
|
236
|
-
const sharedSource = secretSource(shared.source, sharedMeta
|
|
235
|
+
const sharedSource = secretSource(shared.source, sharedMeta);
|
|
237
236
|
base.overrides = sharedSource === "vault" ? "vault" : "workspace";
|
|
238
237
|
}
|
|
239
238
|
}
|
|
@@ -513,7 +512,7 @@ function metaToPayload(meta) {
|
|
|
513
512
|
name: meta.key,
|
|
514
513
|
scope: meta.scope,
|
|
515
514
|
scopeId: meta.scopeId,
|
|
516
|
-
source: secretSource(meta.scope, meta
|
|
515
|
+
source: secretSource(meta.scope, meta),
|
|
517
516
|
description: meta.description,
|
|
518
517
|
last4: meta.last4,
|
|
519
518
|
urlAllowlist: meta.urlAllowlist,
|
package/dist/secrets/schema.d.ts
CHANGED
|
@@ -138,6 +138,57 @@ export declare const appSecrets: import("drizzle-orm/pg-core").PgTableWithColumn
|
|
|
138
138
|
identity: undefined;
|
|
139
139
|
generated: undefined;
|
|
140
140
|
}, {}, {}>;
|
|
141
|
+
label: import("drizzle-orm/pg-core").PgColumn<{
|
|
142
|
+
name: "label";
|
|
143
|
+
tableName: "app_secrets";
|
|
144
|
+
dataType: "string";
|
|
145
|
+
columnType: "PgText";
|
|
146
|
+
data: string;
|
|
147
|
+
driverParam: string;
|
|
148
|
+
notNull: false;
|
|
149
|
+
hasDefault: false;
|
|
150
|
+
isPrimaryKey: false;
|
|
151
|
+
isAutoincrement: false;
|
|
152
|
+
hasRuntimeDefault: false;
|
|
153
|
+
enumValues: [string, ...string[]];
|
|
154
|
+
baseColumn: never;
|
|
155
|
+
identity: undefined;
|
|
156
|
+
generated: undefined;
|
|
157
|
+
}, {}, {}>;
|
|
158
|
+
provider: import("drizzle-orm/pg-core").PgColumn<{
|
|
159
|
+
name: "provider";
|
|
160
|
+
tableName: "app_secrets";
|
|
161
|
+
dataType: "string";
|
|
162
|
+
columnType: "PgText";
|
|
163
|
+
data: string;
|
|
164
|
+
driverParam: string;
|
|
165
|
+
notNull: false;
|
|
166
|
+
hasDefault: false;
|
|
167
|
+
isPrimaryKey: false;
|
|
168
|
+
isAutoincrement: false;
|
|
169
|
+
hasRuntimeDefault: false;
|
|
170
|
+
enumValues: [string, ...string[]];
|
|
171
|
+
baseColumn: never;
|
|
172
|
+
identity: undefined;
|
|
173
|
+
generated: undefined;
|
|
174
|
+
}, {}, {}>;
|
|
175
|
+
createdBy: import("drizzle-orm/pg-core").PgColumn<{
|
|
176
|
+
name: "created_by";
|
|
177
|
+
tableName: "app_secrets";
|
|
178
|
+
dataType: "string";
|
|
179
|
+
columnType: "PgText";
|
|
180
|
+
data: string;
|
|
181
|
+
driverParam: string;
|
|
182
|
+
notNull: false;
|
|
183
|
+
hasDefault: false;
|
|
184
|
+
isPrimaryKey: false;
|
|
185
|
+
isAutoincrement: false;
|
|
186
|
+
hasRuntimeDefault: false;
|
|
187
|
+
enumValues: [string, ...string[]];
|
|
188
|
+
baseColumn: never;
|
|
189
|
+
identity: undefined;
|
|
190
|
+
generated: undefined;
|
|
191
|
+
}, {}, {}>;
|
|
141
192
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
142
193
|
name: "created_at";
|
|
143
194
|
tableName: "app_secrets";
|
package/dist/secrets/schema.js
CHANGED
|
@@ -8,6 +8,9 @@ export const appSecrets = table("app_secrets", {
|
|
|
8
8
|
sharedEncryptedValue: text("shared_encrypted_value"),
|
|
9
9
|
description: text("description"),
|
|
10
10
|
urlAllowlist: text("url_allowlist"),
|
|
11
|
+
label: text("label"),
|
|
12
|
+
provider: text("provider"),
|
|
13
|
+
createdBy: text("created_by"),
|
|
11
14
|
createdAt: bigint("created_at", { mode: "number" }).notNull(),
|
|
12
15
|
updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
|
|
13
16
|
});
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* Secret values are NEVER logged and NEVER returned from any route handler.
|
|
19
19
|
*/
|
|
20
20
|
import type { SecretScope } from "./register.js";
|
|
21
|
+
import { VAULT_SYNC_DESCRIPTION_PREFIX } from "./vault-managed.js";
|
|
22
|
+
export { VAULT_SYNC_DESCRIPTION_PREFIX };
|
|
21
23
|
export declare const appSecretsStore: import("../db/store-registry.js").Store;
|
|
22
24
|
export declare function ensureTable(): Promise<void>;
|
|
23
|
-
export declare const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
|
|
24
25
|
export declare function last4(value: string): string;
|
|
25
26
|
export interface SecretRef {
|
|
26
27
|
key: string;
|
|
@@ -31,6 +32,19 @@ export interface WriteSecretArgs extends SecretRef {
|
|
|
31
32
|
value: string;
|
|
32
33
|
description?: string;
|
|
33
34
|
urlAllowlist?: string;
|
|
35
|
+
/** Display name. A write that omits it keeps the stored one. */
|
|
36
|
+
label?: string | null;
|
|
37
|
+
/** Provider tag. A write that omits it keeps the stored one. */
|
|
38
|
+
provider?: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Who created the key through the Dispatch Vault, which marks the row as
|
|
41
|
+
* Vault-managed. The first creator is kept: later writes never replace it.
|
|
42
|
+
*/
|
|
43
|
+
createdBy?: string | null;
|
|
44
|
+
}
|
|
45
|
+
export declare class AppSecretKeyConflictError extends Error {
|
|
46
|
+
readonly statusCode = 409;
|
|
47
|
+
constructor(key: string);
|
|
34
48
|
}
|
|
35
49
|
export declare class AppSecretKeyMismatchError extends Error {
|
|
36
50
|
constructor(ref: SecretRef);
|
|
@@ -75,12 +89,29 @@ export declare function getAppSecretMeta(ref: SecretRef): Promise<{
|
|
|
75
89
|
updatedAt: number;
|
|
76
90
|
} | null>;
|
|
77
91
|
export interface SecretMeta {
|
|
92
|
+
id: string;
|
|
78
93
|
key: string;
|
|
79
94
|
scope: SecretScope;
|
|
80
95
|
scopeId: string;
|
|
81
96
|
last4: string;
|
|
82
97
|
description: string | null;
|
|
83
98
|
urlAllowlist: string[] | null;
|
|
99
|
+
label: string | null;
|
|
100
|
+
provider: string | null;
|
|
101
|
+
createdBy: string | null;
|
|
102
|
+
createdAt: number;
|
|
103
|
+
updatedAt: number;
|
|
104
|
+
}
|
|
105
|
+
/** A stored key's metadata with nothing derived from its value. */
|
|
106
|
+
export interface AppSecretSummary {
|
|
107
|
+
id: string;
|
|
108
|
+
key: string;
|
|
109
|
+
scope: SecretScope;
|
|
110
|
+
scopeId: string;
|
|
111
|
+
description: string | null;
|
|
112
|
+
label: string | null;
|
|
113
|
+
provider: string | null;
|
|
114
|
+
createdBy: string | null;
|
|
84
115
|
createdAt: number;
|
|
85
116
|
updatedAt: number;
|
|
86
117
|
}
|
|
@@ -97,6 +128,57 @@ export declare function readAppSecretMeta(ref: SecretRef): Promise<SecretMeta |
|
|
|
97
128
|
* user-created keys.
|
|
98
129
|
*/
|
|
99
130
|
export declare function listAppSecretsForScope(scope: SecretScope, scopeId: string): Promise<SecretMeta[]>;
|
|
131
|
+
/** The key's metadata by stored id, within one scope. Null when that scope holds no such row. */
|
|
132
|
+
export declare function readAppSecretMetaById(target: {
|
|
133
|
+
id: string;
|
|
134
|
+
scope: SecretScope;
|
|
135
|
+
scopeId: string;
|
|
136
|
+
}): Promise<SecretMeta | null>;
|
|
137
|
+
/**
|
|
138
|
+
* Every key in one scope with nothing derived from its value, so no
|
|
139
|
+
* decryption: the cheap read for pickers, counts and lookups. Use
|
|
140
|
+
* `listAppSecretsForScope` when the masked preview is needed.
|
|
141
|
+
*/
|
|
142
|
+
export declare function listAppSecretSummaries(scope: SecretScope, scopeId: string): Promise<AppSecretSummary[]>;
|
|
143
|
+
export interface UpdateAppSecretPatch {
|
|
144
|
+
/** Rename the key; the row keeps its id, value and creator. */
|
|
145
|
+
key?: string;
|
|
146
|
+
/** Replace the value. Omit to leave the stored ciphertext untouched. */
|
|
147
|
+
value?: string;
|
|
148
|
+
label?: string | null;
|
|
149
|
+
provider?: string | null;
|
|
150
|
+
description?: string | null;
|
|
151
|
+
/**
|
|
152
|
+
* The Dispatch Vault is writing this row, so it is Vault-managed from now
|
|
153
|
+
* on. Recorded as the creator only when the row has none.
|
|
154
|
+
*/
|
|
155
|
+
adoptedBy?: string;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Edit one stored key by its id within a scope, in one statement: rename,
|
|
159
|
+
* replace the value, and change the display fields together or alone. A field
|
|
160
|
+
* the patch leaves out keeps its stored value; `null` clears `label`,
|
|
161
|
+
* `provider` and `description`. Returns the key now stored and the one it
|
|
162
|
+
* replaced, or null when that scope holds no row with this id.
|
|
163
|
+
*/
|
|
164
|
+
export declare function updateAppSecretById(target: {
|
|
165
|
+
id: string;
|
|
166
|
+
scope: SecretScope;
|
|
167
|
+
scopeId: string;
|
|
168
|
+
}, patch: UpdateAppSecretPatch): Promise<{
|
|
169
|
+
key: string;
|
|
170
|
+
previousKey: string;
|
|
171
|
+
} | null>;
|
|
172
|
+
/**
|
|
173
|
+
* Delete one row by its id within a scope. Unlike `deleteAppSecret` this cannot
|
|
174
|
+
* remove a different row that took over the key name after a rename. Returns
|
|
175
|
+
* whether a row was removed.
|
|
176
|
+
*/
|
|
177
|
+
export declare function deleteAppSecretById(target: {
|
|
178
|
+
id: string;
|
|
179
|
+
scope: SecretScope;
|
|
180
|
+
scopeId: string;
|
|
181
|
+
}): Promise<boolean>;
|
|
100
182
|
export declare function deleteAppSecret(ref: SecretRef): Promise<boolean>;
|
|
101
183
|
/**
|
|
102
184
|
* Delete only if the stored ciphertext is still the version the caller read.
|
package/dist/secrets/storage.js
CHANGED
|
@@ -29,6 +29,8 @@ import { encryptSecretValue as encryptLegacyValue, encryptSharedSecretValue as e
|
|
|
29
29
|
import { AppSecretDecryptionError } from "./errors.js";
|
|
30
30
|
import { invalidateOptionalKeyCache } from "./optional-key-cache.js";
|
|
31
31
|
import { APP_SECRETS_CREATE_SQL } from "./schema.js";
|
|
32
|
+
import { VAULT_SYNC_DESCRIPTION_PREFIX } from "./vault-managed.js";
|
|
33
|
+
export { VAULT_SYNC_DESCRIPTION_PREFIX };
|
|
32
34
|
export const appSecretsStore = defineStore({
|
|
33
35
|
id: "app_secrets",
|
|
34
36
|
migrations: [
|
|
@@ -42,12 +44,19 @@ export const appSecretsStore = defineStore({
|
|
|
42
44
|
await ensureColumnExists("app_secrets", "shared_encrypted_value", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS shared_encrypted_value TEXT`);
|
|
43
45
|
},
|
|
44
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: "vault-metadata-columns",
|
|
49
|
+
run: async () => {
|
|
50
|
+
await ensureColumnExists("app_secrets", "label", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS label TEXT`);
|
|
51
|
+
await ensureColumnExists("app_secrets", "provider", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS provider TEXT`);
|
|
52
|
+
await ensureColumnExists("app_secrets", "created_by", `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS created_by TEXT`);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
45
55
|
],
|
|
46
56
|
});
|
|
47
57
|
export function ensureTable() {
|
|
48
58
|
return appSecretsStore.ready();
|
|
49
59
|
}
|
|
50
|
-
export const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
|
|
51
60
|
export function last4(value) {
|
|
52
61
|
if (!value)
|
|
53
62
|
return "";
|
|
@@ -55,6 +64,13 @@ export function last4(value) {
|
|
|
55
64
|
return "••••";
|
|
56
65
|
return "••••" + value.slice(-4);
|
|
57
66
|
}
|
|
67
|
+
export class AppSecretKeyConflictError extends Error {
|
|
68
|
+
statusCode = 409;
|
|
69
|
+
constructor(key) {
|
|
70
|
+
super(`Credential key "${key}" is already in use`);
|
|
71
|
+
this.name = "AppSecretKeyConflictError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
58
74
|
export class AppSecretKeyMismatchError extends Error {
|
|
59
75
|
constructor(ref) {
|
|
60
76
|
super(`Refusing to write secret "${ref.key}" (${ref.scope} scope): this development process is connected to a shared database, and the secrets stored there use encryption key material it does not prefer. Set WORKSPACE_SECRETS_ENCRYPTION_KEY to match the deployments that use this database, or point DATABASE_URL at a local database.`);
|
|
@@ -102,7 +118,7 @@ async function assertWriteKeepsSecretsReadable(ref) {
|
|
|
102
118
|
*/
|
|
103
119
|
export async function writeAppSecret(args) {
|
|
104
120
|
await ensureTable();
|
|
105
|
-
const { key, value, scope, scopeId, description, urlAllowlist } = args;
|
|
121
|
+
const { key, value, scope, scopeId, description, urlAllowlist, label, provider, createdBy, } = args;
|
|
106
122
|
if (!key || !value || !scope || !scopeId) {
|
|
107
123
|
throw new Error("writeAppSecret: key, value, scope, and scopeId are all required");
|
|
108
124
|
}
|
|
@@ -138,13 +154,20 @@ export async function writeAppSecret(args) {
|
|
|
138
154
|
// `populateSharedAppSecret`, which fills a NULL column or compare-and-swap
|
|
139
155
|
// replaces the exact legacy ciphertext it just decrypted. A temporary miss
|
|
140
156
|
// is safer than serving rotated-away plaintext.
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
//
|
|
158
|
+
// label, provider and created_by follow the opposite rule: a write that does
|
|
159
|
+
// not pass them (every writer but the Dispatch Vault) leaves the stored
|
|
160
|
+
// values alone, and the first creator is never replaced.
|
|
161
|
+
const upsertSql = `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, label, provider, created_by, created_at, updated_at)
|
|
162
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
143
163
|
ON CONFLICT (scope, scope_id, key) DO UPDATE SET
|
|
144
164
|
encrypted_value = excluded.encrypted_value,
|
|
145
165
|
shared_encrypted_value = excluded.shared_encrypted_value,
|
|
146
166
|
description = excluded.description,
|
|
147
167
|
url_allowlist = COALESCE(excluded.url_allowlist, app_secrets.url_allowlist),
|
|
168
|
+
label = COALESCE(excluded.label, app_secrets.label),
|
|
169
|
+
provider = COALESCE(excluded.provider, app_secrets.provider),
|
|
170
|
+
created_by = COALESCE(app_secrets.created_by, excluded.created_by),
|
|
148
171
|
updated_at = excluded.updated_at`;
|
|
149
172
|
const upsertArgs = [
|
|
150
173
|
id,
|
|
@@ -155,6 +178,9 @@ export async function writeAppSecret(args) {
|
|
|
155
178
|
sharedEncrypted,
|
|
156
179
|
description ?? null,
|
|
157
180
|
urlAllowlist ?? null,
|
|
181
|
+
label ?? null,
|
|
182
|
+
provider ?? null,
|
|
183
|
+
createdBy ?? null,
|
|
158
184
|
now,
|
|
159
185
|
now,
|
|
160
186
|
];
|
|
@@ -173,6 +199,7 @@ export async function updateAppSecretUrlAllowlist(ref, origins) {
|
|
|
173
199
|
const { rows } = await getDbExec().execute({
|
|
174
200
|
sql: `UPDATE app_secrets SET url_allowlist = ?, updated_at = ?
|
|
175
201
|
WHERE scope = ? AND scope_id = ? AND key = ?
|
|
202
|
+
AND created_by IS NULL
|
|
176
203
|
AND (description IS NULL OR description NOT LIKE ?)
|
|
177
204
|
RETURNING id`,
|
|
178
205
|
args: [
|
|
@@ -494,7 +521,7 @@ export async function readAppSecretMeta(ref) {
|
|
|
494
521
|
const { key, scope, scopeId } = ref;
|
|
495
522
|
const client = getDbExec();
|
|
496
523
|
const { rows } = await client.execute({
|
|
497
|
-
sql: `SELECT id, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
|
|
524
|
+
sql: `SELECT id, encrypted_value, shared_encrypted_value, description, url_allowlist, label, provider, created_by, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
|
|
498
525
|
args: [scope, scopeId, key],
|
|
499
526
|
});
|
|
500
527
|
if (rows.length === 0)
|
|
@@ -513,12 +540,16 @@ export async function readAppSecretMeta(ref) {
|
|
|
513
540
|
last4Value = "";
|
|
514
541
|
}
|
|
515
542
|
return {
|
|
543
|
+
id: String(row.id),
|
|
516
544
|
key,
|
|
517
545
|
scope,
|
|
518
546
|
scopeId,
|
|
519
547
|
last4: last4Value,
|
|
520
548
|
description: row.description ?? null,
|
|
521
549
|
urlAllowlist: parseAllowlist(row.url_allowlist),
|
|
550
|
+
label: row.label ?? null,
|
|
551
|
+
provider: row.provider ?? null,
|
|
552
|
+
createdBy: row.created_by ?? null,
|
|
522
553
|
createdAt: Number(row.created_at ?? 0),
|
|
523
554
|
updatedAt: Number(row.updated_at ?? 0),
|
|
524
555
|
};
|
|
@@ -532,7 +563,7 @@ export async function listAppSecretsForScope(scope, scopeId) {
|
|
|
532
563
|
await ensureTable();
|
|
533
564
|
const client = getDbExec();
|
|
534
565
|
const { rows } = await client.execute({
|
|
535
|
-
sql: `SELECT id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,
|
|
566
|
+
sql: `SELECT id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, label, provider, created_by, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,
|
|
536
567
|
args: [scope, scopeId],
|
|
537
568
|
});
|
|
538
569
|
const results = [];
|
|
@@ -550,18 +581,175 @@ export async function listAppSecretsForScope(scope, scopeId) {
|
|
|
550
581
|
last4Value = "";
|
|
551
582
|
}
|
|
552
583
|
results.push({
|
|
584
|
+
id: String(row.id),
|
|
553
585
|
key: row.key,
|
|
554
586
|
scope,
|
|
555
587
|
scopeId,
|
|
556
588
|
last4: last4Value,
|
|
557
589
|
description: row.description ?? null,
|
|
558
590
|
urlAllowlist: parseAllowlist(row.url_allowlist),
|
|
591
|
+
label: row.label ?? null,
|
|
592
|
+
provider: row.provider ?? null,
|
|
593
|
+
createdBy: row.created_by ?? null,
|
|
559
594
|
createdAt: Number(row.created_at ?? 0),
|
|
560
595
|
updatedAt: Number(row.updated_at ?? 0),
|
|
561
596
|
});
|
|
562
597
|
}
|
|
563
598
|
return results;
|
|
564
599
|
}
|
|
600
|
+
/** The key's metadata by stored id, within one scope. Null when that scope holds no such row. */
|
|
601
|
+
export async function readAppSecretMetaById(target) {
|
|
602
|
+
await ensureTable();
|
|
603
|
+
const { rows } = await getDbExec().execute({
|
|
604
|
+
sql: `SELECT key FROM app_secrets WHERE id = ? AND scope = ? AND scope_id = ? LIMIT 1`,
|
|
605
|
+
args: [target.id, target.scope, target.scopeId],
|
|
606
|
+
});
|
|
607
|
+
if (rows.length === 0)
|
|
608
|
+
return null;
|
|
609
|
+
return readAppSecretMeta({
|
|
610
|
+
key: String(rows[0].key),
|
|
611
|
+
scope: target.scope,
|
|
612
|
+
scopeId: target.scopeId,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Every key in one scope with nothing derived from its value, so no
|
|
617
|
+
* decryption: the cheap read for pickers, counts and lookups. Use
|
|
618
|
+
* `listAppSecretsForScope` when the masked preview is needed.
|
|
619
|
+
*/
|
|
620
|
+
export async function listAppSecretSummaries(scope, scopeId) {
|
|
621
|
+
await ensureTable();
|
|
622
|
+
const { rows } = await getDbExec().execute({
|
|
623
|
+
sql: `SELECT id, key, description, label, provider, created_by, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,
|
|
624
|
+
args: [scope, scopeId],
|
|
625
|
+
});
|
|
626
|
+
return rows.map((row) => ({
|
|
627
|
+
id: String(row.id),
|
|
628
|
+
key: String(row.key),
|
|
629
|
+
scope,
|
|
630
|
+
scopeId,
|
|
631
|
+
description: row.description ?? null,
|
|
632
|
+
label: row.label ?? null,
|
|
633
|
+
provider: row.provider ?? null,
|
|
634
|
+
createdBy: row.created_by ?? null,
|
|
635
|
+
createdAt: Number(row.created_at ?? 0),
|
|
636
|
+
updatedAt: Number(row.updated_at ?? 0),
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Edit one stored key by its id within a scope, in one statement: rename,
|
|
641
|
+
* replace the value, and change the display fields together or alone. A field
|
|
642
|
+
* the patch leaves out keeps its stored value; `null` clears `label`,
|
|
643
|
+
* `provider` and `description`. Returns the key now stored and the one it
|
|
644
|
+
* replaced, or null when that scope holds no row with this id.
|
|
645
|
+
*/
|
|
646
|
+
export async function updateAppSecretById(target, patch) {
|
|
647
|
+
await ensureTable();
|
|
648
|
+
const client = getDbExec();
|
|
649
|
+
const { rows } = await client.execute({
|
|
650
|
+
sql: `SELECT key FROM app_secrets WHERE id = ? AND scope = ? AND scope_id = ? LIMIT 1`,
|
|
651
|
+
args: [target.id, target.scope, target.scopeId],
|
|
652
|
+
});
|
|
653
|
+
if (rows.length === 0)
|
|
654
|
+
return null;
|
|
655
|
+
const previousKey = String(rows[0].key);
|
|
656
|
+
const key = patch.key ?? previousKey;
|
|
657
|
+
if (!key)
|
|
658
|
+
throw new Error("updateAppSecretById: key cannot be empty");
|
|
659
|
+
if (patch.value !== undefined && !patch.value) {
|
|
660
|
+
throw new Error("updateAppSecretById: value cannot be empty");
|
|
661
|
+
}
|
|
662
|
+
if (key !== previousKey) {
|
|
663
|
+
const { rows: clash } = await client.execute({
|
|
664
|
+
sql: `SELECT 1 FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
|
|
665
|
+
args: [target.scope, target.scopeId, key],
|
|
666
|
+
});
|
|
667
|
+
if (clash.length > 0)
|
|
668
|
+
throw new AppSecretKeyConflictError(key);
|
|
669
|
+
}
|
|
670
|
+
let encrypted = null;
|
|
671
|
+
let sharedEncrypted = null;
|
|
672
|
+
if (patch.value !== undefined) {
|
|
673
|
+
await assertWriteKeepsSecretsReadable({
|
|
674
|
+
key,
|
|
675
|
+
scope: target.scope,
|
|
676
|
+
scopeId: target.scopeId,
|
|
677
|
+
});
|
|
678
|
+
encrypted = encryptLegacyValue(patch.value);
|
|
679
|
+
sharedEncrypted = hasSharedSecretEncryptionKeyMaterial()
|
|
680
|
+
? encryptValue(patch.value)
|
|
681
|
+
: null;
|
|
682
|
+
}
|
|
683
|
+
// Each CASE flag says whether the patch carries that field: a field left out
|
|
684
|
+
// keeps its stored value, and an explicit null clears it.
|
|
685
|
+
const { rows: updated } = await client.execute({
|
|
686
|
+
sql: `UPDATE app_secrets SET
|
|
687
|
+
key = CASE WHEN ? THEN ? ELSE key END,
|
|
688
|
+
encrypted_value = CASE WHEN ? THEN ? ELSE encrypted_value END,
|
|
689
|
+
shared_encrypted_value = CASE WHEN ? THEN ? ELSE shared_encrypted_value END,
|
|
690
|
+
label = CASE WHEN ? THEN ? ELSE label END,
|
|
691
|
+
provider = CASE WHEN ? THEN ? ELSE provider END,
|
|
692
|
+
description = CASE WHEN ? THEN ? ELSE description END,
|
|
693
|
+
created_by = COALESCE(created_by, ?),
|
|
694
|
+
updated_at = ?
|
|
695
|
+
WHERE id = ? AND scope = ? AND scope_id = ?
|
|
696
|
+
RETURNING id`,
|
|
697
|
+
args: [
|
|
698
|
+
patch.key !== undefined,
|
|
699
|
+
patch.key ?? null,
|
|
700
|
+
patch.value !== undefined,
|
|
701
|
+
encrypted,
|
|
702
|
+
patch.value !== undefined,
|
|
703
|
+
sharedEncrypted,
|
|
704
|
+
patch.label !== undefined,
|
|
705
|
+
patch.label ?? null,
|
|
706
|
+
patch.provider !== undefined,
|
|
707
|
+
patch.provider ?? null,
|
|
708
|
+
patch.description !== undefined,
|
|
709
|
+
patch.description ?? null,
|
|
710
|
+
patch.adoptedBy ?? null,
|
|
711
|
+
Date.now(),
|
|
712
|
+
target.id,
|
|
713
|
+
target.scope,
|
|
714
|
+
target.scopeId,
|
|
715
|
+
],
|
|
716
|
+
});
|
|
717
|
+
if (updated.length === 0)
|
|
718
|
+
return null;
|
|
719
|
+
for (const staleKey of new Set([previousKey, key])) {
|
|
720
|
+
invalidateRequestSecret({
|
|
721
|
+
key: staleKey,
|
|
722
|
+
scope: target.scope,
|
|
723
|
+
scopeId: target.scopeId,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
invalidateOptionalKeyCache();
|
|
727
|
+
invalidateAgentEngineStatusCache();
|
|
728
|
+
return { key, previousKey };
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Delete one row by its id within a scope. Unlike `deleteAppSecret` this cannot
|
|
732
|
+
* remove a different row that took over the key name after a rename. Returns
|
|
733
|
+
* whether a row was removed.
|
|
734
|
+
*/
|
|
735
|
+
export async function deleteAppSecretById(target) {
|
|
736
|
+
await ensureTable();
|
|
737
|
+
const client = getDbExec();
|
|
738
|
+
const { rows } = await client.execute({
|
|
739
|
+
sql: `DELETE FROM app_secrets WHERE id = ? AND scope = ? AND scope_id = ? RETURNING key`,
|
|
740
|
+
args: [target.id, target.scope, target.scopeId],
|
|
741
|
+
});
|
|
742
|
+
if (rows.length === 0)
|
|
743
|
+
return false;
|
|
744
|
+
invalidateRequestSecret({
|
|
745
|
+
key: String(rows[0].key),
|
|
746
|
+
scope: target.scope,
|
|
747
|
+
scopeId: target.scopeId,
|
|
748
|
+
});
|
|
749
|
+
invalidateOptionalKeyCache();
|
|
750
|
+
invalidateAgentEngineStatusCache();
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
565
753
|
function parseAllowlist(raw) {
|
|
566
754
|
if (raw === null)
|
|
567
755
|
return null;
|
package/dist/secrets/usage.js
CHANGED
|
@@ -10,7 +10,7 @@ import { AGENT_PROVIDER_CATALOG, providerIdForEngine, } from "../client/agent-pr
|
|
|
10
10
|
import { getRequestOrgId } from "../server/request-context.js";
|
|
11
11
|
import { managedSecretUsage, resolveSecretManagedBy } from "./managed-keys.js";
|
|
12
12
|
import { getRegisteredSecretUsage, getRequiredSecret, } from "./register.js";
|
|
13
|
-
import {
|
|
13
|
+
import { isVaultManagedSecret } from "./vault-managed.js";
|
|
14
14
|
/** App id used in effects that apply to every app. */
|
|
15
15
|
export const ALL_APPS = "all";
|
|
16
16
|
function providerForKey(key) {
|
|
@@ -73,9 +73,7 @@ async function resolveSharedFallback(key) {
|
|
|
73
73
|
: null;
|
|
74
74
|
return {
|
|
75
75
|
status: "shared",
|
|
76
|
-
source: meta
|
|
77
|
-
? "vault"
|
|
78
|
-
: shared.source,
|
|
76
|
+
source: isVaultManagedSecret(meta) ? "vault" : shared.source,
|
|
79
77
|
};
|
|
80
78
|
}
|
|
81
79
|
function engineDisplayLabel(entry) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker on `app_secrets.description` that releases before the Vault moved
|
|
3
|
+
* onto `app_secrets` wrote on every Vault-owned row. New Vault writes mark a
|
|
4
|
+
* row with `created_by` instead: such a release deletes marked rows that its
|
|
5
|
+
* own Vault table has no entry for, and it must never find a new key that way.
|
|
6
|
+
*/
|
|
7
|
+
export declare const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
|
|
8
|
+
/**
|
|
9
|
+
* True for a key the Dispatch Vault owns: created or edited through the Vault
|
|
10
|
+
* (`created_by` is set), or written by a release that marked it in the
|
|
11
|
+
* description. Rows an app registered for itself, and a person's own keys, are
|
|
12
|
+
* neither. Callers rule out `user` scope themselves.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isVaultManagedSecret(meta: {
|
|
15
|
+
description?: string | null | undefined;
|
|
16
|
+
createdBy?: string | null | undefined;
|
|
17
|
+
} | null | undefined): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker on `app_secrets.description` that releases before the Vault moved
|
|
3
|
+
* onto `app_secrets` wrote on every Vault-owned row. New Vault writes mark a
|
|
4
|
+
* row with `created_by` instead: such a release deletes marked rows that its
|
|
5
|
+
* own Vault table has no entry for, and it must never find a new key that way.
|
|
6
|
+
*/
|
|
7
|
+
export const VAULT_SYNC_DESCRIPTION_PREFIX = "Synced from Dispatch vault:";
|
|
8
|
+
/**
|
|
9
|
+
* True for a key the Dispatch Vault owns: created or edited through the Vault
|
|
10
|
+
* (`created_by` is set), or written by a release that marked it in the
|
|
11
|
+
* description. Rows an app registered for itself, and a person's own keys, are
|
|
12
|
+
* neither. Callers rule out `user` scope themselves.
|
|
13
|
+
*/
|
|
14
|
+
export function isVaultManagedSecret(meta) {
|
|
15
|
+
if (!meta)
|
|
16
|
+
return false;
|
|
17
|
+
return (!!meta.createdBy ||
|
|
18
|
+
!!meta.description?.startsWith(VAULT_SYNC_DESCRIPTION_PREFIX));
|
|
19
|
+
}
|
|
@@ -54,9 +54,9 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
54
54
|
error: string;
|
|
55
55
|
}>;
|
|
56
56
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
-
errorCode?: undefined;
|
|
58
57
|
code?: undefined;
|
|
59
58
|
error: string;
|
|
59
|
+
errorCode?: undefined;
|
|
60
60
|
} | {
|
|
61
61
|
code?: undefined;
|
|
62
62
|
error: string;
|
|
@@ -67,8 +67,8 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
|
|
|
67
67
|
code: ProviderKeyCheckCode;
|
|
68
68
|
} | {
|
|
69
69
|
error?: undefined;
|
|
70
|
-
errorCode?: undefined;
|
|
71
70
|
code?: undefined;
|
|
71
|
+
errorCode?: undefined;
|
|
72
72
|
ok: boolean;
|
|
73
73
|
key: string;
|
|
74
74
|
baseUrlKey?: string | undefined;
|
|
@@ -66,9 +66,10 @@ export declare function createBuilderSignupExperiment(dependencies: {
|
|
|
66
66
|
status: "unavailable";
|
|
67
67
|
code: string;
|
|
68
68
|
} | {
|
|
69
|
-
status: "disabled";
|
|
70
69
|
code?: undefined;
|
|
70
|
+
status: "disabled";
|
|
71
71
|
} | {
|
|
72
|
+
code?: undefined;
|
|
72
73
|
id: string;
|
|
73
74
|
variant: "automatic" | "control";
|
|
74
75
|
issuedAt: number;
|
|
@@ -79,7 +80,6 @@ export declare function createBuilderSignupExperiment(dependencies: {
|
|
|
79
80
|
policy: "builder-signup-2026-11-06";
|
|
80
81
|
status: "assigned";
|
|
81
82
|
experimentId: string;
|
|
82
|
-
code?: undefined;
|
|
83
83
|
} | {
|
|
84
84
|
status: "disabled";
|
|
85
85
|
code: string;
|
|
@@ -106,9 +106,10 @@ export declare function createBuilderSignupExperiment(dependencies: {
|
|
|
106
106
|
status: "unavailable";
|
|
107
107
|
code: string;
|
|
108
108
|
} | {
|
|
109
|
-
status: "disabled";
|
|
110
109
|
code?: undefined;
|
|
110
|
+
status: "disabled";
|
|
111
111
|
} | {
|
|
112
|
+
code?: undefined;
|
|
112
113
|
id: string;
|
|
113
114
|
variant: "automatic" | "control";
|
|
114
115
|
issuedAt: number;
|
|
@@ -119,7 +120,6 @@ export declare function createBuilderSignupExperiment(dependencies: {
|
|
|
119
120
|
policy: "builder-signup-2026-11-06";
|
|
120
121
|
status: "assigned";
|
|
121
122
|
experimentId: string;
|
|
122
|
-
code?: undefined;
|
|
123
123
|
} | {
|
|
124
124
|
status: "disabled";
|
|
125
125
|
code: string;
|