26.x → 27.0.0, entry by entry
Two entries — CHANGELOG.md's 27.0.0 breaking entries in their order, grouped by
package, lowest tier first. No legacy path, no codemod, no shim. Entry 2 can 404 a read your app
relies on: find every un-scoped key a page links before the deploy (upgrade step 2). A later
slice appends its rows below the last one and never renumbers; each row's CHANGELOG.md line names
it as (#N) after the dash.
The upgrade, top to bottom
| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every @ultimat3/* to the one new version, bun install | nothing yet — a mixed install is untested | — |
| 2 | before the deploy: find apps -type f -exec grep -nE '/_storage/|/media/|signedUrl\(' {} + — for each key that is NOT built with scopedKey(orgId, …) (no org/<id>/ prefix) and that readers must still fetch, add its prefix to defineStorage({ shared: { <disk>: ['<prefix>/'] } }); leave everything else unshared | 404 X_STORAGE_KEY_UNSHARED on that link | 2 |
| 3 | bun run typecheck — a hand-written AsyncContext or Storage object | TS2741 naming exit or isShared | 1, 2 |
| 4 | x verify | green, or a finding whose fix: is the edit | — |
Entry by entry
Tier 0 — @ultimat3/core (1). Tier 1 — @ultimat3/storage, with @ultimat3/cli's routes (2).
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | AsyncContext.exit(fn) | you implement AsyncContext yourself rather than calling asyncContext(): TS2741. Add exit: (fn) => fn() for a context with no store, or your store's own exit |
| 2 | GET /_storage/:disk/*key, GET /media/*key, StorageConfig.shared, Storage.isShared | a page, island or client reads a key outside org/<id>/ through either route — a brand asset on a disk, a shared upload. 26.x served it to every holder of storage:read; 27.0.0 answers 404 X_STORAGE_KEY_UNSHARED unless defineStorage({ disks, shared: { uploads: ['brand/'] } }) lists its prefix for that disk (whole segments ending in /, never org/, never the whole disk — X_CONFIG_INVALID at boot otherwise). Or move a truly public file to apps/web/site/assets/ and name it with asset(). A hand-built Storage adds isShared(disk, key) (TS2741). Tenant-scoped reads are unchanged |