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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight per-IP API rate limiter for HTTP/TCP requests.
|
|
3
|
+
*
|
|
4
|
+
* Uses a fixed time window with in-memory counters. Intended to mitigate
|
|
5
|
+
* accidental or malicious request floods against the browser-facing API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ApiRateLimiterOptions {
|
|
9
|
+
maxRequests: number;
|
|
10
|
+
windowMs: number;
|
|
11
|
+
cleanupIntervalMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ApiRateLimitResult {
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
retryAfterMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ApiWindow {
|
|
20
|
+
count: number;
|
|
21
|
+
windowStart: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ApiRateLimiter {
|
|
25
|
+
private windows = new Map<string, ApiWindow>();
|
|
26
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
private readonly cleanupIntervalMs: number;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly options: ApiRateLimiterOptions) {
|
|
30
|
+
this.cleanupIntervalMs = options.cleanupIntervalMs ?? Math.max(5000, options.windowMs);
|
|
31
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), this.cleanupIntervalMs);
|
|
32
|
+
if (this.cleanupTimer.unref) {
|
|
33
|
+
this.cleanupTimer.unref();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
consume(key: string): ApiRateLimitResult {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const existing = this.windows.get(key);
|
|
40
|
+
|
|
41
|
+
if (!existing || now - existing.windowStart >= this.options.windowMs) {
|
|
42
|
+
this.windows.set(key, { count: 1, windowStart: now });
|
|
43
|
+
return { allowed: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (existing.count >= this.options.maxRequests) {
|
|
47
|
+
return {
|
|
48
|
+
allowed: false,
|
|
49
|
+
retryAfterMs: existing.windowStart + this.options.windowMs - now,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
existing.count += 1;
|
|
54
|
+
return { allowed: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private cleanup(): void {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const [key, windowState] of this.windows) {
|
|
60
|
+
if (now - windowState.windowStart >= this.options.windowMs) {
|
|
61
|
+
this.windows.delete(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy(): void {
|
|
67
|
+
if (this.cleanupTimer) {
|
|
68
|
+
clearInterval(this.cleanupTimer);
|
|
69
|
+
this.cleanupTimer = null;
|
|
70
|
+
}
|
|
71
|
+
this.windows.clear();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication middleware for HTTP requests.
|
|
3
|
+
*
|
|
4
|
+
* Unix socket requests bypass auth (trusted local access).
|
|
5
|
+
* HTTP/TCP requests require a valid session token.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isIP } from "node:net";
|
|
9
|
+
import { hashToken } from "./authService";
|
|
10
|
+
|
|
11
|
+
/** Routes that do not require authentication */
|
|
12
|
+
const PUBLIC_ROUTES: Array<{ method: string; pattern: RegExp }> = [
|
|
13
|
+
{ method: "GET", pattern: /^\/health$/ },
|
|
14
|
+
{ method: "GET", pattern: /^\/version$/ },
|
|
15
|
+
{ method: "GET", pattern: /^\/auth\/status$/ },
|
|
16
|
+
{ method: "POST", pattern: /^\/auth\/setup$/ },
|
|
17
|
+
{ method: "POST", pattern: /^\/auth\/login$/ },
|
|
18
|
+
{ method: "POST", pattern: /^\/hooks\/claude$/ },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function isPublicRoute(method: string, pathname: string): boolean {
|
|
22
|
+
return PUBLIC_ROUTES.some((r) => r.method === method && r.pattern.test(pathname));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract session token from request.
|
|
27
|
+
* Checks Authorization header first, then cookie.
|
|
28
|
+
*/
|
|
29
|
+
export function extractToken(req: Request): string | null {
|
|
30
|
+
// 1. Bearer token (must be 64-char hex)
|
|
31
|
+
const authHeader = req.headers.get("Authorization");
|
|
32
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
33
|
+
const token = authHeader.slice(7);
|
|
34
|
+
if (/^[a-f0-9]{64}$/.test(token)) {
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Cookie
|
|
40
|
+
const cookieHeader = req.headers.get("Cookie");
|
|
41
|
+
if (cookieHeader) {
|
|
42
|
+
const match = cookieHeader.match(/codepiper_session=([a-f0-9]{64})/);
|
|
43
|
+
const cookieToken = match?.[1];
|
|
44
|
+
if (cookieToken) return cookieToken;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract token and hash it for DB lookup.
|
|
52
|
+
*/
|
|
53
|
+
export function extractAndHashToken(req: Request): string | null {
|
|
54
|
+
const token = extractToken(req);
|
|
55
|
+
if (!token) return null;
|
|
56
|
+
return hashToken(token);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract onboarding token from request cookies.
|
|
61
|
+
*/
|
|
62
|
+
export function extractOnboardingToken(req: Request): string | null {
|
|
63
|
+
const cookieHeader = req.headers.get("Cookie");
|
|
64
|
+
if (!cookieHeader) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const match = cookieHeader.match(/codepiper_onboarding=([a-f0-9]{64})/);
|
|
69
|
+
const token = match?.[1];
|
|
70
|
+
if (!token) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract onboarding token and hash it for DB lookup.
|
|
78
|
+
*/
|
|
79
|
+
export function extractAndHashOnboardingToken(req: Request): string | null {
|
|
80
|
+
const token = extractOnboardingToken(req);
|
|
81
|
+
if (!token) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return hashToken(token);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build Set-Cookie header for session token.
|
|
89
|
+
* Includes Secure flag when served over HTTPS to prevent cookie leakage.
|
|
90
|
+
*/
|
|
91
|
+
export function buildSessionCookie(token: string, maxAgeSeconds: number, isSecure = false): string {
|
|
92
|
+
const flags = [
|
|
93
|
+
`codepiper_session=${token}`,
|
|
94
|
+
"HttpOnly",
|
|
95
|
+
"SameSite=Strict",
|
|
96
|
+
"Path=/",
|
|
97
|
+
`Max-Age=${maxAgeSeconds}`,
|
|
98
|
+
];
|
|
99
|
+
if (isSecure) {
|
|
100
|
+
flags.push("Secure");
|
|
101
|
+
}
|
|
102
|
+
return flags.join("; ");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build Set-Cookie header for temporary onboarding token.
|
|
107
|
+
*/
|
|
108
|
+
export function buildOnboardingCookie(
|
|
109
|
+
token: string,
|
|
110
|
+
maxAgeSeconds: number,
|
|
111
|
+
isSecure = false
|
|
112
|
+
): string {
|
|
113
|
+
const flags = [
|
|
114
|
+
`codepiper_onboarding=${token}`,
|
|
115
|
+
"HttpOnly",
|
|
116
|
+
"SameSite=Strict",
|
|
117
|
+
"Path=/",
|
|
118
|
+
`Max-Age=${maxAgeSeconds}`,
|
|
119
|
+
];
|
|
120
|
+
if (isSecure) {
|
|
121
|
+
flags.push("Secure");
|
|
122
|
+
}
|
|
123
|
+
return flags.join("; ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build Set-Cookie header to clear the session cookie.
|
|
128
|
+
*/
|
|
129
|
+
export function buildClearSessionCookie(isSecure = false): string {
|
|
130
|
+
const flags = ["codepiper_session=", "HttpOnly", "SameSite=Strict", "Path=/", "Max-Age=0"];
|
|
131
|
+
if (isSecure) {
|
|
132
|
+
flags.push("Secure");
|
|
133
|
+
}
|
|
134
|
+
return flags.join("; ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build Set-Cookie header to clear the onboarding cookie.
|
|
139
|
+
*/
|
|
140
|
+
export function buildClearOnboardingCookie(isSecure = false): string {
|
|
141
|
+
const flags = ["codepiper_onboarding=", "HttpOnly", "SameSite=Strict", "Path=/", "Max-Age=0"];
|
|
142
|
+
if (isSecure) {
|
|
143
|
+
flags.push("Secure");
|
|
144
|
+
}
|
|
145
|
+
return flags.join("; ");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Determine whether auth cookies should include the Secure attribute.
|
|
150
|
+
*
|
|
151
|
+
* Secure cookies are required for HTTPS deployments. Local HTTP development remains
|
|
152
|
+
* supported by default, and operators can force secure cookies via env flag.
|
|
153
|
+
*/
|
|
154
|
+
export function shouldUseSecureCookies(req: Request): boolean {
|
|
155
|
+
if (process.env.CODEPIPER_FORCE_SECURE_COOKIES === "1") {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (process.env.CODEPIPER_TRUST_PROXY_HEADERS === "1") {
|
|
160
|
+
const forwardedProtoHeader = req.headers.get("X-Forwarded-Proto");
|
|
161
|
+
if (forwardedProtoHeader) {
|
|
162
|
+
const normalized = forwardedProtoHeader.split(",")[0]?.trim().toLowerCase();
|
|
163
|
+
if (normalized === "https") {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (normalized === "http") {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const forwardedHeader = req.headers.get("Forwarded");
|
|
172
|
+
if (forwardedHeader) {
|
|
173
|
+
const protoMatch = forwardedHeader.match(/(?:^|[;,]\s*)proto=(https|http)(?:$|[;,])/i);
|
|
174
|
+
if (protoMatch?.[1]) {
|
|
175
|
+
return protoMatch[1].toLowerCase() === "https";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return new URL(req.url).protocol === "https:";
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Add security headers to a response.
|
|
189
|
+
*/
|
|
190
|
+
export function addSecurityHeaders(response: Response): Response {
|
|
191
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
192
|
+
const scriptSrc = isDevelopment
|
|
193
|
+
? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
|
194
|
+
: "script-src 'self'";
|
|
195
|
+
|
|
196
|
+
const headers = new Headers(response.headers);
|
|
197
|
+
// API responses are real-time data — never cache them
|
|
198
|
+
headers.set("Cache-Control", "no-store");
|
|
199
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
200
|
+
headers.set("X-Frame-Options", "DENY");
|
|
201
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
202
|
+
// Permissions-Policy: restrict access to sensitive browser APIs
|
|
203
|
+
headers.set("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=(), usb=()");
|
|
204
|
+
headers.set(
|
|
205
|
+
"Content-Security-Policy",
|
|
206
|
+
[
|
|
207
|
+
"default-src 'self'",
|
|
208
|
+
// Development mode needs relaxed script policy for HMR and Monaco tooling.
|
|
209
|
+
// Production uses a strict script policy (no inline/eval).
|
|
210
|
+
scriptSrc,
|
|
211
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
212
|
+
"img-src 'self' data: blob:",
|
|
213
|
+
"font-src 'self' data: https://fonts.gstatic.com",
|
|
214
|
+
"connect-src 'self' ws://localhost:* wss://localhost:* ws://127.0.0.1:* wss://127.0.0.1:*",
|
|
215
|
+
"worker-src 'self' blob:",
|
|
216
|
+
"frame-ancestors 'none'",
|
|
217
|
+
"base-uri 'self'",
|
|
218
|
+
"form-action 'self'",
|
|
219
|
+
].join("; ")
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return new Response(response.body, {
|
|
223
|
+
status: response.status,
|
|
224
|
+
statusText: response.statusText,
|
|
225
|
+
headers,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
230
|
+
export const MAX_IMAGE_BODY_SIZE = 10 * 1024 * 1024 + 1024; // 10MB + multipart overhead
|
|
231
|
+
|
|
232
|
+
function normalizeProxyIpCandidate(raw: string | null): string | null {
|
|
233
|
+
if (!raw) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const trimmed = raw.trim();
|
|
238
|
+
if (!trimmed) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (isIP(trimmed) !== 0) {
|
|
243
|
+
return trimmed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const bracketIpv6 = trimmed.match(/^\[([^\]]+)\](?::\d+)?$/);
|
|
247
|
+
if (bracketIpv6?.[1] && isIP(bracketIpv6[1]) !== 0) {
|
|
248
|
+
return bracketIpv6[1];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ipv4WithPort = trimmed.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
|
|
252
|
+
if (ipv4WithPort?.[1] && isIP(ipv4WithPort[1]) !== 0) {
|
|
253
|
+
return ipv4WithPort[1];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if request body is too large.
|
|
261
|
+
*/
|
|
262
|
+
export function isBodyTooLarge(req: Request): boolean {
|
|
263
|
+
const contentLength = Number(req.headers.get("content-length") || 0);
|
|
264
|
+
return contentLength > MAX_BODY_SIZE;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if image upload body is too large (higher limit for multipart image data).
|
|
269
|
+
*/
|
|
270
|
+
export function isImageBodyTooLarge(req: Request): boolean {
|
|
271
|
+
const contentLength = Number(req.headers.get("content-length") || 0);
|
|
272
|
+
return contentLength > MAX_IMAGE_BODY_SIZE;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extract client IP from request.
|
|
277
|
+
* Only trusts X-Forwarded-For when explicitly configured with a trusted proxy.
|
|
278
|
+
* Falls back to connection IP or "127.0.0.1".
|
|
279
|
+
*/
|
|
280
|
+
export function getClientIp(
|
|
281
|
+
req: Request,
|
|
282
|
+
_server?: { requestIP?: (req: Request) => { address: string } | null }
|
|
283
|
+
): string {
|
|
284
|
+
if (process.env.CODEPIPER_TRUST_PROXY_HEADERS === "1") {
|
|
285
|
+
const xForwardedFor = req.headers.get("X-Forwarded-For");
|
|
286
|
+
const forwardedCandidate = xForwardedFor ? (xForwardedFor.split(",")[0] ?? null) : null;
|
|
287
|
+
const forwardedIp = normalizeProxyIpCandidate(forwardedCandidate);
|
|
288
|
+
if (forwardedIp) {
|
|
289
|
+
return forwardedIp;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const xRealIp = normalizeProxyIpCandidate(req.headers.get("X-Real-IP"));
|
|
293
|
+
if (xRealIp) {
|
|
294
|
+
return xRealIp;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Use the actual connection IP when available (Bun.serve provides this)
|
|
299
|
+
if (_server?.requestIP) {
|
|
300
|
+
const info = _server.requestIP(req);
|
|
301
|
+
if (info?.address) return info.address;
|
|
302
|
+
}
|
|
303
|
+
// Fallback — do NOT trust X-Forwarded-For by default (spoofable)
|
|
304
|
+
return "127.0.0.1";
|
|
305
|
+
}
|