@tivility/dsh-session-share 0.2.0 → 0.3.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/README.md CHANGED
@@ -103,6 +103,51 @@ is repository-bound (it locates packages by globbing inside the harness
103
103
  checkout), so the four rules are written out instead. They are short, and they
104
104
  are the whole of what makes a bundle loadable.
105
105
 
106
+ ## Profile scope, and links behind a proxy
107
+
108
+ **This row is safe in any profile.** It activates without `webServer` and
109
+ simply serves nothing, so a suite installed once in `$DSH_HOME/cordis.patch.yml`
110
+ does not turn a Web feature into a headless startup failure.
111
+
112
+ **`publicBaseUrl` is the origin browsers actually reach.** The local bind is
113
+ not the public origin behind a reverse proxy, tunnel, port forwarder, or TLS
114
+ terminator, and no inference recovers one — so a deployment that has one says
115
+ so:
116
+
117
+ ```yaml
118
+ publicBaseUrl: https://dsh.example.com
119
+ ```
120
+
121
+ Origin only: scheme, host, optional port. Credentials, queries, fragments, and
122
+ path prefixes are refused at activation rather than half-supported — every
123
+ route is appended whole, so a prefix would vanish from the middle of each link.
124
+ `DSH_PUBLIC_BASE_URL` sets it process-wide; explicit configuration outranks it.
125
+
126
+ Forwarded headers are deliberately never consulted: `Host` and `X-Forwarded-*`
127
+ are attacker-controlled on any request that reaches the server, so trusting
128
+ them would let a visitor choose the origin the model puts in front of a user.
129
+
130
+ **`publicBaseUrl` says where links point; `trustedHosts` says who is let in.**
131
+ They are two halves of one deployment, and configuring either alone fails in a
132
+ way that reads as a bug in this package. A browser that reached a forwarded
133
+ origin sends `Host: dsh.example.com`, and the browser-trust fence refuses an
134
+ authority it was never told about — so `publicBaseUrl` on its own produces
135
+ links that look correct and answer `403`. Behind a proxy, set both:
136
+
137
+ ```yaml
138
+ publicBaseUrl: https://dsh.example.com
139
+ trustedHosts: ['dsh.example.com']
140
+ ```
141
+
142
+ That the fence still refuses an undeclared authority is the point, not a
143
+ leftover: `publicBaseUrl` changes what a link says, and is deliberately unable
144
+ to widen what the server answers.
145
+
146
+ With no `webServer` **and** no `publicBaseUrl`, the prompt section renders
147
+ nothing rather than naming a loopback address — in a headless profile that
148
+ would point at a server which is not running. Configure the origin of the Web
149
+ profile serving the same data and headless agents can link into it.
150
+
106
151
  ## Configuration
107
152
 
108
153
  ```yaml
package/lib/index.js CHANGED
@@ -21,17 +21,24 @@
21
21
  * @module @tivility/dsh-session-share
22
22
  */
23
23
  import z from '@deepseek-ai/schemastery';
24
- import { absoluteUrl, isTrustedRequest, sendStatus } from '@tivility/dsh-web-kit';
24
+ import { isTrustedRequest, normalizePublicBaseUrl, resolvePublicBaseUrl, sendStatus } from '@tivility/dsh-web-kit';
25
25
  import { SESSION_PARAM, shareQuery } from './param.js';
26
26
  export { SESSION_PARAM, shareQuery } from './param.js';
27
27
  /** Stable Cordis plugin name. */
28
28
  export const name = 'session-share';
29
- /** The HTTP carrier this plugin mounts its redirect on. */
30
- export const inject = ['webServer'];
29
+ /**
30
+ * Nothing hard. The redirect needs `webServer`, but requiring it here would
31
+ * make this row a startup failure in every profile that serves no HTTP — and a
32
+ * plugin suite installed once in the home overlay is exactly how that happens.
33
+ * The route mounts through a nested injection instead, so a headless profile
34
+ * activates the row and still teaches its agents the link format.
35
+ */
36
+ export const inject = [];
31
37
  export const Config = z.object({
32
38
  route: z.string().default('/s'),
33
39
  prompt: z.boolean().default(true),
34
40
  trustedHosts: z.array(String).default([]),
41
+ publicBaseUrl: z.string().default(''),
35
42
  });
36
43
  /** A session id as it may appear in a URL: the harness mints uuids. */
37
44
  const SESSION_ID = /^[A-Za-z0-9_-]{1,128}$/;
@@ -84,12 +91,17 @@ function createHandler(route, trustedHosts) {
84
91
  * @param sessionId - the session the calling agent belongs to.
85
92
  * @returns the prompt text.
86
93
  */
87
- function promptSection(ctx, route, sessionId) {
88
- const base = absoluteUrl(ctx, '/');
89
- const link = `${base}${shareQuery(sessionId)}`;
94
+ function promptSection(ctx, route, sessionId, publicBaseUrl) {
95
+ const origin = resolvePublicBaseUrl(ctx, publicBaseUrl);
96
+ // A headless profile with no configured origin has nowhere to point: the
97
+ // sessions may well be served by a Web profile elsewhere, but only the
98
+ // operator knows where, and a guessed loopback link points at nothing.
99
+ if (origin === undefined)
100
+ return '';
101
+ const link = `${origin}/${shareQuery(sessionId)}`;
90
102
  const shortForm = route === ''
91
103
  ? ''
92
- : ` The shorter ${absoluteUrl(ctx, route)}/${sessionId} redirects to the same place.`;
104
+ : ` The shorter ${origin}${route}/${sessionId} redirects to the same place.`;
93
105
  return [
94
106
  `This conversation has a direct link: ${link}`,
95
107
  `Opening it in a browser selects this session instead of whatever was last open.${shortForm}`,
@@ -108,15 +120,24 @@ function promptSection(ctx, route, sessionId) {
108
120
  export function apply(ctx, config = {}) {
109
121
  const route = config.route ?? '/s';
110
122
  const trustedHosts = config.trustedHosts ?? [];
123
+ const publicBaseUrl = config.publicBaseUrl ?? '';
124
+ // Validate a configured origin at activation: a typo should stop the load,
125
+ // not quietly put broken links in front of the model.
126
+ if (publicBaseUrl !== '')
127
+ normalizePublicBaseUrl(publicBaseUrl);
111
128
  if (route !== '') {
112
129
  if (!route.startsWith('/') || route.endsWith('/')) {
113
130
  throw new Error(`session-share: route ${JSON.stringify(route)} must be an absolute path without a trailing slash`);
114
131
  }
115
- ctx.effect(() => ctx.webServer.register({
116
- kind: 'prefix',
117
- path: route,
118
- handler: createHandler(route, trustedHosts),
119
- }), `session-share: ${route} route`);
132
+ // Nested, so a profile with no HTTP server activates this row and simply
133
+ // serves no redirect.
134
+ ctx.inject(['webServer'], (web) => {
135
+ web.effect(() => web.webServer.register({
136
+ kind: 'prefix',
137
+ path: route,
138
+ handler: createHandler(route, trustedHosts),
139
+ }), `session-share: ${route} route`);
140
+ });
120
141
  }
121
142
  if (config.prompt === false)
122
143
  return;
@@ -128,7 +149,7 @@ export function apply(ctx, config = {}) {
128
149
  // Resolved per assembly: the port is known only after the server binds,
129
150
  // and the session id is whichever agent this prompt is being built for.
130
151
  const sessionId = context.agent?.session.id;
131
- return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId));
152
+ return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId), publicBaseUrl);
132
153
  },
133
154
  });
134
155
  });
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,13 @@ import z from '@deepseek-ai/schemastery';
25
25
  export { SESSION_PARAM, shareQuery } from './param.js';
26
26
  /** Stable Cordis plugin name. */
27
27
  export declare const name = "session-share";
28
- /** The HTTP carrier this plugin mounts its redirect on. */
28
+ /**
29
+ * Nothing hard. The redirect needs `webServer`, but requiring it here would
30
+ * make this row a startup failure in every profile that serves no HTTP — and a
31
+ * plugin suite installed once in the home overlay is exactly how that happens.
32
+ * The route mounts through a nested injection instead, so a headless profile
33
+ * activates the row and still teaches its agents the link format.
34
+ */
29
35
  export declare const inject: string[];
30
36
  /** Plugin configuration. */
31
37
  export interface Config {
@@ -38,6 +44,13 @@ export interface Config {
38
44
  prompt?: boolean;
39
45
  /** Non-loopback authorities this deployment serves, as `host` or `host:port`. */
40
46
  trustedHosts?: string[];
47
+ /**
48
+ * The origin browsers actually reach this deployment at, when that is not
49
+ * the local bind — behind a reverse proxy, tunnel, or port forwarder. Also
50
+ * what lets a headless profile link into a Web profile serving the same
51
+ * sessions. Origin only: scheme, host, optional port.
52
+ */
53
+ publicBaseUrl?: string;
41
54
  }
42
55
  export declare const Config: z<Config>;
43
56
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAQxC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtD,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,2DAA2D;AAC3D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC,4BAA4B;AAC5B,MAAM,WAAW,MAAM;IACrB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAI3B,CAAA;AA0EF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CA2B7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAQxC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtD,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC,4BAA4B;AAC5B,MAAM,WAAW,MAAM;IACrB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAK3B,CAAA;AA8EF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAoC7D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tivility/dsh-session-share",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Link straight to one DeepSeek Harness session: the GUI opens on it instead of wherever it was left.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "dependencies": {
47
- "@tivility/dsh-web-kit": "^0.1.0"
47
+ "@tivility/dsh-web-kit": "^0.2.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@deepseek-ai/cordis": "*",
package/src/index.ts CHANGED
@@ -28,7 +28,7 @@ import type {} from '@deepseek-ai/dsh-host-webserver'
28
28
  import type {} from '@deepseek-ai/dsh-system-prompt'
29
29
  // Type-only: contributes the `agent` field on the prompt assembly context.
30
30
  import type {} from '@deepseek-ai/dsh-agent'
31
- import { absoluteUrl, isTrustedRequest, sendStatus } from '@tivility/dsh-web-kit'
31
+ import { isTrustedRequest, normalizePublicBaseUrl, resolvePublicBaseUrl, sendStatus } from '@tivility/dsh-web-kit'
32
32
  import { SESSION_PARAM, shareQuery } from './param.js'
33
33
 
34
34
  export { SESSION_PARAM, shareQuery } from './param.js'
@@ -36,8 +36,14 @@ export { SESSION_PARAM, shareQuery } from './param.js'
36
36
  /** Stable Cordis plugin name. */
37
37
  export const name = 'session-share'
38
38
 
39
- /** The HTTP carrier this plugin mounts its redirect on. */
40
- export const inject = ['webServer']
39
+ /**
40
+ * Nothing hard. The redirect needs `webServer`, but requiring it here would
41
+ * make this row a startup failure in every profile that serves no HTTP — and a
42
+ * plugin suite installed once in the home overlay is exactly how that happens.
43
+ * The route mounts through a nested injection instead, so a headless profile
44
+ * activates the row and still teaches its agents the link format.
45
+ */
46
+ export const inject: string[] = []
41
47
 
42
48
  /** Plugin configuration. */
43
49
  export interface Config {
@@ -50,12 +56,20 @@ export interface Config {
50
56
  prompt?: boolean
51
57
  /** Non-loopback authorities this deployment serves, as `host` or `host:port`. */
52
58
  trustedHosts?: string[]
59
+ /**
60
+ * The origin browsers actually reach this deployment at, when that is not
61
+ * the local bind — behind a reverse proxy, tunnel, or port forwarder. Also
62
+ * what lets a headless profile link into a Web profile serving the same
63
+ * sessions. Origin only: scheme, host, optional port.
64
+ */
65
+ publicBaseUrl?: string
53
66
  }
54
67
 
55
68
  export const Config: z<Config> = z.object({
56
69
  route: z.string().default('/s'),
57
70
  prompt: z.boolean().default(true),
58
71
  trustedHosts: z.array(String).default([]),
72
+ publicBaseUrl: z.string().default(''),
59
73
  })
60
74
 
61
75
  /** A session id as it may appear in a URL: the harness mints uuids. */
@@ -113,12 +127,16 @@ function createHandler(
113
127
  * @param sessionId - the session the calling agent belongs to.
114
128
  * @returns the prompt text.
115
129
  */
116
- function promptSection(ctx: Context, route: string, sessionId: string): string {
117
- const base = absoluteUrl(ctx, '/')
118
- const link = `${base}${shareQuery(sessionId)}`
130
+ function promptSection(ctx: Context, route: string, sessionId: string, publicBaseUrl: string): string {
131
+ const origin = resolvePublicBaseUrl(ctx, publicBaseUrl)
132
+ // A headless profile with no configured origin has nowhere to point: the
133
+ // sessions may well be served by a Web profile elsewhere, but only the
134
+ // operator knows where, and a guessed loopback link points at nothing.
135
+ if (origin === undefined) return ''
136
+ const link = `${origin}/${shareQuery(sessionId)}`
119
137
  const shortForm = route === ''
120
138
  ? ''
121
- : ` The shorter ${absoluteUrl(ctx, route)}/${sessionId} redirects to the same place.`
139
+ : ` The shorter ${origin}${route}/${sessionId} redirects to the same place.`
122
140
  return [
123
141
  `This conversation has a direct link: ${link}`,
124
142
  `Opening it in a browser selects this session instead of whatever was last open.${shortForm}`,
@@ -138,15 +156,24 @@ function promptSection(ctx: Context, route: string, sessionId: string): string {
138
156
  export function apply(ctx: Context, config: Config = {}): void {
139
157
  const route = config.route ?? '/s'
140
158
  const trustedHosts = config.trustedHosts ?? []
159
+ const publicBaseUrl = config.publicBaseUrl ?? ''
160
+ // Validate a configured origin at activation: a typo should stop the load,
161
+ // not quietly put broken links in front of the model.
162
+ if (publicBaseUrl !== '') normalizePublicBaseUrl(publicBaseUrl)
163
+
141
164
  if (route !== '') {
142
165
  if (!route.startsWith('/') || route.endsWith('/')) {
143
166
  throw new Error(`session-share: route ${JSON.stringify(route)} must be an absolute path without a trailing slash`)
144
167
  }
145
- ctx.effect(() => ctx.webServer.register({
146
- kind: 'prefix',
147
- path: route,
148
- handler: createHandler(route, trustedHosts),
149
- }), `session-share: ${route} route`)
168
+ // Nested, so a profile with no HTTP server activates this row and simply
169
+ // serves no redirect.
170
+ ctx.inject(['webServer'], (web) => {
171
+ web.effect(() => web.webServer.register({
172
+ kind: 'prefix',
173
+ path: route,
174
+ handler: createHandler(route, trustedHosts),
175
+ }), `session-share: ${route} route`)
176
+ })
150
177
  }
151
178
 
152
179
  if (config.prompt === false) return
@@ -158,7 +185,7 @@ export function apply(ctx: Context, config: Config = {}): void {
158
185
  // Resolved per assembly: the port is known only after the server binds,
159
186
  // and the session id is whichever agent this prompt is being built for.
160
187
  const sessionId = context.agent?.session.id
161
- return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId))
188
+ return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId), publicBaseUrl)
162
189
  },
163
190
  })
164
191
  })