refactor: split oversized modules and start server src/stores layout
Consolidate format helpers under lib/format, split React Query hooks by domain, extract AuthPicker/ConfigRefHint and HTTP trigger routing, demote file-private exports, and move KV/secrets/variables/profiles/http-auths stores under src/stores with root re-exports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,390 +1 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { db } from "./db.js";
|
||||
import { assertHttpStatus } from "./http-pages-store.js";
|
||||
|
||||
const MAX_NAME_LENGTH = 128;
|
||||
const NAME_RE = /^[A-Za-z0-9._-]+$/;
|
||||
const UUID_RE =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
const ALLOWED_TYPES = new Set(["bearer", "basic", "header"]);
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} id
|
||||
* @returns {string}
|
||||
*/
|
||||
export function assertAuthId(id) {
|
||||
if (typeof id !== "string" || !UUID_RE.test(id)) {
|
||||
const err = new Error("invalid auth id");
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
return id.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function assertAuthName(name) {
|
||||
if (typeof name !== "string" || !NAME_RE.test(name)) {
|
||||
const err = new Error("invalid auth name");
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
const err = new Error(`auth name must be at most ${MAX_NAME_LENGTH} characters`);
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} type
|
||||
* @returns {"bearer" | "basic" | "header"}
|
||||
*/
|
||||
export function assertAuthType(type) {
|
||||
const t = String(type ?? "");
|
||||
if (!ALLOWED_TYPES.has(t)) {
|
||||
const err = new Error('auth type must be "bearer", "basic", or "header"');
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
return /** @type {"bearer" | "basic" | "header"} */ (t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect value source without exposing literal values.
|
||||
* @param {unknown} value
|
||||
* @returns {"literal" | "kv" | "secret" | "missing"}
|
||||
*/
|
||||
export function valueSourceKind(value) {
|
||||
if (value == null) return "missing";
|
||||
if (typeof value === "string") return "literal";
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
if ("secret" in value) return "secret";
|
||||
if ("kv" in value) return "kv";
|
||||
}
|
||||
return "literal";
|
||||
}
|
||||
|
||||
/**
|
||||
* Redact config for API responses: replace literal strings with source markers.
|
||||
* @param {Record<string, unknown>} config
|
||||
* @param {string} type
|
||||
*/
|
||||
export function publicConfig(config, type) {
|
||||
/** @type {Record<string, unknown>} */
|
||||
const out = {};
|
||||
if (type === "bearer") {
|
||||
out.token = redactField(config.token);
|
||||
} else if (type === "basic") {
|
||||
out.user = redactField(config.user);
|
||||
out.password = redactField(config.password);
|
||||
} else if (type === "header") {
|
||||
out.header = typeof config.header === "string" ? config.header : null;
|
||||
out.value = redactField(config.value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
*/
|
||||
function redactField(value) {
|
||||
const kind = valueSourceKind(value);
|
||||
if (kind === "missing") return { source: "missing" };
|
||||
if (kind === "kv") {
|
||||
const v = /** @type {{ kv: string, namespace?: string }} */ (value);
|
||||
return {
|
||||
source: "kv",
|
||||
kv: v.kv,
|
||||
...(v.namespace != null ? { namespace: v.namespace } : {}),
|
||||
};
|
||||
}
|
||||
if (kind === "secret") {
|
||||
const v = /** @type {{ secret: string }} */ (value);
|
||||
return { source: "secret", secret: v.secret };
|
||||
}
|
||||
return { source: "literal", set: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize auth config for storage.
|
||||
* @param {string} type
|
||||
* @param {unknown} config
|
||||
* @param {{ keepLiteralsFrom?: Record<string, unknown> }} [opts]
|
||||
*/
|
||||
export function normalizeAuthConfig(type, config, opts = {}) {
|
||||
const raw = config && typeof config === "object" && !Array.isArray(config)
|
||||
? /** @type {Record<string, unknown>} */ (config)
|
||||
: {};
|
||||
const keep = opts.keepLiteralsFrom ?? {};
|
||||
|
||||
if (type === "bearer") {
|
||||
return {
|
||||
token: normalizeCredentialField(raw.token, keep.token, "token"),
|
||||
};
|
||||
}
|
||||
if (type === "basic") {
|
||||
return {
|
||||
user: normalizeCredentialField(raw.user, keep.user, "user"),
|
||||
password: normalizeCredentialField(raw.password, keep.password, "password", {
|
||||
allowEmpty: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
// header
|
||||
if (typeof raw.header !== "string" || raw.header.length === 0) {
|
||||
const err = new Error("header name must be a non-empty string");
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
header: raw.header,
|
||||
value: normalizeCredentialField(raw.value, keep.value, "value"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @param {unknown} previous
|
||||
* @param {string} label
|
||||
* @param {{ allowEmpty?: boolean }} [opts]
|
||||
*/
|
||||
function normalizeCredentialField(value, previous, label, opts = {}) {
|
||||
// Explicit "keep previous literal" marker from UI when editing without re-entering
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
/** @type {{ keep?: boolean }} */ (value).keep === true
|
||||
) {
|
||||
if (typeof previous === "string") return previous;
|
||||
if (previous && typeof previous === "object") return previous;
|
||||
const err = new Error(`${label} was not previously set`);
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (value == null || value === "") {
|
||||
if (opts.allowEmpty && value === "") return "";
|
||||
// Allow empty password for basic
|
||||
if (opts.allowEmpty && (value === "" || value == null)) {
|
||||
if (typeof previous === "string") return previous;
|
||||
return "";
|
||||
}
|
||||
const err = new Error(`${label} is required`);
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (typeof value === "string") return value;
|
||||
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
const v = /** @type {Record<string, unknown>} */ (value);
|
||||
if (typeof v.secret === "string" && v.secret.length > 0) {
|
||||
return { secret: v.secret };
|
||||
}
|
||||
if (typeof v.kv === "string" && v.kv.length > 0) {
|
||||
/** @type {{ kv: string, namespace?: string }} */
|
||||
const out = { kv: v.kv };
|
||||
if (typeof v.namespace === "string" && v.namespace.length > 0) {
|
||||
out.namespace = v.namespace;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
const err = new Error(
|
||||
`${label} must be a string, { kv }, { secret }, or { keep: true }`,
|
||||
);
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
function parseConfig(raw) {
|
||||
if (typeof raw !== "string") return raw ?? {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function publicAuth(row, { includeConfig = true } = {}) {
|
||||
const type = row.type;
|
||||
const config = parseConfig(row.config);
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type,
|
||||
...(includeConfig ? { config: publicConfig(config, type) } : {}),
|
||||
unauthorized_status: row.unauthorized_status ?? null,
|
||||
unauthorized_response: row.unauthorized_response ?? null,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: full config including literals (for runtime auth checks).
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function getHttpAuthInternal(id) {
|
||||
const authId = assertAuthId(id);
|
||||
const row = await db("http_auths").where({ id: authId }).first();
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
config: parseConfig(row.config),
|
||||
unauthorized_status: row.unauthorized_status ?? null,
|
||||
unauthorized_response: row.unauthorized_response ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only plaintext literal credential fields (not KV refs or encrypted secrets).
|
||||
* @param {string} id
|
||||
* @returns {Promise<{ id: string, name: string, type: string, literals: Record<string, string> } | null>}
|
||||
*/
|
||||
export async function revealHttpAuthLiterals(id) {
|
||||
const internal = await getHttpAuthInternal(id);
|
||||
if (!internal) return null;
|
||||
/** @type {Record<string, string>} */
|
||||
const literals = {};
|
||||
const cfg = internal.config ?? {};
|
||||
for (const key of ["token", "user", "password", "value"]) {
|
||||
const v = cfg[key];
|
||||
if (typeof v === "string") literals[key] = v;
|
||||
}
|
||||
return {
|
||||
id: internal.id,
|
||||
name: internal.name,
|
||||
type: internal.type,
|
||||
literals,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listHttpAuths() {
|
||||
const rows = await db("http_auths").select("*").orderBy("name", "asc");
|
||||
return rows.map((r) => publicAuth(r));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function getHttpAuthById(id) {
|
||||
let authId;
|
||||
try {
|
||||
authId = assertAuthId(id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const row = await db("http_auths").where({ id: authId }).first();
|
||||
return row ? publicAuth(row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* id?: string | null,
|
||||
* name: string,
|
||||
* type: string,
|
||||
* config?: unknown,
|
||||
* unauthorized_status?: number | null,
|
||||
* unauthorized_response?: string | null,
|
||||
* }} opts
|
||||
*/
|
||||
export async function upsertHttpAuth({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
config,
|
||||
unauthorized_status,
|
||||
unauthorized_response,
|
||||
}) {
|
||||
const authName = assertAuthName(name);
|
||||
const authType = assertAuthType(type);
|
||||
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let existing = null;
|
||||
if (id != null && String(id).length > 0) {
|
||||
const authId = assertAuthId(id);
|
||||
existing = await db("http_auths").where({ id: authId }).first();
|
||||
if (!existing) {
|
||||
const err = new Error("auth not found");
|
||||
err.statusCode = 404;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const nameClash = await db("http_auths").where({ name: authName }).first();
|
||||
if (nameClash && (!existing || nameClash.id !== existing.id)) {
|
||||
const err = new Error(`auth name "${authName}" already exists`);
|
||||
err.statusCode = 409;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const prevConfig = existing ? parseConfig(existing.config) : {};
|
||||
const normalized = normalizeAuthConfig(authType, config, {
|
||||
keepLiteralsFrom: prevConfig,
|
||||
});
|
||||
|
||||
let unauthStatus = null;
|
||||
if (unauthorized_status != null && unauthorized_status !== "") {
|
||||
unauthStatus = assertHttpStatus(unauthorized_status, 401);
|
||||
}
|
||||
let unauthResponse = null;
|
||||
if (
|
||||
unauthorized_response != null &&
|
||||
String(unauthorized_response).length > 0
|
||||
) {
|
||||
unauthResponse = String(unauthorized_response);
|
||||
}
|
||||
|
||||
const now = nowIso();
|
||||
const configJson = JSON.stringify(normalized);
|
||||
|
||||
if (existing) {
|
||||
await db("http_auths")
|
||||
.where({ id: existing.id })
|
||||
.update({
|
||||
name: authName,
|
||||
type: authType,
|
||||
config: configJson,
|
||||
unauthorized_status: unauthStatus,
|
||||
unauthorized_response: unauthResponse,
|
||||
updated_at: now,
|
||||
});
|
||||
return getHttpAuthById(/** @type {string} */ (existing.id));
|
||||
}
|
||||
|
||||
const newId = randomUUID();
|
||||
await db("http_auths").insert({
|
||||
id: newId,
|
||||
name: authName,
|
||||
type: authType,
|
||||
config: configJson,
|
||||
unauthorized_status: unauthStatus,
|
||||
unauthorized_response: unauthResponse,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
return getHttpAuthById(newId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function deleteHttpAuth(id) {
|
||||
const authId = assertAuthId(id);
|
||||
const n = await db("http_auths").where({ id: authId }).del();
|
||||
return n > 0;
|
||||
}
|
||||
export * from "./src/stores/http-auths-store.js";
|
||||
|
||||
Reference in New Issue
Block a user