@funnelchat-hq/sandbox 0.1.1 → 0.2.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 +123 -4
- package/dist/{chunk-KH6A3I2A.js → chunk-M5E6NAXM.js} +2397 -235
- package/dist/cli.js +3 -1
- package/dist/index.d.ts +92 -2
- package/dist/index.js +1 -1
- package/funnelchat-sandbox.config.example.json +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ npx funnelchat-sandbox
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
-
funnelchat-sandbox 0.
|
|
32
|
+
funnelchat-sandbox 0.2.0 at http://127.0.0.1:4010
|
|
33
33
|
API: http://127.0.0.1:4010/api/v1/... (Authorization: Bearer 1|fc_live_sandbox, or any well-formed key)
|
|
34
34
|
panel: http://127.0.0.1:4010/_sandbox
|
|
35
35
|
example numbers: 3 (official, with the 24-hour window) and 4 (QR, no window); contact 2001; template 45
|
|
@@ -73,6 +73,7 @@ await sandbox.close();
|
|
|
73
73
|
|---|---|---|
|
|
74
74
|
| `-p`, `--port <n>` | The port. `0` takes a free one. | `4010` |
|
|
75
75
|
| `--host <host>` | Where it listens. | `127.0.0.1` (in Docker, `0.0.0.0`) |
|
|
76
|
+
| `--public-url <url>` | The base of the sign-up links' `url` (`<url>/connect/<token>`). Set it when the sandbox is reached through another address, as in Docker. | Where it listens |
|
|
76
77
|
| `-c`, `--config <file>` | A JSON configuration file. | `./funnelchat-sandbox.config.json`, if it exists |
|
|
77
78
|
| `--status-delays <s,d,r>` | Seconds until `sent`, from there to `delivered` and from there to `read`. A `-` stops there (for example `1,2,-`: never read). | `1,2,3` |
|
|
78
79
|
| `--retry-delays <a,b,c>` | Seconds between the attempts of a webhook delivery. | `60,300,1800`, like the portal |
|
|
@@ -82,6 +83,7 @@ await sandbox.close();
|
|
|
82
83
|
| `--no-antiblock` | Do not apply the spam brakes. | — |
|
|
83
84
|
| `--no-rate-limit` | Do not apply the per-number pace. | — |
|
|
84
85
|
| `--template-approval <s>` | Seconds until a template created through the API is approved. A `-` leaves it pending. | `5` |
|
|
86
|
+
| `--meta-connect` | Turns on the [Meta sign-up by link](#connecting-numbers), off like in production. | off |
|
|
85
87
|
| `-q`, `--quiet` | No line per request and per delivery on the console. | — |
|
|
86
88
|
|
|
87
89
|
Options win over the file. The file has the same options and, besides, subscriptions created at start (and after every reset) with a fixed secret, so your receiver keeps working when you restart the sandbox:
|
|
@@ -101,6 +103,9 @@ Options win over the file. The file has the same options and, besides, subscript
|
|
|
101
103
|
}
|
|
102
104
|
```
|
|
103
105
|
|
|
106
|
+
The file also takes `numbers`, the quota of the account for [connecting numbers](#connecting-numbers): `{"limit": 5, "included": true, "trial": false, "metaConnect": false, "linksPerMinute": 10}`, and `publicUrl`. `metaConnect`
|
|
107
|
+
is the Meta sign-up by link: off by default, like production; `true` (or `--meta-connect`) turns it on to test it.
|
|
108
|
+
|
|
104
109
|
There is a complete one in [funnelchat-sandbox.config.example.json](funnelchat-sandbox.config.example.json). The file `funnelchat-sandbox.config.json` is in `.gitignore`: do not commit a secret, even a test one.
|
|
105
110
|
|
|
106
111
|
## The key
|
|
@@ -149,7 +154,7 @@ Every session starts with this:
|
|
|
149
154
|
|
|
150
155
|
`POST /_sandbox/reset` goes back to this and cancels everything that was scheduled.
|
|
151
156
|
|
|
152
|
-
**With state** (what you create, change or delete stays): contacts (`/api/v1/contacts…`, with their tags, fields and the `meta-window`), conversations (`/api/v1/conversations…`), messages (send, history, reactions, forward, cards and the lookup by id), tags, custom fields, templates, [WhatsApp forms](#whatsapp-forms), numbers (
|
|
157
|
+
**With state** (what you create, change or delete stays): contacts (`/api/v1/contacts…`, with their tags, fields and the `meta-window`), conversations (`/api/v1/conversations…`), messages (send, history, reactions, forward, cards and the lookup by id), tags, custom fields, templates, [WhatsApp forms](#whatsapp-forms), numbers and sign-up links ([connecting numbers](#connecting-numbers)) and webhook subscriptions with their history.
|
|
153
158
|
|
|
154
159
|
**The numbers** are those of accounts: `GET /api/v1/devices` always paginates (`{data, links, meta}`, `perPage` from 1 to 100), and each number carries `capabilities`, with what the API can do through it:
|
|
155
160
|
|
|
@@ -320,6 +325,116 @@ curl -s http://localhost:4010/_sandbox/forms/complete -H 'Content-Type: applicat
|
|
|
320
325
|
|
|
321
326
|
It emits `whatsapp_form.completed` once per send, with `form` (`id` is the `family_id`, `version_id` the version sent), `message_id`, `contact_id`, `device_id` and `answers` (`{key, label, value}`, one per question, in order). What you do not send in `answers` takes an example value; options go by their id. `run_id` also works, or `contact_id` (the last pending send to that contact).
|
|
322
327
|
|
|
328
|
+
## Connecting numbers
|
|
329
|
+
|
|
330
|
+
Create and connect WhatsApp numbers through the API: a number by QR, or a sign-up link your customer opens. It is in
|
|
331
|
+
production in accounts since 25-10; the sandbox follows the contract of 22-10, its amendments v0.1 and v0.2 of 23-10
|
|
332
|
+
and the changes of 25-10 (the QR every 5 seconds, `country`). accounts' OpenAPI will be the final word (see
|
|
333
|
+
[Pending of the spec](#pending-of-the-spec)).
|
|
334
|
+
|
|
335
|
+
**The official sign-up (`type: meta`) is not available in production yet:** it stays off until the domain is
|
|
336
|
+
registered with Meta, and `POST /api/v1/connect-links` with `type: meta` answers `503 meta_connect_disabled`. The sandbox
|
|
337
|
+
starts the same way. To test it, turn it on with `--meta-connect`, `"numbers": {"metaConnect": true}` in the file, or
|
|
338
|
+
`POST /_sandbox/numbers {"meta_connect": true}` while it runs.
|
|
339
|
+
|
|
340
|
+
| Route | What it does |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `POST /api/v1/devices` | A number by QR: `{"connection": "qr"}`, optional `alias`, `external_reference`, `same_number_as` and `country` (two letters, any case: the country of the phone, where the WhatsApp session is opened from). `201 {"data": <number>}` with `connected: 0` and no phone yet. |
|
|
343
|
+
| `GET /api/v1/devices/{device}/qr` | `{"data": {"connected", "qr_code", "expires_in"}}`: a PNG as a data URI, `null` and `expires_in: 0` once connected, with `Cache-Control: no-store`. `expires_in` is `5`: ask for the QR every 5 seconds and show the new one at once, since WhatsApp changes the code and an old one does not link. It is fake: nothing scans it. An official number answers `422 provider_unsupported`. |
|
|
344
|
+
| `GET /api/v1/devices?external_reference=…` | Only the numbers with exactly that reference. `PUT /api/v1/devices/{device}` changes it (`null` clears it). |
|
|
345
|
+
| `DELETE /api/v1/devices/{device}` | `200` with an empty body: deletes it, frees its place and emits `device.deleted`. |
|
|
346
|
+
| `POST /api/v1/connect-links` | A sign-up link, `201 {"data": <link>}` with `Cache-Control: no-store`. The `url` with the token is only in this answer. |
|
|
347
|
+
| `GET /api/v1/connect-links`, `GET …/{link}` | The links without their token, newest first, paginated, filtered by `status` and `external_reference`. The `id` is an integer. `status`: `pending`, `completed`, `failed`, `expired` (`error_code: link_expired`) or `revoked` (`error_code: revoked`). |
|
|
348
|
+
| `DELETE /api/v1/connect-links/{link}` | Revokes a `pending` link: `204`, `409 link_not_pending` otherwise, `404` if it does not exist. |
|
|
349
|
+
|
|
350
|
+
**The body of a link**, by `type`:
|
|
351
|
+
|
|
352
|
+
| Field | `type: qr` | `type: meta` |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| `allowed_connection_types` | prohibited | required: `coexistence`, `dedicated` or both, without repeating |
|
|
355
|
+
| `provision_us_number` | prohibited (even `false`) | optional: a US number of the pool (3 per account, 1 on trial accounts). With `true`, `allowed_connection_types` must include `dedicated` and `device_id` is not allowed: a pool number is always dedicated, and new |
|
|
356
|
+
| `same_number_as` | optional, not with `device_id`: QR on a number in coexistence | prohibited |
|
|
357
|
+
| `country` | optional, not with `device_id`: two letters, the country of the phone; the link returns it in lower case | prohibited |
|
|
358
|
+
| `device_id` | optional: reconnects that number (`422 connection_type_mismatch` if it is not QR) | the same, for an official number |
|
|
359
|
+
| `external_reference` | optional, up to 255 | optional, up to 255 |
|
|
360
|
+
| `return_url` | optional, `https`, up to 2048 | the same |
|
|
361
|
+
| `expires_in_minutes` | 30 to 10080, 1440 by default | the same |
|
|
362
|
+
|
|
363
|
+
`null` counts as missing, like Laravel's `prohibited`. A broken rule answers
|
|
364
|
+
`422 {"error": "invalid_request", "message": "…", "errors": {"field": ["…"]}}`, and so does `POST /devices`. A
|
|
365
|
+
`device_id` or `same_number_as` that is not a number of the account answers `422 invalid_reference_device`.
|
|
366
|
+
|
|
367
|
+
**Each number has** `external_reference` (your text, not unique) and `connection` (`qr`, `coexistence` or
|
|
368
|
+
`dedicated`). The four `device.*` events carry both inside `device`, and `connect_link_id`: in `device.connected`, the
|
|
369
|
+
link of that connection; in the other three, the link that connected the number last; `null` without one. `device.disconnected` brings `reason`: `qr_logged_out`, `meta_account_not_registered`,
|
|
370
|
+
`shadow_ban_protection`, `provider_error`, `unknown` or `disconnected_by_user`. Type the new keys as optional:
|
|
371
|
+
production does not send them until accounts deploys.
|
|
372
|
+
|
|
373
|
+
**The quota** counts phones: QR and Meta of the same phone count once. The sandbox account allows 5 and the two
|
|
374
|
+
example numbers use two; change it with `POST /_sandbox/numbers`. The rejections:
|
|
375
|
+
|
|
376
|
+
- `403 numbers_not_included`, and `403 number_limit_reached` with `limit`, `used` and `pending_device_ids`;
|
|
377
|
+
- `409 pending_number_exists` with `device_id`: there is already a QR number not connected yet, so connect it or
|
|
378
|
+
delete it first;
|
|
379
|
+
- `422 qr_requires_coexistence` (QR on an official number that is not in coexistence) and
|
|
380
|
+
`422 method_already_present` (with `device_id`).
|
|
381
|
+
|
|
382
|
+
A reconnection, and QR on a number in coexistence, take no place. Up to 20 open links (`429 too_many_open_links`)
|
|
383
|
+
and 10 creations per minute (Laravel's `429 {"message": "Too Many Attempts."}`, with `Retry-After`). While the Meta
|
|
384
|
+
sign-up is off (the default), a `meta` link answers `503 meta_connect_disabled`, and so does the page of a `meta` link
|
|
385
|
+
made while it was on.
|
|
386
|
+
|
|
387
|
+
**`Idempotency-Key`** works on both `POST`, per resource: the same key with the same body gives back the same `2xx`
|
|
388
|
+
with `Idempotent-Replayed: true`, without creating anything; with another body, `422 idempotency_key_mismatch`; a
|
|
389
|
+
malformed key (empty, over 255 or with spaces), `422 invalid_idempotency_key`. A rejection is not kept: fix it and
|
|
390
|
+
retry with the same key.
|
|
391
|
+
|
|
392
|
+
A number by QR, its code and the scan (the scan is a sandbox route):
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
KEY='Authorization: Bearer 1|fc_live_sandbox'
|
|
396
|
+
curl -s -X POST localhost:4010/api/v1/devices -H "$KEY" -H 'Content-Type: application/json' \
|
|
397
|
+
-H 'Idempotency-Key: alta-cliente-8841' -d '{"connection": "qr", "external_reference": "cliente-8841"}'
|
|
398
|
+
curl -s localhost:4010/api/v1/devices/5/qr -H "$KEY" # {"data": {"connected": false, "qr_code": "data:image/png;base64,…", "expires_in": 5}}
|
|
399
|
+
curl -s -X POST localhost:4010/_sandbox/devices/5/connect # device.connected
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
A sign-up link and its page (`meta` links need the Meta sign-up on: `npx funnelchat-sandbox --meta-connect`):
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
curl -s -X POST localhost:4010/api/v1/connect-links -H "$KEY" -H 'Content-Type: application/json' \
|
|
406
|
+
-d '{"type": "meta", "allowed_connection_types": ["coexistence", "dedicated"], "external_reference": "cliente-8841", "return_url": "https://your-app.example/connected"}'
|
|
407
|
+
# {"data": {"id": 1, "type": "meta", "status": "pending", "url": "http://localhost:4010/connect/<token>", …}}
|
|
408
|
+
curl -si -X POST 'http://localhost:4010/connect/<token>/start' -d connection=coexistence
|
|
409
|
+
# 303 Location: https://your-app.example/connected?connect_link_id=1&status=completed&device_id=6
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**The page** is a test page. Opening `GET /connect/{token}` does not use the link, as in production, so a
|
|
413
|
+
messenger's preview cannot burn it: it shows a button (and, for a `meta` link that allows both, the choice of
|
|
414
|
+
connection) and takes the token out of the address bar (`/connect/reopen`). The button, `POST /connect/{token}/start`,
|
|
415
|
+
connects at once: it creates the number (or reconnects it), marks the link `completed`, emits `device.connected` and
|
|
416
|
+
redirects to `return_url` with `connect_link_id`, `status`, `device_id` (if it completed) and `error_code` (if it
|
|
417
|
+
failed), or says it on the page. `POST /connect/{token}/complete` does the same. A link works once; then the page
|
|
418
|
+
answers `410`.
|
|
419
|
+
|
|
420
|
+
Two answers of the page do not end the link. A `connection` the link does not allow answers
|
|
421
|
+
`422 connection_type_not_allowed`. A failure of Facebook or Meta on a `meta` link (`error_code=facebook_auth_failed`
|
|
422
|
+
or `meta_error`) leaves it `pending` so your customer retries: `{"state": "retry", "error_code": "…", "attempts_left": 3}`
|
|
423
|
+
when you send JSON, the page with the button again otherwise; the fifth failure ends it `failed`. `quota_exceeded`,
|
|
424
|
+
`trial_number_blocked`, `number_not_owned` and the QR's `qr_timeout` end it at the first. In the body (form or JSON) or the query you choose what happens: `outcome=failed` with `error_code`
|
|
425
|
+
(`qr_timeout` or `meta_error` by default), `connection` when the link allows both, and `number`. With `Authorization`
|
|
426
|
+
or `X-Api-Key` it answers `403 credentials_not_allowed`. Its headers are production's: `Cache-Control: no-store, private`,
|
|
427
|
+
`Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `X-Robots-Tag: noindex`, HSTS, COOP and a CSP with a nonce.
|
|
428
|
+
|
|
429
|
+
With the Node SDK, once it has the new methods:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
const device = await funnelchat.devices.create({ connection: 'qr', external_reference: 'cliente-8841' });
|
|
433
|
+
const { qr_code } = await funnelchat.devices.qr(device.id);
|
|
434
|
+
const link = await funnelchat.connectLinks.create({ type: 'qr', return_url: 'https://your-app.example/connected' });
|
|
435
|
+
await fetch(`${link.url}/start`, { method: 'POST', redirect: 'manual' }); // the button of the sandbox page
|
|
436
|
+
```
|
|
437
|
+
|
|
323
438
|
## Webhooks
|
|
324
439
|
|
|
325
440
|
You subscribe with the same routes the SDKs and the n8n trigger use (the unified subscriptions of accounts):
|
|
@@ -376,7 +491,9 @@ These routes take no key: they belong to the sandbox, not to the API.
|
|
|
376
491
|
| `POST /_sandbox/forms/complete` | The contact completes a sent form: emits `whatsapp_form.completed` (see [WhatsApp forms](#whatsapp-forms)). |
|
|
377
492
|
| `POST /_sandbox/mac`, `GET /_sandbox/mac` | The account's MAC: `{"lockout": true, "active_contact_ids": [2001], "inactive_contact_ids": []}`. With `lockout`, sends to contacts that are not active answer `429 mac_lockout_24h`; an inactive contact always gets it. |
|
|
378
493
|
| `POST /_sandbox/contacts/{id}/tags` with `{"tag_id": 4001}`, `DELETE /_sandbox/contacts/{id}/tags/{tag}` | Someone tags the contact in the panel: `contact.tag_added` and `contact.tag_removed`. |
|
|
379
|
-
| `POST /_sandbox/devices/{id}/disconnect`, `…/connect` | The number disconnects or connects: `device.disconnected` and `device.connected`. Disconnected, sends answer `422 device_unavailable`. |
|
|
494
|
+
| `POST /_sandbox/devices/{id}/disconnect`, `…/connect` | The number disconnects (`{"reason": "disconnected_by_user"}`, any of the list; `qr_logged_out` for QR and `unknown` for official ones by default) or connects, which is the scan of a QR (`{"number": "5491155550101"}`, a fake one otherwise): `device.disconnected` and `device.connected`. Disconnected, sends answer `422 device_unavailable`. |
|
|
495
|
+
| `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. |
|
|
496
|
+
| `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. |
|
|
380
497
|
| `POST /_sandbox/templates/{id}/status` | Meta approves or rejects a template: `{"status": "REJECTED", "reason": "…"}` emits `template.status_changed`. |
|
|
381
498
|
| `POST /_sandbox/events/{type}` | Any event of the catalog, with its example; what you send in `{"data": {…}}` overrides it. `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`. |
|
|
382
499
|
| `POST /_sandbox/window` | Opens or closes the window by hand: `{"contact_id": 2001, "device_id": 3, "open": false}`. |
|
|
@@ -469,6 +586,7 @@ And at `http://localhost:4010/_sandbox` you see each request and each delivery w
|
|
|
469
586
|
- **There are no groups or campaigns with state:** their events are fired from `/_sandbox`, and their routes answer examples.
|
|
470
587
|
- **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). There are no metrics (`…/metrics`) and no dynamic forms.
|
|
471
588
|
- **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.
|
|
589
|
+
- **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.
|
|
472
590
|
- **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.
|
|
473
591
|
|
|
474
592
|
## Pending of the spec
|
|
@@ -488,6 +606,7 @@ Besides, `spec/catalog.json` is the event catalog of accounts v1.4.24, with its
|
|
|
488
606
|
| The file by URL: `file_type`, `file_name`, their `422`s and the `415` | conversations v1.18.10 (`SendFileType`) | In production. |
|
|
489
607
|
| Forms: `/api/v1/forms…`, `POST /api/v1/contacts/{contact}/message/form` and `whatsapp_form.completed` | conversations v1.18.10 | In production. The portal does not document them yet. |
|
|
490
608
|
| `capabilities` and the new keys of the numbers | accounts v1.4.24 (`DeviceResource`, `DeviceCapabilities`) | In production. |
|
|
609
|
+
| Connecting numbers: `POST /api/v1/devices`, `GET …/{device}/qr`, `external_reference`, `/api/v1/connect-links…`, `/connect/{token}`, `Idempotency-Key`, `device.deleted`, `device.health_changed` and the new keys of `device.connected` and `device.disconnected` | The contract of the sign-up by API of 22-10 (A1, A3) and its amendments v0.1 and v0.2 of 23-10; the QR every 5 seconds (accounts #1335) and `country`, read from accounts `main` `e61bb7633` | In production since 25-10, with the Meta sign-up off until the domain is registered with Meta. Missing from the portal's snapshot. |
|
|
491
610
|
| `X-Request-Id` and the edge's errors (`route_not_found`, `origin_unreachable`, `edge_misconfigured`) | api-edge v0.3.0, platform-gate v0.8.0 | In production. |
|
|
492
611
|
|
|
493
612
|
## Regenerating
|
|
@@ -542,7 +661,7 @@ src/sandbox/ the /_sandbox routes and the panel
|
|
|
542
661
|
test/ vitest
|
|
543
662
|
```
|
|
544
663
|
|
|
545
|
-
**Publishing:** push a `vX.Y.Z` tag on `main` that matches the `version` in `package.json
|
|
664
|
+
**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`).
|
|
546
665
|
|
|
547
666
|
## License
|
|
548
667
|
|