Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 75 additions & 36 deletions MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

---

Expand Down Expand Up @@ -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
Expand All @@ -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.<apex>` 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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading
Loading