refactor(ssh_command): make it stable@@ -14,7 +14,6 @@ stages:
- git checkout master
- yarn
- yarn build
- -
cached:
- /home/node/build/gitfoss/dist/
- /home/node/build/gitfoss/public/
@@ -18,12 +18,23 @@ COPY ./tsconfig.json /usr/src/app/tsconfig.json
COPY ./yarn.lock /usr/src/app/yarn.lock
# Install dependencies
-RUN apk upgrade --no-cache openssl
+RUN apk add --no-cache curl libcurl curl-dev crystal shards && apk upgrade --no-cache openssl
RUN npm install --global yarn
RUN yarn install
# [local tree] [builder tree]
+COPY ./packages /usr/src/app/packages
+
+# Install Crystal dependencies
+RUN export TRUST_CERT_PATH=`curl-config --ca` && echo -n | openssl s_client -showcerts -connect gitfoss.dev:443 -servername gitfoss.dev 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >> $TRUST_CERT_PATH
+RUN cd /usr/src/app/packages/gitfoss-ci && shards install
+# RUN cd /usr/src/app/packages/gitfoss-ci-runner && shards install
+# RUN GIT_CURL_VERBOSE=1 GIT_TRACE=1 git clone https://gitfoss.dev/ethicdevs/crystal-docker-api.git
+RUN cd /usr/src/app/packages/gitfoss-ssh-command && shards install
+
+# [local tree] [builder tree]
+
COPY ./.git /usr/src/app/.git
COPY ./app /usr/src/app/app
COPY ./db /usr/src/app/db
@@ -47,10 +58,16 @@ RUN yarn test # Test code to ensure it is regression-free (jest)
RUN yarn build:ts # Transpile TypeScript to JavaScript (node)
RUN yarn bundle:islands # Bundle Islands (react-monolith) to ESM/CJS/UMD
+# Crystal build
+RUN crystal build --release ./packages/gitfoss-ci/src/gitfoss-ci.cr
+# RUN crystal build --release ./packages/gitfoss-ci-runner/src/gitfoss-ci-runner.cr
+RUN crystal build --release ./packages/gitfoss-ssh-command/src/ssh-command.cr
+
# [local tree] [builder tree]
COPY ./public /usr/src/app/public
COPY ./app.manifest.json /usr/src/app/app.manifest.json
+COPY ./packages/gitfoss-ssh-command/ssh-command /usr/src/app/bin/ssh-command
# COPY ./dist /usr/src/app/dist
FROM node:slim AS base
@@ -66,7 +83,9 @@ ENV HOST=${HOST}
# Install required dependencies
RUN apt-get update -y && \
apt-get install --no-install-recommends \
- git-core git openssl openssh-server gnupg sudo curl jq ca-certificates wget -y && \
+ git-core git openssl openssh-server \
+ gnupg sudo curl jq ca-certificates \
+ wget -y && \
rm -rf /var/lib/apt/lists/*
# Install Node 20.0.0 from nodesource
@@ -156,10 +175,11 @@ RUN chown git:git git-shell-commands/no-interactive-login
RUN chmod +x git-shell-commands/no-interactive-login
# Add ssh command to force client command
-COPY ./data/ssh_command /usr/bin/
+COPY --from=builder /usr/src/app/bin/ssh-command /usr/bin/ssh_command
+# COPY ./data/ssh_command /usr/bin/
COPY ./data/ssh_command_node /usr/bin/
COPY ./data/http_client.js /usr/bin/
-RUN chmod +x /usr/bin/ssh_command
+# RUN chmod +x /usr/bin/ssh_command
RUN chmod +x /usr/bin/ssh_command_node
RUN touch /opt/ssh_commands.log
@@ -66,9 +66,18 @@ $ docker exec -it <container_id_here> yarn migrate:deploy
```sh
$ docker-compose up -d
$ docker-compose exec web yarn migrate:deploy
-$ docker-compose exec traefik cat /var/log/traefik/traefik.log
```
+We also have a `docker-compose.caddy.yml` and `docker-compose.traefik.yml` for running GitFOSS using Caddy and Traefik reverse-proxies respectively.
+
+### Logs
+
+- `/var/log/gitfoss/git_ssh.log`: Whatever happens on git over ssh
+- `/var/log/gitfoss/git_http.log`: Whatever happens on git over http(s)
+- `/var/log/gitfoss/web.log`: Whatever happens over web ui
+- `/var/log/gitfoss/ci.log`: Whatever happens over CI
+- `/var/log/gitfoss/ci-runner-$n.log`: Whatever happens over CI runner $n
+
## License
This project is licensed under the [MIT](LICENSE) license.
@@ -1,4 +1,4 @@
-// 3rd-party
+// 3rd-party !
import React, { FC, useState } from "react";
import styled, { css } from "styled-components";
// app
@@ -21,7 +21,8 @@ const getRepositoryDetailsView: ReqHandler<
AppRouteParams,
AppRoute.REPOSITORY_DETAILS
> = async (request, reply) => {
- const { orgSlug, repoSlug } = request.params;
+ let { orgSlug, repoSlug } = request.params;
+ repoSlug = repoSlug.replace(/\.git$/, "");
const orgService = makeOrganizationService({ request });
const repoService = makeRepositoryService({ request });
@@ -5,6 +5,7 @@ import { GitServer } from "@ethicdevs/fastify-git-server";
import { AppRoute, AppRouteParams } from "../routes.defs";
import { makeGitServerService } from "../services/gitServer";
+// POST /_ssh/auth - SSH auth request handler
const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
request,
reply,
@@ -1,3 +1,3 @@
-command="ssh_command wnemencha",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGo9GDFyJ8RJ4F+Y4Vxl2bCl8sskRYbiSWItmoBZoYFVR2qAvUyowDY2xlDa9JaG/+zbBB6sUwUr8oC/GdSaV+zp5CTP2RxcXDW2Aup6w1/a4qiilSKORMXBWvIgjyvVvHjG0TiCfeC0PfBbXCO8FhLxP5lgTrl1kVUduM0LR4/gcH3vIrrKbORWtfAC7Bw6z3qc/X9CysPxtQZYu6+AknJ1vwUtLAH2H9cKS8uwaJ5N/k0n8Sc8ANozdpp7EyodA12nFFwvf5oPakTdm5cBnnnEIe2p+GA4nP2DyybmtIR/wttJGMs6Bmz0bXO6AfFdhcGKbzwT2qEGRX5drQj0qUI+gLSZ42/9DsGN7kr2gRDpXG2ATx+c4H2XvR3fqS1cyFq+ZmezK4l32BH/KjQMR1zfgeX2Ky46YxOLQn84PvWILmpzYPLTJ02kXFr0pjofraX2h0E/0Ke2ZBPlOUcaNZOU2dJDYn/B0GVrJ0niopvseYpVXHoTzMPYpr+ReCfAc= admin@Admins-MacBook-Pro.local
-command="ssh_command willi.wonka38",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCbpfyW60ydd3p0xQAozzB2lecE4NG9KyUbd/mRl+EPEOm4pnh3oIqbGLs42QYgKc1H8bkEEddFvnayN+JHJIwsRBSv3YMMsbTRHp6onHgsVHMaO+nNyubONwb7eLfR+5PvZ4eeOwbJpgfLwW2tVpoqLiTRpF8DFijYQePqGqQzFwWXx9niHHb3s1pqDPGjiqlEqbJPT4MyIdHo66M8SJMFL12uIUq33Wm3PRCtPDMs4FdW0fpvQpaBqu6cb71WrH8MDRC5IGT2KC3MD7flVfayUoSsdo3I/zzkom8t34Ee0b7NkQ0jvcsVves/Vz36YCZ/W7pNiKYi18B3sLIxpwI9a9k+kYt5a/5kz5izmeVbiVK2PDk45GN2b0RCfMb3uT3v5MwcJjKf5h52WYVjh0Uzj8AC4ZOtW84BAVrnX4hkBirN6oOYJB8/8pHvtAq5WSFtJnPzz1rL/mOFSsr/HS5Jo8KSsrH8uZHz2QWr6T6PDDGOjiEc44JTPOxqywUXo7Cs5TvicLhqWXdPokTmVftoG0pRpVWi9ctvGc1GRXviEWgvJZyqx/yDdtvrjKWDNjIM3jww1Blrb0R1o/0wvvHFfTf8flvW4SC7POXG0bXp/ANSGnTXIiAQk9JGmU6ntCqVebtNis60Xvbfj5gMGsEv1rhM1cyMcvIfT20ASZo6hw== aremy.dev@gmail.com
-command="ssh_command willi.wonka38",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2gwbtNQtNTLbvyd8/B7k+dzOiYMYKfjbmr5x8DQUZr+K9367D3TzQ6wDMQBWnDu5FIKoKvAFTP3tHhV5p+3GMXqzS/2PCtzV7WHlUtJ7vVxsREoQVrHCS0KZ7+KT5F+daV4VKmnCqr6qfeirvCc1XS/feBnp+P9ctGjMj+PEdkaAv3JaOVpH3bROaokTiTdr4NPF73hM2jAhEo8V7IJwd6e/GlWM4X6rp1QL5cB38utbxIBqq8GYUeX021OLoQoRazXTC27Xwn3i1+w9uqA97KYfSk5pKwrv1+V9JfiHUmaQRB08+nd9hi0dDARJaiar5ipJZ6fioWTM+lqDhEFxBTz9Xfioa1HkpBfh8gW3nsQAJBYUaqUifn5ucJJCu3zZJwTynP4/CGe6g1K7uNjKCHXUx4Lnmw5wcQGCLsSEFc6Pc8QUxjuJ7R5pzvNOTC2omAAoZvHtinj+pHCG6mM4FmL0Wic8qDOLDCeyFS8zHMa2bzulFpwr2tyNcX3xGFT8= xana@blackstorm
+command="/usr/local/bin/git-upload-pack-wrapper wnemencha",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGo9GDFyJ8RJ4F+Y4Vxl2bCl8sskRYbiSWItmoBZoYFVR2qAvUyowDY2xlDa9JaG/+zbBB6sUwUr8oC/GdSaV+zp5CTP2RxcXDW2Aup6w1/a4qiilSKORMXBWvIgjyvVvHjG0TiCfeC0PfBbXCO8FhLxP5lgTrl1kVUduM0LR4/gcH3vIrrKbORWtfAC7Bw6z3qc/X9CysPxtQZYu6+AknJ1vwUtLAH2H9cKS8uwaJ5N/k0n8Sc8ANozdpp7EyodA12nFFwvf5oPakTdm5cBnnnEIe2p+GA4nP2DyybmtIR/wttJGMs6Bmz0bXO6AfFdhcGKbzwT2qEGRX5drQj0qUI+gLSZ42/9DsGN7kr2gRDpXG2ATx+c4H2XvR3fqS1cyFq+ZmezK4l32BH/KjQMR1zfgeX2Ky46YxOLQn84PvWILmpzYPLTJ02kXFr0pjofraX2h0E/0Ke2ZBPlOUcaNZOU2dJDYn/B0GVrJ0niopvseYpVXHoTzMPYpr+ReCfAc= admin@Admins-MacBook-Pro.local
+command="/usr/local/bin/git-upload-pack-wrapper willi.wonka38",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCbpfyW60ydd3p0xQAozzB2lecE4NG9KyUbd/mRl+EPEOm4pnh3oIqbGLs42QYgKc1H8bkEEddFvnayN+JHJIwsRBSv3YMMsbTRHp6onHgsVHMaO+nNyubONwb7eLfR+5PvZ4eeOwbJpgfLwW2tVpoqLiTRpF8DFijYQePqGqQzFwWXx9niHHb3s1pqDPGjiqlEqbJPT4MyIdHo66M8SJMFL12uIUq33Wm3PRCtPDMs4FdW0fpvQpaBqu6cb71WrH8MDRC5IGT2KC3MD7flVfayUoSsdo3I/zzkom8t34Ee0b7NkQ0jvcsVves/Vz36YCZ/W7pNiKYi18B3sLIxpwI9a9k+kYt5a/5kz5izmeVbiVK2PDk45GN2b0RCfMb3uT3v5MwcJjKf5h52WYVjh0Uzj8AC4ZOtW84BAVrnX4hkBirN6oOYJB8/8pHvtAq5WSFtJnPzz1rL/mOFSsr/HS5Jo8KSsrH8uZHz2QWr6T6PDDGOjiEc44JTPOxqywUXo7Cs5TvicLhqWXdPokTmVftoG0pRpVWi9ctvGc1GRXviEWgvJZyqx/yDdtvrjKWDNjIM3jww1Blrb0R1o/0wvvHFfTf8flvW4SC7POXG0bXp/ANSGnTXIiAQk9JGmU6ntCqVebtNis60Xvbfj5gMGsEv1rhM1cyMcvIfT20ASZo6hw== aremy.dev@gmail.com
+command="/usr/local/bin/git-upload-pack-wrapper willi.wonka38",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2gwbtNQtNTLbvyd8/B7k+dzOiYMYKfjbmr5x8DQUZr+K9367D3TzQ6wDMQBWnDu5FIKoKvAFTP3tHhV5p+3GMXqzS/2PCtzV7WHlUtJ7vVxsREoQVrHCS0KZ7+KT5F+daV4VKmnCqr6qfeirvCc1XS/feBnp+P9ctGjMj+PEdkaAv3JaOVpH3bROaokTiTdr4NPF73hM2jAhEo8V7IJwd6e/GlWM4X6rp1QL5cB38utbxIBqq8GYUeX021OLoQoRazXTC27Xwn3i1+w9uqA97KYfSk5pKwrv1+V9JfiHUmaQRB08+nd9hi0dDARJaiar5ipJZ6fioWTM+lqDhEFxBTz9Xfioa1HkpBfh8gW3nsQAJBYUaqUifn5ucJJCu3zZJwTynP4/CGe6g1K7uNjKCHXUx4Lnmw5wcQGCLsSEFc6Pc8QUxjuJ7R5pzvNOTC2omAAoZvHtinj+pHCG6mM4FmL0Wic8qDOLDCeyFS8zHMa2bzulFpwr2tyNcX3xGFT8= xana@blackstorm
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "UserSSHKey" ADD COLUMN "key_fingerprint" TEXT NOT NULL DEFAULT '',
+ADD COLUMN "lastUsedAt" TIMESTAMP(3);
@@ -150,24 +150,25 @@ model User {
}
model UserSSHKey {
- id String @id @default(cuid())
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- // lastUsedAt DateTime?
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ lastUsedAt DateTime?
- user User @relation("ManyUserSSHKeyToOneUser", fields: [userId], references: [id])
- userId String
+ user User @relation("ManyUserSSHKeyToOneUser", fields: [userId], references: [id])
+ userId String
- name String
- key String
+ name String
+ key String
+ key_fingerprint String @default("")
- revoked Boolean @default(false)
+ revoked Boolean @default(false)
}
model Pipeline {
id String @id @default(cuid())
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
name String
status PipelineStatus @default(PENDING)
@@ -14,6 +14,13 @@ services:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data/caddy/data:/data
- ./data/caddy/config:/config
+ # anti-bot/spam (cloudflare-like open-source alternative)
+ anubis:
+ image: ghcr.io/techarohq/anubis:latest
+ # pull_policy: always
+ environment:
+ BIND: ":1337"
+ TARGET: http://gitfoss_web:1337
db:
restart: always
container_name: gitfoss_db
@@ -15,24 +15,24 @@ services:
- 5432:5432
volumes:
- ./data/postgres_data:/var/lib/postgresql/data
- # web:
- # container_name: gitfoss_web
- # build:
- # context: .
- # dockerfile: Dockerfile
- # args:
- # - HOST=0.0.0.0
- # - PORT=1337
- # depends_on:
- # - db
- # ports:
- # - 1337:1337
- # - 22:22
- # volumes:
- # - ./data/gitfoss_repos:/var/lib/gitfoss/repos
- # - ./data/gitfoss_repos:/home/git/repos
- # - ./data/authorized_keys:/home/git/.ssh/authorized_keys
- # env_file: .env.docker
+ web:
+ container_name: gitfoss_web
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ - HOST=0.0.0.0
+ - PORT=1337
+ depends_on:
+ - db
+ ports:
+ - 1337:1337
+ - 22:22
+ volumes:
+ - ./data/gitfoss_repos:/var/lib/gitfoss/repos
+ - ./data/gitfoss_repos:/home/git/repos
+ - ./data/authorized_keys:/home/git/.ssh/authorized_keys
+ env_file: .env.docker
# environment:
# - COOKIE_NAME=gitfoss_ssid
# - COOKIE_SECRET=gitfoss-cookie-secret
@@ -55,7 +55,7 @@
"markdown-to-jsx": "^7.1.7",
"markdown-toc": "^1.2.0",
"mime-db": "^1.52.0",
- "patch-package": "^6.4.7",
+ "patch-package": "^7.0.0",
"pg": "^8.7.3",
"postinstall-postinstall": "^2.1.0",
"prismjs": "^1.29.0",
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+SSH_ORIGINAL_COMMAND="git-receive-pack aaaa" ./ssh-command skz aaaa ffff
@@ -0,0 +1,2 @@
+version: 2.0
+shards: {}
@@ -0,0 +1,21 @@
+name: gitfoss-ssh-command
+version: 0.1.0
+
+authors:
+ - Willi Wonka <willi.wonka38@proton.me>
+
+targets:
+ ssh_command:
+ main: src/ssh-command.cr
+
+crystal: ">= 1.20.0"
+
+license: MIT
+
+# dependencies:
+# crystar:
+# github: naqvis/crystar
+# docker:
+# # github: aca-labs/crystal-docker
+# git: https://gitfoss.dev/ethicdevs/crystal-docker-api.git
+# tag: 0.4.5
@@ -0,0 +1,354 @@
+require "http"
+require "http/client"
+require "openssl"
+
+module Fetch
+ private def self.fetch(
+ method : String,
+ url : String,
+ query : URI::Params? = URI::Params.new,
+ headers : HTTP::Headers? = HTTP::Headers.new,
+ body : String? = nil,
+ max_hops : Int32? = 10,
+ ) : HTTP::Client::Response
+ context = OpenSSL::SSL::Context::Client.new
+ context.verify_mode = OpenSSL::SSL::VerifyMode::NONE # re-enable in prod
+ uri = URI.parse(url)
+ uri.query_params = query unless query.nil?
+ body = "" if body.nil?
+ headers_hash = HTTP::Headers.new if headers.nil?
+ headers_hash = headers.clone unless headers.nil?
+ hops = 0
+ max_hops = max_hops.nil? ? 10 : max_hops
+ loop do
+ raise "too many redirects" if hops >= (max_hops.to_i || 10)
+ hops += 1
+ begin
+ headers ||= HTTP::Headers.new
+ headers_hash ||= HTTP::Headers.new
+ headers_hash.each { |k, v| headers[k] = v }
+ headers["Host"] = uri.host.not_nil! unless uri.host.nil?
+ headers["Content-Type"] = "application/json" if method == "POST"
+ resp = case method.upcase
+ when "POST"
+ HTTP::Client.post(uri.to_s, headers: headers, body: body, tls: context)
+ when "PUT"
+ HTTP::Client.put(uri.to_s, headers: headers, body: body, tls: context)
+ when "DELETE"
+ HTTP::Client.delete(uri.to_s, headers: headers, tls: context)
+ when "OPTIONS"
+ HTTP::Client.options(uri.to_s, headers: headers, tls: context)
+ when "HEAD"
+ HTTP::Client.head(uri.to_s, headers: headers, tls: context)
+ else
+ HTTP::Client.get(uri.to_s, headers: headers, tls: context)
+ end
+ # pp uri.to_s
+ # pp headers
+ # pp body
+ # puts "[redirect #{hops}] #{method} #{uri.to_s} -> #{resp.status_code} Location=#{resp.headers["Location"]?}"
+ # pp resp
+ # pp resp.headers.to_h
+ # not a redirect -> return
+ if resp && !(300..399).includes?(resp.status_code)
+ return resp
+ end
+ # redirect handling
+ loc = resp.headers["Location"]? || resp.headers["location"]? || ""
+ raise "redirect with no Location header" if loc.empty?
+ loc = self.normalize_location(uri, loc)
+ new_uri = URI.parse(loc)
+ if self.same_origin?(self.canonical_uri(new_uri), self.canonical_uri(uri))
+ # treat as final to avoid loop
+ return resp
+ end
+ # decide method change: RFC practice -> 303 always GET;
+ # many clients also convert 301/302 for non-GET/HEAD to GET
+ if resp.status_code == 303 || (
+ (resp.status_code == 301 || resp.status_code == 302) &&
+ method != "GET" && method != "HEAD"
+ )
+ method = "GET"
+ body = nil
+ # drop content headers
+ headers_hash.delete("Content-Length")
+ headers_hash.delete("Content-Type")
+ end
+ # drop Authorization on cross-origin redirects
+ unless same_origin?(uri, new_uri)
+ headers_hash.delete("Authorization")
+ end
+ uri = new_uri
+ return resp
+ rescue ex : Exception
+ puts ex.message
+ puts ex.backtrace
+ raise ex
+ end
+ end
+ end
+
+ private def self.normalize_location(base : URI, loc : String) : String
+ begin
+ # absolute URI
+ URI.parse(loc).to_s
+ rescue
+ # relative -> build a new URI by resolving path/query against base
+ # ensure base.path ends with '/' if needed for relative resolution
+ base_path = base.path.empty? ? "/" : base.path
+ # If loc starts with '/', it's absolute path on same origin
+ resolved_path = if loc.starts_with?("/")
+ loc
+ else
+ # join base path directory with relative segment
+ dir = base_path.ends_with?("/") ? base_path : File.dirname(base_path)
+ File.join(dir, loc)
+ end
+
+ # construct new URI preserving scheme, host, port
+ uri = URI.new("#{base.scheme}://#{base.host}#{base.port && base.port != 80 && base.port != 443 ? ":#{base.port}" : ""}#{resolved_path}")
+ uri.to_s
+ end
+ end
+
+ private def self.same_origin?(a : URI, b : URI) : Bool
+ a.scheme == b.scheme && a.host == b.host && a.port == b.port
+ end
+
+ private def self.canonical(u : URI) : String
+ scheme = u.scheme.not_nil!.downcase
+ host = u.host.not_nil!.downcase
+ port = u.port
+ if (scheme == "http" && port == 80) || (scheme == "https" && port == 443)
+ port = nil
+ end
+ path = u.path.empty? ? "/" : File.expand_path(u.path, "/")
+ "#{scheme}://#{host}#{port ? ":#{port}" : ""}#{path}#{u.query ? "?#{u.query}" : ""}"
+ end
+
+ private def self.canonical_uri(u : URI) : URI
+ URI.parse(self.canonical(u))
+ end
+
+ {% for method in %w(GET POST PUT DELETE OPTIONS HEAD) %}
+ def self.{{method.id.downcase}}(
+ url : String,
+ query : URI::Params? = URI::Params.new,
+ headers : HTTP::Headers? = HTTP::Headers.new,
+ body : String? = nil,
+ max_redirects : Int32? = 10,
+ )
+ self.fetch(
+ method: {{method.downcase}},
+ url: url,
+ query: query,
+ headers: headers,
+ body: body,
+ max_redirects: max_redirects,
+ )
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: URI::Params,
+ headers: HTTP::Headers,
+ body: String,
+ max_redirects: Int32,
+ ),
+ )
+ self.fetch(
+ method: {{method.downcase}},
+ url: url,
+ query: args[:query],
+ headers: args[:headers],
+ body: args[:body],
+ max_redirects: args[:max_redirects],
+ )
+ end
+
+ def self.{{method.id.downcase}}(url : String)
+ self.fetch({{method.downcase}}, url, nil, nil, nil, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: URI::Params,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, args[:query], nil, nil, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ headers: HTTP::Headers,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, nil, args[:headers], nil, nil)
+ end
+
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: URI::Params,
+ headers: HTTP::Headers,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, args[:query], args[:headers], nil, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: URI::Params,
+ body: String,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, args[:query], nil, args[:body], nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: URI::Params,
+ headers: HTTP::Headers,
+ body: String,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, args[:query], args[:headers], args[:body], nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ headers: HTTP::Headers,
+ body: String,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, nil, args[:headers], args[:body], nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: URI::Params,
+ headers: HTTP::Headers,
+ body: String,
+ max_redirects: Int32,
+ ),
+ )
+ self.fetch({{method.downcase}}, url, args[:query], args[:headers], args[:body], args[:max_redirects])
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: Hash(String, String),
+ headers: Hash(String, String),
+ body: Hash(String, String),
+ ),
+ )
+ query = URI::Params.build do |p|
+ args[:query].each do |k, v|
+ p[k] = v
+ end
+ end
+ headers = HTTP::Headers.new
+ args[:headers].each do |k, v|
+ headers[k] = v
+ end
+ body = args[:body].to_json.to_s
+ self.fetch({{method.downcase}}, url, query, headers, body, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ headers: Hash(String, String),
+ body: Hash(String, String),
+ ),
+ )
+ headers = HTTP::Headers.new
+ args[:headers].each do |k, v|
+ headers[k] = v
+ end
+ body = args[:body].to_json.to_s
+ self.fetch({{method.downcase}}, url, nil, headers, body, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: Hash(String, String),
+ body: Hash(String, String),
+ ),
+ )
+ query = URI::Params.build do |p|
+ args[:query].each do |k, v|
+ p[k] = v
+ end
+ end
+ body = args[:body].to_json.to_s
+ self.fetch({{method.downcase}}, url, query, nil, body, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: Hash(String, String),
+ headers: Hash(String, String),
+ ),
+ )
+ query = URI::Params.build do |p|
+ args[:query].each do |k, v|
+ p[k] = v
+ end
+ end
+ headers = HTTP::Headers.new
+ args[:headers].each do |k, v|
+ headers[k] = v
+ end
+ self.fetch({{method.downcase}}, url, query, headers, nil, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ query: Hash(String, String),
+ ),
+ )
+ query = URI::Params.build do |p|
+ args[:query].each do |k, v|
+ p[k] = v
+ end
+ end
+ self.fetch({{method.downcase}}, url, query, nil, nil, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ headers: Hash(String, String),
+ ),
+ )
+ headers = HTTP::Headers.new
+ args[:headers].each do |k, v|
+ headers[k] = v
+ end
+ self.fetch({{method.downcase}}, url, nil, headers, nil, nil)
+ end
+
+ def self.{{method.id.downcase}}(
+ url : String,
+ args : NamedTuple(
+ body: Hash(String, String),
+ ),
+ )
+ body = args[:body].to_json.to_s
+ self.fetch({{method.downcase}}, url, nil, nil, body, nil)
+ end
+ {% end %}
+end
@@ -0,0 +1,459 @@
+#!/usr/bin/env crystal
+# ssh-key-wrapper-crystal
+#
+# - Uses only stdlib
+# - Args: argv[0]=user argv[1]=key_blob argv[2]=fingerprint (fingerprint optional)
+# - Validates key via HTTP POST to /auth/ssh
+# - For git-receive-pack (push): scans incoming pack for secrets, triggers pipelines, enforces policy
+# - For git-upload-pack / git-upload-archive (fetch/pull/clone): enforces auth and allows fetch (no server-side pack scanning)
+# - Build: crystal build --release ssh-key-wrapper-crystal.cr -o /usr/local/bin/ssh-key-wrapper-crystal
+require "file_utils"
+require "json"
+require "process"
+require "uri"
+require "io"
+
+require "./fetch"
+
+# ---------- Config ----------
+MAX_PACK_BYTES = 50 * 1024 * 1024 # 50 MiB
+CHUNK_SIZE = 64 * 1024 # 64 KiB
+OVERLAP = 4096 # 4 KiB overlap
+MAX_MATCHES = 500
+LOG_FILE = "/var/log/gitfoss/git_ssh.log"
+
+PATTERNS = [
+ /AKIA[0-9A-Z]{16}/,
+ /AIza[0-9A-Za-z\-_]{35}/,
+ /-----BEGIN PRIVATE KEY-----/,
+ /password\s*[:=]\s*[^ \t\r\n]{6,}/i
+]
+
+# prefix
+SIDEBAND_PREFIX = "remote: "
+
+# ---------- Helpers ----------
+def write_to_file(msg : String)
+ File.write(filename: LOG_FILE, content: "#{msg}\n", encoding: "utf-8")
+end
+
+def die_with_message(msg : String, code = 1)
+ txt = "#{SIDEBAND_PREFIX}#{msg}"
+ STDOUT.puts txt
+ write_to_file txt
+ exit(code)
+end
+
+def sideband_println(msg : String)
+ txt = "#{SIDEBAND_PREFIX}#{msg}"
+ STDOUT.puts txt
+ write_to_file txt
+end
+
+def bytes_to_utf8_str(buf : Bytes) : String
+ String.new(buf) rescue buf.map(&.chr).join
+end
+
+def compute_line_col(text : String, index_in_bytes : Int32)
+ line = 1
+ col = 1
+ bcount = 0
+ text.each_codepoint do |cp|
+ ch = cp.chr # returns a one-char String
+ bytes = ch.bytesize
+ break if bcount + bytes > index_in_bytes
+ if cp == 10
+ line += 1
+ col = 1
+ else
+ col += 1
+ end
+ bcount += bytes
+ end
+ {line: line, column: col}
+end
+
+def run_cmd_capture(cmd : Array(String), cwd = nil)
+ output_io = IO::Memory.new
+ error_io = IO::Memory.new
+ process = Process.run(
+ command: cmd[0],
+ args: cmd[1..-1],
+ chdir: cwd,
+ output: output_io,
+ error: error_io,
+ )
+ out = output_io.gets_to_end
+ err = error_io.gets_to_end
+ code = process.exit_code
+ { code: code, out: out, err: err }
+end
+
+def exec_replace(cmd : Array(String))
+ Process.exec(cmd[0], cmd[1..-1])
+end
+
+# Extract repo path (org/repo.git) from SSH_ORIGINAL_COMMAND argument
+def extract_org_repo(original_cmd : String)
+ arg = nil
+ if original_cmd.includes?(" ")
+ parts = original_cmd.split(" ", 2)
+ arg = parts[1].strip
+ if (arg.starts_with?("'") && arg.ends_with?("'")) || (arg.starts_with?("\"") && arg.ends_with?("\""))
+ arg = arg[1..-2]
+ end
+ end
+ return {org: nil, repo: nil, host: nil} if arg.nil? || arg.empty?
+
+ # If URL
+ begin
+ if arg.starts_with?("http://") || arg.starts_with?("https://")
+ u = URI.parse(arg)
+ segs = u.path.split("/").reject &.empty?
+ if segs.size >= 2
+ org = segs[-2]
+ repo = segs[-1]
+ repo = repo[0, repo.size - 4] if repo.ends_with?(".git")
+ host = "#{u.scheme}://#{u.host}"
+ return {org: org, repo: repo, host: host}
+ end
+ end
+ rescue
+ end
+
+ # If filesystem or bare "org/repo.git" or "/srv/git/org/repo.git"
+ segs = arg.split("/").reject &.empty?
+ if segs.size >= 2
+ org = segs[-2]
+ repo = segs[-1]
+ repo = repo[0, repo.size - 4] if repo.ends_with?(".git")
+ return {org: org, repo: repo, host: nil}
+ end
+
+ {org: nil, repo: nil, host: nil}
+end
+
+# Parse pkt-line data into lines (simple)
+def parse_pkt_lines(data : Bytes) : Array(String)
+ lines = [] of String
+ i = 0
+ while i < data.size
+ break if i + 4 > data.size
+ len_hex = data[i,4].to_s
+ if len_hex == "0000"
+ i += 4
+ next
+ end
+ len = len_hex.to_i(16)
+ break if len < 4
+ payload_len = len - 4
+ break if i + 4 + payload_len > data.size
+ payload = data[i+4, payload_len].to_s
+ lines << payload
+ i += 4 + payload_len
+ end
+ lines
+end
+
+# ---------- Step 0: get params ----------
+# Endpoint base for auth and pipeline trigger (adjust as needed)
+AUTH_URL = "https://gitfoss.dev/_ssh/auth"
+TRIGGER_BASE = "https://gitfoss.dev" # base host for pipeline triggers
+
+# ---------- Step 1: gather inputs ----------
+if ARGV.size < 3
+ die_with_message "usage: ssh-command <user> <key_blob> [fingerprint]\n", 2
+end
+
+user = ARGV[0]
+key_blob = ARGV[1]
+fingerprint = ARGV[2] rescue nil
+
+if ENV["SSH_ORIGINAL_COMMAND"]?.nil?
+ die_with_message "Script has not been invoked by ssh. Missing environment variable SSH_ORIGINAL_COMMAND.\n", 2
+end
+
+SSH_ORIGINAL_COMMAND = ENV["SSH_ORIGINAL_COMMAND"]? || ""
+
+if user.nil? || key_blob.nil?
+ die_with_message "usage: ssh-key-wrapper <user> <key_blob> [fingerprint]", 2
+end
+
+# ---------- Step 2: validate via HTTP POST (auth) ----------
+begin
+ parsed = URI.parse(AUTH_URL)
+ info = extract_org_repo(SSH_ORIGINAL_COMMAND)
+ resp = Fetch.post(AUTH_URL.to_s, {
+ headers: {
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ },
+ body: {
+ "command" => SSH_ORIGINAL_COMMAND,
+ "username" => user,
+ "repoSlug" => "#{info[:org]}/#{info[:repo]}.git",
+ "publicKey" => key_blob,
+ "publicKeyFingerprint" => fingerprint || "",
+ },
+ })
+
+ if resp.status != HTTP::Status::OK
+ die_with_message("Key validation failed (auth service returned #{resp.status_code})")
+ # end
+ # if j.is_a?(Hash) && j["success"]?.try(&.as_bool) == true
+ # authorized
+ else
+ # j = JSON.parse(resp.body) rescue nil
+ # pp j
+ # authorized
+ # todo: handle authMode
+ # todo: set repoDir => j["gitRepositoryDir"]
+ # todo: check command hasn't been tampered
+ end
+rescue ex
+ die_with_message("Key validation error: #{ex.message}")
+end
+
+# ---------- Step 3: dispatch by SSH_ORIGINAL_COMMAND ----------
+if SSH_ORIGINAL_COMMAND.starts_with?("git-receive-pack")
+ mode = :receive
+elsif SSH_ORIGINAL_COMMAND.starts_with?("git-upload-pack") || SSH_ORIGINAL_COMMAND.starts_with?("git-upload-archive")
+ mode = :upload
+else
+ die_with_message("Unsupported command: only git-receive-pack, git-upload-pack, git-upload-archive are allowed")
+end
+
+# For upload (fetch/pull), just exec the original command (auth already done).
+# if mode == :upload
+sideband_println("Auth OK; allowing command (#{SSH_ORIGINAL_COMMAND.split(' ')[0]})")
+exec_replace(["/bin/sh", "-c", SSH_ORIGINAL_COMMAND])
+# end
+
+# ---------- From here: mode == :receive (push) ----------
+# Read stdin fully into memory (preamble + pack) but enforce size limit
+tmpdir = Dir.tempdir #("gitpack-")
+raw = "".to_slice # Bytes.new
+total = 0
+begin
+ loop do
+ buf = Bytes.new(CHUNK_SIZE)
+ slice = buf[0, CHUNK_SIZE]
+ read = STDIN.read(slice) rescue 0
+ break if read == 0
+ total += read
+ if total > MAX_PACK_BYTES
+ FileUtils.rm_rf(tmpdir) rescue nil
+ die_with_message("Pack exceeds limit of #{MAX_PACK_BYTES} bytes; rejecting push")
+ end
+ raw += buf[0, read]
+ end
+rescue ex
+ FileUtils.rm_rf(tmpdir) rescue nil
+ die_with_message("Error reading stdin: #{ex.message}")
+end
+
+# Split preamble (pkt-lines) and pack data by locating "PACK" header
+pack_sig = "PACK".to_slice.as(Bytes)
+pack_index = raw.index(pack_sig) || -1
+pre_bytes = raw[0, pack_index] if pack_index >= 0
+pre_bytes ||= raw
+pack_bytes = raw[pack_index, raw.size - pack_index] if pack_index >= 0
+pack_bytes ||= "".to_slice
+
+# Parse pkt-lines to get ref updates
+pkt_lines = parse_pkt_lines(pre_bytes)
+ref_updates = [] of Hash(String, String)
+pkt_lines.each do |line|
+ next if line.nil? || line.empty?
+ if line.includes?("\0")
+ line = line.split("\0", 2)[0]
+ end
+ parts = line.split(" ")
+ if parts.size >= 3 && parts[0] =~ /^[0-9a-f]{40}$/ && parts[1] =~ /^[0-9a-f]{40}$/
+ old_sha = parts[0]
+ new_sha = parts[1]
+ refname = parts[2].strip
+ ref_updates << {"old" => old_sha, "new" => new_sha, "ref" => refname}
+ end
+end
+
+pushed_refs = [] of Hash(String, String)
+ref_updates.each do |r|
+ ref = r["ref"].as(String)
+ if ref.starts_with?("refs/heads/")
+ pushed_refs << {
+ "type" => "branch",
+ "name" => ref["refs/heads/".size .. -1],
+ "new" => r["new"],
+ }
+ elsif ref.starts_with?("refs/tags/")
+ pushed_refs << {
+ "type" => "tag",
+ "name" => ref["refs/tags/".size .. -1],
+ "new" => r["new"],
+ }
+ else
+ pushed_refs << {
+ "type" => "other",
+ "name" => ref,
+ "new" => r["new"],
+ }
+ end
+end
+
+# Write pack bytes to file (if any)
+pack_path = File.join(tmpdir, "incoming.pack")
+File.open(pack_path, "w") do |f|
+ f.write(pack_bytes) if pack_bytes.size > 0
+end
+
+# If pack exists, index/unpack into temp bare repo and scan blobs
+detections = [] of Hash(String, String | Int32)
+
+if File.size(pack_path).to_i > 0
+ repo_dir = File.join(tmpdir, "repo.git")
+ if !Dir.exists?(repo_dir)
+ run_cmd_capture(["git", "init", "--bare", repo_dir])
+ end
+
+ pack_dest_dir = File.join(repo_dir, "objects", "pack")
+ FileUtils.mkdir_p(pack_dest_dir)
+ pack_dest = File.join(pack_dest_dir, "incoming.pack")
+ FileUtils.mv(pack_path, pack_dest) rescue FileUtils.cp(pack_path, pack_dest)
+
+ index_res = run_cmd_capture(["git", "index-pack", "--stdin"], repo_dir)
+ if index_res[:code] != 0
+ index_res = run_cmd_capture(["git", "index-pack", pack_dest], repo_dir)
+ if index_res[:code] != 0
+ FileUtils.rm_rf(tmpdir) rescue nil
+ die_with_message("git index-pack failed: #{index_res[:err]}")
+ end
+ end
+
+ idx_file = Dir.glob(File.join(pack_dest_dir, "*.idx"))[0] rescue nil
+ unless idx_file
+ FileUtils.rm_rf(tmpdir) rescue nil
+ die_with_message("git index-pack did not produce .idx; aborting")
+ end
+
+ verify = run_cmd_capture(["git", "verify-pack", "-v", idx_file], repo_dir)
+ if verify[:code] != 0
+ FileUtils.rm_rf(tmpdir) rescue nil
+ die_with_message("git verify-pack failed: #{verify[:err]}")
+ end
+
+ blob_shas = [] of String
+ verify[:out].each_line do |l|
+ l = l.chomp
+ next if l.empty?
+ parts = l.split
+ sha = parts[0]
+ next unless sha && sha =~ /^[0-9a-f]{40}$/
+ t = run_cmd_capture(["git", "cat-file", "-t", sha], repo_dir)
+ next if t[:code] != 0
+ typ = t[:out].strip
+ if typ == "blob"
+ blob_shas << sha
+ end
+ end
+
+ blob_shas.each do |sha|
+ break if detections.size >= MAX_MATCHES
+ cat = run_cmd_capture(["git", "cat-file", "-p", sha], repo_dir)
+ next if cat[:code] != 0
+ blob_text = bytes_to_utf8_str(cat[:out].to_slice)
+
+ PATTERNS.each do |pat|
+ start = 0
+ while m = pat.match(blob_text, start)
+ match_str = m[0]
+ prefix = blob_text[0, m.begin(0)]
+ byte_offset = prefix.bytesize
+ lc = compute_line_col(blob_text, byte_offset)
+
+ paths = [] of String
+ revs = run_cmd_capture(["git", "rev-list", "--all"], repo_dir)
+ if revs[:code] == 0
+ revs[:out].each_line do |commit|
+ commit = commit.strip
+ next if commit.empty?
+ ls = run_cmd_capture(["git", "ls-tree", "-r", "--full-tree", commit], repo_dir)
+ next if ls[:code] != 0
+ ls[:out].each_line do |line|
+ if m2 = line.match(/^\d+\s+blob\s+([0-9a-f]{40})\t(.+)$/)
+ if m2[1] == sha
+ paths << m2[2]
+ end
+ end
+ end
+ break if !paths.empty?
+ end
+ end
+
+ detections << {
+ "pattern" => pat.to_s,
+ "blob" => sha,
+ "paths" => paths.join(", "),
+ "line" => lc[:line],
+ "column" => lc[:column],
+ "excerpt" => match_str
+ }
+
+ start = m.end(0)
+ break if detections.size >= MAX_MATCHES
+ end
+ break if detections.size >= MAX_MATCHES
+ end
+ end
+end
+
+# ---------- Decision ----------
+if detections.size > 0
+ sideband_println("Push rejected: #{detections.size} secret(s) detected in incoming pack")
+ detections.each_with_index do |d, i|
+ sideband_println("Match #{i+1}: pattern=#{d["pattern"]} blob=#{d["blob"]} paths=#{d["paths"]} line=#{d["line"]} col=#{d["column"]}")
+ sideband_println("Excerpt: #{d["excerpt"]}")
+ end
+ FileUtils.rm_rf(tmpdir) rescue nil
+ exit 1
+end
+
+# ---------- Trigger pipelines (only when auth OK and no detections) ----------
+orgrepo = extract_org_repo(SSH_ORIGINAL_COMMAND)
+org = orgrepo[:org]
+repo = orgrepo[:repo]
+host = orgrepo[:host]
+
+if org.nil? || repo.nil?
+ sideband_println("Warning: could not determine org/repo from SSH_ORIGINAL_COMMAND; skipping pipeline triggers")
+else
+ pushed_refs.each do |pr|
+ next unless pr["type"] == "branch" || pr["type"] == "tag"
+ commit = pr["new"].as(String)
+ branch = pr["type"] == "branch" ? (pr["name"].as(String)) : nil
+ tag = pr["type"] == "tag" ? (pr["name"].as(String)) : nil
+ next if commit.nil? || commit.strip.empty? || commit == ("0" * 40)
+
+ path = "/#{org}/#{repo}/pipeline/trigger"
+ params = URI::Params.build do |p|
+ p.add "commit", URI.encode_path(commit)
+ p.add "branch", URI.encode_path(branch) if branch
+ p.add "tag", URI.encode_path(tag) if tag
+ p.add "trigger_username", URI.encode_path(user)
+ p.add "trigger_pubkey", URI.encode_path(key_blob)
+ p.add "trigger_fingerprint", URI.encode_path(fingerprint || "")
+ end
+ url = "#{(host || TRIGGER_BASE)}#{path}?#{params.to_s}"
+
+ begin
+ res = Fetch.post(url)
+ sideband_println("Pipeline trigger for #{pr["type"]}: HTTP #{res.status_code}")
+ rescue ex
+ sideband_println("Pipeline trigger error: #{ex.message}")
+ end
+ end
+end
+
+FileUtils.rm_rf(tmpdir) rescue nil
+sideband_println("No secrets detected; allowing push")
+exec_replace(["/bin/sh", "-c", SSH_ORIGINAL_COMMAND])
@@ -0,0 +1,30 @@
+diff --git a/node_modules/@ethicdevs/react-monolith/dist/components/Router.js b/node_modules/@ethicdevs/react-monolith/dist/components/Router.js
+index 3bb6f12..c26dc2a 100644
+--- a/node_modules/@ethicdevs/react-monolith/dist/components/Router.js
++++ b/node_modules/@ethicdevs/react-monolith/dist/components/Router.js
+@@ -54,8 +54,8 @@ function Root({ children, }) {
+ setLocation;
+ const pathPattern = new routing_1.Path("/:orgSlug/:repoSlug/:currentRef/tree/*");
+ const pathRealUrl = new routing_1.Path("/ethicdevs/gitfoss/main/tree/app/views/HomeView.tsx");
+- console.log("pathPattern:", pathPattern);
+- console.log("pathRealUrl:", pathRealUrl);
++ // console.log("pathPattern:", pathPattern);
++ // console.log("pathRealUrl:", pathRealUrl);
+ const addRoute = (route) => {
+ // console.log("addRoute called", route);
+ if (route.path == null)
+diff --git a/node_modules/@ethicdevs/react-monolith/src/components/Router.tsx b/node_modules/@ethicdevs/react-monolith/src/components/Router.tsx
+index 26c710f..2d3c232 100644
+--- a/node_modules/@ethicdevs/react-monolith/src/components/Router.tsx
++++ b/node_modules/@ethicdevs/react-monolith/src/components/Router.tsx
+@@ -83,8 +83,8 @@ function Root<RoutesParams extends IRouteParams>({
+ "/ethicdevs/gitfoss/main/tree/app/views/HomeView.tsx",
+ );
+
+- console.log("pathPattern:", pathPattern);
+- console.log("pathRealUrl:", pathRealUrl);
++ // console.log("pathPattern:", pathPattern);
++ // console.log("pathRealUrl:", pathRealUrl);
+
+ const addRoute: RouterContext["addRoute"] = (route) => {
+ // console.log("addRoute called", route);
@@ -1290,6 +1290,11 @@ asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+at-least-node@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+ integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
+
atomic-sleep@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
@@ -1485,7 +1490,7 @@ caniuse-lite@^1.0.30001359:
version "1.0.30001361"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001361.tgz#ba2adb2527566fb96f3ac7c67698ae7fc495a28d"
-chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@^2.0.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
dependencies:
@@ -1493,9 +1498,10 @@ chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@^4.0.0:
+chalk@^4.0.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
@@ -1518,14 +1524,15 @@ chokidar@^3.5.1:
optionalDependencies:
fsevents "~2.3.2"
-ci-info@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
-
ci-info@^3.2.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128"
+ci-info@^3.7.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
+ integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
+
cjs-module-lexer@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
@@ -2368,13 +2375,15 @@ fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-fs-extra@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
+fs-extra@^9.0.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
+ integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
dependencies:
- graceful-fs "^4.1.2"
- jsonfile "^4.0.0"
- universalify "^0.1.0"
+ at-least-node "^1.0.0"
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
fs.realpath@^1.0.0:
version "1.0.0"
@@ -2567,6 +2576,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.9:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+graceful-fs@^4.2.0:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
gray-matter@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e"
@@ -2789,12 +2803,6 @@ is-callable@^1.1.4, is-callable@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
-is-ci@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
- dependencies:
- ci-info "^2.0.0"
-
is-core-module@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
@@ -3483,9 +3491,12 @@ json5@2.x, json5@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
-jsonfile@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+jsonfile@^6.0.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.1.tgz#b6e31717f22cc37330b081ce0051ed5de53af2f6"
+ integrity sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==
+ dependencies:
+ universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
@@ -4055,23 +4066,25 @@ parse5@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
-patch-package@^6.4.7:
- version "6.4.7"
- resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148"
+patch-package@^7.0.0:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-7.0.2.tgz#c01589bb6964854b5210506a5845d47900641f5a"
+ integrity sha512-PMYfL8LXxGIRmxXLqlEaBxzKPu7/SdP13ld6GSfAUJUZRmBDPp8chZs0dpzaAFn9TSPnFiMwkC6PJt6pBiAl8Q==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
- chalk "^2.4.2"
- cross-spawn "^6.0.5"
+ chalk "^4.1.2"
+ ci-info "^3.7.0"
+ cross-spawn "^7.0.3"
find-yarn-workspace-root "^2.0.0"
- fs-extra "^7.0.1"
- is-ci "^2.0.0"
+ fs-extra "^9.0.0"
klaw-sync "^6.0.0"
- minimist "^1.2.0"
+ minimist "^1.2.6"
open "^7.4.2"
rimraf "^2.6.3"
- semver "^5.6.0"
+ semver "^7.5.3"
slash "^2.0.0"
tmp "^0.0.33"
+ yaml "^2.2.2"
path-exists@^4.0.0:
version "4.0.0"
@@ -4580,6 +4593,11 @@ semver@^7.3.2:
dependencies:
lru-cache "^6.0.0"
+semver@^7.5.3:
+ version "7.8.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df"
+ integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==
+
send@^0.17.1:
version "0.17.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820"
@@ -5145,10 +5163,15 @@ underscore@>=1.1.0:
version "1.13.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee"
-universalify@^0.1.0, universalify@^0.1.2:
+universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+universalify@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
+ integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
+
update-browserslist-db@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz#dbfc5a789caa26b1db8990796c2c8ebbce304824"
@@ -5338,6 +5361,11 @@ yallist@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+yaml@^2.2.2:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4"
+ integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==
+
yargs-parser@20.x, yargs-parser@^20.2.2:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"