reveclicat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format based on [Keep a Changelog](https://keepachangelog.com/); versions follow SemVer.
4
+
5
+ ## [Unreleased]
6
+
7
+ _Nothing yet._
8
+
9
+ ## [0.1.0] — 2026-08-29
10
+
11
+ First release. Unofficial project — not affiliated with RevenueCat, Inc.
12
+
13
+ ### Fixed
14
+ - Lifecycle schemas: `is_family_share` and `country_code` are nullable, `renewal_number`/`metadata` accept explicit `null` — real `CANCELLATION`/`EXPIRATION` events (store `PROMOTIONAL`, captured 2026-08-29) carry nulls where the docs say "Always". (T-064)
15
+ - `rcc listen`, `rcc tail` and `rcc inbox` no longer reject well-formed events of types outside the seven (`NON_RENEWING_PURCHASE`, future types): they are accepted (200), shown as `UNSUPPORTED <TYPE>`, stored and forwarded. New `classifyEnvelope()` / `UnknownEventSchema` in the public API. (T-065)
16
+ - `TEST` event schema: a real dashboard test event (captured 2026-08-29) carries `null` in every subscription-lifecycle field (`transaction_id`, `is_family_share`, prices, `renewal_number`, `metadata`…) and `store: PLAY_STORE`; the schema now accepts that and is no longer provisional. (T-004)
17
+
18
+ ### Added (receiving real webhooks)
19
+ - `rcc tail --smee [url]` — receive **real** RevenueCat webhooks on your machine through the public smee.io relay (zero setup, no persistence); validates and prints each event like `rcc listen`; `--forward <url>` re-POSTs body + original `Authorization` to a local handler; `--verbose`. (T-061)
20
+ - `rcc inbox` — self-hosted persistent webhook inbox (`--token`, `--auth-header`, `--port`, `--data-dir`, `--max-events`; env `INBOX_TOKEN`, `RC_WEBHOOK_AUTH`, `PORT`, `INBOX_DATA_DIR`) with `POST /webhook`, `GET /events`, `GET /events/stream` (SSE), `GET /health`; `rcc tail --inbox <url> --token <t> [--since <seq>|--all]` reads from it. `examples/inbox/` Dockerfile + Caddyfile. (T-062)
21
+
22
+ ### Added (generating & simulating)
23
+ - `rcc run` prints the header up front and each table row as soon as its event is delivered (visible with `--speed`). (T-066)
24
+ - `rcc send <EVENT_TYPE>` — send one schema-valid event; flags `--to`, `--store`, `--user`, `--product`, `--auth-header`, `--environment`, `--set key=value` (repeatable, dot paths), `--seed`, `--dry-run`. Runs a minimal coherent prelude so any of the 7 event types can be sent alone. (T-020)
25
+ - `rcc listen` — local receiver with `--port`, `--forward`, `--auth-header` (flags `AUTH MISMATCH`, 401), `--verbose`; validates envelopes against the official schemas. (T-021)
26
+ - `rcc run <scenario.yaml>` — executes YAML scenarios on a virtual clock; flags `--to`, `--auth-header`, `--speed instant|<ms>`, `--seed`, `--dry-run`, `--json`. Table + summary output; exit 1 on non-2xx or failed expectations. (T-031, T-040)
27
+ - Scenario format: `subscriber` (`app_user_id`, `product_id`, `period`, `trial`, `grace_period`, `store`, `environment`), `steps` of `event`/`advance` with optional `set` and `expect.response_status`, scenario-level `expect.all_responses_status` / `expect.max_response_ms`. Validation errors point to `file:line:column`. (T-030, T-040)
28
+ - Six example scenarios: `trial-converts`, `trial-churns`, `billing-issue-recovers`, `billing-issue-churns`, `cancel-then-uncancel`, `happy-year`. (T-032)
29
+ - Zod schemas for `TEST`, `INITIAL_PURCHASE`, `RENEWAL`, `CANCELLATION`, `UNCANCELLATION`, `BILLING_ISSUE`, `EXPIRATION` and the webhook envelope, derived from the official docs (see `docs/payload-sources.md`). (T-003)
30
+ - `examples/express-handler.ts` — minimal idempotent Express handler (auth check, schema validation, dedupe by `event.id`). (T-041)
31
+ - `examples/github-action.yml` — CI workflow that starts the handler and runs two scenarios with `--json`. (T-042)
32
+ - `rcc init` — creates `reveclicat.config.json` and `scenarios/` with the six examples; `--force` to overwrite. `send`/`run` read defaults (`to`, `authHeader`, `store`, `environment`) from the config file; flags win. (T-051)
33
+ - MIT license, CONTRIBUTING guide. (T-051)
34
+ - Consistent error output: every error prints `✖ message` + `→ hint`; usage errors (unknown command/option, missing argument) exit with code 2, other failures with 1; `RCC_DEBUG=1` shows stack traces; `NO_COLOR` honoured. (T-052)
35
+ - Programmatic API (`reveclicat` package): schemas, `Subscriber`, `runScenario`, `loadScenario`, `VirtualClock`, `createRng`.
36
+
37
+ [Unreleased]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.0...HEAD
38
+ [0.1.0]: https://github.com/RadW2020/ReveCliCat/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RadW2020
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ > **Unofficial project — not affiliated with RevenueCat, Inc.**, nor endorsed or supported by it. "RevenueCat" is a trademark of RevenueCat, Inc. ReveCliCat generates *synthetic* webhook events for testing; it never talks to RevenueCat's servers.
2
+
3
+ # ReveCliCat 🐱 `rcc`
4
+
5
+ [![ci](https://github.com/RadW2020/ReveCliCat/actions/workflows/ci.yml/badge.svg)](https://github.com/RadW2020/ReveCliCat/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/reveclicat)](https://www.npmjs.com/package/reveclicat) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **Deterministic time travel for subscriptions: simulate a year of a subscriber's life in 30 seconds, locally and in CI, without touching Apple's sandbox.**
8
+
9
+ ReveCliCat is a small TypeScript CLI that generates RevenueCat-shaped webhook events, chains them through a subscription state machine so they stay coherent (same IDs, forward-moving timestamps, fields that mutate the way they really do), and POSTs them to your endpoint — `localhost` included, no tunnel needed. Think of it as the Stripe CLI that RevenueCat does not have.
10
+
11
+ ![rcc run happy-year against rcc listen, then rcc send and rcc tail --smee](docs/demo.gif)
12
+
13
+ <!-- Regenerate with: npm run build && vhs docs/demo.tape -->
14
+
15
+ ```
16
+ $ rcc run scenarios/billing-issue-recovers.yaml
17
+ ▶ billing-issue-recovers — Trial converts, the first renewal charge fails, and payment recovers within the grace period
18
+ # event virtual time status latency
19
+ 1 INITIAL_PURCHASE 2025-01-01T00:00:00.000Z 200 26 ms
20
+ 2 RENEWAL 2025-01-08T00:00:00.000Z 200 3 ms
21
+ 3 BILLING_ISSUE 2025-02-08T00:00:00.000Z 200 2 ms
22
+ 4 RENEWAL 2025-02-11T00:00:00.000Z 200 1 ms
23
+ ✔ 4 events · 4 ok · 0 failed · virtual span 41d (2025-01-01 → 2025-02-11) · 2/2 expectations passed
24
+ ```
25
+
26
+ ## What it is
27
+
28
+ - **`rcc send <EVENT_TYPE>`** — fire one schema-valid event (`INITIAL_PURCHASE`, `RENEWAL`, `CANCELLATION`, `UNCANCELLATION`, `BILLING_ISSUE`, `EXPIRATION`, `TEST`) at any URL.
29
+ - **`rcc run <scenario.yaml>`** — play a whole subscriber lifecycle from a YAML file on a **virtual clock** (`advance: P1M`), with per-event expectations and a CI-friendly exit code / `--json` output.
30
+ - **`rcc listen`** — a local receiver that validates incoming events against the schemas and pretty-prints them (handy to see what your own server will get).
31
+ - **`rcc init`** — drop a config file and six ready-made scenarios into your project.
32
+
33
+ The binary is `rcc`; `purr` is an alias for the same binary. Payload schemas are derived from RevenueCat's official webhook documentation — every field and enum is traced in [`docs/payload-sources.md`](docs/payload-sources.md).
34
+
35
+ ## Why it exists
36
+
37
+ Testing RevenueCat webhooks today means either making real sandbox purchases (slow, flaky, tied to a device) or firing isolated events from the dashboard (no coherence between events). Neither lets you reproduce *"trial → charge fails → grace period → recovers → renews"* on your laptop, and neither fits in a CI pipeline.
38
+
39
+ ReveCliCat generates the events itself, so:
40
+
41
+ - **Coherence is guaranteed** — a state machine rejects impossible sequences (`RENEWAL` before `INITIAL_PURCHASE`, `EXPIRATION` before the period ends) and keeps `app_user_id`, `original_transaction_id`, `expiration_at_ms`, `period_type`… consistent across the sequence.
42
+ - **Time is simulated** — `advance: P1Y` costs nothing; a year of renewals takes milliseconds.
43
+ - **It is deterministic** — `--seed 42` gives byte-identical payloads on every run.
44
+ - **It runs anywhere** — `localhost`, Docker, GitHub Actions. No tunnel, no dashboard, no App Store.
45
+
46
+ ## Quickstart (60 seconds)
47
+
48
+ ```bash
49
+ npm i -g reveclicat # installs `rcc` (and `purr`)
50
+ cd my-backend
51
+ rcc init # → reveclicat.config.json + scenarios/*.yaml
52
+ rcc listen # terminal 1: a local receiver on http://localhost:8787/webhook
53
+ rcc run scenarios/trial-churns.yaml --to http://localhost:8787/webhook # terminal 2
54
+ ```
55
+
56
+ Point it at your real handler instead (`--to http://localhost:3000/webhook`, or set `to` in `reveclicat.config.json`) and you are testing your code, not ours. Requires Node.js ≥ 20.
57
+
58
+ ## Commands
59
+
60
+ | Command | What it does | Key flags |
61
+ |---------|--------------|-----------|
62
+ | `rcc send <EVENT_TYPE>` | POST one event. Runs the shortest legal history first (e.g. a purchase before a `RENEWAL`) and sends only the requested event. | `--to`, `--auth-header`, `--user`, `--product`, `--store`, `--environment`, `--set key=value` (repeatable, dot paths), `--seed`, `--dry-run` |
63
+ | `rcc run <scenario.yaml>` | Execute a scenario on a virtual clock and deliver every event. Exit 1 on any non-2xx or failed `expect`. | `--to`, `--auth-header`, `--speed instant\|<ms>`, `--seed`, `--dry-run`, `--json` |
64
+ | `rcc listen` | Local HTTP receiver: validates envelopes, checks the auth header, pretty-prints, optionally forwards. | `--port` (8787), `--auth-header`, `--forward <url>`, `--verbose` |
65
+ | `rcc init` | Create `reveclicat.config.json` and `scenarios/` with the six examples. | `--force` |
66
+ | `rcc tail` | Receive **real** RevenueCat webhooks on your machine through a relay and forward them to a local URL. | `--smee [url]` or `--inbox <url> --token <t>` (`--since <seq>`, `--all`), `--forward <url>`, `--verbose` |
67
+ | `rcc inbox` | Self-hosted, persistent webhook inbox that `rcc tail --inbox` reads from. | `--token`, `--auth-header`, `--port` (8788), `--data-dir`, `--max-events` |
68
+
69
+ Every command has `--help` with defaults and examples. Set `NO_COLOR=1` to disable colours; `RCC_DEBUG=1` prints stack traces.
70
+
71
+ ```bash
72
+ rcc send RENEWAL --to http://localhost:3000/webhook --auth-header "Bearer dev"
73
+ rcc send CANCELLATION --set cancel_reason=BILLING_ERROR --dry-run | jq .event
74
+ rcc run scenarios/happy-year.yaml --speed 250 # 13 events, one virtual year, a pause between each
75
+ rcc run scenarios/trial-churns.yaml --json > out.json
76
+ ```
77
+
78
+ ### Config file
79
+
80
+ `rcc init` writes `reveclicat.config.json`; `send` and `run` read it from the current directory. Precedence: **flag > config > built-in default**.
81
+
82
+ ```json
83
+ { "to": "http://localhost:3000/webhook", "authHeader": "Bearer dev", "store": "app_store", "environment": "SANDBOX" }
84
+ ```
85
+
86
+ ## Scenario format
87
+
88
+ ```yaml
89
+ name: trial-billing-issue-recovers
90
+ description: Trial converts, first renewal fails, recovers within grace period
91
+ subscriber: # all optional
92
+ app_user_id: auto # "auto" → $RCAnonymousID:… derived from the seed, or a fixed string
93
+ product_id: com.example.premium.monthly
94
+ period: P1M # ISO-8601 duration
95
+ trial: P1W # omit → no trial
96
+ grace_period: P16D # billing-retry window after BILLING_ISSUE
97
+ store: app_store # v0.1: app_store only
98
+ environment: SANDBOX # SANDBOX | PRODUCTION
99
+ steps:
100
+ - event: INITIAL_PURCHASE # starts the trial (period_type: TRIAL, price 0)
101
+ - advance: P1W
102
+ - event: RENEWAL # trial → paid (is_trial_conversion: true)
103
+ - advance: P1M
104
+ - event: BILLING_ISSUE # opens the grace period
105
+ - advance: P3D
106
+ - event: RENEWAL # recovery
107
+ set: # optional overrides, dot paths allowed
108
+ price: 4.99
109
+ expect:
110
+ response_status: 200
111
+ expect:
112
+ all_responses_status: 200
113
+ max_response_ms: 500
114
+ ```
115
+
116
+ Rules: a step is exactly one of `event` or `advance`; unknown keys are errors; validation errors point at `file:line:column`. Illegal transitions stop the run with the step number and the list of legal events. `EXPIRATION` is only allowed once the virtual clock has reached `expiration_at_ms` (or the end of the grace period) — the error tells you exactly how much to `advance`.
117
+
118
+ Shipped examples (`rcc init` copies them): `trial-converts`, `trial-churns`, `billing-issue-recovers`, `billing-issue-churns`, `cancel-then-uncancel`, `happy-year` (12 renewals).
119
+
120
+ ## CI
121
+
122
+ `rcc run … --json` prints one JSON document on stdout (`{ scenario, seed, events[], expectations[], ok }`) and exits **1** when any delivery is non-2xx or any `expect` fails — that is all a pipeline needs.
123
+
124
+ - [`examples/github-action.yml`](examples/github-action.yml) — a GitHub Actions job that starts a handler, waits for it, and runs two scenarios against it.
125
+ - [`examples/express-handler.ts`](examples/express-handler.ts) — a minimal, idempotent Express handler: checks the `Authorization` header, validates the envelope with the published schemas, dedupes by `event.id`, answers 200 fast. Run it with `PORT=3000 RC_WEBHOOK_AUTH="Bearer dev" npx tsx examples/express-handler.ts`.
126
+
127
+ ## Receive real webhooks (`rcc tail`)
128
+
129
+ Everything above generates synthetic events. When you want the *real* thing — the dashboard's test event, a sandbox purchase — RevenueCat needs a public HTTPS URL, and your laptop is not one. `rcc tail` gives you one through a relay and streams the events back:
130
+
131
+ ```bash
132
+ rcc tail --smee --forward http://localhost:3000/webhook
133
+ # ● Tailing https://smee.io/AbCdEf123456
134
+ # Paste this URL in RevenueCat → Integrations → Webhooks: https://smee.io/AbCdEf123456
135
+ # 10:42:07 real TEST $RCAnonymousID:… com.example.premium.monthly → 200 (14 ms)
136
+ ```
137
+
138
+ - `--smee` uses [smee.io](https://smee.io) (GitHub's public webhook relay): zero setup, no account. It keeps **no history** — events only arrive while `rcc tail` is running — and payloads transit a third-party service, so use it for development, not production. Pass an existing channel (`--smee https://smee.io/…`) to keep the URL you already configured in the dashboard.
139
+ - Receivers (`listen`, `tail`, `inbox`) accept **every** RevenueCat event type: the seven ReveCliCat can *generate* are validated in full, anything else (e.g. `NON_RENEWING_PURCHASE`, future types) is accepted and shown as `UNSUPPORTED <TYPE>`. Tip: set the dashboard webhook to "Both Production and Sandbox" — promotional grants made through the API are `PRODUCTION` events.
140
+ - Each event is validated against the schemas (`INVALID` lines tell you what is off), printed in the same format as `rcc listen`, and, with `--forward`, re-POSTed to your local handler with the original `Authorization` header — the same workflow as the Stripe CLI's `listen` command forwarding to localhost.
141
+ - Need history, retries and your own infrastructure? Run the **self-hosted inbox**:
142
+
143
+ ```bash
144
+ # on your server (behind HTTPS — see examples/inbox/Dockerfile and Caddyfile)
145
+ rcc inbox --token s3cret --auth-header "Bearer from-dashboard" # RevenueCat → POST https://hooks.example.com/webhook
146
+ # on your laptop
147
+ rcc tail --inbox https://hooks.example.com --token s3cret --all --forward http://localhost:3000/webhook
148
+ ```
149
+
150
+ The inbox stores every delivery as JSONL (including RevenueCat's retries, linked by `event.id`), answers 200 to valid events so RevenueCat stops retrying, flags `AUTH MISMATCH`/`INVALID`, and exposes `GET /events`, `GET /events/stream` (SSE) and `GET /health`. Nothing leaves your infrastructure — see `docs/adr/ADR-004-self-hosted-inbox-no-data-custody.md`.
151
+
152
+ ## Authorization
153
+
154
+ RevenueCat authenticates webhooks with a **plain, static `Authorization` header** whose value you choose in the dashboard — there is no payload signature to verify. ReveCliCat mirrors that: `--auth-header "Bearer dev"` is sent verbatim, and `rcc listen --auth-header …` flags mismatches in red and answers 401. Your handler should compare the header with a constant-time equality check and treat anything else as unauthorized. (RevenueCat also offers an *opt-in* HMAC header, `X-RevenueCat-Webhook-Signature`; it is out of scope for v0.1.)
155
+
156
+ ## State machine
157
+
158
+ ```
159
+ none ──INITIAL_PURCHASE──▶ trial ──RENEWAL (conversion)──▶ active ◀─┐
160
+ │ │ │ │ │ RENEWAL
161
+ └──INITIAL_PURCHASE (no trial)─────────────────────────────┘ │ │
162
+ │ CANCELLATION / BILLING_ISSUE / EXPIRATION (same from active)
163
+
164
+ cancelled_pending_expiration ──UNCANCELLATION──▶ back to trial | active
165
+ │ EXPIRATION billing_issue ──RENEWAL (recovery)──▶ active
166
+ ▼ │ EXPIRATION (BILLING_ERROR) / CANCELLATION
167
+ expired ◀───────────────────────────────┘
168
+ │ INITIAL_PURCHASE (resubscribe, no trial)
169
+ └──────────────────────────────────────▶ active
170
+ ```
171
+
172
+ `TEST` is legal from any state and changes nothing. Full tables: [`specs/F1-state-machine.md`](specs/F1-state-machine.md).
173
+
174
+ ## Fidelity & scope
175
+
176
+ - Schemas, enums and inclusion rules come from the official docs (fetched 2026-08-29) and the official sample payloads are used as test fixtures. The `TEST` event has no published sample, so its schema is marked *provisional* — a captured real one is very welcome (see `docs/BACKLOG.md`, T-004).
177
+ - v0.1 models the **App Store** only. Google Play, Stripe, Amazon and Roku stores, a built-in tunnel, a web UI and hosted mode are intentionally out of scope (see the Icebox in `docs/BACKLOG.md`).
178
+ - Programmatic use: `import { runScenario, Subscriber, WebhookEnvelopeSchema } from "reveclicat"`.
179
+
180
+ ## Contributing
181
+
182
+ Spec-driven, tests-first, one ticket per commit — see [`CONTRIBUTING.md`](CONTRIBUTING.md), the backlog in `docs/BACKLOG.md` and the decision log in `docs/adr/`.
183
+
184
+ ## License
185
+
186
+ MIT © 2026 RadW2020. Not affiliated with RevenueCat, Inc.