orionfold-relay 0.25.0 → 0.25.1

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/cli.js CHANGED
@@ -4614,6 +4614,19 @@ var init_history = __esm({
4614
4614
  }
4615
4615
  });
4616
4616
 
4617
+ // src/lib/http/self-base-url.ts
4618
+ function getSelfBaseUrl() {
4619
+ const explicit = process.env.RELAY_SELF_BASE_URL || process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL;
4620
+ if (explicit) return explicit;
4621
+ const port = process.env.PORT || "3000";
4622
+ return `http://127.0.0.1:${port}`;
4623
+ }
4624
+ var init_self_base_url = __esm({
4625
+ "src/lib/http/self-base-url.ts"() {
4626
+ "use strict";
4627
+ }
4628
+ });
4629
+
4617
4630
  // src/lib/workflows/delay.ts
4618
4631
  function parseDuration(input) {
4619
4632
  const match = input.match(DURATION_PATTERN);
@@ -15281,7 +15294,7 @@ Guidelines for schema inference:
15281
15294
  try {
15282
15295
  const table = await getTable(args.tableId);
15283
15296
  if (!table) return err("Table not found");
15284
- const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
15297
+ const baseUrl = getSelfBaseUrl();
15285
15298
  return ok({
15286
15299
  url: `${baseUrl}/api/tables/${args.tableId}/export?format=${args.format}`,
15287
15300
  table: table.name,
@@ -15727,7 +15740,7 @@ Guidelines for schema inference:
15727
15740
  ];
15728
15741
  }
15729
15742
  function getBaseUrl() {
15730
- return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
15743
+ return getSelfBaseUrl();
15731
15744
  }
15732
15745
  var init_table_tools = __esm({
15733
15746
  "src/lib/chat/tools/table-tools.ts"() {
@@ -15738,6 +15751,7 @@ var init_table_tools = __esm({
15738
15751
  init_history();
15739
15752
  init_import();
15740
15753
  init_enrichment();
15754
+ init_self_base_url();
15741
15755
  }
15742
15756
  });
15743
15757
 
@@ -25379,13 +25393,15 @@ async function fireAction(actionType, config, rowData, meta) {
25379
25393
  Trigger data: ${JSON.stringify(rowData, null, 2)}` : `Triggered by table row change.
25380
25394
 
25381
25395
  Data: ${JSON.stringify(rowData, null, 2)}`;
25382
- await fetch(`${getBaseUrl2()}/api/tasks`, {
25396
+ await fetch(`${getSelfBaseUrl()}/api/tasks`, {
25383
25397
  method: "POST",
25384
25398
  headers: { "Content-Type": "application/json" },
25385
25399
  body: JSON.stringify({
25386
25400
  title: config.title ?? "Triggered Task",
25387
25401
  description,
25388
- projectId: config.projectId ?? null
25402
+ // createTaskSchema wants `string | undefined`, not null — sending null
25403
+ // 400s the self-call and the task is never created. Omit when absent.
25404
+ ...config.projectId ? { projectId: config.projectId } : {}
25389
25405
  })
25390
25406
  });
25391
25407
  return;
@@ -25411,7 +25427,7 @@ Data: ${JSON.stringify(rowData, null, 2)}`;
25411
25427
  return;
25412
25428
  }
25413
25429
  if (actionType === "run_workflow" && config.workflowId) {
25414
- await fetch(`${getBaseUrl2()}/api/workflows/${config.workflowId}/execute`, {
25430
+ await fetch(`${getSelfBaseUrl()}/api/workflows/${config.workflowId}/execute`, {
25415
25431
  method: "POST",
25416
25432
  headers: { "Content-Type": "application/json" },
25417
25433
  body: JSON.stringify({
@@ -25429,14 +25445,12 @@ function deriveAppIdFromBlueprintId(blueprintId) {
25429
25445
  if (idx <= 0) return null;
25430
25446
  return blueprintId.slice(0, idx);
25431
25447
  }
25432
- function getBaseUrl2() {
25433
- return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
25434
- }
25435
25448
  var init_trigger_evaluator = __esm({
25436
25449
  "src/lib/tables/trigger-evaluator.ts"() {
25437
25450
  "use strict";
25438
25451
  init_db();
25439
25452
  init_schema();
25453
+ init_self_base_url();
25440
25454
  }
25441
25455
  });
25442
25456
 
@@ -25899,8 +25913,8 @@ import { execFileSync as execFileSync3 } from "child_process";
25899
25913
  import yaml12 from "js-yaml";
25900
25914
  import semver from "semver";
25901
25915
  function relayCoreVersion() {
25902
- if (semver.valid("0.25.0")) {
25903
- return "0.25.0";
25916
+ if (semver.valid("0.25.1")) {
25917
+ return "0.25.1";
25904
25918
  }
25905
25919
  try {
25906
25920
  const root = getAppRoot(import.meta.dirname, 3);
@@ -27548,6 +27562,11 @@ Falling back to development mode for this run \u2014 Relay still works, but slow
27548
27562
  RELAY_DATA_DIR: DATA_DIR,
27549
27563
  RELAY_LAUNCH_CWD: launchCwd2,
27550
27564
  PORT: String(actualPort),
27565
+ // Origin for Relay's internal loopback self-calls (trigger dispatch,
27566
+ // compose table tools). The server always listens on loopback even when
27567
+ // bound to a non-loopback host, so self-calls target 127.0.0.1 + the real
27568
+ // port — never :3000, never the LAN IP. Fixes issue #29.
27569
+ RELAY_SELF_BASE_URL: buildSidecarUrl(actualPort, "127.0.0.1"),
27551
27570
  ...opts.safeMode ? { RELAY_SAFE_MODE: "true" } : {},
27552
27571
  // In dev mode, Next blocks cross-origin /_next/* dev-asset requests from
27553
27572
  // the LAN client's IP, breaking the app over the network (issue #13).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orionfold-relay",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Orionfold Relay — a local-first, multi-agent orchestration runtime and builder scaffold for AI-native work.",
5
5
  "keywords": [
6
6
  "ai",
@@ -51,7 +51,10 @@ export async function POST(req: NextRequest) {
51
51
  );
52
52
  }
53
53
 
54
- const validRuntimes = ["claude-code", "openai-codex-app-server"];
54
+ // "ollama" is first-class: getRuntimeForModel() returns it for local models,
55
+ // engine.ts routes it to sendOllamaMessage. Omitting it here 400'd the
56
+ // "Best privacy (local only)" tier's first chat/compose on a fresh install (#30).
57
+ const validRuntimes = ["claude-code", "openai-codex-app-server", "ollama"];
55
58
  if (!validRuntimes.includes(runtimeId)) {
56
59
  return NextResponse.json(
57
60
  { error: `Invalid runtimeId. Must be one of: ${validRuntimes.join(", ")}` },
@@ -297,7 +297,16 @@ export function ChatSessionProvider({ children }: { children: ReactNode }) {
297
297
  ...(opts?.title ? { title: opts.title } : {}),
298
298
  }),
299
299
  });
300
- if (!res.ok) return null;
300
+ if (!res.ok) {
301
+ // Never swallow a create failure silently again (#30): a rejected
302
+ // runtime/model used to clear the composer with no signal at all.
303
+ const detail = await res
304
+ .json()
305
+ .then((b) => (b as { error?: string })?.error)
306
+ .catch(() => null);
307
+ toast.error(detail || "Couldn't start a chat with this model. Try another model.");
308
+ return null;
309
+ }
301
310
  const conversation = (await res.json()) as ConversationRow;
302
311
  setConversations((prev) => [conversation, ...prev]);
303
312
  // Set empty messages BEFORE activating so the conversation has an
@@ -26,6 +26,7 @@ import {
26
26
  createImportRecord,
27
27
  } from "@/lib/tables/import";
28
28
  import { createEnrichmentWorkflow } from "@/lib/tables/enrichment";
29
+ import { getSelfBaseUrl } from "@/lib/http/self-base-url";
29
30
  import type { ColumnDef } from "@/lib/tables/types";
30
31
 
31
32
  export function tableTools(ctx: ToolContext) {
@@ -599,7 +600,7 @@ Guidelines for schema inference:
599
600
  try {
600
601
  const table = await getTable(args.tableId);
601
602
  if (!table) return err("Table not found");
602
- const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
603
+ const baseUrl = getSelfBaseUrl();
603
604
  return ok({
604
605
  url: `${baseUrl}/api/tables/${args.tableId}/export?format=${args.format}`,
605
606
  table: table.name,
@@ -1067,5 +1068,7 @@ Guidelines for schema inference:
1067
1068
  }
1068
1069
 
1069
1070
  function getBaseUrl(): string {
1070
- return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
1071
+ // Loopback self-call origin derived from the real bind port, not a bare
1072
+ // :3000 literal (issue #29). See @/lib/http/self-base-url.
1073
+ return getSelfBaseUrl();
1071
1074
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Base URL for Relay's internal loopback self-calls (server → its OWN API).
3
+ *
4
+ * Several server-side paths call Relay's own HTTP API — the table-trigger
5
+ * dispatcher fires `POST /api/tasks` / `/api/workflows/:id/execute`, and the
6
+ * compose chat tools hit `/api/tables/:id/triggers`, `/export`, `/templates`,
7
+ * etc. These are loopback self-fetches, so the origin must be *this* server's
8
+ * own address, not a hardcoded one.
9
+ *
10
+ * The bug this fixes: the old fallback was a bare `http://localhost:3000`, which
11
+ * silently broke every instance NOT on port 3000 (`--port`, `--hostname
12
+ * 0.0.0.0`, containers, staging on :3199). The CLI sets neither `NEXTAUTH_URL`
13
+ * nor `NEXT_PUBLIC_APP_URL`, so that fallback was always used → `TypeError:
14
+ * fetch failed`, caught + swallowed to a server-log line, zero user-facing
15
+ * signal (issue #29). See memory `self-http-calls-hardcode-3000`.
16
+ *
17
+ * Precedence:
18
+ * 1. `RELAY_SELF_BASE_URL` — explicit override the CLI threads from the known
19
+ * bind port (loopback host + real port).
20
+ * 2. `NEXTAUTH_URL` / `NEXT_PUBLIC_APP_URL` — reverse-proxy / custom-origin
21
+ * escape hatch (unchanged behavior for those deployments).
22
+ * 3. `http://127.0.0.1:${PORT}` — loopback + the real port. The server always
23
+ * listens on loopback even when bound to `0.0.0.0` (INADDR_ANY includes the
24
+ * loopback interface), so a self-call must target 127.0.0.1, never the LAN
25
+ * IP or `0.0.0.0`. `PORT` is already in the child env (`bin/cli.ts`).
26
+ * 4. `http://127.0.0.1:3000` — last-resort default when even `PORT` is absent.
27
+ *
28
+ * ZERO-IMPORT LEAF: this module reads only `process.env` and does string work.
29
+ * It is imported by `table-tools.ts`, which is reachable from the runtime
30
+ * catalog — a non-leaf import here would risk the module-load cycle the
31
+ * smoke-budget rule guards. Keep it import-free. See memory
32
+ * `shared-constant-zero-import-leaf`.
33
+ */
34
+ export function getSelfBaseUrl(): string {
35
+ const explicit =
36
+ process.env.RELAY_SELF_BASE_URL ||
37
+ process.env.NEXTAUTH_URL ||
38
+ process.env.NEXT_PUBLIC_APP_URL;
39
+ if (explicit) return explicit;
40
+ const port = process.env.PORT || "3000";
41
+ return `http://127.0.0.1:${port}`;
42
+ }
@@ -6,6 +6,7 @@
6
6
  import { db } from "@/lib/db";
7
7
  import { userTableTriggers } from "@/lib/db/schema";
8
8
  import { eq, and } from "drizzle-orm";
9
+ import { getSelfBaseUrl } from "@/lib/http/self-base-url";
9
10
  import type { FilterSpec } from "./types";
10
11
 
11
12
  type TriggerEvent = "row_added" | "row_updated" | "row_deleted";
@@ -147,13 +148,15 @@ async function fireAction(
147
148
  ? `${config.description}\n\nTrigger data: ${JSON.stringify(rowData, null, 2)}`
148
149
  : `Triggered by table row change.\n\nData: ${JSON.stringify(rowData, null, 2)}`;
149
150
 
150
- await fetch(`${getBaseUrl()}/api/tasks`, {
151
+ await fetch(`${getSelfBaseUrl()}/api/tasks`, {
151
152
  method: "POST",
152
153
  headers: { "Content-Type": "application/json" },
153
154
  body: JSON.stringify({
154
155
  title: config.title ?? "Triggered Task",
155
156
  description,
156
- projectId: config.projectId ?? null,
157
+ // createTaskSchema wants `string | undefined`, not null — sending null
158
+ // 400s the self-call and the task is never created. Omit when absent.
159
+ ...(config.projectId ? { projectId: config.projectId } : {}),
157
160
  }),
158
161
  });
159
162
  return;
@@ -187,7 +190,7 @@ async function fireAction(
187
190
  }
188
191
 
189
192
  if (actionType === "run_workflow" && config.workflowId) {
190
- await fetch(`${getBaseUrl()}/api/workflows/${config.workflowId}/execute`, {
193
+ await fetch(`${getSelfBaseUrl()}/api/workflows/${config.workflowId}/execute`, {
191
194
  method: "POST",
192
195
  headers: { "Content-Type": "application/json" },
193
196
  body: JSON.stringify({
@@ -212,6 +215,3 @@ function deriveAppIdFromBlueprintId(blueprintId: string): string | null {
212
215
  return blueprintId.slice(0, idx);
213
216
  }
214
217
 
215
- function getBaseUrl(): string {
216
- return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
217
- }