cinatra 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Pure URL-shape helper for the MCP server's `publicBaseUrl`.
3
+ //
4
+ // Plain ESM `.mjs`, NO imports, NO `server-only`, NO `@/` aliases. Importable
5
+ // from anywhere:
6
+ // - TS in-process callers (`packages/mcp-server/src/llm-credentials.ts`)
7
+ // via `import { normaliseMcpPublicBaseUrl } from "./mcp-public-base-url-shape.mjs"`.
8
+ // - The plain-`.mjs` Cinatra CLI (`packages/cli/src/index.mjs`) directly.
9
+ //
10
+ // Why a separate file: the CLI must write `mcp_server.publicBaseUrl` into a
11
+ // clone's Postgres DB, but the existing TS
12
+ // `setMcpPublicBaseUrl` lives behind `server-only` + Next path aliases and
13
+ // `@cinatra-ai/mcp-server` ships no compiled `dist/`. Extracting only the pure
14
+ // URL-shape rules (validation, normalisation, origin-only contract) into this
15
+ // module gives both writers a single source of truth without forcing either
16
+ // to bend toward the other's import constraints.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * The metadata row key under which the MCP server's settings live. Stable
21
+ * across the writers; matches `MCP_SETTINGS_KEY` in
22
+ * `packages/mcp-server/src/llm-credentials.ts` and `packages/cli/src/index.mjs`.
23
+ */
24
+ export const MCP_PUBLIC_BASE_URL_METADATA_KEY = "connector_config:mcp_server";
25
+
26
+ /**
27
+ * The set of `publicBaseUrlSource` values callers can request when writing.
28
+ * `"unknown"` is reserved for the null-URL path (cannot be passed in).
29
+ *
30
+ * - `"manual"` (default): operator pasted the URL into the dev tab form.
31
+ * - `"tailscale-auto"`: `cinatra clone start` minted a
32
+ * Tailscale Funnel URL via the Nango-stored OAuth client.
33
+ * - `"tailscale-funnel"`: Tailscale sidecar path that read `TS_AUTHKEY`
34
+ * from env.
35
+ *
36
+ * @typedef {"manual" | "tailscale-auto" | "tailscale-funnel"} McpPublicBaseUrlSource
37
+ */
38
+
39
+ /**
40
+ * Normalise an operator-supplied (or env-derived) MCP public base URL.
41
+ * Returns the canonical origin-only form plus the matching
42
+ * `publicBaseUrlSource`.
43
+ *
44
+ * Rules:
45
+ * - `null` / `undefined` / empty / whitespace → `{ url: null, source: "unknown" }`.
46
+ * - Trailing slashes stripped before parsing.
47
+ * - Scheme MUST be http(s); other schemes throw.
48
+ * - URL MUST be origin-only — no path, no query, no fragment. Throws otherwise.
49
+ * - Successful normalisation: `{ url, source: options.source ?? "manual" }`.
50
+ *
51
+ * Error messages mirror the prior in-place validation in
52
+ * `setMcpPublicBaseUrl()` so callers who relied on the wording continue to see
53
+ * an equivalent rejection.
54
+ *
55
+ * @param {string | null | undefined} input
56
+ * @param {{ source?: McpPublicBaseUrlSource }} [options]
57
+ * @returns {{ url: string | null, source: McpPublicBaseUrlSource | "unknown" }}
58
+ */
59
+ export function normaliseMcpPublicBaseUrl(input, options) {
60
+ if (input == null) return { url: null, source: "unknown" };
61
+ if (typeof input !== "string") {
62
+ return { url: null, source: "unknown" };
63
+ }
64
+ // Strip trailing slashes via a LINEAR char-index trim. The previous
65
+ // `/\/+$/` is an anchored greedy slash-repetition — polynomial-ReDoS on
66
+ // input with many trailing slashes (CodeQL js/polynomial-redos, high).
67
+ const trimmedInput = input.trim();
68
+ let trimEnd = trimmedInput.length;
69
+ while (trimEnd > 0 && trimmedInput.charCodeAt(trimEnd - 1) === 47) trimEnd--; // 47 = "/"
70
+ const trimmed = trimmedInput.slice(0, trimEnd);
71
+ if (trimmed.length === 0) return { url: null, source: "unknown" };
72
+
73
+ let parsed;
74
+ try {
75
+ parsed = new URL(trimmed);
76
+ } catch {
77
+ throw new Error(
78
+ `MCP publicBaseUrl: URL must be a valid http(s)://… origin, got ${JSON.stringify(input)}`,
79
+ );
80
+ }
81
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
82
+ throw new Error(
83
+ `MCP publicBaseUrl: URL must use http(s) scheme, got ${JSON.stringify(input)}`,
84
+ );
85
+ }
86
+ if (parsed.pathname !== "/" && parsed.pathname !== "") {
87
+ throw new Error(
88
+ `MCP publicBaseUrl: URL must be an origin without a path (got ${JSON.stringify(input)}). ` +
89
+ `Save just the host, e.g. https://my-tunnel.example.ts.net`,
90
+ );
91
+ }
92
+ if (parsed.search.length > 0 || parsed.hash.length > 0) {
93
+ throw new Error(
94
+ `MCP publicBaseUrl: URL must not have a query string or fragment (got ${JSON.stringify(input)}).`,
95
+ );
96
+ }
97
+ const requestedSource = options?.source;
98
+ const source =
99
+ requestedSource === "tailscale-auto" || requestedSource === "tailscale-funnel"
100
+ ? requestedSource
101
+ : "manual";
102
+ return { url: `${parsed.protocol}//${parsed.host}`, source };
103
+ }
104
+
105
+ /**
106
+ * Compute the next metadata-row body for a publicBaseUrl write, preserving
107
+ * any sibling fields and dropping retired columns. Mirrors the in-place
108
+ * merge in `setMcpPublicBaseUrl()` so the CLI writer + the in-process writer
109
+ * produce byte-equivalent rows for the same input.
110
+ *
111
+ * @param {Record<string, unknown>} current Existing row contents (may be empty).
112
+ * @param {string | null | undefined} url Operator-supplied URL.
113
+ * @param {{ source?: McpPublicBaseUrlSource }} [options] Tag the write with a
114
+ * non-default `publicBaseUrlSource` (e.g. `"tailscale-auto"` for the
115
+ * auto-tunnel path). Defaults to `"manual"`.
116
+ * @returns {Record<string, unknown>}
117
+ */
118
+ export function buildMcpPublicBaseUrlRow(current, url, options) {
119
+ const { url: nextUrl, source: nextSource } = normaliseMcpPublicBaseUrl(
120
+ url,
121
+ options,
122
+ );
123
+ const next = {
124
+ ...current,
125
+ publicBaseUrl: nextUrl,
126
+ publicBaseUrlSource: nextSource,
127
+ updatedAt: new Date().toISOString(),
128
+ };
129
+ // Drop fields no longer used by the public base URL row shape.
130
+ delete next.tunnelMode;
131
+ delete next.externalUrl;
132
+ delete next.cloudflaredMissing;
133
+ return next;
134
+ }