Files
warp-engine-client/src/infrastructure/engine/dialects/CatalogDialect.ts
T
mr.zeroandClaude Opus 5 26c7aa9be1
ci/woodpecker/push/woodpecker Pipeline was successful
A catalog that can say a title is not yours
A store with paid titles had nothing to tell this client and no way for it to
listen: the catalog carried no price, no entitlement and no sign-in, so a gated
download could only come back 403 and leave the window guessing why.

The knowledge belongs on the server, not here. This client serves whichever
catalog a registry names, so anything it knew about a particular shop would be
a rule that breaks every other one. WarpEngine 0.5 answers GET /api/service with
what it offers and puts an `access` block on every entry; this reads both. There
is no store name anywhere in the diff.

- **0.5 is a dialect of its own**, the older shape with `access` added. The
  version list is exhaustive over the selector, so adding it was a compile error
  until somebody said what it reads like — which is what that switch is for.
- **A card shows a price and a Buy button** when a title is not yours, opening
  the store's own page. Buying stays in a browser: a checkout rebuilt here would
  be a second place to get card handling wrong.
- **Signing in is the device grant**: a short code, the person's own browser, and
  no password crossing this window. The token goes in the OS keychain through
  safeStorage — one per store — and where no keychain exists it is not stored at
  all rather than written out in the clear.
- **Owned / To buy** join the categories, since owning something is not the same
  as having installed it.

Three things worth stating about the shape:

The bearer token stops at the origin that issued it. A gated download redirects
to signed storage — often somebody else's host — and some object stores refuse a
request outright when an Authorization header arrives alongside the signature.

An absent access block is not "free". It is an engine too old to have an
opinion, and only one of those two is a reason to offer somebody a sign-in, so
the three states are kept apart all the way to the card.

state.json does not carry entitlement. Whether somebody may download a title is
the server's answer to a question asked now; a copy on disk would go stale on the
next purchase or refund, and a stale yes is the dangerous direction.

A store with no sign-in shows none, and every WarpEngine before 0.5 is such a
store: no Account block, no prices, no new categories. The smoke test against the
live catalog reports exactly that — `sign-in: not offered`, `access: open:13`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:03:54 +02:00

67 lines
2.4 KiB
TypeScript

import type { CatalogAccess } from '../../../domain/models/CatalogAccess'
import type { SupportedWarpEngineVersion } from '../../../domain/models/WarpEngineVersion'
/**
* How one WarpEngine version's catalog is shaped.
*
* The catalog is the one thing this client reads that it does not own the shape of, so
* it is the one thing an engine version can change under it. A dialect turns that
* foreign JSON into the typed records below, and everything downstream — the survey,
* the release choice — sees only those. When a future engine renames a field or nests a
* release differently, a new dialect is the whole change.
*/
export interface CatalogDialect {
readonly version: SupportedWarpEngineVersion
/** One entry per title in the catalog. */
listEntries: (catalog: unknown) => readonly CatalogEntry[]
}
export interface CatalogEntry {
readonly software: CatalogSoftware
/**
* What the catalog says about getting this title, or null where it says nothing.
*
* Null is not "free": it is an engine too old to have an opinion, and a store that
* never gated anything reads the same as one that could not say. Both mean the same
* thing in practice — try the download — but only one of them is worth offering a
* sign-in for.
*/
readonly access: CatalogAccess | null
/**
* The release the catalog itself calls newest-and-stable, or null when it names none.
*
* Kept separate from the candidates because it is also what an unavailable title's
* card shows a version from — and a catalog with releases but no `latestRelease` has
* nothing to show there.
*/
readonly latestRelease: CatalogRelease | null
/**
* Every release worth trying, newest first and deduplicated.
*
* `latestRelease` comes first where there is one, then the rest, so that a game whose
* newest build is missing an asset still installs from an older one.
*/
readonly releaseCandidates: readonly CatalogRelease[]
}
export interface CatalogSoftware {
readonly name: string
readonly title: string
readonly platform: string
readonly status: string
readonly description: string
readonly author: string
readonly imageUrl: string | null
}
export interface CatalogRelease {
readonly version: string
readonly createdAt: string | null
readonly assets: readonly CatalogAsset[]
}
export interface CatalogAsset {
readonly kind: string
readonly path: string
}