### Install and Run SubMan Development Server Source: https://github.com/knowsky404/subman/blob/main/README.en.md Installs dependencies and starts the local development server for SubMan. Use this for local development and testing. ```bash bun install bun run dev bun run preview ``` -------------------------------- ### Install and Run Project Commands Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-agent-guide.md Use these bun commands for project installation, development, checking, linting, building, and deployment. ```bash bun install bun run dev bun run check bun run lint bun run build bun run deploy ``` -------------------------------- ### Install Dependencies with Bun Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/deployment.md Use this command to install project dependencies using the Bun package manager. ```bash bun install ``` -------------------------------- ### Start Local Vite Dev Server Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/deployment.md Run this command to start the local development server powered by Vite. ```bash bun run dev ``` -------------------------------- ### Install Dependencies with bun Source: https://context7.com/knowsky404/subman/llms.txt Installs project dependencies using the bun package manager. This is a prerequisite for building and deploying the Cloudflare Worker. ```bash # Install dependencies bun install ``` -------------------------------- ### Node Upsert Endpoint Example Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/specs/2026-05-06-server-api-design.md Example of using the PUT /api/nodes/by-key endpoint to idempotently create or update a node. This is the primary endpoint for integrations like sing-box-vps. ```bash curl -X PUT "https://subman.example.com/api/nodes/by-key/vps-1-vless" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["sing-box-vps"]}' ``` -------------------------------- ### Build and Preview SubMan Application Source: https://github.com/knowsky404/subman/blob/main/README.md Build the SubMan application for production and start a local server to preview the built application. Requires bun. ```bash bun run build bun run preview ``` -------------------------------- ### Node Upsert Example Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Example of how to upsert a node using the SubMan Server API. This endpoint is useful for backend scripts to manage node information. ```APIDOC ## Server API SubMan can expose owner-operated API endpoints for backend scripts such as `sing-box-vps`. The API uses Cloudflare Worker secrets: ```bash bun wrangler secret put GITHUB_TOKEN bun wrangler secret put SUBMAN_API_TOKEN ``` Example node upsert: ```bash curl -X PUT "https://subman.example.com/api/nodes/by-key/vps-1-vless" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["sing-box-vps"]}' ``` The first API version is intended for trusted backend scripts, so it does not enable broad browser CORS by default. ``` -------------------------------- ### SubMan Configuration Example Source: https://context7.com/knowsky404/subman/llms.txt This JSON object represents a typical configuration for SubMan, indicating that GitHub and SubMan API tokens are enabled. ```json { "ok": true, "config": { "githubToken": true, "submanApiToken": true } } ``` -------------------------------- ### Shell Integration Example Source: https://github.com/knowsky404/subman/blob/main/docs/api/server-api.md This bash script demonstrates how to automate node upsert operations using the Subman API. It dynamically sets node details based on the hostname and requires the SUBMAN_API_TOKEN environment variable. ```bash #!/usr/bin/env bash set -euo pipefail SUBMAN_BASE_URL="https://subman.example.com" SUBMAN_API_TOKEN="${SUBMAN_API_TOKEN:?SUBMAN_API_TOKEN is required}" HOSTNAME="$(hostname)" NODE_KEY="${HOSTNAME}-vless-reality" NODE_NAME="${HOSTNAME} vless reality" NODE_RAW="vless://..." curl -fsS -X PUT "${SUBMAN_BASE_URL}/api/nodes/by-key/${NODE_KEY}" \ -H "Authorization: Bearer ${SUBMAN_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"name\":\"${NODE_NAME}\",\"type\":\"vless\",\"raw\":\"${NODE_RAW}\",\"enabled\":true,\"tags\":[\"sing-box-vps\",\"auto\"]}" ``` -------------------------------- ### Local Development Server Source: https://context7.com/knowsky404/subman/llms.txt Starts the Vite development server for local testing and development of the SubMan application. This provides a fast feedback loop during development. ```bash # Local development (Vite dev server) bun run dev ``` -------------------------------- ### API Error Responses Examples Source: https://context7.com/knowsky404/subman/llms.txt Examples of common API error responses, including unauthorized access, node not found, and bad requests. These illustrate the JSON error envelope structure. ```bash # Missing or invalid token → 401 curl -sS "https://subman.example.com/api/nodes" # { "error": { "code": "unauthorized", "message": "Unauthorized" } } ``` ```bash # Node not found → 404 curl -sS "https://subman.example.com/api/nodes/nonexistent-id" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" # { "error": { "code": "not_found", "message": "Node not found" } } ``` ```bash # Invalid body → 400 curl -sS -X POST "https://subman.example.com/api/nodes" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"test"}' # missing required "type" and "raw" # { "error": { "code": "bad_request", "message": "..." } } ``` ```bash # Missing GITHUB_TOKEN secret → 500 # { "error": { "code": "server_error", "message": "GITHUB_TOKEN is not configured" } } ``` -------------------------------- ### Idempotent Node Creation/Update Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/server-api.md This endpoint is preferred for VPS installers and repeatable node synchronization. It allows for idempotent creation or update of nodes using a stable external key. ```APIDOC ## PUT /api/nodes/by-key/:externalKey ### Description Idempotently creates or updates a node using a stable external key. The API stores the external key as a tag label in the format `external:`. ### Method PUT ### Endpoint `/api/nodes/by-key/:externalKey` ### Parameters #### Path Parameters - **externalKey** (string) - Required - A stable key to identify the node, e.g., `vps-1-vless`, `hostname-vless-reality`, or `server-id-protocol-port`. #### Request Body - **name** (string) - Required - The name of the node. - **type** (string) - Required - The type of the node. Allowed values: `vless`, `vmess`, `trojan`, `ss`, `ssr`, `hysteria2`, `tuic`, `anytls`, `other`. - **raw** (string) - Required - The raw subscription link or configuration. - **enabled** (boolean) - Optional - Whether the node is enabled. Defaults to `true`. - **tags** (array) - Optional - An array of tags. Tags can be strings or objects with `id` and `label` properties. Defaults to an empty array. - **source** (string) - Optional - The source of the node. Defaults to `single`. ### Request Example ```bash curl -fsS -X PUT "https://subman.example.com/api/nodes/by-key/vps-1-vless" \ -H "Authorization: Bearer ${SUBMAN_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["sing-box-vps"]}' ``` ### Request Body Example ```json { "name": "vps-1 vless", "type": "vless", "raw": "vless://...", "enabled": true, "tags": ["sing-box-vps", "auto"], "source": "single" } ``` ### Response #### Success Response (200) - The response body will typically be empty or contain a success message upon successful creation or update. ``` -------------------------------- ### Create/Update Node by Key Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/server-api.md Use this PUT endpoint for VPS installers and repeatable node synchronization. It is idempotent and stores the external key as a tag label: external:. ```bash curl -fsS -X PUT "https://subman.example.com/api/nodes/by-key/vps-1-vless" \ -H "Authorization: Bearer ${SUBMAN_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["sing-box-vps"]}' ``` -------------------------------- ### Implement Shared Node API Route (GET/POST) Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Handles GET requests to list all nodes and POST requests to create a new node. Includes authentication and workspace state management. ```typescript import { ApiError, jsonError } from "$lib/server/api/errors"; import { getServerApiEnv } from "$lib/server/api/env"; import { isAuthorized } from "$lib/server/api/auth"; import { applyNodeCreate, parseNodePayload } from "$lib/server/api/nodes"; import { loadWorkspaceState, saveWorkspaceState } from "$lib/server/api/workspace"; async function requireApiAccess(request: Request, platform: App.Platform | undefined) { const env = getServerApiEnv(platform); if (!(await isAuthorized(request.headers.get("Authorization"), env.submanApiToken))) { throw new ApiError(401, "unauthorized", "Unauthorized"); } if (!env.githubToken) { throw new ApiError(500, "server_error", "GITHUB_TOKEN is not configured"); } return env.githubToken; } function handleError(error: unknown): Response { if (error instanceof ApiError) { return jsonError(error); } return jsonError(new ApiError(500, "server_error", "Internal server error")); } export async function GET({ request, platform }: { request: Request; platform?: App.Platform }) { try { const githubToken = await requireApiAccess(request, platform); const workspace = await loadWorkspaceState(githubToken); return Response.json({ data: workspace.state.nodes, workspace: { gistId: workspace.gist.id, file: workspace.state.activeGistFile, }, }); } catch (error) { return handleError(error); } } export async function POST({ request, platform }: { request: Request; platform?: App.Platform }) { try { const githubToken = await requireApiAccess(request, platform); const workspace = await loadWorkspaceState(githubToken); const payload = parseNodePayload(await request.json()); const result = applyNodeCreate(workspace.state, payload); const gist = await saveWorkspaceState(githubToken, workspace.gist.id, result.state); return Response.json( { data: result.node, workspace: { gistId: gist.id, file: result.state.activeGistFile, }, }, { status: 201 }, ); } catch (error) { return handleError(error); } } ``` -------------------------------- ### Create Node API Endpoint Source: https://github.com/knowsky404/subman/blob/main/docs/api/server-api.md Create a new node in the SubMan workspace. For installer scripts, prefer PUT /api/nodes/by-key/:externalKey to prevent duplicates. Requires API token and JSON content type. ```http POST /api/nodes ``` ```bash curl -sS -X POST "https://subman.example.com/api/nodes" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["manual"]}' ``` -------------------------------- ### Shell Script for Idempotent Node Upsert Source: https://context7.com/knowsky404/subman/llms.txt This bash script demonstrates how to use the upsert node by key endpoint within a VPS installer. It dynamically sets node details based on the server's hostname and environment variables. ```bash #!/usr/bin/env bash set -euo pipefail SUBMAN_BASE_URL="https://subman.example.com" SUBMAN_API_TOKEN="${SUBMAN_API_TOKEN:?SUBMAN_API_TOKEN is required}" HOTNAME="$(hostname)" NODE_KEY="${HOSTNAME}-vless-reality" NODE_NAME="${HOSTNAME} vless reality" NODE_RAW="vless://..." curl -fsS -X PUT "${SUBMAN_BASE_URL}/api/nodes/by-key/${NODE_KEY}" \ -H "Authorization: Bearer ${SUBMAN_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{"name":"${NODE_NAME}","type":"vless","raw":"${NODE_RAW}","enabled":true,"tags":["sing-box-vps","auto"]}" ``` -------------------------------- ### Automation API - Update Node by Key Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-agent-guide.md This endpoint is used for idempotent updates of nodes by their external key. It's suitable for VPS installers and repeatable node updates. Avoid using POST /api/nodes for automation unless duplicates are intended. ```APIDOC ## PUT /api/nodes/by-key/:externalKey ### Description Stores the external key as a node tag label `external:`. This endpoint is idempotent and recommended for machine-created nodes. ### Method PUT ### Endpoint /api/nodes/by-key/:externalKey ### Parameters #### Path Parameters - **externalKey** (string) - Required - The unique external key of the node. #### Request Body - **name** (string) - Required - The name of the node. - **type** (string) - Required - The type of the node (e.g., "vless"). - **raw** (string) - Required - The raw configuration data for the node. - **enabled** (boolean) - Required - Whether the node is enabled. - **tags** (array of strings) - Optional - Tags associated with the node. ### Request Example ```bash curl -fsS -X PUT "https://subman.example.com/api/nodes/by-key/vps-1-vless" \ -H "Authorization: Bearer ${SUBMAN_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["sing-box-vps"]}' ``` ### Response #### Success Response (200) - The response body will typically be empty or confirm the successful update. ``` -------------------------------- ### Implement Node by ID API Route (GET/PATCH) Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Handles GET requests to retrieve a specific node by its ID and PATCH requests to update a node. Includes authentication and error handling. ```typescript import { ApiError, jsonError } from "$lib/server/api/errors"; import { getServerApiEnv } from "$lib/server/api/env"; import { isAuthorized } from "$lib/server/api/auth"; import { applyNodeDelete, applyNodePatch, parseNodePayload } from "$lib/server/api/nodes"; import { loadWorkspaceState, saveWorkspaceState } from "$lib/server/api/workspace"; import { nowIso } from "$lib/utils/time"; async function requireApiAccess(request: Request, platform: App.Platform | undefined) { const env = getServerApiEnv(platform); if (!(await isAuthorized(request.headers.get("Authorization"), env.submanApiToken))) { throw new ApiError(401, "unauthorized", "Unauthorized"); } if (!env.githubToken) { throw new ApiError(500, "server_error", "GITHUB_TOKEN is not configured"); } return env.githubToken; } function handleError(error: unknown): Response { if (error instanceof ApiError) { return jsonError(error); } return jsonError(new ApiError(500, "server_error", "Internal server error")); } export async function GET({ request, platform, params, }: { request: Request; platform?: App.Platform; params: { id: string }; }) { try { const githubToken = await requireApiAccess(request, platform); const workspace = await loadWorkspaceState(githubToken); const node = workspace.state.nodes.find((item) => item.id === params.id); if (!node) { throw new ApiError(404, "not_found", "Node not found"); } return Response.json({ data: node }); } catch (error) { return handleError(error); } } export async function PATCH({ request, platform, params, }: { request: Request; platform?: App.Platform; params: { id: string }; }) { try { const githubToken = await requireApiAccess(request, platform); const workspace = await loadWorkspaceState(githubToken); const payload = parseNodePayload(await request.json()); const result = applyNodePatch(workspace.state, params.id, payload, nowIso()); if (!result.node) { throw new ApiError(404, "not_found", "Node not found"); } ``` -------------------------------- ### Build and Deploy SubMan Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/deployment.md Commands to build the project for production and deploy it. ```bash bun run build bun run deploy ``` -------------------------------- ### Get Node Source: https://github.com/knowsky404/subman/blob/main/docs/api/server-api.md Retrieves a specific node by its ID. ```APIDOC ## GET /api/nodes/:id ### Description Retrieves a specific node by its ID. ### Method GET ### Endpoint /api/nodes/:id ### Parameters #### Path Parameters - **id** (string) - Required - The unique identifier of the node. ### Request Example ```bash curl -sS "https://subman.example.com/api/nodes/node-id" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" ``` ``` -------------------------------- ### Initialize Theme Mode in SvelteKit Source: https://github.com/knowsky404/subman/blob/main/src/app.html This script initializes the theme mode by checking local storage for user preferences or falling back to system preferences. It applies the theme to the document element and sets the color scheme accordingly. Use this at the beginning of your application's head or body to ensure themes are applied early. ```javascript (() => { const storageKey = "subman:theme-mode:v1"; const mediaQuery = "(prefers-color-scheme: dark)"; try { const stored = localStorage.getItem(storageKey); const mode = stored === "light" || stored === "dark" ? stored : "system"; const resolved = mode === "system" && window.matchMedia(mediaQuery).matches ? "dark" : mode === "system" ? "light" : mode; const root = document.documentElement; root.dataset.themeMode = mode; root.style.colorScheme = resolved; if (mode === "system") { delete root.dataset.theme; return; } root.dataset.theme = mode; } catch { document.documentElement.style.colorScheme = window.matchMedia(mediaQuery).matches ? "dark" : "light"; } })(); ``` -------------------------------- ### Bash Script for Git Staging and Committing Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Stages specified SvelteKit API route files and commits them with a descriptive message. ```bash git add src/routes/api/health/+server.ts src/routes/api/nodes/+server.ts src/routes/api/nodes/[id]/+server.ts src/routes/api/nodes/by-key/[externalKey]/+server.ts git commit -m "feat: add server api node routes" ``` -------------------------------- ### Get Node by ID Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Retrieves a specific node by its ID. ```APIDOC ## GET /api/nodes/[id] ### Description Retrieves a specific node by its ID. ### Method GET ### Endpoint /api/nodes/[id] ### Parameters #### Path Parameters - **id** (string) - Required - The ID of the node to retrieve. ### Response #### Success Response (200) - **data** (object) - The requested node object. - **workspace** (object) - Workspace information including gistId and activeGistFile. ``` -------------------------------- ### Get a Specific Node Source: https://context7.com/knowsky404/subman/llms.txt Retrieves a specific node by its internal ID. ```bash # Get a specific node curl -sS "https://subman.example.com/api/nodes/node-id" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" ``` -------------------------------- ### Run Verification Tests Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Execute the provided commands to run full verification for the server API. This includes running specific tests, checking code quality, and building the project. ```bash bun test src/lib/server/api/auth.test.ts src/lib/server/api/nodes.test.ts src/lib/server/api/workspace.test.ts ``` ```bash bun run check ``` ```bash bun run lint ``` ```bash bun run build ``` -------------------------------- ### Get Node by ID Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/server-api.md Retrieves a single node by its unique identifier. ```APIDOC ## GET /api/nodes/:id ### Description Retrieves a single node by its unique identifier. ### Method GET ### Endpoint `/api/nodes/:id` ### Parameters #### Path Parameters - **id** (string) - Required - The unique identifier of the node. ### Authentication Requires `Authorization: Bearer ` header. ``` -------------------------------- ### GET /api/nodes Source: https://context7.com/knowsky404/subman/llms.txt Retrieves all nodes from the workspace Gist's subman.json. Requires SUBMAN_API_TOKEN for authentication. ```APIDOC ## Server API — List Nodes ### `GET /api/nodes` Returns all nodes from the workspace Gist's `subman.json`. Requires `Authorization: Bearer `. ```bash curl -sS "https://subman.example.com/api/nodes" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" # { # "data": [ # { # "id": "node-id", # "name": "vps-1 vless", # "type": "vless", # "raw": "vless://...", # "tags": [], # "enabled": true, # "updatedAt": "2026-05-06T00:00:00.000Z", # "source": "single" # } # ], # "workspace": { "gistId": "gist-id", "file": "subman.json" } # } ``` ``` -------------------------------- ### GET /api/health Source: https://context7.com/knowsky404/subman/llms.txt Checks the health of the Subman API. Returns whether GITHUB_TOKEN and SUBMAN_API_TOKEN are configured. ```APIDOC ## Server API — Health Check ### `GET /api/health` No authentication required. Returns whether `GITHUB_TOKEN` and `SUBMAN_API_TOKEN` are configured as Cloudflare Worker secrets. Secret values are never exposed. ```bash curl -sS "https://subman.example.com/api/health" # { # "ok": true, # "config": { # "githubToken": true, # "submanApiToken": true # } # } ``` ``` -------------------------------- ### Build and Deploy Cloudflare Worker Source: https://context7.com/knowsky404/subman/llms.txt Builds the SvelteKit application for deployment and then deploys it as a Cloudflare Worker. These commands are essential for making the SubMan API live. ```bash # Build and deploy bun run build bun run deploy ``` -------------------------------- ### Get Nodes Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Retrieves a list of nodes. This endpoint is part of the main nodes API route. ```APIDOC ## GET /api/nodes ### Description Retrieves a list of nodes. ### Method GET ### Endpoint /api/nodes ### Response #### Success Response (200) - **data** (array) - A list of node objects. - **workspace** (object) - Workspace information including gistId and activeGistFile. ``` -------------------------------- ### Authentication Header Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/server-api.md All endpoints except GET /api/health require this Authorization header. SUBMAN_API_TOKEN is distinct from a GitHub token. ```http Authorization: Bearer ``` -------------------------------- ### Perform Static and Type Checks Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/deployment.md Commands to run static analysis and type checking for the project. ```bash bun run check bun run lint ``` -------------------------------- ### SvelteKit API Route for Node Creation/Update Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Handles the creation or update of a node in the workspace state. It requires API access, loads workspace state, applies changes, and saves the updated state. ```typescript const gist = await saveWorkspaceState(githubToken, workspace.gist.id, result.state); return Response.json({ data: result.node, workspace: { gistId: gist.id, file: result.state.activeGistFile, }, }); } catch (error) { return handleError(error); } } ``` -------------------------------- ### Get Built-in Region Flag Rules Count Source: https://context7.com/knowsky404/subman/llms.txt Logs the number of built-in region rules. Custom rules take precedence when merged. ```typescript // Built-in rules cover 60+ regions; custom rules take precedence when merged console.log(BUILT_IN_REGION_FLAG_RULES.length); // 60+ ``` -------------------------------- ### Get Node API Request Source: https://github.com/knowsky404/subman/blob/main/docs/api/server-api.md Use this endpoint to retrieve details of a specific node by its ID. Ensure you have a valid API token for authorization. ```http GET /api/nodes/:id ``` ```bash curl -sS "https://subman.example.com/api/nodes/node-id" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" ``` -------------------------------- ### List Nodes Source: https://github.com/knowsky404/subman/blob/main/docs/agents/subman-skill/references/server-api.md Retrieves a list of all configured nodes. ```APIDOC ## GET /api/nodes ### Description Retrieves a list of all configured nodes. ### Method GET ### Endpoint `/api/nodes` ### Authentication Requires `Authorization: Bearer ` header. ``` -------------------------------- ### Add and Commit Documentation Changes Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Stage and commit the documentation changes to the README files. This ensures that the updated API documentation is version controlled. ```bash git add README.md README.en.md ``` ```bash git commit -m "docs: document server api" ``` -------------------------------- ### Implement Workspace State Management Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Provides functions for reading, loading, and saving workspace state. It handles serialization/deserialization of application state to/from a GitHub Gist file. Includes fallback to default state and ensures workspace Gist existence. ```typescript import type { AppState, GistMeta } from "$lib/models"; import { getGistFileContent, updateGist } from "$lib/gist"; import { exportSyncState, importState } from "$lib/serialization"; import { defaultState } from "$lib/stores/app"; import { ensureWorkspaceGist, WORKSPACE_FILE } from "$lib/workspace"; export type WorkspaceState = { gist: GistMeta; state: AppState; }; export function readStateFromWorkspaceContent(content: string): AppState { if (!content.trim()) { return defaultState; } return importState(content); } export async function loadWorkspaceState(githubToken: string): Promise { const initialContent = exportSyncState(defaultState); const { gist } = await ensureWorkspaceGist(githubToken, initialContent); const content = await getGistFileContent(githubToken, gist.id, WORKSPACE_FILE); return { gist, state: { ...readStateFromWorkspaceContent(content), act iveGistId: gist.id, act iveGistFile: WORKSPACE_FILE, }, }; } export async function saveWorkspaceState( githubToken: string, gistId: string, state: AppState, ): Promise { return updateGist(githubToken, { gistId, files: { [WORKSPACE_FILE]: { content: exportSyncState(state) }, }, }); } ``` -------------------------------- ### Test Auth Helpers with Bun Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Write and run failing tests for `getBearerToken` and `isAuthorized` using Bun's testing framework. Ensure tests fail before implementing the functions. ```typescript import { describe, expect, it } from "bun:test"; import { getBearerToken, isAuthorized } from "./auth"; describe("getBearerToken", () => { it("extracts a bearer token from an authorization header", () => { expect(getBearerToken("Bearer subman-secret")).toBe("subman-secret"); }); it("rejects missing or non-bearer authorization values", () => { expect(getBearerToken(null)).toBeNull(); expect(getBearerToken("Basic abc")).toBeNull(); expect(getBearerToken("Bearer ")).toBeNull(); }); }); describe("isAuthorized", () => { it("accepts matching bearer tokens", async () => { expect(await isAuthorized("Bearer subman-secret", "subman-secret")).toBe(true); }); it("rejects missing configured tokens and mismatched request tokens", async () => { expect(await isAuthorized("Bearer subman-secret", "")).toBe(false); expect(await isAuthorized("Bearer wrong", "subman-secret")).toBe(false); expect(await isAuthorized(null, "subman-secret")).toBe(false); }); }); ``` -------------------------------- ### Build Aggregate Output Source: https://context7.com/knowsky404/subman/llms.txt Constructs a base64-encoded subscription string based on a defined rule, applying exclusions, renames, type filtering, and sorting. It also fetches live subscription URLs and returns the processed content along with metadata like line count and warnings. ```typescript import { buildAggregateOutput } from "$lib/aggregate"; import type { AggregateRule, NodeItem, SubscriptionItem } from "$lib/models"; const rule: AggregateRule = { id: "r1", name: "Filtered Output", nodeIds: ["n1", "n2"], subscriptionIds: ["s1"], excludeTagIds: ["blocked"], renameMap: {}, renameRules: [ "/^vps-(\d+) vless/=SRV-$1 VLESS", // regex: /pattern/flags=replacement "old name = new name", // literal: exact match = replacement ], allowedTypes: ["vless", "vmess", "trojan"], prependRegionFlags: true, customRegionFlagMap: "JP = Tokyo, Osaka\nSG = Singapore", sortMode: "region", sortPriority: "JP\nUS\nSG", updatedAt: "2026-05-06T00:00:00.000Z", }; const nodes: NodeItem[] = [ { id: "n1", name: "vps-1 vless", type: "vless", raw: "vless://...#vps-1 vless", tags: [], enabled: true, updatedAt: "2026-05-06T00:00:00.000Z", source: "single" }, ]; const subscriptions: SubscriptionItem[] = [ { id: "s1", name: "Provider A", url: "https://example.com/sub", enabled: true, tags: [], updatedAt: "2026-05-06T00:00:00.000Z" }, ]; const result = await buildAggregateOutput(rule, nodes, subscriptions); // { // content: "🇯🇵 SRV-1 VLESS\nvless://...#%F0%9F%87%AF%F0%9F%87%B5%20SRV-1%20VLESS\n...", // lines: 12, // warnings: [], // e.g. ["Failed to fetch https://..."] // errors: [] // e.g. ["Error: timeout"] // } console.log(`Published ${result.lines} nodes`); ``` -------------------------------- ### Start and Read Auto Sync Status Source: https://context7.com/knowsky404/subman/llms.txt Initiates automatic synchronization with a specified debounce delay and provides a function to read the current sync status. Listen for real-time status updates using a CustomEvent. ```typescript import { startAutoSync, readAutoSyncStatus, getAutoSyncStatusEventName } from "$lib/sync"; // Start auto-sync (typically called in +layout.svelte onMount) const stopSync = startAutoSync(1200); // debounce delay in ms // Read persisted sync status (useful for UI indicators) const status = readAutoSyncStatus(); // { // status: "success", // idle | syncing | success | error // gistId: "abc123", // lastAttemptAt: "2026-05-06T...", // lastSuccessAt: "2026-05-06T...", // lastErrorAt: null, // lastErrorMessage: null, // lastSyncedFile: "subman.json" // } // Listen for real-time sync status changes window.addEventListener(getAutoSyncStatusEventName(), (event) => { const detail = (event as CustomEvent).detail; // AutoSyncStatus console.log("Sync status:", detail.status); }); // Stop sync when component is destroyed onDestroy(() => stopSync()); ``` -------------------------------- ### List All Nodes from Workspace Gist Source: https://context7.com/knowsky404/subman/llms.txt Retrieves all nodes from the workspace Gist's subman.json. Requires Authorization header with SUBMAN_API_TOKEN. ```bash curl -sS "https://subman.example.com/api/nodes" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" # { # "data": [ # { # "id": "node-id", # "name": "vps-1 vless", # "type": "vless", # "raw": "vless://...", # "tags": [], # "enabled": true, # "updatedAt": "2026-05-06T00:00:00.000Z", # "source": "single" # } # ], # "workspace": { "gistId": "gist-id", "file": "subman.json" } # } ``` -------------------------------- ### GitHub Gist API Wrappers Source: https://context7.com/knowsky404/subman/llms.txt Provides low-level wrappers for the GitHub Gist API. All functions require a GitHub gist-scoped token. These functions allow for listing, retrieving, creating, and updating gists, as well as converting gist URLs. ```APIDOC ## listGists, getGist, getGistFileContent, createGist, updateGist, toStableGistRawUrl ### Description Low-level wrappers around the GitHub Gist API. All functions require a GitHub `gist`-scoped token. `toStableGistRawUrl` strips the commit hash from a raw Gist URL so the link stays stable across updates. ### Functions - **listGists**(token: string): Promise> Lists all gists for the authenticated user. - **getGist**(token: string, gistId: string): Promise Gets a single gist by ID. - **getGistFileContent**(token: string, gistId: string, filename: string): Promise Reads a specific file from a gist. - **createGist**(token: string, gistData: CreateGistData): Promise Creates a new gist. - **updateGist**(token: string, updateData: UpdateGistData): Promise Updates files in an existing gist. - **toStableGistRawUrl**(rawUrl: string): string Converts a versioned raw URL to a stable (no commit hash) raw URL. ### Parameters - **token** (string) - Required - A GitHub gist-scoped token. - **gistId** (string) - Required - The ID of the gist. - **filename** (string) - Required - The name of the file within the gist. - **gistData** (object) - Required - Data for creating a new gist. - **description** (string) - Optional - Description for the gist. - **isPublic** (boolean) - Optional - Whether the gist is public. - **files** (object) - Required - Files to include in the gist. - **[filename]** (object) - Required - File content and details. - **content** (string) - Required - The content of the file. - **updateData** (object) - Required - Data for updating an existing gist. - **gistId** (string) - Required - The ID of the gist to update. - **files** (object) - Required - Files to update or delete. - **[filename]** (object | null) - Required - File content or null to delete. - **content** (string) - Required - The content of the file. - **rawUrl** (string) - Required - The raw URL of a gist file. ### Response Example (listGists) ```json [ { "id": "gist-id", "description": "SubMan-Data", "updatedAt": "2023-10-27T10:00:00Z", "url": "https://api.github.com/gists/gist-id", "files": [ { "filename": "subman.json", "language": "JSON", "size": 1234, "rawUrl": "https://gist.githubusercontent.com/user/gist-id/raw/commit-hash/subman.json" } ] } ] ``` ### Response Example (getGist) ```json { "id": "gist-id", "description": "SubMan-Data", "updatedAt": "2023-10-27T10:00:00Z", "url": "https://api.github.com/gists/gist-id", "files": [ { "filename": "subman.json", "language": "JSON", "size": 1234, "rawUrl": "https://gist.githubusercontent.com/user/gist-id/raw/commit-hash/subman.json" } ] } ``` ### Response Example (getGistFileContent) ```json { "version": 1, "exportedAt": "2026-05-06T12:00:00.000Z", "data": { "nodes": [], "subscriptions": [], "aggregates": [], "publishTargets": [] } } ``` ### Response Example (createGist) ```json { "id": "new-gist-id", "description": "SubMan-Data", "isPublic": false, "owner": { "login": "user" }, "files": { "subman.json": { "content": "{\"version\": 1, \"data\": {}}" } }, "updatedAt": "2023-10-27T11:00:00Z", "url": "https://api.github.com/gists/new-gist-id" } ``` ### Response Example (updateGist) ```json { "id": "gist-id", "description": "SubMan-Data", "owner": { "login": "user" }, "files": { "output.txt": { "content": "vless://...\nvmess://..." } }, "updatedAt": "2023-10-27T12:00:00Z", "url": "https://api.github.com/gists/gist-id" } ``` ### Response Example (toStableGistRawUrl) ```json "https://gist.githubusercontent.com/user/gist-id/raw/output.txt" ``` ``` -------------------------------- ### Local Cloudflare Workers Preview Source: https://context7.com/knowsky404/subman/llms.txt Runs the Cloudflare Worker locally using `wrangler` for previewing and testing the deployment environment. This simulates the Cloudflare runtime. ```bash # Local Cloudflare Workers preview (uses wrangler) bun run dev:cf ``` -------------------------------- ### GET /api/nodes/:id, PATCH /api/nodes/:id, DELETE /api/nodes/:id Source: https://context7.com/knowsky404/subman/llms.txt Read, partially update, or delete a node by its internal ID. PATCH accepts a subset of the create body; DELETE also removes the node ID from aggregate rules. ```APIDOC ## Server API — Get / Update / Delete Node ### `GET /api/nodes/:id`, `PATCH /api/nodes/:id`, `DELETE /api/nodes/:id` Read, partially update, or delete a node by its internal ID. `PATCH` accepts any subset of the node create body; missing fields keep their existing values. `DELETE` also removes the node ID from all aggregate rules' `nodeIds` arrays. ```bash # Get a specific node curl -sS "https://subman.example.com/api/nodes/node-id" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" # Partially update (disable and rename) curl -sS -X PATCH "https://subman.example.com/api/nodes/node-id" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled": false, "name": "vps-1 disabled"}' ``` ``` -------------------------------- ### Create a New Node Source: https://context7.com/knowsky404/subman/llms.txt Creates a new node. For automation, prefer PUT /api/nodes/by-key/:externalKey to avoid duplicates. Returns 201 Created on success. ```bash curl -sS -X POST "https://subman.example.com/api/nodes" \ -H "Authorization: Bearer $SUBMAN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "vps-1 vless", "type": "vless", "raw": "vless://uuid@host:443?...", "enabled": true, "tags": ["manual", "hk"] }' # { # "data": { "id": "...", "name": "vps-1 vless", "type": "vless", ... }, # "workspace": { "gistId": "...", "file": "subman.json" } # } ``` -------------------------------- ### Implement Auth Helpers Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Implement `getBearerToken` to extract tokens from Authorization headers and `isAuthorized` to securely compare tokens using SHA-256 hashing and timing-safe comparison. Requires `crypto.subtle`. ```typescript const BEARER_PREFIX = "Bearer "; function toBytes(value: string): Uint8Array { return new TextEncoder().encode(value); } async function sha256(value: string): Promise { return crypto.subtle.digest("SHA-256", toBytes(value)); } export function getBearerToken(authorization: string | null): string | null { if (!authorization?.startsWith(BEARER_PREFIX)) { return null; } const token = authorization.slice(BEARER_PREFIX.length).trim(); return token ? token : null; } export async function isAuthorized( authorization: string | null, configuredToken: string | undefined, ): Promise { const requestToken = getBearerToken(authorization); if (!requestToken || !configuredToken) { return false; } const [requestHash, configuredHash] = await Promise.all([ sha256(requestToken), sha256(configuredToken), ]); return crypto.subtle.timingSafeEqual(requestHash, configuredHash); } ``` -------------------------------- ### Implement Server API Environment Helper Source: https://github.com/knowsky404/subman/blob/main/docs/superpowers/plans/2026-05-06-server-api.md Provides a function to retrieve server API environment variables, prioritizing platform-specific environment variables over private dynamic environment variables. This ensures flexibility in token management. ```typescript import { env as privateEnv } from "$env/dynamic/private"; export type ServerApiEnv = { githubToken?: string; submanApiToken?: string; }; export function getServerApiEnv(platform: App.Platform | undefined): ServerApiEnv { return { githubToken: platform?.env?.GITHUB_TOKEN ?? privateEnv.GITHUB_TOKEN, submanApiToken: platform?.env?.SUBMAN_API_TOKEN ?? privateEnv.SUBMAN_API_TOKEN, }; } ``` -------------------------------- ### Subscription Content Utilities Source: https://context7.com/knowsky404/subman/llms.txt Provides utilities for fetching, decoding, and normalizing subscription content from various sources. It handles base64 encoding, splitting concatenated URIs, and inferring node types and names. ```APIDOC ## Subscription Content Utilities (`src/lib/subscription.ts`) Utilities for fetching, decoding, and normalizing subscription content. Handles base64-encoded responses (common for clash/v2ray subscriptions), splits concatenated proxy URIs on a single line, and infers proxy type and display name from a raw URI string. ### Usage ```typescript import { loadSubscriptionContent, normalizeSubscriptionContent, extractSubscriptionNodeLines, inferNodeTypeFromRaw, inferNodeNameFromRaw, looksLikeBase64, decodeBase64Utf8, } from "$lib/subscription"; // Fetch a subscription URL and auto-decode base64 if needed const { content, warning } = await loadSubscriptionContent( "https://example.com/sub?token=abc" ); if (warning) console.warn(warning); // e.g. "Failed to fetch https://..." // content: "vless://...\nvmess://...\ntrojan://..." // Normalize multi-line content: split concatenated URIs, dedupe blank lines const normalized = normalizeSubscriptionContent( "vless://aaa#Node1\nvmess://bbb#Node2\n\n" ); // "vless://aaa#Node1\nvmess://bbb#Node2" // Extract only lines that contain "://" const lines = extractSubscriptionNodeLines(content); // ["vless://...", "vmess://...", ...] // Infer proxy type from raw URI scheme inferNodeTypeFromRaw("hy2://..."); // "hysteria2" inferNodeTypeFromRaw("vless://...#My Node"); // "vless" inferNodeTypeFromRaw("unknown://..."); // "other" // Infer display name from raw URI (URL-encoded fragment or vmess ps field) inferNodeNameFromRaw("vless://...#My%20Node", "fallback"); // "My Node" inferNodeNameFromRaw("vmess://BASE64_PAYLOAD", "fallback"); // ps field from JSON payload ``` ### Functions * **`loadSubscriptionContent(url: string)`**: Fetches content from a URL, attempting to auto-decode base64. Returns `{ content: string, warning?: string }`. * **`normalizeSubscriptionContent(content: string)`**: Cleans up subscription content by splitting URIs and removing blank lines. * **`extractSubscriptionNodeLines(content: string)`**: Extracts individual subscription node lines from the content. * **`inferNodeTypeFromRaw(raw: string)`**: Infers the proxy type from a raw subscription URI string. * **`inferNodeNameFromRaw(raw: string, fallback?: string)`**: Infers a display name for a subscription node. * **`looksLikeBase64(str: string)`**: Checks if a string appears to be base64 encoded. * **`decodeBase64Utf8(encoded: string)`**: Decodes a base64 string to UTF-8. ```