0.1.0
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 16s
CI / check (push) Successful in 19s
CI / build (push) Successful in 32s

This commit is contained in:
2025-12-24 18:43:08 -06:00
parent 26e154ebb4
commit 5e07339709
38 changed files with 6661 additions and 0 deletions

23
.dockerignore Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

39
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,39 @@
name: CI
on:
push:
branches:
- '*'
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Svelte check (fail on warnings)
run: bash scripts/check.sh
build:
runs-on: ubuntu-latest
needs: check
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: bash scripts/build.sh

View File

@@ -0,0 +1,20 @@
name: OSV-Scanner PR Scan
on:
pull_request:
branches: [master]
merge_group:
branches: [master]
permissions:
contents: read
jobs:
scan-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: OSV scan
run: bash scripts/osv_scan.sh

View File

@@ -0,0 +1,20 @@
name: OSV-Scanner Scheduled Scan
on:
schedule:
- cron: '30 12 * * 1'
push:
branches: [master]
permissions:
contents: read
jobs:
scan-scheduled:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: OSV scan
run: bash scripts/osv_scan.sh

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.svelte-kit
build
dist
.DS_Store

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM cgr.dev/chainguard/node:latest-dev AS builder
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci
RUN npm install --save-dev @sveltejs/adapter-node@latest
COPY --chown=node:node . .
COPY --chown=node:node svelte.config.docker.js svelte.config.js
RUN npm run build
FROM cgr.dev/chainguard/node:latest AS runtime
WORKDIR /app
COPY --from=builder --chown=node:node /app/package.json /app/package-lock.json ./
RUN npm install --omit=dev && \
npm cache clean --force
COPY --from=builder --chown=node:node /app/build ./build
COPY --from=builder --chown=node:node /app/package.json ./
EXPOSE 3000
ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0
CMD ["build/index.js"]

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 Quad4.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32
Makefile Normal file
View File

@@ -0,0 +1,32 @@
.PHONY: help install dev build preview check lint format clean
help:
@echo 'Usage: make [target]'
@echo ''
@echo 'Available targets:'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
install:
npm install
dev:
npm run dev
build:
npm run build
preview:
npm run preview
check:
npm run check
lint:
npm run lint
format:
npm run format
clean:
rm -rf .svelte-kit build node_modules/.vite

76
eslint.config.js Normal file
View File

@@ -0,0 +1,76 @@
import js from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import sveltePlugin from 'eslint-plugin-svelte';
import svelteParser from 'svelte-eslint-parser';
export default [
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs,ts,svelte}'],
languageOptions: {
parser: tsParser,
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
},
globals: {
fetch: 'readonly',
caches: 'readonly',
URL: 'readonly',
console: 'readonly',
HTMLElement: 'readonly',
HTMLImageElement: 'readonly',
HTMLInputElement: 'readonly',
HTMLTextAreaElement: 'readonly',
HTMLSelectElement: 'readonly',
HTMLDivElement: 'readonly',
SVGSVGElement: 'readonly',
navigator: 'readonly',
window: 'readonly',
document: 'readonly',
Blob: 'readonly',
Event: 'readonly',
MouseEvent: 'readonly',
WheelEvent: 'readonly',
KeyboardEvent: 'readonly',
URLSearchParams: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
localStorage: 'readonly',
atob: 'readonly',
btoa: 'readonly',
alert: 'readonly',
prompt: 'readonly',
XMLSerializer: 'readonly',
Image: 'readonly',
FileReader: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
svelte: sveltePlugin,
},
rules: {
...tsPlugin.configs.recommended.rules,
},
},
{
files: ['**/*.svelte'],
languageOptions: {
parser: svelteParser,
parserOptions: {
parser: tsParser,
},
},
plugins: {
svelte: sveltePlugin,
},
rules: {
...sveltePlugin.configs.recommended.rules,
},
},
{
ignores: ['node_modules/**', '.svelte-kit/**', 'build/**', 'dist/**', 'archive/**'],
},
];

4125
package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "quad4-linking-tool",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",
"build": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite build",
"preview": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "eslint ."
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@typescript-eslint/eslint-plugin": "^8.50.1",
"@typescript-eslint/parser": "^8.50.1",
"eslint": "^9.39.2",
"eslint-plugin-svelte": "^3.13.1",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.45.6",
"svelte-check": "^4.3.4",
"svelte-eslint-parser": "^1.4.1",
"typescript": "^5.9.3",
"vite": "^7.2.6"
},
"dependencies": {
"autoprefixer": "^10.4.23",
"lucide-svelte": "^0.562.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

6
scripts/build.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Building app..."
VITE_APP_VERSION=$(node -p "require('./package.json').version") npm run build

9
scripts/check.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Running Svelte sync..."
npx svelte-kit sync
echo "Running svelte-check (fail on errors)..."
npx svelte-check --tsconfig ./tsconfig.json

42
scripts/osv_scan.sh Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
OSV_VERSION="${OSV_VERSION:-v2.3.1}"
echo "Installing OSV-Scanner ${OSV_VERSION}..."
curl -sSL "https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_linux_amd64" -o /tmp/osv-scanner
chmod +x /tmp/osv-scanner
sudo mv /tmp/osv-scanner /usr/local/bin/osv-scanner
echo "Running OSV-Scanner recursively..."
OSV_JSON="$(mktemp)"
trap 'rm -f "$OSV_JSON"' EXIT
osv-scanner --recursive ./ --format json > "$OSV_JSON" || true
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is not installed. Please install jq to parse OSV results."
exit 1
fi
VULNS=$(jq -r '
.results[]? |
.source as $src |
.vulns[]? |
select(
(.database_specific.severity // "" | ascii_upcase | test("HIGH|CRITICAL")) or
(.severity[]?.score // "" | tostring | split("/")[0] | tonumber? // 0 | . >= 7.0)
) |
"\(.id) (source: \($src))"
' "$OSV_JSON")
if [ -n "$VULNS" ]; then
echo "OSV scan found HIGH/CRITICAL vulnerabilities:"
echo "$VULNS" | while IFS= read -r line; do
echo " - $line"
done
exit 1
else
echo "OSV scan: no HIGH/CRITICAL vulnerabilities found."
fi

78
src/app.css Normal file
View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src:
local(''),
url('/fonts/nunito-v16-latin-regular.woff2') format('woff2');
}
@layer base {
:root {
color-scheme: dark;
}
html {
@apply bg-bg-primary text-text-primary;
}
body {
@apply m-0 p-0 min-h-screen bg-bg-primary text-text-primary font-sans antialiased;
}
}
@layer components {
.btn {
@apply bg-accent-red text-text-primary border-none px-4 py-2 rounded-md cursor-pointer text-sm font-medium flex items-center gap-2 transition-colors;
}
.btn:hover {
@apply bg-accent-red-dark;
}
.btn:active {
@apply bg-accent-red-dark;
}
.btn.active {
@apply bg-accent-red-dark;
}
.btn-icon-only {
@apply p-2;
}
.toolbar {
@apply flex items-center gap-1;
}
.toolbar-btn {
@apply p-1.5 rounded text-text-primary hover:bg-bg-primary/50 transition-colors cursor-pointer border-none bg-transparent;
}
.toolbar-btn.active {
@apply bg-accent-red/20 text-accent-red-light;
}
.toolbar-select {
@apply text-xs;
}
.toolbar-select option {
background-color: #171717;
color: #fafafa;
}
.loading-bar {
@apply h-1 bg-accent-red transition-all duration-300;
width: 0%;
}
.loading-bar.active {
@apply w-full;
}
}

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

17
src/app.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#dc2626" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Linking Tool" />
<link rel="apple-touch-icon" href="/favicon.svg" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="4" fill="none"/>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>

After

Width:  |  Height:  |  Size: 374 B

45
src/lib/identityGraph.ts Normal file
View File

@@ -0,0 +1,45 @@
import {
User,
Mail,
Phone,
MapPin,
Globe,
Building2,
Network,
AtSign,
} from 'lucide-svelte';
import type { ComponentType } from 'svelte';
export type NodeType =
| 'person'
| 'email'
| 'phone'
| 'address'
| 'domain'
| 'org'
| 'ip'
| 'social';
export const iconMap: Record<NodeType, ComponentType> = {
person: User,
email: Mail,
phone: Phone,
address: MapPin,
domain: Globe,
org: Building2,
ip: Network,
social: AtSign,
};
export const nodeTypes = Object.keys(iconMap) as NodeType[];
export const typeColors: Record<NodeType, string> = {
person: '#ef4444',
email: '#f97316',
phone: '#eab308',
address: '#10b981',
domain: '#f43f5e',
org: '#be123c',
ip: '#71717a',
social: '#db2777',
};

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

1
src/lib/version.ts Normal file
View File

@@ -0,0 +1 @@
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'dev';

19
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,19 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
onMount(() => {
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered:', registration);
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
});
}
});
</script>
<slot />

57
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,57 @@
<script lang="ts">
import IdentityGraph from '../components/IdentityGraph.svelte';
import { APP_VERSION } from '$lib/version';
import { Link as LinkIcon, GitBranch } from 'lucide-svelte';
</script>
<svelte:head>
<title>Linking Tool - Identity Graph</title>
<meta
name="description"
content="Linking Tool - A client-side identity graph visualization tool for mapping relationships between entities."
/>
<meta property="og:type" content="website" />
<meta property="og:title" content="Linking Tool - Identity Graph" />
<meta
property="og:description"
content="A client-side identity graph visualization tool for mapping relationships between entities."
/>
<meta property="og:image" content="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</svelte:head>
<div class="flex flex-col h-screen bg-bg-primary text-text-primary">
<header
class="bg-neutral-950 border-b border-neutral-800 px-4 sm:px-6 py-3 flex flex-col sm:flex-row justify-between items-center gap-2 flex-shrink-0 shadow-lg"
>
<h1 class="text-lg sm:text-xl font-semibold text-accent-red-light flex items-center gap-2">
<div
class="h-5 w-5 rounded border border-accent-red-light flex items-center justify-center bg-neutral-900"
>
<LinkIcon size={14} class="text-accent-red-light" />
</div>
Linking Tool
</h1>
<div class="text-text-secondary text-xs sm:text-sm flex items-center gap-2">
<span
>Created by <a
href="https://quad4.io"
target="_blank"
rel="noopener noreferrer"
class="text-accent-red-light hover:text-accent-red-dark transition-colors">Quad4</a
>
-
<a
href="https://git.quad4.io/Quad4-Software/Linking-Tool"
target="_blank"
rel="noopener noreferrer"
class="text-accent-red-light hover:text-accent-red-dark transition-colors inline-flex items-center gap-1"
>v{APP_VERSION} <GitBranch size={12} /></a
></span
>
</div>
</header>
<main class="flex-1 relative overflow-hidden bg-bg-primary p-4">
<IdentityGraph />
</main>
</div>

5
static/favicon.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="4" fill="none"/>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>

After

Width:  |  Height:  |  Size: 374 B

View File

19
static/manifest.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "Linking Tool",
"short_name": "Linking Tool",
"description": "A client-side identity graph visualization tool for mapping relationships between entities",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0a",
"theme_color": "#dc2626",
"orientation": "any",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}

3
static/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# disallow all
User-agent: *
Disallow: /

58
static/sw.js Normal file
View File

@@ -0,0 +1,58 @@
const CACHE_NAME = 'quad4-linking-tool-v1';
const urlsToCache = [
'/',
'/favicon.svg',
'/fonts/nunito-v16-latin-regular.woff2',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache).catch(() => {
console.log('Some resources failed to cache');
});
})
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});

11
svelte.config.docker.js Normal file
View File

@@ -0,0 +1,11 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
export default config;

18
svelte.config.js Normal file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
},
};
export default config;

22
tailwind.config.js Normal file
View File

@@ -0,0 +1,22 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {
colors: {
'bg-primary': '#0a0a0a',
'bg-secondary': '#171717',
'text-primary': '#fafafa',
'text-secondary': '#a3a3a3',
'accent-red': '#dc2626',
'accent-red-dark': '#991b1b',
'accent-red-light': '#ef4444',
'border-color': '#262626',
},
fontFamily: {
sans: ['Nunito', 'Inter', 'Segoe UI', 'system-ui', '-apple-system', 'sans-serif'],
},
},
},
plugins: [],
};

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

6
vite.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
});