(
_hookName: string,
{app}: ArgsExpressType,
cb: Function,
)
| 125 | }; |
| 126 | |
| 127 | export const expressCreateServer = ( |
| 128 | _hookName: string, |
| 129 | {app}: ArgsExpressType, |
| 130 | cb: Function, |
| 131 | ): void => { |
| 132 | // Tier "off" disables the entire updater feature, including its HTTP surface. |
| 133 | if (settings.updates.tier === 'off') return cb(); |
| 134 | |
| 135 | // Public endpoint. Cached for 60s per (padId, authorId) key. |
| 136 | app.get('/api/version-status', wrapAsync(async (req, res) => { |
| 137 | const padId = typeof req.query.padId === 'string' ? req.query.padId : null; |
| 138 | const authorId = await resolveRequestAuthor(req); |
| 139 | const key = `${padId ?? ''}|${authorId ?? ''}`; |
| 140 | const now = Date.now(); |
| 141 | |
| 142 | const hit = cache.get(key); |
| 143 | if (hit && now - hit.at <= TTL_MS) { |
| 144 | res.json(hit.value); |
| 145 | return; |
| 146 | } |
| 147 | |
| 148 | let flight = inFlight.get(key); |
| 149 | if (!flight) { |
| 150 | flight = computeOutdated(padId, authorId).finally(() => { inFlight.delete(key); }); |
| 151 | inFlight.set(key, flight); |
| 152 | } |
| 153 | const value = await flight; |
| 154 | cache.set(key, {value, at: now}); |
| 155 | res.json(value); |
| 156 | })); |
| 157 | |
| 158 | // Admin UI status endpoint. By default this is open: the running version is already |
| 159 | // exposed publicly via /health, and latest/changelog come from a public GitHub |
| 160 | // release. Admins who want the endpoint gated to authenticated admin sessions — |
| 161 | // without disabling the updater entirely — set updates.requireAdminForStatus=true. |
| 162 | app.get('/admin/update/status', wrapAsync(async (req, res) => { |
| 163 | const isAdmin = !!req.session?.user?.is_admin; |
| 164 | if (settings.updates.requireAdminForStatus) { |
| 165 | const user = req.session?.user; |
| 166 | if (!user) return res.status(401).send('Authentication required'); |
| 167 | if (!user.is_admin) return res.status(403).send('Forbidden'); |
| 168 | } |
| 169 | const state = await loadState(stateFilePath()); |
| 170 | const current = getEpVersion(); |
| 171 | const installMethod = getDetectedInstallMethod(); |
| 172 | const policy = state.latest |
| 173 | ? evaluatePolicy({ |
| 174 | installMethod, |
| 175 | tier: settings.updates.tier, |
| 176 | current, |
| 177 | latest: state.latest.version, |
| 178 | executionStatus: state.execution.status, |
| 179 | maintenanceWindow: settings.updates.maintenanceWindow, |
| 180 | }) |
| 181 | : null; |
| 182 | const lockHeld = await isHeld(path.join(settings.root, 'var', 'update.lock')); |
| 183 | // Tier 4: surface the configured window + the next opening so the admin UI |
| 184 | // can render the picker and the "deferred until..." subtitle on the |
no test coverage detected