From 578b7d7b59d7475160dfd8af4b1c4d440bba0604 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Wed, 29 Jul 2026 23:40:14 +0300 Subject: [PATCH] feat(registry): Beacon is a surface, and the invoice contract is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CF-49 — Beacon was linked from nothing while being the page you go to when something is already broken. One entry in SURFACES was enough: PRODUCTS, SWITCHER_SURFACES and KNOWN_SUBS all derive from it, so no fourth hand-maintained list was added. CF-38 — the invoice/fiat contract is deleted. The endpoints 404 and the product thesis is crypto-native: Shards are acquired by on-chain deposit and no other way. Co-Authored-By: Claude --- .github/workflows/ci.yml | 62 ++++++++++++++++++ MAP.md | 111 +++++++++++++++++++++----------- packages/shared/CONTRACTS.md | 64 ++++++++++++++---- packages/shared/package.json | 2 +- packages/shared/src/pay.ts | 69 +++++++++----------- packages/shared/src/products.ts | 25 +++++++ packages/ui/package.json | 2 +- 7 files changed, 246 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1079b8a..d6b3140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,68 @@ jobs: console.log('ok: published artifacts import and resolve') " + # The invoice/fiat contract was deleted rather than configured (forge-pay MAP.md §8), and + # one half of it — `PAY_PROVIDER=mock` plus `POST /invoices/:id/mock-pay` — was a live + # free-Shards hole. Publishing the types or the endpoint rows again is the first step of + # rebuilding it, and it is an easy step to take by accident: a contract-vs-code diff reads + # a missing row as an omission to be filled back in. Both files carry a written "retired, + # do not restore" note; this job is what makes the note load-bearing. + # + # Matched narrowly on purpose. The patterns are the *contract* forms — an `export` of a + # retired symbol, and a markdown table row for a retired route — so the retirement notes, + # which necessarily name all of them in prose, do not trip it. + # + # No `\b` in the export pattern, deliberately. `git grep -E` compiles through the + # platform's POSIX regex, where `\b` is a GNU extension: it matches nothing at all under + # BSD regex, so the same pattern that catches a regression on a glibc runner silently + # catches nothing on a macOS checkout and the step exits 0 having proved nothing. A + # deny-list is the one place where "too loose" is the safe direction, so the boundary is + # simply dropped — the cost is that `export interface InvoiceThing` also trips, which is + # the answer we want anyway. + # + # Both steps assert against a fixture before they assert against the tree. A guard whose + # pattern never matches is indistinguishable from a clean repo, which is exactly how the + # `\b` above survived review; the self-check turns that failure into a red job. + retired-contract: + name: The retired invoice contract stays retired + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: No retired invoice/package exports in shared + env: + PATTERN: '^export (const|type|interface) (SHARD_PACKAGES|ShardPackage|Invoice|InvoiceStatus|createInvoiceSchema)' + run: | + set -euo pipefail + mkdir -p /tmp/guard + printf 'export interface ShardPackage {\nexport const SHARD_PACKAGES = []\nexport type InvoiceStatus = 1\nexport interface Invoice {\nexport const createInvoiceSchema = 1\n' > /tmp/guard/regressed.ts + found=$(cd /tmp/guard && git grep --no-index -cE "$PATTERN" -- regressed.ts | cut -d: -f2) + if [ "$found" != "5" ]; then + echo "::error::guard self-check failed — the export pattern matched $found of 5 known-bad lines, so this job proves nothing" + exit 1 + fi + if git grep -nE "$PATTERN" -- 'packages/shared/src'; then + echo "::error::the invoice/fiat contract is retired — Shards are funded by on-chain deposit only (see packages/shared/src/pay.ts, forge-pay MAP.md §8)" + exit 1 + fi + echo "ok: no retired invoice exports" + - name: No retired invoice/mock-pay rows in CONTRACTS.md + env: + PATTERN: '^\|.*(/invoices|mock-pay|nowpayments|`/packages`)' + run: | + set -euo pipefail + mkdir -p /tmp/guard + printf '| POST | `/invoices` | Bearer | x | y |\n| POST | `/invoices/:id/mock-pay` | Bearer | x | y |\n| POST | `/webhooks/nowpayments` | HMAC | x | y |\n| GET | `/packages` | — | — | x |\n' > /tmp/guard/regressed.md + found=$(cd /tmp/guard && git grep --no-index -cE "$PATTERN" -- regressed.md | cut -d: -f2) + if [ "$found" != "4" ]; then + echo "::error::guard self-check failed — the CONTRACTS.md pattern matched $found of 4 known-bad rows, so this job proves nothing" + exit 1 + fi + if git grep -nE "$PATTERN" -- 'packages/shared/CONTRACTS.md'; then + echo "::error::CONTRACTS.md documents a retired endpoint — restoring the documented MockProvider toggle restores an unlimited free-Shards hole" + exit 1 + fi + echo "ok: CONTRACTS.md documents no retired payment endpoints" + secret-hygiene: name: Secret hygiene runs-on: ubuntu-latest diff --git a/MAP.md b/MAP.md index d54a817..8f54ef0 100644 --- a/MAP.md +++ b/MAP.md @@ -9,8 +9,8 @@ Every claim below is verified against source. Paths are relative to this repo. | Package | Version | What it is | Consumers | | --- | --- | --- | --- | -| `@cloudsforge/shared` | 0.4.0 (`packages/shared/package.json:3`) | zod schemas, wire types, the surface registry | 9 repos | -| `@cloudsforge/ui` | 0.5.0 (`packages/ui/package.json:3`) | `CloudsForgeBar`, SSO helpers, design tokens | 5 front-ends | +| `@cloudsforge/shared` | 0.5.0 (`packages/shared/package.json:3`) | zod schemas, wire types, the surface registry | 9 repos | +| `@cloudsforge/ui` | 0.6.0 (`packages/ui/package.json:3`) | `CloudsForgeBar`, SSO helpers, design tokens | 5 front-ends | --- @@ -60,7 +60,7 @@ Three type layers: | `markId` | `:77` | asset-forge id for the brand mark, or `null` | | `inSwitcher` / `adminOnly` | `:78`, `:80` | switcher visibility | -The fourteen entries are `SURFACES` (`:86-283`). Three derived views: +The fifteen entries are `SURFACES`. Three derived views: `surface(key)` (`:287`, throws on unknown), `PRODUCTS` (`:298`, `kind === 'product'` — the five the marketing site counts), `SWITCHER_SURFACES` (`:302`), and `KNOWN_SUBS` (`:307`), the set used to strip a subdomain when deriving the @@ -72,12 +72,21 @@ inside the game client, and the comment at `:142-146` records that it used to claim `pay`'s subdomain and port, which resolved to the Forge Pay API, an address with no wallet on it. `pay` (`:245-256`) is the API, `inSwitcher: false`. -`admin` (`:174-186`) and `lantern` (`:188-200`) are `kind: 'service'` with -`inSwitcher: true` and `adminOnly: true` — which `:44-48` now documents as the -sanctioned case rather than a contradiction. +`admin`, `lantern` and `beacon` are `kind: 'service'` with `inSwitcher: true` +and `adminOnly: true` — which `:44-48` now documents as the sanctioned case +rather than a contradiction. + +**`beacon` is the newest entry and the reason the gap was worth closing.** +`beacon.` and `localhost:4011` were on the estate's CORS allowlist *and* +its portal return-URL allowlist — both of which exist for "a page a user signs +into, linked from the product switcher" — while the registry had no row for it, +so no front-end linked to the status page at all. It is `adminOnly`, so it +appears only for a viewer the bar already knows holds `admin`. `CLOUDSFORGE_EMBER = '#e8622c'` (`:84`) is the company accent; `site`, `admin`, -`nimbus`, `account` and `keyvault` all use it. +`nimbus`, `account`, `keyvault` and `beacon` all use it. That is also why +`beacon` needs no `[data-cf-product]` block in `tokens.css` (§2.2): the default +accent is already the company ember. ### 1.3 `deposits.ts` — the crypto contract, 294 lines @@ -120,24 +129,30 @@ The money arithmetic is here, not in the service: Schemas: `createDepositAddressSchema` (`:223`) and `convertCoinSchema` (`:229`), whose `idempotencyKey` (`:238`) is documented as REQUIRED for conversions. -### 1.4 `pay.ts` — Shards and monetization, 190 lines +### 1.4 `pay.ts` — Shards and monetization, 185 lines -The live half: `Wallet` (`:50`), `LedgerEntry` (`:42`), `spendSchema` (`:59`), -`Entitlement` (`:175`), and the four static catalogs the Shop renders — -`COSMETICS` (14 items, `:95-116`), `CONVENIENCE_ITEMS` (`:129-134`), -`SEASON_PASS` (`:147`), `PRIVATE_WORLD_OFFERS` (`:167`). `CosmeticKind` (`:72`) +The live half: `Wallet` (`:45`), `LedgerEntry` (`:37`), `spendSchema` (`:54`), +`Entitlement` (`:170`), and the four static catalogs the Shop renders — +`COSMETICS` (14 items, `:90-111`), `CONVENIENCE_ITEMS` (`:124-129`), +`SEASON_PASS` (`:142`), `PRIVATE_WORLD_OFFERS` (`:162`). `CosmeticKind` (`:67`) has six values; only three are equippable anywhere (see the game map). -The hard rule is stated in the file at `:66-70`: everything monetized is +The hard rule is stated in the file at `:59-65`: everything monetized is cosmetic, convenience or private-worlds; nothing touches `resolveDay`. -**Dead weight.** `SHARD_PACKAGES` (`:13-18`), `InvoiceStatus` (`:20`), `Invoice` -(`:22-34`) and `createInvoiceSchema` (`:36-40`) survive the deletion of -forge-pay's invoice path. A grep across all nine repos finds no consumer of any -of them; forge-pay's only remaining mention is a comment recording that -`pricing.ts:166` *used* to derive a worst-case rate from `SHARD_PACKAGES`. They -are 28 lines of contract for endpoints that now 404. (This is a shared-libs -defect the audit's dead-export list did not name.) +**The invoice contract was removed in 0.5.0, and the removal is documented in +place.** `SHARD_PACKAGES`, `ShardPackage`, `InvoiceStatus`, `Invoice` and +`createInvoiceSchema` outlived forge-pay's invoice path by publishing types for +endpoints that 404. They had no consumer in any of the nine repos — the hazard +was a future one: `provider: 'mock' | 'nowpayments'` and CONTRACTS.md's +`PAY_PROVIDER=mock` row together read as a supported dev toggle, and that toggle +was the free-Shards hole forge-pay deleted (`forge-pay/MAP.md` §8 "Retired, and +why", point 2 at `:659-662`). A silent deletion would read as an accidental +omission to the next contract-vs-code diff, so `:4-35` states outright that they +are retired, crypto-native only, and must not be restored — and the +`retired-contract` CI job fails the build if they come back. Removing exports is +breaking under caret-on-`0.x`, which is why the package is `0.5.0`; see §3 for +why the consumer re-lock is the half of that release that is still outstanding. ### 1.5 `game.ts` — 535 lines @@ -321,13 +336,29 @@ workflow runs, and the same command `package.json:11` exposes as `publish:all`. Treat the workflow as documentation of the correct invocation, not as the mechanism. -**shared 0.4.0 and ui 0.5.0 are committed but NOT published.** They carry the -Hearth EVM change (`deposits.ts`, §1.3). ui bumps only because it declares -`@cloudsforge/shared: workspace:^`, which pnpm rewrites to the published range at -pack time — a ui tarball left at 0.4.0 would point at `^0.3.0` and pin consumers -to the 8-decimal contract. Publishing and the eight-consumer re-lock are done +**shared 0.5.0 and ui 0.6.0 are committed but NOT published.** shared 0.5.0 is the +removal of the invoice/fiat contract (§1.4). ui bumps only because it declares +`@cloudsforge/shared: workspace:^` in `dependencies` +(`packages/ui/package.json:46`), which pnpm rewrites to the workspace version's +caret range at pack time — the *published* ui 0.5.0 tarball therefore declares +`^0.4.0`, so leaving ui alone would drag shared 0.4.0, and with it the retired +`SHARD_PACKAGES`/`Invoice`/`createInvoiceSchema` exports, back into every +front-end that installs the bar. Publishing and the consumer re-lock are done centrally, in that order. +**The consumer re-lock for 0.5.0 has not happened yet, and until it does the +removal is not enforced anywhere but here.** All thirteen consumer manifests +still ask for `"@cloudsforge/shared": "^0.4.0"` — asset-forge, crucible (×2), +forge-keyvault, forge-mint (×2), forge-pay, hearth/site, ninety-days-after (×2) +and platform (×3) — and caret-on-`0.x` is patch-only, so none of them will ever +resolve 0.5.0 on their own. A developer in any of those repos today still +installs 0.4.0, whose published `dist` exports the retired symbols, and +`import { createInvoiceSchema } from '@cloudsforge/shared'` still typechecks for +them and still POSTs to an endpoint that 404s. The type-system stop this release +buys exists only once each range moves to `^0.5.0` (and, where the bar is used, +`@cloudsforge/ui` to `^0.6.0`) with the matching `minimumReleaseAgeExclude` +entries. Those edits belong in the consumer repos, not this one. + **A release is two edits per consumer, not one.** Both packages are `0.x`, where a caret range means patch-only — `^0.3.0` allows `0.3.x`, not `0.4.0` (`README.md:52-54`). So a minor bump requires every consumer to: @@ -338,18 +369,23 @@ a caret range means patch-only — `^0.3.0` allows `0.3.x`, not `0.4.0` Step 2 is not optional. Those workspaces set `verifyDepsBeforeRun: error`, so a version the release-age gate refuses breaks *every* command in the repo, not just -install. All eight consumers currently list `@cloudsforge/shared@0.3.0` and, where -they use it, `@cloudsforge/ui@0.4.0`. Two spellings are in use — one line per -version (platform, ninety-days-after, forge-pay, forge-keyvault, asset-forge) and -a `||` range on one line (crucible, forge-mint); both work. +install. All eight consumer repos currently top out at +`@cloudsforge/shared@0.4.0` and, where they use it, `@cloudsforge/ui@0.5.0`. Two +spellings are in use — one line per version (platform, ninety-days-after, +forge-pay, forge-keyvault, asset-forge, hearth/site) and a `||` range on one line +(crucible, forge-mint); both work. **CI does prove the artifact.** `.github/workflows/ci.yml` typechecks, then packs both tarballs and asserts `dist/index.js` and `dist/index.d.ts` exist and that `src/` is *not* shipped (`:34-36`), that the ui tarball carries `tokens.css` (`:38-39`), and then — in a separate job (`:45-74`) — installs both tarballs into a clean `/tmp/consume` project outside the workspace, where pnpm symlinks cannot -mask a broken exports map, and resolves all three entry points. A `secret-hygiene` -job (`:76-94`) refuses a tracked `.env` or a committed `sk-proj-` token. +mask a broken exports map, and resolves all three entry points. A +`retired-contract` job (`:98-136`) refuses a re-published invoice export or +CONTRACTS.md row, and self-checks each pattern against a known-bad fixture first +so a regex that has stopped matching cannot pass as a clean repo. A +`secret-hygiene` job (`:138-156`) refuses a tracked `.env` or a committed +`sk-proj-` token. `pnpm-workspace.yaml:11-12` pins `allowBuilds: { esbuild: false }` and `:16 strictDepBuilds: false`, with the comment at `:4-10` recording that omitting @@ -373,11 +409,14 @@ that then fail the next command. - **No theme switching.** `.cf-dark` exists so the chrome survives inside a light host, but no app is light and nothing toggles it. - **No React version negotiation.** React 19 is a hard peer range (`:49-50`). -- **`CONTRACTS.md` is not verified.** `packages/shared/CONTRACTS.md` is a 344-line - M0 specification. It documents endpoints in prose; nothing tests it against a - running service, and it has drifted (it describes `GET /health` and CORS - conventions for three app origins, and predates deposits, withdrawals and the - deletion of invoices). Read the services, not this file. +- **`CONTRACTS.md` is not verified.** `packages/shared/CONTRACTS.md` is a 380-line + prose specification, written by hand; nothing tests it against a running + service, and parts of it still describe M0 (`GET /health`, CORS for three app + origins). Read the services, not this file — the file now says so itself + (`:6-9`). Its Pay table was rewritten for 0.5.0 and does describe the deposit, + conversion and withdrawal routes, and the one thing CI does enforce is that the + retired invoice/mock-payment rows cannot come back + (`.github/workflows/ci.yml`, job `retired-contract`). ## Non-obvious constraints diff --git a/packages/shared/CONTRACTS.md b/packages/shared/CONTRACTS.md index 68923d6..ad7caff 100644 --- a/packages/shared/CONTRACTS.md +++ b/packages/shared/CONTRACTS.md @@ -1,8 +1,14 @@ # CloudsForge REST Contracts (M0) -Single source of truth for every HTTP endpoint. All request/response bodies use the -TypeScript types & zod schemas exported from `@cloudsforge/shared` (`src/auth.ts`, -`src/game.ts`, `src/pay.ts`). JSON everywhere. Timestamps are ISO-8601 strings. +The HTTP surface every service agrees to. Request/response bodies use the TypeScript +types & zod schemas exported from `@cloudsforge/shared` (`src/auth.ts`, `src/game.ts`, +`src/pay.ts`, `src/deposits.ts`, `src/forgemint.ts`). JSON everywhere. Timestamps are +ISO-8601 strings. + +**This file is written by hand and nothing tests it against a running service.** Where it +and a service's route table disagree, the route table wins and this file is the bug. What +CI does enforce is narrower and deliberate: the retired invoice/mock-payment contract +(see *Retired* under Pay) may not reappear here or in `src/pay.ts`. ## Conventions @@ -122,14 +128,27 @@ inside the same `resolveDay` transaction as player/stock/report updates. ## Pay — payments & wallet (`pay`, `:4003`) +**Shards are funded by on-chain deposit only.** There is no fiat rail, no payment provider +and no invoice — see *Retired* below before adding one back. + +Deposit/withdrawal types & schemas come from `@cloudsforge/shared` `src/deposits.ts`; +`WithdrawalView` and `CoinSwap` are forge-pay's own types (`services/pay/src/store.ts`), +because nothing outside that service constructs them. + | Method | Path | Auth | Body | Response | |---|---|---|---|---| -| GET | `/packages` | — | — | `ShardPackage[]` (= `SHARD_PACKAGES`) | -| GET | `/wallet` | Bearer | — | `Wallet` (creates empty on first read) | -| POST | `/invoices` | Bearer | `createInvoiceSchema` | 201 `Invoice` | -| GET | `/invoices/:id` | Bearer | — | `Invoice` (owner only) | -| POST | `/invoices/:id/mock-pay` | Bearer | — | `Invoice` (dev/MockProvider only → paid + credits Shards) | -| POST | `/webhooks/nowpayments` | HMAC | NOWPayments IPN | 200 `{ ok: true }` | +| GET | `/deposit-coins` | — | — | `DepositChainInfo[]` (= `SUPPORTED_DEPOSIT_COINS`) | +| GET | `/wallet` | Bearer | — | `Wallet` (creates empty on first read; `coins[]` = per-coin deposit balances) | +| POST | `/deposits` | Bearer | `createDepositAddressSchema` | 201 `DepositAddress` (find-or-create; 502 `keyvault_error`, 503 `address_retired`) | +| GET | `/deposits` | Bearer | — | `{ addresses: DepositAddress[], payments: DepositPayment[] }` | +| GET | `/deposits/:id` | Bearer | — | `DepositPayment` (owner only; 403/404) | +| GET | `/coins/rates` | — | — | `CoinRate[]` (`usable: false` ⇒ `/coins/convert` will refuse that coin) | +| POST | `/coins/convert` | Bearer | `convertCoinSchema` | `CoinConversion` (402 `insufficient_coin_balance`, 422 `amount_too_small`/`shard_overflow`, 503 `rate_unavailable`) | +| POST | `/coins/convert-to-ember` | Bearer | `convertCoinSchema` minus an EMBER source | `CoinSwap` (same codes, plus 422 `same_coin`) | +| GET | `/withdrawal-coins` | Bearer | — | `{ coins: [{ coin, network, supported, reason, fee, minimum }] }` | +| POST | `/withdrawals` | Bearer | `{ coin, destination, amount, network?, idempotencyKey? }` | 201 `{ withdrawal: WithdrawalView, wallet, replayed }` (402 `insufficient_coin_balance`, 422 `invalid_destination`/`amount_too_small`, 501 `withdrawal_unsupported`, 503 `withdrawals_disabled`/`fee_unavailable`) | +| GET | `/withdrawals` | Bearer | — | `{ withdrawals: WithdrawalView[] }` (newest first) | +| GET | `/withdrawals/:id` | Bearer | — | `WithdrawalView` (owner only; 403/404) | | POST | `/spend` | Bearer | `spendSchema` | `Wallet` (409 if insufficient) | | GET | `/cosmetics` | — | — | `Cosmetic[]` (= `COSMETICS`) | | GET | `/convenience` | — | — | `ConvenienceItem[]` (= `CONVENIENCE_ITEMS`) | @@ -143,11 +162,28 @@ inside the same `resolveDay` transaction as player/stock/report updates. - **Never pay-to-win:** the store sells cosmetics / convenience / private-worlds ONLY — no AP, resources, XP, or combat power is ever purchasable; nothing here touches `resolveDay`. -- **Provider abstraction:** `interface PayProvider { createInvoice(...); verifyWebhook(req); }`. - `MockProvider` (dev, `PAY_PROVIDER=mock`) returns a fake `payUrl` and enables `/mock-pay`. - `NowPaymentsProvider` (prod) calls NOWPayments API + verifies IPN HMAC (`NOWPAYMENTS_IPN_SECRET`, - sorted-JSON `sha512` HMAC in `x-nowpayments-sig`). -- Crediting is idempotent per invoice: only the first transition to `paid` writes a `LedgerEntry`. +- **Funding is a watcher, not a checkout.** `POST /deposits` mints a per-user, per-coin address + whose key is custodied by ForgeKeyvault; the pay watcher credits a payment only once it reaches + that coin's `confirmations` depth (`SUPPORTED_DEPOSIT_COINS` — EMBER 60, ETH 12, BTC/SOL/XRP 1). + Coin balances are spendable as Shards only after `POST /coins/convert`. +- **Every money-moving POST requires an idempotency key** (`Idempotency-Key` header or an + `idempotencyKey` body field, 8–200 chars): `/coins/convert`, `/coins/convert-to-ember` and + `/withdrawals` reject without one, and `/spend` debits twice on a blind retry without one. The + claim is written in the same transaction as the money, so a retry replays the stored answer. +- **`network` is the deployment's, never the caller's.** A body naming the other network is + refused with 400; the request is settled on `PAY_DEPOSIT_NETWORK` regardless. +- **Amounts are decimal strings, never numbers.** A whole 18-decimal coin (EMBER, ETH) is above + `Number.MAX_SAFE_INTEGER`, and precision starts being lost at ~0.01 of a coin. Conversion + arithmetic is BigInt end to end (`shardsForCoinAmount`, `RATE_SCALE`). +- **Retired — do not restore.** `GET /packages`, `POST /invoices`, `GET /invoices/:id`, + `POST /invoices/:id/mock-pay` and `POST /webhooks/nowpayments`, with `SHARD_PACKAGES`, + `Invoice` and `createInvoiceSchema`, were **deleted rather than configured** when the + ecosystem went crypto-native (forge-pay `MAP.md` §8; the `invoices` table is dropped on every + boot). They are absent on purpose, not by omission. The `MockProvider` half in particular must + never come back as written: `PAY_PROVIDER` defaulted to `'mock'` and `/mock-pay` was gated on + exactly that value, so any authenticated user on the public host could mint unlimited free + Shards. Shards are anchored at a flat 100/USD (forge-pay `services/pay/src/pricing.ts`), not by + a retail catalog. - **Buy/grant** (`purchaseEntitlement`) reuses the wallet/ledger shard debit in ONE transaction: it inserts the `Entitlement` first (own-once SKUs deduped by a partial unique index on `(user_id, sku)`, `private_world` excluded so rentals repeat), then debits shards via a negative diff --git a/packages/shared/package.json b/packages/shared/package.json index e1a5e98..998170d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudsforge/shared", - "version": "0.4.0", + "version": "0.5.0", "description": "Shared zod schemas, API contracts and domain types for the CloudsForge platform.", "license": "MIT", "repository": { diff --git a/packages/shared/src/pay.ts b/packages/shared/src/pay.ts index 02d0767..0594fda 100644 --- a/packages/shared/src/pay.ts +++ b/packages/shared/src/pay.ts @@ -1,43 +1,38 @@ import { z } from 'zod' import type { CoinBalance } from './deposits.js' -export interface ShardPackage { - id: string - name: string - shards: number - usdPrice: number - badge?: string // e.g. "Best value" -} - -/** Catalog is static config shared by pay service and frontends. */ -export const SHARD_PACKAGES: ShardPackage[] = [ - { id: 'pouch', name: 'Shard Pouch', shards: 500, usdPrice: 4.99 }, - { id: 'cache', name: 'Shard Cache', shards: 1200, usdPrice: 9.99, badge: 'Popular' }, - { id: 'vault', name: 'Shard Vault', shards: 2800, usdPrice: 19.99, badge: 'Best value' }, - { id: 'hoard', name: 'Shard Hoard', shards: 7500, usdPrice: 49.99 }, -] - -export type InvoiceStatus = 'pending' | 'confirming' | 'paid' | 'expired' | 'failed' - -export interface Invoice { - id: string - userId: string - packageId: string - shards: number - usdPrice: number - status: InvoiceStatus - provider: 'mock' | 'nowpayments' - /** URL the user opens to pay (hosted checkout). */ - payUrl: string | null - payCurrency: string | null - createdAt: string -} - -export const createInvoiceSchema = z.object({ - packageId: z.string(), - /** Preferred crypto currency ticker, e.g. 'btc' | 'eth' | 'usdttrc20'. Provider may ignore. */ - payCurrency: z.string().optional(), -}) +// --------------------------------------------------------------------------- +// RETIRED — the invoice / fiat-checkout contract. Do not restore. +// +// `SHARD_PACKAGES`, `ShardPackage`, `Invoice`, `InvoiceStatus` and +// `createInvoiceSchema` used to live here and were removed in 0.5.0. They +// described `GET /packages`, `POST /invoices`, `GET /invoices/:id` and +// `POST /invoices/:id/mock-pay`, none of which exist: forge-pay deleted the +// whole provider/invoice stack rather than configuring it, and drops the +// `invoices` table on every boot (forge-pay `services/pay/src/db/migrate.ts:10`, +// forge-pay `MAP.md` §8 "Retired, and why", `:650-669`). +// +// Two reasons this is a deletion and not a gap waiting to be filled: +// +// 1. The ecosystem is crypto-native. There is no fiat rail. Shards are funded +// by on-chain deposit and coin conversion only — see `deposits.ts` and the +// Pay table in CONTRACTS.md. +// 2. `provider: 'mock' | 'nowpayments'` named a payment provider that WAS a +// live free-Shards hole. `PAY_PROVIDER` defaulted to `'mock'` and +// `POST /invoices/:id/mock-pay` was gated on exactly that value, so any +// authenticated user on the public host could open an invoice and settle it +// for free, unlimited (forge-pay `MAP.md` §8 point 2, `:659-662`). +// Re-publishing the type is the first step of rebuilding that hole. +// +// The USD anchor went with it: `pricing.ts` used to derive a floor price from +// the worst tier of `SHARD_PACKAGES` so a conversion could not undercut a +// retail purchase; with no retail purchase left it is the flat +// `shardsPerUsdScaled = 100n * RATE_SCALE` (forge-pay +// `services/pay/src/pricing.ts:163-182`) — a Shard is one US cent. That is the +// same anchor Crucible prices its paper trading with, `SHARDS_PER_USD = 100` in +// `crucible/packages/contracts/src/index.ts`; if you arrived here from a comment +// naming `SHARD_PACKAGES` or `shardsPerUsd()`, those are the two live names. +// --------------------------------------------------------------------------- export interface LedgerEntry { id: string diff --git a/packages/shared/src/products.ts b/packages/shared/src/products.ts index 8b35ab2..7adccb3 100644 --- a/packages/shared/src/products.ts +++ b/packages/shared/src/products.ts @@ -23,6 +23,7 @@ export type ProductKey = | 'crucible' | 'admin' | 'lantern' + | 'beacon' /** Every addressable CloudsForge surface, including ones with no UI. */ export type SurfaceKey = @@ -198,6 +199,30 @@ export const SURFACES: readonly CloudsForgeSurface[] = [ inSwitcher: true, adminOnly: true, }, + { + // Lantern's opposite number, and it was the one hostname in the estate on + // both the CORS allowlist and the portal return-URL allowlist with no row + // here — the allowlists exist for "a page a user signs into, linked from + // the switcher", and nothing linked to it. An operator looking for the + // status page had to type the hostname. + // + // The accent is the company ember rather than a colour of its own, as Admin + // uses: Beacon's own page (infra/beacon/public/style.css) is built on + // --cf-ember and reserves green/amber/red for probe verdicts, so giving the + // surface a second hue would be the one thing a status page must not do. + key: 'beacon', + name: 'Beacon', + verb: null, + blurb: 'Status & uptime', + kind: 'service', + subdomain: 'beacon', + devPort: 4011, + accent: CLOUDSFORGE_EMBER, + glyph: '◉', + markId: null, + inSwitcher: true, + adminOnly: true, + }, /* --- no UI of their own, but they have hostnames ------------------ */ { diff --git a/packages/ui/package.json b/packages/ui/package.json index 1e2a4f8..fb7fd7c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@cloudsforge/ui", - "version": "0.5.0", + "version": "0.6.0", "description": "Shared CloudsForge account chrome, identity bar and design tokens.", "license": "MIT", "repository": {