railcode 0.1.22 → 0.1.24
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.
- package/README.md +17 -0
- package/dist/connections.js +179 -0
- package/dist/connectors.js +4 -1
- package/dist/index.js +1005 -3
- package/dist/observability.js +220 -0
- package/dist/orgadmin.js +227 -0
- package/package.json +1 -1
- package/static/sdk.js +76 -8
package/README.md
CHANGED
|
@@ -22,10 +22,27 @@ railcode login --setup-token <token> Sign in with a one-time onboarding setup t
|
|
|
22
22
|
railcode init <app> [--template react|static]
|
|
23
23
|
railcode deploy Build (if configured) and deploy the app here
|
|
24
24
|
railcode db <list|query> ... List data connectors / run read-only SQL
|
|
25
|
+
railcode connections <list|create|delete> ... Manage org data connectors (admin)
|
|
26
|
+
railcode connector <list|docs|fetch|native|enable|create|delete> ...
|
|
27
|
+
Call service connectors / manage them (admin)
|
|
25
28
|
railcode agent <list|show|create|update|delete|run|schedule> ...
|
|
26
29
|
Manage org-scoped managed agents
|
|
30
|
+
railcode members <list|set-role|remove|add> ... Members + system roles (admin)
|
|
31
|
+
railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|revoke|materialize|effective|catalog> ...
|
|
32
|
+
Org roles + the granular grants table (admin)
|
|
33
|
+
railcode apps <list|show|access|set-access|transfer|delete> ...
|
|
34
|
+
Apps + access policy (owner/admin)
|
|
35
|
+
railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics
|
|
36
|
+
railcode logs <connector|service-connector|llm|email|agent> [filters]
|
|
37
|
+
Org observability logs (admin)
|
|
27
38
|
```
|
|
28
39
|
|
|
40
|
+
The `members`, `roles`, `apps`, `analytics`, `logs`, `connections`, and the
|
|
41
|
+
`connector` admin subcommands mirror the web console's admin surfaces — everything
|
|
42
|
+
an owner/admin can do in the dashboard is available from the CLI. They all target
|
|
43
|
+
your saved org and are gated server-side by the same capabilities as the console
|
|
44
|
+
(a plain member gets 403; app list/show/access follow the per-app access policy).
|
|
45
|
+
|
|
29
46
|
- **`login`** prompts for the server URL (default `https://api.railcode.app`),
|
|
30
47
|
email, and password. It exchanges credentials for a short-lived JWT, mints a
|
|
31
48
|
personal API token, resolves your organization, and saves everything to
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Pure helpers behind `railcode connections` (org data connections) and the admin
|
|
2
|
+
// half of `railcode connector` (service-connector CRUD + native presets). No I/O or
|
|
3
|
+
// process state — the command layer in index.ts does the HTTP and printing.
|
|
4
|
+
import { formatTable } from "./db.js";
|
|
5
|
+
export class ConnectionAdminError extends Error {
|
|
6
|
+
}
|
|
7
|
+
function fmtTime(iso) {
|
|
8
|
+
if (!iso)
|
|
9
|
+
return "-";
|
|
10
|
+
return iso.slice(0, 16).replace("T", " ");
|
|
11
|
+
}
|
|
12
|
+
// Parse a JSON object option (`--config`, `--credentials`, `--auth-config`).
|
|
13
|
+
// Empty/undefined → {}. Non-object JSON is rejected.
|
|
14
|
+
export function parseJsonObject(value, label) {
|
|
15
|
+
if (value === undefined || value.trim() === "")
|
|
16
|
+
return {};
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(value);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new ConnectionAdminError(`${label} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
23
|
+
}
|
|
24
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25
|
+
throw new ConnectionAdminError(`${label} must be a JSON object (e.g. '{"key":"value"}').`);
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
// ── Data connections (postgres | bigquery | turso) ──────────────────────────
|
|
30
|
+
// The engines the create API accepts (snowflake is enum-only, rejected on create).
|
|
31
|
+
export const CONNECTION_ENGINES = ["postgres", "bigquery", "turso"];
|
|
32
|
+
// Per-engine hint of the config/credentials fields, surfaced in --help and errors.
|
|
33
|
+
export const ENGINE_FIELDS = {
|
|
34
|
+
postgres: {
|
|
35
|
+
config: ["host", "database", "username", "port?", "sslmode?"],
|
|
36
|
+
credentials: ["password"],
|
|
37
|
+
},
|
|
38
|
+
bigquery: { config: ["project_id", "dataset"], credentials: ["service_account_json"] },
|
|
39
|
+
turso: { config: ["url"], credentials: ["auth_token"] },
|
|
40
|
+
};
|
|
41
|
+
export function parseConnectionEngine(value) {
|
|
42
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
43
|
+
if (!v) {
|
|
44
|
+
throw new ConnectionAdminError(`--kind is required (${CONNECTION_ENGINES.join(" | ")}).`);
|
|
45
|
+
}
|
|
46
|
+
if (!CONNECTION_ENGINES.includes(v)) {
|
|
47
|
+
throw new ConnectionAdminError(`--kind must be one of: ${CONNECTION_ENGINES.join(", ")} (got "${value}").`);
|
|
48
|
+
}
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
export function connectionTable(conns) {
|
|
52
|
+
return formatTable(["name", "kind", "target", "created"], conns.map((c) => [c.name, c.kind, c.target, fmtTime(c.created_at)]));
|
|
53
|
+
}
|
|
54
|
+
export function resolveConnectionRef(conns, ref) {
|
|
55
|
+
const needle = ref.trim();
|
|
56
|
+
if (!needle)
|
|
57
|
+
throw new ConnectionAdminError("Expected a connection (name or UUID).");
|
|
58
|
+
const byName = conns.find((c) => c.name === needle);
|
|
59
|
+
if (byName)
|
|
60
|
+
return byName;
|
|
61
|
+
const byUuid = conns.find((c) => c.uuid === needle);
|
|
62
|
+
if (byUuid)
|
|
63
|
+
return byUuid;
|
|
64
|
+
throw new ConnectionAdminError(`No connection matches "${ref}" (try a name or UUID).`);
|
|
65
|
+
}
|
|
66
|
+
// Assemble the `POST /connections` body. The server validates required per-engine
|
|
67
|
+
// fields and dials before saving; we only shape the request.
|
|
68
|
+
export function buildConnectionCreateBody(input) {
|
|
69
|
+
const name = (input.name ?? "").trim();
|
|
70
|
+
if (!name)
|
|
71
|
+
throw new ConnectionAdminError("--name is required.");
|
|
72
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) {
|
|
73
|
+
throw new ConnectionAdminError(`--name "${name}" is invalid (letters/digits/_/-, must start alphanumeric).`);
|
|
74
|
+
}
|
|
75
|
+
return { name, kind: input.kind, config: input.config, credentials: input.credentials };
|
|
76
|
+
}
|
|
77
|
+
// ── Service connectors (admin CRUD) ─────────────────────────────────────────
|
|
78
|
+
export const SERVICE_CONNECTOR_AUTH_TYPES = ["none", "bearer", "header", "query", "basic"];
|
|
79
|
+
export function parseAuthType(value) {
|
|
80
|
+
const v = (value ?? "none").trim().toLowerCase();
|
|
81
|
+
if (!SERVICE_CONNECTOR_AUTH_TYPES.includes(v)) {
|
|
82
|
+
throw new ConnectionAdminError(`--auth-type must be one of: ${SERVICE_CONNECTOR_AUTH_TYPES.join(", ")}.`);
|
|
83
|
+
}
|
|
84
|
+
return v;
|
|
85
|
+
}
|
|
86
|
+
// A null content_type is a pre-default row: the proxy still sends application/json with
|
|
87
|
+
// any body, so show the effective value rather than a bare "-".
|
|
88
|
+
export const DEFAULT_CONTENT_TYPE = "application/json";
|
|
89
|
+
export function serviceConnectorAdminTable(list) {
|
|
90
|
+
return formatTable(["name", "base_url", "auth", "cred", "content_type", "methods", "enabled", "native"], list.map((c) => [
|
|
91
|
+
c.name,
|
|
92
|
+
c.base_url,
|
|
93
|
+
c.auth_type,
|
|
94
|
+
c.has_credential ? "yes" : "no",
|
|
95
|
+
c.content_type || DEFAULT_CONTENT_TYPE,
|
|
96
|
+
(c.allowed_methods ?? []).join(", ") || "all",
|
|
97
|
+
c.enabled ? "yes" : "no",
|
|
98
|
+
c.native_key ?? "-",
|
|
99
|
+
]));
|
|
100
|
+
}
|
|
101
|
+
export function resolveServiceConnectorRef(list, ref) {
|
|
102
|
+
const needle = ref.trim();
|
|
103
|
+
if (!needle)
|
|
104
|
+
throw new ConnectionAdminError("Expected a connector (name or UUID).");
|
|
105
|
+
const byName = list.find((c) => c.name === needle);
|
|
106
|
+
if (byName)
|
|
107
|
+
return byName;
|
|
108
|
+
const byUuid = list.find((c) => c.uuid === needle);
|
|
109
|
+
if (byUuid)
|
|
110
|
+
return byUuid;
|
|
111
|
+
throw new ConnectionAdminError(`No service connector matches "${ref}" (try a name or UUID).`);
|
|
112
|
+
}
|
|
113
|
+
export function nativeConnectorTable(list) {
|
|
114
|
+
return formatTable(["key", "name", "enabled", "credential_fields"], list.map((n) => [
|
|
115
|
+
n.key,
|
|
116
|
+
n.name,
|
|
117
|
+
n.enabled ? "yes" : "no",
|
|
118
|
+
(n.credentials ?? []).map((c) => c.key).join(", "),
|
|
119
|
+
]));
|
|
120
|
+
}
|
|
121
|
+
// Assemble the `POST /service-connectors` body for a custom connector.
|
|
122
|
+
export function buildServiceConnectorCreateBody(input) {
|
|
123
|
+
const name = (input.name ?? "").trim().toLowerCase();
|
|
124
|
+
if (!name)
|
|
125
|
+
throw new ConnectionAdminError("--name is required.");
|
|
126
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) {
|
|
127
|
+
throw new ConnectionAdminError(`--name "${name}" is invalid (lowercase letters/digits/_/-, must start alphanumeric).`);
|
|
128
|
+
}
|
|
129
|
+
const baseUrl = (input.baseUrl ?? "").trim();
|
|
130
|
+
if (!baseUrl)
|
|
131
|
+
throw new ConnectionAdminError("--base-url is required.");
|
|
132
|
+
if (!/^https?:\/\//i.test(baseUrl)) {
|
|
133
|
+
throw new ConnectionAdminError("--base-url must be an http(s) URL.");
|
|
134
|
+
}
|
|
135
|
+
if (input.authType !== "none" && Object.keys(input.authConfig).length === 0) {
|
|
136
|
+
throw new ConnectionAdminError(`--auth-config is required for auth-type ${input.authType} (e.g. '{"token":"..."}').`);
|
|
137
|
+
}
|
|
138
|
+
const body = {
|
|
139
|
+
name,
|
|
140
|
+
base_url: baseUrl,
|
|
141
|
+
auth_type: input.authType,
|
|
142
|
+
enabled: input.enabled,
|
|
143
|
+
};
|
|
144
|
+
if (input.authType !== "none")
|
|
145
|
+
body.auth_config = input.authConfig;
|
|
146
|
+
if (input.description !== undefined)
|
|
147
|
+
body.description = input.description;
|
|
148
|
+
// Omitted → the server stores application/json. Only send the field when the caller
|
|
149
|
+
// actually chose a type (e.g. a form-encoded API).
|
|
150
|
+
if (input.contentType !== undefined)
|
|
151
|
+
body.content_type = parseContentType(input.contentType);
|
|
152
|
+
if (input.allowedMethods && input.allowedMethods.length > 0) {
|
|
153
|
+
body.allowed_methods = input.allowedMethods;
|
|
154
|
+
}
|
|
155
|
+
return body;
|
|
156
|
+
}
|
|
157
|
+
// `--content-type <type>`: a single header value, so no CR/LF and nothing blank (an
|
|
158
|
+
// empty value would read as "unset" server-side — just leave the flag off for that).
|
|
159
|
+
export function parseContentType(value) {
|
|
160
|
+
const text = value.trim();
|
|
161
|
+
if (!text) {
|
|
162
|
+
throw new ConnectionAdminError("--content-type requires a value (e.g. application/x-www-form-urlencoded); omit it for application/json.");
|
|
163
|
+
}
|
|
164
|
+
if (/[\r\n]/.test(text)) {
|
|
165
|
+
throw new ConnectionAdminError("--content-type must not contain newlines.");
|
|
166
|
+
}
|
|
167
|
+
if (text.length > 255) {
|
|
168
|
+
throw new ConnectionAdminError("--content-type must be 255 characters or fewer.");
|
|
169
|
+
}
|
|
170
|
+
return text;
|
|
171
|
+
}
|
|
172
|
+
export function parseMethodList(value) {
|
|
173
|
+
if (value === undefined || value.trim() === "")
|
|
174
|
+
return [];
|
|
175
|
+
return value
|
|
176
|
+
.split(",")
|
|
177
|
+
.map((m) => m.trim().toUpperCase())
|
|
178
|
+
.filter((m) => m.length > 0);
|
|
179
|
+
}
|
package/dist/connectors.js
CHANGED
|
@@ -49,7 +49,10 @@ export function connectorOpenapiOutput(docs) {
|
|
|
49
49
|
// API docs (link or inline), and the OpenAPI spec (link or inline) when present.
|
|
50
50
|
export function renderConnectorDocs(docs) {
|
|
51
51
|
const methods = (docs.allowed_methods ?? []).join(", ") || "(all)";
|
|
52
|
-
const
|
|
52
|
+
const identity = [`${docs.name} (${docs.auth_type}) — methods: ${methods}`];
|
|
53
|
+
if (docs.content_type)
|
|
54
|
+
identity.push(`body Content-Type: ${docs.content_type}`);
|
|
55
|
+
const sections = [identity.join("\n")];
|
|
53
56
|
if (docs.description)
|
|
54
57
|
sections.push(docs.description);
|
|
55
58
|
const hasAny = docs.usage_instructions ||
|