@agent-native/dispatch 0.17.6 → 0.19.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.
- package/dist/actions/grant-vault-secrets-to-app.js +1 -1
- package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
- package/dist/actions/list-vault-audit.js +1 -1
- package/dist/actions/list-vault-audit.js.map +1 -1
- package/dist/actions/list-vault-secret-options.js +2 -9
- package/dist/actions/list-vault-secret-options.js.map +1 -1
- package/dist/actions/list-vault-secrets.js +1 -1
- package/dist/actions/list-vault-secrets.js.map +1 -1
- package/dist/actions/set-app-creation-settings.js +1 -1
- package/dist/actions/set-app-creation-settings.js.map +1 -1
- package/dist/actions/set-vault-access-settings.js +1 -1
- package/dist/actions/set-vault-access-settings.js.map +1 -1
- package/dist/actions/sync-vault-to-app.js +1 -1
- package/dist/actions/sync-vault-to-app.js.map +1 -1
- package/dist/actions/view-screen.js +4 -3
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/admin-navigation.d.ts +15 -0
- package/dist/components/admin-navigation.d.ts.map +1 -0
- package/dist/components/admin-navigation.js +174 -0
- package/dist/components/admin-navigation.js.map +1 -0
- package/dist/components/automation-run-history-dialog.js +1 -1
- package/dist/components/automation-run-history-dialog.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Header.d.ts.map +1 -1
- package/dist/components/layout/Header.js +9 -0
- package/dist/components/layout/Header.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +4 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +14 -117
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts +5 -0
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -0
- package/dist/components/layout/workspace-apps-rail.js +76 -0
- package/dist/components/layout/workspace-apps-rail.js.map +1 -0
- package/dist/hooks/use-navigation-state.js +38 -15
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/automation-display.js +1 -1
- package/dist/lib/automation-display.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +22 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/admin._index.d.ts +5 -0
- package/dist/routes/pages/admin._index.d.ts.map +1 -0
- package/dist/routes/pages/admin._index.js +19 -0
- package/dist/routes/pages/admin._index.js.map +1 -0
- package/dist/routes/pages/admin.d.ts +5 -0
- package/dist/routes/pages/admin.d.ts.map +1 -0
- package/dist/routes/pages/admin.js +10 -0
- package/dist/routes/pages/admin.js.map +1 -0
- package/dist/routes/pages/approval.js +2 -2
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +3 -2
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +2 -1
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/operations.js +3 -3
- package/dist/routes/pages/operations.js.map +1 -1
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +1 -1
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/routes/pages/transactional-email.$appId.$id.d.ts.map +1 -1
- package/dist/routes/pages/transactional-email.$appId.$id.js +5 -2
- package/dist/routes/pages/transactional-email.$appId.$id.js.map +1 -1
- package/dist/routes/pages/transactional-email.d.ts.map +1 -1
- package/dist/routes/pages/transactional-email.js +6 -2
- package/dist/routes/pages/transactional-email.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +20 -16
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/server/lib/app-creation-store.js +2 -2
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/vault-store.d.ts +15 -0
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +110 -17
- package/dist/server/lib/vault-store.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/grant-vault-secrets-to-app.ts +1 -1
- package/src/actions/list-vault-audit.ts +1 -1
- package/src/actions/list-vault-secret-options.ts +2 -9
- package/src/actions/list-vault-secrets.ts +1 -1
- package/src/actions/set-app-creation-settings.ts +1 -1
- package/src/actions/set-vault-access-settings.ts +1 -1
- package/src/actions/sync-vault-to-app.ts +1 -1
- package/src/actions/view-screen.spec.ts +1 -0
- package/src/actions/view-screen.ts +5 -3
- package/src/components/admin-navigation.tsx +278 -0
- package/src/components/automation-run-history-dialog.tsx +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/Header.tsx +8 -0
- package/src/components/layout/Layout.spec.tsx +85 -6
- package/src/components/layout/Layout.tsx +62 -206
- package/src/components/layout/workspace-apps-rail.tsx +154 -0
- package/src/hooks/use-navigation-state.spec.ts +11 -0
- package/src/hooks/use-navigation-state.ts +36 -15
- package/src/lib/automation-display.spec.ts +1 -1
- package/src/lib/automation-display.ts +1 -1
- package/src/routes/index.spec.ts +8 -0
- package/src/routes/index.ts +25 -0
- package/src/routes/pages/admin._index.tsx +39 -0
- package/src/routes/pages/admin.tsx +15 -0
- package/src/routes/pages/approval.tsx +2 -2
- package/src/routes/pages/apps.$appId.tsx +7 -2
- package/src/routes/pages/integrations.tsx +3 -2
- package/src/routes/pages/operations.tsx +3 -3
- package/src/routes/pages/settings.tsx +7 -3
- package/src/routes/pages/transactional-email.$appId.$id.tsx +9 -2
- package/src/routes/pages/transactional-email.tsx +6 -2
- package/src/routes/pages/vault.tsx +152 -89
- package/src/server/lib/app-creation-store.ts +2 -2
- package/src/server/lib/approval-fencing.spec.ts +69 -0
- package/src/server/lib/vault-store.spec.ts +59 -0
- package/src/server/lib/vault-store.ts +123 -17
|
@@ -50,6 +50,43 @@ beforeEach(async () => {
|
|
|
50
50
|
await runMigrations(dispatchMigrations, {
|
|
51
51
|
table: "dispatch_migrations",
|
|
52
52
|
})({});
|
|
53
|
+
|
|
54
|
+
const { getDbExec } = await import("@agent-native/core/db");
|
|
55
|
+
const exec = getDbExec();
|
|
56
|
+
await exec.execute({
|
|
57
|
+
sql: `CREATE TABLE IF NOT EXISTS org_members (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
org_id TEXT NOT NULL,
|
|
60
|
+
email TEXT NOT NULL,
|
|
61
|
+
role TEXT NOT NULL,
|
|
62
|
+
joined_at INTEGER NOT NULL
|
|
63
|
+
)`,
|
|
64
|
+
args: [],
|
|
65
|
+
});
|
|
66
|
+
await exec.execute({
|
|
67
|
+
sql: "INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)",
|
|
68
|
+
args: ["approval-owner", orgId, ownerEmail, "owner", Date.now()],
|
|
69
|
+
});
|
|
70
|
+
await exec.execute({
|
|
71
|
+
sql: "INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)",
|
|
72
|
+
args: [
|
|
73
|
+
"approval-other-owner",
|
|
74
|
+
otherOrgId,
|
|
75
|
+
otherOwnerEmail,
|
|
76
|
+
"owner",
|
|
77
|
+
Date.now(),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
await exec.execute({
|
|
81
|
+
sql: "INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)",
|
|
82
|
+
args: [
|
|
83
|
+
"approval-owner-other-org",
|
|
84
|
+
otherOrgId,
|
|
85
|
+
ownerEmail,
|
|
86
|
+
"owner",
|
|
87
|
+
Date.now(),
|
|
88
|
+
],
|
|
89
|
+
});
|
|
53
90
|
});
|
|
54
91
|
|
|
55
92
|
afterEach(async () => {
|
|
@@ -65,6 +102,38 @@ afterEach(async () => {
|
|
|
65
102
|
});
|
|
66
103
|
|
|
67
104
|
describe("dispatch approval request status fencing", () => {
|
|
105
|
+
it("does not approve a request through an owner-email match from another org", async () => {
|
|
106
|
+
const [{ runWithRequestContext }, { getDbExec }, vaultStore] =
|
|
107
|
+
await Promise.all([
|
|
108
|
+
import("@agent-native/core/server"),
|
|
109
|
+
import("@agent-native/core/db"),
|
|
110
|
+
import("./vault-store.js"),
|
|
111
|
+
]);
|
|
112
|
+
const exec = getDbExec();
|
|
113
|
+
|
|
114
|
+
const created = await runWithRequestContext(
|
|
115
|
+
{ userEmail: ownerEmail, orgId },
|
|
116
|
+
() =>
|
|
117
|
+
vaultStore.createRequest({
|
|
118
|
+
credentialKey: "CROSS_ORG_KEY",
|
|
119
|
+
appId: "test-app",
|
|
120
|
+
reason: "must stay in the request org",
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
runWithRequestContext({ userEmail: ownerEmail, orgId: otherOrgId }, () =>
|
|
126
|
+
vaultStore.approveRequest((created as any).id, "secret-value"),
|
|
127
|
+
),
|
|
128
|
+
).rejects.toThrow("Request not found");
|
|
129
|
+
|
|
130
|
+
const rows = await exec.execute({
|
|
131
|
+
sql: "SELECT status FROM vault_requests WHERE id = ?",
|
|
132
|
+
args: [(created as any).id],
|
|
133
|
+
});
|
|
134
|
+
expect(rows.rows[0]).toMatchObject({ status: "pending" });
|
|
135
|
+
});
|
|
136
|
+
|
|
68
137
|
it("applies the change once when approveRequest is called twice on the same request", async () => {
|
|
69
138
|
const [{ runWithRequestContext }, { getDbExec }, dispatchStore] =
|
|
70
139
|
await Promise.all([
|
|
@@ -5,6 +5,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
5
5
|
currentOwnerEmail: vi.fn(),
|
|
6
6
|
deleteAppSecret: vi.fn(),
|
|
7
7
|
discoverAgents: vi.fn(),
|
|
8
|
+
getDbExec: vi.fn(),
|
|
8
9
|
getDb: vi.fn(),
|
|
9
10
|
getOrgSetting: vi.fn(),
|
|
10
11
|
getUserSetting: vi.fn(),
|
|
@@ -21,6 +22,11 @@ vi.mock("@agent-native/core/secrets", () => ({
|
|
|
21
22
|
writeAppSecret: mocks.writeAppSecret,
|
|
22
23
|
}));
|
|
23
24
|
|
|
25
|
+
vi.mock("@agent-native/core/db", async (importOriginal) => ({
|
|
26
|
+
...(await importOriginal<typeof import("@agent-native/core/db")>()),
|
|
27
|
+
getDbExec: mocks.getDbExec,
|
|
28
|
+
}));
|
|
29
|
+
|
|
24
30
|
vi.mock("@agent-native/core/server/agent-discovery", () => ({
|
|
25
31
|
discoverAgents: mocks.discoverAgents,
|
|
26
32
|
}));
|
|
@@ -49,9 +55,12 @@ vi.mock("../../db/index.js", async (importOriginal) => {
|
|
|
49
55
|
import { readAppSecret } from "@agent-native/core/secrets";
|
|
50
56
|
|
|
51
57
|
import {
|
|
58
|
+
assertCanManageVault,
|
|
52
59
|
cleanupSyncedCredentialKeysIfUnused,
|
|
53
60
|
credentialStoreScopeForVaultCtx,
|
|
54
61
|
isTrustedEnvVarSyncAgentUrl,
|
|
62
|
+
getGrant,
|
|
63
|
+
listGrants,
|
|
55
64
|
resyncAllVaultSecretsToCredentialStore,
|
|
56
65
|
syncGrantsToApp,
|
|
57
66
|
syncSecretsToCredentialStore,
|
|
@@ -85,6 +94,56 @@ describe("credentialStoreScopeForVaultCtx", () => {
|
|
|
85
94
|
});
|
|
86
95
|
});
|
|
87
96
|
|
|
97
|
+
describe("vault authorization", () => {
|
|
98
|
+
it("rejects an organization member before reading shared vault values", async () => {
|
|
99
|
+
mocks.currentOrgId.mockReturnValue("org_123");
|
|
100
|
+
mocks.currentOwnerEmail.mockReturnValue("member@example.test");
|
|
101
|
+
mocks.getDbExec.mockReturnValue({
|
|
102
|
+
execute: vi.fn().mockResolvedValue({ rows: [{ role: "member" }] }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await expect(assertCanManageVault()).rejects.toThrow(
|
|
106
|
+
"Only organization owners and admins can manage the workspace vault.",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("rejects organization members before listing or reading grants", async () => {
|
|
111
|
+
mocks.currentOrgId.mockReturnValue("org_123");
|
|
112
|
+
mocks.currentOwnerEmail.mockReturnValue("member@example.test");
|
|
113
|
+
mocks.getDbExec.mockReturnValue({
|
|
114
|
+
execute: vi.fn().mockResolvedValue({ rows: [{ role: "member" }] }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await expect(listGrants()).rejects.toThrow(
|
|
118
|
+
"Only organization owners and admins can manage the workspace vault.",
|
|
119
|
+
);
|
|
120
|
+
await expect(getGrant("grant-1")).rejects.toThrow(
|
|
121
|
+
"Only organization owners and admins can manage the workspace vault.",
|
|
122
|
+
);
|
|
123
|
+
expect(mocks.getDb).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it.each(["owner", "admin"])(
|
|
127
|
+
"allows an organization %s to manage shared vault values",
|
|
128
|
+
async (role) => {
|
|
129
|
+
mocks.currentOrgId.mockReturnValue("org_123");
|
|
130
|
+
mocks.currentOwnerEmail.mockReturnValue(`${role}@example.test`);
|
|
131
|
+
mocks.getDbExec.mockReturnValue({
|
|
132
|
+
execute: vi.fn().mockResolvedValue({ rows: [{ role }] }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await expect(assertCanManageVault()).resolves.toBeUndefined();
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
it("allows a solo workspace owner to manage their own vault", async () => {
|
|
140
|
+
mocks.currentOrgId.mockReturnValue(null);
|
|
141
|
+
|
|
142
|
+
await expect(assertCanManageVault()).resolves.toBeUndefined();
|
|
143
|
+
expect(mocks.getDbExec).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
88
147
|
describe("isTrustedEnvVarSyncAgentUrl", () => {
|
|
89
148
|
it("allows localhost development app URLs", () => {
|
|
90
149
|
expect(isTrustedEnvVarSyncAgentUrl("http://localhost:9201")).toBe(true);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
3
4
|
import { and, desc, eq, isNull, or, sql } from "@agent-native/core/db/schema";
|
|
4
5
|
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
|
5
6
|
import {
|
|
@@ -72,6 +73,15 @@ function ctxScope<T extends { ownerEmail: any; orgId: any }>(
|
|
|
72
73
|
return or(eq(table.ownerEmail, ctx.ownerEmail), eq(table.orgId, ctx.orgId));
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/** Scope an approval operation to the active tenant, never just the owner email. */
|
|
77
|
+
function tenantScope<T extends { ownerEmail: any; orgId: any }>(
|
|
78
|
+
table: T,
|
|
79
|
+
ctx: VaultCtx,
|
|
80
|
+
) {
|
|
81
|
+
if (ctx.orgId) return eq(table.orgId, ctx.orgId);
|
|
82
|
+
return and(eq(table.ownerEmail, ctx.ownerEmail), isNull(table.orgId));
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
/** Build a ctx that scopes to a specific row's owner/org (used when a
|
|
76
86
|
* request approver acts on behalf of the original requester so the
|
|
77
87
|
* created secret lands in the request's org). */
|
|
@@ -140,6 +150,45 @@ function scopedFilter<T extends { ownerEmail: any; orgId: any }>(table: T) {
|
|
|
140
150
|
return ctxScope(table, requireVaultCtx());
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Shared vault values and grants are organization administration data. Keep
|
|
155
|
+
* the member path available for request creation and safe key metadata, but
|
|
156
|
+
* fail closed before a member can read or mutate a raw secret.
|
|
157
|
+
*/
|
|
158
|
+
export async function assertCanManageVault(): Promise<void> {
|
|
159
|
+
const orgId = currentOrgId();
|
|
160
|
+
if (!orgId) return;
|
|
161
|
+
|
|
162
|
+
const email = currentOwnerEmail().trim().toLowerCase();
|
|
163
|
+
let role: unknown = null;
|
|
164
|
+
try {
|
|
165
|
+
const result = await getDbExec().execute({
|
|
166
|
+
sql: "SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1",
|
|
167
|
+
args: [orgId, email],
|
|
168
|
+
});
|
|
169
|
+
role = result.rows[0]?.role;
|
|
170
|
+
} catch {
|
|
171
|
+
// coercion-ok: unknown membership is treated as unauthorized below.
|
|
172
|
+
// A failed membership check must not become an authorization bypass.
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (role !== "owner" && role !== "admin") {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"Only organization owners and admins can manage the workspace vault.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function canManageVault(): Promise<boolean> {
|
|
183
|
+
try {
|
|
184
|
+
await assertCanManageVault();
|
|
185
|
+
return true;
|
|
186
|
+
} catch {
|
|
187
|
+
// coercion-ok: the UI must treat an authorization lookup failure as no access.
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
143
192
|
function normalizeCredentialKey(value: string) {
|
|
144
193
|
return value.trim();
|
|
145
194
|
}
|
|
@@ -169,6 +218,7 @@ export async function getVaultAccessSettings(): Promise<VaultAccessSettings> {
|
|
|
169
218
|
export async function setVaultAccessSettings(input: {
|
|
170
219
|
mode: VaultAccessMode;
|
|
171
220
|
}): Promise<VaultAccessSettings> {
|
|
221
|
+
await assertCanManageVault();
|
|
172
222
|
const scope = vaultAccessScope();
|
|
173
223
|
const next = { mode: parseVaultAccessMode(input.mode) };
|
|
174
224
|
if (scope.scope === "org") {
|
|
@@ -215,6 +265,7 @@ export async function recordVaultAudit(input: {
|
|
|
215
265
|
}
|
|
216
266
|
|
|
217
267
|
export async function listVaultAudit(limit = 50) {
|
|
268
|
+
await assertCanManageVault();
|
|
218
269
|
const db = getDb();
|
|
219
270
|
return db
|
|
220
271
|
.select()
|
|
@@ -227,6 +278,7 @@ export async function listVaultAudit(limit = 50) {
|
|
|
227
278
|
// ─── Secrets ──────────────────────────────────────────────────────
|
|
228
279
|
|
|
229
280
|
export async function listSecrets() {
|
|
281
|
+
await assertCanManageVault();
|
|
230
282
|
const db = getDb();
|
|
231
283
|
return db
|
|
232
284
|
.select()
|
|
@@ -235,7 +287,24 @@ export async function listSecrets() {
|
|
|
235
287
|
.orderBy(desc(schema.vaultSecrets.updatedAt));
|
|
236
288
|
}
|
|
237
289
|
|
|
290
|
+
/** Safe metadata for app creation and integration readiness checks. */
|
|
291
|
+
export async function listSecretOptions() {
|
|
292
|
+
const db = getDb();
|
|
293
|
+
return db
|
|
294
|
+
.select({
|
|
295
|
+
id: schema.vaultSecrets.id,
|
|
296
|
+
name: schema.vaultSecrets.name,
|
|
297
|
+
credentialKey: schema.vaultSecrets.credentialKey,
|
|
298
|
+
provider: schema.vaultSecrets.provider,
|
|
299
|
+
description: schema.vaultSecrets.description,
|
|
300
|
+
})
|
|
301
|
+
.from(schema.vaultSecrets)
|
|
302
|
+
.where(scopedFilter(schema.vaultSecrets))
|
|
303
|
+
.orderBy(desc(schema.vaultSecrets.updatedAt));
|
|
304
|
+
}
|
|
305
|
+
|
|
238
306
|
export async function getSecret(secretId: string, ctx: VaultCtx) {
|
|
307
|
+
await assertCanManageVault();
|
|
239
308
|
const db = getDb();
|
|
240
309
|
const [row] = await db
|
|
241
310
|
.select()
|
|
@@ -260,6 +329,7 @@ export async function createSecret(
|
|
|
260
329
|
},
|
|
261
330
|
ctx: VaultCtx = requireVaultCtx(),
|
|
262
331
|
) {
|
|
332
|
+
await assertCanManageVault();
|
|
263
333
|
const db = getDb();
|
|
264
334
|
const timestamp = now();
|
|
265
335
|
const credentialKey = normalizeCredentialKey(input.credentialKey);
|
|
@@ -372,6 +442,7 @@ export async function updateSecret(
|
|
|
372
442
|
},
|
|
373
443
|
ctx: VaultCtx = requireVaultCtx(),
|
|
374
444
|
) {
|
|
445
|
+
await assertCanManageVault();
|
|
375
446
|
const db = getDb();
|
|
376
447
|
const existing = await getSecret(secretId, ctx);
|
|
377
448
|
if (!existing) throw new Error("Secret not found");
|
|
@@ -478,6 +549,7 @@ export async function deleteSecret(
|
|
|
478
549
|
secretId: string,
|
|
479
550
|
ctx: VaultCtx = requireVaultCtx(),
|
|
480
551
|
) {
|
|
552
|
+
await assertCanManageVault();
|
|
481
553
|
const db = getDb();
|
|
482
554
|
const existing = await getSecret(secretId, ctx);
|
|
483
555
|
if (!existing) throw new Error("Secret not found");
|
|
@@ -524,6 +596,7 @@ export async function listGrants(filter?: {
|
|
|
524
596
|
secretId?: string;
|
|
525
597
|
appId?: string;
|
|
526
598
|
}) {
|
|
599
|
+
await assertCanManageVault();
|
|
527
600
|
const db = getDb();
|
|
528
601
|
const conditions = [scopedFilter(schema.vaultGrants)];
|
|
529
602
|
if (filter?.secretId) {
|
|
@@ -543,6 +616,7 @@ export async function getGrant(
|
|
|
543
616
|
grantId: string,
|
|
544
617
|
ctx: VaultCtx = requireVaultCtx(),
|
|
545
618
|
) {
|
|
619
|
+
await assertCanManageVault();
|
|
546
620
|
const db = getDb();
|
|
547
621
|
const [row] = await db
|
|
548
622
|
.select()
|
|
@@ -562,6 +636,7 @@ export async function createGrant(
|
|
|
562
636
|
appId: string,
|
|
563
637
|
ctx: VaultCtx = requireVaultCtx(),
|
|
564
638
|
) {
|
|
639
|
+
await assertCanManageVault();
|
|
565
640
|
const db = getDb();
|
|
566
641
|
const secret = await getSecret(secretId, ctx);
|
|
567
642
|
if (!secret) throw new Error("Secret not found");
|
|
@@ -657,6 +732,7 @@ export async function grantSecretsToApp(
|
|
|
657
732
|
appId: string,
|
|
658
733
|
ctx: VaultCtx = requireVaultCtx(),
|
|
659
734
|
) {
|
|
735
|
+
await assertCanManageVault();
|
|
660
736
|
const access = await getVaultAccessSettings();
|
|
661
737
|
const uniqueSecretIds = Array.from(new Set(secretIds));
|
|
662
738
|
if (access.mode === "all-apps") {
|
|
@@ -695,6 +771,7 @@ export async function revokeGrant(
|
|
|
695
771
|
grantId: string,
|
|
696
772
|
ctx: VaultCtx = requireVaultCtx(),
|
|
697
773
|
) {
|
|
774
|
+
await assertCanManageVault();
|
|
698
775
|
const db = getDb();
|
|
699
776
|
const grant = await getGrant(grantId, ctx);
|
|
700
777
|
if (!grant) throw new Error("Grant not found");
|
|
@@ -878,6 +955,7 @@ export async function syncGrantsToApp(
|
|
|
878
955
|
appId: string,
|
|
879
956
|
ctx: VaultCtx = requireVaultCtx(),
|
|
880
957
|
) {
|
|
958
|
+
await assertCanManageVault();
|
|
881
959
|
const db = getDb();
|
|
882
960
|
const access = await getVaultAccessSettings();
|
|
883
961
|
const agents = await discoverAgents("dispatch");
|
|
@@ -1023,7 +1101,13 @@ export async function syncGrantsToApp(
|
|
|
1023
1101
|
|
|
1024
1102
|
export async function listRequests(filter?: { status?: string }) {
|
|
1025
1103
|
const db = getDb();
|
|
1026
|
-
const
|
|
1104
|
+
const ctx = requireVaultCtx();
|
|
1105
|
+
const isAdmin = await canManageVault();
|
|
1106
|
+
const conditions = [
|
|
1107
|
+
isAdmin
|
|
1108
|
+
? ctxScope(schema.vaultRequests, ctx)
|
|
1109
|
+
: eq(schema.vaultRequests.ownerEmail, ctx.ownerEmail),
|
|
1110
|
+
];
|
|
1027
1111
|
if (filter?.status) {
|
|
1028
1112
|
conditions.push(eq(schema.vaultRequests.status, filter.status) as any);
|
|
1029
1113
|
}
|
|
@@ -1052,6 +1136,21 @@ export async function getRequest(
|
|
|
1052
1136
|
return row ?? null;
|
|
1053
1137
|
}
|
|
1054
1138
|
|
|
1139
|
+
async function getRequestForTenant(requestId: string, ctx: VaultCtx) {
|
|
1140
|
+
const db = getDb();
|
|
1141
|
+
const [row] = await db
|
|
1142
|
+
.select()
|
|
1143
|
+
.from(schema.vaultRequests)
|
|
1144
|
+
.where(
|
|
1145
|
+
and(
|
|
1146
|
+
eq(schema.vaultRequests.id, requestId),
|
|
1147
|
+
tenantScope(schema.vaultRequests, ctx),
|
|
1148
|
+
),
|
|
1149
|
+
)
|
|
1150
|
+
.limit(1);
|
|
1151
|
+
return row ?? null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1055
1154
|
export async function createRequest(input: {
|
|
1056
1155
|
credentialKey: string;
|
|
1057
1156
|
appId: string;
|
|
@@ -1095,8 +1194,9 @@ export async function approveRequest(
|
|
|
1095
1194
|
secretName?: string,
|
|
1096
1195
|
ctx: VaultCtx = requireVaultCtx(),
|
|
1097
1196
|
) {
|
|
1197
|
+
await assertCanManageVault();
|
|
1098
1198
|
const db = getDb();
|
|
1099
|
-
const request = await
|
|
1199
|
+
const request = await getRequestForTenant(requestId, ctx);
|
|
1100
1200
|
if (!request) throw new Error("Request not found");
|
|
1101
1201
|
|
|
1102
1202
|
const timestamp = now();
|
|
@@ -1119,7 +1219,7 @@ export async function approveRequest(
|
|
|
1119
1219
|
.where(
|
|
1120
1220
|
and(
|
|
1121
1221
|
eq(schema.vaultRequests.id, requestId),
|
|
1122
|
-
|
|
1222
|
+
tenantScope(schema.vaultRequests, ctx),
|
|
1123
1223
|
or(
|
|
1124
1224
|
eq(schema.vaultRequests.status, "pending"),
|
|
1125
1225
|
and(
|
|
@@ -1132,7 +1232,7 @@ export async function approveRequest(
|
|
|
1132
1232
|
.returning();
|
|
1133
1233
|
|
|
1134
1234
|
if (claimed.length === 0) {
|
|
1135
|
-
const current = await
|
|
1235
|
+
const current = await getRequestForTenant(requestId, ctx);
|
|
1136
1236
|
if (current?.status === "applying") {
|
|
1137
1237
|
throw new Error("Vault request is already being applied");
|
|
1138
1238
|
}
|
|
@@ -1180,7 +1280,7 @@ export async function approveRequest(
|
|
|
1180
1280
|
.where(
|
|
1181
1281
|
and(
|
|
1182
1282
|
eq(schema.vaultRequests.id, requestId),
|
|
1183
|
-
|
|
1283
|
+
tenantScope(schema.vaultRequests, ctx),
|
|
1184
1284
|
eq(schema.vaultRequests.status, "applying"),
|
|
1185
1285
|
),
|
|
1186
1286
|
);
|
|
@@ -1198,7 +1298,7 @@ export async function approveRequest(
|
|
|
1198
1298
|
.where(
|
|
1199
1299
|
and(
|
|
1200
1300
|
eq(schema.vaultRequests.id, requestId),
|
|
1201
|
-
|
|
1301
|
+
tenantScope(schema.vaultRequests, ctx),
|
|
1202
1302
|
eq(schema.vaultRequests.status, "applying"),
|
|
1203
1303
|
),
|
|
1204
1304
|
);
|
|
@@ -1210,7 +1310,7 @@ export async function approveRequest(
|
|
|
1210
1310
|
metadata: { requestId, reviewer },
|
|
1211
1311
|
});
|
|
1212
1312
|
|
|
1213
|
-
return
|
|
1313
|
+
return getRequestForTenant(requestId, ctx);
|
|
1214
1314
|
}
|
|
1215
1315
|
|
|
1216
1316
|
export async function denyRequest(
|
|
@@ -1218,8 +1318,9 @@ export async function denyRequest(
|
|
|
1218
1318
|
reason?: string | null,
|
|
1219
1319
|
ctx: VaultCtx = requireVaultCtx(),
|
|
1220
1320
|
) {
|
|
1321
|
+
await assertCanManageVault();
|
|
1221
1322
|
const db = getDb();
|
|
1222
|
-
const request = await
|
|
1323
|
+
const request = await getRequestForTenant(requestId, ctx);
|
|
1223
1324
|
if (!request) throw new Error("Request not found");
|
|
1224
1325
|
|
|
1225
1326
|
const timestamp = now();
|
|
@@ -1236,14 +1337,14 @@ export async function denyRequest(
|
|
|
1236
1337
|
.where(
|
|
1237
1338
|
and(
|
|
1238
1339
|
eq(schema.vaultRequests.id, requestId),
|
|
1239
|
-
|
|
1340
|
+
tenantScope(schema.vaultRequests, ctx),
|
|
1240
1341
|
eq(schema.vaultRequests.status, "pending"),
|
|
1241
1342
|
),
|
|
1242
1343
|
)
|
|
1243
1344
|
.returning();
|
|
1244
1345
|
|
|
1245
1346
|
if (claimed.length === 0) {
|
|
1246
|
-
return
|
|
1347
|
+
return getRequestForTenant(requestId, ctx);
|
|
1247
1348
|
}
|
|
1248
1349
|
|
|
1249
1350
|
const claimedRequest = claimed[0];
|
|
@@ -1255,7 +1356,7 @@ export async function denyRequest(
|
|
|
1255
1356
|
metadata: { requestId, reviewer, reason },
|
|
1256
1357
|
});
|
|
1257
1358
|
|
|
1258
|
-
return
|
|
1359
|
+
return getRequestForTenant(requestId, ctx);
|
|
1259
1360
|
}
|
|
1260
1361
|
|
|
1261
1362
|
// ─── Integrations Catalog ────────────────────────────────────────
|
|
@@ -1282,8 +1383,8 @@ export interface AppIntegrations {
|
|
|
1282
1383
|
export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
|
|
1283
1384
|
const access = await getVaultAccessSettings();
|
|
1284
1385
|
const agents = await discoverAgents("dispatch");
|
|
1285
|
-
const grants = await listGrants();
|
|
1286
|
-
const secrets = await
|
|
1386
|
+
const grants = (await canManageVault()) ? await listGrants() : [];
|
|
1387
|
+
const secrets = await listSecretOptions();
|
|
1287
1388
|
|
|
1288
1389
|
const secretByKey = new Map(secrets.map((s) => [s.credentialKey, s]));
|
|
1289
1390
|
|
|
@@ -1366,9 +1467,10 @@ export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
|
|
|
1366
1467
|
// ─── Vault Overview (for dashboard) ──────────────────────────────
|
|
1367
1468
|
|
|
1368
1469
|
export async function listVaultOverview() {
|
|
1470
|
+
const isAdmin = await canManageVault();
|
|
1369
1471
|
const [secrets, grants, requests, access] = await Promise.all([
|
|
1370
|
-
|
|
1371
|
-
listGrants(),
|
|
1472
|
+
listSecretOptions(),
|
|
1473
|
+
isAdmin ? listGrants() : Promise.resolve([]),
|
|
1372
1474
|
listRequests(),
|
|
1373
1475
|
getVaultAccessSettings(),
|
|
1374
1476
|
]);
|
|
@@ -1378,8 +1480,12 @@ export async function listVaultOverview() {
|
|
|
1378
1480
|
accessMode: access.mode,
|
|
1379
1481
|
secretCount: secrets.length,
|
|
1380
1482
|
activeGrantCount:
|
|
1381
|
-
access.mode === "all-apps"
|
|
1382
|
-
|
|
1483
|
+
access.mode === "all-apps"
|
|
1484
|
+
? secrets.length
|
|
1485
|
+
: isAdmin
|
|
1486
|
+
? manualGrantCount
|
|
1487
|
+
: 0,
|
|
1488
|
+
manualGrantCount: isAdmin ? manualGrantCount : 0,
|
|
1383
1489
|
pendingRequestCount: requests.filter((r) => r.status === "pending").length,
|
|
1384
1490
|
};
|
|
1385
1491
|
}
|