@junejs/server 0.1.2-dev.7 → 1.0.0-dev.10

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.
@@ -1,5 +1,5 @@
1
1
  import { Channel, ChannelFactory } from "@junejs/core/agent-config";
2
- import { InboundEvent, Model, Msg, ProactiveTrigger, SessionStore, Tool, TurnEvent } from "@junejs/core/agent-runtime";
2
+ import { InboundEvent, Model, Msg, ProactiveTrigger, SessionStore, Tool, TurnError, TurnEvent, TurnFailurePhase } from "@junejs/core/agent-runtime";
3
3
  import { Resources } from "@junejs/core/resources";
4
4
  //#region src/agent-durable.d.ts
5
5
  type SqlStorageValue = ArrayBuffer | string | number | null;
@@ -53,9 +53,9 @@ type DoAgentDef = {
53
53
  services?: unknown;
54
54
  onTurnError?: (failure: {
55
55
  turnId: string;
56
- error: {
57
- message: string;
58
- };
56
+ error: TurnError;
57
+ phase?: TurnFailurePhase;
58
+ step?: string;
59
59
  }) => void | Promise<void>;
60
60
  };
61
61
  declare class AgentDurableObject {
@@ -1,3 +1,4 @@
1
+ import { assertCoreRuntimeVersion } from "./core-version.js";
1
2
  import { channelDispatch, resolveChannel } from "@junejs/core/agent-config";
2
3
  import { AgentSession, ResumeAuthorizationError, withSystem } from "@junejs/core/agent-runtime";
3
4
  import { ensureScope, runInScope } from "@junejs/db";
@@ -83,6 +84,7 @@ var AgentDurableObject = class {
83
84
  resources;
84
85
  services;
85
86
  constructor(state, def) {
87
+ assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`);
86
88
  const store = new DoSessionStore(state.storage);
87
89
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
88
90
  this.resources = def.resources ?? {};
@@ -91,11 +93,13 @@ var AgentDurableObject = class {
91
93
  const sink = new InProcEventSink();
92
94
  sink.subscribe((e) => {
93
95
  if (e.type !== "turn.failed") return;
94
- const defaultLog = () => console.error(`[june] agent "${name}" turn ${e.turnId} failed: ${e.error.message}`);
96
+ const defaultLog = () => console.error(`[june] agent "${name}" turn ${e.turnId} failed${e.step ? ` at ${e.step}` : ""}: ${e.error.stack ?? e.error.message}${e.error.causeChain?.length ? `\ncaused by: ${e.error.causeChain.join(" ← ")}` : ""}`);
95
97
  if (def.onTurnError) try {
96
98
  const out = def.onTurnError({
97
99
  turnId: e.turnId,
98
- error: e.error
100
+ error: e.error,
101
+ phase: e.phase,
102
+ step: e.step
99
103
  });
100
104
  if (out && typeof out.then === "function") out.then(void 0, (hookErr) => {
101
105
  console.error(`[june] agent "${name}": onTurnError hook rejected:`, hookErr);
@@ -262,7 +266,7 @@ async function sseTurnFinalText(res) {
262
266
  if (!line) continue;
263
267
  const e = JSON.parse(line.slice(5).trim());
264
268
  if (e.type === "turn.completed") return e.text;
265
- if (e.type === "turn.failed") throw new Error(e.error.message);
269
+ if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error });
266
270
  }
267
271
  if (done) break;
268
272
  }
@@ -1,3 +1,4 @@
1
+ import { assertCoreRuntimeVersion } from "./core-version.js";
1
2
  import { openLocalSqliteSync } from "./sqlite-driver.js";
2
3
  import { channelFetch } from "@junejs/core/agent-config";
3
4
  import { AgentSession, withSystem } from "@junejs/core/agent-runtime";
@@ -73,6 +74,7 @@ var NativeRuntime = class {
73
74
  agents;
74
75
  db;
75
76
  constructor(agents, db) {
77
+ assertCoreRuntimeVersion("NativeRuntime");
76
78
  this.agents = agents;
77
79
  this.db = db;
78
80
  initSchema(db);
@@ -131,6 +133,7 @@ var MemoryRuntime = class {
131
133
  stores = /* @__PURE__ */ new Map();
132
134
  agents;
133
135
  constructor(agents) {
136
+ assertCoreRuntimeVersion("MemoryRuntime");
134
137
  this.agents = agents;
135
138
  }
136
139
  session(agent, id) {
@@ -0,0 +1,5 @@
1
+ //#region src/core-version.d.ts
2
+ declare function assertCoreRuntimeVersion(surface: string): void;
3
+ declare function checkCoreRuntimeVersion(surface: string, actual: number | undefined, expected?: number): void;
4
+ //#endregion
5
+ export { assertCoreRuntimeVersion, checkCoreRuntimeVersion };
@@ -0,0 +1,12 @@
1
+ import * as coreRuntime from "@junejs/core/agent-runtime";
2
+ //#region src/core-version.ts
3
+ const EXPECTED_CORE_RUNTIME_API = 1;
4
+ function assertCoreRuntimeVersion(surface) {
5
+ checkCoreRuntimeVersion(surface, coreRuntime.RUNTIME_API_VERSION);
6
+ }
7
+ function checkCoreRuntimeVersion(surface, actual, expected = EXPECTED_CORE_RUNTIME_API) {
8
+ if (actual === expected) return;
9
+ throw new Error(`${surface}: @junejs/server is built against @junejs/core runtime API v${expected}, but the @junejs/core it resolved provides ${actual === void 0 ? "no RUNTIME_API_VERSION (a pre-0.1.1-dev.16 core)" : `v${actual}`} — two copies of @junejs/core are likely installed (one nested under @junejs/server). Align the app's @junejs/core with @junejs/server's peer range, then reinstall/dedupe so one copy remains.`);
10
+ }
11
+ //#endregion
12
+ export { assertCoreRuntimeVersion, checkCoreRuntimeVersion };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, staticSite, vercel, workers } from "./adapter.js";
2
2
  import { CreateAppOptions, JuneApp, createApp } from "./app.js";
3
3
  import { R2Bucket, localBlob, r2 } from "./blob.js";
4
- import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-CCp9idQ4.js";
4
+ import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-cCR6MGNK.js";
5
5
  import { WorkerManifest, createWorker } from "./worker.js";
6
6
  import { BuildResult, buildManifest, freezeConfig, generateContent, juneBuild, scanRoutes } from "./build.js";
7
7
  import { loadJuneConfig } from "./config-loader.js";
@@ -4,6 +4,7 @@ import React from "react";
4
4
  import { DocumentConfig } from "@junejs/core/document";
5
5
  import { I18nConfig } from "@junejs/core/i18n";
6
6
  import { Resources } from "@junejs/core/resources";
7
+ import { Principal, Session } from "@junejs/core/context";
7
8
  //#region src/pipeline.d.ts
8
9
  type LayoutComponent = React.ComponentType<{
9
10
  children: React.ReactNode;
@@ -32,6 +33,13 @@ type PipelineConfig = {
32
33
  resolve: RouteResolver;
33
34
  resources?: () => Promise<Resources> | Resources;
34
35
  services?: () => Promise<unknown> | unknown;
36
+ identity?: (request: Request) => Promise<{
37
+ user?: Principal;
38
+ session?: Session;
39
+ }> | {
40
+ user?: Principal;
41
+ session?: Session;
42
+ };
35
43
  earlyHints?: string[];
36
44
  htmlCacheControl?: string;
37
45
  notFoundComponent?: React.ComponentType<{
@@ -1,2 +1,2 @@
1
- import { a as Pipeline, c as ResolvedResource, d as createPipeline, i as MiddlewareHandler, l as ResourceHandler, n as LayoutComponent, o as PipelineConfig, r as LoadingComponent, s as Resolved, t as ExtraHandler, u as RouteResolver } from "./pipeline-CCp9idQ4.js";
1
+ import { a as Pipeline, c as ResolvedResource, d as createPipeline, i as MiddlewareHandler, l as ResourceHandler, n as LayoutComponent, o as PipelineConfig, r as LoadingComponent, s as Resolved, t as ExtraHandler, u as RouteResolver } from "./pipeline-cCR6MGNK.js";
2
2
  export { ExtraHandler, LayoutComponent, LoadingComponent, MiddlewareHandler, Pipeline, PipelineConfig, Resolved, ResolvedResource, ResourceHandler, RouteResolver, createPipeline };
package/dist/pipeline.js CHANGED
@@ -171,7 +171,10 @@ function createPipeline(cfg) {
171
171
  const url = new URL(request.url);
172
172
  if (url.pathname === "/mcp") {
173
173
  if (!agent.mcp) return notFoundResponse("view", url.pathname);
174
- return mcpHandler(request, { request });
174
+ return mcpHandler(request, {
175
+ request,
176
+ ...cfg.identity ? await cfg.identity(request) : void 0
177
+ });
175
178
  }
176
179
  if (request.method === "GET" && agent.discovery) {
177
180
  const d = await discovery(url);
package/dist/worker.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-CCp9idQ4.js";
1
+ import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-cCR6MGNK.js";
2
2
  import { BrandedRoute } from "@junejs/core/route";
3
3
  import { AgentConfig } from "@junejs/core/config";
4
4
  import React from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.1.2-dev.7",
3
+ "version": "1.0.0-dev.10",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -125,14 +125,14 @@
125
125
  "build": "tsdown"
126
126
  },
127
127
  "dependencies": {
128
- "@junejs/core": ">=0.1.1-dev.14",
129
- "@junejs/db": ">=0.0.34-dev.0",
128
+ "@junejs/db": ">=0.0.34-dev.1",
130
129
  "@momiji-rs/sparkdown": "^0.0.4",
131
130
  "lightningcss": "^1.27.0",
132
131
  "oxc-parser": "^0.137.0",
133
132
  "rolldown": "^1.1.0"
134
133
  },
135
134
  "peerDependencies": {
135
+ "@junejs/core": ">=0.2.0-dev.26",
136
136
  "react": "^19.2.0",
137
137
  "react-dom": "^19.2.0",
138
138
  "react-server-dom-webpack": "19.2.7",
@@ -156,7 +156,8 @@
156
156
  },
157
157
  "repository": {
158
158
  "type": "git",
159
- "url": "https://github.com/junebuild/june"
159
+ "url": "git+https://github.com/junebuild/june.git",
160
+ "directory": "packages/june"
160
161
  },
161
162
  "devDependencies": {
162
163
  "@libsql/client": "^0.17.3",
@@ -28,6 +28,8 @@ import {
28
28
  type SessionStore,
29
29
  type ProactiveTrigger,
30
30
  type Tool,
31
+ type TurnError,
32
+ type TurnFailurePhase,
31
33
  } from "@junejs/core/agent-runtime";
32
34
  import type { Resources } from "@junejs/core/resources";
33
35
  import {
@@ -39,6 +41,7 @@ import {
39
41
  type ChannelFactory,
40
42
  } from "@junejs/core/agent-config";
41
43
  import { ensureScope, runInScope } from "@junejs/db";
44
+ import { assertCoreRuntimeVersion } from "./core-version";
42
45
 
43
46
  // ── minimal structural Cloudflare surface (no @cloudflare/workers-types dep) ──
44
47
  // SqlStorage row values are exactly what workerd's SQLite returns. Mirroring
@@ -185,11 +188,14 @@ export type DoAgentDef = {
185
188
  // Called on every turn.failed for this session — the app's seam for routing turn
186
189
  // failures to its own telemetry (Sentry, a ledger, …). Providing it REPLACES the
187
190
  // default console.error; if the hook throws (or an async hook rejects), the default
188
- // log fires anyway (both errors), so a failure is never silent. It happens to run
189
- // inside the failed turn's request scope today (turn.failed is emitted mid-turn and
190
- // ALS propagates), but that is NOT contract — don't rely on ambient db/services;
191
- // close over what you need.
192
- onTurnError?: (failure: { turnId: string; error: { message: string } }) => void | Promise<void>;
191
+ // log fires anyway (both errors), so a failure is never silent. `error` carries the
192
+ // full serialized failure (stack, cause chain — see TurnError, #96), and phase/step
193
+ // name the engine step that was in flight — for a DETACHED turn this hook is the
194
+ // only failure-surfacing path, so nothing may be flattened before it fires. It
195
+ // happens to run inside the failed turn's request scope today (turn.failed is
196
+ // emitted mid-turn and ALS propagates), but that is NOT contract — don't rely on
197
+ // ambient db/services; close over what you need.
198
+ onTurnError?: (failure: { turnId: string; error: TurnError; phase?: TurnFailurePhase; step?: string }) => void | Promise<void>;
193
199
  };
194
200
 
195
201
  // The agent runtime INSIDE a Durable Object. A plain class (constructor takes the
@@ -226,6 +232,7 @@ export class AgentDurableObject {
226
232
  private readonly resources: Resources;
227
233
  private readonly services: unknown;
228
234
  constructor(state: DurableObjectState, def: DoAgentDef) {
235
+ assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`); // #94: fail power-on, not mid-turn
229
236
  const store = new DoSessionStore(state.storage);
230
237
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
231
238
  this.resources = def.resources ?? {};
@@ -241,10 +248,13 @@ export class AgentDurableObject {
241
248
  const sink = new InProcEventSink();
242
249
  sink.subscribe((e) => {
243
250
  if (e.type !== "turn.failed") return;
244
- const defaultLog = () => console.error(`[june] agent "${name}" turn ${e.turnId} failed: ${e.error.message}`);
251
+ // The stack already opens with the message; log it INSTEAD of the message line
252
+ // when present so wrangler tail shows one coherent trace, not the message twice.
253
+ const defaultLog = () =>
254
+ console.error(`[june] agent "${name}" turn ${e.turnId} failed${e.step ? ` at ${e.step}` : ""}: ${e.error.stack ?? e.error.message}${e.error.causeChain?.length ? `\ncaused by: ${e.error.causeChain.join(" ← ")}` : ""}`);
245
255
  if (def.onTurnError) {
246
256
  try {
247
- const out = def.onTurnError({ turnId: e.turnId, error: e.error });
257
+ const out = def.onTurnError({ turnId: e.turnId, error: e.error, phase: e.phase, step: e.step });
248
258
  // An async hook (e.g. `await sendToSentry(...)`) types as void but returns a
249
259
  // Promise — a rejection there must hit the same fallback as a sync throw, or
250
260
  // the failure goes silent again (plus an unhandled rejection). No waitUntil
@@ -469,7 +479,7 @@ export async function sseTurnFinalText(res: Response): Promise<string> {
469
479
  if (!line) continue;
470
480
  const e = JSON.parse(line.slice(5).trim()) as TurnEvent;
471
481
  if (e.type === "turn.completed") return e.text;
472
- if (e.type === "turn.failed") throw new Error(e.error.message);
482
+ if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error }); // full TurnError rides along
473
483
  }
474
484
  if (done) break;
475
485
  }
@@ -20,6 +20,7 @@ import {
20
20
  } from "@junejs/core/agent-runtime";
21
21
  import { channelFetch, type AgentDefinition, type ChannelContext } from "@junejs/core/agent-config";
22
22
  import { openLocalSqliteSync, type SyncSqlite } from "./sqlite-driver";
23
+ import { assertCoreRuntimeVersion } from "./core-version";
23
24
 
24
25
  function initSchema(db: SyncSqlite) {
25
26
  db.exec(`CREATE TABLE IF NOT EXISTS agent_sessions (session_id TEXT PRIMARY KEY, status TEXT)`);
@@ -99,6 +100,7 @@ export class NativeRuntime implements Runtime {
99
100
  private readonly db: SyncSqlite;
100
101
 
101
102
  constructor(agents: Record<string, AgentDef>, db: SyncSqlite) {
103
+ assertCoreRuntimeVersion("NativeRuntime"); // #94: fail power-on, not mid-turn
102
104
  this.agents = agents;
103
105
  this.db = db;
104
106
  initSchema(db);
@@ -154,6 +156,7 @@ export class MemoryRuntime implements Runtime {
154
156
  private stores = new Map<string, MemorySessionStore>();
155
157
  private readonly agents: Record<string, AgentDef>;
156
158
  constructor(agents: Record<string, AgentDef>) {
159
+ assertCoreRuntimeVersion("MemoryRuntime"); // #94: fail power-on, not mid-turn
157
160
  this.agents = agents;
158
161
  }
159
162
  session(agent: string, id: string): AgentSession {
@@ -0,0 +1,34 @@
1
+ // core-version.ts — the #94 belt-and-braces: fail at POWER-ON when the @junejs/core
2
+ // copy this server resolved is not the one it was built for.
3
+ //
4
+ // The primary fix is @junejs/core being a peerDependency (the app's single resolution
5
+ // always wins), but a nested second copy can still appear (npm legacy peer handling,
6
+ // a stale lockfile, a transitive pin). When it does, the failure without this check
7
+ // is far from the cause and mid-turn ("sink.emit is not a function", first seen in
8
+ // crisp-agent production); with it, construction throws with both versions named.
9
+ //
10
+ // Namespace import + optional read on purpose: a PRE-#94 core has no
11
+ // RUNTIME_API_VERSION export, and a named import of a missing export would die at
12
+ // module link time with an unhelpful SyntaxError — exactly the confusion this
13
+ // module exists to remove.
14
+ import * as coreRuntime from "@junejs/core/agent-runtime";
15
+
16
+ // The contract number this server tree is built against. Bump in lockstep with
17
+ // core's RUNTIME_API_VERSION whenever the server↔core runtime contract changes.
18
+ const EXPECTED_CORE_RUNTIME_API = 1;
19
+
20
+ export function assertCoreRuntimeVersion(surface: string): void {
21
+ checkCoreRuntimeVersion(surface, (coreRuntime as { RUNTIME_API_VERSION?: number }).RUNTIME_API_VERSION);
22
+ }
23
+
24
+ // The pure check, split out so the mismatch paths are unit-testable (the ambient
25
+ // read above can only ever see the workspace's own — matching — core).
26
+ export function checkCoreRuntimeVersion(surface: string, actual: number | undefined, expected: number = EXPECTED_CORE_RUNTIME_API): void {
27
+ if (actual === expected) return;
28
+ throw new Error(
29
+ `${surface}: @junejs/server is built against @junejs/core runtime API v${expected}, ` +
30
+ `but the @junejs/core it resolved provides ${actual === undefined ? "no RUNTIME_API_VERSION (a pre-0.1.1-dev.16 core)" : `v${actual}`} — ` +
31
+ `two copies of @junejs/core are likely installed (one nested under @junejs/server). ` +
32
+ `Align the app's @junejs/core with @junejs/server's peer range, then reinstall/dedupe so one copy remains.`,
33
+ );
34
+ }
package/src/pipeline.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  sitemapXml,
35
35
  } from "@junejs/core/discovery";
36
36
  import { mcpHandler, mcpTools } from "@junejs/core/mcp";
37
+ import type { Principal, Session } from "@junejs/core/context";
37
38
 
38
39
  import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
39
40
  import type { AgentConfig } from "@junejs/core/config";
@@ -126,6 +127,14 @@ export type PipelineConfig = {
126
127
  // `currentServices()` resolves in loaders/views/actions. Absent → currentServices()
127
128
  // is undefined. This is the Worker-side twin of a Durable Object seeding services.
128
129
  services?: () => Promise<unknown> | unknown;
130
+ // Resolve the request's authenticated identity (the auth integration's job —
131
+ // e.g. Better Auth reading the session cookie). The result rides every
132
+ // ActionContext the pipeline builds, starting with the /mcp mount, so
133
+ // requiresPrincipal actions and per-call connection auth see the SAME
134
+ // principal a UI dispatch would. Absent → anonymous (identity-gated actions
135
+ // reject); a THROWING resolver propagates as a server error — it must never
136
+ // silently degrade to anonymous.
137
+ identity?: (request: Request) => Promise<{ user?: Principal; session?: Session }> | { user?: Principal; session?: Session };
129
138
  earlyHints?: string[];
130
139
  htmlCacheControl?: string;
131
140
  notFoundComponent?: React.ComponentType<{ pathname: string }>;
@@ -473,9 +482,11 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
473
482
  if (url.pathname === "/mcp") {
474
483
  if (!agent.mcp) return notFoundResponse("view", url.pathname);
475
484
  // The agent's tool calls run inside the same request scope, so an action's
476
- // ambient `db` is the SAME resource the UI uses (and, once auth is wired,
477
- // the same principal via ctx). ctx carries identity only — not resources.
478
- return mcpHandler(request, { request });
485
+ // ambient `db` is the SAME resource the UI uses, and — via cfg.identity —
486
+ // the same principal: requiresPrincipal actions and per-call connection
487
+ // auth are live on this surface. ctx carries identity only — not resources.
488
+ const identity = cfg.identity ? await cfg.identity(request) : undefined;
489
+ return mcpHandler(request, { request, ...identity });
479
490
  }
480
491
  if (request.method === "GET" && agent.discovery) {
481
492
  const d = await discovery(url);