dsh-deeppilot 0.7.0 → 0.8.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/COMPATIBILITY.md +70 -4
- package/README.md +22 -15
- package/README.zh-CN.md +15 -11
- package/icon.svg +13 -0
- package/lib/client.js +364 -165
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +118 -97
- package/lib/index.js +209 -77
- package/lib/index.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +22 -17
package/lib/index.js
CHANGED
|
@@ -203,7 +203,7 @@ function validateRequest(type, value) {
|
|
|
203
203
|
"c2s.workspaces.list",
|
|
204
204
|
"c2s.directory.pick"
|
|
205
205
|
]);
|
|
206
|
-
if (value === void 0 && (type === "c2s.session.create" || type === "c2s.directory.list")) value = {};
|
|
206
|
+
if (value === void 0 && (type === "c2s.session.create" || type === "c2s.directory.list" || type === "c2s.device.revoke")) value = {};
|
|
207
207
|
if (value == null && empty.has(type)) return;
|
|
208
208
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "payload must be an object";
|
|
209
209
|
const p = value;
|
|
@@ -213,6 +213,7 @@ function validateRequest(type, value) {
|
|
|
213
213
|
if (type.startsWith("c2s.session.") && type !== "c2s.session.create" && !text(p.sessionId)) return "invalid sessionId";
|
|
214
214
|
if (type === "c2s.session.create" && (!optional("workspaceId", (v) => text(v)) || !optional("cwd", (v) => text(v, 32768)) || p.workspaceId !== void 0 && p.cwd !== void 0)) return "invalid workspace selection";
|
|
215
215
|
if (type === "c2s.directory.list" && !optional("path", (v) => text(v, 32768, false))) return "invalid path";
|
|
216
|
+
if (type === "c2s.device.revoke" && !optional("deviceId", (v) => text(v))) return "invalid deviceId";
|
|
216
217
|
if (type === "c2s.workspace.create" && !text(p.path, 32768)) return "invalid path";
|
|
217
218
|
if (type === "c2s.session.open" && !optional("tailCount", (v) => integer(v, 1, 1e4))) return "invalid tailCount";
|
|
218
219
|
if (type === "c2s.session.history" && (!integer(p.beforeSeq, 0, Number.MAX_SAFE_INTEGER) || !optional("limit", (v) => integer(v, 1, 500)))) return "invalid history range";
|
|
@@ -530,6 +531,7 @@ var DeviceStore = class DeviceStore {
|
|
|
530
531
|
}
|
|
531
532
|
const current = record.apns;
|
|
532
533
|
if (current && current.token === next.token && current.environment === next.environment && JSON.stringify(current.categories ?? {}) === JSON.stringify(next.categories ?? {})) return;
|
|
534
|
+
this.detachDuplicateToken("apns", normalized, deviceId);
|
|
533
535
|
record.apns = next;
|
|
534
536
|
this.flush();
|
|
535
537
|
}
|
|
@@ -566,10 +568,12 @@ var DeviceStore = class DeviceStore {
|
|
|
566
568
|
setWidgetPushToken(deviceId, token, environment, now) {
|
|
567
569
|
const record = this.authorized(deviceId);
|
|
568
570
|
if (!record || !isValidApnsToken(token)) return;
|
|
571
|
+
const normalized = token.toLowerCase();
|
|
569
572
|
const previous = record.widgetApns;
|
|
570
|
-
if (previous?.token ===
|
|
573
|
+
if (previous?.token === normalized && previous.environment === environment && now - previous.updatedAt < 36e5) return;
|
|
574
|
+
this.detachDuplicateToken("widgetApns", normalized, deviceId);
|
|
571
575
|
record.widgetApns = {
|
|
572
|
-
token,
|
|
576
|
+
token: normalized,
|
|
573
577
|
environment,
|
|
574
578
|
updatedAt: now
|
|
575
579
|
};
|
|
@@ -582,6 +586,22 @@ var DeviceStore = class DeviceStore {
|
|
|
582
586
|
delete record.widgetApns;
|
|
583
587
|
this.flush();
|
|
584
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* One physical device owns one APNs token, but a re-pair or a botched unbind
|
|
591
|
+
* can leave the same token attached to a second deviceId. Registering it
|
|
592
|
+
* under `keepDeviceId` detaches it from every other record — only that one
|
|
593
|
+
* token field, never the other record's pairing key or scopes. No flush of
|
|
594
|
+
* its own: the caller batches it with the write that follows, so a single
|
|
595
|
+
* registry write covers both changes.
|
|
596
|
+
*/
|
|
597
|
+
detachDuplicateToken(field, token, keepDeviceId) {
|
|
598
|
+
for (const record of this.devices.values()) {
|
|
599
|
+
if (record.deviceId === keepDeviceId) continue;
|
|
600
|
+
if (field === "apns") {
|
|
601
|
+
if (record.apns?.token === token) delete record.apns;
|
|
602
|
+
} else if (record.widgetApns?.token === token) delete record.widgetApns;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
585
605
|
/** Serialized so concurrent touches can never interleave half-written JSON. */
|
|
586
606
|
flush() {
|
|
587
607
|
const next = this.flushTail.then(() => this.writeFile());
|
|
@@ -640,6 +660,7 @@ function isEnvelope(value) {
|
|
|
640
660
|
function requiredScope(type) {
|
|
641
661
|
if (typeof type !== "string") return void 0;
|
|
642
662
|
if (type === "c2s.ping" || type === "c2s.resume") return void 0;
|
|
663
|
+
if (type === "c2s.device.revoke") return void 0;
|
|
643
664
|
if (type === "c2s.session.sendPrompt" || type === "c2s.session.delivery") return "prompt.send";
|
|
644
665
|
if (type === "c2s.pending.list") return "interactions.respond";
|
|
645
666
|
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
@@ -712,6 +733,12 @@ var BridgeConnection = class {
|
|
|
712
733
|
authenticated = false;
|
|
713
734
|
authenticationSettled = false;
|
|
714
735
|
closed = false;
|
|
736
|
+
/**
|
|
737
|
+
* Set the moment c2s.device.revoke tears this device down. `ws.close()` is
|
|
738
|
+
* asynchronous, so frames that arrive while the 4401 close is in flight must
|
|
739
|
+
* already be refused — a revoked device may not keep driving the bridge.
|
|
740
|
+
*/
|
|
741
|
+
revoked = false;
|
|
715
742
|
helloTimer;
|
|
716
743
|
openSessions = /* @__PURE__ */ new Set();
|
|
717
744
|
/**
|
|
@@ -868,6 +895,7 @@ var BridgeConnection = class {
|
|
|
868
895
|
}
|
|
869
896
|
async onMessage(raw) {
|
|
870
897
|
this.lastActivity = Date.now();
|
|
898
|
+
if (this.revoked) return;
|
|
871
899
|
if (!this.authenticated && raw.length > 65536) {
|
|
872
900
|
this.close(1009, "pre-auth frame too large");
|
|
873
901
|
return;
|
|
@@ -906,7 +934,9 @@ var BridgeConnection = class {
|
|
|
906
934
|
"c2s.sessions.list",
|
|
907
935
|
"c2s.pending.list",
|
|
908
936
|
"c2s.widget.push.register"
|
|
909
|
-
].includes(env.type))
|
|
937
|
+
].includes(env.type)) {
|
|
938
|
+
if (env.type !== "c2s.device.revoke") return this.fail(env.id, "E_FORBIDDEN", "widget connection is read-only");
|
|
939
|
+
}
|
|
910
940
|
const required = requiredScope(env.type);
|
|
911
941
|
if (required !== void 0 && !this.scopes.has(required)) {
|
|
912
942
|
this.fail(env.id, "E_FORBIDDEN", `scope ${required} required`);
|
|
@@ -1164,6 +1194,23 @@ var BridgeConnection = class {
|
|
|
1164
1194
|
this.send("s2c.ack", {}, env.id);
|
|
1165
1195
|
return;
|
|
1166
1196
|
}
|
|
1197
|
+
case "c2s.device.revoke": {
|
|
1198
|
+
const p = env.payload;
|
|
1199
|
+
if (p?.deviceId !== void 0 && typeof p.deviceId !== "string") return this.fail(env.id, "E_PROTOCOL", "deviceId must be a string");
|
|
1200
|
+
if (typeof p?.deviceId === "string" && p.deviceId !== this.deviceId) return this.fail(env.id, "E_FORBIDDEN", "deviceId does not match the authenticated device");
|
|
1201
|
+
if (this.widgetClient) return this.fail(env.id, "E_FORBIDDEN", "widget connection cannot revoke its device");
|
|
1202
|
+
const deviceId = this.deviceId;
|
|
1203
|
+
this.deps.devices.revoke(deviceId, Date.now());
|
|
1204
|
+
this.revoked = true;
|
|
1205
|
+
try {
|
|
1206
|
+
await this.deps.onDeviceRevoke?.(deviceId, this);
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
if (this.deps.debug === true) this.deps.log("device revoke hook failed: " + String(error));
|
|
1209
|
+
}
|
|
1210
|
+
this.send("s2c.ack", { revoked: true }, env.id);
|
|
1211
|
+
this.close(4401, "device revoked");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1167
1214
|
case "c2s.liveActivity.unregister": {
|
|
1168
1215
|
const p = env.payload;
|
|
1169
1216
|
if (this.pendingLiveActivityId === p.activityId) {
|
|
@@ -1238,6 +1285,7 @@ var BridgeConnection = class {
|
|
|
1238
1285
|
}
|
|
1239
1286
|
}
|
|
1240
1287
|
async prove(env) {
|
|
1288
|
+
if (this.revoked) return;
|
|
1241
1289
|
const p = env.payload ?? {};
|
|
1242
1290
|
if (!p.deviceId) {
|
|
1243
1291
|
this.fail(env.id, "E_PROTOCOL", "deviceId required");
|
|
@@ -1891,7 +1939,8 @@ var HostBridge = class {
|
|
|
1891
1939
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
1892
1940
|
push: this.pushOutlet?.isAvailable() === true,
|
|
1893
1941
|
widgetPush: true,
|
|
1894
|
-
liveActivityPush: true
|
|
1942
|
+
liveActivityPush: true,
|
|
1943
|
+
deviceRevoke: true
|
|
1895
1944
|
};
|
|
1896
1945
|
}
|
|
1897
1946
|
diagnostic(message) {
|
|
@@ -2430,11 +2479,38 @@ var HostBridge = class {
|
|
|
2430
2479
|
hasMore: Boolean(result.hasMore) || page.dropped > 0
|
|
2431
2480
|
});
|
|
2432
2481
|
this.deriveTitleFallback(sessionId, messages);
|
|
2482
|
+
if (this.apiProxy.supportsProjections === true) this.refreshProjections(sessionId);
|
|
2433
2483
|
return true;
|
|
2434
2484
|
} catch {
|
|
2435
2485
|
return false;
|
|
2436
2486
|
}
|
|
2437
2487
|
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Pull one session's projection baseline (DSH 0.1.7+) and fold every key
|
|
2490
|
+
* through applyProjection — unknown keys are ignored there, `null` means the
|
|
2491
|
+
* session no longer exists, and any failure degrades to a diagnostic: opening
|
|
2492
|
+
* a session must never fail because a baseline could not be read.
|
|
2493
|
+
*/
|
|
2494
|
+
async refreshProjections(sessionId) {
|
|
2495
|
+
try {
|
|
2496
|
+
const response = await this.apiProxy.sessions.projections?.({
|
|
2497
|
+
rpcId: randomUUID(),
|
|
2498
|
+
payload: { sessionId }
|
|
2499
|
+
});
|
|
2500
|
+
if (this.disposed) return;
|
|
2501
|
+
const result = response?.result;
|
|
2502
|
+
if (!result) return;
|
|
2503
|
+
if (!result.ok) {
|
|
2504
|
+
this.diagnostic("sessions.projections failed: " + result.error.code);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
const baseline = result.value;
|
|
2508
|
+
if (baseline === null || baseline === void 0) return;
|
|
2509
|
+
for (const [key, value] of Object.entries(baseline.values ?? {})) this.applyProjection(sessionId, key, value);
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
this.diagnostic("sessions.projections threw: " + String(error?.message ?? error));
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2438
2514
|
async historyPage(sessionId, beforeSeq, limit) {
|
|
2439
2515
|
try {
|
|
2440
2516
|
const response = await this.apiProxy.sessions.history({
|
|
@@ -3391,6 +3467,14 @@ var Dsh012ApiProxy = class {
|
|
|
3391
3467
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
3392
3468
|
}
|
|
3393
3469
|
/**
|
|
3470
|
+
* Whether the host controller implements `session.projections` (added in
|
|
3471
|
+
* DSH 0.1.7). HostBridge gates its open-time baseline refresh on this so
|
|
3472
|
+
* older hosts keep their current list-row-hint behaviour with no RPC noise.
|
|
3473
|
+
*/
|
|
3474
|
+
get supportsProjections() {
|
|
3475
|
+
return typeof this.session.projections === "function";
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3394
3478
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
3395
3479
|
* fails open to false so a broken device registry delegates this delivery
|
|
3396
3480
|
* while the official Web Client remains able to answer.
|
|
@@ -3444,7 +3528,12 @@ var Dsh012ApiProxy = class {
|
|
|
3444
3528
|
selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
|
|
3445
3529
|
rename: async (request) => this.call(() => this.session.rename(request.payload)),
|
|
3446
3530
|
cancel: async (request) => this.call(() => this.session.cancel(request.payload)),
|
|
3447
|
-
attachment: async (request) => this.call(() => this.session.attachment(request.payload))
|
|
3531
|
+
attachment: async (request) => this.call(() => this.session.attachment(request.payload)),
|
|
3532
|
+
projections: async (request) => this.call(() => {
|
|
3533
|
+
const read = this.session.projections;
|
|
3534
|
+
if (typeof read !== "function") throw unavailable("session projections unavailable");
|
|
3535
|
+
return read.call(this.session, request.payload);
|
|
3536
|
+
})
|
|
3448
3537
|
};
|
|
3449
3538
|
workspace = {
|
|
3450
3539
|
list: async () => this.call(async () => {
|
|
@@ -4018,6 +4107,35 @@ const scopesSchema = { parse(value) {
|
|
|
4018
4107
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
4019
4108
|
return normalizeDeviceScopes(value);
|
|
4020
4109
|
} };
|
|
4110
|
+
const booleanSchema = { parse(value) {
|
|
4111
|
+
if (typeof value !== "boolean") reject("boolean");
|
|
4112
|
+
return value;
|
|
4113
|
+
} };
|
|
4114
|
+
/**
|
|
4115
|
+
* One strict boundary codec carrying both generation shapes of the supported
|
|
4116
|
+
* `0.1.x` host family.
|
|
4117
|
+
*
|
|
4118
|
+
* Through `0.1.6-alpha.1` a descriptor published the schema value itself and
|
|
4119
|
+
* every consumer parsed through `codec.schema.parse(value)`. `0.1.6-alpha.2`
|
|
4120
|
+
* switched to a lazily materialized process-realm factory: `TypertRegistry`
|
|
4121
|
+
* now rejects a strict codec without `create()`, and the Gateway parses
|
|
4122
|
+
* through `codec.create().parse(value)`.
|
|
4123
|
+
*
|
|
4124
|
+
* Publishing both keys — `create()` returning the same hand-written,
|
|
4125
|
+
* dependency-free codec — keeps one package valid across the entire declared
|
|
4126
|
+
* peer range instead of forking the wire contract per host generation. The
|
|
4127
|
+
* cast is required because each generation's `TypertCodec` declares only its
|
|
4128
|
+
* own key, so a fresh object literal carrying both fails excess-property
|
|
4129
|
+
* checking against either one.
|
|
4130
|
+
*/
|
|
4131
|
+
function strictCodec(typeSymbol, schema) {
|
|
4132
|
+
return {
|
|
4133
|
+
mode: "strict",
|
|
4134
|
+
typeSymbol,
|
|
4135
|
+
schema,
|
|
4136
|
+
create: () => schema
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4021
4139
|
const REPORT_HOST_CONTRIBUTION = {
|
|
4022
4140
|
package: REPORT_REMOTE_PACKAGE,
|
|
4023
4141
|
face: "host",
|
|
@@ -4035,11 +4153,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4035
4153
|
method: "report",
|
|
4036
4154
|
invocation: { kind: "direct" },
|
|
4037
4155
|
parameters: [],
|
|
4038
|
-
result: {
|
|
4039
|
-
mode: "strict",
|
|
4040
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeepPilotReport`,
|
|
4041
|
-
schema: reportSchema
|
|
4042
|
-
}
|
|
4156
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeepPilotReport`, reportSchema)
|
|
4043
4157
|
},
|
|
4044
4158
|
{
|
|
4045
4159
|
id: `${REPORT_REMOTE_PACKAGE}#${BEGIN_PAIRING_ENDPOINT}`,
|
|
@@ -4048,11 +4162,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4048
4162
|
method: "beginPairing",
|
|
4049
4163
|
invocation: { kind: "direct" },
|
|
4050
4164
|
parameters: [],
|
|
4051
|
-
result: {
|
|
4052
|
-
mode: "strict",
|
|
4053
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`,
|
|
4054
|
-
schema: pairingGrantSchema
|
|
4055
|
-
}
|
|
4165
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`, pairingGrantSchema)
|
|
4056
4166
|
},
|
|
4057
4167
|
{
|
|
4058
4168
|
id: `${REPORT_REMOTE_PACKAGE}#${REVOKE_DEVICE_ENDPOINT}`,
|
|
@@ -4064,20 +4174,9 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4064
4174
|
name: "deviceId",
|
|
4065
4175
|
wire: "deviceId",
|
|
4066
4176
|
source: "json",
|
|
4067
|
-
codec: {
|
|
4068
|
-
mode: "strict",
|
|
4069
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
4070
|
-
schema: deviceIdSchema
|
|
4071
|
-
}
|
|
4177
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
4072
4178
|
}],
|
|
4073
|
-
result: {
|
|
4074
|
-
mode: "strict",
|
|
4075
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#Boolean`,
|
|
4076
|
-
schema: { parse(value) {
|
|
4077
|
-
if (typeof value !== "boolean") reject("boolean");
|
|
4078
|
-
return value;
|
|
4079
|
-
} }
|
|
4080
|
-
}
|
|
4179
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
4081
4180
|
},
|
|
4082
4181
|
{
|
|
4083
4182
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
@@ -4089,26 +4188,14 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4089
4188
|
name: "deviceId",
|
|
4090
4189
|
wire: "deviceId",
|
|
4091
4190
|
source: "json",
|
|
4092
|
-
codec: {
|
|
4093
|
-
mode: "strict",
|
|
4094
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
4095
|
-
schema: deviceIdSchema
|
|
4096
|
-
}
|
|
4191
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
4097
4192
|
}, {
|
|
4098
4193
|
name: "scopes",
|
|
4099
4194
|
wire: "scopes",
|
|
4100
4195
|
source: "json",
|
|
4101
|
-
codec: {
|
|
4102
|
-
mode: "strict",
|
|
4103
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
4104
|
-
schema: scopesSchema
|
|
4105
|
-
}
|
|
4196
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
4106
4197
|
}],
|
|
4107
|
-
result: {
|
|
4108
|
-
mode: "strict",
|
|
4109
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
4110
|
-
schema: scopesSchema
|
|
4111
|
-
}
|
|
4198
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
4112
4199
|
},
|
|
4113
4200
|
{
|
|
4114
4201
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testRelay`,
|
|
@@ -4117,11 +4204,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4117
4204
|
method: "testRelay",
|
|
4118
4205
|
invocation: { kind: "direct" },
|
|
4119
4206
|
parameters: [],
|
|
4120
|
-
result: {
|
|
4121
|
-
mode: "strict",
|
|
4122
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#RelayTestResult`,
|
|
4123
|
-
schema: relayTestSchema
|
|
4124
|
-
}
|
|
4207
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#RelayTestResult`, relayTestSchema)
|
|
4125
4208
|
},
|
|
4126
4209
|
{
|
|
4127
4210
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testPush`,
|
|
@@ -4130,11 +4213,7 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4130
4213
|
method: "testPush",
|
|
4131
4214
|
invocation: { kind: "direct" },
|
|
4132
4215
|
parameters: [],
|
|
4133
|
-
result: {
|
|
4134
|
-
mode: "strict",
|
|
4135
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PushTestResult`,
|
|
4136
|
-
schema: pushTestSchema
|
|
4137
|
-
}
|
|
4216
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PushTestResult`, pushTestSchema)
|
|
4138
4217
|
}
|
|
4139
4218
|
]
|
|
4140
4219
|
};
|
|
@@ -5289,18 +5368,18 @@ function localListenError(error, port) {
|
|
|
5289
5368
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
5290
5369
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
5291
5370
|
const Config = z.object({
|
|
5292
|
-
enabled: z.boolean().default(true),
|
|
5371
|
+
enabled: live(z.boolean().default(true)),
|
|
5293
5372
|
devicesPath: z.string().default(join(bridgeDataDir(), "devices-v2.json")),
|
|
5294
5373
|
historyBufferMax: z.natural().min(100).default(2e3),
|
|
5295
|
-
debug: z.boolean().default(false),
|
|
5296
|
-
local: z.object({
|
|
5374
|
+
debug: live(z.boolean().default(false)),
|
|
5375
|
+
local: live(z.object({
|
|
5297
5376
|
enabled: z.boolean().default(true),
|
|
5298
5377
|
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
|
|
5299
5378
|
}).default({
|
|
5300
5379
|
enabled: true,
|
|
5301
5380
|
port: DEFAULT_LOCAL_PORT
|
|
5302
|
-
}),
|
|
5303
|
-
remote: z.object({
|
|
5381
|
+
})),
|
|
5382
|
+
remote: live(z.object({
|
|
5304
5383
|
enabled: z.boolean().default(false),
|
|
5305
5384
|
provider: z.union(["tailscale-funnel"]).default("tailscale-funnel"),
|
|
5306
5385
|
hostname: z.string().default(DEFAULT_REMOTE_HOSTNAME),
|
|
@@ -5320,7 +5399,7 @@ const Config = z.object({
|
|
|
5320
5399
|
helperPath: "",
|
|
5321
5400
|
funnelPort: 443,
|
|
5322
5401
|
maxConnectionsPerSource: 8
|
|
5323
|
-
}),
|
|
5402
|
+
})),
|
|
5324
5403
|
push: z.object({
|
|
5325
5404
|
provider: z.union([
|
|
5326
5405
|
"none",
|
|
@@ -5346,14 +5425,50 @@ const Config = z.object({
|
|
|
5346
5425
|
})
|
|
5347
5426
|
});
|
|
5348
5427
|
/**
|
|
5428
|
+
* Declare one live-update (volatile) config node when the host supports it.
|
|
5429
|
+
*
|
|
5430
|
+
* DSH 0.1.7's schemastery adds `Schema#volatile()`; the schemas this package
|
|
5431
|
+
* typechecks against (3.18.x line) do not have the method, so the call goes
|
|
5432
|
+
* through a structural check instead of a direct chain — a direct
|
|
5433
|
+
* `.volatile()` would both fail typecheck today and throw at import on an
|
|
5434
|
+
* older host, killing plugin activation.
|
|
5435
|
+
*/
|
|
5436
|
+
function live(node) {
|
|
5437
|
+
const schema = node;
|
|
5438
|
+
if (typeof schema.volatile === "function") return schema.volatile();
|
|
5439
|
+
if (typeof schema.extra === "function") return schema.extra("volatile", true);
|
|
5440
|
+
return node;
|
|
5441
|
+
}
|
|
5442
|
+
/**
|
|
5443
|
+
* Deep-unwrap live-update (volatile) references to plain values.
|
|
5444
|
+
*
|
|
5445
|
+
* On a 0.1.7 host, `apply()`'s options — and even a direct `Config(raw)` call
|
|
5446
|
+
* — carry `{ get() }` reference objects for volatile fields. Every consumer of
|
|
5447
|
+
* `currentConfig()` compares plain values (`=== true`, numeric bounds, object
|
|
5448
|
+
* fields), and schemastery refuses to re-parse a reference, so normalization
|
|
5449
|
+
* unwraps on the way in and on the way out. A JSON-shaped config value cannot
|
|
5450
|
+
* carry a function, so the `get` check cannot false-positive; plain input
|
|
5451
|
+
* passes through unchanged, keeping older-host behavior identical.
|
|
5452
|
+
*/
|
|
5453
|
+
function plainConfig(value) {
|
|
5454
|
+
if (value === null || typeof value !== "object") return value;
|
|
5455
|
+
if (typeof value.get === "function") return plainConfig(value.get());
|
|
5456
|
+
if (Array.isArray(value)) return value.map(plainConfig);
|
|
5457
|
+
const plain = {};
|
|
5458
|
+
for (const [key, item] of Object.entries(value)) plain[key] = plainConfig(item);
|
|
5459
|
+
return plain;
|
|
5460
|
+
}
|
|
5461
|
+
/**
|
|
5349
5462
|
* Cordis hands the second argument in different shapes depending on host
|
|
5350
5463
|
* composition: a reactive options getter, the resolved config value, or
|
|
5351
|
-
* nothing when the patch row omits `config`. Normalize all of them
|
|
5464
|
+
* nothing when the patch row omits `config`. Normalize all of them — and
|
|
5465
|
+
* unwrap volatile references so the returned Config is plain data on every
|
|
5466
|
+
* host generation.
|
|
5352
5467
|
*/
|
|
5353
5468
|
function normalizeOptions(options) {
|
|
5354
|
-
if (typeof options === "function") return options();
|
|
5355
|
-
if (options && typeof options === "object") return options;
|
|
5356
|
-
return Config(void 0) ?? {};
|
|
5469
|
+
if (typeof options === "function") return plainConfig(options());
|
|
5470
|
+
if (options && typeof options === "object") return plainConfig(options);
|
|
5471
|
+
return plainConfig(Config(void 0) ?? {});
|
|
5357
5472
|
}
|
|
5358
5473
|
//#endregion
|
|
5359
5474
|
//#region src/phone-http.ts
|
|
@@ -5955,8 +6070,13 @@ function apply(ctx, options) {
|
|
|
5955
6070
|
})
|
|
5956
6071
|
};
|
|
5957
6072
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
6073
|
+
if (typeof settingsCtx.settings?.installSection !== "function") return;
|
|
5958
6074
|
settingsCtx.settings.installSection(ctx, "deeppilot", Config, normalizeOptions(void 0), settingsHooks);
|
|
5959
6075
|
});
|
|
6076
|
+
ctx.on("loader/volatile-update", () => queueMicrotask(() => {
|
|
6077
|
+
scheduleLocalReconcile?.();
|
|
6078
|
+
scheduleRemoteReconcile?.();
|
|
6079
|
+
}));
|
|
5960
6080
|
if (currentConfig().enabled !== true) log("disabled via settings; bridge stays inactive (rumors of /phone below are skipped)");
|
|
5961
6081
|
const dataDir = bridgeDataDir();
|
|
5962
6082
|
const pushRelayPath = join(dataDir, "push-relay.json");
|
|
@@ -6110,6 +6230,28 @@ function apply(ctx, options) {
|
|
|
6110
6230
|
for (const connection of connections) connection.closeForServerStop();
|
|
6111
6231
|
connections.clear();
|
|
6112
6232
|
};
|
|
6233
|
+
/**
|
|
6234
|
+
* Unbind one device. Single path shared by the settings page (report-service
|
|
6235
|
+
* revokeDevice) and the wire-level c2s.device.revoke frame: mark the registry
|
|
6236
|
+
* tombstone (which also drops every APNs/WidgetKit/LiveActivity token, so the
|
|
6237
|
+
* push fan-out can no longer select it) and hard-drop its live sockets.
|
|
6238
|
+
* `except` is the connection that sent the revoke frame — it closes itself
|
|
6239
|
+
* with 4401 after acking, so it is not terminated here.
|
|
6240
|
+
*/
|
|
6241
|
+
const revokeDevice = async (deviceId, except) => {
|
|
6242
|
+
const { devices } = await ready;
|
|
6243
|
+
if (!devices) throw new Error("device registry unavailable");
|
|
6244
|
+
const revoked = devices.revoke(deviceId, Date.now());
|
|
6245
|
+
if (revoked) {
|
|
6246
|
+
for (const connection of [...connections]) {
|
|
6247
|
+
if (connection === except || connection.connectedDeviceId !== deviceId) continue;
|
|
6248
|
+
connection.terminate();
|
|
6249
|
+
connections.delete(connection);
|
|
6250
|
+
}
|
|
6251
|
+
log(`device revoked id=${auditLabel(deviceId)}`);
|
|
6252
|
+
}
|
|
6253
|
+
return revoked;
|
|
6254
|
+
};
|
|
6113
6255
|
const resolvePushConfig = (config) => {
|
|
6114
6256
|
const push = config.push ?? {};
|
|
6115
6257
|
const configured = push.provider ?? "none";
|
|
@@ -6500,18 +6642,7 @@ function apply(ctx, options) {
|
|
|
6500
6642
|
devices
|
|
6501
6643
|
};
|
|
6502
6644
|
}, beginPairing, async (deviceId) => {
|
|
6503
|
-
|
|
6504
|
-
if (!devices) throw new Error("device registry unavailable");
|
|
6505
|
-
const revoked = devices.revoke(deviceId, Date.now());
|
|
6506
|
-
if (revoked) {
|
|
6507
|
-
for (const connection of [...connections]) {
|
|
6508
|
-
if (connection.connectedDeviceId !== deviceId) continue;
|
|
6509
|
-
connection.terminate();
|
|
6510
|
-
connections.delete(connection);
|
|
6511
|
-
}
|
|
6512
|
-
log(`device revoked id=${auditLabel(deviceId)}`);
|
|
6513
|
-
}
|
|
6514
|
-
return revoked;
|
|
6645
|
+
return await revokeDevice(deviceId);
|
|
6515
6646
|
}, async (deviceId, scopes) => {
|
|
6516
6647
|
const { devices } = await ready;
|
|
6517
6648
|
if (!devices) throw new Error("device registry unavailable");
|
|
@@ -6720,7 +6851,8 @@ function apply(ctx, options) {
|
|
|
6720
6851
|
onDeviceAuthenticated: (deviceId) => {
|
|
6721
6852
|
log(`device authenticated id=${auditLabel(deviceId)} source=${auditLabel(source)}`);
|
|
6722
6853
|
},
|
|
6723
|
-
onPushEnrollKey: handlePushEnrollKey
|
|
6854
|
+
onPushEnrollKey: handlePushEnrollKey,
|
|
6855
|
+
onDeviceRevoke: (deviceId, except) => revokeDevice(deviceId, except)
|
|
6724
6856
|
});
|
|
6725
6857
|
connections.add(connection);
|
|
6726
6858
|
} catch (error) {
|