@funnelchat-hq/sandbox 0.2.0 → 0.3.1

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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { ConfigError, parseArgs, VERSION, DEFAULT_PORT, DEFAULT_CONFIG_FILE, resolveConfig, createSandbox } from './chunk-M5E6NAXM.js';
2
+ import { ConfigError, parseArgs, VERSION, DEFAULT_PORT, DEFAULT_CONFIG_FILE, resolveConfig, createSandbox } from './chunk-FUK4UDIM.js';
3
3
 
4
4
  // src/cli.ts
5
5
  var HELP = `funnelchat-sandbox ${VERSION}
@@ -21,6 +21,7 @@ Options:
21
21
  --no-rate-limit Do not apply the per-number pace.
22
22
  --template-approval <s> Seconds until a new template is approved (default 5; "-" leaves it pending).
23
23
  --meta-connect Turn on the Meta sign-up by link (off by default, like production).
24
+ --no-group-actions Turn off the send to a group by API (on by default, like production).
24
25
  -q, --quiet No line per request and per delivery.
25
26
  -h, --help This help.
26
27
  -v, --version The version.
package/dist/index.d.ts CHANGED
@@ -10,6 +10,12 @@ interface PresetSubscription {
10
10
  /** Optional: a fixed id (UUID). */
11
11
  id?: string;
12
12
  }
13
+ /** A workspace created at start (and after every reset) besides the default one, with fixed keys. */
14
+ interface PresetWorkspace {
15
+ name: string;
16
+ /** Keys of that workspace (`<id>|fc_live_<letters and digits>`). Any other key belongs to the default workspace. */
17
+ keys: string[];
18
+ }
13
19
  interface SandboxConfig {
14
20
  port: number;
15
21
  host: string;
@@ -39,8 +45,10 @@ interface SandboxConfig {
39
45
  };
40
46
  /** Seconds until a template created through the API goes from `PENDING` to `APPROVED`. `null`: it stays pending. */
41
47
  templateApprovalSeconds: number | null;
42
- /** Subscriptions to create at start and after every reset. */
48
+ /** Subscriptions to create at start and after every reset, in the default workspace. */
43
49
  subscriptions: PresetSubscription[];
50
+ /** Workspaces to create at start and after every reset, besides the default one (ids 2, 3… in this order). */
51
+ workspaces: PresetWorkspace[];
44
52
  /**
45
53
  * The number quota of the account and the Meta sign-up switch; `POST /_sandbox/numbers` changes them while it runs.
46
54
  * QR and Meta of the same phone count once; the two example numbers already use two places.
@@ -60,6 +68,12 @@ interface SandboxConfig {
60
68
  /** `POST /connect-links` per minute (accounts: 10); one more answers Laravel's `429 Too Many Attempts.`. */
61
69
  linksPerMinute: number;
62
70
  };
71
+ /**
72
+ * The group actions by API (`POST /groups/{group}/messages`, `GET /groups/actions/{action}`). `true` (the default,
73
+ * like production, where communities' kill switch `API_GROUP_ACTIONS_ENABLED` is on). `false` imitates the switch
74
+ * off: every key gets `403 route_not_available`.
75
+ */
76
+ groupActions: boolean;
63
77
  /**
64
78
  * Where the sign-up page is reachable, for the `url` of a link (`<publicUrl>/connect/<token>`). Never taken from the
65
79
  * request's `Host`. `null`: the address the sandbox listens on.
@@ -86,8 +100,20 @@ interface MessageError {
86
100
  type DeviceKind = 'cloud' | 'qr';
87
101
  /** The aggregated health of an official number (accounts `MetaHealthStatusService`). */
88
102
  type HealthState = 'available' | 'limited' | 'blocked' | 'unknown';
103
+ /**
104
+ * A workspace of the account (accounts `workspaces`). Every API key belongs to one and only sees and creates there
105
+ * (accounts v1.5.97, workspaces-api-keys). What has no workspace (`null`) counts as the default one's.
106
+ */
107
+ interface Workspace {
108
+ id: number;
109
+ name: string;
110
+ isDefault: boolean;
111
+ createdAt: Date;
112
+ }
89
113
  interface Device {
90
114
  id: number;
115
+ /** The workspace the number is in; `null` counts as the default one. */
116
+ workspaceId: number | null;
91
117
  alias: string;
92
118
  /** Digits only, without `+`. `null` while a number created by QR has not been scanned. */
93
119
  number: string | null;
@@ -114,6 +140,8 @@ type ConnectLinkStatus = 'pending' | 'completed' | 'failed' | 'expired' | 'revok
114
140
  /** A sign-up link (contract A1 `connect-links`): it creates (or reconnects) a number when the page completes. */
115
141
  interface ConnectLink {
116
142
  id: number;
143
+ /** The workspace of the key that created it: the number it creates lands there. */
144
+ workspaceId: number | null;
117
145
  type: 'meta' | 'qr';
118
146
  status: ConnectLinkStatus;
119
147
  /** sha256 of the token: the raw token is only in the `url` of the create answer. */
@@ -146,6 +174,8 @@ interface IdempotentAnswer {
146
174
  }
147
175
  interface Contact {
148
176
  id: number;
177
+ /** The workspace the contact is in; `null` counts as the default one. */
178
+ workspaceId: number | null;
149
179
  name: string | null;
150
180
  email: string | null;
151
181
  /** Digits only: the API stores the phone without `+`. */
@@ -207,9 +237,51 @@ interface CustomField {
207
237
  createdAt: Date;
208
238
  updatedAt: Date;
209
239
  }
240
+ /** A participant of a group: the phone in digits, and whether it is an admin. */
241
+ interface GroupParticipant {
242
+ phone: string;
243
+ name: string | null;
244
+ admin: boolean;
245
+ }
246
+ /** A WhatsApp group of a number of the account (communities `groups`). */
247
+ interface Group {
248
+ id: number;
249
+ /** The WhatsApp id of the group, `120363…@g.us`: the `{group}` of the routes and `group.uid` of the events. */
250
+ uid: string;
251
+ name: string;
252
+ description: string;
253
+ /** The number that is in the group (a QR number: the sandbox account has no official groups). */
254
+ deviceId: number;
255
+ /** The workspace the group is in; `null` counts as the default one. */
256
+ workspaceId: number | null;
257
+ participants: GroupParticipant[];
258
+ link: string;
259
+ createdAt: Date;
260
+ updatedAt: Date;
261
+ }
262
+ /** `queued` → `running` → `completed` | `failed` (communities `ApiGroupAction::publicStatus()`). */
263
+ type GroupActionStatus = 'queued' | 'running' | 'completed' | 'failed';
264
+ /** A group action by API (communities `api_group_actions`): here, only `send_message`. */
265
+ interface GroupAction {
266
+ uuid: string;
267
+ type: 'send_message';
268
+ status: GroupActionStatus;
269
+ deviceId: number;
270
+ groupId: number;
271
+ /** The workspace of the key that called (`call_context.workspace_id`). */
272
+ workspaceId: number;
273
+ result: Record<string, unknown> | null;
274
+ reason: string | null;
275
+ createdAt: Date;
276
+ startedAt: Date | null;
277
+ completedAt: Date | null;
278
+ failedAt: Date | null;
279
+ }
210
280
  type TemplateStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'PAUSED' | 'DISABLED';
211
281
  interface Template {
212
282
  id: number;
283
+ /** The workspace it belongs to; `null` counts as the default one. */
284
+ workspaceId: number | null;
213
285
  uid: string;
214
286
  name: string;
215
287
  body: string;
@@ -235,6 +307,8 @@ interface Template {
235
307
  type SubscriptionStatus = 'active' | 'paused' | 'disabled';
236
308
  interface Subscription {
237
309
  id: string;
310
+ /** The workspace it belongs to: it only gets that workspace's events. `null` counts as the default one. */
311
+ workspaceId: number | null;
238
312
  url: string;
239
313
  description: string | null;
240
314
  events: string[];
@@ -322,6 +396,18 @@ interface RequestLogEntry {
322
396
  requestBody: unknown;
323
397
  responseBody: unknown;
324
398
  }
399
+ /** An event the sandbox emitted, delivered or not: the panel shows them, so a status is seen without a receiver. */
400
+ interface EmittedEvent {
401
+ envelope: {
402
+ id: string;
403
+ type: string;
404
+ version: number;
405
+ occurred_at: string;
406
+ data: unknown;
407
+ };
408
+ /** How many subscriptions got it. */
409
+ subscriptions: number;
410
+ }
325
411
  /** A failure armed with `POST /_sandbox/failures` or the `X-Sandbox-*` headers. */
326
412
  interface ArmedFailure {
327
413
  id: number;
@@ -345,6 +431,9 @@ declare class Sequence {
345
431
  }
346
432
  declare class Store {
347
433
  readonly historyLimit: number;
434
+ workspaces: Map<number, Workspace>;
435
+ /** The sandbox keys minted for a workspace (`POST /_sandbox/workspaces`, `/_sandbox/keys`): any other key is the default one's. */
436
+ keys: Map<string, number>;
348
437
  devices: Map<number, Device>;
349
438
  contacts: Map<number, Contact>;
350
439
  messages: Map<number, Message>;
@@ -354,11 +443,14 @@ declare class Store {
354
443
  subscriptions: Map<string, Subscription>;
355
444
  deliveries: Delivery[];
356
445
  requests: RequestLogEntry[];
446
+ events: EmittedEvent[];
357
447
  failures: ArmedFailure[];
358
448
  forms: Map<number, FormVersion>;
359
449
  formFamilies: Map<number, FormFamily>;
360
450
  formRuns: Map<number, FormRun>;
361
451
  connectLinks: Map<number, ConnectLink>;
452
+ groups: Map<number, Group>;
453
+ groupActions: Map<string, GroupAction>;
362
454
  /** When each `POST /connect-links` arrived in the last minute: the per-minute limit. */
363
455
  linkAttempts: number[];
364
456
  /** Answers by `Idempotency-Key`, per resource (`devices`, `connect-links`): one account. */
@@ -389,6 +481,8 @@ declare class Store {
389
481
  /** Salt of the deterministic event ids: new on every reset, so ids never repeat across sessions. */
390
482
  eventSalt: string;
391
483
  readonly ids: {
484
+ workspace: Sequence;
485
+ apiKey: Sequence;
392
486
  contact: Sequence;
393
487
  message: Sequence;
394
488
  tag: Sequence;
@@ -403,9 +497,13 @@ declare class Store {
403
497
  formRun: Sequence;
404
498
  device: Sequence;
405
499
  connectLink: Sequence;
500
+ group: Sequence;
406
501
  };
407
502
  constructor(historyLimit?: number);
503
+ /** The default workspace of the account: where what has no workspace belongs. */
504
+ get defaultWorkspace(): Workspace;
408
505
  logRequest(entry: Omit<RequestLogEntry, 'id'>): RequestLogEntry;
506
+ logEvent(entry: EmittedEvent): void;
409
507
  addDelivery(entry: Omit<Delivery, 'id'>): Delivery;
410
508
  findContactByPhone(phone: string): Contact | undefined;
411
509
  contactMessages(contactId: number): Message[];
@@ -425,12 +523,20 @@ declare class Store {
425
523
  * `429` is retried, any other answer that is not `2xx` is final;
426
524
  * - a delivery that runs out of attempts counts one consecutive failure; at 5 the subscription is disabled
427
525
  * (`too_many_failures`); a success resets the count;
428
- * - each attempt checks again that the subscription exists, is `active` and still wants the type.
526
+ * - each attempt checks again that the subscription exists, is `active` and still wants the type;
527
+ * - an event reaches only the subscriptions of its workspace, and `data.workspace_id` says which one (accounts
528
+ * v1.5.97 `EventWorkspaceRouting`, Q35 of 04-11): the workspace the emitter declares when it is one of the account,
529
+ * the default one otherwise. A subscription without a workspace counts as the default one's.
429
530
  */
430
531
 
431
532
  interface EmitOptions {
432
533
  id?: string;
433
534
  occurredAt?: Date;
535
+ /**
536
+ * The workspace the event belongs to: the number's for `device.*`, the contact's or the group's for the rest.
537
+ * Missing, `null` or not a workspace of the account: the default one.
538
+ */
539
+ workspaceId?: number | null;
434
540
  }
435
541
  interface Envelope$1 {
436
542
  id: string;
@@ -451,7 +557,10 @@ declare class Webhooks {
451
557
  get maxAttempts(): number;
452
558
  /** Builds the envelope, exactly as it is delivered. */
453
559
  envelope(type: string, data: unknown, options?: EmitOptions): Envelope$1;
454
- /** Emits one event to every active subscription that wants it. Returns the envelope and how many got it. */
560
+ /**
561
+ * Emits one event to every active subscription of its workspace that wants it. Returns the envelope and how many
562
+ * got it.
563
+ */
455
564
  emit(type: string, data: unknown, options?: EmitOptions): {
456
565
  envelope: Envelope$1;
457
566
  subscriptions: number;
@@ -460,7 +569,10 @@ declare class Webhooks {
460
569
  send(subscription: Subscription, type: string, body: string): Promise<SendResult>;
461
570
  private shouldRetry;
462
571
  private attempt;
463
- /** `POST /api/v1/webhooks/{id}/test`: one synchronous `webhook.test`, no retries, no counters touched. */
572
+ /**
573
+ * `POST /api/v1/webhooks/{id}/test`: one synchronous `webhook.test`, no retries, no counters touched. Its
574
+ * `data.workspace_id` is the subscription's workspace.
575
+ */
464
576
  test(subscription: Subscription): Promise<Record<string, unknown>>;
465
577
  }
466
578
 
@@ -577,4 +689,4 @@ declare const FORM_TEMPLATES: FormTemplate[];
577
689
 
578
690
  declare const VERSION: string;
579
691
 
580
- export { type CatalogEntry, type ConfigInput, type CreateSandboxOptions, DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, type PresetSubscription, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, type Sandbox, type SandboxConfig, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, signature };
692
+ export { type CatalogEntry, type ConfigInput, type CreateSandboxOptions, DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, type PresetSubscription, type PresetWorkspace, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, type Sandbox, type SandboxConfig, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, signature };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, signature } from './chunk-M5E6NAXM.js';
1
+ export { DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, signature } from './chunk-FUK4UDIM.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -5,7 +5,9 @@
5
5
  "rules": { "window": true, "antiblock": true, "rateLimit": true },
6
6
  "templateApprovalSeconds": 5,
7
7
  "numbers": { "limit": 5, "included": true, "trial": false, "metaConnect": false, "linksPerMinute": 10 },
8
+ "groupActions": true,
8
9
  "publicUrl": null,
10
+ "workspaces": [{ "name": "Cliente B", "keys": ["2|fc_live_clienteB"] }],
9
11
  "subscriptions": [
10
12
  {
11
13
  "url": "http://localhost:3000/hooks/funnelchat",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelchat-hq/sandbox",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A local server that imitates the Funnelchat public API and sends signed webhooks, with no real effects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -2,63 +2,67 @@
2
2
  "description": "Where each file of spec/ came from. Written by `npm run generate -- --spec <file>` or `-- --pending <file>`; do not edit it by hand. spec/pending-sandbox.json belongs to this repo: its sources are inside it.",
3
3
  "snapshot": {
4
4
  "file": "spec/funnelchat.json",
5
- "sha256": "8bbbae569b4cab520884db98f28204156c1bd6c629578bee609343c193e0dadb",
6
- "bytes": 635293,
5
+ "sha256": "8228ea7369daed0655b779ca6af381355a6c1cc13dd702ed6e9c322779bafe94",
6
+ "bytes": 1214988,
7
7
  "source": {
8
8
  "repository": "git@github.com:Funnelchat20/developers-portal.git",
9
- "path": "out/specs/funnelchat.json",
10
- "branch": "feat/openapi-unificada",
11
- "commit": "0ef4abb008870ed8c7cae49a08900a81e5e57ed8",
12
- "commit_date": "2026-10-15T10:53:11-03:00",
9
+ "path": "generated/specs/funnelchat-completa.json",
10
+ "branch": "main",
11
+ "commit": "f5464516",
12
+ "commit_date": null,
13
13
  "file_tracked_in_git": false,
14
- "tracked_files_clean": true,
15
- "file_modified_at": "2026-10-15T14:11:20.569Z"
14
+ "tracked_files_clean": null,
15
+ "file_modified_at": "2026-11-05T16:59:01.577Z",
16
+ "note": "Built by `npm run build` of developers-portal at the merge of PR #37, deployed as v0.6.2 on 05-11. Not the published /specs/ file, which carries only the visible operations. communities' snapshot was taken with the group actions on."
16
17
  },
17
- "imported_at": "2026-10-16T11:46:40.094Z",
18
+ "imported_at": "2026-11-05T17:01:38.054Z",
18
19
  "spec": {
19
20
  "openapi": "3.1.0",
20
21
  "title": "Funnelchat API",
21
22
  "version": "v0",
22
- "paths": 191,
23
- "operations": 230,
24
- "webhooks": 28,
23
+ "paths": 217,
24
+ "operations": 264,
25
+ "webhooks": 29,
25
26
  "domains": [
26
27
  {
27
28
  "domain": "conversations",
28
- "sha": "9d723d52c",
29
- "branch": "snapshot/referencia-1310",
30
- "snapshot_at": "2026-10-13T10:41:30-03:00"
29
+ "tag": "v1.19.21",
30
+ "sha": "f2937a71",
31
+ "branch": null,
32
+ "snapshot_at": null
31
33
  },
32
34
  {
33
35
  "domain": "communities",
34
- "sha": "fab0bb10",
35
- "branch": "snapshot/referencia-1310",
36
- "snapshot_at": "2026-10-13T10:41:34-03:00"
36
+ "tag": "v2.35.5",
37
+ "sha": "2be913d4",
38
+ "branch": null,
39
+ "snapshot_at": null
37
40
  },
38
41
  {
39
42
  "domain": "accounts",
40
- "sha": "c234b6f8",
41
- "branch": "snapshot/referencia-1310",
42
- "snapshot_at": "2026-10-13T10:41:36-03:00"
43
+ "tag": "v1.5.97",
44
+ "sha": "558332bcf",
45
+ "branch": null,
46
+ "snapshot_at": null
43
47
  }
44
48
  ]
45
49
  }
46
50
  },
47
51
  "pending": {
48
52
  "file": "spec/pending.json",
49
- "sha256": "a14bc4dbc3025d3e958ffda8afd1fa9fdab34af264418060825f3300799aceb0",
50
- "bytes": 32775,
53
+ "sha256": "051680bd82944c0e248d20d1499d89e61a231c6ba3ef6a5d864cf18a97aae1ad",
54
+ "bytes": 112110,
51
55
  "source": {
52
56
  "repository": "git@github.com:Funnelchat20/funnelchat-node.git",
53
57
  "path": "spec/pending.json",
54
- "branch": "feat/inicial",
55
- "commit": "88557ce7819c5a4061e84c58053a1b6c49f35ec2",
56
- "commit_date": "2026-10-16T02:46:14-03:00",
58
+ "branch": "feat/enviar-por-telefono",
59
+ "commit": "a8f83509d4a40d6116a9c0487bf0aee639948273",
60
+ "commit_date": "2026-11-05T14:02:36-03:00",
57
61
  "file_tracked_in_git": true,
58
62
  "tracked_files_clean": true,
59
- "file_modified_at": "2026-10-16T05:29:09.348Z"
63
+ "file_modified_at": "2026-11-05T17:01:15.771Z"
60
64
  },
61
- "imported_at": "2026-10-16T11:46:40.294Z",
65
+ "imported_at": "2026-11-05T17:03:20.911Z",
62
66
  "note": "The pending layer of the Node SDK, unchanged. What belongs to the sandbox goes in spec/pending-sandbox.json."
63
67
  }
64
68
  }