@linxin666/dsh-remote-web-ui 0.2.9 → 0.3.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/README.i18n.yaml +2 -2
- package/README.md +41 -8
- package/README.zh.md +7 -2
- package/lib/client.js +65 -34
- package/lib/client.js.map +1 -1
- package/lib/index.js +645 -61
- package/lib/mobile.js +874 -64
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +35 -5
- package/lib/types/client/remote-channel.d.ts +4 -5
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +13 -28
- package/lib/types/http.d.ts +37 -8
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +76 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +25 -1
- package/lib/types/mobile/api.d.ts +57 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +29 -0
- package/lib/types/mobile/markdown.d.ts +7 -1
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +16 -7
- package/lib/types/mobile/markdown.test.js +8 -0
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +325 -6
- package/lib/types/mobile/views/ChatView.d.ts +10 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +205 -8
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +46 -5
- package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
- package/lib/types/mobile-api.d.ts +9 -3
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +61 -16
- package/lib/types/mobile-pending.d.ts +38 -0
- package/lib/types/mobile-pending.d.ts.map +1 -0
- package/lib/types/mobile-pending.js +59 -0
- package/lib/types/paired-model-catalog.d.ts +17 -0
- package/lib/types/paired-model-catalog.d.ts.map +1 -0
- package/lib/types/paired-model-catalog.js +252 -0
- package/lib/types/pairing.d.ts +13 -4
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +13 -5
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.d.ts +26 -0
- package/lib/types/remote-channel-boot.d.ts.map +1 -0
- package/lib/types/remote-channel-boot.js +128 -0
- package/lib/types/remote-channel-rules.d.ts +40 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -0
- package/lib/types/remote-channel-rules.js +32 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +12 -23
- package/lib/types/uuid-polyfill.d.ts +10 -0
- package/lib/types/uuid-polyfill.d.ts.map +1 -0
- package/lib/types/uuid-polyfill.js +27 -0
- package/package.json +1 -1
- package/src/client/index.ts +41 -5
- package/src/client/remote-channel.ts +18 -28
- package/src/http.ts +87 -12
- package/src/index.ts +27 -2
- package/src/mobile/api.ts +91 -0
- package/src/mobile/markdown.test.ts +9 -0
- package/src/mobile/markdown.ts +17 -7
- package/src/mobile/mobile-styles.ts +326 -6
- package/src/mobile/views/ChatView.stream.test.tsx +231 -0
- package/src/mobile/views/ChatView.tsx +328 -10
- package/src/mobile/views/WorkspaceView.test.tsx +163 -2
- package/src/mobile/views/WorkspaceView.tsx +152 -26
- package/src/mobile-api.ts +59 -17
- package/src/mobile-pending.ts +88 -0
- package/src/paired-model-catalog.ts +224 -0
- package/src/pairing.ts +13 -5
- package/src/remote-api.ts +0 -1
- package/src/remote-channel-boot.ts +131 -0
- package/src/remote-channel-rules.ts +62 -0
- package/src/routes.ts +13 -24
- package/src/uuid-polyfill.ts +28 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readCookie } from "./gate.js";
|
|
3
|
+
import { readBoundedJson, writeJson } from "./http.js";
|
|
4
|
+
import { isTrustedApiRequest, publicHostOf } from "./routes.js";
|
|
5
|
+
const MAX_BODY_BYTES = 16 * 1024;
|
|
6
|
+
const MAX_IDENTIFIER_LENGTH = 160;
|
|
7
|
+
const MAX_DISPLAY_NAME_LENGTH = 240;
|
|
8
|
+
const MAX_TOKEN_LIMIT = 10_000_000;
|
|
9
|
+
const REASONING_EFFORTS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
10
|
+
const stringField = (max) => z.string().trim().min(1).max(max).refine(value => !/[\r\n\0]/.test(value), 'must not contain a newline or NUL');
|
|
11
|
+
const providerSchema = z.object({ provider: stringField(MAX_IDENTIFIER_LENGTH) }).strict();
|
|
12
|
+
const modelSchema = z.object({
|
|
13
|
+
id: stringField(MAX_IDENTIFIER_LENGTH),
|
|
14
|
+
name: stringField(MAX_DISPLAY_NAME_LENGTH).optional(),
|
|
15
|
+
contextWindow: z.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
|
|
16
|
+
maxTokens: z.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
|
|
17
|
+
reasoningEfforts: z.array(z.enum(REASONING_EFFORTS)).min(1).refine(efforts => efforts.every((effort, index) => index === 0 || REASONING_EFFORTS.indexOf(efforts[index - 1]) < REASONING_EFFORTS.indexOf(effort)), 'reasoning efforts must be unique and in canonical order').optional(),
|
|
18
|
+
}).strict();
|
|
19
|
+
const upsertSchema = z.object({ provider: stringField(MAX_IDENTIFIER_LENGTH), model: modelSchema }).strict();
|
|
20
|
+
export const PAIRED_MODEL_CATALOG_PATHS = {
|
|
21
|
+
catalog: '/api/pair/model-catalog',
|
|
22
|
+
discover: '/api/pair/model-catalog/discover',
|
|
23
|
+
upsert: '/api/pair/model-catalog/upsert',
|
|
24
|
+
};
|
|
25
|
+
let rpcSequence = 0;
|
|
26
|
+
function request(payload) {
|
|
27
|
+
rpcSequence += 1;
|
|
28
|
+
return { rpcId: `paired-model-catalog-${Date.now().toString(36)}-${rpcSequence.toString(36)}`, payload };
|
|
29
|
+
}
|
|
30
|
+
function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
31
|
+
function valueOf(response) { return response.result.ok ? response.result.value : undefined; }
|
|
32
|
+
function errorCode(response) { return response.result.ok ? undefined : response.result.error.code; }
|
|
33
|
+
function reject(res, status, error) { writeJson(res, status, { error }); }
|
|
34
|
+
function publicProviderId(value) {
|
|
35
|
+
const normalized = value.replace(/[\r\n\0]/g, ' ').trim().slice(0, MAX_IDENTIFIER_LENGTH);
|
|
36
|
+
return normalized === '' ? 'unknown' : normalized;
|
|
37
|
+
}
|
|
38
|
+
function publicCatalog(value) {
|
|
39
|
+
return {
|
|
40
|
+
groups: value.groups.map(group => ({ id: group.id, name: group.name, models: group.models.map(model => ({
|
|
41
|
+
id: model.id, name: model.name,
|
|
42
|
+
...model.description === undefined ? {} : { description: model.description },
|
|
43
|
+
...model.reasoning === undefined ? {} : { reasoning: model.reasoning },
|
|
44
|
+
})) })),
|
|
45
|
+
failures: value.failures.map(failure => ({
|
|
46
|
+
id: failure.id,
|
|
47
|
+
name: failure.name,
|
|
48
|
+
message: `model catalog unavailable for provider ${publicProviderId(failure.id)}`,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function publicCandidates(models) {
|
|
53
|
+
return models.map(model => ({ id: model.id,
|
|
54
|
+
...model.name === undefined ? {} : { name: model.name },
|
|
55
|
+
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
56
|
+
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
function fieldsOf(model, includeId) {
|
|
60
|
+
const fields = {
|
|
61
|
+
...includeId ? { id: model.id } : {},
|
|
62
|
+
...model.name === undefined ? {} : { name: model.name },
|
|
63
|
+
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
64
|
+
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
65
|
+
};
|
|
66
|
+
if (model.reasoningEfforts !== undefined)
|
|
67
|
+
fields.reasoningEfforts = Object.fromEntries(model.reasoningEfforts.map(effort => [effort, effort === 'off' ? null : effort]));
|
|
68
|
+
return fields;
|
|
69
|
+
}
|
|
70
|
+
function groupFor(catalog, provider) { return catalog.groups.find(group => group.id === provider); }
|
|
71
|
+
async function readCatalog(apiProxy) {
|
|
72
|
+
try {
|
|
73
|
+
return valueOf(await apiProxy.llm.models(request({})));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Resolve one exact active llm-pi-ai provider address, never an input-supplied path. */
|
|
80
|
+
async function eligibleProvider(apiProxy, requested) {
|
|
81
|
+
let providersResponse;
|
|
82
|
+
let settingsResponse;
|
|
83
|
+
try {
|
|
84
|
+
providersResponse = await apiProxy.llm.providers(request({}));
|
|
85
|
+
settingsResponse = await apiProxy.settings.describe(request({}));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { status: 502, error: 'model catalog capability is unavailable' };
|
|
89
|
+
}
|
|
90
|
+
const providers = valueOf(providersResponse);
|
|
91
|
+
const settings = valueOf(settingsResponse);
|
|
92
|
+
if (providers === undefined || settings === undefined)
|
|
93
|
+
return { status: 502, error: 'model catalog capability is unavailable' };
|
|
94
|
+
const route = providers.providers.find(candidate => candidate.provider === requested);
|
|
95
|
+
if (route === undefined)
|
|
96
|
+
return { status: 404, error: `unknown provider ${requested}` };
|
|
97
|
+
if (!settings.writable || !route.active || route.settingsNs !== 'llm-pi-ai' || route.settingsPath.length !== 2
|
|
98
|
+
|| route.settingsPath[0] !== 'providers' || route.settingsPath[1] !== requested)
|
|
99
|
+
return { status: 403, error: `provider ${requested} is not eligible for the paired model catalog` };
|
|
100
|
+
const namespace = settings.namespaces.find(candidate => candidate.ns === 'llm-pi-ai');
|
|
101
|
+
const root = namespace !== undefined && isRecord(namespace.value) ? namespace.value : undefined;
|
|
102
|
+
const providersValue = root !== undefined && isRecord(root.providers) ? root.providers : undefined;
|
|
103
|
+
const profile = providersValue !== undefined && isRecord(providersValue[requested]) ? providersValue[requested] : undefined;
|
|
104
|
+
if (namespace === undefined || profile === undefined)
|
|
105
|
+
return { status: 403, error: `provider ${requested} is not eligible for the paired model catalog` };
|
|
106
|
+
return { provider: { provider: requested, displayName: route.displayName, namespace, profile } };
|
|
107
|
+
}
|
|
108
|
+
function updatedModels(existing, model) {
|
|
109
|
+
const next = existing.map(entry => isRecord(entry) ? { ...entry } : entry);
|
|
110
|
+
const index = next.findIndex(entry => isRecord(entry) && entry.id === model.id);
|
|
111
|
+
const patch = fieldsOf(model, true);
|
|
112
|
+
if (index >= 0 && isRecord(next[index]))
|
|
113
|
+
next[index] = { ...next[index], ...patch };
|
|
114
|
+
else
|
|
115
|
+
next.push(patch);
|
|
116
|
+
return next;
|
|
117
|
+
}
|
|
118
|
+
function mutationPlan(profile, provider, model, catalog) {
|
|
119
|
+
if (Array.isArray(profile.models))
|
|
120
|
+
return { path: ['providers', provider, 'models'], value: updatedModels(profile.models, model) };
|
|
121
|
+
const group = groupFor(catalog, provider);
|
|
122
|
+
if (group?.models.some(entry => entry.id === model.id) === true) {
|
|
123
|
+
const overrides = isRecord(profile.modelOverrides) ? profile.modelOverrides : undefined;
|
|
124
|
+
const current = overrides?.[model.id];
|
|
125
|
+
const existing = isRecord(current) ? current : {};
|
|
126
|
+
return { path: ['providers', provider, 'modelOverrides', model.id], value: { ...existing, ...fieldsOf(model, false) } };
|
|
127
|
+
}
|
|
128
|
+
if (group === undefined || catalog.failures.some(failure => failure.id === provider))
|
|
129
|
+
return undefined;
|
|
130
|
+
return { path: ['providers', provider, 'models'], value: [...group.models.map(entry => ({ id: entry.id })), fieldsOf(model, true)] };
|
|
131
|
+
}
|
|
132
|
+
function paired(req, service) {
|
|
133
|
+
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
134
|
+
return deviceId !== undefined && service.touchDevice(deviceId);
|
|
135
|
+
}
|
|
136
|
+
async function bodyOf(req, schema) {
|
|
137
|
+
try {
|
|
138
|
+
return schema.parse(await readBoundedJson(req, MAX_BODY_BYTES));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Build the narrow paired-device model catalog routes. */
|
|
145
|
+
export function makePairedModelCatalogRoutes(deps) {
|
|
146
|
+
const { service, apiProxy, lanAddresses } = deps;
|
|
147
|
+
/** Same fence as the /api/pair family: loopback, the advertised LAN literals, or the configured public host. */
|
|
148
|
+
const fence = (req) => {
|
|
149
|
+
const publicHost = publicHostOf(service.publicBaseUrl);
|
|
150
|
+
return isTrustedApiRequest(req, publicHost === undefined ? lanAddresses : [...lanAddresses, publicHost]);
|
|
151
|
+
};
|
|
152
|
+
const gate = (req, res) => {
|
|
153
|
+
if (!fence(req)) {
|
|
154
|
+
req.resume();
|
|
155
|
+
reject(res, 403, 'paired model catalog is not reachable for this origin');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (!paired(req, service)) {
|
|
159
|
+
req.resume();
|
|
160
|
+
reject(res, 403, 'paired model catalog requires a live paired device');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
const catalog = async (req, res) => {
|
|
166
|
+
if (!gate(req, res))
|
|
167
|
+
return;
|
|
168
|
+
if (req.method !== 'GET')
|
|
169
|
+
return reject(res, 405, 'method not allowed');
|
|
170
|
+
let providersResponse;
|
|
171
|
+
let settingsResponse;
|
|
172
|
+
try {
|
|
173
|
+
providersResponse = await apiProxy.llm.providers(request({}));
|
|
174
|
+
settingsResponse = await apiProxy.settings.describe(request({}));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return reject(res, 502, 'model catalog capability is unavailable');
|
|
178
|
+
}
|
|
179
|
+
const providers = valueOf(providersResponse);
|
|
180
|
+
const settings = valueOf(settingsResponse);
|
|
181
|
+
if (providers === undefined || settings === undefined)
|
|
182
|
+
return reject(res, 502, 'model catalog capability is unavailable');
|
|
183
|
+
const namespace = settings.namespaces.find(candidate => candidate.ns === 'llm-pi-ai');
|
|
184
|
+
const providerMap = namespace !== undefined && isRecord(namespace.value) && isRecord(namespace.value.providers) ? namespace.value.providers : undefined;
|
|
185
|
+
const eligible = settings.writable && providerMap !== undefined ? providers.providers.filter(candidate => candidate.active && candidate.settingsNs === 'llm-pi-ai'
|
|
186
|
+
&& candidate.settingsPath.length === 2 && candidate.settingsPath[0] === 'providers' && candidate.settingsPath[1] === candidate.provider && isRecord(providerMap[candidate.provider]))
|
|
187
|
+
.map(candidate => ({ provider: candidate.provider, displayName: candidate.displayName })) : [];
|
|
188
|
+
writeJson(res, 200, { capability: 'paired-model-catalog', providers: eligible });
|
|
189
|
+
};
|
|
190
|
+
const discover = async (req, res) => {
|
|
191
|
+
if (!gate(req, res))
|
|
192
|
+
return;
|
|
193
|
+
if (req.method !== 'POST')
|
|
194
|
+
return reject(res, 405, 'method not allowed');
|
|
195
|
+
const body = await bodyOf(req, providerSchema);
|
|
196
|
+
if (body === undefined)
|
|
197
|
+
return reject(res, 400, 'invalid model catalog request');
|
|
198
|
+
const eligible = await eligibleProvider(apiProxy, body.provider);
|
|
199
|
+
if (eligible.provider === undefined)
|
|
200
|
+
return reject(res, eligible.status ?? 502, eligible.error ?? 'model catalog capability is unavailable');
|
|
201
|
+
try {
|
|
202
|
+
const result = valueOf(await apiProxy.llm.discoverModels(request({ settingsNs: 'llm-pi-ai', provider: eligible.provider.provider })));
|
|
203
|
+
if (result === undefined)
|
|
204
|
+
return reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
|
|
205
|
+
writeJson(res, 200, { models: publicCandidates(result.models) });
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const upsert = async (req, res) => {
|
|
212
|
+
if (!gate(req, res))
|
|
213
|
+
return;
|
|
214
|
+
if (req.method !== 'POST')
|
|
215
|
+
return reject(res, 405, 'method not allowed');
|
|
216
|
+
const body = await bodyOf(req, upsertSchema);
|
|
217
|
+
if (body === undefined)
|
|
218
|
+
return reject(res, 400, 'invalid model catalog request');
|
|
219
|
+
const eligible = await eligibleProvider(apiProxy, body.provider);
|
|
220
|
+
if (eligible.provider === undefined)
|
|
221
|
+
return reject(res, eligible.status ?? 502, eligible.error ?? 'model catalog capability is unavailable');
|
|
222
|
+
const before = await readCatalog(apiProxy);
|
|
223
|
+
if (before === undefined)
|
|
224
|
+
return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
|
|
225
|
+
const plan = mutationPlan(eligible.provider.profile, body.provider, body.model, before);
|
|
226
|
+
if (plan === undefined)
|
|
227
|
+
return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
|
|
228
|
+
let mutation;
|
|
229
|
+
try {
|
|
230
|
+
mutation = await apiProxy.settings.mutate(request({ ns: 'llm-pi-ai', expectedRevision: eligible.provider.namespace.revision, ops: [{ op: 'set', path: plan.path, value: plan.value }] }));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return reject(res, 502, `model update failed for provider ${body.provider}`);
|
|
234
|
+
}
|
|
235
|
+
const code = errorCode(mutation);
|
|
236
|
+
if (code === 'settings-conflict')
|
|
237
|
+
return reject(res, 409, `model update conflicted for provider ${body.provider}`);
|
|
238
|
+
if (code === 'settings-rejected')
|
|
239
|
+
return reject(res, 422, `model update was rejected for provider ${body.provider}`);
|
|
240
|
+
if (code !== undefined)
|
|
241
|
+
return reject(res, 502, `model update failed for provider ${body.provider}`);
|
|
242
|
+
const after = await readCatalog(apiProxy);
|
|
243
|
+
if (after === undefined)
|
|
244
|
+
return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
|
|
245
|
+
writeJson(res, 200, publicCatalog(after));
|
|
246
|
+
};
|
|
247
|
+
return [
|
|
248
|
+
{ kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.catalog, handler: catalog },
|
|
249
|
+
{ kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.discover, handler: discover },
|
|
250
|
+
{ kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.upsert, handler: upsert },
|
|
251
|
+
];
|
|
252
|
+
}
|
package/lib/types/pairing.d.ts
CHANGED
|
@@ -147,10 +147,13 @@ export interface PairingClock {
|
|
|
147
147
|
/** Real clock/entropy: 32 random hex chars per token. */
|
|
148
148
|
export declare const defaultClock: PairingClock;
|
|
149
149
|
/**
|
|
150
|
-
* The pairing state machine.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
150
|
+
* The pairing state machine. Structural mutations issue/accept/stop/revoke
|
|
151
|
+
* and config updates (LAN bases, tunnel, posture) notify state listeners
|
|
152
|
+
* after the commit point that makes them true, and notification dedupes
|
|
153
|
+
* against the last emitted snapshot. Presence-only updates (touchDevice /
|
|
154
|
+
* heartbeat) just mark the store dirty and broadcast on the next sweep,
|
|
155
|
+
* which also surfaces time-driven transitions (a device aging offline)
|
|
156
|
+
* without any mutation.
|
|
154
157
|
*/
|
|
155
158
|
export declare class PairingService {
|
|
156
159
|
config: PairingConfig;
|
|
@@ -259,6 +262,12 @@ export declare class PairingService {
|
|
|
259
262
|
* The api/gate path: record activity for a device id and report whether
|
|
260
263
|
* the request may proceed. Unknown or revoked ids (including any device
|
|
261
264
|
* after stop() or idle expiry) are refused.
|
|
265
|
+
*
|
|
266
|
+
* Presence refreshes are throttled on purpose: every gated request (and
|
|
267
|
+
* the mobile SSE keepalive) lands here, so a broadcast per call would fan
|
|
268
|
+
* a full snapshot out to every status stream on the hot path. The refresh
|
|
269
|
+
* only updates lastSeenAt and marks the store dirty; the snapshot reaches
|
|
270
|
+
* listeners at the next sweep() (structural changes notify immediately).
|
|
262
271
|
* @param deviceId - the cookie value of the requesting device.
|
|
263
272
|
* @returns true when the device session is live and was refreshed.
|
|
264
273
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|
package/lib/types/pairing.js
CHANGED
|
@@ -39,10 +39,13 @@ export const defaultClock = {
|
|
|
39
39
|
randomToken: () => randomBytes(16).toString('hex'),
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
|
-
* The pairing state machine.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* The pairing state machine. Structural mutations issue/accept/stop/revoke
|
|
43
|
+
* and config updates (LAN bases, tunnel, posture) notify state listeners
|
|
44
|
+
* after the commit point that makes them true, and notification dedupes
|
|
45
|
+
* against the last emitted snapshot. Presence-only updates (touchDevice /
|
|
46
|
+
* heartbeat) just mark the store dirty and broadcast on the next sweep,
|
|
47
|
+
* which also surfaces time-driven transitions (a device aging offline)
|
|
48
|
+
* without any mutation.
|
|
46
49
|
*/
|
|
47
50
|
export class PairingService {
|
|
48
51
|
config;
|
|
@@ -297,6 +300,12 @@ export class PairingService {
|
|
|
297
300
|
* The api/gate path: record activity for a device id and report whether
|
|
298
301
|
* the request may proceed. Unknown or revoked ids (including any device
|
|
299
302
|
* after stop() or idle expiry) are refused.
|
|
303
|
+
*
|
|
304
|
+
* Presence refreshes are throttled on purpose: every gated request (and
|
|
305
|
+
* the mobile SSE keepalive) lands here, so a broadcast per call would fan
|
|
306
|
+
* a full snapshot out to every status stream on the hot path. The refresh
|
|
307
|
+
* only updates lastSeenAt and marks the store dirty; the snapshot reaches
|
|
308
|
+
* listeners at the next sweep() (structural changes notify immediately).
|
|
300
309
|
* @param deviceId - the cookie value of the requesting device.
|
|
301
310
|
* @returns true when the device session is live and was refreshed.
|
|
302
311
|
*/
|
|
@@ -306,7 +315,6 @@ export class PairingService {
|
|
|
306
315
|
return false;
|
|
307
316
|
session.lastSeenAt = this.clock.now();
|
|
308
317
|
this.dirty = true;
|
|
309
|
-
this.notify();
|
|
310
318
|
return true;
|
|
311
319
|
}
|
|
312
320
|
/** Explicit presence heartbeat (the phone's client sends these). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAalD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CACjD;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsBxE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA2CnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CA2BjF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-time remote-channel boot patch (issue #987): the browser-half patch
|
|
3
|
+
* (client/remote-channel.ts) installs when this plugin's boot entry runs,
|
|
4
|
+
* but `dsh-client-connection` boots earlier and opens its event streams
|
|
5
|
+
* unrewritten — on a non-loopback origin the SDK fence rejects them and the
|
|
6
|
+
* workspace list never loads. The host therefore inlines this classic script
|
|
7
|
+
* right after the opening <head> tag (webserver/index-inject), so the
|
|
8
|
+
* fetch/WebSocket/EventSource/src rewrite is active before ANY boot entry
|
|
9
|
+
* executes. The plugin's client apply later adopts the installed seat
|
|
10
|
+
* (hooks + pending unpaired signal) instead of patching twice.
|
|
11
|
+
*
|
|
12
|
+
* The rewrite decisions are generated from REMOTE_CHANNEL_RULES, the same
|
|
13
|
+
* data the browser patch consumes — the two cannot drift apart. The script
|
|
14
|
+
* self-skips on loopback origins and never throws.
|
|
15
|
+
* @module @linxin666/dsh-remote-web-ui/remote-channel-boot
|
|
16
|
+
*/
|
|
17
|
+
import { type RemoteChannelRules } from './remote-channel-rules.ts';
|
|
18
|
+
/**
|
|
19
|
+
* Build the inline boot script. The result contains no `</script` sequence
|
|
20
|
+
* (the injection contract) and installs a {@link RemoteChannelBootSeat} on
|
|
21
|
+
* the window global.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildRemoteChannelBootScript(rules?: RemoteChannelRules): string;
|
|
24
|
+
/** The script this plugin contributes; built once from the live rules. */
|
|
25
|
+
export declare const REMOTE_CHANNEL_BOOT_SCRIPT: string;
|
|
26
|
+
//# sourceMappingURL=remote-channel-boot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAuGrG;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-time remote-channel boot patch (issue #987): the browser-half patch
|
|
3
|
+
* (client/remote-channel.ts) installs when this plugin's boot entry runs,
|
|
4
|
+
* but `dsh-client-connection` boots earlier and opens its event streams
|
|
5
|
+
* unrewritten — on a non-loopback origin the SDK fence rejects them and the
|
|
6
|
+
* workspace list never loads. The host therefore inlines this classic script
|
|
7
|
+
* right after the opening <head> tag (webserver/index-inject), so the
|
|
8
|
+
* fetch/WebSocket/EventSource/src rewrite is active before ANY boot entry
|
|
9
|
+
* executes. The plugin's client apply later adopts the installed seat
|
|
10
|
+
* (hooks + pending unpaired signal) instead of patching twice.
|
|
11
|
+
*
|
|
12
|
+
* The rewrite decisions are generated from REMOTE_CHANNEL_RULES, the same
|
|
13
|
+
* data the browser patch consumes — the two cannot drift apart. The script
|
|
14
|
+
* self-skips on loopback origins and never throws.
|
|
15
|
+
* @module @linxin666/dsh-remote-web-ui/remote-channel-boot
|
|
16
|
+
*/
|
|
17
|
+
import { REMOTE_CHANNEL_BOOT_GLOBAL, REMOTE_CHANNEL_RULES } from "./remote-channel-rules.js";
|
|
18
|
+
/**
|
|
19
|
+
* Build the inline boot script. The result contains no `</script` sequence
|
|
20
|
+
* (the injection contract) and installs a {@link RemoteChannelBootSeat} on
|
|
21
|
+
* the window global.
|
|
22
|
+
*/
|
|
23
|
+
export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
24
|
+
const json = JSON.stringify(rules);
|
|
25
|
+
const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
|
|
26
|
+
// Modern syntax is fine: module scripts already gate the GUI to current
|
|
27
|
+
// browsers. Keep everything inside the IIFE and fail closed.
|
|
28
|
+
return '(function(){' +
|
|
29
|
+
'try{' +
|
|
30
|
+
'var w=window,loc=w.location,h=loc.hostname;' +
|
|
31
|
+
// Loopback origins keep the original paths (mirrors isLoopbackHostname).
|
|
32
|
+
"if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
|
|
33
|
+
'var R=' + json + ';' +
|
|
34
|
+
'function sf(p){' +
|
|
35
|
+
'if(p.indexOf(R.pairPrefix)===0)return false;' +
|
|
36
|
+
'if(p.indexOf(R.updatePrefix)===0)return false;' +
|
|
37
|
+
'if(p===R.desktopLauncherPrefix||p.indexOf(R.desktopLauncherPrefix+"/")===0)return false;' +
|
|
38
|
+
'if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+"/")===0)return false;' +
|
|
39
|
+
'if(p.indexOf(R.apiPrefix)===0)return true;' +
|
|
40
|
+
'if(p.indexOf(R.sidebarPrefix)===0||p==="/sidebar")return true;' +
|
|
41
|
+
'if(p.indexOf(R.gitPrefix)===0||p==="/git")return true;' +
|
|
42
|
+
'if(p.indexOf(R.petPrefix)===0||p==="/pet")return true;' +
|
|
43
|
+
'return false}' +
|
|
44
|
+
'function sw(p){return R.wsPaths.indexOf(p)!==-1}' +
|
|
45
|
+
'function rp(p){return R.remotePrefix+p}' +
|
|
46
|
+
'function so(u){return u.origin===loc.origin}' +
|
|
47
|
+
'function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}' +
|
|
48
|
+
'if(u.origin!==loc.origin)return raw;' +
|
|
49
|
+
'if(!sf(u.pathname))return raw;' +
|
|
50
|
+
'u.pathname=rp(u.pathname);' +
|
|
51
|
+
'if(raw.charAt(0)==="/"&&raw.charAt(1)!=="/")return u.pathname+u.search+u.hash;' +
|
|
52
|
+
'return u.href}' +
|
|
53
|
+
// The unpaired code reader mirrors isUnpairedDenied's envelope shapes.
|
|
54
|
+
'function uc(v){' +
|
|
55
|
+
'if(typeof v!=="object"||v===null)return undefined;' +
|
|
56
|
+
'var n=v.result;' +
|
|
57
|
+
'if(typeof n==="object"&&n!==null){' +
|
|
58
|
+
'var e=n.error;' +
|
|
59
|
+
'if(typeof e==="object"&&e!==null&&typeof e.code==="string")return e.code}' +
|
|
60
|
+
'var t=v.error;' +
|
|
61
|
+
'if(typeof t==="object"&&t!==null&&typeof t.code==="string")return t.code;' +
|
|
62
|
+
'return undefined}' +
|
|
63
|
+
'var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};' +
|
|
64
|
+
'function denied(res){' +
|
|
65
|
+
'if(res.status!==403)return false;' +
|
|
66
|
+
'return res.clone().json().then(function(b){return uc(b)==="unpaired"}).catch(function(){return false})}' +
|
|
67
|
+
'function signal(unpaired){' +
|
|
68
|
+
'if(unpaired){' +
|
|
69
|
+
'if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true' +
|
|
70
|
+
'}else{' +
|
|
71
|
+
'seat.pendingUnpaired=false;' +
|
|
72
|
+
'if(seat.onPaired)seat.onPaired()}}' +
|
|
73
|
+
'var of=w.fetch;' +
|
|
74
|
+
'w.fetch=function(input,init){' +
|
|
75
|
+
'var raw=typeof input==="string"||input instanceof URL?input.toString():input.url;' +
|
|
76
|
+
'var url=new URL(raw,loc.href);' +
|
|
77
|
+
'if(so(url)&&sf(url.pathname)){' +
|
|
78
|
+
'var next=new URL(url);' +
|
|
79
|
+
'next.pathname=rp(url.pathname);' +
|
|
80
|
+
'var target=typeof input==="string"||input instanceof URL?next.toString():new Request(next,input);' +
|
|
81
|
+
'return Promise.resolve(of.call(w,target,init)).then(function(res){' +
|
|
82
|
+
// denied() is sync-false for non-403 and a promise otherwise.
|
|
83
|
+
'void Promise.resolve(denied(res)).then(signal);' +
|
|
84
|
+
'return res})}' +
|
|
85
|
+
'return of.call(w,input,init)};' +
|
|
86
|
+
'var OW=w.WebSocket;' +
|
|
87
|
+
'w.WebSocket=function(url,protocols){' +
|
|
88
|
+
'var p=new URL(url.toString(),loc.href);' +
|
|
89
|
+
'var o=p.protocol==="wss:"?"https://"+p.host:p.protocol==="ws:"?"http://"+p.host:"";' +
|
|
90
|
+
'if(o!==""&&o===loc.origin&&sw(p.pathname)){' +
|
|
91
|
+
'var nx=new URL(p);' +
|
|
92
|
+
'nx.pathname=rp(p.pathname);' +
|
|
93
|
+
'return protocols!==undefined?new OW(nx,protocols):new OW(nx)}' +
|
|
94
|
+
'return protocols!==undefined?new OW(url,protocols):new OW(url)};' +
|
|
95
|
+
'w.WebSocket.prototype=OW.prototype;' +
|
|
96
|
+
'w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;' +
|
|
97
|
+
'var OE=w.EventSource;' +
|
|
98
|
+
'if(OE!==undefined){' +
|
|
99
|
+
'w.EventSource=function(url,cfg){' +
|
|
100
|
+
'var p=new URL(url.toString(),loc.href);' +
|
|
101
|
+
'if(so(p)&&sf(p.pathname)){' +
|
|
102
|
+
'var nx=new URL(p);' +
|
|
103
|
+
'nx.pathname=rp(p.pathname);' +
|
|
104
|
+
'return new OE(nx,cfg)}' +
|
|
105
|
+
'return new OE(url,cfg)};' +
|
|
106
|
+
'w.EventSource.prototype=OE.prototype}' +
|
|
107
|
+
// Resource src accessors (img/script/iframe), mirroring patchSrcAccessor.
|
|
108
|
+
'var restores=[];' +
|
|
109
|
+
'function patchSrc(C){' +
|
|
110
|
+
'if(C===undefined)return;' +
|
|
111
|
+
'var d=Object.getOwnPropertyDescriptor(C.prototype,"src");' +
|
|
112
|
+
'if(d===undefined||d.configurable===false||d.set===undefined)return;' +
|
|
113
|
+
'var os=d.set;' +
|
|
114
|
+
'Object.defineProperty(C.prototype,"src",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});' +
|
|
115
|
+
'restores.push(function(){Object.defineProperty(C.prototype,"src",d)})}' +
|
|
116
|
+
'patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);' +
|
|
117
|
+
'seat.restore=function(){' +
|
|
118
|
+
'w.fetch=of;' +
|
|
119
|
+
'w.WebSocket=OW;' +
|
|
120
|
+
'if(OE!==undefined)w.EventSource=OE;' +
|
|
121
|
+
'for(var i=0;i<restores.length;i++)restores[i]();' +
|
|
122
|
+
'try{delete w[' + seat + ']}catch(e){w[' + seat + ']=undefined}};' +
|
|
123
|
+
'w[' + seat + ']=seat' +
|
|
124
|
+
'}catch(e){}' +
|
|
125
|
+
'})();';
|
|
126
|
+
}
|
|
127
|
+
/** The script this plugin contributes; built once from the live rules. */
|
|
128
|
+
export const REMOTE_CHANNEL_BOOT_SCRIPT = buildRemoteChannelBootScript();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote-channel rewrite contract as pure data (issue #987): both the
|
|
3
|
+
* browser patch (client/remote-channel.ts) and the parse-time boot patch
|
|
4
|
+
* (remote-channel-boot.ts, inlined into index.html by the host) decide from
|
|
5
|
+
* these tables, so the two can never drift apart.
|
|
6
|
+
* @module @linxin666/dsh-remote-web-ui/remote-channel-rules
|
|
7
|
+
*/
|
|
8
|
+
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
9
|
+
export declare const REMOTE_PREFIX = "/remote";
|
|
10
|
+
/** Connection-plugin method prefix under the gated channel. */
|
|
11
|
+
export declare const REMOTE_API_PREFIX = "/remote/api";
|
|
12
|
+
/** Every decision input of the remote-channel rewrite, JSON-serializable. */
|
|
13
|
+
export interface RemoteChannelRules {
|
|
14
|
+
readonly remotePrefix: string;
|
|
15
|
+
readonly apiPrefix: string;
|
|
16
|
+
readonly pairPrefix: string;
|
|
17
|
+
readonly updatePrefix: string;
|
|
18
|
+
readonly desktopLauncherPrefix: string;
|
|
19
|
+
readonly settingsBridgePrefix: string;
|
|
20
|
+
readonly sidebarPrefix: string;
|
|
21
|
+
readonly gitPrefix: string;
|
|
22
|
+
readonly petPrefix: string;
|
|
23
|
+
readonly wsPaths: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/** The live rule set. */
|
|
26
|
+
export declare const REMOTE_CHANNEL_RULES: RemoteChannelRules;
|
|
27
|
+
/** The window global the boot patch publishes its seat under. */
|
|
28
|
+
export declare const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
29
|
+
/**
|
|
30
|
+
* The seat the parse-time boot patch installs: hook seats the plugin's
|
|
31
|
+
* client apply adopts, a pending-unpaired flag for signals raised before
|
|
32
|
+
* adoption, and restore() retiring the patch (also removes the global).
|
|
33
|
+
*/
|
|
34
|
+
export interface RemoteChannelBootSeat {
|
|
35
|
+
onUnpaired: (() => void) | null;
|
|
36
|
+
onPaired: (() => void) | null;
|
|
37
|
+
pendingUnpaired: boolean;
|
|
38
|
+
restore(): void;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=remote-channel-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAiBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote-channel rewrite contract as pure data (issue #987): both the
|
|
3
|
+
* browser patch (client/remote-channel.ts) and the parse-time boot patch
|
|
4
|
+
* (remote-channel-boot.ts, inlined into index.html by the host) decide from
|
|
5
|
+
* these tables, so the two can never drift apart.
|
|
6
|
+
* @module @linxin666/dsh-remote-web-ui/remote-channel-rules
|
|
7
|
+
*/
|
|
8
|
+
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
9
|
+
export const REMOTE_PREFIX = '/remote';
|
|
10
|
+
/** Connection-plugin method prefix under the gated channel. */
|
|
11
|
+
export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
|
|
12
|
+
/** The live rule set. */
|
|
13
|
+
export const REMOTE_CHANNEL_RULES = {
|
|
14
|
+
remotePrefix: REMOTE_PREFIX,
|
|
15
|
+
apiPrefix: '/api/',
|
|
16
|
+
pairPrefix: '/api/pair/',
|
|
17
|
+
updatePrefix: '/api/update/',
|
|
18
|
+
desktopLauncherPrefix: '/api/dsh-desktop-launcher',
|
|
19
|
+
settingsBridgePrefix: '/api/dsh-web-ui-settings',
|
|
20
|
+
sidebarPrefix: '/sidebar/',
|
|
21
|
+
gitPrefix: '/git/',
|
|
22
|
+
petPrefix: '/pet/',
|
|
23
|
+
wsPaths: [
|
|
24
|
+
'/api/events.mux',
|
|
25
|
+
'/api/events.host',
|
|
26
|
+
'/sidebar/ws/terminal',
|
|
27
|
+
'/sidebar/ws/agent-terminals',
|
|
28
|
+
'/api/dsh-ssh/terminal',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
/** The window global the boot patch publishes its seat under. */
|
|
32
|
+
export const REMOTE_CHANNEL_BOOT_GLOBAL = '__DSH_REMOTE_CHANNEL_BOOT__';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;;CAQb,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;;CAQb,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AAoBjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;gBACS,OAAO,EAAE,cAAc;IAInC,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;IAiBrD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYrC,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,sEAAsE;IACtE,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CACjD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAkO3D"}
|