dsh-deeppilot 0.7.1 → 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 +46 -3
- package/README.md +16 -13
- package/README.zh-CN.md +12 -10
- package/icon.svg +13 -0
- package/lib/client.js +107 -14
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +118 -97
- package/lib/index.js +171 -29
- 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.d.ts
CHANGED
|
@@ -264,6 +264,13 @@ interface SessionsApiLike {
|
|
|
264
264
|
};
|
|
265
265
|
data: string;
|
|
266
266
|
}>>;
|
|
267
|
+
/** DSH 0.1.7+: complete projection baseline for one session; null when the session is gone. */
|
|
268
|
+
projections?(req: RpcRequestLike<{
|
|
269
|
+
sessionId: string;
|
|
270
|
+
}>): Promise<RpcResponseLike<{
|
|
271
|
+
asOfSeq: number;
|
|
272
|
+
values: Record<string, unknown>;
|
|
273
|
+
} | null>>;
|
|
267
274
|
}
|
|
268
275
|
interface WorkspaceApiLike {
|
|
269
276
|
list?(req: RpcRequestLike<Record<string, never>>): Promise<RpcResponseLike<{
|
|
@@ -439,6 +446,12 @@ interface ApiProxyLike {
|
|
|
439
446
|
sessions: SessionsApiLike;
|
|
440
447
|
workspace?: WorkspaceApiLike;
|
|
441
448
|
host?: HostApiLike;
|
|
449
|
+
/**
|
|
450
|
+
* DSH 0.1.7 added `session.projections`. Absent/false keeps the bridge on
|
|
451
|
+
* its existing list-row projection hints — the call is gated on this flag so
|
|
452
|
+
* older hosts never see a failing RPC.
|
|
453
|
+
*/
|
|
454
|
+
supportsProjections?: boolean;
|
|
442
455
|
respond(message: {
|
|
443
456
|
type: 'client-response';
|
|
444
457
|
rpcId: string;
|
|
@@ -552,6 +565,7 @@ declare class HostBridge {
|
|
|
552
565
|
push: boolean;
|
|
553
566
|
widgetPush: boolean;
|
|
554
567
|
liveActivityPush: boolean;
|
|
568
|
+
deviceRevoke: boolean;
|
|
555
569
|
};
|
|
556
570
|
diagnostic(message: string): void;
|
|
557
571
|
currentCursor(): number;
|
|
@@ -624,6 +638,13 @@ declare class HostBridge {
|
|
|
624
638
|
private loadApprovalArguments;
|
|
625
639
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
626
640
|
openSession(sink: BridgeSink, sessionId: string, tailCount: number): Promise<boolean>;
|
|
641
|
+
/**
|
|
642
|
+
* Pull one session's projection baseline (DSH 0.1.7+) and fold every key
|
|
643
|
+
* through applyProjection — unknown keys are ignored there, `null` means the
|
|
644
|
+
* session no longer exists, and any failure degrades to a diagnostic: opening
|
|
645
|
+
* a session must never fail because a baseline could not be read.
|
|
646
|
+
*/
|
|
647
|
+
private refreshProjections;
|
|
627
648
|
historyPage(sessionId: string, beforeSeq: number, limit: number): Promise<{
|
|
628
649
|
sessionId: string;
|
|
629
650
|
messages: MessageProjection[];
|
|
@@ -722,103 +743,103 @@ interface Config {
|
|
|
722
743
|
relayToken?: string;
|
|
723
744
|
};
|
|
724
745
|
}
|
|
725
|
-
declare const Config: z<Schemastery.ObjectS<{
|
|
726
|
-
enabled: z<boolean, boolean>;
|
|
727
|
-
devicesPath: z<string, string>;
|
|
728
|
-
historyBufferMax: z<number, number>;
|
|
729
|
-
debug: z<boolean, boolean>;
|
|
730
|
-
local: z<Schemastery.ObjectS<{
|
|
731
|
-
enabled: z<boolean, boolean>;
|
|
732
|
-
port: z<number, number>;
|
|
733
|
-
}
|
|
734
|
-
enabled: z<boolean, boolean>;
|
|
735
|
-
port: z<number, number>;
|
|
736
|
-
}
|
|
737
|
-
remote: z<Schemastery.ObjectS<{
|
|
738
|
-
enabled: z<boolean, boolean>;
|
|
739
|
-
provider: z<"tailscale-funnel", "tailscale-funnel">;
|
|
740
|
-
hostname: z<string, string>;
|
|
741
|
-
statePath: z<string, string>;
|
|
742
|
-
helperPath: z<string, string>;
|
|
743
|
-
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000>;
|
|
744
|
-
maxConnectionsPerSource: z<number, number>;
|
|
745
|
-
}
|
|
746
|
-
enabled: z<boolean, boolean>;
|
|
747
|
-
provider: z<"tailscale-funnel", "tailscale-funnel">;
|
|
748
|
-
hostname: z<string, string>;
|
|
749
|
-
statePath: z<string, string>;
|
|
750
|
-
helperPath: z<string, string>;
|
|
751
|
-
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000>;
|
|
752
|
-
maxConnectionsPerSource: z<number, number>;
|
|
753
|
-
}
|
|
754
|
-
push: z<Schemastery.ObjectS<{
|
|
755
|
-
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay">;
|
|
756
|
-
contentMode: z<"generic" | "preview", "generic" | "preview">;
|
|
757
|
-
teamId: z<string, string>;
|
|
758
|
-
keyId: z<string, string>;
|
|
759
|
-
keyPath: z<string, string>;
|
|
760
|
-
bundleId: z<string, string>;
|
|
761
|
-
relayUrl: z<string, string>;
|
|
762
|
-
relayToken: z<string, string>;
|
|
763
|
-
}
|
|
764
|
-
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay">;
|
|
765
|
-
contentMode: z<"generic" | "preview", "generic" | "preview">;
|
|
766
|
-
teamId: z<string, string>;
|
|
767
|
-
keyId: z<string, string>;
|
|
768
|
-
keyPath: z<string, string>;
|
|
769
|
-
bundleId: z<string, string>;
|
|
770
|
-
relayUrl: z<string, string>;
|
|
771
|
-
relayToken: z<string, string>;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
enabled: z<boolean, boolean>;
|
|
775
|
-
devicesPath: z<string, string>;
|
|
776
|
-
historyBufferMax: z<number, number>;
|
|
777
|
-
debug: z<boolean, boolean>;
|
|
778
|
-
local: z<Schemastery.ObjectS<{
|
|
779
|
-
enabled: z<boolean, boolean>;
|
|
780
|
-
port: z<number, number>;
|
|
781
|
-
}
|
|
782
|
-
enabled: z<boolean, boolean>;
|
|
783
|
-
port: z<number, number>;
|
|
784
|
-
}
|
|
785
|
-
remote: z<Schemastery.ObjectS<{
|
|
786
|
-
enabled: z<boolean, boolean>;
|
|
787
|
-
provider: z<"tailscale-funnel", "tailscale-funnel">;
|
|
788
|
-
hostname: z<string, string>;
|
|
789
|
-
statePath: z<string, string>;
|
|
790
|
-
helperPath: z<string, string>;
|
|
791
|
-
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000>;
|
|
792
|
-
maxConnectionsPerSource: z<number, number>;
|
|
793
|
-
}
|
|
794
|
-
enabled: z<boolean, boolean>;
|
|
795
|
-
provider: z<"tailscale-funnel", "tailscale-funnel">;
|
|
796
|
-
hostname: z<string, string>;
|
|
797
|
-
statePath: z<string, string>;
|
|
798
|
-
helperPath: z<string, string>;
|
|
799
|
-
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000>;
|
|
800
|
-
maxConnectionsPerSource: z<number, number>;
|
|
801
|
-
}
|
|
802
|
-
push: z<Schemastery.ObjectS<{
|
|
803
|
-
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay">;
|
|
804
|
-
contentMode: z<"generic" | "preview", "generic" | "preview">;
|
|
805
|
-
teamId: z<string, string>;
|
|
806
|
-
keyId: z<string, string>;
|
|
807
|
-
keyPath: z<string, string>;
|
|
808
|
-
bundleId: z<string, string>;
|
|
809
|
-
relayUrl: z<string, string>;
|
|
810
|
-
relayToken: z<string, string>;
|
|
811
|
-
}
|
|
812
|
-
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay">;
|
|
813
|
-
contentMode: z<"generic" | "preview", "generic" | "preview">;
|
|
814
|
-
teamId: z<string, string>;
|
|
815
|
-
keyId: z<string, string>;
|
|
816
|
-
keyPath: z<string, string>;
|
|
817
|
-
bundleId: z<string, string>;
|
|
818
|
-
relayUrl: z<string, string>;
|
|
819
|
-
relayToken: z<string, string>;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
746
|
+
declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
747
|
+
enabled: z<boolean, boolean, "defined">;
|
|
748
|
+
devicesPath: z<string, string, "defined">;
|
|
749
|
+
historyBufferMax: z<number, number, "defined">;
|
|
750
|
+
debug: z<boolean, boolean, "defined">;
|
|
751
|
+
local: z<Schemastery.ObjectS<NoInfer<{
|
|
752
|
+
enabled: z<boolean, boolean, "defined">;
|
|
753
|
+
port: z<number, number, "defined">;
|
|
754
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
755
|
+
enabled: z<boolean, boolean, "defined">;
|
|
756
|
+
port: z<number, number, "defined">;
|
|
757
|
+
}>>, "defined">;
|
|
758
|
+
remote: z<Schemastery.ObjectS<NoInfer<{
|
|
759
|
+
enabled: z<boolean, boolean, "defined">;
|
|
760
|
+
provider: z<"tailscale-funnel", "tailscale-funnel", "defined">;
|
|
761
|
+
hostname: z<string, string, "defined">;
|
|
762
|
+
statePath: z<string, string, "defined">;
|
|
763
|
+
helperPath: z<string, string, "defined">;
|
|
764
|
+
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000, "defined">;
|
|
765
|
+
maxConnectionsPerSource: z<number, number, "defined">;
|
|
766
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
767
|
+
enabled: z<boolean, boolean, "defined">;
|
|
768
|
+
provider: z<"tailscale-funnel", "tailscale-funnel", "defined">;
|
|
769
|
+
hostname: z<string, string, "defined">;
|
|
770
|
+
statePath: z<string, string, "defined">;
|
|
771
|
+
helperPath: z<string, string, "defined">;
|
|
772
|
+
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000, "defined">;
|
|
773
|
+
maxConnectionsPerSource: z<number, number, "defined">;
|
|
774
|
+
}>>, "defined">;
|
|
775
|
+
push: z<Schemastery.ObjectS<NoInfer<{
|
|
776
|
+
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay", "defined">;
|
|
777
|
+
contentMode: z<"generic" | "preview", "generic" | "preview", "defined">;
|
|
778
|
+
teamId: z<string, string, "defined">;
|
|
779
|
+
keyId: z<string, string, "defined">;
|
|
780
|
+
keyPath: z<string, string, "defined">;
|
|
781
|
+
bundleId: z<string, string, "defined">;
|
|
782
|
+
relayUrl: z<string, string, "defined">;
|
|
783
|
+
relayToken: z<string, string, "defined">;
|
|
784
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
785
|
+
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay", "defined">;
|
|
786
|
+
contentMode: z<"generic" | "preview", "generic" | "preview", "defined">;
|
|
787
|
+
teamId: z<string, string, "defined">;
|
|
788
|
+
keyId: z<string, string, "defined">;
|
|
789
|
+
keyPath: z<string, string, "defined">;
|
|
790
|
+
bundleId: z<string, string, "defined">;
|
|
791
|
+
relayUrl: z<string, string, "defined">;
|
|
792
|
+
relayToken: z<string, string, "defined">;
|
|
793
|
+
}>>, "defined">;
|
|
794
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
795
|
+
enabled: z<boolean, boolean, "defined">;
|
|
796
|
+
devicesPath: z<string, string, "defined">;
|
|
797
|
+
historyBufferMax: z<number, number, "defined">;
|
|
798
|
+
debug: z<boolean, boolean, "defined">;
|
|
799
|
+
local: z<Schemastery.ObjectS<NoInfer<{
|
|
800
|
+
enabled: z<boolean, boolean, "defined">;
|
|
801
|
+
port: z<number, number, "defined">;
|
|
802
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
803
|
+
enabled: z<boolean, boolean, "defined">;
|
|
804
|
+
port: z<number, number, "defined">;
|
|
805
|
+
}>>, "defined">;
|
|
806
|
+
remote: z<Schemastery.ObjectS<NoInfer<{
|
|
807
|
+
enabled: z<boolean, boolean, "defined">;
|
|
808
|
+
provider: z<"tailscale-funnel", "tailscale-funnel", "defined">;
|
|
809
|
+
hostname: z<string, string, "defined">;
|
|
810
|
+
statePath: z<string, string, "defined">;
|
|
811
|
+
helperPath: z<string, string, "defined">;
|
|
812
|
+
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000, "defined">;
|
|
813
|
+
maxConnectionsPerSource: z<number, number, "defined">;
|
|
814
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
815
|
+
enabled: z<boolean, boolean, "defined">;
|
|
816
|
+
provider: z<"tailscale-funnel", "tailscale-funnel", "defined">;
|
|
817
|
+
hostname: z<string, string, "defined">;
|
|
818
|
+
statePath: z<string, string, "defined">;
|
|
819
|
+
helperPath: z<string, string, "defined">;
|
|
820
|
+
funnelPort: z<443 | 8443 | 10000, 443 | 8443 | 10000, "defined">;
|
|
821
|
+
maxConnectionsPerSource: z<number, number, "defined">;
|
|
822
|
+
}>>, "defined">;
|
|
823
|
+
push: z<Schemastery.ObjectS<NoInfer<{
|
|
824
|
+
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay", "defined">;
|
|
825
|
+
contentMode: z<"generic" | "preview", "generic" | "preview", "defined">;
|
|
826
|
+
teamId: z<string, string, "defined">;
|
|
827
|
+
keyId: z<string, string, "defined">;
|
|
828
|
+
keyPath: z<string, string, "defined">;
|
|
829
|
+
bundleId: z<string, string, "defined">;
|
|
830
|
+
relayUrl: z<string, string, "defined">;
|
|
831
|
+
relayToken: z<string, string, "defined">;
|
|
832
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
833
|
+
provider: z<"apns" | "none" | "relay", "apns" | "none" | "relay", "defined">;
|
|
834
|
+
contentMode: z<"generic" | "preview", "generic" | "preview", "defined">;
|
|
835
|
+
teamId: z<string, string, "defined">;
|
|
836
|
+
keyId: z<string, string, "defined">;
|
|
837
|
+
keyPath: z<string, string, "defined">;
|
|
838
|
+
bundleId: z<string, string, "defined">;
|
|
839
|
+
relayUrl: z<string, string, "defined">;
|
|
840
|
+
relayToken: z<string, string, "defined">;
|
|
841
|
+
}>>, "defined">;
|
|
842
|
+
}>>, "plain">;
|
|
822
843
|
//#endregion
|
|
823
844
|
//#region src/push-policy.d.ts
|
|
824
845
|
/** Prune only when the provider supplies an authoritative token-lifecycle verdict. */
|
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 () => {
|
|
@@ -5279,18 +5368,18 @@ function localListenError(error, port) {
|
|
|
5279
5368
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
5280
5369
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
5281
5370
|
const Config = z.object({
|
|
5282
|
-
enabled: z.boolean().default(true),
|
|
5371
|
+
enabled: live(z.boolean().default(true)),
|
|
5283
5372
|
devicesPath: z.string().default(join(bridgeDataDir(), "devices-v2.json")),
|
|
5284
5373
|
historyBufferMax: z.natural().min(100).default(2e3),
|
|
5285
|
-
debug: z.boolean().default(false),
|
|
5286
|
-
local: z.object({
|
|
5374
|
+
debug: live(z.boolean().default(false)),
|
|
5375
|
+
local: live(z.object({
|
|
5287
5376
|
enabled: z.boolean().default(true),
|
|
5288
5377
|
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
|
|
5289
5378
|
}).default({
|
|
5290
5379
|
enabled: true,
|
|
5291
5380
|
port: DEFAULT_LOCAL_PORT
|
|
5292
|
-
}),
|
|
5293
|
-
remote: z.object({
|
|
5381
|
+
})),
|
|
5382
|
+
remote: live(z.object({
|
|
5294
5383
|
enabled: z.boolean().default(false),
|
|
5295
5384
|
provider: z.union(["tailscale-funnel"]).default("tailscale-funnel"),
|
|
5296
5385
|
hostname: z.string().default(DEFAULT_REMOTE_HOSTNAME),
|
|
@@ -5310,7 +5399,7 @@ const Config = z.object({
|
|
|
5310
5399
|
helperPath: "",
|
|
5311
5400
|
funnelPort: 443,
|
|
5312
5401
|
maxConnectionsPerSource: 8
|
|
5313
|
-
}),
|
|
5402
|
+
})),
|
|
5314
5403
|
push: z.object({
|
|
5315
5404
|
provider: z.union([
|
|
5316
5405
|
"none",
|
|
@@ -5336,14 +5425,50 @@ const Config = z.object({
|
|
|
5336
5425
|
})
|
|
5337
5426
|
});
|
|
5338
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
|
+
/**
|
|
5339
5462
|
* Cordis hands the second argument in different shapes depending on host
|
|
5340
5463
|
* composition: a reactive options getter, the resolved config value, or
|
|
5341
|
-
* 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.
|
|
5342
5467
|
*/
|
|
5343
5468
|
function normalizeOptions(options) {
|
|
5344
|
-
if (typeof options === "function") return options();
|
|
5345
|
-
if (options && typeof options === "object") return options;
|
|
5346
|
-
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) ?? {});
|
|
5347
5472
|
}
|
|
5348
5473
|
//#endregion
|
|
5349
5474
|
//#region src/phone-http.ts
|
|
@@ -5945,8 +6070,13 @@ function apply(ctx, options) {
|
|
|
5945
6070
|
})
|
|
5946
6071
|
};
|
|
5947
6072
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
6073
|
+
if (typeof settingsCtx.settings?.installSection !== "function") return;
|
|
5948
6074
|
settingsCtx.settings.installSection(ctx, "deeppilot", Config, normalizeOptions(void 0), settingsHooks);
|
|
5949
6075
|
});
|
|
6076
|
+
ctx.on("loader/volatile-update", () => queueMicrotask(() => {
|
|
6077
|
+
scheduleLocalReconcile?.();
|
|
6078
|
+
scheduleRemoteReconcile?.();
|
|
6079
|
+
}));
|
|
5950
6080
|
if (currentConfig().enabled !== true) log("disabled via settings; bridge stays inactive (rumors of /phone below are skipped)");
|
|
5951
6081
|
const dataDir = bridgeDataDir();
|
|
5952
6082
|
const pushRelayPath = join(dataDir, "push-relay.json");
|
|
@@ -6100,6 +6230,28 @@ function apply(ctx, options) {
|
|
|
6100
6230
|
for (const connection of connections) connection.closeForServerStop();
|
|
6101
6231
|
connections.clear();
|
|
6102
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
|
+
};
|
|
6103
6255
|
const resolvePushConfig = (config) => {
|
|
6104
6256
|
const push = config.push ?? {};
|
|
6105
6257
|
const configured = push.provider ?? "none";
|
|
@@ -6490,18 +6642,7 @@ function apply(ctx, options) {
|
|
|
6490
6642
|
devices
|
|
6491
6643
|
};
|
|
6492
6644
|
}, beginPairing, async (deviceId) => {
|
|
6493
|
-
|
|
6494
|
-
if (!devices) throw new Error("device registry unavailable");
|
|
6495
|
-
const revoked = devices.revoke(deviceId, Date.now());
|
|
6496
|
-
if (revoked) {
|
|
6497
|
-
for (const connection of [...connections]) {
|
|
6498
|
-
if (connection.connectedDeviceId !== deviceId) continue;
|
|
6499
|
-
connection.terminate();
|
|
6500
|
-
connections.delete(connection);
|
|
6501
|
-
}
|
|
6502
|
-
log(`device revoked id=${auditLabel(deviceId)}`);
|
|
6503
|
-
}
|
|
6504
|
-
return revoked;
|
|
6645
|
+
return await revokeDevice(deviceId);
|
|
6505
6646
|
}, async (deviceId, scopes) => {
|
|
6506
6647
|
const { devices } = await ready;
|
|
6507
6648
|
if (!devices) throw new Error("device registry unavailable");
|
|
@@ -6710,7 +6851,8 @@ function apply(ctx, options) {
|
|
|
6710
6851
|
onDeviceAuthenticated: (deviceId) => {
|
|
6711
6852
|
log(`device authenticated id=${auditLabel(deviceId)} source=${auditLabel(source)}`);
|
|
6712
6853
|
},
|
|
6713
|
-
onPushEnrollKey: handlePushEnrollKey
|
|
6854
|
+
onPushEnrollKey: handlePushEnrollKey,
|
|
6855
|
+
onDeviceRevoke: (deviceId, except) => revokeDevice(deviceId, except)
|
|
6714
6856
|
});
|
|
6715
6857
|
connections.add(connection);
|
|
6716
6858
|
} catch (error) {
|