relay-dsh-plugin-events 0.2.2 → 0.2.3-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/README.zh.md +14 -5
- package/docs/acceptance-scenarios.md +1 -1
- package/lib/client.js +41 -41
- package/lib/host-plugin.js +186 -72
- package/lib/host-plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Relay Events for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
> **Now supports DSH `0.1.2-rc.1` while retaining `0.1.2-alpha.3` compatibility.** Plugin `0.2.
|
|
3
|
+
> **Now supports DSH `0.1.2-rc.1` while retaining `0.1.2-alpha.3` compatibility.** Plugin `0.2.3` is verified on both releases and safely upgrades existing Relay databases. [Install it from npm](https://www.npmjs.com/package/relay-dsh-plugin-events) · [Compatibility evidence](https://github.com/yangbobo2021/Relay/tree/codex/relay-foundation/dsh-lab/dsh-0.1.2-rc.1-20260903).
|
|
4
4
|
|
|
5
|
-
> **Release channels:** `latest` → `0.2.
|
|
5
|
+
> **Release channels:** `latest` → `0.2.3`; `next` → `0.2.3-rc.2`.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-events@0.2.
|
|
8
|
+
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-events@0.2.3
|
|
9
9
|
npx @deepseek-ai/dsh@0.1.2-rc.1 web
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
12
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
13
13
|
|
|
14
14
|
English | [中文](README.zh.md)
|
|
15
15
|
|
|
@@ -19,8 +19,8 @@ delivery to the owning existing Session, recovery, and a Waiting Events settings
|
|
|
19
19
|
surface without adding an execution backend.
|
|
20
20
|
|
|
21
21
|
The older `internal` npm channel remains available for integration testing and
|
|
22
|
-
does not carry this compatibility guarantee. Use the exact `0.2.
|
|
23
|
-
|
|
22
|
+
does not carry this compatibility guarantee. Use the exact `0.2.3`
|
|
23
|
+
version in the latest-DSH command above; do not substitute `@internal`.
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
dsh plugin --profile web add --save-exact relay-dsh-plugin-events@internal
|
|
@@ -31,6 +31,18 @@ Semantic routing and Monitor execution are separate plugins. Without a Router,
|
|
|
31
31
|
Events uses exact event-type matching. Without Monitors, Wait-only registration
|
|
32
32
|
and external ingress remain fully usable.
|
|
33
33
|
|
|
34
|
+
## Database lifecycle
|
|
35
|
+
|
|
36
|
+
The Relay database is persistent and is not removed when the plugin is
|
|
37
|
+
uninstalled. On startup, the plugin initializes a missing or empty database at
|
|
38
|
+
the current schema. A recognized older schema is integrity-checked, backed up to
|
|
39
|
+
`<database>.backup-v<old>-to-v<current>.sqlite`, and upgraded in one transaction
|
|
40
|
+
before dependent indexes are created. The backup is verified and reused on a
|
|
41
|
+
retry. Migration failure rolls back all schema changes and reports the backup
|
|
42
|
+
path. An unknown non-empty schema, a corrupt database, foreign-key violations,
|
|
43
|
+
or a schema newer than the plugin supports is rejected rather than guessed or
|
|
44
|
+
silently modified.
|
|
45
|
+
|
|
34
46
|
See [SPEC.md](SPEC.md) and
|
|
35
47
|
[delivery scenarios](docs/acceptance-scenarios.md).
|
|
36
48
|
|
package/README.zh.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# 面向 DeepSeek Harness 的 Relay Events
|
|
2
2
|
|
|
3
|
-
> **现已支持 DSH `0.1.2-rc.1`,并保留对 `0.1.2-alpha.3` 的兼容。** 插件 `0.2.
|
|
3
|
+
> **现已支持 DSH `0.1.2-rc.1`,并保留对 `0.1.2-alpha.3` 的兼容。** 插件 `0.2.3` 已在两个版本上完成验证,并可安全升级已有 Relay 数据库。[从 npm 安装](https://www.npmjs.com/package/relay-dsh-plugin-events) · [兼容性证据](https://github.com/yangbobo2021/Relay/tree/codex/relay-foundation/dsh-lab/dsh-0.1.2-rc.1-20260903)。
|
|
4
4
|
|
|
5
|
-
> **发布通道:** `latest` → `0.2.
|
|
5
|
+
> **发布通道:** `latest` → `0.2.3`;`next` → `0.2.3-rc.2`。
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-events@0.2.
|
|
8
|
+
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-events@0.2.3
|
|
9
9
|
npx @deepseek-ai/dsh@0.1.2-rc.1 web
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
12
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
13
13
|
|
|
14
14
|
[English](README.md) | 中文
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ Wait/Event/Delivery 核心。它提供 Agent 工具、通用 JSON 事件入口
|
|
|
18
18
|
Session 投递、故障恢复以及 Waiting Events 设置界面,但不增加执行后端。
|
|
19
19
|
|
|
20
20
|
旧的 `internal` npm 通道继续用于集成测试,不包含此次兼容保证。请使用上方最新版
|
|
21
|
-
DSH 命令中精确的 `0.2.
|
|
21
|
+
DSH 命令中精确的 `0.2.3` 版本,不要替换为 `@internal`。
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
dsh plugin --profile web add --save-exact relay-dsh-plugin-events@internal
|
|
@@ -28,6 +28,15 @@ dsh web
|
|
|
28
28
|
语义路由与 Monitor 执行由独立插件提供。未安装 Router 时,Events 使用精确事件类型
|
|
29
29
|
匹配;未安装 Monitors 时,Wait 注册与外部事件入口仍然可以完整使用。
|
|
30
30
|
|
|
31
|
+
## 数据库生命周期
|
|
32
|
+
|
|
33
|
+
Relay 数据库是持久化数据,卸载插件时不会被删除。启动时,插件会将不存在或空数据库
|
|
34
|
+
直接初始化为当前 schema。对于可识别的旧 schema,会先校验完整性,再备份到
|
|
35
|
+
`<database>.backup-v<old>-to-v<current>.sqlite`,然后在同一个事务内执行升级,
|
|
36
|
+
最后创建依赖新字段的索引。备份会被校验,重试时会复用。迁移失败会回滚所有 schema 修改,
|
|
37
|
+
并在错误中给出备份路径。未知的非空 schema、损坏的数据库、外键违规,或高于插件所支持版本的
|
|
38
|
+
schema 都会被拒绝,不会猜测结构或静默修改数据。
|
|
39
|
+
|
|
31
40
|
详见 [SPEC.md](SPEC.md) 与[投递场景](docs/acceptance-scenarios.md)。
|
|
32
41
|
|
|
33
42
|
将 `DSH_ROOT` 指向准备好的官方 DSH 只读检出,然后执行
|
|
@@ -29,7 +29,7 @@ Official DSH references: `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`,
|
|
|
29
29
|
| EVT-021 | Non-exclusive fan-out | Exact non-exclusive matches create one atomic Delivery per Session with complete matched cards. | SQLite integration |
|
|
30
30
|
| EVT-022 | Trusted bound source | Only a registered source capability can submit a binding; public payload ownership fields have no authority. | service + protocol security |
|
|
31
31
|
| EVT-023 | Stale/cross-Session binding | Invalid owner, Wait, version, or Session/Wait pair escalates with no claim or Delivery. | SQLite integration |
|
|
32
|
-
| EVT-024 |
|
|
32
|
+
| EVT-024 | Safe schema initialization and upgrade | Missing, empty, and metadata-only storage initializes directly at schema v10; every historical schema v1-v9 is integrity-checked, verified-backup protected, transactionally upgraded, structurally validated, and idempotent while preserving data. Failed migration rolls back; unknown non-empty and future schemas fail closed without mutation. | real file migration matrix + official DSH startup |
|
|
33
33
|
| EVT-025 | Router failure terminal state | Repeated Router failure reaches the configured budget, commits one inspectable escalation, creates no Delivery, and omits raw provider errors/secrets. | unit + SQLite |
|
|
34
34
|
| EVT-026 | Incomplete Event recovery | The background scan resumes `received`/`routing` Events and reaches a single terminal decision without redelivery or duplicate attempts. | fake-clock integration + restart |
|
|
35
35
|
| EVT-027 | Conflicting provider identity | Reuse of one source delivery identity with different content fails closed, preserves the original terminal Event, and creates no second record. | SQLite security integration |
|
package/lib/client.js
CHANGED
|
@@ -18,58 +18,58 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
document.head.appendChild(tag);
|
|
19
19
|
}
|
|
20
20
|
var WaitingEventsSection_module_css_default = {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"details": "bDRFYa_details",
|
|
24
|
-
"eventHeader": "bDRFYa_eventHeader",
|
|
25
|
-
"pagination": "bDRFYa_pagination",
|
|
26
|
-
"cadenceAction": "bDRFYa_cadenceAction",
|
|
27
|
-
"connectorNote": "bDRFYa_connectorNote",
|
|
28
|
-
"counts": "bDRFYa_counts",
|
|
29
|
-
"registration": "bDRFYa_registration",
|
|
30
|
-
"statusBadge": "bDRFYa_statusBadge",
|
|
31
|
-
"filters": "bDRFYa_filters",
|
|
21
|
+
"historyHeader": "bDRFYa_historyHeader",
|
|
22
|
+
"failure": "bDRFYa_failure",
|
|
32
23
|
"emptyHistory": "bDRFYa_emptyHistory",
|
|
33
|
-
"monitors": "bDRFYa_monitors",
|
|
34
|
-
"connectors": "bDRFYa_connectors",
|
|
35
24
|
"iconButton": "bDRFYa_iconButton",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"emptyCatalog": "bDRFYa_emptyCatalog",
|
|
25
|
+
"details": "bDRFYa_details",
|
|
26
|
+
"waitText": "bDRFYa_waitText",
|
|
39
27
|
"bundleRemediation": "bDRFYa_bundleRemediation",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"historyHeader": "bDRFYa_historyHeader",
|
|
43
|
-
"rowState": "bDRFYa_rowState",
|
|
44
|
-
"activation": "bDRFYa_activation",
|
|
28
|
+
"empty": "bDRFYa_empty",
|
|
29
|
+
"connectors": "bDRFYa_connectors",
|
|
45
30
|
"connector": "bDRFYa_connector",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"section": "bDRFYa_section",
|
|
51
|
-
"secretAction": "bDRFYa_secretAction",
|
|
52
|
-
"waitText": "bDRFYa_waitText",
|
|
53
|
-
"sessionButton": "bDRFYa_sessionButton",
|
|
31
|
+
"bundlePermissions": "bDRFYa_bundlePermissions",
|
|
32
|
+
"error": "bDRFYa_error",
|
|
33
|
+
"counts": "bDRFYa_counts",
|
|
34
|
+
"monitors": "bDRFYa_monitors",
|
|
54
35
|
"connectorHeader": "bDRFYa_connectorHeader",
|
|
55
|
-
"
|
|
36
|
+
"accounts": "bDRFYa_accounts",
|
|
56
37
|
"monitorTimes": "bDRFYa_monitorTimes",
|
|
38
|
+
"message": "bDRFYa_message",
|
|
57
39
|
"catalogHeader": "bDRFYa_catalogHeader",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
40
|
+
"registrations": "bDRFYa_registrations",
|
|
41
|
+
"registration": "bDRFYa_registration",
|
|
42
|
+
"registrationHeader": "bDRFYa_registrationHeader",
|
|
60
43
|
"bundleHeader": "bDRFYa_bundleHeader",
|
|
61
|
-
"
|
|
62
|
-
"message": "bDRFYa_message",
|
|
63
|
-
"title": "bDRFYa_title",
|
|
44
|
+
"rowState": "bDRFYa_rowState",
|
|
64
45
|
"bundleCatalog": "bDRFYa_bundleCatalog",
|
|
65
|
-
"
|
|
46
|
+
"sessionButton": "bDRFYa_sessionButton",
|
|
47
|
+
"summary": "bDRFYa_summary",
|
|
48
|
+
"activation": "bDRFYa_activation",
|
|
49
|
+
"connectorNote": "bDRFYa_connectorNote",
|
|
50
|
+
"bundleTypes": "bDRFYa_bundleTypes",
|
|
51
|
+
"eventHeader": "bDRFYa_eventHeader",
|
|
52
|
+
"filters": "bDRFYa_filters",
|
|
53
|
+
"statusBadge": "bDRFYa_statusBadge",
|
|
54
|
+
"actions": "bDRFYa_actions",
|
|
55
|
+
"waitContent": "bDRFYa_waitContent",
|
|
56
|
+
"monitorMain": "bDRFYa_monitorMain",
|
|
57
|
+
"section": "bDRFYa_section",
|
|
58
|
+
"emptyCatalog": "bDRFYa_emptyCatalog",
|
|
59
|
+
"pagination": "bDRFYa_pagination",
|
|
60
|
+
"timerSummary": "bDRFYa_timerSummary",
|
|
61
|
+
"cadenceAction": "bDRFYa_cadenceAction",
|
|
66
62
|
"toolbar": "bDRFYa_toolbar",
|
|
67
|
-
"
|
|
68
|
-
"history": "bDRFYa_history",
|
|
69
|
-
"accounts": "bDRFYa_accounts",
|
|
70
|
-
"failure": "bDRFYa_failure",
|
|
63
|
+
"total": "bDRFYa_total",
|
|
71
64
|
"eventDetails": "bDRFYa_eventDetails",
|
|
72
|
-
"
|
|
65
|
+
"confirmation": "bDRFYa_confirmation",
|
|
66
|
+
"history": "bDRFYa_history",
|
|
67
|
+
"waits": "bDRFYa_waits",
|
|
68
|
+
"events": "bDRFYa_events",
|
|
69
|
+
"secretAction": "bDRFYa_secretAction",
|
|
70
|
+
"overdue": "bDRFYa_overdue",
|
|
71
|
+
"title": "bDRFYa_title",
|
|
72
|
+
"stateDot": "bDRFYa_stateDot"
|
|
73
73
|
};
|
|
74
74
|
//#endregion
|
|
75
75
|
//#region src/client/WaitingEventsSection.tsx
|
package/lib/host-plugin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { Service } from "@deepseek-ai/cordis";
|
|
@@ -443,7 +443,7 @@ function boundRoutingDecision(sessions, binding, providerId) {
|
|
|
443
443
|
}
|
|
444
444
|
//#endregion
|
|
445
445
|
//#region src/runtime/schema.mjs
|
|
446
|
-
const
|
|
446
|
+
const SCHEMA_TABLES_SQL = `
|
|
447
447
|
CREATE TABLE IF NOT EXISTS relay_schema (
|
|
448
448
|
version INTEGER PRIMARY KEY,
|
|
449
449
|
applied_at TEXT NOT NULL
|
|
@@ -489,10 +489,6 @@ const SCHEMA_SQL = `
|
|
|
489
489
|
updated_at TEXT NOT NULL
|
|
490
490
|
) STRICT;
|
|
491
491
|
|
|
492
|
-
CREATE UNIQUE INDEX IF NOT EXISTS activations_one_active_per_session
|
|
493
|
-
ON activations(session_id)
|
|
494
|
-
WHERE state = 'active';
|
|
495
|
-
|
|
496
492
|
CREATE TABLE IF NOT EXISTS runs (
|
|
497
493
|
id TEXT PRIMARY KEY,
|
|
498
494
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
@@ -521,14 +517,6 @@ const SCHEMA_SQL = `
|
|
|
521
517
|
updated_at TEXT NOT NULL
|
|
522
518
|
) STRICT;
|
|
523
519
|
|
|
524
|
-
CREATE UNIQUE INDEX IF NOT EXISTS waits_live_exclusive_owner
|
|
525
|
-
ON waits(exclusive_owner_key)
|
|
526
|
-
WHERE exclusive_owner_key IS NOT NULL
|
|
527
|
-
AND status IN ('active', 'claimed');
|
|
528
|
-
|
|
529
|
-
CREATE INDEX IF NOT EXISTS waits_session_status
|
|
530
|
-
ON waits(session_id, status);
|
|
531
|
-
|
|
532
520
|
CREATE TABLE IF NOT EXISTS events (
|
|
533
521
|
id TEXT PRIMARY KEY,
|
|
534
522
|
source TEXT NOT NULL,
|
|
@@ -544,17 +532,6 @@ const SCHEMA_SQL = `
|
|
|
544
532
|
updated_at TEXT NOT NULL
|
|
545
533
|
) STRICT;
|
|
546
534
|
|
|
547
|
-
CREATE UNIQUE INDEX IF NOT EXISTS events_source_identity
|
|
548
|
-
ON events(source, source_event_id)
|
|
549
|
-
WHERE source_event_id IS NOT NULL;
|
|
550
|
-
|
|
551
|
-
CREATE UNIQUE INDEX IF NOT EXISTS events_source_fingerprint
|
|
552
|
-
ON events(source, fingerprint);
|
|
553
|
-
|
|
554
|
-
CREATE UNIQUE INDEX IF NOT EXISTS events_trusted_correlation
|
|
555
|
-
ON events(correlation_key)
|
|
556
|
-
WHERE correlation_key IS NOT NULL;
|
|
557
|
-
|
|
558
535
|
CREATE TABLE IF NOT EXISTS routing_attempts (
|
|
559
536
|
id TEXT PRIMARY KEY,
|
|
560
537
|
event_id TEXT NOT NULL REFERENCES events(id),
|
|
@@ -599,12 +576,6 @@ const SCHEMA_SQL = `
|
|
|
599
576
|
PRIMARY KEY (delivery_id, wait_id)
|
|
600
577
|
) STRICT;
|
|
601
578
|
|
|
602
|
-
CREATE INDEX IF NOT EXISTS deliveries_session_state
|
|
603
|
-
ON deliveries(session_id, state, created_at);
|
|
604
|
-
|
|
605
|
-
CREATE INDEX IF NOT EXISTS deliveries_event_state
|
|
606
|
-
ON deliveries(event_id, state);
|
|
607
|
-
|
|
608
579
|
CREATE TABLE IF NOT EXISTS monitors (
|
|
609
580
|
id TEXT PRIMARY KEY,
|
|
610
581
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
@@ -677,15 +648,6 @@ const SCHEMA_SQL = `
|
|
|
677
648
|
UNIQUE(monitor_id, trigger_key)
|
|
678
649
|
) STRICT;
|
|
679
650
|
|
|
680
|
-
CREATE INDEX IF NOT EXISTS monitors_due
|
|
681
|
-
ON monitors(state, next_check_at);
|
|
682
|
-
|
|
683
|
-
CREATE INDEX IF NOT EXISTS monitors_session_state
|
|
684
|
-
ON monitors(session_id, state);
|
|
685
|
-
|
|
686
|
-
CREATE INDEX IF NOT EXISTS monitor_checks_monitor_time
|
|
687
|
-
ON monitor_checks(monitor_id, started_at);
|
|
688
|
-
|
|
689
651
|
CREATE TABLE IF NOT EXISTS notification_outcomes (
|
|
690
652
|
event_id TEXT PRIMARY KEY REFERENCES events(id),
|
|
691
653
|
provider TEXT,
|
|
@@ -697,31 +659,119 @@ const SCHEMA_SQL = `
|
|
|
697
659
|
updated_at TEXT NOT NULL
|
|
698
660
|
) STRICT;
|
|
699
661
|
`;
|
|
662
|
+
const SCHEMA_INDEXES_SQL = `
|
|
663
|
+
CREATE UNIQUE INDEX IF NOT EXISTS activations_one_active_per_session
|
|
664
|
+
ON activations(session_id)
|
|
665
|
+
WHERE state = 'active';
|
|
666
|
+
|
|
667
|
+
CREATE UNIQUE INDEX IF NOT EXISTS waits_live_exclusive_owner
|
|
668
|
+
ON waits(exclusive_owner_key)
|
|
669
|
+
WHERE exclusive_owner_key IS NOT NULL
|
|
670
|
+
AND status IN ('active', 'claimed');
|
|
671
|
+
|
|
672
|
+
CREATE INDEX IF NOT EXISTS waits_session_status
|
|
673
|
+
ON waits(session_id, status);
|
|
674
|
+
|
|
675
|
+
CREATE UNIQUE INDEX IF NOT EXISTS events_source_identity
|
|
676
|
+
ON events(source, source_event_id)
|
|
677
|
+
WHERE source_event_id IS NOT NULL;
|
|
678
|
+
|
|
679
|
+
CREATE UNIQUE INDEX IF NOT EXISTS events_source_fingerprint
|
|
680
|
+
ON events(source, fingerprint);
|
|
681
|
+
|
|
682
|
+
CREATE UNIQUE INDEX IF NOT EXISTS events_trusted_correlation
|
|
683
|
+
ON events(correlation_key)
|
|
684
|
+
WHERE correlation_key IS NOT NULL;
|
|
685
|
+
|
|
686
|
+
CREATE INDEX IF NOT EXISTS deliveries_session_state
|
|
687
|
+
ON deliveries(session_id, state, created_at);
|
|
688
|
+
|
|
689
|
+
CREATE INDEX IF NOT EXISTS deliveries_event_state
|
|
690
|
+
ON deliveries(event_id, state);
|
|
691
|
+
|
|
692
|
+
CREATE INDEX IF NOT EXISTS monitors_due
|
|
693
|
+
ON monitors(state, next_check_at);
|
|
694
|
+
|
|
695
|
+
CREATE INDEX IF NOT EXISTS monitors_session_state
|
|
696
|
+
ON monitors(session_id, state);
|
|
697
|
+
|
|
698
|
+
CREATE INDEX IF NOT EXISTS monitor_checks_monitor_time
|
|
699
|
+
ON monitor_checks(monitor_id, started_at);
|
|
700
|
+
`;
|
|
701
|
+
const SCHEMA_SQL = `${SCHEMA_TABLES_SQL}\n${SCHEMA_INDEXES_SQL}`;
|
|
700
702
|
//#endregion
|
|
701
703
|
//#region src/runtime/store.mjs
|
|
702
704
|
var RelayStore = class {
|
|
703
705
|
constructor(path = ":memory:", { clock = () => /* @__PURE__ */ new Date(), idFactory = randomUUID } = {}) {
|
|
704
706
|
this.clock = clock;
|
|
705
707
|
this.idFactory = idFactory;
|
|
708
|
+
this.migrationBackupPath = null;
|
|
706
709
|
this.database = new DatabaseSync(path);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
710
|
+
try {
|
|
711
|
+
this.database.exec("PRAGMA foreign_keys = ON");
|
|
712
|
+
this.database.exec("PRAGMA busy_timeout = 5000");
|
|
713
|
+
this.initializeSchema(path);
|
|
714
|
+
this.database.prepare("INSERT OR IGNORE INTO runtime_counters (name, value) VALUES ('routing_epoch', 0)").run();
|
|
715
|
+
} catch (error) {
|
|
716
|
+
this.database.close();
|
|
717
|
+
const backup = this.migrationBackupPath ? ` Backup: ${this.migrationBackupPath}.` : "";
|
|
718
|
+
throw new Error(`Relay database initialization failed: ${error.message}.${backup}`, { cause: error });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
initializeSchema(path) {
|
|
722
|
+
const tables = this.database.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map((row) => row.name);
|
|
723
|
+
if (tables.length === 0 || tables.length === 1 && tables[0] === "relay_schema" && this.database.prepare("SELECT COUNT(*) AS count FROM relay_schema").get().count === 0) {
|
|
724
|
+
this.database.exec("PRAGMA journal_mode = WAL");
|
|
725
|
+
this.transaction(() => {
|
|
726
|
+
this.database.exec(SCHEMA_SQL);
|
|
717
727
|
this.database.prepare("INSERT INTO relay_schema (version, applied_at) VALUES (?, ?)").run(10, this.now());
|
|
718
|
-
|
|
728
|
+
this.assertCurrentSchema();
|
|
729
|
+
});
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
assert.ok(tables.includes("relay_schema"), "non-empty Relay database has no relay_schema metadata; refusing an unsafe implicit migration");
|
|
733
|
+
const schemaVersion = this.database.prepare("SELECT MAX(version) AS version FROM relay_schema").get().version;
|
|
734
|
+
assert.ok(Number.isSafeInteger(schemaVersion) && schemaVersion > 0, "non-empty Relay database has no valid schema version");
|
|
735
|
+
assert.ok(schemaVersion <= 10, `database schema version ${schemaVersion} is newer than supported version 10`);
|
|
736
|
+
this.database.exec("PRAGMA journal_mode = WAL");
|
|
737
|
+
this.assertDatabaseIntegrity("before schema initialization");
|
|
738
|
+
if (schemaVersion < 10) this.migrationBackupPath = this.createMigrationBackup(path, schemaVersion);
|
|
739
|
+
this.transaction(() => {
|
|
740
|
+
this.database.exec(SCHEMA_TABLES_SQL);
|
|
741
|
+
if (schemaVersion < 10) this.migrateSchema(schemaVersion);
|
|
742
|
+
this.assertRequiredTablesAndColumns();
|
|
743
|
+
this.database.exec(SCHEMA_INDEXES_SQL);
|
|
744
|
+
this.assertCurrentSchema();
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
createMigrationBackup(path, fromVersion) {
|
|
748
|
+
if (path === ":memory:" || typeof path !== "string") return null;
|
|
749
|
+
const backupPath = `${path}.backup-v${fromVersion}-to-v10.sqlite`;
|
|
750
|
+
if (existsSync(backupPath)) {
|
|
751
|
+
assertDatabaseFileIsHealthy(backupPath, "existing migration backup");
|
|
752
|
+
return backupPath;
|
|
753
|
+
}
|
|
754
|
+
const temporaryPath = `${backupPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
755
|
+
try {
|
|
756
|
+
this.database.exec(`VACUUM INTO ${sqlString(temporaryPath)}`);
|
|
757
|
+
assertDatabaseFileIsHealthy(temporaryPath, "new migration backup");
|
|
758
|
+
renameSync(temporaryPath, backupPath);
|
|
759
|
+
return backupPath;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (existsSync(temporaryPath)) rmSync(temporaryPath);
|
|
762
|
+
throw new Error(`could not create verified migration backup ${backupPath}: ${error.message}`, { cause: error });
|
|
719
763
|
}
|
|
720
|
-
this.database.prepare("INSERT OR IGNORE INTO runtime_counters (name, value) VALUES ('routing_epoch', 0)").run();
|
|
721
764
|
}
|
|
722
765
|
migrateSchema(fromVersion) {
|
|
723
|
-
|
|
724
|
-
|
|
766
|
+
for (let targetVersion = fromVersion + 1; targetVersion <= 10; targetVersion += 1) {
|
|
767
|
+
this.applyMigration(targetVersion);
|
|
768
|
+
this.database.prepare("INSERT INTO relay_schema (version, applied_at) VALUES (?, ?)").run(targetVersion, this.now());
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
applyMigration(targetVersion) {
|
|
772
|
+
if (targetVersion === 2) return;
|
|
773
|
+
if (targetVersion === 3) {
|
|
774
|
+
if (!this.database.prepare("PRAGMA table_info(runs)").all().some((column) => column.name === "activation_id")) {
|
|
725
775
|
this.database.exec("ALTER TABLE runs ADD COLUMN activation_id TEXT");
|
|
726
776
|
const runs = this.database.prepare("SELECT * FROM runs ORDER BY started_at, id").all();
|
|
727
777
|
for (const run of runs) {
|
|
@@ -729,17 +779,18 @@ var RelayStore = class {
|
|
|
729
779
|
const state = run.state === "running" ? "active" : "committed";
|
|
730
780
|
const timestamp = run.finished_at ?? run.started_at;
|
|
731
781
|
this.database.prepare(`
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
782
|
+
INSERT INTO activations (
|
|
783
|
+
id, session_id, trigger_type, state, delivery_ids_json,
|
|
784
|
+
provisional_outcome_json, provisional_at, committed_at,
|
|
785
|
+
created_at, updated_at
|
|
786
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
787
|
+
`).run(activationId, run.session_id, run.trigger_type, state, run.delivery_ids_json, run.outcome_json, run.outcome_json == null ? null : timestamp, state === "committed" ? timestamp : null, run.started_at, timestamp);
|
|
738
788
|
this.database.prepare("UPDATE runs SET activation_id = ? WHERE id = ?").run(activationId, run.id);
|
|
739
789
|
}
|
|
740
|
-
}
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
741
792
|
}
|
|
742
|
-
if (
|
|
793
|
+
if (targetVersion === 4) {
|
|
743
794
|
const columns = this.database.prepare("PRAGMA table_info(activations)").all();
|
|
744
795
|
const names = new Set(columns.map((column) => column.name));
|
|
745
796
|
for (const [name, type] of [
|
|
@@ -748,8 +799,8 @@ var RelayStore = class {
|
|
|
748
799
|
["accepted_at", "TEXT"],
|
|
749
800
|
["last_error", "TEXT"]
|
|
750
801
|
]) if (!names.has(name)) this.database.exec(`ALTER TABLE activations ADD COLUMN ${name} ${type}`);
|
|
751
|
-
this.
|
|
752
|
-
|
|
802
|
+
const routingSql = this.database.prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = 'routing_decisions'").get()?.sql ?? "";
|
|
803
|
+
if (/\bspawn\b/u.test(routingSql)) this.database.exec(`
|
|
753
804
|
ALTER TABLE routing_decisions RENAME TO routing_decisions_v3;
|
|
754
805
|
CREATE TABLE routing_decisions (
|
|
755
806
|
id TEXT PRIMARY KEY,
|
|
@@ -771,15 +822,16 @@ var RelayStore = class {
|
|
|
771
822
|
FROM routing_decisions_v3;
|
|
772
823
|
DROP TABLE routing_decisions_v3;
|
|
773
824
|
`);
|
|
774
|
-
|
|
825
|
+
return;
|
|
775
826
|
}
|
|
776
|
-
if (
|
|
827
|
+
if (targetVersion === 5) {
|
|
777
828
|
const columns = this.database.prepare("PRAGMA table_info(delivery_waits)").all();
|
|
778
829
|
const names = new Set(columns.map((column) => column.name));
|
|
779
830
|
if (!names.has("ordinal")) this.database.exec("ALTER TABLE delivery_waits ADD COLUMN ordinal INTEGER NOT NULL DEFAULT 0");
|
|
780
831
|
if (!names.has("wait_snapshot_json")) this.database.exec("ALTER TABLE delivery_waits ADD COLUMN wait_snapshot_json TEXT");
|
|
832
|
+
return;
|
|
781
833
|
}
|
|
782
|
-
if (
|
|
834
|
+
if (targetVersion === 6) {
|
|
783
835
|
const columns = this.database.prepare("PRAGMA table_info(monitors)").all();
|
|
784
836
|
const names = new Set(columns.map((column) => column.name));
|
|
785
837
|
for (const [name, type] of [
|
|
@@ -789,8 +841,10 @@ var RelayStore = class {
|
|
|
789
841
|
["terminal_actor", "TEXT"],
|
|
790
842
|
["terminal_at", "TEXT"]
|
|
791
843
|
]) if (!names.has(name)) this.database.exec(`ALTER TABLE monitors ADD COLUMN ${name} ${type}`);
|
|
844
|
+
return;
|
|
792
845
|
}
|
|
793
|
-
if (
|
|
846
|
+
if (targetVersion === 7) {
|
|
847
|
+
this.database.exec(`
|
|
794
848
|
CREATE TABLE IF NOT EXISTS notification_outcomes (
|
|
795
849
|
event_id TEXT PRIMARY KEY REFERENCES events(id),
|
|
796
850
|
provider TEXT,
|
|
@@ -800,11 +854,13 @@ var RelayStore = class {
|
|
|
800
854
|
updated_at TEXT NOT NULL
|
|
801
855
|
) STRICT;
|
|
802
856
|
`);
|
|
803
|
-
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (targetVersion === 8) {
|
|
804
860
|
if (!this.database.prepare("PRAGMA table_info(events)").all().some((column) => column.name === "correlation_key")) this.database.exec("ALTER TABLE events ADD COLUMN correlation_key TEXT");
|
|
805
|
-
|
|
861
|
+
return;
|
|
806
862
|
}
|
|
807
|
-
if (
|
|
863
|
+
if (targetVersion === 9) {
|
|
808
864
|
const columns = new Set(this.database.prepare("PRAGMA table_info(activations)").all().map((column) => column.name));
|
|
809
865
|
for (const [name, type] of [
|
|
810
866
|
["attempt_count", "INTEGER NOT NULL DEFAULT 0"],
|
|
@@ -812,12 +868,36 @@ var RelayStore = class {
|
|
|
812
868
|
["terminal_reason_code", "TEXT"],
|
|
813
869
|
["terminal_at", "TEXT"]
|
|
814
870
|
]) if (!columns.has(name)) this.database.exec(`ALTER TABLE activations ADD COLUMN ${name} ${type}`);
|
|
871
|
+
return;
|
|
815
872
|
}
|
|
816
|
-
if (
|
|
873
|
+
if (targetVersion === 10) {
|
|
817
874
|
const columns = new Set(this.database.prepare("PRAGMA table_info(notification_outcomes)").all().map((column) => column.name));
|
|
818
875
|
if (!columns.has("receipt_id")) this.database.exec("ALTER TABLE notification_outcomes ADD COLUMN receipt_id TEXT");
|
|
819
876
|
if (!columns.has("attempt_count")) this.database.exec("ALTER TABLE notification_outcomes ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0");
|
|
877
|
+
return;
|
|
820
878
|
}
|
|
879
|
+
throw new Error(`no Relay database migration is defined for schema version ${targetVersion}`);
|
|
880
|
+
}
|
|
881
|
+
assertRequiredTablesAndColumns() {
|
|
882
|
+
const expected = expectedSchemaShape();
|
|
883
|
+
for (const [table, expectedColumns] of expected.tables) {
|
|
884
|
+
const actualColumns = new Set(this.database.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all().map((column) => column.name));
|
|
885
|
+
assert.ok(actualColumns.size > 0, `required table ${table} is missing`);
|
|
886
|
+
for (const column of expectedColumns) assert.ok(actualColumns.has(column), `required column ${table}.${column} is missing`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
assertCurrentSchema() {
|
|
890
|
+
this.assertRequiredTablesAndColumns();
|
|
891
|
+
const expected = expectedSchemaShape();
|
|
892
|
+
const actualIndexes = new Map(this.database.prepare("SELECT name, sql FROM sqlite_schema WHERE type = 'index' AND sql IS NOT NULL").all().map((row) => [row.name, normalizeSql(row.sql)]));
|
|
893
|
+
for (const [name, sql] of expected.indexes) assert.equal(actualIndexes.get(name), sql, `required index ${name} does not match the current schema`);
|
|
894
|
+
this.assertDatabaseIntegrity("after schema initialization");
|
|
895
|
+
const foreignKeyFailures = this.database.prepare("PRAGMA foreign_key_check").all();
|
|
896
|
+
assert.deepEqual(foreignKeyFailures, [], "Relay database contains foreign-key violations");
|
|
897
|
+
}
|
|
898
|
+
assertDatabaseIntegrity(stage) {
|
|
899
|
+
const results = this.database.prepare("PRAGMA quick_check").all();
|
|
900
|
+
assert.deepEqual(results.map((row) => row.quick_check), ["ok"], `Relay database failed integrity validation ${stage}`);
|
|
821
901
|
}
|
|
822
902
|
close() {
|
|
823
903
|
this.database.close();
|
|
@@ -2114,6 +2194,40 @@ var RelayStore = class {
|
|
|
2114
2194
|
this.database.prepare("UPDATE runtime_counters SET value = value + 1 WHERE name = 'routing_epoch'").run();
|
|
2115
2195
|
}
|
|
2116
2196
|
};
|
|
2197
|
+
let cachedExpectedSchemaShape;
|
|
2198
|
+
function expectedSchemaShape() {
|
|
2199
|
+
if (cachedExpectedSchemaShape) return cachedExpectedSchemaShape;
|
|
2200
|
+
const reference = new DatabaseSync(":memory:");
|
|
2201
|
+
try {
|
|
2202
|
+
reference.exec(SCHEMA_SQL);
|
|
2203
|
+
const tableNames = reference.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map((row) => row.name);
|
|
2204
|
+
cachedExpectedSchemaShape = {
|
|
2205
|
+
tables: new Map(tableNames.map((table) => [table, reference.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all().map((column) => column.name)])),
|
|
2206
|
+
indexes: new Map(reference.prepare("SELECT name, sql FROM sqlite_schema WHERE type = 'index' AND sql IS NOT NULL ORDER BY name").all().map((row) => [row.name, normalizeSql(row.sql)]))
|
|
2207
|
+
};
|
|
2208
|
+
return cachedExpectedSchemaShape;
|
|
2209
|
+
} finally {
|
|
2210
|
+
reference.close();
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
function assertDatabaseFileIsHealthy(path, label) {
|
|
2214
|
+
const database = new DatabaseSync(path, { readOnly: true });
|
|
2215
|
+
try {
|
|
2216
|
+
const results = database.prepare("PRAGMA quick_check").all().map((row) => row.quick_check);
|
|
2217
|
+
assert.deepEqual(results, ["ok"], `${label} failed SQLite integrity validation: ${path}`);
|
|
2218
|
+
} finally {
|
|
2219
|
+
database.close();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function normalizeSql(value) {
|
|
2223
|
+
return value.replace(/\s+/gu, " ").trim().replace(/;$/u, "");
|
|
2224
|
+
}
|
|
2225
|
+
function quoteIdentifier(value) {
|
|
2226
|
+
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
2227
|
+
}
|
|
2228
|
+
function sqlString(value) {
|
|
2229
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2230
|
+
}
|
|
2117
2231
|
function hydrateWaitRegistration(row, waits) {
|
|
2118
2232
|
return {
|
|
2119
2233
|
session_id: row.id,
|