@tivility/dsh-session-share 0.2.0 → 0.3.0
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 +29 -0
- package/lib/index.js +34 -13
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +14 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +40 -13
package/README.md
CHANGED
|
@@ -103,6 +103,35 @@ 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
|
+
With no `webServer` **and** no `publicBaseUrl`, the prompt section renders
|
|
131
|
+
nothing rather than naming a loopback address — in a headless profile that
|
|
132
|
+
would point at a server which is not running. Configure the origin of the Web
|
|
133
|
+
profile serving the same data and headless agents can link into it.
|
|
134
|
+
|
|
106
135
|
## Configuration
|
|
107
136
|
|
|
108
137
|
```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 {
|
|
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
|
-
/**
|
|
30
|
-
|
|
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
|
|
89
|
-
|
|
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 ${
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAKxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
/**
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
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 {
|
|
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
|
-
/**
|
|
40
|
-
|
|
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
|
|
118
|
-
|
|
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 ${
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
})
|