dsh-edge 0.1.0 → 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.
- package/LICENSE +1 -1
- package/README.i18n.yaml +2 -2
- package/README.md +6 -4
- package/README.zh.md +6 -4
- package/THIRD_PARTY_NOTICES.md +42 -14
- package/dist/index.html +1 -1
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +7 -7
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +61 -61
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +14 -14
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +287 -287
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +10 -10
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +11 -11
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +15 -15
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +8 -8
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +27 -27
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +8 -8
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +31 -31
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +23 -23
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +19 -19
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +29 -29
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +7 -7
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +64 -64
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +183 -183
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +44 -44
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +24 -24
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +79 -79
- package/dist/plugins/{@deepseek-ai/dsh-client-ui-edge → dsh-edge-client-ui}/client.js +12 -12
- package/package.json +26 -23
- package/scripts/assemble-web.mjs +1 -1
- package/scripts/bundle.mjs +41 -8
- package/scripts/cli.d.mts +1 -0
- package/scripts/cli.mjs +37 -6
- package/scripts/install.d.mts +6 -6
- package/scripts/install.mjs +36 -19
- package/scripts/legal-files.mjs +65 -7
- package/scripts/smoke-installed.mjs +47 -0
- package/scripts/verify-packed.d.mts +24 -0
- package/scripts/verify-packed.mjs +85 -23
- package/scripts/wrangler-config.d.mts +19 -2
- package/scripts/wrangler-config.mjs +68 -27
- package/worker/direct/index.js +2347 -0
- package/worker/isolated/index.js +44491 -0
- package/src/agent.ts +0 -119
- package/src/auth.ts +0 -352
- package/src/deepseek.ts +0 -231
- package/src/deployment.ts +0 -147
- package/src/direct-shell-core-empty.ts +0 -10
- package/src/direct-shell-protocol.ts +0 -5
- package/src/direct-shell.ts +0 -341
- package/src/do-session-persistence.ts +0 -1064
- package/src/edge-api.ts +0 -796
- package/src/edge-credentials.ts +0 -56
- package/src/edge-remotes.ts +0 -41
- package/src/edge-workspace-store.ts +0 -339
- package/src/http.ts +0 -212
- package/src/index.ts +0 -196
- package/src/instance.ts +0 -1007
- package/src/isolated-direct-shell-unavailable.ts +0 -11
- package/src/protocol.ts +0 -39
- package/src/release.ts +0 -9
- package/src/session-store.ts +0 -1110
- package/src/sse.ts +0 -86
- package/src/web-search.ts +0 -38
- package/src/workspace.ts +0 -270
package/src/agent.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/** Cloudflare-specific runtime bindings exposed through upstream DSH tool seams. */
|
|
2
|
-
|
|
3
|
-
import type { SessionId } from '@deepseek-ai/dsh-session'
|
|
4
|
-
import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
5
|
-
import { EDGE_SHELL_OUTPUT_LIMIT_BYTES } from './direct-shell-protocol.ts'
|
|
6
|
-
import type { EdgeExecutionId } from './protocol.ts'
|
|
7
|
-
|
|
8
|
-
export const EDGE_SYSTEM_PROMPT = 'You are dsh-edge, a coding agent running in a Cloudflare Worker. '
|
|
9
|
-
+ 'Your persistent working directory is /workspace. Use the bash tool when '
|
|
10
|
-
+ 'you need to inspect or modify workspace files, then answer with the result. '
|
|
11
|
-
+ 'The shell is just-bash, not Linux: native binaries and background processes are unavailable.'
|
|
12
|
-
|
|
13
|
-
export interface EdgeShellResult {
|
|
14
|
-
executionId: EdgeExecutionId
|
|
15
|
-
status: 'completed' | 'failed' | 'cancelled'
|
|
16
|
-
timedOut: boolean
|
|
17
|
-
exitCode: number
|
|
18
|
-
stdout: string
|
|
19
|
-
stderr: string
|
|
20
|
-
outputTruncated: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface EdgeShell {
|
|
24
|
-
exec(command: string, options: {
|
|
25
|
-
cwd: string
|
|
26
|
-
timeoutMs?: number
|
|
27
|
-
signal?: AbortSignal
|
|
28
|
-
}): Promise<EdgeShellResult>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Request-scoped Computer workspaces keyed by the upstream agent/session identity. */
|
|
32
|
-
export class EdgeShellBindings {
|
|
33
|
-
private readonly shells = new Map<SessionId, EdgeShell>()
|
|
34
|
-
|
|
35
|
-
bind(sessionId: SessionId, shell: EdgeShell): () => void {
|
|
36
|
-
if (this.shells.has(sessionId)) {
|
|
37
|
-
throw new Error(`dsh-edge: shell is already bound for session "${sessionId}"`)
|
|
38
|
-
}
|
|
39
|
-
this.shells.set(sessionId, shell)
|
|
40
|
-
return () => {
|
|
41
|
-
if (this.shells.get(sessionId) === shell) this.shells.delete(sessionId)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
require(sessionId: SessionId): EdgeShell {
|
|
46
|
-
const shell = this.shells.get(sessionId)
|
|
47
|
-
if (shell === undefined) {
|
|
48
|
-
throw new Error(`dsh-edge: no active Computer workspace for session "${sessionId}"`)
|
|
49
|
-
}
|
|
50
|
-
return shell
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Native DSH tool definition whose body is the Cloudflare Computer adapter. */
|
|
55
|
-
export function createEdgeBashTool(bindings: EdgeShellBindings): ToolDefinition {
|
|
56
|
-
return defineTool({
|
|
57
|
-
name: 'bash',
|
|
58
|
-
description: 'Execute a just-bash command against the persistent /workspace virtual filesystem. Each call starts in /workspace unless workdir is supplied.',
|
|
59
|
-
parameters: {
|
|
60
|
-
command: {
|
|
61
|
-
type: 'string',
|
|
62
|
-
required: true,
|
|
63
|
-
description: 'The shell command to execute.',
|
|
64
|
-
},
|
|
65
|
-
description: {
|
|
66
|
-
type: 'string',
|
|
67
|
-
required: true,
|
|
68
|
-
description: 'A short explanation of what the command does.',
|
|
69
|
-
},
|
|
70
|
-
workdir: {
|
|
71
|
-
type: 'string',
|
|
72
|
-
description: 'An absolute directory below /workspace.',
|
|
73
|
-
},
|
|
74
|
-
timeoutMs: {
|
|
75
|
-
type: 'number',
|
|
76
|
-
description: 'Optional execution timeout in milliseconds.',
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
output: {
|
|
80
|
-
schema: {
|
|
81
|
-
type: 'object',
|
|
82
|
-
additionalProperties: false,
|
|
83
|
-
properties: {
|
|
84
|
-
executionId: { type: 'string', required: true },
|
|
85
|
-
status: {
|
|
86
|
-
type: 'string',
|
|
87
|
-
enum: ['completed', 'failed', 'cancelled'],
|
|
88
|
-
required: true,
|
|
89
|
-
},
|
|
90
|
-
timedOut: { type: 'boolean', required: true },
|
|
91
|
-
exitCode: { type: 'number', required: true },
|
|
92
|
-
stdout: { type: 'string', required: true },
|
|
93
|
-
stderr: { type: 'string', required: true },
|
|
94
|
-
outputTruncated: { type: 'boolean', required: true },
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
render: (_args, result) => [{ type: 'text', text: formatExecution(result) }],
|
|
98
|
-
},
|
|
99
|
-
async execute(args, exec) {
|
|
100
|
-
const agent = exec.agent
|
|
101
|
-
if (agent === undefined) throw new Error('dsh-edge: bash requires an initiating agent')
|
|
102
|
-
return bindings.require(agent.id).exec(args.command, {
|
|
103
|
-
cwd: args.workdir ?? '/workspace',
|
|
104
|
-
...args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs },
|
|
105
|
-
signal: exec.signal,
|
|
106
|
-
})
|
|
107
|
-
},
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatExecution(result: Omit<EdgeShellResult, 'executionId'>): string {
|
|
112
|
-
const output = `${result.stdout}${result.stderr}`
|
|
113
|
-
const truncated = result.outputTruncated
|
|
114
|
-
? `\n[output truncated after ${EDGE_SHELL_OUTPUT_LIMIT_BYTES} UTF-8 bytes]`
|
|
115
|
-
: ''
|
|
116
|
-
const timedOut = result.timedOut ? '\n[command timed out]' : ''
|
|
117
|
-
const suffix = result.exitCode === 0 ? '' : `\n[exit code: ${result.exitCode}]`
|
|
118
|
-
return output + truncated + timedOut + suffix || '(no output)'
|
|
119
|
-
}
|
package/src/auth.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EdgeHttpError,
|
|
3
|
-
readBoundedText,
|
|
4
|
-
} from './http.ts'
|
|
5
|
-
|
|
6
|
-
const LOCAL_COOKIE_NAME = 'dsh_edge_owner'
|
|
7
|
-
const SECURE_COOKIE_NAME = '__Host-dsh_edge_owner'
|
|
8
|
-
const COOKIE_VERSION = 'v1'
|
|
9
|
-
const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60
|
|
10
|
-
const MAX_LOGIN_BODY_BYTES = 4_096
|
|
11
|
-
const MIN_ACCESS_KEY_BYTES = 32
|
|
12
|
-
const MAX_ACCESS_KEY_BYTES = 512
|
|
13
|
-
const SESSION_SIGNING_CONTEXT = 'dsh-edge-owner-session\0'
|
|
14
|
-
const OWNER_AUTH_CHALLENGE = 'DshEdgeOwner'
|
|
15
|
-
const textEncoder = new TextEncoder()
|
|
16
|
-
|
|
17
|
-
/** Trusted Worker-to-Durable-Object metadata; callers cannot select its value. */
|
|
18
|
-
export const OWNER_SESSION_EXPIRY_HEADER = 'x-dsh-edge-owner-session-expires-at'
|
|
19
|
-
|
|
20
|
-
/** Validated single-owner authentication configuration. */
|
|
21
|
-
export interface OwnerAuthConfig {
|
|
22
|
-
accessKey: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Claims recovered from a valid owner cookie. */
|
|
26
|
-
export interface OwnerSession {
|
|
27
|
-
expiresAt: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Fail closed unless the deployment has one suitably strong owner key. */
|
|
31
|
-
export function resolveOwnerAuthConfig(accessKey: string | undefined): OwnerAuthConfig {
|
|
32
|
-
const byteLength = accessKey === undefined ? 0 : textEncoder.encode(accessKey).byteLength
|
|
33
|
-
if (accessKey === undefined
|
|
34
|
-
|| byteLength < MIN_ACCESS_KEY_BYTES
|
|
35
|
-
|| byteLength > MAX_ACCESS_KEY_BYTES
|
|
36
|
-
|| accessKey.trim() !== accessKey
|
|
37
|
-
|| /[\u0000-\u001f\u007f]/u.test(accessKey)) {
|
|
38
|
-
throw new EdgeHttpError(
|
|
39
|
-
503,
|
|
40
|
-
'Owner authentication is not configured safely. Set DSH_EDGE_ACCESS_KEY to a random value of at least 32 bytes.',
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
return { accessKey }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Handle the small login/session/logout surface owned by the Edge adapter. */
|
|
47
|
-
export async function handleOwnerAuthRoute(
|
|
48
|
-
request: Request,
|
|
49
|
-
config: OwnerAuthConfig,
|
|
50
|
-
): Promise<Response | undefined> {
|
|
51
|
-
const url = new URL(request.url)
|
|
52
|
-
|
|
53
|
-
if (request.method === 'GET' && url.pathname === '/login') {
|
|
54
|
-
return loginPageResponse(await isOwnerAuthenticated(request, config))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (request.method === 'GET' && url.pathname === '/api/auth/session') {
|
|
58
|
-
return noStoreJson({ authenticated: await isOwnerAuthenticated(request, config) })
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (request.method === 'POST' && url.pathname === '/api/auth/login') {
|
|
62
|
-
requireSameOrigin(request)
|
|
63
|
-
const contentType = request.headers.get('content-type') ?? ''
|
|
64
|
-
if (!contentType.toLowerCase().startsWith('application/x-www-form-urlencoded')) {
|
|
65
|
-
throw new EdgeHttpError(415, 'Login requires a form-encoded request.')
|
|
66
|
-
}
|
|
67
|
-
const source = await readBoundedText(
|
|
68
|
-
request,
|
|
69
|
-
MAX_LOGIN_BODY_BYTES,
|
|
70
|
-
`Login requests are limited to ${MAX_LOGIN_BODY_BYTES} bytes.`,
|
|
71
|
-
)
|
|
72
|
-
const candidate = new URLSearchParams(source).get('accessKey') ?? ''
|
|
73
|
-
if (!await accessKeysMatch(candidate, config.accessKey)) {
|
|
74
|
-
return loginPageResponse(false, 'The access key is not valid.', 401)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const expiresAt = Math.floor(Date.now() / 1_000) + SESSION_TTL_SECONDS
|
|
78
|
-
const cookie = await createSessionCookie(config, expiresAt, url.protocol === 'https:')
|
|
79
|
-
return new Response(null, {
|
|
80
|
-
status: 303,
|
|
81
|
-
headers: {
|
|
82
|
-
'cache-control': 'no-store',
|
|
83
|
-
location: '/',
|
|
84
|
-
'set-cookie': cookie,
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (request.method === 'POST' && url.pathname === '/api/auth/logout') {
|
|
90
|
-
requireSameOrigin(request)
|
|
91
|
-
return new Response(null, {
|
|
92
|
-
status: 303,
|
|
93
|
-
headers: {
|
|
94
|
-
'cache-control': 'no-store',
|
|
95
|
-
location: '/login',
|
|
96
|
-
'set-cookie': expiredSessionCookie(url.protocol === 'https:'),
|
|
97
|
-
},
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return undefined
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Validate the opaque, signed owner session carried by an HttpOnly cookie. */
|
|
105
|
-
export async function isOwnerAuthenticated(
|
|
106
|
-
request: Request,
|
|
107
|
-
config: OwnerAuthConfig,
|
|
108
|
-
nowSeconds = Math.floor(Date.now() / 1_000),
|
|
109
|
-
): Promise<boolean> {
|
|
110
|
-
return await resolveOwnerSession(request, config, nowSeconds) !== undefined
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Validate the owner cookie and expose its bounded lifetime to trusted adapters. */
|
|
114
|
-
export async function resolveOwnerSession(
|
|
115
|
-
request: Request,
|
|
116
|
-
config: OwnerAuthConfig,
|
|
117
|
-
nowSeconds = Math.floor(Date.now() / 1_000),
|
|
118
|
-
): Promise<OwnerSession | undefined> {
|
|
119
|
-
const secure = new URL(request.url).protocol === 'https:'
|
|
120
|
-
const cookies = readCookies(request.headers.get('cookie'), sessionCookieName(secure))
|
|
121
|
-
if (cookies.length === 0) return undefined
|
|
122
|
-
|
|
123
|
-
const key = await importSigningKey(config.accessKey)
|
|
124
|
-
for (const cookie of cookies) {
|
|
125
|
-
const parts = cookie.split('.')
|
|
126
|
-
const version = parts[0]
|
|
127
|
-
const expiresSource = parts[1]
|
|
128
|
-
const signatureSource = parts[2]
|
|
129
|
-
if (parts.length !== 3
|
|
130
|
-
|| version !== COOKIE_VERSION
|
|
131
|
-
|| expiresSource === undefined
|
|
132
|
-
|| signatureSource === undefined
|
|
133
|
-
|| !/^\d{10}$/u.test(expiresSource)) {
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
const expiresAt = Number(expiresSource)
|
|
137
|
-
if (!Number.isSafeInteger(expiresAt) || expiresAt <= nowSeconds) continue
|
|
138
|
-
if (expiresAt > nowSeconds + SESSION_TTL_SECONDS) continue
|
|
139
|
-
|
|
140
|
-
const signature = decodeBase64Url(signatureSource)
|
|
141
|
-
if (signature === undefined) continue
|
|
142
|
-
const valid = await crypto.subtle.verify(
|
|
143
|
-
'HMAC',
|
|
144
|
-
key,
|
|
145
|
-
signature,
|
|
146
|
-
textEncoder.encode(`${SESSION_SIGNING_CONTEXT}${expiresSource}`),
|
|
147
|
-
)
|
|
148
|
-
if (valid) return { expiresAt }
|
|
149
|
-
}
|
|
150
|
-
return undefined
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Redirect unauthenticated document requests to the local login shell. */
|
|
154
|
-
export function loginRedirect(): Response {
|
|
155
|
-
return new Response(null, {
|
|
156
|
-
status: 302,
|
|
157
|
-
headers: {
|
|
158
|
-
'cache-control': 'no-store',
|
|
159
|
-
location: '/login',
|
|
160
|
-
},
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Return a credential-safe API rejection. */
|
|
165
|
-
export function unauthorizedResponse(): Response {
|
|
166
|
-
const response = noStoreJson({ ok: false, error: 'Owner authentication required.' }, 401)
|
|
167
|
-
response.headers.set('www-authenticate', OWNER_AUTH_CHALLENGE)
|
|
168
|
-
return response
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function createSessionCookie(
|
|
172
|
-
config: OwnerAuthConfig,
|
|
173
|
-
expiresAt: number,
|
|
174
|
-
secure: boolean,
|
|
175
|
-
): Promise<string> {
|
|
176
|
-
const key = await importSigningKey(config.accessKey)
|
|
177
|
-
const signature = await crypto.subtle.sign(
|
|
178
|
-
'HMAC',
|
|
179
|
-
key,
|
|
180
|
-
textEncoder.encode(`${SESSION_SIGNING_CONTEXT}${String(expiresAt)}`),
|
|
181
|
-
)
|
|
182
|
-
const value = `${COOKIE_VERSION}.${String(expiresAt)}.${encodeBase64Url(signature)}`
|
|
183
|
-
const expires = new Date(expiresAt * 1_000).toUTCString()
|
|
184
|
-
return `${sessionCookieName(secure)}=${value}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${String(SESSION_TTL_SECONDS)}; Expires=${expires}${secure ? '; Secure' : ''}`
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function expiredSessionCookie(secure: boolean): string {
|
|
188
|
-
return `${sessionCookieName(secure)}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT${secure ? '; Secure' : ''}`
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function sessionCookieName(secure: boolean): string {
|
|
192
|
-
return secure ? SECURE_COOKIE_NAME : LOCAL_COOKIE_NAME
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function accessKeysMatch(candidate: string, configured: string): Promise<boolean> {
|
|
196
|
-
const candidateBytes = textEncoder.encode(candidate)
|
|
197
|
-
if (candidateBytes.byteLength > MAX_ACCESS_KEY_BYTES) return false
|
|
198
|
-
const [candidateDigest, configuredDigest] = await Promise.all([
|
|
199
|
-
crypto.subtle.digest('SHA-256', candidateBytes),
|
|
200
|
-
crypto.subtle.digest('SHA-256', textEncoder.encode(configured)),
|
|
201
|
-
])
|
|
202
|
-
const left = new Uint8Array(candidateDigest)
|
|
203
|
-
const right = new Uint8Array(configuredDigest)
|
|
204
|
-
let difference = candidateBytes.byteLength < MIN_ACCESS_KEY_BYTES ? 1 : 0
|
|
205
|
-
for (let index = 0; index < left.byteLength; index += 1) {
|
|
206
|
-
difference |= (left[index] ?? 0) ^ (right[index] ?? 0)
|
|
207
|
-
}
|
|
208
|
-
return difference === 0
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function importSigningKey(accessKey: string): Promise<CryptoKey> {
|
|
212
|
-
return crypto.subtle.importKey(
|
|
213
|
-
'raw',
|
|
214
|
-
textEncoder.encode(accessKey),
|
|
215
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
216
|
-
false,
|
|
217
|
-
['sign', 'verify'],
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function requireSameOrigin(request: Request): void {
|
|
222
|
-
const fetchSite = request.headers.get('sec-fetch-site')
|
|
223
|
-
if (fetchSite === 'same-origin') return
|
|
224
|
-
if (fetchSite !== null) {
|
|
225
|
-
throw new EdgeHttpError(403, 'Cross-origin authentication requests are not allowed.')
|
|
226
|
-
}
|
|
227
|
-
const origin = request.headers.get('origin')
|
|
228
|
-
if (origin !== null && origin !== new URL(request.url).origin) {
|
|
229
|
-
throw new EdgeHttpError(403, 'Cross-origin authentication requests are not allowed.')
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function readCookies(header: string | null, name: string): string[] {
|
|
234
|
-
if (header === null) return []
|
|
235
|
-
const values: string[] = []
|
|
236
|
-
for (const part of header.split(';')) {
|
|
237
|
-
const separator = part.indexOf('=')
|
|
238
|
-
if (separator === -1 || part.slice(0, separator).trim() !== name) continue
|
|
239
|
-
const value = part.slice(separator + 1).trim()
|
|
240
|
-
if (value.length > 0) values.push(value)
|
|
241
|
-
}
|
|
242
|
-
return values
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function encodeBase64Url(value: ArrayBuffer): string {
|
|
246
|
-
let binary = ''
|
|
247
|
-
for (const byte of new Uint8Array(value)) binary += String.fromCharCode(byte)
|
|
248
|
-
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '')
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function decodeBase64Url(value: string): ArrayBuffer | undefined {
|
|
252
|
-
if (!/^[A-Za-z0-9_-]{43}$/u.test(value)) return undefined
|
|
253
|
-
try {
|
|
254
|
-
const base64 = value.replaceAll('-', '+').replaceAll('_', '/')
|
|
255
|
-
const binary = atob(`${base64}${'='.repeat((4 - base64.length % 4) % 4)}`)
|
|
256
|
-
const bytes = new Uint8Array(binary.length)
|
|
257
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
258
|
-
bytes[index] = binary.charCodeAt(index)
|
|
259
|
-
}
|
|
260
|
-
return bytes.buffer
|
|
261
|
-
} catch {
|
|
262
|
-
return undefined
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function loginPageResponse(authenticated: boolean, error?: string, status = 200): Response {
|
|
267
|
-
const body = authenticated ? authenticatedPage() : loginPage(error)
|
|
268
|
-
return new Response(body, {
|
|
269
|
-
status,
|
|
270
|
-
headers: {
|
|
271
|
-
'cache-control': 'no-store',
|
|
272
|
-
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
273
|
-
'content-type': 'text/html; charset=utf-8',
|
|
274
|
-
'referrer-policy': 'no-referrer',
|
|
275
|
-
'x-content-type-options': 'nosniff',
|
|
276
|
-
},
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function noStoreJson(value: unknown, status = 200): Response {
|
|
281
|
-
return Response.json(value, {
|
|
282
|
-
status,
|
|
283
|
-
headers: {
|
|
284
|
-
'cache-control': 'no-store',
|
|
285
|
-
'content-type': 'application/json; charset=utf-8',
|
|
286
|
-
},
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function loginPage(error?: string): string {
|
|
291
|
-
return `<!doctype html>
|
|
292
|
-
<html lang="en">
|
|
293
|
-
<head>
|
|
294
|
-
<meta charset="utf-8">
|
|
295
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
296
|
-
<title>Unlock dsh-edge</title>
|
|
297
|
-
<style>${pageStyles()}</style>
|
|
298
|
-
</head>
|
|
299
|
-
<body>
|
|
300
|
-
<main>
|
|
301
|
-
<p class="eyebrow">dsh-edge</p>
|
|
302
|
-
<h1>Unlock this deployment</h1>
|
|
303
|
-
<p>Enter the owner access key configured for this Cloudflare deployment.</p>
|
|
304
|
-
${error === undefined ? '' : `<p class="error" role="alert">${error}</p>`}
|
|
305
|
-
<form method="post" action="/api/auth/login">
|
|
306
|
-
<label for="access-key">Owner access key</label>
|
|
307
|
-
<input id="access-key" name="accessKey" type="password" autocomplete="current-password" required autofocus>
|
|
308
|
-
<button type="submit">Unlock</button>
|
|
309
|
-
</form>
|
|
310
|
-
</main>
|
|
311
|
-
</body>
|
|
312
|
-
</html>`
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function authenticatedPage(): string {
|
|
316
|
-
return `<!doctype html>
|
|
317
|
-
<html lang="en">
|
|
318
|
-
<head>
|
|
319
|
-
<meta charset="utf-8">
|
|
320
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
321
|
-
<title>dsh-edge owner session</title>
|
|
322
|
-
<style>${pageStyles()}</style>
|
|
323
|
-
</head>
|
|
324
|
-
<body>
|
|
325
|
-
<main>
|
|
326
|
-
<p class="eyebrow">dsh-edge</p>
|
|
327
|
-
<h1>This browser is unlocked</h1>
|
|
328
|
-
<p>The owner session is stored in an HttpOnly cookie on this device.</p>
|
|
329
|
-
<a class="button" href="/">Continue to DeepSeek Harness</a>
|
|
330
|
-
<form method="post" action="/api/auth/logout">
|
|
331
|
-
<button class="secondary" type="submit">Sign out</button>
|
|
332
|
-
</form>
|
|
333
|
-
</main>
|
|
334
|
-
</body>
|
|
335
|
-
</html>`
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function pageStyles(): string {
|
|
339
|
-
return `
|
|
340
|
-
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
341
|
-
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: #0b1020; color: #edf2ff; }
|
|
342
|
-
main { box-sizing: border-box; width: min(30rem, calc(100% - 2rem)); padding: 2rem; border: 1px solid #29314b; border-radius: 1rem; background: #141a2d; box-shadow: 0 1.5rem 5rem #0008; }
|
|
343
|
-
h1 { margin: .35rem 0 .75rem; font-size: 1.75rem; }
|
|
344
|
-
p { color: #b7c0d9; line-height: 1.55; }
|
|
345
|
-
.eyebrow { margin: 0; color: #7fa8ff; font-size: .8rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
|
|
346
|
-
label { display: block; margin: 1.5rem 0 .5rem; font-weight: 650; }
|
|
347
|
-
input { box-sizing: border-box; width: 100%; padding: .8rem .9rem; border: 1px solid #3a4565; border-radius: .6rem; background: #0c1222; color: inherit; font: inherit; }
|
|
348
|
-
button, .button { display: inline-block; box-sizing: border-box; margin-top: 1rem; padding: .75rem 1rem; border: 0; border-radius: .6rem; background: #5a87ff; color: #071022; font: inherit; font-weight: 750; text-decoration: none; cursor: pointer; }
|
|
349
|
-
.secondary { background: #29314b; color: #edf2ff; }
|
|
350
|
-
.error { padding: .75rem; border-radius: .5rem; background: #461d2b; color: #ffd6df; }
|
|
351
|
-
`
|
|
352
|
-
}
|
package/src/deepseek.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import type { AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
|
|
2
|
-
import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
|
|
3
|
-
import {
|
|
4
|
-
LlmAdapter,
|
|
5
|
-
LlmError,
|
|
6
|
-
assertUsableApiKey,
|
|
7
|
-
resolveRetryPolicy,
|
|
8
|
-
type GenerateOptions,
|
|
9
|
-
type LlmModelInfo,
|
|
10
|
-
type LlmProviderInfo,
|
|
11
|
-
type LlmResolvedModelInfo,
|
|
12
|
-
type ResolvedRetryPolicy,
|
|
13
|
-
type StreamChunk,
|
|
14
|
-
} from '@deepseek-ai/dsh-llm'
|
|
15
|
-
import type { SessionId } from '@deepseek-ai/dsh-session'
|
|
16
|
-
import {
|
|
17
|
-
DEFAULT_CONTEXT_WINDOW,
|
|
18
|
-
DeepSeekAdapter,
|
|
19
|
-
type DeepSeekConnectionOptions,
|
|
20
|
-
} from '@deepseek-ai/dsh-llm-deepseek'
|
|
21
|
-
|
|
22
|
-
const API_KEY_REF = 'DEEPSEEK_API_KEY' as CredentialRef
|
|
23
|
-
const DEFAULT_BASE_URL = 'https://api.deepseek.com'
|
|
24
|
-
const DEFAULT_MAX_TOKENS = 8_192
|
|
25
|
-
const DEFAULT_MODEL = 'deepseek-v4-flash'
|
|
26
|
-
const DEFAULT_REASONING_EFFORT = 'off'
|
|
27
|
-
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000
|
|
28
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
29
|
-
const MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,127}$/
|
|
30
|
-
|
|
31
|
-
export type EdgeReasoningEffort = 'off' | 'low' | 'high' | 'max'
|
|
32
|
-
|
|
33
|
-
/** Resolve and validate the DeepSeek HTTP endpoint selected by deployment configuration. */
|
|
34
|
-
export function resolveEdgeBaseURL(raw?: string): string {
|
|
35
|
-
const value = raw ?? DEFAULT_BASE_URL
|
|
36
|
-
let parsed: URL
|
|
37
|
-
try {
|
|
38
|
-
parsed = new URL(value)
|
|
39
|
-
} catch {
|
|
40
|
-
throw new Error('dsh-edge: DEEPSEEK_BASE_URL must be a valid HTTP(S) URL')
|
|
41
|
-
}
|
|
42
|
-
if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
43
|
-
|| parsed.username.length > 0
|
|
44
|
-
|| parsed.password.length > 0) {
|
|
45
|
-
throw new Error('dsh-edge: DEEPSEEK_BASE_URL must be a valid HTTP(S) URL without credentials')
|
|
46
|
-
}
|
|
47
|
-
return value
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Resolve the DeepSeek model id selected by Worker deployment configuration. */
|
|
51
|
-
export function resolveEdgeModel(raw?: string): string {
|
|
52
|
-
if (raw === undefined) return DEFAULT_MODEL
|
|
53
|
-
if (!MODEL_PATTERN.test(raw)) {
|
|
54
|
-
throw new Error('dsh-edge: DEEPSEEK_MODEL must be a valid model id of at most 128 characters')
|
|
55
|
-
}
|
|
56
|
-
return raw
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Resolve the DeepSeek thinking policy selected by Worker deployment configuration. */
|
|
60
|
-
export function resolveEdgeReasoningEffort(raw?: string): EdgeReasoningEffort {
|
|
61
|
-
if (raw === undefined) return DEFAULT_REASONING_EFFORT
|
|
62
|
-
if (raw !== 'off' && raw !== 'low' && raw !== 'high' && raw !== 'max') {
|
|
63
|
-
throw new Error('dsh-edge: DEEPSEEK_REASONING_EFFORT must be one of off, low, high, or max')
|
|
64
|
-
}
|
|
65
|
-
return raw
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Resolve the optional Worker deployment override before an SSE turn opens. */
|
|
69
|
-
export function resolveEdgeStreamIdleTimeoutMs(raw?: string): number {
|
|
70
|
-
if (raw === undefined) return DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
71
|
-
if (!/^[1-9]\d*$/.test(raw)) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
'dsh-edge: DEEPSEEK_STREAM_IDLE_TIMEOUT_MS must be a positive integer',
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
const value = Number(raw)
|
|
77
|
-
if (!Number.isSafeInteger(value) || value > MAX_TIMER_DELAY_MS) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`dsh-edge: DEEPSEEK_STREAM_IDLE_TIMEOUT_MS must be no greater than ${MAX_TIMER_DELAY_MS}`,
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
return value
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Resolve the request output cap selected by Worker deployment configuration. */
|
|
86
|
-
export function resolveEdgeMaxOutputTokens(raw?: string): number {
|
|
87
|
-
if (raw === undefined) return DEFAULT_MAX_TOKENS
|
|
88
|
-
if (!/^[1-9]\d*$/.test(raw)) {
|
|
89
|
-
throw new Error(
|
|
90
|
-
'dsh-edge: DEEPSEEK_MAX_OUTPUT_TOKENS must be a positive integer',
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
const value = Number(raw)
|
|
94
|
-
if (!Number.isSafeInteger(value)) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`dsh-edge: DEEPSEEK_MAX_OUTPUT_TOKENS must be no greater than ${Number.MAX_SAFE_INTEGER}`,
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
return value
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Construct one turn-scoped DSH DeepSeek adapter that resolves its bearer token per operation. */
|
|
103
|
-
function createEdgeDeepSeekModel(options: {
|
|
104
|
-
resolveApiKey: () => Promise<string | undefined>
|
|
105
|
-
baseURL?: string
|
|
106
|
-
maxTokens?: number
|
|
107
|
-
reasoningEffort?: EdgeReasoningEffort
|
|
108
|
-
streamIdleTimeoutMs?: number
|
|
109
|
-
}): DeepSeekAdapter {
|
|
110
|
-
const connection: DeepSeekConnectionOptions = {
|
|
111
|
-
apiKeyEnv: API_KEY_REF,
|
|
112
|
-
baseURL: options.baseURL ?? DEFAULT_BASE_URL,
|
|
113
|
-
defaults: { reasoningEffort: options.reasoningEffort ?? DEFAULT_REASONING_EFFORT },
|
|
114
|
-
maxTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
115
|
-
defaultContextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
116
|
-
models: [],
|
|
117
|
-
streamIdleTimeoutMs: options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
118
|
-
retryPolicy: resolveRetryPolicy(undefined, 'dsh-edge.retryPolicy'),
|
|
119
|
-
}
|
|
120
|
-
const anonymousRequestId = crypto.randomUUID() as AnonymousUserId
|
|
121
|
-
return new DeepSeekAdapter({
|
|
122
|
-
options: () => connection,
|
|
123
|
-
resolveApiKey: async () => {
|
|
124
|
-
const apiKey = await options.resolveApiKey()
|
|
125
|
-
if (apiKey === undefined) {
|
|
126
|
-
throw new LlmError(
|
|
127
|
-
'dsh-edge: no API key for provider route "deepseek-official"; configure the DEEPSEEK_API_KEY Worker secret',
|
|
128
|
-
'MISSING_CREDENTIAL',
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
return assertUsableApiKey(apiKey, 'dsh-edge', API_KEY_REF)
|
|
132
|
-
},
|
|
133
|
-
// Provider telemetry must not derive identity from the owner or Durable Object id.
|
|
134
|
-
resolveUserId: () => anonymousRequestId,
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* One upstream LLM route with per-turn deployment configuration selected by
|
|
140
|
-
* the loop-stamped session id. Credentials resolve independently through the
|
|
141
|
-
* upstream credential service for each model operation.
|
|
142
|
-
*/
|
|
143
|
-
export class EdgeDeepSeekAdapter extends LlmAdapter {
|
|
144
|
-
private readonly active = new Map<SessionId, LlmAdapter>()
|
|
145
|
-
private metadataMaxTokens = DEFAULT_MAX_TOKENS
|
|
146
|
-
private metadataReasoningEffort: EdgeReasoningEffort = DEFAULT_REASONING_EFFORT
|
|
147
|
-
private metadata: DeepSeekAdapter
|
|
148
|
-
|
|
149
|
-
constructor(private readonly resolveApiKey?: () => Promise<string | undefined>) {
|
|
150
|
-
super()
|
|
151
|
-
this.metadata = createEdgeDeepSeekModel({
|
|
152
|
-
resolveApiKey: () => Promise.resolve('dsh-edge-model-metadata'),
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
bind(sessionId: SessionId, options: {
|
|
157
|
-
baseURL?: string
|
|
158
|
-
maxTokens?: number
|
|
159
|
-
reasoningEffort?: EdgeReasoningEffort
|
|
160
|
-
streamIdleTimeoutMs?: number
|
|
161
|
-
}): () => void {
|
|
162
|
-
this.configureDefaults(
|
|
163
|
-
options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
164
|
-
options.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
|
|
165
|
-
)
|
|
166
|
-
return this.bindAdapter(sessionId, createEdgeDeepSeekModel({
|
|
167
|
-
...options,
|
|
168
|
-
resolveApiKey: this.resolveApiKey ?? (() => Promise.resolve(undefined)),
|
|
169
|
-
}))
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private configureDefaults(
|
|
173
|
-
maxTokens: number,
|
|
174
|
-
reasoningEffort: EdgeReasoningEffort,
|
|
175
|
-
): void {
|
|
176
|
-
if (maxTokens === this.metadataMaxTokens
|
|
177
|
-
&& reasoningEffort === this.metadataReasoningEffort) return
|
|
178
|
-
if (this.active.size > 0) {
|
|
179
|
-
throw new LlmError(
|
|
180
|
-
'dsh-edge: deployment model defaults changed while another session is active',
|
|
181
|
-
'EDGE_MODEL_BUSY',
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
this.metadata = createEdgeDeepSeekModel({
|
|
185
|
-
resolveApiKey: () => Promise.resolve('dsh-edge-model-metadata'),
|
|
186
|
-
maxTokens,
|
|
187
|
-
reasoningEffort,
|
|
188
|
-
})
|
|
189
|
-
this.metadataMaxTokens = maxTokens
|
|
190
|
-
this.metadataReasoningEffort = reasoningEffort
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
bindAdapter(sessionId: SessionId, adapter: LlmAdapter): () => void {
|
|
194
|
-
if (this.active.has(sessionId)) {
|
|
195
|
-
throw new LlmError(`dsh-edge: model is already bound for session "${sessionId}"`, 'EDGE_MODEL_BUSY')
|
|
196
|
-
}
|
|
197
|
-
this.active.set(sessionId, adapter)
|
|
198
|
-
return () => {
|
|
199
|
-
if (this.active.get(sessionId) === adapter) this.active.delete(sessionId)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
override providerInfo(provider: string): LlmProviderInfo {
|
|
204
|
-
return this.metadata.providerInfo(provider)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
override providerRetryPolicy(provider: string): ResolvedRetryPolicy {
|
|
208
|
-
return this.metadata.providerRetryPolicy(provider)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
|
212
|
-
return this.metadata.listModels(provider)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
override resolveModel(
|
|
216
|
-
provider: string,
|
|
217
|
-
model: string,
|
|
218
|
-
signal?: AbortSignal,
|
|
219
|
-
): Promise<LlmResolvedModelInfo> {
|
|
220
|
-
return this.metadata.resolveModel(provider, model, signal)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
override stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
224
|
-
const sessionId = options.sessionId
|
|
225
|
-
const adapter = sessionId === undefined ? undefined : this.active.get(sessionId)
|
|
226
|
-
if (adapter === undefined) {
|
|
227
|
-
throw new LlmError('dsh-edge: no request-scoped DeepSeek configuration is bound', 'EDGE_MODEL_NOT_BOUND')
|
|
228
|
-
}
|
|
229
|
-
return adapter.stream(options)
|
|
230
|
-
}
|
|
231
|
-
}
|