@sema-agent/server 7.50.0 → 7.51.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/dist/boot/device-lane.d.ts +79 -0
- package/dist/boot/device-lane.js +63 -0
- package/dist/boot/execution-env.d.ts +48 -2
- package/dist/boot/execution-env.js +57 -5
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +12 -9
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +4 -1
- package/dist/boot/shutdown.d.ts +8 -0
- package/dist/boot/shutdown.js +3 -1
- package/dist/boot/stores.js +9 -0
- package/dist/config-center/types.d.ts +10 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +18 -0
- package/dist/config-types.d.ts +12 -0
- package/dist/config.js +46 -9
- package/dist/device-enrollment.d.ts +125 -0
- package/dist/device-enrollment.js +156 -0
- package/dist/device-store.d.ts +385 -0
- package/dist/device-store.js +407 -0
- package/dist/device-ws-hub.d.ts +182 -0
- package/dist/device-ws-hub.js +1012 -0
- package/dist/device-ws-protocol.d.ts +429 -0
- package/dist/device-ws-protocol.js +464 -0
- package/dist/env-facts.d.ts +4 -1
- package/dist/env-facts.js +1 -0
- package/dist/execution-lane-caps.d.ts +152 -0
- package/dist/execution-lane-caps.js +166 -0
- package/dist/http/routes/capabilities.js +7 -2
- package/dist/http/server.d.ts +14 -0
- package/dist/http/server.js +4 -1
- package/dist/leader/wire.js +4 -2
- package/dist/main.js +10 -3
- package/dist/orchestration/hardened-vm-runner.d.ts +7 -0
- package/dist/orchestration/hardened-vm-runner.js +11 -1
- package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
- package/dist/plugins/device-store-sql.d.ts +130 -0
- package/dist/plugins/device-store-sql.js +574 -0
- package/dist/plugins/remote-env-device.d.ts +271 -0
- package/dist/plugins/remote-env-device.js +727 -0
- package/dist/plugins/remote-scratchpad.js +1 -1
- package/dist/plugins/store-backend.d.ts +9 -0
- package/dist/plugins/store-backend.js +3 -0
- package/dist/task-cwd.d.ts +25 -0
- package/dist/task-cwd.js +3 -0
- package/dist/task-settings.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
2
|
+
import { isDupKeyError } from "./sql-errors.js";
|
|
3
|
+
import { DEVICE_AUDIT_EVENTS, assertAcquireShape, assertDeviceIdShape, assertDeviceOwnerShape, assertEnrollTokenHashShape, assertIssueTokenShape, assertNewDeviceShape, assertRevokeShape, assertRootSessionIdShape, assertWholeSeconds, parseDeviceAuditEvent, parseDevicePlatformOs, parseDeviceStatus, } from "../device-store.js";
|
|
4
|
+
export const DEVICES_TABLE = "device";
|
|
5
|
+
export const DEVICE_ENROLL_TOKENS_TABLE = "device_enroll_token";
|
|
6
|
+
export const DEVICE_SESSIONS_TABLE = "device_session";
|
|
7
|
+
export const DEVICE_AUDIT_TABLE = "device_audit";
|
|
8
|
+
const UQ_DEVICES_PUBKEY = "uq_device_pubkey";
|
|
9
|
+
const AUDIT_EVENT_CHECK = DEVICE_AUDIT_EVENTS.map((e) => `'${e}'`).join(", ");
|
|
10
|
+
export const TIDB_DEVICE_STATEMENTS = [
|
|
11
|
+
`CREATE TABLE IF NOT EXISTS ${DEVICES_TABLE} (
|
|
12
|
+
-- 身份键 = VARBINARY(字节等价);展示/内容列才用 VARCHAR(见文件头注的键列段)。
|
|
13
|
+
device_id VARBINARY(64) NOT NULL,
|
|
14
|
+
-- O11 复合身份结构列(device-lane-local,不是全局 principal —— ../device-store.ts 头注)。
|
|
15
|
+
owner_tenant VARBINARY(190) NOT NULL,
|
|
16
|
+
owner_subject VARBINARY(190) NOT NULL,
|
|
17
|
+
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
|
18
|
+
platform_os VARCHAR(16) NOT NULL,
|
|
19
|
+
platform_arch VARCHAR(32) NOT NULL DEFAULT '',
|
|
20
|
+
-- Ed25519 公钥 b64(44 字符);VARBINARY 因为它参与 UNIQUE 与身份比对。
|
|
21
|
+
pubkey VARBINARY(128) NOT NULL,
|
|
22
|
+
pubkey_rotated_at DATETIME(3) NULL,
|
|
23
|
+
-- 设备工作区根:上界取 task-cwd.ts 的 MAX_CWD_CHARS 家族(4096 过宽、1024 是路径的实用上界)。
|
|
24
|
+
workspace_root VARCHAR(1024) NOT NULL,
|
|
25
|
+
workspace_root_rev BIGINT NOT NULL DEFAULT 0,
|
|
26
|
+
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
|
27
|
+
revoked_at DATETIME(3) NULL,
|
|
28
|
+
revoked_by VARCHAR(190) NULL,
|
|
29
|
+
revoke_reason VARCHAR(512) NULL,
|
|
30
|
+
-- presence 投影(durable-first;内存 Hub 只是缓存,#261 教训:fleet-bus 自 declare NOT durable)。
|
|
31
|
+
conn_epoch VARCHAR(64) NULL,
|
|
32
|
+
replica_id VARCHAR(128) NULL,
|
|
33
|
+
conn_generation BIGINT NOT NULL DEFAULT 0,
|
|
34
|
+
lease_expires_at DATETIME(3) NULL,
|
|
35
|
+
connected_at DATETIME(3) NULL,
|
|
36
|
+
last_seen_at DATETIME(3) NULL,
|
|
37
|
+
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
38
|
+
rev BIGINT NOT NULL DEFAULT 0,
|
|
39
|
+
PRIMARY KEY (device_id),
|
|
40
|
+
KEY idx_device_owner (owner_tenant, owner_subject),
|
|
41
|
+
UNIQUE KEY ${UQ_DEVICES_PUBKEY} (owner_tenant, pubkey)
|
|
42
|
+
) COLLATE utf8mb4_bin`,
|
|
43
|
+
`CREATE TABLE IF NOT EXISTS ${DEVICE_ENROLL_TOKENS_TABLE} (
|
|
44
|
+
-- sha256 hex(64 字符);明文一现,永不落库。
|
|
45
|
+
token_hash VARBINARY(128) NOT NULL,
|
|
46
|
+
target_tenant VARBINARY(190) NOT NULL,
|
|
47
|
+
target_subject VARBINARY(190) NOT NULL,
|
|
48
|
+
created_by VARCHAR(190) NOT NULL,
|
|
49
|
+
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
50
|
+
expires_at DATETIME(3) NOT NULL,
|
|
51
|
+
consumed_at DATETIME(3) NULL,
|
|
52
|
+
consumed_device_id VARBINARY(64) NULL,
|
|
53
|
+
-- AR-R1-2 的判别位:响应丢失重放(同 pubkey)vs 真重用(异 pubkey),全靠这一列。
|
|
54
|
+
consumed_pubkey VARBINARY(128) NULL,
|
|
55
|
+
rev BIGINT NOT NULL DEFAULT 0,
|
|
56
|
+
PRIMARY KEY (token_hash)
|
|
57
|
+
) COLLATE utf8mb4_bin`,
|
|
58
|
+
`CREATE TABLE IF NOT EXISTS ${DEVICE_SESSIONS_TABLE} (
|
|
59
|
+
-- 键 = 根会话(task-R1-3:后代继承不另绑);一根会话恒一设备(改绑 = 显式动作)。
|
|
60
|
+
root_session_id VARBINARY(128) NOT NULL,
|
|
61
|
+
device_id VARBINARY(64) NOT NULL,
|
|
62
|
+
owner_tenant VARBINARY(190) NOT NULL,
|
|
63
|
+
owner_subject VARBINARY(190) NOT NULL,
|
|
64
|
+
bound_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
65
|
+
rev BIGINT NOT NULL DEFAULT 0,
|
|
66
|
+
PRIMARY KEY (root_session_id),
|
|
67
|
+
KEY idx_device_session_device (device_id)
|
|
68
|
+
) COLLATE utf8mb4_bin`,
|
|
69
|
+
`CREATE TABLE IF NOT EXISTS ${DEVICE_AUDIT_TABLE} (
|
|
70
|
+
id BIGINT AUTO_INCREMENT,
|
|
71
|
+
at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
72
|
+
event VARCHAR(64) NOT NULL,
|
|
73
|
+
device_id VARBINARY(64) NULL,
|
|
74
|
+
owner_tenant VARBINARY(190) NULL,
|
|
75
|
+
owner_subject VARBINARY(190) NULL,
|
|
76
|
+
detail JSON NULL,
|
|
77
|
+
PRIMARY KEY (id),
|
|
78
|
+
KEY idx_device_audit_device (device_id, at),
|
|
79
|
+
KEY idx_device_audit_event (event, at),
|
|
80
|
+
-- ⚠️ TiDB 默认不执行 CHECK(见 AUTO_INCREMENT 上方的 AUDIT_EVENT_CHECK 头注);闭集执法在应用层。
|
|
81
|
+
CONSTRAINT ck_device_audit_event CHECK (event IN (${AUDIT_EVENT_CHECK}))
|
|
82
|
+
) COLLATE utf8mb4_bin`,
|
|
83
|
+
];
|
|
84
|
+
export async function ensureTiDBDeviceSchema(pool) {
|
|
85
|
+
for (const stmt of TIDB_DEVICE_STATEMENTS)
|
|
86
|
+
await pool.query(stmt);
|
|
87
|
+
}
|
|
88
|
+
export async function ensurePgDeviceSchema(q) {
|
|
89
|
+
await q(`CREATE TABLE IF NOT EXISTS ${DEVICES_TABLE} (
|
|
90
|
+
device_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
91
|
+
owner_tenant VARCHAR(190) COLLATE "C" NOT NULL,
|
|
92
|
+
owner_subject VARCHAR(190) COLLATE "C" NOT NULL,
|
|
93
|
+
display_name VARCHAR(255) COLLATE "C" NOT NULL DEFAULT '',
|
|
94
|
+
platform_os VARCHAR(16) COLLATE "C" NOT NULL,
|
|
95
|
+
platform_arch VARCHAR(32) COLLATE "C" NOT NULL DEFAULT '',
|
|
96
|
+
pubkey VARCHAR(128) COLLATE "C" NOT NULL,
|
|
97
|
+
pubkey_rotated_at TIMESTAMPTZ(3),
|
|
98
|
+
workspace_root VARCHAR(1024) COLLATE "C" NOT NULL,
|
|
99
|
+
workspace_root_rev BIGINT NOT NULL DEFAULT 0,
|
|
100
|
+
status VARCHAR(16) COLLATE "C" NOT NULL DEFAULT 'active',
|
|
101
|
+
revoked_at TIMESTAMPTZ(3),
|
|
102
|
+
revoked_by VARCHAR(190) COLLATE "C",
|
|
103
|
+
revoke_reason VARCHAR(512) COLLATE "C",
|
|
104
|
+
conn_epoch VARCHAR(64) COLLATE "C",
|
|
105
|
+
replica_id VARCHAR(128) COLLATE "C",
|
|
106
|
+
conn_generation BIGINT NOT NULL DEFAULT 0,
|
|
107
|
+
lease_expires_at TIMESTAMPTZ(3),
|
|
108
|
+
connected_at TIMESTAMPTZ(3),
|
|
109
|
+
last_seen_at TIMESTAMPTZ(3),
|
|
110
|
+
created_at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
|
|
111
|
+
rev BIGINT NOT NULL DEFAULT 0,
|
|
112
|
+
PRIMARY KEY (device_id),
|
|
113
|
+
CONSTRAINT ${UQ_DEVICES_PUBKEY} UNIQUE (owner_tenant, pubkey)
|
|
114
|
+
)`);
|
|
115
|
+
await q(`CREATE INDEX IF NOT EXISTS idx_device_owner ON ${DEVICES_TABLE} (owner_tenant, owner_subject)`);
|
|
116
|
+
await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_ENROLL_TOKENS_TABLE} (
|
|
117
|
+
token_hash VARCHAR(128) COLLATE "C" NOT NULL,
|
|
118
|
+
target_tenant VARCHAR(190) COLLATE "C" NOT NULL,
|
|
119
|
+
target_subject VARCHAR(190) COLLATE "C" NOT NULL,
|
|
120
|
+
created_by VARCHAR(190) COLLATE "C" NOT NULL,
|
|
121
|
+
created_at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
|
|
122
|
+
expires_at TIMESTAMPTZ(3) NOT NULL,
|
|
123
|
+
consumed_at TIMESTAMPTZ(3),
|
|
124
|
+
consumed_device_id VARCHAR(64) COLLATE "C",
|
|
125
|
+
consumed_pubkey VARCHAR(128) COLLATE "C",
|
|
126
|
+
rev BIGINT NOT NULL DEFAULT 0,
|
|
127
|
+
PRIMARY KEY (token_hash)
|
|
128
|
+
)`);
|
|
129
|
+
await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_SESSIONS_TABLE} (
|
|
130
|
+
root_session_id VARCHAR(128) COLLATE "C" NOT NULL,
|
|
131
|
+
device_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
132
|
+
owner_tenant VARCHAR(190) COLLATE "C" NOT NULL,
|
|
133
|
+
owner_subject VARCHAR(190) COLLATE "C" NOT NULL,
|
|
134
|
+
bound_at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
|
|
135
|
+
rev BIGINT NOT NULL DEFAULT 0,
|
|
136
|
+
PRIMARY KEY (root_session_id)
|
|
137
|
+
)`);
|
|
138
|
+
await q(`CREATE INDEX IF NOT EXISTS idx_device_session_device ON ${DEVICE_SESSIONS_TABLE} (device_id)`);
|
|
139
|
+
await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_AUDIT_TABLE} (
|
|
140
|
+
id BIGSERIAL PRIMARY KEY,
|
|
141
|
+
at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
|
|
142
|
+
event VARCHAR(64) COLLATE "C" NOT NULL,
|
|
143
|
+
device_id VARCHAR(64) COLLATE "C",
|
|
144
|
+
owner_tenant VARCHAR(190) COLLATE "C",
|
|
145
|
+
owner_subject VARCHAR(190) COLLATE "C",
|
|
146
|
+
detail JSONB,
|
|
147
|
+
CONSTRAINT ck_device_audit_event CHECK (event IN (${AUDIT_EVENT_CHECK}))
|
|
148
|
+
)`);
|
|
149
|
+
await q(`CREATE INDEX IF NOT EXISTS idx_device_audit_device ON ${DEVICE_AUDIT_TABLE} (device_id, at)`);
|
|
150
|
+
await q(`CREATE INDEX IF NOT EXISTS idx_device_audit_event ON ${DEVICE_AUDIT_TABLE} (event, at)`);
|
|
151
|
+
}
|
|
152
|
+
const ms = (dialect, col, alias) => dialect === "tidb" ? `ROUND(UNIX_TIMESTAMP(${col}) * 1000) AS ${alias}` : `(EXTRACT(EPOCH FROM ${col}) * 1000)::bigint AS ${alias}`;
|
|
153
|
+
const NOW = (dialect) => (dialect === "tidb" ? "NOW(3)" : "now()");
|
|
154
|
+
const nowPlusSeconds = (dialect, placeholder) => dialect === "tidb" ? `NOW(3) + INTERVAL ${placeholder} SECOND` : `now() + (${placeholder}::double precision * INTERVAL '1 second')`;
|
|
155
|
+
function deviceCols(dialect) {
|
|
156
|
+
return [
|
|
157
|
+
"device_id",
|
|
158
|
+
"owner_tenant",
|
|
159
|
+
"owner_subject",
|
|
160
|
+
"display_name",
|
|
161
|
+
"platform_os",
|
|
162
|
+
"platform_arch",
|
|
163
|
+
"pubkey",
|
|
164
|
+
ms(dialect, "pubkey_rotated_at", "pubkey_rotated_at_ms"),
|
|
165
|
+
"workspace_root",
|
|
166
|
+
"workspace_root_rev",
|
|
167
|
+
"status",
|
|
168
|
+
ms(dialect, "revoked_at", "revoked_at_ms"),
|
|
169
|
+
"revoked_by",
|
|
170
|
+
"revoke_reason",
|
|
171
|
+
"conn_epoch",
|
|
172
|
+
"replica_id",
|
|
173
|
+
"conn_generation",
|
|
174
|
+
ms(dialect, "lease_expires_at", "lease_expires_at_ms"),
|
|
175
|
+
ms(dialect, "connected_at", "connected_at_ms"),
|
|
176
|
+
ms(dialect, "last_seen_at", "last_seen_at_ms"),
|
|
177
|
+
ms(dialect, "created_at", "created_at_ms"),
|
|
178
|
+
"rev",
|
|
179
|
+
].join(", ");
|
|
180
|
+
}
|
|
181
|
+
function onlineCol(dialect) {
|
|
182
|
+
return `(CASE WHEN status = 'active' AND lease_expires_at IS NOT NULL AND lease_expires_at > ${NOW(dialect)} THEN 1 ELSE 0 END) AS online`;
|
|
183
|
+
}
|
|
184
|
+
function tokenCols(dialect) {
|
|
185
|
+
return [
|
|
186
|
+
"token_hash",
|
|
187
|
+
"target_tenant",
|
|
188
|
+
"target_subject",
|
|
189
|
+
"created_by",
|
|
190
|
+
ms(dialect, "created_at", "created_at_ms"),
|
|
191
|
+
ms(dialect, "expires_at", "expires_at_ms"),
|
|
192
|
+
ms(dialect, "consumed_at", "consumed_at_ms"),
|
|
193
|
+
"consumed_device_id",
|
|
194
|
+
"consumed_pubkey",
|
|
195
|
+
"rev",
|
|
196
|
+
].join(", ");
|
|
197
|
+
}
|
|
198
|
+
function sessionCols(dialect) {
|
|
199
|
+
return ["root_session_id", "device_id", "owner_tenant", "owner_subject", ms(dialect, "bound_at", "bound_at_ms"), "rev"].join(", ");
|
|
200
|
+
}
|
|
201
|
+
function auditCols(dialect) {
|
|
202
|
+
return ["id", ms(dialect, "at", "at_ms"), "event", "device_id", "owner_tenant", "owner_subject", "detail"].join(", ");
|
|
203
|
+
}
|
|
204
|
+
function bufToStr(v) {
|
|
205
|
+
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
206
|
+
}
|
|
207
|
+
function strOrNull(v) {
|
|
208
|
+
return v === null || v === undefined ? null : bufToStr(v);
|
|
209
|
+
}
|
|
210
|
+
function numOrNull(v) {
|
|
211
|
+
return v === null || v === undefined ? null : Number(v);
|
|
212
|
+
}
|
|
213
|
+
function mapDeviceRow(r) {
|
|
214
|
+
return {
|
|
215
|
+
deviceId: bufToStr(r.device_id),
|
|
216
|
+
owner: { tenant: bufToStr(r.owner_tenant), subject: bufToStr(r.owner_subject) },
|
|
217
|
+
displayName: bufToStr(r.display_name),
|
|
218
|
+
platformOs: parseDevicePlatformOs(bufToStr(r.platform_os)),
|
|
219
|
+
platformArch: bufToStr(r.platform_arch),
|
|
220
|
+
pubkey: bufToStr(r.pubkey),
|
|
221
|
+
pubkeyRotatedAtMs: numOrNull(r.pubkey_rotated_at_ms),
|
|
222
|
+
workspaceRoot: bufToStr(r.workspace_root),
|
|
223
|
+
workspaceRootRev: Number(r.workspace_root_rev),
|
|
224
|
+
status: parseDeviceStatus(bufToStr(r.status)),
|
|
225
|
+
revokedAtMs: numOrNull(r.revoked_at_ms),
|
|
226
|
+
revokedBy: strOrNull(r.revoked_by),
|
|
227
|
+
revokeReason: strOrNull(r.revoke_reason),
|
|
228
|
+
connEpoch: strOrNull(r.conn_epoch),
|
|
229
|
+
replicaId: strOrNull(r.replica_id),
|
|
230
|
+
connGeneration: Number(r.conn_generation),
|
|
231
|
+
leaseExpiresAtMs: numOrNull(r.lease_expires_at_ms),
|
|
232
|
+
connectedAtMs: numOrNull(r.connected_at_ms),
|
|
233
|
+
lastSeenAtMs: numOrNull(r.last_seen_at_ms),
|
|
234
|
+
createdAtMs: Number(r.created_at_ms),
|
|
235
|
+
rev: Number(r.rev),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function mapTokenRow(r) {
|
|
239
|
+
return {
|
|
240
|
+
tokenHash: bufToStr(r.token_hash),
|
|
241
|
+
target: { tenant: bufToStr(r.target_tenant), subject: bufToStr(r.target_subject) },
|
|
242
|
+
createdBy: bufToStr(r.created_by),
|
|
243
|
+
createdAtMs: Number(r.created_at_ms),
|
|
244
|
+
expiresAtMs: Number(r.expires_at_ms),
|
|
245
|
+
consumedAtMs: numOrNull(r.consumed_at_ms),
|
|
246
|
+
consumedDeviceId: strOrNull(r.consumed_device_id),
|
|
247
|
+
consumedPubkey: strOrNull(r.consumed_pubkey),
|
|
248
|
+
rev: Number(r.rev),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function mapSessionRow(r) {
|
|
252
|
+
return {
|
|
253
|
+
rootSessionId: bufToStr(r.root_session_id),
|
|
254
|
+
deviceId: bufToStr(r.device_id),
|
|
255
|
+
owner: { tenant: bufToStr(r.owner_tenant), subject: bufToStr(r.owner_subject) },
|
|
256
|
+
boundAtMs: Number(r.bound_at_ms),
|
|
257
|
+
rev: Number(r.rev),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function parseJsonColumn(v) {
|
|
261
|
+
return JSON.parse(v);
|
|
262
|
+
}
|
|
263
|
+
function mapAuditRow(r) {
|
|
264
|
+
const tenant = strOrNull(r.owner_tenant);
|
|
265
|
+
const subject = strOrNull(r.owner_subject);
|
|
266
|
+
const rawDetail = r.detail;
|
|
267
|
+
return {
|
|
268
|
+
id: Number(r.id),
|
|
269
|
+
atMs: Number(r.at_ms),
|
|
270
|
+
event: parseDeviceAuditEvent(bufToStr(r.event)),
|
|
271
|
+
deviceId: strOrNull(r.device_id),
|
|
272
|
+
owner: tenant !== null && subject !== null ? { tenant, subject } : null,
|
|
273
|
+
detail: rawDetail === null || rawDetail === undefined ? null : typeof rawDetail === "string" ? parseJsonColumn(rawDetail) : rawDetail,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function ph(dialect, n) {
|
|
277
|
+
return dialect === "tidb" ? "?" : `$${n}`;
|
|
278
|
+
}
|
|
279
|
+
function dupKeyName(dialect, err) {
|
|
280
|
+
if (dialect === "tidb") {
|
|
281
|
+
const msg = String(err?.sqlMessage ?? err?.message ?? "");
|
|
282
|
+
if (new RegExp(UQ_DEVICES_PUBKEY, "i").test(msg))
|
|
283
|
+
return "pubkey";
|
|
284
|
+
if (/PRIMARY/i.test(msg))
|
|
285
|
+
return "primary";
|
|
286
|
+
return "other";
|
|
287
|
+
}
|
|
288
|
+
const c = String(err?.constraint ?? "");
|
|
289
|
+
if (c === UQ_DEVICES_PUBKEY)
|
|
290
|
+
return "pubkey";
|
|
291
|
+
if (/pkey$/i.test(c))
|
|
292
|
+
return "primary";
|
|
293
|
+
return "other";
|
|
294
|
+
}
|
|
295
|
+
export class SqlDeviceStore {
|
|
296
|
+
db;
|
|
297
|
+
constructor(db) {
|
|
298
|
+
this.db = db;
|
|
299
|
+
}
|
|
300
|
+
q(tidb, pg) {
|
|
301
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
302
|
+
}
|
|
303
|
+
json(value) {
|
|
304
|
+
return dialectJsonEncoder(this.db.dialect)(value);
|
|
305
|
+
}
|
|
306
|
+
async issueEnrollToken(input) {
|
|
307
|
+
assertIssueTokenShape(input);
|
|
308
|
+
try {
|
|
309
|
+
await this.db.query(this.q(`INSERT INTO ${DEVICE_ENROLL_TOKENS_TABLE} (token_hash, target_tenant, target_subject, created_by, created_at, expires_at, consumed_at, consumed_device_id, consumed_pubkey, rev)
|
|
310
|
+
VALUES (?, ?, ?, ?, NOW(3), ${nowPlusSeconds("tidb", "?")}, NULL, NULL, NULL, 0)`, `INSERT INTO ${DEVICE_ENROLL_TOKENS_TABLE} (token_hash, target_tenant, target_subject, created_by, created_at, expires_at, consumed_at, consumed_device_id, consumed_pubkey, rev)
|
|
311
|
+
VALUES ($1, $2, $3, $4, now(), ${nowPlusSeconds("pg", "$5")}, NULL, NULL, NULL, 0)`), [input.tokenHash, input.target.tenant, input.target.subject, input.createdBy, input.ttlSeconds]);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (!isDupKeyError(this.db.dialect, err))
|
|
315
|
+
throw err;
|
|
316
|
+
throw new Error("device-store: enrollment token hash already exists (a token hash is minted once)");
|
|
317
|
+
}
|
|
318
|
+
const row = await this.getEnrollToken(input.tokenHash);
|
|
319
|
+
if (!row)
|
|
320
|
+
throw new Error("device-store: enrollment token vanished immediately after insert");
|
|
321
|
+
return row;
|
|
322
|
+
}
|
|
323
|
+
async getEnrollToken(tokenHash) {
|
|
324
|
+
assertEnrollTokenHashShape(tokenHash);
|
|
325
|
+
const { rows } = await this.db.query(this.q(`SELECT ${tokenCols("tidb")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = ?`, `SELECT ${tokenCols("pg")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = $1`), [tokenHash]);
|
|
326
|
+
return rows[0] ? mapTokenRow(rows[0]) : null;
|
|
327
|
+
}
|
|
328
|
+
async consumeEnrollToken(input) {
|
|
329
|
+
assertEnrollTokenHashShape(input.tokenHash);
|
|
330
|
+
assertDeviceOwnerShape(input.requester);
|
|
331
|
+
assertNewDeviceShape(input.device);
|
|
332
|
+
if (input.device.owner.tenant !== input.requester.tenant || input.device.owner.subject !== input.requester.subject) {
|
|
333
|
+
return { outcome: "principal_mismatch" };
|
|
334
|
+
}
|
|
335
|
+
const conn = await this.db.connect();
|
|
336
|
+
try {
|
|
337
|
+
await conn.beginPessimistic();
|
|
338
|
+
const cas = await conn.query(this.q(`UPDATE ${DEVICE_ENROLL_TOKENS_TABLE} SET consumed_at = NOW(3), consumed_device_id = ?, consumed_pubkey = ?, rev = rev + 1
|
|
339
|
+
WHERE token_hash = ? AND consumed_at IS NULL AND expires_at > NOW(3) AND target_tenant = ? AND target_subject = ?`, `UPDATE ${DEVICE_ENROLL_TOKENS_TABLE} SET consumed_at = now(), consumed_device_id = $1, consumed_pubkey = $2, rev = rev + 1
|
|
340
|
+
WHERE token_hash = $3 AND consumed_at IS NULL AND expires_at > now() AND target_tenant = $4 AND target_subject = $5`), [input.device.deviceId, input.device.pubkey, input.tokenHash, input.requester.tenant, input.requester.subject]);
|
|
341
|
+
if (cas.affected === 1) {
|
|
342
|
+
try {
|
|
343
|
+
await conn.query(this.q(`INSERT INTO ${DEVICES_TABLE} (device_id, owner_tenant, owner_subject, display_name, platform_os, platform_arch, pubkey, workspace_root, workspace_root_rev, status, conn_generation, created_at, rev)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 'active', 0, NOW(3), 0)`, `INSERT INTO ${DEVICES_TABLE} (device_id, owner_tenant, owner_subject, display_name, platform_os, platform_arch, pubkey, workspace_root, workspace_root_rev, status, conn_generation, created_at, rev)
|
|
345
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0, 'active', 0, now(), 0)`), [
|
|
346
|
+
input.device.deviceId,
|
|
347
|
+
input.device.owner.tenant,
|
|
348
|
+
input.device.owner.subject,
|
|
349
|
+
input.device.displayName ?? "",
|
|
350
|
+
input.device.platformOs,
|
|
351
|
+
input.device.platformArch ?? "",
|
|
352
|
+
input.device.pubkey,
|
|
353
|
+
input.device.workspaceRoot,
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
await conn.rollback();
|
|
358
|
+
if (!isDupKeyError(this.db.dialect, err))
|
|
359
|
+
throw err;
|
|
360
|
+
const which = dupKeyName(this.db.dialect, err);
|
|
361
|
+
if (which !== "pubkey")
|
|
362
|
+
throw err;
|
|
363
|
+
return { outcome: "pubkey_taken" };
|
|
364
|
+
}
|
|
365
|
+
await this.insertAuditOn(conn, { event: "device_enrolled", deviceId: input.device.deviceId, owner: input.device.owner, detail: { platformOs: input.device.platformOs } });
|
|
366
|
+
await conn.commit();
|
|
367
|
+
const device = await this.readDeviceOn(conn, input.device.deviceId);
|
|
368
|
+
if (!device)
|
|
369
|
+
throw new Error("device-store: enrolled device vanished immediately after commit");
|
|
370
|
+
return { outcome: "enrolled", device };
|
|
371
|
+
}
|
|
372
|
+
await conn.rollback();
|
|
373
|
+
const { rows } = await conn.query(this.q(`SELECT ${tokenCols("tidb")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = ?`, `SELECT ${tokenCols("pg")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = $1`), [input.tokenHash]);
|
|
374
|
+
const token = rows[0] ? mapTokenRow(rows[0]) : null;
|
|
375
|
+
if (!token)
|
|
376
|
+
return { outcome: "unknown_token" };
|
|
377
|
+
if (token.target.tenant !== input.requester.tenant || token.target.subject !== input.requester.subject)
|
|
378
|
+
return { outcome: "principal_mismatch" };
|
|
379
|
+
if (token.consumedAtMs !== null) {
|
|
380
|
+
if (token.consumedPubkey !== input.device.pubkey)
|
|
381
|
+
return { outcome: "token_reuse" };
|
|
382
|
+
const device = token.consumedDeviceId ? await this.readDeviceOn(conn, token.consumedDeviceId) : null;
|
|
383
|
+
return { outcome: "replayed", device };
|
|
384
|
+
}
|
|
385
|
+
return { outcome: "expired" };
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
await conn.rollback().catch(() => undefined);
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
conn.release();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async readDeviceOn(conn, deviceId) {
|
|
396
|
+
const { rows } = await conn.query(this.q(`SELECT ${deviceCols("tidb")} FROM ${DEVICES_TABLE} WHERE device_id = ?`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1`), [deviceId]);
|
|
397
|
+
return rows[0] ? mapDeviceRow(rows[0]) : null;
|
|
398
|
+
}
|
|
399
|
+
async readDeviceForAdmission(deviceId, owner) {
|
|
400
|
+
assertDeviceIdShape(deviceId);
|
|
401
|
+
assertDeviceOwnerShape(owner);
|
|
402
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("tidb")}, ${onlineCol("tidb")} FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ?`, `SELECT ${deviceCols("pg")}, ${onlineCol("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3`), [deviceId, owner.tenant, owner.subject]);
|
|
403
|
+
const r = rows[0];
|
|
404
|
+
return r ? { row: mapDeviceRow(r), online: Number(r.online) === 1 } : null;
|
|
405
|
+
}
|
|
406
|
+
async getDeviceUnscoped(deviceId) {
|
|
407
|
+
assertDeviceIdShape(deviceId);
|
|
408
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("tidb")} FROM ${DEVICES_TABLE} WHERE device_id = ?`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1`), [deviceId]);
|
|
409
|
+
return rows[0] ? mapDeviceRow(rows[0]) : null;
|
|
410
|
+
}
|
|
411
|
+
async listDevicesByOwner(owner) {
|
|
412
|
+
assertDeviceOwnerShape(owner);
|
|
413
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("tidb")} FROM ${DEVICES_TABLE} WHERE owner_tenant = ? AND owner_subject = ? ORDER BY device_id ASC`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE owner_tenant = $1 AND owner_subject = $2 ORDER BY device_id ASC`), [owner.tenant, owner.subject]);
|
|
414
|
+
return rows.map(mapDeviceRow);
|
|
415
|
+
}
|
|
416
|
+
async revokeDevice(input) {
|
|
417
|
+
assertRevokeShape(input);
|
|
418
|
+
const conn = await this.db.connect();
|
|
419
|
+
try {
|
|
420
|
+
await conn.beginPessimistic();
|
|
421
|
+
const cas = await conn.query(this.q(`UPDATE ${DEVICES_TABLE} SET status = 'revoked', revoked_at = NOW(3), revoked_by = ?, revoke_reason = ?, lease_expires_at = NULL, conn_generation = conn_generation + 1, rev = rev + 1
|
|
422
|
+
WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? AND status = 'active'`, `UPDATE ${DEVICES_TABLE} SET status = 'revoked', revoked_at = now(), revoked_by = $1, revoke_reason = $2, lease_expires_at = NULL, conn_generation = conn_generation + 1, rev = rev + 1
|
|
423
|
+
WHERE device_id = $3 AND owner_tenant = $4 AND owner_subject = $5 AND status = 'active'`), [input.revokedBy, input.reason ?? null, input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
424
|
+
if (cas.affected === 1) {
|
|
425
|
+
await this.insertAuditOn(conn, { event: "device_revoked", deviceId: input.deviceId, owner: input.owner, detail: { revokedBy: input.revokedBy, reason: input.reason ?? null } });
|
|
426
|
+
await conn.commit();
|
|
427
|
+
const row = await this.readDeviceOn(conn, input.deviceId);
|
|
428
|
+
if (!row)
|
|
429
|
+
throw new Error("device-store: revoked device vanished immediately after commit");
|
|
430
|
+
return { outcome: "revoked", row };
|
|
431
|
+
}
|
|
432
|
+
await conn.rollback();
|
|
433
|
+
const { rows } = await conn.query(this.q(`SELECT ${deviceCols("tidb")} FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ?`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3`), [input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
434
|
+
if (!rows[0])
|
|
435
|
+
return { outcome: "not_found" };
|
|
436
|
+
return { outcome: "already_revoked", row: mapDeviceRow(rows[0]) };
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
await conn.rollback().catch(() => undefined);
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
conn.release();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async acquireConnection(input) {
|
|
447
|
+
assertAcquireShape(input);
|
|
448
|
+
const conn = await this.db.connect();
|
|
449
|
+
try {
|
|
450
|
+
await conn.beginPessimistic();
|
|
451
|
+
const cas = await conn.query(this.q(`UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = ?, replica_id = ?, lease_expires_at = ${nowPlusSeconds("tidb", "?")}, connected_at = NOW(3), last_seen_at = NOW(3), rev = rev + 1
|
|
452
|
+
WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? AND status = 'active' AND (lease_expires_at IS NULL OR lease_expires_at <= NOW(3))`, `UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = $1, replica_id = $2, lease_expires_at = ${nowPlusSeconds("pg", "$3")}, connected_at = now(), last_seen_at = now(), rev = rev + 1
|
|
453
|
+
WHERE device_id = $4 AND owner_tenant = $5 AND owner_subject = $6 AND status = 'active' AND (lease_expires_at IS NULL OR lease_expires_at <= now())`), [input.connEpoch, input.replicaId, input.leaseSeconds, input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
454
|
+
const { rows } = await conn.query(this.q(`SELECT conn_generation, status FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? FOR UPDATE`, `SELECT conn_generation, status FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3 FOR UPDATE`), [input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
455
|
+
await conn.commit();
|
|
456
|
+
if (cas.affected === 1)
|
|
457
|
+
return { outcome: "acquired", generation: Number(rows[0]?.conn_generation) };
|
|
458
|
+
if (!rows[0])
|
|
459
|
+
return { outcome: "device_not_found" };
|
|
460
|
+
if (parseDeviceStatus(bufToStr(rows[0].status)) !== "active")
|
|
461
|
+
return { outcome: "device_revoked" };
|
|
462
|
+
return { outcome: "attached_elsewhere", generation: Number(rows[0].conn_generation) };
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
await conn.rollback().catch(() => undefined);
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
conn.release();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async heartbeatConnection(input) {
|
|
473
|
+
assertDeviceIdShape(input.deviceId);
|
|
474
|
+
assertWholeSeconds("leaseSeconds", input.leaseSeconds);
|
|
475
|
+
const res = await this.db.query(this.q(`UPDATE ${DEVICES_TABLE} SET last_seen_at = NOW(3), lease_expires_at = ${nowPlusSeconds("tidb", "?")}, rev = rev + 1 WHERE device_id = ? AND conn_generation = ? AND status = 'active'`, `UPDATE ${DEVICES_TABLE} SET last_seen_at = now(), lease_expires_at = ${nowPlusSeconds("pg", "$1")}, rev = rev + 1 WHERE device_id = $2 AND conn_generation = $3 AND status = 'active'`), [input.leaseSeconds, input.deviceId, input.generation]);
|
|
476
|
+
return res.affected === 1;
|
|
477
|
+
}
|
|
478
|
+
async releaseConnection(deviceId, generation) {
|
|
479
|
+
assertDeviceIdShape(deviceId);
|
|
480
|
+
const res = await this.db.query(this.q(`UPDATE ${DEVICES_TABLE} SET lease_expires_at = NULL, rev = rev + 1 WHERE device_id = ? AND conn_generation = ?`, `UPDATE ${DEVICES_TABLE} SET lease_expires_at = NULL, rev = rev + 1 WHERE device_id = $1 AND conn_generation = $2`), [deviceId, generation]);
|
|
481
|
+
return res.affected === 1;
|
|
482
|
+
}
|
|
483
|
+
async bindSession(input) {
|
|
484
|
+
assertRootSessionIdShape(input.rootSessionId);
|
|
485
|
+
assertDeviceIdShape(input.deviceId);
|
|
486
|
+
assertDeviceOwnerShape(input.owner);
|
|
487
|
+
const conn = await this.db.connect();
|
|
488
|
+
try {
|
|
489
|
+
await conn.beginPessimistic();
|
|
490
|
+
const dev = await conn.query(this.q(`SELECT status FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? FOR UPDATE`, `SELECT status FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3 FOR UPDATE`), [input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
491
|
+
if (!dev.rows[0]) {
|
|
492
|
+
await conn.rollback();
|
|
493
|
+
return { outcome: "device_not_found" };
|
|
494
|
+
}
|
|
495
|
+
if (parseDeviceStatus(bufToStr(dev.rows[0].status)) !== "active") {
|
|
496
|
+
await conn.rollback();
|
|
497
|
+
return { outcome: "device_revoked" };
|
|
498
|
+
}
|
|
499
|
+
const ins = await conn.query(this.q(`INSERT IGNORE INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES (?, ?, ?, ?, NOW(3), 0)`, `INSERT INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES ($1, $2, $3, $4, now(), 0) ON CONFLICT (root_session_id) DO NOTHING`), [input.rootSessionId, input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
500
|
+
const { rows } = await conn.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ? FOR UPDATE`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1 FOR UPDATE`), [input.rootSessionId]);
|
|
501
|
+
await conn.commit();
|
|
502
|
+
const row = rows[0] ? mapSessionRow(rows[0]) : null;
|
|
503
|
+
if (!row)
|
|
504
|
+
throw new Error("device-store: session binding vanished immediately after insert");
|
|
505
|
+
if (ins.affected === 1)
|
|
506
|
+
return { outcome: "bound", row };
|
|
507
|
+
const same = row.deviceId === input.deviceId && row.owner.tenant === input.owner.tenant && row.owner.subject === input.owner.subject;
|
|
508
|
+
return { outcome: same ? "idempotent" : "conflict", row };
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
await conn.rollback().catch(() => undefined);
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
conn.release();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async getSessionBinding(rootSessionId) {
|
|
519
|
+
assertRootSessionIdShape(rootSessionId);
|
|
520
|
+
const { rows } = await this.db.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ?`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1`), [rootSessionId]);
|
|
521
|
+
return rows[0] ? mapSessionRow(rows[0]) : null;
|
|
522
|
+
}
|
|
523
|
+
async deleteByRootSession(rootSessionId) {
|
|
524
|
+
assertRootSessionIdShape(rootSessionId);
|
|
525
|
+
const res = await this.db.query(this.q(`DELETE FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ?`, `DELETE FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1`), [rootSessionId]);
|
|
526
|
+
return res.affected;
|
|
527
|
+
}
|
|
528
|
+
async appendAudit(input) {
|
|
529
|
+
await this.insertAuditOn(this.db, input);
|
|
530
|
+
}
|
|
531
|
+
async insertAuditOn(exec, input) {
|
|
532
|
+
if (input.deviceId != null)
|
|
533
|
+
assertDeviceIdShape(input.deviceId);
|
|
534
|
+
if (input.owner)
|
|
535
|
+
assertDeviceOwnerShape(input.owner);
|
|
536
|
+
await exec.query(this.q(`INSERT INTO ${DEVICE_AUDIT_TABLE} (at, event, device_id, owner_tenant, owner_subject, detail) VALUES (NOW(3), ?, ?, ?, ?, ?)`, `INSERT INTO ${DEVICE_AUDIT_TABLE} (at, event, device_id, owner_tenant, owner_subject, detail) VALUES (now(), $1, $2, $3, $4, $5::jsonb)`), [input.event, input.deviceId ?? null, input.owner?.tenant ?? null, input.owner?.subject ?? null, input.detail === undefined ? null : this.json(input.detail)]);
|
|
537
|
+
}
|
|
538
|
+
async listAudit(filter) {
|
|
539
|
+
const dialect = this.db.dialect;
|
|
540
|
+
const where = [];
|
|
541
|
+
const params = [];
|
|
542
|
+
if (filter?.deviceId !== undefined) {
|
|
543
|
+
assertDeviceIdShape(filter.deviceId);
|
|
544
|
+
params.push(filter.deviceId);
|
|
545
|
+
where.push(`device_id = ${ph(dialect, params.length)}`);
|
|
546
|
+
}
|
|
547
|
+
if (filter?.event !== undefined) {
|
|
548
|
+
params.push(filter.event);
|
|
549
|
+
where.push(`event = ${ph(dialect, params.length)}`);
|
|
550
|
+
}
|
|
551
|
+
if (filter?.owner !== undefined) {
|
|
552
|
+
assertDeviceOwnerShape(filter.owner);
|
|
553
|
+
params.push(filter.owner.tenant);
|
|
554
|
+
const t = ph(dialect, params.length);
|
|
555
|
+
params.push(filter.owner.subject);
|
|
556
|
+
where.push(`owner_tenant = ${t} AND owner_subject = ${ph(dialect, params.length)}`);
|
|
557
|
+
}
|
|
558
|
+
const limit = Math.max(1, Math.min(1000, filter?.limit ?? 100));
|
|
559
|
+
params.push(limit);
|
|
560
|
+
const { rows } = await this.db.query(`SELECT ${auditCols(dialect)} FROM ${DEVICE_AUDIT_TABLE}${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY at ASC, id ASC LIMIT ${ph(dialect, params.length)}`, params);
|
|
561
|
+
return rows.map(mapAuditRow);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
export class TiDBDeviceStore extends SqlDeviceStore {
|
|
565
|
+
constructor(pool) {
|
|
566
|
+
super(mysqlDriver(pool));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
export class PgDeviceStore extends SqlDeviceStore {
|
|
570
|
+
constructor(pool) {
|
|
571
|
+
super(pgDriver(pool));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
//# sourceMappingURL=device-store-sql.js.map
|