@sema-agent/server 7.50.0 → 7.52.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 (222) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +83 -20
  4. package/dist/adoption/plan.d.ts +4 -4
  5. package/dist/adoption/plan.js +11 -0
  6. package/dist/approval-card.d.ts +26 -2
  7. package/dist/approval-card.js +9 -0
  8. package/dist/approval-hmac.d.ts +1 -1
  9. package/dist/approval-reconciler.d.ts +2 -2
  10. package/dist/approval-reconciler.js +1 -1
  11. package/dist/bench/l8/artifact.d.ts +1 -1
  12. package/dist/bench/s1/arms.d.ts +1 -1
  13. package/dist/bench/s1/live-deps.d.ts +1 -1
  14. package/dist/bench/s1/oracle.d.ts +1 -1
  15. package/dist/bench/s1/row.d.ts +1 -1
  16. package/dist/boot/budget-tracing.d.ts +1 -1
  17. package/dist/boot/budget-tracing.js +2 -5
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
  20. package/dist/boot/device-lane.d.ts +79 -0
  21. package/dist/boot/device-lane.js +63 -0
  22. package/dist/boot/engine-lease.d.ts +1 -1
  23. package/dist/boot/execution-env.d.ts +48 -2
  24. package/dist/boot/execution-env.js +57 -5
  25. package/dist/boot/governance-seams.d.ts +2 -2
  26. package/dist/boot/leader.d.ts +1 -1
  27. package/dist/boot/memory-boundary.d.ts +2 -2
  28. package/dist/boot/memory-consolidation.d.ts +6 -6
  29. package/dist/boot/org-memory.d.ts +1 -1
  30. package/dist/boot/parked-revive-gate.d.ts +3 -3
  31. package/dist/boot/reapers.d.ts +2 -2
  32. package/dist/boot/reapers.js +17 -4
  33. package/dist/boot/resolve-spec.d.ts +15 -0
  34. package/dist/boot/resolve-spec.js +40 -15
  35. package/dist/boot/retention-lane.d.ts +2 -2
  36. package/dist/boot/runner-deps.d.ts +17 -7
  37. package/dist/boot/runner-deps.js +4 -2
  38. package/dist/boot/runtime-caps.d.ts +23 -0
  39. package/dist/boot/runtime-caps.js +24 -2
  40. package/dist/boot/session-faces.d.ts +6 -1
  41. package/dist/boot/session-faces.js +17 -3
  42. package/dist/boot/shutdown.d.ts +8 -0
  43. package/dist/boot/shutdown.js +3 -1
  44. package/dist/boot/stores.js +40 -1
  45. package/dist/boot/task-list-lane.d.ts +2 -2
  46. package/dist/brain.js +51 -31
  47. package/dist/budget.d.ts +2 -2
  48. package/dist/capabilities/center-plugins.d.ts +1 -1
  49. package/dist/capabilities/collab-workflows.d.ts +1 -1
  50. package/dist/capabilities/hands-lane.d.ts +3 -3
  51. package/dist/capabilities/scenarios.d.ts +3 -3
  52. package/dist/capabilities/skills.d.ts +1 -1
  53. package/dist/config-center/hot-keys-registry.js +5 -0
  54. package/dist/config-center/http-client.d.ts +2 -2
  55. package/dist/config-center/read-face.d.ts +1 -1
  56. package/dist/config-center/restart-signal.d.ts +1 -1
  57. package/dist/config-center/types.d.ts +10 -3
  58. package/dist/config-invariants.d.ts +2 -2
  59. package/dist/config-invariants.js +18 -0
  60. package/dist/config-types.d.ts +90 -18
  61. package/dist/config.js +91 -19
  62. package/dist/deployment-governance.d.ts +1 -1
  63. package/dist/device-enrollment.d.ts +125 -0
  64. package/dist/device-enrollment.js +156 -0
  65. package/dist/device-store.d.ts +385 -0
  66. package/dist/device-store.js +407 -0
  67. package/dist/device-ws-hub.d.ts +182 -0
  68. package/dist/device-ws-hub.js +1012 -0
  69. package/dist/device-ws-protocol.d.ts +429 -0
  70. package/dist/device-ws-protocol.js +464 -0
  71. package/dist/env-facts.d.ts +5 -2
  72. package/dist/env-facts.js +1 -0
  73. package/dist/execution-lane-caps.d.ts +152 -0
  74. package/dist/execution-lane-caps.js +166 -0
  75. package/dist/fleet/fleet-bus.d.ts +1 -1
  76. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  77. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  78. package/dist/http/active-run-conflict.d.ts +2 -2
  79. package/dist/http/principal-gate.d.ts +1 -1
  80. package/dist/http/route-ctx.d.ts +24 -4
  81. package/dist/http/routes/admin-memory-optout.d.ts +48 -0
  82. package/dist/http/routes/admin-memory-optout.js +75 -0
  83. package/dist/http/routes/agents-roster.d.ts +1 -1
  84. package/dist/http/routes/approvals-assistant.js +21 -21
  85. package/dist/http/routes/capabilities.js +27 -11
  86. package/dist/http/routes/diagnostics.d.ts +1 -1
  87. package/dist/http/routes/leader.d.ts +1 -1
  88. package/dist/http/routes/memory-bundle.d.ts +1 -1
  89. package/dist/http/routes/memory-bundle.js +3 -1
  90. package/dist/http/routes/memory-compliance.d.ts +4 -4
  91. package/dist/http/routes/memory-origin.d.ts +4 -4
  92. package/dist/http/routes/memory-origin.js +3 -1
  93. package/dist/http/routes/memory-policy.js +6 -2
  94. package/dist/http/routes/retention-ops.d.ts +1 -1
  95. package/dist/http/routes/rules.d.ts +5 -2
  96. package/dist/http/routes/rules.js +84 -5
  97. package/dist/http/routes/run-memory-capture.d.ts +34 -0
  98. package/dist/http/routes/run-memory-capture.js +97 -0
  99. package/dist/http/routes/runs.d.ts +1 -0
  100. package/dist/http/routes/runs.js +212 -6
  101. package/dist/http/routes/session-sync.d.ts +4 -6
  102. package/dist/http/routes/session-sync.js +60 -47
  103. package/dist/http/routes/sessions.js +61 -188
  104. package/dist/http/routes/shared-memory.d.ts +1 -1
  105. package/dist/http/routes/tasks.js +1 -1
  106. package/dist/http/routes/workflows.js +19 -0
  107. package/dist/http/server.d.ts +49 -13
  108. package/dist/http/server.js +109 -13
  109. package/dist/http/verify-rounds.d.ts +1 -1
  110. package/dist/http/wire-types.d.ts +41 -8
  111. package/dist/leader/endpoint.d.ts +1 -1
  112. package/dist/leader/leader.d.ts +1 -1
  113. package/dist/leader/repair-oracle.d.ts +2 -2
  114. package/dist/leader/repair-oracle.js +1 -1
  115. package/dist/leader/wire.js +4 -2
  116. package/dist/main.js +23 -9
  117. package/dist/memory-bundle-engine.d.ts +1 -1
  118. package/dist/memory-operator-faces.d.ts +5 -5
  119. package/dist/memory-origin-fence.d.ts +50 -0
  120. package/dist/memory-origin-fence.js +15 -0
  121. package/dist/memory-posture.d.ts +13 -1
  122. package/dist/memory-posture.js +1 -0
  123. package/dist/memory-scope.d.ts +6 -1
  124. package/dist/memory-scope.js +3 -2
  125. package/dist/model-select.d.ts +2 -2
  126. package/dist/observability/fail-open.d.ts +17 -5
  127. package/dist/observability/fail-open.js +17 -5
  128. package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
  129. package/dist/orchestration/hardened-vm-runner.js +11 -1
  130. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  131. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
  132. package/dist/plugins/background-shell-support.d.ts +2 -2
  133. package/dist/plugins/blob-backend.d.ts +7 -2
  134. package/dist/plugins/blob-backend.js +22 -18
  135. package/dist/plugins/caching-session-store.d.ts +7 -3
  136. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  137. package/dist/plugins/device-store-sql.d.ts +130 -0
  138. package/dist/plugins/device-store-sql.js +574 -0
  139. package/dist/plugins/file-history-store-sql.d.ts +170 -0
  140. package/dist/plugins/file-history-store-sql.js +827 -0
  141. package/dist/plugins/file-run-store.d.ts +44 -5
  142. package/dist/plugins/file-run-store.js +85 -13
  143. package/dist/plugins/fork-routing-session-store.d.ts +1 -1
  144. package/dist/plugins/fork-routing-session-store.js +2 -2
  145. package/dist/plugins/leader-run-store-sql.d.ts +1 -1
  146. package/dist/plugins/local-session-store.d.ts +9 -1
  147. package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
  148. package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
  149. package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
  150. package/dist/plugins/memory-origin-law.d.ts +2 -2
  151. package/dist/plugins/memory-run-store.d.ts +4 -3
  152. package/dist/plugins/memory-run-store.js +2 -1
  153. package/dist/plugins/permission-rule-store-file.d.ts +16 -6
  154. package/dist/plugins/permission-rule-store-file.js +77 -13
  155. package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
  156. package/dist/plugins/permission-rule-store-sql.js +123 -21
  157. package/dist/plugins/pg-pool.js +59 -0
  158. package/dist/plugins/pg-safe-json.d.ts +1 -1
  159. package/dist/plugins/remote-env-device.d.ts +271 -0
  160. package/dist/plugins/remote-env-device.js +727 -0
  161. package/dist/plugins/remote-env-k8s.d.ts +1 -1
  162. package/dist/plugins/remote-scratchpad.js +1 -1
  163. package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
  164. package/dist/plugins/retention-store-sql.d.ts +7 -7
  165. package/dist/plugins/retention-store-sql.js +6 -0
  166. package/dist/plugins/run-store-sql.d.ts +9 -3
  167. package/dist/plugins/run-store-sql.js +2 -2
  168. package/dist/plugins/send-user-file.d.ts +1 -1
  169. package/dist/plugins/session-placement.d.ts +1 -1
  170. package/dist/plugins/sql-errors.d.ts +1 -1
  171. package/dist/plugins/store-backend.d.ts +68 -29
  172. package/dist/plugins/store-backend.js +78 -15
  173. package/dist/plugins/store-contracts.d.ts +5 -3
  174. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  175. package/dist/plugins/tidb-pool.js +59 -0
  176. package/dist/plugins/web-search.d.ts +1 -1
  177. package/dist/prompts-domain-validate.js +1 -1
  178. package/dist/resource-suspend.d.ts +1 -1
  179. package/dist/resource-window.d.ts +3 -3
  180. package/dist/rules-consent.d.ts +231 -9
  181. package/dist/rules-consent.js +411 -13
  182. package/dist/run-local.js +1 -0
  183. package/dist/runs.d.ts +15 -1
  184. package/dist/runs.js +19 -0
  185. package/dist/runtime-caps-resolver.d.ts +36 -1
  186. package/dist/runtime-caps-resolver.js +25 -0
  187. package/dist/runtime-governance.d.ts +1 -1
  188. package/dist/security.d.ts +5 -5
  189. package/dist/security.js +1 -1
  190. package/dist/session-sync.d.ts +52 -37
  191. package/dist/session-sync.js +19 -16
  192. package/dist/shared-memory-scope-authorizer.d.ts +1 -1
  193. package/dist/sighup-idle.d.ts +1 -1
  194. package/dist/spec-fields.d.ts +22 -1
  195. package/dist/spec-fields.js +11 -0
  196. package/dist/store-live-probe.d.ts +2 -2
  197. package/dist/task-a2a.d.ts +1 -1
  198. package/dist/task-cwd.d.ts +25 -0
  199. package/dist/task-cwd.js +3 -0
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/task-settings.d.ts +3 -3
  202. package/dist/task-settings.js +3 -1
  203. package/dist/task-workflow.d.ts +1 -1
  204. package/dist/text-bidi.d.ts +2 -2
  205. package/dist/tool-approval.d.ts +191 -9
  206. package/dist/tool-approval.js +248 -29
  207. package/dist/trace/core-keyset-guard.d.ts +1 -1
  208. package/dist/trace/engine-notice-wire.d.ts +6 -4
  209. package/dist/trace/engine-notice-wire.js +2 -0
  210. package/dist/trace/ledger-events.d.ts +1 -1
  211. package/dist/trace/ledger-sink.d.ts +2 -2
  212. package/dist/trace/project.d.ts +3 -2
  213. package/dist/trace/project.js +1 -0
  214. package/dist/turn-activity.d.ts +1 -1
  215. package/dist/wall-clock-jump-guard.d.ts +3 -3
  216. package/package.json +2 -2
  217. package/dist/http/tar.d.ts +0 -17
  218. package/dist/http/tar.js +0 -53
  219. package/dist/http/workspace-content.d.ts +0 -11
  220. package/dist/http/workspace-content.js +0 -20
  221. package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
  222. package/dist/plugins/file-snapshot-store-sql.js +0 -213
@@ -0,0 +1,407 @@
1
+ import { createHash, randomBytes, randomInt } from "node:crypto";
2
+ export const DEVICE_ERROR_CODES = {
3
+ NOT_BOUND: "device.not_bound",
4
+ OFFLINE: "device.offline",
5
+ IDENTITY_MISMATCH: "device.identity_mismatch",
6
+ REVOKED: "device.revoked",
7
+ PROVIDER_MISMATCH: "device.provider_mismatch",
8
+ ATTACHED_ELSEWHERE: "device.attached_elsewhere",
9
+ CLAIM_CONFLICT: "device.claim_conflict",
10
+ BUSY: "device.busy",
11
+ ENROLLMENT_INVALID: "device.enrollment_invalid",
12
+ NOT_FOUND: "not_found.device",
13
+ };
14
+ export const DEVICE_RATE_LIMIT_CODE = "limit.rate_exceeded";
15
+ export const DEVICE_ERROR_STATUS = {
16
+ "device.not_bound": 409,
17
+ "device.offline": 409,
18
+ "device.identity_mismatch": 403,
19
+ "device.revoked": 403,
20
+ "device.provider_mismatch": 409,
21
+ "device.attached_elsewhere": 409,
22
+ "device.claim_conflict": 409,
23
+ "device.busy": 429,
24
+ "device.enrollment_invalid": 403,
25
+ "not_found.device": 404,
26
+ "limit.rate_exceeded": 429,
27
+ };
28
+ export const DEVICE_AUDIT_EVENTS = [
29
+ "device_enrolled",
30
+ "device_revoked",
31
+ "device_token_rotated",
32
+ "device_enroll_token_reuse",
33
+ "device_frame_unexpected",
34
+ "device_identity_mismatch",
35
+ "device_superseded",
36
+ "device_attached_elsewhere",
37
+ ];
38
+ export const DEVICE_PLATFORM_OS = ["darwin", "linux"];
39
+ export const DEVICE_STATUSES = ["active", "revoked"];
40
+ export const DEVICE_KEY_MAX_BYTES = 190;
41
+ export const DEVICE_ID_MAX_BYTES = 64;
42
+ export const DEVICE_SESSION_ID_MAX_BYTES = 128;
43
+ export const DEVICE_PUBKEY_MAX_BYTES = 128;
44
+ function assertIdentityKey(what, v, maxBytes) {
45
+ if (v.length === 0)
46
+ throw new Error(`device-store: ${what} must not be empty (identity keys are byte-exact addressable)`);
47
+ for (let i = 0; i < v.length; i++) {
48
+ const c = v.charCodeAt(i);
49
+ if (c >= 0xd800 && c <= 0xdbff) {
50
+ const next = v.charCodeAt(i + 1);
51
+ if (!(next >= 0xdc00 && next <= 0xdfff))
52
+ throw new Error(`device-store: ${what} is not well-formed Unicode (lone high surrogate at ${i}) — it would be silently replaced with U+FFFD on the wire and alias a different key`);
53
+ i++;
54
+ }
55
+ else if (c >= 0xdc00 && c <= 0xdfff) {
56
+ throw new Error(`device-store: ${what} is not well-formed Unicode (lone low surrogate at ${i}) — it would be silently replaced with U+FFFD on the wire and alias a different key`);
57
+ }
58
+ }
59
+ if (v.trim() !== v)
60
+ throw new Error(`device-store: ${what} has leading/trailing whitespace (MySQL PAD SPACE would make it collide with the trimmed key)`);
61
+ if ([...v].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) {
62
+ throw new Error(`device-store: ${what} contains control characters (refusing — identity keys must be byte-exact addressable)`);
63
+ }
64
+ if (Buffer.byteLength(v, "utf8") > maxBytes) {
65
+ throw new Error(`device-store: ${what} exceeds the ${maxBytes}-byte key column (refusing — a truncated key would desync from its byte-exact lookups)`);
66
+ }
67
+ }
68
+ function assertBoundedText(what, v, maxBytes) {
69
+ if (Buffer.byteLength(v, "utf8") > maxBytes) {
70
+ throw new Error(`device-store: ${what} exceeds the ${maxBytes}-byte column (refusing — a silently truncated value is a different value)`);
71
+ }
72
+ }
73
+ export const DEVICE_TEXT_LIMITS = {
74
+ displayName: 255,
75
+ platformArch: 32,
76
+ workspaceRoot: 1024,
77
+ actor: 190,
78
+ revokeReason: 512,
79
+ connEpoch: 64,
80
+ replicaId: 128,
81
+ };
82
+ export function assertDeviceOwnerShape(owner) {
83
+ assertIdentityKey("owner.tenant", owner.tenant, DEVICE_KEY_MAX_BYTES);
84
+ assertIdentityKey("owner.subject", owner.subject, DEVICE_KEY_MAX_BYTES);
85
+ }
86
+ export function assertDeviceIdShape(deviceId) {
87
+ assertIdentityKey("deviceId", deviceId, DEVICE_ID_MAX_BYTES);
88
+ }
89
+ export function assertRootSessionIdShape(rootSessionId) {
90
+ assertIdentityKey("rootSessionId", rootSessionId, DEVICE_SESSION_ID_MAX_BYTES);
91
+ }
92
+ export function assertDevicePubkeyShape(pubkey) {
93
+ assertIdentityKey("pubkey", pubkey, DEVICE_PUBKEY_MAX_BYTES);
94
+ }
95
+ export function assertNewDeviceShape(input) {
96
+ assertDeviceIdShape(input.deviceId);
97
+ assertDeviceOwnerShape(input.owner);
98
+ assertDevicePubkeyShape(input.pubkey);
99
+ assertBoundedText("displayName", input.displayName ?? "", DEVICE_TEXT_LIMITS.displayName);
100
+ assertBoundedText("platformArch", input.platformArch ?? "", DEVICE_TEXT_LIMITS.platformArch);
101
+ assertBoundedText("workspaceRoot", input.workspaceRoot, DEVICE_TEXT_LIMITS.workspaceRoot);
102
+ }
103
+ export function assertIssueTokenShape(input) {
104
+ assertEnrollTokenHashShape(input.tokenHash);
105
+ assertDeviceOwnerShape(input.target);
106
+ assertWholeSeconds("ttlSeconds", input.ttlSeconds);
107
+ assertBoundedText("createdBy", input.createdBy, DEVICE_TEXT_LIMITS.actor);
108
+ }
109
+ export function assertRevokeShape(input) {
110
+ assertDeviceIdShape(input.deviceId);
111
+ assertDeviceOwnerShape(input.owner);
112
+ assertBoundedText("revokedBy", input.revokedBy, DEVICE_TEXT_LIMITS.actor);
113
+ assertBoundedText("revokeReason", input.reason ?? "", DEVICE_TEXT_LIMITS.revokeReason);
114
+ }
115
+ export function assertAcquireShape(input) {
116
+ assertDeviceIdShape(input.deviceId);
117
+ assertDeviceOwnerShape(input.owner);
118
+ assertWholeSeconds("leaseSeconds", input.leaseSeconds);
119
+ assertBoundedText("connEpoch", input.connEpoch, DEVICE_TEXT_LIMITS.connEpoch);
120
+ assertBoundedText("replicaId", input.replicaId, DEVICE_TEXT_LIMITS.replicaId);
121
+ }
122
+ export function assertEnrollTokenHashShape(tokenHash) {
123
+ if (!/^[0-9a-f]{64}$/.test(tokenHash)) {
124
+ throw new Error(`device-store: tokenHash must be a lowercase sha256 hex digest (64 chars) — a non-canonical form would be a second key for the same token`);
125
+ }
126
+ }
127
+ export function parseDeviceStatus(v) {
128
+ const hit = DEVICE_STATUSES.find((s) => s === v);
129
+ if (!hit)
130
+ throw new Error(`device-store: unknown device status ${JSON.stringify(v)} (closed set: ${DEVICE_STATUSES.join("|")})`);
131
+ return hit;
132
+ }
133
+ export function parseDevicePlatformOs(v) {
134
+ const hit = DEVICE_PLATFORM_OS.find((s) => s === v);
135
+ if (!hit)
136
+ throw new Error(`device-store: unknown platform os ${JSON.stringify(v)} (closed set: ${DEVICE_PLATFORM_OS.join("|")})`);
137
+ return hit;
138
+ }
139
+ export function parseDeviceAuditEvent(v) {
140
+ const hit = DEVICE_AUDIT_EVENTS.find((s) => s === v);
141
+ if (!hit)
142
+ throw new Error(`device-store: unknown device audit event ${JSON.stringify(v)} (closed set: ${DEVICE_AUDIT_EVENTS.join("|")})`);
143
+ return hit;
144
+ }
145
+ export function assertWholeSeconds(what, seconds) {
146
+ if (!Number.isInteger(seconds))
147
+ throw new Error(`device-store: ${what} must be a whole number of seconds (got ${seconds}) — fractional INTERVAL semantics diverge between MySQL and PostgreSQL`);
148
+ if (!Number.isFinite(seconds) || Math.abs(seconds) > 86_400 * 365)
149
+ throw new Error(`device-store: ${what} out of range (got ${seconds})`);
150
+ }
151
+ export function hashEnrollToken(plaintext) {
152
+ return createHash("sha256").update(plaintext, "utf8").digest("hex");
153
+ }
154
+ export function mintEnrollToken() {
155
+ return randomBytes(16).toString("base64url");
156
+ }
157
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
158
+ export function mintDeviceId(nowMs = Date.now()) {
159
+ let ts = Math.max(0, Math.floor(nowMs));
160
+ const time = [];
161
+ for (let i = 0; i < 10; i++) {
162
+ time.unshift(CROCKFORD[ts % 32]);
163
+ ts = Math.floor(ts / 32);
164
+ }
165
+ let rand = "";
166
+ for (let i = 0; i < 16; i++)
167
+ rand += CROCKFORD[randomInt(32)];
168
+ return `dev_${time.join("")}${rand}`;
169
+ }
170
+ function sameOwner(a, b) {
171
+ return a.tenant === b.tenant && a.subject === b.subject;
172
+ }
173
+ function cloneOwner(o) {
174
+ return { tenant: o.tenant, subject: o.subject };
175
+ }
176
+ export class InMemoryDeviceStore {
177
+ devices = new Map();
178
+ sessions = new Map();
179
+ tokens = new Map();
180
+ audit = [];
181
+ auditSeq = 0;
182
+ now;
183
+ constructor(opts) {
184
+ this.now = opts?.now ?? (() => Date.now());
185
+ }
186
+ async issueEnrollToken(input) {
187
+ assertIssueTokenShape(input);
188
+ const now = this.now();
189
+ const row = {
190
+ tokenHash: input.tokenHash,
191
+ target: cloneOwner(input.target),
192
+ createdBy: input.createdBy,
193
+ createdAtMs: now,
194
+ expiresAtMs: now + Math.round(input.ttlSeconds * 1000),
195
+ consumedAtMs: null,
196
+ consumedDeviceId: null,
197
+ consumedPubkey: null,
198
+ rev: 0,
199
+ };
200
+ if (this.tokens.has(row.tokenHash))
201
+ throw new Error("device-store: enrollment token hash already exists (a token hash is minted once)");
202
+ this.tokens.set(row.tokenHash, row);
203
+ return { ...row, target: cloneOwner(row.target) };
204
+ }
205
+ async consumeEnrollToken(input) {
206
+ assertEnrollTokenHashShape(input.tokenHash);
207
+ assertDeviceOwnerShape(input.requester);
208
+ assertNewDeviceShape(input.device);
209
+ const token = this.tokens.get(input.tokenHash);
210
+ if (!token)
211
+ return { outcome: "unknown_token" };
212
+ if (!sameOwner(token.target, input.requester))
213
+ return { outcome: "principal_mismatch" };
214
+ if (!sameOwner(input.device.owner, input.requester))
215
+ return { outcome: "principal_mismatch" };
216
+ const now = this.now();
217
+ if (token.consumedAtMs !== null) {
218
+ if (token.consumedPubkey !== input.device.pubkey)
219
+ return { outcome: "token_reuse" };
220
+ const existing = token.consumedDeviceId ? this.devices.get(token.consumedDeviceId) : undefined;
221
+ return { outcome: "replayed", device: existing ? this.snapshotDevice(existing) : null };
222
+ }
223
+ if (token.expiresAtMs <= now)
224
+ return { outcome: "expired" };
225
+ for (const d of this.devices.values()) {
226
+ if (d.owner.tenant === input.device.owner.tenant && d.pubkey === input.device.pubkey)
227
+ return { outcome: "pubkey_taken" };
228
+ }
229
+ if (this.devices.has(input.device.deviceId)) {
230
+ throw new Error(`device-store: minted deviceId ${JSON.stringify(input.device.deviceId)} collided with an existing row (server-minted ids must be unique)`);
231
+ }
232
+ const device = {
233
+ deviceId: input.device.deviceId,
234
+ owner: cloneOwner(input.device.owner),
235
+ displayName: input.device.displayName ?? "",
236
+ platformOs: input.device.platformOs,
237
+ platformArch: input.device.platformArch ?? "",
238
+ pubkey: input.device.pubkey,
239
+ pubkeyRotatedAtMs: null,
240
+ workspaceRoot: input.device.workspaceRoot,
241
+ workspaceRootRev: 0,
242
+ status: "active",
243
+ revokedAtMs: null,
244
+ revokedBy: null,
245
+ revokeReason: null,
246
+ connEpoch: null,
247
+ replicaId: null,
248
+ connGeneration: 0,
249
+ leaseExpiresAtMs: null,
250
+ connectedAtMs: null,
251
+ lastSeenAtMs: null,
252
+ createdAtMs: now,
253
+ rev: 0,
254
+ };
255
+ token.consumedAtMs = now;
256
+ token.consumedDeviceId = device.deviceId;
257
+ token.consumedPubkey = device.pubkey;
258
+ token.rev += 1;
259
+ this.devices.set(device.deviceId, device);
260
+ this.pushAudit({ event: "device_enrolled", deviceId: device.deviceId, owner: device.owner, detail: { platformOs: device.platformOs } });
261
+ return { outcome: "enrolled", device: this.snapshotDevice(device) };
262
+ }
263
+ async getEnrollToken(tokenHash) {
264
+ assertEnrollTokenHashShape(tokenHash);
265
+ const row = this.tokens.get(tokenHash);
266
+ return row ? { ...row, target: cloneOwner(row.target) } : null;
267
+ }
268
+ async readDeviceForAdmission(deviceId, owner) {
269
+ assertDeviceIdShape(deviceId);
270
+ assertDeviceOwnerShape(owner);
271
+ const row = this.devices.get(deviceId);
272
+ if (!row || !sameOwner(row.owner, owner))
273
+ return null;
274
+ return { row: this.snapshotDevice(row), online: this.isOnline(row) };
275
+ }
276
+ async getDeviceUnscoped(deviceId) {
277
+ assertDeviceIdShape(deviceId);
278
+ const row = this.devices.get(deviceId);
279
+ return row ? this.snapshotDevice(row) : null;
280
+ }
281
+ async listDevicesByOwner(owner) {
282
+ assertDeviceOwnerShape(owner);
283
+ return [...this.devices.values()].filter((d) => sameOwner(d.owner, owner)).sort((a, b) => (a.deviceId < b.deviceId ? -1 : 1)).map((d) => this.snapshotDevice(d));
284
+ }
285
+ async revokeDevice(input) {
286
+ assertRevokeShape(input);
287
+ const row = this.devices.get(input.deviceId);
288
+ if (!row || !sameOwner(row.owner, input.owner))
289
+ return { outcome: "not_found" };
290
+ if (row.status === "revoked")
291
+ return { outcome: "already_revoked", row: this.snapshotDevice(row) };
292
+ row.status = "revoked";
293
+ row.revokedAtMs = this.now();
294
+ row.revokedBy = input.revokedBy;
295
+ row.revokeReason = input.reason ?? null;
296
+ row.leaseExpiresAtMs = null;
297
+ row.connGeneration += 1;
298
+ row.rev += 1;
299
+ this.pushAudit({ event: "device_revoked", deviceId: row.deviceId, owner: row.owner, detail: { revokedBy: input.revokedBy, reason: input.reason ?? null } });
300
+ return { outcome: "revoked", row: this.snapshotDevice(row) };
301
+ }
302
+ async acquireConnection(input) {
303
+ assertAcquireShape(input);
304
+ const row = this.devices.get(input.deviceId);
305
+ if (!row || !sameOwner(row.owner, input.owner))
306
+ return { outcome: "device_not_found" };
307
+ if (row.status !== "active")
308
+ return { outcome: "device_revoked" };
309
+ const now = this.now();
310
+ if (row.leaseExpiresAtMs !== null && row.leaseExpiresAtMs > now)
311
+ return { outcome: "attached_elsewhere", generation: row.connGeneration };
312
+ row.connGeneration += 1;
313
+ row.connEpoch = input.connEpoch;
314
+ row.replicaId = input.replicaId;
315
+ row.leaseExpiresAtMs = now + Math.round(input.leaseSeconds * 1000);
316
+ row.connectedAtMs = now;
317
+ row.lastSeenAtMs = now;
318
+ row.rev += 1;
319
+ return { outcome: "acquired", generation: row.connGeneration };
320
+ }
321
+ async heartbeatConnection(input) {
322
+ assertDeviceIdShape(input.deviceId);
323
+ assertWholeSeconds("leaseSeconds", input.leaseSeconds);
324
+ const row = this.devices.get(input.deviceId);
325
+ if (!row || row.connGeneration !== input.generation || row.status !== "active")
326
+ return false;
327
+ const now = this.now();
328
+ row.lastSeenAtMs = now;
329
+ row.leaseExpiresAtMs = now + Math.round(input.leaseSeconds * 1000);
330
+ row.rev += 1;
331
+ return true;
332
+ }
333
+ async releaseConnection(deviceId, generation) {
334
+ assertDeviceIdShape(deviceId);
335
+ const row = this.devices.get(deviceId);
336
+ if (!row || row.connGeneration !== generation)
337
+ return false;
338
+ row.leaseExpiresAtMs = null;
339
+ row.rev += 1;
340
+ return true;
341
+ }
342
+ async bindSession(input) {
343
+ assertRootSessionIdShape(input.rootSessionId);
344
+ assertDeviceIdShape(input.deviceId);
345
+ assertDeviceOwnerShape(input.owner);
346
+ const device = this.devices.get(input.deviceId);
347
+ if (!device || !sameOwner(device.owner, input.owner))
348
+ return { outcome: "device_not_found" };
349
+ if (device.status !== "active")
350
+ return { outcome: "device_revoked" };
351
+ const existing = this.sessions.get(input.rootSessionId);
352
+ if (existing) {
353
+ const same = existing.deviceId === input.deviceId && sameOwner(existing.owner, input.owner);
354
+ return { outcome: same ? "idempotent" : "conflict", row: this.snapshotSession(existing) };
355
+ }
356
+ const row = {
357
+ rootSessionId: input.rootSessionId,
358
+ deviceId: input.deviceId,
359
+ owner: cloneOwner(input.owner),
360
+ boundAtMs: this.now(),
361
+ rev: 0,
362
+ };
363
+ this.sessions.set(row.rootSessionId, row);
364
+ return { outcome: "bound", row: this.snapshotSession(row) };
365
+ }
366
+ async getSessionBinding(rootSessionId) {
367
+ assertRootSessionIdShape(rootSessionId);
368
+ const row = this.sessions.get(rootSessionId);
369
+ return row ? this.snapshotSession(row) : null;
370
+ }
371
+ async deleteByRootSession(rootSessionId) {
372
+ assertRootSessionIdShape(rootSessionId);
373
+ return this.sessions.delete(rootSessionId) ? 1 : 0;
374
+ }
375
+ async appendAudit(input) {
376
+ this.pushAudit(input);
377
+ }
378
+ async listAudit(filter) {
379
+ const limit = filter?.limit ?? 100;
380
+ return this.audit
381
+ .filter((r) => (filter?.deviceId === undefined || r.deviceId === filter.deviceId) && (filter?.event === undefined || r.event === filter.event) && (filter?.owner === undefined || (r.owner !== null && sameOwner(r.owner, filter.owner))))
382
+ .slice(0, limit)
383
+ .map((r) => ({ ...r, owner: r.owner ? cloneOwner(r.owner) : null }));
384
+ }
385
+ isOnline(row) {
386
+ return row.status === "active" && row.leaseExpiresAtMs !== null && row.leaseExpiresAtMs > this.now();
387
+ }
388
+ pushAudit(input) {
389
+ this.auditSeq += 1;
390
+ this.audit.push({
391
+ id: this.auditSeq,
392
+ atMs: this.now(),
393
+ event: input.event,
394
+ deviceId: input.deviceId ?? null,
395
+ owner: input.owner ? cloneOwner(input.owner) : null,
396
+ detail: input.detail === undefined ? null : structuredClone(input.detail),
397
+ });
398
+ }
399
+ snapshotDevice(row) {
400
+ return { ...row, owner: cloneOwner(row.owner) };
401
+ }
402
+ snapshotSession(row) {
403
+ return { ...row, owner: cloneOwner(row.owner) };
404
+ }
405
+ }
406
+ export { sameOwner as sameDeviceOwner, cloneOwner as cloneDeviceOwner };
407
+ //# sourceMappingURL=device-store.js.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * device lane **WS 汇聚端**(design/device-executor-lane-v2 §5.2 连接状态机 / §5.3 双层超时 /
3
+ * §5.4 重连与幂等 / §5.5 背压 / §8-R7 发版排空)—— 本仓**首个** WS 服务端。
4
+ *
5
+ * ── 三条它必须同时守住的不变量 ───────────────────────────────────────────────────────────────────
6
+ * ① **at-most-once**:一条指令至多在设备上执行一次。`dispatch_committed` 是**不可回退的单向门**,
7
+ * 且在**首次 socket 写之前**原子落定 —— `ws.send()` 成功只证明字节进了本机缓冲。commit 之后的
8
+ * 一切不确定恒收敛到 `outcome_unknown`,**永不重发**(§5.3/§5.4)。
9
+ * ② **世代闸**:设备侧**所有**可变状态帧携 `gen`,判据 = **严格等于**本 socket 协商世代(非 ≥)。
10
+ * 旧世代的 result/chunk/heartbeat/goodbye 在进任何缓冲之前即被拒 + 审计(D-P4 四格)。
11
+ * ③ **结果帧四不变量**(§5.1):已鉴权且 deviceId 与 pending 行一致 / instructionId 在本进程 pending
12
+ * 表且态 = `dispatch_committed` / `deviceSeq` 单调(重复 = 幂等吸收) / `streamSeq` 连续
13
+ * (乱序缓冲,缺片 = 指令失败**而不是**静默截断)。
14
+ *
15
+ * ── A-2 → A-3 的移交义务(工作稿「着陆实况」三条,逐条兑现在哪儿)────────────────────────────────
16
+ * ① **下发边界必须重新断言世代**(codex F4):`admitSession` 给的 `connGeneration` 只是一个**快照**,
17
+ * 它与真正写 socket 之间可以插进一次 `revokeDevice`。所以 {@link createDeviceWsHub} 的 dispatch 在
18
+ * 写 socket 之前做三重断言:socket 世代 === 判决世代、本 conn 仍是该设备的当前连接、以及一次
19
+ * **带世代谓词的库写**(`heartbeatConnection`)—— 影响 0 行 = 本连接已失权 ⇒ 停发 + `bye` + 断连。
20
+ * 🔴 **诚实残余**:这三条关不掉「断言通过之后、`ws.send` 之前」的那一瞬。窗口上界 = 租约续期拍距
21
+ * (`leaseRenewIntervalMs`,默认 = 心跳周期):吊销后最迟一拍,连接就会在续租里发现失权并断开。
22
+ * 窗内已发出的指令按 §8-R4 记账 —— 在途结果帧丢弃 + 审计,对应调用按 outcome-unknown 收尾。
23
+ * 真正的最终防线在**设备侧**(§5.4 ⑦ 单实例锁 + 严格等值世代判据),云侧关不了这一格。
24
+ * ② 事务内回读同连接(`readDeviceOn`)是**店内**规矩;本车不新增任何 SQL,只消费店契约。
25
+ * ③ 消费 A-2 预留的两个码(`device.busy` / `device.attached_elsewhere`)与两个审计词
26
+ * (`device_frame_unexpected` / `device_superseded`)—— 见下方各自的落点。
27
+ *
28
+ * ── 为什么 upgrade 处理器必须显式处理「不认的路径」────────────────────────────────────────────────
29
+ * node 的默认行为是「没有 'upgrade' 监听器就销毁 socket」。挂上第一个监听器之后那条默认**失效**,
30
+ * 于是不认的路径若直接 `return`,socket 会挂着直到超时。所以这里显式回 404 并销毁。
31
+ * ⚠️ 将来出现**第二个** upgrade 消费方时,这里要改成一张路由表 —— 两个 `server.on("upgrade")`
32
+ * 监听器会**同时**收到每一次 upgrade,各自「不认就 404」会互相把对方的连接打死。
33
+ *
34
+ * ── 安全轴姿势(#157)────────────────────────────────────────────────────────────────────────────
35
+ * 未知帧型 / 坏形帧 / 世代不符 / pending 外 instructionId = **响亮拒 + 审计行**,没有一处空 catch,
36
+ * 没有一处「认不出就当默认值」。审计写失败本身也是响亮的(logger.error + 计数),不是静默吞。
37
+ */
38
+ import http from "node:http";
39
+ import { type DeviceOwner, type DeviceStore } from "./device-store.js";
40
+ import type { DeviceEnrollment, DeviceRejection } from "./device-enrollment.js";
41
+ import { type DeviceChunkKind, type DeviceInstructionArgsByKind, type DeviceInstructionKind, type DeviceInstructionOutcome } from "./device-ws-protocol.js";
42
+ /** 交给调用方的一片上行流(`data` 已从 base64 解回字节)。 */
43
+ export interface DeviceChunkDelivery {
44
+ streamSeq: number;
45
+ kind: DeviceChunkKind;
46
+ data?: Buffer;
47
+ exitCode?: number;
48
+ }
49
+ /**
50
+ * 一次下发的入参。`kind` 与 `args` **按 kind 判别**(§5.1 task-R1-8),不是 `any`。
51
+ *
52
+ * `payload` = 大载荷下行(writeFile 族):内容不进指令帧,走 `payload` begin→chunk→commit 子序;
53
+ * 长度与 sha256 由**本层**按真字节重算写回 args —— 调用方报的摘要不作数(它不是那份字节的权威)。
54
+ */
55
+ export type DeviceDispatchInput = {
56
+ [K in DeviceInstructionKind]: {
57
+ rootSessionId: string;
58
+ requester: DeviceOwner;
59
+ kind: K;
60
+ args: DeviceInstructionArgsByKind[K];
61
+ cwd?: string;
62
+ shellEnv?: Record<string, string>;
63
+ timeoutMs: number;
64
+ payload?: Uint8Array;
65
+ payloadPartBytes?: number;
66
+ onChunk?: (chunk: DeviceChunkDelivery) => void;
67
+ /**
68
+ * 铸造期快照的**否定面**(codex R3-F1):调用方在铸自己那一刻**没有**看到这几个指令 kind,
69
+ * 所以它的对外面上没有对应的方法。若投递门这一刻的连接**声明了**其中任何一个 ⇒ 拒。
70
+ *
71
+ * 🔴 为什么这条判据非放在这里不可:调用方侧那一次检查与真正的下发之间隔着 `admitSession` 的一次
72
+ * SQL 往返 —— 设备可以在这个窗口里断开、以**更强**的能力连回来,于是检查看到的是旧连接、指令却
73
+ * 打到新连接上(TOCTOU)。而投递门是本进程里**唯一**同时持有「最终选中的连接」与「不可回退的
74
+ * commit」的地方,原子复核只能在这儿做。语义与世代重断言同族(A-2 移交义务①),放在同一段。
75
+ *
76
+ * 用途在 device lane 上是**保护型写原语**(`writeFileGuarded`/`writeFileExclusive`):它们的缺席被
77
+ * core 读成「后端没有这个原语」并降级成非原子写,所以「铸的时候没有、下发的时候有了」= 一次静默的
78
+ * 保护降级(#157)。本座本身与语义无关,只做集合判断。
79
+ */
80
+ refuseIfDeclared?: readonly DeviceInstructionKind[];
81
+ /**
82
+ * 车A-4(A-3 残余⑤):调用方的取消轴。
83
+ *
84
+ * 🔴 **为什么这个座必须在 hub 而不是调用方自己接**:`instructionId` 是在**投递门内部**铸的,
85
+ * `dispatch` 直到结算才 resolve —— 调用方结构上拿不到那个 id,所以它自己的 abort 监听只能做到
86
+ * 「放弃等待」,而设备上那条命令还在跑(§5.4 at-most-once 的反面:一次不可见的执行)。座放在这里,
87
+ * abort ⇒ 真发 `cancel` 帧,终局回执照常走 §5.3 的 10s 宽限。
88
+ * 投递门**之前** abort ⇒ `terminal_never_started`(设备结构上没见过它,安全)。
89
+ */
90
+ signal?: AbortSignal;
91
+ };
92
+ }[DeviceInstructionKind];
93
+ /**
94
+ * 下发结果的三态 —— 与 {@link DeviceInstructionState} 的两条路径 1:1。
95
+ *
96
+ * • `settled`:过了投递门(`dispatch_committed`)并拿到终局。**含**失败结局(设备回的 error /
97
+ * 云侧兜底的 `outcome_unknown`)—— 它们都是「设备可能已经执行过」的世界。
98
+ * • `never_started`:`terminal_never_started` —— 设备结构性不可能见过它(能力未声明)。可安全重试。
99
+ * • `rejected`:准入/背压/失权,**在投递门之前**。同样安全(§5.3 投递边界)。
100
+ */
101
+ export type DeviceDispatchResult = {
102
+ kind: "settled";
103
+ instructionId: string;
104
+ outcome: DeviceInstructionOutcome;
105
+ } | {
106
+ kind: "never_started";
107
+ outcome: DeviceInstructionOutcome;
108
+ } | {
109
+ kind: "rejected";
110
+ reject: DeviceRejection;
111
+ };
112
+ export interface DeviceWsHubLogger {
113
+ warn(msg: string, fields?: Record<string, unknown>): void;
114
+ error(msg: string, fields?: Record<string, unknown>): void;
115
+ info?(msg: string, fields?: Record<string, unknown>): void;
116
+ }
117
+ export interface DeviceWsHubOptions {
118
+ store: DeviceStore;
119
+ /** 准入链(§4.3.2)。WS 汇聚端**不自建判据**,一律经它。 */
120
+ enrollment: DeviceEnrollment;
121
+ /** 本副本 id(写进连接租约行,跨副本排障用)。 */
122
+ replicaId: string;
123
+ logger?: DeviceWsHubLogger;
124
+ metrics?: {
125
+ inc(name: string, labels?: Record<string, string>): void;
126
+ };
127
+ /** 下发给设备的心跳周期(helloAck 协商);丢 `heartbeatMisses` 拍判 STALE。 */
128
+ heartbeatIntervalMs?: number;
129
+ heartbeatMisses?: number;
130
+ /** 连接租约时长(整秒,自 DB now 起算)。 */
131
+ leaseSeconds?: number;
132
+ /** 服务端续租/失权侦测的拍距。缺省 = `heartbeatIntervalMs`(见文件头「诚实残余」)。 */
133
+ leaseRenewIntervalMs?: number;
134
+ /** per-device 在途上限(§11-O3 clay 已裁 = 4)。 */
135
+ maxInflightPerDevice?: number;
136
+ /** 等在途槽的上限;到期 = `device.busy`(§5.5,429 语义)。 */
137
+ dispatchTimeoutMs?: number;
138
+ /** 云侧执行超时兜底的宽限(§5.3:`timeoutMs + grace` ⇒ outcome_unknown)。 */
139
+ execGraceMs?: number;
140
+ /** cancel 终局回执宽限(§5.3)。 */
141
+ cancelGraceMs?: number;
142
+ /** 断连后在途不判丢的窗(§5.4 同 epoch 重连);窗尽 ⇒ outcome_unknown。 */
143
+ reconnectGraceMs?: number;
144
+ /** 上行流片缺口的等待上限;到期 = 指令失败(**不是**静默截断)。 */
145
+ chunkGapTimeoutMs?: number;
146
+ /** §5.2 顶替旋钮:`stale_only`(默认)= 只顶替已 STALE 的旧连;`deny` = 重连也须等旧连宽限尽。 */
147
+ supersede?: "stale_only" | "deny";
148
+ /** codex R1-F5:per-instruction 乱序缓冲的条目帽(超出 = 指令失败 + 审计)。 */
149
+ chunkBufferMaxEntries?: number;
150
+ /** codex R1-F5:per-instruction 乱序缓冲的**解码后**字节帽(§5.5 的 2MiB)。 */
151
+ chunkBufferMaxBytes?: number;
152
+ /** codex R1-F3:本副本**未鉴权**连接的并发帽(超额在 upgrade 阶段就 503 拒)。 */
153
+ maxPreAuthConnections?: number;
154
+ /** codex R1-F3:单来源地址的未鉴权连接并发帽。 */
155
+ maxPreAuthConnectionsPerRemote?: number;
156
+ }
157
+ export interface DeviceWsHub {
158
+ /** 把 `GET /v1/device/ws` 的 upgrade 处理器挂到裸 `http.Server` 上。 */
159
+ attach(server: http.Server): void;
160
+ dispatch(input: DeviceDispatchInput): Promise<DeviceDispatchResult>;
161
+ /** 幂等:重复 cancel 同一指令 = no-op(§5.3)。 */
162
+ cancel(instructionId: string): Promise<void>;
163
+ /** §8-R7:停发新指令 + 拒新 upgrade,但**保持既有 WS**,在途结果帧照收。 */
164
+ beginDrain(): void;
165
+ isDraining(): boolean;
166
+ connectedDeviceIds(): string[];
167
+ /**
168
+ * 车A-4:该设备**当前连接**在 hello 里声明的指令面(与本 server 的闭集取过交集之后的那一份)。
169
+ * 没有活连接 ⇒ `undefined`(**不是**空集:「不知道」与「明确什么都不支持」是两件事)。
170
+ *
171
+ * 🔴 用途只有一个,而且是 core 契约逼出来的:`writeFileExclusive`/`writeFileGuarded` 是
172
+ * **presence-typed** 可选面 —— core 的降级律逐字要求「后端没有该原语就把方法留 undefined,禁两步
173
+ * 仿真」,所以「在不在场」必须在**铸 env 的那一刻**答得出来,不能等到第一次调用再回 not_supported
174
+ * (那时调用方已经按「有原子面」走了)。
175
+ * 诚实残余:设备**在铸 env 之后**才连上时,这一问答的是 undefined ⇒ 该面缺席 ⇒ 写门如实降级成
176
+ * 「advisory adjudication」而不是宣称关闭了 TOCTOU —— 方向是安全的那一侧。
177
+ */
178
+ declaredSupports(deviceId: string): ReadonlySet<DeviceInstructionKind> | undefined;
179
+ close(): Promise<void>;
180
+ }
181
+ export declare function createDeviceWsHub(options: DeviceWsHubOptions): DeviceWsHub;
182
+ //# sourceMappingURL=device-ws-hub.d.ts.map