@junejs/server 0.1.2-dev.9 → 1.0.0-dev.11

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,3 +1,4 @@
1
+ import { isolateLocal } from "./isolate-local.js";
1
2
  import { assertCoreRuntimeVersion } from "./core-version.js";
2
3
  import { channelDispatch, resolveChannel } from "@junejs/core/agent-config";
3
4
  import { AgentSession, ResumeAuthorizationError, withSystem } from "@junejs/core/agent-runtime";
@@ -325,6 +326,18 @@ function durableAgentSurface(getNamespace, opts) {
325
326
  return Response.json({ text: await sseTurnFinalText(res) });
326
327
  };
327
328
  }
329
+ function resolveServices(factory, env, agentName) {
330
+ if (!factory) return void 0;
331
+ if (typeof env !== "object" || env === null) return factory(env);
332
+ const cache = isolateLocal("june.durableChannelSurface.services", () => /* @__PURE__ */ new WeakMap());
333
+ let perAgent = cache.get(env);
334
+ if (!perAgent) {
335
+ perAgent = /* @__PURE__ */ new Map();
336
+ cache.set(env, perAgent);
337
+ }
338
+ if (!perAgent.has(agentName)) perAgent.set(agentName, factory(env));
339
+ return perAgent.get(agentName);
340
+ }
328
341
  function durableChannelSurface(getNamespace, opts) {
329
342
  return channelDispatch(opts.channels.map((c) => resolveChannel(c, opts.env)), {
330
343
  agent: {
@@ -335,7 +348,7 @@ function durableChannelSurface(getNamespace, opts) {
335
348
  channels: [],
336
349
  connections: []
337
350
  },
338
- services: opts.services?.(opts.env),
351
+ services: resolveServices(opts.services, opts.env, opts.agentName),
339
352
  waitUntil: opts.waitUntil,
340
353
  run: async (message, o) => {
341
354
  const namespace = getNamespace();
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";
@@ -0,0 +1,6 @@
1
+ //#region src/isolate-local.d.ts
2
+ type IsolateLocal = <T>(key: string | symbol, make: () => T) => T;
3
+ declare function makeIsolateLocalFallback(): IsolateLocal;
4
+ declare const isolateLocal: IsolateLocal;
5
+ //#endregion
6
+ export { IsolateLocal, isolateLocal, makeIsolateLocalFallback };
@@ -0,0 +1,12 @@
1
+ import * as juneDb from "@junejs/db";
2
+ //#region src/isolate-local.ts
3
+ function makeIsolateLocalFallback() {
4
+ const values = /* @__PURE__ */ new Map();
5
+ return (key, make) => {
6
+ if (!values.has(key)) values.set(key, make());
7
+ return values.get(key);
8
+ };
9
+ }
10
+ const isolateLocal = juneDb.isolateLocal ?? makeIsolateLocalFallback();
11
+ //#endregion
12
+ export { isolateLocal, makeIsolateLocalFallback };
@@ -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.9",
3
+ "version": "1.0.0-dev.11",
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/db": ">=0.0.34-dev.0",
128
+ "@junejs/db": ">=0.1.0-dev.2",
129
129
  "@momiji-rs/sparkdown": "^0.0.4",
130
130
  "lightningcss": "^1.27.0",
131
131
  "oxc-parser": "^0.137.0",
132
132
  "rolldown": "^1.1.0"
133
133
  },
134
134
  "peerDependencies": {
135
- "@junejs/core": ">=0.1.1-dev.16",
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",
@@ -41,6 +41,7 @@ import {
41
41
  type ChannelFactory,
42
42
  } from "@junejs/core/agent-config";
43
43
  import { ensureScope, runInScope } from "@junejs/db";
44
+ import { isolateLocal } from "./isolate-local";
44
45
  import { assertCoreRuntimeVersion } from "./core-version";
45
46
 
46
47
  // ── minimal structural Cloudflare surface (no @cloudflare/workers-types dep) ──
@@ -605,6 +606,43 @@ export function durableAgentSurface(
605
606
  // fails in the background — surfaced via the channel's onError IF one is configured,
606
607
  // otherwise swallowed (runBackground never rejects). The app is expected to bind
607
608
  // env.AGENT when it mounts channels. Returns null for unclaimed requests.
609
+ // The services bag, memoized per (env object, agentName).
610
+ //
611
+ // Why that key. `env` identity is what makes the memo survive: a worker builds
612
+ // the surface inside `fetch`, so the only thing stable across requests is the
613
+ // env object the host hands in — and NOT the provider function, which the
614
+ // documented shape (`services: (e) => makeServices(e)`) recreates on every
615
+ // fetch. But env alone is too coarse: two surfaces mounted in one worker over
616
+ // the same env would then share whichever bag was built first, making the
617
+ // result depend on mount order. `agentName` is the surface's own stable
618
+ // identity, so it partitions them. (One agent must therefore have ONE services
619
+ // provider — mounting the same agentName twice with different providers is
620
+ // incoherent and keeps the first bag.)
621
+ //
622
+ // Held in an isolateLocal so duplicate module instances (workspace symlinks)
623
+ // share one cache; the outer WeakMap is keyed by env so a host handing out
624
+ // fresh env objects gets fresh bags instead of a leak. A non-object env (tests
625
+ // passing a primitive) skips memoization rather than throwing.
626
+ function resolveServices(
627
+ factory: ((env: unknown) => unknown) | undefined,
628
+ env: unknown,
629
+ agentName: string,
630
+ ): unknown {
631
+ if (!factory) return undefined;
632
+ if (typeof env !== "object" || env === null) return factory(env);
633
+ const cache = isolateLocal(
634
+ "june.durableChannelSurface.services",
635
+ () => new WeakMap<object, Map<string, unknown>>(),
636
+ );
637
+ let perAgent = cache.get(env);
638
+ if (!perAgent) {
639
+ perAgent = new Map<string, unknown>();
640
+ cache.set(env, perAgent);
641
+ }
642
+ if (!perAgent.has(agentName)) perAgent.set(agentName, factory(env));
643
+ return perAgent.get(agentName);
644
+ }
645
+
608
646
  export function durableChannelSurface(
609
647
  getNamespace: () => DurableObjectNamespace | undefined,
610
648
  opts: {
@@ -614,7 +652,17 @@ export function durableChannelSurface(
614
652
  // The app's services factory — the SAME one AgentDurableObject uses — resolved HERE
615
653
  // (worker isolate) and exposed to channel hooks as ctx.services. Channel hooks run at
616
654
  // the edge, outside the DO, so they can't read the DO's ambient currentServices(); this
617
- // gives them the same DI bag. Resolved once per surface construction.
655
+ // gives them the same DI bag.
656
+ //
657
+ // Called once per (env object, agentName) — NOT once per surface. A worker
658
+ // typically constructs the surface inside `fetch` (env only exists inside an
659
+ // invocation), so resolving per construction rebuilt every client on every
660
+ // request and any cache the bag held could never hit. Memoizing on env
661
+ // identity fixes that; see resolveServices above for why the key is env ×
662
+ // agentName and not the provider function. Practical consequences: the
663
+ // provider must be a function of `env` alone, it runs again whenever the
664
+ // host hands in a different env object (a new isolate, or a test), and one
665
+ // agentName must have one provider.
618
666
  services?: (env: unknown) => unknown;
619
667
  waitUntil?: (p: Promise<unknown>) => void;
620
668
  },
@@ -624,7 +672,7 @@ export function durableChannelSurface(
624
672
  // A minimal but complete AgentDefinition — channels only read ctx.agent.name; the
625
673
  // full def isn't present in the worker (tools/model live in the DO).
626
674
  agent: { name: opts.agentName, instructions: "", tools: [], skills: [], channels: [], connections: [] } satisfies AgentDefinition,
627
- services: opts.services?.(opts.env),
675
+ services: resolveServices(opts.services, opts.env, opts.agentName),
628
676
  waitUntil: opts.waitUntil,
629
677
  run: async (message, o) => {
630
678
  const namespace = getNamespace();
@@ -0,0 +1,33 @@
1
+ // isolate-local.ts — read @junejs/db's `isolateLocal` WITHOUT a named import.
2
+ //
3
+ // Same hazard core-version.ts exists for: a NAMED import of an export the
4
+ // resolved copy doesn't have dies at module LINK time with an unhelpful
5
+ // SyntaxError, before any of our own diagnostics can run. `isolateLocal` is new
6
+ // in @junejs/db 0.1.0-dev.2, and a version range cannot save us here — the
7
+ // floor has to stay satisfiable by the workspace's own db (which is the very
8
+ // version that lacks it), and `bun pm pack` rewrites a `workspace:*` dep to
9
+ // whatever version it resolves, which can be OLDER still.
10
+ //
11
+ // So: namespace import + optional read, with a local fallback. Unlike the core
12
+ // runtime contract, this one does NOT warrant throwing — isolate-scoped
13
+ // memoization is a lifetime optimization, not a correctness contract. The
14
+ // fallback memoizes in this module instance, so callers keep their cache; the
15
+ // only thing lost against an older db is sharing one value across DUPLICATE
16
+ // copies of the module (workspace symlinks), which costs a missed cache hit,
17
+ // never a wrong answer.
18
+ import * as juneDb from "@junejs/db";
19
+
20
+ export type IsolateLocal = <T>(key: string | symbol, make: () => T) => T;
21
+
22
+ // Exported for tests: the ambient read below can only ever see the workspace's
23
+ // own (matching) db, so the degraded path needs a direct handle to be provable.
24
+ export function makeIsolateLocalFallback(): IsolateLocal {
25
+ const values = new Map<string | symbol, unknown>();
26
+ return <T>(key: string | symbol, make: () => T): T => {
27
+ if (!values.has(key)) values.set(key, make());
28
+ return values.get(key) as T;
29
+ };
30
+ }
31
+
32
+ export const isolateLocal: IsolateLocal =
33
+ (juneDb as { isolateLocal?: IsolateLocal }).isolateLocal ?? makeIsolateLocalFallback();
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);