@funnelchat-hq/sandbox 0.3.3 → 0.5.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A local server that imitates the Funnelchat public API and sends signed webhooks, with no real effects: nothing goes out to WhatsApp, nothing is charged, everything stays in memory. Build and test your integration without an account.
4
4
 
5
- It imitates production on 06-11: accounts v1.5.97, conversations v1.19.37, communities v2.35.7, platform-gate v0.9.1 and the API edge v0.3.0.
5
+ It comes from the spec the developer portal publishes, v0.6.14: the same operations and events (see [Pending of the spec](#pending-of-the-spec)). What it simulates with state follows production as it was checked on 06-11.
6
6
 
7
7
  ## Try it in 3 minutes, without an account
8
8
 
@@ -115,6 +115,7 @@ What it imitates, beyond the first use:
115
115
  - Webhooks signed with the portal's exact algorithm, with its retries, and the event catalog of production.
116
116
  - The WhatsApp rules the API applies: the 24-hour window on official numbers, the per-number pace, the spam brakes and the MAC lockout.
117
117
  - `X-Request-Id` on every response, like the API.
118
+ - Every other operation of the spec, **shape only**: validated like the rest, it answers the spec's success status with a body that fits the response schema, keeps nothing and emits nothing, and says so in `X-Sandbox-Simulated: shape-only`. See [With state or shape only](#with-state-or-shape-only).
118
119
 
119
120
  What it does not imitate is in [What it does not imitate](#what-it-does-not-imitate).
120
121
 
@@ -128,7 +129,7 @@ npx funnelchat-sandbox
128
129
  ```
129
130
 
130
131
  ```
131
- funnelchat-sandbox 0.3.3 at http://127.0.0.1:4010
132
+ funnelchat-sandbox 0.5.0 at http://127.0.0.1:4010
132
133
  API: http://127.0.0.1:4010/api/v1/... (Authorization: Bearer 1|fc_live_sandbox, or any well-formed key)
133
134
  panel: http://127.0.0.1:4010/_sandbox
134
135
  example numbers: 3 (official, with the 24-hour window) and 4 (QR, no window); contact 2001; template 45
@@ -148,12 +149,12 @@ npx funnelchat-sandbox
148
149
 
149
150
  ```bash
150
151
  docker build -t funnelchat-sandbox .
151
- docker run --rm -p 4010:4010 funnelchat-sandbox
152
+ docker run --rm -p 127.0.0.1:4010:4010 funnelchat-sandbox
152
153
  # with your configuration:
153
- docker run --rm -p 4010:4010 -v "$PWD/funnelchat-sandbox.config.json:/app/funnelchat-sandbox.config.json:ro" funnelchat-sandbox
154
+ docker run --rm -p 127.0.0.1:4010:4010 -v "$PWD/funnelchat-sandbox.config.json:/app/funnelchat-sandbox.config.json:ro" funnelchat-sandbox
154
155
  ```
155
156
 
156
- Inside the container it listens on `0.0.0.0`. If your webhook receiver runs on your machine, from the container it is `http://host.docker.internal:<port>` (on Linux, add `--add-host=host.docker.internal:host-gateway`).
157
+ Inside the container it listens on `0.0.0.0`; `127.0.0.1:` publishes the port only on your machine (with `-p 4010:4010` the whole network reaches the sandbox, which takes no key on `/_sandbox`). If your webhook receiver runs on your machine, from the container it is `http://host.docker.internal:<port>` (on Linux, add `--add-host=host.docker.internal:host-gateway`).
157
158
 
158
159
  **From your tests**, with no other process:
159
160
 
@@ -273,13 +274,9 @@ Every session starts with this:
273
274
 
274
275
  The official number (`3`) has no groups (the sandbox account is not in the official groups beta); the QR one (`4`) does, and has no templates. `health` is `null` on the QR number.
275
276
 
276
- **Polls and events** (`/message/poll`, `/message/event`) are closed to API keys in production: with any key they answer `403 route_not_available`, as they do here.
277
+ **Polls and events** (`/message/poll`, `/message/event`) are closed to API keys in production (`403 route_not_available`) and the published spec does not carry them, so here they are not routes: `404 route_not_found`.
277
278
 
278
- ```json
279
- { "error": "route_not_available", "message": "Esta ruta no está disponible para API keys en el piloto. No es un permiso que falte: ninguna key la habilita." }
280
- ```
281
-
282
- **Everything else** in the spec (broadcasts, flows, kanban, the rest of groups, campaigns, alerts…) answers an example built from the response schema: the spec's operations carry no examples of their own. The key, the route, the query and the body are still validated.
279
+ **Everything else** in the spec (broadcasts, flows, kanban boards, contact notes, the rest of groups, campaigns, alerts, the account…) answers **shape only**: see [With state or shape only](#with-state-or-shape-only).
283
280
 
284
281
  **Validation** follows the spec and reads values like Laravel (`"5"` works as an integer, `"1"` as a boolean). A validation error is a `422` with Laravel's messages per field; a route that does not exist, `404 route_not_found`:
285
282
 
@@ -298,6 +295,35 @@ The official number (`3`) has no groups (the sandbox account is not in the offic
298
295
 
299
296
  The complete spec the sandbox uses is at `GET /_sandbox/openapi.json`.
300
297
 
298
+ ### With state or shape only
299
+
300
+ The sandbox answers every operation of that spec, in one of two ways:
301
+
302
+ - **With state**, the first use: numbers and sign-up links, contacts with their tags and fields, conversations and messages, groups (the live list, the list, one group, the import and the send), WhatsApp forms, mark as read and «typing…», and webhook subscriptions. What you create stays (in memory) and the events go out, signed.
303
+ - **Shape only**, everything else. The request is validated like any other (the key, the route, the path, the query and the body, with the same `401`, `404` and `422`), and the answer is the success status the spec gives (`200`, `201`, `202` or `204`) with a body that fits its response schema: the spec's example of the response when it has one that fits, otherwise one built from the schema. It is the same on every call to that operation, whatever you send. Nothing is stored and no event goes out: creating a board answers `201` with a board, and the list of boards does not have it.
304
+
305
+ A shape-only answer carries `X-Sandbox-Simulated: shape-only`; the body stays as the schema says. Nothing else carries that header.
306
+
307
+ ```bash
308
+ curl -si -X POST http://localhost:4010/api/v1/kanban-settings -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
309
+ -H 'Content-Type: application/json' -d '{"name": "Ventas", "mode": "stages"}'
310
+ # HTTP/1.1 201 Created
311
+ # X-Sandbox-Simulated: shape-only
312
+ # {"id": 1, "user_id": 1, "name": "Ejemplo", "mode": "ejemplo", …}
313
+ ```
314
+
315
+ **To see which is which**, `GET /_sandbox/operations` lists every operation of the spec with `simulated`: `stateful`, `shape-only`, or `closed` (with `--no-group-actions`, the send to a group and its action, which answer `403 route_not_available`). A shape-only one also says its `status`, where its body comes from (`body_from`: `example`, `schema` or `none`) and whether it fits the schema (`body_fits_schema`). The list comes from the spec and the sandbox's handlers, so it follows the spec when it is regenerated.
316
+
317
+ ```bash
318
+ curl -s http://localhost:4010/_sandbox/operations | jq '.meta'
319
+ # {"total": 253, "stateful": 88, "shape_only": 165, "closed": 0}
320
+ curl -s http://localhost:4010/_sandbox/operations | jq -r '.data[] | select(.simulated == "shape-only") | "\(.method) \(.path)"'
321
+ ```
322
+
323
+ The panel (`/_sandbox`) counts them and marks each shape-only call of **Last requests** as **shape only**; `GET /_sandbox/requests` says `"simulated": "shape-only"` on those.
324
+
325
+ **How the body is built** when the spec has no example (the current one has none): for each schema, its `example`, `const`, `default` or first `enum` value when it fits; otherwise a plausible value of its type within the schema's limits (date, email, URL and UUID formats, lengths, bounds, number of items, `pattern`). `anyOf` and `oneOf` take the first branch whose value fits, and a list of the same schema it is inside of (a folder's folders, an action's sub-actions) stays empty. If a body ever did not fit (a schema asking for something the builder cannot make), the console says which operation, once, and `body_fits_schema` is `false`.
326
+
301
327
  ## Sending a message
302
328
 
303
329
  **By phone, without `contact_id`** (conversations v1.19.21): `POST /api/v1/message/send-message` and `POST /api/v1/devices/{device}/templates/{template}/send-template` take `phone` instead of `contact_id`. The contact of that phone is found, or created in the key's workspace (with the `422`s of `update-or-create` for a phone that is not one), and the answer's `message.contact_id` says which. The contact stays created even if a rule (the window, the brakes, the MAC, a disconnected number) then stops the send. A phone of a contact of another workspace answers `404` and creates nothing. `whatsapp_id` (Meta's BSUID, `US.13491208655302741918`) works instead of `phone` only with an official number; with a QR number, together with `phone` or without the shape of a BSUID, it is a `422` on `whatsapp_id`. With `contact_id`, a `phone` or `whatsapp_id` in the body must be that contact's: `422 recipient_mismatch`, on `send-message` too.
@@ -554,10 +580,9 @@ You subscribe with the same routes the SDKs and the n8n trigger use (the unified
554
580
  | `GET /api/v1/webhooks`, `GET /api/v1/webhooks/{id}` | Lists or shows them, without the secret. |
555
581
  | `PUT /api/v1/webhooks/{id}` | Changes `url`, `events`, `description`, `filters` and, optionally, `status` (`active` or `paused`). The secret does not change. |
556
582
  | `DELETE /api/v1/webhooks/{id}` | Deletes it (`204`). |
557
- | `POST /api/v1/webhooks/{id}/rotate-secret` | A new secret; the previous one stops working. |
558
583
  | `POST /api/v1/webhooks/{id}/test` | Sends a signed `webhook.test` now and answers `{succeeded, response_status, error, duration_ms, delivery_id}`. |
559
584
  | `GET /api/v1/webhooks/{id}/deliveries` | The history: one record per attempt, newest first. |
560
- | `GET /api/v1/webhooks/events` | The catalog of accounts in production: 59 types (54 `active`), with `area`, `status`, `is_critical`, `label`, `description` and `example`. |
585
+ | `GET /api/v1/webhooks/events` | The event catalog of the spec: the 55 types of the `events` enum of `POST /api/v1/webhooks` and `webhook.test` (not subscribable), all `active`, each with `area`, `status`, `description`, `subscribable` and `example`. |
561
586
 
562
587
  **Each delivery** is a `POST` with the portal's envelope and these headers:
563
588
 
@@ -607,15 +632,16 @@ These routes take no key: they belong to the sandbox, not to the API.
607
632
  | `POST /_sandbox/devices/{id}/health` | The health of an official number changes: `{"state": "limited"}` (`available`, `limited`, `blocked`, `unknown`) emits `device.health_changed`, only if it changed. |
608
633
  | `POST /_sandbox/numbers`, `GET /_sandbox/numbers` | The quota of the account: `{"limit": 3, "included": true, "trial": false, "meta_connect": true, "links_per_minute": 10}` (`meta_connect` turns the Meta sign-up on or off). `GET` also says how many places are in use and which QR numbers are pending. |
609
634
  | `POST /_sandbox/templates/{id}/status` | Meta approves or rejects a template: `{"status": "REJECTED", "reason": "…"}` emits `template.status_changed`. |
610
- | `POST /_sandbox/events/{type}` | Any event of the catalog, with its example; what you send in `{"data": {…}}` overrides it, and `{"workspace_id": 2}` sends it to that workspace (the default one otherwise). `GET /_sandbox/events` lists them. `group.message_received` and `group.reaction_added` go out with the shape communities emits (`message.uid` and every key of the message), not with the example of the accounts catalog, which uses `message.id`. |
635
+ | `POST /_sandbox/events/{type}` | Any event of the catalog, with its example; what you send in `{"data": {…}}` overrides it, and `{"workspace_id": 2}` sends it to that workspace (the default one otherwise). `GET /_sandbox/events` lists them. The example is the one of the catalog, the spec's. |
611
636
  | `POST /_sandbox/window` | Opens or closes the window by hand: `{"contact_id": 2001, "device_id": 3, "open": false}`. |
612
637
  | `POST /_sandbox/workspaces`, `GET /_sandbox/workspaces` | A [workspace](#workspaces) with a key that only sees it: `{"name": "Cliente B"}` answers `201 {workspace, key}`. `POST /_sandbox/keys` with `{"workspace_id": 2}` mints another key (of the default workspace without `workspace_id`); with `"permissions": [...]` it has only those (the sandbox checks just `communities:operate`, to import a group). |
613
638
  | `POST /_sandbox/reset` | Back to the initial state. |
614
- | `GET /_sandbox/state`, `/_sandbox/requests`, `/_sandbox/deliveries`, `/_sandbox/emitted` | The state, the last requests, the last deliveries with what your server answered, and every event emitted, delivered or not (with how many subscriptions got it). |
639
+ | `GET /_sandbox/state`, `/_sandbox/requests`, `/_sandbox/deliveries`, `/_sandbox/emitted` | The state, the last requests (`simulated: "shape-only"` on the ones answered [shape only](#with-state-or-shape-only)), the last deliveries with what your server answered, and every event emitted, delivered or not (with how many subscriptions got it). |
640
+ | `GET /_sandbox/operations` | Every operation of the spec and how the sandbox answers it: `stateful`, `shape-only` or `closed`. See [With state or shape only](#with-state-or-shape-only). |
615
641
 
616
642
  Besides, as in the API: creating a contact emits `contact.created`, closing a conversation (`active_conversation: false`) emits `conversation.closed`, assigning it emits `conversation.assigned`, and a template created through the API goes from `PENDING` to `APPROVED` after 5 seconds with `template.status_changed`.
617
643
 
618
- **The panel** is at `http://localhost:4010/_sandbox`: the numbers (with **Scan the QR** for one waiting for it), every event emitted, the last requests, the webhook deliveries with your server's answer, and a form to send an incoming message.
644
+ **The panel** is at `http://localhost:4010/_sandbox`: the numbers (with **Scan the QR** for one waiting for it), every event emitted, the last requests (the ones answered shape only, marked **shape only**), the webhook deliveries with your server's answer, and a form to send an incoming message.
619
645
 
620
646
  ## End to end
621
647
 
@@ -689,37 +715,36 @@ And at `http://localhost:4010/_sandbox` you see each request and each delivery w
689
715
  ## What it does not imitate
690
716
 
691
717
  - **It sends nothing to WhatsApp** and charges nothing: the `sent`, `delivered` and `read` statuses are made up by the sandbox, with the delays you configure. In the real API, a message still `queued` after an hour reads as `status: null`; in the sandbox it always moves on.
692
- - **One account and every permission:** any well-formed key gets in, is never revoked and there is no `403 permission_denied`. Workspaces are created and their keys minted through `/_sandbox`, never deleted, and a number does not move between workspaces. API keys are not created here, and subscriptions are not filtered by the key's areas (the real API answers `422` on `events` if the key lacks the event's `{area}:read`). The only `403 route_not_available` is the one of the routes no key enables.
718
+ - **One account and every permission:** any well-formed key gets in, is never revoked and there is no `403 permission_denied`. Workspaces are created and their keys minted through `/_sandbox`, never deleted, and a number does not move between workspaces. API keys are not created here, and subscriptions are not filtered by the key's areas (the real API answers `422` on `events` if the key lacks the event's `{area}:read`). The only `403 route_not_available` is the send to a group with `--no-group-actions`.
693
719
  - **There are no daily call caps** (`429 daily_cap_exceeded`), no `503 gate_unavailable` and no balance: to test those cases, force the failure. Of the MAC there is the lockout (`429 mac_lockout_24h`); `429 mac_limit_reached` is only forced, and the real API also stores in the history an error row for every send stopped by the MAC, which the sandbox does not.
694
720
  - **The state is memory only:** it is lost when the process stops or with `POST /_sandbox/reset`. So your receiver does not lose the secret, put the subscription in the configuration file.
695
- - **The routes without state** answer an example built from the schema, always the same, even if it does not match your data.
721
+ - **The shape-only routes** answer the shape of the spec, always the same: it does not match your data, a write keeps nothing and no event goes out. See [With state or shape only](#with-state-or-shape-only).
696
722
  - **The validation messages** are Laravel's in Spanish, built from the schema: they may not match the API's word for word (decide by the HTTP status and by `error`, not by `message`). The real API's validation `422` is Laravel's, `{message, errors}`, without `error`; the sandbox adds `"error": "validation_failed"`.
697
723
  - **Webhook URLs** may be `http` and local, without the real API's protection against internal destinations (try `--require-https`). Do not expose the sandbox to the internet: the `/_sandbox` routes take no key and the sandbox `POST`s to any URL subscribed to it.
698
- - **The catalog** is rendered from the code of accounts v1.5.97 (`spec/catalog.json`, with `scripts/transcribe-catalog.php`), not read from a response: if accounts changes it, it has to be rendered again.
699
- - **Groups have state only for the number's live list, the list, one group, the import and the send of a message** (the action is `completed` after the delays of `sent` and `delivered`; it never fails on its own). The live list is never cached (production keeps it a minute for a key). The other group actions, the campaigns and the rest of the groups' routes answer examples, and their events are fired from `/_sandbox`. There are no official groups (the account is not in their beta), no `too_many_pending_actions` and no rate limit of 30 sends per minute.
724
+ - **The event catalog** is what the spec documents (`generated/catalog.json`, transcribed by `npm run generate`): fields of production's catalog the spec does not document, such as `is_critical` and `label`, are not there.
725
+ - **Groups have state only for the number's live list, the list, one group, the import and the send of a message** (the action is `completed` after the delays of `sent` and `delivered`; it never fails on its own). The live list is never cached (production keeps it a minute for a key). The other group actions, the campaigns and the rest of the groups' routes answer shape only, and their events are fired from `/_sandbox`. There are no official groups (the account is not in their beta), no `too_many_pending_actions` and no rate limit of 30 sends per minute.
700
726
  - **Workspaces confine** numbers, sign-up links, webhook subscriptions, contacts, conversations, messages, templates and groups. Tags, custom fields and forms are not confined in the sandbox. A contact belongs to one workspace, and the phone stays unique in the account: `POST /api/v1/contacts`, `update-or-create` and a send by phone with a phone of another workspace answer `404`.
701
- - **Forms do not talk to Meta:** `sync` and `publish` succeed unless the linter finds an error, and the linter is a short version of the real one (screens without a title, without an ending, without questions, with a connection to the server). The metrics (`…/metrics`) answer the example of their schema, and there are no dynamic forms.
727
+ - **Forms do not talk to Meta:** `sync` and `publish` succeed unless the linter finds an error, and the linter is a short version of the real one (screens without a title, without an ending, without questions, with a connection to the server). The metrics (`…/metrics`) answer shape only, and there are no dynamic forms.
702
728
  - **Files are not downloaded:** the type comes from the extension or from `file_type`. The «known» extensions (the ones that give `415` instead of `422 errors.file_type`) are the common ones; the API uses symfony/mime's full list.
703
729
  - **Connecting numbers** follows a contract, not production: no real QR, no Meta Embedded Signup and no 15-minute hold of a pool number. The page has no `POST /connect/{token}/qr` (the button connects at once) and no `POST /connect/{token}/offer` (the pool number is given when the link completes). A QR link that fails does not create (and delete) a number, links do not die when a key is revoked, the per-minute limit counts only the calls that pass the schema, and `409 operation_in_progress` only appears if two calls with the same `Idempotency-Key` overlap. The errors that depend on the plan, the payment or the provider (`connect_not_allowed`, `number_suspended_by_plan`, `payment_required`, `device_inactive`, `provider_error`…) are documented but never answered.
704
- - **Template metrics and the WhatsApp Business accounts** (`GET /api/v1/templates/{template}/metrics`, `GET /api/v1/me/whatsapp-business-accounts…`) answer the example of their schema, not data of the sandbox. **Mark as read and «typing…»** have no per-second ceilings (`429 rate_limited`) and never fail at WhatsApp (`502 provider_error`).
705
- - **`POST /api/v1/webhooks/{id}/rotate-secret` works with a key here**, as it always did in the sandbox; production keeps it out of the keys' routes (the secret is rotated from the panel), so a key gets `403 route_not_available` there.
730
+ - **Template metrics and the WhatsApp Business accounts** (`GET /api/v1/templates/{template}/metrics`, `GET /api/v1/me/whatsapp-business-accounts…`) answer shape only, not data of the sandbox. **Mark as read and «typing…»** have no per-second ceilings (`429 rate_limited`) and never fail at WhatsApp (`502 provider_error`).
731
+ - **The secret of a subscription is rotated from the panel:** `POST /api/v1/webhooks/{id}/rotate-secret` is not in the published spec, so here it is `404 route_not_found` (a key gets `403 route_not_available` in production).
706
732
  - **Forms** are served here, but the API edge does not route `forms` yet: through the public URL they answer `404 route_not_found`.
707
733
  - **The pace and the brakes** use the numbers of conversations in production (`config/send_guards.php`, `config/antiblock.php`); the official number paired with a QR one in coexistence (15 per second, shared) is not there.
708
734
 
709
735
  ## Pending of the spec
710
736
 
711
- The spec (`spec/funnelchat.json`) is the portal's full unified spec of 06-11 (developers-portal `feat/plantillas-de-meta` @ `51c0aeb`, v0.6.7), made from accounts v1.5.97, conversations v1.19.37 and communities v2.35.7 (with the send to a group and the import): every route an API key reaches, not only the ones the portal shows. What production has and that spec lacks is in two separate layers, which the next regeneration drops on its own once the spec carries it:
737
+ The spec (`spec/funnelchat.json`) is the one the portal publishes, v0.6.14 (https://developers-portal.funnelchat.app/specs/funnelchat.json, 13-11): only the operations and events the portal shows. **The sandbox offers only that:** a layer corrects shapes where the spec says otherwise than production answers, but never adds an operation or an event (the generator refuses it and says so). The corrections are in two layers, which the next regeneration drops on its own once the spec carries them:
712
738
 
713
- - `spec/pending.json`: the pending layer of the Node SDK, copied unchanged (`funnelchat-node` `feat/enviar-por-telefono` @ `a8f8350`). It brings what the spec lacks (the forms, `rotate-secret`, the accounts and communities events) and, guarded by JSON Pointer conditions, corrections where the spec says otherwise than production answers (the send answers, `devices/{device}/qr`, the template list, `PUT webhooks/{id}`, `GET groups/actions/{action}`, and `message.received`, `message.status_changed` and `whatsapp_form.completed`).
714
- - `spec/pending-sandbox.json`: what the sandbox adds, with its sources inside.
739
+ - `spec/pending.json`: the pending layer of the Node SDK (`funnelchat-node` @ `bde8679`), with the corrections of 13-11 made here ahead of the SDK: `answers[].label` of `whatsapp_form.completed` is always a string, `message.type` of `message.received` accepts types the spec does not list yet, and its sources say what portal v0.6.14 shows (its note in `spec/procedencia.json` lists them). Guarded by JSON Pointer conditions: the send answers, `devices/{device}/qr`, the template list, `PUT webhooks/{id}`, `GET groups/actions/{action}`, and the shapes of the number, group, billing, form and message events.
740
+ - `spec/pending-sandbox.json`: what the sandbox corrects on top of it, with its sources inside.
715
741
 
716
- Besides, `spec/catalog.json` is the event catalog of accounts v1.5.97, with its provenance.
742
+ The event catalog (`generated/catalog.json`) comes out of the merged spec, with the same command: see [Regenerating](#regenerating).
717
743
 
718
- What `spec/pending-sandbox.json` still adds:
744
+ What `spec/pending-sandbox.json` still corrects:
719
745
 
720
746
  | What | Why neither the spec nor the Node SDK's layer has it |
721
747
  |---|---|
722
- | Polls and events, `POST /api/v1/contacts/{contact}/message/poll` and `…/event` | Closed to every key by the gate: they are kept to answer its `403 route_not_available`, not a `404` |
723
748
  | `data.workspace_id` in every event (accounts v1.5.97, Q35) | The events of the spec do not carry it yet |
724
749
  | `null` in `allowed_connection_types` and `provision_us_number` of `POST /api/v1/connect-links` | A correction: accounts' rules depend on `type`, and Laravel's `prohibited` lets `null` through |
725
750
 
@@ -728,13 +753,15 @@ What `spec/pending-sandbox.json` still adds:
728
753
  Everything generated comes from a single script:
729
754
 
730
755
  ```bash
731
- # With a new full spec of the portal (`npm run build` in developers-portal writes generated/specs/funnelchat-completa.json;
732
- # never the published public/specs/funnelchat.json, which carries only the visible operations). It is a build
733
- # artifact, so --origin says where it comes from (repository, branch, commit, path and the tag of each domain):
734
- npm run generate -- --spec ../developers-portal/generated/specs/funnelchat-completa.json --origin origin.json
756
+ # With a new spec of the portal: always the one it publishes, only the operations and events the portal shows (the
757
+ # tools are generated only from what the portal publishes). Download it, and say with --origin which version of the
758
+ # portal it is, for example {"url": "https://developers-portal.funnelchat.app/specs/funnelchat.json",
759
+ # "portal_version": "v0.6.14"}; it is the only place the version is written, and spec/procedencia.json keeps it:
760
+ curl -sO https://developers-portal.funnelchat.app/specs/funnelchat.json
761
+ npm run generate -- --spec funnelchat.json --origin <that JSON>
735
762
 
736
- # With a new pending layer of the Node SDK:
737
- npm run generate -- --pending ../funnelchat-node/spec/pending.json
763
+ # With a new pending layer of the Node SDK (its spec/pending.json):
764
+ npm run generate -- --pending <the SDK's spec/pending.json>
738
765
 
739
766
  # With what is already in spec/ (for example, after touching spec/pending-sandbox.json):
740
767
  npm run generate
@@ -743,15 +770,18 @@ npm run generate
743
770
  npm run generate:check
744
771
  ```
745
772
 
746
- The event catalog (`spec/catalog.json`) is rendered from accounts' code with `scripts/transcribe-catalog.php` (PHP 8.2 or later, no Laravel needed); its header says how.
773
+ The event catalog comes out of the same command, so it is never transcribed by hand: the types are the `events` enum of `POST /api/v1/webhooks`, in its order, and each entry is what the spec documents for the type in `webhooks`, its example always the whole envelope (a correction that brings only `data` takes `id`, `version` and `occurred_at` from the portal's example); `webhook.test`, which only the test route sends, goes last and is not subscribable. A type of the enum that `webhooks` does not document stops the generation.
747
774
 
748
775
  What it does:
749
776
 
750
777
  1. With `--spec` or `--pending`, it copies the file to `spec/` and writes in `spec/procedencia.json` the repository, the branch, the commit, the date and the `sha256` (from git, or from `--origin` for what git cannot say).
751
- 2. It applies over the snapshot `spec/pending.json` and then `spec/pending-sandbox.json`, but **only what the snapshot lacks**. At the end it lists what the snapshot already carries, so you can delete it.
778
+ 2. It applies over the snapshot `spec/pending.json` and then `spec/pending-sandbox.json`, but **only what the snapshot lacks**, and never an operation or an event the snapshot does not carry: it refuses those and lists them. At the end it lists what the snapshot already carries, so you can delete it. An event a layer types without an example keeps the portal's example.
752
779
  3. It writes `generated/openapi.json`, the spec the sandbox validates and answers with.
780
+ 4. It writes `generated/catalog.json`, the event catalog, from that spec.
753
781
 
754
- **The current spec** is the portal's full unified spec of 06-11 (`feat/plantillas-de-meta` @ `51c0aeb`, `generated/specs/funnelchat-completa.json`): 219 paths, 266 operations and 29 events of accounts v1.5.97, conversations v1.19.37 (`a4d0136be`) and communities v2.35.7. The detail is in `spec/procedencia.json`.
782
+ **A handler whose operation is not in the spec is not reached:** the route answers `404 route_not_found`, like a route the API does not have. After a regeneration, `GET /_sandbox/operations` says what has state and what answers [shape only](#with-state-or-shape-only).
783
+
784
+ **The current spec** is the one the portal publishes, v0.6.14, at https://developers-portal.funnelchat.app/specs/funnelchat.json (sha256 `37c54a5aad4cc467e088af50294012484692ef668714dcdd748babc7314b587f`, as served on 13-11): 209 paths, 253 operations and 57 events (56 types: the 55 of the `events` enum and `webhook.test`, which two areas document), in 30 groups: 1 of «Primer uso», 9 of «Funciones de WhatsApp» and 20 of «Funciones de Funnelchat». The detail is in `spec/procedencia.json`.
755
785
 
756
786
  ## Development
757
787
 
@@ -760,18 +790,18 @@ npm install
760
790
  npm run generate:check # generated/ is up to date
761
791
  npm run typecheck # tsc
762
792
  npm run lint
763
- npm test # vitest, no network: the receivers are local servers
793
+ npm test # vitest (Node 20.19 or later), no network: the receivers are local servers
764
794
  npm run build # dist/ in ESM, with .d.ts
765
795
  ```
766
796
 
767
797
  The tests verify the deliveries with unchanged copies of the Node SDK's `verify` and of the portal's snippet (`test/vendor/`, with their provenance). If the `funnelchat-node` repo is next to this one (or in `FUNNELCHAT_NODE_SDK`), they also run against its current code, including the whole client pointed at the sandbox.
768
798
 
769
799
  ```
770
- spec/ the spec, its provenance, the two pending layers and the event catalog
771
- scripts/ generate.mjs, the only generator, and transcribe-catalog.php for the catalog
772
- generated/ the merged spec (not edited)
773
- src/openapi/ routes, validation and examples from the spec
774
- src/handlers/ the routes with state (numbers, links, contacts, messages, forms, groups, webhooks) and the ones the gate closes
800
+ spec/ the spec, its provenance and the two pending layers
801
+ scripts/ generate.mjs, the only generator
802
+ generated/ the merged spec and the event catalog (not edited)
803
+ src/openapi/ routes, validation and the shape-only answers, from the spec
804
+ src/handlers/ the routes with state (numbers, links, contacts, messages, forms, groups, webhooks); index.ts is the table
775
805
  src/forms/ the form templates
776
806
  src/messaging/ send rules, the file by URL, forced failures and message statuses
777
807
  src/webhooks/ signature, catalog, deliveries and retries
@@ -780,7 +810,9 @@ src/sandbox/ the /_sandbox routes and the panel
780
810
  test/ vitest
781
811
  ```
782
812
 
783
- **Publishing:** push a `vX.Y.Z` tag on `main` that matches the `version` in `package.json`; GitHub Actions publishes it through npm Trusted Publishing (`.github/workflows/publish.yml`).
813
+ **Checks:** every pull request and every push to `main` run `.github/workflows/ci.yml`: `generate:check`, `typecheck`, `lint`, the tests and the build on Node 20.19, 22 and 24, and the built package started on Node 20.11, the minimum of `engines` (the test tools need 20.19).
814
+
815
+ **Publishing:** push a `vX.Y.Z` tag, on a commit of `main`, that matches the `version` in `package.json`. `.github/workflows/publish.yml` stops before publishing if the tag's commit is not on `main`, runs the same checks and the tests, and publishes through npm Trusted Publishing. A pre-release tag (`vX.Y.Z-rc.N`, `vX.Y.Z-beta.N`) goes out under the `next` dist-tag, never as `latest`.
784
816
 
785
817
  ## License
786
818