@junejs/server 1.0.0-dev.28 → 1.0.0-dev.36

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.
Files changed (75) hide show
  1. package/dist/agent-compile.d.ts +8 -9
  2. package/dist/agent-discover.d.ts +3 -4
  3. package/dist/agent-durable.d.ts +20 -19
  4. package/dist/agent-durable.js +41 -6
  5. package/dist/agent-native.d.ts +10 -11
  6. package/dist/agent-native.js +6 -2
  7. package/dist/app.d.ts +4 -5
  8. package/dist/app.js +2 -1
  9. package/dist/blob.d.ts +4 -5
  10. package/dist/build.d.ts +6 -6
  11. package/dist/build.js +34 -7
  12. package/dist/client-bundle.d.ts +8 -9
  13. package/dist/{config-freeze-DwI-Rmvq.d.ts → config-freeze-ZWC_Qd4h.d.ts} +1 -1
  14. package/dist/config-freeze.d.ts +1 -1
  15. package/dist/config-loader.d.ts +3 -4
  16. package/dist/connection-auth.d.ts +8 -9
  17. package/dist/content.d.ts +9 -10
  18. package/dist/core-version.d.ts +3 -4
  19. package/dist/css-modules-ambient.d.ts +2 -1
  20. package/dist/css-modules.d.ts +10 -11
  21. package/dist/css.d.ts +9 -10
  22. package/dist/d1-migrate.d.ts +6 -7
  23. package/dist/d1-migrate.js +3 -2
  24. package/dist/db.d.ts +2 -2
  25. package/dist/deploy.d.ts +3 -4
  26. package/dist/deploy.js +3 -2
  27. package/dist/dev-reload.d.ts +3 -4
  28. package/dist/dev.d.ts +4 -5
  29. package/dist/favicon.d.ts +16 -17
  30. package/dist/host.d.ts +5 -5
  31. package/dist/icon-letter.d.ts +2 -3
  32. package/dist/index.d.ts +4 -4
  33. package/dist/instrumentation.d.ts +2 -3
  34. package/dist/instrumentation.js +2 -1
  35. package/dist/island-registry.d.ts +6 -7
  36. package/dist/isolate-local.d.ts +4 -5
  37. package/dist/kv.d.ts +3 -4
  38. package/dist/llms-links.d.ts +9 -0
  39. package/dist/llms-links.js +65 -0
  40. package/dist/{manifest-gcNWqWoI.d.ts → manifest-DSNX4I-G.d.ts} +2 -2
  41. package/dist/manifest.d.ts +1 -1
  42. package/dist/negotiate.d.ts +4 -3
  43. package/dist/negotiate.js +24 -14
  44. package/dist/{pipeline-cCR6MGNK.d.ts → pipeline-BHUjBO0G.d.ts} +5 -1
  45. package/dist/pipeline.d.ts +2 -2
  46. package/dist/pipeline.js +263 -39
  47. package/dist/resources.d.ts +5 -6
  48. package/dist/router.d.ts +12 -13
  49. package/dist/rsc-build.d.ts +6 -7
  50. package/dist/rsc-bundle.d.ts +6 -7
  51. package/dist/rsc-manifest.d.ts +5 -6
  52. package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
  53. package/dist/rsc-runtime/flight-render.d.ts +2 -3
  54. package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
  55. package/dist/rsc-runtime/flight-to-html.js +3 -2
  56. package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
  57. package/dist/segment.d.ts +4 -5
  58. package/dist/sqlite-driver.d.ts +10 -11
  59. package/dist/static-files.d.ts +4 -5
  60. package/dist/tsconfig-jsx.d.ts +4 -5
  61. package/dist/turn-events.d.ts +2 -3
  62. package/dist/web-manifest.d.ts +3 -4
  63. package/dist/web-manifest.js +2 -6
  64. package/dist/{worker-BkFHGtxp.d.ts → worker-Dv4l6Ybs.d.ts} +4 -2
  65. package/dist/worker.d.ts +1 -1
  66. package/dist/worker.js +35 -18
  67. package/package.json +5 -5
  68. package/src/agent-durable.ts +39 -10
  69. package/src/agent-native.ts +5 -1
  70. package/src/build.ts +63 -5
  71. package/src/llms-links.ts +96 -0
  72. package/src/negotiate.ts +39 -15
  73. package/src/pipeline.ts +437 -47
  74. package/src/web-manifest.ts +2 -6
  75. package/src/worker.ts +55 -17
@@ -1,6 +1,6 @@
1
1
  import { JuneDb } from "@junejs/core/resources";
2
2
  //#region src/sqlite-driver.d.ts
3
- type SyncStatement = {
3
+ export type SyncStatement = {
4
4
  all(...params: unknown[]): unknown[];
5
5
  get(...params: unknown[]): unknown;
6
6
  run(...params: unknown[]): {
@@ -8,18 +8,17 @@ type SyncStatement = {
8
8
  lastInsertRowid?: number | bigint;
9
9
  };
10
10
  };
11
- type SyncSqlite = {
11
+ export type SyncSqlite = {
12
12
  query(sql: string): SyncStatement;
13
13
  exec(sql: string): void;
14
14
  close(): void;
15
15
  };
16
- declare const NODE_SQLITE_MIN_LTS = "22.13.0";
17
- declare const NODE_SQLITE_MIN_ODD = "23.4.0";
18
- declare function nodeSqliteHelp(nodeVersion: string): string;
19
- declare function isNodeSqliteExperimentalWarning(type: unknown, message: string): boolean;
16
+ export declare const NODE_SQLITE_MIN_LTS = "22.13.0";
17
+ export declare const NODE_SQLITE_MIN_ODD = "23.4.0";
18
+ export declare function nodeSqliteHelp(nodeVersion: string): string;
19
+ export declare function isNodeSqliteExperimentalWarning(type: unknown, message: string): boolean;
20
20
  type EmitWarning = (warning: string | Error, ...rest: unknown[]) => void;
21
- declare function makeWarningFilter(original: EmitWarning): EmitWarning;
22
- declare function openLocalSqliteSync(path: string): Promise<SyncSqlite>;
23
- declare function openLocalSqlite(path: string): Promise<JuneDb>;
24
- //#endregion
25
- export { NODE_SQLITE_MIN_LTS, NODE_SQLITE_MIN_ODD, SyncSqlite, SyncStatement, isNodeSqliteExperimentalWarning, makeWarningFilter, nodeSqliteHelp, openLocalSqlite, openLocalSqliteSync };
21
+ export declare function makeWarningFilter(original: EmitWarning): EmitWarning;
22
+ export declare function openLocalSqliteSync(path: string): Promise<SyncSqlite>;
23
+ export declare function openLocalSqlite(path: string): Promise<JuneDb>;
24
+ //#endregion
@@ -1,6 +1,5 @@
1
1
  //#region src/static-files.d.ts
2
- declare const RESERVED_PREFIX = "_june";
3
- declare function contentTypeFor(pathname: string): string;
4
- declare function safeRelativePath(pathname: string): string | null;
5
- //#endregion
6
- export { RESERVED_PREFIX, contentTypeFor, safeRelativePath };
2
+ export declare const RESERVED_PREFIX = "_june";
3
+ export declare function contentTypeFor(pathname: string): string;
4
+ export declare function safeRelativePath(pathname: string): string | null;
5
+ //#endregion
@@ -1,16 +1,15 @@
1
1
  //#region src/tsconfig-jsx.d.ts
2
- declare const stripJsonc: (s: string) => string;
2
+ export declare const stripJsonc: (s: string) => string;
3
3
  /** Read the app's tsconfig.json and return compilerOptions.jsxImportSource.
4
4
  * Follows one level of `extends` so apps that inherit from a base tsconfig
5
5
  * (e.g. "@kurajs/docs/tsconfig.kura.json") are handled correctly.
6
6
  * Returns undefined when absent or unreadable. */
7
- declare function appJsxImportSource(appRoot: string): Promise<string | undefined>;
7
+ export declare function appJsxImportSource(appRoot: string): Promise<string | undefined>;
8
8
  /** The transform.jsx both bundles use: June's island JSX runtime, with the explicit importSource
9
9
  * skipped when the app's tsconfig already declares it — rolldown then reads it from tsconfig
10
10
  * silently instead of warning about the (same-value) override. */
11
- declare function jsxTransform(appRoot: string): Promise<{
11
+ export declare function jsxTransform(appRoot: string): Promise<{
12
12
  runtime: "automatic";
13
13
  importSource?: string;
14
14
  }>;
15
- //#endregion
16
- export { appJsxImportSource, jsxTransform, stripJsonc };
15
+ //#endregion
@@ -1,5 +1,4 @@
1
1
  import { AgentSession, TurnEvent } from "@junejs/core/agent-runtime";
2
2
  //#region src/turn-events.d.ts
3
- declare function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable<TurnEvent>;
4
- //#endregion
5
- export { observeTurnEvents };
3
+ export declare function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable<TurnEvent>;
4
+ //#endregion
@@ -1,6 +1,5 @@
1
1
  import { DocumentConfig } from "@junejs/core/document";
2
2
  //#region src/web-manifest.d.ts
3
- declare function shortName(site: DocumentConfig["site"]): string | undefined;
4
- declare function webManifest(config: DocumentConfig): string;
5
- //#endregion
6
- export { shortName, webManifest };
3
+ export declare const shortName: (site: DocumentConfig["site"]) => string | undefined;
4
+ export declare function webManifest(config: DocumentConfig): string;
5
+ //#endregion
@@ -1,11 +1,7 @@
1
+ import { siteShortName } from "@junejs/core/config";
1
2
  import { resolveThemeColor } from "@junejs/core/document";
2
3
  //#region src/web-manifest.ts
3
- function shortName(site) {
4
- if (site.shortName) return site.shortName;
5
- const name = site.name?.trim();
6
- if (!name) return void 0;
7
- return name.split(/\s+[—–|-]\s+|:\s+/)[0].trim() || name;
8
- }
4
+ const shortName = (site) => siteShortName(site);
9
5
  function webManifest(config) {
10
6
  const base = config.basePath ?? "";
11
7
  const theme = resolveThemeColor(config);
@@ -1,4 +1,4 @@
1
- import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-cCR6MGNK.js";
1
+ import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-BHUjBO0G.js";
2
2
  import { Channel, ChannelFactory } from "@junejs/core/agent-config";
3
3
  import { BrandedRoute } from "@junejs/core/route";
4
4
  import { AgentConfig } from "@junejs/core/config";
@@ -40,7 +40,9 @@ type WorkerManifest = {
40
40
  resources?: (env?: unknown) => Promise<Resources> | Resources;
41
41
  services?: (env?: unknown) => unknown;
42
42
  };
43
- declare function createWorker(manifest: WorkerManifest): {
43
+ declare function createWorker(manifest: WorkerManifest, opts?: {
44
+ staticBuild?: boolean;
45
+ }): {
44
46
  fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response>;
45
47
  };
46
48
  type AssetEnv = {
package/dist/worker.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as withDenoAssets, i as withAssets, n as WorkerManifest, r as createWorker, t as WorkerExecutionContext } from "./worker-BkFHGtxp.js";
1
+ import { a as withDenoAssets, i as withAssets, n as WorkerManifest, r as createWorker, t as WorkerExecutionContext } from "./worker-Dv4l6Ybs.js";
2
2
  export { WorkerExecutionContext, WorkerManifest, createWorker, withAssets, withDenoAssets };
package/dist/worker.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { durableAgentSurface, durableChannelSurface } from "./agent-durable.js";
2
+ import { acceptTarget } from "./negotiate.js";
2
3
  import { createPipeline } from "./pipeline.js";
3
4
  import { RESERVED_PREFIX, contentTypeFor, safeRelativePath } from "./static-files.js";
4
5
  //#region src/worker.ts
@@ -45,7 +46,7 @@ function compilePattern(pattern) {
45
46
  names
46
47
  };
47
48
  }
48
- function createWorker(manifest) {
49
+ function createWorker(manifest, opts = {}) {
49
50
  const dynamic = (manifest.dynamicRoutes ?? []).map((d) => ({
50
51
  ...compilePattern(d.pattern),
51
52
  def: d.def,
@@ -92,6 +93,7 @@ function createWorker(manifest) {
92
93
  agentSurface,
93
94
  i18n: manifest.i18n,
94
95
  routeList: () => routeList,
96
+ staticBuild: opts.staticBuild,
95
97
  earlyHints: manifest.earlyHints,
96
98
  htmlCacheControl: manifest.htmlCacheControl,
97
99
  notFoundComponent: manifest.notFound,
@@ -155,28 +157,41 @@ function createWorker(manifest) {
155
157
  } };
156
158
  }
157
159
  const estimateTokens = (s) => String(Math.ceil(s.length / 4));
160
+ function varyAccept(headers) {
161
+ const vary = headers.get("vary");
162
+ if (!vary) headers.set("vary", "accept");
163
+ else if (vary.trim() !== "*" && !vary.split(",").some((v) => v.trim().toLowerCase() === "accept")) headers.set("vary", `${vary}, accept`);
164
+ }
158
165
  function withAssets(pipeline, opts = {}) {
159
166
  return { async fetch(request, env, ctx) {
160
167
  const assets = env?.ASSETS;
161
168
  const url = new URL(request.url);
162
169
  const isPagePath = !/\.[a-z0-9]+$/i.test(url.pathname);
163
- const accept = request.headers.get("accept") ?? "";
164
- if (assets && request.method === "GET" && isPagePath && /text\/markdown/.test(accept)) {
165
- const base = url.pathname === "/" ? "/index" : url.pathname.replace(/\/+$/, "");
166
- const mdUrl = new URL(url);
167
- mdUrl.pathname = `${base}.md`;
168
- const a = await assets.fetch(new Request(mdUrl.toString(), { headers: request.headers }));
169
- if (a.ok) {
170
- const body = await a.text();
171
- const headers = new Headers(a.headers);
172
- headers.set("content-type", "text/markdown; charset=utf-8");
173
- headers.set("x-markdown-tokens", estimateTokens(body));
174
- if (opts.link) headers.set("link", opts.link);
175
- return new Response(body, {
176
- status: 200,
177
- headers
178
- });
170
+ const target = isPagePath ? acceptTarget(request.headers.get("accept") ?? "") : null;
171
+ if (target && (request.method === "GET" || request.method === "HEAD")) {
172
+ if (assets && request.method === "GET" && (target === "md" || target === "json")) {
173
+ const base = url.pathname === "/" ? "/index" : url.pathname.replace(/\/+$/, "");
174
+ const assetUrl = new URL(url);
175
+ assetUrl.pathname = `${base}.${target}`;
176
+ const a = await assets.fetch(new Request(assetUrl.toString(), { headers: request.headers }));
177
+ if (a.ok) {
178
+ const headers = new Headers(a.headers);
179
+ if (opts.link) headers.set("link", opts.link);
180
+ varyAccept(headers);
181
+ if (target === "json") return new Response(a.body, {
182
+ status: 200,
183
+ headers
184
+ });
185
+ const body = await a.text();
186
+ headers.set("content-type", "text/markdown; charset=utf-8");
187
+ headers.set("x-markdown-tokens", estimateTokens(body));
188
+ return new Response(body, {
189
+ status: 200,
190
+ headers
191
+ });
192
+ }
179
193
  }
194
+ return pipeline.fetch(request, env, ctx);
180
195
  }
181
196
  if (assets && (request.method === "GET" || request.method === "HEAD")) {
182
197
  const a = await assets.fetch(request);
@@ -184,10 +199,12 @@ function withAssets(pipeline, opts = {}) {
184
199
  const ct = a.headers.get("content-type") ?? "";
185
200
  const addLink = !!opts.link && ct.includes("text/html") && !a.headers.has("link");
186
201
  const immutable = /\.[a-f0-9]{8,}\.(css|js)$/.test(url.pathname);
187
- if (addLink || immutable) {
202
+ const negotiated = isPagePath && ct.includes("text/html");
203
+ if (addLink || immutable || negotiated) {
188
204
  const headers = new Headers(a.headers);
189
205
  if (addLink) headers.set("link", opts.link);
190
206
  if (immutable) headers.set("cache-control", "public, max-age=31536000, immutable");
207
+ if (negotiated) varyAccept(headers);
191
208
  return new Response(a.body, {
192
209
  status: a.status,
193
210
  headers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "1.0.0-dev.28",
3
+ "version": "1.0.0-dev.36",
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",
@@ -136,14 +136,14 @@
136
136
  "@momiji-rs/sparkdown": "^0.0.4",
137
137
  "@resvg/resvg-wasm": "^2.6.2",
138
138
  "lightningcss": "^1.27.0",
139
- "oxc-parser": "^0.137.0",
139
+ "oxc-parser": "^0.151.0",
140
140
  "rolldown": "^1.1.0"
141
141
  },
142
142
  "peerDependencies": {
143
- "@junejs/core": ">=0.2.0-dev.48",
143
+ "@junejs/core": ">=0.2.0-dev.57",
144
144
  "react": "^19.2.0",
145
145
  "react-dom": "^19.2.0",
146
- "react-server-dom-webpack": "19.2.7",
146
+ "react-server-dom-webpack": "19.3.0",
147
147
  "pg": "^8.0.0",
148
148
  "mysql2": "^3.0.0",
149
149
  "@libsql/client": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
@@ -176,6 +176,6 @@
176
176
  "@types/pg": "^8.20.0",
177
177
  "mysql2": "^3.22.5",
178
178
  "pg": "^8.21.0",
179
- "react-server-dom-webpack": "19.2.7"
179
+ "react-server-dom-webpack": "19.3.0"
180
180
  }
181
181
  }
@@ -17,8 +17,11 @@
17
17
 
18
18
  import {
19
19
  AgentSession,
20
+ grantAnswer,
20
21
  ResumeAuthorizationError,
21
22
  withSystem,
23
+ type Answerers,
24
+ type AuthorizeAnswer,
22
25
  type ChannelPolicy,
23
26
  type EventSink,
24
27
  type TurnEvent,
@@ -32,6 +35,7 @@ import {
32
35
  type TurnError,
33
36
  type TurnFailurePhase,
34
37
  } from "@junejs/core/agent-runtime";
38
+ import type { Principal } from "@junejs/core/context";
35
39
  import type { Resources } from "@junejs/core/resources";
36
40
  import { observeTurnEvents } from "./turn-events";
37
41
  import {
@@ -238,6 +242,9 @@ export type DoAgentDef = {
238
242
  // emitted mid-turn and ALS propagates), but that is NOT contract — don't rely on
239
243
  // ambient db/services; close over what you need.
240
244
  onTurnError?: (failure: { turnId: string; error: TurnError; phase?: TurnFailurePhase; step?: string }) => void | Promise<void>;
245
+ // Decides a { policy } answerer of a parked requestInput at /resume (#261) — the same hook
246
+ // an AgentDefinition carries natively; assembleDurable passes agent.ts's through.
247
+ authorizeAnswer?: AuthorizeAnswer;
241
248
  };
242
249
 
243
250
  // The agent runtime INSIDE a Durable Object. A plain class (constructor takes the
@@ -280,6 +287,7 @@ export class AgentDurableObject {
280
287
  private connectionsOpened: Promise<void> | undefined;
281
288
  private readonly doEnv: unknown;
282
289
  private readonly services: unknown;
290
+ private readonly authorizeAnswer?: AuthorizeAnswer;
283
291
  private resolveResources(): Promise<Resources> {
284
292
  return (this.resourcesOpened ??= Promise.resolve(
285
293
  typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : (this.resourcesInput ?? {}),
@@ -321,6 +329,7 @@ export class AgentDurableObject {
321
329
  this.connectionsInput = def.connections;
322
330
  this.doEnv = def.env;
323
331
  this.services = def.services;
332
+ this.authorizeAnswer = def.authorizeAnswer;
324
333
  const name = def.name ?? "agent";
325
334
  // Failure observability (#76): a turn that dies after the fast-ACK has no other
326
335
  // observable surface on the edge — the webhook already 200'd and runBackground
@@ -564,8 +573,8 @@ export class AgentDurableObject {
564
573
  // VERIFIED identity, so the app must authenticate it upstream (e.g. take the user id from a
565
574
  // signature-checked Slack interaction payload), never expose this endpoint raw to clients.
566
575
  if (req.method === "POST" && url.pathname.endsWith("/resume")) {
567
- const { turnId, inputId, input, by, source, target } = (await req.json()) as {
568
- turnId: string; inputId: string; input: unknown; by?: string;
576
+ const { turnId, inputId, input, by, principal, source, target } = (await req.json()) as {
577
+ turnId: string; inputId: string; input: unknown; by?: string; principal?: Principal;
569
578
  source?: string; target?: ResumeDeliveryTarget;
570
579
  };
571
580
  await ensureScope();
@@ -595,7 +604,23 @@ export class AgentDurableObject {
595
604
  let session: AgentSession;
596
605
  try {
597
606
  session = this.resolveSession(key);
598
- runInScope({ resources, services: this.services }, () => session.resume(turnId, inputId, input, { by }));
607
+ } catch (err) {
608
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
609
+ }
610
+ // A { policy } answerer is the app's call (#261), made before the synchronous
611
+ // resume-then-subscribe section and inside the request scope (the hook may read the db).
612
+ // The hook is app code: a failure there (a db outage) is a 500 — never the 409 a
613
+ // resume-state conflict gets, which would tell the caller its answer is stale.
614
+ let granted: Answerers | undefined;
615
+ try {
616
+ granted = await runInScope({ resources, services: this.services }, () =>
617
+ grantAnswer(session, { turnId, inputId, by, principal }, this.authorizeAnswer));
618
+ } catch (err) {
619
+ console.error(`[june] agent "${this.name}": authorizeAnswer failed for input "${inputId}" of turn ${turnId}:`, err);
620
+ return Response.json({ error: `authorizeAnswer failed: ${err instanceof Error ? err.message : String(err)} — the answer was NOT applied` }, { status: 500 });
621
+ }
622
+ try {
623
+ runInScope({ resources, services: this.services }, () => session.resume(turnId, inputId, input, { by, granted }));
599
624
  } catch (err) {
600
625
  const message = err instanceof Error ? err.message : String(err);
601
626
  // 403: the resumer may not answer; 409: not suspended / wrong turn / wrong input id / key mismatch
@@ -1042,16 +1067,20 @@ export function durableChannelSurface(
1042
1067
  // run the turn, so on a serialization failure we drop it rather than let an
1043
1068
  // unserializable payload take down turn forwarding entirely. (raw is optional on
1044
1069
  // InboundEvent precisely because it may not survive this boundary.)
1045
- // Serialize a /resume RPC body. `input` is `unknown` — the human's answer — so a
1046
- // (third-party) host could hand us something JSON.stringify chokes on (BigInt, circular).
1047
- // Unlike serializeTurn's `raw`, `input` is essential: silently dropping it would resume the
1048
- // turn with the wrong answer. So fail loudly with a clear message rather than corrupt the resume.
1049
- function serializeResume(o: { turnId: string; inputId: string; input: unknown; by?: string; source?: string; target?: ResumeDeliveryTarget }): string {
1070
+ // Serialize a /resume RPC body. `input` (the human's answer) and `principal` (the resumer,
1071
+ // whose extra fields are `unknown`) come from a (third-party) host and could hold something
1072
+ // JSON.stringify chokes on (BigInt, circular). Unlike serializeTurn's `raw`, neither is
1073
+ // droppable: resuming without them answers wrongly or as the wrong person. So fail loudly,
1074
+ // naming the field that failed, rather than corrupt the resume.
1075
+ function serializeResume(o: { turnId: string; inputId: string; input: unknown; by?: string; principal?: Principal; source?: string; target?: ResumeDeliveryTarget }): string {
1050
1076
  try {
1051
- return JSON.stringify({ turnId: o.turnId, inputId: o.inputId, input: o.input, by: o.by, source: o.source, target: o.target });
1077
+ return JSON.stringify({ turnId: o.turnId, inputId: o.inputId, input: o.input, by: o.by, principal: o.principal, source: o.source, target: o.target });
1052
1078
  } catch (err) {
1079
+ const culprit = (["input", "principal"] as const).find((field) => {
1080
+ try { JSON.stringify(o[field]); return false; } catch { return true; }
1081
+ }) ?? "payload";
1053
1082
  // API-neutral prefix: this serializer backs both resumeStream and resumeDelivered.
1054
- throw new Error(`resume: input is not JSON-serializable (${(err as Error).message}) — a resume answer must round-trip to the DO`);
1083
+ throw new Error(`resume: ${culprit} is not JSON-serializable (${(err as Error).message}) — a resume must round-trip to the DO`);
1055
1084
  }
1056
1085
  }
1057
1086
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  import {
11
11
  AgentSession,
12
+ grantAnswer,
12
13
  withSystem,
13
14
  type ChannelPolicy,
14
15
  type EventSink,
@@ -363,7 +364,10 @@ export function mountAgent(
363
364
  // HITL: answer a parked turn and stream its continuation (the approval-button path).
364
365
  resumeStream: async function* (o) {
365
366
  const session = runtime.session(agent.name, o.session ?? "default");
366
- const { turnId } = session.resume(o.turnId, o.inputId, o.input, { by: o.by });
367
+ // A { policy } answerer is the app's call (#261): decided here, before the synchronous
368
+ // resume, so resume-then-subscribe stays free of awaits.
369
+ const granted = await grantAnswer(session, o, agent.authorizeAnswer);
370
+ const { turnId } = session.resume(o.turnId, o.inputId, o.input, { by: o.by, granted });
367
371
  yield* observeTurnEvents(session, turnId);
368
372
  },
369
373
  // SESSION RESET (#129): same seam as durableChannelSurface — the in-process runtimes'
package/src/build.ts CHANGED
@@ -314,7 +314,9 @@ export async function juneBuild(
314
314
  // The Link header is frozen here from the same builder the pipeline uses, so
315
315
  // the static and dynamic surfaces advertise identically. The adapter wraps the
316
316
  // portable pipeline for its target (workers() → withAssets).
317
- const linkHeader = buildLinkHeader(frozen.agent);
317
+ // (catalogs: the same publication rule as the pipeline's catalogOrigin — a
318
+ // basePath site publishes no /.well-known catalog, so it links none.)
319
+ const linkHeader = buildLinkHeader(frozen.agent, { catalogs: !frozen.document.basePath });
318
320
  const adapterEntry = adapter.entry({ linkHeader });
319
321
 
320
322
  // Resources (when declared) are bound from the worker's env (env.DB → D1) by
@@ -506,7 +508,16 @@ ${doClass}`;
506
508
  if (cssAsset) manifest.document.styles = `/${cssAsset}`; // prerendered HTML links the hashed sheet
507
509
  if (clientAsset) manifest.document.clientScript = `/${clientAsset}`;
508
510
  if (moduleCssAsset) manifest.document.moduleStyles = `/${moduleCssAsset}`;
509
- const worker = createWorker(manifest);
511
+ // A static site has no server: /mcp (and the WebMCP tools that proxy to it)
512
+ // and the HTTP API (POST /api/<id>, /openapi.json) do not exist there. Render
513
+ // with both projected out, so the published files advertise only what is
514
+ // deployed: llms.txt drops its MCP and HTTP API sections, the catalogs and the
515
+ // generated skill list neither, pages register no WebMCP tools.
516
+ const isStatic = adapter.capabilities.runtime === "static";
517
+ if (isStatic) manifest.agent = { ...manifest.agent, mcp: false, api: false, webmcp: false };
518
+ // The static() target prerenders its sitemap too; that build is where the
519
+ // route contract lets `staticPaths` run, so its sitemap lists those pages.
520
+ const worker = createWorker(manifest, { staticBuild: isStatic });
510
521
  let hasAssets = false;
511
522
 
512
523
  // ---- public/ → assets/ : verbatim static files (favicon, images, fonts) ----
@@ -567,7 +578,6 @@ ${doClass}`;
567
578
  // dynamic routes via their staticPaths, and write <stem>/index.html so clean URLs
568
579
  // resolve on a dumb file host with no rewrite server. Other targets keep the
569
580
  // opt-in `prerender: true` behavior and the flat <stem>.html naming (byte-identical).
570
- const isStatic = adapter.capabilities.runtime === "static";
571
581
  const i18n = frozen.i18n;
572
582
 
573
583
  // Render ONE route pathname through the worker → HTML (+ .md/.json projections).
@@ -604,6 +614,17 @@ ${doClass}`;
604
614
 
605
615
  // Locale variants a static route is emitted at: no i18n → [path]; with i18n → one
606
616
  // per locale (defaultLocale keeps the bare path, others get their localeHref prefix).
617
+ // A static build is ONE file tree on ONE host: a locale on its own domain has no
618
+ // path in it (and a basePath can't apply there), so say so instead of failing
619
+ // later with a prerender 404.
620
+ if (isStatic && i18n) {
621
+ const onDomain = Object.keys(i18n.locales).filter((l) => i18n.locales[l]!.domain);
622
+ if (onDomain.length) {
623
+ throw new Error(
624
+ `[june build] the static() target serves one host, so i18n locales with their own domain are not supported (${onDomain.join(", ")}). Give them a path prefix instead, e.g. { path: "/${onDomain[0]}" }.`,
625
+ );
626
+ }
627
+ }
607
628
  const localeVariants = (path: string): string[] =>
608
629
  isStatic && i18n
609
630
  ? [...new Set(Object.keys(i18n.locales).map((l) => localeHref(i18n, path, l)))]
@@ -624,6 +645,9 @@ ${doClass}`;
624
645
  const sp = dyn.def.staticPaths;
625
646
  if (!sp) continue;
626
647
  const paths = typeof sp === "function" ? await sp() : sp;
648
+ // Resolve a producer ONCE: the prerendered sitemap below reads this same
649
+ // def, so it lists exactly the set prerendered here without a second call.
650
+ dyn.def.staticPaths = paths;
627
651
  for (const p of paths) await prerenderOne(p, dyn.def);
628
652
  }
629
653
  // Framework surfaces the worker would otherwise generate on the fly. Guarded on
@@ -635,11 +659,45 @@ ${doClass}`;
635
659
  ["/llms.txt", "llms.txt"],
636
660
  ["/sitemap.xml", "sitemap.xml"],
637
661
  ];
638
- for (const [reqPath, file] of extra) {
662
+ // A sitemap needs absolute URLs, and the prerender host is a placeholder: with
663
+ // no public origin configured the pipeline answers 404 for it (and the loop
664
+ // below skips it), so say why rather than silently ship no sitemap.
665
+ if (manifest.agent.discovery && !manifest.document.site.url && !manifest.document.deployOrigin) {
666
+ console.warn(
667
+ "[june build] no sitemap.xml: set site.url (or deploy.domain) in june.config.ts so its URLs are absolute.",
668
+ );
669
+ }
670
+ // The agent catalogs (ARD, Agent Skills) — JSON/Markdown files a static host
671
+ // can serve as-is — only when this site publishes them: agent discovery on, a
672
+ // root deploy (a basePath site doesn't own /.well-known), and a known public
673
+ // origin (the files name absolute URLs). Same rule as the pipeline's
674
+ // catalogOrigin, stated here too so an app catch-all route can never answer
675
+ // these paths into the build. The generated skill's path carries its name, so
676
+ // it is read back from the index.
677
+ const doc = manifest.document;
678
+ if (manifest.agent.discovery && !doc.basePath && (doc.site.url || doc.deployOrigin)) {
679
+ extra.push(
680
+ ["/.well-known/ai-catalog.json", ".well-known/ai-catalog.json"],
681
+ ["/.well-known/ard.json", ".well-known/ard.json"],
682
+ ["/.well-known/agent-skills/index.json", ".well-known/agent-skills/index.json"],
683
+ );
684
+ }
685
+ for (let i = 0; i < extra.length; i++) {
686
+ const [reqPath, file] = extra[i]!;
639
687
  const res = await worker.fetch(new Request(`${PRERENDER_ORIGIN}${reqPath}`));
640
688
  if (!res.ok) continue;
641
- await writeFile(join(assetsDir, file), Buffer.from(await res.arrayBuffer()));
689
+ const bytes = Buffer.from(await res.arrayBuffer());
690
+ await mkdir(dirname(join(assetsDir, file)), { recursive: true });
691
+ await writeFile(join(assetsDir, file), bytes);
642
692
  hasAssets = true;
693
+ if (file.endsWith("agent-skills/index.json")) {
694
+ const index = JSON.parse(bytes.toString("utf8")) as { skills?: Array<{ url?: string }> };
695
+ for (const s of index.skills ?? []) {
696
+ if (!s.url) continue;
697
+ const p = new URL(s.url, PRERENDER_ORIGIN).pathname;
698
+ extra.push([p, p.slice(1)]);
699
+ }
700
+ }
643
701
  }
644
702
  // 404.html — GitHub Pages serves it for any unmatched URL. A deliberately-missing
645
703
  // path renders June's not-found HTML (body is written regardless of the 404 status).
@@ -0,0 +1,96 @@
1
+ // /llms.txt links, collected from the routes themselves. Each route may export
2
+ // `llms` (see LlmsEntry in @junejs/core/route): a static route defaults to one link
3
+ // under "Pages" built from its static metadata; a dynamic route — a [param]
4
+ // template an agent can't fetch — lists each real page it serves, or is skipped.
5
+ // Links point at the markdown projection (<path>.md) unless the route turned md off,
6
+ // so an agent reads the page rather than its HTML (llmstxt.org's recommendation).
7
+ import type { LlmsLink, SitemapPage } from "@junejs/core/discovery";
8
+ import type { LlmsEntry } from "@junejs/core/route";
9
+
10
+ import type { RouteResolver } from "./pipeline";
11
+
12
+ const DEFAULT_SECTION = "Pages";
13
+
14
+ const mdPath = (path: string) => (path === "/" ? "/index.md" : `${path.replace(/\/+$/, "")}.md`);
15
+
16
+ export async function collectLlmsLinks(origin: string, routes: string[], resolve: RouteResolver): Promise<LlmsLink[]> {
17
+ const links: LlmsLink[] = [];
18
+ const seen = new Set<string>();
19
+ for (const route of routes) {
20
+ // A dynamic template resolves against its own pattern ("/docs/[slug]" matches
21
+ // /docs/:slug), which is all we need: the route definition, not its data.
22
+ const resolved = await resolve(route);
23
+ if (!resolved || !("def" in resolved)) continue; // resource routes have no page to list
24
+ const { def } = resolved;
25
+ if (def.llms === false) continue;
26
+
27
+ const dynamic = route.includes("[");
28
+ const declared = typeof def.llms === "function" ? await def.llms() : def.llms;
29
+ const entries: LlmsEntry[] = declared ? (Array.isArray(declared) ? declared : [declared]) : dynamic ? [] : [{}];
30
+ // Static metadata only: a metadata FUNCTION needs loader data we don't run here.
31
+ const meta = typeof def.metadata === "object" ? def.metadata : undefined;
32
+
33
+ for (const entry of entries) {
34
+ const path = entry.path ?? (dynamic ? undefined : route);
35
+ if (!path) continue; // a dynamic entry must name its page
36
+ const url = origin + (def.md === false ? path : mdPath(path));
37
+ // dedupe on the FINAL url: "/a" and "/a/" both render /a.md, so they're one page
38
+ if (seen.has(url)) continue;
39
+ seen.add(url);
40
+ const own = path === route; // the route's own page → its metadata describes it
41
+ links.push({
42
+ title: entry.title ?? (own ? meta?.title : undefined) ?? path,
43
+ url,
44
+ description: entry.description ?? (own ? meta?.description : undefined),
45
+ section: entry.section ?? DEFAULT_SECTION,
46
+ optional: entry.optional === true,
47
+ });
48
+ }
49
+ }
50
+ return links;
51
+ }
52
+
53
+ // /sitemap.xml pages, enumerated like /llms.txt. A static route is one page; a
54
+ // dynamic route lists its real pages via its `llms` entries (their `lastModified`
55
+ // becomes <lastmod>) — the runtime-safe hook /llms.txt already runs. A route
56
+ // that names none stays out: a [param] template isn't a URL. Resource routes
57
+ // (route.*) are machine endpoints, not pages. `llms = false` keeps a static
58
+ // route listed (its URL is known without entries); on a dynamic route it leaves
59
+ // no entries, so its pages drop out of the runtime sitemap too.
60
+ // `staticPaths` join in only when opts.staticPaths is set — the static() build's
61
+ // prerender, the one place the route contract runs them. Even then they're
62
+ // skipped under i18n: they arrive locale-prefixed, while the sitemap derives
63
+ // each page's locale variants from its canonical path.
64
+ export async function collectSitemapPages(
65
+ routes: string[],
66
+ resolve: RouteResolver,
67
+ opts: { i18n?: boolean; staticPaths?: boolean } = {},
68
+ ): Promise<SitemapPage[]> {
69
+ const pages = new Map<string, SitemapPage>();
70
+ const add = (path: string, lastModified?: string | Date) => {
71
+ const key = path === "/" ? "/" : path.replace(/\/+$/, "");
72
+ const prev = pages.get(key);
73
+ if (!prev) pages.set(key, lastModified ? { path: key, lastModified } : { path: key });
74
+ else if (lastModified && !prev.lastModified) prev.lastModified = lastModified;
75
+ };
76
+ for (const route of routes) {
77
+ const resolved = await resolve(route);
78
+ if (!resolved || !("def" in resolved)) continue;
79
+ const { def } = resolved;
80
+ const dynamic = route.includes("[");
81
+ const declared = def.llms ? (typeof def.llms === "function" ? await def.llms() : def.llms) : undefined;
82
+ const entries: LlmsEntry[] = declared ? (Array.isArray(declared) ? declared : [declared]) : [];
83
+ // A static route is exactly one URL — its own. An entry naming another path
84
+ // (an llms.txt pointer elsewhere) is not a page this route serves.
85
+ if (!dynamic) {
86
+ add(route, entries.find((e) => (e.path ?? route) === route)?.lastModified);
87
+ continue;
88
+ }
89
+ for (const e of entries) if (e.path) add(e.path, e.lastModified);
90
+ if (opts.staticPaths && !opts.i18n && def.staticPaths) {
91
+ const sp = typeof def.staticPaths === "function" ? await def.staticPaths() : def.staticPaths;
92
+ for (const p of sp) add(p);
93
+ }
94
+ }
95
+ return [...pages.values()];
96
+ }