diff --git a/CLAUDE.md b/CLAUDE.md index 541d58b..9f1c1c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,8 @@ The split exists because `xcodebuild` needs macOS and the Mac mini is the only m - **Data**: `/Users/m4mini/AppStoreBuilder/data/` (SQLite + ASC keys + source archives + build artifacts + logs) - **Process supervision**: launchd — `~/Library/LaunchAgents/com.88oak.appstorebuilder.plist` (KeepAlive, RunAtLoad) - **Env vars** (in `builder/.env`, loaded non-destructively by `src/server.js`): - - `ADMIN_PASSWORD`, `SESSION_SECRET`, `DATA_DIR`, `PORT`, `BUILDER_SHARED_SECRET` + - `ADMIN_PASSWORD`, `SESSION_SECRET`, `DATA_DIR`, `PORT` +- **ASC API keys** live in the `asc_keys` table (one row per Apple Developer team), not in env/settings. Columns: `team_id`, `team_name`, `key_id`, `issuer_id`, `p8_filename`. Managed at `/settings` → "Developer Accounts". `.p8` files stored at `$DATA_DIR/asc/.p8` (0600). At build time, the worker reads `DEVELOPMENT_TEAM` from `xcodebuild -showBuildSettings` and looks up the matching key. **Important**: The builder code must NOT live under `~/Desktop/` when running via launchd. macOS TCC blocks launchd-spawned processes from reading Desktop, which causes the Node process to hang on `__getcwd` during startup. That's why we copy to `/Users/m4mini/AppStoreBuilder/app/` via the deploy script instead of pointing launchd directly at the git checkout in `~/Desktop/code/ios-appstore/builder/`. diff --git a/builder/public/css/style.css b/builder/public/css/style.css index 2f2ef91..cf5ff52 100644 --- a/builder/public/css/style.css +++ b/builder/public/css/style.css @@ -443,6 +443,37 @@ input:focus, select:focus { border-color: var(--accent); } .badge.failed { background: rgba(255,59,48,0.15); color: var(--danger); } .badge.synced { background: rgba(48,209,88,0.15); color: var(--success); } .badge.unsynced { background: rgba(255,149,0,0.15); color: #ff9500; } +.badge-valid { background: rgba(48,209,88,0.15); color: var(--success); display:inline-block; padding:3px 10px; border-radius:10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.03em; } +.badge-expiring { background: rgba(255,149,0,0.15); color: #ff9500; display:inline-block; padding:3px 10px; border-radius:10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.03em; } +.badge-expired { background: rgba(255,59,48,0.15); color: var(--danger); display:inline-block; padding:3px 10px; border-radius:10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.03em; } + +/* Data tables (settings / profiles) */ +.data-table { width: 100%; border-collapse: collapse; } +.data-table th, .data-table td { + text-align: left; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + font-size: 13px; + vertical-align: middle; +} +.data-table th { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} +.data-table tr:last-child td { border-bottom: none; } +.data-table code { font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 12px; color: var(--text-muted); } + +.action-cell { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } + +.btn-xs { padding: 4px 10px; font-size: 12px; border-radius: 6px; cursor: pointer; border: 1px solid var(--border); } +.btn-xs.btn-secondary { background: transparent; color: var(--text); } +.btn-xs.btn-secondary:hover { background: var(--surface-hover); } +.btn-danger { background: transparent; color: var(--danger); border: 1px solid var(--danger); padding: 4px 10px; font-size: 12px; border-radius: 6px; cursor: pointer; } +.btn-danger:hover { background: rgba(255,59,48,0.1); } +.upload-label { display: inline-flex; align-items: center; cursor: pointer; } /* Tables */ .table { @@ -495,6 +526,20 @@ input:focus, select:focus { border-color: var(--accent); } .toast.success { border-color: var(--success); } .toast.error { border-color: var(--danger); } +/* Filesystem browser */ +.path-bar { display: flex; gap: 4px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; padding-bottom: 12px; border-bottom: 1px solid var(--border); } +.path-segment { color: var(--accent); cursor: pointer; font-size: 14px; font-family: ui-monospace, 'SF Mono', Menlo, monospace; } +.path-segment:hover { text-decoration: underline; } +.path-separator { color: var(--text-muted); font-size: 12px; user-select: none; } + +.file-list { display: flex; flex-direction: column; } +.file-entry { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: background 0.1s; } +.file-entry:hover { background: var(--surface-hover); } +.file-entry.selected { background: rgba(0,122,255,0.1); border: 1px solid rgba(0,122,255,0.3); } +.file-entry .icon { width: 24px; text-align: center; font-size: 16px; flex-shrink: 0; } +.file-entry .name { font-size: 14px; } +.file-entry.xcode-project .name { color: var(--accent); font-weight: 600; } + /* Log viewer */ .log-viewer { background: #000; diff --git a/builder/public/js/build.js b/builder/public/js/build.js index f505546..9f970ed 100644 --- a/builder/public/js/build.js +++ b/builder/public/js/build.js @@ -1,4 +1,8 @@ const $ = (s) => document.querySelector(s); +const esc = (s) => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }; + +let selectedProject = null; + function toast(msg, kind = '') { const t = $('#toast'); t.textContent = msg; @@ -6,46 +10,147 @@ function toast(msg, kind = '') { setTimeout(() => t.classList.remove('show'), 3500); } -$('#upload-form').addEventListener('submit', async (e) => { - e.preventDefault(); - const fd = new FormData(e.target); - const btn = e.target.querySelector('button[type=submit]'); - btn.disabled = true; - btn.textContent = 'Uploading…'; +// --- Path bar (breadcrumbs) --- + +function renderPathBar(currentPath) { + const bar = $('#path-bar'); + const segments = currentPath.split('/').filter(Boolean); + let html = ''; + for (let i = 0; i < segments.length; i++) { + const fullPath = '/' + segments.slice(0, i + 1).join('/'); + if (i > 0) html += '/'; + html += `${esc(segments[i])}`; + } + bar.innerHTML = html; + bar.querySelectorAll('.path-segment').forEach((el) => { + el.addEventListener('click', () => browse(el.dataset.path)); + }); +} + +// --- File list --- + +function renderFileList(entries) { + const list = $('#file-list'); + + if (!entries.length) { + list.innerHTML = '

No Xcode projects or subdirectories found here.

'; + return; + } + + list.innerHTML = entries.map((e) => { + let icon, cls; + if (e.type === 'xcworkspace') { + icon = '\u{1F4E6}'; + cls = 'xcode-project'; + } else if (e.type === 'xcodeproj') { + icon = '\u{1F528}'; + cls = 'xcode-project'; + } else { + icon = '\u{1F4C1}'; + cls = ''; + } + return `
+ ${icon} + ${esc(e.name)} +
`; + }).join(''); + + list.querySelectorAll('.file-entry').forEach((el) => { + el.addEventListener('click', () => { + if (el.dataset.type === 'directory') { + browse(el.dataset.path); + } else { + selectProject(el.dataset.path); + } + }); + }); +} + +// --- Browse directory --- + +async function browse(dirPath) { + const list = $('#file-list'); + list.innerHTML = '

Loading...

'; + + const url = dirPath + ? `/api/filesystem/browse?path=${encodeURIComponent(dirPath)}` + : '/api/filesystem/browse'; + try { - const r = await fetch('/api/build/upload', { method: 'POST', body: fd }); + const r = await fetch(url); + if (r.status === 401) { location.href = '/login'; return; } const data = await r.json(); - if (!r.ok) throw new Error(data.error || 'Upload failed'); - location.href = `/builds#${data.job_id}`; + if (!r.ok) throw new Error(data.error); + + renderPathBar(data.path); + renderFileList(data.entries); + } catch (err) { + list.innerHTML = `

${esc(err.message)}

`; + } +} + +// --- Select a project --- + +async function selectProject(projectPath) { + selectedProject = projectPath; + const config = $('#project-config'); + config.style.display = 'block'; + $('#selected-project').textContent = projectPath; + + const select = $('#scheme-select'); + select.disabled = true; + select.innerHTML = ''; + $('#build-btn').disabled = true; + + document.querySelectorAll('.file-entry').forEach((el) => { + el.classList.toggle('selected', el.dataset.path === projectPath); + }); + + try { + const r = await fetch(`/api/filesystem/schemes?projectPath=${encodeURIComponent(projectPath)}`); + const data = await r.json(); + if (!r.ok) throw new Error(data.error); + + if (!data.schemes.length) { + select.innerHTML = ''; + return; + } + + select.innerHTML = data.schemes.map((s) => + `` + ).join(''); + select.disabled = false; + $('#build-btn').disabled = false; + } catch (err) { + select.innerHTML = ``; + toast(err.message, 'error'); + } +} + +// --- Build button --- + +$('#build-btn').addEventListener('click', async () => { + if (!selectedProject) return; + const scheme = $('#scheme-select').value; + const btn = $('#build-btn'); + btn.disabled = true; + btn.textContent = 'Starting build...'; + + try { + const r = await fetch('/api/build/local', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectPath: selectedProject, scheme }), + }); + const data = await r.json(); + if (!r.ok) throw new Error(data.error || 'Build failed to start'); + location.href = `/#${data.job_id}`; } catch (err) { toast(err.message, 'error'); btn.disabled = false; - btn.textContent = 'Queue Build'; + btn.textContent = 'Build'; } }); -$('#git-form').addEventListener('submit', async (e) => { - e.preventDefault(); - const body = { - url: e.target.url.value.trim(), - branch: e.target.branch.value.trim() || null, - scheme: e.target.scheme.value.trim() || null, - }; - const btn = e.target.querySelector('button[type=submit]'); - btn.disabled = true; - btn.textContent = 'Cloning…'; - try { - const r = await fetch('/api/build/git', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - const data = await r.json(); - if (!r.ok) throw new Error(data.error || 'Clone failed'); - location.href = `/builds#${data.job_id}`; - } catch (err) { - toast(err.message, 'error'); - btn.disabled = false; - btn.textContent = 'Queue Build'; - } -}); +// Start browsing at default path +browse(null); diff --git a/builder/public/js/builds.js b/builder/public/js/builds.js index 8ac05cb..b3ba4d4 100644 --- a/builder/public/js/builds.js +++ b/builder/public/js/builds.js @@ -40,14 +40,14 @@ async function loadJobs() { container.innerHTML = ` - + ${jobs.map(j => ` - + @@ -72,7 +72,7 @@ async function openJob(id) { $('#detail-meta').innerHTML = `
bundle: ${esc(job.bundle_id || '—')}
scheme: ${esc(job.scheme || '—')}
-
source: ${esc(job.source_kind)} ${esc(job.source_ref || '')}
+
project: ${esc(job.source_ref || job.project_path || '--')}
started: ${esc(formatDate(job.started_at))} · finished: ${esc(formatDate(job.finished_at))}
${job.install_url ? `
install: ${esc(job.install_url.slice(0, 80))}…
` : ''} ${job.error ? `
error: ${esc(job.error)}
` : ''} diff --git a/builder/public/js/devices.js b/builder/public/js/devices.js deleted file mode 100644 index 579458e..0000000 --- a/builder/public/js/devices.js +++ /dev/null @@ -1,83 +0,0 @@ -const $ = (s) => document.querySelector(s); -const esc = (s) => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }; - -function toast(msg, kind = '') { - const t = $('#toast'); - t.textContent = msg; - t.className = 'toast show ' + kind; - setTimeout(() => t.classList.remove('show'), 3500); -} - -async function load() { - const res = await fetch('/api/devices'); - if (res.status === 401) { location.href = '/login'; return; } - const devices = await res.json(); - const container = $('#devices-container'); - - if (!devices.length) { - container.innerHTML = '

No devices registered yet.

'; - return; - } - - container.innerHTML = ` -
StatusBundleSourceStartedDuration
StatusBundleProjectStartedDuration
${statusBadge(j.status)} ${esc(j.bundle_id) || ''}${esc(j.source_kind)}: ${esc((j.source_ref || '').slice(0, 40))}${esc((j.source_ref || '').replace(/\.(xcodeproj|xcworkspace)$/, ''))} ${esc(formatDate(j.started_at))} ${esc(duration(j.started_at, j.finished_at))} ${j.install_url ? `Install` : ''}
- - - - - ${devices.map(d => ` - - - - - - - - `).join('')} - -
NameUDIDStatusAdded
${esc(d.name) || 'unnamed'}${esc(d.udid)}${d.synced_at - ? 'Synced' - : 'Local only'}${esc(d.added_at)}
- `; - - container.querySelectorAll('.delete-btn').forEach(btn => { - btn.addEventListener('click', async () => { - if (!confirm('Remove this device locally? (It will remain in the Apple portal.)')) return; - const udid = btn.getAttribute('data-udid'); - const r = await fetch(`/api/devices/${udid}`, { method: 'DELETE' }); - if (r.ok) { toast('Removed', 'success'); load(); } - else toast('Delete failed', 'error'); - }); - }); -} - -$('#add-form').addEventListener('submit', async (e) => { - e.preventDefault(); - const form = e.target; - const body = { - udid: form.udid.value.trim(), - name: form.name.value.trim(), - }; - const btn = form.querySelector('button[type=submit]'); - btn.disabled = true; - btn.textContent = 'Registering…'; - - const r = await fetch('/api/devices', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - const data = await r.json().catch(() => ({})); - btn.disabled = false; - btn.textContent = 'Add Device'; - - if (r.ok) { - toast(data.synced ? 'Registered with Apple' : 'Saved locally (ASC not configured)', 'success'); - form.reset(); - load(); - } else { - toast(data.error || 'Register failed', 'error'); - } -}); - -load(); diff --git a/builder/public/js/profiles.js b/builder/public/js/profiles.js new file mode 100644 index 0000000..11918fc --- /dev/null +++ b/builder/public/js/profiles.js @@ -0,0 +1,210 @@ +const $ = (s) => document.querySelector(s); +const esc = (s) => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }; + +function toast(msg, kind = '') { + const t = $('#toast'); + t.textContent = msg; + t.className = 'toast show ' + kind; + setTimeout(() => t.classList.remove('show'), 3500); +} + +function formatDate(s) { + if (!s) return '--'; + return new Date(s).toLocaleDateString(); +} + +function statusBadge(status) { + const cls = status === 'valid' ? 'succeeded' + : status === 'expiring' ? 'pending' + : status === 'expired' ? 'failed' + : 'pending'; + return `${status}`; +} + +function methodBadge(method) { + if (method === 'ad-hoc') return 'ad-hoc'; + if (method === 'development') return 'dev'; + if (method === 'app-store') return 'app-store'; + if (method === 'enterprise') return 'enterprise'; + return `${method || 'unknown'}`; +} + +// --- Installed profiles --- + +let installedProfiles = []; + +async function loadProfiles() { + const r = await fetch('/api/profiles'); + if (r.status === 401) { location.href = '/login'; return; } + installedProfiles = await r.json(); + const container = $('#profiles-container'); + + if (!installedProfiles.length) { + container.innerHTML = '

No provisioning profiles installed. Generate one from the bundle IDs above.

'; + return; + } + + // Only show ad-hoc profiles + const adhoc = installedProfiles.filter(p => p.method === 'ad-hoc'); + if (!adhoc.length) { + container.innerHTML = '

No ad-hoc profiles found. Other profile types are installed but not shown.

'; + return; + } + + container.innerHTML = ` + + + + + + + + + + + + + ${adhoc.map(p => ` + + + + + + + + + `).join('')} + +
Bundle IDNameDevicesExpiresStatus
${esc(p.bundleIdentifier || '--')}${esc(p.name || '--')}${p.deviceCount ?? '--'}${esc(formatDate(p.expiresAt))}${statusBadge(p.status)} + ${p.bundleIdentifier ? `` : ''} + +
+ `; + + bindProfileActions(container); +} + +function bindProfileActions(container) { + container.querySelectorAll('.delete-btn').forEach((btn) => { + btn.addEventListener('click', async () => { + if (!confirm('Delete this profile?')) return; + btn.disabled = true; + btn.textContent = '...'; + try { + const r = await fetch(`/api/profiles/${btn.dataset.uuid}`, { method: 'DELETE' }); + if (!r.ok) throw new Error((await r.json()).error || 'Delete failed'); + toast('Profile deleted', 'success'); + loadProfiles(); + loadBundleIds(); + } catch (err) { + toast(err.message, 'error'); + btn.disabled = false; + btn.textContent = 'Delete'; + } + }); + }); + + container.querySelectorAll('.regen-btn').forEach((btn) => { + btn.addEventListener('click', () => generateProfile(btn.dataset.bundle, null, btn)); + }); +} + +async function generateProfile(bundleId, teamId, btn) { + if (!teamId) { + // Regenerate button on the installed-profiles table doesn't know the team yet — look it up + const installed = installedProfiles.find(p => p.bundleIdentifier === bundleId); + teamId = installed?.teamId; + } + if (!teamId) { + toast('No team ID available for this bundle. Generate from the ASC bundle list above.', 'error'); + return; + } + const origText = btn.textContent; + btn.disabled = true; + btn.textContent = 'Generating...'; + try { + const r = await fetch('/api/profiles/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ bundleId, teamId }), + }); + if (!r.ok) throw new Error((await r.json()).error || 'Generation failed'); + toast(`Profile generated for ${bundleId}`, 'success'); + loadProfiles(); + loadBundleIds(); + } catch (err) { + toast(err.message, 'error'); + } finally { + btn.disabled = false; + btn.textContent = origText; + } +} + +// --- Bundle IDs from ASC --- + +async function loadBundleIds() { + const container = $('#bundle-ids-container'); + try { + const r = await fetch('/api/bundle-ids'); + if (r.status === 401) { location.href = '/login'; return; } + const groups = await r.json(); + + if (groups.error) throw new Error(groups.error); + if (!Array.isArray(groups) || !groups.length) { + container.innerHTML = '

No developer accounts configured. Add one in Settings.

'; + return; + } + + const installedBundles = new Set( + installedProfiles + .filter(p => p.method === 'ad-hoc') + .map(p => p.bundleIdentifier) + ); + + container.innerHTML = groups.map(g => { + if (g.error) { + return `
+

${esc(g.teamName)} ${esc(g.teamId)}

+

${esc(g.error)}

+
`; + } + if (!g.bundleIds.length) { + return `
+

${esc(g.teamName)} ${esc(g.teamId)}

+

No bundle IDs registered.

+
`; + } + return `
+
+

${esc(g.teamName)} ${esc(g.teamId)}

+
+ ${g.bundleIds.map(b => { + const hasProfile = installedBundles.has(b.identifier); + return `
+
+ ${esc(b.identifier)} + ${esc(b.name)} + ${esc(b.platform)} +
+
+ ${hasProfile + ? 'has profile' + : `` + } +
+
`; + }).join('')} +
`; + }).join(''); + + container.querySelectorAll('.gen-btn').forEach((btn) => { + btn.addEventListener('click', () => generateProfile(btn.dataset.bundle, btn.dataset.team, btn)); + }); + + } catch (err) { + container.innerHTML = `

${esc(err.message)}

Configure at least one Developer Account in Settings to fetch bundle IDs.

`; + } +} + +// Load both in parallel +loadProfiles().then(() => loadBundleIds()); diff --git a/builder/public/js/settings.js b/builder/public/js/settings.js index 046c20a..b6c6bd0 100644 --- a/builder/public/js/settings.js +++ b/builder/public/js/settings.js @@ -6,23 +6,131 @@ const toast = (msg, kind = '') => { setTimeout(() => t.classList.remove('show'), 3000); }; -async function load() { +const escapeHtml = (str) => (str || '').replace(/[&<>"']/g, (c) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', +}[c])); + +async function loadStorefront() { const res = await fetch('/api/settings'); if (res.status === 401) { location.href = '/login'; return; } const s = await res.json(); - - $('[name=asc_key_id]').value = s.asc_key_id || ''; - $('[name=asc_issuer_id]').value = s.asc_issuer_id || ''; $('[name=unraid_url]').value = s.unraid_url || ''; $('[name=unraid_token]').value = s.unraid_token || ''; - - $('#p8-status').textContent = s.asc_key_uploaded - ? `✓ .p8 uploaded for key ${s.asc_key_id}` - : 'No .p8 uploaded yet'; } -async function saveForm(formEl, keys) { - const data = Object.fromEntries(keys.map(k => [k, formEl.querySelector(`[name=${k}]`)?.value || ''])); +async function loadKeys() { + const res = await fetch('/api/asc-keys'); + if (res.status === 401) { location.href = '/login'; return; } + const keys = await res.json(); + const container = $('#asc-keys-table'); + + if (!keys.length) { + container.innerHTML = '

No developer accounts configured yet.

'; + return; + } + + container.innerHTML = ` + + + + + + + + + + + + + ${keys.map((k) => ` + + + + + + + + + `).join('')} + +
Team NameTeam IDKey IDIssuer ID.p8Actions
${escapeHtml(k.team_name || '')}${escapeHtml(k.team_id)}${escapeHtml(k.key_id)}${escapeHtml(k.issuer_id)}${k.p8_uploaded ? 'uploaded' : 'missing'} + + + +
+ `; + + // Wire row actions + container.querySelectorAll('tr[data-team-id]').forEach((row) => { + const teamId = row.dataset.teamId; + row.querySelector('.p8-input').addEventListener('change', (e) => uploadP8(teamId, e.target.files[0])); + row.querySelector('.test-btn').addEventListener('click', () => testKey(teamId)); + row.querySelector('.delete-btn').addEventListener('click', () => deleteKey(teamId)); + }); +} + +async function uploadP8(teamId, file) { + if (!file) return; + const fd = new FormData(); + fd.append('p8', file); + const res = await fetch(`/api/asc-keys/${encodeURIComponent(teamId)}/p8`, { method: 'POST', body: fd }); + const data = await res.json().catch(() => ({})); + if (res.ok) { + toast('.p8 uploaded', 'success'); + loadKeys(); + } else { + toast(data.error || 'Upload failed', 'error'); + } +} + +async function testKey(teamId) { + toast('Testing...', ''); + const res = await fetch(`/api/asc-keys/${encodeURIComponent(teamId)}/test`, { method: 'POST' }); + const data = await res.json().catch(() => ({})); + if (res.ok) toast(`Team ${teamId} authenticated OK`, 'success'); + else toast(data.error || 'Test failed', 'error'); +} + +async function deleteKey(teamId) { + if (!confirm(`Delete ASC key for team ${teamId}? The .p8 file will be removed from disk.`)) return; + const res = await fetch(`/api/asc-keys/${encodeURIComponent(teamId)}`, { method: 'DELETE' }); + const data = await res.json().catch(() => ({})); + if (res.ok) { + toast('Deleted', 'success'); + loadKeys(); + } else { + toast(data.error || 'Delete failed', 'error'); + } +} + +$('#add-key-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const fd = new FormData(e.target); + const payload = Object.fromEntries(fd.entries()); + const res = await fetch('/api/asc-keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + if (res.ok) { + toast('Saved', 'success'); + e.target.reset(); + loadKeys(); + } else { + toast(data.error || 'Save failed', 'error'); + } +}); + +$('#storefront-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const data = { + unraid_url: e.target.querySelector('[name=unraid_url]').value, + unraid_token: e.target.querySelector('[name=unraid_token]').value, + }; const res = await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -30,45 +138,14 @@ async function saveForm(formEl, keys) { }); if (res.ok) toast('Saved', 'success'); else toast('Save failed', 'error'); -} - -$('#asc-form').addEventListener('submit', async (e) => { - e.preventDefault(); - await saveForm(e.target, ['asc_key_id', 'asc_issuer_id']); - - // Upload .p8 if one was selected - const file = $('#p8-input').files[0]; - if (file) { - const fd = new FormData(); - fd.append('p8', file); - const res = await fetch('/api/settings/p8', { method: 'POST', body: fd }); - if (res.ok) { - toast('.p8 uploaded', 'success'); - load(); - } else { - const err = await res.json().catch(() => ({})); - toast(err.error || 'Upload failed', 'error'); - } - } }); -$('#unraid-form').addEventListener('submit', async (e) => { - e.preventDefault(); - await saveForm(e.target, ['unraid_url', 'unraid_token']); -}); - -$('#test-asc').addEventListener('click', async () => { - const res = await fetch('/api/settings/test-asc', { method: 'POST' }); +$('#test-storefront').addEventListener('click', async () => { + const res = await fetch('/api/settings/test-storefront', { method: 'POST' }); const data = await res.json(); - if (res.ok) toast(`Connected — ${data.device_count} devices in portal`, 'success'); + if (res.ok) toast(`Connected -- ${data.app_count} apps on storefront`, 'success'); else toast(data.error || 'Connection failed', 'error'); }); -$('#test-unraid').addEventListener('click', async () => { - const res = await fetch('/api/settings/test-unraid', { method: 'POST' }); - const data = await res.json(); - if (res.ok) toast(`Connected to unraid — ${data.app_count} apps`, 'success'); - else toast(data.error || 'Connection failed', 'error'); -}); - -load(); +loadStorefront(); +loadKeys(); diff --git a/builder/src/asc-api.js b/builder/src/asc-api.js deleted file mode 100644 index 0825835..0000000 --- a/builder/src/asc-api.js +++ /dev/null @@ -1,141 +0,0 @@ -// App Store Connect API client. -// Authenticates with ES256 JWTs signed by the user's .p8 key. -// Docs: https://developer.apple.com/documentation/appstoreconnectapi - -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); -const { getSetting, DATA_DIR } = require('./db'); - -const API_BASE = 'https://api.appstoreconnect.apple.com'; -const AUDIENCE = 'appstoreconnect-v1'; -const TTL_SECONDS = 15 * 60; // Apple allows up to 20 min - -let cachedJwt = null; -let cachedExpiry = 0; - -function b64url(buf) { - return Buffer.from(buf) - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); -} - -function loadKey() { - const keyId = getSetting('asc_key_id'); - if (!keyId) throw new Error('ASC key ID not configured (Settings page)'); - const keyPath = path.join(DATA_DIR, 'asc', `${keyId}.p8`); - if (!fs.existsSync(keyPath)) throw new Error('.p8 file not uploaded'); - return { keyId, keyPem: fs.readFileSync(keyPath, 'utf8') }; -} - -function signJwt() { - // Return a cached token if still fresh (>60s of life left). - const now = Math.floor(Date.now() / 1000); - if (cachedJwt && cachedExpiry - now > 60) return cachedJwt; - - const issuerId = getSetting('asc_issuer_id'); - if (!issuerId) throw new Error('ASC Issuer ID not configured (Settings page)'); - const { keyId, keyPem } = loadKey(); - - const header = { alg: 'ES256', kid: keyId, typ: 'JWT' }; - const payload = { - iss: issuerId, - iat: now, - exp: now + TTL_SECONDS, - aud: AUDIENCE, - }; - - const headerB64 = b64url(JSON.stringify(header)); - const payloadB64 = b64url(JSON.stringify(payload)); - const signingInput = `${headerB64}.${payloadB64}`; - - const signer = crypto.createSign('SHA256'); - signer.update(signingInput); - signer.end(); - - // Apple's .p8 files are PKCS8 EC keys. Node signs them as DER by default; - // we need the raw IEEE P1363 r||s form for JWS. - const derSig = signer.sign({ key: keyPem, dsaEncoding: 'ieee-p1363' }); - const sigB64 = b64url(derSig); - - cachedJwt = `${signingInput}.${sigB64}`; - cachedExpiry = now + TTL_SECONDS; - return cachedJwt; -} - -async function ascFetch(pathAndQuery, init = {}) { - const token = signJwt(); - const url = `${API_BASE}${pathAndQuery}`; - const res = await fetch(url, { - ...init, - headers: { - ...(init.headers || {}), - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - const text = await res.text(); - let body = null; - if (text) { - try { body = JSON.parse(text); } - catch { body = { raw: text }; } - } - if (!res.ok) { - const err = body?.errors?.[0]; - const msg = err - ? `${err.title || 'ASC error'}: ${err.detail || err.code || ''}` - : `ASC request failed (${res.status})`; - const e = new Error(msg); - e.status = res.status; - e.body = body; - throw e; - } - return body; -} - -// --- Public API --- - -async function listDevices() { - // ASC paginates; 200 is the max per page. For a personal store, one page is plenty. - const body = await ascFetch('/v1/devices?limit=200'); - return body.data || []; -} - -async function registerDevice({ udid, name, platform = 'IOS' }) { - const body = await ascFetch('/v1/devices', { - method: 'POST', - body: JSON.stringify({ - data: { - type: 'devices', - attributes: { name: name || udid.slice(0, 8), udid, platform }, - }, - }), - }); - return body.data; -} - -async function listBundleIds(identifier) { - const q = identifier ? `?filter[identifier]=${encodeURIComponent(identifier)}` : ''; - const body = await ascFetch(`/v1/bundleIds${q}`); - return body.data || []; -} - -async function listProfiles() { - const body = await ascFetch('/v1/profiles?limit=200'); - return body.data || []; -} - -async function deleteProfile(profileId) { - await ascFetch(`/v1/profiles/${profileId}`, { method: 'DELETE' }); -} - -module.exports = { - signJwt, - listDevices, - registerDevice, - listBundleIds, - listProfiles, - deleteProfile, -}; diff --git a/builder/src/auth.js b/builder/src/auth.js index 80f4629..626c73e 100644 --- a/builder/src/auth.js +++ b/builder/src/auth.js @@ -1,5 +1,4 @@ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; -const BUILDER_SHARED_SECRET = process.env.BUILDER_SHARED_SECRET; // Session auth for the browser UI function requireLogin(req, res, next) { @@ -10,18 +9,8 @@ function requireLogin(req, res, next) { res.redirect('/login'); } -// Shared-secret auth for enrollment callbacks coming from unraid -function requireBuilderSecret(req, res, next) { - const header = req.headers['authorization'] || ''; - const match = header.match(/^Bearer\s+(.+)$/); - if (!match || !BUILDER_SHARED_SECRET || match[1] !== BUILDER_SHARED_SECRET) { - return res.status(401).json({ error: 'Invalid shared secret' }); - } - next(); -} - function validatePassword(password) { return password && password === ADMIN_PASSWORD; } -module.exports = { requireLogin, requireBuilderSecret, validatePassword }; +module.exports = { requireLogin, validatePassword }; diff --git a/builder/src/build-routes.js b/builder/src/build-routes.js index 8b9820e..0f51aeb 100644 --- a/builder/src/build-routes.js +++ b/builder/src/build-routes.js @@ -1,71 +1,24 @@ -// Build pipeline HTTP routes. +// Build pipeline + filesystem browser HTTP routes. // Attached to the main Express app in server.js via `register(app)`. const path = require('path'); const fs = require('fs'); const os = require('os'); -const { spawn } = require('child_process'); -const multer = require('multer'); const { v4: uuidv4 } = require('uuid'); const { db, DATA_DIR } = require('./db'); const buildWorker = require('./build-worker'); -const SOURCE_DIR = path.join(DATA_DIR, 'source'); const LOGS_DIR = path.join(DATA_DIR, 'builds'); -const TMP_DIR = path.join(DATA_DIR, 'tmp'); +fs.mkdirSync(LOGS_DIR, { recursive: true }); -[SOURCE_DIR, LOGS_DIR, TMP_DIR].forEach((d) => fs.mkdirSync(d, { recursive: true })); +const DEFAULT_BROWSE_ROOT = path.join(os.homedir(), 'Desktop', 'code'); -const archiveUpload = multer({ - dest: TMP_DIR, - limits: { fileSize: 500 * 1024 * 1024 }, - fileFilter: (req, file, cb) => { - const name = file.originalname.toLowerCase(); - if (name.endsWith('.zip') || name.endsWith('.tar.gz') || name.endsWith('.tgz')) { - return cb(null, true); - } - cb(new Error('Only .zip, .tar.gz, or .tgz archives')); - }, -}); - -function extractArchive(archivePath, destDir) { - return new Promise((resolve, reject) => { - fs.mkdirSync(destDir, { recursive: true }); - const lower = archivePath.toLowerCase(); - let cmd, args; - if (lower.endsWith('.zip')) { - cmd = '/usr/bin/unzip'; - args = ['-q', archivePath, '-d', destDir]; - } else { - cmd = '/usr/bin/tar'; - args = ['-xzf', archivePath, '-C', destDir]; - } - const child = spawn(cmd, args); - let stderr = ''; - child.stderr.on('data', (c) => { stderr += c.toString(); }); - child.on('close', (code) => { - if (code === 0) resolve(); - else reject(new Error(`${cmd} exited ${code}: ${stderr}`)); - }); - child.on('error', reject); - }); -} - -function cloneGitRepo({ url, branch, destDir, logPath }) { - return new Promise((resolve, reject) => { - fs.mkdirSync(destDir, { recursive: true }); - const args = ['clone', '--depth', '1']; - if (branch) args.push('--branch', branch); - args.push(url, destDir); - fs.appendFileSync(logPath, `$ git ${args.join(' ')}\n`); - const child = spawn('/usr/bin/git', args); - child.stdout.on('data', (c) => fs.appendFileSync(logPath, c)); - child.stderr.on('data', (c) => fs.appendFileSync(logPath, c)); - child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`git clone failed (${code})`))); - child.on('error', reject); - }); -} +// Directories to skip when browsing +const SKIP_DIRS = new Set([ + 'node_modules', 'Pods', 'build', 'DerivedData', + '.build', '.git', '__MACOSX', 'Carthage', +]); function register(app, { requireLogin }) { // --- Pages --- @@ -76,60 +29,116 @@ function register(app, { requireLogin }) { res.sendFile(path.join(__dirname, '..', 'views', 'builds.html')); }); - // --- Upload a source archive --- - app.post('/api/build/upload', requireLogin, archiveUpload.single('source'), async (req, res) => { - if (!req.file) return res.status(400).json({ error: 'No source file provided' }); + // --- Filesystem browser --- + + app.get('/api/filesystem/browse', requireLogin, (req, res) => { + const targetPath = req.query.path || DEFAULT_BROWSE_ROOT; + + // Block path traversal + if (targetPath.includes('..')) { + return res.status(400).json({ error: 'Path traversal not allowed' }); + } + + if (!fs.existsSync(targetPath)) { + return res.status(404).json({ error: 'Path not found' }); + } + + const stat = fs.statSync(targetPath); + if (!stat.isDirectory()) { + return res.status(400).json({ error: 'Path is not a directory' }); + } + + let entries; try { - const jobId = uuidv4(); - const sourceDir = path.join(SOURCE_DIR, jobId); - await extractArchive(req.file.path, sourceDir); - fs.unlinkSync(req.file.path); - - const scheme = (req.body.scheme || '').trim() || null; - - db.prepare(` - INSERT INTO build_jobs (id, source_kind, source_ref, scheme, status) - VALUES (?, 'upload', ?, ?, 'pending') - `).run(jobId, req.file.originalname, scheme); - - buildWorker.kick(); - res.json({ success: true, job_id: jobId }); + entries = fs.readdirSync(targetPath, { withFileTypes: true }); + } catch (err) { + return res.status(403).json({ error: `Cannot read directory: ${err.message}` }); + } + + const result = []; + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; + if (SKIP_DIRS.has(entry.name)) continue; + + if (entry.isDirectory()) { + if (entry.name.endsWith('.xcodeproj')) { + result.push({ name: entry.name, type: 'xcodeproj', path: path.join(targetPath, entry.name) }); + } else if (entry.name.endsWith('.xcworkspace')) { + result.push({ name: entry.name, type: 'xcworkspace', path: path.join(targetPath, entry.name) }); + } else { + result.push({ name: entry.name, type: 'directory', path: path.join(targetPath, entry.name) }); + } + } + } + + // Sort: Xcode projects first, then directories alphabetically + result.sort((a, b) => { + const aIsXcode = a.type === 'xcodeproj' || a.type === 'xcworkspace'; + const bIsXcode = b.type === 'xcodeproj' || b.type === 'xcworkspace'; + if (aIsXcode && !bIsXcode) return -1; + if (!aIsXcode && bIsXcode) return 1; + return a.name.localeCompare(b.name); + }); + + res.json({ + path: targetPath, + parent: path.dirname(targetPath), + entries: result, + }); + }); + + // --- List schemes for a project --- + + app.get('/api/filesystem/schemes', requireLogin, async (req, res) => { + const projectPath = req.query.projectPath; + if (!projectPath) { + return res.status(400).json({ error: 'projectPath is required' }); + } + if (!fs.existsSync(projectPath)) { + return res.status(404).json({ error: 'Project not found' }); + } + if (!projectPath.endsWith('.xcodeproj') && !projectPath.endsWith('.xcworkspace')) { + return res.status(400).json({ error: 'Not an Xcode project or workspace' }); + } + + try { + const schemes = await buildWorker.listSchemesForPath(projectPath); + res.json({ schemes }); } catch (err) { - if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); res.status(500).json({ error: err.message }); } }); - // --- Clone a git repo --- - app.post('/api/build/git', requireLogin, async (req, res) => { - const { url, branch, scheme } = req.body || {}; - if (!url) return res.status(400).json({ error: 'url is required' }); + // --- Trigger a local build --- + + app.post('/api/build/local', requireLogin, (req, res) => { + const { projectPath, scheme } = req.body || {}; + if (!projectPath) { + return res.status(400).json({ error: 'projectPath is required' }); + } + if (!fs.existsSync(projectPath)) { + return res.status(404).json({ error: 'Project not found' }); + } + if (!projectPath.endsWith('.xcodeproj') && !projectPath.endsWith('.xcworkspace')) { + return res.status(400).json({ error: 'Not an Xcode project or workspace' }); + } const jobId = uuidv4(); - const logPath = path.join(LOGS_DIR, `${jobId}.log`); - fs.writeFileSync(logPath, `Cloning ${url}${branch ? ` (branch ${branch})` : ''}…\n`); + const projectName = path.basename(projectPath); db.prepare(` - INSERT INTO build_jobs (id, source_kind, source_ref, scheme, status, log_path) - VALUES (?, 'git', ?, ?, 'pending', ?) - `).run(jobId, url, scheme || null, logPath); + INSERT INTO build_jobs (id, project_path, source_ref, scheme, status) + VALUES (?, ?, ?, ?, 'pending') + `).run(jobId, projectPath, projectName, scheme || null); - try { - const sourceDir = path.join(SOURCE_DIR, jobId); - await cloneGitRepo({ url, branch, destDir: sourceDir, logPath }); - buildWorker.kick(); - res.json({ success: true, job_id: jobId }); - } catch (err) { - db.prepare("UPDATE build_jobs SET status = 'failed', error = ?, finished_at = datetime('now') WHERE id = ?") - .run(err.message, jobId); - res.status(500).json({ error: err.message }); - } + buildWorker.kick(); + res.json({ success: true, job_id: jobId }); }); // --- List jobs --- app.get('/api/builds', requireLogin, (req, res) => { const rows = db.prepare(` - SELECT id, bundle_id, source_kind, source_ref, scheme, status, started_at, finished_at, error, unraid_build_id, install_url + SELECT id, bundle_id, project_path, source_ref, scheme, status, started_at, finished_at, error, unraid_build_id, install_url FROM build_jobs ORDER BY COALESCE(started_at, created_at) DESC LIMIT 100 @@ -167,7 +176,6 @@ function register(app, { requireLogin }) { fs.readSync(fd, buf, 0, buf.length, position); fs.closeSync(fd); position = stat.size; - // SSE: prefix every line with "data: " const lines = buf.toString('utf8').split('\n'); for (const line of lines) { if (line.length) res.write(`data: ${line}\n\n`); @@ -177,7 +185,6 @@ function register(app, { requireLogin }) { sendNew(); const interval = setInterval(() => { sendNew(); - // Check if job finished — send one more time and close after a grace period. const current = db.prepare('SELECT status FROM build_jobs WHERE id = ?').get(req.params.id); if (current && (current.status === 'succeeded' || current.status === 'failed')) { sendNew(); @@ -189,11 +196,6 @@ function register(app, { requireLogin }) { req.on('close', () => clearInterval(interval)); }); - - // --- Rebuild a finished job (reuses the last known source if available) --- - app.post('/api/builds/:id/rebuild', requireLogin, (req, res) => { - res.status(501).json({ error: 'rebuild not implemented yet' }); - }); } module.exports = { register }; diff --git a/builder/src/build-worker.js b/builder/src/build-worker.js index 1cdd0cd..53b636e 100644 --- a/builder/src/build-worker.js +++ b/builder/src/build-worker.js @@ -1,4 +1,4 @@ -// Build worker — consumes `build_jobs` rows, runs xcodebuild + fastlane + upload. +// Build worker — consumes `build_jobs` rows, runs xcodebuild + upload. // Single in-process loop; SQLite is the queue. const path = require('path'); @@ -11,11 +11,10 @@ const execFileAsync = promisify(execFile); const { db, getSetting, DATA_DIR } = require('./db'); const profileManager = require('./profile-manager'); -const SOURCE_DIR = path.join(DATA_DIR, 'source'); const BUILD_DIR = path.join(DATA_DIR, 'build'); const LOGS_DIR = path.join(DATA_DIR, 'builds'); -[SOURCE_DIR, BUILD_DIR, LOGS_DIR].forEach((d) => fs.mkdirSync(d, { recursive: true })); +[BUILD_DIR, LOGS_DIR].forEach((d) => fs.mkdirSync(d, { recursive: true })); const POLL_INTERVAL_MS = 2000; let running = false; @@ -62,28 +61,29 @@ function runCommand(cmd, args, { cwd, env, logPath }) { }); } -// --- Project locator --- +// --- Scheme listing (standalone, no log file required) --- -function findProjectRoot(sourceDir) { - // Walk up to 3 levels looking for .xcodeproj/.xcworkspace. - const walk = (dir, depth) => { - if (depth > 3) return null; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - const workspace = entries.find((e) => e.isDirectory() && e.name.endsWith('.xcworkspace') && !e.name.endsWith('.xcodeproj/project.xcworkspace')); - if (workspace) return { dir, type: 'workspace', name: workspace.name }; - const project = entries.find((e) => e.isDirectory() && e.name.endsWith('.xcodeproj')); - if (project) return { dir, type: 'project', name: project.name }; - for (const e of entries) { - if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') { - const found = walk(path.join(dir, e.name), depth + 1); - if (found) return found; - } - } - return null; - }; - return walk(sourceDir, 0); +async function listSchemesForPath(projectPath) { + const projectName = path.basename(projectPath); + const projectDir = path.dirname(projectPath); + const type = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project'; + const flag = type === 'workspace' ? '-workspace' : '-project'; + + const { stdout } = await execFileAsync('/usr/bin/xcodebuild', ['-list', '-json', flag, projectName], { + cwd: projectDir, + env: process.env, + timeout: 60000, + }); + try { + const parsed = JSON.parse(stdout); + return parsed.workspace?.schemes || parsed.project?.schemes || []; + } catch { + return []; + } } +// --- Build helpers --- + async function listSchemes({ projectRoot, logPath }) { const args = projectRoot.type === 'workspace' ? ['-list', '-json', '-workspace', projectRoot.name] @@ -98,7 +98,7 @@ async function listSchemes({ projectRoot, logPath }) { } async function getBuildSettings({ projectRoot, scheme, logPath }) { - const args = ['-showBuildSettings', '-json', '-scheme', scheme]; + const args = ['-showBuildSettings', '-json', '-scheme', scheme, '-configuration', 'Release']; if (projectRoot.type === 'workspace') args.unshift('-workspace', projectRoot.name); else args.unshift('-project', projectRoot.name); const { stdout } = await runCommand('/usr/bin/xcodebuild', args, { cwd: projectRoot.dir, env: process.env, logPath }); @@ -144,14 +144,14 @@ ${entries} `; } -// --- Upload to unraid --- +// --- Upload to storefront --- -async function uploadToUnraid({ ipaPath, notes, logPath }) { - const unraidUrl = getSetting('unraid_url'); - const unraidToken = getSetting('unraid_token'); - if (!unraidUrl || !unraidToken) throw new Error('unraid URL/token not configured'); +async function uploadToStorefront({ ipaPath, notes, logPath }) { + const storefrontUrl = getSetting('unraid_url'); + const storefrontToken = getSetting('unraid_token'); + if (!storefrontUrl || !storefrontToken) throw new Error('Storefront URL/token not configured'); - appendLog(logPath, `\nUploading IPA to ${unraidUrl}/api/upload`); + appendLog(logPath, `\nUploading IPA to ${storefrontUrl}/api/upload`); const buf = fs.readFileSync(ipaPath); const blob = new Blob([buf], { type: 'application/octet-stream' }); @@ -159,16 +159,16 @@ async function uploadToUnraid({ ipaPath, notes, logPath }) { form.append('ipa', blob, path.basename(ipaPath)); if (notes) form.append('notes', notes); - const res = await fetch(`${unraidUrl}/api/upload`, { + const res = await fetch(`${storefrontUrl}/api/upload`, { method: 'POST', - headers: { 'X-Api-Token': unraidToken }, + headers: { 'X-Api-Token': storefrontToken }, body: form, }); const body = await res.json().catch(() => ({})); if (!res.ok || !body.success) { - throw new Error(`unraid upload failed (${res.status}): ${JSON.stringify(body)}`); + throw new Error(`Storefront upload failed (${res.status}): ${JSON.stringify(body)}`); } - appendLog(logPath, `✓ Uploaded: ${JSON.stringify(body)}`); + appendLog(logPath, `Uploaded: ${JSON.stringify(body)}`); return body; } @@ -177,19 +177,25 @@ async function uploadToUnraid({ ipaPath, notes, logPath }) { async function runBuild(job) { const jobId = job.id; const logPath = path.join(LOGS_DIR, `${jobId}.log`); - markStatus(jobId, 'preparing', { log_path: logPath, started_at: "datetime('now')" }); - // Reset started_at properly (datetime() doesn't work via binding above). + markStatus(jobId, 'preparing', { log_path: logPath }); db.prepare("UPDATE build_jobs SET started_at = datetime('now') WHERE id = ?").run(jobId); fs.writeFileSync(logPath, `Build ${jobId} started at ${new Date().toISOString()}\n`); section(logPath, 'PREPARING'); - const sourceDir = path.join(SOURCE_DIR, jobId); - if (!fs.existsSync(sourceDir)) throw new Error(`source dir missing: ${sourceDir}`); + // Resolve the local project path + const projectPath = job.project_path; + if (!projectPath || !fs.existsSync(projectPath)) { + throw new Error(`Project not found: ${projectPath}`); + } - const projectRoot = findProjectRoot(sourceDir); - if (!projectRoot) throw new Error('No .xcodeproj or .xcworkspace found in source'); - appendLog(logPath, `Found ${projectRoot.type}: ${projectRoot.name} in ${projectRoot.dir}`); + const projectName = path.basename(projectPath); + const projectDir = path.dirname(projectPath); + const type = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project'; + const projectRoot = { dir: projectDir, type, name: projectName }; + + appendLog(logPath, `Project: ${projectPath}`); + appendLog(logPath, `Type: ${type}, Name: ${projectName}`); // Pick the scheme const schemes = await listSchemes({ projectRoot, logPath }); @@ -207,7 +213,6 @@ async function runBuild(job) { appendLog(logPath, `Team: ${teamId}`); appendLog(logPath, `Bundle IDs: ${bundleIds.join(', ')}`); - // Persist the primary bundle id on the job row db.prepare('UPDATE build_jobs SET bundle_id = ?, scheme = ? WHERE id = ?') .run(bundleIds[0], scheme, jobId); @@ -215,12 +220,17 @@ async function runBuild(job) { section(logPath, 'SIGNING'); markStatus(jobId, 'signing'); + if (!teamId) { + throw new Error('Could not determine DEVELOPMENT_TEAM from Xcode project'); + } + const profilesByBundleId = {}; for (const bid of bundleIds) { - appendLog(logPath, `Ensuring profile for ${bid}…`); - const info = await profileManager.getProfile(bid); + appendLog(logPath, `Ensuring profile for ${bid} (team ${teamId})...`); + const info = await profileManager.getProfile(bid, { teamId }); profilesByBundleId[bid] = info.profile_uuid; - appendLog(logPath, ` → ${info.profile_uuid} (${info.fromCache ? 'cache' : 'fresh'}, ${info.device_count} devices, expires ${info.expires_at})`); + const source = info.fromCache ? 'cache' : info.fromDisk ? 'disk' : 'fastlane'; + appendLog(logPath, ` -> ${info.profile_uuid} (${source}, ${info.device_count} devices, expires ${info.expires_at})`); } // --- Archiving phase --- @@ -235,13 +245,9 @@ async function runBuild(job) { '-destination', 'generic/platform=iOS', '-archivePath', archivePath, '-allowProvisioningUpdates', - 'CODE_SIGN_STYLE=Manual', `DEVELOPMENT_TEAM=${teamId}`, 'archive', ]; - // We can't specify per-target PROVISIONING_PROFILE_SPECIFIER globally, so we rely on - // xcodebuild finding the installed profiles in ~/Library/MobileDevice/Provisioning Profiles/ - // by matching bundle id + team id. await runCommand('/usr/bin/xcodebuild', archiveArgs, { cwd: projectRoot.dir, env: process.env, logPath }); // --- Exporting phase --- @@ -274,9 +280,9 @@ async function runBuild(job) { section(logPath, 'UPLOADING'); markStatus(jobId, 'uploading', { ipa_path: ipaPath }); - const uploadResult = await uploadToUnraid({ + const uploadResult = await uploadToStorefront({ ipaPath, - notes: `Built by ${os.hostname()} job ${jobId}`, + notes: `Built from ${projectName} by ${os.hostname()} job ${jobId}`, logPath, }); @@ -285,9 +291,8 @@ async function runBuild(job) { install_url: uploadResult.build?.install_url || null, }); - // --- Cleanup: keep log + IPA, remove source + archive --- + // Cleanup: remove archive only (keep IPA, never touch user's project) try { - fs.rmSync(sourceDir, { recursive: true, force: true }); fs.rmSync(archivePath, { recursive: true, force: true }); } catch (e) { appendLog(logPath, `Cleanup warning: ${e.message}`); @@ -302,7 +307,7 @@ async function processJob(job) { await runBuild(job); } catch (err) { console.error(`[build-worker] job ${job.id} failed:`, err); - try { appendLog(logPath, `\n✗ FAILED: ${err.message}\n${err.stack || ''}`); } catch {} + try { appendLog(logPath, `\nFAILED: ${err.message}\n${err.stack || ''}`); } catch {} markStatus(job.id, 'failed', { error: err.message }); } } @@ -322,7 +327,6 @@ async function loop() { } function kick() { - // Non-blocking: fire and forget. loop().catch((err) => console.error('[build-worker] loop error:', err)); } @@ -331,4 +335,4 @@ function start() { kick(); } -module.exports = { start, kick, runBuild, processJob }; +module.exports = { start, kick, listSchemesForPath }; diff --git a/builder/src/db.js b/builder/src/db.js index 3fd8df3..fb80a28 100644 --- a/builder/src/db.js +++ b/builder/src/db.js @@ -15,26 +15,6 @@ db.exec(` value TEXT ); - CREATE TABLE IF NOT EXISTS devices ( - udid TEXT PRIMARY KEY, - name TEXT, - model TEXT, - platform TEXT DEFAULT 'IOS', - apple_device_id TEXT, - synced_at TEXT, - added_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS apps ( - id TEXT PRIMARY KEY, - bundle_id TEXT UNIQUE NOT NULL, - name TEXT, - scheme TEXT, - team_id TEXT, - last_built_at TEXT, - created_at TEXT DEFAULT (datetime('now')) - ); - CREATE TABLE IF NOT EXISTS profiles ( bundle_id TEXT PRIMARY KEY, profile_uuid TEXT, @@ -42,15 +22,15 @@ db.exec(` team_id TEXT, expires_at TEXT, device_count INTEGER, + method TEXT DEFAULT 'ad-hoc', path TEXT, updated_at TEXT ); CREATE TABLE IF NOT EXISTS build_jobs ( id TEXT PRIMARY KEY, - app_id TEXT, bundle_id TEXT, - source_kind TEXT, + project_path TEXT, source_ref TEXT, scheme TEXT, status TEXT DEFAULT 'pending', @@ -63,8 +43,38 @@ db.exec(` error TEXT, created_at TEXT DEFAULT (datetime('now')) ); + + CREATE TABLE IF NOT EXISTS asc_keys ( + team_id TEXT PRIMARY KEY, + team_name TEXT, + key_id TEXT NOT NULL, + issuer_id TEXT NOT NULL, + p8_filename TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); `); +// Idempotent migrations for existing databases +try { db.exec("ALTER TABLE build_jobs ADD COLUMN project_path TEXT"); } catch {} +try { db.exec("ALTER TABLE profiles ADD COLUMN method TEXT DEFAULT 'ad-hoc'"); } catch {} + +// Backfill asc_keys from legacy single-key settings (one-shot on first run after upgrade) +try { + const existing = db.prepare('SELECT COUNT(*) AS c FROM asc_keys').get(); + if (existing.c === 0) { + const keyId = db.prepare("SELECT value FROM settings WHERE key = 'asc_key_id'").get()?.value; + const issuerId = db.prepare("SELECT value FROM settings WHERE key = 'asc_issuer_id'").get()?.value; + if (keyId && issuerId) { + const p8Path = path.join(DATA_DIR, 'asc', `${keyId}.p8`); + const p8Filename = fs.existsSync(p8Path) ? `${keyId}.p8` : null; + db.prepare(` + INSERT INTO asc_keys (team_id, team_name, key_id, issuer_id, p8_filename) + VALUES (?, ?, ?, ?, ?) + `).run('QND55P4443', 'Legacy', keyId, issuerId, p8Filename); + } + } +} catch {} + function getSetting(key) { const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key); return row ? row.value : null; diff --git a/builder/src/profile-manager.js b/builder/src/profile-manager.js index 1b0be96..0746617 100644 --- a/builder/src/profile-manager.js +++ b/builder/src/profile-manager.js @@ -1,15 +1,15 @@ -// Profile manager: wraps `fastlane sigh` to generate/cache ad-hoc provisioning profiles -// keyed by bundle identifier. Handles ASC key JSON materialization, profile parsing, -// cache invalidation, and installation into ~/Library/MobileDevice/Provisioning Profiles/. +// Profile manager: finds existing ad-hoc provisioning profiles on disk, +// or generates new ones via `fastlane sigh`. Caches results in SQLite. const path = require('path'); const fs = require('fs'); const os = require('os'); +const crypto = require('crypto'); const { execFile } = require('child_process'); const { promisify } = require('util'); const execFileAsync = promisify(execFile); -const { db, getSetting, DATA_DIR } = require('./db'); +const { db, DATA_DIR } = require('./db'); const PROFILES_DIR = path.join(DATA_DIR, 'profiles'); const ASC_DIR = path.join(DATA_DIR, 'asc'); @@ -18,20 +18,31 @@ const INSTALLED_PROFILES_DIR = path.join(os.homedir(), 'Library/MobileDevice/Pro fs.mkdirSync(PROFILES_DIR, { recursive: true }); -// Minimum lifetime on a cached profile before we regenerate it proactively. const MIN_LIFETIME_DAYS = 30; -function buildAscKeyJsonPath() { - const keyId = getSetting('asc_key_id'); - const issuerId = getSetting('asc_issuer_id'); - if (!keyId || !issuerId) throw new Error('ASC key id / issuer id not configured'); - const p8Path = path.join(ASC_DIR, `${keyId}.p8`); - if (!fs.existsSync(p8Path)) throw new Error('.p8 file not uploaded'); - const keyContent = fs.readFileSync(p8Path, 'utf8'); - const jsonPath = path.join(ASC_DIR, `${keyId}.json`); +// --- ASC key resolution (per-team) --- + +function listAscKeys() { + return db.prepare('SELECT * FROM asc_keys ORDER BY created_at').all(); +} + +function getAscKey(teamId) { + if (!teamId) throw new Error('teamId is required'); + const row = db.prepare('SELECT * FROM asc_keys WHERE team_id = ?').get(teamId); + if (!row) throw new Error(`No ASC key configured for team ${teamId}. Add it on the Settings page.`); + if (!row.p8_filename) throw new Error(`ASC key for team ${teamId} exists but .p8 has not been uploaded.`); + const p8Path = path.join(ASC_DIR, row.p8_filename); + if (!fs.existsSync(p8Path)) throw new Error(`.p8 file missing on disk for team ${teamId}: ${row.p8_filename}`); + return { ...row, p8Path }; +} + +function buildAscKeyJsonPath(teamId) { + const key = getAscKey(teamId); + const keyContent = fs.readFileSync(key.p8Path, 'utf8'); + const jsonPath = path.join(ASC_DIR, `${key.key_id}.json`); const json = { - key_id: keyId, - issuer_id: issuerId, + key_id: key.key_id, + issuer_id: key.issuer_id, key: keyContent, duration: 1200, in_house: false, @@ -40,9 +51,9 @@ function buildAscKeyJsonPath() { return jsonPath; } +// --- Profile parser --- + function parseMobileprovision(filePath) { - // Extract the plist contents from the CMS-wrapped .mobileprovision via `security cms -D`. - // Falls back to a regex scan if `security` isn't available. const { execFileSync } = require('child_process'); let xml; try { @@ -74,11 +85,30 @@ function parseMobileprovision(filePath) { || (xml.match(/TeamIdentifier<\/key>\s*\s*([^<]+)<\/string>/)?.[1] ?? null); const expiresAt = pickDate('ExpirationDate'); - // Devices count from the ProvisionedDevices array. + // Device count from the ProvisionedDevices array const devicesMatch = xml.match(/ProvisionedDevices<\/key>\s*([\s\S]*?)<\/array>/); const deviceCount = devicesMatch ? (devicesMatch[1].match(//g) || []).length : 0; - return { uuid, name, teamId, expiresAt, deviceCount }; + // Distribution method detection + const hasDevices = xml.includes('ProvisionedDevices'); + const provisionsAll = xml.includes('ProvisionsAllDevices'); + const getTaskAllow = xml.includes('get-task-allow') + && /get-task-allow<\/key>\s*/.test(xml); + + let method; + if (provisionsAll) method = 'enterprise'; + else if (hasDevices && !getTaskAllow) method = 'ad-hoc'; + else if (hasDevices && getTaskAllow) method = 'development'; + else method = 'app-store'; + + // Bundle ID from application-identifier entitlement + const appIdMatch = xml.match(/application-identifier<\/key>\s*([^<]+)<\/string>/); + const applicationIdentifier = appIdMatch ? appIdMatch[1] : null; + const bundleIdentifier = applicationIdentifier + ? applicationIdentifier.replace(/^[A-Z0-9]+\./, '') + : null; + + return { uuid, name, teamId, expiresAt, deviceCount, method, bundleIdentifier, filePath }; } function installProfile(srcPath, uuid) { @@ -88,6 +118,68 @@ function installProfile(srcPath, uuid) { return dest; } +// --- Scan installed profiles --- + +function scanInstalledProfiles(bundleId) { + if (!fs.existsSync(INSTALLED_PROFILES_DIR)) return []; + + const files = fs.readdirSync(INSTALLED_PROFILES_DIR) + .filter((f) => f.endsWith('.mobileprovision')); + + const matches = []; + for (const file of files) { + try { + const filePath = path.join(INSTALLED_PROFILES_DIR, file); + const info = parseMobileprovision(filePath); + if (info.method !== 'ad-hoc') continue; + + // Match bundle ID: exact match or wildcard (e.g., "TEAM.*" matches anything) + if (info.bundleIdentifier === bundleId || info.bundleIdentifier === '*') { + // Check not expired + if (info.expiresAt) { + const expiresMs = Date.parse(info.expiresAt); + if (!Number.isNaN(expiresMs) && expiresMs > Date.now()) { + matches.push(info); + } + } + } + } catch { + // Skip unparseable profiles + } + } + + // Sort by expiry date descending (longest remaining validity first) + matches.sort((a, b) => { + const aExp = Date.parse(a.expiresAt) || 0; + const bExp = Date.parse(b.expiresAt) || 0; + return bExp - aExp; + }); + + return matches; +} + +function scanAllInstalledProfiles() { + if (!fs.existsSync(INSTALLED_PROFILES_DIR)) return []; + + const files = fs.readdirSync(INSTALLED_PROFILES_DIR) + .filter((f) => f.endsWith('.mobileprovision')); + + const profiles = []; + for (const file of files) { + try { + const filePath = path.join(INSTALLED_PROFILES_DIR, file); + const info = parseMobileprovision(filePath); + profiles.push(info); + } catch { + // Skip unparseable + } + } + + return profiles; +} + +// --- DB cache helpers --- + function cachedRow(bundleId) { return db.prepare('SELECT * FROM profiles WHERE bundle_id = ?').get(bundleId); } @@ -101,13 +193,41 @@ function isCacheFresh(row) { return daysLeft >= MIN_LIFETIME_DAYS; } -async function runFastlaneSigh({ bundleId, outputPath, apiKeyJson, logStream }) { +function upsertProfileCache(bundleId, info) { + db.prepare(` + INSERT INTO profiles (bundle_id, profile_uuid, name, team_id, expires_at, device_count, method, path, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + ON CONFLICT(bundle_id) DO UPDATE SET + profile_uuid = excluded.profile_uuid, + name = excluded.name, + team_id = excluded.team_id, + expires_at = excluded.expires_at, + device_count = excluded.device_count, + method = excluded.method, + path = excluded.path, + updated_at = excluded.updated_at + `).run( + bundleId, + info.uuid, + info.name, + info.teamId, + info.expiresAt, + info.deviceCount, + info.method || 'ad-hoc', + info.filePath, + ); +} + +// --- Fastlane sigh --- + +async function runFastlaneSigh({ bundleId, teamId, outputPath, apiKeyJson, logStream }) { const args = [ 'run', 'sigh', `adhoc:true`, `force:true`, `app_identifier:${bundleId}`, + `team_id:${teamId}`, `api_key_path:${apiKeyJson}`, `output_path:${outputPath}`, `skip_install:true`, @@ -137,23 +257,52 @@ async function runFastlaneSigh({ bundleId, outputPath, apiKeyJson, logStream }) }); } -async function getProfile(bundleId, { force = false, logStream = null } = {}) { - if (!bundleId) throw new Error('bundleId is required'); +// --- Main profile resolution --- - const existing = cachedRow(bundleId); - if (!force && isCacheFresh(existing)) { - // Make sure it's installed locally so xcodebuild can find it. - try { installProfile(existing.path, existing.profile_uuid); } catch {} - return { ...existing, fromCache: true }; +async function getProfile(bundleId, { teamId, force = false, logStream = null } = {}) { + if (!bundleId) throw new Error('bundleId is required'); + if (!teamId) throw new Error('teamId is required'); + + // 1. Check DB cache + if (!force) { + const existing = cachedRow(bundleId); + if (isCacheFresh(existing)) { + try { installProfile(existing.path, existing.profile_uuid); } catch {} + return { ...existing, fromCache: true }; + } } - const apiKeyJson = buildAscKeyJsonPath(); + // 2. Scan installed profiles on disk + if (!force) { + const installed = scanInstalledProfiles(bundleId); + if (installed.length > 0) { + const best = installed[0]; + const daysLeft = (Date.parse(best.expiresAt) - Date.now()) / (1000 * 60 * 60 * 24); + if (daysLeft >= MIN_LIFETIME_DAYS) { + upsertProfileCache(bundleId, best); + return { + bundle_id: bundleId, + profile_uuid: best.uuid, + name: best.name, + team_id: best.teamId, + expires_at: best.expiresAt, + device_count: best.deviceCount, + method: best.method, + path: best.filePath, + fromCache: false, + fromDisk: true, + }; + } + } + } + + // 3. Generate via fastlane sigh + const apiKeyJson = buildAscKeyJsonPath(teamId); const outputPath = path.join(PROFILES_DIR, bundleId); fs.mkdirSync(outputPath, { recursive: true }); - await runFastlaneSigh({ bundleId, outputPath, apiKeyJson, logStream }); + await runFastlaneSigh({ bundleId, teamId, outputPath, apiKeyJson, logStream }); - // Find the .mobileprovision fastlane produced. const candidates = fs.readdirSync(outputPath) .filter((f) => f.endsWith('.mobileprovision')) .map((f) => ({ @@ -171,33 +320,13 @@ async function getProfile(bundleId, { force = false, logStream = null } = {}) { const parsed = parseMobileprovision(produced.path); if (!parsed.uuid) throw new Error('Could not parse UUID from produced profile'); - // Normalize storage: rename to .mobileprovision inside the per-bundle dir. const finalPath = path.join(outputPath, `${parsed.uuid}.mobileprovision`); if (produced.path !== finalPath) { fs.renameSync(produced.path, finalPath); } + parsed.filePath = finalPath; - db.prepare(` - INSERT INTO profiles (bundle_id, profile_uuid, name, team_id, expires_at, device_count, path, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) - ON CONFLICT(bundle_id) DO UPDATE SET - profile_uuid = excluded.profile_uuid, - name = excluded.name, - team_id = excluded.team_id, - expires_at = excluded.expires_at, - device_count = excluded.device_count, - path = excluded.path, - updated_at = excluded.updated_at - `).run( - bundleId, - parsed.uuid, - parsed.name, - parsed.teamId, - parsed.expiresAt, - parsed.deviceCount, - finalPath, - ); - + upsertProfileCache(bundleId, parsed); installProfile(finalPath, parsed.uuid); return { @@ -207,9 +336,132 @@ async function getProfile(bundleId, { force = false, logStream = null } = {}) { team_id: parsed.teamId, expires_at: parsed.expiresAt, device_count: parsed.deviceCount, + method: parsed.method, path: finalPath, fromCache: false, }; } -module.exports = { getProfile, parseMobileprovision }; +// --- ASC API: fetch registered bundle IDs --- + +function b64url(buf) { + return Buffer.from(buf).toString('base64url'); +} + +const jwtCache = new Map(); // teamId -> { jwt, exp } + +function signAscJwt(teamId) { + const cached = jwtCache.get(teamId); + if (cached && Date.now() / 1000 < cached.exp - 60) return cached.jwt; + + const key = getAscKey(teamId); + const privateKey = fs.readFileSync(key.p8Path, 'utf8'); + + const now = Math.floor(Date.now() / 1000); + const exp = now + 1200; + const header = b64url(JSON.stringify({ alg: 'ES256', kid: key.key_id, typ: 'JWT' })); + const payload = b64url(JSON.stringify({ iss: key.issuer_id, iat: now, exp, aud: 'appstoreconnect-v1' })); + const sig = crypto.sign('sha256', Buffer.from(`${header}.${payload}`), { key: privateKey, dsaEncoding: 'ieee-p1363' }); + + const jwt = `${header}.${payload}.${b64url(sig)}`; + jwtCache.set(teamId, { jwt, exp }); + return jwt; +} + +function invalidateJwtCache(teamId) { + if (teamId) jwtCache.delete(teamId); + else jwtCache.clear(); +} + +async function fetchBundleIdsForTeam(teamId) { + const jwt = signAscJwt(teamId); + const results = []; + let url = 'https://api.appstoreconnect.apple.com/v1/bundleIds?limit=200&sort=identifier'; + + while (url) { + const res = await fetch(url, { + headers: { Authorization: `Bearer ${jwt}` }, + }); + if (!res.ok) { + const body = await res.text(); + throw new Error(`ASC API error (${res.status}): ${body.slice(0, 500)}`); + } + const json = await res.json(); + for (const item of json.data || []) { + results.push({ + id: item.id, + identifier: item.attributes.identifier, + name: item.attributes.name, + platform: item.attributes.platform, + }); + } + url = json.links?.next || null; + } + + return results; +} + +async function fetchAllBundleIds() { + const keys = listAscKeys(); + const groups = []; + for (const key of keys) { + try { + const bundleIds = await fetchBundleIdsForTeam(key.team_id); + groups.push({ + teamId: key.team_id, + teamName: key.team_name || key.team_id, + bundleIds, + error: null, + }); + } catch (err) { + groups.push({ + teamId: key.team_id, + teamName: key.team_name || key.team_id, + bundleIds: [], + error: err.message, + }); + } + } + return groups; +} + +async function testAscKey(teamId) { + const jwt = signAscJwt(teamId); + const res = await fetch('https://api.appstoreconnect.apple.com/v1/apps?limit=1', { + headers: { Authorization: `Bearer ${jwt}` }, + }); + if (!res.ok) { + const body = await res.text(); + throw new Error(`ASC API (${res.status}): ${body.slice(0, 300)}`); + } + const json = await res.json(); + return { ok: true, app_count_sample: (json.data || []).length }; +} + +// --- Delete a profile --- + +function deleteProfile(uuid) { + // Remove from installed profiles dir + const installedPath = path.join(INSTALLED_PROFILES_DIR, `${uuid}.mobileprovision`); + if (fs.existsSync(installedPath)) fs.unlinkSync(installedPath); + + // Remove from DB cache + db.prepare('DELETE FROM profiles WHERE profile_uuid = ?').run(uuid); +} + +module.exports = { + getProfile, + parseMobileprovision, + scanInstalledProfiles, + scanAllInstalledProfiles, + deleteProfile, + installProfile, + listAscKeys, + getAscKey, + fetchBundleIdsForTeam, + fetchAllBundleIds, + testAscKey, + invalidateJwtCache, + INSTALLED_PROFILES_DIR, + ASC_DIR, +}; diff --git a/builder/src/server.js b/builder/src/server.js index 21eb522..97610df 100644 --- a/builder/src/server.js +++ b/builder/src/server.js @@ -21,7 +21,8 @@ const path = require('path'); const fs = require('fs'); const { db, getSetting, setSetting, DATA_DIR } = require('./db'); -const { requireLogin, requireBuilderSecret, validatePassword } = require('./auth'); +const { requireLogin, validatePassword } = require('./auth'); +const profileManager = require('./profile-manager'); const app = express(); const PORT = process.env.PORT || 3090; @@ -78,173 +79,123 @@ app.get('/settings', requireLogin, (req, res) => { res.sendFile(path.join(__dirname, '..', 'views', 'settings.html')); }); -app.get('/devices', requireLogin, (req, res) => { - res.sendFile(path.join(__dirname, '..', 'views', 'devices.html')); +app.get('/profiles', requireLogin, (req, res) => { + res.sendFile(path.join(__dirname, '..', 'views', 'profiles.html')); }); -// --- Device API --- +// --- Profile management API --- -function invalidateProfilesForDeviceChange() { - db.prepare('UPDATE profiles SET updated_at = NULL').run(); -} - -app.get('/api/devices', requireLogin, (req, res) => { - const rows = db.prepare('SELECT * FROM devices ORDER BY added_at DESC').all(); - res.json(rows); -}); - -app.post('/api/devices', requireLogin, async (req, res) => { - const { udid, name, model, platform = 'IOS' } = req.body || {}; - if (!udid || typeof udid !== 'string') { - return res.status(400).json({ error: 'UDID is required' }); - } - - // Upsert locally first so we always have a record even if Apple call fails. - db.prepare(` - INSERT INTO devices (udid, name, model, platform) - VALUES (?, ?, ?, ?) - ON CONFLICT(udid) DO UPDATE SET - name = COALESCE(NULLIF(excluded.name, ''), devices.name), - model = COALESCE(NULLIF(excluded.model, ''), devices.model), - platform = excluded.platform - `).run(udid, name || null, model || null, platform); - - // Try to register with Apple. - let synced = false; +app.get('/api/profiles', requireLogin, (req, res) => { try { - const asc = require('./asc-api'); - const appleDevice = await asc.registerDevice({ udid, name, platform }); - const appleDeviceId = appleDevice?.id || null; - db.prepare(` - UPDATE devices - SET apple_device_id = ?, synced_at = datetime('now') - WHERE udid = ? - `).run(appleDeviceId, udid); - synced = true; - invalidateProfilesForDeviceChange(); + const profiles = profileManager.scanAllInstalledProfiles(); + const now = Date.now(); + + const result = profiles.map((p) => { + const expiresMs = p.expiresAt ? Date.parse(p.expiresAt) : null; + let status = 'unknown'; + if (expiresMs) { + const daysLeft = (expiresMs - now) / (1000 * 60 * 60 * 24); + if (daysLeft <= 0) status = 'expired'; + else if (daysLeft < 30) status = 'expiring'; + else status = 'valid'; + } + + return { + uuid: p.uuid, + name: p.name, + bundleIdentifier: p.bundleIdentifier, + teamId: p.teamId, + method: p.method, + expiresAt: p.expiresAt, + deviceCount: p.deviceCount, + status, + filePath: p.filePath, + }; + }); + + // Sort: ad-hoc first, then by bundle ID + result.sort((a, b) => { + if (a.method === 'ad-hoc' && b.method !== 'ad-hoc') return -1; + if (a.method !== 'ad-hoc' && b.method === 'ad-hoc') return 1; + return (a.bundleIdentifier || '').localeCompare(b.bundleIdentifier || ''); + }); + + res.json(result); } catch (err) { - // Don't fail the request; the device is saved locally. - console.warn('[devices] ASC sync failed:', err.message); - return res.json({ success: true, synced: false, warning: err.message }); + res.status(500).json({ error: err.message }); } - - res.json({ success: true, synced }); }); -app.delete('/api/devices/:udid', requireLogin, (req, res) => { - db.prepare('DELETE FROM devices WHERE udid = ?').run(req.params.udid); - invalidateProfilesForDeviceChange(); - res.json({ success: true }); -}); - -// --- Enrollment bridge (called by unraid's /enroll/callback over the LAN) --- - -app.post('/api/devices/from-enrollment', requireBuilderSecret, async (req, res) => { - const { udid, name, model, platform = 'IOS' } = req.body || {}; - if (!udid || typeof udid !== 'string') { - return res.status(400).json({ error: 'UDID is required' }); - } - - db.prepare(` - INSERT INTO devices (udid, name, model, platform) - VALUES (?, ?, ?, ?) - ON CONFLICT(udid) DO UPDATE SET - name = COALESCE(NULLIF(excluded.name, ''), devices.name), - model = COALESCE(NULLIF(excluded.model, ''), devices.model), - platform = excluded.platform - `).run(udid, name || null, model || null, platform); - - let synced = false; +app.get('/api/bundle-ids', requireLogin, async (req, res) => { try { - const asc = require('./asc-api'); - const appleDevice = await asc.registerDevice({ udid, name, platform }); - const appleDeviceId = appleDevice?.id || null; - db.prepare(` - UPDATE devices - SET apple_device_id = ?, synced_at = datetime('now') - WHERE udid = ? - `).run(appleDeviceId, udid); - synced = true; - invalidateProfilesForDeviceChange(); + const groups = await profileManager.fetchAllBundleIds(); + res.json(groups); } catch (err) { - console.warn('[enrollment] ASC sync failed:', err.message); - return res.json({ success: true, synced: false, warning: err.message }); + res.status(500).json({ error: err.message }); } - - res.json({ success: true, synced }); }); -// --- Settings API --- +app.delete('/api/profiles/:uuid', requireLogin, (req, res) => { + try { + profileManager.deleteProfile(req.params.uuid); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); -const SETTINGS_KEYS = [ - 'asc_key_id', - 'asc_issuer_id', - 'unraid_url', - 'unraid_token', -]; +app.post('/api/profiles/regenerate', requireLogin, async (req, res) => { + const { bundleId, teamId } = req.body || {}; + if (!bundleId) return res.status(400).json({ error: 'bundleId is required' }); + if (!teamId) return res.status(400).json({ error: 'teamId is required' }); + + try { + const info = await profileManager.getProfile(bundleId, { teamId, force: true }); + res.json({ success: true, profile: info }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/profiles/generate', requireLogin, async (req, res) => { + const { bundleId, teamId } = req.body || {}; + if (!bundleId) return res.status(400).json({ error: 'bundleId is required' }); + if (!teamId) return res.status(400).json({ error: 'teamId is required' }); + + try { + const info = await profileManager.getProfile(bundleId, { teamId, force: true }); + res.json({ success: true, profile: info }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- Settings API (storefront only now; ASC keys have their own endpoints) --- app.get('/api/settings', requireLogin, (req, res) => { - const out = {}; - for (const k of SETTINGS_KEYS) { - out[k] = getSetting(k) || ''; - } - // Never expose the raw token; just indicate whether it's set - out.unraid_token = out.unraid_token ? '••••••••' : ''; - // Has the p8 been uploaded? - const keyId = out.asc_key_id; - out.asc_key_uploaded = keyId - ? fs.existsSync(path.join(DATA_DIR, 'asc', `${keyId}.p8`)) - : false; + const out = { + unraid_url: getSetting('unraid_url') || '', + unraid_token: getSetting('unraid_token') ? '••••••••' : '', + }; res.json(out); }); app.post('/api/settings', requireLogin, (req, res) => { - const { asc_key_id, asc_issuer_id, unraid_url, unraid_token } = req.body; - if (asc_key_id !== undefined) setSetting('asc_key_id', asc_key_id || ''); - if (asc_issuer_id !== undefined) setSetting('asc_issuer_id', asc_issuer_id || ''); + const { unraid_url, unraid_token } = req.body; if (unraid_url !== undefined) setSetting('unraid_url', unraid_url || ''); - // Only update the token if a real value was provided (not the placeholder) if (unraid_token && unraid_token !== '••••••••') { setSetting('unraid_token', unraid_token); } res.json({ success: true }); }); -app.post('/api/settings/p8', requireLogin, p8Upload.single('p8'), (req, res) => { - try { - if (!req.file) return res.status(400).json({ error: 'No file' }); - const keyId = getSetting('asc_key_id'); - if (!keyId) { - fs.unlinkSync(req.file.path); - return res.status(400).json({ error: 'Save Key ID before uploading .p8' }); - } - const dest = path.join(ASC_DIR, `${keyId}.p8`); - fs.renameSync(req.file.path, dest); - fs.chmodSync(dest, 0o600); - res.json({ success: true }); - } catch (err) { - if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); - res.status(500).json({ error: err.message }); - } -}); - -app.post('/api/settings/test-asc', requireLogin, async (req, res) => { - try { - const asc = require('./asc-api'); - const devices = await asc.listDevices(); - res.json({ success: true, device_count: devices.length }); - } catch (err) { - res.status(400).json({ error: err.message }); - } -}); - -app.post('/api/settings/test-unraid', requireLogin, async (req, res) => { +app.post('/api/settings/test-storefront', requireLogin, async (req, res) => { try { const url = getSetting('unraid_url'); const token = getSetting('unraid_token'); if (!url || !token) return res.status(400).json({ error: 'Set URL and token first' }); const r = await fetch(`${url}/api/apps`, { headers: { 'X-Api-Token': token } }); - if (!r.ok) return res.status(400).json({ error: `unraid returned ${r.status}` }); + if (!r.ok) return res.status(400).json({ error: `Storefront returned ${r.status}` }); const apps = await r.json(); res.json({ success: true, app_count: apps.length }); } catch (err) { @@ -252,35 +203,106 @@ app.post('/api/settings/test-unraid', requireLogin, async (req, res) => { } }); +// --- ASC keys API (per-team developer accounts) --- + +app.get('/api/asc-keys', requireLogin, (req, res) => { + try { + const keys = profileManager.listAscKeys(); + const out = keys.map((k) => ({ + team_id: k.team_id, + team_name: k.team_name, + key_id: k.key_id, + issuer_id: k.issuer_id, + p8_uploaded: !!k.p8_filename && fs.existsSync(path.join(profileManager.ASC_DIR, k.p8_filename)), + created_at: k.created_at, + })); + res.json(out); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/asc-keys', requireLogin, (req, res) => { + const { team_id, team_name, key_id, issuer_id } = req.body || {}; + if (!team_id || !key_id || !issuer_id) { + return res.status(400).json({ error: 'team_id, key_id, and issuer_id are required' }); + } + try { + db.prepare(` + INSERT INTO asc_keys (team_id, team_name, key_id, issuer_id) + VALUES (?, ?, ?, ?) + ON CONFLICT(team_id) DO UPDATE SET + team_name = excluded.team_name, + key_id = excluded.key_id, + issuer_id = excluded.issuer_id + `).run(team_id, team_name || team_id, key_id, issuer_id); + profileManager.invalidateJwtCache(team_id); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/asc-keys/:team_id/p8', requireLogin, p8Upload.single('p8'), (req, res) => { + try { + if (!req.file) return res.status(400).json({ error: 'No file' }); + const row = db.prepare('SELECT * FROM asc_keys WHERE team_id = ?').get(req.params.team_id); + if (!row) { + fs.unlinkSync(req.file.path); + return res.status(404).json({ error: 'Team not found — save key details first' }); + } + const filename = `${row.key_id}.p8`; + const dest = path.join(profileManager.ASC_DIR, filename); + fs.renameSync(req.file.path, dest); + fs.chmodSync(dest, 0o600); + db.prepare('UPDATE asc_keys SET p8_filename = ? WHERE team_id = ?').run(filename, row.team_id); + profileManager.invalidateJwtCache(row.team_id); + res.json({ success: true }); + } catch (err) { + if (req.file && fs.existsSync(req.file.path)) { + try { fs.unlinkSync(req.file.path); } catch {} + } + res.status(500).json({ error: err.message }); + } +}); + +app.delete('/api/asc-keys/:team_id', requireLogin, (req, res) => { + try { + const row = db.prepare('SELECT * FROM asc_keys WHERE team_id = ?').get(req.params.team_id); + if (!row) return res.status(404).json({ error: 'Team not found' }); + if (row.p8_filename) { + const p8Path = path.join(profileManager.ASC_DIR, row.p8_filename); + if (fs.existsSync(p8Path)) fs.unlinkSync(p8Path); + const jsonPath = path.join(profileManager.ASC_DIR, `${row.key_id}.json`); + if (fs.existsSync(jsonPath)) fs.unlinkSync(jsonPath); + } + db.prepare('DELETE FROM asc_keys WHERE team_id = ?').run(row.team_id); + profileManager.invalidateJwtCache(row.team_id); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/asc-keys/:team_id/test', requireLogin, async (req, res) => { + try { + const result = await profileManager.testAscKey(req.params.team_id); + res.json({ success: true, ...result }); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + // --- Build pipeline --- require('./build-routes').register(app, { requireLogin }); -// --- Profile API --- - -app.get('/api/profile/:bundleId', requireLogin, async (req, res) => { - try { - const profileManager = require('./profile-manager'); - const force = req.query.force === '1'; - const info = await profileManager.getProfile(req.params.bundleId, { force }); - if (req.query.download === '1') { - res.set('Content-Type', 'application/x-apple-aspen-config'); - res.set('Content-Disposition', `attachment; filename="${info.profile_uuid}.mobileprovision"`); - return res.sendFile(info.path); - } - res.json({ success: true, profile: info }); - } catch (err) { - console.error('[profile]', err); - res.status(500).json({ error: err.message }); - } -}); - // --- Health --- app.get('/api/health', (req, res) => { res.json({ status: 'ok', - version: '1.0.0', + version: '2.0.0', service: 'ios-appstore-builder', host: require('os').hostname(), }); @@ -289,7 +311,6 @@ app.get('/api/health', (req, res) => { app.listen(PORT, '0.0.0.0', () => { console.log(`iOS App Store Builder running on port ${PORT}`); console.log(`Data dir: ${DATA_DIR}`); - // Start the build worker loop. require('./build-worker').start(); console.log('Build worker started'); }); diff --git a/builder/views/_partial_nav.html b/builder/views/_partial_nav.html index a9ff8d9..1de5620 100644 --- a/builder/views/_partial_nav.html +++ b/builder/views/_partial_nav.html @@ -1,9 +1,10 @@
-

🔨 Builder

+

Builder

diff --git a/builder/views/build.html b/builder/views/build.html index d20e3ee..f947d55 100644 --- a/builder/views/build.html +++ b/builder/views/build.html @@ -8,11 +8,11 @@
-

🔨 Builder

+

Builder

@@ -22,40 +22,26 @@

New Build

-

From source archive

+

Select Xcode Project

-
- - - - -
- -
-
+
+
+

Loading...

+
-
-

From git URL

+ diff --git a/builder/views/builds.html b/builder/views/builds.html index 266277c..1d9ea2e 100644 --- a/builder/views/builds.html +++ b/builder/views/builds.html @@ -8,11 +8,11 @@
-

🔨 Builder

+

Builder

diff --git a/builder/views/devices.html b/builder/views/devices.html deleted file mode 100644 index aa86dcf..0000000 --- a/builder/views/devices.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - Devices - Builder - - - -
-

🔨 Builder

- -
- -
-

Devices

- -
-

Register a device

-
-
-
-
- - -
-
- - -
-
-
- -
-
-
-
- -
-

Registered devices

-
-

Loading…

-
-
- -
-
- - - - diff --git a/builder/views/index.html b/builder/views/index.html index c2e4c84..8780fc3 100644 --- a/builder/views/index.html +++ b/builder/views/index.html @@ -8,10 +8,11 @@
-

🔨 Builder

+

Builder

diff --git a/builder/views/profiles.html b/builder/views/profiles.html new file mode 100644 index 0000000..3f3bca4 --- /dev/null +++ b/builder/views/profiles.html @@ -0,0 +1,43 @@ + + + + + + Profiles - Builder + + + +
+

Builder

+ +
+ +
+

Provisioning Profiles

+ +
+

App Store Connect Bundle IDs

+
+

Loading bundle IDs from Apple...

+
+
+ +
+

Installed Ad-Hoc Profiles

+
+

Loading...

+
+
+ +
+
+ + + + diff --git a/builder/views/settings.html b/builder/views/settings.html index 9f5f7ef..aef0ef4 100644 --- a/builder/views/settings.html +++ b/builder/views/settings.html @@ -8,10 +8,11 @@
-

🔨 Builder

+

Builder

@@ -21,41 +22,58 @@

Settings

-

App Store Connect API

+

Developer Accounts

+

+ One App Store Connect API key per Apple Developer team. Used by fastlane to generate ad-hoc provisioning profiles. + The build worker auto-picks the key whose team_id matches the Xcode project's DEVELOPMENT_TEAM. +

-
+
+
+ +

Add Developer Account

+
+ +
+
+ + +
+
+ + +
+
- +
- +
- - -

- - +
+

After saving, use the Upload .p8 button in the table above.

-

unraid App Store

+

Storefront

+

Where built IPAs get uploaded for OTA distribution.

-
+ - +
- +
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..02ff187 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2686 @@ +{ + "name": "ios-appstore", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ios-appstore", + "version": "1.0.0", + "dependencies": { + "bcrypt": "^5.1.1", + "better-sqlite3": "^11.7.0", + "bplist-parser": "^0.3.2", + "express": "^4.21.0", + "express-session": "^1.18.1", + "multer": "^1.4.5-lts.1", + "plist": "^3.1.0", + "sharp": "^0.33.5", + "unzipper": "^0.12.3", + "uuid": "^10.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", + "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json index 982eca0..68da158 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "dev": "node --watch src/server.js" }, "dependencies": { + "bcrypt": "^5.1.1", "better-sqlite3": "^11.7.0", + "bplist-parser": "^0.3.2", "express": "^4.21.0", "express-session": "^1.18.1", "multer": "^1.4.5-lts.1", "plist": "^3.1.0", + "sharp": "^0.33.5", "unzipper": "^0.12.3", - "bcrypt": "^5.1.1", - "uuid": "^10.0.0", - "sharp": "^0.33.5" + "uuid": "^10.0.0" } } diff --git a/src/ipa-parser.js b/src/ipa-parser.js index 5f3aa63..1ff370b 100644 --- a/src/ipa-parser.js +++ b/src/ipa-parser.js @@ -1,9 +1,23 @@ const unzipper = require('unzipper'); const plist = require('plist'); +const bplist = require('bplist-parser'); const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); +async function parsePlist(buffer) { + // Try XML plist first + try { + return plist.parse(buffer.toString('utf-8')); + } catch {} + + // Try binary plist + const parsed = bplist.parseBuffer(buffer); + if (parsed && parsed.length > 0) return parsed[0]; + + throw new Error('Could not parse plist (neither XML nor binary)'); +} + async function parseIPA(ipaPath, outputDir) { const directory = await unzipper.Open.file(ipaPath); @@ -17,13 +31,7 @@ async function parseIPA(ipaPath, outputDir) { } const plistBuffer = await infoPlistEntry.buffer(); - let info; - try { - info = plist.parse(plistBuffer.toString('utf-8')); - } catch { - // Binary plist — try to parse differently - throw new Error('Binary plist detected. Please ensure IPA contains XML plist.'); - } + const info = await parsePlist(plistBuffer); const metadata = { bundleId: info.CFBundleIdentifier,