@misar/billing-guards (0.1.0)

Published 2026-07-21 09:08:05 +00:00 by misaradmin

Installation

@misar:registry=
npm install @misar/billing-guards@0.1.0
"@misar/billing-guards": "0.1.0"

About this package

@misar/billing-guards

Zero-dependency CI guards that statically validate Stripe payment + subscription lifecycle correctness across the Misar AI monorepo. Advisory-first: findings emit ::warning and exit 0 until a repo opts into blocking.

Sibling of @misar/guards — same architecture (config schema, guard shape, CLI flags, Forgejo annotations), scoped to billing. Node built-ins only, plain ESM .mjs.

pnpm dlx @misar/billing-guards            # advisory (default): warnings, exit 0
pnpm dlx @misar/billing-guards --blocking # error findings fail the build
pnpm dlx @misar/billing-guards --list     # list guard ids
pnpm dlx @misar/billing-guards --json     # machine-readable { ran, counts, findings }

CLI contract

misar-billing-guards [options]

  --advisory        Emit ::warning and exit 0 (this is the DEFAULT).
  --blocking        Make error findings fail the build (overrides advisory default).
  --only=<ids>      Run only these guard ids (comma-separated). Bypasses config.
  --json            JSON output ({ ran, counts, advisory, findings }). Still sets exit.
  --config=<path>   Config file (default: ./billing-guards.config.json).
  --list            List guard ids + default-on/opt-in and exit.
  -h, --help        Help.

Exit: 0 no findings (or advisory)   1 error findings (blocking)   2 usage/config error

Each finding is { guard, file, line, message, severity } (severity is "error" or "warning"), rendered as ::error file=<f>,line=<n>,title=misar-billing-guards/<id>::<message>.

The nine guards

id asserts
webhook-handler-coverage every event in the route's required-event manifest has a handler in the dispatch (eventHandlers map / switch (event.type)); recommended events warn (e.g. invoice.upcoming)
webhook-idempotency the dedup gate (check_and_record_webhook_event / insert into *webhook_events*) runs before handler dispatch; missing gate is an error
webhook-secret-assert the route reads its STRIPE_WEBHOOK_SECRET (+ Connect secret), returns non-2xx when absent, and declares the env name in .env.example
webhook-middleware-exemption webhook paths aren't SSO-redirected — misar-io /io/wallet/webhook stays OUTSIDE PROTECTED_PREFIXES; matcher-style middleware exempts the webhook namespace
stripe-apiversion-pinning a single pinned apiVersion across every new Stripe(...) site; flags drift and unpinned clients
plan-price-integrity every non-free plan slug declares a stripe_price_id (+ yearly when a yearly price exists)
email-per-event-coverage handled notifying events (trial-ending, payment-failed, canceled) have a reachable send*Email call — flags a webhook that sends none
no-direct-billing-table-writes user_subscriptions / subscription_plans / user_wallets / wallet_transactions are mutated only from an allowlisted surface
idempotency-fn-boolean-guard the idempotency SQL fn captures ROW_COUNT into an INTEGER variable (guards the assisters migration-257 boolean>integer regression)

All nine are default-on; each no-ops gracefully when the manifest it needs is absent (e.g. no billing.webhookRoutes ⇒ the coverage/idempotency/secret/email guards find nothing). stripe-apiversion-pinning and no-direct-billing-table-writes work with no manifest at all.

Config — billing-guards.config.json

{
  "advisory": true,                    // default true; set false to block
  "ignore": ["packages/billing-guards"],
  "billing": { /* per-repo manifest, see below */ },
  "guards": {
    "stripe-apiversion-pinning": { "expectedVersion": "2025-12-15.clover", "allowUnpinned": false },
    "no-direct-billing-table-writes": true
    // a guard listed true/{opts} is on; false is off; unlisted ⇒ default-on
  }
}

The billing block is the per-repo manifest shared across guards. Full, copy-pasteable per-repo variants (assisters / misar-io / MisarReach / MisarMail) live in billing-guards.config.example.json. Field reference:

"billing": {
  "envManifest": ".env.example",              // where secret env names must be declared
  "webhookRoutes": [
    {
      "path": "apps/web/src/app/api/webhooks/stripe/route.ts", // repo-relative route
      "secretEnv": "STRIPE_WEBHOOK_SECRET",
      "connectSecretEnv": "STRIPE_CONNECT_WEBHOOK_SECRET",     // optional (Connect)
      "dedupFn": "check_and_record_webhook_event",            // dedup gate name
      "requiredEvents": ["customer.subscription.updated", "..."], // error if unhandled
      "recommendedEvents": ["invoice.upcoming"],                  // warn if unhandled
      "notifyEvents": ["customer.subscription.trial_will_end", "invoice.payment_failed", "customer.subscription.deleted"],
      "handlerDir": "apps/web/src/app/api/webhooks/stripe/handlers", // extra dir scanned for send*Email
      "emailFnPattern": "send\\w*Email\\s*\\(",  // optional override
      "requireIdempotency": true,                // set false for a pure verify-only proxy
      "noEmail": true                            // waive email-per-event-coverage for this route
    }
  ],
  "middleware": {
    "path": "apps/web/src/middleware.ts",
    "protectedArrayName": "PROTECTED_PREFIXES",  // shape A (misar-io)
    "mustStayUnprotected": ["/io/wallet/webhook"],
    "matcherMustExempt": ["api", "webhooks", "unsubscribe"]  // shape B (matcher tokens)
  },
  "plansFiles": [
    { "path": "src/lib/billing/reach-plans.ts", "priceField": "stripe_price_id",
      "yearlyPriceField": "stripe_price_id_yearly", "yearlyMarkerField": "price_yearly",
      "freeSlugs": ["free", "enterprise"] }
  ],
  "billingTables": ["user_subscriptions", "subscription_plans", "user_wallets", "wallet_transactions"],
  "billingWriteAllowlist": ["webhook", "checkout", "start-trial", "upsert-subscription", "supabase/migrations", "scripts/seed"],
  "idempotencyFnNames": ["check_and_record_webhook_event"]
}

Waivers & false positives

  • Per-route email waiver: "noEmail": true on the route, or a // billing-guards:no-email comment in the route file.
  • Repo-wide skips: add path prefixes to the top-level ignore array.
  • stripe-apiversion-pinning treats a client pinned via an identifier ({ apiVersion: STRIPE_API_VERSION }) as pinned and consistent.

CI

Reusable + drop-in Forgejo workflow lives at .forgejo/workflows/billing-guards.yml (advisory pull_request + workflow_call). The nightly Stripe test-clock lifecycle suite is scaffolded at .forgejo/workflows/billing-e2e.yml. See docs/DEVOPS/billing-validation.md for the per-PR-vs-nightly split, honest limits, and the per-product event manifest.

Test

node tests/test.mjs   # fixture-based, temp-dir, 28 assertions, zero deps

Keywords

misar ci guards stripe billing subscription webhook idempotency
Details
npm
2026-07-21 09:08:05 +00:00
3
latest
20 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-07-21