railcode 0.1.6 → 0.1.8

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/dist/index.js CHANGED
@@ -26,8 +26,11 @@ Usage:
26
26
  railcode login [--api-url <url>]
27
27
  railcode deploy [--private | --public]
28
28
  railcode access [public|private|restricted] [--users <a,b>]
29
+ railcode network
29
30
  railcode db list
30
31
  railcode db query "<sql>" [--connection <name>] [--engine <postgres|mysql>]
32
+ railcode connector list
33
+ railcode connector fetch <name> <path> [--method <verb>] [--body <data>]
31
34
  railcode get design-system
32
35
  railcode init <app>
33
36
  railcode upgrade
@@ -61,9 +64,16 @@ async function main() {
61
64
  case "access":
62
65
  await commandAccess(args);
63
66
  return;
67
+ case "network":
68
+ await commandNetwork(args);
69
+ return;
64
70
  case "db":
65
71
  await commandDb(args);
66
72
  return;
73
+ case "connector":
74
+ case "connectors":
75
+ await commandConnector(args);
76
+ return;
67
77
  case "get":
68
78
  await commandGet(args);
69
79
  return;
@@ -625,7 +635,8 @@ async function handleLocalApi(ctx, request, response, url) {
625
635
  await handleLocalFiles(ctx, request, response, path);
626
636
  return;
627
637
  }
628
- if (path === "/connections" && (!ctx.token || !ctx.apiBase)) {
638
+ if ((path === "/connections" || path === "/service-connectors") &&
639
+ (!ctx.token || !ctx.apiBase)) {
629
640
  sendJson(response, 200, []);
630
641
  return;
631
642
  }
@@ -1599,6 +1610,70 @@ function appUrlFromOrigin(app, origin) {
1599
1610
  url.hash = "";
1600
1611
  return url.toString();
1601
1612
  }
1613
+ const NETWORK_HELP = `Usage:
1614
+ railcode network Show the platform default network mode for new apps
1615
+
1616
+ Network mode is the per-app egress policy, enforced as a CSP:
1617
+ open No CSP — the app may fetch any origin.
1618
+ restricted External calls only via the Railcode proxy (connect-src 'self').
1619
+ sandboxed Same-origin only — no inline <script>, no external CDN/fonts.
1620
+
1621
+ Admins set the default and flip individual apps from the admin console; the CLI
1622
+ only reports the default so you know how to build a new app.
1623
+ `;
1624
+ const NETWORK_MODE_BUILD_HINTS = {
1625
+ open: "Build freely: direct fetch() to any origin is allowed.",
1626
+ restricted: "Don't fetch external origins directly — route them through the Railcode proxy. Inline scripts and CDN assets are fine.",
1627
+ sandboxed: "Same-origin only: no external fetch, no inline <script>, no external CDN/font links. Use the SDK and bundle every asset locally.",
1628
+ };
1629
+ async function commandNetwork(args) {
1630
+ if (booleanOption(args, "help") || args.rest[0] === "help") {
1631
+ console.log(NETWORK_HELP);
1632
+ return;
1633
+ }
1634
+ if (args.rest.length > 0) {
1635
+ throw new CliError("railcode network shows the platform default. Admins flip individual apps from the admin console.", 2);
1636
+ }
1637
+ rejectUnknownOptions(args, ["help", "json", "apiUrl"], "network");
1638
+ const config = loadConfig();
1639
+ const manifestApiUrl = readRailcodeManifest(process.cwd())?.manifest.deploy?.apiUrl;
1640
+ const authUrl = await resolveRequiredAuthOrigin({
1641
+ config,
1642
+ args,
1643
+ action: "Network mode",
1644
+ fallbackApiUrl: manifestApiUrl,
1645
+ missingUrlHint: "Pass --api-url <url>, set deploy.apiUrl in railcode.json, or set RAILCODE_API_URL.",
1646
+ });
1647
+ const apiUrl = canonicalApiOrigin(authUrl);
1648
+ let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Network mode");
1649
+ let response = await fetchNetworkDefault(apiUrl, authedConfig);
1650
+ if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
1651
+ console.log("Saved login expired. Log in again to continue.");
1652
+ const retryConfig = { ...authedConfig, apiUrl: authUrl };
1653
+ delete retryConfig.apiToken;
1654
+ delete retryConfig.apiTokenPrefix;
1655
+ delete retryConfig.cookies;
1656
+ saveConfig(retryConfig);
1657
+ authedConfig = await ensureApiToken(retryConfig, authUrl, "Network mode");
1658
+ response = await fetchNetworkDefault(apiUrl, authedConfig);
1659
+ }
1660
+ const view = (await readDbResponse(response, "Network mode"));
1661
+ printNetworkDefault(view, booleanOption(args, "json"));
1662
+ }
1663
+ function fetchNetworkDefault(apiUrl, config) {
1664
+ return dbFetch(apiUrl, config, "/v1/network-default", { redirect: "manual" });
1665
+ }
1666
+ function printNetworkDefault(view, json) {
1667
+ if (json) {
1668
+ console.log(JSON.stringify(view, null, 2));
1669
+ return;
1670
+ }
1671
+ const mode = view.network_mode || "restricted";
1672
+ console.log(`Platform default network mode: ${mode}`);
1673
+ const hint = NETWORK_MODE_BUILD_HINTS[mode];
1674
+ if (hint)
1675
+ console.log(hint);
1676
+ }
1602
1677
  const DB_ENGINES = new Set(["postgres", "mysql"]);
1603
1678
  const DB_HELP = `Query Railcode data connectors.
1604
1679
 
@@ -1836,6 +1911,137 @@ function printSqlResult(result, asJson) {
1836
1911
  const count = typeof result.rowcount === "number" ? result.rowcount : rows.length;
1837
1912
  console.log(`${columns.length > 0 ? "\n" : ""}${count} ${count === 1 ? "row" : "rows"}${result.truncated ? " (truncated to first 1000)" : ""}`);
1838
1913
  }
1914
+ const CONNECTOR_HELP = `Call admin-configured HTTP service connectors (Stripe, PostHog, …).
1915
+
1916
+ Usage:
1917
+ railcode connector list
1918
+ railcode connector fetch <name> <path> [options]
1919
+
1920
+ Commands:
1921
+ list List service connectors you can call (name + auth type).
1922
+ fetch <name> <path> Proxy an HTTP request through a connector.
1923
+
1924
+ Fetch options:
1925
+ --method <verb> HTTP method (default GET). Must be allowed by the connector.
1926
+ --body <data> Raw request body (the connector sets the Content-Type).
1927
+ --file <path> Read the request body from a file instead.
1928
+ --json Print the raw JSON envelope instead of the body.
1929
+
1930
+ Options:
1931
+ --api-url <url> Railcode auth/API URL. Defaults to saved config or railcode.json.
1932
+
1933
+ Examples:
1934
+ railcode connector list
1935
+ railcode connector fetch stripe /v1/charges
1936
+ railcode connector fetch stripe /v1/charges --method POST --body "amount=100&currency=usd"
1937
+ `;
1938
+ async function commandConnector(args) {
1939
+ const subcommand = args.rest[0];
1940
+ const rest = { ...args, rest: args.rest.slice(1) };
1941
+ switch (subcommand) {
1942
+ case "list":
1943
+ case "ls":
1944
+ await commandConnectorList(rest);
1945
+ return;
1946
+ case "fetch":
1947
+ case "request":
1948
+ await commandConnectorFetch(rest);
1949
+ return;
1950
+ case undefined:
1951
+ case "help":
1952
+ console.log(CONNECTOR_HELP);
1953
+ return;
1954
+ default:
1955
+ if (booleanOption(args, "help")) {
1956
+ console.log(CONNECTOR_HELP);
1957
+ return;
1958
+ }
1959
+ throw new CliError(`Unknown connector command: ${subcommand}\n\n${CONNECTOR_HELP}`, 2);
1960
+ }
1961
+ }
1962
+ async function commandConnectorList(args) {
1963
+ if (booleanOption(args, "help") || args.rest[0] === "help") {
1964
+ console.log(CONNECTOR_HELP);
1965
+ return;
1966
+ }
1967
+ rejectUnknownOptions(args, ["help", "json", "apiUrl"], "connector list");
1968
+ if (args.rest.length > 0) {
1969
+ throw new CliError("railcode connector list does not accept positional arguments.", 2);
1970
+ }
1971
+ const response = await dbAuthedRequest(args, "Service connectors list", fetchServiceConnectors);
1972
+ const connectors = (await readDbResponse(response, "Service connectors list"));
1973
+ printServiceConnectors(connectors, booleanOption(args, "json"));
1974
+ }
1975
+ async function commandConnectorFetch(args) {
1976
+ if (booleanOption(args, "help") || args.rest[0] === "help") {
1977
+ console.log(CONNECTOR_HELP);
1978
+ return;
1979
+ }
1980
+ rejectUnknownOptions(args, ["help", "json", "apiUrl", "method", "body", "file"], "connector fetch");
1981
+ const name = args.rest[0];
1982
+ const path = args.rest[1];
1983
+ if (!name || !path) {
1984
+ throw new CliError("Usage: railcode connector fetch <name> <path> [--method <verb>] [--body <data>].", 2);
1985
+ }
1986
+ if (args.rest.length > 2) {
1987
+ throw new CliError("railcode connector fetch takes a single connector name and path.", 2);
1988
+ }
1989
+ if (!path.startsWith("/")) {
1990
+ throw new CliError("The connector path must start with '/'.", 2);
1991
+ }
1992
+ const method = (stringOption(args, "method") || "GET").trim().toUpperCase();
1993
+ const body = resolveConnectorBody(args);
1994
+ const requestBody = { connector: name, method, path, body };
1995
+ const response = await dbAuthedRequest(args, "Connector request", (apiUrl, config) => postConnectorRequest(apiUrl, config, requestBody));
1996
+ const result = (await readDbResponse(response, "Connector request", "Run `railcode connector list` to see connectors you can call."));
1997
+ printConnectorResponse(result, booleanOption(args, "json"));
1998
+ }
1999
+ function resolveConnectorBody(args) {
2000
+ const file = stringOption(args, "file");
2001
+ const inline = stringOption(args, "body");
2002
+ if (file && inline !== undefined) {
2003
+ throw new CliError("Pass --body or --file, not both.", 2);
2004
+ }
2005
+ if (file) {
2006
+ const path = resolve(file);
2007
+ if (!existsSync(path))
2008
+ throw new CliError(`Body file not found: ${path}`, 1);
2009
+ return readFileSync(path, "utf8");
2010
+ }
2011
+ return inline;
2012
+ }
2013
+ function fetchServiceConnectors(apiUrl, config) {
2014
+ return dbFetch(apiUrl, config, "/v1/service-connectors", { redirect: "manual" });
2015
+ }
2016
+ function postConnectorRequest(apiUrl, config, body) {
2017
+ return dbFetch(apiUrl, config, "/v1/service-connectors/request", {
2018
+ method: "POST",
2019
+ headers: { "Content-Type": "application/json" },
2020
+ body: JSON.stringify(body),
2021
+ redirect: "manual",
2022
+ });
2023
+ }
2024
+ function printServiceConnectors(connectors, asJson) {
2025
+ if (asJson) {
2026
+ console.log(JSON.stringify(connectors, null, 2));
2027
+ return;
2028
+ }
2029
+ if (!connectors || connectors.length === 0) {
2030
+ console.log("No service connectors available.");
2031
+ return;
2032
+ }
2033
+ console.log(formatTable(["NAME", "AUTH", "METHODS"], connectors.map((c) => [c.name, c.auth_type ?? "none", (c.allowed_methods ?? []).join(",")])));
2034
+ }
2035
+ function printConnectorResponse(result, asJson) {
2036
+ if (asJson) {
2037
+ console.log(JSON.stringify(result, null, 2));
2038
+ return;
2039
+ }
2040
+ const status = result.status ?? 0;
2041
+ console.log(`${status} ${result.ok ? "OK" : "ERROR"}${result.truncated ? " (truncated)" : ""}`);
2042
+ if (result.body)
2043
+ console.log(result.body);
2044
+ }
1839
2045
  function rejectUnknownOptions(args, allowed, label) {
1840
2046
  const set = new Set(allowed);
1841
2047
  const unknown = Object.keys(args.options).filter((option) => !set.has(option));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "railcode",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "CLI for building, testing, and deploying Railcode apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/static/sdk.js CHANGED
@@ -371,6 +371,39 @@
371
371
  var dataConnectors = () => track("connections", "dataConnectors()", () => call("GET", "/connections"));
372
372
  var databaseConnectors = dataConnectors;
373
373
 
374
+ // src/service-connectors.ts
375
+ var PATH_LABEL_MAX = 60;
376
+ var toResponse = (env) => {
377
+ const body = env.body ?? "";
378
+ return {
379
+ status: env.status,
380
+ ok: env.ok,
381
+ headers: env.headers || {},
382
+ truncated: Boolean(env.truncated),
383
+ text: () => Promise.resolve(body),
384
+ json: () => Promise.resolve(JSON.parse(body))
385
+ };
386
+ };
387
+ var request = (name, path, opts) => {
388
+ const method = (opts?.method || "GET").toUpperCase();
389
+ const label = `connector(${q(name)}).fetch(${q(method)} ${q(shorten(path, PATH_LABEL_MAX))})`;
390
+ return track(
391
+ "connector",
392
+ label,
393
+ () => call("POST", "/service-connectors/request", {
394
+ json: { connector: name, method, path, body: opts?.body }
395
+ }).then(toResponse)
396
+ );
397
+ };
398
+ var connector = (name) => ({
399
+ fetch: (path, opts) => request(name, path, opts)
400
+ });
401
+ var serviceConnectors = () => track(
402
+ "service-connectors",
403
+ "serviceConnectors()",
404
+ () => call("GET", "/service-connectors")
405
+ );
406
+
374
407
  // src/design-system.ts
375
408
  var designSystem = () => track("design-system", "designSystem()", async () => {
376
409
  const response = await call("GET", "/config/design-system");
@@ -437,6 +470,8 @@
437
470
  window.mysql = mysql;
438
471
  window.dataConnectors = dataConnectors;
439
472
  window.databaseConnectors = databaseConnectors;
473
+ window.connector = connector;
474
+ window.serviceConnectors = serviceConnectors;
440
475
  window.designSystem = designSystem;
441
476
  window.me = me;
442
477
  window.appUsers = appUsers;