codepiper 0.1.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/.env.example +28 -0
- package/CHANGELOG.md +10 -0
- package/LEGAL_NOTICE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/package.json +90 -0
- package/packages/cli/package.json +13 -0
- package/packages/cli/src/commands/analytics.ts +157 -0
- package/packages/cli/src/commands/attach.ts +299 -0
- package/packages/cli/src/commands/audit.ts +50 -0
- package/packages/cli/src/commands/auth.ts +261 -0
- package/packages/cli/src/commands/daemon.ts +162 -0
- package/packages/cli/src/commands/doctor.ts +303 -0
- package/packages/cli/src/commands/env-set.ts +162 -0
- package/packages/cli/src/commands/hook-forward.ts +268 -0
- package/packages/cli/src/commands/keys.ts +77 -0
- package/packages/cli/src/commands/kill.ts +19 -0
- package/packages/cli/src/commands/logs.ts +419 -0
- package/packages/cli/src/commands/model.ts +172 -0
- package/packages/cli/src/commands/policy-set.ts +185 -0
- package/packages/cli/src/commands/policy.ts +227 -0
- package/packages/cli/src/commands/providers.ts +114 -0
- package/packages/cli/src/commands/resize.ts +34 -0
- package/packages/cli/src/commands/send.ts +184 -0
- package/packages/cli/src/commands/sessions.ts +202 -0
- package/packages/cli/src/commands/slash.ts +92 -0
- package/packages/cli/src/commands/start.ts +243 -0
- package/packages/cli/src/commands/stop.ts +19 -0
- package/packages/cli/src/commands/tail.ts +137 -0
- package/packages/cli/src/commands/workflow.ts +786 -0
- package/packages/cli/src/commands/workspace.ts +127 -0
- package/packages/cli/src/lib/api.ts +78 -0
- package/packages/cli/src/lib/args.ts +72 -0
- package/packages/cli/src/lib/format.ts +93 -0
- package/packages/cli/src/main.ts +563 -0
- package/packages/core/package.json +7 -0
- package/packages/core/src/config.ts +30 -0
- package/packages/core/src/errors.ts +38 -0
- package/packages/core/src/eventBus.ts +56 -0
- package/packages/core/src/eventBusAdapter.ts +143 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/sqliteEventBus.ts +336 -0
- package/packages/core/src/types.ts +63 -0
- package/packages/daemon/package.json +11 -0
- package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
- package/packages/daemon/src/api/authRoutes.ts +344 -0
- package/packages/daemon/src/api/bodyLimit.ts +133 -0
- package/packages/daemon/src/api/envSetRoutes.ts +170 -0
- package/packages/daemon/src/api/gitRoutes.ts +409 -0
- package/packages/daemon/src/api/hooks.ts +588 -0
- package/packages/daemon/src/api/inputPolicy.ts +249 -0
- package/packages/daemon/src/api/notificationRoutes.ts +532 -0
- package/packages/daemon/src/api/policyRoutes.ts +234 -0
- package/packages/daemon/src/api/policySetRoutes.ts +445 -0
- package/packages/daemon/src/api/routeUtils.ts +28 -0
- package/packages/daemon/src/api/routes.ts +1004 -0
- package/packages/daemon/src/api/server.ts +1388 -0
- package/packages/daemon/src/api/settingsRoutes.ts +367 -0
- package/packages/daemon/src/api/sqliteErrors.ts +47 -0
- package/packages/daemon/src/api/stt.ts +143 -0
- package/packages/daemon/src/api/terminalRoutes.ts +200 -0
- package/packages/daemon/src/api/validation.ts +287 -0
- package/packages/daemon/src/api/validationRoutes.ts +174 -0
- package/packages/daemon/src/api/workflowRoutes.ts +567 -0
- package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
- package/packages/daemon/src/api/ws.ts +1588 -0
- package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
- package/packages/daemon/src/auth/authMiddleware.ts +305 -0
- package/packages/daemon/src/auth/authService.ts +496 -0
- package/packages/daemon/src/auth/rateLimiter.ts +137 -0
- package/packages/daemon/src/config/pricing.ts +79 -0
- package/packages/daemon/src/crypto/encryption.ts +196 -0
- package/packages/daemon/src/db/db.ts +2745 -0
- package/packages/daemon/src/db/index.ts +16 -0
- package/packages/daemon/src/db/migrations.ts +182 -0
- package/packages/daemon/src/db/policyDb.ts +349 -0
- package/packages/daemon/src/db/schema.sql +408 -0
- package/packages/daemon/src/db/workflowDb.ts +464 -0
- package/packages/daemon/src/git/gitUtils.ts +544 -0
- package/packages/daemon/src/index.ts +6 -0
- package/packages/daemon/src/main.ts +525 -0
- package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
- package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
- package/packages/daemon/src/providers/registry.ts +111 -0
- package/packages/daemon/src/providers/types.ts +82 -0
- package/packages/daemon/src/sessions/auditLogger.ts +103 -0
- package/packages/daemon/src/sessions/policyEngine.ts +165 -0
- package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
- package/packages/daemon/src/sessions/policyTypes.ts +94 -0
- package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
- package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
- package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
- package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
- package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
- package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
- package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
- package/packages/daemon/src/workflows/contextManager.ts +83 -0
- package/packages/daemon/src/workflows/index.ts +31 -0
- package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
- package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
- package/packages/daemon/src/workflows/workflowParser.ts +217 -0
- package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
- package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
- package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
- package/packages/providers/claude-code/package.json +11 -0
- package/packages/providers/claude-code/src/index.ts +7 -0
- package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
- package/packages/providers/claude-code/src/provider.ts +311 -0
- package/packages/web/dist/android-chrome-192x192.png +0 -0
- package/packages/web/dist/android-chrome-512x512.png +0 -0
- package/packages/web/dist/apple-touch-icon.png +0 -0
- package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
- package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
- package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
- package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
- package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
- package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
- package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
- package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
- package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
- package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
- package/packages/web/dist/assets/index-hgphORiw.js +204 -0
- package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
- package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
- package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
- package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
- package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
- package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
- package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
- package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
- package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
- package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
- package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
- package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
- package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/icon.svg +1 -0
- package/packages/web/dist/index.html +29 -0
- package/packages/web/dist/manifest.json +29 -0
- package/packages/web/dist/og-image.png +0 -0
- package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
- package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
- package/packages/web/dist/originals/apple-touch-icon.png +0 -0
- package/packages/web/dist/originals/favicon.ico +0 -0
- package/packages/web/dist/piper.svg +1 -0
- package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
- package/packages/web/dist/sw.js +257 -0
- package/scripts/postinstall-link-workspaces.mjs +58 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy API route handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PolicyRule } from "../db/policyDb";
|
|
6
|
+
import { createPolicyDb } from "../db/policyDb";
|
|
7
|
+
import type { RouteContext } from "./routes";
|
|
8
|
+
import { jsonError, parseJsonBody } from "./routeUtils";
|
|
9
|
+
import {
|
|
10
|
+
getErrorMessage,
|
|
11
|
+
isSqliteForeignKeyConstraintError,
|
|
12
|
+
isSqliteUniqueConstraintError,
|
|
13
|
+
} from "./sqliteErrors";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GET /policies - List all policies
|
|
17
|
+
*/
|
|
18
|
+
export async function handleListPolicies(req: Request, ctx: RouteContext): Promise<Response> {
|
|
19
|
+
const policyDb = createPolicyDb(ctx.db);
|
|
20
|
+
|
|
21
|
+
// Parse query parameters
|
|
22
|
+
const url = new URL(req.url);
|
|
23
|
+
const enabledParam = url.searchParams.get("enabled");
|
|
24
|
+
const sessionIdParam = url.searchParams.get("sessionId");
|
|
25
|
+
|
|
26
|
+
const options: any = {};
|
|
27
|
+
|
|
28
|
+
if (enabledParam !== null) {
|
|
29
|
+
options.enabled = enabledParam === "true";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sessionIdParam !== null) {
|
|
33
|
+
options.sessionId = sessionIdParam === "null" ? null : sessionIdParam;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const policies = policyDb.getPolicies(options);
|
|
37
|
+
|
|
38
|
+
return Response.json({ policies });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* POST /policies - Create a new policy
|
|
43
|
+
*/
|
|
44
|
+
export async function handleCreatePolicy(req: Request, ctx: RouteContext): Promise<Response> {
|
|
45
|
+
const parsed = await parseJsonBody<Record<string, unknown>>(req);
|
|
46
|
+
if (!parsed.ok) {
|
|
47
|
+
return parsed.response;
|
|
48
|
+
}
|
|
49
|
+
const body = parsed.body as any;
|
|
50
|
+
|
|
51
|
+
// Validate required fields
|
|
52
|
+
if (!body.id) {
|
|
53
|
+
return jsonError(400, "Missing required field: id");
|
|
54
|
+
}
|
|
55
|
+
if (!body.name) {
|
|
56
|
+
return jsonError(400, "Missing required field: name");
|
|
57
|
+
}
|
|
58
|
+
if (body.enabled === undefined) {
|
|
59
|
+
return jsonError(400, "Missing required field: enabled");
|
|
60
|
+
}
|
|
61
|
+
if (body.priority === undefined) {
|
|
62
|
+
return jsonError(400, "Missing required field: priority");
|
|
63
|
+
}
|
|
64
|
+
if (body.sessionId === undefined) {
|
|
65
|
+
return jsonError(400, "Missing required field: sessionId");
|
|
66
|
+
}
|
|
67
|
+
if (!body.rules) {
|
|
68
|
+
return jsonError(400, "Missing required field: rules");
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(body.rules)) {
|
|
71
|
+
return jsonError(400, "Field 'rules' must be an array");
|
|
72
|
+
}
|
|
73
|
+
if (typeof body.enabled !== "boolean") {
|
|
74
|
+
return jsonError(400, "Field 'enabled' must be a boolean");
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
typeof body.priority !== "number" ||
|
|
78
|
+
!Number.isInteger(body.priority) ||
|
|
79
|
+
!Number.isFinite(body.priority)
|
|
80
|
+
) {
|
|
81
|
+
return jsonError(400, "Field 'priority' must be an integer");
|
|
82
|
+
}
|
|
83
|
+
if (body.sessionId !== null && typeof body.sessionId !== "string") {
|
|
84
|
+
return jsonError(400, "Field 'sessionId' must be a string or null");
|
|
85
|
+
}
|
|
86
|
+
if (body.sessionId !== null && !ctx.db.getSession(body.sessionId)) {
|
|
87
|
+
return jsonError(404, `Session not found: ${body.sessionId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const policyDb = createPolicyDb(ctx.db);
|
|
92
|
+
|
|
93
|
+
policyDb.createPolicy({
|
|
94
|
+
id: body.id,
|
|
95
|
+
name: body.name,
|
|
96
|
+
description: body.description,
|
|
97
|
+
enabled: body.enabled,
|
|
98
|
+
priority: body.priority,
|
|
99
|
+
sessionId: body.sessionId,
|
|
100
|
+
rules: body.rules as PolicyRule[],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const policy = policyDb.getPolicy(body.id);
|
|
104
|
+
|
|
105
|
+
return Response.json({ policy }, { status: 201 });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isSqliteUniqueConstraintError(error)) {
|
|
108
|
+
return jsonError(409, `Policy already exists: ${body.id}`);
|
|
109
|
+
}
|
|
110
|
+
if (isSqliteForeignKeyConstraintError(error)) {
|
|
111
|
+
return jsonError(422, "Policy references a missing related record");
|
|
112
|
+
}
|
|
113
|
+
return jsonError(500, getErrorMessage(error));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* GET /policies/:id - Get a specific policy
|
|
119
|
+
*/
|
|
120
|
+
export async function handleGetPolicy(
|
|
121
|
+
_req: Request,
|
|
122
|
+
ctx: RouteContext,
|
|
123
|
+
policyId: string
|
|
124
|
+
): Promise<Response> {
|
|
125
|
+
const policyDb = createPolicyDb(ctx.db);
|
|
126
|
+
const policy = policyDb.getPolicy(policyId);
|
|
127
|
+
|
|
128
|
+
if (!policy) {
|
|
129
|
+
return Response.json({ error: `Policy not found: ${policyId}` }, { status: 404 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Response.json({ policy });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* PUT /policies/:id - Update a policy
|
|
137
|
+
*/
|
|
138
|
+
export async function handleUpdatePolicy(
|
|
139
|
+
req: Request,
|
|
140
|
+
ctx: RouteContext,
|
|
141
|
+
policyId: string
|
|
142
|
+
): Promise<Response> {
|
|
143
|
+
const parsed = await parseJsonBody<Record<string, unknown>>(req);
|
|
144
|
+
if (!parsed.ok) {
|
|
145
|
+
return parsed.response;
|
|
146
|
+
}
|
|
147
|
+
const body = parsed.body as any;
|
|
148
|
+
|
|
149
|
+
// Validate rules if provided
|
|
150
|
+
if (body.rules !== undefined && !Array.isArray(body.rules)) {
|
|
151
|
+
return jsonError(400, "Field 'rules' must be an array");
|
|
152
|
+
}
|
|
153
|
+
if (body.name !== undefined && typeof body.name !== "string") {
|
|
154
|
+
return jsonError(400, "Field 'name' must be a string");
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
body.description !== undefined &&
|
|
158
|
+
body.description !== null &&
|
|
159
|
+
typeof body.description !== "string"
|
|
160
|
+
) {
|
|
161
|
+
return jsonError(400, "Field 'description' must be a string or null");
|
|
162
|
+
}
|
|
163
|
+
if (body.enabled !== undefined && typeof body.enabled !== "boolean") {
|
|
164
|
+
return jsonError(400, "Field 'enabled' must be a boolean");
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
body.priority !== undefined &&
|
|
168
|
+
(typeof body.priority !== "number" ||
|
|
169
|
+
!Number.isInteger(body.priority) ||
|
|
170
|
+
!Number.isFinite(body.priority))
|
|
171
|
+
) {
|
|
172
|
+
return jsonError(400, "Field 'priority' must be an integer");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const policyDb = createPolicyDb(ctx.db);
|
|
176
|
+
|
|
177
|
+
// Check if policy exists
|
|
178
|
+
const existing = policyDb.getPolicy(policyId);
|
|
179
|
+
if (!existing) {
|
|
180
|
+
return jsonError(404, `Policy not found: ${policyId}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const updates: any = {};
|
|
185
|
+
|
|
186
|
+
if (body.name !== undefined) {
|
|
187
|
+
updates.name = body.name;
|
|
188
|
+
}
|
|
189
|
+
if (body.description !== undefined) {
|
|
190
|
+
updates.description = body.description;
|
|
191
|
+
}
|
|
192
|
+
if (body.enabled !== undefined) {
|
|
193
|
+
updates.enabled = body.enabled;
|
|
194
|
+
}
|
|
195
|
+
if (body.priority !== undefined) {
|
|
196
|
+
updates.priority = body.priority;
|
|
197
|
+
}
|
|
198
|
+
if (body.rules !== undefined) {
|
|
199
|
+
updates.rules = body.rules;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
policyDb.updatePolicy(policyId, updates);
|
|
203
|
+
|
|
204
|
+
const policy = policyDb.getPolicy(policyId);
|
|
205
|
+
|
|
206
|
+
return Response.json({ policy });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return jsonError(500, getErrorMessage(error));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* DELETE /policies/:id - Delete a policy
|
|
214
|
+
*/
|
|
215
|
+
export async function handleDeletePolicy(
|
|
216
|
+
_req: Request,
|
|
217
|
+
ctx: RouteContext,
|
|
218
|
+
policyId: string
|
|
219
|
+
): Promise<Response> {
|
|
220
|
+
const policyDb = createPolicyDb(ctx.db);
|
|
221
|
+
|
|
222
|
+
// Check if policy exists
|
|
223
|
+
const existing = policyDb.getPolicy(policyId);
|
|
224
|
+
if (!existing) {
|
|
225
|
+
return jsonError(404, `Policy not found: ${policyId}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
policyDb.deletePolicy(policyId);
|
|
230
|
+
return Response.json({ success: true });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return jsonError(500, getErrorMessage(error));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Set API route handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RouteContext } from "./routes";
|
|
6
|
+
import { hasAnyDefinedField, jsonError, parseJsonBody } from "./routeUtils";
|
|
7
|
+
import {
|
|
8
|
+
getErrorMessage,
|
|
9
|
+
isSqliteForeignKeyConstraintError,
|
|
10
|
+
isSqliteUniqueConstraintError,
|
|
11
|
+
} from "./sqliteErrors";
|
|
12
|
+
|
|
13
|
+
const VALID_POLICY_DECISIONS = new Set(["allow", "deny", "ask"]);
|
|
14
|
+
|
|
15
|
+
function setContainsPolicy(ctx: RouteContext, setId: string, policyId: string): boolean {
|
|
16
|
+
return ctx.db.getPolicySetMembers(setId).some((policy) => policy.id === policyId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sessionHasPolicySet(ctx: RouteContext, sessionId: string, setId: string): boolean {
|
|
20
|
+
return ctx.db.getSessionPolicySets(sessionId).some((set) => set.id === setId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /policy-sets - List all policy sets
|
|
25
|
+
*/
|
|
26
|
+
export async function handleListPolicySets(_req: Request, ctx: RouteContext): Promise<Response> {
|
|
27
|
+
const policySets = ctx.db.listPolicySets();
|
|
28
|
+
return Response.json({ policySets });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* POST /policy-sets - Create a new policy set
|
|
33
|
+
*/
|
|
34
|
+
export async function handleCreatePolicySet(req: Request, ctx: RouteContext): Promise<Response> {
|
|
35
|
+
const parsed = await parseJsonBody(req);
|
|
36
|
+
if (!parsed.ok) {
|
|
37
|
+
return parsed.response;
|
|
38
|
+
}
|
|
39
|
+
const body = parsed.body as any;
|
|
40
|
+
|
|
41
|
+
if (!body.id) {
|
|
42
|
+
return jsonError(400, "Missing required field: id");
|
|
43
|
+
}
|
|
44
|
+
if (!body.name) {
|
|
45
|
+
return jsonError(400, "Missing required field: name");
|
|
46
|
+
}
|
|
47
|
+
if (typeof body.id !== "string") {
|
|
48
|
+
return jsonError(400, "Field 'id' must be a string");
|
|
49
|
+
}
|
|
50
|
+
if (typeof body.name !== "string") {
|
|
51
|
+
return jsonError(400, "Field 'name' must be a string");
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
body.description !== undefined &&
|
|
55
|
+
body.description !== null &&
|
|
56
|
+
typeof body.description !== "string"
|
|
57
|
+
) {
|
|
58
|
+
return jsonError(400, "Field 'description' must be a string or null");
|
|
59
|
+
}
|
|
60
|
+
if (body.isDefault !== undefined && typeof body.isDefault !== "boolean") {
|
|
61
|
+
return jsonError(400, "Field 'isDefault' must be a boolean");
|
|
62
|
+
}
|
|
63
|
+
if (body.policyIds !== undefined) {
|
|
64
|
+
if (!Array.isArray(body.policyIds)) {
|
|
65
|
+
return jsonError(400, "Field 'policyIds' must be an array of strings");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const policyId of body.policyIds) {
|
|
69
|
+
if (typeof policyId !== "string") {
|
|
70
|
+
return jsonError(400, "Field 'policyIds' must be an array of strings");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const missingPolicyIds = [...new Set<string>(body.policyIds)].filter(
|
|
75
|
+
(policyId) => !ctx.db.getPolicy(policyId)
|
|
76
|
+
);
|
|
77
|
+
if (missingPolicyIds.length > 0) {
|
|
78
|
+
return jsonError(422, "One or more referenced policies were not found", {
|
|
79
|
+
missingPolicyIds,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
ctx.db.createPolicySet({
|
|
86
|
+
id: body.id,
|
|
87
|
+
name: body.name,
|
|
88
|
+
description: body.description,
|
|
89
|
+
isDefault: body.isDefault ?? false,
|
|
90
|
+
policyIds: body.policyIds,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const policySet = ctx.db.getPolicySet(body.id);
|
|
94
|
+
const members = ctx.db.getPolicySetMembers(body.id);
|
|
95
|
+
|
|
96
|
+
return Response.json({ policySet: { ...policySet, policies: members } }, { status: 201 });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (isSqliteUniqueConstraintError(error)) {
|
|
99
|
+
return jsonError(409, `Policy set already exists: ${body.id ?? body.name}`);
|
|
100
|
+
}
|
|
101
|
+
if (isSqliteForeignKeyConstraintError(error)) {
|
|
102
|
+
return jsonError(422, "Policy set references a missing related record");
|
|
103
|
+
}
|
|
104
|
+
return jsonError(500, getErrorMessage(error));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* GET /policy-sets/:id - Get a specific policy set with member policies
|
|
110
|
+
*/
|
|
111
|
+
export async function handleGetPolicySet(
|
|
112
|
+
_req: Request,
|
|
113
|
+
ctx: RouteContext,
|
|
114
|
+
setId: string
|
|
115
|
+
): Promise<Response> {
|
|
116
|
+
const policySet = ctx.db.getPolicySet(setId);
|
|
117
|
+
if (!policySet) {
|
|
118
|
+
return Response.json({ error: `Policy set not found: ${setId}` }, { status: 404 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const members = ctx.db.getPolicySetMembers(setId);
|
|
122
|
+
return Response.json({ policySet: { ...policySet, policies: members } });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* PUT /policy-sets/:id - Update a policy set
|
|
127
|
+
*/
|
|
128
|
+
export async function handleUpdatePolicySet(
|
|
129
|
+
req: Request,
|
|
130
|
+
ctx: RouteContext,
|
|
131
|
+
setId: string
|
|
132
|
+
): Promise<Response> {
|
|
133
|
+
const parsed = await parseJsonBody(req);
|
|
134
|
+
if (!parsed.ok) {
|
|
135
|
+
return parsed.response;
|
|
136
|
+
}
|
|
137
|
+
const body = parsed.body as any;
|
|
138
|
+
|
|
139
|
+
const existing = ctx.db.getPolicySet(setId);
|
|
140
|
+
if (!existing) {
|
|
141
|
+
return jsonError(404, `Policy set not found: ${setId}`);
|
|
142
|
+
}
|
|
143
|
+
if (body.name !== undefined && typeof body.name !== "string") {
|
|
144
|
+
return jsonError(400, "Field 'name' must be a string");
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
body.description !== undefined &&
|
|
148
|
+
body.description !== null &&
|
|
149
|
+
typeof body.description !== "string"
|
|
150
|
+
) {
|
|
151
|
+
return jsonError(400, "Field 'description' must be a string or null");
|
|
152
|
+
}
|
|
153
|
+
if (body.isDefault !== undefined && typeof body.isDefault !== "boolean") {
|
|
154
|
+
return jsonError(400, "Field 'isDefault' must be a boolean");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const hasUpdates = hasAnyDefinedField(body, ["name", "description", "isDefault"]);
|
|
158
|
+
if (!hasUpdates) {
|
|
159
|
+
return jsonError(422, "At least one field must be provided: name, description, or isDefault");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const updates: any = {};
|
|
164
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
165
|
+
if (body.description !== undefined) updates.description = body.description;
|
|
166
|
+
if (body.isDefault !== undefined) updates.isDefault = body.isDefault;
|
|
167
|
+
|
|
168
|
+
ctx.db.updatePolicySet(setId, updates);
|
|
169
|
+
|
|
170
|
+
const policySet = ctx.db.getPolicySet(setId);
|
|
171
|
+
const members = ctx.db.getPolicySetMembers(setId);
|
|
172
|
+
|
|
173
|
+
return Response.json({ policySet: { ...policySet, policies: members } });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (isSqliteUniqueConstraintError(error)) {
|
|
176
|
+
return jsonError(409, `Policy set name already exists: ${body.name}`);
|
|
177
|
+
}
|
|
178
|
+
return jsonError(500, getErrorMessage(error));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* DELETE /policy-sets/:id - Delete a policy set
|
|
184
|
+
*/
|
|
185
|
+
export async function handleDeletePolicySet(
|
|
186
|
+
_req: Request,
|
|
187
|
+
ctx: RouteContext,
|
|
188
|
+
setId: string
|
|
189
|
+
): Promise<Response> {
|
|
190
|
+
const existing = ctx.db.getPolicySet(setId);
|
|
191
|
+
if (!existing) {
|
|
192
|
+
return jsonError(404, `Policy set not found: ${setId}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
ctx.db.deletePolicySet(setId);
|
|
197
|
+
return Response.json({ success: true });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return jsonError(500, getErrorMessage(error));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* POST /policy-sets/:id/policies - Add a policy to a set
|
|
205
|
+
*/
|
|
206
|
+
export async function handleAddPolicyToSet(
|
|
207
|
+
req: Request,
|
|
208
|
+
ctx: RouteContext,
|
|
209
|
+
setId: string
|
|
210
|
+
): Promise<Response> {
|
|
211
|
+
const parsed = await parseJsonBody(req);
|
|
212
|
+
if (!parsed.ok) {
|
|
213
|
+
return parsed.response;
|
|
214
|
+
}
|
|
215
|
+
const body = parsed.body as any;
|
|
216
|
+
|
|
217
|
+
if (!body.policyId) {
|
|
218
|
+
return jsonError(400, "Missing required field: policyId");
|
|
219
|
+
}
|
|
220
|
+
if (typeof body.policyId !== "string") {
|
|
221
|
+
return jsonError(400, "Field 'policyId' must be a string");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const set = ctx.db.getPolicySet(setId);
|
|
225
|
+
if (!set) {
|
|
226
|
+
return jsonError(404, `Policy set not found: ${setId}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const policy = ctx.db.getPolicy(body.policyId);
|
|
230
|
+
if (!policy) {
|
|
231
|
+
return jsonError(404, `Policy not found: ${body.policyId}`);
|
|
232
|
+
}
|
|
233
|
+
if (setContainsPolicy(ctx, setId, body.policyId)) {
|
|
234
|
+
return jsonError(409, `Policy ${body.policyId} is already a member of set ${setId}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
ctx.db.addPolicyToSet(setId, body.policyId);
|
|
239
|
+
return Response.json({ success: true });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (isSqliteForeignKeyConstraintError(error)) {
|
|
242
|
+
return jsonError(422, "Policy set membership references a missing related record");
|
|
243
|
+
}
|
|
244
|
+
return jsonError(500, getErrorMessage(error));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* DELETE /policy-sets/:id/policies/:policyId - Remove a policy from a set
|
|
250
|
+
*/
|
|
251
|
+
export async function handleRemovePolicyFromSet(
|
|
252
|
+
_req: Request,
|
|
253
|
+
ctx: RouteContext,
|
|
254
|
+
setId: string,
|
|
255
|
+
policyId: string
|
|
256
|
+
): Promise<Response> {
|
|
257
|
+
const set = ctx.db.getPolicySet(setId);
|
|
258
|
+
if (!set) {
|
|
259
|
+
return jsonError(404, `Policy set not found: ${setId}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const policy = ctx.db.getPolicy(policyId);
|
|
263
|
+
if (!policy) {
|
|
264
|
+
return jsonError(404, `Policy not found: ${policyId}`);
|
|
265
|
+
}
|
|
266
|
+
if (!setContainsPolicy(ctx, setId, policyId)) {
|
|
267
|
+
return jsonError(404, `Policy ${policyId} is not a member of set ${setId}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
ctx.db.removePolicyFromSet(setId, policyId);
|
|
272
|
+
return Response.json({ success: true });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return jsonError(500, getErrorMessage(error));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* GET /sessions/:id/policy-sets - Get policy sets applied to a session
|
|
280
|
+
*/
|
|
281
|
+
export async function handleGetSessionPolicySets(
|
|
282
|
+
_req: Request,
|
|
283
|
+
ctx: RouteContext,
|
|
284
|
+
sessionId: string
|
|
285
|
+
): Promise<Response> {
|
|
286
|
+
const session = ctx.db.getSession(sessionId);
|
|
287
|
+
if (!session) {
|
|
288
|
+
return jsonError(404, `Session not found: ${sessionId}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const policySets = ctx.db.getSessionPolicySets(sessionId);
|
|
292
|
+
return Response.json({ policySets });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* POST /sessions/:id/policy-sets - Apply a policy set to a session
|
|
297
|
+
*/
|
|
298
|
+
export async function handleApplyPolicySetToSession(
|
|
299
|
+
req: Request,
|
|
300
|
+
ctx: RouteContext,
|
|
301
|
+
sessionId: string
|
|
302
|
+
): Promise<Response> {
|
|
303
|
+
const parsed = await parseJsonBody(req);
|
|
304
|
+
if (!parsed.ok) {
|
|
305
|
+
return parsed.response;
|
|
306
|
+
}
|
|
307
|
+
const body = parsed.body as any;
|
|
308
|
+
|
|
309
|
+
if (!body.policySetId) {
|
|
310
|
+
return jsonError(400, "Missing required field: policySetId");
|
|
311
|
+
}
|
|
312
|
+
if (typeof body.policySetId !== "string") {
|
|
313
|
+
return jsonError(400, "Field 'policySetId' must be a string");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const session = ctx.db.getSession(sessionId);
|
|
317
|
+
if (!session) {
|
|
318
|
+
return jsonError(404, `Session not found: ${sessionId}`);
|
|
319
|
+
}
|
|
320
|
+
const set = ctx.db.getPolicySet(body.policySetId);
|
|
321
|
+
if (!set) {
|
|
322
|
+
return jsonError(404, `Policy set not found: ${body.policySetId}`);
|
|
323
|
+
}
|
|
324
|
+
if (sessionHasPolicySet(ctx, sessionId, body.policySetId)) {
|
|
325
|
+
return jsonError(
|
|
326
|
+
409,
|
|
327
|
+
`Policy set ${body.policySetId} is already applied to session ${sessionId}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
ctx.db.applyPolicySetToSession(sessionId, body.policySetId);
|
|
333
|
+
return Response.json({ success: true });
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (isSqliteForeignKeyConstraintError(error)) {
|
|
336
|
+
return jsonError(422, "Session policy set references a missing related record");
|
|
337
|
+
}
|
|
338
|
+
return jsonError(500, getErrorMessage(error));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* DELETE /sessions/:id/policy-sets/:setId - Remove a policy set from a session
|
|
344
|
+
*/
|
|
345
|
+
export async function handleRemovePolicySetFromSession(
|
|
346
|
+
_req: Request,
|
|
347
|
+
ctx: RouteContext,
|
|
348
|
+
sessionId: string,
|
|
349
|
+
setId: string
|
|
350
|
+
): Promise<Response> {
|
|
351
|
+
const session = ctx.db.getSession(sessionId);
|
|
352
|
+
if (!session) {
|
|
353
|
+
return jsonError(404, `Session not found: ${sessionId}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const set = ctx.db.getPolicySet(setId);
|
|
357
|
+
if (!set) {
|
|
358
|
+
return jsonError(404, `Policy set not found: ${setId}`);
|
|
359
|
+
}
|
|
360
|
+
if (!sessionHasPolicySet(ctx, sessionId, setId)) {
|
|
361
|
+
return jsonError(404, `Policy set ${setId} is not applied to session ${sessionId}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
ctx.db.removePolicySetFromSession(sessionId, setId);
|
|
366
|
+
return Response.json({ success: true });
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return jsonError(500, getErrorMessage(error));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* GET /sessions/:id/effective-policies - Get resolved policies for a session
|
|
374
|
+
*/
|
|
375
|
+
export async function handleGetEffectivePolicies(
|
|
376
|
+
_req: Request,
|
|
377
|
+
ctx: RouteContext,
|
|
378
|
+
sessionId: string
|
|
379
|
+
): Promise<Response> {
|
|
380
|
+
const session = ctx.db.getSession(sessionId);
|
|
381
|
+
if (!session) {
|
|
382
|
+
return jsonError(404, `Session not found: ${sessionId}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const policies = ctx.db.getEffectivePolicies(sessionId);
|
|
386
|
+
return Response.json({ policies });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* GET /policy-decisions - List all policy decisions (audit log)
|
|
391
|
+
*/
|
|
392
|
+
export async function handleListPolicyDecisions(
|
|
393
|
+
req: Request,
|
|
394
|
+
ctx: RouteContext
|
|
395
|
+
): Promise<Response> {
|
|
396
|
+
const url = new URL(req.url);
|
|
397
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
398
|
+
const decisionParam = url.searchParams.get("decision");
|
|
399
|
+
if (decisionParam !== null && !VALID_POLICY_DECISIONS.has(decisionParam)) {
|
|
400
|
+
return jsonError(422, "Query param 'decision' must be one of: allow, deny, ask");
|
|
401
|
+
}
|
|
402
|
+
const decision = decisionParam as "allow" | "deny" | "ask" | null;
|
|
403
|
+
|
|
404
|
+
const limitParam = url.searchParams.get("limit");
|
|
405
|
+
const limit = limitParam ? Number.parseInt(limitParam, 10) : 100;
|
|
406
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 1000) {
|
|
407
|
+
return jsonError(422, "Query param 'limit' must be an integer between 1 and 1000");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (sessionId) {
|
|
411
|
+
const decisions = ctx.db.getPolicyDecisionsBySessionId(sessionId, {
|
|
412
|
+
decision: decision || undefined,
|
|
413
|
+
limit,
|
|
414
|
+
});
|
|
415
|
+
return Response.json({ decisions });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// All decisions across sessions (query raw)
|
|
419
|
+
const bunDb = (ctx.db as any).db;
|
|
420
|
+
let sql = "SELECT * FROM policy_decisions WHERE 1=1";
|
|
421
|
+
const values: unknown[] = [];
|
|
422
|
+
|
|
423
|
+
if (decision) {
|
|
424
|
+
sql += " AND decision = ?";
|
|
425
|
+
values.push(decision);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
sql += " ORDER BY id DESC LIMIT ?";
|
|
429
|
+
values.push(limit);
|
|
430
|
+
|
|
431
|
+
const rows = bunDb.prepare(sql).all(...values) as any[];
|
|
432
|
+
const decisions = rows.map((row: any) => ({
|
|
433
|
+
id: row.id,
|
|
434
|
+
sessionId: row.session_id,
|
|
435
|
+
eventId: row.event_id ?? undefined,
|
|
436
|
+
policyId: row.policy_id ?? undefined,
|
|
437
|
+
toolName: row.tool_name,
|
|
438
|
+
args: row.args_json ? JSON.parse(row.args_json) : undefined,
|
|
439
|
+
decision: row.decision,
|
|
440
|
+
reason: row.reason ?? undefined,
|
|
441
|
+
timestamp: new Date(row.timestamp),
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
return Response.json({ decisions });
|
|
445
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for consistent API route validation and error responses.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function jsonError(
|
|
6
|
+
status: number,
|
|
7
|
+
message: string,
|
|
8
|
+
extra?: Record<string, unknown>
|
|
9
|
+
): Response {
|
|
10
|
+
return Response.json(extra ? { error: message, ...extra } : { error: message }, { status });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ParsedJsonBody<T> = { ok: true; body: T } | { ok: false; response: Response };
|
|
14
|
+
|
|
15
|
+
export async function parseJsonBody<T = any>(req: Request): Promise<ParsedJsonBody<T>> {
|
|
16
|
+
try {
|
|
17
|
+
return { ok: true, body: (await req.json()) as T };
|
|
18
|
+
} catch {
|
|
19
|
+
return { ok: false, response: jsonError(400, "Invalid JSON in request body") };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hasAnyDefinedField(
|
|
24
|
+
body: Record<string, unknown>,
|
|
25
|
+
fields: readonly string[]
|
|
26
|
+
): boolean {
|
|
27
|
+
return fields.some((field) => body[field] !== undefined);
|
|
28
|
+
}
|