@dollhousemcp/mcp-server 2.0.11 → 2.0.12-rc.2
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/CHANGELOG.md +33 -0
- package/dist/config/ConfigManager.d.ts +34 -0
- package/dist/config/ConfigManager.d.ts.map +1 -1
- package/dist/config/ConfigManager.js +46 -1
- package/dist/config/env.d.ts +18 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +89 -2
- package/dist/di/Container.d.ts +1 -0
- package/dist/di/Container.d.ts.map +1 -1
- package/dist/di/Container.js +38 -2
- package/dist/elements/agents/AgentManager.js +2 -2
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.d.ts.map +1 -1
- package/dist/generated/version.js +3 -3
- package/dist/handlers/mcp-aql/GatekeeperSession.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/GatekeeperSession.js +3 -2
- package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/MCPAQLHandler.js +11 -3
- package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/OperationSchema.js +11 -6
- package/dist/handlers/mcp-aql/policies/ToolClassification.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/policies/ToolClassification.js +8 -1
- package/dist/index.js +53 -3
- package/dist/security/InputNormalizer.d.ts +4 -6
- package/dist/security/InputNormalizer.d.ts.map +1 -1
- package/dist/security/InputNormalizer.js +10 -16
- package/dist/security/audit/config/suppressions.d.ts.map +1 -1
- package/dist/security/audit/config/suppressions.js +26 -1
- package/dist/security/constants.d.ts +7 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security/constants.js +10 -1
- package/dist/security/contentValidator.d.ts.map +1 -1
- package/dist/security/contentValidator.js +14 -6
- package/dist/security/encryption/PatternEncryptor.d.ts.map +1 -1
- package/dist/security/encryption/PatternEncryptor.js +18 -10
- package/dist/security/securityMonitor.d.ts +1 -1
- package/dist/security/securityMonitor.d.ts.map +1 -1
- package/dist/security/securityMonitor.js +1 -1
- package/dist/security/tokenManager.d.ts +18 -2
- package/dist/security/tokenManager.d.ts.map +1 -1
- package/dist/security/tokenManager.js +47 -15
- package/dist/security/validators/unicodeValidator.d.ts +4 -5
- package/dist/security/validators/unicodeValidator.d.ts.map +1 -1
- package/dist/security/validators/unicodeValidator.js +19 -20
- package/dist/services/VerificationNotifier.d.ts.map +1 -1
- package/dist/services/VerificationNotifier.js +15 -13
- package/dist/services/validation/GenericElementValidator.js +5 -5
- package/dist/web/console/IngestRoutes.d.ts +14 -2
- package/dist/web/console/IngestRoutes.d.ts.map +1 -1
- package/dist/web/console/IngestRoutes.js +69 -5
- package/dist/web/console/LeaderElection.d.ts +37 -3
- package/dist/web/console/LeaderElection.d.ts.map +1 -1
- package/dist/web/console/LeaderElection.js +67 -7
- package/dist/web/console/LeaderForwardingSink.d.ts +19 -3
- package/dist/web/console/LeaderForwardingSink.d.ts.map +1 -1
- package/dist/web/console/LeaderForwardingSink.js +44 -9
- package/dist/web/console/PromotionManager.d.ts +29 -0
- package/dist/web/console/PromotionManager.d.ts.map +1 -0
- package/dist/web/console/PromotionManager.js +94 -0
- package/dist/web/console/SessionNames.d.ts +7 -0
- package/dist/web/console/SessionNames.d.ts.map +1 -1
- package/dist/web/console/SessionNames.js +10 -1
- package/dist/web/console/UnifiedConsole.d.ts +26 -1
- package/dist/web/console/UnifiedConsole.d.ts.map +1 -1
- package/dist/web/console/UnifiedConsole.js +143 -20
- package/dist/web/portDiscovery.d.ts +4 -3
- package/dist/web/portDiscovery.d.ts.map +1 -1
- package/dist/web/portDiscovery.js +7 -5
- package/dist/web/public/app.js +35 -18
- package/dist/web/public/consoleAuth.js +152 -0
- package/dist/web/public/index.html +243 -9
- package/dist/web/public/logs.js +1 -1
- package/dist/web/public/metrics.js +3 -3
- package/dist/web/public/permissions.js +1 -1
- package/dist/web/public/security.css +396 -0
- package/dist/web/public/security.js +393 -0
- package/dist/web/public/sessions.css +54 -4
- package/dist/web/public/sessions.js +46 -6
- package/dist/web/public/setup.css +439 -0
- package/dist/web/public/setup.js +490 -18
- package/dist/web/public/styles.css +10 -0
- package/dist/web/routes/permissionRoutes.d.ts.map +1 -1
- package/dist/web/routes/permissionRoutes.js +8 -3
- package/dist/web/routes/setupRoutes.d.ts +4 -0
- package/dist/web/routes/setupRoutes.d.ts.map +1 -1
- package/dist/web/routes/setupRoutes.js +418 -7
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +9 -5
- package/dist/web/server.d.ts +49 -3
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +282 -82
- package/package.json +11 -2
- package/server.json +2 -2
package/dist/web/public/setup.js
CHANGED
|
@@ -39,24 +39,25 @@
|
|
|
39
39
|
const npxCmd = (tag) => `npx -y ${PKG}@${tag}`;
|
|
40
40
|
|
|
41
41
|
/** Build all platform configs for a given pinned version */
|
|
42
|
-
function buildConfigs(version) {
|
|
42
|
+
function buildConfigs(version, channel = 'latest') {
|
|
43
|
+
const ch = channel;
|
|
43
44
|
const result = {};
|
|
44
45
|
for (const { id, rootKey, cli, toml } of PLATFORMS) {
|
|
45
46
|
const entry = {
|
|
46
47
|
npx: cli
|
|
47
|
-
? { code: `${cli} mcp add dollhousemcp -- ${npxCmd(
|
|
48
|
-
: jsonConfig(rootKey, npxCmd(
|
|
48
|
+
? { code: `${cli} mcp add dollhousemcp -- ${npxCmd(ch)}`, isTerminal: true }
|
|
49
|
+
: jsonConfig(rootKey, npxCmd(ch)),
|
|
49
50
|
global: cli
|
|
50
51
|
? { code: `${cli} mcp add dollhousemcp -- ${npxCmd(version)}`, isTerminal: true }
|
|
51
52
|
: jsonConfig(rootKey, npxCmd(version)),
|
|
52
53
|
};
|
|
53
54
|
if (cli) {
|
|
54
|
-
entry.npxJson = jsonConfig(rootKey, npxCmd(
|
|
55
|
+
entry.npxJson = jsonConfig(rootKey, npxCmd(ch));
|
|
55
56
|
entry.globalJson = jsonConfig(rootKey, npxCmd(version));
|
|
56
57
|
}
|
|
57
58
|
if (toml) {
|
|
58
59
|
const tomlBlock = (tag) => `[mcp_servers.dollhousemcp]\ncommand = "npx"\nargs = ["-y", "${PKG}@${tag}"]`;
|
|
59
|
-
entry.npxToml = { code: tomlBlock(
|
|
60
|
+
entry.npxToml = { code: tomlBlock(ch) };
|
|
60
61
|
entry.globalToml = { code: tomlBlock(version) };
|
|
61
62
|
}
|
|
62
63
|
result[id] = entry;
|
|
@@ -66,7 +67,8 @@
|
|
|
66
67
|
|
|
67
68
|
// Start with a placeholder version, update once we fetch from server
|
|
68
69
|
let pinnedVersion = 'latest';
|
|
69
|
-
let
|
|
70
|
+
let currentChannel = 'latest';
|
|
71
|
+
let configs = buildConfigs(pinnedVersion, currentChannel);
|
|
70
72
|
|
|
71
73
|
// ── Current method state ──────────────────────────────────────────────
|
|
72
74
|
|
|
@@ -111,6 +113,7 @@
|
|
|
111
113
|
// Cache DOM elements queried on every toggle click
|
|
112
114
|
const prereq = document.getElementById('setup-pinned-prereq');
|
|
113
115
|
const mcpbSection = document.getElementById('setup-mcpb-section');
|
|
116
|
+
const channelToggle = document.getElementById('setup-channel-toggle');
|
|
114
117
|
|
|
115
118
|
const handleToggle = (btn) => {
|
|
116
119
|
const method = btn.dataset.method;
|
|
@@ -125,6 +128,7 @@
|
|
|
125
128
|
|
|
126
129
|
if (prereq) prereq.hidden = method !== 'global';
|
|
127
130
|
if (mcpbSection) mcpbSection.hidden = method !== 'global';
|
|
131
|
+
if (channelToggle) channelToggle.hidden = method === 'global';
|
|
128
132
|
|
|
129
133
|
updateAllConfigs(method);
|
|
130
134
|
updateInstallButtonLabels();
|
|
@@ -134,6 +138,34 @@
|
|
|
134
138
|
buttons.forEach((btn) => {
|
|
135
139
|
btn.addEventListener('click', () => handleToggle(btn));
|
|
136
140
|
});
|
|
141
|
+
|
|
142
|
+
// Sync initial visibility — if the browser restored a non-default
|
|
143
|
+
// active button (e.g. pinned was selected before reload), apply
|
|
144
|
+
// the hidden state now without waiting for a click.
|
|
145
|
+
if (channelToggle) channelToggle.hidden = currentMethod === 'global';
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ── Channel selector ──────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** User-facing descriptions for each release channel, shown below the selector. */
|
|
151
|
+
const CHANNEL_HINTS = {
|
|
152
|
+
latest: 'Recommended for most users.',
|
|
153
|
+
rc: 'Preview of the next stable release. May have minor issues.',
|
|
154
|
+
beta: 'Cutting-edge features. May be unstable.',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const initChannelSelector = () => {
|
|
158
|
+
const select = document.getElementById('setup-channel-select');
|
|
159
|
+
const hint = document.getElementById('setup-channel-hint');
|
|
160
|
+
if (!select) return;
|
|
161
|
+
|
|
162
|
+
select.addEventListener('change', () => {
|
|
163
|
+
currentChannel = select.value;
|
|
164
|
+
if (hint) hint.textContent = CHANNEL_HINTS[currentChannel] || '';
|
|
165
|
+
configs = buildConfigs(pinnedVersion, currentChannel);
|
|
166
|
+
updateAllConfigs(currentMethod);
|
|
167
|
+
updateInstallButtonLabels();
|
|
168
|
+
});
|
|
137
169
|
};
|
|
138
170
|
|
|
139
171
|
/** Rewrite code blocks and copy-text for the selected method */
|
|
@@ -285,9 +317,10 @@
|
|
|
285
317
|
const isPinned = currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest';
|
|
286
318
|
|
|
287
319
|
// Update Install buttons
|
|
320
|
+
const channelLabel = currentChannel === 'latest' ? '' : ` (${currentChannel})`;
|
|
288
321
|
document.querySelectorAll('.setup-install-btn').forEach((btn) => {
|
|
289
322
|
if (btn.classList.contains('is-success') || btn.classList.contains('is-match')) return;
|
|
290
|
-
btn.textContent = isPinned ? `
|
|
323
|
+
btn.textContent = isPinned ? `Configure v${pinnedVersion}` : `Configure Now${channelLabel}`;
|
|
291
324
|
});
|
|
292
325
|
|
|
293
326
|
// Update auto-install badges and descriptions
|
|
@@ -311,7 +344,7 @@
|
|
|
311
344
|
|
|
312
345
|
// ── Install buttons ────────────────────────────────────────────────────
|
|
313
346
|
|
|
314
|
-
/** Handle
|
|
347
|
+
/** Handle Configure Now button click */
|
|
315
348
|
const handleInstallClick = async (btn) => {
|
|
316
349
|
const client = btn.dataset.installClient;
|
|
317
350
|
if (!client) return;
|
|
@@ -320,7 +353,7 @@
|
|
|
320
353
|
const originalText = btn.textContent;
|
|
321
354
|
|
|
322
355
|
btn.disabled = true;
|
|
323
|
-
btn.textContent = '
|
|
356
|
+
btn.textContent = 'Configuring...';
|
|
324
357
|
btn.classList.add('is-loading');
|
|
325
358
|
if (status) {
|
|
326
359
|
status.textContent = '';
|
|
@@ -331,9 +364,11 @@
|
|
|
331
364
|
const payload = { client };
|
|
332
365
|
if (currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest') {
|
|
333
366
|
payload.version = pinnedVersion;
|
|
367
|
+
} else if (currentChannel !== 'latest') {
|
|
368
|
+
payload.channel = currentChannel;
|
|
334
369
|
}
|
|
335
370
|
|
|
336
|
-
const res = await
|
|
371
|
+
const res = await DollhouseAuth.apiFetch('/api/setup/install', {
|
|
337
372
|
method: 'POST',
|
|
338
373
|
headers: { 'Content-Type': 'application/json' },
|
|
339
374
|
body: JSON.stringify(payload),
|
|
@@ -490,7 +525,7 @@
|
|
|
490
525
|
};
|
|
491
526
|
|
|
492
527
|
try {
|
|
493
|
-
const res = await
|
|
528
|
+
const res = await DollhouseAuth.apiFetch('/api/setup/open-config', {
|
|
494
529
|
method: 'POST',
|
|
495
530
|
headers: { 'Content-Type': 'application/json' },
|
|
496
531
|
body: JSON.stringify({ client }),
|
|
@@ -516,7 +551,7 @@
|
|
|
516
551
|
|
|
517
552
|
const fetchVersion = async () => {
|
|
518
553
|
try {
|
|
519
|
-
const res = await
|
|
554
|
+
const res = await DollhouseAuth.apiFetch('/api/setup/version');
|
|
520
555
|
if (!res.ok) return;
|
|
521
556
|
const data = await res.json();
|
|
522
557
|
|
|
@@ -524,8 +559,8 @@
|
|
|
524
559
|
pinnedVersion = data.latest?.version || data.running?.version || pinnedVersion;
|
|
525
560
|
if (pinnedVersion === 'latest') return;
|
|
526
561
|
|
|
527
|
-
// Rebuild configs with real version
|
|
528
|
-
configs = buildConfigs(pinnedVersion);
|
|
562
|
+
// Rebuild configs with real version and current channel
|
|
563
|
+
configs = buildConfigs(pinnedVersion, currentChannel);
|
|
529
564
|
|
|
530
565
|
// Update prereq section
|
|
531
566
|
const versionLabel = document.getElementById('pinned-version-label');
|
|
@@ -680,7 +715,7 @@
|
|
|
680
715
|
installBtn.classList.add('is-match');
|
|
681
716
|
} else {
|
|
682
717
|
const isPinned = currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest';
|
|
683
|
-
installBtn.textContent = isPinned ? `
|
|
718
|
+
installBtn.textContent = isPinned ? `Configure v${pinnedVersion}` : 'Configure Now';
|
|
684
719
|
installBtn.disabled = false;
|
|
685
720
|
installBtn.classList.remove('is-match');
|
|
686
721
|
}
|
|
@@ -727,7 +762,7 @@
|
|
|
727
762
|
/** Fetch detection results from API and update all platform states */
|
|
728
763
|
const fetchDetection = async () => {
|
|
729
764
|
try {
|
|
730
|
-
const res = await
|
|
765
|
+
const res = await DollhouseAuth.apiFetch('/api/setup/detect');
|
|
731
766
|
if (!res.ok) return;
|
|
732
767
|
const data = await res.json();
|
|
733
768
|
for (const [clientId, info] of Object.entries(data)) {
|
|
@@ -751,12 +786,12 @@
|
|
|
751
786
|
const openBtnHtml = (openClient) =>
|
|
752
787
|
openClient ? ` <button class="setup-open-btn" type="button" data-open-client="${openClient}">Open config file</button>` : '';
|
|
753
788
|
|
|
754
|
-
/** Build the
|
|
789
|
+
/** Build the Configure Now + CLI terminal command section */
|
|
755
790
|
const renderInstallSection = (p) => {
|
|
756
791
|
let html = '';
|
|
757
792
|
if (p.installClient) {
|
|
758
793
|
html += '<div class="setup-method setup-method-primary">';
|
|
759
|
-
html += `<div class="setup-install-row"><button class="setup-btn setup-btn-primary setup-install-btn" type="button" data-install-client="${p.installClient}">
|
|
794
|
+
html += `<div class="setup-install-row"><button class="setup-btn setup-btn-primary setup-install-btn" type="button" data-install-client="${p.installClient}">Configure Now</button>`;
|
|
760
795
|
html += `<span class="setup-install-status" data-install-status="${p.installClient}"></span></div>`;
|
|
761
796
|
}
|
|
762
797
|
if (p.cli) {
|
|
@@ -827,16 +862,453 @@
|
|
|
827
862
|
}
|
|
828
863
|
};
|
|
829
864
|
|
|
865
|
+
// ── License selector (#1746) ──────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
function formatActivationDate(license) {
|
|
868
|
+
if (license.verifiedAt) return new Date(license.verifiedAt).toLocaleString();
|
|
869
|
+
if (license.attestedAt) return new Date(license.attestedAt).toLocaleString();
|
|
870
|
+
return '—';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function initLicense() {
|
|
874
|
+
const tiers = document.getElementById('license-tiers');
|
|
875
|
+
if (!tiers) return;
|
|
876
|
+
|
|
877
|
+
const tierButtons = tiers.querySelectorAll('.license-tier');
|
|
878
|
+
const details = {
|
|
879
|
+
'agpl': document.getElementById('license-detail-agpl'),
|
|
880
|
+
'free-commercial': document.getElementById('license-detail-free-commercial'),
|
|
881
|
+
'paid-commercial': document.getElementById('license-detail-paid-commercial'),
|
|
882
|
+
};
|
|
883
|
+
const savedBanner = document.getElementById('license-saved');
|
|
884
|
+
const savedText = document.getElementById('license-saved-text');
|
|
885
|
+
|
|
886
|
+
function selectTier(tier) {
|
|
887
|
+
// Update button states
|
|
888
|
+
tierButtons.forEach(btn => {
|
|
889
|
+
const selected = btn.dataset.tier === tier;
|
|
890
|
+
btn.classList.toggle('is-selected', selected);
|
|
891
|
+
btn.setAttribute('aria-pressed', String(selected));
|
|
892
|
+
});
|
|
893
|
+
// Show/hide detail panels. If the selected tier has an active license,
|
|
894
|
+
// keep the form hidden — showLicenseDetails() will display the details
|
|
895
|
+
// card instead (#1841).
|
|
896
|
+
const hideForm = activeLicense?.status === 'active' && activeLicense?.tier === tier && tier !== 'agpl';
|
|
897
|
+
for (const [key, el] of Object.entries(details)) {
|
|
898
|
+
if (el) el.hidden = key !== tier || hideForm;
|
|
899
|
+
}
|
|
900
|
+
if (hideForm) showLicenseDetails(activeLicense);
|
|
901
|
+
// Hide saved banner when switching
|
|
902
|
+
if (savedBanner) savedBanner.hidden = true;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Click handlers for tier buttons
|
|
906
|
+
tierButtons.forEach(btn => {
|
|
907
|
+
btn.addEventListener('click', () => selectTier(btn.dataset.tier));
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Form submission: Free Commercial
|
|
911
|
+
const freeForm = document.getElementById('license-form-free-commercial');
|
|
912
|
+
if (freeForm) {
|
|
913
|
+
freeForm.addEventListener('submit', async (e) => {
|
|
914
|
+
e.preventDefault();
|
|
915
|
+
const status = document.getElementById('license-status-free-commercial');
|
|
916
|
+
const data = new FormData(freeForm);
|
|
917
|
+
await submitLicense({
|
|
918
|
+
tier: 'free-commercial',
|
|
919
|
+
email: data.get('email'),
|
|
920
|
+
telemetryAcknowledged: !!data.get('telemetry'),
|
|
921
|
+
attributionAcknowledged: !!data.get('attribution'),
|
|
922
|
+
revenueAttested: !!data.get('attestation'),
|
|
923
|
+
}, status, 'Commercial license activated');
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Form submission: Paid Commercial
|
|
928
|
+
const paidForm = document.getElementById('license-form-paid-commercial');
|
|
929
|
+
if (paidForm) {
|
|
930
|
+
paidForm.addEventListener('submit', async (e) => {
|
|
931
|
+
e.preventDefault();
|
|
932
|
+
const status = document.getElementById('license-status-paid-commercial');
|
|
933
|
+
const data = new FormData(paidForm);
|
|
934
|
+
await submitLicense({
|
|
935
|
+
tier: 'paid-commercial',
|
|
936
|
+
email: data.get('email'),
|
|
937
|
+
revenueScale: data.get('revenueScale'),
|
|
938
|
+
companyName: data.get('companyName') || undefined,
|
|
939
|
+
useCase: data.get('useCase') || undefined,
|
|
940
|
+
telemetryAcknowledged: !!data.get('telemetry'),
|
|
941
|
+
}, status, 'Enterprise inquiry sent — our team will reach out within 2 business days');
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const verificationPanel = document.getElementById('license-verification');
|
|
946
|
+
const verifyForm = document.getElementById('license-verify-form');
|
|
947
|
+
const verifyEmailSpan = document.getElementById('license-verify-email');
|
|
948
|
+
const verifyStatus = document.getElementById('license-verify-status');
|
|
949
|
+
const verifyTimer = document.getElementById('license-verify-timer');
|
|
950
|
+
const resendBtn = document.getElementById('license-resend-btn');
|
|
951
|
+
let countdownInterval = null;
|
|
952
|
+
|
|
953
|
+
let verificationPollInterval = null;
|
|
954
|
+
|
|
955
|
+
function showVerificationUI(email) {
|
|
956
|
+
if (verificationPanel) verificationPanel.hidden = false;
|
|
957
|
+
if (verifyEmailSpan) verifyEmailSpan.textContent = email;
|
|
958
|
+
if (verifyStatus) { verifyStatus.textContent = ''; verifyStatus.className = 'license-form-status'; }
|
|
959
|
+
startCountdown(10 * 60);
|
|
960
|
+
startVerificationPolling();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function hideVerificationUI() {
|
|
964
|
+
if (verificationPanel) verificationPanel.hidden = true;
|
|
965
|
+
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
|
966
|
+
stopVerificationPolling();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Poll license status every 3 seconds while verification is pending.
|
|
970
|
+
* When the user clicks the verify link in another tab, this tab
|
|
971
|
+
* auto-detects the activation and updates without a refresh. */
|
|
972
|
+
function startVerificationPolling() {
|
|
973
|
+
stopVerificationPolling();
|
|
974
|
+
verificationPollInterval = setInterval(async () => {
|
|
975
|
+
try {
|
|
976
|
+
const res = await fetch('/api/setup/license');
|
|
977
|
+
if (!res.ok) return;
|
|
978
|
+
const license = await res.json();
|
|
979
|
+
if (license.status === 'active') {
|
|
980
|
+
handleVerifySuccess(license);
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
// Ignore polling errors
|
|
984
|
+
}
|
|
985
|
+
}, 3000);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function stopVerificationPolling() {
|
|
989
|
+
if (verificationPollInterval) {
|
|
990
|
+
clearInterval(verificationPollInterval);
|
|
991
|
+
verificationPollInterval = null;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function startCountdown(seconds) {
|
|
996
|
+
if (countdownInterval) clearInterval(countdownInterval);
|
|
997
|
+
let remaining = seconds;
|
|
998
|
+
function update() {
|
|
999
|
+
if (!verifyTimer) return;
|
|
1000
|
+
if (remaining <= 0) {
|
|
1001
|
+
verifyTimer.textContent = 'Code expired. Click "Resend code" to get a new one.';
|
|
1002
|
+
clearInterval(countdownInterval);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const m = Math.floor(remaining / 60);
|
|
1006
|
+
const s = remaining % 60;
|
|
1007
|
+
verifyTimer.textContent = `Code expires in ${m}:${String(s).padStart(2, '0')}`;
|
|
1008
|
+
remaining--;
|
|
1009
|
+
}
|
|
1010
|
+
update();
|
|
1011
|
+
countdownInterval = setInterval(update, 1000);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function submitLicense(body, statusEl, successMsg) {
|
|
1015
|
+
if (statusEl) {
|
|
1016
|
+
statusEl.textContent = '';
|
|
1017
|
+
statusEl.className = 'license-form-status';
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const res = await fetch('/api/setup/license', {
|
|
1021
|
+
method: 'POST',
|
|
1022
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1023
|
+
body: JSON.stringify(body),
|
|
1024
|
+
});
|
|
1025
|
+
const json = await res.json();
|
|
1026
|
+
if (!res.ok) {
|
|
1027
|
+
if (statusEl) {
|
|
1028
|
+
statusEl.textContent = json.error || 'Failed to save';
|
|
1029
|
+
statusEl.className = 'license-form-status is-error';
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Commercial tiers: show verification code input
|
|
1035
|
+
if (json.verificationRequired) {
|
|
1036
|
+
if (statusEl) {
|
|
1037
|
+
statusEl.textContent = 'Check your email for a verification code.';
|
|
1038
|
+
statusEl.className = 'license-form-status is-success';
|
|
1039
|
+
}
|
|
1040
|
+
showVerificationUI(body.email);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// AGPL or already verified: show success
|
|
1045
|
+
if (statusEl) {
|
|
1046
|
+
statusEl.textContent = '';
|
|
1047
|
+
statusEl.className = 'license-form-status is-success';
|
|
1048
|
+
}
|
|
1049
|
+
hideVerificationUI();
|
|
1050
|
+
if (savedBanner && savedText) {
|
|
1051
|
+
savedText.textContent = successMsg;
|
|
1052
|
+
savedBanner.hidden = false;
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
console.debug('License submission failed:', err);
|
|
1056
|
+
if (statusEl) {
|
|
1057
|
+
statusEl.textContent = 'Network error — is the server running?';
|
|
1058
|
+
statusEl.className = 'license-form-status is-error';
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Verification form submission
|
|
1064
|
+
if (verifyForm) {
|
|
1065
|
+
verifyForm.addEventListener('submit', async (e) => {
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
const data = new FormData(verifyForm);
|
|
1068
|
+
const code = data.get('code');
|
|
1069
|
+
if (verifyStatus) { verifyStatus.textContent = ''; verifyStatus.className = 'license-form-status'; }
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
const res = await fetch('/api/setup/license/verify', {
|
|
1073
|
+
method: 'POST',
|
|
1074
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1075
|
+
body: JSON.stringify({ code }),
|
|
1076
|
+
});
|
|
1077
|
+
const json = await res.json();
|
|
1078
|
+
if (!res.ok) {
|
|
1079
|
+
if (verifyStatus) {
|
|
1080
|
+
verifyStatus.textContent = json.error || 'Verification failed';
|
|
1081
|
+
verifyStatus.className = 'license-form-status is-error';
|
|
1082
|
+
}
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Verification succeeded
|
|
1087
|
+
handleVerifySuccess(json.license);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
console.debug('Verification failed:', err);
|
|
1090
|
+
if (verifyStatus) {
|
|
1091
|
+
verifyStatus.textContent = 'Network error — is the server running?';
|
|
1092
|
+
verifyStatus.className = 'license-form-status is-error';
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Resend verification code
|
|
1099
|
+
if (resendBtn) {
|
|
1100
|
+
resendBtn.addEventListener('click', async () => {
|
|
1101
|
+
resendBtn.disabled = true;
|
|
1102
|
+
if (verifyStatus) { verifyStatus.textContent = 'Sending new code...'; verifyStatus.className = 'license-form-status'; }
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
const res = await fetch('/api/setup/license/resend', {
|
|
1106
|
+
method: 'POST',
|
|
1107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1108
|
+
});
|
|
1109
|
+
const json = await res.json();
|
|
1110
|
+
if (res.ok) {
|
|
1111
|
+
if (verifyStatus) {
|
|
1112
|
+
verifyStatus.textContent = 'New code sent. Check your email.';
|
|
1113
|
+
verifyStatus.className = 'license-form-status is-success';
|
|
1114
|
+
}
|
|
1115
|
+
startCountdown(10 * 60);
|
|
1116
|
+
} else if (verifyStatus) {
|
|
1117
|
+
verifyStatus.textContent = json.error || 'Failed to resend';
|
|
1118
|
+
verifyStatus.className = 'license-form-status is-error';
|
|
1119
|
+
}
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
console.debug('Resend failed:', err);
|
|
1122
|
+
if (verifyStatus) {
|
|
1123
|
+
verifyStatus.textContent = 'Network error';
|
|
1124
|
+
verifyStatus.className = 'license-form-status is-error';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
setTimeout(() => { resendBtn.disabled = false; }, 5000);
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Track whether the user has an active commercial license
|
|
1132
|
+
let activeLicense = null;
|
|
1133
|
+
|
|
1134
|
+
// AGPL selection: confirm if downgrading from active commercial license
|
|
1135
|
+
tierButtons.forEach(btn => {
|
|
1136
|
+
if (btn.dataset.tier === 'agpl') {
|
|
1137
|
+
btn.addEventListener('click', async () => {
|
|
1138
|
+
if (activeLicense?.status === 'active' && activeLicense?.tier !== 'agpl') {
|
|
1139
|
+
const tierLabel = activeLicense.tier === 'free-commercial' ? 'Commercial' : 'Enterprise';
|
|
1140
|
+
const confirmed = confirm(
|
|
1141
|
+
`You have an active ${tierLabel} license. Switching to AGPL will deactivate your ${tierLabel} license.\n\nYou can reactivate your ${tierLabel} license at any time.\n\nAre you sure?`
|
|
1142
|
+
);
|
|
1143
|
+
if (!confirmed) {
|
|
1144
|
+
// Restore the previous tier selection (selectTier handles
|
|
1145
|
+
// re-hiding the form when activeLicense is set)
|
|
1146
|
+
selectTier(activeLicense.tier);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
await submitLicense({ tier: 'agpl' }, null, 'AGPL-3.0 license selected');
|
|
1151
|
+
activeLicense = null;
|
|
1152
|
+
hideLicenseDetails();
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
const licenseDetailsPanel = document.getElementById('license-active-details');
|
|
1158
|
+
const licenseInfoTable = document.getElementById('license-info-tbody');
|
|
1159
|
+
|
|
1160
|
+
function showLicenseDetails(license) {
|
|
1161
|
+
if (!licenseDetailsPanel || !licenseInfoTable) return;
|
|
1162
|
+
if (license?.status !== 'active' || license?.tier === 'agpl') {
|
|
1163
|
+
licenseDetailsPanel.hidden = true;
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Hide the activation form for the active tier — the details panel replaces it (#1841)
|
|
1168
|
+
const activeForm = details[license.tier];
|
|
1169
|
+
if (activeForm) activeForm.hidden = true;
|
|
1170
|
+
|
|
1171
|
+
const tierLabel = license.tier === 'free-commercial' ? 'Free Commercial' : 'Enterprise';
|
|
1172
|
+
const rows = [
|
|
1173
|
+
['License type', tierLabel],
|
|
1174
|
+
['Email', license.email || '—'],
|
|
1175
|
+
['Status', 'Active'],
|
|
1176
|
+
['Activated', formatActivationDate(license)],
|
|
1177
|
+
['Telemetry', license.telemetryRequired ? 'Enabled (license requirement)' : 'Not required'],
|
|
1178
|
+
];
|
|
1179
|
+
if (license.tier === 'paid-commercial') {
|
|
1180
|
+
if (license.revenueScale) rows.push(['Revenue scale', license.revenueScale]);
|
|
1181
|
+
if (license.companyName) rows.push(['Company', license.companyName]);
|
|
1182
|
+
if (license.useCase) rows.push(['Use case', license.useCase]);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
licenseInfoTable.innerHTML = rows
|
|
1186
|
+
.map(([label, value]) => `<tr><td>${label}</td><td>${value}</td></tr>`)
|
|
1187
|
+
.join('');
|
|
1188
|
+
licenseDetailsPanel.hidden = false;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function hideLicenseDetails() {
|
|
1192
|
+
if (licenseDetailsPanel) licenseDetailsPanel.hidden = true;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function prefillFreeCommercialForm(license) {
|
|
1196
|
+
if (freeForm && license.email) {
|
|
1197
|
+
freeForm.querySelector('[name="email"]').value = license.email;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function prefillEnterpriseForm(license) {
|
|
1202
|
+
if (!paidForm) return;
|
|
1203
|
+
if (license.email) paidForm.querySelector('[name="email"]').value = license.email;
|
|
1204
|
+
if (license.revenueScale) paidForm.querySelector('[name="revenueScale"]').value = license.revenueScale;
|
|
1205
|
+
if (license.companyName) paidForm.querySelector('[name="companyName"]').value = license.companyName;
|
|
1206
|
+
if (license.useCase) paidForm.querySelector('[name="useCase"]').value = license.useCase;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function showSavedBanner(license) {
|
|
1210
|
+
if (license.tier === 'agpl' || !license.attestedAt) return;
|
|
1211
|
+
if (!savedBanner || !savedText) return;
|
|
1212
|
+
const tierLabel = license.tier === 'free-commercial' ? 'Commercial' : 'Enterprise';
|
|
1213
|
+
savedText.textContent = tierLabel + ' license active';
|
|
1214
|
+
savedBanner.hidden = false;
|
|
1215
|
+
showLicenseDetails(license);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Load saved license on page load
|
|
1219
|
+
async function loadSavedLicense() {
|
|
1220
|
+
try {
|
|
1221
|
+
const res = await fetch('/api/setup/license');
|
|
1222
|
+
if (!res.ok) return;
|
|
1223
|
+
const license = await res.json();
|
|
1224
|
+
if (!license.tier || !details[license.tier]) return;
|
|
1225
|
+
selectTier(license.tier);
|
|
1226
|
+
if (license.tier === 'free-commercial') prefillFreeCommercialForm(license);
|
|
1227
|
+
if (license.tier === 'paid-commercial') prefillEnterpriseForm(license);
|
|
1228
|
+
|
|
1229
|
+
// Show verification UI if license is pending
|
|
1230
|
+
if (license.status === 'pending' && license.email) {
|
|
1231
|
+
showVerificationUI(license.email);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (license.status === 'active') {
|
|
1236
|
+
activeLicense = license;
|
|
1237
|
+
showSavedBanner(license);
|
|
1238
|
+
}
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
// Default AGPL is fine — log for debugging only
|
|
1241
|
+
if (typeof console !== 'undefined') console.debug('License load skipped:', err);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function handleVerifySuccess(license) {
|
|
1246
|
+
hideVerificationUI();
|
|
1247
|
+
for (const el of Object.values(details)) {
|
|
1248
|
+
if (el) el.hidden = true;
|
|
1249
|
+
}
|
|
1250
|
+
activeLicense = license;
|
|
1251
|
+
if (savedBanner && savedText) {
|
|
1252
|
+
const tierLabel = license.tier === 'free-commercial' ? 'Commercial' : 'Enterprise';
|
|
1253
|
+
savedText.textContent = tierLabel + ' license verified and activated';
|
|
1254
|
+
savedBanner.hidden = false;
|
|
1255
|
+
}
|
|
1256
|
+
showLicenseDetails(license);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function handleAutoVerifyFailure(json) {
|
|
1260
|
+
const license = await (await fetch('/api/setup/license')).json();
|
|
1261
|
+
if (license.status === 'pending' && license.email) {
|
|
1262
|
+
showVerificationUI(license.email);
|
|
1263
|
+
}
|
|
1264
|
+
if (verifyStatus) {
|
|
1265
|
+
verifyStatus.textContent = json.error || 'Verification failed';
|
|
1266
|
+
verifyStatus.className = 'license-form-status is-error';
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Auto-verify from email click-through link (#verify=CODE)
|
|
1271
|
+
async function checkHashVerification() {
|
|
1272
|
+
const hash = globalThis.location.hash;
|
|
1273
|
+
const match = /^#verify=(\d{6})$/.exec(hash);
|
|
1274
|
+
if (!match) return;
|
|
1275
|
+
|
|
1276
|
+
const code = match[1];
|
|
1277
|
+
history.replaceState(null, '', globalThis.location.pathname + '#setup');
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
const res = await fetch('/api/setup/license/verify', {
|
|
1281
|
+
method: 'POST',
|
|
1282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1283
|
+
body: JSON.stringify({ code }),
|
|
1284
|
+
});
|
|
1285
|
+
const json = await res.json();
|
|
1286
|
+
if (res.ok) {
|
|
1287
|
+
handleVerifySuccess(json.license);
|
|
1288
|
+
} else {
|
|
1289
|
+
await handleAutoVerifyFailure(json);
|
|
1290
|
+
}
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
console.debug('Auto-verification failed:', err);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
loadSavedLicense();
|
|
1297
|
+
checkHashVerification();
|
|
1298
|
+
}
|
|
1299
|
+
|
|
830
1300
|
// ── Init ──────────────────────────────────────────────────────────────
|
|
831
1301
|
|
|
832
1302
|
const os = detectOS();
|
|
833
1303
|
renderGeneratedPanels();
|
|
834
1304
|
highlightOSPaths(os);
|
|
835
1305
|
initMethodToggle();
|
|
1306
|
+
initChannelSelector();
|
|
836
1307
|
initPlatformTabs();
|
|
837
1308
|
initCopyButtons();
|
|
838
1309
|
initInstallButtons();
|
|
839
1310
|
initOpenButtons();
|
|
840
1311
|
fetchVersion();
|
|
841
1312
|
fetchDetection();
|
|
1313
|
+
initLicense();
|
|
842
1314
|
})();
|
|
@@ -1375,6 +1375,10 @@ body.modal-open { overflow: hidden; }
|
|
|
1375
1375
|
.site-footer {
|
|
1376
1376
|
border-top: 1px solid var(--line);
|
|
1377
1377
|
padding: 0.85rem 0 1.35rem;
|
|
1378
|
+
position: sticky;
|
|
1379
|
+
bottom: 0;
|
|
1380
|
+
background: var(--paper);
|
|
1381
|
+
z-index: 10;
|
|
1378
1382
|
}
|
|
1379
1383
|
|
|
1380
1384
|
.footer-inner {
|
|
@@ -1396,6 +1400,12 @@ body.modal-open { overflow: hidden; }
|
|
|
1396
1400
|
}
|
|
1397
1401
|
.footer-link:hover { color: var(--accent); }
|
|
1398
1402
|
|
|
1403
|
+
.footer-copyright {
|
|
1404
|
+
color: var(--ink-500);
|
|
1405
|
+
font-size: 0.75rem;
|
|
1406
|
+
margin-left: auto;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1399
1409
|
.footer-updated {
|
|
1400
1410
|
margin-left: auto;
|
|
1401
1411
|
font-family: var(--font-mono);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissionRoutes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/permissionRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAgB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAkD7E;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"permissionRoutes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/permissionRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAgB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAkD7E;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAuGrF"}
|