device-center 0.2.0-beta.3 → 0.2.0-beta.5
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/CONTRIBUTING.md +2 -0
- package/Dockerfile +2 -1
- package/README.md +24 -7
- package/SECURITY.md +8 -2
- package/bin/device-center.mjs +6 -5
- package/docs/agent-install.md +43 -9
- package/docs/driver-binding.md +14 -4
- package/docs/friend-trial.md +5 -5
- package/docs/hosted-access.md +1 -1
- package/docs/npm.md +13 -9
- package/docs/onboarding.md +5 -3
- package/docs/remote-access.md +2 -2
- package/docs/roadmap.md +13 -1
- package/docs/security-and-sync.md +4 -0
- package/docs/troubleshooting.md +11 -0
- package/docs/windows-acceptance.md +57 -0
- package/favicon.svg +4 -0
- package/index.html +1 -0
- package/login.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -2
- package/scripts/access.mjs +0 -1
- package/scripts/driver.mjs +27 -11
- package/scripts/service.mjs +42 -16
- package/server/cloudAccess.mjs +7 -1
- package/server/drivers/access.mjs +15 -14
- package/server/drivers/browserPairing.mjs +46 -0
- package/server/drivers/cloudLink.mjs +19 -20
- package/server/drivers/cloudRegistry.mjs +23 -6
- package/server/drivers/privateFiles.mjs +57 -0
- package/server/drivers/windowsSecurity.mjs +119 -0
- package/server/drivers/windowsService.mjs +77 -0
- package/server/index.mjs +30 -7
- package/server/unifiedAccess.mjs +24 -4
- package/src/api.js +4 -0
- package/src/install-prompt.js +2 -2
- package/src/install-walkthrough.js +49 -0
- package/src/login-flow.js +37 -0
- package/src/login.css +3 -1
- package/src/login.js +20 -11
- package/src/main.js +20 -12
- package/src/messages.js +1745 -162
- package/src/onboarding.js +60 -17
- package/src/pair.css +23 -0
- package/src/pair.js +60 -0
- package/src/pairing-handoff.js +30 -0
- package/src/start.css +265 -45
- package/src/start.js +155 -26
- package/start.html +143 -27
package/src/main.js
CHANGED
|
@@ -12,7 +12,7 @@ Object.assign(state, { pairCode: '', pairing: null, pairBusy: false, pairError:
|
|
|
12
12
|
Object.assign(state, { cloudChoice: 'official', customOrigin: '', logManager: '', logTarget: '', logPath: '', logLabel: 'app', remotePeers: [], remoteLoading: false, remoteError: null });
|
|
13
13
|
let cloudRefreshTimer, cloudRefreshBusy = false, cloudRefreshFailures = 0;
|
|
14
14
|
Object.assign(state, { profileDevice: null, profileDraft: null, profileError: null, profileSaving: false, collectingDevice: null, pendingCollections: {} });
|
|
15
|
-
Object.assign(state, { installMode: 'local', installOrigin: '', installBackground: true });
|
|
15
|
+
Object.assign(state, { installMode: 'local', installOs: 'posix', installOrigin: '', installBackground: true });
|
|
16
16
|
const RELAY_PROVIDERS = [
|
|
17
17
|
{ id: 'self-hosted', get name() { return t("panel.1"); }, get description() { return t("panel.2"); } },
|
|
18
18
|
{ id: 'managed', get name() { return t("panel.3"); }, get description() { return t("panel.4"); } },
|
|
@@ -234,12 +234,17 @@ function openAgentInstall() {
|
|
|
234
234
|
state.toast = null; state.dialog = 'agent-install';
|
|
235
235
|
}
|
|
236
236
|
function installDraft() {
|
|
237
|
-
try { return { text: agentInstallPrompt({ locale: getLocale(), mode: state.installMode, background: state.installBackground, ...(state.installMode === 'self-hosted' ? { origin: state.installOrigin.trim() } : {}) }), error: null }; }
|
|
237
|
+
try { return { text: agentInstallPrompt({ locale: getLocale(), mode: state.installMode, os: state.installOs, background: state.installBackground, ...(state.installMode === 'self-hosted' ? { origin: state.installOrigin.trim() } : {}) }), error: null }; }
|
|
238
238
|
catch (error) { return { text: '', error: t(error.translationKey ?? 'error.install') }; }
|
|
239
239
|
}
|
|
240
|
+
function manualInstallHref(draft) {
|
|
241
|
+
const manualParams = new URLSearchParams({ lang: getLocale(), method: 'manual', mode: state.installMode, os: state.installOs });
|
|
242
|
+
if (state.installMode === 'self-hosted' && !draft.error) manualParams.set('origin', state.installOrigin.trim());
|
|
243
|
+
return `/start?${manualParams}#install-title`;
|
|
244
|
+
}
|
|
240
245
|
function agentInstallBody() {
|
|
241
246
|
const draft = installDraft();
|
|
242
|
-
return `<p class="modal-intro">${h("panel.219")}</p><div class="method-tabs install-mode-tabs" role="group" aria-label="${h("panel.220")}">${[['local', t("panel.210")], ['official', t("panel.211")], ['self-hosted', t("panel.212")]].map(([mode, label]) => `<button data-action="install-mode" data-mode="${mode}" aria-pressed="${state.installMode === mode}" class="${state.installMode === mode ? 'active' : ''}">${label}</button>`).join('')}</div><p class="small-note">${state.installMode === 'local' ? t("panel.213") : state.installMode === 'official' ? t("panel.214") : t("panel.215")}</p>${state.installMode === 'self-hosted' ? `<label class="field-label" for="install-origin">${h("panel.216")}</label><input id="install-origin" data-field="installOrigin" type="url" value="${escapeHtml(state.installOrigin)}" placeholder="https://devices.example.com" autocomplete="off" spellcheck="false">` : ''}<label class="binding-scope"><input type="checkbox" data-field="installBackground" ${state.installBackground ? 'checked' : ''}>${h("panel.221")}</label><ol class="install-flow"><li>${h("panel.222")}</li><li>${state.installMode === 'local' ? t("panel.217") : t("panel.218")}</li><li>${h("panel.223")}</li></ol><p id="install-error" class="form-error" role="alert" ${draft.error ? '' : 'hidden'}>${escapeHtml(draft.error ?? '')}</p><button class="button button-primary install-copy" data-action="agent-install-copy" ${draft.error ? 'disabled' : ''}>${icon('copy', 15)} ${h("panel.224")}</button><details class="install-details"><summary>${h("panel.225")}</summary><textarea id="agent-install-text" aria-label="${h("panel.226")}" readonly rows="7" spellcheck="false">${escapeHtml(draft.text)}</textarea></details><p class="small-note">${h("panel.227")}</p>`;
|
|
247
|
+
return `<p class="modal-intro">${h("panel.219")}</p><label class="field-label" for="install-os">${h("guide.os.title")}</label><select id="install-os" data-field="installOs"><option value="posix" ${state.installOs === "posix" ? "selected" : ""}>macOS / Linux</option><option value="windows" ${state.installOs === "windows" ? "selected" : ""}>Windows 11</option></select><p class="small-note">${h("guide.windowsCandidate")}</p><p class="small-note"><a data-manual-install href="${escapeHtml(manualInstallHref(draft))}">${h('guide.manual.openGuide')}</a></p><div class="method-tabs install-mode-tabs" role="group" aria-label="${h("panel.220")}">${[['local', t("panel.210")], ['official', t("panel.211")], ['self-hosted', t("panel.212")]].map(([mode, label]) => `<button data-action="install-mode" data-mode="${mode}" aria-pressed="${state.installMode === mode}" class="${state.installMode === mode ? 'active' : ''}">${label}</button>`).join('')}</div><p class="small-note">${state.installMode === 'local' ? t("panel.213") : state.installMode === 'official' ? t("panel.214") : t("panel.215")}</p>${state.installMode === 'self-hosted' ? `<label class="field-label" for="install-origin">${h("panel.216")}</label><input id="install-origin" data-field="installOrigin" type="url" value="${escapeHtml(state.installOrigin)}" placeholder="https://devices.example.com" autocomplete="off" spellcheck="false">` : ''}<label class="binding-scope"><input type="checkbox" data-field="installBackground" ${state.installBackground ? 'checked' : ''}>${h("panel.221")}</label><ol class="install-flow"><li>${h("panel.222")}</li><li>${state.installMode === 'local' ? t("panel.217") : t("panel.218")}</li><li>${h("panel.223")}</li></ol><p id="install-error" class="form-error" role="alert" ${draft.error ? '' : 'hidden'}>${escapeHtml(draft.error ?? '')}</p><button class="button button-primary install-copy" data-action="agent-install-copy" ${draft.error ? 'disabled' : ''}>${icon('copy', 15)} ${h("panel.224")}</button><details class="install-details"><summary>${h("panel.225")}</summary><textarea id="agent-install-text" aria-label="${h("panel.226")}" readonly rows="7" spellcheck="false">${escapeHtml(draft.text)}</textarea></details><p class="small-note">${h("panel.227")}</p>`;
|
|
243
248
|
}
|
|
244
249
|
async function copyAgentInstall() {
|
|
245
250
|
const field = root.querySelector('#agent-install-text');
|
|
@@ -306,7 +311,7 @@ function cloudConnectBody() {
|
|
|
306
311
|
function logCommandDraft() {
|
|
307
312
|
const peers = cloudMode() ? state.overview?.drivers?.filter(p=>p.revokedAt == null) ?? [] : state.remotePeers;
|
|
308
313
|
const manager = peers.find(p=>p.id === state.logManager), target = peers.find(p=>p.id === state.logTarget);
|
|
309
|
-
try { return { ...logAccessCommands({manager:manager?.fingerprint,target:target?.fingerprint,path:state.logPath,label:state.logLabel}), error:null }; }
|
|
314
|
+
try { return { ...logAccessCommands({manager:manager?.fingerprint,target:target?.fingerprint,path:state.logPath,label:state.logLabel,os:/Windows/i.test(target?.platform ?? '')?'windows':'posix',managerOs:/Windows/i.test(manager?.platform ?? '')?'windows':'posix'}), error:null }; }
|
|
310
315
|
catch(error) { return {trust:'',grant:'',error:t(error.translationKey ?? 'error.devices')}; }
|
|
311
316
|
}
|
|
312
317
|
function logCommandFields() {
|
|
@@ -319,7 +324,7 @@ function logCommandFields() {
|
|
|
319
324
|
return `<div class="plain-panel"><h3>${h("panel.319")}</h3><label class="field-label" for="log-manager">${h("panel.320")}</label><select id="log-manager" data-log-field="logManager">${options(state.logManager)}</select><label class="field-label" for="log-target">${h("panel.321")}</label><select id="log-target" data-log-field="logTarget">${options(state.logTarget)}</select><label class="field-label" for="log-path">${h("panel.322")}</label><input id="log-path" data-log-field="logPath" value="${escapeHtml(state.logPath)}" placeholder="/home/me/app.log" autocomplete="off" spellcheck="false"><label class="field-label" for="log-label">${h("panel.323")}</label><input id="log-label" data-log-field="logLabel" value="${escapeHtml(state.logLabel)}" maxlength="40"><p id="log-command-error" class="small-note">${escapeHtml(draft.error??t("panel.318"))}</p><label class="field-label" for="log-trust-command">${h("panel.324")}</label><textarea id="log-trust-command" readonly rows="2">${escapeHtml(draft.trust)}</textarea><button class="button button-light" data-action="log-command-copy" data-field-id="log-trust-command" ${draft.error?'disabled':''}>${h("panel.325")}</button><label class="field-label" for="log-grant-command">${h("panel.326")}</label><textarea id="log-grant-command" readonly rows="3">${escapeHtml(draft.grant)}</textarea><button class="button button-primary" data-action="log-command-copy" data-field-id="log-grant-command" ${draft.error?'disabled':''}>${h("panel.327")}</button><p class="small-note">${h("panel.328")}</p></div>`;
|
|
320
325
|
}
|
|
321
326
|
function secureHelpBody() {
|
|
322
|
-
return `<p class="modal-intro">${h("panel.329")}</p>${logCommandFields()}<ol class="simple-steps"><li>${h("panel.330")} <code>npm install -g device-center@0.2.0-beta.
|
|
327
|
+
return `<p class="modal-intro">${h("panel.329")}</p>${logCommandFields()}<ol class="simple-steps"><li>${h("panel.330")} <code>npm install -g device-center@0.2.0-beta.4</code>${h("panel.331")} <code>device-center access enable --confirm</code>.</li><li>${h("panel.332")} <code>device-center access peers</code>${h("panel.333")}</li><li>${h("panel.334")}</li></ol><details open><summary>${h("panel.324")}</summary><code class="block-code">${h("panel.335")}</code></details><details open><summary>${h("panel.326")}</summary><code class="block-code">${h("panel.336")}</code></details><p>${h("panel.337")} <code>device_center_read_log</code>${h("panel.338")}</p><details><summary>${h("panel.339")}</summary><p>${h("panel.340")} <code>device-center access listen --bind 192.168.1.10 --port 5181 --confirm</code>${h("panel.341")} <code>--lan 192.168.1.10 --port 5181</code>${h("panel.342")}</p><p>${h("panel.343")}</p></details><p class="small-note">${h("panel.344")} <code>${h("panel.345")}</code> ${h("panel.346")}</p>`;
|
|
323
328
|
}
|
|
324
329
|
function cloudBindingBody() {
|
|
325
330
|
const pairing = state.pairing, disabled = state.pairBusy ? 'disabled' : '';
|
|
@@ -327,9 +332,9 @@ function cloudBindingBody() {
|
|
|
327
332
|
const bindings = state.overview?.drivers ?? [];
|
|
328
333
|
return `<p class="modal-intro">${h("panel.365")}</p>
|
|
329
334
|
<section class="binding-step"><h3><span>1</span> ${h("panel.366")}</h3><p>${h("panel.367")}</p><details><summary>${h("panel.368")}</summary><a class="button button-light" href="/api/driver/download" download>${h("panel.369")}</a><p>${h("panel.370")} <code>npm ci --ignore-scripts</code>${h("panel.371")} <code>${escapeHtml(sourceCommand)}</code>${h("panel.372")}</p></details></section>
|
|
330
|
-
<section class="binding-step"><h3><span>2</span> ${h("panel.373")}</h3><label class="binding-scope"><input id="request-ssh" type="checkbox" data-field="requestSsh" ${state.requestSsh ? 'checked' : ''} ${disabled}>${h("panel.374")}</label><textarea id="driver-command" readonly rows="3" spellcheck="false" aria-label="${h("panel.375")}">${escapeHtml(driverCommand())}</textarea><button class="button button-primary" data-action="driver-copy">${icon('copy', 15)} ${h("panel.309")}</button><p>${h("
|
|
331
|
-
<
|
|
332
|
-
${pairing ? `<div class="pair-review"><strong>${escapeHtml(pairing.name)}</strong><p>${escapeHtml(pairing.platform)} ${h("panel.350")} ${escapeHtml(new Date(pairing.expiresAt).toLocaleTimeString(getLocale()))}</p><label class="field-label">${h("panel.351")}</label><code class="fingerprint">${escapeHtml(pairing.fingerprint)}</code><p>${h("panel.352")}</p>${pairing.requestedSsh ? `<label class="binding-scope"><input id="pair-share" type="checkbox" data-field="pairShare" ${state.pairShare ? 'checked' : ''} ${disabled}>${h("panel.349")}</label>` : ''}<button class="button button-primary" data-action="pair-approve" ${disabled}>${h("panel.353")}</button></div>` : `<p class="small-note">${h("panel.354")}</p>`}</
|
|
335
|
+
<section class="binding-step"><h3><span>2</span> ${h("panel.373")}</h3><label class="binding-scope"><input id="request-ssh" type="checkbox" data-field="requestSsh" ${state.requestSsh ? 'checked' : ''} ${disabled}>${h("panel.374")}</label><textarea id="driver-command" readonly rows="3" spellcheck="false" aria-label="${h("panel.375")}">${escapeHtml(driverCommand())}</textarea><button class="button button-primary" data-action="driver-copy">${icon('copy', 15)} ${h("panel.309")}</button><p>${h("pair.browserHint")}</p></section>
|
|
336
|
+
<details class="binding-step" ${pairing || state.pairError ? 'open' : ''}><summary>${h("pair.manual")}</summary><p>${h("pair.manualHint")}</p><textarea id="manual-driver-command" readonly rows="3" spellcheck="false" aria-label="${h("panel.375")}">${escapeHtml(driverCommand().replace(' --browser', ''))}</textarea><button class="button button-light" data-action="driver-manual-copy">${h("panel.309")}</button><h3><span>3</span> ${h("panel.377")}</h3><div class="pair-code-row"><label for="pair-code" class="sr-only">${h("panel.378")}</label><input id="pair-code" data-field="pairCode" autocomplete="off" spellcheck="false" maxlength="9" placeholder="XXXX-XXXX" value="${escapeHtml(state.pairCode)}" ${disabled}><button class="button button-light" data-action="pair-lookup" ${disabled}>${state.pairBusy ? t("panel.347") : t("panel.348")}</button></div>${state.pairError ? `<p class="form-error" role="alert">${escapeHtml(t(state.pairError))}</p>` : ''}
|
|
337
|
+
${pairing ? `<div class="pair-review"><strong>${escapeHtml(pairing.name)}</strong><p>${escapeHtml(pairing.platform)} ${h("panel.350")} ${escapeHtml(new Date(pairing.expiresAt).toLocaleTimeString(getLocale()))}</p><label class="field-label">${h("panel.351")}</label><code class="fingerprint">${escapeHtml(pairing.fingerprint)}</code><p>${h("panel.352")}</p>${pairing.requestedSsh ? `<label class="binding-scope"><input id="pair-share" type="checkbox" data-field="pairShare" ${state.pairShare ? 'checked' : ''} ${disabled}>${h("panel.349")}</label>` : ''}<button class="button button-primary" data-action="pair-approve" ${disabled}>${h("panel.353")}</button></div>` : `<p class="small-note">${h("panel.354")}</p>`}</details>
|
|
333
338
|
${bindings.length ? `<details class="binding-management" open><summary>${h("count.active", { count: bindings.filter(item => item.revokedAt == null).length })}</summary>${bindings.map(item => `<div class="binding-row"><div><strong>${escapeHtml(item.name)}</strong><small>${item.revokedAt != null ? t("panel.355") : item.lastSeen == null ? t("panel.356") : `${h("panel.14")} ${formatAgo(item.lastSeen)}`}</small><code class="fingerprint">${escapeHtml(item.fingerprint)}</code></div>${item.revokedAt == null ? state.revokeId === item.id ? `<div class="revoke-confirm"><p>${h("panel.357")}</p><button class="button button-light" data-action="revoke-confirm" data-device="${item.id}" ${disabled}>${h("panel.358")}</button><button class="text-button" data-action="revoke-cancel" ${disabled}>${h("panel.359")}</button></div>` : `<button class="text-button" data-action="revoke-arm" data-device="${item.id}" ${disabled}>${h("panel.360")}</button>` : ''}</div>`).join('')}</details>` : ''}
|
|
334
339
|
<p class="small-note">${state.overview?.accountMode === 'multi-user' ? t("panel.363") : t("panel.364")}${h("panel.379")}</p>`;
|
|
335
340
|
}
|
|
@@ -350,13 +355,14 @@ async function pairAction(action, id) {
|
|
|
350
355
|
} catch (error) { state.pairError = pairErrorText(error.message); }
|
|
351
356
|
finally { state.pairBusy = false; render(); }
|
|
352
357
|
}
|
|
353
|
-
async function copyDriverCommand() {
|
|
354
|
-
const
|
|
358
|
+
async function copyDriverCommand(manual = false) {
|
|
359
|
+
const selector = manual ? '#manual-driver-command' : state.dialog === 'cloud-connect' ? '#cloud-connect-command' : '#driver-command';
|
|
360
|
+
const field = root.querySelector(selector); if (!field || !field.value) return;
|
|
355
361
|
field.focus(); field.select(); let copied = false;
|
|
356
362
|
try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(field.value); copied = true; } } catch { /* manual selection remains available */ }
|
|
357
363
|
if (!copied) try { copied = document.execCommand('copy'); } catch { /* manual selection */ }
|
|
358
364
|
toast(copied ? t("panel.390") : t("panel.391"));
|
|
359
|
-
if (!copied) { const current = root.querySelector(
|
|
365
|
+
if (!copied) { const current = root.querySelector(selector); current?.focus(); current?.select(); }
|
|
360
366
|
}
|
|
361
367
|
function initializationBody() {
|
|
362
368
|
if (state.sshLoading) return `<p class="modal-intro">${h("panel.392")}</p>`;
|
|
@@ -535,7 +541,7 @@ root.addEventListener('click', async (event) => {
|
|
|
535
541
|
if (action === 'agent-install-copy') return copyAgentInstall();
|
|
536
542
|
if (action === 'logout') { try { await api.logout(); location.replace('/login?lang=' + getLocale()); } catch { state.toast = t("panel.491"); render(); } return; }
|
|
537
543
|
if (cloudMode() && action === 'add') { state.dialog = 'add'; state.toast = null; state.pairError = null; state.revokeId = null; render(); return; }
|
|
538
|
-
if (['driver-copy', 'cloud-command-copy'].includes(action)) return copyDriverCommand();
|
|
544
|
+
if (['driver-copy', 'cloud-command-copy', 'driver-manual-copy'].includes(action)) return copyDriverCommand(action === 'driver-manual-copy');
|
|
539
545
|
if (action === 'secure-help') { state.dialog='secure-help'; state.toast=null; state.remoteError=null; render(); if (!cloudMode()) { state.remoteLoading=true; render(); try { const data=await api.remotePeers(); state.remotePeers=data.peers; state.logManager=data.driverId??''; } catch { state.remoteError=true; } finally { state.remoteLoading=false; render(); } } return; }
|
|
540
546
|
if (action === 'log-command-copy') { const field=root.querySelector('#'+button.dataset.fieldId); if(!field?.value)return;field.focus();field.select();try {await navigator.clipboard.writeText(field.value);toast(t("panel.492"));}catch{toast(t("panel.493"));} return; }
|
|
541
547
|
if (action === 'cloud-connect') { state.dialog = 'cloud-connect'; state.toast = null; render(); return; }
|
|
@@ -578,6 +584,7 @@ root.addEventListener('click', async (event) => {
|
|
|
578
584
|
render();
|
|
579
585
|
});
|
|
580
586
|
root.addEventListener('change', (event) => {
|
|
587
|
+
if (event.target.dataset.field === 'installOs') { state.installOs = event.target.value; state.toast = null; render(); return; }
|
|
581
588
|
if (event.target.dataset.field === 'installBackground') { state.installBackground = event.target.checked; render(); return; }
|
|
582
589
|
if (event.target.dataset.profileField && state.profileDraft) { state.profileDraft[event.target.dataset.profileField] = event.target.value; return; }
|
|
583
590
|
if (['requestSsh', 'pairShare'].includes(event.target.dataset.field)) { state[event.target.dataset.field] = event.target.checked; render(); return; }
|
|
@@ -594,6 +601,7 @@ root.addEventListener('input', (event) => {
|
|
|
594
601
|
root.querySelector('#agent-install-text').value = draft.text;
|
|
595
602
|
const message = root.querySelector('#install-error'); message.textContent = draft.error ?? ''; message.hidden = !draft.error;
|
|
596
603
|
root.querySelector('[data-action="agent-install-copy"]').disabled = Boolean(draft.error);
|
|
604
|
+
root.querySelector('[data-manual-install]').href = manualInstallHref(draft);
|
|
597
605
|
return;
|
|
598
606
|
}
|
|
599
607
|
if (event.target.dataset.profileField && state.profileDraft) { state.profileDraft[event.target.dataset.profileField] = event.target.value; return; }
|