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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +183 -0
  4. package/README.zh.md +183 -0
  5. package/THIRD_PARTY_NOTICES.md +199 -0
  6. package/dist/_headers +3 -0
  7. package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  8. package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  9. package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  10. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  11. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  12. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  13. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  14. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  15. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  16. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  17. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  18. package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  19. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  20. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  21. package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  22. package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  23. package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  24. package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  25. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  26. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  27. package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  28. package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  29. package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  30. package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  31. package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  32. package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  33. package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  34. package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  35. package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  36. package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  37. package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  38. package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  39. package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  40. package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  41. package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  42. package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  43. package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  44. package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  45. package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  46. package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  47. package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  48. package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  49. package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  50. package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  52. package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  53. package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  54. package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  55. package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  56. package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  57. package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  58. package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  59. package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  60. package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  61. package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  62. package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  63. package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  64. package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  65. package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  66. package/dist/assets/index-C-1AiF3k.js +112 -0
  67. package/dist/assets/index-CSGf6Qzd.css +1 -0
  68. package/dist/assets/langs/c-BIGW1oBm.js +2 -0
  69. package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
  70. package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
  71. package/dist/assets/langs/css-CLj8gQPS.js +2 -0
  72. package/dist/assets/langs/go-C27-OAKa.js +2 -0
  73. package/dist/assets/langs/html-CfGypltT.js +2 -0
  74. package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
  75. package/dist/assets/langs/java-CylS5w8V.js +2 -0
  76. package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
  77. package/dist/assets/langs/less-B1dDrJ26.js +2 -0
  78. package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
  79. package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
  80. package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
  81. package/dist/assets/langs/php-D-uCvoST.js +2 -0
  82. package/dist/assets/langs/python-B6aJPvgy.js +2 -0
  83. package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
  84. package/dist/assets/langs/rust-B1yitclQ.js +2 -0
  85. package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
  86. package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
  87. package/dist/assets/langs/swift-D82vCrfD.js +2 -0
  88. package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
  89. package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
  90. package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
  91. package/dist/assets/vendor-Cjbwl5VI.js +411 -0
  92. package/dist/assets/vendor-CjyC-hUb.css +1 -0
  93. package/dist/favicon.svg +8 -0
  94. package/dist/index.html +35 -0
  95. package/dist/manifest.webmanifest +16 -0
  96. package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
  97. package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
  98. package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
  99. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
  100. package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
  101. package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
  102. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
  103. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
  104. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
  105. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
  106. package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
  107. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
  108. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
  109. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
  110. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
  111. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
  112. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
  113. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
  114. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
  115. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
  116. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
  117. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
  118. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
  119. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
  120. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
  121. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
  122. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
  123. package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
  124. package/package.json +97 -0
  125. package/scripts/assemble-web.mjs +230 -0
  126. package/scripts/bundle-size.d.mts +2 -0
  127. package/scripts/bundle-size.mjs +26 -0
  128. package/scripts/bundle.mjs +53 -0
  129. package/scripts/cli.d.mts +39 -0
  130. package/scripts/cli.mjs +496 -0
  131. package/scripts/install.d.mts +149 -0
  132. package/scripts/install.mjs +989 -0
  133. package/scripts/legal-files.mjs +24 -0
  134. package/scripts/verify-packed.mjs +32 -0
  135. package/scripts/wrangler-config.d.mts +18 -0
  136. package/scripts/wrangler-config.mjs +84 -0
  137. package/src/agent.ts +119 -0
  138. package/src/auth.ts +352 -0
  139. package/src/deepseek.ts +231 -0
  140. package/src/deployment.ts +147 -0
  141. package/src/direct-shell-core-empty.ts +10 -0
  142. package/src/direct-shell-protocol.ts +5 -0
  143. package/src/direct-shell.ts +341 -0
  144. package/src/do-session-persistence.ts +1064 -0
  145. package/src/edge-api.ts +796 -0
  146. package/src/edge-credentials.ts +56 -0
  147. package/src/edge-remotes.ts +41 -0
  148. package/src/edge-workspace-store.ts +339 -0
  149. package/src/http.ts +212 -0
  150. package/src/index.ts +196 -0
  151. package/src/instance.ts +1007 -0
  152. package/src/isolated-direct-shell-unavailable.ts +11 -0
  153. package/src/protocol.ts +39 -0
  154. package/src/release.ts +9 -0
  155. package/src/session-store.ts +1110 -0
  156. package/src/sse.ts +86 -0
  157. package/src/web-search.ts +38 -0
  158. package/src/workspace.ts +270 -0
  159. package/wrangler.jsonc +56 -0
@@ -0,0 +1,56 @@
1
+ /** Read-only upstream credential provider over Cloudflare Worker secrets. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import CredentialProvider, {
5
+ credentialRef,
6
+ type CredentialInfo,
7
+ type CredentialRef,
8
+ type ResolvedCredential,
9
+ } from '@deepseek-ai/dsh-credentials'
10
+
11
+ export const EDGE_DEEPSEEK_API_KEY_REF = credentialRef('DEEPSEEK_API_KEY')
12
+
13
+ export interface EdgeCredentialProviderConfig {
14
+ /** Read the current deployment secret without copying it into Durable Object storage. */
15
+ readDeepSeekApiKey(): string | undefined
16
+ }
17
+
18
+ /** Resolve deployment credentials through the same seam used by upstream providers. */
19
+ export class EdgeCredentialProvider extends CredentialProvider {
20
+ constructor(
21
+ ctx: Context,
22
+ private readonly config: EdgeCredentialProviderConfig,
23
+ ) {
24
+ super(ctx)
25
+ }
26
+
27
+ /** Resolve the supported Worker secret for one operation. */
28
+ override resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined> {
29
+ if (ref !== EDGE_DEEPSEEK_API_KEY_REF) return Promise.resolve(undefined)
30
+ const value = this.config.readDeepSeekApiKey()?.trim()
31
+ if (value === undefined || value.length === 0) return Promise.resolve(undefined)
32
+ return Promise.resolve({ value, source: 'worker-secret' })
33
+ }
34
+
35
+ /** Describe a Worker secret without exposing its value. */
36
+ override async describe(ref: CredentialRef): Promise<CredentialInfo> {
37
+ const resolved = await this.resolve(ref)
38
+ return {
39
+ configured: resolved !== undefined,
40
+ ...resolved === undefined ? {} : { source: resolved.source },
41
+ writable: false,
42
+ }
43
+ }
44
+
45
+ /** Cloudflare secrets are changed through deployment tooling, not runtime RPCs. */
46
+ override set(_ref: CredentialRef, _value: string): Promise<void> {
47
+ return Promise.reject(new Error('dsh-edge credentials are read-only; update the Cloudflare Worker secret.'))
48
+ }
49
+
50
+ /** Cloudflare secrets are changed through deployment tooling, not runtime RPCs. */
51
+ override unset(_ref: CredentialRef): Promise<void> {
52
+ return Promise.reject(new Error('dsh-edge credentials are read-only; update the Cloudflare Worker secret.'))
53
+ }
54
+ }
55
+
56
+ export default EdgeCredentialProvider
@@ -0,0 +1,41 @@
1
+ /** Minimal generated-Remote compatibility required by the upstream Web UI. */
2
+
3
+ import type { ServerResponse } from '@deepseek-ai/dsh-host-apiproxy/api'
4
+ import { clientRequestSchema } from '@deepseek-ai/dsh-host-apiproxy/api/rpc.schema'
5
+
6
+ const COMMAND_LIST_PATH = '/api/commands/list'
7
+
8
+ /**
9
+ * Handle Edge-owned endpoints on upstream's shared generic `/api` channel.
10
+ * The Edge preset currently registers no slash commands, so its truthful
11
+ * catalog is empty; retaining the upstream carrier envelope lets ui-commands
12
+ * stay mounted without a parallel Edge-only client implementation.
13
+ */
14
+ export async function handleEdgeRemote(request: Request): Promise<Response | undefined> {
15
+ const url = new URL(request.url)
16
+ if (url.pathname !== COMMAND_LIST_PATH) return undefined
17
+ if (request.method !== 'POST') {
18
+ return new Response('method not allowed', { status: 405, headers: { allow: 'POST' } })
19
+ }
20
+ if (request.headers.get('content-type')?.split(';', 1).at(0)?.trim() !== 'application/json') {
21
+ return new Response('expected application/json', { status: 415 })
22
+ }
23
+
24
+ let wire: unknown
25
+ try {
26
+ wire = await request.json()
27
+ } catch {
28
+ return new Response('invalid JSON body', { status: 400 })
29
+ }
30
+ const parsed = clientRequestSchema.safeParse(wire)
31
+ if (!parsed.success || parsed.data.method !== 'commands/list') {
32
+ return new Response('request path and method do not match', { status: 400 })
33
+ }
34
+
35
+ const body: ServerResponse = {
36
+ type: 'server-response',
37
+ rpcId: parsed.data.rpcId,
38
+ result: { ok: true, value: [] },
39
+ }
40
+ return Response.json(body)
41
+ }
@@ -0,0 +1,339 @@
1
+ /** Cloudflare Durable Object adapter for the upstream workspace domain. */
2
+
3
+ import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-host-apiproxy/api'
4
+ import type { SessionId } from '@deepseek-ai/dsh-session'
5
+ import type { EdgeApiSessionSummary } from './session-store.ts'
6
+
7
+ export const EDGE_WORKSPACE_ID = 'edge-workspace' as WorkspaceId
8
+ export const EDGE_WORKSPACE_PATH = '/workspace'
9
+
10
+ const WORKSPACE_DOMAIN_STATE_KEY = 'dsh-edge:workspace-domain-state:v2'
11
+ const WORKSPACE_RECORD_KEY = 'dsh-edge:workspace-domain-workspaces:edge-workspace:v2'
12
+
13
+ /** Exact upstream workspace-domain global value, backed by DO storage. */
14
+ interface EdgeWorkspaceDomainState {
15
+ initialized: boolean
16
+ workspaceIds: WorkspaceId[]
17
+ archivedSessionIds: SessionId[]
18
+ }
19
+
20
+ /** Exact upstream workspaces-table value, backed by DO storage. */
21
+ interface EdgeWorkspaceRecord {
22
+ path: string
23
+ title: string
24
+ sessionIds: SessionId[]
25
+ createdAt: string
26
+ updatedAt: string
27
+ }
28
+
29
+ export class EdgeWorkspaceStoreError extends Error {
30
+ constructor(
31
+ readonly code: 'NOT_FOUND' | 'INVALID_PATH' | 'MOVE_INVALID',
32
+ message: string,
33
+ ) {
34
+ super(message)
35
+ this.name = 'EdgeWorkspaceStoreError'
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Preserve the upstream workspace records/global-state split while replacing
41
+ * its Node filesystem and storage-domain backends with one DO transaction.
42
+ */
43
+ export class EdgeWorkspaceStore {
44
+ private operationTail: Promise<void> = Promise.resolve()
45
+
46
+ constructor(private readonly storage: DurableObjectStorage) {}
47
+
48
+ list(sessions: readonly EdgeApiSessionSummary[]): Promise<{
49
+ items: WorkspaceView[]
50
+ archivedSessionIds: SessionId[]
51
+ }> {
52
+ return this.serialize(async () => {
53
+ const state = await this.readState()
54
+ if (!state.workspaceIds.includes(EDGE_WORKSPACE_ID)) {
55
+ return { items: [], archivedSessionIds: state.archivedSessionIds }
56
+ }
57
+ const record = await this.readOrCreateRecord(sessions)
58
+ return {
59
+ items: [workspaceView(record)],
60
+ archivedSessionIds: state.archivedSessionIds,
61
+ }
62
+ })
63
+ }
64
+
65
+ create(
66
+ path: string,
67
+ sessions: readonly EdgeApiSessionSummary[],
68
+ ): Promise<{ workspace: WorkspaceView; created: boolean }> {
69
+ return this.serialize(async () => {
70
+ if (path !== EDGE_WORKSPACE_PATH) {
71
+ throw new EdgeWorkspaceStoreError(
72
+ 'INVALID_PATH',
73
+ `Edge workspaces must use ${EDGE_WORKSPACE_PATH}.`,
74
+ )
75
+ }
76
+ const state = await this.readState()
77
+ if (state.workspaceIds.includes(EDGE_WORKSPACE_ID)) {
78
+ const record = await this.readOrCreateRecord(sessions)
79
+ return { workspace: workspaceView(record), created: false }
80
+ }
81
+ const now = new Date().toISOString()
82
+ const record: EdgeWorkspaceRecord = {
83
+ path: EDGE_WORKSPACE_PATH,
84
+ title: 'workspace',
85
+ sessionIds: [],
86
+ createdAt: now,
87
+ updatedAt: now,
88
+ }
89
+ await this.storage.transaction(async (txn) => {
90
+ await txn.put(WORKSPACE_RECORD_KEY, record)
91
+ await txn.put(WORKSPACE_DOMAIN_STATE_KEY, {
92
+ ...state,
93
+ workspaceIds: [EDGE_WORKSPACE_ID],
94
+ } satisfies EdgeWorkspaceDomainState)
95
+ })
96
+ return { workspace: workspaceView(record), created: true }
97
+ })
98
+ }
99
+
100
+ rename(workspaceId: WorkspaceId, title: string, sessions: readonly EdgeApiSessionSummary[]): Promise<{
101
+ workspace: WorkspaceView
102
+ changed: boolean
103
+ }> {
104
+ return this.serialize(async () => {
105
+ await this.requireWorkspace(workspaceId)
106
+ const current = await this.readOrCreateRecord(sessions)
107
+ if (current.title === title) return { workspace: workspaceView(current), changed: false }
108
+ const next = { ...current, title, updatedAt: new Date().toISOString() }
109
+ await this.storage.put(WORKSPACE_RECORD_KEY, next)
110
+ return { workspace: workspaceView(next), changed: true }
111
+ })
112
+ }
113
+
114
+ delete(workspaceId: WorkspaceId): Promise<void> {
115
+ return this.serialize(async () => {
116
+ const state = await this.requireWorkspace(workspaceId)
117
+ await this.storage.transaction(async (txn) => {
118
+ await txn.delete(WORKSPACE_RECORD_KEY)
119
+ await txn.put(WORKSPACE_DOMAIN_STATE_KEY, {
120
+ ...state,
121
+ workspaceIds: [],
122
+ } satisfies EdgeWorkspaceDomainState)
123
+ })
124
+ })
125
+ }
126
+
127
+ insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise<WorkspaceId[]> {
128
+ return this.serialize(async () => {
129
+ const state = await this.requireWorkspace(workspaceId)
130
+ if (beforeWorkspaceId !== undefined && beforeWorkspaceId !== EDGE_WORKSPACE_ID) {
131
+ throw new EdgeWorkspaceStoreError(
132
+ 'NOT_FOUND',
133
+ `Workspace "${beforeWorkspaceId}" is not available in this Edge instance.`,
134
+ )
135
+ }
136
+ return [...state.workspaceIds]
137
+ })
138
+ }
139
+
140
+ insertSessionBefore(
141
+ workspaceId: WorkspaceId,
142
+ sessionId: SessionId,
143
+ beforeSessionId: SessionId | undefined,
144
+ sessions: readonly EdgeApiSessionSummary[],
145
+ ): Promise<{ workspace: WorkspaceView; changed: boolean }> {
146
+ return this.serialize(async () => {
147
+ await this.requireWorkspace(workspaceId)
148
+ const current = await this.readOrCreateRecord(sessions)
149
+ if (!current.sessionIds.includes(sessionId)) {
150
+ throw new EdgeWorkspaceStoreError(
151
+ 'MOVE_INVALID',
152
+ `Session "${sessionId}" is not accounted by workspace "${workspaceId}".`,
153
+ )
154
+ }
155
+ if (beforeSessionId !== undefined && !current.sessionIds.includes(beforeSessionId)) {
156
+ throw new EdgeWorkspaceStoreError(
157
+ 'MOVE_INVALID',
158
+ `Anchor session "${beforeSessionId}" is not accounted by workspace "${workspaceId}".`,
159
+ )
160
+ }
161
+ if (beforeSessionId === sessionId) {
162
+ return { workspace: workspaceView(current), changed: false }
163
+ }
164
+ const without = current.sessionIds.filter(id => id !== sessionId)
165
+ const index = beforeSessionId === undefined ? without.length : without.indexOf(beforeSessionId)
166
+ const sessionIds = [...without.slice(0, index), sessionId, ...without.slice(index)]
167
+ if (sameIds(sessionIds, current.sessionIds)) {
168
+ return { workspace: workspaceView(current), changed: false }
169
+ }
170
+ const next = { ...current, sessionIds, updatedAt: new Date().toISOString() }
171
+ await this.storage.put(WORKSPACE_RECORD_KEY, next)
172
+ return { workspace: workspaceView(next), changed: true }
173
+ })
174
+ }
175
+
176
+ attachSession(
177
+ workspaceId: WorkspaceId,
178
+ sessionId: SessionId,
179
+ sessions: readonly EdgeApiSessionSummary[],
180
+ ): Promise<WorkspaceView> {
181
+ return this.serialize(async () => {
182
+ await this.requireWorkspace(workspaceId)
183
+ const current = await this.readOrCreateRecord(sessions)
184
+ if (current.sessionIds.includes(sessionId)) return workspaceView(current)
185
+ const next = {
186
+ ...current,
187
+ sessionIds: [sessionId, ...current.sessionIds],
188
+ updatedAt: new Date().toISOString(),
189
+ }
190
+ await this.storage.put(WORKSPACE_RECORD_KEY, next)
191
+ return workspaceView(next)
192
+ })
193
+ }
194
+
195
+ workspaceForSession(
196
+ sessionId: SessionId,
197
+ sessions: readonly EdgeApiSessionSummary[],
198
+ ): Promise<WorkspaceId | undefined> {
199
+ return this.serialize(async () => {
200
+ const state = await this.readState()
201
+ if (!state.workspaceIds.includes(EDGE_WORKSPACE_ID)) return undefined
202
+ const current = await this.readOrCreateRecord(sessions)
203
+ return current.sessionIds.includes(sessionId) ? EDGE_WORKSPACE_ID : undefined
204
+ })
205
+ }
206
+
207
+ archivedSessionIds(): Promise<SessionId[]> {
208
+ return this.serialize(async () => [...(await this.readState()).archivedSessionIds])
209
+ }
210
+
211
+ archiveSession(sessionId: SessionId, requireSession: () => Promise<unknown>): Promise<{
212
+ archivedSessionIds: SessionId[]
213
+ changed: boolean
214
+ }> {
215
+ return this.serialize(async () => {
216
+ const state = await this.readState()
217
+ if (state.archivedSessionIds.includes(sessionId)) {
218
+ return { archivedSessionIds: [...state.archivedSessionIds], changed: false }
219
+ }
220
+ await requireSession()
221
+ const archivedSessionIds = [...state.archivedSessionIds, sessionId]
222
+ await this.storage.put(WORKSPACE_DOMAIN_STATE_KEY, {
223
+ ...state,
224
+ archivedSessionIds,
225
+ } satisfies EdgeWorkspaceDomainState)
226
+ return { archivedSessionIds, changed: true }
227
+ })
228
+ }
229
+
230
+ private async requireWorkspace(workspaceId: WorkspaceId): Promise<EdgeWorkspaceDomainState> {
231
+ const state = await this.readState()
232
+ if (workspaceId !== EDGE_WORKSPACE_ID || !state.workspaceIds.includes(workspaceId)) {
233
+ throw new EdgeWorkspaceStoreError(
234
+ 'NOT_FOUND',
235
+ `Workspace "${workspaceId}" is not available in this Edge instance.`,
236
+ )
237
+ }
238
+ return state
239
+ }
240
+
241
+ private async readOrCreateRecord(
242
+ sessions: readonly EdgeApiSessionSummary[],
243
+ ): Promise<EdgeWorkspaceRecord> {
244
+ const stored = await this.storage.get(WORKSPACE_RECORD_KEY)
245
+ if (stored !== undefined) return workspaceRecordFromStorage(stored)
246
+ const record = defaultWorkspaceRecord(sessions)
247
+ await this.storage.put(WORKSPACE_RECORD_KEY, record)
248
+ return record
249
+ }
250
+
251
+ private async readState(): Promise<EdgeWorkspaceDomainState> {
252
+ const stored = await this.storage.get(WORKSPACE_DOMAIN_STATE_KEY)
253
+ if (stored === undefined) {
254
+ return {
255
+ initialized: true,
256
+ workspaceIds: [EDGE_WORKSPACE_ID],
257
+ archivedSessionIds: [],
258
+ }
259
+ }
260
+ return workspaceDomainStateFromStorage(stored)
261
+ }
262
+
263
+ private serialize<T>(operation: () => Promise<T>): Promise<T> {
264
+ const result = this.operationTail.then(operation)
265
+ this.operationTail = result.then(() => undefined, () => undefined)
266
+ return result
267
+ }
268
+ }
269
+
270
+ function defaultWorkspaceRecord(sessions: readonly EdgeApiSessionSummary[]): EdgeWorkspaceRecord {
271
+ const createdAt = sessions.length === 0
272
+ ? new Date(0).toISOString()
273
+ : new Date(Math.min(...sessions.map(session => session.createdAt))).toISOString()
274
+ const updatedAt = sessions.length === 0
275
+ ? createdAt
276
+ : new Date(Math.max(...sessions.map(session => session.updatedAt))).toISOString()
277
+ return {
278
+ path: EDGE_WORKSPACE_PATH,
279
+ title: 'Workspace',
280
+ sessionIds: sessions.map(session => session.id),
281
+ createdAt,
282
+ updatedAt,
283
+ }
284
+ }
285
+
286
+ function workspaceView(record: EdgeWorkspaceRecord): WorkspaceView {
287
+ return { workspaceId: EDGE_WORKSPACE_ID, ...record, sessionIds: [...record.sessionIds] }
288
+ }
289
+
290
+ function sameIds(left: readonly SessionId[], right: readonly SessionId[]): boolean {
291
+ return left.length === right.length && left.every((id, index) => id === right[index])
292
+ }
293
+
294
+ function workspaceDomainStateFromStorage(stored: unknown): EdgeWorkspaceDomainState {
295
+ if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) {
296
+ throw new Error('dsh-edge workspace domain state is invalid.')
297
+ }
298
+ const value = stored as Record<string, unknown>
299
+ const workspaceIds = value.workspaceIds
300
+ const archivedSessionIds = value.archivedSessionIds
301
+ if (value.initialized !== true
302
+ || !Array.isArray(workspaceIds)
303
+ || workspaceIds.some(id => id !== EDGE_WORKSPACE_ID)
304
+ || new Set(workspaceIds).size !== workspaceIds.length
305
+ || !Array.isArray(archivedSessionIds)
306
+ || archivedSessionIds.some(id => typeof id !== 'string' || id.length === 0)
307
+ || new Set(archivedSessionIds).size !== archivedSessionIds.length
308
+ || Object.hasOwn(value, 'pendingMutation')) {
309
+ throw new Error('dsh-edge workspace domain state is invalid.')
310
+ }
311
+ return {
312
+ initialized: true,
313
+ workspaceIds: workspaceIds.map(() => EDGE_WORKSPACE_ID),
314
+ archivedSessionIds: archivedSessionIds as SessionId[],
315
+ }
316
+ }
317
+
318
+ function workspaceRecordFromStorage(stored: unknown): EdgeWorkspaceRecord {
319
+ if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) {
320
+ throw new Error('dsh-edge workspace record is invalid.')
321
+ }
322
+ const value = stored as Record<string, unknown>
323
+ if (value.path !== EDGE_WORKSPACE_PATH
324
+ || typeof value.title !== 'string'
325
+ || !Array.isArray(value.sessionIds)
326
+ || value.sessionIds.some(id => typeof id !== 'string' || id.length === 0)
327
+ || new Set(value.sessionIds).size !== value.sessionIds.length
328
+ || typeof value.createdAt !== 'string'
329
+ || typeof value.updatedAt !== 'string') {
330
+ throw new Error('dsh-edge workspace record is invalid.')
331
+ }
332
+ return {
333
+ path: EDGE_WORKSPACE_PATH,
334
+ title: value.title,
335
+ sessionIds: value.sessionIds as SessionId[],
336
+ createdAt: value.createdAt,
337
+ updatedAt: value.updatedAt,
338
+ }
339
+ }
package/src/http.ts ADDED
@@ -0,0 +1,212 @@
1
+ /** Shared HTTP parsing, response, and error translation for dsh-edge routes. */
2
+
3
+ import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm'
4
+ import { EdgeSessionStoreError } from './session-store.ts'
5
+ import { EdgeWorkspaceRequestError } from './workspace.ts'
6
+
7
+ const textEncoder = new TextEncoder()
8
+
9
+ export const MAX_SESSION_CREATE_BODY_BYTES = 8_192
10
+ export const MAX_TURN_BODY_BYTES = 524_288
11
+ export const MAX_WORKSPACE_EXEC_BODY_BYTES = 131_072
12
+
13
+ /** Invalid caller input with an explicit HTTP response status. */
14
+ export class EdgeHttpError extends Error {
15
+ constructor(readonly status: number, message: string) {
16
+ super(message)
17
+ }
18
+ }
19
+
20
+ /** Consume a request body without retaining bytes beyond its route limit. */
21
+ export async function readBoundedBody(
22
+ request: Request,
23
+ maxBytes: number,
24
+ message: string,
25
+ ): Promise<Uint8Array<ArrayBuffer>> {
26
+ const declared = request.headers.get('content-length')
27
+ let exceeded = declared !== null && /^\d+$/.test(declared) && Number(declared) > maxBytes
28
+ if (request.body === null) return new Uint8Array()
29
+
30
+ const chunks: Uint8Array[] = []
31
+ let byteLength = 0
32
+ const stream = request.body as ReadableStream<Uint8Array>
33
+ const reader = stream.getReader()
34
+ try {
35
+ while (true) {
36
+ const next = await reader.read()
37
+ if (next.done) break
38
+ byteLength += next.value.byteLength
39
+ if (exceeded || byteLength > maxBytes) {
40
+ exceeded = true
41
+ chunks.length = 0
42
+ continue
43
+ }
44
+ chunks.push(next.value)
45
+ }
46
+ } finally {
47
+ reader.releaseLock()
48
+ }
49
+
50
+ if (exceeded) throw new EdgeHttpError(413, message)
51
+
52
+ const body = new Uint8Array(byteLength)
53
+ let offset = 0
54
+ for (const chunk of chunks) {
55
+ body.set(chunk, offset)
56
+ offset += chunk.byteLength
57
+ }
58
+ return body
59
+ }
60
+
61
+ /** Consume one bounded UTF-8 request body. */
62
+ export async function readBoundedText(
63
+ request: Request,
64
+ maxBytes: number,
65
+ message: string,
66
+ ): Promise<string> {
67
+ const body = await readBoundedBody(request, maxBytes, message)
68
+ try {
69
+ return new TextDecoder('utf-8', { fatal: true }).decode(body)
70
+ } catch {
71
+ throw new EdgeHttpError(400, 'The request body must contain valid UTF-8.')
72
+ }
73
+ }
74
+
75
+ /** Parse a size-bounded JSON object request body. */
76
+ export async function readJsonObject(
77
+ request: Request,
78
+ maxBytes: number,
79
+ ): Promise<Record<string, unknown>> {
80
+ try {
81
+ const source = await readBoundedText(
82
+ request,
83
+ maxBytes,
84
+ `JSON request bodies are limited to ${maxBytes} bytes.`,
85
+ )
86
+ const body: unknown = JSON.parse(source)
87
+ if (!isRecord(body)) {
88
+ throw new EdgeHttpError(400, 'The request body must be a JSON object.')
89
+ }
90
+ return body
91
+ } catch (error) {
92
+ if (error instanceof EdgeHttpError) throw error
93
+ throw new EdgeHttpError(400, 'The request body must contain valid JSON.')
94
+ }
95
+ }
96
+
97
+ /** Release an unread request body before returning an early Worker response. */
98
+ export async function discardUnreadRequestBody(request: Request): Promise<void> {
99
+ if (request.body === null || request.bodyUsed) return
100
+ await request.body.cancel().catch(() => undefined)
101
+ }
102
+
103
+ /** Validate a non-empty bounded string field from an HTTP request. */
104
+ export function requireBoundedString(
105
+ value: unknown,
106
+ field: string,
107
+ maxLength: number,
108
+ ): string {
109
+ if (typeof value !== 'string' || value.trim().length === 0) {
110
+ throw new EdgeHttpError(400, `${field} must be a non-empty string.`)
111
+ }
112
+ if (value.length > maxLength) {
113
+ throw new EdgeHttpError(413, `${field} is limited to ${maxLength} characters.`)
114
+ }
115
+ return value
116
+ }
117
+
118
+ /** Validate a non-empty string field whose wire limit is defined in UTF-8 bytes. */
119
+ export function requireBoundedUtf8String(
120
+ value: unknown,
121
+ field: string,
122
+ maxBytes: number,
123
+ ): string {
124
+ if (typeof value !== 'string' || value.trim().length === 0) {
125
+ throw new EdgeHttpError(400, `${field} must be a non-empty string.`)
126
+ }
127
+ if (textEncoder.encode(value).byteLength > maxBytes) {
128
+ throw new EdgeHttpError(413, `${field} is limited to ${maxBytes} UTF-8 bytes.`)
129
+ }
130
+ return value
131
+ }
132
+
133
+ /** Resolve the deployment-scoped DeepSeek Worker secret. */
134
+ export function resolveDeepSeekApiKey(configuredKey?: string): string {
135
+ const apiKey = configuredKey
136
+ if (apiKey === undefined) {
137
+ throw new EdgeHttpError(
138
+ 401,
139
+ 'Configure the DEEPSEEK_API_KEY Worker secret.',
140
+ )
141
+ }
142
+ return assertUsableApiKey(
143
+ apiKey,
144
+ 'dsh-edge',
145
+ 'DEEPSEEK_API_KEY',
146
+ )
147
+ }
148
+
149
+ /** Build one JSON response with the development CORS policy. */
150
+ export function jsonResponse(value: unknown, status = 200): Response {
151
+ return Response.json(value, { status, headers: corsHeaders() })
152
+ }
153
+
154
+ /** Development-only CORS headers shared by every route. */
155
+ export function corsHeaders(): Record<string, string> {
156
+ return {
157
+ 'access-control-allow-headers': 'content-type, last-event-id',
158
+ 'access-control-allow-methods': 'GET, PUT, POST, DELETE, OPTIONS',
159
+ 'access-control-allow-origin': '*',
160
+ 'access-control-expose-headers': 'x-dsh-edge-has-more, x-dsh-edge-next-after',
161
+ }
162
+ }
163
+
164
+ /** Translate known runtime failures without exposing stacks or credentials. */
165
+ export function errorResponse(error: unknown): Response {
166
+ if (error instanceof EdgeHttpError || error instanceof EdgeWorkspaceRequestError) {
167
+ return jsonResponse({ ok: false, error: error.message }, error.status)
168
+ }
169
+ if (error instanceof EdgeSessionStoreError) {
170
+ const status = error.code === 'NOT_FOUND' ? 404 : error.code === 'BUSY' ? 409 : 500
171
+ return jsonResponse({ ok: false, error: error.message, code: error.code }, status)
172
+ }
173
+ if (error instanceof LlmError) {
174
+ return jsonResponse({
175
+ ok: false,
176
+ error: error.message,
177
+ code: error.code,
178
+ ...error.failure.status === undefined
179
+ ? {}
180
+ : { providerStatus: error.failure.status },
181
+ ...error.failure.requestId === undefined
182
+ ? {}
183
+ : { providerRequestId: error.failure.requestId },
184
+ }, llmHttpStatus(error.code))
185
+ }
186
+ if (isErrorCode(error, 'ENOENT')) {
187
+ return jsonResponse({ ok: false, error: 'Workspace entry not found.' }, 404)
188
+ }
189
+ console.error(error)
190
+ return jsonResponse({ ok: false, error: 'Internal runtime error.' }, 500)
191
+ }
192
+
193
+ function llmHttpStatus(code: string): number {
194
+ if (code === 'AUTH' || code === 'MISSING_CREDENTIAL') return 401
195
+ if (code === 'INVALID_CREDENTIAL'
196
+ || code === 'INVALID_REQUEST'
197
+ || code === 'CONTEXT_WINDOW_EXCEEDED') return 400
198
+ if (code === 'RATE_LIMIT' || code === 'QUOTA') return 429
199
+ if (code === 'TIMEOUT') return 504
200
+ return 502
201
+ }
202
+
203
+ function isRecord(value: unknown): value is Record<string, unknown> {
204
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
205
+ }
206
+
207
+ function isErrorCode(error: unknown, code: string): boolean {
208
+ return typeof error === 'object'
209
+ && error !== null
210
+ && 'code' in error
211
+ && error.code === code
212
+ }