dsh-edge 0.1.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +183 -0
- package/README.zh.md +183 -0
- package/THIRD_PARTY_NOTICES.md +199 -0
- package/dist/_headers +3 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-C-1AiF3k.js +112 -0
- package/dist/assets/index-CSGf6Qzd.css +1 -0
- package/dist/assets/langs/c-BIGW1oBm.js +2 -0
- package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
- package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
- package/dist/assets/langs/css-CLj8gQPS.js +2 -0
- package/dist/assets/langs/go-C27-OAKa.js +2 -0
- package/dist/assets/langs/html-CfGypltT.js +2 -0
- package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
- package/dist/assets/langs/java-CylS5w8V.js +2 -0
- package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
- package/dist/assets/langs/less-B1dDrJ26.js +2 -0
- package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
- package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
- package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
- package/dist/assets/langs/php-D-uCvoST.js +2 -0
- package/dist/assets/langs/python-B6aJPvgy.js +2 -0
- package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
- package/dist/assets/langs/rust-B1yitclQ.js +2 -0
- package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
- package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
- package/dist/assets/langs/swift-D82vCrfD.js +2 -0
- package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
- package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
- package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
- package/dist/assets/vendor-Cjbwl5VI.js +411 -0
- package/dist/assets/vendor-CjyC-hUb.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +35 -0
- package/dist/manifest.webmanifest +16 -0
- package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
- package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
- package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
- package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
- package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
- package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
- package/package.json +97 -0
- package/scripts/assemble-web.mjs +230 -0
- package/scripts/bundle-size.d.mts +2 -0
- package/scripts/bundle-size.mjs +26 -0
- package/scripts/bundle.mjs +53 -0
- package/scripts/cli.d.mts +39 -0
- package/scripts/cli.mjs +496 -0
- package/scripts/install.d.mts +149 -0
- package/scripts/install.mjs +989 -0
- package/scripts/legal-files.mjs +24 -0
- package/scripts/verify-packed.mjs +32 -0
- package/scripts/wrangler-config.d.mts +18 -0
- package/scripts/wrangler-config.mjs +84 -0
- package/src/agent.ts +119 -0
- package/src/auth.ts +352 -0
- package/src/deepseek.ts +231 -0
- package/src/deployment.ts +147 -0
- package/src/direct-shell-core-empty.ts +10 -0
- package/src/direct-shell-protocol.ts +5 -0
- package/src/direct-shell.ts +341 -0
- package/src/do-session-persistence.ts +1064 -0
- package/src/edge-api.ts +796 -0
- package/src/edge-credentials.ts +56 -0
- package/src/edge-remotes.ts +41 -0
- package/src/edge-workspace-store.ts +339 -0
- package/src/http.ts +212 -0
- package/src/index.ts +196 -0
- package/src/instance.ts +1007 -0
- package/src/isolated-direct-shell-unavailable.ts +11 -0
- package/src/protocol.ts +39 -0
- package/src/release.ts +9 -0
- package/src/session-store.ts +1110 -0
- package/src/sse.ts +86 -0
- package/src/web-search.ts +38 -0
- package/src/workspace.ts +270 -0
- package/wrangler.jsonc +56 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const legalFiles = ['LICENSE', 'THIRD_PARTY_NOTICES.md']
|
|
5
|
+
|
|
6
|
+
function paths(name) {
|
|
7
|
+
return {
|
|
8
|
+
source: fileURLToPath(new URL(`../../../${name}`, import.meta.url)),
|
|
9
|
+
target: fileURLToPath(new URL(`../${name}`, import.meta.url)),
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function verify() {
|
|
14
|
+
for (const name of legalFiles) {
|
|
15
|
+
const { source, target } = paths(name)
|
|
16
|
+
if (!readFileSync(target).equals(readFileSync(source))) {
|
|
17
|
+
throw new Error(`${name} differs from the repository-root authority`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const action = process.argv[2]
|
|
23
|
+
if (action === 'verify') verify()
|
|
24
|
+
else throw new Error('Usage: node scripts/legal-files.mjs verify')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const input = process.argv[2]
|
|
7
|
+
if (input === undefined) throw new Error('Usage: node scripts/verify-packed.mjs <tarball>')
|
|
8
|
+
const tarball = isAbsolute(input) ? input : resolve(process.cwd(), input)
|
|
9
|
+
const directory = mkdtempSync(join(tmpdir(), 'dsh-edge-packed-'))
|
|
10
|
+
|
|
11
|
+
function run(command, args) {
|
|
12
|
+
execFileSync(command, args, { cwd: directory, stdio: 'inherit' })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball])
|
|
17
|
+
const packageDirectory = join(directory, 'node_modules', 'dsh-edge')
|
|
18
|
+
const license = join(packageDirectory, 'LICENSE')
|
|
19
|
+
const notices = join(packageDirectory, 'THIRD_PARTY_NOTICES.md')
|
|
20
|
+
if (!existsSync(license) || !readFileSync(license, 'utf8').startsWith('MIT License')) {
|
|
21
|
+
throw new Error('packed dsh-edge is missing its MIT license')
|
|
22
|
+
}
|
|
23
|
+
if (!existsSync(notices) || !readFileSync(notices, 'utf8').includes('# Third-Party Notices')) {
|
|
24
|
+
throw new Error('packed dsh-edge is missing third-party notices')
|
|
25
|
+
}
|
|
26
|
+
run(process.execPath, [join(packageDirectory, 'scripts', 'cli.mjs'), '--version'])
|
|
27
|
+
run(process.execPath, [join(packageDirectory, 'scripts', 'cli.mjs'), '--help'])
|
|
28
|
+
run(process.execPath, [join(packageDirectory, 'scripts', 'bundle.mjs'), 'direct'])
|
|
29
|
+
run(process.execPath, [join(packageDirectory, 'scripts', 'bundle.mjs'), 'isolated'])
|
|
30
|
+
} finally {
|
|
31
|
+
rmSync(directory, { recursive: true, force: true })
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RuntimeMode } from './install.mjs'
|
|
2
|
+
|
|
3
|
+
export interface WranglerConfigOptions {
|
|
4
|
+
appDirectory?: string
|
|
5
|
+
sourceConfigPath?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function renderModeWranglerConfig(
|
|
9
|
+
mode: RuntimeMode,
|
|
10
|
+
source: string,
|
|
11
|
+
options?: WranglerConfigOptions,
|
|
12
|
+
): string
|
|
13
|
+
|
|
14
|
+
export function writeModeWranglerConfig(
|
|
15
|
+
mode: RuntimeMode,
|
|
16
|
+
destination: string,
|
|
17
|
+
options?: WranglerConfigOptions,
|
|
18
|
+
): Promise<void>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { parse, printParseErrorCode } from 'jsonc-parser'
|
|
5
|
+
|
|
6
|
+
const appDirectory = fileURLToPath(new URL('..', import.meta.url))
|
|
7
|
+
const sourceConfigPath = fileURLToPath(new URL('../wrangler.jsonc', import.meta.url))
|
|
8
|
+
const DIRECT_SHELL_CORE = '@cloudflare/computer/shell/core'
|
|
9
|
+
const DIRECT_SHELL_CORE_REPLACEMENT = 'src/direct-shell-core-empty.ts'
|
|
10
|
+
const ISOLATED_DIRECT_SHELL = './direct-shell.ts'
|
|
11
|
+
const ISOLATED_DIRECT_SHELL_REPLACEMENT = 'src/isolated-direct-shell-unavailable.ts'
|
|
12
|
+
const RESERVED_ALIASES = new Set([DIRECT_SHELL_CORE, ISOLATED_DIRECT_SHELL])
|
|
13
|
+
|
|
14
|
+
/** Render one absolute-path Wrangler configuration from the repository source. */
|
|
15
|
+
export function renderModeWranglerConfig(mode, source, options = {}) {
|
|
16
|
+
requireRuntimeMode(mode)
|
|
17
|
+
const root = options.appDirectory ?? appDirectory
|
|
18
|
+
const errors = []
|
|
19
|
+
const parsed = parse(source, errors, {
|
|
20
|
+
allowEmptyContent: false,
|
|
21
|
+
allowTrailingComma: true,
|
|
22
|
+
disallowComments: false,
|
|
23
|
+
})
|
|
24
|
+
if (errors.length > 0) {
|
|
25
|
+
const detail = errors.map(error => printParseErrorCode(error.error)).join(', ')
|
|
26
|
+
throw new Error(`Could not parse wrangler.jsonc: ${detail}.`)
|
|
27
|
+
}
|
|
28
|
+
if (!isRecord(parsed)) throw new Error('wrangler.jsonc must contain one object.')
|
|
29
|
+
if (typeof parsed.main !== 'string' || parsed.main === '') {
|
|
30
|
+
throw new Error('wrangler.jsonc must declare a main entry point.')
|
|
31
|
+
}
|
|
32
|
+
if (!isRecord(parsed.assets) || typeof parsed.assets.directory !== 'string'
|
|
33
|
+
|| parsed.assets.directory === '') {
|
|
34
|
+
throw new Error('wrangler.jsonc must declare an assets directory.')
|
|
35
|
+
}
|
|
36
|
+
if (parsed.alias !== undefined && !isStringRecord(parsed.alias)) {
|
|
37
|
+
throw new Error('wrangler.jsonc aliases must map module names to paths.')
|
|
38
|
+
}
|
|
39
|
+
const reservedAlias = parsed.alias === undefined
|
|
40
|
+
? undefined
|
|
41
|
+
: Object.keys(parsed.alias).find(alias => RESERVED_ALIASES.has(alias))
|
|
42
|
+
if (reservedAlias !== undefined) {
|
|
43
|
+
throw new Error(`wrangler.jsonc reserves the ${reservedAlias} alias for mode builds.`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = structuredClone(parsed)
|
|
47
|
+
delete config.$schema
|
|
48
|
+
config.main = resolve(root, parsed.main)
|
|
49
|
+
config.assets.directory = resolve(root, parsed.assets.directory)
|
|
50
|
+
config.minify = true
|
|
51
|
+
if (mode === 'direct') {
|
|
52
|
+
config.alias = {
|
|
53
|
+
...config.alias,
|
|
54
|
+
[DIRECT_SHELL_CORE]: resolve(root, DIRECT_SHELL_CORE_REPLACEMENT),
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
config.alias = {
|
|
58
|
+
...config.alias,
|
|
59
|
+
[ISOLATED_DIRECT_SHELL]: resolve(root, ISOLATED_DIRECT_SHELL_REPLACEMENT),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return `${JSON.stringify(config, undefined, 2)}\n`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Write the private Wrangler configuration used by one installation attempt. */
|
|
66
|
+
export async function writeModeWranglerConfig(mode, destination, options = {}) {
|
|
67
|
+
const source = await readFile(options.sourceConfigPath ?? sourceConfigPath, 'utf8')
|
|
68
|
+
const rendered = renderModeWranglerConfig(mode, source, options)
|
|
69
|
+
await writeFile(destination, rendered, { encoding: 'utf8', mode: 0o600, flag: 'wx' })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requireRuntimeMode(mode) {
|
|
73
|
+
if (mode !== 'direct' && mode !== 'isolated') {
|
|
74
|
+
throw new Error(`Unsupported runtime mode: ${String(mode)}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isStringRecord(value) {
|
|
83
|
+
return isRecord(value) && Object.values(value).every(entry => typeof entry === 'string')
|
|
84
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
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
|
+
}
|