Skip to content

upup v2 (3.0.0): headless core, six framework packages, hardened server — CI validation draft#320

Draft
AminDhouib wants to merge 1123 commits into
masterfrom
v2-clean
Draft

upup v2 (3.0.0): headless core, six framework packages, hardened server — CI validation draft#320
AminDhouib wants to merge 1123 commits into
masterfrom
v2-clean

Conversation

@AminDhouib

Copy link
Copy Markdown
Member

What this is

The v2 integration branch: the full rewrite of upup into a headless core + six native framework packages, followed by a staged foundational audit (22 fix plans), a quality-gates hardening pass (55 commits), and a final convergence fix batch (9 commits). Everything below has been verified locally; this PR exists first and foremost to give the CI workflows their first real execution — they were authored and revised during the audit but have never run on GitHub's runners.

Draft — do not merge yet. Merging to master and publishing 3.0.0 is a separate maintainer decision. publish.yml only triggers on push to master, so this PR cannot publish anything.

Contents

  • v2 rewrite@upup/core headless engine (file state, upload pipeline, web-worker offload, cloud-drive plugins, i18n, theme) + native UI packages for React, Vue, Svelte, Angular, Vanilla, Preact, plus @upup/next and @upup/server (HMAC-signed server-mode uploads to S3/MinIO).
  • Foundational audit Stage 3 — 22 executed fix plans: server trust model hardening (403-by-default, signed length, key/uploadId binding), handler decomposition, drive-plugin unification (PopupOAuthPlugin), curated public API (@upup/core/internal), i18n engine collapse, release pipeline, docs truth pass.
  • Quality gates — ESLint 10 flat-config across all packages, strict TS flags (exactOptionalPropertyTypes, noUncheckedIndexedAccess), oxlint, knip, husky pre-commit/pre-push, env drift guard, coverage floors, and the CI jobs this PR is exercising.
  • Convergence fix batch (2026-07-07) — GHA actions v4, dependency pins, LF enforcement for husky hooks, MinIO fallback ports, health-through-Responder + response-contract structural pin, audit lessons codified into CLAUDE.md.

Local verification (HEAD e32dd601)

  • All 10 static gates exit 0: typecheck (23/23), test (26/26), build (15/15), lint (12/12), lint:ox, knip, prettier-check, size, audit:prod, env:check.
  • Full e2e against real MinIO: deep React suite 52/52, cross-framework parity 67 passed / 5 skipped (the 5 are interactive-OAuth specs, skipped by design in OAuth-free runs).

What CI should show on this PR

  • main.yml: prettier → unit suites + coverage floors → typecheck → build → size-limit → prod dependency audit → oxlint → knip → env drift → Status-Check aggregate gate.
  • e2e.yml: full pnpm run e2e (MinIO provisioned from the example env) + Smoke-Packages (real npm-tarball consumer).

If a job fails here but passed locally, that's exactly the signal this draft exists to surface (runner resources, env provisioning, action versions).

🤖 Generated with Claude Code

AminDhouib and others added 30 commits July 3, 2026 22:22
…ete (F-109)

onUploadComplete had zero call-sites and onFileUploaded only fired on the
drive-transfer path. The /multipart/complete route IS server-side
completion, so fire both hooks there, building the UploadedFile from data
already trusted at complete-time (result.key, uploadedSize, result.downloadUrl)
-- never from client-declared name/type, and without extending the signed
token or touching the size-envelope check.

README + a config.ts doc-comment spell out which hook fires on which path,
including the honest gap: client-direct presigned-PUT uploads bypass the
server entirely, so no server-side hook can fire for them.
…/F-155/F-406/F-408/F-409/F-412/F-413/F-414/F-601/F-603)
…, v2-clean branch (F-441, F-444, F-445, F-448)
…ror event (F-146)

P6 C1. The core event surface now has exactly ONE upload-failure event: upload-error.
resume()'s catch emits upload-error (matching upload()/retry()); CoreEvents['error']
is deleted; the lone HEIC step diagnostic is renamed 'error' -> 'pipeline-error'.

Per the S3-D10 ruling (Option A), the two live cross-framework bridges of the bare
'error' event and the one remaining producer are migrated in the same commit so the
contract is true end-to-end (a literal grep missed them — they used data-driven
forwarding tables):
- angular FORWARDED ['error','error'] -> ['upload-error','error']; @output() error
  payload widens to { error: Error; file?: UploadFile } (additive file? from the
  upload-error channel).
- vanilla element.ts FORWARDED ['error','upup:error'] -> ['upload-error','upup:error']
  (public upup:error DOM event name unchanged).
- vanilla url-uploader producer emit('error',{message}) -> emit('upload-error',{error})
  (also fixes a payload-shape defect against the typed {error: Error} channel).

Behavior change: angular's error output and vanilla's upup:error DOM event now fire
for EVERY upload failure (per-file), not just the practically-dead resume path — the
correct semantic for their names. Public names @output() error / upup:error stay
byte-identical: no DOM change, no parity regen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P6 C2. Each handler in emit() is wrapped in try/catch: a consumer's listener throwing
no longer aborts later handlers or escapes emit(). A caught exception is reported
dev-only via console.error and NEVER re-emitted — a second error event would be a
second failure channel AND would misattribute a render bug as a pipeline/upload error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (F-144)

P6 C3. Add FileManager.updateFile(id, patch): the sole writer of a file's
status/key/metadata. Each transition produces a NEW UploadFile reference (F-144), so
the orchestrator's ref-diff projection tracks status changes it previously missed
(F-145), and FileManager becomes the real owner of file state (F-143 write-side).

The 6 in-place Object.assign+set status sites (onFileStart/onFileComplete/onFileError/
markFilesReady/updatePendingFileStatuses/markUnsuccessfulFilesFailed) now route through
updateFile. Sites 1-3 use 'if (!updated) return' (subsumes the old !files.has guard);
sites 5-6 snapshot the map before iterating (updateFile replaces entries mid-loop).

updateFile re-wraps the same backing bytes into a fresh File (a view, not a copy) and
copies the UploadFile fields incl. non-enumerable relativePath — mirroring
cloneUploadFile. Object-spread would strip File's blob slots and break xhr.send; a
constraint comment guards against that regression, and a contract pin asserts the
transitioned object stays instanceof File with the same size.

core-upload-targets 'preserves the native File ... through transitions' updated: it
pinned reference identity (toBe(file)); immutability yields a new reference, so it now
asserts data-identity (same File, same name, same bytes) — the property that matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P6 C4. uploadStatus is now a projection of core's state-change{status}, not written by
three divergent listeners. rebuildFilesProjection becomes projectFromCore(payload): it
derives uploadStatus from payload.status (core sets _status before every status-carrying
emit) and rebuilds files from core.files (no-op when unchanged, preserving ref stability).

The upload-start / upload-all-complete / upload-error listeners drop their
setState({uploadStatus}) writes and keep only their callbacks; upload-error keeps
uploadError + P4's uploadErrorCode (only the FAILED write is removed — status now arrives
via the state-change{FAILED} core emits right after). removeFile reads the projected
state.files (the last direct core.files read in the class).

Result: one writer of uploadStatus, and pause/resume/cancel — which emit state-change{status}
but were not among the three listened events — now propagate for free (fixes the stale
'SUCCESSFUL over an empty list' drift after cancel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P6 C5. A private activeRun: Promise<UploadFile[]> | null gates the run entry points so
a single UploadManager owns any run. upload() and retry() become thin gates that return
the in-flight promise if a run is active, otherwise set activeRun around the extracted
runUpload()/runRetry() body and clear it in finally. resume() no-ops if activeRun is set,
else assigns its uploadFiles(incomplete) chain to activeRun and clears it in finally (the
F-146 upload-error catch stays inside the chain). upload() also throws after destroy()
(F-148 down-payment; the rest of terminal-destroy lands in C6).

Behavior change (plan §4): concurrent upload/retry now share one run; a mid-run resume is
a no-op — was: orphaned managers from double-clicks / auto-upload races. plugin-integration
'plugin observes retry events' now awaits between calls (two synchronous retries collapse
onto one run under F-149); its intent (a plugin can observe retry) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P6 C6. destroy() is now terminal. runUpload() no longer resets destroyed=false, and
upload/resume/retry/addFiles/setFiles throw a plain Error after destroy() (upload was
guarded in C5). destroy() releases the crashRecovery and pipelineEngine manager refs; it
does NOT clear crash-recovery STORAGE (a normal unmount must stay recoverable) and does
NOT null fileManager (the files/progress getters are asserted to work post-destroy).

Companion: the orchestrator gains a disposed flag (set in destroy(), above P7's blob-revoke
line) and guards the auto-upload setTimeout — an unmount that races the 0-ms timer no longer
reaches the terminal core.upload(); a stray call is also .catch-swallowed.

Verified across all six frameworks (fresh core per mount; no teardown path throws; no
unhandled rejection in the react suite). CLAUDE.md records the three P6 contract rulings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P6 C7. updateOptions() now nulls the cached pipelineEngine when any auto-pipeline flag
(heicConversion/stripExifData/imageCompression/thumbnailGenerator/checksumVerification)
is in the partial, so the next upload() rebuilds the pipeline from the new flags. An
explicit pipeline stays construction-only and is never invalidated (guarded by
!this.options.pipeline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…minor)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AminDhouib and others added 30 commits July 10, 2026 15:08
…shallow

Test #59 ran the affected-tests resolver against the host repo's
HEAD~1...HEAD, which does not exist under actions/checkout's fetch-depth-1
clone: 'fatal: ambiguous argument HEAD~1' -- red in CI since the test's
own commit, green locally where clones are full. The test now builds its
own two-commit throwaway repo and runs the CLI there, so the real
git-diff subprocess path stays exercised end to end while the assertions
get stronger (exact changedFiles + full suites verdict instead of key
presence). RED proven in a shallow clone and a no-parent cwd repo; GREEN
proven in both plus the full 80-test suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drive-sandbox harness landed refresh-onedrive-token.mjs, and the
retired-vocabulary census (first CI execution since the harness -- an
earlier step failed first on every prior run) flagged its three
references. Renamed to refresh-one-drive-token.mjs (kebab wire form per
the F-654/F-725 two-form ruling) and swept the references: the nightly
rotation step, the providers.mjs comment, the file's own header, and the
CLAUDE.md pointer. Slug-to-env mapping is a hardcoded lookup, so the
UPUP_TEST_ONEDRIVE_* secret names are untouched. vocab:check red (3
hits) -> green (1197 files clean), no new exceptions pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ser flake

The keyboard-only spec failed twice in CI on the browse-files Enter
press: the trace snapshot showed the button still [active] (focus was
never stolen -- no product a11y defect; the browse path is a synchronous
native-button handler), yet the raw page.keyboard.press Enter was
dropped before native activation under runner load, so no filechooser
event ever fired. All four tab-then-press sites now assert toBeFocused
and press via the locator (re-commits focus + actionability right before
the real key), keeping Tab-reachability and keyboard activation both
proven. Reproduced locally 2/30 failed at 16 workers before; 40/40,
60/60 single-worker, and the full 63-test deep suite green after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeQL js/incomplete-sanitization (high): the markdown escape replaced
pipes but never backslashes, so a reason containing a literal backslash
left the following pipe behind an even-length backslash run -- still a
live table delimiter, able to inject a column into GITHUB_STEP_SUMMARY.
Backslash is now escaped first, then pipe. Routing semantics untouched;
a regression test drives the real CLI with a pipe-bearing filename and
counts the structural pipes in the rendered row (81/81 script tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llution

CodeQL js/prototype-pollution-utility (medium): the dotted-path setter
checked for __proto__/constructor/prototype in one upfront pass, which
CodeQL cannot recognize as a barrier for the write sink -- and an inline
check at each write is also the stronger shape. The guard now sits at
both write sites (walk keys and the leaf assignment); behavior for
legitimate keys is identical. setByPath is exported for testability and
a new spec pins that __proto__.x and constructor.prototype.x attempts
leave Object.prototype and the target untouched (18 files / 80 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…leg + GitHub token sync)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…filenames now transfer

fetchDropboxFile built the Dropbox-API-Arg HTTP header with plain
JSON.stringify. HTTP headers are latin-1, and Dropbox file ids ARE path
strings, so a unicode filename either makes undici reject the header outright
(code points > 0xFF) or is sent mis-encoded as latin-1 bytes that Dropbox
cannot match to its UTF-8 path (a 409 path/not_found). Either way the
server-mode download of a unicode-named file breaks.

Fix: httpHeaderSafeJson() escapes every char >= 0x7F as a \uXXXX sequence
(Dropbox's prescribed "HTTP header safe JSON"), and fetchDropboxFile uses it
for the Dropbox-API-Arg header. The list/search calls are unaffected — their
JSON rides the request BODY, where UTF-8 is fine.

Proven test-first against the real sandbox accounts. A committed fixture
(upup-sandbox-ünï 'q' & (1).txt — ü/ï are non-latin1-safe in headers, the
apostrophe breaks GDrive/OData quoting, and &/space/parens exercise
URL-encoding) is seeded to all four providers and rides the existing
list/download/byte-integrity loops plus a new unicode-search live test. The
seeder gained matching escaping so it can upload the fixture in the first place
(headerSafeJson for its own Dropbox-API-Arg headers, escapeGDriveQueryValue for
the Drive find queries).

RED before the fix (drive:sandbox vitest live — ONLY dropbox's download fails;
box/google-drive/one-drive downloads stay green because their fetch does not
carry JSON in a header):

  FAIL  tests/integration/drive-clients-live.integration.test.ts >
    drive-clients live — dropbox > a client can download each fixture and the
    bytes are byte-exact (sha256 matches the committed fixture) from dropbox
  UpupNetworkError: Drive API 409: {"error":{".tag":"path","path":{".tag":"not_found"}},"error_summary":"path/not_found/"}
   ❯ driveFetch src/drive-clients.ts:74:15
   ❯ Object.fetchDropboxFile [as fetchFile] src/drive-clients.ts:305:19
   ❯ tests/integration/drive-clients-live.integration.test.ts:261:34

   Test Files  1 failed (1)
        Tests  1 failed | 19 passed (20)
  (exit code 1)

GREEN after the fix: vitest live 24 passed (4 providers × 6 tests), the
Playwright HTTP-surface layer 12 passed (drive→S3 transfer of every fixture,
all four providers), and a self-contained unit pin (httpHeaderSafeJson plus a
fetchDropboxFile header-capture mock) 2 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aywright caps requests at 30s by default)

The >5 MiB multipart test raised the TEST budget with test.setTimeout(240_000),
but Playwright's APIRequestContext keeps its default 30s PER-REQUEST timeout. The
transfer POST blocks on a server-side 6 MiB provider download before the MinIO
upload, so a slow/cold provider in nightly CI could exceed the 30s per-request
cap despite the 240s test budget — a latent flake.

Pass an explicit 180s request timeout to the large-transfer POST and to the
presigned-URL GET of the 6 MiB object.

Verified (rotation-free): prettier + e2e-test typecheck clean, and one bare
Playwright drive-sandbox run — 9 passed / 3 skipped (box/dropbox/google-drive
live, one-drive skipped by design) — exercises the changed request options on
three providers live.

Task-3 quality-review Important item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h real routes + MinIO

The drive-sandbox Playwright suite proved live drive->S3 transfers but never
exercised the server's POLICY gates. This adds a SECOND in-process @upup/server
-- same node-bridge plumbing, same seeded token store -- configured with
maxFileSize 1 MiB + allowedTypes ['text/plain'], and proves all three rejection
paths live through the real routes against real MinIO:

  - an over-declared size is rejected 413 before any drive call (the cheap
    client-declared-size gate in drive-routes.ts),
  - a disallowed mimeType is rejected 415 before any drive call, and
  - the crown jewel: when the declared size is OMITTED so the cheap 413 gate
    cannot fire, the AUTHORITATIVE streamed-byte cap inside
    transferDriveFileToS3 aborts the real 6 MiB Box transfer mid-stream --
    error status, no object persisted, no lingering incomplete multipart upload
    (F-743, exercised with real provider bytes; Task 3's countObjectsWithSuffix
    forward-infrastructure finally gets called).

The 6 MiB fixture routes to the multipart path (its real size exceeds the 5 MiB
single-put threshold); the first 5 MiB part uploads, then totalBytes (5242880)
> maxBytes (1048576) throws, the catch aborts the MPU, and the route surfaces
500 STORAGE_ERROR -- so the no-object + no-lingering-MPU asserts are the
meaningful ones. The abort was verified from the server error log, not just the
status colour:

  UpupStorageError: Drive file exceeds the configured maxFileSize
  (5242880 > 1048576 bytes)
    at streamingMultipart (packages/server/src/transfer.ts:144:23)

Plumbing: the inline node-bridge boot in beforeAll is factored into a reusable
bootNodeServer(handler) helper (identical logic, parameterized over the handler)
so both servers share it; listFiles is generalized into
listFilesAt(request, base, ...) with listFiles kept as a thin wrapper -- every
existing test compiles unchanged; afterAll closes both servers. The policy
describe is Box-only (Client-Credentials mint, no rotating token) so it costs
zero rotation budget.

The streamed-cap POST carries an explicit timeout: 180_000 (matching the
existing multipart proof) -- Playwright's default 30s per-request cap is NOT
raised by test.setTimeout, and the server downloads ~5 MiB from Box before the
cap fires.

Verified (rotation-free, one bare drive-sandbox run):
  12 passed / 3 skipped (26.8s) -- box/dropbox/google-drive live, one-drive
  skipped by access-token-only design; the +3 are the new policy tests
  (413 in 9ms, 415 in 4ms, streamed-cap abort in 1.9s).
Gates: full typecheck 32/32 tasks green (incl. @upup/e2e-test), test:quality
345 files + 4 workflows clean, prettier --write applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ransfer fails clean 500)

A native Google Doc has no downloadable binary content: Drive's alt=media
returns 403 fileNotDownloadable ("Only files with binary content can be
downloaded. Use Export with Docs Editors files."). The transfer route catches
the resulting UpupNetworkError(403) -- 403 != 401, so it is NOT a reauth -- and
surfaces it through res.fail as a clean 500 { error, code:STORAGE_ERROR } with
nothing persisted. This pins the CURRENT no-export limitation so export support
stays a deliberate future maintainer decision, not silent breakage or silent
addition.

- seed.mjs: seedGoogleDrive idempotently creates one native Doc
  (application/vnd.google-apps.document) in the sandbox folder; drive.file
  scope sees app-created files, so the seeder creating it is what makes it
  visible.
- vitest live suite: pins that it lists cleanly (document mimeType, not a
  folder).
- e2e HTTP spec: pins the transfer failing clean (500 JSON error body, object
  count unchanged before/after).

Empirical adaptation vs the plan snippet: Drive v3 DOES report a size (~1 KiB
storage footprint) for a native Doc, so the listing assertion pins
`typeof size === 'number'`, not `size === undefined` -- the listing looks
ordinary; the no-binary limitation only surfaces at transfer time.

Verified (rotation-respecting):
- one full `drive:sandbox` wrapper run: OneDrive rotated + written back +
  GitHub-synced; seed shows the native doc created; vitest surfaced the wrong
  size guess (24 passed / 1 failed).
- fix re-validated gdrive-only (no OneDrive rotation): vitest -t "native Google
  Doc" 1 passed / 24 skipped against the real Google API.
- bare Playwright drive-sandbox: 13 passed / 3 skipped (one-drive skipped by
  design) -- the new gdoc transfer test passes; server log shows the 403->500.
Gates: typecheck 32/32, test:quality 345 files + 4 workflows clean,
prettier --check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mment accuracy

- multipart test: note the >5 MiB path is guaranteed by declared-size routing
  (size > SINGLE_PUT_MAX_BYTES = MIN_PART_SIZE, non-configurable), not by the
  MPU-count assert; name that assert; note countOpenMultipartUploads does not
  paginate (unlike countObjectsWithSuffix)
- assert labels: interpolate ${fixture.name} into the presigned-URL check and
  ${POLICY_PROVIDER} into the policy request paths + "seeded in" message (the
  describe title stays a literal, greppable string — noted inline)
- comment accuracy: streamed-cap test accumulates the first full ~5 MiB part
  (not ~1 MiB) before the cap trips; spec + playwright config headers now cover
  all four providers; fixtures.mjs reworded — the final part is short because
  6 MiB is not a whole multiple of the 5 MiB part size, and +45 exercises the
  generator truncation branch
- seed.mjs: throw an actionable error if one-drive createUploadSession returns
  no uploadUrl (mirrors the google-drive no-Location guard)
- drive-clients.ts: bidirectional twin back-refs from httpHeaderSafeJson /
  escapeDriveQueryValue to their seed.mjs twins (the twins have no unit pin)
- drive-clients.test.ts: tighten the header-legal bound to <= 0x7E (0x7F DEL is
  escaped too, so the test proves the stated claim)
- live integration test: note the unicode-search Array.isArray assert is
  deliberately weak, and cross-ref the Playwright spec's OneDrive
  access-token-only divergence
- rotate-and-test.mjs: correct the stale-token recovery message (re-mint via
  drive:sandbox:mint — the old token is already consumed) and hedge the
  "both passed" summary re skip-green ambiguity

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…echanics, local runbook

- drive-sandbox-setup.md: the Playwright HTTP-surface layer now covers all four
  providers and the full server-mode journey (browse → pick → download → drive→S3
  → sha256, >5 MiB multipart, unicode filenames, 413/415/streamed-cap policy
  boundaries, native-Google-Doc pin); fix the stale spec filename
  (box-dropbox-server-transfer.spec.ts → server-transfer.spec.ts)
- setup.md fixture inventory rewritten: three disk fixtures (incl. the unicode
  name), the generated large fixture (name/size/sha256 pin, regenerated not
  stored), and the native Google Doc (no-export pin; Drive reports a ~1 KiB quota
  size); per-provider large-upload mechanics (Box direct ≤50 MB, Dropbox ≤150 MB,
  GDrive resumable, OneDrive upload session)
- setup.md runbook: `pnpm run drive:sandbox` one-shot (rotate → seed → both live
  layers; MinIO :9100 prereq; `--no-sync-github` opt-out) and an OneDrive
  token-lineage note (nightly rotation kills the local token → re-mint)
- testing.md: drive-sandbox layer row + nightly CI note updated to all four
  providers, multipart, unicode, policy boundary, native-GDoc pin; spec filename
- CLAUDE.md: one surgical sentence extending the nightly Drive-Sandbox bullet —
  the HTTP-surface layer also proves the >5 MiB multipart path and the
  policy-boundary rejections

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… NAME)

Deferred optional rider (Task-6 quality review). The native-Google-Doc test
repeated the literal 'upup-sandbox-native-doc' four times (name filter, POST
fileName, and twice in countObjectsWithSuffix). Extract a test-local
`const NAME` and derive the counted suffix as `-${NAME}`, so the transferred
fileName and the counted object suffix are structurally guaranteed to match.
No behavior change (identical string values); a separate commit because rider
Commit A (9da15e7) was already committed — not amended per the review routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
keywords (incl. drag-and-drop/dropzone family-wide), homepage, bugs, repository-object on all 9 publishable package.json; core gains description. READMEs restructured as agent landing pages: runnable client-mode example first, canonical docs/quickstart links, core error taxonomy fixed to UpupError/UpupErrorCode, mandatory uploadTokenSecret added to both @upup/next handler examples, server repo URL fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ar, blog removal

@easyops-cn/docusaurus-search-local wired; docusaurus-plugin-llms emits llms.txt + llms-full.txt into the build; docs routeBasePath 'docs'->'/' so pages serve at /documentation/<slug> (redirect, footer, search base updated); GitHub/npm navbar links + footer; template blog deleted; config rebranded to the six-framework story; @docusaurus/theme-mermaid registered and pinned 3.7.0 (docusaurus requires exact family version); docs version 0.0.0->2.2.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…omy, retired vocab

createUpupHandler examples gain the mandatory uploadTokenSecret (throws without it); error-handling.md rewritten to the real UpupError/UpupErrorCode surface (UploadError/UpupProvider phantoms removed); fictional signUpload replaced with a real AWS-SDK presign matching PresignedUrlResponse; retired 'restrictions' wording replaced with flat maxFiles/maxFileSize/allowedFileTypes; getting-started reframed to six frameworks with quickstart links; code-examples gains a front-matter description (llms.txt index quality); endpoint naming unified on /api/upload-token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stants page

quickstarts for react/vue/svelte/angular/vanilla/preact/next (client-mode-first, runnable, uploadTokenSecret in every server snippet); fact-checked comparisons vs uppy/filepond/react-dropzone/uploadthing; 'Use upup with AI assistants' page with a paste-ready context block and llms.txt pointers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, llms.txt

hero + metadata repositioned to 'one file uploader, every framework'; six-tab framework switcher (APG tabs pattern, zero-dep highlighter); JSON-LD SoftwareApplication + FAQPage; metadataBase; App Router sitemap covering landing + 15 docs URLs (static sitemap files removed - they shadow the route); public/llms.txt per llmstxt.org.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
root README rewritten: six-framework value prop, 9-package install matrix, runnable React example, server-mode teaser with the mandatory uploadTokenSecret (canonical env name); dev.to CDN banner hotlink replaced by committed assets/upup-banner.png; npm badges deferred until first publish; CI badge points at main.yml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single exact-substring codemod (@Upup -> @useupup) over every tracked text
file; pnpm-lock.yaml regenerated by install. The old scope never reached npm
(v1 shipped as upup-react-file-uploader), so no published consumer breaks.

Derived-name fallout: ng-packagr names its bundle from the package name, so
@useupup/angular now emits dist/fesm2022/useupup-angular.mjs — angular
package.json main/module/exports updated (caught by smoke:packages; the
size-limit entry survived because it globs dist/**/*.mjs).

Riders folded in: publishConfig.access=public on all 9 publishable packages
(scoped packages default restricted; changesets access:public only covers
changeset publish); '@Upup' added to the retired-vocabulary census with zero
exceptions; CLAUDE.md records the scope ruling. 117 touched files carried
pre-existing prettier drift (predates this change at HEAD; verified on HEAD
blobs) — normalized here because lint-staged requires staged files clean.

Gates at this tree: typecheck 32/32, test 27/27, build 15/15, lint 21/21,
lint:ox, knip, vocab:check (14 tokens / 1212 files), test:quality (345),
test:scripts, size, env:check, audit:prod, prettier-check, smoke:packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p publish

changeset publish spawns pnpm publish (verified in @changesets/cli 2.30.0
source), which reads auth from the setup-node userconfig. Trusted publishing
cannot cover a package's FIRST publish (per docs.npmjs.com, trust is
configured on an existing package's settings), so the publish step now
carries NODE_AUTH_TOKEN from the NPM_TOKEN repo secret; registry-url is added
on a publish-gated setup-node step so install/gate steps never evaluate a
token-referencing .npmrc. Once all nine packages exist and trusted publishers
are configured, OIDC takes precedence and the secret can be revoked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
420 s died on a cold 2-core CI runner (PR #320: the slowest of six
storybook boots never finished, zero tests started) and 660 s died on a
loaded dev box (angular webpack hit 100% at the buzzer and never bound the
port). The ceiling only delays reporting genuinely wedged boots; warm
reuseExistingServer starts remain instant. Warm re-run at this tree:
cross-framework 68 passed / 5 skipped in 4.5 m against real MinIO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…8n guides

Wave 4 publish-prerequisite docs for the @useupup launch.

New pages:
- migration/v1-to-v2.md — full v1->v2 upgrade: package rename, complete prop
  map, UpupError/UpupErrorCode taxonomy, client- vs server-mode uploads.
- guides/storage-providers.md — S3-compatible provider matrix + createUpupHandler
  recipes (AWS, R2, MinIO, B2, DO Spaces, Wasabi, generic label).
- guides/server-auth.md — @useupup/server HMAC upload-token trust model,
  secure-by-default 403s, per-user key scoping, signed size envelope.
- guides/theming.md — theme prop, tokens as CSS vars, slot overrides, DOM hooks.
- guides/headless.md — useUpupUpload hook, driving UpupCore directly, pipeline
  opt-ins (webWorker/HEIC/resumable).

Edits:
- localization.md — namespaced-overrides fix, i18n-prop section, locale table,
  next-steps nav, sentence-case headings.
- getting-started.md / error-handling.md — relative-link fixes to the new pages.
- guides/{modes,server-mode-setup,error-monitoring}.md — description front
  matter for the llms.txt generator.
- migration/v2-to-v2.1.md — sidebar_position bump so v1->v2 sorts first.

Two MDX-hazard fixes: inline-code spans that wrapped a line break (headless
progress-shape, v1->v2 resumable {mode} example) were reflowed to keep the span
atomic — a wrapped span was parsed as a JSX expression and failed the build.

Verified: docusaurus build (onBrokenLinks:'throw') green, 36 docs, llms.txt +
llms-full.txt regenerated; prettier --check green; vocab:check green (migration/
is an intentional retired-token exemption); rendered HTML spot-checked on both
MDX-sensitive pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consume the 19 pending changesets (P-series contracts, quality-gates strict
flags, v2.1 legacy-prop removal, DOM-vocab unfreeze). Every publishable
package carries at least one major changeset, so all nine bump uniformly to
3.0.0 — the first @useupup release and a clean signal of the breaking scope
rename from @Upup. Private workspace consumers (storybook apps, examples) took
their dependent patch bumps. changelog:false in the changeset config, so no
CHANGELOG files are generated by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the v2 npm scope to the final @upupjs org. @useupup was published as
@useupup/*@3.0.0 minutes earlier this session; those packages are superseded
and will be deprecated in favor of @upupjs/*@3.0.0.

Method: single exact-substring codemod over `git ls-files` (@useupup -> @upupjs,
1520 occurrences across 544 files), plus the ng-packagr derived FESM bundle name
useupup-angular -> upupjs-angular in packages/angular/package.json (the derived
name is not the scope token, so it is fixed explicitly; smoke:packages is the
gate that catches it). pnpm-lock regenerated via install.

Deliberately NOT changed: the docs/landing domain useupup.com (85 refs) — the
npm scope (@upupjs) and the web domain (useupup.com) are a deliberate split;
revisit only if upupjs.com is adopted. Brand, DOM contract strings, and the
GitHub repo stay bare `upup`.

Gates green at this HEAD: vocab:check (1216 files), prettier-check, typecheck
(32/32), build (15/15), smoke:packages (real @upupjs tarball consumer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All nine @upupjs packages now exist on npm (first published 2026-07-13),
so the token-bootstrap phase is over and OIDC trusted publishing is the
intended path for future releases.

npm OIDC trusted publishing requires Node >=22.14, so the publish-scoped
setup-node now pins 22.14.0 (the gate steps above still run on .nvmrc /
Node 20 — build output is target-pinned and Node-version-independent).
npm bumped 11.5.1 -> 11.16.0; id-token: write was already present.

NODE_AUTH_TOKEN (NPM_TOKEN secret) stays as a graceful fallback until a
trusted publisher is configured per package on npmjs.com (repo
DevinoSolutions/upup, workflow publish.yml). `npm trust` CLI cannot do
this with a token (403 — trusted-publisher config is an account change
that bypass-2FA tokens are forbidden from); it must be set per package in
the npm web UI. Once OIDC is verified in a real release the secret and
the env can be dropped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants