co-maintainer 0.4.4 → 0.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -4,6 +4,7 @@ import { type Platform } from "../../util/runtime.ts";
4
4
  /** `undefined` on Linux, otherwise one line naming the platform (a pure
5
5
  * function so it is testable without actually being off Linux). */
6
6
  export declare function platformWarning(os: Platform): string | undefined;
7
+ export declare function resolveTrustProxy(args: string[], env?: (name: string) => string | undefined): boolean;
7
8
  /** `--password=` replaces the stored password. With no flag the stored one is
8
9
  * kept, and a first start generates one, returned so the caller can print it. */
9
10
  export declare function ensureDashboardPassword(args: string[], hasStoredPassword: boolean, store: PasswordStore): Promise<string | undefined>;
@@ -27,6 +27,9 @@ function generatePassword() {
27
27
  crypto.getRandomValues(bytes);
28
28
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
29
29
  }
30
+ export function resolveTrustProxy(args, env = getEnv) {
31
+ return args.includes("--trust-proxy") || env("CM_TRUST_PROXY") === "1";
32
+ }
30
33
  /** `--password=` replaces the stored password. With no flag the stored one is
31
34
  * kept, and a first start generates one, returned so the caller can print it. */
32
35
  export async function ensureDashboardPassword(args, hasStoredPassword, store) {
@@ -168,6 +171,10 @@ export async function runServe(args) {
168
171
  startRemakeScheduler();
169
172
  if (auth.github)
170
173
  console.log(`[serve] GitHub sign-in enabled for ${githubOAuth.allowedUser}`);
174
+ const trustProxy = resolveTrustProxy(args);
175
+ if (trustProxy) {
176
+ console.log("[serve] trusting x-forwarded-for and x-forwarded-proto from a reverse proxy");
177
+ }
171
178
  const inject500 = args.includes("--inject-500") || getEnv("CM_INJECT_500") === "1";
172
179
  if (inject500) {
173
180
  console.log("[serve] --inject-500 is on. Mutating /api requests return 500.");
@@ -177,6 +184,7 @@ export async function runServe(args) {
177
184
  passwordStore,
178
185
  webhookUrl,
179
186
  inject500,
187
+ trustProxy,
180
188
  auth,
181
189
  githubOAuth,
182
190
  });
@@ -6,6 +6,9 @@ export type AppDeps = {
6
6
  passwordStore?: PasswordStore;
7
7
  webhookUrl?: string;
8
8
  secureCookie?: boolean;
9
+ /** Behind a reverse proxy this process trusts, the client address and the
10
+ * scheme come from `x-forwarded-for` and `x-forwarded-proto`. */
11
+ trustProxy?: boolean;
9
12
  inject500?: boolean;
10
13
  /** Defaults to password-only when omitted, matching every caller that
11
14
  * pre-dates GitHub sign-in. */
@@ -4,6 +4,7 @@ import { VERSION } from "../version.js";
4
4
  import { errorResponse } from "./errors.js";
5
5
  import { hasCsrfHeader, login, logout, readSessionToken, sessionCookieHeader, verifySession, } from "./auth.js";
6
6
  import { memoryPasswordStore } from "./auth.js";
7
+ import { clientAddress, forwardedHttps } from "./proxy_headers.js";
7
8
  import { readConfig } from "../config.js";
8
9
  import { listJobs } from "../store/jobs.js";
9
10
  import { handleJobsRoute } from "./api/jobs.js";
@@ -96,8 +97,13 @@ function liveGithubConfig() {
96
97
  export function createApp(deps) {
97
98
  const passwords = deps.passwordStore ?? memoryPasswordStore(deps.password);
98
99
  return {
99
- async fetch(request, remoteAddr = "unknown") {
100
+ async fetch(request, socketAddress = "unknown") {
100
101
  const url = new URL(request.url);
102
+ const remoteAddr = deps.trustProxy
103
+ ? clientAddress(request, socketAddress)
104
+ : socketAddress;
105
+ const secureCookie = Boolean(deps.secureCookie) ||
106
+ (Boolean(deps.trustProxy) && forwardedHttps(request));
101
107
  const { githubApp, webhookSecret } = liveGithubConfig();
102
108
  const mutating = ["POST", "PATCH", "PUT", "DELETE"].includes(request.method);
103
109
  if (url.pathname === "/api/health" && request.method === "GET") {
@@ -106,7 +112,7 @@ export function createApp(deps) {
106
112
  if (url.pathname === "/github/webhook" && request.method === "POST") {
107
113
  return await handleWebhookRequest(request, webhookSecret);
108
114
  }
109
- const page = await handlePageRequest(request, { ...deps, githubApp, passwordStore: passwords }, remoteAddr);
115
+ const page = await handlePageRequest(request, { ...deps, githubApp, passwordStore: passwords, secureCookie }, remoteAddr);
110
116
  if (page)
111
117
  return page;
112
118
  if (url.pathname.startsWith("/api/remote/")) {
@@ -117,7 +123,7 @@ export function createApp(deps) {
117
123
  return errorResponse(403, "csrf", `every mutating request needs the X-Requested-With header`);
118
124
  }
119
125
  if (url.pathname === "/api/login" && request.method === "POST") {
120
- return await handleLogin(request, deps, passwords, remoteAddr);
126
+ return await handleLogin(request, { ...deps, secureCookie }, passwords, remoteAddr);
121
127
  }
122
128
  const session = await requireSession(request);
123
129
  if (!session) {
@@ -0,0 +1,4 @@
1
+ /** The rightmost entry is the one the nearest trusted proxy appended, so a
2
+ * client cannot forge it the way it can the entries to its left. */
3
+ export declare function clientAddress(request: Request, socketAddress: string): string;
4
+ export declare function forwardedHttps(request: Request): boolean;
@@ -0,0 +1,16 @@
1
+ import { isIP } from "node:net";
2
+ /** The rightmost entry is the one the nearest trusted proxy appended, so a
3
+ * client cannot forge it the way it can the entries to its left. */
4
+ export function clientAddress(request, socketAddress) {
5
+ const last = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim();
6
+ return last && isIP(last) ? last : socketAddress;
7
+ }
8
+ export function forwardedHttps(request) {
9
+ const last = request.headers
10
+ .get("x-forwarded-proto")
11
+ ?.split(",")
12
+ .pop()
13
+ ?.trim()
14
+ .toLowerCase();
15
+ return last === "https";
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {