2903 lines
107 KiB
TypeScript
2903 lines
107 KiB
TypeScript
#!/usr/bin/env tsx
|
||
/**
|
||
* persona-walkthrough-smoke.
|
||
*
|
||
* Structural sentinel-pins for the Part 119 persona-walkthrough
|
||
* fixes (Bob, Sally, Sally-operator). Each scenario greps the
|
||
* source for the canonical phrase or call-site that proves the
|
||
* fix is still wired. If a later refactor removes the protection,
|
||
* this smoke fails loudly and the maintainer is forced to update
|
||
* either the fix or this file in the same commit.
|
||
*
|
||
* Sentinel-grep is the deliberate choice over runtime testing:
|
||
* - Most fixes touch operator-doc surfaces that have no runtime
|
||
* equivalent (no way to "render" a markdown file's accuracy).
|
||
* - The component-level fixes (Tooltip, FundsSentModal, backup-
|
||
* keys) touch reactive Svelte runes that would require the
|
||
* full svelte-kit harness to exercise; the sentinel-grep is
|
||
* proportionate.
|
||
* - Doc-vs-code drift is exactly the kind of regression that
|
||
* surfaces months later when nobody remembers the fix; this
|
||
* smoke catches it in CI immediately.
|
||
*
|
||
* Coverage:
|
||
* B-2 /backup-keys paired-readonly explanation card
|
||
* S-11 FundsSentModal txid help line
|
||
* S-12 Tooltip i18n-aware default ariaLabel + post page cleanup
|
||
* So-1 vps-bootstrap callout in RUN-A-MORPHIT-NODE + OPERATIONS
|
||
* So-2 ops-cli main.ts JSDoc lists all 14 subcommands
|
||
* So-3 verbose-health env-opt-in callouts (3 docs)
|
||
* So-4 init.ts JSDoc step-count disclaimer
|
||
* So-6 systemd install uses the path-aware installer (beta14 — retires the ~/morphit drop-in)
|
||
*
|
||
* Plus several drift-catchers added during the Part 119 docs
|
||
* audit pass that aren't tied to a persona but matter equally:
|
||
*
|
||
* D-1 morphit-ops (binary name, NO space) in operator docs
|
||
* D-2 no MORPHIT_INDEXER_FEES_ACCOUNT ghost env var anywhere
|
||
* D-3 monorepo install paths consistent in OPERATIONS.md
|
||
* D-4 schema version in PRE-LAUNCH reflects v31
|
||
* D-5 --dry-run flag claim removed from PRE-LAUNCH
|
||
* D-7 POST-LAUNCH backup recipe uses real systemd timer
|
||
* D-8 /v1/health diagnostics field paths match real shape
|
||
* D-9 PRE-LAUNCH wizard step-count realistic ("~17"/disclaimer)
|
||
* D-10 Postgres-version doc claim not over-restrictive
|
||
* D-11 Operator-register CLI command matches real subcommand
|
||
* D-12 Indexer nginx path /v1/ in §12 troubleshooting
|
||
* D-13 /v1/health field name "lag_blocks" not "head_lag_blocks"
|
||
*
|
||
* Part 120 additions:
|
||
*
|
||
* P120-FAQ public_api + qr_login FAQ entries wired in FAQ_KEYS
|
||
* (orphan-entry catch: translated in 10 locales but
|
||
* never rendered because not in the FAQ_KEYS array).
|
||
*
|
||
* Part 121 additions:
|
||
*
|
||
* P121-DOC Four operator/launch-doc sentinels pinning the
|
||
* `npm install` workspace-symlinks + smoke-suite
|
||
* ERR_MODULE_NOT_FOUND troubleshooting in RUN-A-NODE,
|
||
* OPERATIONS, and PRE-LAUNCH-CHECKLIST (P121-DOC 1-3),
|
||
* plus the Part 121 fee_method enum-freeze forward-
|
||
* note on ADR-0011 (P121-DOC-4). The ADR-0011
|
||
* sentinel was added as a Part 121 cp2 catch-up after
|
||
* the discipline-correction memory edit (#24): when
|
||
* shipping a code-level invariant, the ADR that
|
||
* established the original wire format MUST gain a
|
||
* forward-note pointing at the freeze.
|
||
*
|
||
* P121-USDT Five sentinels pinning the USDT integration
|
||
* across canonical registry, frontend registry,
|
||
* per-network metadata module, indexer validation
|
||
* gates, and the orderbook row UI. If any of these
|
||
* surfaces silently lose their Part 121 shape (e.g.
|
||
* USDT.canPayListingFee flipped to true, defaultNetwork
|
||
* changed from null, the indexer's per-network gates
|
||
* stripped, or the orderbook row dropping the network
|
||
* chip), the smoke fails loudly.
|
||
*
|
||
* P121-CP6 Five sentinels pinning the operator-stance
|
||
* surfacing work (item 3 from the cp6 plow-through
|
||
* session). /v1/instance now exposes
|
||
* `disabled_assets`; indexer-client mirrors the
|
||
* optional field for back-compat with pre-cp6
|
||
* indexers; the frontend instance store hydrates
|
||
* it with `[]` fallback; /about-this-instance
|
||
* renders the per-instance stance; /run-a-node
|
||
* carries the prospective-operator explainer with
|
||
* the MORPHIT_INDEXER_DISABLED_ASSETS env var
|
||
* named directly. Memory #25 ("every new tradable
|
||
* asset ships default-ON instance-wide; operators
|
||
* opt out per-asset") is what these sentinels are
|
||
* defending — losing the surface would silently
|
||
* erase the federation-stance feature.
|
||
*
|
||
* P121-CP9 Eight sentinels pinning the matrix-bot operator-
|
||
* alert sidecar + user→operator contact surfaces.
|
||
* SSoT parseMxid + parseRoomAlias with branded
|
||
* types prevent the privacy-violating @↔#
|
||
* confusion at compile time; indexer config refuses
|
||
* @-prefixed input in the room slot with privacy
|
||
* framing; /v1/instance exposes only operator_matrix_room
|
||
* (never an MXID); bot config refuses #-prefixed input
|
||
* in the MXID slot; sendDm() signature is typed against
|
||
* MatrixMxid (branded); main loop wires CRITICAL bypass
|
||
* + WARN gate + INFO accumulator; indexer-client mirror
|
||
* preserves the split; wizard step validates both
|
||
* prefixes with examples shown.
|
||
*
|
||
* P121-CP10 Five sentinels pinning the host-resource monitor
|
||
* sidecar. The POSIX-sh script at
|
||
* ops/scripts/morphit-host-monitor.sh polls
|
||
* /proc/meminfo + df + /proc/loadavg + /proc/vmstat
|
||
* and emits structured JSON to journalctl matching
|
||
* the LogRecord envelope; the systemd unit + timer
|
||
* mirror the hardening posture used for indexer/
|
||
* relay and are opt-in via timer enable; the
|
||
* classifier knows about every host-resource event
|
||
* at every tier (5 CRITICAL, 5 WARN, 4 INFO); the
|
||
* ALERT_COPY ships friendly ELI5 advice for all 14
|
||
* host-resource events; and the bot's default
|
||
* JOURNALCTL_UNITS list includes
|
||
* morphit-host-monitor.service so alerts route
|
||
* without any operator-side wiring.
|
||
*
|
||
* P121-CP11 Seven sentinels pinning the extended monitoring
|
||
* sidecars (smartctl, fail2ban, mdadm) + the
|
||
* Ansible integration that ties everything
|
||
* together. Three new POSIX-sh sidecars at
|
||
* ops/scripts/morphit-{smartctl,fail2ban,mdadm}-
|
||
* monitor.sh each emit structured JSON in the
|
||
* LogRecord envelope; the classifier knows about
|
||
* every cp11 event at every tier (7 CRITICAL,
|
||
* 5 WARN, 3 INFO); ALERT_COPY has ELI5 advice for
|
||
* all of them; the bot's default JOURNALCTL_UNITS
|
||
* list covers all five sidecar units; the Ansible
|
||
* playbook at ops/ansible/playbook.yml integrates
|
||
* the cp9 matrix_bot + cp10 host_monitor +
|
||
* cp11 smartctl/fail2ban/mdadm roles with opt-in
|
||
* enable_* flags (all default false); and the
|
||
* matrix-bot npm-install requirement (better-sqlite3
|
||
* native build needing nodejs.org access) is
|
||
* documented in OPERATIONS.md §16.
|
||
*
|
||
* P121-CP12 Four sentinels pinning the cp12 quality-gates +
|
||
* extended-extended sidecars work. Two new
|
||
* tsx-based smokes (ansible-structural-smoke +
|
||
* ansible-lint-smoke) registered in run-smokes.sh
|
||
* catch playbook drift on every CI run. Three new
|
||
* POSIX-sh sidecars at ops/scripts/morphit-{dmesg,
|
||
* trivy,postfix}-monitor.sh emit structured JSON
|
||
* via systemd-cat: dmesg-monitor scans the kernel
|
||
* ring buffer for OOM/oops/panic/MCE/segfaults
|
||
* using a cursor-based state file so successive
|
||
* runs don't re-alert; trivy-monitor daily-scans
|
||
* running Docker images for CRITICAL+HIGH CVEs;
|
||
* postfix-monitor checks mail queue depth + age
|
||
* so silent alerting failures (smarthost down,
|
||
* credentials rotated) become alerts of their
|
||
* own. Classifier extended with 17 new events
|
||
* (8 CRITICAL, 5 WARN, 5 INFO) + ALERT_COPY
|
||
* entries with ELI5 advice including the exact
|
||
* debug commands an operator should run. Bot's
|
||
* default JOURNALCTL_UNITS now covers ALL EIGHT
|
||
* sidecar units (indexer + relay + 6 monitors)
|
||
* with zero operator-side wiring needed.
|
||
*
|
||
* P121-CP13 Five sentinels pinning the cp13 CI workflow +
|
||
* three more sidecars + matrix-bot deps-pin check.
|
||
* Forgejo Actions workflow at .forgejo/workflows/
|
||
* ci.yml runs three gate jobs on every push and PR
|
||
* (typecheck-sweep, ansible-lint, smokes triple-
|
||
* pulse) — the same discipline manually applied at
|
||
* every tarball seal now runs automatically. Three
|
||
* new POSIX-sh sidecars: certbot-monitor catches
|
||
* the killer "renewal silently broke months ago"
|
||
* pattern by checking cert expiry AGAINST recent
|
||
* successful renewal age (not just expiry alone);
|
||
* apt-monitor surfaces pending security update
|
||
* counts that operators stop reading off the motd
|
||
* after the first month; compose-monitor watches
|
||
* Docker Compose service health + restart loops.
|
||
* matrix-bot-deps-pin-check smoke catches the
|
||
* "tested against 0.7.1, deployed against 0.8.0"
|
||
* class of bug by comparing declared semver ranges
|
||
* in apps/matrix-bot/package.json against what's
|
||
* actually installed in node_modules. Bot's
|
||
* default JOURNALCTL_UNITS now covers ALL ELEVEN
|
||
* sidecar units (indexer + relay + 9 monitors).
|
||
*
|
||
* P121-CP14 Five sentinels pinning the cross-language drift
|
||
* gap close + systemd/journald sidecars + cross-
|
||
* workspace deps-pin + tag-push release workflow.
|
||
* sidecar-envelope-smoke.ts captures the output of
|
||
* every bash sidecar with mocked systemd-cat and
|
||
* validates each emission against a zod schema
|
||
* matching the canonical LogRecord interface — the
|
||
* missing regression test that locks down the
|
||
* bash-emits-JSON / TS-classifier-consumes-JSON
|
||
* contract. Additionally validates that every
|
||
* event name in every sidecar follows the
|
||
* lowercase_snake convention (catches the cp9 bug
|
||
* class at source). workspace-deps-pin-check.ts
|
||
* generalizes the cp13 matrix-bot-only check to
|
||
* ALL workspaces (apps/ + packages/), catching
|
||
* version drift across the monorepo not just one
|
||
* corner. Two new POSIX-sh sidecars: systemd-
|
||
* monitor watches morphit-* unit health + restart
|
||
* counts — fills the gap journalctl-alerting can't
|
||
* cover (a unit that fails to start emits no
|
||
* journal output); journald-monitor watches the
|
||
* journal's own disk usage + rotation health,
|
||
* catching the "journal silently grew to 8 GB
|
||
* over 6 months" pattern. Forgejo release.yml
|
||
* workflow fires on tag push to build a signed
|
||
* release tarball after running the full
|
||
* validation gate again. Bot's default
|
||
* JOURNALCTL_UNITS now covers ALL FOURTEEN units.
|
||
*
|
||
* P121-CP15 Five sentinels pinning the API-response zod
|
||
* smoke + shared emit() lib + host-monitor mount
|
||
* sweep + smartctl SCT thermal-log extension.
|
||
* api-response-shape-smoke.ts extends the
|
||
* envelope-smoke pattern from sidecars to HTTP
|
||
* API: zod schemas for 10 representative response
|
||
* shapes (HealthResponse, InstanceResponse,
|
||
* OrderRecord, etc.) with TS-type-cross-check via
|
||
* `satisfies` clauses on sample literals — drift
|
||
* between the schema and the canonical TS
|
||
* interface from @morphit/indexer-client fails
|
||
* typecheck at CI time, not at production-debug
|
||
* time. ops/scripts/lib/emit.sh extracts iso_now()
|
||
* json_str() emit() helpers into a shared lib that
|
||
* all 12 sidecars source via `. "$(dirname "$0")/
|
||
* lib/emit.sh"` — removed ~180 lines of duplicate
|
||
* boilerplate across the sidecar fleet. host-
|
||
* monitor extended with a bind-mount + tmpfs sweep
|
||
* via `df --output=target,pcent,fstype` that skips
|
||
* pseudo-filesystems (proc/sysfs/cgroup/squashfs)
|
||
* and emits mount_critical/warn/info for any non-
|
||
* root mount crossing thresholds — catches Docker
|
||
* volumes filling, runaway tmpfs, encrypted
|
||
* overlay mounts the operator forgot. smartctl-
|
||
* monitor extended with SCT thermal-log scraping
|
||
* via `smartctl -l scttempsts` — surfaces
|
||
* temperature_sustained_high (drive hit WARN+
|
||
* range at some point in its lifetime) and
|
||
* temperature_overlimit_count (drive firmware
|
||
* itself flagged thermal stress) that the
|
||
* instantaneous-temp check can't see.
|
||
*
|
||
* P121-CP16 Three sentinels pinning the SSE-stream shape
|
||
* smoke + expanded REST-API coverage.
|
||
* sse-stream-shape-smoke.ts ports cp15's contract-
|
||
* validation pattern to the three Server-Sent
|
||
* Events endpoints (/v1/orderbook/stream,
|
||
* /v1/instances/stream, /v1/chat/:a/:b/stream).
|
||
* Each event-type payload gets a zod schema and a
|
||
* `satisfies` cross-check against the canonical
|
||
* TS interface — drift between server emit and
|
||
* client parse fails CI rather than breaking
|
||
* every connected EventSource at once. The
|
||
* api-response-shape smoke is also expanded from
|
||
* 10 schemas to 27 — covering OrderViews,
|
||
* Orderbook (paged), Featured slots, Account
|
||
* orders, Profiles, Operator stats, Chat identity,
|
||
* Conversations, Blocks, Chat history, and
|
||
* Instance directory paged responses. Together
|
||
* 54 REST-shape + 18 SSE-shape contract checks
|
||
* on every CI run.
|
||
*
|
||
* P121-CP17 Two sentinels pinning the final lower-traffic
|
||
* schema coverage. api-response-shape smoke now
|
||
* covers ALL @morphit/indexer-client response
|
||
* types (76 checks across 38 interfaces): the
|
||
* cp16 set plus ClearingPricePoint, ClearingPrice-
|
||
* HistoryResponse, BatchProfilesResponse, Feedback-
|
||
* Record (with rating literal-union + nested
|
||
* responses array), FeedbackResponseRecord,
|
||
* AccountFeedback{,Given}Response, ChatReadState-
|
||
* Entry/Response, AttestorEligibilityResponse,
|
||
* StrangerFeeQuoteResponse. Schema-as-contract
|
||
* coverage for the indexer is complete; relay-
|
||
* side ad-hoc JSON responses remain a separate
|
||
* follow-up since they don't yet share a types
|
||
* package the smoke can satisfies-check against.
|
||
*
|
||
* Usage:
|
||
* cd apps/web && npx tsx scripts/persona-walkthrough-smoke.ts
|
||
*/
|
||
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
const REPO_WEB = join(import.meta.dirname, '..');
|
||
const REPO_ROOT = join(REPO_WEB, '..', '..');
|
||
|
||
interface Scenario {
|
||
readonly name: string;
|
||
/** Path relative to repo root (web app paths get rebased onto REPO_WEB). */
|
||
readonly file: string;
|
||
/** Substrings that must ALL appear in the file. */
|
||
readonly mustHave?: readonly string[];
|
||
/** Substrings that must NOT appear (regression sentinels). */
|
||
readonly mustNotHave?: readonly string[];
|
||
/** When true, resolve `file` relative to REPO_ROOT instead of REPO_WEB. */
|
||
readonly rootRelative?: boolean;
|
||
/** Optional positional check: `before` substring must appear at a smaller
|
||
* byte offset than `after`. Useful when ordering of two phrases matters
|
||
* (e.g. a security warning must come before a free-text input field). */
|
||
readonly assertOrdering?: {
|
||
readonly before: string;
|
||
readonly after: string;
|
||
};
|
||
/** Optional regex-based regression sentinels. Fails if ANY match exists in
|
||
* the file. Use this for class-of-pattern checks where listing every
|
||
* literal forbidden string would be incomplete (e.g. "no hardcoded
|
||
* English ariaLabel anywhere", which would silently miss any new ticker
|
||
* added without updating the literal list). Each entry is a (pattern,
|
||
* reason) pair so failure diagnostics can name the invariant being
|
||
* defended. Patterns must be `RegExp` (not string) so the smoke
|
||
* surfaces flag-handling explicitly. */
|
||
readonly assertNoRegexMatch?: readonly {
|
||
readonly pattern: RegExp;
|
||
readonly reason: string;
|
||
}[];
|
||
}
|
||
|
||
const SCENARIOS: readonly Scenario[] = [
|
||
// ─── Bob ─────────────────────────────────────────────────────────────
|
||
{
|
||
name: 'B-2 — backup-keys paired-readonly explanation card wired',
|
||
file: 'src/routes/[lang]/backup-keys/+page.svelte',
|
||
mustHave: [
|
||
'isPairedReadOnly',
|
||
'backup_keys.paired.heading',
|
||
'backup_keys.paired.body',
|
||
'backup_keys.paired.deeplink_hint',
|
||
'backup_keys.paired.deeplink_cta',
|
||
'web+morphit://backup-keys',
|
||
'Bob finding B-2'
|
||
]
|
||
},
|
||
{
|
||
name: 'B-2 — paired locale strings present in all 10 locales',
|
||
file: 'src/lib/i18n/locales/en.json',
|
||
mustHave: [
|
||
'"paired":',
|
||
'"heading":',
|
||
'"body":',
|
||
'"deeplink_hint":',
|
||
'"deeplink_cta":'
|
||
]
|
||
},
|
||
|
||
// ─── Sally (user) ────────────────────────────────────────────────────
|
||
{
|
||
name: 'S-11 — FundsSentModal renders inline txid help line',
|
||
file: 'src/lib/components/FundsSentModal.svelte',
|
||
mustHave: ['chat.funds_sent.txid_help', 'Sally finding S-11']
|
||
},
|
||
{
|
||
name: 'S-12 — Tooltip default ariaLabel reads from i18n',
|
||
file: 'src/lib/components/Tooltip.svelte',
|
||
mustHave: [
|
||
'a11y.tooltip_more_info',
|
||
'effectiveAriaLabel',
|
||
'Sally finding S-12'
|
||
],
|
||
// Pre-fix the default was a hardcoded English string. Verify
|
||
// that hardcoded default is gone.
|
||
mustNotHave: ["ariaLabel = 'More info'"]
|
||
},
|
||
{
|
||
name: 'S-12 — /post asset-explainer Tooltips no longer pass hardcoded ariaLabel',
|
||
file: 'src/routes/[lang]/post/+page.svelte',
|
||
// cp396: the per-asset Tooltip chain was replaced by a single
|
||
// alphabetized loop. Each block is a Tooltip whose textKey is the
|
||
// derived explainer key (post_order.form.asset_explainer.<ticker>),
|
||
// built once in `assetPickerItems`. So the evidence is the dynamic
|
||
// key construction + the Tooltip wiring it, not three literal keys.
|
||
mustHave: [
|
||
'post_order.form.asset_explainer.${a.toLowerCase()}',
|
||
'textKey={item.explainerKey}'
|
||
],
|
||
mustNotHave: [
|
||
'ariaLabel="What is BLURT?"',
|
||
'ariaLabel="What is BTC?"',
|
||
'ariaLabel="What is XMR?"'
|
||
],
|
||
// Part 122 cp6 F7: broaden coverage beyond any literal list. Any
|
||
// asset Tooltip added with a hardcoded `ariaLabel="What is X?"` prop
|
||
// would regress S-12. The regex catches every `ariaLabel="..."` Svelte
|
||
// prop with a literal string value on this page, regardless of asset.
|
||
// Acceptable forms derive from i18n via `effectiveAriaLabel` (no
|
||
// ariaLabel prop — the cp396 asset blocks) or a `{$_("...")}` value.
|
||
assertNoRegexMatch: [
|
||
{
|
||
pattern: /\bariaLabel="[^"]*"/,
|
||
reason:
|
||
'Tooltip ariaLabel must come from i18n (omit prop → effectiveAriaLabel default, OR pass {$_("...")} expression). Hardcoded literal blocks translation for the screen-reader label.'
|
||
}
|
||
]
|
||
},
|
||
|
||
// ─── Sally (operator) ────────────────────────────────────────────────
|
||
{
|
||
name: 'So-1 — vps-bootstrap.sh fast-path callout documented (OPERATIONS.md)',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: ['scripts/vps-bootstrap.sh', 'fast-path']
|
||
},
|
||
{
|
||
name: 'So-1 — OPERATIONS.md preamble mirrors the vps-bootstrap callout',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: ['scripts/vps-bootstrap.sh', 'experienced operators']
|
||
},
|
||
{
|
||
name: 'So-2 — ops-cli main.ts JSDoc lists all 14 subcommands',
|
||
file: 'apps/ops-cli/src/main.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
// Just check the ones most-recently-added are listed in
|
||
// the JSDoc header (pre-fix the header had 8 only).
|
||
'init [--check-only]',
|
||
'edit [--out=PATH]',
|
||
'import-altnet-key',
|
||
'export-altnet-key',
|
||
'register',
|
||
'payment-method',
|
||
'Sally-operator finding So-2'
|
||
]
|
||
},
|
||
{
|
||
name: 'So-3 — OPERATIONS.md §0a verbose-health env-opt-in callout',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: ['MORPHIT_INDEXER_VERBOSE_HEALTH=true', 'NEW-9-8']
|
||
},
|
||
{
|
||
name: 'So-3 — LAUNCH-DAY.md verbose-health env-opt-in warning',
|
||
file: 'docs/LAUNCH-DAY.md',
|
||
rootRelative: true,
|
||
mustHave: ['Sally-operator finding So-3', 'MORPHIT_INDEXER_VERBOSE_HEALTH']
|
||
},
|
||
{
|
||
name: 'So-3 — POST-LAUNCH-WEEK-ONE.md verbose-health reminder',
|
||
file: 'docs/POST-LAUNCH-WEEK-ONE.md',
|
||
rootRelative: true,
|
||
mustHave: ['Sally-operator finding So-3', 'MORPHIT_INDEXER_VERBOSE_HEALTH=true']
|
||
},
|
||
{
|
||
name: 'So-4 — init.ts JSDoc has realistic step count + disclaimer',
|
||
file: 'apps/ops-cli/src/commands/init.ts',
|
||
rootRelative: true,
|
||
mustHave: ['23 ELI5', 'check steps.ts'],
|
||
// Pre-fix said "Nine ELI5-style configuration prompts."
|
||
mustNotHave: ['Nine ELI5-style configuration prompts']
|
||
},
|
||
{
|
||
name: 'So-6 — OPERATIONS.md systemd install uses the path-aware installer',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
// beta14: the shipped units target /opt/morphit; this manual guide
|
||
// clones to ~/morphit. The installer detects the real checkout and
|
||
// writes the units with correct paths — replacing the old
|
||
// `systemctl edit` drop-in workaround (now retired). The callout
|
||
// must point operators at the installer, and the drop-in text must
|
||
// not creep back.
|
||
mustHave: [
|
||
'ops/scripts/install-systemd-units.sh',
|
||
'detects where you actually cloned',
|
||
'systemctl enable --now morphit-indexer'
|
||
],
|
||
mustNotHave: [
|
||
'This manual guide instead clones to',
|
||
'the systemd idiom for overriding'
|
||
]
|
||
},
|
||
{
|
||
name: 'F-3 — /dev has a landing page (cp136 walkthrough fix)',
|
||
file: 'apps/web/src/routes/[lang]/dev/+page.svelte',
|
||
rootRelative: true,
|
||
// The landing lists the three diagnostic children. If any
|
||
// future PR removes the landing, /en/dev 404s again — same
|
||
// dead-end Sally-operator hit pre-cp136.
|
||
mustHave: [
|
||
'/dev/icons',
|
||
'/dev/responsive',
|
||
'/dev/yubikey-probe',
|
||
"routeKey=\"dev_index\""
|
||
]
|
||
},
|
||
|
||
// ─── Drift-catchers from Part 119 docs audit ─────────────────────────
|
||
{
|
||
name: 'D-1 — no `morphit ops ` (space) typos in operator docs',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
// Hyphenated binary name is the canonical form. The space
|
||
// form `morphit ops ` would imply a non-existent binary.
|
||
mustNotHave: ['`morphit ops ', '"morphit ops ']
|
||
},
|
||
{
|
||
name: 'D-1 — no `morphit ops ` typos in RUN-A-MORPHIT-NODE.md',
|
||
file: 'docs/RUN-A-MORPHIT-NODE.md',
|
||
rootRelative: true,
|
||
mustNotHave: ['`morphit ops ', '"morphit ops ']
|
||
},
|
||
{
|
||
name: 'D-2 — no ghost MORPHIT_INDEXER_FEES_ACCOUNT env var',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
// The real env var is MORPHIT_INDEXER_FEE_RECIPIENT.
|
||
mustNotHave: ['MORPHIT_INDEXER_FEES_ACCOUNT']
|
||
},
|
||
{
|
||
name: 'D-2 — LAUNCH-DAY.md uses MORPHIT_INDEXER_FEE_RECIPIENT',
|
||
file: 'docs/LAUNCH-DAY.md',
|
||
rootRelative: true,
|
||
mustHave: ['MORPHIT_INDEXER_FEE_RECIPIENT'],
|
||
mustNotHave: ['MORPHIT_INDEXER_FEES_ACCOUNT']
|
||
},
|
||
{
|
||
name: 'D-3 — OPERATIONS.md uses monorepo paths, not separate-dir',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
// Stale paths from before the monorepo collapse.
|
||
mustNotHave: ['cd /opt/morphit-relay', 'cd /opt/morphit-indexer']
|
||
},
|
||
{
|
||
name: 'D-4 — PRE-LAUNCH reflects collapsed schema with v33 features',
|
||
file: 'docs/PRE-LAUNCH-CHECKLIST.md',
|
||
rootRelative: true,
|
||
// Originally pinned v31 (Part 119); bumped to v32 in Part 121
|
||
// cp3 when orders.asset_network landed; bumped to v33 in Part
|
||
// 122 cp13 when push_subscriptions + push_pending landed.
|
||
// Part 122 cp82 refactored the wording to reflect the
|
||
// collapsed-migration reality (MIGRATIONS[] stops at v1 which
|
||
// subsumes v1-v27; v28-v33 features live inline in schema.sql)
|
||
// — the anchor now pins both `schema_migrations.version = 1`
|
||
// (the current MIGRATIONS[] head) AND the v33-feature
|
||
// inventory phrase that any future v34 addition must update.
|
||
// If a future part adds another migration, bump BOTH this
|
||
// sentinel + the doc together.
|
||
mustHave: [
|
||
'`schema_migrations.version = 1`',
|
||
'`push_subscriptions`',
|
||
'`extension_count`'
|
||
],
|
||
mustNotHave: ['currently at v29 as of Part 108++', 'currently at v32 as of']
|
||
},
|
||
{
|
||
// Part 122 cp2 — F5 finding from cp1 audit.
|
||
//
|
||
// The migration model collapsed v2-v27 into v1 (May 2026 audit).
|
||
// v28-v32 changes live INLINE in schema.sql, but MIGRATIONS[]
|
||
// in migrations.ts stops at v1. This works pre-launch (fresh
|
||
// deploys apply schema.sql which contains all v28-v32 DDL), but
|
||
// has a latent foot-gun post-launch: if someone adds v33 DDL to
|
||
// schema.sql without ALSO adding a MIGRATIONS[33] entry, an
|
||
// upgrade-install would silently miss v33's changes (because
|
||
// schema_migrations sees v1 already applied, and MIGRATIONS[]
|
||
// has nothing after v1). This sentinel catches the drift at
|
||
// PR time: pinning the canonical head version in schema.sql
|
||
// against this assertion means any v33 addition fails the
|
||
// smoke until either (a) MIGRATIONS[33] is added too, or (b)
|
||
// this sentinel is consciously bumped to v33 (which requires
|
||
// thinking about whether the inline-without-MIGRATIONS pattern
|
||
// is still right). Belt-and-braces with the existing
|
||
// validateMigrationsContract() runtime check.
|
||
//
|
||
// Maintenance: every schema version bump REQUIRES updating
|
||
// THREE places in the same work unit:
|
||
// 1. apps/indexer/src/db/schema.sql (the actual DDL)
|
||
// 2. docs/PRE-LAUNCH-CHECKLIST.md (D-4 above)
|
||
// 3. this sentinel
|
||
// If post-launch you also add MIGRATIONS[vN], that's a fourth
|
||
// site. Three drift-anchors all pulling the same direction.
|
||
name: 'P122-CP2-F5 — schema.sql canonical head version pinned (cp1 F5 fix)',
|
||
file: 'apps/indexer/src/db/schema.sql',
|
||
rootRelative: true,
|
||
// The canonical head is v33 (Part 122 cp13 — push_subscriptions
|
||
// + push_pending tables, with cp14 adding the `locale` column).
|
||
// This line is a late version-header comment in schema.sql; if
|
||
// it changes the sentinel fails, forcing the maintainer to
|
||
// either bump the sentinel (and check D-4 above) or revert
|
||
// the schema change. Part 122 cp82 audit bumped this anchor
|
||
// from v32 (the original pin) to v33 — the sentinel was
|
||
// passing on a stale anchor because schema.sql contained
|
||
// BOTH `v32 / Part 121` AND `v33 / Part 122 cp13` headers
|
||
// (the inline-without-MIGRATIONS pattern this sentinel was
|
||
// designed to catch had already started to drift). cp82
|
||
// re-anchors at v33 to restore the load-bearing property.
|
||
mustHave: ['v33 / Part 122 cp13 — Web Push subscription storage + delivery queue']
|
||
},
|
||
{
|
||
// Part 122 cp3 — DNS-rebinding closure in federationProbe.
|
||
//
|
||
// Cp7 REVISIT §A documented the gap: the existing hostname
|
||
// denylist catches `https://127.0.0.1/` etc., but a hostname
|
||
// resolving to a private IP at fetch time would bypass the
|
||
// check. cp3 closes the gap with a three-layer defense:
|
||
// 1. isPrivateHostname() — literal-string check (existing)
|
||
// 2. resolveAndValidatePublicIp() — DNS lookup +
|
||
// per-IP validation against private-network deny list
|
||
// 3. buildPinnedAgent() — undici dispatcher whose
|
||
// connect-time lookup is hard-coded to the pre-validated
|
||
// IP (closes the TOCTOU between our validation and
|
||
// undici's own resolution)
|
||
//
|
||
// Sentinel pins all three layers + the test-injection hook
|
||
// that keeps smokes offline-deterministic. If a future
|
||
// refactor strips the dispatcher or skips the pre-validation,
|
||
// the sentinel catches it at PR time.
|
||
// Cp154 — the helper bodies (isPrivateHostname,
|
||
// isPrivateIp) were lifted to `@morphit/net-defense`
|
||
// so the mcp-server can consume the same primitives.
|
||
// federationProbe.ts now imports + re-exports them; the
|
||
// next scenario pins the lifted package's contents
|
||
// (including the subtle CGNAT and ::ffff: branches).
|
||
name: 'P122-CP3 — federationProbe has 3-layer DNS-rebinding defense (hostname denylist + DNS-resolved IP validation + pinned dispatcher)',
|
||
file: 'apps/indexer/src/indexer/federationProbe.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"import { isPrivateHostname, isPrivateIp } from '@morphit/net-defense'",
|
||
'export { isPrivateHostname }',
|
||
'export { isPrivateIp }',
|
||
'resolveAndValidatePublicIp',
|
||
'buildPinnedAgent',
|
||
'dispatcher: pinnedAgent',
|
||
"import { Agent } from 'undici'",
|
||
"import { lookup as dnsLookup } from 'node:dns/promises'"
|
||
]
|
||
},
|
||
{
|
||
// cp154 — net-defense package self-pin. The
|
||
// IPv4/IPv6 branches that used to live in
|
||
// federationProbe.ts are now here. Sentinel pins both
|
||
// pure-function exports + the subtle branches (IPv4-
|
||
// mapped IPv6 unwrap, CGNAT range) that an audit-by-eye
|
||
// would otherwise need to chase across files.
|
||
name: 'P122-CP3-cp154 — @morphit/net-defense ships the lifted SSRF primitives (hostname denylist + IP-form denylist with IPv4-mapped IPv6 unwrap + CGNAT)',
|
||
file: 'packages/net-defense/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export function isPrivateHostname',
|
||
'export function isPrivateIp',
|
||
// IPv4-mapped IPv6 unwrap is the subtle one — explicitly
|
||
// pin its presence so a future refactor doesn't drop it.
|
||
'::ffff:',
|
||
// CGNAT range (RFC 6598) — added in cp3 because operators
|
||
// sometimes have internal services in 100.64/10.
|
||
'100\\.(6[4-9]'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp4 — F9 (LOW) — paired-session "far past" check.
|
||
//
|
||
// pairedSession.ts docblock promised "Reject obviously-bogus
|
||
// timestamps (negative, far past, far future)". Pre-cp4 the
|
||
// code only checked "negative" and "far future" — the "far
|
||
// past" leg was missing. Defense-contract drift surfaced
|
||
// during cp4's Matrix/relay black-hat redux. Cp4 closed it
|
||
// with a `MAX_PAIRED_AGE_SECONDS` constant (365 days) +
|
||
// matching test case. Sentinel pins all three legs of the
|
||
// contract against future drift.
|
||
name: 'P122-CP4-F9 — pairedSession validator rejects far-past timestamps (matches docblock contract)',
|
||
file: 'apps/web/src/lib/crypto/pairedSession.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'MAX_PAIRED_AGE_SECONDS',
|
||
'365 * 86400',
|
||
// All three legs of the docblock contract must be enforced.
|
||
// Negative check:
|
||
'r.pairedAt < 0',
|
||
// Far-future check:
|
||
'r.pairedAt > now + 86400',
|
||
// Far-past check (the cp4 fix):
|
||
'r.pairedAt < now - MAX_PAIRED_AGE_SECONDS'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp5 — F10 (HIGH) — Jinja variable-name typo in
|
||
// the Ansible npm-install task's changed_when expression.
|
||
// Pre-cp5 the second clause referenced `npm_install_result`
|
||
// which doesn't exist (registered name is
|
||
// `morphit_npm_install_result`). When npm produces output
|
||
// without 'changed' (the typical first-install case), Jinja
|
||
// evaluates the undefined variable and Ansible aborts the
|
||
// playbook with `'npm_install_result' is undefined`.
|
||
// Cp5 fixed by aligning both clauses on the registered name.
|
||
// Sentinel pins the absence of the typo + the presence of
|
||
// the correctly-named pair.
|
||
name: 'P122-CP5-F10 — Ansible clone_and_build.yml npm-install changed_when references the actual registered name twice',
|
||
file: 'ops/ansible/roles/morphit/tasks/clone_and_build.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"register: morphit_npm_install_result",
|
||
"'changed' in morphit_npm_install_result.stdout or 'added' in morphit_npm_install_result.stdout"
|
||
],
|
||
mustNotHave: [
|
||
// The pre-cp5 typo MUST NOT reappear.
|
||
"'added' in npm_install_result.stdout"
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp5 — F11 (MEDIUM), corrected in the v1.9.9 doc
|
||
// accuracy audit. The shipped morphit-relay.service (and
|
||
// morphit-indexer.service) run as `User=root` — NOT
|
||
// `User=morphit-relay`, which no install path ever creates
|
||
// (verified: ops/systemd/morphit-relay.service). So the env
|
||
// files must be chowned `root:morphit` mode 0640 (root owns
|
||
// them; the `morphit` service group reads them), exactly as
|
||
// the Ansible role (roles/morphit/tasks/main.yml) and the
|
||
// init.ts wizard do for the other /etc/morphit/*.env files.
|
||
// The earlier `morphit-relay:morphit-relay` guidance chowned
|
||
// to a nonexistent user and would fail the command outright.
|
||
// Sentinel pins the corrected chown and forbids both the
|
||
// pre-cp5 `morphit:morphit` combined-chown and the
|
||
// nonexistent-user `morphit-relay:morphit-relay` form.
|
||
name: 'P122-CP5-F11 — OPERATIONS.md chowns /etc/morphit env files root:morphit 0640 (units run User=root; matches Ansible + init.ts)',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'sudo chown root:morphit /etc/morphit/indexer.env /etc/morphit/relay.env'
|
||
],
|
||
mustNotHave: [
|
||
// The pre-cp5 buggy combined morphit:morphit chown MUST NOT reappear.
|
||
'sudo chown morphit:morphit /etc/morphit/indexer.env /etc/morphit/relay.env',
|
||
// The nonexistent-user form (units run as root, not morphit-relay) MUST NOT reappear.
|
||
'chown morphit-relay:morphit-relay /etc/morphit/relay.env'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp5 — F12 (HIGH) — Ansible base role must
|
||
// create the `morphit-relay` system user BEFORE the
|
||
// morphit role tries to enable the morphit-relay.service.
|
||
// Pre-cp5 the playbook ran systemctl enable + start on
|
||
// services that reference `User=morphit-relay` without
|
||
// the user ever being created — a fresh-deploy hard fail.
|
||
// Sentinel pins the user-creation task in the base role.
|
||
name: 'P122-CP5-F12 — Ansible base role creates morphit-relay system user (referenced by shipped systemd unit User=morphit-relay)',
|
||
file: 'ops/ansible/roles/base/tasks/main.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'Create morphit-relay system group',
|
||
'Create morphit-relay system user',
|
||
'name: morphit-relay',
|
||
// Group membership: the relay user must be in the
|
||
// morphit_service_group so it can read /etc/morphit/relay.env
|
||
// (chowned root:morphit_service_group mode 0640 by the
|
||
// morphit role).
|
||
'groups: "{{ morphit_service_group }}"'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp5 — F13 (LOW) — the relay.env.j2 template
|
||
// must NOT carry a MORPHIT_RELAY_PASSPHRASE line. No code
|
||
// path consumes that env var (the relay unlocks via TTY
|
||
// prompt or systemd LoadCredential); having it in the
|
||
// template invites operators to leak their passphrase to
|
||
// disk out of misplaced template-completionism. Sentinel
|
||
// pins the absence + the explanatory comment that
|
||
// documents why.
|
||
name: 'P122-CP5-F13 — relay.env.j2 has no dead MORPHIT_RELAY_PASSPHRASE env var (consumed by nothing; would leak passphrase to disk)',
|
||
file: 'ops/ansible/roles/morphit/templates/relay.env.j2',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'NO MORPHIT_RELAY_PASSPHRASE env var'
|
||
],
|
||
mustNotHave: [
|
||
'MORPHIT_RELAY_PASSPHRASE={{'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp5 — F14 (MEDIUM) — operator-doc wizard-step
|
||
// number had drifted from the code. OPERATIONS.md said
|
||
// "morphit-ops init step 12 asks: Enable daily DB backup"
|
||
// but the backup prompt has migrated forward as steps were
|
||
// inserted ahead of it: pre-Part-109 it was step 12; Part 109
|
||
// added stepFeeExplorers + stepChatLink; later the Homepage-SEO
|
||
// meta step (step 16) landed just ahead of it, taking backup to
|
||
// step 17 of 23.
|
||
//
|
||
// Sentinel pins both legs of the contract:
|
||
// (a) TOTAL_STEPS = 23 in steps.ts (the canonical count)
|
||
// (b) OPERATIONS.md references "step 17" for backup
|
||
// (matching stepBackup's actual position)
|
||
// If a future wizard restructure changes TOTAL_STEPS or
|
||
// reorders stepBackup, this sentinel fails and forces
|
||
// either the count or the doc to be updated.
|
||
name: 'P122-CP5-F14 — wizard backup-step doc reference matches stepBackup position in code',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'`morphit-ops init` step 17 asks: "Enable daily DB backup automation?"'
|
||
],
|
||
mustNotHave: [
|
||
'`morphit-ops init` step 15 asks: "Enable daily DB backup automation?"',
|
||
'`morphit-ops init` step 16 asks: "Enable daily DB backup automation?"'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp5 — F14 companion — TOTAL_STEPS pinned in
|
||
// steps.ts. If the wizard ever grows or shrinks the step
|
||
// count, this sentinel fails so the OPERATIONS.md doc
|
||
// references can be re-audited at the same turn.
|
||
//
|
||
// cp167 update: was 18, now 20. cp167 added the MCP step
|
||
// (default-Yes, federation-wide discoverability) and the
|
||
// stepMatrixSurfaces/stepRpcEndpoints explicit-numbering
|
||
// pass that took the orchestrator's effective step count
|
||
// from 18 to 20.
|
||
//
|
||
// cp182 update: now 22. cp182 added stepBunkerWeb (the
|
||
// reverse-proxy/WAF decision that wires
|
||
// MORPHIT_RELAY_TRUSTED_PROXY_IPS, 20 → 21) and stepHardening
|
||
// (the host-hardening checklist generator, 21 → 22).
|
||
//
|
||
// cp208 update: now 23. cp208 inserted step 14
|
||
// "Payment-method policy" (the Barter enable/disable toggle,
|
||
// writing MORPHIT_INDEXER_DISABLED_PAYMENT_METHODS) right
|
||
// after the trade-only asset policy step, bumping the later
|
||
// steps by one (22 → 23).
|
||
name: 'P122-CP5-F14b — wizard TOTAL_STEPS pinned (must update F14 doc reference if this changes)',
|
||
file: 'apps/ops-cli/src/init/steps.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'const TOTAL_STEPS = 23;'
|
||
]
|
||
},
|
||
{
|
||
// Part 122 cp6 — schema-as-contract first layer.
|
||
//
|
||
// The signupClient module must import RelayErrorCode from
|
||
// the shared @morphit/relay-client package, not duplicate
|
||
// the literal union inline. Pre-cp6 signupClient.ts had
|
||
// its own copy of the ~25 relay error codes; if the relay
|
||
// added a new code (e.g. 'name_reserved_for_operator') and
|
||
// the client didn't add it too, the client would fall
|
||
// through to 'broadcast_failed' for the new case — a
|
||
// real-world drift class. The shared package is the
|
||
// single source of truth; this sentinel pins that the
|
||
// import survives.
|
||
name: 'P122-CP6 — signupClient imports RelayErrorCode from @morphit/relay-client (schema-as-contract)',
|
||
file: 'apps/web/src/lib/auth/signupClient.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"import('@morphit/relay-client').RelayErrorCode"
|
||
],
|
||
// Pre-cp6 the codes were duplicated inline. This sentinel
|
||
// rejects the inline duplication pattern. The mustNotHave
|
||
// targets the two most distinctive relay-only codes; if
|
||
// either reappears as a string literal in signupClient,
|
||
// someone reinlined the union and broke the contract.
|
||
mustNotHave: [
|
||
"| 'invite_rate_limited'",
|
||
"| 'spacing_cooldown'"
|
||
]
|
||
},
|
||
{
|
||
name: 'D-5 — PRE-LAUNCH does not reference nonexistent --dry-run flag',
|
||
file: 'docs/PRE-LAUNCH-CHECKLIST.md',
|
||
rootRelative: true,
|
||
// The indexer has no --dry-run flag; verify the bad
|
||
// recommendation is gone.
|
||
mustNotHave: ['npm run start -- --dry-run']
|
||
},
|
||
{
|
||
name: 'D-7 — POST-LAUNCH backup recipe uses real systemd timer',
|
||
file: 'docs/POST-LAUNCH-WEEK-ONE.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'morphit-backup.timer',
|
||
'/usr/local/lib/morphit/morphit-backup.sh'
|
||
],
|
||
// Pre-fix referenced a fictitious cron entry at this path.
|
||
mustNotHave: ['/opt/morphit-indexer/scripts/backup.sh']
|
||
},
|
||
{
|
||
name: 'D-8 — POST-LAUNCH monitoring uses real /v1/health fields',
|
||
file: 'docs/POST-LAUNCH-WEEK-ONE.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'.lag_blocks',
|
||
'.diagnostics.operator_balances',
|
||
'.diagnostics.price'
|
||
],
|
||
// Pre-fix referenced fields that don't exist in the
|
||
// actual /v1/health response shape.
|
||
mustNotHave: [
|
||
'.diagnostics.indexer.blocks_behind',
|
||
'.diagnostics.relay.balance_blurt',
|
||
'.diagnostics.treasury.address_source'
|
||
]
|
||
},
|
||
{
|
||
name: 'D-8 — LAUNCH-DAY monitoring loop uses real /v1/health fields',
|
||
file: 'docs/LAUNCH-DAY.md',
|
||
rootRelative: true,
|
||
mustHave: ['.lag_blocks', '.diagnostics.operator_balances'],
|
||
mustNotHave: [
|
||
'.diagnostics.indexer.blocks_behind',
|
||
'.diagnostics.relay.balance_blurt',
|
||
'.diagnostics.treasury.address_source'
|
||
]
|
||
},
|
||
{
|
||
name: 'D-9 — PRE-LAUNCH wizard step-count realistic',
|
||
file: 'docs/PRE-LAUNCH-CHECKLIST.md',
|
||
rootRelative: true,
|
||
mustHave: ['~23 prompts', 'steps.ts'],
|
||
mustNotHave: ['covers all 14 steps']
|
||
},
|
||
{
|
||
name: 'D-10 — OPERATIONS.md Postgres version accepts 15+',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
// Pre-fix said "should show 15.x or 16.x" which rejects PG 17.
|
||
mustHave: ['15.x or higher'],
|
||
mustNotHave: ['should show 15.x or 16.x']
|
||
},
|
||
{
|
||
name: 'D-11 — RUN-A-NODE §9.1 uses real `morphit-ops register` invocation',
|
||
file: 'docs/RUN-A-MORPHIT-NODE.md',
|
||
rootRelative: true,
|
||
mustHave: ['npx morphit-ops register'],
|
||
// Pre-fix referenced a fictitious flag-driven invocation
|
||
// against a nonexistent dist/ path.
|
||
mustNotHave: [
|
||
'node apps/ops-cli/dist/index.js register-operator',
|
||
'morphit_register_operator'
|
||
]
|
||
},
|
||
{
|
||
name: 'D-12 — RUN-A-NODE §11 troubleshooting curl uses nginx path /v1/ (indexer)',
|
||
file: 'docs/RUN-A-MORPHIT-NODE.md',
|
||
rootRelative: true,
|
||
mustHave: ['curl https://yourdomain.com/v1/health'],
|
||
// The indexer client issues absolute /v1/* requests, so the
|
||
// colocated nginx routes /v1/ -> indexer (NOT /api/indexer/,
|
||
// which the leading-slash URL semantics drop, and NOT the bare
|
||
// /indexer/ a naive reader might guess).
|
||
mustNotHave: [
|
||
'curl https://yourdomain.com/api/indexer/v1/health',
|
||
'curl https://yourdomain.com/indexer/v1/health'
|
||
]
|
||
},
|
||
{
|
||
name: 'D-13 — RUN-A-NODE references real /v1/health field "lag_blocks"',
|
||
file: 'docs/RUN-A-MORPHIT-NODE.md',
|
||
rootRelative: true,
|
||
mustHave: ['"lag_blocks":'],
|
||
// "head_lag_blocks" is not a field name in the actual response.
|
||
mustNotHave: ['"head_lag_blocks":']
|
||
},
|
||
|
||
// ─── Part 120 — FAQ orphan catch ─────────────────────────────────────
|
||
// Two FAQ entries (public_api, qr_login) had been translated into
|
||
// all 10 locales but never rendered because they weren't listed in
|
||
// FAQ_KEYS. Both are flagship-feature entries. These sentinels
|
||
// fail if a future refactor removes them from FAQ_KEYS, OR if a
|
||
// translator deletes the locale entries without removing the keys.
|
||
{
|
||
name: 'P120-FAQ-1 — public_api FAQ key wired in FAQ_KEYS',
|
||
file: 'src/lib/utils/faqIndex.ts',
|
||
mustHave: ["'public_api'"]
|
||
},
|
||
{
|
||
name: 'P120-FAQ-2 — qr_login FAQ key wired in FAQ_KEYS',
|
||
file: 'src/lib/utils/faqIndex.ts',
|
||
mustHave: ["'qr_login'"]
|
||
},
|
||
{
|
||
name: 'P120-FAQ-3 — public_api FAQ entry present in en.json',
|
||
file: 'src/lib/i18n/locales/en.json',
|
||
mustHave: ['"public_api":']
|
||
},
|
||
{
|
||
name: 'P120-FAQ-4 — qr_login FAQ entry present in en.json',
|
||
file: 'src/lib/i18n/locales/en.json',
|
||
mustHave: ['"qr_login":']
|
||
},
|
||
|
||
// ─── Part 121 — workspace symlink / smoke-failure note ──────────
|
||
// New operators following RUN-A-MORPHIT-NODE.md need to know that
|
||
// `npm install` creates the workspace symlinks under
|
||
// `node_modules/@morphit/*` that the smoke suite depends on, and
|
||
// that a fresh-clone snapshot without `npm install` yet will see
|
||
// several smoke runners fail with `ERR_MODULE_NOT_FOUND`
|
||
// referencing a `@morphit/*` package (count drifts across releases
|
||
// as smokes are added or refactored; cp1 saw 13, cp22 saw 6, the
|
||
// number itself is not load-bearing — the symptom + fix is what
|
||
// matters). These sentinels pin the doc claim against future
|
||
// drift so an operator hitting the symptom finds the right
|
||
// troubleshooting in three places.
|
||
{
|
||
name: 'P121-DOC-1 — OPERATIONS.md mentions workspace symlinks + ERR_MODULE_NOT_FOUND',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: ['workspace symlinks', 'ERR_MODULE_NOT_FOUND', '@morphit/asset-registry']
|
||
},
|
||
{
|
||
name: 'P121-DOC-2 — OPERATIONS.md has Smoke-suite troubleshooting block',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'Smoke-suite troubleshooting',
|
||
'ERR_MODULE_NOT_FOUND',
|
||
'npm install --no-audit --no-fund'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-DOC-3 — PRE-LAUNCH-CHECKLIST §C has smoke-suite verification step',
|
||
file: 'docs/PRE-LAUNCH-CHECKLIST.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'bash scripts/run-smokes.sh',
|
||
'ERR_MODULE_NOT_FOUND',
|
||
'Part 121 audit'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-DOC-4 — ADR-0011 carries Part 121 fee_method enum-freeze forward-note',
|
||
file: 'docs/adr/0011-dynamic-fee-model.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'2026-05-13 forward note',
|
||
'wire-format-frozen invariant',
|
||
'memory #23',
|
||
'fee-method-enum-frozen-smoke',
|
||
'first-buy-waiver-payment-agnostic-smoke'
|
||
]
|
||
},
|
||
// ─── Part 121 cp3 — USDT shipped ─────────────────────────
|
||
{
|
||
name: 'P121-USDT-1 — canonical asset registry has USDT entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'USDT'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['erc20', 'trc20', 'spl', 'bep20']",
|
||
'defaultNetwork: null',
|
||
"privacyWarningKey: 'usdt_centralized'"
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-USDT-2 — frontend asset registry has matching USDT entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'usdt'",
|
||
"displayName: 'Tether'",
|
||
'canBeUsedForListingFee: false',
|
||
'defaultNetwork: null'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-USDT-3 — per-network metadata module ships ERC-20 + TRC-20 + SPL + BEP-20 bundled explorers',
|
||
file: 'apps/web/src/lib/assets/networks.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'etherscan.io/tx/{txid}',
|
||
'tronscan.org/#/transaction/{txid}',
|
||
'solscan.io/tx/{txid}',
|
||
'bscscan.com/tx/{txid}',
|
||
'validateUsdtAddress',
|
||
'validateUsdtTxid'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-USDT-4 — indexer order handler rejects USDT orders missing/wrong/extra asset_network',
|
||
file: 'apps/indexer/src/indexer/handlers/order.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'asset_network_required_for_usdt',
|
||
'asset_network_unknown',
|
||
'asset_network_not_permitted_for_asset',
|
||
'asset_disabled_on_instance'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-USDT-5a — orderbook page derives the USDT network chip',
|
||
file: 'apps/web/src/routes/[lang]/orderbook/+page.svelte',
|
||
rootRelative: true,
|
||
// cp404 — the verbose order_row.network_hint sentence was replaced by
|
||
// the compact network-name chip; the page derives which network and
|
||
// looks up its display name, then passes the chip to OrderCard.
|
||
// cp442 — that derivation moved OUT of this page's row loop into the
|
||
// shared `$lib/orders/networkChip` helper, because the featured strip
|
||
// renders the same OrderCard and had no chip at all (a featured USDT
|
||
// order that doesn't name its chain is a lose-your-funds ambiguity).
|
||
mustHave: [
|
||
'networkChipFor',
|
||
'networkChip'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-USDT-5a2 — the shared helper names the USDT/USDC/DAI network',
|
||
file: 'apps/web/src/lib/orders/networkChip.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'assets.usdt.network.',
|
||
'assets.usdc.network.',
|
||
'assets.dai.network.',
|
||
'isUsdtNetwork'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-USDT-5a3 — featured cards pass the network chip too',
|
||
file: 'apps/web/src/lib/components/FeaturedOrders.svelte',
|
||
rootRelative: true,
|
||
mustHave: ['networkChipFor', '{networkChip}']
|
||
},
|
||
{
|
||
name: 'P121-STABLECOIN-5b — OrderCard renders the network chip + stablecoin price subline',
|
||
file: 'apps/web/src/lib/components/OrderCard.svelte',
|
||
rootRelative: true,
|
||
// cp404 — the row rendering moved into the shared OrderCard: it renders
|
||
// the passed networkChip and, for stablecoin orders, the peg subline.
|
||
// cp417 — generalised from USDT-only to all three stablecoins
|
||
// (USDT/USDC/DAI) via isStablecoinSublineTicker + StablecoinPriceSubline.
|
||
mustHave: [
|
||
'networkChip',
|
||
'<StablecoinPriceSubline',
|
||
'isStablecoinSublineTicker(order.asset)'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP6-1 — /v1/instance surfaces disabled_assets in API + indexer-client',
|
||
file: 'apps/indexer/src/api/instance.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'disabled_assets: readonly string[]',
|
||
'disabled_assets: config.disabledAssets'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP6-2 — indexer-client InstanceResponse mirrors disabled_assets (optional for back-compat)',
|
||
file: 'packages/indexer-client/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: ['readonly disabled_assets?: readonly string[]']
|
||
},
|
||
{
|
||
name: 'P121-CP6-3 — frontend instance store hydrates disabled_assets with [] fallback',
|
||
file: 'apps/web/src/lib/stores/instance.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'readonly disabled_assets: readonly string[]',
|
||
'disabled_assets: result.data.disabled_assets ?? []',
|
||
'disabled_assets: []'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP6-4 — /about-this-instance renders asset-stance panel using $instance.disabled_assets',
|
||
file: 'apps/web/src/routes/[lang]/about-this-instance/+page.svelte',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"$_('about_this_instance.section.asset_stance')",
|
||
'$instance.disabled_assets.length === 0',
|
||
"$_('about_this_instance.asset_stance.federation_note')"
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP6-5 — /run-a-node carries operator-stance explainer panel',
|
||
file: 'apps/web/src/routes/[lang]/run-a-node/+page.svelte',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"$_('run_a_node.asset_policy_heading')",
|
||
"$_('run_a_node.asset_policy_default_label')",
|
||
"$_('run_a_node.asset_policy_federation_label')",
|
||
'MORPHIT_INDEXER_DISABLED_ASSETS'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP6-6 — per-locale prerendering path helpers shipped in $i18n/path.ts',
|
||
file: 'apps/web/src/lib/i18n/path.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export function localePath',
|
||
'export function stripLocalePrefix',
|
||
'export function pickLocaleFromAcceptLanguages',
|
||
'export function isLocalePrefixed',
|
||
// path.ts must NOT import from ./index (which would
|
||
// pull $app/environment in via the import chain) —
|
||
// the constants come from ./locales (pure module).
|
||
"from './locales'"
|
||
],
|
||
mustNotHave: ["from './index'"]
|
||
},
|
||
{
|
||
name: 'P121-CP6-7 — i18n module split: SUPPORTED_LOCALES SSoT in $i18n/locales (pure, no $app/environment)',
|
||
file: 'apps/web/src/lib/i18n/locales.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export const SUPPORTED_LOCALES',
|
||
'export const PLANNED_LOCALES',
|
||
'export const DEFAULT_LOCALE',
|
||
'export function matchSupported'
|
||
],
|
||
// The whole point of this split is that locales.ts has
|
||
// ZERO SvelteKit deps so it can be imported from any
|
||
// context (smoke, prerender-redirect shell, web worker).
|
||
mustNotHave: ['$app/environment', 'svelte-i18n', 'svelte/store']
|
||
},
|
||
{
|
||
name: 'P121-CP7-1 — [lang]/+layout.ts carries prerender + ssr=true + load() that validates lang, initI18nFor, and (cp201) redirects an unknown prefix-stripped segment to the detected locale instead of 404ing',
|
||
file: 'apps/web/src/routes/[lang]/+layout.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export const prerender = true',
|
||
'export const ssr = true',
|
||
'initI18nFor',
|
||
'waitLocale',
|
||
'pickLocaleFromAcceptLanguages',
|
||
'throw redirect(307'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP7-2 — [lang]/+page.ts carries entries() enumerating SUPPORTED_LOCALES (entries() must live on +page.ts per SvelteKit)',
|
||
file: 'apps/web/src/routes/[lang]/+page.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export function entries',
|
||
'SUPPORTED_LOCALES.map'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP7-3 — root +page.svelte is the detection-redirect shell (pickLocale + window.location.replace + noscript fallback)',
|
||
file: 'apps/web/src/routes/+page.svelte',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'pickLocaleFromAcceptLanguages',
|
||
'navigator.languages',
|
||
'window.location.replace',
|
||
'meta http-equiv="refresh"'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP7-4 — svelte.config.js carries handleUnseenRoutes:ignore for dynamic-param routes',
|
||
file: 'apps/web/svelte.config.js',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"handleUnseenRoutes: 'ignore'"
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP7-5 — Head.svelte gates url.search/url.hash behind building flag (prerender forbids reading them)',
|
||
file: 'apps/web/src/lib/components/Head.svelte',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"import { building } from '$app/environment'",
|
||
"building ? '' : $page.url.search",
|
||
"building ? '' : $page.url.hash"
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP7-6 — LanguageSwitcher navigates via locale-prefixed URL (goto + localePath + stripLocalePrefix)',
|
||
file: 'apps/web/src/lib/components/LanguageSwitcher.svelte',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'localePath',
|
||
'stripLocalePrefix',
|
||
'goto(target)'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-1 — Matrix address SSoT: parseMxid + parseRoomAlias in @morphit/operator-config (no SvelteKit deps; branded types prevent @↔# confusion)',
|
||
file: 'packages/operator-config/src/matrixAddress.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export function parseMxid',
|
||
'export function parseRoomAlias',
|
||
'MatrixMxid',
|
||
'MatrixRoomAlias',
|
||
"__brand: 'MatrixMxid'",
|
||
"__brand: 'MatrixRoomAlias'"
|
||
],
|
||
mustNotHave: ['$app/environment', 'svelte-i18n', 'svelte/store']
|
||
},
|
||
{
|
||
name: 'P121-CP9-2 — indexer config refuses @-prefixed value in OPERATOR_MATRIX_ROOM (privacy framing in error)',
|
||
file: 'apps/indexer/src/config/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'MORPHIT_INDEXER_OPERATOR_MATRIX_ROOM',
|
||
'parseRoomAlias',
|
||
'public API',
|
||
'MORPHIT_MATRIX_BOT_ALERT_MXID'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-3 — /v1/instance InstanceResponse exposes operator_matrix_room: string | null (public room only)',
|
||
file: 'apps/indexer/src/api/instance.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'operator_matrix_room: string | null',
|
||
'config.operatorMatrixRoom'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-4 — matrix-bot config refuses #-prefixed value in ALERT_MXID with privacy-violation framing in error',
|
||
file: 'apps/matrix-bot/src/config.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"raw.startsWith('#')",
|
||
'privacy violation',
|
||
'private MXID',
|
||
'parseMxid'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-5 — matrix-bot sendDm() typed signature requires MatrixMxid (branded type — compile-time prevents room-alias misuse)',
|
||
file: 'apps/matrix-bot/src/matrix.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'sendDm(to: MatrixMxid',
|
||
"from '@morphit/operator-config'"
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-6 — matrix-bot main loop iterates config.alertMxids (typed list); CRITICAL bypasses rate limiter; WARN gates through it; INFO accumulates to digest',
|
||
file: 'apps/matrix-bot/src/main.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'for (const mxid of config.alertMxids)',
|
||
"classified.tier === 'CRITICAL'",
|
||
"classified.tier === 'WARN'",
|
||
'rateLimiter.isLimited',
|
||
'state.pushInfoEvent'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-7 — indexer-client InstanceResponse mirror exposes operator_matrix_room (optional for back-compat)',
|
||
file: 'packages/indexer-client/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'operator_matrix_room?: string | null'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP9-8 — ops-cli wizard stepMatrixSurfaces validates both prefixes; rejects @ in room field and # in MXID field with explicit guidance',
|
||
file: 'apps/ops-cli/src/init/steps.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'stepMatrixSurfaces',
|
||
'parseMxid',
|
||
'parseRoomAlias',
|
||
"startsWith('#')",
|
||
"startsWith('@')",
|
||
'MATRIX_EXAMPLE_MXID',
|
||
'MATRIX_EXAMPLE_ROOM_ALIAS'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP10 — host-resource monitor sidecar ──────────────
|
||
//
|
||
// Five sentinels pinning the cp10 invariants: the sidecar
|
||
// script exists + is executable, the systemd unit + timer
|
||
// exist, the classifier knows about host-resource events at
|
||
// every tier, the ALERT_COPY has friendly ELI5 advice for
|
||
// every host-resource event, and the bot's default
|
||
// JOURNALCTL_UNITS list includes morphit-host-monitor.service
|
||
// so alerts route automatically.
|
||
|
||
{
|
||
name: 'P121-CP10-1 — host-resource sidecar script exists, is POSIX-sh, emits structured JSON in LogRecord envelope shape',
|
||
file: 'ops/scripts/morphit-host-monitor.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'#!/bin/sh',
|
||
'MORPHIT_EMIT_MODULE="host-resource"',
|
||
'MORPHIT_EMIT_TAG="morphit-host-monitor"',
|
||
'/proc/meminfo',
|
||
'/proc/loadavg',
|
||
'/proc/vmstat',
|
||
'pswpin',
|
||
'pswpout',
|
||
'MORPHIT_HOST_DISK_CRITICAL',
|
||
'MORPHIT_HOST_MEM_CRITICAL',
|
||
'MORPHIT_HOST_SWAP_THRASH_CRITICAL',
|
||
'MORPHIT_HOST_CPU_CRITICAL'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP10-2 — systemd service + timer for host-monitor exist, mirror hardening posture, opt-in via timer enable',
|
||
file: 'ops/systemd/morphit-host-monitor.service',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'Type=oneshot',
|
||
'User=morphit-host-monitor',
|
||
'ProtectSystem=strict',
|
||
'NoNewPrivileges=true',
|
||
'PrivateNetwork=true',
|
||
'ReadWritePaths=/var/lib/morphit-host-monitor',
|
||
'EnvironmentFile=-/etc/morphit/host-monitor.env',
|
||
'/opt/morphit/ops/scripts/morphit-host-monitor.sh'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP10-3 — classifier knows about host-resource events at every tier (5 CRITICAL, 5 WARN, 4 INFO)',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'host-resource' && a.event === 'disk_critical'",
|
||
"'host-resource' && a.event === 'mem_critical'",
|
||
"'host-resource' && a.event === 'swap_critical'",
|
||
"'host-resource' && a.event === 'swap_thrashing_critical'",
|
||
"'host-resource' && a.event === 'cpu_saturated_critical'",
|
||
"'host-resource' && a.event === 'disk_warn'",
|
||
"'host-resource' && a.event === 'mem_warn'",
|
||
"'host-resource' && a.event === 'swap_warn'",
|
||
"'host-resource' && a.event === 'swap_thrashing_warn'",
|
||
"'host-resource' && a.event === 'cpu_saturated_warn'"
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP10-4 — ALERT_COPY has friendly ELI5 advice for every host-resource event (17 entries: disk/mem/swap × 3 + swap_thrashing × 2 + cpu_saturated × 3 + mount × 3)',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'host-resource:disk_critical'",
|
||
"'host-resource:disk_warn'",
|
||
"'host-resource:disk_info'",
|
||
"'host-resource:mem_critical'",
|
||
"'host-resource:mem_warn'",
|
||
"'host-resource:mem_info'",
|
||
"'host-resource:swap_critical'",
|
||
"'host-resource:swap_warn'",
|
||
"'host-resource:swap_info'",
|
||
"'host-resource:swap_thrashing_critical'",
|
||
"'host-resource:swap_thrashing_warn'",
|
||
"'host-resource:cpu_saturated_critical'",
|
||
"'host-resource:cpu_saturated_warn'",
|
||
"'host-resource:cpu_saturated_info'",
|
||
"'host-resource:mount_critical'",
|
||
"'host-resource:mount_warn'",
|
||
"'host-resource:mount_info'",
|
||
'OOM ',
|
||
'sudo journalctl --vacuum-time=7d',
|
||
'ps aux --sort=-%mem'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP10-5 — matrix-bot default JOURNALCTL_UNITS includes morphit-host-monitor.service for automatic routing',
|
||
file: 'apps/matrix-bot/src/config.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'MORPHIT_MATRIX_BOT_JOURNALCTL_UNITS',
|
||
'morphit-indexer.service',
|
||
'morphit-relay.service',
|
||
'morphit-host-monitor.service'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP11 — extended monitoring sidecars + Ansible ─────
|
||
//
|
||
// Seven sentinels pinning: the three extended-monitor scripts
|
||
// exist + emit module:event names matching the classifier;
|
||
// the bot's default JOURNALCTL_UNITS extends to cover them;
|
||
// the classifier has matchers + ALERT_COPY for every cp11
|
||
// event; the Ansible playbook integrates the new roles with
|
||
// opt-in enable_* flags; and the matrix-bot npm-install
|
||
// requirement is documented for operators.
|
||
|
||
{
|
||
name: 'P121-CP11-1 — smartctl monitor script exists, emits module:smartctl with real event names',
|
||
file: 'ops/scripts/morphit-smartctl-monitor.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'#!/bin/sh',
|
||
'MORPHIT_EMIT_MODULE="smartctl"',
|
||
'MORPHIT_EMIT_TAG="morphit-smartctl-monitor"',
|
||
'smart_failed',
|
||
'self_test_failed',
|
||
'reallocated_sectors',
|
||
'pending_sectors',
|
||
'temperature_critical',
|
||
'temperature_warn',
|
||
'MORPHIT_SMART_TEMP_CRITICAL'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP11-2 — fail2ban monitor script exists, emits module:fail2ban with real event names + per-jail override pattern',
|
||
file: 'ops/scripts/morphit-fail2ban-monitor.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'#!/bin/sh',
|
||
'MORPHIT_EMIT_MODULE="fail2ban"',
|
||
'MORPHIT_EMIT_TAG="morphit-fail2ban-monitor"',
|
||
'daemon_unreachable',
|
||
'jail_critical_ban_count',
|
||
'jail_high_ban_count',
|
||
'jail_ban_rate_warn',
|
||
'MORPHIT_FAIL2BAN_BAN_CRITICAL',
|
||
'MORPHIT_FAIL2BAN_${jail_upper}_CRITICAL'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP11-3 — mdadm monitor script exists, emits module:mdadm with real event names; exits silently on no-RAID hosts',
|
||
file: 'ops/scripts/morphit-mdadm-monitor.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'#!/bin/sh',
|
||
'MORPHIT_EMIT_MODULE="mdadm"',
|
||
'MORPHIT_EMIT_TAG="morphit-mdadm-monitor"',
|
||
'array_failed',
|
||
'array_degraded',
|
||
'array_resyncing',
|
||
'/proc/mdstat'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP11-4 — bot default JOURNALCTL_UNITS includes all five sidecar units (cp10 host-monitor + cp11 smartctl/fail2ban/mdadm)',
|
||
file: 'apps/matrix-bot/src/config.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'morphit-host-monitor.service',
|
||
'morphit-smartctl-monitor.service',
|
||
'morphit-fail2ban-monitor.service',
|
||
'morphit-mdadm-monitor.service'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP11-5 — classifier knows about cp11 events at every tier; ALERT_COPY has ELI5 advice for all of them',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'smartctl' && a.event === 'smart_failed'",
|
||
"'smartctl' && a.event === 'self_test_failed'",
|
||
"'smartctl' && a.event === 'temperature_critical'",
|
||
"'fail2ban' && a.event === 'daemon_unreachable'",
|
||
"'fail2ban' && a.event === 'jail_critical_ban_count'",
|
||
"'mdadm' && a.event === 'array_failed'",
|
||
"'mdadm' && a.event === 'array_degraded'",
|
||
"'smartctl:smart_failed'",
|
||
"'fail2ban:daemon_unreachable'",
|
||
"'mdadm:array_degraded'",
|
||
'overall-health FAILED',
|
||
'fail2ban-client status',
|
||
'cat /proc/mdstat'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP11-6 — Ansible playbook integrates cp9 + cp10 + cp11 sidecar roles with opt-in enable_* flags',
|
||
file: 'ops/ansible/playbook.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'role: matrix_bot',
|
||
'role: host_monitor',
|
||
'role: smartctl_monitor',
|
||
'role: fail2ban_monitor',
|
||
'role: mdadm_monitor',
|
||
'enable_matrix_bot | default(false)',
|
||
'enable_host_monitor | default(false)',
|
||
'enable_smartctl_monitor | default(false)',
|
||
'enable_fail2ban_monitor | default(false)',
|
||
'enable_mdadm_monitor | default(false)',
|
||
', monitors]'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP11-7 — matrix-bot npm install requirement (better-sqlite3 native build + nodejs.org access) is documented in OPERATIONS.md',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'better-sqlite3',
|
||
'native build for better-sqlite3',
|
||
'nodejs.org',
|
||
'npm ci --workspaces',
|
||
'build-essential'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP12 — quality gates + further sidecars ────────────
|
||
//
|
||
// Four sentinels pinning: the Ansible quality gates
|
||
// (ansible-lint + structural smoke), the three new sidecar
|
||
// scripts (dmesg, trivy, postfix) emitting correct LogRecord
|
||
// envelopes, classifier extension covering all cp12 events,
|
||
// and bot's default JOURNALCTL_UNITS extending to all eight
|
||
// monitoring sidecar units.
|
||
|
||
{
|
||
name: 'P121-CP12-1 — Ansible quality gates: ansible-lint + structural smoke registered in run-smokes.sh',
|
||
file: 'scripts/run-smokes.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'apps/ops-cli:ansible-structural-smoke',
|
||
'apps/ops-cli:ansible-lint-smoke'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP12-2 — dmesg monitor script + classifier matchers + ALERT_COPY for OOM/oops/panic/MCE/segfaults',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'dmesg' && a.event === 'oom_kill'",
|
||
"'dmesg' && a.event === 'kernel_oops'",
|
||
"'dmesg' && a.event === 'kernel_panic'",
|
||
"'dmesg' && a.event === 'hardware_error'",
|
||
"'dmesg' && a.event === 'segfault_in_morphit'",
|
||
"'dmesg' && a.event === 'segfault_other'",
|
||
"'dmesg' && a.event === 'fd_exhausted'",
|
||
"'dmesg:oom_kill'",
|
||
"'dmesg:kernel_panic'",
|
||
'OOM-killer activated',
|
||
'Kernel panic detected'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP12-3 — trivy + postfix monitor scripts + classifier matchers + ALERT_COPY',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'trivy' && a.event === 'image_critical_vulns'",
|
||
"'trivy' && a.event === 'image_high_vulns'",
|
||
"'postfix' && a.event === 'queue_critical'",
|
||
"'postfix' && a.event === 'queue_warn'",
|
||
"'trivy:image_critical_vulns'",
|
||
"'postfix:queue_critical'",
|
||
'CRITICAL severity CVEs',
|
||
'alerting may be FAILING'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP12-4 — bot default JOURNALCTL_UNITS includes ALL eight sidecar units (indexer + relay + 6 monitor sidecars)',
|
||
file: 'apps/matrix-bot/src/config.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'morphit-host-monitor.service',
|
||
'morphit-smartctl-monitor.service',
|
||
'morphit-fail2ban-monitor.service',
|
||
'morphit-mdadm-monitor.service',
|
||
'morphit-dmesg-monitor.service',
|
||
'morphit-trivy-monitor.service',
|
||
'morphit-postfix-monitor.service'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP13 — CI workflow + 3 more sidecars + deps-pin ───
|
||
//
|
||
// Five sentinels pinning: the Forgejo CI workflow ships three
|
||
// gate jobs (typecheck + ansible-lint + smokes triple-pulse);
|
||
// the three new cp13 sidecars emit correct LogRecord envelopes
|
||
// with their full event sets; classifier knows all cp13
|
||
// events at every tier with ELI5 advice; the deps-pin-check
|
||
// smoke catches matrix-bot-sdk drift; and the bot's default
|
||
// JOURNALCTL_UNITS extends to all 11 sidecar units (indexer
|
||
// + relay + 9 monitor sidecars).
|
||
|
||
{
|
||
name: 'P121-CP13-1 — Forgejo CI workflow ships three gate jobs (typecheck + ansible-lint + smokes)',
|
||
file: '.forgejo/workflows/ci.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'name: morphit-ci',
|
||
'job',
|
||
'typecheck:',
|
||
'ansible-lint:',
|
||
'smokes:',
|
||
'bash scripts/typecheck-sweep.sh',
|
||
'bash scripts/run-smokes.sh',
|
||
'ansible-lint --offline --strict',
|
||
'for i in 1 2 3'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP13-2 — certbot monitor script + classifier matchers + renewal-stall detector',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'certbot' && a.event === 'cert_expiry_critical'",
|
||
"'certbot' && a.event === 'renewal_stalled'",
|
||
"'certbot' && a.event === 'cert_expiry_warn'",
|
||
"'certbot:renewal_stalled'",
|
||
"'certbot:cert_expiry_critical'",
|
||
'silently failing',
|
||
'sudo certbot renew'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP13-3 — apt + compose monitor scripts + classifier matchers + ALERT_COPY',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'apt' && a.event === 'security_updates_critical'",
|
||
"'apt' && a.event === 'security_updates_warn'",
|
||
"'compose' && a.event === 'service_unhealthy'",
|
||
"'compose' && a.event === 'service_exited'",
|
||
"'compose' && a.event === 'service_restart_loop'",
|
||
"'apt:security_updates_critical'",
|
||
"'compose:service_unhealthy'",
|
||
'sudo apt update',
|
||
'docker compose logs'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP13-4 — matrix-bot deps-pin-check smoke registered in run-smokes.sh + handles matrix-bot-sdk + better-sqlite3 + zod',
|
||
file: 'apps/matrix-bot/scripts/deps-pin-check.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'TRACKED_DEPS',
|
||
'matrix-bot-sdk',
|
||
'better-sqlite3',
|
||
'zod',
|
||
'satisfies',
|
||
'DRIFT'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP13-5 — bot default JOURNALCTL_UNITS covers ALL 11 sidecar units (indexer + relay + 9 monitor sidecars)',
|
||
file: 'apps/matrix-bot/src/config.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'morphit-indexer.service',
|
||
'morphit-relay.service',
|
||
'morphit-host-monitor.service',
|
||
'morphit-smartctl-monitor.service',
|
||
'morphit-fail2ban-monitor.service',
|
||
'morphit-mdadm-monitor.service',
|
||
'morphit-dmesg-monitor.service',
|
||
'morphit-trivy-monitor.service',
|
||
'morphit-postfix-monitor.service',
|
||
'morphit-certbot-monitor.service',
|
||
'morphit-apt-monitor.service',
|
||
'morphit-compose-monitor.service'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP14 — envelope schema + cross-workspace deps-pin +
|
||
// systemd-health + journald sidecars + release workflow
|
||
{
|
||
name: 'P121-CP14-1 — sidecar envelope smoke validates LogRecord schema across all sidecars',
|
||
file: 'apps/matrix-bot/scripts/sidecar-envelope-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'LogRecordSchema',
|
||
'morphit-host-monitor.sh',
|
||
'morphit-dmesg-monitor.sh',
|
||
'morphit-systemd-monitor.sh',
|
||
'morphit-journald-monitor.sh',
|
||
'lowercase_snake convention',
|
||
'z.enum'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP14-2 — workspace-deps-pin-check covers every workspace, not just matrix-bot',
|
||
file: 'apps/ops-cli/scripts/workspace-deps-pin-check.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'findWorkspaces',
|
||
'apps',
|
||
'packages',
|
||
'satisfies',
|
||
"'workspace:'",
|
||
'workspace deps-pin-check'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP14-3 — systemd unit-health sidecar + classifier matchers + ALERT_COPY',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'systemd' && a.event === 'unit_failed'",
|
||
"'systemd' && a.event === 'unit_restart_loop'",
|
||
"'systemd' && a.event === 'unit_missing'",
|
||
"'systemd:unit_failed'",
|
||
"'systemd:unit_restart_loop'",
|
||
'reset-failed',
|
||
'unit isnt running to emit'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP14-4 — journald disk-usage sidecar + classifier matchers + ALERT_COPY',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'journald' && a.event === 'journal_size_critical'",
|
||
"'journald' && a.event === 'journal_size_warn'",
|
||
"'journald' && a.event === 'journal_rotation_stale'",
|
||
"'journald:journal_size_critical'",
|
||
'SystemMaxUse',
|
||
'journalctl --vacuum'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP14-5 — Forgejo release workflow + bot default JOURNALCTL_UNITS covers ALL 14 units',
|
||
file: 'apps/matrix-bot/src/config.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'morphit-systemd-monitor.service',
|
||
'morphit-journald-monitor.service'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP15 — API-shape zod smoke + emit.sh lib refactor
|
||
// + host-monitor mount sweep + smartctl SCT thermal log
|
||
{
|
||
name: 'P121-CP15-1 — API-response-shape smoke extends envelope-smoke pattern to HTTP API with TS-type-cross-check',
|
||
file: 'apps/matrix-bot/scripts/api-response-shape-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'HealthSchema',
|
||
'InstanceResponseSchema',
|
||
'OrderRecordSchema',
|
||
'satisfies HealthResponse',
|
||
'satisfies InstanceResponse',
|
||
'@morphit/indexer-client',
|
||
'invalidate',
|
||
'safeParse'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP15-2 — shared emit() helper lib at ops/scripts/lib/emit.sh',
|
||
file: 'ops/scripts/lib/emit.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'iso_now()',
|
||
'json_str()',
|
||
'emit()',
|
||
'MORPHIT_EMIT_MODULE',
|
||
'MORPHIT_EMIT_TAG',
|
||
'systemd-cat -t'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP15-3 — host-monitor sources lib/emit.sh AND has mount-sweep section for bind-mounts + tmpfs',
|
||
file: 'ops/scripts/morphit-host-monitor.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'. "$(dirname "$0")/lib/emit.sh"',
|
||
'MORPHIT_EMIT_MODULE="host-resource"',
|
||
'MORPHIT_EMIT_TAG="morphit-host-monitor"',
|
||
'MORPHIT_HOST_SCAN_MOUNTS',
|
||
'--output=target,pcent,fstype',
|
||
'mount_critical',
|
||
'mount_warn',
|
||
'mount_info',
|
||
'squashfs'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP15-4 — smartctl-monitor SCT thermal-log extension + classifier matchers + ALERT_COPY',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'smartctl' && a.event === 'temperature_sustained_high'",
|
||
"'smartctl' && a.event === 'temperature_overlimit_count'",
|
||
"'smartctl:temperature_sustained_high'",
|
||
"'smartctl:temperature_overlimit_count'",
|
||
'scttempsts',
|
||
'over-temperature'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP15-5 — host-resource mount_* classifier matchers + ALERT_COPY across CRITICAL/WARN/INFO tiers',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"'host-resource' && a.event === 'mount_critical'",
|
||
"'host-resource' && a.event === 'mount_warn'",
|
||
"'host-resource:mount_critical'",
|
||
"'host-resource:mount_warn'",
|
||
"'host-resource:mount_info'",
|
||
'bind-mount'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP16 — SSE stream shapes + expanded REST coverage
|
||
{
|
||
name: 'P121-CP16-1 — SSE-stream-shape smoke validates all three streams (orderbook, instances, chat)',
|
||
file: 'apps/matrix-bot/scripts/sse-stream-shape-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'OrderbookSnapshotSchema',
|
||
'OrderbookOrderUpsertedSchema',
|
||
'OrderbookOrderRemovedSchema',
|
||
'InstancesSnapshotSchema',
|
||
'InstancesRemovedSchema',
|
||
'ChatSnapshotSchema',
|
||
'ChatMessageAppendedSchema',
|
||
'satisfies OrderRecord',
|
||
'satisfies InstanceDirectoryEntry',
|
||
'satisfies ChatMessageRecord'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP16-2 — api-response-shape smoke expanded to cover 27 interfaces (was 10 at cp15)',
|
||
file: 'apps/matrix-bot/scripts/api-response-shape-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'OrderViewsResponseSchema',
|
||
'OrderbookResponseSchema',
|
||
'FeaturedOrderbookResponseSchema',
|
||
'AccountOrdersResponseSchema',
|
||
'ProfileResponseSchema',
|
||
'OperatorsResponseSchema',
|
||
'ChatIdentityResponseSchema',
|
||
'ConversationsResponseSchema',
|
||
'BlocksResponseSchema',
|
||
'ChatHistoryResponseSchema',
|
||
'InstanceDirectoryResponseSchema'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP16-3 — every cp16 schema is anchored by a satisfies-clause cross-check against the TS interface',
|
||
file: 'apps/matrix-bot/scripts/api-response-shape-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'satisfies OrderViewsResponse',
|
||
'satisfies OrderbookResponse',
|
||
'satisfies FeaturedOrderbookResponse',
|
||
'satisfies ProfileResponse',
|
||
'satisfies OperatorsResponse',
|
||
'satisfies ChatIdentityResponse',
|
||
'satisfies ConversationsResponse',
|
||
'satisfies BlocksResponse',
|
||
'satisfies InstanceDirectoryResponse'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP17 — final lower-traffic schema coverage ──────
|
||
{
|
||
name: 'P121-CP17-1 — api-response-shape smoke now covers ALL @morphit/indexer-client response types',
|
||
file: 'apps/matrix-bot/scripts/api-response-shape-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'ClearingPricePointSchema',
|
||
'ClearingPriceHistoryResponseSchema',
|
||
'BatchProfilesResponseSchema',
|
||
'FeedbackRecordSchema',
|
||
'FeedbackResponseRecordSchema',
|
||
'AccountFeedbackResponseSchema',
|
||
'AccountFeedbackGivenResponseSchema',
|
||
'ChatReadStateEntrySchema',
|
||
'ChatReadStateResponseSchema',
|
||
'AttestorEligibilityResponseSchema',
|
||
'StrangerFeeQuoteResponseSchema'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP17-2 — every cp17 schema is anchored by a satisfies-clause cross-check against the TS interface',
|
||
file: 'apps/matrix-bot/scripts/api-response-shape-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'satisfies ClearingPricePoint',
|
||
'satisfies ClearingPriceHistoryResponse',
|
||
'satisfies BatchProfilesResponse',
|
||
'satisfies FeedbackRecord',
|
||
'satisfies FeedbackResponseRecord',
|
||
'satisfies AccountFeedbackResponse',
|
||
'satisfies AccountFeedbackGivenResponse',
|
||
'satisfies ChatReadStateResponse',
|
||
'satisfies AttestorEligibilityResponse',
|
||
'satisfies StrangerFeeQuoteResponse'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP18 — deep-deep security audit fixes ───────────
|
||
{
|
||
name: 'P121-CP18-1 — AUDIT-1 fix: json_str() encodes ALL C0 control chars (0x00-0x1F) per RFC 8259',
|
||
file: 'ops/scripts/lib/emit.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-1',
|
||
'sed -z',
|
||
'\\u0000',
|
||
'\\u001b',
|
||
'\\u001f',
|
||
'LC_ALL=C'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP18-2 — AUDIT-1 regression smoke covers the dmesg-comm-injection attack vector',
|
||
file: 'apps/matrix-bot/scripts/json-str-injection-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-1',
|
||
'embedded newline (THE primary attack vector',
|
||
'all C0 control chars',
|
||
'invalid UTF-8',
|
||
'callJsonStr',
|
||
'JSON.parse'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP18-3 — AUDIT-CI-7 fix: release.yml validates tag format strictly + passes via env, not ${{}} interpolation',
|
||
file: '.forgejo/workflows/release.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-CI-7',
|
||
'v[0-9]*.[0-9]*.[0-9]*',
|
||
'TARBALL: ${{ steps.ver.outputs.tarball }}',
|
||
'tar --exclude',
|
||
'$TARBALL'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP19 — MEDIUM/LOW audit-finding cleanup ──────────
|
||
{
|
||
name: 'P121-CP19-1 — AUDIT-ANSIBLE-1 fix: nodejs.yml uses apt-repo + GPG pattern, NO setup-script-as-root',
|
||
file: 'ops/ansible/roles/morphit/tasks/nodejs.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-ANSIBLE-1',
|
||
'/etc/apt/keyrings/nodesource.asc',
|
||
'signed-by=/etc/apt/keyrings/nodesource.asc',
|
||
'morphit_node_version'
|
||
],
|
||
mustNotHave: [
|
||
'setup_{{ morphit_node_version }}',
|
||
'bash /tmp/nodesource_setup.sh'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP19-2 — AUDIT-NUMERIC fix: emit.sh exports json_num() helper; host-monitor + fail2ban + compose-monitor use it',
|
||
file: 'ops/scripts/lib/emit.sh',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-NUMERIC',
|
||
'json_num()',
|
||
'*[!0-9.-]*'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP19-3 — AUDIT-2/3/4 fixes: matrix-bot renderAlertBody sanitizes payload (control-char strip + mxid defang + size cap)',
|
||
file: 'apps/matrix-bot/src/classifier.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-2',
|
||
'AUDIT-3',
|
||
'AUDIT-4',
|
||
'function sanitize',
|
||
'MAX_FIELD_BYTES',
|
||
'MAX_PAYLOAD_BYTES',
|
||
'\\u200d'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP19-4 — render-alert-hardening smoke validates the AUDIT-2/3/4 defenses',
|
||
file: 'apps/matrix-bot/scripts/render-alert-hardening-smoke.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-2',
|
||
'AUDIT-3',
|
||
'AUDIT-4',
|
||
'ESC char',
|
||
'mxid is defanged',
|
||
'truncated'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP19-5 — AUDIT-CI-2 fix: third-party actions SHA-pinned with version-comment annotations',
|
||
file: '.forgejo/workflows/ci.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'AUDIT-CI-2',
|
||
'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683',
|
||
'actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b',
|
||
'# v4.2.2',
|
||
'# v4.0.3'
|
||
]
|
||
},
|
||
|
||
// ─── P121-CP20 — beta-tester intake form wired into Forgejo
|
||
{
|
||
name: 'P121-CP20-1 — Forgejo issue template ships at .forgejo/issue_template/bug_report.md with auto-load frontmatter',
|
||
file: '.forgejo/issue_template/bug_report.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'name: "Bug report"',
|
||
'title: "[bug] "',
|
||
'needs-triage',
|
||
'## 1. One-line summary',
|
||
'## 17. Anything else?',
|
||
'@agorise:matrix.org'
|
||
]
|
||
},
|
||
{
|
||
name: 'P121-CP20-2 — Forgejo issue-picker config disables blank issues + surfaces public community Matrix room (NOT security DM)',
|
||
file: '.forgejo/issue_template/config.yml',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'blank_issues_enabled: false',
|
||
'contact_links',
|
||
'Community chat',
|
||
'matrix.to/#/#agorise:matrix.org'
|
||
],
|
||
mustNotHave: [
|
||
// The picker UI must NOT surface the operator's personal
|
||
// DM mxid; security disclosures route via §16 of the
|
||
// bug-report template, not the picker.
|
||
'matrix.to/#/@agorise:matrix.org',
|
||
'Security disclosure (private)'
|
||
]
|
||
},
|
||
|
||
// ─── P122-CP1 F1 fix — STOP banner above §1
|
||
//
|
||
// Beta-tester intake form (cp20) put the security-disclosure
|
||
// warning at §16, fifteen sections below §1 ("one-line
|
||
// summary"). A tester reporting a security vuln would have
|
||
// typed it into §1 before scrolling to §16. Cp1 prepended a
|
||
// STOP banner BEFORE §1 so the warning fires before any
|
||
// field is filled in. Sentinel locks the placement: the
|
||
// banner phrase MUST appear in the file AND it MUST appear
|
||
// at a byte offset BEFORE the §1 header.
|
||
{
|
||
name: 'P122-CP1-F1 — Forgejo template has STOP banner before §1 (security warning placement)',
|
||
file: '.forgejo/issue_template/bug_report.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'STOP — read this first if your bug involves security',
|
||
'DO NOT POST IT HERE',
|
||
'`@agorise:matrix.org`'
|
||
],
|
||
// Custom assertion not expressible via mustHave: the banner
|
||
// substring must appear at a smaller file-offset than the
|
||
// `## 1. One-line summary` header. We piggyback on the
|
||
// existing readFileSync body + a downstream check below;
|
||
// see the post-loop assertion for the ordering check.
|
||
assertOrdering: {
|
||
before: 'STOP — read this first',
|
||
after: '## 1. One-line summary'
|
||
}
|
||
},
|
||
|
||
// ─── cp131 HIGH-006 — warrant canary ghost op ────────────────────────
|
||
// Pre-cp131, OPERATIONS.md §0a and PRE-LAUNCH-CHECKLIST L140
|
||
// claimed @morphit "broadcasts morphit_warrant_canary_v1
|
||
// weekly". No such chain op exists — the canary is a
|
||
// PGP-signed static file at /canary.txt regenerated by
|
||
// scripts/canary/generate.sh. The ghost op-id misled
|
||
// operators into thinking @morphit needed mana for ~52
|
||
// weekly broadcasts/year, and into searching the indexer
|
||
// for an op that would never appear. Sentinel pins both
|
||
// docs to the corrected wording.
|
||
{
|
||
name: 'cp131 HIGH-006 — OPERATIONS.md does not reference a `morphit_warrant_canary_v1` chain op',
|
||
file: 'docs/OPERATIONS.md',
|
||
rootRelative: true,
|
||
// The canary subsystem itself is described in §36, but
|
||
// the chain-op variant of the canary is a ghost.
|
||
mustNotHave: ['morphit_warrant_canary_v1']
|
||
},
|
||
{
|
||
name: 'cp131 HIGH-006 — PRE-LAUNCH-CHECKLIST does not call for a `morphit_warrant_canary_v1` weekly broadcast',
|
||
file: 'docs/PRE-LAUNCH-CHECKLIST.md',
|
||
rootRelative: true,
|
||
// One acknowledgment of the historical mistake is
|
||
// allowed in the cp131 fix-note; the literal claim that
|
||
// the op exists is forbidden. Disambiguating substring:
|
||
// the false claim used "periodic `morphit_warrant_canary_v1` ops".
|
||
mustNotHave: ['periodic `morphit_warrant_canary_v1` ops']
|
||
},
|
||
{
|
||
name: 'cp131 HIGH-006 — README does not reference a `morphit_warrant_canary_v1` chain op',
|
||
file: 'README.md',
|
||
rootRelative: true,
|
||
mustNotHave: ['morphit_warrant_canary_v1']
|
||
},
|
||
|
||
// ─── cp131 LOW-010 — tar extract safety flags ────────────────────────
|
||
// upgrade.ts extracts a downloaded release tarball with
|
||
// `tar -xzf`. GNU tar defaults refuse path traversal and
|
||
// absolute paths, but DO honor archived uid/gid, setuid/
|
||
// setgid bits, and same-name directory→file overwrites.
|
||
// A compromised build host (or supply-chain replacement
|
||
// of both the tarball and its .sha256) could exploit any
|
||
// of those. cp131 added explicit defense-in-depth flags;
|
||
// this sentinel pins them so the upgrade path can't
|
||
// silently regress.
|
||
{
|
||
name: 'cp131 LOW-010 — upgrade.ts tar extract uses --no-same-owner',
|
||
file: 'apps/ops-cli/src/commands/upgrade.ts',
|
||
rootRelative: true,
|
||
mustHave: ['--no-same-owner']
|
||
},
|
||
{
|
||
name: 'cp131 LOW-010 — upgrade.ts tar extract uses --no-same-permissions',
|
||
file: 'apps/ops-cli/src/commands/upgrade.ts',
|
||
rootRelative: true,
|
||
mustHave: ['--no-same-permissions']
|
||
},
|
||
{
|
||
name: 'cp131 LOW-010 — upgrade.ts tar extract uses --no-overwrite-dir',
|
||
file: 'apps/ops-cli/src/commands/upgrade.ts',
|
||
rootRelative: true,
|
||
mustHave: ['--no-overwrite-dir']
|
||
},
|
||
|
||
// ─── cp131 LOW-005 — consolidated price-source wiring ─────────────────
|
||
// Pre-cp131 main.ts started a standalone BLURT price source
|
||
// AND a multi-asset map that ALSO contained BLURT — two
|
||
// independent fetchers making duplicate outbound HTTP calls
|
||
// on every refresh interval. cp131 derives `priceSource`
|
||
// from `multiAssetSources.get('BLURT')`. These sentinels
|
||
// pin the consolidation so the duplication can't return.
|
||
{
|
||
name: 'cp131 LOW-005 — main.ts no longer imports createPriceSource (single fetch loop)',
|
||
file: 'apps/indexer/src/main.ts',
|
||
rootRelative: true,
|
||
// The import line was: `import { createPriceSource, createMultiAssetPriceSources } from '$indexer/price/factory';`
|
||
// cp131 removed `createPriceSource` from the import set.
|
||
mustNotHave: ["import { createPriceSource"]
|
||
},
|
||
{
|
||
name: 'cp131 LOW-005 — main.ts derives priceSource from multiAssetSources',
|
||
file: 'apps/indexer/src/main.ts',
|
||
rootRelative: true,
|
||
mustHave: ["multiAssetSources.get('BLURT')"]
|
||
},
|
||
// ─── cp137 — per-asset structural sentinels for the 12 assets
|
||
// shipped after USDT. USDT got 5 sentinels (P121-USDT-1..5)
|
||
// when it shipped at Part 121 cp3 — the subsequent additions
|
||
// (USDC cp30, BCH cp23, LTC cp24, DAI cp31, DASH cp27,
|
||
// DOGE cp33, ZEC cp39, ARRR cp41, DCR cp43, SOL cp45, ETH
|
||
// cp47, XRP cp49) shipped with dedicated per-checkpoint
|
||
// sentinels in OTHER smokes (asset-registry-smoke,
|
||
// fee-method-enum-frozen-smoke, per-asset-key-family-*,
|
||
// what-is-morphit-asset-enum-smoke, etc.) but not in
|
||
// persona-walkthrough. This cluster adds 3 sentinels per
|
||
// asset: (1) canonical-registry invariants, (2) frontend-
|
||
// registry parity, (3) supporting metadata or chat-link
|
||
// URL constant. Structural defense at the persona-walk
|
||
// layer so any asset rip-out or invariant drift fires here
|
||
// in addition to the per-checkpoint smokes.
|
||
// ─── USDC (Part 122 cp30) ────────────────────────────────
|
||
{
|
||
name: 'cp137-USDC-1 — canonical asset registry has USDC entry with trade-only invariant + 4 networks',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'USDC'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['erc20', 'spl', 'base', 'polygon']",
|
||
'defaultNetwork: null',
|
||
"privacyWarningKey: 'usdc_centralized'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-USDC-2 — frontend asset registry matches USDC entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'usdc'",
|
||
"displayName: 'USD Coin'",
|
||
'canBeUsedForListingFee: false',
|
||
'defaultNetwork: null',
|
||
"privacyWarningKey: 'usdc_centralized'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-USDC-3 — per-network metadata ships ERC-20/SPL/Base/Polygon bundled explorers',
|
||
file: 'apps/web/src/lib/assets/networks.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'USDC_NETWORK_METADATA',
|
||
"key: 'erc20'",
|
||
"key: 'spl'",
|
||
"key: 'base'",
|
||
"key: 'polygon'"
|
||
]
|
||
},
|
||
// ─── BCH (Part 122 cp23) ─────────────────────────────────
|
||
{
|
||
name: 'cp137-BCH-1 — canonical asset registry has BCH entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'BCH'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-BCH-2 — frontend asset registry matches BCH entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'bch'",
|
||
"displayName: 'Bitcoin Cash'",
|
||
'canBeUsedForListingFee: false',
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-BCH-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_BCH_CHAT_LINK_URL', 'BCH_TXID_RE']
|
||
},
|
||
// ─── LTC (Part 122 cp24) ─────────────────────────────────
|
||
{
|
||
name: 'cp137-LTC-1 — canonical asset registry has LTC entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'LTC'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-LTC-2 — frontend asset registry matches LTC entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'ltc'",
|
||
"displayName: 'Litecoin'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-LTC-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_LTC_CHAT_LINK_URL', 'LTC_TXID_RE']
|
||
},
|
||
// ─── DAI (Part 122 cp31) ─────────────────────────────────
|
||
{
|
||
name: 'cp137-DAI-1 — canonical asset registry has DAI entry with trade-only invariant + 4 networks',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'DAI'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['erc20', 'polygon', 'base', 'arbitrum']",
|
||
'defaultNetwork: null',
|
||
"privacyWarningKey: 'dai_partly_centralized'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DAI-2 — frontend asset registry matches DAI entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'dai'",
|
||
"displayName: 'Dai'",
|
||
'canBeUsedForListingFee: false',
|
||
'defaultNetwork: null',
|
||
"privacyWarningKey: 'dai_partly_centralized'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DAI-3 — per-network metadata ships ERC-20/Polygon/Base/Arbitrum bundled explorers',
|
||
file: 'apps/web/src/lib/assets/networks.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'DAI_NETWORK_METADATA',
|
||
"key: 'erc20'",
|
||
"key: 'polygon'",
|
||
"key: 'base'",
|
||
"key: 'arbitrum'"
|
||
]
|
||
},
|
||
// ─── DASH (Part 122 cp27) ────────────────────────────────
|
||
{
|
||
name: 'cp137-DASH-1 — canonical asset registry has DASH entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'DASH'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DASH-2 — frontend asset registry matches DASH entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'dash'",
|
||
"displayName: 'Dash'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DASH-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_DASH_CHAT_LINK_URL', 'DASH_TXID_RE']
|
||
},
|
||
// ─── DOGE (Part 122 cp33) ────────────────────────────────
|
||
{
|
||
name: 'cp137-DOGE-1 — canonical asset registry has DOGE entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'DOGE'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DOGE-2 — frontend asset registry matches DOGE entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'doge'",
|
||
"displayName: 'Dogecoin'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DOGE-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_DOGE_CHAT_LINK_URL', 'DOGE_TXID_RE']
|
||
},
|
||
// ─── ZEC (Part 122 cp39) ─────────────────────────────────
|
||
{
|
||
name: 'cp137-ZEC-1 — canonical asset registry has ZEC entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'ZEC'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-ZEC-2 — frontend asset registry matches ZEC entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'zec'",
|
||
"displayName: 'Zcash'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-ZEC-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_ZEC_CHAT_LINK_URL', 'ZEC_TXID_RE']
|
||
},
|
||
// ─── ARRR / Pirate Chain (Part 122 cp41) ─────────────────
|
||
{
|
||
name: 'cp137-ARRR-1 — canonical asset registry has ARRR entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'ARRR'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-ARRR-2 — frontend asset registry matches ARRR entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'arrr'",
|
||
"displayName: 'Pirate Chain'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-ARRR-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_ARRR_CHAT_LINK_URL', 'ARRR_TXID_RE']
|
||
},
|
||
// ─── DCR / Decred (Part 122 cp43) ────────────────────────
|
||
{
|
||
name: 'cp137-DCR-1 — canonical asset registry has DCR entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'DCR'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DCR-2 — frontend asset registry matches DCR entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'dcr'",
|
||
"displayName: 'Decred'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-DCR-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_DCR_CHAT_LINK_URL', 'DCR_TXID_RE']
|
||
},
|
||
// ─── SOL / Solana (Part 122 cp45) ────────────────────────
|
||
{
|
||
name: 'cp137-SOL-1 — canonical asset registry has SOL entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'SOL'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-SOL-2 — frontend asset registry matches SOL entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'sol'",
|
||
"displayName: 'Solana'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-SOL-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_SOL_CHAT_LINK_URL', 'SOL_TXID_RE']
|
||
},
|
||
// ─── ETH / Ethereum (Part 122 cp47) ──────────────────────
|
||
{
|
||
name: 'cp137-ETH-1 — canonical asset registry has ETH entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'ETH'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-ETH-2 — frontend asset registry matches ETH entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'eth'",
|
||
"displayName: 'Ethereum'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-ETH-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_ETH_CHAT_LINK_URL', 'ETH_TXID_RE']
|
||
},
|
||
// ─── XRP / Ripple (Part 122 cp49) ────────────────────────
|
||
{
|
||
name: 'cp137-XRP-1 — canonical asset registry has XRP entry with trade-only invariant',
|
||
file: 'packages/asset-registry/src/index.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'XRP'",
|
||
'canPayListingFee: false',
|
||
"supportedNetworks: ['mainnet']",
|
||
"defaultNetwork: 'mainnet'"
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-XRP-2 — frontend asset registry matches XRP entry',
|
||
file: 'apps/web/src/lib/assets/registry.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
"ticker: 'xrp'",
|
||
"displayName: 'Ripple (XRP)'",
|
||
'canBeUsedForListingFee: false'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp137-XRP-3 — bundled chat-link explorer constant exists',
|
||
file: 'apps/web/src/lib/explorer/urlsCore.ts',
|
||
rootRelative: true,
|
||
mustHave: ['BUNDLED_XRP_CHAT_LINK_URL', 'XRP_TXID_RE']
|
||
},
|
||
// ─── cp138 — DB/Relay hardening sentinels ─────────────────
|
||
{
|
||
name: 'cp138-D-2 — push_subscriptions enforces per-account cap with sliding-window eviction',
|
||
file: 'apps/relay/src/policy/pushSubscriptions.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'MAX_SUBSCRIPTIONS_PER_ACCOUNT',
|
||
'cp138 D-2',
|
||
'this.db.withTx',
|
||
'ORDER BY created_at ASC',
|
||
'evicted_for_cap'
|
||
],
|
||
// Pre-cp138 the upsert was a single client.query() with no
|
||
// cap check. Defend against accidental regression to that
|
||
// pattern by requiring the withTx wrapper + eviction query.
|
||
mustNotHave: [
|
||
'await this.db.query<RawRow>(\n\t\t\t`INSERT INTO push_subscriptions'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp138-C-1 — keystore.ts KDF floor matches INTERACTIVE (downgrade-attack defense)',
|
||
file: 'apps/web/src/lib/crypto/keystore.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'cp138 C-1',
|
||
'MIN_KDF_OPSLIMIT = 2',
|
||
'MIN_KDF_MEMLIMIT = 64 * 1024 * 1024'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp138-C-1 — yubikey/wrap.ts KDF floor matches INTERACTIVE',
|
||
file: 'apps/web/src/lib/crypto/yubikey/wrap.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'cp138 C-1',
|
||
'MIN_ARGON_OPSLIMIT = 2',
|
||
'MIN_ARGON_MEMLIMIT = 64 * 1024 * 1024'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp138-I-1 — repo-root SECURITY.md exists with Matrix DM + Forgejo paths',
|
||
file: 'SECURITY.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'@agorise:matrix.org',
|
||
'docs/SECURITY.md',
|
||
'Matrix DM',
|
||
'72 hours',
|
||
'7 days'
|
||
]
|
||
},
|
||
// ─── Josie (sysadmin) ────────────────────────────────────────────────
|
||
// cp182 — Josie joins Bob/Sally/Charlie as a standing persona. These
|
||
// pins lock the setup-wizard fixes found during his walkthrough so they
|
||
// cannot silently regress: dead doc/command references, the opt-in→
|
||
// default-on Matrix reframe + sidecar wiring, and the new BunkerWeb step.
|
||
{
|
||
name: 'cp182 Josie Jo-1 — wizard next-steps cite real docs + the morphit-ops binary (no dead refs, no dev preview)',
|
||
file: 'apps/ops-cli/src/commands/init.ts',
|
||
rootRelative: true,
|
||
mustHave: ['morphit-ops status', 'morphit-ops register', 'RUN-A-MORPHIT-NODE.md §10'],
|
||
mustNotHave: ['OPERATOR-RUN-BOOK', 'npm run preview', '9 questions', '§10c', 'npx tsx apps/ops-cli/src/main.ts']
|
||
},
|
||
{
|
||
name: 'cp182 Josie Jo-2 — Matrix step is default-on (recommended) and walks the sidecar env setup',
|
||
file: 'apps/ops-cli/src/init/steps.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'Matrix alerting + public contact (recommended)',
|
||
'/etc/morphit/matrix-bot.env',
|
||
'MORPHIT_MATRIX_BOT_ACCESS_TOKEN',
|
||
// cp272 replaced the raw `systemctl enable morphit-matrix-bot.service`
|
||
// step with the one-command lifecycle switch; cp275 added the
|
||
// one-command delivery self-test. Assert the walkthrough uses both.
|
||
'morphit-ops matrix set',
|
||
'morphit-ops matrix test'
|
||
],
|
||
mustNotHave: ["if you don't use Matrix"]
|
||
},
|
||
{
|
||
name: 'cp182 Josie Jo-3 — MCP step cites the real section (§45) and a real command (no `morphit ops doctor`)',
|
||
file: 'apps/ops-cli/src/init/steps.ts',
|
||
rootRelative: true,
|
||
mustHave: ['OPERATIONS.md §45'],
|
||
mustNotHave: ['morphit ops doctor', 'morphit ops init']
|
||
},
|
||
{
|
||
name: 'cp182 Josie Jo-4 — BunkerWeb wizard step exists and wires the trusted-proxy CIDR',
|
||
file: 'apps/ops-cli/src/init/steps.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export async function stepBunkerWeb',
|
||
'MORPHIT_RELAY_TRUSTED_PROXY_IPS',
|
||
'172.20.0.0/16',
|
||
'OPERATIONS.md §32'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp182 Josie Jo-5 — render writes trusted-proxy only when BunkerWeb chosen',
|
||
file: 'apps/ops-cli/src/init/render.ts',
|
||
rootRelative: true,
|
||
mustHave: ['MORPHIT_RELAY_TRUSTED_PROXY_IPS=172.20.0.0/16', 'answers.bunkerWeb.enabled', 'BunkerWebResult']
|
||
},
|
||
{
|
||
name: 'cp182 Josie Jo-6a — hardening wizard step exists and leads with SSH-lockout safety',
|
||
file: 'apps/ops-cli/src/init/steps.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export async function stepHardening',
|
||
'Server hardening checklist (strongly recommended)',
|
||
'SECOND terminal',
|
||
'OPERATIONS.md §34'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp182 Josie Jo-6b — render emits a personalized hardening checklist tailored to BunkerWeb-vs-nginx',
|
||
file: 'apps/ops-cli/src/init/render.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'function renderHardeningChecklist',
|
||
'morphit-hardening-checklist.md',
|
||
'SSH LOCKOUT SAFETY',
|
||
'AUTO_LETS_ENCRYPT',
|
||
'ops/nginx/web.conf'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp186 Josie Jo-7a — edit reminds re-register after origin/tag; init guards re-runs with Edit/Overwrite/Cancel',
|
||
file: 'apps/ops-cli/src/commands/edit.ts',
|
||
rootRelative: true,
|
||
mustHave: ['originChanged = true', 'tagChanged = true', 'morphit-ops register', 'reach the federation']
|
||
},
|
||
{
|
||
name: 'cp186 Josie Jo-7b — init re-run guard offers Edit (recommended) / Overwrite / Cancel and hands off',
|
||
file: 'apps/ops-cli/src/commands/init.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'This instance is already set up',
|
||
'Edit a few settings (recommended)',
|
||
'Overwrite EVERYTHING',
|
||
'return await runEdit('
|
||
]
|
||
},
|
||
{
|
||
name: 'cp186/187 Josie Jo-7c — bare morphit-ops menu offers harden; harden is its own reuse-not-reimplement command',
|
||
file: 'apps/ops-cli/src/commands/harden.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export async function runHarden',
|
||
"from '../init/steps.ts'",
|
||
'renderHardeningChecklist',
|
||
'stepBunkerWeb',
|
||
'stepBackup'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp189 Josie Jo-8 — upgrade carries the operator config + keystore + altnet forward (survives release upgrade)',
|
||
file: 'apps/ops-cli/src/commands/upgrade.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'Carry the operator',
|
||
'morphit.config.env',
|
||
'morphit.env',
|
||
'apps/relay/keystore.json',
|
||
'apps/relay/altnet',
|
||
'copyFileSync',
|
||
'rollback(installDir, backupDir, tmpDir'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp192 Josie Jo-9a — guided install command reuses init/harden + offers PATH symlink',
|
||
file: 'apps/ops-cli/src/commands/install.ts',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'export async function runInstall',
|
||
'runInit',
|
||
'runHarden',
|
||
'/usr/local/bin/morphit-ops',
|
||
'ops/ansible/'
|
||
]
|
||
},
|
||
{
|
||
name: 'cp192 Josie Jo-9b — start-here hub routes operators by intent (install / upgrade / migrate)',
|
||
file: 'docs/start-here/README.md',
|
||
rootRelative: true,
|
||
mustHave: [
|
||
'RUN-A-MORPHIT-NODE.md',
|
||
'UPGRADING.md',
|
||
'MIGRATE-TO-RELEASE-TRACK.md',
|
||
'npx morphit-ops'
|
||
]
|
||
}
|
||
];
|
||
|
||
let failed = 0;
|
||
let passed = 0;
|
||
|
||
console.log('persona-walkthrough smoke:\n');
|
||
|
||
for (const sc of SCENARIOS) {
|
||
const base = sc.rootRelative ? REPO_ROOT : REPO_WEB;
|
||
const path = join(base, sc.file);
|
||
let body: string;
|
||
try {
|
||
body = readFileSync(path, 'utf8');
|
||
} catch (err) {
|
||
console.error(` ✗ ${sc.name}`);
|
||
console.error(` file not readable: ${sc.file}`);
|
||
console.error(` ${err}`);
|
||
failed++;
|
||
continue;
|
||
}
|
||
|
||
const missing = (sc.mustHave ?? []).filter((s) => !body.includes(s));
|
||
const present = (sc.mustNotHave ?? []).filter((s) => body.includes(s));
|
||
|
||
// Optional positional check. Both substrings must appear,
|
||
// AND `before` must precede `after` in the file.
|
||
let orderingError: string | null = null;
|
||
if (sc.assertOrdering) {
|
||
const beforeIdx = body.indexOf(sc.assertOrdering.before);
|
||
const afterIdx = body.indexOf(sc.assertOrdering.after);
|
||
if (beforeIdx === -1) {
|
||
orderingError = `ordering: "before" substring not found: ${JSON.stringify(sc.assertOrdering.before.slice(0, 60))}`;
|
||
} else if (afterIdx === -1) {
|
||
orderingError = `ordering: "after" substring not found: ${JSON.stringify(sc.assertOrdering.after.slice(0, 60))}`;
|
||
} else if (beforeIdx >= afterIdx) {
|
||
orderingError = `ordering: "${sc.assertOrdering.before.slice(0, 40)}..." at byte ${beforeIdx} but "${sc.assertOrdering.after.slice(0, 40)}..." at byte ${afterIdx}; first must precede second`;
|
||
}
|
||
}
|
||
|
||
// Optional regex-based forbidden-pattern checks. Each pattern that
|
||
// matches contributes an entry to regexMatches with the first match
|
||
// span shown to the operator for diagnosis.
|
||
const regexMatches: Array<{ pattern: RegExp; reason: string; match: string }> = [];
|
||
if (sc.assertNoRegexMatch) {
|
||
for (const { pattern, reason } of sc.assertNoRegexMatch) {
|
||
// Force a fresh scan each time — patterns may have the /g flag
|
||
// from the scenario author.
|
||
const localRe = new RegExp(pattern.source, pattern.flags.replace('g', ''));
|
||
const m = localRe.exec(body);
|
||
if (m !== null) {
|
||
regexMatches.push({ pattern, reason, match: m[0] });
|
||
}
|
||
}
|
||
}
|
||
|
||
if (
|
||
missing.length === 0 &&
|
||
present.length === 0 &&
|
||
orderingError === null &&
|
||
regexMatches.length === 0
|
||
) {
|
||
console.log(` ✓ ${sc.name}`);
|
||
passed++;
|
||
} else {
|
||
console.error(` ✗ ${sc.name}`);
|
||
for (const s of missing) {
|
||
console.error(` MUST HAVE (not found): ${JSON.stringify(s.slice(0, 100))}`);
|
||
}
|
||
for (const s of present) {
|
||
console.error(` MUST NOT HAVE (found): ${JSON.stringify(s.slice(0, 100))}`);
|
||
}
|
||
if (orderingError) {
|
||
console.error(` ${orderingError}`);
|
||
}
|
||
for (const { pattern, reason, match } of regexMatches) {
|
||
console.error(
|
||
` REGEX MATCH (forbidden pattern fired): ${pattern.toString()} — ${reason}`
|
||
);
|
||
console.error(` first hit: ${JSON.stringify(match.slice(0, 100))}`);
|
||
}
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
console.log(`\n${passed} passed, ${failed} failed (${SCENARIOS.length} total)`);
|
||
|
||
// cp192 — the start-here hub is the new front door for operators; a
|
||
// broken link there is exactly the kind of hiccup we're killing.
|
||
// Verify every relative markdown link [text](../X.md) in the hub
|
||
// resolves to a real file (the generic fenced-path smoke skips
|
||
// `../`-prefixed tokens, so pin them here explicitly).
|
||
{
|
||
const hubRel = 'docs/start-here/README.md';
|
||
const hubAbs = join(REPO_ROOT, hubRel);
|
||
let hubChecked = 0;
|
||
let hubBroken = 0;
|
||
try {
|
||
const hub = readFileSync(hubAbs, 'utf8');
|
||
const linkRe = /\]\((\.\.\/[A-Za-z0-9._/-]+\.md)\)/g;
|
||
let m: RegExpExecArray | null;
|
||
while ((m = linkRe.exec(hub)) !== null) {
|
||
const rel = m[1]!;
|
||
const target = join(REPO_ROOT, 'docs', 'start-here', rel);
|
||
hubChecked++;
|
||
if (!existsSync(target)) {
|
||
hubBroken++;
|
||
console.error(` ✗ start-here hub link broken: ${rel} (resolved ${target})`);
|
||
failed++;
|
||
}
|
||
}
|
||
if (hubBroken === 0) {
|
||
console.log(` ✓ start-here hub: all ${hubChecked} doc links resolve`);
|
||
}
|
||
} catch {
|
||
console.error(` ✗ start-here hub not readable at ${hubRel}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
if (failed > 0) {
|
||
console.error('\npersona-walkthrough smoke FAILED');
|
||
process.exit(1);
|
||
}
|
||
// Canonical success line — run-smokes.sh greps for `^✓ all` to tally
|
||
// scenarios.
|
||
console.log(`✓ all ${SCENARIOS.length} persona-walkthrough scenarios passed`);
|