dsh-edge 0.15.0-alpha.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +5 -0
- package/README.zh.md +5 -0
- package/package.json +1 -1
- package/scripts/activation.d.mts +5 -0
- package/scripts/activation.mjs +42 -3
- package/scripts/cli.mjs +7 -7
- package/scripts/install.d.mts +2 -0
- package/scripts/install.mjs +2 -0
- package/scripts/smoke-installed.mjs +10 -0
- package/worker/direct/index.js +657 -651
- package/worker/isolated/index.js +452 -446
- package/wrangler.jsonc +2 -0
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority.
|
|
3
3
|
# After editing either side, update both and re-record every pair with:
|
|
4
4
|
# pnpm run doc-pairs -- --write
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 313b3877f1f962865c6a8d8ba3e3a05f11599346
|
|
6
|
+
README.zh.md: 786e48302fb57a8d7b9646991a1b600eace86fec
|
package/README.md
CHANGED
|
@@ -325,8 +325,13 @@ pnpm --filter dsh-edge example:install
|
|
|
325
325
|
- After each committed inbox splice, mux publishes a complete `session/queue` snapshot. Reconnecting clients receive pending live-inbox baselines.
|
|
326
326
|
- `POST /api/commands/list` uses the upstream generated-Remote envelope with an empty catalog because the Edge preset registers no human commands.
|
|
327
327
|
- `GET /api/health` returns the public release/mode identifier and configured attachment default (`private-r2` or `temporary-do`). It validates owner authentication, deployment-scoped DeepSeek credentials, model/transport choices, and command timeouts before reporting ready.
|
|
328
|
+
- `GET /api/ready` requires the owner cookie and waits for session migration, workspace registration, the browser's session/workspace controllers, and registration of the DeepSeek model adapter. It does not make a provider request. It returns HTTP 503 with `runtime-initialization-failed` if startup fails; public health alone does not prove runtime readiness. Ordinary authenticated routes wait for base initialization without requiring the model adapter, preserving history and recovery access when only model initialization failed.
|
|
328
329
|
- Health does not call the provider, Durable Object, R2, VFS, or shell. The authenticated agent-preset projection reports the pinned backend, temporary cap, deployment-default model, and runtime-derived upstream catalog with session selection scope.
|
|
329
330
|
|
|
331
|
+
Release upgrades automatically normalize the known legacy Edge cancellation records and migrate all outdated session logs, summaries, and blank headers in one SQLite transaction. All old logs are decoded and validated before the first migration write, so incompatible later logs cannot repeatedly consume earlier migration writes on restart. Preflight discards each decoded artifact; migration decodes again in the same synchronous transaction to avoid retaining every history in memory. If any session is incompatible or a write fails, that startup migration is rolled back in full. No session is silently skipped or deleted. This transaction covers session-format migration, not arbitrary plugin storage changes or external side effects. The installer matches the Cloudflare Worker version ID from upload output against both public health and authenticated readiness, so an earlier deployment of the same package cannot satisfy the check. The version metadata binding is included in both modes. The installer checks runtime readiness before reporting success; a timeout is reported as unverified, and a confirmed startup failure reports an error with recovery details. It never automatically rolls back Worker code after a schema upgrade. Release tests boot both prebuilt modes with legacy cancellation data, verify continuation, and reject false readiness on incompatible data; the packed npm artifact is also checked for authenticated runtime readiness.
|
|
332
|
+
|
|
333
|
+
SQLite `DELETE` operations consume the daily row-write allowance too. During format migration, Edge compares the number of legacy and current event rows: a single index-backed probe stops once copying current rows would cost more; when copying is cheaper, it writes the migrated compact logs into a replacement table and swaps tables inside the same transaction, avoiding a billed delete for every legacy token delta. Otherwise it replaces only the affected sessions. Current session logs are preserved, and failed migrations roll back the table swap. This reduces migration writes but cannot guarantee that every database fits the Free plan's 100,000 daily row-write allowance; sufficiently large upgrades or earlier daily usage can still exhaust it. Point-in-time recovery restores data, not the account's consumed daily quota.
|
|
334
|
+
|
|
330
335
|
### Diagnostic REST routes
|
|
331
336
|
|
|
332
337
|
- `PUT /api/workspace/file?path=/workspace/...` writes a UTF-8 file.
|
package/README.zh.md
CHANGED
|
@@ -325,8 +325,13 @@ pnpm --filter dsh-edge example:install
|
|
|
325
325
|
- 每次 inbox splice 提交后,mux 都发布完整的 `session/queue` snapshot。客户端重连时会收到待处理的 live-inbox baseline。
|
|
326
326
|
- `POST /api/commands/list` 使用上游 generated-Remote envelope 返回空 catalog,因为 Edge preset 没有注册 human command。
|
|
327
327
|
- `GET /api/health` 返回公开 release/mode identifier 与配置的 attachment 默认值(`private-r2` 或 `temporary-do`)。它先验证 owner authentication、部署级 DeepSeek 凭据、模型/传输选择与命令 timeout,再报告 ready。
|
|
328
|
+
- `GET /api/ready` 要求 owner cookie,等待会话迁移、工作区注册、浏览器所需的会话/工作区控制器就绪,以及 DeepSeek 模型适配器完成注册。此检查不会向模型服务发起请求。启动失败时返回 HTTP 503 和 `runtime-initialization-failed`;公开 health 响应不能单独证明运行时可用。普通认证路由只等待基础初始化,不要求模型适配器可用;仅模型初始化失败时,仍保留历史记录和恢复访问。
|
|
328
329
|
- Health 不调用 provider、Durable Object、R2、VFS 或 shell。认证后的 agent-preset projection 会报告固定 backend、临时存储上限、部署默认模型,以及 runtime 实际读取的上游 catalog 与 session 选择范围。
|
|
329
330
|
|
|
331
|
+
Release 升级会自动兼容已知的旧版 Edge 取消记录,并在一个 SQLite 事务中迁移所有旧格式会话日志、摘要和空会话头。首次迁移写入前会解码并校验所有旧日志,避免后面的不兼容日志导致每次重启都重复消耗前面会话的迁移写入。预检会释放每份解码结果;迁移阶段在同一同步事务内再次解码,避免把所有历史同时保留在内存中。任一会话不兼容或写入失败时,本次启动的迁移会整体回滚,不会静默跳过或删除会话。此事务覆盖会话格式迁移,不涵盖任意插件存储改动或外部副作用。安装器会把上传输出中的 Cloudflare Worker 版本 ID 与公开 health 和经过认证的就绪响应分别比对,同一软件包的旧部署不能满足该检查;两种模式都包含版本元数据绑定。安装器检查运行时就绪状态,通过后才报告成功;超时会报告尚未验证,确认启动失败则报告错误和恢复信息。它不会在存储格式升级后自动回滚 Worker 代码。发布测试使用含旧取消记录的数据启动两种预构建模式,验证继续聊天,并确认不兼容数据不会误报就绪;打包后的 npm 产物也会验证经过认证的运行时就绪状态。
|
|
332
|
+
|
|
333
|
+
SQLite 的 `DELETE` 操作也会消耗每日行写入额度。迁移格式时,Edge 会比较旧格式与当前格式的事件行数:单条利用索引的探测查询在确认复制成本更高时即停止;如果复制当前事件的成本更低,就把迁移后的压缩日志写入替代表,并在同一事务内交换表,避免为每个旧 token 片段支付逐行删除费用;否则只替换需要迁移的会话。已有当前格式日志会保留,迁移失败也会回滚表交换。这会降低迁移写入量,但不能保证所有数据库都满足 Free 计划每日 100,000 行写入额度;数据库足够大或当天已使用较多额度时,仍可能触及上限。时间点恢复恢复的是数据,不会恢复账户已经消耗的每日额度。
|
|
334
|
+
|
|
330
335
|
### 诊断 REST 路由
|
|
331
336
|
|
|
332
337
|
- `PUT /api/workspace/file?path=/workspace/...` 写入 UTF-8 文件。
|
package/package.json
CHANGED
package/scripts/activation.d.mts
CHANGED
|
@@ -7,6 +7,7 @@ export interface ActivationObservation {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface ExpectedHealth {
|
|
10
|
+
workerVersionId: string
|
|
10
11
|
deploymentId: string
|
|
11
12
|
shell: 'just-bash-direct' | 'just-bash-isolated'
|
|
12
13
|
}
|
|
@@ -18,6 +19,8 @@ export const ACTIVATION_RETRY_MS: number
|
|
|
18
19
|
export function observePublicActivation(options: {
|
|
19
20
|
publicUrl: string
|
|
20
21
|
mode: RuntimeMode
|
|
22
|
+
ownerSecret?: string
|
|
23
|
+
versionId?: string
|
|
21
24
|
fetchImpl?: typeof fetch
|
|
22
25
|
now?: () => number
|
|
23
26
|
requestTimeoutMs?: number
|
|
@@ -32,3 +35,5 @@ export function observePublicActivation(options: {
|
|
|
32
35
|
}): Promise<ActivationObservation>
|
|
33
36
|
|
|
34
37
|
export function isExpectedHealth(value: unknown, expected: ExpectedHealth): boolean
|
|
38
|
+
|
|
39
|
+
export class RuntimeActivationError extends Error {}
|
package/scripts/activation.mjs
CHANGED
|
@@ -7,10 +7,12 @@ export const ACTIVATION_RETRY_MS = 1_500
|
|
|
7
7
|
|
|
8
8
|
const MAX_HEALTH_BYTES = 64 * 1024
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/** Verify the exact uploaded release and its authenticated runtime before reporting ready. */
|
|
11
11
|
export async function observePublicActivation({
|
|
12
12
|
publicUrl,
|
|
13
13
|
mode,
|
|
14
|
+
ownerSecret,
|
|
15
|
+
versionId,
|
|
14
16
|
fetchImpl = globalThis.fetch,
|
|
15
17
|
now = Date.now,
|
|
16
18
|
requestTimeoutMs = ACTIVATION_REQUEST_TIMEOUT_MS,
|
|
@@ -29,6 +31,7 @@ export async function observePublicActivation({
|
|
|
29
31
|
|
|
30
32
|
const healthUrl = publicHealthUrl(publicUrl)
|
|
31
33
|
const expected = {
|
|
34
|
+
workerVersionId: versionId,
|
|
32
35
|
deploymentId: `dsh-edge@${edgePackage.version}/${mode}`,
|
|
33
36
|
shell: mode === 'direct' ? 'just-bash-direct' : 'just-bash-isolated',
|
|
34
37
|
}
|
|
@@ -59,12 +62,18 @@ export async function observePublicActivation({
|
|
|
59
62
|
if (response.ok) {
|
|
60
63
|
const health = await readBoundedJson(response, MAX_HEALTH_BYTES)
|
|
61
64
|
if (isExpectedHealth(health, expected)) {
|
|
62
|
-
|
|
65
|
+
if (typeof ownerSecret !== 'string' || Buffer.byteLength(ownerSecret, 'utf8') < 32 || Buffer.byteLength(ownerSecret, 'utf8') > 512) {
|
|
66
|
+
throw new RuntimeActivationError('Runtime verification requires the owner access key.')
|
|
67
|
+
}
|
|
68
|
+
if (await verifyRuntime({ publicUrl, ownerSecret, fetchImpl, signal: requestSignal, expected })) {
|
|
69
|
+
return activationResult('ready', attempts, startedAt, now())
|
|
70
|
+
}
|
|
63
71
|
}
|
|
64
72
|
} else {
|
|
65
73
|
await response.body?.cancel()
|
|
66
74
|
}
|
|
67
|
-
} catch {
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof RuntimeActivationError) throw error
|
|
68
77
|
if (signal?.aborted) signal.throwIfAborted()
|
|
69
78
|
// DNS, routing, challenge, timeout, and placeholder responses are all
|
|
70
79
|
// transient observations until the bounded wait expires.
|
|
@@ -85,6 +94,8 @@ export function isExpectedHealth(value, expected) {
|
|
|
85
94
|
&& value.deploymentId === expected.deploymentId
|
|
86
95
|
&& value.shell === expected.shell
|
|
87
96
|
&& value.version === edgePackage.version
|
|
97
|
+
&& typeof expected.workerVersionId === 'string' && expected.workerVersionId.length > 0
|
|
98
|
+
&& value.workerVersionId === expected.workerVersionId
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
function publicHealthUrl(publicUrl) {
|
|
@@ -131,3 +142,31 @@ function activationResult(status, attempts, startedAt, finishedAt) {
|
|
|
131
142
|
status,
|
|
132
143
|
}
|
|
133
144
|
}
|
|
145
|
+
|
|
146
|
+
/** An upload succeeded, but its application is definitively not ready. */
|
|
147
|
+
export class RuntimeActivationError extends Error {}
|
|
148
|
+
|
|
149
|
+
async function verifyRuntime({ publicUrl, ownerSecret, fetchImpl, signal, expected }) {
|
|
150
|
+
// Login and readiness use the same validated origin. Never follow redirects
|
|
151
|
+
// with owner credentials, and keep the short-lived probe cookie in memory.
|
|
152
|
+
const login = await fetchImpl(new URL('/api/auth/login', publicUrl).href, {
|
|
153
|
+
method: 'POST', redirect: 'manual', signal,
|
|
154
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded', origin: new URL(publicUrl).origin },
|
|
155
|
+
body: new URLSearchParams({ accessKey: ownerSecret }).toString(),
|
|
156
|
+
})
|
|
157
|
+
const cookie = login.headers.get('set-cookie')?.split(';', 1)[0]
|
|
158
|
+
await login.body?.cancel()
|
|
159
|
+
// A same-version deployment may still serve the previous owner key during propagation.
|
|
160
|
+
if (login.status === 401) return false
|
|
161
|
+
if (login.status !== 303 || !/^__Host-dsh_edge_owner=v1\.[0-9]+\.[A-Za-z0-9_-]+$/u.test(cookie ?? '')) return false
|
|
162
|
+
const response = await fetchImpl(new URL('/api/ready', publicUrl).href, {
|
|
163
|
+
redirect: 'manual', signal,
|
|
164
|
+
headers: { accept: 'application/json', 'cache-control': 'no-cache', cookie },
|
|
165
|
+
})
|
|
166
|
+
const state = await readBoundedJson(response, MAX_HEALTH_BYTES)
|
|
167
|
+
if (response.status === 503 && state?.code === 'runtime-initialization-failed'
|
|
168
|
+
&& state.workerVersionId === expected.workerVersionId) {
|
|
169
|
+
throw new RuntimeActivationError('Worker uploaded, but session or workspace initialization failed. Upgrade is not ready. Do not delete stored data; install a compatible release. No automatic rollback was attempted.')
|
|
170
|
+
}
|
|
171
|
+
return response.ok && state?.runtime === true && isExpectedHealth(state, expected)
|
|
172
|
+
}
|
package/scripts/cli.mjs
CHANGED
|
@@ -279,9 +279,9 @@ export function createInstallerUi(
|
|
|
279
279
|
},
|
|
280
280
|
activationFinish(result) {
|
|
281
281
|
if (activationSpinner === undefined) return
|
|
282
|
-
if (result?.status === 'ready') activationSpinner.stop('
|
|
282
|
+
if (result?.status === 'ready') activationSpinner.stop('Chat and workspace services are ready.')
|
|
283
283
|
else if (result?.status === 'pending') {
|
|
284
|
-
activationSpinner.stop('Worker uploaded;
|
|
284
|
+
activationSpinner.stop('Worker uploaded; application readiness is not yet verified.')
|
|
285
285
|
} else {
|
|
286
286
|
activationSpinner.stop('Stopped waiting for public URL activation.')
|
|
287
287
|
}
|
|
@@ -319,9 +319,9 @@ export function createInstallerUi(
|
|
|
319
319
|
...(result.activation?.status === 'ready'
|
|
320
320
|
? ['Status: Ready']
|
|
321
321
|
: [
|
|
322
|
-
'Status:
|
|
323
|
-
'
|
|
324
|
-
'
|
|
322
|
+
'Status: Application readiness has not been verified.',
|
|
323
|
+
'The public URL or application may still be starting.',
|
|
324
|
+
'Open the URL and confirm your chats and workspaces load before using it.',
|
|
325
325
|
]),
|
|
326
326
|
'',
|
|
327
327
|
`URL: ${result.publicUrl}`,
|
|
@@ -349,11 +349,11 @@ export function createInstallerUi(
|
|
|
349
349
|
const ready = result.activation?.status === 'ready'
|
|
350
350
|
const title = ready
|
|
351
351
|
? (command === 'upgrade' ? 'dsh-edge upgrade is live' : 'dsh-edge is ready')
|
|
352
|
-
: 'Worker uploaded —
|
|
352
|
+
: 'Worker uploaded — readiness unverified'
|
|
353
353
|
note(lines.join('\n'), title)
|
|
354
354
|
const outro = ready
|
|
355
355
|
? (command === 'upgrade' ? 'Your dsh-edge upgrade is live.' : 'Your dsh-edge is ready.')
|
|
356
|
-
:
|
|
356
|
+
: 'Worker uploaded; application readiness remains unverified.'
|
|
357
357
|
log(clack.outro, outro)
|
|
358
358
|
},
|
|
359
359
|
}
|
package/scripts/install.d.mts
CHANGED
|
@@ -166,6 +166,8 @@ export function installEdge(options: {
|
|
|
166
166
|
createTemporaryDirectory?: () => Promise<string>
|
|
167
167
|
removePath?: typeof import('node:fs/promises').rm
|
|
168
168
|
observeActivation?: (options: {
|
|
169
|
+
ownerSecret: string
|
|
170
|
+
versionId?: string
|
|
169
171
|
publicUrl: string
|
|
170
172
|
mode: RuntimeMode
|
|
171
173
|
signal?: AbortSignal
|
package/scripts/install.mjs
CHANGED
|
@@ -813,6 +813,8 @@ export async function installEdge({
|
|
|
813
813
|
const activation = await observeActivation({
|
|
814
814
|
mode,
|
|
815
815
|
publicUrl: result.publicUrl,
|
|
816
|
+
ownerSecret: result.ownerSecret,
|
|
817
|
+
versionId: result.versionId,
|
|
816
818
|
signal,
|
|
817
819
|
})
|
|
818
820
|
result = { ...result, activation }
|
|
@@ -42,6 +42,16 @@ try {
|
|
|
42
42
|
assert.equal(health.service, 'dsh-edge')
|
|
43
43
|
assert.equal(health.shell, mode === 'direct' ? 'just-bash-direct' : 'just-bash-isolated')
|
|
44
44
|
assert.equal(health.deploymentId, `dsh-edge@${edgePackage.version}/${mode}`)
|
|
45
|
+
const login = await worker.fetch('http://dsh-edge.test/api/auth/login', {
|
|
46
|
+
method: 'POST', redirect: 'manual', headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
47
|
+
body: new URLSearchParams({ accessKey: ACCESS_KEY }).toString(),
|
|
48
|
+
})
|
|
49
|
+
assert.equal(login.status, 303)
|
|
50
|
+
const cookie = login.headers.get('set-cookie')?.split(';', 1)[0]
|
|
51
|
+
assert.ok(cookie)
|
|
52
|
+
const ready = await worker.fetch('http://dsh-edge.test/api/ready', { headers: { cookie } })
|
|
53
|
+
assert.equal(ready.status, 200)
|
|
54
|
+
assert.equal((await ready.json()).runtime, true)
|
|
45
55
|
await worker.stop()
|
|
46
56
|
worker = undefined
|
|
47
57
|
process.stdout.write(`Installed ${mode} Worker artifact started successfully.\n`)
|