@manta-eu/horns-server 0.1.2 → 0.1.3

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.
@@ -20,6 +20,8 @@ export type ProxyToMantaAgentsArgs = {
20
20
  request: Request;
21
21
  agentsUrl?: string;
22
22
  session?: MantaSessionState;
23
+ /** The host's own session cookie names, where it has them. Defaults to Horns'. */
24
+ sessionCookiePrefix?: string;
23
25
  };
24
26
  export declare function agentsPathUrl(agentsUrl: string, pathAndQuery: string): URL;
25
27
  export declare function agentsUpstreamUrl(agentsUrl: string, requestUrl: string): URL;
@@ -72,15 +72,15 @@ export function buildAgentsProxyRequest(args) {
72
72
  return { url, init };
73
73
  }
74
74
  function sessionCookiesToWrite(args) {
75
- const { request, session, clearSession } = args;
75
+ const { request, session, clearSession, sessionCookiePrefix: prefix } = args;
76
76
  const secure = requestIsSecure(request.url);
77
77
  if (clearSession)
78
- return clearSessionSetCookies({ secure });
78
+ return clearSessionSetCookies({ secure }, prefix);
79
79
  if (!sessionsDiffer(session.before, session.after))
80
80
  return [];
81
81
  return isPresent(Object.keys(session.after.upstreamCookies))
82
- ? sessionSetCookies(session.after, { secure })
83
- : clearSessionSetCookies({ secure });
82
+ ? sessionSetCookies(session.after, { secure }, prefix)
83
+ : clearSessionSetCookies({ secure }, prefix);
84
84
  }
85
85
  // Agents' cookies never reach the browser: they are folded into this worker's own session cookies.
86
86
  function withSessionCookies(args) {
@@ -97,7 +97,7 @@ function withSessionCookies(args) {
97
97
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
98
98
  }
99
99
  export async function proxyToMantaAgents(args) {
100
- const { request, agentsUrl, session } = args;
100
+ const { request, agentsUrl, session, sessionCookiePrefix } = args;
101
101
  const decision = await evaluateProxyPolicy(request);
102
102
  if (!decision.allow) {
103
103
  return Response.json({ error: decision.error }, { status: decision.status });
@@ -127,5 +127,6 @@ export async function proxyToMantaAgents(args) {
127
127
  request,
128
128
  session: { before, after: captureUpstreamCookies(before, response) },
129
129
  clearSession: new URL(request.url).pathname === logoutPath,
130
+ ...(isPresent(sessionCookiePrefix) ? { sessionCookiePrefix } : {}),
130
131
  });
131
132
  }
@@ -6,7 +6,7 @@ export declare const logoutPath = "/logout";
6
6
  export declare const forwardedOriginHeader = "x-manta-forwarded-origin";
7
7
  export declare function safeRedirect(raw: string | null | undefined): string;
8
8
  export declare function logoutResponseReturningTo(response: Response, request: Request): Response;
9
- export declare function localSignOutResponse(request: Request): Response;
9
+ export declare function localSignOutResponse(request: Request, sessionCookiePrefix?: string): Response;
10
10
  export declare const authUnavailableMessage = "Manta could not be reached, so signing in is not possible right now.";
11
11
  export declare function authUnavailableResponse(request: Request): Response;
12
12
  export declare function loginRequestWithMarker(request: Request): Request;
@@ -52,10 +52,10 @@ function rewrittenLogoutLocation(location, returnTo) {
52
52
  keycloak.searchParams.set("post_logout_redirect_uri", returnTo);
53
53
  return keycloak.toString();
54
54
  }
55
- export function localSignOutResponse(request) {
55
+ export function localSignOutResponse(request, sessionCookiePrefix) {
56
56
  const url = new URL(request.url);
57
57
  const headers = new Headers({ location: url.origin + safeRedirect(url.searchParams.get("redirect")) });
58
- for (const cookie of clearSessionSetCookies({ secure: requestIsSecure(request.url) })) {
58
+ for (const cookie of clearSessionSetCookies({ secure: requestIsSecure(request.url) }, sessionCookiePrefix)) {
59
59
  headers.append("set-cookie", cookie);
60
60
  }
61
61
  return new Response(null, { status: 302, headers });
@@ -1,5 +1,7 @@
1
1
  import { type MantaSessionState } from "./session.ts";
2
2
  export type DevSessionEnv = {
3
3
  DEV_AGENTS_COOKIE?: string;
4
+ /** A host that signs people in itself names its own session cookies here. Defaults to Horns' own. */
5
+ HORNS_SESSION_COOKIE_PREFIX?: string;
4
6
  };
5
7
  export declare function requestSession(request: Request, env: DevSessionEnv): MantaSessionState | undefined;
@@ -1,11 +1,11 @@
1
1
  import { isPresent } from "@manta-eu/lib/present";
2
- import { agentsJwtCookieName, parseCookieHeader, readSessionState } from "./session.js";
2
+ import { agentsJwtCookieName, parseCookieHeader, readSessionState, sessionCookiePrefixOf, } from "./session.js";
3
3
  const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
4
4
  // HireManta staging will not send a sign-in back to localhost, so `pnpm dev` can borrow a staging agents session
5
5
  // captured by `dev:borrow-session`. It stands in only for requests to this machine that carry no Horns session
6
6
  // yet (agents' token refreshes then land in a real one), and production builds drop it with import.meta.env.DEV.
7
7
  export function requestSession(request, env) {
8
- const session = readSessionState(request.headers.get("cookie"));
8
+ const session = readSessionState(request.headers.get("cookie"), sessionCookiePrefixOf(env));
9
9
  if (isPresent(session) || !import.meta.env.DEV || !isPresent(env.DEV_AGENTS_COOKIE))
10
10
  return session;
11
11
  if (!loopbackHosts.has(new URL(request.url).hostname))
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { datastoreAccessFor, datastoreAccessUnavailableMessage, datastoreNotGran
11
11
  export { agentsSparqlSelect } from "./sparql-select.ts";
12
12
  export { agentsSparqlUpdate, WriteRefusedError } from "./sparql-update.ts";
13
13
  export { criticModelFromEnv, studioModelFromEnv, type StudioModelEnv, type StudioModelResult } from "./llm.ts";
14
- export { isSignedIn, sessionSetCookies, type MantaSessionState } from "./session.ts";
14
+ export { badSessionCookiePrefixMessage, isSignedIn, sessionCookiePrefix, sessionCookiePrefixOf, sessionSetCookies, type MantaSessionState, } from "./session.ts";
15
15
  export { requestSession, type DevSessionEnv } from "./dev-session.ts";
16
16
  export { hornTemplatePath, renderHornDocument } from "./render-document.ts";
17
17
  export { sentryOptionsForDeploy } from "./sentry.ts";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { agentsSparqlUpdate, WriteRefusedError } from "./sparql-update.js";
12
12
  // Also its own entry, `@manta-eu/horns-server/llm`: building a model client must not drag in the render
13
13
  // path, which is JSX and which a Node script cannot load.
14
14
  export { criticModelFromEnv, studioModelFromEnv } from "./llm.js";
15
- export { isSignedIn, sessionSetCookies } from "./session.js";
15
+ export { badSessionCookiePrefixMessage, isSignedIn, sessionCookiePrefix, sessionCookiePrefixOf, sessionSetCookies, } from "./session.js";
16
16
  export { requestSession } from "./dev-session.js";
17
17
  export { hornTemplatePath, renderHornDocument } from "./render-document.js";
18
18
  export { sentryOptionsForDeploy } from "./sentry.js";
package/dist/session.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const agentsJwtCookieName = "auth";
2
+ /** What Horns names its own session cookies. A host with a session of its own overrides it; see `sessionCookiePrefixOf`. */
2
3
  export declare const sessionCookiePrefix = "mh_session";
3
4
  export declare const maxSessionCookieChunks = 8;
4
5
  export type MantaSessionState = {
@@ -12,9 +13,17 @@ export declare function isForwardableCookie(name: string, value: string): boolea
12
13
  export declare function parseCookieHeader(header: string | null | undefined): Record<string, string>;
13
14
  export declare function serializeCookie(name: string, value: string, attributes: CookieAttributes): string;
14
15
  export declare function requestIsSecure(requestUrl: string): boolean;
15
- export declare function sessionSetCookies(state: MantaSessionState, attributes: CookieAttributes): string[];
16
- export declare function clearSessionSetCookies(attributes: CookieAttributes): string[];
17
- export declare function readSessionState(cookieHeader: string | null | undefined): MantaSessionState | undefined;
16
+ export declare function sessionSetCookies(state: MantaSessionState, attributes: CookieAttributes, prefix?: string): string[];
17
+ export declare function clearSessionSetCookies(attributes: CookieAttributes, prefix?: string): string[];
18
+ export declare const badSessionCookiePrefixMessage = "HORNS_SESSION_COOKIE_PREFIX is not a cookie name \u2014 use RFC 6265 token characters.";
19
+ /**
20
+ * Which cookies these routes read and write. A host that signs people in itself names its own prefix, or the
21
+ * worker looks for a Horns session the host never wrote and answers every call "not signed in".
22
+ */
23
+ export declare function sessionCookiePrefixOf(env: {
24
+ HORNS_SESSION_COOKIE_PREFIX?: string;
25
+ }): string;
26
+ export declare function readSessionState(cookieHeader: string | null | undefined, prefix?: string): MantaSessionState | undefined;
18
27
  export declare function isSignedIn(state: MantaSessionState | undefined): boolean;
19
28
  export declare function upstreamCookieHeader(state: MantaSessionState): string;
20
29
  export declare function setCookieHeadersOf(response: Response): string[];
package/dist/session.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isPresent } from "@manta-eu/lib/present";
2
2
  import { isRecord } from "@manta-eu/horns";
3
3
  export const agentsJwtCookieName = "auth";
4
+ /** What Horns names its own session cookies. A host with a session of its own overrides it; see `sessionCookiePrefixOf`. */
4
5
  export const sessionCookiePrefix = "mh_session";
5
6
  export const maxSessionCookieChunks = 8;
6
7
  const chunkSize = 3500;
@@ -57,23 +58,37 @@ function chunk(value) {
57
58
  chunks.push(value.slice(i, i + chunkSize));
58
59
  return chunks;
59
60
  }
60
- export function sessionSetCookies(state, attributes) {
61
+ export function sessionSetCookies(state, attributes, prefix = sessionCookiePrefix) {
61
62
  const chunks = chunk(encodeJson(state));
62
63
  if (chunks.length > maxSessionCookieChunks) {
63
64
  throw new Error(`Horns session does not fit in ${maxSessionCookieChunks} cookies.`);
64
65
  }
65
- const written = chunks.map((value, index) => serializeCookie(`${sessionCookiePrefix}${index}`, value, attributes));
66
- const cleared = Array.from({ length: maxSessionCookieChunks - chunks.length }, (_unused, offset) => serializeCookie(`${sessionCookiePrefix}${chunks.length + offset}`, "", { ...attributes, maxAge: 0 }));
66
+ const written = chunks.map((value, index) => serializeCookie(`${prefix}${index}`, value, attributes));
67
+ const cleared = Array.from({ length: maxSessionCookieChunks - chunks.length }, (_unused, offset) => serializeCookie(`${prefix}${chunks.length + offset}`, "", { ...attributes, maxAge: 0 }));
67
68
  return [...written, ...cleared];
68
69
  }
69
- export function clearSessionSetCookies(attributes) {
70
- return Array.from({ length: maxSessionCookieChunks }, (_unused, index) => serializeCookie(`${sessionCookiePrefix}${index}`, "", { ...attributes, maxAge: 0 }));
71
- }
72
- export function readSessionState(cookieHeader) {
70
+ export function clearSessionSetCookies(attributes, prefix = sessionCookiePrefix) {
71
+ return Array.from({ length: maxSessionCookieChunks }, (_unused, index) => serializeCookie(`${prefix}${index}`, "", { ...attributes, maxAge: 0 }));
72
+ }
73
+ export const badSessionCookiePrefixMessage = "HORNS_SESSION_COOKIE_PREFIX is not a cookie name — use RFC 6265 token characters.";
74
+ /**
75
+ * Which cookies these routes read and write. A host that signs people in itself names its own prefix, or the
76
+ * worker looks for a Horns session the host never wrote and answers every call "not signed in".
77
+ */
78
+ export function sessionCookiePrefixOf(env) {
79
+ const configured = env.HORNS_SESSION_COOKIE_PREFIX;
80
+ if (!isPresent(configured))
81
+ return sessionCookiePrefix;
82
+ // `${prefix}0` has to be a cookie name; a prefix that is not one would be dropped silently by the browser.
83
+ if (!cookieNamePattern.test(`${configured}0`))
84
+ throw new Error(badSessionCookiePrefixMessage);
85
+ return configured;
86
+ }
87
+ export function readSessionState(cookieHeader, prefix = sessionCookiePrefix) {
73
88
  const jar = parseCookieHeader(cookieHeader);
74
89
  let encoded = "";
75
90
  for (let index = 0; index < maxSessionCookieChunks; index += 1) {
76
- const part = jar[`${sessionCookiePrefix}${index}`];
91
+ const part = jar[`${prefix}${index}`];
77
92
  if (!isPresent(part))
78
93
  break;
79
94
  encoded += part;
package/dist/worker.js CHANGED
@@ -10,6 +10,7 @@ import { studioRoutes } from "./studio-routes.js";
10
10
  import { contentSecurityPolicy } from "./csp.js";
11
11
  import { criticModelFromEnv } from "./llm.js";
12
12
  import { requestSession } from "./dev-session.js";
13
+ import { sessionCookiePrefixOf } from "./session.js";
13
14
  import { asJsonRequest, followedLink, noPageToOpen, signInFirst } from "./entity-links.js";
14
15
  import { isSignedIn } from "./session.js";
15
16
  import { sentryOptionsForDeploy } from "./sentry.js";
@@ -59,6 +60,7 @@ export function createHornsApp({ shapes, componentApi, typecheck, criticModel =
59
60
  return proxyToMantaAgents({
60
61
  request,
61
62
  agentsUrl: c.env.MANTA_AGENTS_URL,
63
+ sessionCookiePrefix: sessionCookiePrefixOf(c.env),
62
64
  ...(isPresent(session) ? { session } : {}),
63
65
  });
64
66
  }
@@ -80,9 +82,9 @@ export function createHornsApp({ shapes, componentApi, typecheck, criticModel =
80
82
  const response = await agentsProxy(c);
81
83
  // 5xx: agents never dropped our cookies — sign out locally instead.
82
84
  if (response.status >= 500)
83
- return localSignOutResponse(c.req.raw);
85
+ return localSignOutResponse(c.req.raw, sessionCookiePrefixOf(c.env));
84
86
  return logoutResponseReturningTo(response, c.req.raw);
85
- }, c.req.raw, localSignOutResponse));
87
+ }, c.req.raw, (request) => localSignOutResponse(request, sessionCookiePrefixOf(c.env))));
86
88
  const signedOutLoginStatus = { valid: false, canSwitchDatastore: false, allowedModels: [] };
87
89
  app.get("/api/login", async (c) => {
88
90
  const denied = sameOriginDeniedResponse(c.req.raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manta-eu/horns-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The Manta Horns worker as a library: Hono routes a Cloudflare host mounts on its own app.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,8 +49,8 @@
49
49
  "sparqljs": "^3.7.4",
50
50
  "typescript": "^5.9.0",
51
51
  "zod": "^4.5.4",
52
- "@manta-eu/horns": "0.1.2",
53
- "@manta-eu/horns-kit": "0.1.2"
52
+ "@manta-eu/horns": "0.1.3",
53
+ "@manta-eu/horns-kit": "0.1.3"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@cloudflare/vite-plugin": "1.47.0",