dsh-deeppilot 0.7.1 → 0.8.1
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 +79 -4
- package/README.md +16 -13
- package/README.zh-CN.md +12 -10
- package/icon.svg +12 -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 +195 -30
- 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({
|
|
@@ -3293,9 +3369,32 @@ function createAgentScope(ctx, identity) {
|
|
|
3293
3369
|
}
|
|
3294
3370
|
});
|
|
3295
3371
|
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Arity of the Host's published `wireStream.open`. 0.1.7-alpha.1 declares
|
|
3374
|
+
* `(endpoint, payload, uplink, peer, signal)`; every earlier generation
|
|
3375
|
+
* declares `(endpoint, payload, signal)`. Both are plain arrow functions with
|
|
3376
|
+
* no default or rest parameters, so `length` is a reliable contract selector —
|
|
3377
|
+
* unlike a capability probe, it opens no trial stream. A future signature that
|
|
3378
|
+
* rest-parameterizes (reporting `0`) falls back to the 3-argument call, which
|
|
3379
|
+
* every generation through `0.1.6-*` accepts.
|
|
3380
|
+
*/
|
|
3381
|
+
function hostWireStreamArity(open) {
|
|
3382
|
+
return open.length;
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Open one Host logical stream on whichever `wireStream.open` contract the
|
|
3386
|
+
* running Host publishes. DeepPilot's carrier owns the Host in process and has
|
|
3387
|
+
* no Client-to-Host uplink, so the 5-argument form sends `undefined` for both
|
|
3388
|
+
* `uplink` and `peer` — the documented "operator's in-process carrier" case.
|
|
3389
|
+
*/
|
|
3390
|
+
function openHostStream(gateway, endpoint, payload, signal) {
|
|
3391
|
+
const open = gateway.wireStream.open;
|
|
3392
|
+
if (hostWireStreamArity(open) >= 5) return open(endpoint, payload, void 0, void 0, signal);
|
|
3393
|
+
return open(endpoint, payload, signal);
|
|
3394
|
+
}
|
|
3296
3395
|
function inProcessStream(gateway, endpoint, payload, signal) {
|
|
3297
3396
|
return (async function* () {
|
|
3298
|
-
yield* await gateway
|
|
3397
|
+
yield* await openHostStream(gateway, endpoint, payload, signal);
|
|
3299
3398
|
})();
|
|
3300
3399
|
}
|
|
3301
3400
|
/**
|
|
@@ -3391,6 +3490,14 @@ var Dsh012ApiProxy = class {
|
|
|
3391
3490
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
3392
3491
|
}
|
|
3393
3492
|
/**
|
|
3493
|
+
* Whether the host controller implements `session.projections` (added in
|
|
3494
|
+
* DSH 0.1.7). HostBridge gates its open-time baseline refresh on this so
|
|
3495
|
+
* older hosts keep their current list-row-hint behaviour with no RPC noise.
|
|
3496
|
+
*/
|
|
3497
|
+
get supportsProjections() {
|
|
3498
|
+
return typeof this.session.projections === "function";
|
|
3499
|
+
}
|
|
3500
|
+
/**
|
|
3394
3501
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
3395
3502
|
* fails open to false so a broken device registry delegates this delivery
|
|
3396
3503
|
* while the official Web Client remains able to answer.
|
|
@@ -3444,7 +3551,12 @@ var Dsh012ApiProxy = class {
|
|
|
3444
3551
|
selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
|
|
3445
3552
|
rename: async (request) => this.call(() => this.session.rename(request.payload)),
|
|
3446
3553
|
cancel: async (request) => this.call(() => this.session.cancel(request.payload)),
|
|
3447
|
-
attachment: async (request) => this.call(() => this.session.attachment(request.payload))
|
|
3554
|
+
attachment: async (request) => this.call(() => this.session.attachment(request.payload)),
|
|
3555
|
+
projections: async (request) => this.call(() => {
|
|
3556
|
+
const read = this.session.projections;
|
|
3557
|
+
if (typeof read !== "function") throw unavailable("session projections unavailable");
|
|
3558
|
+
return read.call(this.session, request.payload);
|
|
3559
|
+
})
|
|
3448
3560
|
};
|
|
3449
3561
|
workspace = {
|
|
3450
3562
|
list: async () => this.call(async () => {
|
|
@@ -5279,18 +5391,18 @@ function localListenError(error, port) {
|
|
|
5279
5391
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
5280
5392
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
5281
5393
|
const Config = z.object({
|
|
5282
|
-
enabled: z.boolean().default(true),
|
|
5394
|
+
enabled: live(z.boolean().default(true)),
|
|
5283
5395
|
devicesPath: z.string().default(join(bridgeDataDir(), "devices-v2.json")),
|
|
5284
5396
|
historyBufferMax: z.natural().min(100).default(2e3),
|
|
5285
|
-
debug: z.boolean().default(false),
|
|
5286
|
-
local: z.object({
|
|
5397
|
+
debug: live(z.boolean().default(false)),
|
|
5398
|
+
local: live(z.object({
|
|
5287
5399
|
enabled: z.boolean().default(true),
|
|
5288
5400
|
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
|
|
5289
5401
|
}).default({
|
|
5290
5402
|
enabled: true,
|
|
5291
5403
|
port: DEFAULT_LOCAL_PORT
|
|
5292
|
-
}),
|
|
5293
|
-
remote: z.object({
|
|
5404
|
+
})),
|
|
5405
|
+
remote: live(z.object({
|
|
5294
5406
|
enabled: z.boolean().default(false),
|
|
5295
5407
|
provider: z.union(["tailscale-funnel"]).default("tailscale-funnel"),
|
|
5296
5408
|
hostname: z.string().default(DEFAULT_REMOTE_HOSTNAME),
|
|
@@ -5310,7 +5422,7 @@ const Config = z.object({
|
|
|
5310
5422
|
helperPath: "",
|
|
5311
5423
|
funnelPort: 443,
|
|
5312
5424
|
maxConnectionsPerSource: 8
|
|
5313
|
-
}),
|
|
5425
|
+
})),
|
|
5314
5426
|
push: z.object({
|
|
5315
5427
|
provider: z.union([
|
|
5316
5428
|
"none",
|
|
@@ -5336,14 +5448,50 @@ const Config = z.object({
|
|
|
5336
5448
|
})
|
|
5337
5449
|
});
|
|
5338
5450
|
/**
|
|
5451
|
+
* Declare one live-update (volatile) config node when the host supports it.
|
|
5452
|
+
*
|
|
5453
|
+
* DSH 0.1.7's schemastery adds `Schema#volatile()`; the schemas this package
|
|
5454
|
+
* typechecks against (3.18.x line) do not have the method, so the call goes
|
|
5455
|
+
* through a structural check instead of a direct chain — a direct
|
|
5456
|
+
* `.volatile()` would both fail typecheck today and throw at import on an
|
|
5457
|
+
* older host, killing plugin activation.
|
|
5458
|
+
*/
|
|
5459
|
+
function live(node) {
|
|
5460
|
+
const schema = node;
|
|
5461
|
+
if (typeof schema.volatile === "function") return schema.volatile();
|
|
5462
|
+
if (typeof schema.extra === "function") return schema.extra("volatile", true);
|
|
5463
|
+
return node;
|
|
5464
|
+
}
|
|
5465
|
+
/**
|
|
5466
|
+
* Deep-unwrap live-update (volatile) references to plain values.
|
|
5467
|
+
*
|
|
5468
|
+
* On a 0.1.7 host, `apply()`'s options — and even a direct `Config(raw)` call
|
|
5469
|
+
* — carry `{ get() }` reference objects for volatile fields. Every consumer of
|
|
5470
|
+
* `currentConfig()` compares plain values (`=== true`, numeric bounds, object
|
|
5471
|
+
* fields), and schemastery refuses to re-parse a reference, so normalization
|
|
5472
|
+
* unwraps on the way in and on the way out. A JSON-shaped config value cannot
|
|
5473
|
+
* carry a function, so the `get` check cannot false-positive; plain input
|
|
5474
|
+
* passes through unchanged, keeping older-host behavior identical.
|
|
5475
|
+
*/
|
|
5476
|
+
function plainConfig(value) {
|
|
5477
|
+
if (value === null || typeof value !== "object") return value;
|
|
5478
|
+
if (typeof value.get === "function") return plainConfig(value.get());
|
|
5479
|
+
if (Array.isArray(value)) return value.map(plainConfig);
|
|
5480
|
+
const plain = {};
|
|
5481
|
+
for (const [key, item] of Object.entries(value)) plain[key] = plainConfig(item);
|
|
5482
|
+
return plain;
|
|
5483
|
+
}
|
|
5484
|
+
/**
|
|
5339
5485
|
* Cordis hands the second argument in different shapes depending on host
|
|
5340
5486
|
* composition: a reactive options getter, the resolved config value, or
|
|
5341
|
-
* nothing when the patch row omits `config`. Normalize all of them
|
|
5487
|
+
* nothing when the patch row omits `config`. Normalize all of them — and
|
|
5488
|
+
* unwrap volatile references so the returned Config is plain data on every
|
|
5489
|
+
* host generation.
|
|
5342
5490
|
*/
|
|
5343
5491
|
function normalizeOptions(options) {
|
|
5344
|
-
if (typeof options === "function") return options();
|
|
5345
|
-
if (options && typeof options === "object") return options;
|
|
5346
|
-
return Config(void 0) ?? {};
|
|
5492
|
+
if (typeof options === "function") return plainConfig(options());
|
|
5493
|
+
if (options && typeof options === "object") return plainConfig(options);
|
|
5494
|
+
return plainConfig(Config(void 0) ?? {});
|
|
5347
5495
|
}
|
|
5348
5496
|
//#endregion
|
|
5349
5497
|
//#region src/phone-http.ts
|
|
@@ -5945,8 +6093,13 @@ function apply(ctx, options) {
|
|
|
5945
6093
|
})
|
|
5946
6094
|
};
|
|
5947
6095
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
6096
|
+
if (typeof settingsCtx.settings?.installSection !== "function") return;
|
|
5948
6097
|
settingsCtx.settings.installSection(ctx, "deeppilot", Config, normalizeOptions(void 0), settingsHooks);
|
|
5949
6098
|
});
|
|
6099
|
+
ctx.on("loader/volatile-update", () => queueMicrotask(() => {
|
|
6100
|
+
scheduleLocalReconcile?.();
|
|
6101
|
+
scheduleRemoteReconcile?.();
|
|
6102
|
+
}));
|
|
5950
6103
|
if (currentConfig().enabled !== true) log("disabled via settings; bridge stays inactive (rumors of /phone below are skipped)");
|
|
5951
6104
|
const dataDir = bridgeDataDir();
|
|
5952
6105
|
const pushRelayPath = join(dataDir, "push-relay.json");
|
|
@@ -6100,6 +6253,28 @@ function apply(ctx, options) {
|
|
|
6100
6253
|
for (const connection of connections) connection.closeForServerStop();
|
|
6101
6254
|
connections.clear();
|
|
6102
6255
|
};
|
|
6256
|
+
/**
|
|
6257
|
+
* Unbind one device. Single path shared by the settings page (report-service
|
|
6258
|
+
* revokeDevice) and the wire-level c2s.device.revoke frame: mark the registry
|
|
6259
|
+
* tombstone (which also drops every APNs/WidgetKit/LiveActivity token, so the
|
|
6260
|
+
* push fan-out can no longer select it) and hard-drop its live sockets.
|
|
6261
|
+
* `except` is the connection that sent the revoke frame — it closes itself
|
|
6262
|
+
* with 4401 after acking, so it is not terminated here.
|
|
6263
|
+
*/
|
|
6264
|
+
const revokeDevice = async (deviceId, except) => {
|
|
6265
|
+
const { devices } = await ready;
|
|
6266
|
+
if (!devices) throw new Error("device registry unavailable");
|
|
6267
|
+
const revoked = devices.revoke(deviceId, Date.now());
|
|
6268
|
+
if (revoked) {
|
|
6269
|
+
for (const connection of [...connections]) {
|
|
6270
|
+
if (connection === except || connection.connectedDeviceId !== deviceId) continue;
|
|
6271
|
+
connection.terminate();
|
|
6272
|
+
connections.delete(connection);
|
|
6273
|
+
}
|
|
6274
|
+
log(`device revoked id=${auditLabel(deviceId)}`);
|
|
6275
|
+
}
|
|
6276
|
+
return revoked;
|
|
6277
|
+
};
|
|
6103
6278
|
const resolvePushConfig = (config) => {
|
|
6104
6279
|
const push = config.push ?? {};
|
|
6105
6280
|
const configured = push.provider ?? "none";
|
|
@@ -6490,18 +6665,7 @@ function apply(ctx, options) {
|
|
|
6490
6665
|
devices
|
|
6491
6666
|
};
|
|
6492
6667
|
}, 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;
|
|
6668
|
+
return await revokeDevice(deviceId);
|
|
6505
6669
|
}, async (deviceId, scopes) => {
|
|
6506
6670
|
const { devices } = await ready;
|
|
6507
6671
|
if (!devices) throw new Error("device registry unavailable");
|
|
@@ -6710,7 +6874,8 @@ function apply(ctx, options) {
|
|
|
6710
6874
|
onDeviceAuthenticated: (deviceId) => {
|
|
6711
6875
|
log(`device authenticated id=${auditLabel(deviceId)} source=${auditLabel(source)}`);
|
|
6712
6876
|
},
|
|
6713
|
-
onPushEnrollKey: handlePushEnrollKey
|
|
6877
|
+
onPushEnrollKey: handlePushEnrollKey,
|
|
6878
|
+
onDeviceRevoke: (deviceId, except) => revokeDevice(deviceId, except)
|
|
6714
6879
|
});
|
|
6715
6880
|
connections.add(connection);
|
|
6716
6881
|
} catch (error) {
|