mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-24 10:37:04 +00:00
* docs: design spec for #7799 outdated-notice redesign Per-pad first-author gating, dismissable gritter, minor-or-more rule, drop vulnerable UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for #7799 outdated-notice redesign 12 bite-sized tasks, TDD-first where applicable; closes the spec end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): add isMinorOrMoreBehind, drop major/vulnerable helpers Adds isMinorOrMoreBehind(current, latest) which returns true only when the latest release is at least one minor version ahead (patch-only deltas return false). Removes isMajorBehind, parseVulnerableBelow, and isVulnerable from versionCompare.ts — callers in updateStatus.ts, VersionChecker.ts, and index.ts will be updated in subsequent tasks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(updater): drop vulnerable-below directive and state field Remove VulnerableBelowDirective type, UpdateState.vulnerableBelow field, and all related scraping/checking logic (parseVulnerableBelow, isVulnerable imports). Clean up Notifier, OpenAPI schema, and all test fixtures to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(updater): drop residual EmailSendLog vulnerable fields Remove `vulnerableAt` and `vulnerableNewReleaseTag` from the `EmailSendLog` interface, `EMPTY_STATE`, and the `isValidEmail` validator — these backed the removed `vulnerable`/`vulnerable-new-release` email kinds and are now dead code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add firstAuthorOf helper Export firstAuthorOf() from updateStatus.ts — finds the lowest-numbered author attrib in a pad's pool, skipping empty-string placeholders. Covered by 6 vitest cases in tests/backend-new/specs/hooks/express/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add resolveRequestAuthor helper for HTTP GET Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): pad-aware /api/version-status with first-author gating Replace global badge cache with a per-(padId, authorId) LRU cache. The new response shape is {outdated: 'minor' | null, isFirstAuthor: boolean}; the old 'severe'/'vulnerable' enum is dropped entirely. computeOutdated now resolves the pad's first author and compares it against the session author before returning outdated:'minor', so the notice is only shown to the person who created the pad. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): switch isSevere signal from major-only to minor-or-more behind Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(updater): end-to-end coverage for /api/version-status Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(openapi): /api/version-status pad-aware shape and gating Add the /api/version-status GET operation to the admin OpenAPI spec with the new pad-aware response shape: outdated enum reduced to [minor]|null, isFirstAuthor boolean, and an optional padId query param. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(pad): remove unused #version-badge template and CSS * feat(pad): replace persistent badge with first-author outdated gritter Renames pad_version_badge.ts → pad_outdated_notice.ts and rewrites it as a fire-and-forget gritter notice that only shows when the API reports outdated=minor AND the current user is the pad's first author. Wires the new maybeShowOutdatedNotice() call into pad.ts immediately after showPrivacyBannerIfEnabled(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(pad): playwright coverage for outdated notice gritter Six Playwright specs exercise maybeShowOutdatedNotice: null response, isFirstAuthor:false guard, positive appearance + text, X-dismiss, 500 server error tolerance, and 8 s auto-fade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(pad): outdated-notice redesign + drop vulnerable-below docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(test): remove stale specs for deleted #version-badge surface Delete the GET /api/version-status describe block from the legacy mocha spec (asserted outdated:null and outdated:'severe' — both no longer match the new response shape). The new vitest spec at tests/backend-new/specs/hooks/express/updateStatus.test.ts covers this surface comprehensively. Delete src/tests/frontend-new/specs/pad-version-badge.spec.ts entirely: all three tests reference the #version-badge DOM element removed in Task 8 and stub 'severe'/'vulnerable' enum values that no longer exist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: clean stale references to vulnerable/severe in types, emails, docs - Remove OutdatedLevel type (null|'severe') from types.ts — no consumers remain after the badge redesign removed the severe tier. - Fix Notifier severe-email body: was "more than one major release behind" but isSevere now fires on minor-or-more, so update to "at least one minor release behind the latest published version". - Drop "vulnerability directives" from the /admin/update/status OpenAPI description; replace with the actual response fields. - Remove stale vulnerableBelow field from UpdateStatusPayload in admin/src/store/store.ts — server no longer sends it. - Fix docs/admin/updates.md: "pad-side badge" → "pad-side notice". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
3 KiB
TypeScript
91 lines
3 KiB
TypeScript
import {ReleaseInfo} from './types';
|
|
import {isValidTag} from './refSafety';
|
|
|
|
export interface FetchResult {
|
|
status: number;
|
|
etag: string | null;
|
|
/** Parsed JSON body on 200, otherwise null. */
|
|
json: any;
|
|
}
|
|
|
|
/** Adapter so tests can stub the network. Maps URL+ETag to a FetchResult. */
|
|
export type Fetcher = (url: string, etag: string | null) => Promise<FetchResult>;
|
|
|
|
/** Discriminated union of every outcome the checker can return. */
|
|
export type CheckResult =
|
|
| {kind: 'updated'; release: ReleaseInfo; etag: string | null}
|
|
| {kind: 'notmodified'}
|
|
| {kind: 'ratelimited'}
|
|
| {kind: 'skipped-prerelease'; etag: string | null}
|
|
| {kind: 'error'; status: number};
|
|
|
|
export interface CheckOptions {
|
|
fetcher: Fetcher;
|
|
prevEtag: string | null;
|
|
/** GitHub repo as `owner/name`, e.g. `ether/etherpad`. */
|
|
repo: string;
|
|
}
|
|
|
|
/**
|
|
* Hit `/repos/{repo}/releases/latest` on GitHub. Pass the previous ETag for `If-None-Match`.
|
|
* Returns one of: 'updated' | 'notmodified' | 'ratelimited' | 'skipped-prerelease' | 'error'.
|
|
*/
|
|
export const checkLatestRelease = async (
|
|
{fetcher, prevEtag, repo}: CheckOptions,
|
|
): Promise<CheckResult> => {
|
|
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
const res = await fetcher(url, prevEtag);
|
|
|
|
if (res.status === 304) return {kind: 'notmodified'};
|
|
if (res.status === 403 || res.status === 429) return {kind: 'ratelimited'};
|
|
if (res.status !== 200 || !res.json) return {kind: 'error', status: res.status};
|
|
|
|
const j = res.json;
|
|
if (j.prerelease) return {kind: 'skipped-prerelease', etag: res.etag};
|
|
|
|
if (typeof j.tag_name !== 'string' ||
|
|
typeof j.html_url !== 'string' ||
|
|
typeof j.published_at !== 'string') {
|
|
return {kind: 'error', status: 200};
|
|
}
|
|
|
|
// Reject any tag that would be unsafe to hand to git later. Validating at
|
|
// the persistence boundary (rather than only at the executor) means a
|
|
// malformed tag_name from a misconfigured fork-as-github-repo never lands
|
|
// in update-state.json. Treated as a fetch error so the polling loop will
|
|
// try again next interval.
|
|
if (!isValidTag(j.tag_name)) {
|
|
return {kind: 'error', status: 200};
|
|
}
|
|
|
|
const tag = j.tag_name;
|
|
const version = tag.replace(/^v/, '');
|
|
const body: string = typeof j.body === 'string' ? j.body : '';
|
|
|
|
const release: ReleaseInfo = {
|
|
version,
|
|
tag,
|
|
body,
|
|
publishedAt: j.published_at,
|
|
prerelease: false,
|
|
htmlUrl: j.html_url,
|
|
};
|
|
|
|
return {kind: 'updated', release, etag: res.etag};
|
|
};
|
|
|
|
/** Production fetcher built on Node 18+ native fetch. Honors If-None-Match for cheap polling. */
|
|
export const realFetcher: Fetcher = async (url, etag) => {
|
|
const headers: Record<string, string> = {
|
|
'Accept': 'application/vnd.github+json',
|
|
'User-Agent': 'etherpad-self-update',
|
|
};
|
|
if (etag) headers['If-None-Match'] = etag;
|
|
const r = await fetch(url, {headers});
|
|
const newEtag = r.headers.get('etag');
|
|
let json: any = null;
|
|
if (r.status === 200) {
|
|
try { json = await r.json(); } catch { json = null; }
|
|
}
|
|
return {status: r.status, etag: newEtag, json};
|
|
};
|